一、为什么要用底层调用
我们知道,在一个合约中调用另一个合约的接口,通常使用contractName(address).functionName() 来进行调用,比如:
contract MyContract {function add(address _counter) public {// 调用 Counter 合约的方法Counter(_counter).increment();}
}
但是,有时我们在编写合约时,还不知道要调用的目标合约的接口,甚至是目标合约还没有创建。这时就无法用上面的方法进行调用。
这个问题该如何解决呢?
你也许知道很多编程语言(如Java和Go)有反射的概念,反射允许在运行时动态地调用函数或方法。地址的底层调用和反射非常类似。
使用address的底层调用功能,是在运行时动态地决定调用目标合约和函数, 因此在编译时,可以不知道具体要调用的函数或方法。
二、底层调用
address类型还有3个底层的成员函数:
<address>.call(bytes memory abiEncodeData) returns (bool, bytes memory)<address>.delegatecall(bytes memory abiEncodeData) returns (bool, bytes memory)<address>.staticcall(bytes memory abiEncodeData) returns (bool, bytes memory)
- 其中,
call是常规调用,delegatecall为委托调用,staticcall是静态调用(不修改合约状态, 相当于调用view方法)。
这三个函数都可以用于与目标合约<address>交互,三个函数均接受 abi 编码数据作为参数(abiEncodeData)来调用对应的函数。
这里我们使用底层方法调用一下《手把手教你部署智能合约》中的合约:
contract CallTest {function makeCallGet(address _counter) public view returns (uint) {// staticcall调用bytes memory payload = abi.encodeWithSignature("get()");(bool success, bytes memory returnData) = address(_counter).staticcall(payload);// 判断一下require(success, "Call to target contract failed.");// 将returnData解析成指定类型(e.g. uint)(uint res) = abi.decode(returnData, (uint));return res;}function makeCallCount(address _counter) public {// call调用bytes memory payload = abi.encodeWithSignature("count()");(bool success, ) = address(_counter).call(payload);// 判断一下require(success, "Call to target contract failed.");}
}// https://testnet.routescan.io/address/0xcF10C1b7DA166987a1D9bB81C072C339cb7205fd
使用底层方法调用合约函数时, 当被调用的函数发生异常时(revert),异常不会冒泡到调用者(即不会回退), 而是返回布尔值 false。因此在使用所有这些低级函数时,一定要记得检查返回值。
三、call 与 delegatecall
常规调用 call 与 委托调用 delegatecall 的区别是什么呢?

-
执行上下文:当使用
call函数时,被调用的函数在目标合约的上下文中执行,这意味着它有自己的this和msg.sender。而delegatecall函数则在调用合约(当前合约)的上下文中执行被调用的函数。—— 相当于将函数代码 pull 到当前合约中执行 -
状态存储:
call函数在执行时不会改变调用合约的状态,它只会改变被调用合约的状态。而delegatecall函数则可以改变调用合约的状态,因为它在调用合约的上下文中执行。 -
用途:
call函数通常用于调用其他合约的函数,而delegatecall函数允许一个合约借用另一个合约的代码,在自己的上下文中执行,常用于实现可升级合约和库函数。