合约安全
在这个没有 undo、没有客服、代码即法律的世界,一个 bug 不是崩溃一次,而是资金瞬间被搬空且永不追回。历史上单个漏洞被盗数亿美元的事件反复上演。这一章过一遍最经典的攻击类型,并让你在下面亲手运行一次重入攻击,看金库怎么被一次调用掏干。
头号杀手:重入(Reentrancy)
它是 2016 年 The DAO 事件(直接导致以太坊分叉)的元凶,至今仍高居事故榜首。原理:当你的合约给外部地址转 ETH 时,控制权交给了对方;如果你在更新自己账本之前就转账,对方能在回调里反过来再次调用你的提款函数——此时你的账本还没扣减,于是它能重复提款,直到掏空。
mapping(address => uint256) public balance; function withdraw() public { uint256 amt = balance[msg.sender]; // ✗ 危险:先转账,后清账本 (bool ok, ) = msg.sender.call{value: amt}(""); // 交出控制权! require(ok); balance[msg.sender] = 0; // ← 攻击者在这行之前又重入了 withdraw }
攻击合约的 receive() 里写着"收到钱就再调一次 withdraw"。因为 balance[攻击者] 直到最后才清零,每次重入读到的余额都还在,于是循环提款。运行下面这个模拟器看它发生:
三种防御
- Checks-Effects-Interactions(CEI):永远先检查、先更新自己的状态、最后才做外部调用。把
balance[msg.sender]=0移到转账之前,攻击立刻失效。 - 重入锁:用 OpenZeppelin 的
nonReentrant修饰器,给函数上一把"执行中禁止再进入"的锁。 - 拉取而非推送:让用户自己来提款,而不是你主动批量转账。
权限缺失 / 错配
朴素但致命:某个改关键状态的函数忘了加权限检查,任何人都能调。历史上有合约的初始化函数没保护,被人抢先调用夺走 owner 权限,直接卷走全部资金(著名的 Parity 多签事件)。
function setOwner(address o) public { owner = o; } // ✗ 谁都能改 function setOwner(address o) public onlyOwner { owner = o; } // ✓ 加修饰器
写下任何一个能改状态的函数时,第一反应就该问:"谁应该能调这个?"——然后加上对应的 onlyOwner / 角色检查。别依赖"没人会发现这个函数",链上字节码全公开,机器人 7×24 扫描每一个新合约的每一个 public 函数。默认全世界都在盯着。
预言机操纵
合约常需要"外部价格"(如某代币兑 USD)。如果它天真地用某个 DEX 池子的即时价格做依据,攻击者就能用闪电贷在一笔交易内瞬间砸盘/拉盘、扭曲价格、骗过你的合约、再还款获利。这类"闪电贷 + 预言机操纵"是 DeFi 被盗的重灾区。防御:用时间加权平均价(TWAP)或去中心化预言机(如 Chainlink)聚合多源价格,让单笔交易无法操纵。第 24 章讲 DeFi 会再展开。
其他常见坑
| 漏洞 | 要点 | 防御 |
|---|---|---|
| 整数下溢/溢出 | 0.8 前会绕回;unchecked 块内仍会 | 用 0.8+;unchecked 要谨慎 |
| tx.origin 鉴权 | 用 tx.origin 判身份可被钓鱼中转 | 用 msg.sender |
| 链上"随机数" | block 值可预测/被矿工操纵 | 用 VRF 等可验证随机 |
| gas 耗尽 DoS | 循环可被撑爆导致函数瘫痪 | 避免无界循环;拉取模式 |
| 未检查返回值 | 低层 call 失败不自动 revert | 检查 ok 或用 SafeERC20 |
你在 Android 安全里养成的直觉在这里加倍适用:永远不信任外部输入、永远做权限校验、最小权限原则。区别是后果被放大到极致——移动端漏洞可能被打补丁热修,链上合约一旦部署往往改不动(除非专门设计升级,下一章),损失也是真金白银、不可逆。所以流程上要:严格测试(第 13 章 fuzzing)、第三方审计、慢慢放量、设紧急暂停开关。
安全不是最后加的一道工序,而是贯穿始终的思维方式。记住高频杀手:重入(用 CEI + 重入锁)、权限缺失(每个写函数都问"谁能调")、预言机操纵(别信即时单源价格)。核心心法只有一句:先改自己的账,再和外面打交道;默认全世界都是攻击者。下一章我们解决"部署后发现 bug 怎么办"——可升级合约,它本身又是一个巨大的安全话题。
本章小结
- 重入是头号漏洞:先转账后记账会被回调重复提款;用 CEI 顺序 + nonReentrant 锁防御。
- 权限缺失让任何人能调关键函数;每个改状态的函数都要显式权限检查。
- 预言机操纵靠闪电贷扭曲即时价格;用 TWAP / 去中心化预言机。
- 还有溢出、tx.origin、伪随机、gas DoS、未检查返回值等;安全靠测试+审计+渐进放量,默认全员皆敌。