智能合约安全
智能合约一旦部署就无法修改,安全漏洞可能导致不可挽回的资金损失。本章将介绍智能合约常见的安全漏洞、攻击方式和防御策略。
为什么安全至关重要
不可篡改性
智能合约部署后,代码无法直接修改。发现漏洞后,通常只能:
- 部署新合约并迁移状态
- 通过代理模式升级(如果预先设计)
- 接受损失
资金风险
智能合约通常管理着大量资金,安全漏洞可能导致:
- 资金被盗
- 合约状态被破坏
- 用户信任崩塌
历史案例
| 事件 | 损失 | 原因 |
|---|---|---|
| The DAO | 6000 万美元 | 重入攻击 |
| Parity Wallet | 1.5 亿美元 | 权限漏洞 |
| Poly Network | 6.1 亿美元 | 跨链验证漏洞 |
| Wormhole | 3.2 亿美元 | 签名验证漏洞 |
常见漏洞类型
重入攻击
重入攻击是最著名的安全漏洞,攻击者在合约更新状态前再次调用合约函数。
漏洞代码
contract VulnerableBank {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
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;
}
}
攻击合约
contract Attacker {
VulnerableBank public bank;
constructor(address _bank) {
bank = VulnerableBank(_bank);
}
function attack() external payable {
bank.deposit{value: msg.value}();
bank.withdraw();
}
receive() external payable {
if (address(bank).balance >= 1 ether) {
bank.withdraw();
}
}
}
防御方案
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract SecureBank is ReentrancyGuard {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external 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");
}
}
防御要点:
- 使用 Checks-Effects-Interactions 模式
- 使用 ReentrancyGuard 修饰器
- 先更新状态,再进行外部调用
整数溢出
Solidity 0.8.0 之前,整数运算可能溢出。
漏洞代码(0.8.0 之前)
contract VulnerableToken {
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
防御方案
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
contract SafeToken {
using SafeMath for uint256;
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] = balances[msg.sender].sub(amount);
balances[to] = balances[to].add(amount);
}
}
contract ModernToken {
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
访问控制漏洞
错误的访问控制可能导致敏感函数被恶意调用。
漏洞代码
contract VulnerableContract {
address public owner;
function setOwner(address _owner) external {
owner = _owner;
}
}
防御方案
import "@openzeppelin/contracts/access/Ownable.sol";
contract SecureContract is Ownable {
address public admin;
constructor() Ownable(msg.sender) {}
function setAdmin(address _admin) external onlyOwner {
require(_admin != address(0), "Invalid address");
admin = _admin;
}
modifier onlyAdmin() {
require(msg.sender == admin, "Not admin");
_;
}
}
闪电贷攻击
攻击者利用闪电贷借出大量资金,操纵价格后获利。
攻击原理
┌─────────────────────────────────────────────────────────────┐
│ 闪电贷攻击流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 从 Aave/dYdX 借出大量代币 │
│ │ │
│ ▼ │
│ 2. 在 DEX 上大量买入/卖出,操纵价格 │
│ │ │
│ ▼ │
│ 3. 利用被操纵的价格在目标协议获利 │
│ │ │
│ ▼ │
│ 4. 归还闪电贷 + 手续费 │
│ │ │
│ ▼ │
│ 5. 保留利润 │
│ │
└─────────────────────────────────────────────────────────────┘
防御方案
contract FlashLoanResistant {
uint256 public lastPriceUpdateTime;
uint256 public cachedPrice;
uint256 public constant PRICE_UPDATE_DELAY = 30 minutes;
function getPrice() external view returns (uint256) {
require(
block.timestamp >= lastPriceUpdateTime + PRICE_UPDATE_DELAY,
"Price not updated recently"
);
return cachedPrice;
}
function updatePrice(uint256 _newPrice) external {
require(
block.timestamp >= lastPriceUpdateTime + PRICE_UPDATE_DELAY,
"Update too frequent"
);
lastPriceUpdateTime = block.timestamp;
cachedPrice = _newPrice;
}
}
前端运行(Frontrunning)
攻击者监控待处理交易,抢先执行以获利。
漏洞场景
contract VulnerableDEX {
function swap(
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 minAmountOut
) external {
uint256 amountOut = calculateOutput(amountIn, tokenIn, tokenOut);
require(amountOut >= minAmountOut, "Slippage too high");
IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
IERC20(tokenOut).transfer(msg.sender, amountOut);
}
}
防御方案
contract SecureDEX {
struct Order {
address user;
address tokenIn;
address tokenOut;
uint256 amountIn;
uint256 minAmountOut;
uint256 deadline;
uint256 nonce;
}
mapping(bytes32 => bool) public executedOrders;
function swapWithSignature(
Order calldata order,
bytes calldata signature
) external {
bytes32 orderHash = keccak256(abi.encode(
order.user,
order.tokenIn,
order.tokenOut,
order.amountIn,
order.minAmountOut,
order.deadline,
order.nonce
));
require(!executedOrders[orderHash], "Order already executed");
require(block.timestamp <= order.deadline, "Order expired");
address signer = recoverSigner(orderHash, signature);
require(signer == order.user, "Invalid signature");
executedOrders[orderHash] = true;
uint256 amountOut = calculateOutput(order.amountIn, order.tokenIn, order.tokenOut);
require(amountOut >= order.minAmountOut, "Slippage too high");
IERC20(order.tokenIn).transferFrom(order.user, address(this), order.amountIn);
IERC20(order.tokenOut).transfer(order.user, amountOut);
}
function recoverSigner(
bytes32 hash,
bytes calldata signature
) internal pure returns (address) {
bytes32 ethSignedHash = keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)
);
(bytes32 r, bytes32 s, uint8 v) = splitSignature(signature);
return ecrecover(ethSignedHash, v, r, s);
}
function splitSignature(
bytes calldata sig
) internal pure returns (bytes32 r, bytes32 s, uint8 v) {
require(sig.length == 65, "Invalid signature length");
r = bytes32(sig[0:32]);
s = bytes32(sig[32:64]);
v = uint8(sig[64]);
}
}
拒绝服务(DoS)
恶意攻击者可能导致合约无法正常工作。
漏洞代码
contract VulnerableAuction {
address public highestBidder;
uint256 public highestBid;
address[] public bidders;
function bid() external payable {
require(msg.value > highestBid, "Bid too low");
if (highestBidder != address(0)) {
payable(highestBidder).transfer(highestBid);
}
highestBidder = msg.sender;
highestBid = msg.value;
bidders.push(msg.sender);
}
function refundAll() external {
for (uint256 i = 0; i < bidders.length; i++) {
payable(bidders[i]).transfer(1 ether);
}
}
}
防御方案
contract SecureAuction {
address public highestBidder;
uint256 public highestBid;
mapping(address => uint256) public pendingReturns;
event NewHighestBid(address indexed bidder, uint256 amount);
event BidRefunded(address indexed bidder, uint256 amount);
function bid() external payable {
require(msg.value > highestBid, "Bid too low");
if (highestBidder != address(0)) {
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
emit NewHighestBid(msg.sender, msg.value);
}
function withdraw() external {
uint256 amount = pendingReturns[msg.sender];
require(amount > 0, "No pending returns");
pendingReturns[msg.sender] = 0;
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
emit BidRefunded(msg.sender, amount);
}
}
预言机操纵
攻击者操纵价格预言机,影响合约决策。
防御方案
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract SecurePriceOracle {
AggregatorV3Interface public priceFeed;
uint256 public constant PRICE_TOLERANCE = 5;
constructor(address _priceFeed) {
priceFeed = AggregatorV3Interface(_priceFeed);
}
function getLatestPrice() public view returns (int256) {
(
,
int256 price,
,
,
) = priceFeed.latestRoundData();
require(price > 0, "Invalid price");
return price;
}
function getTWAP(
uint256 _secondsAgo
) public view returns (int256) {
uint256 endTime = block.timestamp;
uint256 startTime = endTime - _secondsAgo;
int256 totalPrice = 0;
uint256 count = 0;
for (uint256 i = 0; i < 10; i++) {
(
uint80 roundId,
int256 price,
,
uint256 updatedAt,
) = priceFeed.getRoundData(i + 1);
if (updatedAt >= startTime && updatedAt <= endTime) {
totalPrice += price;
count++;
}
}
require(count > 0, "No price data in range");
return totalPrice / int256(count);
}
}
安全开发最佳实践
代码审计清单
- 所有外部调用是否安全?
- 是否使用了重入保护?
- 访问控制是否正确?
- 整数运算是否安全?
- 是否处理了边界情况?
- 是否有适当的错误处理?
- 是否使用了安全的随机数?
- 是否有紧急暂停机制?
- 是否有时间锁保护?
- 是否有足够的测试覆盖?
使用 OpenZeppelin
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract SecureContract is
ERC20,
Ownable,
ReentrancyGuard,
Pausable
{
constructor(
string memory name,
string memory symbol
)
ERC20(name, symbol)
Ownable(msg.sender)
{}
function mint(
address to,
uint256 amount
) external onlyOwner whenNotPaused nonReentrant {
_mint(to, amount);
}
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
}
事件日志
contract EventLogging {
event Deposit(
address indexed user,
uint256 amount,
uint256 timestamp
);
event Withdrawal(
address indexed user,
uint256 amount,
uint256 timestamp
);
event EmergencyPause(
address indexed by,
uint256 timestamp
);
function deposit() external payable {
emit Deposit(msg.sender, msg.value, block.timestamp);
}
function withdraw(uint256 amount) external {
emit Withdrawal(msg.sender, amount, block.timestamp);
}
}
紧急情况处理
contract EmergencyProtocol {
bool public paused;
address public admin;
uint256 public pauseTime;
uint256 public constant PAUSE_DURATION = 3 days;
event Paused(address indexed by, uint256 timestamp);
event Unpaused(address indexed by, uint256 timestamp);
modifier whenNotPaused() {
require(!paused, "Contract is paused");
_;
}
modifier whenPaused() {
require(paused, "Contract is not paused");
_;
}
function pause() external {
require(msg.sender == admin, "Not admin");
paused = true;
pauseTime = block.timestamp;
emit Paused(msg.sender, block.timestamp);
}
function unpause() external whenPaused {
require(
block.timestamp >= pauseTime + PAUSE_DURATION,
"Pause duration not elapsed"
);
paused = false;
emit Unpaused(msg.sender, block.timestamp);
}
function emergencyWithdraw(
address token,
address to,
uint256 amount
) external whenPaused {
require(msg.sender == admin, "Not admin");
if (token == address(0)) {
payable(to).transfer(amount);
} else {
IERC20(token).transfer(to, amount);
}
}
}
安全工具
静态分析工具
| 工具 | 类型 | 特点 |
|---|---|---|
| Slither | 静态分析 | 快速、全面 |
| Mythril | 符号执行 | 深度分析 |
| Securify | 静态分析 | 模式匹配 |
| Echidna | 模糊测试 | 属性测试 |
使用 Slither
pip install slither-analyzer
slither ./contracts/
使用 Foundry 测试
contract SecurityTest is Test {
SecureContract target;
address attacker = address(0x1);
address user = address(0x2);
function setUp() public {
target = new SecureContract();
}
function test_ReentrancyProtection() public {
vm.startPrank(attacker);
vm.expectRevert("Reentrant call");
target.attack();
vm.stopPrank();
}
function test_AccessControl() public {
vm.startPrank(attacker);
vm.expectRevert("Not owner");
target.adminFunction();
vm.stopPrank();
}
function testFuzz_Overflow(uint256 amount) public {
vm.assume(amount > 0);
vm.assume(amount < type(uint256).max);
target.deposit{value: amount}();
assertEq(target.balanceOf(attacker), amount);
}
}
小结
智能合约安全是 Web3 开发的核心课题。开发者需要了解常见漏洞类型,遵循安全最佳实践,并使用专业工具进行审计。记住:永远不要在生产环境中部署未经审计的合约。