abi 编码解码详解
来源:https://www.wtf.academy/solidity-advanced/ABIEncode/
ABI
(Application Binary Interface,应用二进制接口)是与以太坊智能合约交互的标准。数据基于他们的类型编码,并且由于编码后不包含类型信息,解码时需要注明它们的类型
Solidity
中,ABI编码
有 4 个函数:abi.encode
,abi.encodePacked
,abi.encodeWithSignature
,abi.encodeWithSelector
。而abi解码
有一个函数:abi.decode
,用于解码abi.encode
的数据。
ABI 编码
这里将用编码 4 个变量,他们的类型分别是 uint256
,address
,string
,uint256[2]
1 | uint x = 10; |
abi.encode
将给定参数利用ABI规则进行编码。ABI
被设计出来跟智能合约交互,他将每个参数填充为 32 字节的数据,并拼接在一起。如果想和合约交互,就需要abi.encode
1 | function encode() public view returns(bytes memory result) { |
编码结果为:
0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
,由于abi.encode
将每个数据都填充为32字节,中间有很多0
。
abi.encodePacked
将给定参数根据其所需最低空间编码。它类似 abi.encode
,但是会把其中填充的很多0
省略。比如,只用1字节来编码uint
类型。当你想省空间,并且不与合约交互的时候,可以使用abi.encodePacked
,例如算一些数据的hash
时。
1 | function encodePacked() public view returns(bytes memory result) { |
编码的结果为:
0x000000000000000000000000000000000000000000000000000000000000000a7a58c0be72be218b41c608b7fe7c5bb630736c713078414100000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006
,由于abi.encodePacked
对编码进行了压缩,长度比abi.encode
短很多。
对于encodePacked
出来的结果,无法使用decode
解码,可用直接使用类型转换。
举例:
使用encodePacked
将两个字符串紧密编码后,想还原原来的字符串内容,此时无法使用decode
进行解码,可以使用String()
强制类型转换将其还原为原来被打包之前的两个字符串内容
abi.encodeWithSignature
与abi.encode
功能类似,只不过第一个参数为函数签名
,比如"foo(uint256,address)"
。当调用其他合约的时候可以使用。
1 | function encodeWithSignature() public view returns(bytes memory result) { |
编码的结果为:
0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
,等同于在abi.encode
编码结果前加上了4字节的函数选择器
(函数选择器:函数选择器就是通过函数名和参数进行签名处理(Keccak–Sha3)来标识函数,可以用于不同合约之间的函数调用)
abi.encodeWithSelector
与abi.encodeWithSignature
功能类似,只不过第一个参数为函数选择器
,为函数签名
Keccak哈希的前4个字节。
1 | function encodeWithSelector() public view returns(bytes memory result) { |
编码的结果为:
0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
,与abi.encodeWithSignature
结果一样。
ABI 解码
abi.decode
abi.decode
用于解码abi.encode
生成的二进制编码,将它还原成原本的参数。(对于encodePacked
编码的结果无法解码)
1 | function decode(bytes memory data) public pure returns(uint dx, address daddr, string memory dname, uint[2] memory darray) { |
我们将abi.encode
的二进制编码输入给decode
,将解码出原来的参数:
新版 Solidity 特性:
在最新版本的 Solidity 编译器中,可以使用字节类型成员和字符串类型成员中内置的方法来直接拼接字符串和字节数组:
bytes.concat(...) returns (bytes memory)
string.concat(...) returns (bytes memory)
在remix上验证
部署合约查看abi.encode方法的编码结果
对比验证四种编码方法的异同点
查看abi.decode方法的解码结果
ABI的使用场景
- 在合约开发中,ABI常配合call来实现对合约的底层调用。
1 | bytes4 selector = contract.getValue.selector; |
- ethers.js中常用ABI实现合约的导入和函数调用。
1 | const wavePortalContract = new ethers.Contract(contractAddress, contractABI, signer); |
- 对不开源合约进行反编译后,某些函数无法查到函数签名,可通过ABI进行调用。
0x533ba33a() 是一个反编译后显示的函数,只有函数编码后的结果,并且无法查到函数签名
这种情况无法通过构造interface接口或contract来进行调用
这种情况下,就可以通过ABI函数选择器来调用
1 | bytes memory data = abi.encodeWithSelector(bytes4(0x533ba33a)); |