已验证合约、代理,以及没人提的那把升级钥匙

·阅读 7 分钟·作者:SSP Editorial Team
SSP Academy 封面:已验证智能合约、代理与升级钥匙

已验证合约、代理,以及没人提的那把升级钥匙

区块浏览器上有一个绿色对勾,写着合约源码已验证,而数量庞大的人把它读成「这个合约是安全的」。

它不是那个意思。它的含义要窄得多,也有用得多,而把这两者混为一谈,正是谨慎的人最终批准了他们本来绝不会同意的东西的方式。

「已验证」究竟证明了什么

合约是以字节码的形式部署的——机器指令,实际上无法阅读。验证这件事,是把原始源码上传上去,让浏览器确认:用声明的编译器版本和设置去编译它,得到的正是那个地址上已经存在的字节码。

整个主张就这么多:你正在读的源码,确实就是正在运行的源码。

这是一项真实而有价值的保证。没有它,你就没有办法知道某个项目 GitHub 上那份看起来体面的代码,跟你的交易即将触碰的东西究竟有没有关系。

但请留意它没有说的一切。验证不是审计。没有人评估过这段代码是否正确、是否有缺陷,或者它做的事情是否与函数名暗示的有任何相似之处。一个带有名为 claimRewards 的函数、却把你的全部余额转给部署者的合约,完全可以是已验证的,而且这份验证是完全诚实的——它如实地证明了:这场盗窃正是代码所声明要做的事。

已验证意味着可读,而不是可信。 它把问题从「这东西是什么?」推进到「我看懂我刚才读的了吗?」,这是进展,但它不会替你回答第二个问题。

反过来也要紧。未验证的合约并不是恶意的证据——许多正当的部署从来没顾上去验证。但它确实意味着,没有人——包括你使用的任何工具、以及任何本可以警示你的研究者——能读懂你即将与之交互的东西。对此保持谨慎是合理的。

然后代理让事情更糟

这里地面开始移动,而这也是通常被略过的部分。

你会打交道的绝大多数非琐碎合约,并不是单一合约。它们是代理。代理就是你看到的那个地址,它保存着全部数据——余额、授权额度、状态。但它几乎不含任何逻辑。它把每一次调用转发给另一个独立的实现合约,然后单纯执行那个合约所说的内容。

原因是可升级性。不可变的合约无法打补丁,于是一个缺陷就是永久的,一个缺失的功能就永远缺失。把代理指向一个新的实现,使团队能够修复缺陷、发布改进,而不必要求每一位用户迁移。考虑到有多少价值因无法修复的缺陷而流失,这是一个站得住脚的工程选择。

它有一个容易被忽略的后果。当你在浏览器上查一个代理,看到它已验证时,你验证的是转发逻辑,而不是行为。 真正决定你的钱会遭遇什么的代码,住在实现合约的地址上——浏览器可能显眼地展示它,也可能不——而且那个地址是可以被替换的。

所以真正的问题从来都不是「这个合约验证了吗?」。而是:

谁能改变这个合约的行为,以及多快能改?

管理员钥匙才是真正的安全模型

每一个可升级合约,都有某个能执行升级的人。无论当前代码多好,那份权限就是安全模型。

这个范围从令人不安一路延伸到合情合理:

单个外部拥有账户。 某人机器上的一把私钥,就能替换掉保管你资金的那个合约的逻辑。如果它被钓鱼、被盗,或者它的主人遭到胁迫,这个合约就会变成攻击者想要的样子。这种安排比它应有的频率更常见。

一个多重签名。 若干签名者必须达成一致。好得多——它移除了单点故障——不过值得了解签名者有几位,以及他们究竟是真正彼此独立的人,还是同一个团队的四台笔记本电脑。

多重签名加时间锁。 升级会先在链上公告,并且只能在一段固定延迟之后执行,通常是 24 到 72 小时。这一种才是有分量的,因为它彻底改变了你的处境:你会提前得知变更,并拥有一个在它生效之前撤出的窗口。时间锁并不阻止恶意升级。它让恶意升级变得可以幸存。

根本没有升级路径。 合约不可变。可预测性最高,一旦发现缺陷则毫无补救。这是一个正当的选择,通常由已经经受足够长时间考验、敢于押上这一注的协议做出。

这些没有哪一种自动就是对的。但「一把钥匙」与「多重签名加两天延迟」之间的差别,就是「指望没人犯错」与「出事时还有时间反应」之间的差别。

为什么这恰恰咬在授权上

这部分连着一件你其实经常在做的事。

当你授予一次代币授权时,你授权的是一个地址去移动你的代币。不是一段代码——是一个地址。这份授权存放在代币合约的存储里,并且无限期持续。

如果那个地址是一个可升级的代理,那么有权移动你代币的逻辑,可以在你授权之后被替换掉,无需你参与,也无需你给出任何新的签名。你审查过的是一样东西,而你持续暴露给的是另一样。

这就是为什么「撤销你不再使用的授权」这条常备建议,分量比乍看之下更重。它不只关乎某个合约后来被发现有缺陷。它关乎的是:给一个可升级合约的无限额度,是一份授予「掌握升级钥匙的那个人」的许可,且在你放任它敞开的整段时间里都有效。

这也是交易模拟最锋利的边界。签名前进行模拟会告诉你一笔交易在当前状态和当前实现下会做什么。它确实有用,而它无法告诉你同一份授权在下个月会允许什么。

SSP 给你看什么,又在哪里止步

当 SSP Key 呈现一次授权时,它会解码调用数据,而不是把原始十六进制摆给你看。它识别标准的 ERC-20 选择器——approve、transfer、transferFrom、increaseAllowance、setApprovalForAll——并用平实的语言呈现,包括标出无限额度,其在代码中定义为任何达到或超过 2^255 的数额。

这个解码器是刻意保守的。它自己的文件头把它描述为纯粹的呈现层:它从不改变被签署的内容,并且以「失败即关闭」的方式运作。未知的选择器、错误的长度、畸形的十六进制、非标准的地址填充,都会让它什么也不返回,退回到一个通用动作,把原始十六进制放在「高级」视图后面。它宁可承认自己不知道,也不愿自信地猜测然后出错。代币符号与小数位,只有在设备内置注册表中确知时才会附上,绝不推测。

现在说说这条诚实的边界。这个解码器没有「代理」这个概念。 它内部任何地方都不存在实现合约的概念。它可以正确而清晰地告诉你:「你正在向 0xABC… 授予无限的 USDC 额度」——而它无法告诉你 0xABC 是一个代理、它的升级钥匙在谁手里,或者它的行为下周可能完全不同。

这与其说是疏忽,不如说是任何钱包解码器都跨不过的边界。解码描述的是摆在你面前的那次调用。交易对手是否可升级、由谁治理,是更广阔世界的属性,必须另行查证。我们宁愿把边界说明白,也不愿让一个看起来笃定的授权界面暗示出一种它并不具备的完整性。

在批准重要事项之前的一次实际检查

它验证了吗? 如果没有,请明白你依靠的只是名声。

它是代理吗? 浏览器会标注这一点——留意代理提示,或者「Read as Proxy」选项。如果有,那么实现合约才是真正要紧的代码。

谁能升级它,有没有延迟? 这是清单上最有价值的问题,也是几乎没人问的那一个。认真想过这件事的项目会把它写进文档。没想过、或者不肯说的项目,已经告诉了你一些东西。

这份授权非得是无限的吗? 通常不必。批准你实际打算花费的数额,就把你的暴露精确限定在那个数额上,无论此后这个合约变成什么。

用完就撤销。 一份敞开的额度是一项常设许可,它比你的注意力活得更久。

值得做出的心智调整很小,却改变很多。验证告诉你此刻在运行的是什么代码。可升级性告诉你明天运行什么由谁决定。几乎全部风险都住在第二个问题里,而市面上提供的几乎全部安慰,只回应了第一个。

分享本文

相关文章