Solidity智能合约安全攻防实战:从重入攻击到闪电贷完整审计

一、引言

2022年DeFi黑客攻击损失超38亿美元,90%来自合约漏洞。最贵的一课:Wormhole桥3.26亿美元(签名验证绕过)。本文将复现Top5真实漏洞并构建完整审计工具链。

二、重入攻击(Reentrancy)

2.1 经典重入:DAO攻击(损失6000万美元)

// ❌ 有漏洞的合约
contract VulnerableBank {
    mapping(address => uint256) public balances;
    
    function withdraw() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");
        
        // ★ 先转账,后更新余额 → 重入漏洞!
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);
        
        balances[msg.sender] = 0;  // 这行永远不会执行
    }
}

// 攻击合约
contract ReentrancyAttack {
    VulnerableBank public bank;
    
    function attack() external payable {
        bank.deposit{value: 1 ether}();
        bank.withdraw();  // 触发重入
    }
    
    receive() external payable {
        if (address(bank).balance >= 1 ether) {
            bank.withdraw();  // ★ 递归调用,在余额清零前重复提款
        }
    }
}

// ✅ 修复方案1: CEI模式(Checks-Effects-Interactions)
contract SafeBank {
    mapping(address => uint256) public balances;
    mapping(address => bool) private locked;
    
    modifier nonReentrant() {
        require(!locked[msg.sender], "Reentrant call");
        locked[msg.sender] = true;
        _;
        locked[msg.sender] = false;
    }
    
    function withdraw() external nonReentrant {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");
        
        // ✅ CEI: 先Effect(更新状态)
        balances[msg.sender] = 0;
        
        // 再Interaction(外部调用)
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);
    }
}

// ✅ 修复方案2: OpenZeppelin ReentrancyGuard
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SafeBankV2 is ReentrancyGuard {
    function withdraw() external nonReentrant {
        // 自动防重入
    }
}

2.2 跨函数重入(更难发现)

contract CrossFunctionReentrancy {
    mapping(address => uint256) public shares;
    
    function withdraw(uint256 amount) external {
        require(shares[msg.sender] >= amount);
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok);
        shares[msg.sender] -= amount;  // 漏洞:先转账
    }
    
    function transfer(address to, uint256 amount) external {
        require(shares[msg.sender] >= amount);
        shares[msg.sender] -= amount;
        shares[to] += amount;
    }
    // 攻击: withdraw回调中调用transfer→读取旧shares值→重复提款
}

三、闪电贷攻击

3.1 价格操纵

// Euler Finance攻击(损失1.97亿美元)简化版
contract LendingProtocol {
    function getPrice(address token) public view returns (uint256) {
        // ★ 依赖Uniswap现货价格 → 可被闪电贷操纵!
        return uniswapPair.getReserves().token0 / uniswapPair.getReserves().token1;
    }
    
    function borrow(address token, uint256 amount) external {
        uint256 collateral = amount * getPrice(token) * 1.5;
        require(collateralTokens[msg.sender] >= collateral);
        token.transfer(msg.sender, amount);
    }
}

// 攻击流程:
// 1. 闪电贷借100M USDC
// 2. 用50M USDC在Uniswap上swap,拉高目标Token价格100x
// 3. 以操纵后的价格在LendingProtocol超额借出所有资产
// 4. 归还闪电贷(0.09%手续费),套利走人

// ✅ 修复:使用TWAP(时间加权平均价格)
contract SafeLending {
    function getPriceTWAP(address token) public view returns (uint256) {
        // Uniswap V2 TWAP: 30分钟均价,操控成本极高
        (uint256 price0Cumulative, , uint32 blockTimestamp) = 
            uniswapPair.getReserves();
        // TWAP = (currentCumulative - lastCumulative) / timeElapsed
        return calculateTWAP(price0Cumulative, lastCumulative, blockTimestamp);
    }
}

四、预言机操纵

// Compound攻击简化版(1.1亿美元):
// 攻击者通过操纵预言机价格,用极低成本抵押品借出超额资产

contract OracleManipulation {
    // Mango Markets漏洞:操纵MNGO代币价格→无限借贷
    function attack() external {
        // 1. 闪电贷借大量USDC
        // 2. 在Serum DEX上大买MNGO,推高价格10x
        // 3. 以膨胀后的MNGO作抵押,借出所有USDC/BTC/ETH
        // 4. 提走所有资产,不还贷
        // 根本原因:预言机直接读取DEX现货价格
    }
}

// ✅ 最佳实践:Chainlink去中心化预言机
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract SafeOracle {
    AggregatorV3Interface internal priceFeed;
    
    constructor() {
        // ETH/USD: 多节点聚合+心跳检测+异常过滤
        priceFeed = AggregatorV3Interface(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419);
    }
    
    function getLatestPrice() public view returns (int256) {
        (, int256 price, , uint256 updatedAt, ) = priceFeed.latestRoundData();
        require(updatedAt >= block.timestamp - 3600, "Stale price");
        return price;
    }
}

五、审计工具链实战

5.1 Slither静态分析

# 安装
pip install slither-analyzer

# 扫描重入漏洞
slither contracts/Bank.sol --detect reentrancy-eth

# 生成调用图+继承图
slither contracts/ --print call-graph
# 输出: Bank.withdraw() → msg.sender.call{value}() [EXTERNAL] [TAINTED]

# 自定义检测器(Detector)
# 写入 detectors/my_detector.py
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification

class UncheckedExternalCall(AbstractDetector):
    ARGUMENT = "unchecked-external"
    HELP = "Missing success check on external call"
    IMPACT = DetectorClassification.HIGH
    CONFIDENCE = DetectorClassification.HIGH
    
    def _detect(self):
        results = []
        for contract in self.compilation_unit.contracts_derived:
            for function in contract.functions:
                for node in function.nodes:
                    if node.contains_require_or_assert(): continue
                    # 检查.call返回值是否被检查
                    for ir in node.irs:
                        if "call" in str(ir) and "success" not in str(ir):
                            results.append(self.generate_result([
                                f"Unchecked external call in {function.name}\n"
                            ]))
        return results

# 运行自定义检测器
slither contracts/ --detect unchecked-external

5.2 Foundry模糊测试

// test/FuzzBank.t.sol
import "forge-std/Test.sol";
import "../src/Bank.sol";

contract FuzzBankTest is Test {
    Bank bank;
    
    function setUp() public {
        bank = new Bank();
    }
    
    // ★ 模糊测试:随机1000次不同金额的存取
    function testFuzz_DepositWithdraw(uint256 amount) public {
        vm.assume(amount > 0 && amount < 1000 ether);
        
        uint256 balanceBefore = address(bank).balance;
        
        vm.deal(address(this), amount);
        bank.deposit{value: amount}();
        
        uint256 bal = bank.balances(address(this));
        bank.withdraw();
        
        // 不变式:取出后银行余额不变
        assertEq(address(bank).balance, balanceBefore);
    }
    
    // ★ 符号执行:Foundry自动尝试所有路径
    function testSymbolic_Invariant() public {
        // handler + invariant检查
    }
}

// 运行: forge test --fuzz-runs 10000 -vvvv

5.3 Mythril符号执行

pip install mythril

# 符号执行扫描
myth analyze contracts/Bank.sol --solv 0.8.19

# 输出示例:
# ==== Unprotected SELFDESTRUCT Instruction ====
# SWC ID: 106
# Severity: High
# Contract: VulnerableWallet
# Function: kill()
# The contract can be killed by anyone

# ==== Integer Overflow ====
# SWC ID: 101
# Severity: High  
# The arithmetic operation can overflow

六、十大安全CheckList

#检查项工具严重性
1CEI模式Slither+手工🔴 致命
2未检查.call返回值Slither🔴 高
3预言机价格操纵手工+监控🔴 高
4tx.origin认证Slither🟡 中
5整数溢出Solidity 0.8自动检查🟢 低
6未初始化storage指针Slither🔴 高
7时间戳依赖(block.timestamp)Mythril🟡 中
8随机数可预测性Mythril🔴 高
9访问控制缺失Slither🔴 致命
10Gas消耗循环手工审查🟡 中

七、总结

智能合约安全三要素:

  1. CEI是铁律 — Checks→Effects→Interactions顺序不能变
  2. 预言机不可信DEX现货 — 必须TWAP/Chainlink多源聚合
  3. 工具链必须全 — Slither(静态)+Foundry(模糊)+Mythril(符号)+Certora(形式化验证)

记住:代码即法律,部署即永恒。审计不是可选项,是必选项。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐