Smart contract security is a core topic in blockchain development. Since contracts are difficult to modify once deployed and involve asset security, preventing vulnerabilities is more important than fixing them. Here are common security vulnerabilities and protection measures.
1. Reentrancy Attack
Vulnerability Principle
Attackers exploit the characteristic of contracts calling external contracts before state updates to recursively call the target contract.
solidity// Vulnerable contract contract VulnerableBank { mapping(address => uint256) public balances; function withdraw() public { uint256 amount = balances[msg.sender]; require(amount > 0); // Dangerous: transfer first, update state later (bool success, ) = msg.sender.call{value: amount}(""); require(success); balances[msg.sender] = 0; // Update too late! } } // Attacker contract contract Attacker { VulnerableBank public bank; constructor(address _bank) { bank = VulnerableBank(_bank); } receive() external payable { if (address(bank).balance >= 1 ether) { bank.withdraw(); // Recursive call } } function attack() external payable { bank.deposit{value: 1 ether}(); bank.withdraw(); } }
Protection Measures
solidityimport "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract SecureBank is ReentrancyGuard { mapping(address => uint256) public balances; bool private locked; // Mutex lock // Solution 1: Checks-Effects-Interactions pattern function withdraw() public { uint256 amount = balances[msg.sender]; require(amount > 0, "Insufficient balance"); // Checks balances[msg.sender] = 0; // Effects: update state first (bool success, ) = msg.sender.call{value: amount}(""); // Interactions require(success, "Transfer failed"); } // Solution 2: Use ReentrancyGuard function withdrawWithGuard() public nonReentrant { uint256 amount = balances[msg.sender]; require(amount > 0, "Insufficient balance"); (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); balances[msg.sender] = 0; } // Solution 3: Custom mutex lock modifier noReentrant() { require(!locked, "Reentrant call"); locked = true; _; locked = false; } }
2. Integer Overflow/Underflow
Vulnerability Principle
Before Solidity 0.8.0, integer arithmetic overflow did not throw errors, leading to unexpected results.
solidity// Vulnerability in Solidity < 0.8.0 contract VulnerableOverflow { uint8 public counter = 255; function increment() public { counter++; // Overflows to 0! } function decrement() public { counter--; // Underflows to 255! } }
Protection Measures
solidity// Solution 1: Use Solidity 0.8.0+ contract SafeInSolidity08 { uint8 public counter = 255; function increment() public { counter++; // Automatic overflow check, reverts on overflow } } // Solution 2: Use SafeMath library (Solidity < 0.8.0) library SafeMath { function add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; require(c >= a, "Addition overflow"); return c; } function sub(uint256 a, uint256 b) internal pure returns (uint256) { require(b <= a, "Subtraction underflow"); return a - b; } function mul(uint256 a, uint256 b) internal pure returns (uint256) { if (a == 0) return 0; uint256 c = a * b; require(c / a == b, "Multiplication overflow"); return c; } } // Solution 3: Use unchecked for explicit control contract ExplicitControl { function safeAdd(uint256 a, uint256 b) internal pure returns (uint256) { unchecked { uint256 c = a + b; require(c >= a, "Overflow"); return c; } } }
3. Access Control Vulnerabilities
Vulnerability Principle
Incorrect permission checks lead to unauthorized access.
solidity// Vulnerable contract contract VulnerableAccess { address public owner; function init() public { // Dangerous: anyone can call owner = msg.sender; } function withdraw() public { // Dangerous: using tx.origin require(tx.origin == owner, "Not owner"); payable(msg.sender).transfer(address(this).balance); } }
Protection Measures
solidityimport "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; // Solution 1: Use OpenZeppelin Ownable contract SecureWithOwnable is Ownable { constructor() { // owner set in constructor } function secureWithdraw() public onlyOwner { payable(owner()).transfer(address(this).balance); } } // Solution 2: Use AccessControl contract SecureWithRoles 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); } function adminFunction() public onlyRole(ADMIN_ROLE) { // Only admin can execute } } // Solution 3: Proper constructor initialization contract ProperInitialization { address public owner; bool private initialized; constructor() { owner = msg.sender; initialized = true; } modifier onlyOwner() { require(msg.sender == owner, "Not owner"); // Use msg.sender _; } modifier notInitialized() { require(!initialized, "Already initialized"); _; } }
4. Front-Running Attacks
Vulnerability Principle
Attackers observe transactions in the mempool and execute with higher Gas prices first.
solidity// Vulnerable contract contract VulnerableFrontRunning { mapping(bytes32 => uint256) public bids; function placeBid(bytes32 itemId, uint256 amount) external { // Attacker can see bids in mempool and place higher bids require(amount > bids[itemId], "Bid too low"); bids[itemId] = amount; } }
Protection Measures
soliditycontract SecureAgainstFrontRunning { struct Bid { bytes32 blindedBid; uint256 deposit; } mapping(address => Bid[]) public bids; mapping(bytes32 => address) public highestBidder; uint256 public highestBid; // Phase 1: Submit blind bid (hash) function bid(bytes32 _blindedBid) external payable { bids[msg.sender].push(Bid({ blindedBid: _blindedBid, deposit: msg.value })); } // Phase 2: Reveal actual bid function reveal( uint256[] calldata _values, bool[] calldata _fake, bytes32[] calldata _secret ) external { // Verify revealed bid matches submitted hash // ... } // Use Commit-Reveal pattern // Or use sealed-bid auction } // Another solution: Use commit-reveal pattern contract CommitRevealAuction { struct Commit { bytes32 commitHash; uint256 deposit; bool revealed; } mapping(address => Commit) public commits; uint256 public revealDeadline; function commitBid(bytes32 _commitHash) external payable { require(block.timestamp < revealDeadline, "Commit period ended"); commits[msg.sender] = Commit({ commitHash: _commitHash, deposit: msg.value, revealed: false }); } function revealBid(uint256 _bid, bytes32 _secret) external { require(block.timestamp >= revealDeadline, "Reveal not started"); Commit storage c = commits[msg.sender]; require(!c.revealed, "Already revealed"); bytes32 hash = keccak256(abi.encodePacked(_bid, _secret)); require(hash == c.commitHash, "Invalid reveal"); c.revealed = true; // Process bid... } }
5. Timestamp Manipulation
Vulnerability Principle
Miners can manipulate block timestamps (within a certain range).
solidity// Vulnerable contract contract VulnerableTime { uint256 public gameEndTime; constructor() { gameEndTime = block.timestamp + 1 days; } function claimPrize() external { // Miner can set timestamp earlier than actual time require(block.timestamp > gameEndTime, "Game not ended"); // Distribute prize... } }
Protection Measures
soliditycontract SecureTime { uint256 public gameStartBlock; uint256 public constant GAME_DURATION_BLOCKS = 7200; // About 1 day constructor() { gameStartBlock = block.number; } // Use block number instead of timestamp function claimPrize() external { require( block.number >= gameStartBlock + GAME_DURATION_BLOCKS, "Game not ended" ); // Distribute prize... } // If timestamp is needed, add buffer function claimPrizeWithBuffer() external { // Add 15 minute buffer (about 90 blocks) require( block.timestamp >= gameEndTime + 15 minutes, "Game not ended with buffer" ); } }
6. Denial of Service (DoS)
Vulnerability Principle
Attacking contracts by exhausting Gas or blocking critical function execution.
solidity// Vulnerable contract contract VulnerableDoS { address[] public investors; mapping(address => uint256) public balances; // Dangerous: iteration may exhaust Gas function distributeDividends() external { for (uint i = 0; i < investors.length; i++) { // If investors array is large, will exhaust Gas payable(investors[i]).transfer(balances[investors[i]]); } } } // Another DoS: blocking transfers through revert contract VulnerableDoS2 { mapping(address => uint256) public balances; function withdraw() external { uint256 amount = balances[msg.sender]; // If recipient is malicious contract, reverts blocking all withdrawals (bool success, ) = msg.sender.call{value: amount}(""); require(success); balances[msg.sender] = 0; } }
Protection Measures
soliditycontract SecureAgainstDoS { mapping(address => uint256) public balances; mapping(address => bool) public withdrawn; // Solution 1: Pull pattern instead of Push pattern function withdraw() external { uint256 amount = balances[msg.sender]; require(amount > 0, "No balance"); require(!withdrawn[msg.sender], "Already withdrawn"); withdrawn[msg.sender] = true; balances[msg.sender] = 0; (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); } // Solution 2: Pagination function distributeDividends(uint256 _start, uint256 _end) external { require(_end <= investors.length, "Invalid range"); require(_end - _start <= 100, "Batch too large"); // Limit batch size for (uint i = _start; i < _end; i++) { address investor = investors[i]; if (!paid[investor]) { paid[investor] = true; payable(investor).transfer(balances[investor]); } } } // Solution 3: Use try-catch (Solidity 0.6.0+) function safeTransfer(address _to, uint256 _amount) internal { (bool success, ) = _to.call{value: _amount}(""); if (!success) { // Record failure, don't block other transfers failedTransfers[_to] += _amount; } } }
7. Random Number Vulnerabilities
Already covered in detail in the random number topic. Key points:
- Don't use block hash, timestamp, or other on-chain data for randomness
- Use oracle solutions like Chainlink VRF
- Use Commit-Reveal pattern
8. Logic Vulnerabilities
Permission Bypass
solidity// Dangerous: logic error leads to permission bypass contract LogicError { mapping(address => bool) public admins; function addAdmin(address _admin) external { // Error: missing permission check admins[_admin] = true; } } // Fix contract FixedLogic { mapping(address => bool) public admins; modifier onlyAdmin() { require(admins[msg.sender], "Not admin"); _; } function addAdmin(address _admin) external onlyAdmin { admins[_admin] = true; } }
Reentrancy Variant: Cross-Function Reentrancy
soliditycontract CrossFunctionReentrancy { mapping(address => uint256) public balances; function transfer(address _to, uint256 _amount) external { require(balances[msg.sender] >= _amount); balances[msg.sender] -= _amount; balances[_to] += _amount; } function withdraw() external { uint256 amount = balances[msg.sender]; require(amount > 0); (bool success, ) = msg.sender.call{value: amount}(""); require(success); // Attacker can call transfer in receive, then withdraw again balances[msg.sender] = 0; } } // Fix: Use mutex lock contract FixedCrossFunction is ReentrancyGuard { function withdraw() external nonReentrant { // ... } }
9. Security Development Best Practices
solidity// Complete secure contract template import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/security/Pausable.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/Address.sol"; contract SecureContractTemplate is ReentrancyGuard, Pausable, Ownable { using Address for address; // 1. Use latest compiler version pragma solidity ^0.8.19; // 2. Use built-in overflow check (0.8.0+) uint256 public counter; // 3. Use ReentrancyGuard function withdraw() external nonReentrant whenNotPaused { // ... } // 4. Use Pausable emergency pause function pause() external onlyOwner { _pause(); } // 5. Proper access control function adminFunction() external onlyOwner { // ... } // 6. Event logging event ActionExecuted(address indexed user, uint256 amount); // 7. Custom errors (Gas optimization) error InsufficientBalance(uint256 requested, uint256 available); error InvalidAddress(); // 8. Input validation function validateInput(address _addr, uint256 _amount) internal pure { if (_addr == address(0)) revert InvalidAddress(); if (_amount == 0) revert InsufficientBalance(_amount, 0); } // 9. Use Checks-Effects-Interactions pattern function secureTransfer(address _to, uint256 _amount) external { // Checks require(balances[msg.sender] >= _amount, "Insufficient balance"); // Effects balances[msg.sender] -= _amount; // Interactions (bool success, ) = _to.call{value: _amount}(""); require(success, "Transfer failed"); } // 10. Safe ETH receiving receive() external payable { emit DepositReceived(msg.sender, msg.value); } }
10. Security Audit Checklist
- Reentrancy protection (Checks-Effects-Interactions, ReentrancyGuard)
- Integer overflow check (use 0.8.0+ or SafeMath)
- Access control verification (Ownable, AccessControl)
- Input validation (zero address check, range check)
- Timestamp manipulation protection (use block number instead of timestamp)
- Random number security (use VRF)
- DoS protection (pull pattern, pagination)
- Event completeness (all state changes trigger events)
- Emergency mechanisms (Pausable, upgradeability)
- Code audit (Slither, Mythril, Certora)
11. Security Tools
- Static Analysis: Slither, Mythril, Manticore
- Formal Verification: Certora, K Framework
- Fuzzing: Echidna, Harvey
- Monitoring: Tenderly, Forta
javascript// hardhat configuration for security tools require("@nomiclabs/hardhat-truffle5"); require("hardhat-gas-reporter"); require("solidity-coverage"); module.exports = { solidity: { version: "0.8.19", settings: { optimizer: { enabled: true, runs: 200 } } }, gasReporter: { enabled: true }, };