solidity-abi编码解码详解

solidity-abi编码解码详解

Solidity ABI 编码与解码详解

参考来源:RareSkills - ABI Encoding


目录

  1. 什么是 ABI 编码
  2. 编码基础原则:32 字节对齐
  3. 静态类型 vs 动态类型
  4. abi.encode
  5. abi.encodePacked
  6. abi.encodeWithSelector
  7. abi.encodeWithSignature
  8. abi.decode
  9. 函数选择器(Function Selector)
  10. 低级调用(Low-level Call)
  11. 实战综合示例
  12. 安全注意事项
  13. 对比总结

1. 什么是 ABI 编码

ABI(Application Binary Interface,应用程序二进制接口) 是 Solidity 合约与外部世界(DApp 前端、其他合约、钱包等)交互的底层数据传输标准。

当你调用一个合约函数时,EVM 接收到的是一串字节数据(calldata),ABI 编码就是将函数名和参数转化为这串字节的规则。

调用 transfer(address recipient, uint256 amount)
         ↓  ABI 编码
0xa9059cbb                              ← 函数选择器(4字节)
000000000000000000000000Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2  ← address(32字节)
0000000000000000000000000000000000000000000000000000000000000064  ← uint256 100(32字节)

2. 编码基础原则:32 字节对齐

Solidity ABI 标准规定:所有参数编码后均以 32 字节(256 位)为基本单位对齐

  • 数值不足 32 字节时,左侧补零(uintaddressbool 等)
  • 字节类型不足 32 字节时,右侧补零(bytes1~bytes32
  • 动态类型(bytesstring、动态数组)通过”偏移量 + 长度 + 数据”三段式存储
uint256(100) 编码:
0x0000000000000000000000000000000000000000000000000000000000000064
  ← 32 字节,左侧补零 →

bytes4(0xdeadbeef) 编码(静态):
0xdeadbeef00000000000000000000000000000000000000000000000000000000
  ← 32 字节,右侧补零 →

3. 静态类型 vs 动态类型

分类类型示例编码方式
静态类型uint256, int128, address, bool, bytes32, bytes4直接 32 字节对齐,原地编码
动态类型bytes, string, uint[], bytes[]先写偏移量(offset),数据放在尾部

动态类型编码结构示意

abi.encode("hello")

偏移量(offset = 32,即动态内容从第32字节处开始):
0x0000000000000000000000000000000000000000000000000000000000000020

长度(length = 5):
0x0000000000000000000000000000000000000000000000000000000000000005

实际数据("hello" 右侧补零至32字节):
0x68656c6c6f000000000000000000000000000000000000000000000000000000

4. abi.encode

说明

  • 按照标准 ABI 规范对所有参数进行编码
  • 每个参数均填充至 32 字节
  • 动态类型包含偏移量、长度和内容
  • 输出为 bytes可以使用 abi.decode 还原

代码示例

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract AbiEncodeDemo {

    // 编码多个静态类型
    function encodeStatic() public pure returns (bytes memory) {
        uint256 a = 100;
        address b = address(0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2);
        bool c = true;
        return abi.encode(a, b, c);
        // 输出:3 × 32 = 96 字节
        // a: 0x0000...0064
        // b: 0x000...Ab8483F6...
        // c: 0x0000...0001
    }

    // 编码动态类型(string)
    function encodeString() public pure returns (bytes memory) {
        return abi.encode("hello");
        // 输出:3 × 32 = 96 字节
        // [offset=32][length=5][data="hello"+padding]
    }

    // 编码混合类型
    function encodeMixed() public pure returns (bytes memory) {
        uint256 x = 42;
        string memory s = "world";
        return abi.encode(x, s);
        // x: 原地32字节
        // s: 偏移量32字节 + 在尾部写入长度 + 数据
    }

    // 查看编码后的字节长度
    function checkLength() public pure returns (uint256 staticLen, uint256 stringLen) {
        staticLen = abi.encode(uint256(1), address(0), bool(true)).length; // 96
        stringLen = abi.encode("hello").length;                            // 96
    }
}

输出分析(encodeStatic)

[0]  0x0000000000000000000000000000000000000000000000000000000000000064  ← uint256(100)
[32] 0x000000000000000000000000Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2  ← address
[64] 0x0000000000000000000000000000000000000000000000000000000000000001  ← bool(true)

5. abi.encodePacked

说明

  • 紧凑编码,不进行 32 字节对齐补零
  • 各类型直接按其原始字节大小拼接
  • 输出更短,不能用 abi.decode 还原(因无填充信息)
  • 常用于 keccak256 哈希计算

代码示例

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract AbiEncodePackedDemo {

    // 对比 encode 与 encodePacked 的长度差异
    function compareLengths() public pure returns (uint256 encodeLen, uint256 packedLen) {
        uint256 a = 1;
        address b = address(0x1234567890123456789012345678901234567890);

        encodeLen = abi.encode(a, b).length;       // 64 字节(2 × 32)
        packedLen = abi.encodePacked(a, b).length; // 52 字节(32 + 20)
    }

    // 用于哈希:生成唯一标识
    function hashId(address user, uint256 nonce) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(user, nonce));
    }

    // 字符串拼接
    function concatStrings(string memory a, string memory b) 
        public pure returns (string memory) 
    {
        return string(abi.encodePacked(a, b));
    }

    // ⚠️ 演示哈希碰撞风险
    function collisionRisk() public pure returns (bool) {
        // 两种不同输入,encodePacked 后结果相同!
        bytes memory a = abi.encodePacked("aaa", "bbb"); // "aaabbb"
        bytes memory b = abi.encodePacked("aa", "abbb"); // "aaabbb"
        return keccak256(a) == keccak256(b);              // true ⚠️ 碰撞!
    }

    // ✅ 使用 abi.encode 避免碰撞
    function noCollision() public pure returns (bool) {
        bytes memory a = abi.encode("aaa", "bbb");  // 含长度信息
        bytes memory b = abi.encode("aa", "abbb");  // 含长度信息
        return keccak256(a) == keccak256(b);         // false ✅ 安全
    }
}

encode vs encodePacked 字节对比

abi.encode(uint8(1), uint8(2)):
0x0000000000000000000000000000000000000000000000000000000000000001  ← 32字节
0x0000000000000000000000000000000000000000000000000000000000000002  ← 32字节
总共:64 字节

abi.encodePacked(uint8(1), uint8(2)):
0x0102
总共:2 字节(直接拼接原始字节)

6. abi.encodeWithSelector

说明

  • abi.encode 的基础上,在头部加上 4 字节函数选择器
  • 主要用于构造低级调用(call)的 calldata
  • 选择器 = keccak256("函数签名") 的前 4 字节

代码示例

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IERC20 {
    function transfer(address to, uint256 amount) external returns (bool);
}

contract EncodeWithSelectorDemo {

    // 方式一:使用函数引用获取 selector(推荐,编译期检查)
    function encodeTransfer_v1(address to, uint256 amount) 
        public pure returns (bytes memory) 
    {
        return abi.encodeWithSelector(IERC20.transfer.selector, to, amount);
    }

    // 方式二:手动计算 selector
    function encodeTransfer_v2(address to, uint256 amount) 
        public pure returns (bytes memory) 
    {
        bytes4 selector = bytes4(keccak256("transfer(address,uint256)"));
        return abi.encodeWithSelector(selector, to, amount);
    }

    // 使用构造的 calldata 发起低级调用
    function callTransfer(address token, address to, uint256 amount) 
        public returns (bool success) 
    {
        bytes memory data = abi.encodeWithSelector(IERC20.transfer.selector, to, amount);
        (success, ) = token.call(data);
    }

    // 查看 selector 值
    function getSelector() public pure returns (bytes4) {
        return IERC20.transfer.selector;
        // 0xa9059cbb
    }
}

calldata 结构

abi.encodeWithSelector(IERC20.transfer.selector, address(0xAB...), 100)

[0-3]   a9059cbb                                                         ← selector(4字节)
[4-35]  000000000000000000000000AB8483F64d9C6d1EcF9b849Ae677dD3315835cb2 ← address(32字节)
[36-67] 0000000000000000000000000000000000000000000000000000000000000064  ← uint256 100(32字节)
总共:68 字节

7. abi.encodeWithSignature

说明

  • abi.encodeWithSelector语法糖
  • 第一个参数传入字符串形式的函数签名,自动计算 selector
  • 注意:签名字符串中不能有空格uint256 不可写成 uint

代码示例

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract EncodeWithSignatureDemo {

    // 使用字符串签名编码
    function encodeCall(address to, uint256 amount) 
        public pure returns (bytes memory) 
    {
        // ✅ 正确写法:类型名必须完整
        return abi.encodeWithSignature("transfer(address,uint256)", to, amount);
    }

    // ⚠️ 错误示例(会产生错误的 selector)
    // return abi.encodeWithSignature("transfer(address, uint256)", to, amount);
    //                                                       ^ 不能有空格!

    // 与 encodeWithSelector 对比验证结果相同
    function compareResult(address to, uint256 amount) 
        public pure returns (bool isSame) 
    {
        bytes memory a = abi.encodeWithSignature("transfer(address,uint256)", to, amount);
        bytes4 selector = bytes4(keccak256("transfer(address,uint256)"));
        bytes memory b = abi.encodeWithSelector(selector, to, amount);
        isSame = keccak256(a) == keccak256(b); // true
    }

    // 调用目标合约(动态调用,适合不知道 ABI 的场景)
    function dynamicCall(
        address target,
        string memory funcSig,
        address arg1,
        uint256 arg2
    ) public returns (bool success, bytes memory result) {
        bytes memory data = abi.encodeWithSignature(funcSig, arg1, arg2);
        (success, result) = target.call(data);
    }
}

8. abi.decode

说明

  • 将 ABI 编码的 bytes 数据解码回 Solidity 变量
  • 只能解码由 abi.encode / abi.encodeWithSelector 等生成的数据(有 32 字节对齐)
  • 不能解码 abi.encodePacked 的结果(无填充信息)
  • 语法:abi.decode(data, (Type1, Type2, ...))

代码示例

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract AbiDecodeDemo {

    // 解码静态类型
    function decodeStatic(bytes memory data) 
        public pure returns (uint256 a, address b, bool c) 
    {
        (a, b, c) = abi.decode(data, (uint256, address, bool));
    }

    // 解码动态类型
    function decodeString(bytes memory data) 
        public pure returns (string memory) 
    {
        return abi.decode(data, (string));
    }

    // 编码后立即解码(完整往返示例)
    function roundTrip() public pure returns (uint256, address, bool) {
        uint256 x = 42;
        address a = address(0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2);
        bool flag = true;

        bytes memory encoded = abi.encode(x, a, flag);
        return abi.decode(encoded, (uint256, address, bool));
        // 返回:42, 0xAb8483..., true
    }

    // 解码合约调用的返回值
    function callAndDecode(address token, address owner) 
        public view returns (uint256 balance) 
    {
        (bool success, bytes memory result) = token.staticcall(
            abi.encodeWithSignature("balanceOf(address)", owner)
        );
        require(success, "Call failed");
        balance = abi.decode(result, (uint256));
    }

    // 解码结构体(tuple)
    struct UserInfo {
        address user;
        uint256 amount;
        bool active;
    }

    function encodeStruct(UserInfo memory info) public pure returns (bytes memory) {
        return abi.encode(info.user, info.amount, info.active);
    }

    function decodeStruct(bytes memory data) public pure returns (UserInfo memory info) {
        (info.user, info.amount, info.active) = abi.decode(data, (address, uint256, bool));
    }

    // 解码数组
    function decodeArray(bytes memory data) 
        public pure returns (uint256[] memory arr) 
    {
        arr = abi.decode(data, (uint256[]));
    }
}

9. 函数选择器(Function Selector)

说明

函数选择器是 函数签名字符串的 keccak256 哈希值的前 4 字节,用于 EVM 识别调用哪个函数。

keccak256("transfer(address,uint256)") = 0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
前 4 字节 = 0xa9059cbb  ← 函数选择器

代码示例

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SelectorDemo {

    // 方法一:通过接口/合约的 .selector 属性获取(推荐)
    function getSelector_v1() public pure returns (bytes4) {
        return this.myFunction.selector;
    }

    function myFunction(uint256 x, address y) public pure returns (uint256) {
        return x;
    }

    // 方法二:手动计算
    function getSelector_v2() public pure returns (bytes4) {
        return bytes4(keccak256("myFunction(uint256,address)"));
    }

    // 验证两种方式结果相同
    function verifySelectors() public pure returns (bool) {
        return this.myFunction.selector == bytes4(keccak256("myFunction(uint256,address)"));
        // true
    }

    // 使用 selector 进行函数路由(简版 dispatcher)
    fallback() external {
        bytes4 selector = bytes4(msg.data[:4]);

        if (selector == bytes4(keccak256("foo()"))) {
            // 处理 foo()
        } else if (selector == bytes4(keccak256("bar(uint256)"))) {
            // 处理 bar(uint256)
        }
    }

    // 常见 ERC20 选择器
    function commonSelectors() public pure returns (
        bytes4 transferSel,
        bytes4 approveSel,
        bytes4 balanceOfSel
    ) {
        transferSel  = bytes4(keccak256("transfer(address,uint256)"));  // 0xa9059cbb
        approveSel   = bytes4(keccak256("approve(address,uint256)"));   // 0x095ea7b3
        balanceOfSel = bytes4(keccak256("balanceOf(address)"));         // 0x70a08231
    }
}

10. 低级调用(Low-level Call)

说明

结合 ABI 编码,可以实现对任意合约的低级调用,适用于:

  • 不知道目标合约 ABI 时
  • 调用代理合约或工厂模式
  • 向合约发送 ETH 同时调用函数

代码示例

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// 目标合约
contract Target {
    uint256 public value;

    function setValue(uint256 _value) external {
        value = _value;
    }

    function getValue() external view returns (uint256) {
        return value;
    }

    function add(uint256 a, uint256 b) external pure returns (uint256) {
        return a + b;
    }
}

// 调用方合约
contract Caller {

    // 低级调用:call(状态变更)
    function lowLevelCall(address target, uint256 newValue) 
        public returns (bool success) 
    {
        bytes memory data = abi.encodeWithSignature("setValue(uint256)", newValue);
        (success, ) = target.call(data);
        require(success, "Call failed");
    }

    // 低级只读调用:staticcall(不改状态)
    function lowLevelStaticCall(address target) 
        public view returns (uint256 result) 
    {
        (bool success, bytes memory returnData) = target.staticcall(
            abi.encodeWithSignature("getValue()")
        );
        require(success, "Static call failed");
        result = abi.decode(returnData, (uint256));
    }

    // 获取并解码有返回值的调用
    function callAndDecode(address target, uint256 a, uint256 b) 
        public returns (uint256 sum) 
    {
        (bool success, bytes memory returnData) = target.call(
            abi.encodeWithSelector(
                bytes4(keccak256("add(uint256,uint256)")),
                a,
                b
            )
        );
        require(success, "Call failed");
        sum = abi.decode(returnData, (uint256));
    }

    // 带 ETH 的调用
    function callWithValue(address target) 
        public payable returns (bool success) 
    {
        (success, ) = target.call{value: msg.value}(
            abi.encodeWithSignature("deposit()")
        );
    }

    // delegatecall:在当前合约上下文执行目标合约逻辑(代理模式核心)
    function delegateCall(address logic, uint256 newValue) 
        public returns (bool success) 
    {
        (success, ) = logic.delegatecall(
            abi.encodeWithSignature("setValue(uint256)", newValue)
        );
    }
}

11. 实战综合示例

场景:多签钱包交易编码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MultiSigEncoding {

    struct Transaction {
        address to;
        uint256 value;
        bytes data;
        uint256 nonce;
    }

    // 编码交易用于签名验证
    function encodeTransaction(
        address to,
        uint256 value,
        bytes memory data,
        uint256 nonce
    ) public pure returns (bytes32 txHash) {
        bytes memory encoded = abi.encode(to, value, data, nonce);
        txHash = keccak256(encoded);
    }

    // 编码带链 ID 的交易(防重放攻击)
    function encodeTransactionWithChainId(
        address to,
        uint256 value,
        bytes memory data,
        uint256 nonce,
        uint256 chainId,
        address contractAddress
    ) public pure returns (bytes32 txHash) {
        txHash = keccak256(
            abi.encode(chainId, contractAddress, to, value, data, nonce)
        );
    }
}

场景:链上元数据存储与解析

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MetadataStorage {

    mapping(uint256 => bytes) private tokenData;

    struct TokenMetadata {
        string name;
        string description;
        uint256 rarity;
        address creator;
    }

    // 存储:编码结构体到 bytes
    function setMetadata(uint256 tokenId, TokenMetadata memory meta) external {
        tokenData[tokenId] = abi.encode(
            meta.name,
            meta.description,
            meta.rarity,
            meta.creator
        );
    }

    // 读取:解码 bytes 回结构体
    function getMetadata(uint256 tokenId) 
        external view returns (TokenMetadata memory meta) 
    {
        bytes memory data = tokenData[tokenId];
        (meta.name, meta.description, meta.rarity, meta.creator) =
            abi.decode(data, (string, string, uint256, address));
    }
}

场景:跨合约消息传递

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MessageBus {

    event MessageSent(address indexed from, address indexed to, bytes payload);

    // 发送方:编码消息
    function sendMessage(
        address targetContract,
        address recipient,
        uint256 amount,
        string memory memo
    ) external {
        bytes memory payload = abi.encode(recipient, amount, memo);
        emit MessageSent(msg.sender, targetContract, payload);

        // 发送到目标合约
        (bool success, ) = targetContract.call(
            abi.encodeWithSignature("receiveMessage(bytes)", payload)
        );
        require(success, "Message delivery failed");
    }
}

contract MessageReceiver {

    event MessageReceived(address recipient, uint256 amount, string memo);

    // 接收方:解码消息
    function receiveMessage(bytes calldata payload) external {
        (address recipient, uint256 amount, string memory memo) =
            abi.decode(payload, (address, uint256, string));

        emit MessageReceived(recipient, amount, memo);
        // 处理业务逻辑...
    }
}

12. 安全注意事项

⚠️ 1. abi.encodePacked 哈希碰撞

// 危险:动态类型用 encodePacked 可能碰撞
function unsafeHash(string memory a, string memory b) public pure returns (bytes32) {
    return keccak256(abi.encodePacked(a, b));
    // "aaa","bbb" 和 "aa","abbb" 哈希相同!
}

// 安全:用 abi.encode 或固定类型
function safeHash(string memory a, string memory b) public pure returns (bytes32) {
    return keccak256(abi.encode(a, b)); // ✅ 包含长度信息,无碰撞
}

// 或者用固定大小类型(无碰撞风险)
function safeHashFixed(bytes32 a, bytes32 b) public pure returns (bytes32) {
    return keccak256(abi.encodePacked(a, b)); // ✅ 固定大小,安全
}

⚠️ 2. encodeWithSignature 签名字符串错误

// ❌ 错误:有空格、类型缩写
abi.encodeWithSignature("transfer(address, uint256)", to, amount); // 空格导致 selector 错误
abi.encodeWithSignature("transfer(address,uint)", to, amount);     // uint 应为 uint256

// ✅ 正确:无空格、完整类型名
abi.encodeWithSignature("transfer(address,uint256)", to, amount);

⚠️ 3. 低级调用不检查返回值

// ❌ 危险:忽略失败
target.call(abi.encodeWithSignature("foo()"));

// ✅ 安全:检查 success
(bool success, bytes memory data) = target.call(abi.encodeWithSignature("foo()"));
require(success, "Call failed");

⚠️ 4. 解码类型不匹配

bytes memory data = abi.encode(uint256(1));

// ❌ 类型不匹配会 revert
uint8 x = abi.decode(data, (uint8)); // revert!

// ✅ 类型必须完全匹配
uint256 x = abi.decode(data, (uint256)); // OK

13. 对比总结

函数32字节对齐含选择器可 decode主要用途
abi.encode(...)✅ 是❌ 否✅ 是标准编码、参数序列化、存储
abi.encodePacked(...)❌ 否(紧凑)❌ 否❌ 否keccak256 哈希、字符串拼接
abi.encodeWithSelector(sel, ...)✅ 是✅ 是(4字节)✅ 是(跳过前4字节)构造低级 call calldata
abi.encodeWithSignature(sig, ...)✅ 是✅ 是(4字节)✅ 是(跳过前4字节)同上,语法更直观
abi.decode(data, (...))解码 ABI 编码的字节流

选择指南

需要标准序列化/跨合约传递数据?         → abi.encode
需要计算哈希 + 固定类型参数?           → abi.encodePacked(固定类型安全)
需要计算哈希 + 动态类型参数?           → abi.encode(避免碰撞)
需要构造 call 的 calldata?            → abi.encodeWithSelector / abi.encodeWithSignature
需要解码返回的字节数据?               → abi.decode
需要拼接字符串?                       → string(abi.encodePacked(a, b))

参考资料