Fallback 扩展模式是一种规避 24 KB 智能合约大小限制的简单方法。
假设我们在 主(primary)合约中有 foo() 和 bar() 函数,并希望添加 baz(),但由于空间不足而无法实现。
我们在主智能合约中添加一个 fallback 函数,将未知的函数调用委托给 扩展(extension)合约,这与代理(proxy)的工作原理类似。
我们将 baz() 放在 扩展 合约中。当我们在主合约上调用 baz() 时,它不会匹配 主合约中的任何函数选择器,从而触发 fallback 函数。然后,baz() 将在 扩展 合约中被 delegatecall 调用。
确保相同的存储布局
为了使这种模式生效,主合约和扩展合约都需要具有完全相同的存储布局。实现这一点的一个简单方法是将 所有 的存储变量(无一例外!)放入一个单一的合约中。然后主合约和扩展合约都继承该合约。示例如下:

更改扩展合约
在上面的示例中,扩展合约的地址存储在一个不可变(immutable)变量中。我们可以在存储合约中添加一个额外的存储变量来保存扩展合约的地址,然后在我们想要更改合约的某些功能时更新扩展合约的地址。
不建议采用这种方法——如果需要可升级性,那么最好使用成熟的代理模式。此外,从存储中读取该扩展地址变量将额外消耗 2,100 gas。
必须注意避免函数选择器冲突
两个随机函数具有相同选择器的概率大约为 400 万分之一。然而,由于生日悖论,当我们有 n 个函数选择器并且只需一次冲突就会导致意外行为时,这种概率会迅速增加。目前没有适用于此问题的自动工具,开发者必须手动检查函数选择器。
Gas 考量
扩展合约本身也可以遵循这种模式并将其发送给另一个委托目标。事实上,我们这样做的次数在原则上是没有限制的。
然而,每一次“跳跃(hop)”都会额外增加 2,600 gas(向新地址发起 CALL 或 DELEGATECALL 所需的最小 gas 量),因此如果调用链很长,成本可能会非常可观。
由于扩展合约中的函数会额外消耗 2,600 gas,我们希望将很少使用的函数放在扩展合约中,或者将主要在链下调用的函数(此时 gas 无关紧要)放在扩展合约中。
将 EIP 2930 与此模式结合使用
将访问列表交易(access list transactions)与此模式结合使用,在调用扩展合约中的函数时将节省 100 gas。一般来说,如果以太坊交易包含跨合约调用或 delegatecall,则应使用访问列表交易。
将 fallback 扩展作为可升级代理的实现合约
这种模式可以与常规代理结合使用。也就是说,代理合约可以委托给一个实现(implementation)合约,然后该实现合约再委托给一个扩展合约。请记住,升级工具(如 Openzeppelin 升级工具)并非专为配合扩展 fallback 模式而设计,可能无法捕获与升级相关的问题。
通过 RareSkills 了解更多
请参阅我们的 Solidity Bootcamp 以了解更多信息。
首发于 2023 年 12 月 28 日