Cover photo

EVM Transaction Parsing

#Web3

持续更新 …


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

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

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

在开发过程中不可避免的要进行单元测试、集成测试,全面的测试 Case 是必要的;比如 0x Protocol,它的内部实现非常多样,支持 uniswapv2uniswapv3pancakeotc ordernative orderliquidity provider 等,而且每个实现中又包含不同的 function,要想在正式上生产之前把这些 Case 全部都测试到,就需要所有这些 function 的链上交易,使用这些真实的交易进行测试,准确度才会更高;所以,如果遇到协议下的某个 function 无法在浏览器的 Transactions 列表找到 (比如这个 0x Protocol Transactions,列表中的交易数据非常多),可以用下面的办法

  1. 找到目标 Function 的 ABI,一般情况下很容易在区块浏览器的已认证合约下面找到;如果找不到,就需要找到项目的合约源码,从代码中找到方法签名

  2. 使用签名函数对步骤 1 的方法进行签名

    1. 在线工具:ABI HashexEVM Function Selector

    2. 命令行工具:web3contract

      1. 按照 Readme 安装 Node.js 等

      2. 执行 npx hardhat selector ““, 比如 npx hardhat selector “transfer(address,uint256)“

    • 得到 Function Selector 后,最好在 bytes4 database 中检索一下,确保方法签名存在,以及是我们想要的,有的时候会搞错

    • 在区块浏览器的高级筛选下对 Function Selector 做筛选,然后根据列表中的 to 可以甄别是不是我们的目标合约以及想要的交易

      1. Advanced Filter: https://etherscan.io/advanced-filter?mtd=0x1baaa00b ,可以把 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: