底层调用 call

本章讲解在 Solidity 中,如何使用 call 来调用其它合约的函数。

官网binschool.app
推特@BinSchool    DiscordBinDAO   微信:bkra50 

在一个智能合约中调用另外一个外部智能合约的函数,我们可以通过接口 interface 的方式进行调用。

关于这方面的内容,我们在基础教程中的 interface 章节中已经学习过。

另外,还有一种比较底层的调用方法,就是使用 call 函数。

Solidity 中,call 函数用于在智能合约内部调用其它合约的函数,与之进行交互。

call 是一种低级、底层的调用方式,它具有更大的灵活性。

Solidity 中,与 call 类似的函数还有 staticcalldelegatecallcallcode。它们有不同的工作方式和使用场景,我们将分别进行讲解。其中的 callcodeSolidity 0.8 及以上版本中已经废弃,我们也不再讲解。

1. call 函数的语法

(bool success, bytes memory result) = address(contractAddress).call{value: valueToSend}(data);

其中的返回值的含义如下:

success:指示调用外部函数是否成功。

result:调用的外部函数的返回值。

其中的参数的含义如下:

contractAddress:要调用的外部合约的地址。

valueToSend:发送到外部合约的 ETH 数量,它的单位是 wei。这是一个可选参数,如果无需发送 ETH,就可以选择忽略这个参数。

data:发送到外部合约的数据。它是对外部函数签名和参数进行编码,而生成的字节数组。

比如,我们要调用一个外部合约的函数 functionName(uint256),那么就需要使用 abi 对函数签名和参数进行编码。

编码方法如下:

abi.encodeWithSignature("functionName(uint256)", parameter)

您如果不清楚的话,可以参考前面的 abi 编码解码章节。

2. call 函数的用法

我们先准备一个被调用的合约,合约中定义了一个函数 setValue

另外,这个合约还定义了 receive 函数,使之具有接收 ETH 的能力。

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

contract Callee {
  uint256 public value;

  function setValue(uint256 _value) external payable returns(uint) {
    // 将参数值保存到状态变量
    value = _value;
    // 返回收到的参数值
    return value;
  }

  // 合约具有接收 ETH 能力
  receive() external payable { }
}

我们使用 Remix 对合约进行编译和部署,就会得到它部署后的地址。比如为: 0x975218C73817F1833b382BF2845B3D1E4eF11836

 

下面就是 3 种使用 call 与外部合约进行交互的示例。

2.1 只调用外部合约的函数

我们编写一个智能合约 Caller,它会调用外部合约 Callee 的函数 setValue(uint256) ,函数的参数值为 8

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

contract Caller {
  // 调用外部合约的函数
  // 参数 contractAddress 是被调用合约的地址
  function callExternalFunc(address contractAddress) 
    external returns(bool, bytes memory) {

    // 对函数签名和参数进行编码
    bytes memory data = abi.encodeWithSignature("setValue(uint256)", 8);

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

在上面的合约中,callExternalFunc 函数功能:调用外部合约 Callee 的函数 setValue(uint256)

我们先要对被调用函数 setValue 的函数签名和参数进行编码,再将编码结果作为参数,使用 call 来调用外部合约 Callee 的函数。

我们使用 Remix 对合约进行编译和部署,然后在 callExternalFunc 方法中,填入被调用合约 Callee 的地址 0x975218C73817F1833b382BF2845B3D1E4eF11836,最后点击调用 callExternalFunc

我们可以在控制台中看到输出的结果:第一个输出值为 true,表示调用函数成功。第二个输出值为 8,这是被调用函数的返回值。

这与我们预想的结果是一致的。

2.2 只向外部合约的发送 ETH

我们再编写一个智能合约 Caller。我们会在这个合约中,通过 call 函数向被调用合约 Callee 发送 1 ETH

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

contract Caller {

  // 构造函数
  constructor() payable {
  }

  // 向外部合约发送以太币
  // 参数 contractAddress 是被调用合约的地址
  function callExternalFunc(address contractAddress) 
    external returns(bool, bytes memory) {
    return contractAddress.call{value: 1 ether}("");
  }
}

在上面的合约中,构造函数的状态可变性为 payable,所以在部署的时候,可以向合约中存入以太币。

callExternalFunc 函数用于向外部合约  Callee 发送以太币,发送数量放置在 value 属性中,值为 1 ether

因为不需要调用外部合约的函数,所以 call 函数的参数设置为空字符串。

我们使用 Remix 对合约进行编译,在部署的时候先存入 10 ETH,然后再点击 callExternalFunc 方法。

我们可以在控制台中看到输出的结果:第一个输出值为 true,表示调用成功。第二个输出值为 0,这是被调用函数的返回值。

这时候 Caller 合约的余额已经变成 9 ETH,另外的 1 ETH,已经转入了被调用合约 Callee 中。

2.3 调用外部合约的函数,并发送 ETH

我们再编写一个合约 Caller。我们会在这个合约中,调用 Callee 合约的 setValue(uint256) 函数,参数值为 8,并向 Callee 合约转入 1 ETH

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

contract Caller {
  // 构造函数
  constructor() payable {
  }

  // 向外部合约发送以太币
  function callExternalFunc(address contractAddress) 
    external returns(bool, bytes memory) {
    // 对函数签名和参数进行编码
    bytes memory data = abi.encodeWithSignature("setValue(uint256)", 8);
    // 调用外部外部函数,并发送以太币
    return contractAddress.call{value: 1 ether}(data);
  }
}

我们使用 Remix 对合约进行编译,在部署的时候先存入 10 ETH,然后再点击 callExternalFunc 方法。

我们可以在控制台中看到输出的结果:第一个输出值为 true,表示调用成功。第二个输出值为 8,这是被调用函数的返回值。

这时候 Caller 合约的余额已经变成 9 ETH,另外的 1 ETH,已经转入了被调用合约 Callee 中。

学习本章的内容,您可以参考 receivefallback 两个章节。