abi 编解码函数及 low-level call
abi 编码,解码函数
在 Solidity 中,内置的 abi 编解码函数有如下 2 个:
-
abi.decode(bytes memory encodedData, (...)) returns (...)
- 根据传入的数据类型解码数据(
encode
,encodePacked
均可解码) - 数据类型通过在括号中作为第二个参数给出。
- example:
(uint a, uint[2] memory b, bytes memory c) = abi.decode(data, (uint, uint[2], bytes))
- 根据传入的数据类型解码数据(
-
abi.encode(...) returns (bytes memory)
- 对给定的数据进行 abi 编码
-
abi.encodePacked(...) returns (bytes memory)
- 对给定的数据进行紧密编码(此编码可能会不明确)
-
abi.encodeWithSelector(bytes4 selector, ...) returns (bytes memory)
- 从第二个数据开始,对给定的参数进行 ABI 编码,并在前面添加给定的 4 bytes 的函数选择器
- 通过该内置函数,我们可以构造我们的 calldata
-
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
函数签名(function signature)
函数签名是函数名称和它将采用的参数类型的集合,组成的一个没有空格的字符串。
例如,我们在 solidity 中有一个函数:
function transfer(address sender, uint256 amount) public {
// some code
}
那么这个函数的函数签名将是:
transfer(address,uint256)
函数签名十分重要,因为我们可以通过函数签名来获取下一个部分:函数选择器(function selector)
函数选择器(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
,其有一个属性(方法),可以直接获得这个函数的函数选择器。
在 solidity 代码中直接获取已声明函数的函数选择器
我们可以使用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
中使用。
low-level call
在 solidity 中,调用函数可以通过直接实例化合约(或接口),直接调用该实例中的函数的方法直接调用函数(高级层面上)。同样,他还有一种调用函数的方法,这种方法,无需将对应地址实例化,只需知道我们要进行调用的地址,我们要调用的函数的函数选择器(函数签名)即可。这就是 low-level call。
low-level call 分为三种,本文仅讨论 call
一种。
还是刚刚的例子,如果我们想调用地址 token
上的 transfer()
函数,使用 low-level call 的方式来调用的话,可以使用下面的方法:
token.call(data);
这就需要我们手动构建我们的调用数据(calldata)—— data。
calldata 实际上就是函数选择器加上函数的参数
构建 calldata 就可以使用我们上面提到的几个 abi 编码函数:
使用 abi.encodeWithSelector
使用该方法,我们就需要知道要调用的函数选择器:
// 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));
使用 abi.encodeWithSignature
使用该方法,我们需要知道函数签名(对于没实例化合约的情况下,使用这种方法居多)
// transfer(address sender, uint256 amount)
token.call(abi.encodeWithSelector("transfer(address,uint256)", address(1), 10));
使用 abi.encodeCall
使用该方法,需要能引用函数(有 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);预期出现的次数
使用 abi.decode 对 low-level call 的返回值进行解码
使用 low-level call 时,会有两个返回值:(bool, bytes)
- 第一个
bool
值表示调用是否成功 - 第二个
bytes
值为调用的返回值(经过abi.encode
编码)
比如下面的例子:
(, bytes memory results) = addr.call(abi.encodeWithSignature("balanceOf(address)"));
uint256 addrBalance = abi.decode(results, (uint256));
版权声明
本文仅代表作者观点,不代表区块链技术网立场。
本文系作者授权本站发表,未经许可,不得转载。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。