noDelegateCall 修饰符详解:阻止你的代码被别人 delegatecall

理解 Uniswap V3 如何用 address(this) 与 immutable 原始地址比对来防止合约被克隆盗用

5 分钟阅读
noDelegateCall 修饰符详解:阻止你的代码被别人 delegatecall

noDelegateCall 修饰符详解:阻止你的代码被别人 delegatecall

目录


一、noDelegateCall 是什么

noDelegateCall 是一个安全修饰符,作用是阻止外部合约通过 delegatecall 调用被它修饰的函数

前面几篇我们一直在用 delegatecall 做代理(让别人借用你的代码)。但有时你不希望自己的代码被别人借用——比如你的合约是精心设计的核心逻辑,不想被竞争对手”套壳”克隆。noDelegateCall 就是为此而生。

二、实现原理

address private immutable originalAddress;

constructor() {
    originalAddress = address(this);
}

modifier noDelegateCall() {
    require(address(this) == originalAddress, "no delegate call");
    _;
}

机制:

  1. 部署时,在构造函数里把合约自己的部署地址记录到 immutable 变量 originalAddress
  2. 被修饰的函数执行时,检查当前的 address(this) 是否等于这个原始地址;
  3. 不相等就 revert。

三、为什么这样能阻止 delegatecall

回顾 delegatecall 的特性(Module 6 第 4 篇):

  • 正常调用时,address(this) 返回合约自己的真实地址 → 等于 originalAddress → 通过;
  • 别人 delegatecall 你的函数时,代码在调用者的上下文执行,address(this) 返回的是调用者的地址 → 不等于 originalAddress → require 失败、revert。

一句话:delegatecall 会改变 address(this),而 originalAddress 记的是”代码原主”的地址,两者一对比就能识破”代码被别人借走执行”的情况。

四、immutable 为什么是关键

文章强调:把 originalAddress 设成 immutable 极其关键

原因:immutable 变量硬编码在字节码里,不在存储插槽。

  • 如果 originalAddress 是普通存储变量,恶意的 delegatecaller 可以在自己的存储里预先伪造一个值,让 address(this) == originalAddress 蒙混过关;
  • 而 immutable 在字节码里,delegatecall 借的就是这段字节码,里面写死的 originalAddress 永远是原合约的地址,调用者无法篡改——比对必然失败。

这也呼应了第 4 篇”delegatecall 共享存储但不共享字节码常量”的结论:immutable 跟着代码走,正好用来识别”代码被搬到别处执行”。

五、真实动机:Uniswap V3 防抄袭

Uniswap V3 采用这个模式,是因为吸取了 V2 被大量抄袭的教训:

  • V3 用 Business Source License(BSL) 限制商业使用;
  • 但即便有许可证限制,竞争对手仍可能部署”克隆代理”,通过 delegatecall 指向 V3 已部署的合约,白嫖 V3 的逻辑(自己不写代码,借 V3 的字节码跑在自己的池子里);
  • noDelegateCall 修饰符从技术上彻底封死这条路——任何试图 delegatecall V3 核心函数的合约都会 revert。

这是”代码即法律”的有趣体现:用代码机制而非仅靠法律条款来保护知识产权。

六、测试行为:delegatecall 不一定整体 revert

一个微妙细节:当合约 B 试图 delegatecall 合约 A 被 noDelegateCall 修饰的 increment() 时:

  • delegatecall 这个底层调用本身可能不会让整笔交易回滚(如果 B 忽略了返回的 success 布尔值,参见第 3 篇”底层 call 失败不自动 revert”);
  • 但 A 的函数内部 require 失败,状态修改不会生效(被 revert 掉)。

也就是说,攻击者借代码执行的部分被废掉了,达到了保护目的。但要注意:如果你想让外层也明确失败,调用方需要检查 delegatecall 的返回值。

七、总结

  • noDelegateCall 阻止你的代码被别人 delegatecall 借用;
  • 原理:构造时记录 address(this)immutable originalAddress,运行时比对;
  • delegatecall 会让 address(this) 变成调用者地址,比对失败即拦截;
  • immutable 是命门——它在字节码里无法被 delegatecaller 伪造;
  • Uniswap V3 用它配合 BSL 许可证防止竞品套壳抄袭。

八、动手练习项目:防克隆合约 GuardedLogic

项目目标

实现带 noDelegateCall 保护的核心逻辑合约,并写一个”攻击者”合约尝试 delegatecall 借用它,验证保护生效;再对比未受保护的版本被成功”白嫖”。部署到 Sepolia。

合约要求

1. GuardedLogic.sol(受保护)

  • address private immutable originalAddress; 构造函数设为 address(this)
  • modifier noDelegateCall():比对 address(this) == originalAddress,失败 revert DelegateCallNotAllowed()
  • uint256 public counter;
  • protectedIncrement() external noDelegateCall { counter++; }
  • unprotectedIncrement() external { counter++; }(对照,不加修饰符);
  • 一个 purecomputeSomething() 也加 noDelegateCall,展示 view/pure 也能保护。

2. Attacker.sol(模拟套壳者)

  • 自己也有 uint256 public counter;(插槽 0,和 GuardedLogic 对齐);
  • stealProtected(address logic)logic.delegatecall(abi.encodeWithSignature("protectedIncrement()"))检查返回值,失败则 revert;
  • stealUnprotected(address logic):delegatecall unprotectedIncrement(),检查返回值。

测试要求(Foundry)

  1. test_DirectCallWorks:直接调 GuardedLogic.protectedIncrement,counter 正常 +1;
  2. test_DelegateCallBlocked:Attacker.stealProtected → 断言 revert(noDelegateCall 拦截),Attacker 自己的 counter 不变;
  3. test_UnprotectedIsStolen(对照):Attacker.stealUnprotected → 成功,Attacker 的 counter 变成 1,证明没保护就会被白嫖;
  4. test_ImmutableCannotBeForged:尝试在 Attacker 里预设一个 counter/storage 值企图让 address(this)==originalAddress 成立,断言仍然失败(因为 originalAddress 在字节码里);
  5. test_OriginalAddressIsDeployAddress:GuardedLogic 的 originalAddress == 它自己的部署地址。

Sepolia 部署与验证步骤

  1. 部署 GuardedLogic 和 Attacker;
  2. 直接调 GuardedLogic.protectedIncrement,读 counter = 1;
  3. 调 Attacker.stealProtected(GuardedLogic 地址),观察交易 revert / Attacker.counter 仍为 0;
  4. 调 Attacker.stealUnprotected,观察 Attacker.counter 变成 1(被成功借用);
  5. 对比两次交易,直观理解 noDelegateCall 的保护效果。

进阶挑战(可选)

  • 把 originalAddress 故意改成普通 storage 变量(去掉 immutable),尝试构造一个能绕过检查的 Attacker(在自己存储里伪造该值),证明为什么必须用 immutable;
  • 写注释回答:noDelegateCall 能阻止”通过 CALL(非 delegatecall)调用”吗?为什么?它保护的到底是什么——代码不被借用,还是合约不被调用?

💬 评论