委托调用 delegatecall

本章讲解在 Solidity 中, delegatecall 的原理、用途以及使用方法。

推特@Hita_DAO    DiscordHitaDAO

在 Solidity 中,delegatecall 是用于在智能合约中调用外部合约函数的一种方式。

delegatecall 是一个低级别的操作,它具有一些独特的特性,通常用于实现可升级合约。

一个合约 A 使用 delegatecall  调用合约 B 的函数,那么会在合约 A 的上下文 Context 中执行合约 B 的函数代码,并将结果作用于合约 A 的状态变量和存储上。

我们可以看一下 delegatecallcall 对比,来理解两者的不同的工作方式。

1. delegatecall 和 call 对比

a. call 的工作方式

当外部调用者 A 通过合约 B ,使用 call 方式调用合约 C 的函数时,将会执行合约 C 的函数代码,该函数所处的上下文 Context 是合约 C 的上下文。

这里的 Context 是指执行中的合约状态和存储环境,

这种调用方式,也就意味着,如果执行的函数改变了一些状态,最后的结果都会保存在合约 C 的状态变量和存储上。

同时,执行函数中的 msg.sender 是合约 B 的地址,msg.value 也是合约 B 设定的数量。

b. delegatecall 的工作方式

当外部调用者 A 通过合约 B ,使用 delegatecall 方式调用合约 C 的函数时,将会执行合约 C 的函数代码,但该函数所处的上下文 Context 仍然是合约 B 的上下文。

也就意味着,如果执行的函数改变了状态,产生的结果都会保存在合约 B 的状态变量和存储上。

同时,执行函数中的 msg.sender 是合约 A 的地址,msg.value 也是合约 A 设定的数量。

2. delegatecall 的使用场景

在智能合约开发中,delegatecall 主要用于以下两种场景:

a. 代理合约

实现代理合约是 delegatecall 最常见的用途。在这种模式下,智能合约的存储和逻辑可以实现分离。

代理合约负责存储所有的状态变量(即:存储),逻辑合约负责实现所有业务逻辑(即:代码)。

代理合约会保存一个指向逻辑合约地址的变量,它会把所有的函数调用转发到逻辑合约上。

如果业务逻辑升级的话,可以直接部署一个新的逻辑合约,代理合约只需更改指向逻辑合约的地址即可。

所以,在 delegatecall 调用方式下,所有数据保存在代理合约中,所以,升级逻辑合约不会对原有数据造成影响。

 

b. 库函数重用

delegatecall 也被用于实现类似于传统编程中的库函数调用。

通过 delegatecall,一个合约可以借用另一个合约的函数,就好像这些函数是在调用合约本身中定义的一样。

这样,开发者可以创建通用的合约库,以减少重复代码,提高代码的复用性和合约的效率。 

3. delegatecall 示例和验证

a. call 方式的示例 

我们先使用 call 方式来测试一个合约:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// 被调用的智能合约
contract C  {
    // 整型状态变量
    uint256 public value = 0;

    /**
     * @dev 改变状态变量 value 的值
     * @param _value 新的变量值
     */
    function setValue(uint256 _value) external {
        value = _value;
    }
}

// 使用 call 方式调用 C 合约
contract B  {
    // 整型状态变量
    uint256 public value = 0;

    /**
     * @dev 使用 call 方式调用外部合约
     * @param contractAddress 外部合约地址
     * @param _value 新的变量值
     */
    function changeValue(address contractAddress, uint256 _value) 
        external returns(bool, bytes memory){
        // 对函数签名和参数进行编码
        bytes memory data = abi.encodeWithSignature("setValue(uint256)", _value);

        // 通过 call 调用外部合约函数
        return contractAddress.call(data);
    }
}

我们要在 B 合约中使用 call 方式调用 C 合约的函数 setValue

我们将上面的合约复制到 Remix,进行编译,然后分别部署 BC 两个合约。

1. 点击 B 合约的函数 changeValue,在 contractAddress 中填写 C 合约的地址,_value 中填入 100,然后点击 transact 执行函数。

2. 函数执行成功后,我们查看 B 合约的状态变量 value,发现它的值并没有改变,依然是 0 。

3. 我们再去查看 C 合约中的状态变量 value,发现它的值变为 100

所以,使用 call 方式不会改变 B 合约的状态变量,只会改变 C 合约的状态变量。

a. delegatecall 方式的示例 

我们再使用 delegatecall 方式来测试一个合约:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// 被调用的智能合约
contract C  {
    // 整型状态变量
    uint256 public value = 0;

    /**
     * @dev 改变状态变量 value 的值
     * @param _value 新的变量值
     */
    function setValue(uint256 _value) external {
        value = _value;
    }
}

// 使用 delegatecall 方式调用 C 合约
contract B  {
    // 整型状态变量
    uint256 public value = 0;

    /**
     * @dev 使用 delegatecall 方式调用外部合约
     * @param contractAddress 外部合约地址
     * @param _value 新的变量值
     */
    function changeValue(address contractAddress, uint256 _value) 
        external returns(bool, bytes memory){
        // 对函数签名和参数进行编码
        bytes memory data = abi.encodeWithSignature("setValue(uint256)", _value);

        // 通过 delegatecall 调用外部合约函数
        return contractAddress.delegatecall(data);
    }
}

我们要在 B 合约中使用 delegatecall 方式调用 C 合约的函数 setValue

我们将上面的合约复制到 Remix,进行编译,然后分别部署 BC 两个合约。

1. 点击 B 合约的函数 changeValue,在 contractAddress 中填写 C 合约地址,_value 中填入 100,然后点击 transact 执行函数。

2. 函数执行成功后,我们查看 B 合约的状态变量 value,发现它的值变成了 100 。

3. 我们再去查看 C 合约中的状态变量 value,发现它的值没有改变,依然是 0

所以,使用 delegatecall 方式执行的是 C 合约的代码,但改变的调用合约 B 的状态变量。