智能合约安全
智能合约安全
智能合约一旦部署,代码不可更改,且直接管理资金。安全漏洞可能导致数百万美元的损失。本文介绍常见漏洞类型和防御策略。
重入攻击(Reentrancy Attack)
重入攻击是最著名的智能合约漏洞,导致了 2016 年 The DAO 事件(损失约 6000 万美元)。
漏洞原理
// ❌ 存在重入漏洞的合约contract VulnerableBank { mapping(address => uint256) public balances;
function deposit() external payable { balances[msg.sender] += msg.value; }
function withdraw(uint256 amount) external { require(balances[msg.sender] >= amount, "Insufficient balance");
// 🚨 危险:在更新状态之前发送 ETH (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed");
balances[msg.sender] -= amount; // 此时已被重入,状态更新太晚 }}
// 攻击合约contract Attacker { VulnerableBank public bank;
constructor(address _bank) { bank = VulnerableBank(_bank); }
function attack() external payable { bank.deposit{value: msg.value}(); bank.withdraw(msg.value); }
// 每次收到 ETH 时,重新调用 withdraw receive() external payable { if (address(bank).balance >= msg.value) { bank.withdraw(msg.value); } }}防御方法
// ✅ 方法1:检查-效果-交互(Checks-Effects-Interactions)模式contract SafeBank { mapping(address => uint256) public balances;
function withdraw(uint256 amount) external { // 1. 检查(Checks) require(balances[msg.sender] >= amount, "Insufficient balance");
// 2. 效果(Effects)- 先更新状态 balances[msg.sender] -= amount;
// 3. 交互(Interactions)- 最后进行外部调用 (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); }}
// ✅ 方法2:使用 ReentrancyGuardimport "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SafeBank2 is ReentrancyGuard { mapping(address => uint256) public balances;
function withdraw(uint256 amount) external nonReentrant { require(balances[msg.sender] >= amount, "Insufficient balance"); balances[msg.sender] -= amount; (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); }}整数溢出/下溢(Integer Overflow/Underflow)
Solidity 0.8 之前的漏洞
// ❌ Solidity < 0.8.0 中的溢出漏洞contract OldToken { mapping(address => uint256) public balances;
// 恶意调用:transfer(victim, MAX_UINT256 - userBalance + 1) // 导致 victim 的余额从0溢出到一个巨大的数 function transfer(address to, uint256 amount) public { balances[msg.sender] -= amount; // 可能下溢 balances[to] += amount; // 可能上溢 }}防御方法
// ✅ Solidity 0.8.0+ 内置溢出检查contract SafeToken { // 自动检查溢出,溢出时 revert mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) public { balances[msg.sender] -= amount; // 余额不足时自动 revert balances[to] += amount; // 溢出时自动 revert }}
// 对于 Solidity < 0.8.0,使用 SafeMathimport "@openzeppelin/contracts/utils/math/SafeMath.sol";contract OldSafeToken { using SafeMath for uint256; mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) public { balances[msg.sender] = balances[msg.sender].sub(amount); balances[to] = balances[to].add(amount); }}访问控制漏洞
// ❌ 缺少访问控制contract NoAccessControl { address public owner;
// 任何人都可以调用! function setOwner(address newOwner) public { owner = newOwner; }
function withdrawAll() public { payable(owner).transfer(address(this).balance); }}
// ✅ 正确的访问控制import "@openzeppelin/contracts/access/Ownable.sol";
contract WithAccessControl is Ownable { constructor(address initialOwner) Ownable(initialOwner) {}
// 只有 owner 可以调用 function withdrawAll() public onlyOwner { payable(owner()).transfer(address(this).balance); }}
// ✅ 基于角色的访问控制(RBAC)import "@openzeppelin/contracts/access/AccessControl.sol";
contract RoleBasedContract is AccessControl { bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
constructor() { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(MINTER_ROLE, msg.sender); }
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { // 铸造逻辑 }}闪电贷攻击(Flash Loan Attack)
闪电贷允许在同一笔交易内无担保借贷巨额资金,被攻击者用于操控预言机价格。
// ❌ 使用单一 DEX 价格作为预言机(可被闪电贷操控)contract VulnerableOracle { IUniswapV2Pair public pair;
function getPrice() public view returns (uint256) { (uint112 reserve0, uint112 reserve1,) = pair.getReserves(); // 🚨 单点价格,可被大额交易操控 return uint256(reserve1) * 1e18 / uint256(reserve0); }}
// ✅ 使用 Chainlink 等去中心化预言机import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract SafeOracle { AggregatorV3Interface public priceFeed;
constructor(address _priceFeed) { priceFeed = AggregatorV3Interface(_priceFeed); }
function getPrice() public view returns (int256) { ( uint80 roundID, int256 price, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound ) = priceFeed.latestRoundData();
require(updatedAt >= block.timestamp - 3600, "Price is stale"); require(price > 0, "Invalid price"); return price; }}时间戳操纵
// ❌ 依赖 block.timestamp 作为随机数contract BadRandom { function random() public view returns (uint256) { // 🚨 矿工可以操控时间戳(在一定范围内) return uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender))); }}
// ✅ 使用 Chainlink VRF 获取真随机数import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
contract SafeRandom is VRFConsumerBaseV2 { // ... 使用 Chainlink VRF}委托调用漏洞(Delegatecall)
// ❌ 不安全的代理合约contract UnsafeProxy { address public implementation; address public owner;
// 通过 delegatecall 执行逻辑合约代码 fallback() external payable { address impl = implementation; assembly { calldatacopy(0, 0, calldatasize()) let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) switch result case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } }}
// 如果逻辑合约有 initialize 函数且没有保护,// 攻击者可以调用 initialize 成为 owner!contract Logic { address public owner; // 🚨 存储槽与代理合约冲突
function initialize(address _owner) external { owner = _owner; // 攻击者调用此函数成为 owner }}tx.origin 钓鱼攻击
// ❌ 使用 tx.origin 进行身份验证contract VulnerableWallet { address public owner;
function withdraw(uint256 amount) external { // 🚨 tx.origin 是原始发送者,可被中间合约利用 require(tx.origin == owner, "Not owner"); payable(tx.origin).transfer(amount); }}
// 攻击流程:// 1. 攻击者部署恶意合约// 2. 诱骗 owner 调用恶意合约的任意函数// 3. 恶意合约中调用 VulnerableWallet.withdraw()// 4. tx.origin == owner 通过,资金被盗
// ✅ 使用 msg.sender 而非 tx.origincontract SafeWallet { address public owner;
function withdraw(uint256 amount) external { require(msg.sender == owner, "Not owner"); // ✅ payable(msg.sender).transfer(amount); }}短地址攻击
// ✅ 使用 OpenZeppelin 的 SafeERC20 防止短地址攻击import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract SafeTransfer { using SafeERC20 for IERC20;
function safeTransferTokens(IERC20 token, address to, uint256 amount) external { token.safeTransfer(to, amount); }}价格操纵(AMM 预言机)
// ✅ 使用 TWAP(时间加权平均价格)防止操纵interface IUniswapV3Pool { function observe(uint32[] calldata secondsAgos) external view returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s);}
contract TWAPOracle { IUniswapV3Pool public pool;
function getTWAP(uint32 secondsAgo) public view returns (uint256) { uint32[] memory secondsAgos = new uint32[](2); secondsAgos[0] = secondsAgo; secondsAgos[1] = 0;
(int56[] memory tickCumulatives, ) = pool.observe(secondsAgos); int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0]; int24 arithmeticMeanTick = int24(tickCumulativesDelta / int56(uint56(secondsAgo)));
// 将 tick 转换为价格 return TickMath.getSqrtRatioAtTick(arithmeticMeanTick); }}合约安全检查清单
在部署前,检查以下内容:
代码审查
- 遵循检查-效果-交互(CEI)模式
- 所有外部调用都有重入保护
- 访问控制完整,关键函数有权限检查
- 不使用
tx.origin进行身份验证 - 不使用区块变量作为随机数源
- 数学运算安全(Solidity 0.8+ 或 SafeMath)
- 正确处理 ERC20 返回值(使用 SafeERC20)
测试
- 单元测试覆盖率 > 90%
- 模糊测试(Foundry fuzz test)
- 对每个已知漏洞类型编写测试
- 在分叉测试中验证与主网合约的交互
工具扫描
- Slither 静态分析
- MythX 或 Echidna 符号执行
- 使用 OpenZeppelin Defender 监控
代码审计
- 至少一次专业安全审计
- 漏洞赏金计划
常用安全工具
# Slither - 静态分析工具pip3 install slither-analyzerslither .
# Mythril - 符号执行工具pip3 install mythrilmyth analyze contracts/Token.sol
# Echidna - 属性测试/模糊测试# 需要安装,见官方文档
# Foundry 内置的模糊测试forge test --fuzz-runs 10000安全模式和最佳实践
// 1. 拉取(Pull)而非推送(Push)支付contract PullPayment { mapping(address => uint256) public credits;
// 不要主动发送 ETH,让用户主动提取 function withdrawCredits() external { uint256 amount = credits[msg.sender]; credits[msg.sender] = 0; (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Withdrawal failed"); }}
// 2. 使用暂停功能应急处理import "@openzeppelin/contracts/security/Pausable.sol";
contract PausableContract is Pausable, Ownable { constructor(address owner) Ownable(owner) {}
function criticalFunction() external whenNotPaused { // 紧急情况可暂停 }
function pause() external onlyOwner { _pause(); }
function unpause() external onlyOwner { _unpause(); }}
// 3. 时间锁(Timelock)用于重要操作contract TimelockController { uint256 public constant MIN_DELAY = 2 days;
mapping(bytes32 => uint256) public scheduledTime;
function schedule(bytes32 id) external onlyOwner { scheduledTime[id] = block.timestamp + MIN_DELAY; }
function execute(bytes32 id) external { require(scheduledTime[id] != 0, "Not scheduled"); require(block.timestamp >= scheduledTime[id], "Too early"); // 执行操作 delete scheduledTime[id]; }}历史重大安全事件
| 事件 | 时间 | 损失 | 漏洞类型 |
|---|---|---|---|
| The DAO | 2016-06 | $60M | 重入攻击 |
| Parity Wallet | 2017-11 | $150M | 访问控制 |
| bZx | 2020-02 | $954K | 闪电贷+预言机操纵 |
| Cream Finance | 2021-10 | $130M | 重入攻击 |
| Nomad Bridge | 2022-08 | $190M | 验证逻辑错误 |
| Euler Finance | 2023-03 | $197M | 闪电贷+逻辑错误 |
总结
智能合约安全的核心原则:
- 最小权限原则:只赋予合约所需的最小权限
- 失败安全:系统故障时应进入安全状态
- 纵深防御:多层安全措施
- 保持简单:代码越简单,漏洞越少
- 审计先行:部署前进行专业安全审计
推荐资源: