前两节介绍了通过合约的源码和接口两种方法调用合约,现在使用call和delegatecall来调用。
这两个函数都是低级函数,应谨慎使用。 具体来说,任何未知的合约都可能是恶意的,我们在调用一个合约的同时就将控制权交给了它,而合约又可以回调合约,所以要准备好在调用返回时改变相应的状态变量(可参考 可重入 ), 与其他合约交互的常规方法是在合约对象上调用函数(x.f())。
call
call 是address类型的低级成员函数,它用来与其他合约交互。它的返回值为(bool, data),分别对应call是否成功以及目标函数的返回值。
- call是solidity官方推荐的通过触发fallback或receive函数发送ETH的方法。
- 当我们不知道对方合约的源代码或ABI,就没法生成合约变量;这时,我们仍可以通过call调用对方合约的函数。
先定义一个合约:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
contract TargetContract{
uint256 private _x = 0;
event Log(string message);
fallback() external payable {
emit Log("function not exist");
}
function setX(uint x) external payable{
_x = x;
}
function getX() external view returns(uint x){
x = _x;
}
}
call的使用规则:
目标合约地址.call(二进制编码);
call在调用有payable的合约函数时可以指定交易发送的ETH数额和gas:
目标合约地址.call{value:发送数额, gas:gas数额}(二进制编码);
函数 abi.encode,abi.encodePacked,abi.encodeWithSelector 和 abi.encodeWithSignature 可用于编码结构化数据。
call调用目标合约
定义状态变量接收call返回值:
bytes public data;
调用setX()函数,转入msg.value数额的ETH,uint类型在abi编码传参中要写成uint256:
function callSetX(address _targetContractAddr, uint x) external payable{
(bool ok, bytes memory _data) = _targetContractAddr.call{value: msg.value}(abi.encodeWithSignature("setX(uint256)", x));
require(ok, "call setX failed");
data = _data;
}
调用getX()函数
function callGetX(address _targetContractAddr) external payable{
(bool ok, bytes memory _data) = _targetContractAddr.call(abi.encodeWithSignature("getX()"));
require(ok, "call getXfailed");
data = _data;
}
调用不存在的函数,因为TargetContract合约中写了fallback()函数,会触发fallback()函数:
function callNotExist(address _targetContractAddr) external{
(bool ok,) = _targetContractAddr.call(abi.encodeWithSignature("notExist()"));
require(ok, "call notExist failed");
}
abi.decode解码返回值:
function abiDecode() external view returns(uint256) {
return abi.decode(data,(uint256));
}
委托调用 delegatecall
delegatecall与call类似,也是solidity中地址类型的低级成员函数。delegate中是委托/代表的意思。
委托合约通俗理解就是调用其他合约的函数来改变自身的状态变量。
定义一个目标合约:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
contract TargetContract {
address public owner;
uint public x;
uint public balance;
function getBalance() external payable{
balance = address(this).balance;
}
function setX(uint _x) external payable {
x = _x;
}
function setOwner(address _owner) external {
owner = _owner;
}
}
调用目标合约:
本合约中使用了abi.encodeWithSelector为二进制编码,与abi.encodeWithSignature是一样的,二选一即可。
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
contract DelegateCall {
address public owner;
uint public x;
uint public balance;
address public targetContractAddr;
constructor(address _addr) {
targetContractAddr = _addr;
}
function callGetBalance() external payable{
(bool ok,) = targetContractAddr.delegatecall(abi.encodeWithSelector(TargetContract.getBalance.selector));
require(ok, "call getBalance failed");
}
function callSetX(uint _x) external payable{
(bool ok,) = targetContractAddr.delegatecall(abi.encodeWithSelector(TargetContract.setX.selector, _x));
require(ok, "call setX failed");
}
function callSetOwner(address _owner) external{
(bool ok,) = targetContractAddr.delegatecall(abi.encodeWithSelector(TargetContract.setOwner.selector, _owner));
require(ok, "call setOwner failed");
}
}
Remix验证发现TargetContract的状态变量没有任何改变,DelegateCall 已经改变。
call和delegatecall的区别
当用户A通过合约B来call合约C的时候,执行的是合约C的函数,语境(Context,可以理解为包含变量和状态的环境)也是合约C的:msg.sender是B的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约C的变量上。
当用户A通过合约B来delegatecall合约C的时候,执行的是合约C的函数,但是语境仍是合约B的:msg.sender是A的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约B的变量上。