智能合约可升级性对比:我翻了DeFi前20的权限设置,'代码即法律'这句话基本是空头支票

🔑 关键词:智能合约可升级性,代理模式,EIP-1967,智能合约安全,智能合约权限

📖 摘要:从EVM代理模式、Solana升级权限到Move语言模块机制,拆解'不可篡改'的营销话术,附Etherscan和cast命令自查教程。

去年帮一个朋友看他那个NFT项目的合约,我随口说了句「你这个 owner 权限随时能把合约里剩下的 ETH 提走」,他回我「我知道啊,但大家不都这样吗」。同一个礼拜他转发的宣传海报上写着「代码即法律」。这事我印象太深了,因为它是当下智能合约领域最典型的一个认知裂缝——写代码的人心里门清,读海报的人什么都不知道。

图片

我后来把 DeFiLlama 上 TVL 排名靠前的 20 个协议挨个拉出来看了一遍合约权限,先说结论:真正意义上完全不可升级的不到三分之一。Uniswap V2 和 V3 的 core 合约算是少见的硬骨头,一旦部署就改不了,所以当年 V3 上线前 Bug 赏金开到 50 万美金、反复审计了好几轮。但更多的协议走的是另一条路,包括很多你在推特上看到喊「去中心化」喊得最响的那几家。

代理模式是个什么东西,为什么大部分项目都在用

EVM 上的可升级合约基本靠一套叫 Proxy 的模式撑起来。逻辑合约里写业务代码,代理合约只干一件事:把所有调用用 DELEGATECALL 转发到逻辑合约地址。用户的资金、状态都存在代理合约这一侧,升级的时候只要把代理指向的地址换一下,逻辑就变了,地址和余额都不动。听起来优雅,问题是——谁能改这个指向?

OpenZeppelin 在 EIP-1967 里把几个关键的存储槽位固定下来了,最常用的是 implementation 槽:0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc,admin 槽是 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103。这两个十六进制串你背不下来也没关系,记住它们的存在意义就好:整个协议的控制权就压在这两个槽位指向谁。

如果你用 Foundry 的 cast 工具,查一个合约的实现地址其实一行命令的事:

图片

cast storage 0x你的合约地址 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc

返回的就是当前逻辑合约的地址。如果这个地址能被某个 EOA 钱包随时替换,那这个协议跟一个后端服务器改路由没什么本质区别,只是加了个链上记账簿。

三个真实案例,比任何白皮书都说明问题

2017 年 11 月那个 Parity 多签钱包事件,很多人只记得「有人黑了 Parity」,其实细节特别荒诞。一个 GitHub ID 叫 devops199 的普通用户,调用了 Parity 钱包库合约里一个没做初始化的函数,把库合约的 owner 抢到了自己手里,然后顺手调了 kill()。这个库合约一死,所有依赖它的多签钱包全部失效,约 51.3 万 ETH 被冻结到今天,当时价值差不多 1.5 亿美元,现在是几十亿。问题根源是什么?库合约里有个函数权限没锁死。代码没问题吗?审计过。审计能查出来吗?查得出来,但没人往那儿想。

图片

2022 年 3 月的 Ronin 桥被盗 6.24 亿美元,攻击者拿到的是 9 个验证者私钥里的 5 个。这不是什么高深的密码学漏洞,是社会工程加基础设施渗透。Wormhole 同年 2 月被卷走 12 万 ETH(3.26 亿美元),靠的是 Solana 那边签名验证逻辑的一个疏漏。两个案子的共同点:合约本身没被改,被改的是人心和运维流程。

所以「不可篡改」这四个字救不了你。Ronin 的合约不可篡改,Wormhole 的合约也不可篡改,钱一样没了。

不同链的权限模型,差别比你想的大

EVM 生态通用的是代理模式,Solana 走的是另一套路。Solana 的程序(Program)可以设置 upgrade authority,如果这个 authority 是一个单签钱包地址,那这个程序随时能被一个人替换掉;如果设成 null,才叫真正冻结。有意思的是 Solana 上很多项目为了使用方便,压根没把 authority 丢掉,只是对外宣称「去中心化」。你去 Solana Explorer 上一查 program 的 loader 状态就知道了。

Move 语言系(Aptos、Sui)的思路又不一样。模块发布之后默认不可变,想升级得用特定的兼容策略,Aptos 上叫 compatible policy,Sui 上也有类似的包升级规则。这种设计是从语言层面堵了一部分坑,但代价是迭代慢,出了问题没法热修,反而催生了「弃旧部署新地址」这种更麻烦的迁移模式,用户的授权和老合约的资产经常就烂在那里。

图片

没有哪种模型是「对」的,只有你清不清楚自己在信任谁。

我自己的观点:可升级性不是罪,不透明才是

圈子里有个很流行的站队:可升级=中心化=坏,不可升级=去中心化=好。我觉得这个二分法特别偷懒。

带 7 天 timelock 的可升级合约,所有变更在链上公开排队七天,人人可查、可退出、可反对,它的信任模型其实比一个不可升级但依赖中心化预言机喂价的协议要干净得多。反过来,一个不可升级的合约如果里面有个 owner 能暂停转账、能改费率上限、能冻结地址(我说的就是稳定币,USDC 在以太坊上的合约地址是 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48,USDT 是 0xdAC17F958D2ee523a2206206994597C13D831ec7,这两个你都可以在 Etherscan 上点开 Write Contract 看权限),它凭什么叫「不可篡改」?

真正该盯的三件事:第一,谁能改?是单人 EOA 还是 5/9 多签?第二,改了之后多久生效?是立刻还是排队?第三,改动有没有链上事件?能不能被监控机器人抓到?

图片

手把手教你查一个合约的权限(十分钟搞定)

别听项目方怎么说,自己打开 Etherscan 走一遍:

第一步,搜合约地址,点 Contract 标签。如果有「Read as Proxy」和「Write as Proxy」按钮冒出来,恭喜,这是个可升级合约。

第二步,点 Read as Proxy,找 owner()admin()getImplementation()proxyAdmin() 这几个函数,看返回的地址是什么。

图片

第三步,点 Write as Proxy,看有哪些写入函数。如果里面有 upgradeToupgradeToAndCalltransferOwnershippauseblacklistsetImplementation 这类名字,逐条点开看它的权限限制说明。

第四步,去 Events 标签搜 Upgraded 事件。这个合约过去升级过几次?每次升级前后实现地址是什么?如果你看到一个合约在过去 12 个月里升级了 8 次,而且团队从不发公告,你心里应该有数了。

第五步,如果嫌麻烦,用 OpenZeppelin 的 Defender 或者 Tenderly 这类工具,把关键地址加进监控,实现地址一变更就给你推送。这个基本是免费的。

再补一句,你查的时候会发现很多「老牌」协议其实也升级过好几次,但人家每次都发 governance 提案,投票、timelock、执行,链路完整。这就是我说的透明度比「可不可改」重要。

最后说回我那个朋友。他后来把 owner 权限转到了一个 3/5 多签,加了 48 小时 timelock,写进了他们项目的文档首页。做这些花了他两个下午。他没有变得更去中心化,但他至少做对了一件事:让用户的信任有了可以核对的地方。智能合约这行最缺的不是技术,是把技术摊开给人看的意愿。

🏷️ 标签: