智能合约安全审计与常见漏洞

智能合约安全的必学内容:重入攻击、整数溢出、访问控制缺失、预言机操纵、闪电贷攻击等十大常见漏洞。附带漏洞代码示例、修复方案与审计流程最佳实践。

导语:代码即法律,漏洞即灾难

智能合约一旦部署就不可修改。这不是灵活性问题——这是安全性问题。

2022 年 Ronin Network 被盗 6.25 亿美元。2021 年 Poly Network 被盗 6.11 亿美元。2022 年 Wormhole 被盗 3.25 亿美元。这些攻击的共同点是:一行 Solidity 代码的疏忽,导致了数亿美元的损失。

一句话总结:智能合约安全不是"可选的提升",而是决定了用户真金白银能否安全的生命线。


1. 重入攻击(Reentrancy)

1.1 漏洞代码

// ❌ 漏洞版本:Checks-Effects-Interactions 顺序错误
contract VulnerableBank {
    mapping(address => uint256) public balances;

    function withdraw() public {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");
        
        // 先发送 ETH(交互)
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
        
        // 后更新余额(效果)——太晚了!
        balances[msg.sender] = 0;
    }
    
    receive() external payable {
        balances[msg.sender] += msg.value;
    }
}

攻击合约利用 receive() 在转账过程中递归调用 withdraw():

contract Attacker {
    VulnerableBank public bank;
    
    receive() external payable {
        if (address(bank).balance >= 1 ether) {
            bank.withdraw();  // ← 递归调用,余额还未归零!
        }
    }
    
    function attack() external payable {
        bank.deposit{value: 1 ether}();
        bank.withdraw();
    }
}

1.2 修复方案

遵循 Checks-Effects-Interactions 模式:

// ✅ 修复:先更新状态,再外部调用
function withdraw() public {
    uint256 amount = balances[msg.sender];
    require(amount > 0, "No balance");
    
    balances[msg.sender] = 0;  // Effects 先完成
    
    (bool success, ) = msg.sender.call{value: amount}("");  // 最后交互
    require(success, "Transfer failed");
}

使用 OpenZeppelin 的 ReentrancyGuard:

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureBank is ReentrancyGuard {
    function withdraw() public nonReentrant {
        // 自动加锁防止重入
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");
        balances[msg.sender] = 0;
        payable(msg.sender).transfer(amount);
    }
}

一句话总结:重入攻击利用 Solidity 的回调机制——修复的核心是"先改状态再转账"或使用 ReentrancyGuard 加锁。


2. 整数溢出与下溢

2.1 漏洞代码

// ❌ Solidity <0.8.0 默认不检查溢出
contract VulnerableToken {
    mapping(address => uint256) public balances;
    
    function transfer(address to, uint256 amount) public {
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;  // 下溢可能!
        balances[to] += amount;          // 溢出可能!
    }
    
    function batchTransfer(address[] memory recipients, uint256 amount) public {
        uint256 total = recipients.length * amount;  // 溢出!
        require(balances[msg.sender] >= total);
        // ...
    }
}

2.2 修复

// ✅ Solidity 0.8.0+ 自动检查溢出/下溢
pragma solidity ^0.8.20;

// 或使用 unchecked 显式控制(明确知道不会溢出时节省 gas)
function transfer(address to, uint256 amount) public {
    require(balances[msg.sender] >= amount, "Insufficient balance");
    unchecked {
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }
}

// 乘法安全检查
function batchTransfer(address[] memory recipients, uint256 amount) public {
    require(recipients.length > 0, "Empty recipients");
    // 避免乘法溢出
    require(
        amount <= type(uint256).max / recipients.length,
        "Multiplication overflow"
    );
    uint256 total = recipients.length * amount;
    require(balances[msg.sender] >= total, "Insufficient balance");
    // ...
}

一句话总结:Solidity 0.8+ 内置溢出检查,但涉及乘法和除法的数学运算仍需手动防护。


3. 访问控制缺失

3.1 危险函数未保护

// ❌ 任何人都可以调用 selfdestruct 提走合约余额
contract Vulnerable {
    function withdrawAll() public {
        selfdestruct(payable(msg.sender));
    }
    
    function mint(address to, uint256 amount) public {
        // 任何人都可以无限铸币!
        _mint(to, amount);
    }
}

3.2 修复

import "@openzeppelin/contracts/access/Ownable.sol";

contract Secure is Ownable {
    function emergencyWithdraw() public onlyOwner {
        payable(owner()).transfer(address(this).balance);
    }
    
    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
}

一句话总结:任何涉及资金、权限、核心参数的函数都必须验证调用者身份——默认不信任任何人。


4. 闪电贷攻击(Flash Loan Attack)

4.1 攻击原理

闪电贷允许用户在一笔交易中借入巨额资产,条件是在交易结束前归还:

攻击者                    AMM 池              借贷协议
  │                         │                    │
  ├─ 借入 10,000 ETH ───────┼────────────┬─────►│
  │                         │            │      │
  ├─ 大额 swap 操纵价格 ────►│            │      │
  │                         │            │      │
  ├─ 在其他协议收割套利 ────┼────────────┤      │
  │                         │            │      │
  ├─ 归还 10,000 ETH+fee ───┼────────────┴─────►│
  │                         │                    │
  └─ 保留套利利润 ◄─────────┼────────────────────┘

4.2 防护策略

策略实施
TWAP 价格使用 30 分钟时间加权平均价,而非瞬时价
多源预言机Chainlink + Uniswap TWAP 交叉验证
闪电贷检测检测 tx.origin == msg.sender(闪电贷通常通过合约执行)
存款冷却期存入资金后 N 个区块内不允许取出
// 使用 Chainlink 价格喂价
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

function getPrice() public view returns (uint256) {
    (
        , int256 price, , ,
    ) = priceFeed.latestRoundData();
    require(price > 0, "Invalid price");
    return uint256(price);
}

// TWAP 价格参考
function getTWAPPrice() public view returns (uint256) {
    uint32[] memory secondsAgos = new uint32[](2);
    secondsAgos[0] = uint32(twapInterval);  // e.g. 1800s = 30min
    secondsAgos[1] = 0;
    
    (int56[] memory tickCumulatives, ) = pool.observe(secondsAgos);
    int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0];
    int24 arithmeticMeanTick = int24(tickCumulativesDelta / int56(uint56(twapInterval)));
    
    return tickToPrice(arithmeticMeanTick);
}

一句话总结:闪电贷攻击的核心武器是"瞬间操纵价格"——防御的关键是使用 TWAP、多源预言机,而不是单一瞬时点价格。


5. 其他常见漏洞

5.1 随机数可预测

// ❌ 区块哈希和时间是公开的,矿工可以操纵
function random() public view returns (uint256) {
    return uint256(keccak256(abi.encodePacked(block.timestamp, block.number)));
}

// ✅ 使用 Chainlink VRF 可验证随机数
import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";

function requestRandomWords() external {
    COORDINATOR.requestRandomWords(
        keyHash, subscriptionId, requestConfirmations, callbackGasLimit, numWords
    );
}

5.2 delegatecall 滥用

// ❌ 在代理合约中错误使用 delegatecall 可能导致存储槽冲突
contract Proxy {
    address public implementation;  // slot 0
    
    fallback() external {
        (bool success, ) = implementation.delegatecall(msg.data);
        require(success);
    }
}

// 如果 implementation 的 slot 0 是其他变量,会导致状态混乱

5.3 交易顺序依赖(Front-running)

// ❌ 拍卖中的漏洞:公开的最高出价可被抢跑
contract BadAuction {
    uint public highestBid;
    address public highestBidder;
    
    function bid() external payable {
        require(msg.value > highestBid, "Too low");
        // 攻击者看到交易后出价更高,抢跑成功
        highestBid = msg.value;
        highestBidder = msg.sender;
    }
}

// ✅ 使用 commit-reveal 机制
contract SealedAuction {
    mapping(address => bytes32) public commitments;
    mapping(address => uint256) public revealedBids;
    
    function commit(bytes32 hash) external {
        commitments[msg.sender] = hash;
    }
    
    function reveal(uint256 amount, bytes32 nonce) external {
        require(keccak256(abi.encodePacked(amount, nonce)) == commitments[msg.sender]);
        revealedBids[msg.sender] = amount;
    }
}

一句话总结:智能合约安全是一个广阔的领域——随机数、升级代理、交易顺序、签名重放等每个方向都有独特的攻击面。


6. 安全审计流程

6.1 自查清单

在提交外部审计前,团队应完成:

  1. 单元测试覆盖率 > 90% — Foundry 的输出明确标出未覆盖行
  2. 静态分析 — Slither(Trail of Bits)、Mythril(ConsenSys)
  3. Fuzz 测试 — Foundry 的 testFuzz_ 覆盖边界条件
  4. Invariant 测试 — 验证"用户总余额永远 ≤ 总供应量"等不变量
  5. 文档完整 — 每个函数的目的、参数限制、调用前提

6.2 审计工具

工具类型功能
Slither静态分析常见漏洞检测、代码复杂度分析
Mythril符号执行深度状态空间搜索
EchidnaFuzzing基于属性的随机测试
Manticore符号执行复杂路径分析
4naly3er静态分析社区版 Slither 扩展
# Slither 安装与使用
pip install slither-analyzer
slither . --filter-paths "lib|test|script"

# Echidna 属性测试
echidna-test . --contract MyToken --test-mode assertion

一句话总结:安全审计不是单次活动,而是"测试 + 静态分析 + Fuzz + 人工审计"的多层防御体系。


7. 总结

智能合约安全的关键原则:

  1. Checks-Effects-Interactions — 永远先检查、再更新状态、最后外部调用
  2. 最小权限原则 — 每个函数暴露最小必要权限
  3. 不信任输入 — 所有外部输入都可能被操纵
  4. 使用已审计库 — OpenZeppelin 的合约经过数千亿 TVL 验证
  5. 多层验证 — 自动化测试 + 静态分析 + Fuzz + 专业审计

历史上因智能合约漏洞导致的损失已超过数十亿美元。每行代码都值得被仔细审视。

延伸阅读:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「blockchain」更多文章

  1. Hardhat 与 Foundry 开发工具链
  2. DeFi 核心协议与流动性挖矿
  3. ERC-20 与 ERC-721 标准详解