委托调用 delegatecall
本章讲解在 Solidity
中, delegatecall
的原理、用途以及使用方法。
在 Solidity 中,delegatecall
是用于在智能合约中调用外部合约函数的一种方式。
delegatecall
是一个低级别的操作,它具有一些独特的特性,通常用于实现可升级合约。
一个合约 A 使用 delegatecall
调用合约 B 的函数,那么会在合约 A 的上下文 Context
中执行合约 B 的函数代码,并将结果作用于合约 A 的状态变量和存储上。
我们可以看一下 delegatecall
和 call
对比,来理解两者的不同的工作方式。
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
,进行编译,然后分别部署 B 和 C 两个合约。
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
,进行编译,然后分别部署 B 和 C 两个合约。
1. 点击 B 合约的函数 changeValue,在 contractAddress 中填写 C 合约地址,_value 中填入 100,然后点击 transact 执行函数。
2. 函数执行成功后,我们查看 B 合约的状态变量 value,发现它的值变成了 100 。
3. 我们再去查看 C 合约中的状态变量 value,发现它的值没有改变,依然是 0 。
所以,使用 delegatecall
方式执行的是 C 合约的代码,但改变的调用合约 B 的状态变量。