Solidity ABI 编码与解码详解
目录
- 什么是 ABI 编码
- 编码基础原则:32 字节对齐
- 静态类型 vs 动态类型
- abi.encode
- abi.encodePacked
- abi.encodeWithSelector
- abi.encodeWithSignature
- abi.decode
- 函数选择器(Function Selector)
- 低级调用(Low-level Call)
- 实战综合示例
- 安全注意事项
- 对比总结
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 字节时,左侧补零(
uint、address、bool等) - 字节类型不足 32 字节时,右侧补零(
bytes1~bytes32) - 动态类型(
bytes、string、动态数组)通过”偏移量 + 长度 + 数据”三段式存储
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))