跳到主要内容

智能合约安全

智能合约一旦部署就无法修改,安全漏洞可能导致不可挽回的资金损失。本章将介绍智能合约常见的安全漏洞、攻击方式和防御策略。

为什么安全至关重要

不可篡改性

智能合约部署后,代码无法直接修改。发现漏洞后,通常只能:

  • 部署新合约并迁移状态
  • 通过代理模式升级(如果预先设计)
  • 接受损失

资金风险

智能合约通常管理着大量资金,安全漏洞可能导致:

  • 资金被盗
  • 合约状态被破坏
  • 用户信任崩塌

历史案例

事件损失原因
The DAO6000 万美元重入攻击
Parity Wallet1.5 亿美元权限漏洞
Poly Network6.1 亿美元跨链验证漏洞
Wormhole3.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");
}
}

防御要点

  1. 使用 Checks-Effects-Interactions 模式
  2. 使用 ReentrancyGuard 修饰器
  3. 先更新状态,再进行外部调用

整数溢出

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 开发的核心课题。开发者需要了解常见漏洞类型,遵循安全最佳实践,并使用专业工具进行审计。记住:永远不要在生产环境中部署未经审计的合约。

参考资料