2019 年冬天,我在 Ropsten 测试网上部署第一个众筹合约,gas price 填了 1 gwei,那笔交易在内存池里躺了三天才被打包。当时我还挺得意——代码上链就再也改不了了嘛。现在回头看,这话对了一半,错的那一半恰恰是整个行业最爱拿来做营销的部分。
先把结论摆出来:你今天用的绝大多数 DeFi 协议,"不可篡改"的是地址和存储布局,不是逻辑。
一、代理模式:把不可变性撕开一道口子
EIP-1967 定义了两个固定存储槽来放实现合约地址和管理员地址:
- 实现地址槽:
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc - 管理员槽:
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
这两个数是 bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1) 算出来的,末尾减 1 是为了避开 keccak256 的预映像攻击。代理合约本身通常只有几千字节,用 delegatecall 把请求转发给实现合约,存储留在代理里。升级的时候,管理员改一下第一个槽里的地址就完事了。
三种主流代理的差别,我按实际踩坑体验列一下:
| 方案 | 升级函数放哪 | Gas 特点 | 坑点 |
|---|---|---|---|
| Transparent | 代理合约 | 每次调用多一次选择器比较 | 管理员调用不走 delegatecall,选择器冲突要小心 |
| UUPS (EIP-1822) | 实现合约 | 部署省 gas(代理更小) | 实现里忘写 upgradeTo 就永久锁死 |
| Diamond (EIP-2535) | 多个 facet | 能突破代码大小限制 | 存储布局管理复杂,工具链支持一般 |
表格里那个"代码大小限制"指的是 EIP-170 定的 24576 字节,超了直接部署失败。很多项目选 Diamond 不是设计上需要,纯粹是代码塞不下。EIP-170 是 2016 年定的规则,那会儿谁也没预料到今天的合约复杂度。
二、重入:从 The DAO 到 Curve,八年了还在犯
2016 年 6 月 17 日,The DAO 被重入抽走约 360 万 ETH,直接催生了 1920000 区块高度的硬分叉,ETH 和 ETC 从此分家。2017 年 11 月 6 日,一个叫 devops199 的用户调用了 Parity 多签钱包里没做权限控制的 initWallet,51.3 万 ETH 被永久冻结,到今天还在那个地址里躺着。
再往后,2023 年 7 月 30 日,一批用 Vyper 0.2.15、0.2.16、0.3.0 编译的 Curve 池子出事——@nonreentrant 装饰器生成的锁逻辑失效了,被直接薅走约 5200 万美元,算上抢跑机器人的部分,受影响资金超过 7000 万。注意,这不是 Vyper 语法写错,是编译器在生成字节码时把锁给优化没了。
所以重入这道防线要防的不只是"你的代码写错了",还有"你的工具链错了"。
实操上,Checks-Effects-Interactions 还是第一道防线:先改状态,再做外部调用。OpenZeppelin 的 ReentrancyGuard 用的是两个常量 _NOT_ENTERED = 1 和 _ENTERED = 2,不是 0 和 1。原因很朴素——EIP-3529(伦敦升级,2021 年 8 月)把 SSTORE 清零退款从 15000 降到了 4800,同时取消了 SELFDESTRUCT 的 24000 退款。0→非0 要 20000 gas,非0→非0 只要 5000,用 1 和 2 的话每次进出都能省下约 15000 gas。这个数字是实打实的,不是我拍脑袋。
还有一类叫只读重入(read-only reentrancy)——在 view 函数里读一个还没更新完的池子状态当价格用,Balancer 在 2023 年初就吃过这个亏。防它的办法是在状态更新完成前加一个 locked 检查,而不是只在写操作上加锁。
三、Gas 优化:别信那些"省 50% gas"的教程
我见过太多人一上来就上 unchecked、把 uint256 全改成 uint8。后面这个是典型的反向优化——EVM 每个栈元素都是 32 字节,uint8 只有在 storage 里跟旁边的变量打包进同一个槽才有意义,单独用反而多出掩码操作。
真正有用的是这几个数:
- 冷访问
SLOAD2100 gas,热访问 100 gas(EIP-2929 之后)。把循环里反复读的 storage 变量提到 memory,一次循环省 2000 gas。 - calldata 里非零字节 16 gas,零字节 4 gas。这就是为什么很多协议宁愿用
bytes32传参也不用字符串。 immutable和constant不占存储槽,值在部署时写进代码里,读的时候是PUSH32而不是SLOAD,差 2100 gas。- 自定义
error(Solidity 0.8.4+)比require带字符串便宜,因为不用存那串字。
不过说句实话,在主网 gas 价格回到 5-20 gwei 的今天,你省下的那点 gas 摊到用户头上可能就几分钱。为了省 gas 把代码写成天书,是负收益。
四、审计报告到底值多少钱
先说数字:Certora Prover、Slither、Foundry 的 invariant testing 这几样加起来,我估摸着能挡住 60%-70% 的常见问题。剩下那 30% 靠审计师的个人经验,很难标准化。
审计过还被黑的例子一只手数不完:Wormhole 2022 年 2 月丢了 12 万 ETH,Ronin 2022 年 3 月丢了 17.36 万 ETH 加 2550 万 USDC,Nomad 2022 年 8 月被薅走约 1.9 亿美元——那次纯粹是 init 参数校验漏了,结果谁来都能分一杯羹。
我的观点可能有点冒犯:审计报告的公关价值,大概和它的技术价值一样高。投资人和交易所要看,所以项目方必须做。真认真的团队会把审计当成一次外部 code review,然后自己再补一套监控和暂停机制。
所以我自己做项目会坚持三件事:
- 有紧急暂停(pause),但配时间锁和分段解锁流程,别让 owner 一秒钟卷走所有钱。
- 设单笔和单日提取上限,把最坏情况从"全部"压到"有限"。
- 用 Foundry 写 invariant test,至少跑 10 万轮,比堆 unit test 有用得多。
追求"零漏洞"是妄想,追求"出事了能兜住"才现实。
最后回到开头那句——不可篡改确实是个好听的词,但真正承载价值的是代码背后的经济模型、治理结构和应急预案。地址写在链上是改不了,逻辑不是,人心更不是。想明白这一层,比背一百份审计 checklist 管用。