跳转到内容

智能合约安全

智能合约安全

智能合约一旦部署,代码不可更改,且直接管理资金。安全漏洞可能导致数百万美元的损失。本文介绍常见漏洞类型和防御策略。

重入攻击(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:使用 ReentrancyGuard
import "@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,使用 SafeMath
import "@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.origin
contract 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 监控

代码审计

  • 至少一次专业安全审计
  • 漏洞赏金计划

常用安全工具

Terminal window
# Slither - 静态分析工具
pip3 install slither-analyzer
slither .
# Mythril - 符号执行工具
pip3 install mythril
myth 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 DAO2016-06$60M重入攻击
Parity Wallet2017-11$150M访问控制
bZx2020-02$954K闪电贷+预言机操纵
Cream Finance2021-10$130M重入攻击
Nomad Bridge2022-08$190M验证逻辑错误
Euler Finance2023-03$197M闪电贷+逻辑错误

总结

智能合约安全的核心原则:

  1. 最小权限原则:只赋予合约所需的最小权限
  2. 失败安全:系统故障时应进入安全状态
  3. 纵深防御:多层安全措施
  4. 保持简单:代码越简单,漏洞越少
  5. 审计先行:部署前进行专业安全审计

推荐资源: