solidity-security
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSolidity Security
Solidity 智能合约安全
Vulnerability prevention, secure patterns, gas-safe optimizations, audit preparation.
漏洞预防、安全编码模式、安全的Gas优化以及审计准备。
Code Style Rules
代码风格规则
No Unicode Separator Comments
禁止使用Unicode分隔符注释
Never use Unicode box-drawing characters (, , , etc.) as comment decorators or section separators in generated code. This includes patterns like:
─━═// ── State ─────────────────────────────────────────
// ══ Errors ═════════════════════════════════════════These are AI slop. They carry no semantic value, are invisible noise in diffs, and mark generated code as low-quality. Use plain labels or nothing at all:
solidity
// State
mapping(address => uint256) public balances;
// Errors
error InsufficientBalance();在生成的代码中,切勿使用Unicode方框绘制字符(、、等)作为注释装饰符或章节分隔符。这包括以下这类模式:
─━═// ── State ─────────────────────────────────────────
// ══ Errors ═════════════════════════════════════════这些是AI生成的冗余内容,不具备任何语义价值,在代码差异对比中是无效噪音,会让生成的代码显得质量低下。请使用普通标签或直接省略:
solidity
// State
mapping(address => uint256) public balances;
// Errors
error InsufficientBalance();Vulnerabilities & Secure Patterns
漏洞与安全模式
1. Reentrancy
1. 重入漏洞
External call before state update lets an attacker re-enter mid-execution.
Vulnerable:
solidity
function withdraw() public {
uint256 amount = balances[msg.sender];
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok);
balances[msg.sender] = 0; // state update after call
}Secure - CEI + ReentrancyGuard:
solidity
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract Vault is ReentrancyGuard {
// Errors
error InsufficientBalance();
error TransferFailed();
function withdraw(uint256 amount) external nonReentrant {
if (balances[msg.sender] < amount) revert InsufficientBalance();
balances[msg.sender] -= amount;
(bool ok, ) = msg.sender.call{value: amount}("");
if (!ok) revert TransferFailed();
}
}Cross-function reentrancy: attacker re-enters a different function that reads stale state. Apply to all functions sharing mutable state, not just the one with the external call.
nonReentrant在更新状态前进行外部调用会让攻击者在执行过程中重新进入合约。
存在漏洞的代码:
solidity
function withdraw() public {
uint256 amount = balances[msg.sender];
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok);
balances[msg.sender] = 0; // state update after call
}安全实现 - CEI模式 + ReentrancyGuard:
solidity
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract Vault is ReentrancyGuard {
// Errors
error InsufficientBalance();
error TransferFailed();
function withdraw(uint256 amount) external nonReentrant {
if (balances[msg.sender] < amount) revert InsufficientBalance();
balances[msg.sender] -= amount;
(bool ok, ) = msg.sender.call{value: amount}("");
if (!ok) revert TransferFailed();
}
}跨函数重入:攻击者重新进入另一个读取过期状态的函数。对所有共享可变状态的函数都应应用修饰符,而不仅仅是包含外部调用的函数。
nonReentrant2. Access Control
2. 访问控制
Vulnerable:
solidity
function withdraw(uint256 amount) public {
payable(msg.sender).transfer(amount);
}Secure:
solidity
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
// Two-step transfer prevents accidental ownership loss
contract SimpleAccess is Ownable2Step {
function emergencyWithdraw() external onlyOwner { /* ... */ }
}
// Role-based for multi-actor systems
contract RoleAccess is AccessControl {
bytes32 public constant OPERATOR = keccak256("OPERATOR");
function sensitiveOp() external onlyRole(OPERATOR) { /* ... */ }
}- Never for auth - only
tx.originmsg.sender - over
Ownable2StepOwnable - Validate on all address parameters
address(0)
存在漏洞的代码:
solidity
function withdraw(uint256 amount) public {
payable(msg.sender).transfer(amount);
}安全实现:
solidity
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
// 两步转移机制可防止意外丢失所有权
contract SimpleAccess is Ownable2Step {
function emergencyWithdraw() external onlyOwner { /* ... */ }
}
// 基于角色的控制适用于多参与者系统
contract RoleAccess is AccessControl {
bytes32 public constant OPERATOR = keccak256("OPERATOR");
function sensitiveOp() external onlyRole(OPERATOR) { /* ... */ }
}- 切勿使用进行身份验证,仅使用
tx.originmsg.sender - 优先使用而非
Ownable2StepOwnable - 对所有地址参数都要验证是否为
address(0)
3. Integer Overflow / Underflow
3. 整数溢出/下溢
Solidity >= 0.8.0 has checked arithmetic by default. For blocks, the surrounding logic must prove bounds:
uncheckedsolidity
uint256 len = arr.length;
for (uint256 i; i < len; ) {
// i < len < type(uint256).max, so ++i cannot overflow
unchecked { ++i; }
}Pre-0.8.0: Use . There is no reason to target < 0.8.0 for new contracts.
SafeMathSolidity >= 0.8.0版本默认启用算术检查。对于代码块,其外围逻辑必须能证明边界安全:
uncheckedsolidity
uint256 len = arr.length;
for (uint256 i; i < len; ) {
// i < len < type(uint256).max,因此++i不会溢出
unchecked { ++i; }
}0.8.0之前的版本: 使用。新合约没有理由再针对<0.8.0的版本开发。
SafeMath4. Front-Running / MEV
4. 抢先交易/MEV
Vulnerable:
solidity
function swap(uint256 amount, uint256 minOutput) public {
uint256 output = calculateOutput(amount);
require(output >= minOutput, "Slippage");
}Secure - Commit-Reveal:
solidity
// State
mapping(bytes32 => uint256) public commitBlock;
uint256 public constant REVEAL_DELAY = 1;
// Errors
error NoCommitment();
error RevealTooEarly();
function commit(bytes32 hash) external {
commitBlock[hash] = block.number;
}
function reveal(uint256 amount, uint256 minOutput, bytes32 secret) external {
bytes32 hash = keccak256(abi.encodePacked(msg.sender, amount, minOutput, secret));
if (commitBlock[hash] == 0) revert NoCommitment();
if (block.number <= commitBlock[hash] + REVEAL_DELAY) revert RevealTooEarly();
delete commitBlock[hash];
}Other mitigations: Flashbots Protect / MEV Blocker, slippage + deadline params, batch auctions (CoW Protocol).
存在漏洞的代码:
solidity
function swap(uint256 amount, uint256 minOutput) public {
uint256 output = calculateOutput(amount);
require(output >= minOutput, "Slippage");
}安全实现 - 提交-披露模式:
solidity
// State
mapping(bytes32 => uint256) public commitBlock;
uint256 public constant REVEAL_DELAY = 1;
// Errors
error NoCommitment();
error RevealTooEarly();
function commit(bytes32 hash) external {
commitBlock[hash] = block.number;
}
function reveal(uint256 amount, uint256 minOutput, bytes32 secret) external {
bytes32 hash = keccak256(abi.encodePacked(msg.sender, amount, minOutput, secret));
if (commitBlock[hash] == 0) revert NoCommitment();
if (block.number <= commitBlock[hash] + REVEAL_DELAY) revert RevealTooEarly();
delete commitBlock[hash];
}其他缓解措施:Flashbots Protect/MEV Blocker、滑点+截止时间参数、批量拍卖(如CoW Protocol)。
5. Unchecked External Calls
5. 未检查的外部调用
Some tokens (USDT) don't return - raw silently fails.
bool.transfer()solidity
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract TokenVault {
using SafeERC20 for IERC20;
function send(IERC20 token, address to, uint256 amount) internal {
token.safeTransfer(to, amount);
}
}Always for token operations.
SafeERC20部分代币(如USDT)不会返回值——直接使用会静默失败。
bool.transfer()solidity
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract TokenVault {
using SafeERC20 for IERC20;
function send(IERC20 token, address to, uint256 amount) internal {
token.safeTransfer(to, amount);
}
}所有代币操作都应始终使用。
SafeERC206. Oracle Manipulation
6. 预言机操控
| Risk | Mitigation |
|---|---|
| Spot price manipulation | TWAP over multiple blocks |
| Single oracle failure | Multiple independent oracles, median |
| Stale data | Freshness check on |
| Flash loan attack | Chainlink |
solidity
error InvalidPrice();
error StaleOracle();
uint256 public constant MAX_STALENESS = 1 hours;
function getPrice(AggregatorV3Interface feed) internal view returns (uint256) {
(, int256 price, , uint256 updatedAt, ) = feed.latestRoundData();
if (price <= 0) revert InvalidPrice();
if (block.timestamp - updatedAt > MAX_STALENESS) revert StaleOracle();
return uint256(price);
}| 风险 | 缓解措施 |
|---|---|
| 现货价格操控 | 多区块时间加权平均价格(TWAP) |
| 单一预言机故障 | 多个独立预言机,取中位数 |
| 数据过时 | 检查 |
| 闪电贷攻击 | Chainlink |
solidity
error InvalidPrice();
error StaleOracle();
uint256 public constant MAX_STALENESS = 1 hours;
function getPrice(AggregatorV3Interface feed) internal view returns (uint256) {
(, int256 price, , uint256 updatedAt, ) = feed.latestRoundData();
if (price <= 0) revert InvalidPrice();
if (block.timestamp - updatedAt > MAX_STALENESS) revert StaleOracle();
return uint256(price);
}7. Proxy / Upgrade Pitfalls
7. 代理/升级陷阱
| Risk | Prevention |
|---|---|
| Storage collision | EIP-1967 slots, OZ upgrades plugin |
| Uninitialized proxy | |
| Selector clash | |
| Re-initialization | |
solidity
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}| 风险 | 预防措施 |
|---|---|
| 存储冲突 | EIP-1967插槽、OpenZeppelin升级插件 |
| 未初始化的代理 | 在部署交易中调用 |
| 选择器冲突 | 使用 |
| 重复初始化 | 在构造函数中调用 |
solidity
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}8. Signature Replay
8. 签名重放
solidity
error InvalidSignature();
error NonceAlreadyUsed();
mapping(bytes32 => bool) public usedNonces;
function executeWithSig(
address signer, uint256 amount, bytes32 nonce, bytes calldata sig
) external {
if (usedNonces[nonce]) revert NonceAlreadyUsed();
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01", DOMAIN_SEPARATOR, keccak256(abi.encode(signer, amount, nonce))
));
if (ECDSA.recover(digest, sig) != signer) revert InvalidSignature();
usedNonces[nonce] = true;
}Use EIP-712 typed data + nonce + in the domain separator.
block.chainidsolidity
error InvalidSignature();
error NonceAlreadyUsed();
mapping(bytes32 => bool) public usedNonces;
function executeWithSig(
address signer, uint256 amount, bytes32 nonce, bytes calldata sig
) external {
if (usedNonces[nonce]) revert NonceAlreadyUsed();
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01", DOMAIN_SEPARATOR, keccak256(abi.encode(signer, amount, nonce))
));
if (ECDSA.recover(digest, sig) != signer) revert InvalidSignature();
usedNonces[nonce] = true;
}在域分隔符中使用EIP-712类型数据 + 随机数(nonce) + 。
block.chainidDesign Patterns
设计模式
Pull Over Push
拉取模式替代推送模式
solidity
// State
mapping(address => uint256) public pending;
// Errors
error NothingToWithdraw();
error TransferFailed();
function recordPayment(address recipient, uint256 amount) internal {
pending[recipient] += amount;
}
function withdraw() external {
uint256 amount = pending[msg.sender];
if (amount == 0) revert NothingToWithdraw();
pending[msg.sender] = 0;
(bool ok, ) = msg.sender.call{value: amount}("");
if (!ok) revert TransferFailed();
}solidity
// State
mapping(address => uint256) public pending;
// Errors
error NothingToWithdraw();
error TransferFailed();
function recordPayment(address recipient, uint256 amount) internal {
pending[recipient] += amount;
}
function withdraw() external {
uint256 amount = pending[msg.sender];
if (amount == 0) revert NothingToWithdraw();
pending[msg.sender] = 0;
(bool ok, ) = msg.sender.call{value: amount}("");
if (!ok) revert TransferFailed();
}Emergency Stop
紧急停止
solidity
import {PausableUpgradeable} from
"@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
contract Protocol is PausableUpgradeable, OwnableUpgradeable {
function deposit() external payable whenNotPaused { /* ... */ }
function pause() external onlyOwner { _pause(); }
function unpause() external onlyOwner { _unpause(); }
}solidity
import {PausableUpgradeable} from
"@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
contract Protocol is PausableUpgradeable, OwnableUpgradeable {
function deposit() external payable whenNotPaused { /* ... */ }
function pause() external onlyOwner { _pause(); }
function unpause() external onlyOwner { _unpause(); }
}Input Validation
输入验证
solidity
error ZeroAddress();
error ZeroAmount();
error InsufficientBalance(uint256 available, uint256 requested);
function transfer(address to, uint256 amount) external {
if (to == address(0)) revert ZeroAddress();
if (amount == 0) revert ZeroAmount();
if (balances[msg.sender] < amount) {
revert InsufficientBalance(balances[msg.sender], amount);
}
balances[msg.sender] -= amount;
balances[to] += amount;
}solidity
error ZeroAddress();
error ZeroAmount();
error InsufficientBalance(uint256 available, uint256 requested);
function transfer(address to, uint256 amount) external {
if (to == address(0)) revert ZeroAddress();
if (amount == 0) revert ZeroAmount();
if (balances[msg.sender] < amount) {
revert InsufficientBalance(balances[msg.sender], amount);
}
balances[msg.sender] -= amount;
balances[to] += amount;
}Gas Optimization
Gas优化
Never sacrifice correctness for gas. Every block must have a provable safety invariant.
unchecked切勿为了Gas消耗牺牲正确性。每个代码块都必须有可证明的安全不变量。
uncheckedStorage Packing
存储打包
solidity
// 1 slot (32 bytes)
struct Packed {
uint128 balance;
uint64 lastUpdate;
uint64 nonce;
}solidity
// 1个插槽(32字节)
struct Packed {
uint128 balance;
uint64 lastUpdate;
uint64 nonce;
}Calldata Over Memory
使用Calldata而非Memory
solidity
function process(uint256[] calldata data) external pure returns (uint256) {
return data[0];
}solidity
function process(uint256[] calldata data) external pure returns (uint256) {
return data[0];
}Custom Errors Over Revert Strings
自定义错误替代回退字符串
Custom errors (Solidity >= 0.8.4) are cheaper than string reverts and encode structured data.
solidity
error WithdrawalExceedsBalance(uint256 requested, uint256 available);
function withdraw(uint256 amount) external {
if (amount > address(this).balance) {
revert WithdrawalExceedsBalance(amount, address(this).balance);
}
}自定义错误(Solidity >= 0.8.4)比字符串回退更便宜,且能编码结构化数据。
solidity
error WithdrawalExceedsBalance(uint256 requested, uint256 available);
function withdraw(uint256 amount) external {
if (amount > address(this).balance) {
revert WithdrawalExceedsBalance(amount, address(this).balance);
}
}Events for Off-Chain Data
使用事件存储链下数据
solidity
event DataStored(address indexed user, uint256 indexed id, bytes data);
function storeData(uint256 id, bytes calldata data) external {
emit DataStored(msg.sender, id, data);
}Only persist to storage what on-chain logic actually reads.
solidity
event DataStored(address indexed user, uint256 indexed id, bytes data);
function storeData(uint256 id, bytes calldata data) external {
emit DataStored(msg.sender, id, data);
}仅将链上逻辑实际需要读取的数据持久化到存储中。
Security Tooling
安全工具
| Category | Tool | Purpose |
|---|---|---|
| Static analysis | Slither | Detector suite for common vulns |
| Static analysis | Aderyn | Rust-based, Foundry-native |
| Fuzzing | Echidna | Property-based Solidity fuzzer |
| Fuzzing | Medusa | Go-based alternative to Echidna |
| Formal verification | Certora | Prover for critical invariants |
| Formal verification | Halmos | Symbolic execution for Foundry |
| SMT | SMTChecker | Built-in bounded model checker |
| 类别 | 工具 | 用途 |
|---|---|---|
| 静态分析 | Slither | 常见漏洞检测套件 |
| 静态分析 | Aderyn | 基于Rust、原生支持Foundry的工具 |
| 模糊测试 | Echidna | 基于属性的Solidity模糊测试工具 |
| 模糊测试 | Medusa | 基于Go的Echidna替代工具 |
| 形式化验证 | Certora | 关键不变量验证器 |
| 形式化验证 | Halmos | 针对Foundry的符号执行工具 |
| SMT检查 | SMTChecker | 内置的有界模型检查器 |
Minimum CI Pipeline
最小化CI流水线
bash
slither . --filter-paths "node_modules|lib"
forge test --fuzz-runs 10000
forge snapshot --checkbash
slither . --filter-paths "node_modules|lib"
forge test --fuzz-runs 10000
forge snapshot --checkTesting for Security (Foundry)
安全测试(Foundry)
solidity
import "forge-std/Test.sol";
contract SecurityTest is Test {
Vault vault;
address attacker = makeAddr("attacker");
function setUp() public {
vault = new Vault();
vm.deal(address(vault), 10 ether);
}
function test_RevertWhen_ReentrancyAttempted() public {
ReentrancyAttacker exploit = new ReentrancyAttacker(address(vault));
vm.deal(address(exploit), 1 ether);
vm.expectRevert();
exploit.attack();
}
function test_RevertWhen_UnauthorizedWithdraw() public {
vm.prank(attacker);
vm.expectRevert(Vault.Unauthorized.selector);
vault.emergencyWithdraw();
}
function testFuzz_TransferNeverExceedsBalance(uint256 amount) public {
vm.assume(amount > 0 && amount <= vault.balanceOf(address(this)));
vault.transfer(attacker, amount);
assertEq(vault.balanceOf(attacker), amount);
}
}solidity
import "forge-std/Test.sol";
contract SecurityTest is Test {
Vault vault;
address attacker = makeAddr("attacker");
function setUp() public {
vault = new Vault();
vm.deal(address(vault), 10 ether);
}
function test_RevertWhen_ReentrancyAttempted() public {
ReentrancyAttacker exploit = new ReentrancyAttacker(address(vault));
vm.deal(address(exploit), 1 ether);
vm.expectRevert();
exploit.attack();
}
function test_RevertWhen_UnauthorizedWithdraw() public {
vm.prank(attacker);
vm.expectRevert(Vault.Unauthorized.selector);
vault.emergencyWithdraw();
}
function testFuzz_TransferNeverExceedsBalance(uint256 amount) public {
vm.assume(amount > 0 && amount <= vault.balanceOf(address(this)));
vault.transfer(attacker, amount);
assertEq(vault.balanceOf(attacker), amount);
}
}Audit Preparation
审计准备
Code Quality
代码质量
- NatSpec on all public/external functions (,
@notice,@dev,@param)@return - CEI on every state-changing function with external calls
- on functions sharing mutable state
nonReentrant - for all token operations
SafeERC20 - Custom errors - no revert strings
- No , no
tx.originrandomness, no on-chain secretsblock.timestamp
- 所有公开/外部函数添加NatSpec注释(、
@notice、@dev、@param)@return - 所有包含外部调用的状态变更函数遵循CEI模式
- 对所有共享可变状态的函数添加修饰符
nonReentrant - 所有代币操作使用
SafeERC20 - 使用自定义错误,避免回退字符串
- 不使用、不依赖
tx.origin生成随机数、不存储链上密钥block.timestamp
Testing
测试
- Unit tests: every function, every revert path
- Fuzz tests: property-based for numeric/state edges
- Invariant tests: global properties that must always hold
- Fork tests: integration against mainnet state
- Static analysis clean (Slither + Aderyn, zero high/medium)
- 单元测试:覆盖每个函数、每个回退路径
- 模糊测试:针对数值/状态边界的基于属性测试
- 不变量测试:验证必须始终成立的全局属性
- 分叉测试:针对主网状态的集成测试
- 静态分析无高/中危问题(Slither + Aderyn)
Documentation
文档
- Architecture overview with contract interaction diagram
- Threat model: what is trusted, what is adversarial
- Known limitations and design trade-offs
- Deployment and upgrade runbook
- Previous audit reports (if any)
- 架构概述及合约交互图
- 威胁模型:信任范围与对抗场景
- 已知限制与设计权衡
- 部署与升级手册
- 过往审计报告(如有)
Deployment
部署
- Access control verified and documented
- Upgrade path tested end-to-end (if proxy)
- Testnet deployment validated
- Emergency pause mechanism tested
- Multi-sig or timelock on admin functions
- 访问控制已验证并记录
- 升级路径已端到端测试(如使用代理)
- 测试网部署已验证
- 紧急暂停机制已测试
- 管理函数使用多签或时间锁
NatSpec Template
NatSpec模板
solidity
/// @title Vault - Collateralized lending vault
/// @notice Accepts collateral deposits and issues vault shares.
/// @dev UUPS-upgradeable. Tiered fee schedule per ADR-018.
contract Vault {
/// @notice Deposit collateral into the vault.
/// @param token Collateral token address.
/// @param amount Deposit amount (must be > 0).
/// @return shares Vault shares minted.
function deposit(address token, uint256 amount) external returns (uint256 shares) {
// ...
}
}solidity
/// @title Vault - 抵押借贷金库
/// @notice 接受抵押品存入并发行金库份额。
/// @dev 基于UUPS可升级。遵循ADR-018的分层费率机制。
contract Vault {
/// @notice 将抵押品存入金库。
/// @param token 抵押品代币地址。
/// @param amount 存入金额(必须>0)。
/// @return shares 铸造的金库份额数量。
function deposit(address token, uint256 amount) external returns (uint256 shares) {
// ...
}
}Quick Reference
快速参考
| Vulnerability | Fix |
|---|---|
| Reentrancy | CEI + |
| Missing access control | |
| Unchecked ERC20 return | |
| Oracle manipulation | TWAP + freshness check + sanity bounds |
| Frontrunning | Commit-reveal, slippage + deadline params |
| Proxy storage collision | EIP-1967, OZ upgrades plugin |
| |
| On-chain randomness | Chainlink VRF |
| Unbounded loop DoS | Pagination or pull pattern |
| Signature replay | EIP-712 + nonce + |
| Flash loan price manipulation | TWAP, multiple oracles |
| Push-payment DoS | Pull-over-push |
| Delegatecall to untrusted | Never; or restrict target via allowlist |
| 漏洞 | 修复方案 |
|---|---|
| 重入漏洞 | CEI模式 + |
| 缺失访问控制 | |
| 未检查ERC20返回值 | |
| 预言机操控 | TWAP + 新鲜度检查 + 合理性边界 |
| 抢先交易 | 提交-披露模式、滑点+截止时间参数 |
| 代理存储冲突 | EIP-1967、OpenZeppelin升级插件 |
| 使用 |
| 链上随机数 | Chainlink VRF |
| 无界循环DoS | 分页或拉取模式 |
| 签名重放 | EIP-712 + 随机数 + |
| 闪电贷价格操控 | TWAP、多预言机 |
| 推送支付DoS | 拉取模式替代推送模式 |
| 委托调用不可信合约 | 禁止;或通过白名单限制目标地址 |