Solidity高级编程——深入学习ABI

Solidity高级编程——深入学习ABI

第一部分: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

02

Event 编码

编码事件日志的参数,用于日志记录和过滤。

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 的理论知识和实际应用技巧,为智能合约开发和逆向解析提供坚实的基础。

相关推荐

米哈游游戏测试一面凉经3.18
365bet体育足球世界

米哈游游戏测试一面凉经3.18

📅 07-01 👁️ 7575
卷轴各部位的名称叫什么呢?杨弘岳山水画
beta365官网app下载

卷轴各部位的名称叫什么呢?杨弘岳山水画

📅 07-05 👁️ 7170
为什么喜欢郭嘉的原因(郭嘉为什么人气那么高呢)
约彩365彩票官方app下载安卓

为什么喜欢郭嘉的原因(郭嘉为什么人气那么高呢)

📅 07-23 👁️ 9862
指猴为何以“指”为名,一文探究其背后的秘密!
beta365官网app下载

指猴为何以“指”为名,一文探究其背后的秘密!

📅 07-21 👁️ 2203