Ethers.js Uniswap UniswapV3 – How to Decode Uniswap Universal Router Transaction in Ethers.js

ethers.jsuniswapuniswapv3

Uniswap has recently rolled out a new Universal router (blog here: https://docs.uniswap.org/contracts/universal-router/technical-reference)

I've decoded a transaction using the Uniswap ABI and ethersjs parseTransaction into something like so:

TransactionDescription {
  args: [
    '0x0b00',
    [
      '0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000009184e72a000',
      '0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000009184e72a00000000000000000000000000000000000000000000000000000000000000038e000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000'
    ],
    BigNumber { _hex: '0x63c87f23', _isBigNumber: true },
    commands: '0x0b00',
    inputs: [
      '0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000009184e72a000',
      '0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000009184e72a00000000000000000000000000000000000000000000000000000000000000038e000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000'
    ],
    deadline: BigNumber { _hex: '0x63c87f23', _isBigNumber: true }
  ],
  functionFragment: {
    type: 'function',
    name: 'execute',
    constant: false,
    inputs: [ [ParamType], [ParamType], [ParamType] ],
    outputs: [],
    payable: true,
    stateMutability: 'payable',
    gas: null,
    _isFragment: true,
    constructor: [Function: FunctionFragment] {
      from: [Function (anonymous)],
      fromObject: [Function (anonymous)],
      fromString: [Function (anonymous)],
      isFunctionFragment: [Function (anonymous)]
    },
    format: [Function (anonymous)]
  },
  name: 'execute',
  signature: 'execute(bytes,bytes[],uint256)',
  sighash: '0x3593564c',
  value: BigNumber {
    _hex: '0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000063c87f2300000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000009184e72a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000009184e72a00000000000000000000000000000000000000000000000000000000000000038e000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000',
    _isBigNumber: true
  }
}

However, I can not understand how I could decode the inputs into the correct values.

The technical reference suggests that the bytes array for input should have the following things in it:

The second parameter for the function is an array of bytes strings. Each element in the array is the abi-encoded input that will be used for the respective command.


V3_SWAP_EXACT_IN
address The recipient of the output of the trade
uint256 The amount of input tokens for the trade
uint256 The minimum amount of output tokens the user wants
bytes The UniswapV3 encoded path to trade along
bool A flag for whether the input tokens should come from the msg.sender (through Permit2) or whether the funds are already in the UniversalRouter

However, the bytes array only has length 2:

        inputs: [
      '0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000009184e72a000',
      '0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000009184e72a00000000000000000000000000000000000000000000000000000000000000038e000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000'
    ],

What am I misunderstanding here, and how can I decode a universal router transaction

Best Answer

The misunderstanding seems to come from the inputs.

You can see that in the object you mentioned there are these lines:

    commands: '0x0b00',
inputs: [
  '0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000009184e72a000',
  '0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000009184e72a00000000000000000000000000000000000000000000000000000000000000038e000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000'
],

Where the commands (Commands.sol) is interpreted as:

  • 0x: Hex symbol
  • 0b: WRAP_ETH (1st function)
  • 00: V3_SWAP_EXACT_IN (2nd function)

That is why you see 2 elements in the inputs array. The first element is the inputs corresponding to the first function (WRAP_ETH) The second element in the array are the inputs corresponding to V3_SWAP_EXACT_IN

Let's look at the first function for example and decode that. enter image description here

  //inputs[0] split every 64 characters
  0000000000000000000000000000000000000000000000000000000000000002 //addres of recipient
  000000000000000000000000000000000000000000000000000009184e72a000 //amount of ETH
  1. Address of recipient: HEX for 2 (2 meaning the contract is the recipient Constants.sol)
  2. Amount of ETH in WEI where 0x9184e72a000 equals 10000000000000 in decimal (10000000000000 wei = 0.00001 Ether)

Using this same method you can do the same thing for V3_SWAP_EXACT_IN.

Note there might be tools to decode these by default if you have the ABI, but this is why you see 2 inputs only.

Related Topic