文章目錄
- 一、ABI 基礎概念
- 1. ABI 與 API 的區別
- 2. ABI 的核心作用
- 二、ABI 接口描述
- 1. 編譯后的產物
- 2. ABI JSON 格式示例
- 3. ABI JSON 關鍵字段說明
- 三、ABI 編碼
- 1. 編碼示例
- 2. 編碼數據的組成
- 3. Solidity 中的編碼函數
- 四、ABI 解碼
- 1. 解碼的基本概念
- 2. 事件日志的解碼
- 五、ABI 編解碼可視化工具
一、ABI 基礎概念
1. ABI 與 API 的區別
- API(應用程序接口):是兩個軟件之間進行通信的橋梁,用于訪問某個服務。
- ABI(應用二進制接口):定義了智能合約中可交互的方法、事件和錯誤,是與 EVM(以太坊虛擬機)交互的橋梁。
2. ABI 的核心作用
EVM 只能識別和運行由 0 和 1 組成的二進制數據,因此在調用函數時,需要借助 ABI 將人類可讀的函數轉化為 EVM 可讀的字節碼。從本質上說,ABI 是編碼和解碼規范,用于規范外部與 EVM 的交互,也可用于合約間的交互。
二、ABI 接口描述
1. 編譯后的產物
在 Solidity 中編譯代碼后,會得到兩個重要的 artifact
(產物):
- bytecode(字節碼):合約部署到區塊鏈上的實際代碼。
- ABI 接口描述:是 JSON 格式的文件,定義了智能合約中外部可交互的方法、事件和可解釋的錯誤。
2. ABI JSON 格式示例
以 Counter 合約為例,其編譯后生成的 ABI 是一個 JSON 格式的數組,每個對象定義了合約中可公開調用的方法(函數)、聲明的事件及錯誤等。以下是 Counter 合約及其對應的 ABI 示例:
contract Counter {uint public counter;address private owner; error NotOwner(); event Set(uint _value); constructor() {owner = msg.sender;} function set(uint x) public { if(owner != msg.sender) revert NotOwner(); counter = x;emit Set(x); }
}
對應的 ABI 如下:
[{"inputs": [], "name": "NotOwner", "type": "error"},{"anonymous": false, "inputs": [{ "indexed": false, "internalType": "uint256", "name": "_value", "type": "uint256"}],"name": "Set", "type": "event"},{"inputs": [], "name": "counter", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},{"inputs": [{ "internalType": "uint256", "name": "x", "type": "uint256"}],"name": "set", "outputs": [], "stateMutability": "nonpayable", "type": "function"}
]
3. ABI JSON 關鍵字段說明
- type:定義是函數、事件或錯誤等。
- name:表示函數名稱、事件名稱、自定義錯誤名稱。
- inputs:函數輸入參數。
- outputs:函數輸出參數。
三、ABI 編碼
1. 編碼示例
以調用部署在 sepolia 網絡上的 Counter 合約的 set()
函數,并傳入參數 10 為例,經過 ABI 編碼后提交到鏈上的數據是 0x60fe47b1000000000000000000000000000000000000000000000000000000000000000a
。
2. 編碼數據的組成
該編碼數據包含兩個部分:
- 函數選擇器(前 4 個字節):
0x60fe47b1
,它是 ABI 描述中函數的簽名set(uint256)
進行 keccak256 哈希運算后取前 4 個字節,即bytes4(keccak256("set(uint256)")) == 0x60fe47b1
。 - 參數編碼:參數 10 的十六進制是
a
,然后擴展到 32 個字節。
3. Solidity 中的編碼函數
Solidity 中有 5 個用于編碼的函數:
- abi.encode:按 EVM 標準規則對參數編碼,每個參數按 32 個字節填充 0 后再拼在一起,用于與合約交互時編碼參數。
- abi.encodePacked:緊密編碼,參數編碼拼接時不填充 0,使用實際占用空間拼接,若結果不是 32 字節整數倍數,在末尾填充 0,例如在使用 EIP712 時需要用到。
- abi.encodeWithSignature:對函數簽名及參數進行編碼,第一個參數是函數簽名,后面按 EVM 標準規則對參數編碼,可直接獲得調用函數所需的 ABI 編碼數據。
- abi.encodeWithSelector:與
abi.encodeWithSignature
功能類似,第一個參數為 4 個字節的函數選擇器。 - abi.encodeCall:通過函數指針對函數及參數編碼,執行編碼時進行完整的類型檢查,確保類型匹配函數簽名。
四、ABI 解碼
1. 解碼的基本概念
解碼是編碼的“逆過程”。區塊鏈瀏覽器能將提交給鏈上的 0x60fe47b10000000...0a
顯示為函數 set(uint256 x)
,就是對數據進行了解碼。需要注意的是,僅能對參數進行解碼,函數選擇器因使用了 keccak256 哈希運算(哈希不可逆)無法直接解碼,但開源合約代碼后,區塊鏈瀏覽器可計算出所有函數的函數選擇器,從而通過函數選擇器匹配對應的函數簽名。
2. 事件日志的解碼
ABI 解碼的一個重要場景是解析交易中的事件日志。日志包含 Topics 和 Data 兩部分,其中 Topics 的第一個主題是事件簽名的 Keccak256 哈希。通過匹配該哈希值,可知道 EVM 產生的日志由哪個事件生成,進而根據事件的參數列表解析日志數據。Solidity、web3.js、ethers.js 庫都提供了解碼函數。
五、ABI 編解碼可視化工具
ChainToolDAO 開發了幾個可視化工具,可幫助進行編解碼:
- 函數選擇器的查詢及反查:https://chaintool.tech/querySelector
- 事件簽名的 Topic 查詢:https://chaintool.tech/topicID
- Hash 工具(提供 Keccak-256 及 Base64):https://chaintool.tech/hashTool
- 交易數據(calldata)的編碼與解碼:https://chaintool.tech/calldata