security
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSmart Contract Security
智能合约安全
What You Probably Got Wrong
你可能忽略的误区
"Solidity 0.8+ prevents overflows, so I'm safe." Overflow is one of dozens of attack vectors. The big ones today: reentrancy, oracle manipulation, approval exploits, and decimal mishandling.
"I tested it and it works." Working correctly is not the same as being secure. Most exploits call functions in orders or with values the developer never considered.
"It's a small contract, it doesn't need an audit." The DAO hack was a simple reentrancy bug. The Euler exploit was a single missing check. Size doesn't correlate with safety.
"Solidity 0.8+版本能防止溢出,所以我的合约很安全。" 溢出只是数十种攻击向量之一。如今最常见的攻击类型包括:重入攻击、预言机操纵、授权漏洞以及小数处理错误。
"我测试过了,合约能正常运行。" 正常运行并不等同于安全。大多数攻击会以开发者从未考虑过的顺序调用函数,或传入意想不到的数值。
"这只是个小型合约,不需要审计。" The DAO黑客事件源于一个简单的重入漏洞,Euler攻击则是因为缺失了一个检查。合约规模与安全性并无关联。
Critical Vulnerabilities (With Defensive Code)
关键漏洞及防御代码
1. Token Decimals Vary
1. 代币小数位数不统一
USDC has 6 decimals, not 18. This is the #1 source of "where did my money go?" bugs.
solidity
// ❌ WRONG — assumes 18 decimals. Transfers 1 TRILLION USDC.
uint256 oneToken = 1e18;
// ✅ CORRECT — check decimals
uint256 oneToken = 10 ** IERC20Metadata(token).decimals();Common decimals:
| Token | Decimals |
|---|---|
| USDC, USDT | 6 |
| WBTC | 8 |
| DAI, WETH, most tokens | 18 |
When doing math across tokens with different decimals, normalize first:
solidity
// Converting USDC amount to 18-decimal internal accounting
uint256 normalized = usdcAmount * 1e12; // 6 + 12 = 18 decimalsUSDC的小数位数是6位,而非18位。 这是导致"我的钱去哪了?"这类问题的头号原因。
solidity
// ❌ 错误写法 — 假设所有代币都是18位小数,会导致转账1万亿USDC
uint256 oneToken = 1e18;
// ✅ 正确写法 — 检查代币小数位数
uint256 oneToken = 10 ** IERC20Metadata(token).decimals();常见代币小数位数:
| 代币 | 小数位数 |
|---|---|
| USDC, USDT | 6 |
| WBTC | 8 |
| DAI, WETH, 多数代币 | 18 |
当处理不同小数位数的代币运算时,需先统一精度:
solidity
// 将USDC金额转换为18位小数的内部记账格式
uint256 normalized = usdcAmount * 1e12; // 6 + 12 = 18位小数2. No Floating Point in Solidity
2. Solidity不支持浮点数
Solidity has no or . Division truncates to zero.
floatdoublesolidity
// ❌ WRONG — this equals 0
uint256 fivePercent = 5 / 100;
// ✅ CORRECT — basis points (1 bp = 0.01%)
uint256 FEE_BPS = 500; // 5% = 500 basis points
uint256 fee = (amount * FEE_BPS) / 10_000;Always multiply before dividing. Division first = precision loss.
solidity
// ❌ WRONG — loses precision
uint256 result = a / b * c;
// ✅ CORRECT — multiply first
uint256 result = (a * c) / b;For complex math, use fixed-point libraries like or .
PRBMathABDKMath64x64Solidity中没有或类型,除法运算会直接截断小数部分取整。
floatdoublesolidity
// ❌ 错误写法 — 结果为0
uint256 fivePercent = 5 / 100;
// ✅ 正确写法 — 使用基点(1 bp = 0.01%)
uint256 FEE_BPS = 500; // 5% = 500基点
uint256 fee = (amount * FEE_BPS) / 10_000;永远先乘后除。先做除法会导致精度丢失。
solidity
// ❌ 错误写法 — 精度丢失
uint256 result = a / b * c;
// ✅ 正确写法 — 先乘后除
uint256 result = (a * c) / b;对于复杂运算,可使用定点数库如或。
PRBMathABDKMath64x643. Reentrancy
3. 重入攻击
An external call can call back into your contract before the first call finishes. If you update state AFTER the external call, the attacker re-enters with stale state.
solidity
// ❌ VULNERABLE — state updated after external call
function withdraw() external {
uint256 bal = balances[msg.sender];
(bool success,) = msg.sender.call{value: bal}(""); // ← attacker re-enters here
require(success);
balances[msg.sender] = 0; // Too late — attacker already withdrew again
}
// ✅ SAFE — Checks-Effects-Interactions pattern + reentrancy guard
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
function withdraw() external nonReentrant {
uint256 bal = balances[msg.sender];
require(bal > 0, "Nothing to withdraw");
balances[msg.sender] = 0; // Effect BEFORE interaction
(bool success,) = msg.sender.call{value: bal}("");
require(success, "Transfer failed");
}The pattern: Checks → Effects → Interactions (CEI)
- Checks — validate inputs and conditions
- Effects — update all state
- Interactions — external calls last
Always use OpenZeppelin's as a safety net on top of CEI.
ReentrancyGuard外部调用可能在首次调用完成前,重新进入你的合约。如果在外部调用后才更新状态,攻击者会利用过期状态重复执行操作。
solidity
// ❌ 存在漏洞 — 状态更新在外部调用之后
function withdraw() external {
uint256 bal = balances[msg.sender];
(bool success,) = msg.sender.call{value: bal}(""); // ← 攻击者在此处重入
require(success);
balances[msg.sender] = 0; // 为时已晚,攻击者已重复提现
}
// ✅ 安全写法 — 采用Checks-Effects-Interactions模式 + 重入防护
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
function withdraw() external nonReentrant {
uint256 bal = balances[msg.sender];
require(bal > 0, "无可用提现金额");
balances[msg.sender] = 0; // 先更新状态,再执行外部交互
(bool success,) = msg.sender.call{value: bal}("");
require(success, "转账失败");
}核心模式:检查 → 状态更新 → 外部交互(CEI)
- 检查 — 验证输入和前置条件
- 状态更新 — 完成所有合约状态修改
- 外部交互 — 最后执行外部调用
除了遵循CEI模式,始终使用OpenZeppelin的作为额外安全保障。
ReentrancyGuard4. SafeERC20
4. 使用SafeERC20
Some tokens (notably USDT) don't return on and . Standard calls will revert even on success.
booltransfer()approve()solidity
// ❌ WRONG — breaks with USDT and other non-standard tokens
token.transfer(to, amount);
token.approve(spender, amount);
// ✅ CORRECT — handles all token implementations
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;
token.safeTransfer(to, amount);
token.safeApprove(spender, amount);Other token quirks to watch for:
- Fee-on-transfer tokens: Amount received < amount sent. Always check balance before and after.
- Rebasing tokens (stETH): Balance changes without transfers. Use wrapped versions (wstETH).
- Pausable tokens (USDC): Transfers can revert if the token is paused.
- Blocklist tokens (USDC, USDT): Specific addresses can be blocked from transacting.
部分代币(尤其是USDT)的和方法不返回值。标准调用会在操作成功时也触发回滚。
transfer()approve()boolsolidity
// ❌ 错误写法 — 与USDT等非标准代币不兼容
token.transfer(to, amount);
token.approve(spender, amount);
// ✅ 正确写法 — 兼容所有代币实现
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;
token.safeTransfer(to, amount);
token.safeApprove(spender, amount);需要注意的其他代币特性:
- 转账手续费代币: 实际到账金额小于转出金额,需前后检查余额。
- 重基代币(如stETH): 无需转账即可变更余额,建议使用包装版本(wstETH)。
- 可暂停代币(如USDC): 代币暂停时转账会触发回滚。
- 黑名单代币(如USDC、USDT): 特定地址可能被限制交易。
5. Never Use DEX Spot Prices as Oracles
5. 切勿将DEX现货价格作为预言机
A flash loan can manipulate any pool's spot price within a single transaction. This has caused hundreds of millions in losses.
solidity
// ❌ DANGEROUS — manipulable in one transaction
function getPrice() internal view returns (uint256) {
(uint112 reserve0, uint112 reserve1,) = uniswapPair.getReserves();
return (reserve1 * 1e18) / reserve0; // Spot price — easily manipulated
}
// ✅ SAFE — Chainlink with staleness + sanity checks
function getPrice() internal view returns (uint256) {
(, int256 price,, uint256 updatedAt,) = priceFeed.latestRoundData();
require(block.timestamp - updatedAt < 3600, "Stale price");
require(price > 0, "Invalid price");
return uint256(price);
}If you must use onchain price data:
- Use TWAP (Time-Weighted Average Price) over 30+ minutes — resistant to single-block manipulation
- Uniswap V3 has built-in TWAP oracles via
observe() - Still less safe than Chainlink for high-value decisions
闪电贷可在单笔交易内操纵任何资金池的现货价格,这已造成数亿美元的损失。
solidity
// ❌ 危险写法 — 单笔交易即可操纵价格
function getPrice() internal view returns (uint256) {
(uint112 reserve0, uint112 reserve1,) = uniswapPair.getReserves();
return (reserve1 * 1e18) / reserve0; // 现货价格——极易被操纵
}
// ✅ 安全写法 — 使用Chainlink并添加时效性与合理性检查
function getPrice() internal view returns (uint256) {
(, int256 price,, uint256 updatedAt,) = priceFeed.latestRoundData();
require(block.timestamp - updatedAt < 3600, "价格已过期");
require(price > 0, "无效价格");
return uint256(price);
}如果必须使用链上价格数据:
- 使用TWAP(时间加权平均价格),周期不少于30分钟——可抵御单区块操纵
- Uniswap V3通过内置了TWAP预言机
observe() - 但对于高价值决策,仍不如Chainlink安全
6. Vault Inflation Attack
6. 金库通胀攻击
The first depositor in an ERC-4626 vault can manipulate the share price to steal from subsequent depositors.
The attack:
- Attacker deposits 1 wei → gets 1 share
- Attacker donates 1000 tokens directly to the vault (not via deposit)
- Now 1 share = 1001 tokens
- Victim deposits 1999 tokens → gets (rounds down)
1999 * 1 / 2000 = 0 shares - Attacker redeems 1 share → gets all 3000 tokens
The fix — virtual offset:
solidity
function convertToShares(uint256 assets) public view returns (uint256) {
return assets.mulDiv(
totalSupply() + 1e3, // Virtual shares
totalAssets() + 1 // Virtual assets
);
}The virtual offset makes the attack uneconomical — the attacker would need to donate enormous amounts to manipulate the ratio.
OpenZeppelin's ERC4626 implementation includes this mitigation by default since v5.
ERC-4626金库的首个存款人可操纵份额价格,窃取后续存款人的资金。
攻击流程:
- 攻击者存入1 wei → 获得1份份额
- 攻击者直接向金库捐赠1000个代币(不通过存款接口)
- 此时1份份额等价于1001个代币
- 受害者存入1999个代币 → 计算得份份额(向下取整)
1999 * 1 / 2000 = 0 - 攻击者赎回1份份额 → 取走全部3000个代币
修复方案——虚拟偏移量:
solidity
function convertToShares(uint256 assets) public view returns (uint256) {
return assets.mulDiv(
totalSupply() + 1e3, // 虚拟份额
totalAssets() + 1 // 虚拟资产
);
}虚拟偏移量会让攻击成本极高——攻击者需要捐赠巨额资金才能操纵份额比例。
OpenZeppelin的ERC4626实现从v5版本开始默认包含该缓解措施。
7. Infinite Approvals
7. 无限授权风险
Never use as approval amount.
type(uint256).maxsolidity
// ❌ DANGEROUS — if this contract is exploited, attacker drains your entire balance
token.approve(someContract, type(uint256).max);
// ✅ SAFE — approve only what's needed
token.approve(someContract, exactAmountNeeded);
// ✅ ACCEPTABLE — approve a small multiple for repeated interactions
token.approve(someContract, amountPerTx * 5); // 5 transactions worthIf a contract with infinite approval gets exploited (proxy upgrade bug, governance attack, undiscovered vulnerability), the attacker can drain every approved token from every user who granted unlimited access.
切勿使用作为授权金额。
type(uint256).maxsolidity
// ❌ 危险写法 — 若合约被攻破,攻击者会转走你所有的代币
token.approve(someContract, type(uint256).max);
// ✅ 安全写法 — 仅授权所需金额
token.approve(someContract, exactAmountNeeded);
// ✅ 可接受写法 — 授权少量多次交互的额度
token.approve(someContract, amountPerTx * 5); // 支持5次交易的额度若拥有无限授权的合约被攻破(如代理升级漏洞、治理攻击、未发现的漏洞),攻击者可转走所有授予无限权限用户的代币。
8. Access Control
8. 权限控制
Every state-changing function needs explicit access control. "Who should be able to call this?" is the first question.
solidity
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
// ❌ WRONG — anyone can drain the contract
function emergencyWithdraw() external {
token.transfer(msg.sender, token.balanceOf(address(this)));
}
// ✅ CORRECT — only owner
function emergencyWithdraw() external onlyOwner {
token.transfer(owner(), token.balanceOf(address(this)));
}For complex permissions, use OpenZeppelin's with role-based separation (ADMIN_ROLE, OPERATOR_ROLE, etc.).
AccessControl所有修改状态的函数都需要明确的权限控制。首先要明确:"谁有权调用这个函数?"
solidity
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
// ❌ 错误写法 — 任何人都可以转走合约资金
function emergencyWithdraw() external {
token.transfer(msg.sender, token.balanceOf(address(this)));
}
// ✅ 正确写法 — 仅合约所有者可调用
function emergencyWithdraw() external onlyOwner {
token.transfer(owner(), token.balanceOf(address(this)));
}对于复杂权限需求,可使用OpenZeppelin的实现基于角色的权限分离(如ADMIN_ROLE、OPERATOR_ROLE等)。
AccessControl9. Input Validation
9. 输入验证
Never trust inputs. Validate everything.
solidity
function deposit(uint256 amount, address recipient) external {
require(amount > 0, "Zero amount");
require(recipient != address(0), "Zero address");
require(amount <= maxDeposit, "Exceeds max");
// Now proceed
}Common missed validations:
- Zero addresses (tokens sent to 0x0 are burned forever)
- Zero amounts (wastes gas, can cause division by zero)
- Array length mismatches in batch operations
- Duplicate entries in arrays
- Values exceeding reasonable bounds
永远不要信任外部输入,所有输入都需要验证。
solidity
function deposit(uint256 amount, address recipient) external {
require(amount > 0, "金额不能为0");
require(recipient != address(0), "不能转账至零地址");
require(amount <= maxDeposit, "超过最大存款限额");
// 后续逻辑
}常见的遗漏验证:
- 零地址(转至0x0的代币会被永久销毁)
- 零金额(浪费Gas,可能导致除零错误)
- 批量操作中的数组长度不匹配
- 数组中的重复条目
- 数值超出合理范围
Pre-Deploy Security Checklist
部署前安全检查清单
Run through this for EVERY contract before deploying to production. No exceptions.
- Access control — every admin/privileged function has explicit restrictions
- Pausable tradeoff — if you added +
Pausable, flag it to the builder. A single key that can freeze all users is a censorship vector. Suggest timelocks or multisig governance.onlyOwner - Reentrancy protection — CEI pattern + on all external-calling functions
nonReentrant - Token decimal handling — no hardcoded for tokens that might have different decimals
1e18 - Oracle safety — using Chainlink or TWAP, not DEX spot prices. Staleness checks present
- Integer math — multiply before divide. No precision loss in critical calculations
- Return values checked — using SafeERC20 for all token operations
- Input validation — zero address, zero amount, bounds checks on all public functions
- Events emitted — every state change emits an event for offchain tracking
- Incentive design — maintenance functions callable by anyone with sufficient incentive
- No infinite approvals — approve exact amounts or small bounded multiples
- Fee-on-transfer safe — if accepting arbitrary tokens, measure actual received amount
- Tested edge cases — zero values, max values, unauthorized callers, reentrancy attempts
- Source verified on block explorer — or
yarn verifyafter every deploy. Unverified contracts can't be audited by users and look indistinguishable from scamsforge verify-contract
部署至主网前,必须对每个合约执行以下检查,无例外。
- 权限控制 — 所有管理员/特权函数都有明确的访问限制
- ** pausable权衡** — 若添加了+
Pausable,需告知开发者。单一密钥即可冻结所有用户操作,这是审查风险点。建议使用时间锁或多签治理。onlyOwner - 重入防护 — 所有涉及外部调用的函数都遵循CEI模式 + 修饰器
nonReentrant - 代币小数处理 — 未硬编码,适配不同小数位数的代币
1e18 - 预言机安全 — 使用Chainlink或TWAP,而非DEX现货价格,且包含时效性检查
- 整数运算 — 先乘后除,关键计算无精度丢失
- 返回值检查 — 所有代币操作使用SafeERC20
- 输入验证 — 对所有公开函数的零地址、零金额、数值范围进行检查
- 事件触发 — 所有状态变更都触发事件,便于链下追踪
- 激励设计 — 维护函数可被任何有足够激励的人调用
- 无无限授权 — 仅授权精确金额或少量有界倍数
- 兼容转账手续费代币 — 若接受任意代币,需校验实际到账金额
- 边缘场景测试 — 零值、最大值、未授权调用者、重入尝试等场景
- 区块浏览器源码验证 — 每次部署后执行或
yarn verify。未验证的合约无法被用户审计,与诈骗合约无异forge verify-contract
MEV & Sandwich Attacks
MEV与三明治攻击
MEV (Maximal Extractable Value): Validators and searchers can reorder, insert, or censor transactions within a block. They profit by frontrunning your transaction, backrunning it, or both.
MEV(最大可提取价值): 验证者和搜索者可在区块内重新排序、插入或审查交易。他们通过抢先交易、滞后交易或两者结合获利。
Sandwich Attacks
三明治攻击
The most common MEV attack on DeFi users:
1. You submit: swap 10 ETH → USDC on Uniswap (slippage 1%)
2. Attacker sees your tx in the mempool
3. Attacker frontruns: buys USDC before you → price rises
4. Your swap executes at a worse price (but within your 1% slippage)
5. Attacker backruns: sells USDC after you → profits from the price difference
6. You got fewer USDC than the true market price这是DeFi用户最常遭遇的MEV攻击:
1. 你提交交易:在Uniswap上用10 ETH兑换USDC(滑点设置1%)
2. 攻击者在内存池中发现你的交易
3. 攻击者抢先交易:在你之前买入USDC → 价格上涨
4. 你的交易以更差的价格执行(但仍在1%滑点范围内)
5. 攻击者滞后交易:在你之后卖出USDC → 从价格差中获利
6. 你实际获得的USDC少于真实市场价格Protection
防护方案
solidity
// ✅ Set explicit minimum output — don't set amountOutMinimum to 0
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter
.ExactInputSingleParams({
tokenIn: WETH,
tokenOut: USDC,
fee: 3000,
recipient: msg.sender,
amountIn: 1 ether,
amountOutMinimum: 1900e6, // ← Minimum acceptable USDC (protects against sandwich)
sqrtPriceLimitX96: 0
});For users/frontends:
- Use Flashbots Protect RPC () — sends transactions to a private mempool, invisible to sandwich bots
https://rpc.flashbots.net - Set tight slippage limits (0.5-1% for majors, 1-3% for small tokens)
- Use MEV-aware DEX aggregators (CoW Swap, 1inch Fusion) that route through solvers instead of the public mempool
When MEV matters:
- Any swap on a DEX (especially large swaps)
- Any large DeFi transaction (deposits, withdrawals, liquidations)
- NFT mints with high demand (bots frontrun to mint first)
When MEV doesn't matter:
- Simple ETH/token transfers
- L2 transactions (sequencers process transactions in order — no public mempool reordering)
- Private mempool transactions (Flashbots, MEV Blocker)
solidity
// ✅ 设置明确的最小输出金额 — 不要将amountOutMinimum设为0
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter
.ExactInputSingleParams({
tokenIn: WETH,
tokenOut: USDC,
fee: 3000,
recipient: msg.sender,
amountIn: 1 ether,
amountOutMinimum: 1900e6, // ← 可接受的最小USDC数量(抵御三明治攻击)
sqrtPriceLimitX96: 0
});针对用户/前端的建议:
- 使用Flashbots Protect RPC () — 将交易发送至私有内存池,对三明治机器人不可见
https://rpc.flashbots.net - 设置严格的滑点限制(主流代币0.5-1%,小众代币1-3%)
- 使用支持MEV的DEX聚合器(如CoW Swap、1inch Fusion),通过求解器而非公开内存池路由交易
MEV需要关注的场景:
- DEX上的任何兑换(尤其是大额交易)
- 任何大额DeFi交易(存款、提现、清算)
- 高需求NFT铸造(机器人抢先铸造)
MEV无需关注的场景:
- 简单的ETH/代币转账
- L2交易( sequencer按顺序处理交易,无公开内存池重排序)
- 私有内存池交易(Flashbots、MEV Blocker)
Proxy Patterns & Upgradeability
代理模式与可升级性
Smart contracts are immutable by default. Proxies let you upgrade the logic while keeping the same address and state.
智能合约默认是不可变的。代理允许你在保持地址和状态不变的情况下升级合约逻辑。
When to Use Proxies
何时使用代理
- Use proxies: Long-lived protocols that may need bug fixes or feature additions post-launch
- Don't use proxies: MVPs, simple tokens, immutable-by-design contracts, contracts where "no one can change this" IS the value proposition
Proxies add complexity, attack surface, and trust assumptions. Users must trust that the admin won't upgrade to a malicious implementation. Don't use proxies just because you can.
- 使用代理: 长期运行的协议,可能需要在上线后修复漏洞或添加功能
- 不使用代理: MVP产品、简单代币、设计为不可变的合约、以"无人能修改"为核心价值的合约
代理会增加复杂度、攻击面和信任假设。 用户必须信任管理员不会将合约升级为恶意实现。不要为了升级而升级。
UUPS vs Transparent Proxy
UUPS vs 透明代理
| UUPS | Transparent | |
|---|---|---|
| Upgrade logic location | In implementation contract | In proxy contract |
| Gas cost for users | Lower (no admin check per call) | Higher (checks msg.sender on every call) |
| Recommended | Yes (by OpenZeppelin) | Legacy pattern |
| Risk | Forgetting | More gas overhead |
Use UUPS. It's cheaper, simpler, and what OpenZeppelin recommends.
| UUPS | 透明代理 | |
|---|---|---|
| 升级逻辑位置 | 实现合约中 | 代理合约中 |
| 用户Gas成本 | 更低(每次调用无需检查管理员) | 更高(每次调用检查msg.sender) |
| 是否推荐 | 是(OpenZeppelin推荐) | 传统模式 |
| 风险 | 忘记实现 | Gas开销更高 |
推荐使用UUPS。 它更便宜、更简单,也是OpenZeppelin的推荐方案。
UUPS Implementation
UUPS实现示例
solidity
// Implementation contract (the logic)
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract MyContractV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable {
uint256 public value;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers(); // Prevent implementation from being initialized
}
function initialize(address owner) public initializer {
__Ownable_init(owner);
__UUPSUpgradeable_init();
value = 42;
}
function _authorizeUpgrade(address) internal override onlyOwner {}
}solidity
// 实现合约(逻辑层)
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract MyContractV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable {
uint256 public value;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers(); // 防止实现合约被直接初始化
}
function initialize(address owner) public initializer {
__Ownable_init(owner);
__UUPSUpgradeable_init();
value = 42;
}
function _authorizeUpgrade(address) internal override onlyOwner {}
}Critical Rules
关键规则
- Use instead of
initializer— proxies don't run constructorsconstructor - Never change storage layout — only append new variables at the end, never delete or reorder
- Use OpenZeppelin's upgradeable contracts — , not
@openzeppelin/contracts-upgradeable@openzeppelin/contracts - Disable initializers in constructor — prevents anyone from initializing the implementation directly
- Transfer upgrade authority to a multisig — never leave upgrade power with a single EOA
solidity
// ❌ WRONG — reordering storage breaks everything
// V1: uint256 a; uint256 b;
// V2: uint256 b; uint256 a; ← Swapped! 'a' now reads 'b's value
// ✅ CORRECT — only append
// V1: uint256 a; uint256 b;
// V2: uint256 a; uint256 b; uint256 c; ← New variable at the end- 使用而非
initializer— 代理不会执行构造函数constructor - 永远不要修改存储布局 — 只能在末尾添加新变量,不能删除或重排现有变量
- 使用OpenZeppelin的可升级合约 — 使用,而非
@openzeppelin/contracts-upgradeable@openzeppelin/contracts - 在构造函数中禁用初始化 — 防止任何人直接初始化实现合约
- 将升级权限转移至多签钱包 — 永远不要将升级权限交给单个EOA账户
solidity
// ❌ 错误写法 — 重排存储会导致合约崩溃
// V1: uint256 a; uint256 b;
// V2: uint256 b; uint256 a; ← 交换顺序!'a'会读取'b'的值
// ✅ 正确写法 — 仅在末尾添加
// V1: uint256 a; uint256 b;
// V2: uint256 a; uint256 b; uint256 c; ← 在末尾添加新变量EIP-712 Signatures & Delegatecall
EIP-712签名与Delegatecall
EIP-712: Typed Structured Data Signing
EIP-712:类型化结构化数据签名
EIP-712 lets users sign structured data (not just raw bytes) with domain separation and replay protection. Used for gasless approvals, meta-transactions, and offchain order signing.
When to use:
- Permit (ERC-2612) — gasless token approvals (user signs, anyone can submit)
- Offchain orders — sign buy/sell orders offchain, settle onchain (0x, Seaport)
- Meta-transactions — user signs intent, relayer submits and pays gas
solidity
// EIP-712 domain separator — prevents replay across contracts and chains
bytes32 public constant DOMAIN_TYPEHASH = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
bytes32 public constant PERMIT_TYPEHASH = keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
);
function permit(
address owner, address spender, uint256 value,
uint256 deadline, uint8 v, bytes32 r, bytes32 s
) external {
require(block.timestamp <= deadline, "Permit expired");
bytes32 structHash = keccak256(abi.encode(
PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline
));
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01", DOMAIN_SEPARATOR(), structHash
));
address recovered = ecrecover(digest, v, r, s);
require(recovered == owner, "Invalid signature");
_approve(owner, spender, value);
}Key properties:
- Domain separator prevents replaying signatures on different contracts or chains
- Nonce prevents replaying the same signature twice
- Deadline prevents stale signatures from being used later
- In practice, use OpenZeppelin's and
EIP712— don't implement from scratchERC20Permit
EIP-712允许用户对结构化数据(而非原始字节)进行签名,支持域分离和重放保护。用于无Gas授权、元交易和链下订单签名。
适用场景:
- Permit(ERC-2612) — 无Gas代币授权(用户签名,任何人可提交)
- 链下订单 — 链下签名买卖订单,链上结算(如0x、Seaport)
- 元交易 — 用户签名意图,中继者提交并支付Gas
solidity
// EIP-712域分隔符 — 防止跨合约和跨链重放
bytes32 public constant DOMAIN_TYPEHASH = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
bytes32 public constant PERMIT_TYPEHASH = keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
);
function permit(
address owner, address spender, uint256 value,
uint256 deadline, uint8 v, bytes32 r, bytes32 s
) external {
require(block.timestamp <= deadline, "Permit已过期");
bytes32 structHash = keccak256(abi.encode(
PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline
));
bytes32 digest = keccak256(abi.encodePacked(
"\\x19\\x01", DOMAIN_SEPARATOR(), structHash
));
address recovered = ecrecover(digest, v, r, s);
require(recovered == owner, "无效签名");
_approve(owner, spender, value);
}核心特性:
- 域分隔符 防止在不同合约或链上重放签名
- Nonce 防止重复使用同一签名
- Deadline 防止过期签名被使用
- 实际开发中,建议使用OpenZeppelin的和
EIP712,不要从零实现ERC20Permit
Delegatecall
Delegatecall
delegatecallThis is extremely dangerous if the target is untrusted.
solidity
// ❌ CRITICAL VULNERABILITY — delegatecall to user-supplied address
function execute(address target, bytes calldata data) external {
target.delegatecall(data); // Attacker can overwrite ANY storage slot
}
// ✅ SAFE — delegatecall only to trusted, immutable implementation
address public immutable trustedImplementation;
function execute(bytes calldata data) external onlyOwner {
trustedImplementation.delegatecall(data);
}Delegatecall rules:
- Never delegatecall to a user-supplied address — allows arbitrary storage manipulation
- Only delegatecall to contracts YOU control — and preferably immutable ones
- Storage layouts must match — the calling contract and target contract must have identical storage variable ordering
- This is how proxies work — the proxy delegatecalls to the implementation, so the implementation's code runs on the proxy's storage. That's why storage layout matters so much for upgradeable contracts.
delegatecall若目标合约不可信,这将极其危险。
solidity
// ❌ 严重漏洞 — delegatecall至用户提供的地址
function execute(address target, bytes calldata data) external {
target.delegatecall(data); // 攻击者可覆盖任意存储槽
}
// ✅ 安全写法 — 仅delegatecall至可信、不可变的实现合约
address public immutable trustedImplementation;
function execute(bytes calldata data) external onlyOwner {
trustedImplementation.delegatecall(data);
}Delegatecall规则:
- 永远不要delegatecall至用户提供的地址 — 允许攻击者任意操纵存储
- 仅delegatecall至你控制的合约 — 且最好是不可变的
- 存储布局必须匹配 — 调用合约和目标合约的存储变量顺序必须完全一致
- 代理的工作原理 — 代理delegatecall至实现合约,因此实现合约的代码在代理的存储上运行。这也是可升级合约对存储布局要求严格的原因
Automated Security Tools
自动化安全工具
Run these before deployment:
bash
undefined部署前运行以下工具:
bash
undefinedStatic analysis
静态分析
slither . # Detects common vulnerabilities
mythril analyze Contract.sol # Symbolic execution
slither . # 检测常见漏洞
mythril analyze Contract.sol # 符号执行
Foundry fuzzing (built-in)
Foundry模糊测试(内置)
forge test --fuzz-runs 10000 # Fuzz all test functions with random inputs
forge test --fuzz-runs 10000 # 用随机输入模糊测试所有测试函数
Gas optimization (bonus)
Gas优化(附加项)
forge test --gas-report # Identify expensive functions
**Slither findings to NEVER ignore:**
- Reentrancy vulnerabilities
- Unchecked return values
- Arbitrary `delegatecall` or `selfdestruct`
- Unprotected state-changing functionsforge test --gas-report # 识别高Gas消耗函数
**Slither发现的以下问题绝不能忽略:**
- 重入漏洞
- 未检查返回值
- 任意`delegatecall`或`selfdestruct`
- 未受保护的状态修改函数Further Reading
扩展阅读
- OpenZeppelin Contracts: https://docs.openzeppelin.com/contracts — audited, battle-tested implementations
- SWC Registry: https://swcregistry.io — comprehensive vulnerability catalog
- Rekt News: https://rekt.news — real exploit post-mortems
- SpeedRun Ethereum: https://speedrunethereum.com — hands-on secure development practice
- OpenZeppelin Contracts: https://docs.openzeppelin.com/contracts — 经过审计、久经考验的合约实现
- SWC Registry: https://swcregistry.io — 全面的漏洞目录
- Rekt News: https://rekt.news — 真实攻击事件复盘
- SpeedRun Ethereum: https://speedrunethereum.com — 实操性安全开发练习 ",