Solidity教程第34课:Call、Staticcall和Delegatecall

  • jpmorais
  • 发布于 2023-03-26 13:14
  • 阅读 10

本文介绍了EVM中用于合约调用的三个操作码:call、delegatecall和staticcall。call用于发送以太币和调用函数,需要注意重入问题,delegatecall在调用合约的存储上下文中执行,staticcall则不允许改变区块链状态。文章通过代码示例详细解释了这三个操作码的用法和区别,包括如何传递参数、处理返回值以及发送以太币。

EVM 有 3 个可以在 Solidity 中直接调用的操作码:calldelegatecallstaticcall。 它们都用于向其他合约发送调用,目的是调用函数或仅向该合约发送以太币。

最近,使用 call 方法向任何地址发送以太币已成为推荐方式。 使用 calltransfer 之间的主要区别在于 call 将所有 gas 转发到地址,该地址可以是合约的地址。 因此,要使用 call 发送以太币,你必须小心重入问题。

顾名思义,重入是目标合约重新进入原始合约的一种方式。 重入方法可能会被恶意使用,正如 TheDAO 项目的著名黑客事件中所发生的那样。 在其中,黑客利用重入从原始合约中提取以太币,而无需更改合约中的账户余额。

Call

让我们看看如何在 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 合约中,我们首先创建一个状态变量,用于存储要调用的合约的地址。 从地址中,我们使用方法 calledcall([ 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

Staticcall 是一种类似于 call 的方法,但它不允许更改区块链的状态。 这意味着,如果被调用的函数更改了一些状态变量,例如,我们就不能使用 staticcall

在下图中,我们将 call 方法替换为 staticcall。 这样做后,编译器会向我们发出警告,即函数 callSetNumber 可以声明为 view,因为 staticcall 不允许更改区块链的状态。

staticcall 方法不允许更改区块链的状态。

如果我们使用 staticcall 来调用更改区块链状态的函数,则函数调用将不会成功。

我们可以使用 staticcall 来读取状态变量。 以下代码行完全有效。

(, bytes memory data) = called.staticcall(abi.encodeWithSignature("number()"));

call 类似,staticcall 也返回两个值。 一个布尔值,指示调用的成功与否,以及一个字节类型的值,它是调用的返回值。 由于 staticcall 不会更改区块链的状态,因此仅当我们想要检索某些值时执行此方法才有意义; 也就是说,当我们期望有返回值时。

Delegatecall

也可以在另一个合约中执行一个函数,但以这样一种方式来更改调用合约的状态变量。 为此,使用 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,因此一切都按计划进行。

现在让我们进行更改。 让我们反转变量 numbercalled 的声明顺序,如下所示。

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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
jpmorais
jpmorais
江湖只有他的大名,没有他的介绍。