abi 编、解码;函数签名;函数选择器及 abi 编解码在 low-level call 中的应用。
在 Solidity 中,内置的 abi 编解码函数有如下 2 个:
abi.decode(bytes memory encodedData, (...)) returns (...)
encode
,encodePacked
均可解码)(uint a, uint[2] memory b, bytes memory c) = abi.decode(data, (uint, uint[2], bytes))
abi.encode(...) returns (bytes memory)
abi.encodePacked(...) returns (bytes memory)
abi.encodeWithSelector(bytes4 selector, ...) returns (bytes memory)
abi.encodeCall(function functionPointer, (...) ) returns (bytes memory)
functionPointer
的调用进行 ABI 编码。abi.encodeWithSelector(functionPointer.selector, ...)
abi.encodeWithSignature(string memory signature, ...) returns (bytes memory)
abi.encodeWithSelector(bytes4(keccak256(bytes(signature))), ...)
看到这里可能会比较迷惑,其中涉及到很多小的问题。
函数签名,函数选择器部分部分内容来自 ethereum stakechange 回答:https://ethereum.stackexchange.com/questions/135205/what-is-a-function-signature-and-function-selector-in-solidity-and-evm-language
函数签名是函数名称和它将采用的参数类型的集合,组成的一个没有空格的字符串。
例如,我们在 solidity 中有一个函数:
function transfer(address sender, uint256 amount) public {
// some code
}
那么这个函数的函数签名将是:
transfer(address,uint256)
函数签名十分重要,因为我们可以通过函数签名来获取下一个部分:函数选择器(function selector)
函数选择器是函数调用的 calldata 的前四个字节,用于指定我们要调用的函数。函数选择器是同一函数签名的 keccak256 哈希的前四个字节。
当我们调用 EVM 智能合约时,智能合约需要知道其将执行的函数,控制这一点的代码叫做函数选择器。函数选择器看起来长这样:
0xa9059cbb
在 solidity 中,我们可以通过如下的代码来求解我们的函数选择器:
bytes4(keccak256(bytes(function_signature)))
我们也可以使用 Foundry 框架中的cast sig
CLI 获取:
$ cast sig "transfer(address,uint256)"
在 solidity 中,对于我们已经在合约中定义的函数,我们可以使用如下的方法进行调用:
function transfer(address sender, uint256 amount) public {
// some code
}
function callTransfer() public {
tranfer(address(0), 1000);
}
如果我们不加括号,那么便是对函数的引用。 也就是上面提到的 functionPointer
(用于 abi.encodeCall
)
而对于函数的 functionPointer
,其有一个属性(方法),可以直接获得这个函数的函数选择器。
我们可以使用functionPointer.selector
来直接获取函数选择器,比如:
function transfer(address sender, uint256 amount) public {
// some code
}
function example() public {
// some logic
// get transfer selector
tranfer.selector;
// some logic
}
这样我们就可以在 Solidity 合约中直接获得已定义函数的函数选择器,无需手动计算。
对于函数选择器,我们将在 abi.encodeWithSelector
中使用。
在 solidity 中,调用函数可以通过直接实例化合约(或接口),直接调用该实例中的函数的方法直接调用函数(高级层面上)。同样,他还有一种调用函数的方法,这种方法,无需将对应地址实例化,只需知道我们要进行调用的地址,我们要调用的函数的函数选择器(函数签名)即可。这就是 low-level call。
low-level call 分为三种,本文仅讨论 call
一种。
还是刚刚的例子,如果我们想调用地址 token
上的 transfer()
函数,使用 low-level call 的方式来调用的话,可以使用下面的方法:
token.call(data);
这就需要我们手动构建我们的调用数据(calldata)—— data。
calldata 实际上就是函数选择器加上函数的参数
构建 calldata 就可以使用我们上面提到的几个 abi 编码函数:
使用该方法,我们就需要知道要调用的函数选择器:
// transfer(address sender, uint256 amount)
token.call(abi.encodeWithSelector(selector, address(1), 10));
在不知道函数选择器,但知道函数签名的情况下可以使用下面的方法(不过不如直接使用 abi.encodeWIthSignature
)
// transfer(address sender, uint256 amount)
token.call(abi.encodeWithSelector(bytes4(keccak256(bytes("transfer(address,uint256)"))), address(1), 10));
使用该方法,我们需要知道函数签名(对于没实例化合约的情况下,使用这种方法居多)
// transfer(address sender, uint256 amount)
token.call(abi.encodeWithSelector("transfer(address,uint256)", address(1), 10));
使用该方法,需要能引用函数(有 functionPointer
)下面是 Foundry cheatcode expectCall
中使用的例子:
address alice = makeAddr("alice");
emit log_address(alice);
vm.expectCall(
address(token), abi.encodeCall(token.transfer, (alice, 10)), 0
);
token.transferFrom(alice, address(0), 10);
expectCall 的两个参数是:预期的函数调用(calldata);预期出现的次数
使用 low-level call 时,会有两个返回值:(bool, bytes)
bool
值表示调用是否成功bytes
值为调用的返回值(经过 abi.encode
编码)比如下面的例子:
(, bytes memory results) = addr.call(abi.encodeWithSignature("balanceOf(address)"));
uint256 addrBalance = abi.decode(results, (uint256));
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!