Degen Code

Degen Code

Optimizing Smart Contracts With Autoresearch

Gas Prices Improving Rapidly

Jun 09, 2026
∙ Paid

I’ve been using AutoResearch (AR) to improve smart contracts.

I don’t see AR discussed often enough, and I think it suffers from a branding problem.

The name suggests it is a tool for finding obscure info, digging into scientific papers, or summarizing dense source material. But it’s really much more practical.

At a high level, AR is a simple technique for performing a series of structured experiments, measuring the impact to some evaluation metric, and choosing to keep or discard those individual changes.

Multi-DEX Execution

At the end of my Uniswap V4 series, I introduced a gnarly “virtual machine” command-based executor contract for handling swaps between multiple DEX and pool versions.

Uniswap V4/V3 Multi-Pool Arbitrage

BowTiedDevil
·
May 22, 2025
Read full story

It was inspired by the Uniswap V4 Router design which used byte encoding of arbitrary commands across multiple contracts supported by the exchange.

Its complexity made it a real pain to build, test, and debug. So I got it working, ensured it satisfied my synthetic test cases, ran some live arbitrage to confirm it was functional, then moved on.

But there was a lot of potential left on the table!

At the end of last year, AI models got really good and CLI coding harnesses became very sophisticated. Suddenly I could dedicate a lot of idle computing resources to background refinement of techniques I had already started, like the VM executor.

Agentic Coding For Degens

Agentic Coding For Degens

BowTiedDevil
·
Feb 10
Read full story

The Goal

Ultimately I want to build a flexible executor contract that supports command-based execution of atomic swaps across any mixture of Uniswap V2, V3, and V4 pools (and all forks).

The Method

Building the contract will be done through a series of AR refinements to a single executor smart contract using Vyper version 0.4.3.

Tests will be written using Ape Framework version 0.8.50.

The AI harness is Pi version 0.79.0 using the pi-autoresearch extension. The backend AI model is GLM-5.1.

Goal: V2-Only Optimization

It would be overwhelming to bring the full set of Uniswap versions in scope initially. So let’s start with something really simple: build a contract that can execute a chain of Uniswap V2 pool swaps, then do AR runs on it until we’ve optimized it and see what we get.

I’ve already created a set of fake contracts that I can use for simulating Uniswap V2 pools and their underlying ERC-20 tokens.

fake_erc20.vy

from ethereum.ercs import IERC20
from ethereum.ercs import IERC20Detailed

implements: IERC20
implements: IERC20Detailed

# ERC20 events inherited from IERC20 — no local definitions needed.

name: public(String[32])
symbol: public(String[32])
decimals: public(uint8)

MINTER: immutable(address)

totalSupply: public(uint256)
balanceOf: public(HashMap[address, uint256])
allowance: public(HashMap[address, HashMap[address, uint256]])


@deploy
def __init__(_name: String[32], _symbol: String[32], decimals: uint8, nominal_initial_supply: uint256):
    MINTER = msg.sender
    init_supply: uint256 = nominal_initial_supply * 10 ** convert(decimals, uint256)
    self.name = _name
    self.symbol = _symbol
    self.decimals = decimals
    self.balanceOf[msg.sender] = init_supply
    self.totalSupply = init_supply


@external
def transfer(_to : address, _value : uint256) -> bool:    
    self.balanceOf[msg.sender] -= _value
    self.balanceOf[_to] += _value
    log IERC20.Transfer(msg.sender, _to, _value)
    return True


@external
def transferFrom(_from : address, _to : address, _value : uint256) -> bool:
    self.balanceOf[_from] -= _value
    self.balanceOf[_to] += _value
    self.allowance[_from][msg.sender] -= _value
    log IERC20.Transfer(_from, _to, _value)
    return True


@external
def approve(_spender : address, _value : uint256) -> bool:    
    self.allowance[msg.sender][_spender] = _value
    log IERC20.Approval(msg.sender, _spender, _value)
    return True


@external
def mint(_to: address, _value: uint256):
    assert msg.sender == MINTER
    assert _to != empty(address)
    self.totalSupply += _value
    self.balanceOf[_to] += _value
    log IERC20.Transfer(empty(address), _to, _value)


@internal
def _burn(_to: address, _value: uint256):
    """
    @dev Internal function that burns an amount of the token of a given
         account.
    @param _to The account whose tokens will be burned.
    @param _value The amount that will be burned.
    """
    assert _to != empty(address)
    self.totalSupply -= _value
    self.balanceOf[_to] -= _value
    log IERC20.Transfer(_to, empty(address), _value)


@external
def burn(_value: uint256):
    """
    @dev Burn an amount of the token of msg.sender.
    @param _value The amount that will be burned.
    """
    self._burn(msg.sender, _value)


@external
def burnFrom(_to: address, _value: uint256):
    """
    @dev Burn an amount of the token from a given account.
    @param _to The account whose tokens will be burned.
    @param _value The amount that will be burned.
    """
    self.allowance[_to][msg.sender] -= _value
    self._burn(_to, _value)

fake_weth.vy

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

@deploy
@payable
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
    log IERC20.Transfer(empty(address), msg.sender, msg.value)


@external
def withdraw(amount: uint256):
    fake_erc20.totalSupply -= amount
    fake_erc20.balanceOf[msg.sender] -= amount
    log IERC20.Transfer(msg.sender, empty(address), amount)
    raw_call(
        msg.sender, 
        b'',
        value=amount
    )

fake_uniswap_v2_pair.vy

"""
Fake Uniswap V2 pair for testing.

Mimics the V2 swap interface with the same invariant enforcement as the
real UniswapV2Pair.sol:
- Reentrancy guard (matches real V2's `unlocked` flag)
- Snapshot balances BEFORE the callback (to compute input amounts via delta)
- K-invariant check after callback (constant product minus fee)
- Callback only invoked when data.length > 0 (matches real V2)

The V2 swap is an optimistic transfer: output tokens are sent to `to`
BEFORE any input is received. If data is non-empty, a callback
(uniswapV2Call) is invoked on `to`, giving the caller a chance to
pay the input tokens. After the callback (or immediately if no callback),
the pair verifies the constant product invariant.

Unlike V3/V4, there is no auto-pay — the executor must transfer tokens
to the pair before or during the callback.

V2 is constant-product: two reserves, no complex tick math. The pair
enforces the K-invariant at runtime via balance deltas — no pre-
configured swap amounts are needed. Just mint liquidity tokens to the
pair and call sync() to initialize reserves, then call swap() with
any amount up to the reserves.

Liquidity setup:
  1. Mint both tokens to the pair (owner or any address)
  2. Call sync() to snapshot current balances as reserves
  3. swap() is now ready — it will enforce K-invariant

Fee parameter: swap_fee is a fraction of 10000.
  Uniswap / SushiSwap: 30 (0.3%)
  PancakeSwap:         25 (0.25%)
"""

from .interfaces.UniswapV2 import IUniswapV2Pair
from .interfaces.UniswapV2 import IUniswapV2Callee
from .interfaces.UniswapV2 import IHookCallee
from .interfaces.UniswapV2 import IPancakeCallee
from ethereum.ercs import IERC20

MAX_CALLDATA_LENGTH: constant(uint256) = 512

implements: IUniswapV2Pair

OWNER: immutable(address)
SWAP_FEE: immutable(uint256)  # Fee as fraction of 10000 (30 = 0.3%, 25 = 0.25%)

token0: public(address)
token1: public(address)

# Which callback selector to invoke: 0 = uniswapV2Call, 1 = hook, 2 = pancakeCall
callback_variant: public(uint256)

# Pre-configured swap amounts (optional — for backward compatibility).
# When non-zero, swap() verifies the exact output matches these values
# IN ADDITION to the K-invariant.
amount_in: public(uint256)
amount_out: public(uint256)

# Reentrancy guard — matches real V2's `unlocked` flag
unlocked: public(uint256)

# Simulated reserves for getReserves(), initialized by sync(). 
# Used by V2 math (getAmountOut/getAmountIn)
reserve0: public(uint112)
reserve1: public(uint112)
block_timestamp_last: public(uint32)


@external
@view
def getReserves() -> (uint112, uint112, uint32):
    return (self.reserve0, self.reserve1, self.block_timestamp_last)


@deploy
def __init__(token0: address, token1: address, _callback_variant: uint256, _swap_fee: uint256):
    OWNER = msg.sender
    self.token0 = token0
    self.token1 = token1
    self.callback_variant = _callback_variant
    SWAP_FEE = _swap_fee
    self.unlocked = 1


@external
@nonpayable
def sync():
    """Update reserves to match current token balances.

    Call after minting liquidity tokens to the pair. This snapshots
    current balances as reserves so that swap() can enforce the
    K-invariant and getReserves() returns correct values for V2 math.
    """
    self.reserve0 = convert(staticcall IERC20(self.token0).balanceOf(self), uint112)
    self.reserve1 = convert(staticcall IERC20(self.token1).balanceOf(self), uint112)
    log IUniswapV2Pair.Sync(self.reserve0, self.reserve1)


@external
@nonpayable
def reset():
    """Drain all tokens and clear state — for test teardown / reuse.

    Transfers all token0 and token1 balances back to the contract owner,
    then zeroes reserves and swap config so the pair can be set up fresh.
    """
    assert msg.sender == OWNER
    bal0: uint256 = staticcall IERC20(self.token0).balanceOf(self)
    bal1: uint256 = staticcall IERC20(self.token1).balanceOf(self)
    if bal0 > 0:
        extcall IERC20(self.token0).transfer(empty(address), bal0)
    if bal1 > 0:
        extcall IERC20(self.token1).transfer(empty(address), bal1)
    self.reserve0 = 0
    self.reserve1 = 0
    self.amount_in = 0
    self.amount_out = 0


@external
@nonpayable
def swap(
    amount0Out: uint256,
    amount1Out: uint256,
    to: address,
    data: Bytes[MAX_CALLDATA_LENGTH],
):
    """
    Perform a V2 swap with constant-product invariant enforcement.

    This matches the real UniswapV2Pair.sol flow:
    1. Reentrancy guard
    2. Output amounts > 0 and <= reserves
    3. to != token0 and to != token1
    4. (Optional) Verify output matches pre-configured amount_out
    5. Optimistic transfer of output tokens
    6. Callback if data.length > 0
    7. Compute input amounts via balance delta
    8. K-invariant check: balance0Adjusted * balance1Adjusted >= reserve0 * reserve1 * 10000^2
    9. Update reserves to match balances
    """
    # ── Reentrancy guard (matches real V2's lock modifier) ──
    assert self.unlocked == 1, "UniswapV2: LOCKED"
    self.unlocked = 0

    assert amount0Out > 0 or amount1Out > 0, "UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT"

    # ── Reserve checks (matches real V2) ──
    assert amount0Out <= convert(self.reserve0, uint256), "UniswapV2: INSUFFICIENT_LIQUIDITY"
    assert amount1Out <= convert(self.reserve1, uint256), "UniswapV2: INSUFFICIENT_LIQUIDITY"

    # ── to != token addresses (matches real V2) ──
    assert to != self.token0 and to != self.token1, "UniswapV2: INVALID_TO"

    # Snapshot reserve values BEFORE the optimistic transfer + callback
    # (matches real V2's getReserves() before the transfer)
    _reserve0: uint112 = self.reserve0
    _reserve1: uint112 = self.reserve1

    # ── Optimistic transfer of output tokens (matches real V2) ──
    if amount0Out > 0:
        extcall IERC20(self.token0).transfer(to, amount0Out)
    if amount1Out > 0:
        extcall IERC20(self.token1).transfer(to, amount1Out)

    # ── Invoke callback if data is non-empty (matches real V2) ──
    if len(data) > 0:
        if self.callback_variant == 0:
            extcall IUniswapV2Callee(to).uniswapV2Call(
                msg.sender, amount0Out, amount1Out, data
            )
        elif self.callback_variant == 1:
            extcall IHookCallee(to).hook(
                msg.sender, amount0Out, amount1Out, data
            )
        else:
            extcall IPancakeCallee(to).pancakeCall(
                msg.sender, amount0Out, amount1Out, data
            )

    # ── Read post-callback balances (matches real V2) ──
    balance0: uint256 = staticcall IERC20(self.token0).balanceOf(self)
    balance1: uint256 = staticcall IERC20(self.token1).balanceOf(self)

    # ── Compute input amounts via balance DELTA (matches real V2) ──
    # Real V2: amountNIn = balanceN > _reserveN - amountNOut ? balanceN - (_reserveN - amountNOut) : 0
    # In Solidity, underflow wraps; in Vyper it reverts. Handle the edge case
    # where amountNOut == _reserveN (allowed by <= check) by using a safe
    # formulation: if balanceN + amountNOut > _reserveN, then amountNIn is
    # the surplus over the expected post-transfer balance.
    amount0In: uint256 = (balance0 + amount0Out - convert(_reserve0, uint256)) if (balance0 + amount0Out > convert(_reserve0, uint256)) else 0
    amount1In: uint256 = (balance1 + amount1Out - convert(_reserve1, uint256)) if (balance1 + amount1Out > convert(_reserve1, uint256)) else 0
    
    assert amount0In > 0 or amount1In > 0, "UniswapV2: INSUFFICIENT_INPUT_AMOUNT"

    # ── K-invariant check ──    
    balance0_adjusted: uint256 = balance0 * 10000 - amount0In * SWAP_FEE
    balance1_adjusted: uint256 = balance1 * 10000 - amount1In * SWAP_FEE
    assert balance0_adjusted * balance1_adjusted >= convert(_reserve0, uint256) * convert(_reserve1, uint256) * 10000 * 10000, "UniswapV2: K"

    # ── Update reserves to match actual balances (matches real V2's _update) ──
    self.reserve0 = convert(balance0, uint112)
    self.reserve1 = convert(balance1, uint112)

    # ── Emit events (matches real V2) ──
    log IUniswapV2Pair.Sync(self.reserve0, self.reserve1)
    log IUniswapV2Pair.Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to)

    self.amount_in = 0
    self.amount_out = 0
    self.unlocked = 1


@external
@payable
def __default__():
    return

Initial Contract

Ask the AI to write the initial contract:

Write a Vyper smart contract, command_executor.vy, to support executing encoded byte command streams at transaction time. The contract will support up to 255 unique 1 byte commands (0x00-0xFE) plus a 1 byte separator (0xFF).

All encoded commands shall be structured as concatenated bytes with fixed offsets:

[1 byte] - COMMAND
[X bytes][Y bytes][Z bytes] - arguments necessary for execution of the given function
[1 byte] - SEPARATOR

The contract will use immutable variables set at deployment time, starting with these:
OWNER, holding the deployer address
WETH_ADDR, holding the Wrapped Ether contract address

The contract shall not use durable storage. The contract may use transient storage using the transient keyword to pass data across call frames where needed.

Provide a fallback function __default__ which accepts zero-calldata Ether transfers but reject all other calls.

Create a single external function called execute_commands. Protect the function with a top-level assertion: msg.sender == self.OWNER.

Provide initial support for one command: CMD_V2_SWAP_EXPLICIT_AMOUNTS that performs a swap call at a given Uniswap V2 pool.

It generated this contract:

command_executor.vy

"""
Command Executor — parses and dispatches an encoded byte stream of
commands at transaction time.

Encoding (per command):
    [1 byte ]  COMMAND_ID       (0x00–0xFE valid; 0xFF is separator)
    [X bytes]  arg1
    [Y bytes]  arg2
    [Z bytes]  arg3
    [1 byte ]  SEPARATOR        (0xFF)

X, Y, Z are fixed per command type and must be known by the encoder.

No durable storage is used. Immutables are set at deploy time and
transient storage is available for cross-call-frame data passing
(e.g. reentrancy guard, flash-swap callback verification).
"""

from .interfaces.UniswapV2 import IUniswapV2Pair

# ─────────────────────────────────────────────────────────────────────────────
# Constants
# ─────────────────────────────────────────────────────────────────────────────

COMMAND_SEPARATOR: constant(uint8) = 255

# Command IDs (0x00–0xFE; 0xFF reserved as separator)
CMD_V2_SWAP_EXPLICIT_AMOUNTS: constant(uint8) = 0

# V2 swap argument sizes (bytes)
V2_POOL_LEN: constant(uint256) = 20
V2_AMOUNT_LEN: constant(uint256) = 32
V2_RECIPIENT_LEN: constant(uint256) = 20
V2_ARGS_LEN: constant(uint256) = 20 + 32 + 32 + 20  # 104 bytes total

# Maximum number of commands per call (Vyper 0.4.x uses for/range, not while)
MAX_COMMANDS: constant(uint256) = 128

# ─────────────────────────────────────────────────────────────────────────────
# Immutables
# ─────────────────────────────────────────────────────────────────────────────

OWNER: immutable(address)
WETH_ADDR: immutable(address)

# ─────────────────────────────────────────────────────────────────────────────
# Transient storage — auto-cleared at end of each transaction (EIP-1153)
# ─────────────────────────────────────────────────────────────────────────────

# Reentrancy guard: 0 = unlocked, 1 = locked.
# Uses transient storage instead of durable storage so the slot is
# automatically reset between transactions, saving gas and avoiding
# the need for a persistent state variable.
_locked: transient(uint256)

# ─────────────────────────────────────────────────────────────────────────────
# Events
# ─────────────────────────────────────────────────────────────────────────────

event CommandExecuted:
    command_id: uint8

# ─────────────────────────────────────────────────────────────────────────────
# Constructor
# ─────────────────────────────────────────────────────────────────────────────

@deploy
def __init__(weth_addr: address):
    OWNER = msg.sender
    WETH_ADDR = weth_addr

# ─────────────────────────────────────────────────────────────────────────────
# Fallback — accept plain ETH transfers only
# ─────────────────────────────────────────────────────────────────────────────

@payable
@external
def __default__():
    """@notice Accepts zero-calldata Ether transfers and rejects all other calls. """
    assert len(msg.data) == 0, "only plain ETH transfers"

# ─────────────────────────────────────────────────────────────────────────────
# External entry point
# ─────────────────────────────────────────────────────────────────────────────

@external
def execute_commands(encoded_commands: Bytes[4096]) -> bool:
    """
    @notice Parse and execute an encoded command stream.
    @dev Each command: [1B COMMAND_ID][N bytes args][1B SEPARATOR=0xFF].
         0x00 CMD_V2_SWAP_EXPLICIT_AMOUNTS:
           20B pool | 32B amount0Out | 32B amount1Out | 20B recipient
    @param encoded_commands Concatenated bytes of command+args+separator tuples.
    @return True when all commands have executed successfully.
    """
    # ── Access control ────────────────────────────────────────────────────
    assert msg.sender == OWNER, "unauthorized"

    # ── Reentrancy guard (transient) ──────────────────────────────────────
    assert self._locked == 0, "reentrant call"
    self._locked = 1

    cursor: uint256 = 0
    end: uint256 = len(encoded_commands)

    for _: uint256 in range(MAX_COMMANDS):
        if cursor >= end:
            break
        # ── Read command byte ────────────────────────────────────────────
        cmd: uint8 = self._read_uint8(encoded_commands, cursor)
        cursor += 1

        # ── Dispatch ─────────────────────────────────────────────────────
        if cmd == CMD_V2_SWAP_EXPLICIT_AMOUNTS:
            # Args: [20B pool][32B amount0Out][32B amount1Out][20B recipient]
            pool: address        = self._read_address(encoded_commands, cursor)
            cursor += V2_POOL_LEN

            amount0_out: uint256 = self._read_uint256(encoded_commands, cursor)
            cursor += V2_AMOUNT_LEN

            amount1_out: uint256 = self._read_uint256(encoded_commands, cursor)
            cursor += V2_AMOUNT_LEN

            recipient: address   = self._read_address(encoded_commands, cursor)
            cursor += V2_RECIPIENT_LEN

            self._v2_swap(pool, amount0_out, amount1_out, recipient)

        else:
            assert False, "unknown command"

        # ── Validate separator ───────────────────────────────────────────
        sep: uint8 = self._read_uint8(encoded_commands, cursor)
        cursor += 1
        assert sep == COMMAND_SEPARATOR, "invalid separator"

        log CommandExecuted(command_id=cmd)

    # ── Unlock (transient storage auto-clears on revert) ──────────────────
    self._locked = 0
    return True

# ─────────────────────────────────────────────────────────────────────────────
# Internal — byte-parsing helpers
# ─────────────────────────────────────────────────────────────────────────────

@internal
@pure
def _read_uint8(data: Bytes[4096], offset: uint256) -> uint8:
    """@dev Read a single byte as uint8 at the given offset. """
    return convert(convert(convert(slice(data, offset, 1), bytes32), uint256) >> 248, uint8)


@internal
@pure
def _read_address(data: Bytes[4096], offset: uint256) -> address:
    """Read a 20-byte address at *offset*."""
    return convert(slice(data, offset, 20), address)


@internal
@pure
def _read_uint256(data: Bytes[4096], offset: uint256) -> uint256:
    """Read a 32-byte uint256 at *offset*."""
    return convert(slice(data, offset, 32), uint256)

# ─────────────────────────────────────────────────────────────────────────────
# Internal — command handlers
# ─────────────────────────────────────────────────────────────────────────────

@internal
def _v2_swap(
    pool: address,
    amount0_out: uint256,
    amount1_out: uint256,
    recipient: address,
):
    """
    Execute a swap() call on a Uniswap V2 pair.

    Calls pair.swap(amount0Out, amount1Out, to, "") with empty data
    (no flash-swap callback).
    """
    extcall IUniswapV2Pair(pool).swap(
        amount0_out, amount1_out, recipient, b""
    )

Experiment Setup

The AR extension comes with an autoresearch-create skill, so I load it and have this brief interaction:

I have the autoresearch skill loaded. What would you like me to optimize? I need to know:

1. Goal — What are we optimizing?
2. Command/Benchmark — How do we measure it?
3. Metric — What number are we trying to improve (and which direction)?
4. Constraints — Any hard rules (tests must pass, no new deps, etc.)?

Or if you’d like me to explore the codebase first to suggest an optimization target, I can do that too.

I respond:

I want to create an autoresearch task with an optimization target of minimizing total gas use of a transaction that captures profit from a synthetic arbitrage scenario through three Uniswap V2 pools (V2-V2-V2).

Build an Ape test that generates the profit scenario using @contracts/fake_erc20.vy, @contracts/fake_uniswap_v2_pair.vy, and @contracts/fake_weth.vy.

Build fake pools as pytest fixtures, initialize them using fake tokens, assign the tokens and mint pool reserves that result in a profit after fees. The test must assert that the transaction results in a positive gross WETH profit at the executor contract, but net WETH profit (considering gas fees) may be ignored.

The model considered this for a while, then added a new command to the contract to support ERC20 transfers between pools. I already know this is sub-optimal because V2 pairs support direct transfers between themselves, but that’s fine. The AR loop should be able to discover this!

The model then wrote an experiment log (log.jsonl), configuration (config.json), scripts for experiment measurement (measure.sh) and checking (checks.sh), and a prompt for the model (prompt.md).

Then it ran a baseline test for the starting V2-V2-V2 scenario: 202,464 gas.

Run

Then I started the AR run using the extension command /autoresearch minimize V2-V2-V2 gas use.

It quickly started iterating over improvements and opened an experiment result overlay in the terminal showing results of its experiments:

It ultimately made 18 passes, reducing the gas use to 169,765 with a hyper-optimized contract that only includes one dedicated V2-V2-V2 function:

"""
Command Executor — specialized for V2-V2-V2 triangle arbitrage.
Uses direct-to-next-pool swap routing to eliminate intermediate transfers.
"""

from ethereum.ercs import IERC20
from .interfaces.UniswapV2 import IUniswapV2Pair

OWNER: immutable(address)
WETH: immutable(address)
_locked: transient(uint256)

@deploy
def __init__(weth: address):
    OWNER = msg.sender
    WETH = weth

@external
def execute_v2_v2_v2(
    pool1: address, 
    pay_amount1: uint256,
    a0out1: uint256, 
    a1out1: uint256,
    pool2: address, 
    a0out2: uint256, 
    a1out2: uint256,
    pool3: address, 
    a0out3: uint256, 
    a1out3: uint256,
) -> bool:
    assert msg.sender == OWNER, "unauthorized"
    assert self._locked == 0, "reentrant call"
    self._locked = 1

    extcall IERC20(WETH).transfer(pool1, pay_amount1)
    extcall IUniswapV2Pair(pool1).swap(a0out1, a1out1, pool2, b"")
    extcall IUniswapV2Pair(pool2).swap(a0out2, a1out2, pool3, b"")
    extcall IUniswapV2Pair(pool3).swap(a0out3, a1out3, self, b"")

    return True

I want to highlight one nice optimization that AR found: Since Uniswap V2 performs the post-swap check against reserve values held in storage, you can transfer tokens to it before calling swap. So you can effectively transfer the intermediate amounts directly between pools by passing pool2’s address to the swap call at pool1. This reduces the number of ERC-20 transfers to the theoretical minimum, n_pools + 1.

But you’ll also noticed that is stripped the command stream mechanism and transient storage features completely! This makes sense because the feedback mechanism only tracked the gas use of a V2-V2-V2 chained swap, so it was correct to focus on that slice and ruthlessly cut anything not supportive of that goal. Hard-coding the swaps is absolutely the right call there, but there are more optimizations still on the table.

IMPORTANT LESSON: optimizations selected by the AR loop are downstream of the feedback mechanism.

Close that AR loop by running /autoresearch off, then start another with a new more specific goal: /autoresearch Optimize V2-V2-V2 arbitrage without relying on an initial balance held by the execution contract.

User's avatar

Continue reading this post for free, courtesy of BowTiedDevil.

Or purchase a paid subscription.
© 2026 BowTiedDevil · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture