Why Web3 Security is Paramount
Smart contract vulnerabilities have resulted in billions of dollars in losses. Unlike traditional software, smart contracts are immutable once deployed, operate with real financial value, and their code is publicly visible — meaning attackers can study your contract and exploit any weakness. Understanding common attack vectors and defense patterns is essential for every Web3 developer.
Top Vulnerability Categories
- Reentrancy: An attacker re-enters your function before the previous execution finishes
- Access Control: Missing or incorrect permission checks allow unauthorized actions
- Oracle Manipulation: Attackers manipulate price feeds to exploit DeFi protocols
- Front-running: Attackers see pending transactions and insert their own before them
- Integer Overflow: Arithmetic operations wrap around (mitigated in Solidity 0.8+)
Reentrancy Attacks
A reentrancy attack occurs when a contract makes an external call to another contract, and the called contract calls back into the original contract before the first execution is complete. This can allow the attacker to drain funds by repeatedly calling the withdrawal function before the balance is updated.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// VULNERABLE CONTRACT
contract VulnerableVault {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
// BUG: External call BEFORE updating state!
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
// State update happens AFTER the external call
// During the call above, the attacker can call withdraw() again!
balances[msg.sender] = 0;
}
}
// ATTACKER CONTRACT
contract Attacker {
VulnerableVault public vault;
constructor(address _vault) {
vault = VulnerableVault(_vault);
}
function attack() external payable {
vault.deposit{value: msg.value}();
vault.withdraw(); // Triggers reentrancy
}
// This is called when the vault sends ETH
receive() external payable {
if (address(vault).balance >= 1 ether) {
vault.withdraw(); // Re-enter before balance is set to 0!
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
// SECURE CONTRACT — Three defense layers
contract SecureVault is ReentrancyGuard {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
// Defense 1: Checks-Effects-Interactions pattern
// Defense 2: ReentrancyGuard (nonReentrant modifier)
function withdraw() external nonReentrant {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance"); // CHECK
balances[msg.sender] = 0; // EFFECT (update state FIRST)
(bool success, ) = msg.sender.call{value: amount}(""); // INTERACTION (external call LAST)
require(success, "Transfer failed");
}
}
Access Control Vulnerabilities
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
contract SecureAccess is AccessControl {
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(ADMIN_ROLE, msg.sender);
}
// BAD: No access control — anyone can call this!
// function dangerousMint(address to, uint256 amount) external {
// _mint(to, amount);
// }
// GOOD: Role-based access control
function secureMint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
// BAD: tx.origin check — vulnerable to phishing attacks
// function badAuth() external {
// require(tx.origin == owner, "Not owner");
// // An attacker can trick the owner into calling a malicious contract
// // that then calls this function — tx.origin will be the owner!
// }
// GOOD: msg.sender check
function goodAuth() external onlyRole(ADMIN_ROLE) {
// msg.sender is always the direct caller
}
function _mint(address to, uint256 amount) internal { /* ... */ }
}
Front-Running and MEV
Front-running occurs when an attacker observes a pending transaction in the mempool, predicts its impact, and submits their own transaction with a higher gas price to execute before the victim. This is part of the broader MEV (Maximal Extractable Value) problem. On Ethereum, validators and specialized bots (called "searchers") extract MEV by reordering, inserting, or censoring transactions.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// Commit-reveal scheme to prevent front-running
contract CommitReveal {
mapping(address => bytes32) public commits;
mapping(address => uint256) public commitTimestamps;
uint256 public constant REVEAL_DELAY = 2; // blocks
// Step 1: Submit a hidden commitment
function commit(bytes32 hash) external {
commits[msg.sender] = hash;
commitTimestamps[msg.sender] = block.number;
}
// Step 2: Reveal after delay (attacker cannot front-run because they
// do not know the value until it is revealed)
function reveal(uint256 value, bytes32 salt) external {
require(
block.number > commitTimestamps[msg.sender] + REVEAL_DELAY,
"Too early"
);
require(
commits[msg.sender] == keccak256(abi.encodePacked(value, salt, msg.sender)),
"Invalid reveal"
);
delete commits[msg.sender];
// Process the value...
}
}
Security Checklist
Before Deploying to Mainnet
- Reentrancy: Follow Checks-Effects-Interactions pattern and use ReentrancyGuard
- Access control: Verify every state-changing function has proper authorization
- Integer safety: Use Solidity 0.8+ for built-in overflow checks; use unchecked only when safe
- External calls: Validate return values, handle failures, limit gas forwarded
- Front-running: Use commit-reveal schemes or Flashbots Protect for sensitive operations
- Oracle security: Use Chainlink or TWAP oracles; never rely on single-block spot prices
- Testing: Achieve 100% test coverage, including edge cases and attack scenarios
- Audit: Get a professional security audit before deploying contracts that hold significant value