Degen Code

Degen Code

Share this post

Degen Code
Degen Code
Uniswap V4/V3 Multi-Pool Arbitrage
Copy link
Facebook
Email
Notes
More

Uniswap V4/V3 Multi-Pool Arbitrage

Part II: Construction & Testing

May 15, 2025
∙ Paid
3

Share this post

Degen Code
Degen Code
Uniswap V4/V3 Multi-Pool Arbitrage
Copy link
Facebook
Email
Notes
More
Share

In Part I we figured out the structure of the smart contract. Now we can turn our attention to writing and testing it.

Uniswap V4/V3 Multi-Pool Arbitrage

Uniswap V4/V3 Multi-Pool Arbitrage

BowTiedDevil
·
May 7
Read full story

This article is focused on building the execution contract and testing that it operates correctly.

If you have not read my article on Testing with Ape Framework, you should! Otherwise you may be confused — I present test scripts and assume you can run them and interpret their results without further explanation.

Testing With Ape Framework

Testing With Ape Framework

BowTiedDevil
·
February 28, 2024
Read full story

Versions and Testing Platform

I am building and testing using Vyper 0.4.1 and Ape Framework version 0.8.33.

The project directory I’m using for these examples is ~/code/uniswap_v4_v3_executor.

I am testing against my Base mainnet node with this minimal Ape config:

ape-config.yaml

name: Uniswap V4-V3 (multi-pool) Executor

default_ecosystem: base

base:
  default_network: mainnet-fork
  mainnet-fork:
    default_provider: foundry

plugins:   
  - foundry
  - vyper

Fake Testing Contracts

It can be very difficult to develop smart contracts that hook into others. Arbitrage execution contracts are especially bad because they rely on the state of multiple pools that are largely outside of our control.

To have any hope of testing the complex byte-decoding scheme, we need a fixed target to shoot at. To that end, I’ve been developing a set of mock contracts that implement the minimal interface necessary to simulate the behavior of real pools and tokens.

Fake ERC-20

The Vyper repository has an ERC-20 implementation that we can use. It’s simple to copy it into a project, save it as contracts/fake_erc20.vy and write a test against it after running ape init:

tests/test_fake_erc20.py

import pytest
from ape.api.accounts import TestAccountAPI
from ape.contracts.base import ContractInstance
from ape.managers.project import ProjectManager
from ape_test.accounts import TestAccount


@pytest.fixture
def owner_account(accounts: list[TestAccountAPI]) -> TestAccount:
    return accounts[0]


@pytest.fixture
def non_owner_account(accounts: list[TestAccountAPI]) -> TestAccount:
    return accounts[1]


@pytest.fixture
def fake_token(
    project: ProjectManager,
    owner_account: TestAccount,
) -> ContractInstance:
    return project.fake_erc20.deploy(
        "Fake ERC-20",
        "FERC20",
        18,
        100_000_000,
        sender=owner_account,
    )


def test_fake_token_mint(
    fake_token: ContractInstance,
    owner_account: TestAccount,
    non_owner_account: TestAccount,
):
    assert fake_token.balanceOf(non_owner_account) == 0
    fake_token.mint(non_owner_account, 1, sender=owner_account)
    assert fake_token.balanceOf(non_owner_account) == 1

Running ape test reveals that the fake ERC-20 contract was deployed by the owner account, and that the owner can mint a fake balance to an arbitrary address.

WETH meets the ERC-20 specification and defines a few more important functions. Since it is designed for value parity with Ether, it implements a no-fee wrapping and unwrapping functions: deposit and withdraw.

Let’s use the Vyper module system to implement the ERC-20 functionality and add the deposit and withdraw functions:

contracts/fake_weth.vy

import fake_erc20
initializes: fake_erc20
exports: (
    fake_erc20.IERC20,
    fake_erc20.IERC20Detailed,
    fake_erc20.mint,
    fake_erc20.burn,    
)

@deploy
def __init__(
    name: String[32], 
    symbol: String[32], 
    decimals: uint8, 
    supply: uint256
):
    fake_erc20.__init__(name, symbol, decimals, supply)


@external
@payable
def deposit():
    fake_erc20.totalSupply += msg.value
    fake_erc20.balanceOf[msg.sender] += msg.value


@external
def withdraw(amount: uint256):
    fake_erc20.totalSupply -= amount    
    fake_erc20.balanceOf[msg.sender] -= amount
    send(msg.sender, amount)

The deposit and withdraw functions are fairly simple. The most important pieces of this contract are the import and export blocks at the top, which instruct the Vyper compiler to use the code defined in fake_erc20.vy through a virtual interface as if the state defined there was already implemented here.

Now, change the fixture to deploy the fake_weth contract instead of fake_erc20, and define new tests to confirm that wrapping and unwrapping works as expected:

tests/test_fake_weth.py

import pytest
from ape.api.accounts import TestAccountAPI
from ape.contracts.base import ContractInstance
from ape.managers.project import ProjectManager
from ape_test.accounts import TestAccount


@pytest.fixture
def weth(
    project: ProjectManager,
    owner_account: TestAccount,
) -> ContractInstance:
    return project.fake_weth.deploy(
        "Fake Wrapped Ether",
        "WETH",
        18,
        100_000_000,
        sender=owner_account,
    )


@pytest.fixture
def owner_account(accounts: list[TestAccountAPI]) -> TestAccount:
    return accounts[0]


@pytest.fixture
def non_owner_account(accounts: list[TestAccountAPI]) -> TestAccount:
    return accounts[1]


def test_fake_weth_deposit_and_unwrap(
    weth: ContractInstance,
    non_owner_account: TestAccount,
):
    assert weth.balanceOf(non_owner_account) == 0
    weth.deposit(value=1, sender=non_owner_account)
    assert weth.balanceOf(non_owner_account) == 1

    weth.withdraw(1, sender=non_owner_account)
    assert weth.balanceOf(non_owner_account) == 0

Running the tests works as expected, so the token is good to go.

Fake Uniswap V3 Pool

Now let’s turn our attention to liquidity pools. A fake liquidity pool should allow us to ask it for token addresses, call a swap, and replicate any checks that the real pool would perform.

I have zero interest in modeling a complete Uniswap V3 pool. All I care about is the ability to simulate a swap, and that the fake pool performs a swap callback that matches the behavior of the real pool.

Therefore I’ll take an approach that gives the pool the minimal ability to check balances and transfer tokens. I’ll write a simple swap “ loading” function called set_swap_amounts, which injects an amount in and amount out value which will be used on the next call to swap. It will perform a sanity check that the contract has a sufficient token balance to perform the requested transfer, and then just act as a dumb receiver & transmitter.

contracts/fake_uniswap_v3_pool.vy

This post is for paid subscribers

Already a paid subscriber? Sign in
© 2025 BowTiedDevil
Privacy ∙ Terms ∙ Collection notice
Start writingGet the app
Substack is the home for great culture

Share

Copy link
Facebook
Email
Notes
More