WTF Solidity极简入门: 22. Call
我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。
所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity
我们曾在第20讲:发送ETH那一讲介绍过利用call
来发送ETH
,这一讲我们将介绍如何利用它调用合约。
Call
call
是address
类型的低级成员函数,它用来与其他合约交互。它的返回值为(bool, bytes memory)
,分别对应call
是否成功以及目标函数的返回值。
call
是Solidity
官方推荐的通过触发fallback
或receive
函数发送ETH
的方法。- 不推荐用
call
来调用另一个合约,因为当你调用不安全合约的函数时,你就把主动权交给了它。推荐的方法仍是声明合约变量后调用函数,见第21讲:调用其他合约 - 当我们不知道对方合约的源代码或
ABI
,就没法生成合约变量;这时,我们仍可以通过call
调用对方合约的函数。
call
的使用规则
call
的使用规则如下:
目标合约地址.call(字节码);
其中字节码
利用结构化编码函数abi.encodeWithSignature
获得:
abi.encodeWithSignature("函数签名", 逗号分隔的具体参数)
函数签名
为"函数名(逗号分隔的参数类型)"
。例如abi.encodeWithSignature("f(uint256,address)", _x, _addr)
。
另外call
在调用合约时可以指定交易发送的ETH
数额和gas
数额:
目标合约地址.call{value:发送数额, gas:gas数额}(字节码);
看起来有点复杂,下面我们举个call
应用的例子。
目标合约
我们先写一个简单的目标合约OtherContract
并部署,代码与第21讲中基本相同,只是多了fallback
函数。
contract OtherContract {
uint256 private _x = 0; // 状态变量x
// 收到eth的事件,记录amount和gas
event Log(uint amount, uint gas);
fallback() external payable{}
// 返回合约ETH余额
function getBalance() view public returns(uint) {
return address(this).balance;
}
// 可以调整状态变量_x的函数,并且可以往合约转ETH (payable)
function setX(uint256 x) external payable{
_x = x;
// 如果转入ETH,则释放Log事件
if(msg.value > 0){
emit Log(msg.value, gasleft());
}
}
// 读取x
function getX() external view returns(uint x){
x = _x;
}
}
这个合约包含一个状态变量x
,一个在收到ETH
时触发的事件Log
,三个函数:
getBalance()
: 返回合约ETH
余额。setX()
:external payable
函数,可以设置x
的值,并向合约发送ETH
。getX()
: 读取x
的值。
利用call
调用目标合约
1. Response事件
我们写一个Call
合约来调用目标合约函数。首先定义一个Response
事件,输出call
返回的success
和data
,方便我们观察返回值。
// 定义Response事件,输出call返回的结果success和data
event Response(bool success, bytes data);
2. 调用setX函数
我们定义callSetX
函数来调用目标合约的setX()
,转入msg.value
数额的ETH
,并释放Response
事件输出success
和data
:
function callSetX(address payable _addr, uint256 x) public payable {
// call setX(),同时可以发送ETH
(bool success, bytes memory data) = _addr.call{value: msg.value}(
abi.encodeWithSignature("setX(uint256)", x)
);
emit Response(success, data); //释放事件
}
接下来我们调用callSetX
把状态变量_x
改为5,参数为OtherContract
地址和5
,由于目标函数setX()
没有返回值,因此Response
事件输出的data
为0x
,也就是空。
3. 调用getX函数
下面我们调用getX()
函数,它将返回目标合约_x
的值,类型为uint256
。我们可以利用abi.decode
来解码call
的返回值data
,并读出数值。
function callGetX(address _addr) external returns(uint256){
// call getX()
(bool success, bytes memory data) = _addr.call(
abi.encodeWithSignature("getX()")
);
emit Response(success, data); //释放事件
return abi.decode(data, (uint256));
}
从Response
事件的输出,我们可以看到data
为0x0000000000000000000000000000000000000000000000000000000000000005
。而经过abi.decode
,最终返回值为5
。
4. 调用不存在的函数
如果我们给call
输入的函数不存在于目标合约,那么目标合约的fallback
函数会被触发。
function callNonExist(address _addr) external{
// call 不存在的函数
(bool success, bytes memory data) = _addr.call(
abi.encodeWithSignature("foo(uint256)")
);
emit Response(success, data); //释放事件
}
上面例子中,我们call
了不存在的foo
函数。call
仍能执行成功,并返回success
,但其实调用的目标合约fallback
函数。
总结
这一讲,我们介绍了如何用call
这一低级函数来调用其他合约。call
不是调用合约的推荐方法,因为不安全。但他能让我们在不知道源代码和ABI
的情况下调用目标合约,很有用。