编码函数:
abi.encode
abi.encodePacked
abi.encodeWithSignature
abi.encodeWithSelector
解码函数:
-
abi.decode
,用于解码被abi.encode
的数据
一、测试合约
为了测试这几个函数的功能,我们写了这样的测试合约:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
// Uncomment this line to use console.log
import "hardhat/console.sol";
contract Test {
uint256 x = 10;
address addr = 0x13a6D1fe418de7e5B03Fb4a15352DfeA3249eAA4;
string str = "This is China";
uint256[2] arr = [1, 2];
function core(uint256 _x, address _addr, string calldata _str, uint256[2] calldata _arr) public {
}
function testEncode() public view returns (bytes memory result) {
result = abi.encode(x, addr, str, arr);
}
function testEncodePacked() public view returns (bytes memory result) {
result = abi.encodePacked(x, addr, str, arr);
}
function testEncodeWithSignature() public view returns (bytes memory result) {
result = abi.encodeWithSignature("core(uint256,address,string,uint256[2])", x, addr, str, arr);
}
function testEncodeWithSelector() public view returns (bytes memory result) {
result = abi.encodeWithSelector(bytes4(keccak256("core(uint256,address,string,uint256[2])")), x, addr, str, arr);
}
function testDecode() public view returns (uint256 _x, address _addr, string memory _str, uint256[2] memory _arr) {
bytes memory result = testEncode();
return abi.decode(result, (uint256, address, string, uint256[2]));
}
}
在发布了合约之后,我们又用hardhat写了task:
task("test-transaction", "This task is broken")
.setAction(async () => {
const contractAddress = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512";
const test = await ethers.getContractAt('Test', contractAddress);
const encodeRes = await test.testEncode();
const encodePackedRes = await test.testEncodePacked();
const encodeWithSignatureRes = await test.testEncodeWithSignature();
const encodeWithSelectorRes = await test.testEncodeWithSelector();
const decodeRes = await test.testDecode();
console.log("encodeRes: ", encodeRes);
console.log("encodePackedRes: ", encodePackedRes);
console.log("encodeWithSignatureRes: ", encodeWithSignatureRes);
console.log("encodeWithSelectorRes: ", encodeWithSelectorRes);
console.log("decodeRes: ", decodeRes);
});
我们下面将通过输出的结果来阐述这几个函数的作用。
二、函数详解
1.encode
将给定参数利用ABI 规则编码。ABI 被设计出来跟智能合约交互,他将每个参数转填充为 32 字节的数据,并拼接在一起。如果你要和合约交互,你要用的就是 abi.encode
。
编码的结果为:
0x000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000013a6d1fe418de7e5b03fb4a15352dfea3249eaa400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000d54686973206973204368696e6100000000000000000000000000000000000000
由于 abi.encode 将每个数据都填充为 32 字节,中间有很多 0。
将其分割开,则有:
0x
000000000000000000000000000000000000000000000000000000000000000a
00000000000000000000000013a6d1fe418de7e5b03fb4a15352dfea3249eaa4
00000000000000000000000000000000000000000000000000000000000000a0
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002
000000000000000000000000000000000000000000000000000000000000000d
54686973206973204368696e6100000000000000000000000000000000000000
第1个32字节存储了x
,它就是uint256 x = 10
;
第2个32字节存储了addr
,即address addr = 0x13a6D1fe418de7e5B03Fb4a15352DfeA3249eAA4
;
第3个32字节存储了动态类型string
的存储位置,0xa0
即160个字节,即说明string类型存储在了160字节的位置,以0位开始计数,则第6个32字节开始存储string
类型的信息(感谢用户925bb9eb72ce的帮助
);
第4个32字节存储了arr
的第一个值arr[0];
第5个32字节存储了arr
的第一个值arr[1];
第6个32字节存储了str
的长度,值为0xd
,即10进制的13,是我们这里This is China
的长度;
第7个32字节即是This is China
的内容本身。
2.encodePacked
将给定参数根据其所需最低空间编码。它类似 abi.encode,但是会把其中填充的很多 0 省略。比如,只用 1 字节来编码 uint 类型。当你想省空间,并且不与合约交互的时候,可以使用 abi.encodePacked,例如算一些数据的 hash 时。
编码的结果为:
0x000000000000000000000000000000000000000000000000000000000000000a13a6d1fe418de7e5b03fb4a15352dfea3249eaa454686973206973204368696e6100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002
将其分割开,则有:
0x
000000000000000000000000000000000000000000000000000000000000000a
13a6d1fe418de7e5b03fb4a15352dfea3249eaa4
54686973206973204368696e61
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002
可以看到这里没有要与EVM底层执行的格式适配,就仅仅是实际存储内容的拼接加密,所以没有多余的要凑齐256位字长的0值。
3.encodeWithSignature
与 abi.encode 功能类似,只不过第一个参数为函数签名,比如”foo(uint256,address)”。当调用其他合约的时候可以使用。
编码的结果为:
0x58d382a9000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000013a6d1fe418de7e5b03fb4a15352dfea3249eaa400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000d54686973206973204368696e6100000000000000000000000000000000000000
等同于在 abi.encode 编码结果前加上了 4 字节的函数选择器。
将其分割开,则有:
0x
58d382a9
000000000000000000000000000000000000000000000000000000000000000a
00000000000000000000000013a6d1fe418de7e5b03fb4a15352dfea3249eaa4
00000000000000000000000000000000000000000000000000000000000000a0
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002
000000000000000000000000000000000000000000000000000000000000000d
54686973206973204368696e6100000000000000000000000000000000000000
这里的第一行的58d382a9
是函数签名对core(uint256,address,string,uint256[2])
进行keccak256运算后取前4字节的结果,这样的结果作为一种函数选择器,作为函数的唯一标识。剩下的字节就跟abi.encode
结果一样了,所以说,abi.encode
是用于合约交互的,因为合约交互就涉及到函数的调用,函数的调用就需要abi.encode
这种对数据的编码格式。
4.encodeWithSelector
与 abi.encodeWithSignature 功能类似,只不过第一个参数为函数选择器,为函数签名 Keccak 哈希的前 4 个字节。
编码的结果为:
0x58d382a9000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000013a6d1fe418de7e5b03fb4a15352dfea3249eaa400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000d54686973206973204368696e6100000000000000000000000000000000000000
将其分割开,则有:
0x
58d382a9
000000000000000000000000000000000000000000000000000000000000000a
00000000000000000000000013a6d1fe418de7e5b03fb4a15352dfea3249eaa4
00000000000000000000000000000000000000000000000000000000000000a0
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002
000000000000000000000000000000000000000000000000000000000000000d
54686973206973204368696e6100000000000000000000000000000000000000
这个从结果上是跟encodeWithSignature一样的,就是用法上存在差别:
result = abi.encodeWithSignature("core(uint256,address,string,uint256[2])", x, addr, str, arr);
result = abi.encodeWithSelector(bytes4(keccak256("core(uint256,address,string,uint256[2])")), x, addr, str, arr);
可以认为encodeWithSignature时一种对encodeWithSelector的简写,因为他会自动对函数签名先进行keccak256运算再取前4字节。
5.decode
abi.decode 用于解码 abi.encode 生成的二进制编码,将它还原成原本的参数。
可以看到我们的decode结果,就是我们加密前的参数。