本文介绍了EVM中用于合约调用的三个操作码:call、delegatecall和staticcall。call用于发送以太币和调用函数,需要注意重入问题,delegatecall在调用合约的存储上下文中执行,staticcall则不允许改变区块链状态。文章通过代码示例详细解释了这三个操作码的用法和区别,包括如何传递参数、处理返回值以及发送以太币。
EVM 有 3 个可以在 Solidity 中直接调用的操作码:call、delegatecall 和 staticcall。 它们都用于向其他合约发送调用,目的是调用函数或仅向该合约发送以太币。
最近,使用 call 方法向任何地址发送以太币已成为推荐方式。 使用 call 和 transfer 之间的主要区别在于 call 将所有 gas 转发到地址,该地址可以是合约的地址。 因此,要使用 call 发送以太币,你必须小心重入问题。
顾名思义,重入是目标合约重新进入原始合约的一种方式。 重入方法可能会被恶意使用,正如 TheDAO 项目的著名黑客事件中所发生的那样。 在其中,黑客利用重入从原始合约中提取以太币,而无需更改合约中的账户余额。
让我们看看如何在 solidity 中使用 call 方法。 首先,我们创建一个将被另一个合约调用的合约。 要调用的合约将被命名为 Called
。
contract Called {
uint public number;
function increment() public {
number++;
}
}
这个合约必须被部署。 在我们的例子中,部署到了地址 0xd9145CCE52D386f254917e481eB44e9943F39138
。 现在让我们编写将调用 Called
的合约。 它的名字将是 Caller
。
contract Caller {
address public called = 0xd9145CCE52D386f254917e481eB44e9943F39138;
function callCalled() public returns(bool, bytes memory) {
(bool success,bytes memory data) = called.call(abi.encodeWithSignature("increment()"));
return (success, data);
}
}
在 Caller
合约中,我们首先创建一个状态变量,用于存储要调用的合约的地址。 从地址中,我们使用方法 called。 call([ payload])。 让我们来谈谈 payload。
调用的 payload 必须包含要调用的函数的签名(4 个字节),以及由 ABI 编码的函数参数。 这可以使用 abi 对象的 encodeWithSignature 方法来完成。 在我们的函数中,没有参数。
call 方法返回一对值:一个布尔值,指示函数是否成功执行,以及一个字节类型的值,其中包含函数的返回值,也经过 ABI 编码。
调用的返回值。
调用的结果可以在上图中看到。 布尔值的值为 true
,表示事务成功。 字节类型的变量为空,因为该函数不返回任何内容。
让我们稍微修改一下要调用的函数。 现在它将接收一个参数并返回变量 number
的新值。
function increment(uint _increment) public returns (uint) {
number = number + _increment;
return number;
}
请注意,你需要再次部署合约,因为 Caller
中的 Called
地址必须更改。 该地址目前直接写入代码中,但可以保存在变量中。 这就是我们编写可升级合约的方式,区别在于我们使用 delegatecall 而不是 call。 我们稍后会了解 delegatecall。
让我们也更改 Caller
中的调用函数。
function callCalled() public returns(bool, bytes memory) {
(bool success,bytes memory data) = called.call(abi.encodeWithSignature("increment(uint256)",2));
return (success, data);
}
现在我们需要将参数传递给 call 函数。 在我们的示例中,参数直接在代码中传递,编码如下:
abi.encodeWithSignature("increment(uint256)",2)
encodeWithSignature 方法接受任意数量的参数,我们必须传递调用该函数所需的所有参数。
具有返回值的函数调用的返回值。
在上图中,我们看到函数的返回值是字节类型。 此返回值由合约的 ABI 编码。 我们还可以解码返回值。 让我们在下面的函数中看到这一点。
function callCalled() public returns(bool, uint) {
(bool success,bytes memory data) = called.call(abi.encodeWithSignature("increment(uint256)",2));
uint decoded = abi.decode(data, (uint256));
return (success, decoded);
}
要解码返回值,我们使用 abi 对象的 decode 方法。 它需要 2 个参数:要解码的字节类型变量和一个包含字节将被解码成的类型的元组。
我们可以将返回值解码为其类型。
在上图中,我们看到使用 decode 方法解码的返回值。
正如已经说过的,目前推荐的向另一个地址发送以太币的方法是使用 call。 使用 call 时,可以指示要发送的值和要转发的 gas 量。
指示要发送的值是通常的做法,但不建议指示要转发的 gas 量。 要向地址(无论是合约还是非合约)发送以太币,我们使用 call 方法,如下所示。
address.call{value: 1 ether}("")
在上面的示例中,1 以太币的值被发送到地址 address,payload 为空。 也可以发送 payload,以防我们还想调用一个函数。
要定义要转发的 gas 量,我们使用 gas 属性,如下所示:
address.call{value: 1 ether, gas: 10000}("")
Staticcall 是一种类似于 call 的方法,但它不允许更改区块链的状态。 这意味着,如果被调用的函数更改了一些状态变量,例如,我们就不能使用 staticcall。
在下图中,我们将 call 方法替换为 staticcall。 这样做后,编译器会向我们发出警告,即函数 callSetNumber
可以声明为 view,因为 staticcall 不允许更改区块链的状态。
staticcall 方法不允许更改区块链的状态。
如果我们使用 staticcall 来调用更改区块链状态的函数,则函数调用将不会成功。
我们可以使用 staticcall 来读取状态变量。 以下代码行完全有效。
(, bytes memory data) = called.staticcall(abi.encodeWithSignature("number()"));
与 call 类似,staticcall 也返回两个值。 一个布尔值,指示调用的成功与否,以及一个字节类型的值,它是调用的返回值。 由于 staticcall 不会更改区块链的状态,因此仅当我们想要检索某些值时执行此方法才有意义; 也就是说,当我们期望有返回值时。
也可以在另一个合约中执行一个函数,但以这样一种方式来更改调用合约的状态变量。 为此,使用 delegatecall 方法。
让我们创建一个将被调用的合约,名为 Called
。
contract Called {
uint public number;
function setNumber(uint _number) public {
number = _number;
}
}
现在让我们编写将调用 Called
的合约,名为 Caller
。
contract Caller {
uint public number;
address public called = 0xd9145CCE52D386f254917e481eB44e9943F39138;
function callSetNumber(uint _number) public {
called.delegatecall(abi.encodeWithSignature("setNumber(uint256)",_number));
}
}
callSetNumber
函数将调用 Called
上的 setNumber
函数,这将更改 number
变量。 但是,它将更改合约 Caller
的变量 number
,而不是定义该函数的合约的变量。
与 call 类似,delegatecall 也返回两个值。 第一个是布尔值,指示事务是否成功,第二个是字节类型,它是函数的返回值(如果有任何返回值)。
在上面的示例中,我们在两个合约 number 中使用了相同的状态变量名称,但这不是必需的。 变量的名称无关紧要,重要的是它在存储中的位置。 我们需要理解这一点。
使用 delegatecall,执行在调用合约的存储中完成。 因此,我们需要确保两个合约的变量正确配对。
以太坊的存储由一系列 32 字节的容器组成,每个容器可以包含一个或多个状态变量。 在调用合约中,变量 number
占用第一个容器,而变量 called
部分占用第二个容器。
当我们调用函数 setNumber
时,它会更改第一个容器,而不管其中有什么变量。 由于在调用合约中,存在我们要更改的变量 number
,因此一切都按计划进行。
现在让我们进行更改。 让我们反转变量 number
和 called
的声明顺序,如下所示。
contract Caller {
address public called = 0xd9145CCE52D386f254917e481eB44e9943F39138;
uint public number;
function callSetNumber(uint _number) public {
called.delegatecall(abi.encodeWithSignature("setNumber(uint256)",_number));
}
}
setNumber
函数将继续更改第一个容器,但是现在变量 called
在第一个容器中,而不是 number
。 执行 callSetNumber
时,变量 called
将被更改,从而更改要调用的合约的地址,从而完全破坏代码。
这就是为什么我们在使用 delegatecall 时应该小心。 有必要确切地了解正在做什么,理想的是在两个合约(调用合约和被调用合约)中配对变量。
感谢阅读!
欢迎对本文提出意见和建议。
欢迎任何贡献。www.buymeacoffee.com/jpmorais。
- 原文链接: medium.com/coinmonks/cal...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!