常见漏洞 #

一、重入攻击 #

1.1 漏洞描述 #

重入攻击是最著名的智能合约漏洞之一,攻击者利用合约在更新状态前进行外部调用的漏洞。

1.2 漏洞示例 #

solidity
// 有漏洞的合约
contract VulnerableBank {
    mapping(address => uint256) public balances;
    
    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }
    
    function withdraw() public {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");
        
        // 危险:先转账,后更新状态
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
        
        balances[msg.sender] = 0;  // 状态更新太晚
    }
}

1.3 攻击合约 #

solidity
contract Attack {
    VulnerableBank public bank;
    
    constructor(address _bank) {
        bank = VulnerableBank(_bank);
    }
    
    receive() external payable {
        if (address(bank).balance >= 1 ether) {
            bank.withdraw();  // 重入调用
        }
    }
    
    function attack() public payable {
        bank.deposit{value: 1 ether}();
        bank.withdraw();
    }
}

1.4 防御方案 #

solidity
contract SafeBank {
    mapping(address => uint256) public balances;
    
    // 方案1:检查-生效-交互模式
    function withdraw() public {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");
        
        // 1. 检查(已在上面的require)
        // 2. 生效:先更新状态
        balances[msg.sender] = 0;
        
        // 3. 交互:后转账
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

contract SafeBankWithReentrancyGuard {
    mapping(address => uint256) public balances;
    bool private _locked;
    
    // 方案2:重入锁
    modifier nonReentrant() {
        require(!_locked, "Reentrant call");
        _locked = true;
        _;
        _locked = false;
    }
    
    function withdraw() public nonReentrant {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");
        
        balances[msg.sender] = 0;
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

二、整数溢出 #

2.1 漏洞描述 #

整数溢出发生在算术运算结果超出类型范围时。

2.2 Solidity 0.8.x 的改进 #

solidity
contract OverflowProtection {
    // Solidity 0.8.x 内置溢出检查
    function safeAdd(uint8 a, uint8 b) public pure returns (uint8) {
        return a + b;  // 如果溢出会自动revert
    }
}

// 0.7.x及以下版本需要SafeMath
library SafeMath {
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: addition overflow");
        return c;
    }
}

2.3 unchecked块 #

solidity
contract UncheckedExample {
    function unsafeAdd(uint256 a, uint256 b) public pure returns (uint256) {
        unchecked {
            return a + b;  // 绕过溢出检查,可能回绕
        }
    }
}

三、权限漏洞 #

3.1 tx.origin钓鱼攻击 #

solidity
// 有漏洞的合约
contract VulnerableWallet {
    address public owner;
    
    constructor() {
        owner = msg.sender;
    }
    
    // 危险:使用tx.origin检查权限
    function transfer(address to, uint256 amount) public {
        require(tx.origin == owner, "Not authorized");
        payable(to).transfer(amount);
    }
}

// 攻击合约
contract PhishingAttack {
    VulnerableWallet public wallet;
    
    constructor(address _wallet) {
        wallet = VulnerableWallet(_wallet);
    }
    
    receive() external payable {
        // 如果owner调用了这个合约,会绕过检查
        wallet.transfer(msg.sender, address(wallet).balance);
    }
}

// 安全的做法
contract SafeWallet {
    address public owner;
    
    constructor() {
        owner = msg.sender;
    }
    
    function transfer(address to, uint256 amount) public {
        require(msg.sender == owner, "Not authorized");  // 安全
        payable(to).transfer(amount);
    }
}

3.2 未初始化的存储指针 #

solidity
// 有漏洞的合约
contract VulnerableStorage {
    struct User {
        string name;
        uint256 balance;
    }
    
    mapping(address => User) public users;
    
    // 危险:未初始化的存储指针
    function updateUser(string memory _name) public {
        User storage user;  // 未初始化,指向slot 0
        user.name = _name;
    }
}

// 安全的做法
contract SafeStorage {
    struct User {
        string name;
        uint256 balance;
    }
    
    mapping(address => User) public users;
    
    function updateUser(string memory _name) public {
        User storage user = users[msg.sender];  // 正确初始化
        user.name = _name;
    }
}

四、可见性问题 #

4.1 意外的public函数 #

solidity
// 有漏洞的合约
contract VulnerableVisibility {
    address public owner;
    uint256 private secret;
    
    // 忘记指定可见性,默认为public
    function initOwner() {
        owner = msg.sender;
    }
    
    // 或者错误地设置为external
    function setSecret(uint256 _secret) external {
        secret = _secret;
    }
}

// 安全的做法
contract SafeVisibility {
    address public owner;
    uint256 private secret;
    
    function initOwner() internal {  // 明确指定internal
        owner = msg.sender;
    }
    
    function setSecret(uint256 _secret) internal {
        secret = _secret;
    }
}

五、随机数问题 #

5.1 可预测的随机数 #

solidity
// 有漏洞的合约
contract VulnerableRandom {
    // 危险:使用区块变量作为随机数源
    function random() public view returns (uint256) {
        return uint256(keccak256(abi.encodePacked(block.timestamp, block.difficulty)));
    }
    
    function play() public view returns (bool) {
        return random() % 2 == 0;
    }
}

// 矿工可以操纵block.timestamp和block.difficulty

// 更好的方案:使用Chainlink VRF
interface VRFCoordinator {
    function requestRandomWords() external returns (uint256);
}

六、拒绝服务攻击 #

6.1 循环DoS #

solidity
// 有漏洞的合约
contract VulnerableRefund {
    address[] public investors;
    
    function refundAll() public {
        for (uint256 i = 0; i < investors.length; i++) {
            payable(investors[i]).transfer(1 ether);  // 如果一个失败,全部失败
        }
    }
}

// 安全的做法
contract SafeRefund {
    address[] public investors;
    mapping(address => bool) public refunded;
    
    function claimRefund() public {
        require(!refunded[msg.sender], "Already refunded");
        refunded[msg.sender] = true;
        payable(msg.sender).transfer(1 ether);
    }
}

七、最佳实践 #

7.1 安全检查清单 #

检查项 说明
重入保护 使用nonReentrant修饰器
整数溢出 使用Solidity 0.8.x或SafeMath
权限检查 使用msg.sender而非tx.origin
可见性 明确指定所有函数可见性
随机数 使用Chainlink VRF等安全方案
外部调用 检查返回值,处理失败情况

7.2 使用OpenZeppelin #

solidity
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";

contract SecureContract is ReentrancyGuard, Ownable, Pausable {
    mapping(address => uint256) public balances;
    
    function withdraw() public nonReentrant whenNotPaused {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");
        
        balances[msg.sender] = 0;
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

八、总结 #

常见漏洞要点:

漏洞 防御方案
重入攻击 检查-生效-交互模式、重入锁
整数溢出 使用Solidity 0.8.x
权限漏洞 使用msg.sender
可见性问题 明确指定可见性
随机数 使用Chainlink VRF
DoS攻击 避免批量操作依赖

接下来,让我们学习安全模式!

最后更新:2026-03-27