ABI(ApplicationBinaryInterface)是智能合约与外部世界(包括其他智能合约和用户)之间的接口。它定义了合约的函数和事件,使得不同语言编写的代码可以相互通信。
什么是 ABI?
特点
uint256
、address
、string
、bytes
等。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 等 |
一个简单的 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 在智能合约开发中至关重要?
由 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,点击复制即可。
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
• 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
• type:总是 error;
• name: Error名称;
• inputs: Error 参数信息,是一个数组对象,每个数组对象会包含:
◦ name: 参数名称。
◦ type: 参数类型。
◦ components: 供元组(tuple) 类型使用;
{
"type": "error",
"name": "CustomError",
"inputs": [
{
"name": "errorCode",
"type": "uint256"
},
{
"name": "errorMessage",
"type": "string"
}
]
}
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
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
02
Event 编码
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);
合约方法的 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);
事件的 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 转账事件。通过这种方式,你可以实时捕获和处理区块链上的特定事件。
题目:当合约部署者没有上传合约源代码时,我们是否能逆向分析合约的方法信息呢?通过学习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
常见编码和解码错误
数据类型不匹配导致的编码/解码错误
当函数调用或事件监听时,数据类型的不匹配可能会导致编码或解码错误。例如,将 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);
}
编写和管理 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,并确保合约调用和事件监听的安全性。
通过这个详细的大纲和内容补充,学习者可以逐步掌握 ABI 的理论知识和实际应用技巧,为智能合约开发和逆向解析提供坚实的基础。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!