Walkthrough: Debugging a Uniswap V3 Transaction on Mainnet

This short tutorial will walk you through the steps of debugging a Uniswap V3 transaction on Ethereum Mainnet. It requires access to a public Ethereum RPC endpoint which the debugger will fork the blockchain state from. You can find an RPC URL from Chainlist.

In this tutorial, we will write a test that will:

  1. Convert ETH to WETH using the WETH contract's deposit() method
  2. Swap WETH for DAI using Uniswap V3 protocol
  3. Use the debugger to step into Uniswap V3's code to observe its inner workings

All while using the actual up-to-date Ethereum mainnet state. Let's begin!

Clone Uniswap from GitHub

To call Uniswap we'll need its Solidity interface files. We can fetch these from the Uniswap/v3-periphery repository on GitHub.

$ git clone https://github.com/Uniswap/v3-periphery.git
$ cd v3-periphery
$ npm install       # install dependencies

In order to be able to step into the Uniswap code, we need to compile its contracts and generate an artifacts folder. The Solidity Debuger reads necessary metadata from these artifacts that helps it match deployed bytecode to source code. Run:

$ npx hardhat compile

Create a Debug Target for 'v3-periphery'

Open the v3-periphery folder in VS Code. Then, from the Solidity Debugger menu click New Target and select Create from currently-open Hardhat project.

Choose a Solidity compiler for the test contract you are about to create. Uniswap was written using Solidity 0.7.6, so that version is automatically selected. However, in this tutorial we will use the latest version. Select compiler version 0.8.17, then click Create.

Once target creation is complete, Test1.t.sol will automatically open in the editor.

Configure Forking

Open the Settings Window and select the Test1 contract, then change the following settings under Debugging Test1:

SettingValue
Forking→Enabledenabled
Forking→Ethereum RPC URLhttps://mainnet.infura.io/v3/YOUR-API-KEY

You can get a URL for a free public node at chainlist.org.

Click OK when you are done.

Performing a Swap

Replace source code of Test1.t.sol with:

// SPDX-License-Identifier: MIT
pragma solidity >= 0.4.21 < 0.9.0;
import "contracts/interfaces/ISwapRouter.sol";

interface IERC20x {
    function balanceOf(address account) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
}

interface IWETHx is IERC20x {
    function deposit() external payable;
}

contract DbgEntry {
    ISwapRouter public constant uniswapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
    IWETHx private constant WETH = IWETHx(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
    IERC20x DAI = IERC20x(0x6B175474E89094C44Da98b954EedeAC495271d0F);

    // solidity debugger cheat code
    event EvmSetBalance(address, uint256);

    constructor() {
        emit EvmSetBalance(address(this), 1000 ether);

        // convert ETH to WETH
        WETH.deposit{value: 2 ether}();
        WETH.approve(address(uniswapRouter), type(uint256).max);

        // get balances before swap
        uint256 startBalanceETH = address(this).balance;
        uint256 startBalanceDAI = DAI.balanceOf(address(this));
        uint256 startBalanceWETH = WETH.balanceOf(address(this));

        // swap WETH for DAI
        ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams(
            address(WETH),     // tokenIn
            address(DAI),      // tokenOut
            3000,              // fee
            address(this),     // recipient
            block.timestamp,   // deadline
            1 ether,           // amountIn
            100,               // amountOutMinimum
            0                  // sqrtPriceLimitX96
            );

        uint256 amountOut = uniswapRouter.exactInputSingle(params);

        // get balances after swap
        uint256 endBalanceETH = address(this).balance;
        uint256 endBalanceWETH = WETH.balanceOf(address(this));
        uint256 endBalanceDAI = DAI.balanceOf(address(this));

        uint256 done = 1;
    }
}

Put a breakpoint on the first line of the constructor (emit EvmSetBalance(address(this), 1000 ether);) and click the Debug button. You should be able to step through the transaction until it is done.

Stepping into Uniswap

Put a breakpoint on the call to uniswapRouter.exactInputSingle(params) and start the debugger. When the breakpoint hits, try stepping into the call and you will find that you're able to step into Uniswap's internals!