第一部分:ABI 概述
1. ABI 定义与重要性
什么是 ABI?
- ABI (Application Binary Interface) 是智能合约与外部世界(包括其他智能合约和用户)之间的接口。它定义了合约的函数和事件,使得不同语言编写的代码可以相互通信。
特点
- 标准化接口: ABI 提供了一种标准化的方式来描述智能合约的接口,包括函数和事件的规范,使得不同的工具和库能够一致地与智能合约进行交互。
- 描述性: ABI 包含了合约中每个函数的名称、输入参数、输出参数及其类型,以及事件的名称和参数类型。通过这些描述,可以清晰地知道如何调用合约中的函数和如何解析事件。
- 静态和动态类型支持: ABI 支持以太坊的静态类型和动态类型,包括
uint256、address、string、bytes等。ABI 使得这些类型的编码和解码变得标准化和自动化。 - 事件日志解析: ABI 定义了事件的格式,使得开发者可以轻松解析区块链上的事件日志。事件日志是以太坊中合约与外界通讯的一种重要机制,通过 ABI 可以准确解析这些日志。
- 自动生成: 当编译 Solidity 合约时,会自动生成对应的 ABI 文件。开发者无需手动编写 ABI,这减少了出错的可能性并提高了开发效率。
- 工具支持广泛: 很多以太坊开发工具(如 Remix、Truffle、Hardhat 等)和库(如 Web3.js、Ethers.js 、viem.sh等)都对 ABI 提供了良好的支持,使得与智能合约的交互更加便捷。
- 增强安全性: ABI 通过明确函数和参数的类型,减少了由于类型错误而导致的安全问题。开发者可以更明确地知道每个函数需要的输入和输出是什么。
Web3 ABI与 Web2 API的对比
特点 Web2 API Web3 ABI 用途 服务器与客户端通信 DApp 与智能合约通信 定义内容 端点、HTTP 方法、请求/响应格式 函数名、参数类型、事件定义 通信协议 HTTP/HTTPS 以太坊 JSON-RPC 抽象层 服务器功能 智能合约功能 依赖 RESTful、GraphQL 等规范 以太坊智能合约规范 工具支持 Postman、Swagger 等 Web3.js、Ethers.js、viem.sh 等 Web2 API 示例
一个简单的 RESTful API 端点可能如下所示:
端点:
/api/users方法:
GET描述:获取用户列表
响应:
[ { "id": 1, "name": "Alice" }, { "id": 2, "name": "Bob" } ]一个简单的智能合约 ABI 定义如下:
[ { "constant": true, "inputs": [], "name": "getUsers", "outputs": [ { "name": "", "type": "address[]" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [ { "name": "name", "type": "string" } ], "name": "addUser", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" } ]为什么 ABI 在智能合约开发中至关重要?
- ABI 是调用智能合约函数和监听事件的必要条件。
- ABI 提供了合约方法和事件的精确定义,确保数据的正确编码和解码。
2. ABI 的构成元素
- 数据类型(基本类型和复杂类型)
- 基本类型:uint、int、bool、address、bytes、string 等。
- 复杂类型:数组、结构体(struct)。
- 函数定义
- 函数签名:包括函数名称和参数类型。
- 返回值类型:函数返回的数据类型。
- 事件定义
- 事件签名:包括事件名称和参数类型。
- 事件索引:事件参数的索引,用于过滤事件日志。
- 构造函数和析构函数
- 构造函数:初始化合约的特殊函数。
- 析构函数:目前 Solidity 不支持析构函数。
3. 生成 ABI 的方式
由 Solidity 编译器生成
https://docs.soliditylang.org/zh/latest/installing-solidity.html
Mac 下安装编译器,使用命令
solc --abi Contract.sol生成 ABI 文件。brew tap ethereum/ethereum brew install solidity solc --abi --pretty-json Counter.sol用 Foundry 调用 solc 编译
使用 Foundry 工具,通过命令
forge build自动生成 ABI 文件forge build Counter.sol此时,在 project/out/Counter.sol/Counter.json 中包含ABI
用 Hardhat 调用 solc 编译
使用 Hardhat 工具,通过命令
npx hardhat compile生成 ABI 文件。npm install --save-dev hardhat npx hardhat npx hardhat compile编译后的合约和 ABI 会存储在
artifacts/contracts目录下,每个合约对应一个 JSON 文件,里面包含 ABI。Remix编译
基于浏览器的 Solidity 开发环境,可以在线编写、编译和部署合约。在 Remix 编辑器中编写你的合约,例如
MyContract.sol,点击 Remix IDE 中的编译按钮,编译合约,在 Remix 中编译合约后,点击 “Details” 按钮,展开后会看到 ABI,点击复制即可。
第二部分:ABI 编码与解码
1. ABI 编码规则
- 固定大小的数据类型编码
- 例如:uint256, address, bool 等。
- 编码方式:每个数据类型按照固定的字节数进行编码,例如 uint256 占用 32 字节。
- 动态大小的数据类型编码
- 例如:string, bytes, 数组等。
- 编码方式:数据的实际内容和长度信息分开存储,长度信息占用 32 字节,实际内容紧随其后。
- 函数选择器编码
- 函数选择器是函数签名的前 4 个字节的哈希值,用于识别函数调用。
2. ABI 解码规则
- 固定大小的数据类型解码
- 解析固定大小的数据类型时,直接从固定位置读取数据。
- 动态大小的数据类型解码
- 解析动态大小的数据类型时,先读取长度信息,再读取实际内容。
3. ABI 语义
function
函数描述是一个带有字段的JSON对象:
• type: function, constructor, receive 或者 fallback ;
• name: 函数名称;
• inputs: 函数入参,是一个数组对象,每个数组对象会包含:
◦ name: 参数名称;
◦ type: 参数类型
◦ components: 供元组(tuple) 类型使用;
• outputs:函数返回值,是一个类似于 inputs 的数组对象。
• stateMutability: 为下列值之一: pure , view , nonpayable 和 payable 。
{ "type": "function" "name": "setValue", "inputs": [ { "internalType": "uint256", "name": "_value", "type": "uint256" } ], "outputs": [], "stateMutability": "nonpayable", }event
- 事件的名称和参数类型定义了事件的接口,用于日志记录,描述也是一个带有字段的 JSON对象:
• type:总是 event;
• name: 事件名称;
• inputs: 事件输出的参数信息,是一个数组对象,每个数组对象会包含:
◦ name: 参数名称。
◦ type: 参数类型。
◦ components: 供元组(tuple) 类型使用;
◦ indexed: 如果字段是日志主题(event topic)的一部分,则为 true;如果它是日志数据段的一部分,则为 false。
• anonymous: 如果事件被声明为 anonymous,则为 true。如果事件被声明为 anonymous,那么 topics[0] 不会被生成。
{ "type": "event", "name": "Transfer", "inputs": [ { "name": "from", "type": "address", "indexed": true }, { "name": "to", "type": "address", "indexed": true }, { "name": "value", "type": "uint256", "indexed": false } ], "anonymous": false }error
- 错误的名称和参数类型定义了错误的接口,用于异常处理。Error 对象的描述:
• type:总是 error;
• name: Error名称;
• inputs: Error 参数信息,是一个数组对象,每个数组对象会包含:
◦ name: 参数名称。
◦ type: 参数类型。
◦ components: 供元组(tuple) 类型使用;
{ "type": "error", "name": "CustomError", "inputs": [ { "name": "errorCode", "type": "uint256" }, { "name": "errorMessage", "type": "string" } ] }
4. ABI 编码
method selector
函数签名的前 4 个字节哈希值,用于识别函数调用。
在以太坊智能合约中,函数调用通过前四个字节来指定具体的函数。这四个字节是函数签名的 Keccak-256 哈希值的前四个字节。函数签名由函数名和括号中的参数类型列表组成,参数类型列表之间用逗号分隔,不包含参数名称、空格,返回值类型和修饰
符。函数ID = hash(函数签名值) 的前4 字节
cast keccak 'setNumber(uint256)' //0x3fb5c1cb9d57cc981b075ac270f9215e697bc33dacd5ce87319656ebf8fc7b92 cast sig "setNumber(uint256)" // 0x3fb5c1cb //Input Data = MethodID + abi.encode(args…) //transferFrom(address,address,uint256) //复杂结构体参数 hashStruct(((string,address),(string,address),string))abi.encode
- 编码函数参数或事件参数为 ABI 格式。


cast abi-encode "bar(uint256 a,uint8 b,bool c,address d,int256 e)" 9 8 true 0x605E0971f416301CF81Cf83C580123DCB6A8277E -2 参数编码如下: 0x000000000000000000000000000000000000000000000000000000000000000900000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000 001000000000000000000000000605e0971f416301cf81cf83c580123dcb6a8277efffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe cast abi-encode "bar(string)" "hi" 参数编码如下: 0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000026869000000000000000000000000000000000000000000000000000000000 000 cast abi-encode "bar(bytes)" 参数编码如下: 0x605e0971f416301cf81cf83c580123dcb6a8277efffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe 0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000034605e0971f416301cf81cf83c580123dcb6a8277effffffffffffffffffffffffffffffff fffffffffffffffffffffffffffffffe000000000000000000000000 cast abi-encode "bar((address,uint256),bool)" "(0x605E0971f416301CF81Cf83C580123DCB6A8277E,8)" true 参数编码如下: 0x000000000000000000000000605e0971f416301cf81cf83c580123dcb6a8277e000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000 01 cast abi-encode "bar((address,uint32)[])" "[(0x605E0971f416301CF81Cf83C580123DCB6A8277E,1),(0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326,2)]" 参数编码如下: 0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000605e0971f416301cf81cf83c580123dcb6a827 7e00000000000000000000000000000000000000000000000000000000000000010000000000000000000000001f9090aae28b8a3dceadf281b0f12828e676c32600000000000000000000000000000000000000000000000000000000000000 02Event 编码
- 编码事件日志的参数,用于日志记录和过滤。

5.demo



ABI数据库 https://openchain.xyz/,可以查询比对。
实际操作
使用 ethers.js 进行 ABI 编码与解码
const { ethers } = require('ethers'); // 编码函数调用数据 const abi = ["function transfer(address to, uint amount)"]; const iface = new ethers.utils.Interface(abi); const data = iface.encodeFunctionData("transfer", ["0xaddress", 1000]); // 解码函数返回数据 const decoded = iface.decodeFunctionResult("transfer", data); console.log(decoded);使用 web3.js 进行 ABI 编码与解码
const Web3 = require('web3'); const web3 = new Web3(); // 编码函数调用数据 const data = web3.eth.abi.encodeFunctionCall({ name: 'transfer', type: 'function', inputs: [{ type: 'address', name: 'to' },{ type: 'uint256', name: 'value' }] }, ['0xaddress', '1000']); // 解码函数返回数据 const decoded = web3.eth.abi.decodeParameters(['bool'], '0xdata'); console.log(decoded);使用 viem.sh 进行 ABI 编码与解码
import { encodeFunctionData, decodeFunctionResult } from 'viem'; // 编码函数调用数据 const data = encodeFunctionData({ name: 'transfer', type: 'function', inputs: [{ type: 'address', name: 'to' }, { type: 'uint256', name: 'value' }] }, ['0xaddress', '1000']); // 解码函数返回数据 const decoded = decodeFunctionResult({ name: 'transfer', type: 'function', outputs: [{ type: 'bool' }] }, '0xdata'); console.log(decoded);
第三部分:函数调用与事件监听
1. 函数调用
合约方法的 ABI 格式 合约方法的 ABI 格式是一个 JSON 对象,描述了函数的名称、参数类型和返回值类型。
函数名称、参数类型、返回值类型的定义
function transfer(address to, uint256 value) public returns (bool)生成合约方法的调用数据 使用编码规则生成调用数据。可以使用 ethers.js 库来实现。
const { ethers } = require("ethers"); // 函数 ABI const abi = [ "function transfer(address to, uint256 value) public returns (bool)" ]; // 创建接口 const iface = new ethers.utils.Interface(abi); // 生成调用数据 const data = iface.encodeFunctionData("transfer", ["0xRecipientAddress", ethers.utils.parseUnits("1.0", 18)]); console.log(data);解析合约方法的调用结果 使用解码规则解析调用结果。可以使用 ethers.js 库来实现。
// 假设 `result` 是从区块链获得的调用结果 const result = "0x"; // 示例数据 // 解码调用结果 const decodedResult = iface.decodeFunctionResult("transfer", result); console.log(decodedResult);2. 事件监听
事件的 ABI 格式 事件的 ABI 格式是一个 JSON 对象,包含事件名称和参数类型。
{ "type": "event", "name": "Transfer", "inputs": [ { "name": "from", "type": "address", "indexed": true }, { "name": "to", "type": "address", "indexed": true }, { "name": "value", "type": "uint256", "indexed": false } ], "anonymous": false }事件名称、参数类型、索引参数的定义
event Transfer(address indexed from, address indexed to, uint256 value);创建事件过滤器 根据事件的 ABI 和过滤条件创建过滤器。可以使用 ethers.js 库来实现。
const { ethers } = require("ethers"); // 连接到以太坊节点 const provider = new ethers.providers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID"); // 创建合约实例 const contractAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; // USDC 合约地址 const abi = [ "event Transfer(address indexed from, address indexed to, uint256 value)" ]; const contract = new ethers.Contract(contractAddress, abi, provider); // 创建过滤器 const filter = contract.filters.Transfer(); // 监听事件 provider.on(filter, (log) => { const parsedLog = contract.interface.parseLog(log); console.log(`从 ${parsedLog.args.from} 转账给 ${parsedLog.args.to} ${ethers.utils.formatUnits(parsedLog.args.value, 6)} USDC`); });解析事件日志 使用解码规则解析事件日志。可以使用 ethers.js 库来实现。
// 假设 `log` 是从区块链获得的事件日志 const log = { // 示例日志数据 data: "0x", topics: [ // 示例主题数据 ] }; // 解析事件日志 const parsedLog = contract.interface.parseLog(log); console.log(parsedLog.args);这个示例代码展示了如何使用事件 ABI 格式创建事件过滤器,并监听和解析 USDC 转账事件。通过这种方式,你可以实时捕获和处理区块链上的特定事件。
第四部分:实战应用(后续补充)
1. ABI 反编译
- 使用公共工具逆向解析 ABI
- 使用 etherscan 等工具查看合约 ABI。
- 手动解析交易数据的意图
- 根据已知的 ABI 格式手动解析交易数据。
2. 高级应用
- 动态生成和管理 ABI
- 根据需要动态生成 ABI,并进行管理。
- 多合约交互中的 ABI 使用技巧
- 在多合约交互中有效使用 ABI 进行调用和监听。
3. 实际案例
- 利用Cast工具逆向解码交易数据
题目:当合约部署者没有上传合约源代码时,我们是否能逆向分析合约的方法信息呢?通过学习ABI相关知识,你可以结合公共数据来尝试逆向解析出一笔交易的执行意图!请解析这笔交易数据所表达的意图0xa9059cbb0000000000000000000000005494befe3ce72a2ca0001fe0ed0c55b42f8c358f000000000000000000000000000000000000000000000000000000000836d54c
1、这笔交易数据对应的合约方法是什么?
前 4 个字节是函数选择器。函数选择器是函数签名的 Keccak-256 哈希的前 4 个字节。我们先来获取前 4 个字节:0xa9059cbb,经过查询https://www.4byte.directory/ 这段编码是一个标准的 ERC-20 transfer 函数的编码调用。ERC-20 transfer 函数的签名为 :transfer(address,uint256)。
2、这笔交易对应的方法调用的第一个参数值是多少?
第一个参数是(地址,32字节)address:0000000000000000000000005494befe3ce72a2ca0001fe0ed0c55b42f8c358f所以地址是0x5494befe3ce72a2ca0001fe0ed0c55b42f8c358f,经过https://web3-tools.netlify.app/的checkAddressChecksum ,地址大小写转换为:0x5494befe3CE72A2CA0001fE0Ed0C55B42F8c358f
3、这笔交易对应的方法调用的第二个参数值是多少?
最后 32 字节是无符号整数参数,经过https://tool.oschina.net/hexconvert ,在线转换为十进制 
使用 Viem 查询 USDC 最近100个区块内的转账记录
详细内容可以查看:https://learnblockchain.cn/article/8758
第五部分:常见问题与解决方案
1. 常见错误及其排查
常见编码和解码错误
数据类型不匹配导致的编码/解码错误
当函数调用或事件监听时,数据类型的不匹配可能会导致编码或解码错误。例如,将
uint256类型的数据编码为uint8会导致错误。应确保输入数据类型与 ABI 定义中的类型一致。const { ethers } = require("ethers"); // 示例:调用 transfer 函数,传入的参数类型不匹配 const abi = [ "function transfer(address to, uint256 value) public returns (bool)" ]; const iface = new ethers.utils.Interface(abi); try { // 错误:value 应该是 uint256 类型,而不是 uint8 const data = iface.encodeFunctionData("transfer", ["0xRecipientAddress", 255]); console.log(data); } catch (error) { console.error("编码错误:", error.message); }函数选择器错误导致的调用失败
函数选择器错误会导致调用失败。函数选择器是由函数签名生成的 4 字节哈希值。确保函数签名正确。
const { ethers } = require("ethers"); const abi = [ "function transfer(address to, uint256 value) public returns (bool)" ]; const iface = new ethers.utils.Interface(abi); try { const data = iface.encodeFunctionData("transferr", ["0xRecipientAddress", ethers.utils.parseUnits("1.0", 18)]); console.log(data); } catch (error) { console.error("函数选择器错误:", error.message); }事件监听中的常见问题
事件过滤器配置错误
事件过滤器配置错误会导致无法正确监听事件。例如,使用错误的地址或主题配置。
const { ethers } = require("ethers"); const provider = new ethers.providers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID"); const abi = [ "event Transfer(address indexed from, address indexed to, uint256 value)" ]; const contract = new ethers.Contract("0xIncorrectAddress", abi, provider); // 错误的地址 const filter = contract.filters.Transfer(); provider.on(filter, (log) => { console.log("监听到事件:", log); });事件日志解析错误
事件日志解析错误可能由于 ABI 定义不正确或日志格式不匹配导致。
const { ethers } = require("ethers"); const abi = [ "event Transfer(address indexed from, address indexed to, uint256 value)" ]; const iface = new ethers.utils.Interface(abi); const log = { data: "0xIncorrectData", // 错误的日志数据 topics: [ "0xIncorrectTopic" ] }; try { const parsedLog = iface.parseLog(log); console.log(parsedLog.args); } catch (error) { console.error("事件日志解析错误:", error.message); }
2. 最佳实践
编写和管理 ABI 的最佳实践
规范化 ABI 定义
编写清晰、规范的 ABI 定义,包含函数、事件和错误的详细描述。
[ { "type": "function", "name": "transfer", "inputs": [ { "name": "to", "type": "address" }, { "name": "value", "type": "uint256" } ], "outputs": [ { "name": "", "type": "bool" } ], "stateMutability": "nonpayable" }, { "type": "event", "name": "Transfer", "inputs": [ { "name": "from", "type": "address", "indexed": true }, { "name": "to", "type": "address", "indexed": true }, { "name": "value", "type": "uint256", "indexed": false } ], "anonymous": false } ]版本控制和管理 ABI 文件
使用版本控制工具(如 Git)管理 ABI 文件,确保不同版本的 ABI 文件能够被追踪和管理。
# 将 ABI 文件提交到 Git 仓库 git add path/to/abi.json git commit -m "添加合约 ABI 文件" git push origin main安全考虑
确保 ABI 定义的函数和事件符合安全规范
在编写 ABI 定义时,确保函数和事件的定义符合安全规范,避免潜在的安全漏洞。
pragma solidity ^0.8.0; contract SafeContract { event Transfer(address indexed from, address indexed to, uint256 value); function transfer(address to, uint256 value) public returns (bool) { require(to != address(0), "无效的接收地址"); // 其他安全检查 return true; } }避免未授权的合约调用和事件监听
确保合约的函数和事件只能被授权的地址调用和监听,避免恶意行为。
pragma solidity ^0.8.0; contract AuthorizedContract { address public owner; modifier onlyOwner() { require(msg.sender == owner, "未授权的调用"); _; } event Transfer(address indexed from, address indexed to, uint256 value); constructor() { owner = msg.sender; } function transfer(address to, uint256 value) public onlyOwner returns (bool) { require(to != address(0), "无效的接收地址"); // 其他安全检查 return true; } }
通过遵循这些最佳实践,可以有效地编写和管理 ABI,并确保合约调用和事件监听的安全性。
附录
1. 参考资料
- 官方文档和标准
- Ethereum 官方文档
- Solidity 官方文档
- 开源库和工具
- ethers.js
- web3.js
- viem
- 在线学习资源和社区
- Ethereum Stack Exchange
- Solidity Gitter
2. 实用工具
- 在线 ABI 编码/解码工具
- Abi.hashex.org
- Etherscan ABI 编码工具
- 开源项目和代码示例
- OpenZeppelin Contracts
- Hardhat 示例项目
通过这个详细的大纲和内容补充,学习者可以逐步掌握 ABI 的理论知识和实际应用技巧,为智能合约开发和逆向解析提供坚实的基础。