delegatecall

delegatecall 解决的问题、使用方式和注意事项。

发布于 2026年5月28日0 views

delegatecall 的核心是:用别的合约代码,修改自己的合约状态。

普通 call 是“去目标合约里执行目标合约的逻辑,修改目标合约的状态”。delegatecall 则是“借用目标合约的代码,但在当前合约的上下文中执行”。

解决什么问题

delegatecall 主要用于:

  • 代理合约升级。
  • 逻辑合约复用。
  • 模块化合约架构。

最常见场景是 Proxy:

  • Proxy 合约保存状态。
  • Implementation 合约保存逻辑。
  • Proxy 通过 delegatecall 调用 Implementation。
  • 升级时只换 Implementation 地址,Proxy 状态不变。

基本例子

调用合约:

contract Caller {
    uint256 public value;
    address public logic;

    constructor(address _logic) {
        logic = _logic;
    }

    function setValue(uint256 _value) external {
        (bool success, ) = logic.delegatecall(
            abi.encodeWithSignature("setValue(uint256)", _value)
        );

        require(success, "delegatecall failed");
    }
}

逻辑合约:

contract Logic {
    uint256 public value;

    function setValue(uint256 _value) external {
        value = _value;
    }
}

Caller 使用 delegatecall 调用 Logic.setValue 时,真正被修改的是 Caller.value,不是 Logic.value

和 call 的区别

call         = 在目标合约上下文执行,修改目标合约状态
delegatecall = 在当前合约上下文执行,修改当前合约状态

delegatecall 执行时:

  • msg.sender 保持原调用者。
  • msg.value 保持原值。
  • 读写的是当前合约 storage。
  • 使用的是目标合约代码。

存储布局

delegatecall 最大的风险是存储布局。

如果 Proxy 和 Logic 的状态变量顺序不一致,就可能写错 slot。

contract Proxy {
    uint256 public value; // slot 0
}

contract Logic {
    uint256 public value; // slot 0
}

这里布局一致,Logic.value 对应 Proxy.value

如果 Logic 第一位换成别的变量,就可能写坏 Proxy 状态。

常见用途

Proxy 升级

升级合约时,Proxy 保留状态,只替换逻辑合约地址。

常见模式:

  • Transparent Proxy。
  • UUPS Proxy。
  • Beacon Proxy。

Diamond

Diamond Standard 会把功能拆成多个 Facet,通过 delegatecall 路由到不同模块。

适合大型协议,但复杂度也更高。

注意事项

  • 使用 delegatecall 必须非常谨慎。
  • Proxy 和 Logic 的 storage layout 必须保持兼容。
  • 不要 delegatecall 到不可信合约。
  • 升级合约前要测试存储布局。
  • delegatecall 失败时要正确 revert。

总结

delegatecall 是一种“借代码、用自己状态”的调用方式。它让代理升级和模块化合约成为可能,但最大风险是存储布局错乱和调用不可信逻辑。