Solidity的ABI编码函数详解:encode、encodePacked、encodeWithSignature、encodeWithSelector

  • Post author:
  • Post category:solidity

编码函数:

  • 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结果

可以看到我们的decode结果,就是我们加密前的参数。