# EVM Transaction Parsing

By [ShaoN](https://paragraph.com/@shaon) · 2024-03-03

---

**#Web3**

持续更新 …

* * *

在做一些 Web3 产品的时候，产品中往往需要对链上交易进行解析，这样可以让用户有更好的可读性以及产品易用性，这对于打磨产品也是非常重要的一个环节，比如钱包类产品、DAPP 等。

在主流的公链生态中，EVM 都居在核心且重要的位置上，所以很多产品都是从 Ethereum, Binance Smart Chain, Polygon, Optimism, Arbitrium One 等链开始起步进行构建产品。然而在实现交易解析的过程中，会遇到比较多的问题

### 快速找到目标协议的相关交易

在开发过程中不可避免的要进行单元测试、集成测试，全面的测试 Case 是必要的；比如 `0x Protocol`，它的内部实现非常多样，支持 `uniswapv2`、`uniswapv3`、`pancake`、`otc order` 、`native order` 、`liquidity provider` 等，而且每个实现中又包含不同的 function，要想在正式上生产之前把这些 Case 全部都测试到，就需要所有这些 function 的链上交易，使用这些真实的交易进行测试，准确度才会更高；所以，如果遇到协议下的某个 function 无法在浏览器的 Transactions 列表找到 (比如这个 [0x Protocol Transactions](https://etherscan.io/txs?a=0xdef1c0ded9bec7f1a1670819833240f027b25eff)，列表中的交易数据非常多)，可以用下面的办法

1.  找到目标 Function 的 ABI，一般情况下很容易在区块浏览器的已认证合约下面找到；如果找不到，就需要找到项目的合约源码，从代码中找到方法签名
    
2.  使用签名函数对步骤 1 的方法进行签名
    
    1.  在线工具：[ABI Hashex](https://abi.hashex.org/)，[EVM Function Selector](https://www.evm-function-selector.click/)
        
    2.  命令行工具：[web3contract](https://github.com/onchainsig/web3contract)
        
        1.  按照 Readme 安装 Node.js 等
            
        2.  执行 npx hardhat selector ““, 比如 npx hardhat selector “transfer(address,uint256)“
            
    
    *   得到 Function Selector 后，最好在 [bytes4 database](https://www.4byte.directory/) 中检索一下，确保方法签名存在，以及是我们想要的，有的时候会搞错
        
    *   在区块浏览器的高级筛选下对 Function Selector 做筛选，然后根据列表中的 to 可以甄别是不是我们的目标合约以及想要的交易
        
        1.  Advanced Filter： [https://etherscan.io/advanced-filter?mtd=0x1baaa00b](https://etherscan.io/advanced-filter?mtd=0x1baaa00b) ，可以把 mtd=[0x1baaa00b](https://etherscan.io/advanced-filter?mtd=0x1baaa00b) 替换成自己的 Function Selector
            
3.  关于 Function Signature
    
    大部分情况下，我们可以很轻松的获取到 Function Signature，但是有些复杂的参数会导致 Function Signature 没有那么容易获取到，比如
    
        library LibNativeOrder {
            struct LimitOrder {
                IERC20Token makerToken;
                IERC20Token takerToken;
                uint128 makerAmount;
                uint128 takerAmount;
                uint128 takerTokenFeeAmount;
                address maker;
                address taker;
                address sender;
                address feeRecipient;
                bytes32 pool;
                uint64 expiry;
                uint256 salt;
            }
        }
        
        library LibSignature {
            enum SignatureType {
                ILLEGAL,
                INVALID,
                EIP712,
                ETHSIGN,
                PRESIGNED
            }
        
            /// @dev Encoded EC signature.
            struct Signature {
                // How to validate the signature.
                SignatureType signatureType;
                // EC Signature data.
                uint8 v;
                // EC Signature data.
                bytes32 r;
                // EC Signature data.
                bytes32 s;
            }
        }
        
        function batchFillLimitOrders(
                LibNativeOrder.LimitOrder[] calldata orders,
                LibSignature.Signature[] calldata signatures,
                uint128[] calldata takerTokenFillAmounts,
                bool revertIfIncomplete
            ) external payable returns (uint128[] memory takerTokenFilledAmounts, uint128[] memory makerTokenFilledAmounts);
        
    
    所以 `batchFillLimitOrders` 函数的签名是什么样的？
    
    *   对于结构体，我们可以用 `()`，也就是说 `struct → ()`, 结构体的参数放在 `()` 中
        
    *   对于 `enum` 我们可以映射成 `uint8`
        
    *   对于 `IERC20Token` 这类指向某个合约的，可以映射到 `address`
        
    *   对于加了 \[\]，我们原样添加\[\] 即可
        
    *   那些基础类型，是什么就用什么
        
    
    综上我们可以得到签名：`batchFillLimitOrders((address,address,uint128,uint128,uint128,address,address,address,address,bytes32,uint64,uint256)[],(uint8,uint8,bytes32,bytes32)[],uint128[],bool)` ，看着是不是很复杂～～
    
    我们使用上面介绍到的 `npx hardhat selector` 就可以得到：
    
    1.  id: `0x1baaa00b1ba411405fdb49f17881677944423e1855d044738abacf54c0703072`
        
    2.  selector: `0x1baaa00b`
        
    
    其实还有一种更加复杂的情况，就是参数是 struct，这个struct 又套了一层 struct，但是原理是一致的，比如
    
        struct MetaTransactionFeeData {
                // ERC20 fee recipient
                address recipient;
                // ERC20 fee amount
                uint256 amount;
            }
        
            struct MetaTransactionDataV2 {
                // Signer of meta-transaction. On whose behalf to execute the MTX.
                address payable signer;
                // Required sender, or NULL for anyone.
                address sender;
                // MTX is invalid after this time.
                uint256 expirationTimeSeconds;
                // Nonce to make this MTX unique.
                uint256 salt;
                // Encoded call data to a function on the exchange proxy.
                bytes callData;
                // ERC20 fee `signer` pays `sender`.
                IERC20Token feeToken;
                // ERC20 fees.
                MetaTransactionFeeData[] fees;
            }
        
        function batchExecuteMetaTransactionsV2(
                MetaTransactionDataV2 calldata mtx,
                LibSignature.Signature calldata signature
            ) external returns (bytes[] memory returnResults);
        
    
    同理我们就可以得到 `executeMetaTransactionV2((address,address,uint256,uint256,bytes,address,(address,uint256)[]),(uint8,uint8,bytes32,bytes32))` ，最终可以得到 Selector 是 `0x3d8d4082`
    
    #### 关于 ABI
    
    有时候我们会用 ABI 文件进行解码，所以有获取 ABI 的需要；那如果遇到上文所说的情况，该怎么做呢，看 `executeMetaTransactionV2` 的例子，我们可以得到如下的 ABI
    
        {
            inputs: [
              {
                'components': [
                  {
                    'internalType': 'address',
                    'name': 'signer',
                    'type': 'address'
                  },
                  {
                    'internalType': 'address',
                    'name': 'sender',
                    'type': 'address'
                  },
                  {
                    'internalType': 'uint256',
                    'name': 'expirationTimeSeconds',
                    'type': 'uint256'
                  },
                  {
                    'internalType': 'uint256',
                    'name': 'salt',
                    'type': 'uint256'
                  },
                  {
                    'internalType': 'bytes',
                    'name': 'callData',
                    'type': 'bytes'
                  },
                  {
                    'internalType': 'address',
                    'name': 'feeToken',
                    'type': 'address'
                  },
                  {
                    'components': [
                      {
                        'internalType': 'address',
                        'name': 'recipient',
                        'type': 'address'
                      },
                      {
                        'internalType': 'uint256',
                        'name': 'amount',
                        'type': 'uint256'
                      }
                    ],
                    'internalType': 'struct IMetaTransactionsFeatureV2.MetaTransactionFeeData[]',
                    'name': 'fees',
                    'type': 'tuple[]'
                  }
                ],
                'internalType': 'struct IMetaTransactionsFeatureV2.MetaTransactionDataV2[]',
                'name': 'mtx',
                'type': 'tuple'
              },
              {
                'components': [
                  {
                    'internalType': 'uint8',
                    'name': 'signatureType',
                    'type': 'uint8'
                  },
                  {
                    'internalType': 'uint8',
                    'name': 'v',
                    'type': 'uint8'
                  },
                  {
                    'internalType': 'bytes32',
                    'name': 'r',
                    'type': 'bytes32'
                  },
                  {
                    'internalType': 'bytes32',
                    'name': 's',
                    'type': 'bytes32'
                  }
                ],
                internalType: 'struct LibSignature.Signature[]',
                name: 'signature',
                type: 'tuple'
              }
            ],
            name: 'executeMetaTransactionV2',
            outputs: [{
              internalType: 'bytes',
              name: 'returnResult',
              type: 'bytes'
            }],
            stateMutability: 'payable',
            type: 'function'
          }
        
    
    其中有两个地方需要注意
    
    1.  struct 参数，需要配合 components 和 internalType
        
        1.  components 里包含的是 struct 中的元素列表，一定要按顺序声明
            
        2.  internalType 其实没那么重要，最好也写正确，可读性更好
            
    2.  嵌套的 struct 同样遵循 1 中所述的规则
        
    
    这样我们在写代码的时候就可以利用 ethers.js 或者 web3.js 进行解码 input data 数据了 (其他编程语言是类似的)。
    
    Reference:
    
    *   [web3.js decodeLog](https://web3js.readthedocs.io/en/v1.2.11/web3-eth-abi.html#decodelog)
        
    *   [web3.js decodeParameter](https://web3js.readthedocs.io/en/v1.2.11/web3-eth-abi.html#decodeparameter)

---

*Originally published on [ShaoN](https://paragraph.com/@shaon/evm-transaction-parsing)*
