Shaving 50,000+ Gas Off EVM Arbitrage Contracts: Direct Pool Swaps, Warm Storage Dust, and Callback Locks

In high-frequency on-chain multihop arbitrage and MEV competition, even small differences in gas efficiency can determine the outcome.

Suppose a large sell order creates a 0.015 WETH gross arbitrage opportunity for a long-tail token between Uniswap V2 and Aerodrome, on Base or Ethereum mainnet. Bots A and B detect it in the same millisecond-scale step and calculate identical optimal input amounts.

Bot A calls the standard UniswapV2Router02 contract and withdraws every token balance after each arbitrage. Bot B uses a custom native flash-loan multihop contract, bypasses the router to interact directly with liquidity pools, and deliberately leaves 1 wei in token balance slots when withdrawing profits.

Bot A consumes 265,000 gas. After priority fees, its net return becomes negative, or its high gas limit delays inclusion until slippage causes a revert. Bot B’s lean instructions and prewarmed storage consume only 173,000 gas: over 90,000 gas saved in one transaction. It can offer the builder a 30% higher bribe, secure top-of-block inclusion, and retain a net profit.

Under EVM Yellow Paper rules, gas optimization is a precise exercise in storage (SSTORE), external calls (CALL), and memory allocation, rather than syntactic shortcuts. Using our production arbitrage contract on Base mainnet (evm-arbitrage/src/FlashMultiHopArb.sol), this article examines 1-wei storage prewarming, direct pool calls that bypass routers, and nested singleton callback locks to save at least 50,000 gas per high-frequency multihop trade.


Gas Optimization: Three Main Opportunities

Before examining the code, compare the sources of gas consumption and their corresponding optimizations:

Dimension Conventional high-gas implementation Production optimization (FlashMultiHopArb) Savings and mechanism
Storage slot temperature Withdraw all profits after each trade; balance returns to 0 Keep 1 wei with withdrawProfitKeepDust Save 17,100 gas per hop (avoid cold 0 -> nonzero writes; 51,300 gas across 3 hops)
Routing Call UniswapV2Router02.swapExactTokensForTokens Transfer directly to Pair and call pair.swap 35%+ less bytecode overhead (remove redundant encoding, memory copies, and proxy calls)
Reentrancy and callbacks OpenZeppelin ReentrancyGuard with extra storage slot Singleton address lock activeCallbackPool, also authenticating callers Save 5,000+ gas (one slot handles reentrancy and callback-source validation)
Transfer-tax compatibility Trust amountIn; high-tax tokens revert Verify balanceOf(pool) - reserveIn Zero extra calls (existing reserves support tax defense and exact accounting)

Optimization 1: SSTORE Costs and 1-Wei Dust Prewarming

EIP-2200 and EIP-2929 introduced tiered pricing for Ethereum’s SSTORE instruction. Understanding the cost of changing a slot from zero to nonzero is fundamental to highly optimized contracts.

1. The Actual Gas Cost of Storage Writes

An IERC20.transfer modifies the token contract’s mapping(address => uint256) balances. The storage transitions cost:

  1. Cold storage write: 0 -> nonzero:
    • Base access: 2,100 gas for the first access to a cold slot.
    • Slot initialization: 20,000 gas.
    • Minimum total: 22,100 gas.
  2. Warm storage modification: nonzero -> nonzero:
    • Base access: 100 gas for a warmed slot.
    • Value update: 2,900 gas if the value changes and remains nonzero.
    • Minimum total: approximately 3,000 gas.

Changing an empty address balance from 0 to a positive amount costs 17,100 gas more than modifying an address that already holds a small balance.

2. Three Cold Writes in a Multihop Trade

In a typical three-hop triangular arbitrage, such as WETH → TokenA → TokenB → WETH:

  • Buying TokenA changes the contract’s TokenA balanceOf from 0 to nonzero: the first cold write, 20,000 gas.
  • Selling TokenA for TokenB changes its TokenB balanceOf from 0 to nonzero: the second cold write, 20,000 gas.
  • Selling TokenB for WETH changes its WETH profit slot from 0 to nonzero: the third cold write, 20,000 gas.

If the bot owner withdraws every token after each trade, all three slots incur cold writes again at the next opportunity, wasting 17, 100 × 3 = 51, 300 Gas.

3. Implementing withdrawProfitKeepDust

FlashMultiHopArb uses dust holding to avoid this cost. Profit withdrawals leave 1 wei (10−18 tokens, effectively zero value) at the contract address, permanently preserving the slot’s nonzero warm state:

// From evm-arbitrage/src/FlashMultiHopArb.sol
    /**
     * @notice Withdraw profit but retain 1 wei (dust holding), keeping this token's balance slot nonzero.
     *         Each later trade avoids the 17,100 gas penalty for an ERC20 balance write from 0 to nonzero.
     */
    function withdrawProfitKeepDust(address token) external onlyOwner {
        uint256 bal = IERC20(token).balanceOf(address(this));
        if (bal > 1) {
            safeTransfer(token, owner, bal - 1);
        }
    }

    /**
     * @notice Clear all tokens, including 1 wei dust (only when abandoning the pair or retiring the contract)
     */
    function withdrawToken(address token, uint256 amount) external onlyOwner {
        safeTransfer(token, owner, amount);
    }

Subsequent arbitrage operations keep all involved ERC-20 balance changes in the cheaper warm-write range, saving at least 50,000 gas per transaction.


Optimization 2: Bypass the Router and Call the Pair Directly

Developers new to DeFi arbitrage often call UniswapV2Router02 from their contracts. The official router is a general-purpose facade designed to protect ordinary frontend users, with many defenses and intermediate steps unnecessary for arbitrage.

1. Four Sources of Router Overhead

Calling router.swapExactTokensForTokens creates these redundant costs:

  1. Repeated encoding and memory allocation: construct path[] in the arbitrage contract, then abi.decode again in the router, paying memory-expansion gas.
  2. Extra approval and transfer calls: the router first uses safeTransferFrom to pull tokens from the arbitrage contract into itself, then transfers them into the pool, making two external ERC-20 calls.
  3. Redundant timestamp and slippage checks: arbitrage is atomic and packaged by the searcher with a flash loan for a specific block. The router’s ensure(deadline) and generic slippage checks add unnecessary computation.
  4. No native fee-on-transfer defense: a long-tail token with a 1% buy tax makes the router’s calculated output differ from the actual pool input, reverting the entire transaction.

2. Direct Pool Interaction (_swapV2)

In evm-arbitrage/src/FlashMultiHopArb.sol, we bypass the router entirely and use IUniswapV2Pair.swap directly:

// From evm-arbitrage/src/FlashMultiHopArb.sol
    function _swapV2(
        SwapStep memory step,
        uint256 amountIn,
        uint256 hopIndex
    ) internal returns (uint256 amountOut) {
        IUniswapV2Pair pair = IUniswapV2Pair(step.pool);
        (uint256 r0, uint256 r1, ) = pair.getReserves();

        bool zeroForOne = step.tokenIn < step.tokenOut;
        (uint256 reserveIn, uint256 reserveOut) = zeroForOne ? (r0, r1) : (r1, r0);

        // 1. Optimistic transfer: push input tokens directly from this contract to Pair
        safeTransfer(step.tokenIn, step.pool, amountIn);

        // 2. Core defense: read actual pool balance increase (handles hidden transfer taxes)
        uint256 actualAdded = IERC20(step.tokenIn).balanceOf(step.pool) - reserveIn;

        // 3. Compute theoretical maximum output after fees from actual received input
        uint256 amountInWithFee = actualAdded * step.feeBps;
        amountOut = (amountInWithFee * reserveOut) / (reserveIn * 10000 + amountInWithFee);
        if (amountOut == 0) revert ZeroOutputOnHop(hopIndex);

        // 4. Record output-token balance before receiving the low-level transfer
        uint256 balBefore = IERC20(step.tokenOut).balanceOf(address(this));
        
        // 5. Call swap directly without bytecode redirection
        pair.swap(
            zeroForOne ? 0 : amountOut,
            zeroForOne ? amountOut : 0,
            address(this),
            new bytes(0)
        );

        // 6. Return the actual principal received for the next hop
        amountOut = IERC20(step.tokenOut).balanceOf(address(this)) - balBefore;
    }

This minimal implementation provides three advantages:

  • Lower gas: removing the routing layer halves the call stack and saves at least 35% gas per swap compared with using the router.
  • Adaptive transfer-tax handling: actualAdded = balanceOf(pool) - reserveIn uses the pool’s actual received amount, even with a 2% transfer tax or burn mechanism, preventing overflow or shortfall.
  • Actual-receipt accounting: balanceOf(this) - balBefore establishes the true next-hop input, eliminating floating-point truncation and rounding discrepancies.

Optimization 3: A Compact Singleton Callback Lock (activeCallbackPool)

Flash-loan arbitrage contracts must reject forged callbacks. An attacker may call uniswapV3FlashCallback directly with fabricated arguments to exploit the contract’s approvals or drain residual funds.

A conventional approach inherits OpenZeppelin ReentrancyGuard. Its separate uint256 private _status slot switches between 1 and 2, requiring another expensive SSTORE. It prevents reentrancy but cannot determine whether the caller is a legitimate flash-loan pool.

1. Combine Reentrancy Protection and Caller Authentication

FlashMultiHopArb uses one address slot, address private activeCallbackPool, as both a reentrancy lock and a live caller allowlist:

// From evm-arbitrage/src/FlashMultiHopArb.sol
    // Callback safety lock: prevent malicious contracts forging Flash/Swap callbacks
    address private activeCallbackPool;

    function executeArbitrage(
        address flashPool,
        address borrowToken,
        bool borrowIsToken0,
        uint256 amountBorrow,
        uint256 minProfit,
        uint256 bribeBps,
        SwapStep[] calldata steps
    ) external onlyOwner returns (uint256 netProfit) {
        // ... Assemble initial parameters ...

        // Lock to the currently authorized flash-loan pool
        activeCallbackPool = flashPool;

        // Initiate V3 flash loan
        IUniswapV3Pool(flashPool).flash(
            address(this),
            borrowIsToken0 ? amountBorrow : 0,
            borrowIsToken0 ? 0 : amountBorrow,
            data
        );

        // Clear immediately after execution to restore unlocked state
        activeCallbackPool = address(0);

        // ... Profit accounting and bribe distribution ...
    }

    // V3 flash-loan callback
    function uniswapV3FlashCallback(uint256 fee0, uint256 fee1, bytes calldata data) external {
        // Reject every caller other than the currently locked flash-loan pool!
        if (msg.sender != activeCallbackPool) revert Unauthorized();

        // ... Execute multihop swaps ...
    }

2. Supporting Nested Callbacks

Multihop trades can nest a swap against another Uniswap V3 pool inside an outer Uniswap V3 flash loan. To support this topology without corrupting the lock, _swapV3 saves and restores it through a stack variable:

// From evm-arbitrage/src/FlashMultiHopArb.sol
    function _swapV3(
        SwapStep memory step,
        uint256 amountIn,
        uint256 hopIndex
    ) internal returns (uint256 amountOut) {
        bool zeroForOne = step.tokenIn < step.tokenOut;
        
        // Save the parent callback lock and temporarily authorize this V3 swap pool
        address prevPool = activeCallbackPool;
        activeCallbackPool = step.pool;

        uint256 balBefore = IERC20(step.tokenOut).balanceOf(address(this));

        IUniswapV3Pool(step.pool).swap(
            address(this),
            zeroForOne,
            int256(amountIn),
            zeroForOne ? MIN_SQRT_RATIO_PLUS_ONE : MAX_SQRT_RATIO_MINUS_ONE,
            abi.encode(step.tokenIn)
        );

        // Restore the parent lock
        activeCallbackPool = prevPool;
        amountOut = IERC20(step.tokenOut).balanceOf(address(this)) - balBefore;
        if (amountOut == 0) revert ZeroOutputOnHop(hopIndex);
    }

This design closes the callback authorization flow at very low gas cost.


Foundry Test Evidence

In evm-arbitrage/test/FlashMultiHopArb.t.sol, Foundry fork tests verify these optimizations against the Base mainnet environment:

// From evm-arbitrage/test/FlashMultiHopArb.t.sol
    function test_RealFlashArbitrageRepaysFeeAndWithdrawsProfit() public {
        _createPriceGap(); // Simulate a real mainnet price gap
        
        uint256 flashBalanceBefore = IERC20(WETH).balanceOf(FLASH_POOL);
        uint256 gasBefore = gasleft();
        
        // Execute arbitrage
        uint256 profit = _execute(0.001 ether, 0);
        uint256 executionGas = gasBefore - gasleft();
        uint256 fee = _flashFee();

        // Assert loan principal and fee repaid, with profit accounted correctly
        require(IERC20(WETH).balanceOf(FLASH_POOL) == flashBalanceBefore + fee, "Flash fee not repaid");
        require(IERC20(WETH).balanceOf(address(arb)) == profit, "Incorrect executor profit");

        // Verify dust-preserving withdrawal leaves exactly 1 wei
        arb.withdrawProfitKeepDust(WETH);
        require(IERC20(WETH).balanceOf(address(arb)) == 1, "Dust missing");

        // Verify complete withdrawal returns the balance to zero
        arb.withdrawToken(WETH, 1);
        require(IERC20(WETH).balanceOf(address(arb)) == 0, "Dust withdrawal failed");
    }

    function test_RejectsUnsolicitedFlashAndSwapCallbacks() public {
        // Assert unauthorized forged callbacks are rejected, preventing reentrancy and spoofing
        vm.prank(ATTACKER);
        vm.expectRevert(abi.encodeWithSelector(FlashMultiHopArb.Unauthorized.selector));
        arb.uniswapV3FlashCallback(0, 0, "");

        vm.prank(ATTACKER);
        vm.expectRevert(abi.encodeWithSelector(FlashMultiHopArb.Unauthorized.selector));
        arb.uniswapV3SwapCallback(1, 0, abi.encode(WETH));
    }

Four Production Boundaries for Gas Optimization

  1. Do not leave 1-wei dust for unknown contracts: dust holding is appropriate only for known tokens you repeatedly arbitrage, such as WETH, USDC, and AERO. Keeping 1 wei of every one-off low-quality meme token accumulates meaningless assets.
  2. Avoid uint112 reserve assumptions: upgraded Aerodrome or custom DEX implementations on Base may use uint256 reserves. Reading uint256 directly prevents numeric overflow.
  3. Preserve atomic rollback: gas savings must not weaken safety. Enforce require(netProfit >= minProfit) at the end of execution; insufficient profit or excess slippage must revert every pool change rather than complete a loss-making trade.
  4. Use safeTransfer instead of standard .transfer(): older or nonstandard tokens such as USDT do not return a bool after successful transfer. Low-level call plus return-data length checks saves gas and is the only way to handle nonstandard ERC-20 behavior safely.

Enterprise DeFi and High-Frequency Trading Architecture Consulting

Shaving 50,000+ Gas Off EVM Arbitrage Contracts: Direct Pool Swaps, Warm Storage Dust, and Callback Locks

Fone works on low-level EVM architecture, market-maker liquidity management, low-latency arbitrage settlement engines, and full-stack white-label DEX delivery. Contact us if you are building a high-frequency trading bot, reducing contract execution gas, or auditing complex on-chain state machines: