Solidity 安全实施手册
此文件包含技能引用的详细模式、清单和代码示例。
Solidity 安全
掌握智能合约安全最佳实践、漏洞预防和安全的 Solidity 开发模式。
何时使用此技能
- 编写安全的智能合约
- 审计现有合约的漏洞
- 实施安全的 DeFi 协议
- 防止重入、溢出和访问控制问题
- 在保持安全的同时优化 Gas 使用
- 为专业审计准备合约
- 理解常见的攻击向量
关键漏洞
1. 重入(Reentrancy)
攻击者在状态更新之前回调你的合约。
有漏洞的代码:
// VULNERABLE TO REENTRANCY contract VulnerableBank { mapping(address => uint256) public balances; function withdraw() public { uint256 amount = balances[msg.sender]; // DANGER: External call before state update (bool success, ) = msg.sender.call{value: amount}(""); require(success); balances[msg.sender] = 0; // Too late! } }安全模式(检查-效果-交互 Checks-Effects-Interactions):
contract SecureBank { mapping(address => uint256) public balances; function withdraw() public { uint256 amount = balances[msg.sender]; require(amount > 0, "Insufficient balance"); // EFFECTS: Update state BEFORE external call balances[msg.sender] = 0; // INTERACTIONS: External call last (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); } }替代方案:ReentrancyGuard
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract SecureBank is ReentrancyGuard { mapping(address => uint256) public balances; function withdraw() public nonReentrant { uint256 amount = balances[msg.sender]; require(amount > 0, "Insufficient balance"); balances[msg.sender] = 0; (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); } }2. 整数溢出/下溢
有漏洞的代码(Solidity < 0.8.0):
// VULNERABLE contract VulnerableToken { mapping(address => uint256) public balances; function transfer(address to, uint256 amount) public { // No overflow check - can wrap around balances[msg.sender] -= amount; // Can underflow! balances[to] += amount; // Can overflow! } }安全模式(Solidity >= 0.8.0):
// Solidity 0.8+ has built-in overflow/underflow checks contract SecureToken { mapping(address => uint256) public balances; function transfer(address to, uint256 amount) public { // Automatically reverts on overflow/underflow balances[msg.sender] -= amount; balances[to] += amount; } }对于 Solidity < 0.8.0,请使用 SafeMath:
import "@openzeppelin/contracts/utils/math/SafeMath.sol"; contract SecureToken { 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); } }3. 访问控制
有漏洞的代码:
// VULNERABLE: Anyone can call critical functions contract VulnerableContract { address public owner; function withdraw(uint256 amount) public { // No access control! payable(msg.sender).transfer(amount); } }安全模式:
import "@openzeppelin/contracts/access/Ownable.sol"; contract SecureContract is Ownable { function withdraw(uint256 amount) public onlyOwner { payable(owner()).transfer(amount); } } // Or implement custom role-based access contract RoleBasedContract { mapping(address => bool) public admins; modifier onlyAdmin() { require(admins[msg.sender], "Not an admin"); _; } function criticalFunction() public onlyAdmin { // Protected function } }4. 抢跑(Front-Running)
有漏洞的:
// VULNERABLE TO FRONT-RUNNING contract VulnerableDEX { function swap(uint256 amount, uint256 minOutput) public { // Attacker sees this in mempool and front-runs uint256 output = calculateOutput(amount); require(output >= minOutput, "Slippage too high"); // Perform swap } }缓解措施:
contract SecureDEX { mapping(bytes32 => bool) public usedCommitments; // Step 1: Commit to trade function commitTrade(bytes32 commitment) public { usedCommitments[commitment] = true; } // Step 2: Reveal trade (next block) function revealTrade( uint256 amount, uint256 minOutput, bytes32 secret ) public { bytes32 commitment = keccak256(abi.encodePacked( msg.sender, amount, minOutput, secret )); require(usedCommitments[commitment], "Invalid commitment"); // Perform swap } }安全最佳实践
检查-效果-交互模式
contract SecurePattern { mapping(address => uint256) public balances; function withdraw(uint256 amount) public { // 1. CHECKS: Validate conditions require(amount <= balances[msg.sender], "Insufficient balance"); require(amount > 0, "Amount must be positive"); // 2. EFFECTS: Update state balances[msg.sender] -= amount; // 3. INTERACTIONS: External calls last (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); } }拉取优于推送模式
// Prefer this (pull) contract SecurePayment { mapping(address => uint256) public pendingWithdrawals; function recordPayment(address recipient, uint256 amount) internal { pendingWithdrawals[recipient] += amount; } function withdraw() public { uint256 amount = pendingWithdrawals[msg.sender]; require(amount > 0, "Nothing to withdraw"); pendingWithdrawals[msg.sender] = 0; payable(msg.sender).transfer(amount); } } // Over this (push) contract RiskyPayment { function distributePayments(address[] memory recipients, uint256[] memory amounts) public { for (uint i = 0; i < recipients.length; i++) { // If any transfer fails, entire batch fails payable(recipients[i]).transfer(amounts[i]); } } }输入验证
contract SecureContract { function transfer(address to, uint256 amount) public { // Validate inputs require(to != address(0), "Invalid recipient"); require(to != address(this), "Cannot send to contract"); require(amount > 0, "Amount must be positive"); require(amount <= balances[msg.sender], "Insufficient balance"); // Proceed with transfer balances[msg.sender] -= amount; balances[to] += amount; } }紧急停止(熔断器)
import "@openzeppelin/contracts/security/Pausable.sol"; contract EmergencyStop is Pausable, Ownable { function criticalFunction() public whenNotPaused { // Function logic } function emergencyStop() public onlyOwner { _pause(); } function resume() public onlyOwner { _unpause(); } }Gas 优化
使用uint256而不是更小的类型
// More gas efficient contract GasEfficient { uint256 public value; // Optimal function set(uint256 _value) public { value = _value; } } // Less efficient contract GasInefficient { uint8 public value; // Still uses 256-bit slot function set(uint8 _value) public { value = _value; // Extra gas for type conversion } }打包存储变量
// Gas efficient (3 variables in 1 slot) contract PackedStorage { uint128 public a; // Slot 0 uint64 public b; // Slot 0 uint64 public c; // Slot 0 uint256 public d; // Slot 1 } // Gas inefficient (each variable in separate slot) contract UnpackedStorage { uint256 public a; // Slot 0 uint256 public b; // Slot 1 uint256 public c; // Slot 2 uint256 public d; // Slot 3 }函数参数使用calldata而不是memory
contract GasOptimized { // More gas efficient function processData(uint256[] calldata data) public pure returns (uint256) { return data[0]; } // Less efficient function processDataMemory(uint256[] memory data) public pure returns (uint256) { return data[0]; } }使用事件存储数据(在适当时)
contract EventStorage { // Emitting events is cheaper than storage event DataStored(address indexed user, uint256 indexed id, bytes data); function storeData(uint256 id, bytes calldata data) public { emit DataStored(msg.sender, id, data); // Don't store in contract storage unless needed } }常见漏洞清单
// Security Checklist Contract contract SecurityChecklist { /** * [ ] Reentrancy protection (ReentrancyGuard or CEI pattern) * [ ] Integer overflow/underflow (Solidity 0.8+ or SafeMath) * [ ] Access control (Ownable, roles, modifiers) * [ ] Input validation (require statements) * [ ] Front-running mitigation (commit-reveal if applicable) * [ ] Gas optimization (packed storage, calldata) * [ ] Emergency stop mechanism (Pausable) * [ ] Pull over push pattern for payments * [ ] No delegatecall to untrusted contracts * [ ] No tx.origin for authentication (use msg.sender) * [ ] Proper event emission * [ ] External calls at end of function * [ ] Check return values of external calls * [ ] No hardcoded addresses * [ ] Upgrade mechanism (if proxy pattern) */ }安全测试
// Hardhat test exampleconst{expect}=require("chai");const{ethers}=require("hardhat");describe("Security Tests",function(){it("Should prevent reentrancy attack",asyncfunction(){const[attacker]=awaitethers.getSigners();constVictimBank=awaitethers.getContractFactory("SecureBank");constbank=awaitVictimBank.deploy();constAttacker=awaitethers.getContractFactory("ReentrancyAttacker");constattackerContract=awaitAttacker.deploy(bank.address);// Deposit fundsawaitbank.deposit({value:ethers.utils.parseEther("10")});// Attempt reentrancy attackawaitexpect(attackerContract.attack({value:ethers.utils.parseEther("1")}),).to.be.revertedWith("ReentrancyGuard: reentrant call");});it("Should prevent integer overflow",asyncfunction(){constToken=awaitethers.getContractFactory("SecureToken");consttoken=awaitToken.deploy();// Attempt overflowawaitexpect(token.transfer(attacker.address,ethers.constants.MaxUint256)).to.be.reverted;});it("Should enforce access control",asyncfunction(){const[owner,attacker]=awaitethers.getSigners();constContract=awaitethers.getContractFactory("SecureContract");constcontract=awaitContract.deploy();// Attempt unauthorized withdrawalawaitexpect(contract.connect(attacker).withdraw(100)).to.be.revertedWith("Ownable: caller is not the owner",);});});审计准备
contract WellDocumentedContract { /** * @title Well Documented Contract * @dev Example of proper documentation for audits * @notice This contract handles user deposits and withdrawals */ /// @notice Mapping of user balances mapping(address => uint256) public balances; /** * @dev Deposits ETH into the contract * @notice Anyone can deposit funds */ function deposit() public payable { require(msg.value > 0, "Must send ETH"); balances[msg.sender] += msg.value; } /** * @dev Withdraws user's balance * @notice Follows CEI pattern to prevent reentrancy * @param amount Amount to withdraw in wei */ function withdraw(uint256 amount) public { // CHECKS require(amount <= balances[msg.sender], "Insufficient balance"); // EFFECTS balances[msg.sender] -= amount; // INTERACTIONS (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); } }资源
- references/reentrancy.md: 全面的重入预防
- references/access-control.md: 基于角色的访问模式
- references/overflow-underflow.md: SafeMath 和整数安全
- references/gas-optimization.md: 节省 Gas 的技术
- references/vulnerability-patterns.md: 常见漏洞目录
- assets/solidity-contracts-templates.sol: 安全合约模板
- assets/security-checklist.md: 审计前清单
- scripts/analyze-contract.sh: 静态分析工具
用于安全分析的工具
- Slither: 静态分析工具
- Mythril: 安全分析工具
- Echidna: 模糊测试工具
- Manticore: 符号执行
- Securify: 自动化安全扫描器
常见陷阱
- 使用
tx.origin进行认证:应使用msg.sender - 未检查的外部调用:始终检查返回值
- 对不受信任的合约进行 Delegatecall:可能劫持你的合约
- 浮动编译指令(Floating Pragma):固定到特定的 Solidity 版本
- 缺少事件:为状态更改发出事件
- 循环中过量的 Gas:可能达到区块 Gas 上限
- 没有升级路径:如果需要升级,请考虑代理模式