noDelegateCall 修饰符用于阻止向合约发送 delegatecall。我们将首先展示实现这一目标的机制,然后再讨论人们采用这种做法的动机。
下面,我们简化了最初由 Uniswap V3 的 noDelegateCall 创建的 noDelegateCall 修饰符:
contract NoDelegateCallExample {
address immutable private originalAddress;
constructor() {
originalAddress = address(this);
}
modifier noDelegateCall() {
require(address(this) == originalAddress, "no delegate call");
_;
}
}
address(this) 会根据执行环境的不同而改变,但 originalAddress 始终是使用了 noDelegateCall 的代码的部署地址。因此,如果另一个合约对带有 noDelegateCall 修饰符的函数发起 delegatecall,那么 address(this) 将不等于 originalAddress,交易将会回滚。极其关键的一点是,原始地址必须是一个 immutable 变量,否则发起 delegatecall 的合约可以策略性地将使用了 noDelegateCall 的合约地址放入该存储槽中,从而绕过 require 语句。
测试 noDelegateCall
下面我们提供测试 noDelegateCall 的代码。
contract noDelegateCall {
address immutable private originalAddress;
constructor() {
originalAddress = address(this);
}
modifier noDelegateCall() {
require(address(this) == originalAddress, "no delegate call");
_;
}
}
contract A is noDelegateCall {
uint256 public x;
function increment() noDelegateCall public {
x++;
}
}
contract B {
uint256 public x; // this variable does not increment
function tryDelegatecall(address a) external {
(bool ok, ) = a.delegatecall(
abi.encodeWithSignature("increment()")
);// ignore ok
}
}
合约 B 对使用了 noDelegateCall 修饰符的 A 发起 delegatecall。尽管对 B.tryDelegatecall 的交易不会回滚(因为忽略了低级调用的返回值),但存储变量 x 不会增加,因为在 delegatecall 上下文内的交易发生了回滚。
noDelegateCall 的动机
Uniswap V2 是历史上被分叉最多的 DeFi 协议。Uniswap V2 协议曾面临着其他项目的竞争,这些项目逐行复制其源代码,并将新产品作为 Uniswap V2 的替代品进行营销,有时还通过提供空投来激励用户。
为了防止这种情况发生,Uniswap 团队在 商业源码许可证(Business Source License) 下对 Uniswap V3 进行了授权——任何人都能够复制代码,但在许可证于 2023 年 4 月到期之前,不能将其用于商业用途。
然而,如果有人想制作一个 Uniswap V3 的“副本”,他们只需创建一个克隆代理(clone proxy)并将其指向 Uniswap V3 的实例——然后将该智能合约作为 Uniswap V3 的替代品进行营销。noDelegateCall 修饰符正是用来防止这种情况发生的。
最初发布于 5 月 11 日