.delegatecall(...)
.delegatecall() 是 Solidity 中的一个方法,用于从一个原始合约中调用目标合约中的一个函数。然而,与其他方法不同的是,当使用.delegatecall()在目标合约中执行函数时,上下文从原始合约中传递,即代码在目标合约中执行,但变量在原始合约中被修改。
通过本教程,我们将了解为什么正确理解.delegatecall()的工作原理很重要,否则会产生一些严重后果。
等等,什么?
让我们先了解它是如何工作的。
使用.delegatecall()时需要注意的是,原始合约的上下文被传递给目标合约,目标合约的所有状态变化都反映在原始合约的状态上,而不是目标合约的状态上,即使该函数是在目标合约上执行的。
嗯,不是很清楚,我明白你的意思。所以让我们试着通过一个例子来理解。
在以太坊中,一个函数可以表示为 4+32*N
个字节,其中 4 个字节为函数选择器,32*N
个字节为函数参数。
- 函数选择器。为了得到函数选择器,我们将函数的名称和它的参数类型进行散列,不留空隙,例如,对于像
putValue(uint value)
这样的东西,你将使用keccak-256
散列putValue(uint)
,这是 Ethereum 使用的一个散列函数,然后取其前 4 个字节。为了更好地理解keccak-256
和散列,我建议你观看这个视频 - 函数参数。将每个参数转换成固定长度为 32 字节的十六进制字符串,并将其连接起来。
我们有两个合约 Student.sol
和 Calculator.sol
。我们不知道 Calculator.sol
的 ABI,但是我们知道他们存在一个 add 函数,这个函数接收两个 uint,并在 Calculator.sol
中把它们加起来。
让我们看看如何使用 delegateCall 来从 Student.sol 中调用这个函数
pragma solidity ^0.8.4;
contract Student {
uint public mySum;
address public studentAddress;
function addTwoNumbers(address calculator, uint a, uint b) public returns (uint) {
(bool success, bytes memory result) = calculator.delegatecall(abi.encodeWithSignature("add(uint256,uint256)", a, b));
require(success, "The call to calculator contract failed");
return abi.decode(result, (uint));
}
}
pragma solidity ^0.8.4;
contract Calculator {
uint public result;
address public user;
function add(uint a, uint b) public returns (uint) {
result = a + b;
user = msg.sender;
return result;
}
}
我们的学生合约
在这里有一个函数 addTwoNumbers
,它接收一个地址和两个要相加的数字。它没有直接执行,而是试图在地址上做一个.delegatecall()
,用于接收两个数字的函数 add
。
我们使用了 abi.encodeWithSignature
,也与 abi.encodeWithSelector
相同,它首先进行哈希运算,然后从函数的名称和参数类型中取出前 4 个字节。在我们的例子中,它做了以下工作。(bytes4(keccak256(add(uint,uint)),然后将参数--a,b 附加到函数选择器的 4 个字节上。这些都是 32 字节的长度(32 字节=256 位,这也是 uint256 可以存储的)。
所有这些在连接后被传递到委托调用方法中,在计算器合约的地址上被调用。
实际的加法部分并不那么有趣,有趣的是计算器合约实际上设置了一些状态变量。但是请记住,当数值在 Calcultor 合约中被分配时,它们实际上是被分配到了学生合约的存储器中,因为 deletgatecall 在目标合约中执行函数时使用了原始合约的存储器。那么到底会发生什么呢?
从以前的课程中你知道,solidity 中的每个变量槽都是 32 字节,也就是 256 位。当我们使用.delegatecall()
从学生到计算器时,我们使用了学生的存储空间,而不是计算器的存储空间,但问题是,即使我们使用了学生的存储空间,槽的数量也是基于计算器合约的,在这种情况下,当你在 Calculator.sol 的 add 函数中给结果赋值时,你实际上是将值赋给了学生合约中的 mySum。
这可能是个问题,因为存储槽可以有不同数据类型的变量。如果学生合约中的值是按这样的顺序定义的呢?
contract Student {
address public studentAddress;
uint public mySum;
}
在这种情况下,地址变量实际上最终会成为结果的值。你可能会想,一个地址数据类型怎么可能包含一个 uint 的值?要回答这个问题,你必须想得低一点。最后,所有的数据类型都只是字节。地址和 uint 都是 32 字节的数据类型,所以结果的 uint 值可以被设置在地址公共 studentAddress 变量中,因为它们都还是 32 字节的数据。