Skip to content

test(fork): swap end-to-end with MEV decay verification - #17

Merged
defidough merged 12 commits into
mainfrom
test/swap-fork-coverage
May 29, 2026
Merged

test(fork): swap end-to-end with MEV decay verification#17
defidough merged 12 commits into
mainfrom
test/swap-fork-coverage

Conversation

@defidough

Copy link
Copy Markdown
Contributor

Summary

Third mainnet-fork suite — actual swaps against real v4 Base mainnet. Exercises the hook's `beforeSwap` / `afterSwap` fee accounting end-to-end including the `BeforeSwapDelta` on specified side, hook 6909 mint, and `sync` / `settle` / `take` reconciliation.

TestSwapper helper

  • Custom `IUnlockCallback` that calls `PoolManager.swap` inside `unlock`, then settles input via `sync` + `transferFrom` + `settle` and takes output to recipient.
  • Sidesteps UniversalRouter `V4_SWAP` plumbing (not in our pinned UR version) while still hitting the full hook lifecycle.
  • ~110 LOC, reusable across all swap-flavoured fork tests.

SwapMainnetFork suite (3 tests, all PASS against real Base)

Test What it verifies
`test_swap_at_launch_pays_mev_decay_fee` A 0.01 WETH buy at t=0 succeeds; buyer's WETH fully consumed; got some TOKEN out (the 80% LP + 16% hook fee doesn't blow up the swap)
`test_swap_after_decay_pays_base_fee` Warp past 120s decay, same 0.01 WETH buy succeeds at the base 1.2% effective rate
`test_swap_at_launch_vs_post_decay_buyer_gets_more_post_decay` Real regression assertion — same WETH input, post-decay buyer gets strictly more TOKEN than snipe buyer. Confirms MEV decay actually works end-to-end.

All three pass locally against real Base mainnet at ~4.7M gas per launch + swap.

Stacking

This branches off main with the #16 fix merged in locally so the test runs. Will rebase cleanly once #16 lands.

Test plan

defidough added 12 commits May 29, 2026 20:57
…llowlist

Two real mainnet integration bugs caught by running the Phase 4
fork tests against live Base. Both pass now (Classic Mainline:
4.5M gas, Rail launch with WEDGE bootstrap: 9.9M gas).

Bug 1 — Permit2 allowance pattern
  v4 PositionManager pulls ERC20s via Permit2 AllowanceTransfer,
  not via direct token allowance. The lockers were doing
  forceApprove(POSITION_MANAGER, supply), so SETTLE_PAIR reverted
  with AllowanceExpired(0). Fix in WedgeLpLocker.placeLiquidity
  and WedgeRailExtension.receiveTokens: approve Permit2 then call
  IAllowanceTransfer(PERMIT2).approve(token, PositionManager,
  amount, block.timestamp + 1 hours). PERMIT2 is the canonical
  0x000000000022D473030F116dDEE9F6B43aC78BA3 deploy, same on
  every EVM chain.

Bug 2 — Rail locker is not a Mainline locker
  WedgeRailLocker has no placeLiquidity and doesn't implement
  IWedgeLpLocker — its custody role is wired through the Rail
  extension (which safeTransferFroms LP NFTs to it). I wrongly
  allowlisted it via Launchpad.setLocker in both the deploy
  script (04_ConfigureAllowlists) and the fork setUp. The
  Launchpad's setLocker calls supportsInterface(IWedgeLpLocker)
  which the Rail locker doesn't implement — the deploy reverted
  on that staticcall. Removed the bad allowlist call in both
  places, with a comment explaining why.

Test status:
- Classic Mainline fork test: PASS against real Base PoolManager
  (0x4985...2b2b) + PositionManager (0x7C5f...9bDc).
- Rail launch fork test: PASS — full WEDGE bootstrap, setProtocolToken,
  then a second-token Rail launch with the on-chain WEDGE/WETH
  spot read producing the right Rail starting tick.

Both ran locally with BASE_RPC_URL set inline; nothing about the
RPC key was persisted to disk.
Third mainnet-fork suite. Exercises a full swap against the real
v4 PoolManager + WedgeMainlineHook + locker, including the hook's
beforeSwap/afterSwap fee accounting (BeforeSwapDelta on specified
side, hook 6909 mint, sync/settle/take pattern).

TestSwapper helper:
  - Custom IUnlockCallback that calls PoolManager.swap inside
    unlock, then settles input via sync + transferFrom + settle
    and takes output to recipient.
  - Sidesteps UniversalRouter V4_SWAP plumbing (not in our pinned
    UR version) while still hitting the full hook lifecycle.
  - ~110 LOC, reusable across all swap-flavoured fork tests.

SwapMainnetFork suite (3 tests, all PASS against real Base):
  - test_swap_at_launch_pays_mev_decay_fee: 0.01 WETH buy at t=0
    succeeds; buyer's WETH fully consumed; got some TOKEN out.
  - test_swap_after_decay_pays_base_fee: warp past 120s decay,
    same 0.01 WETH buy succeeds at base 1.2% effective rate.
  - test_swap_at_launch_vs_post_decay_buyer_gets_more_post_decay:
    REAL regression assertion — same WETH input, post-decay buyer
    gets strictly more TOKEN than the snipe buyer. Confirms MEV
    decay works end-to-end.

Stacked on top of #16 (Permit2 fix) — that PR is merged in
locally for the test to run; will rebase clean once #16 lands.

153 unit tests + 6 fork tests across 3 suites.
Unit tests don't fork mainnet, so the canonical Permit2 address
has no bytecode and the lockers' new
IAllowanceTransfer(PERMIT2).approve calls reverted. Fix: etch a
MockPermit2 (single no-op approve function) at the canonical
address in the affected setUps.

Real Permit2 semantics are exercised by the fork tests, which
hit the live mainnet deploy at 0x0000...78BA3.

153/153 unit tests green.
Fourth mainnet-fork suite. Verifies the complete fee economics
work against real Base v4:

  test_hook_fee_sweeps_to_factory_on_next_swap:
    snipe buy at t=0 → hook accumulates ERC-6909 internally,
    factory still has 0 WETH. Second swap → hook's beforeSwap
    sweeps the prior fee to factory. Asserts factory's WETH
    balance went from 0 to > 0.

  test_claimTeamFees_pushes_to_treasury:
    two swaps to seed the factory with WETH fees. Owner calls
    Launchpad.claimTeamFees(WETH). Asserts treasury receives
    exactly the factory's full WETH balance and factory is
    drained.

  test_locker_collectFees_distributes_to_creator:
    snipe + post-decay swap → LP positions accrue WETH fees.
    Permissionless WedgeLpLocker.collectFees(token) called.
    Asserts creator receives some WETH (LP-side). TOKEN balance
    unchanged because all buys went WETH → TOKEN (no
    TOKEN-side fee accrual yet).

Three tests, all PASS against real Base mainnet. Stacked on top
of the swap fork PR; rebases clean once #16 lands.
The buy tests above hit beforeSwap's BeforeSwapDelta on the
specified side (cases 1 & 2 in WedgeMainlineHook). Sells hit
afterSwap's unspecifiedDelta path (cases 3 & 4). Without this
test, any amount0/1 flipping bug or wrong sign on the afterSwap
unspecifiedDelta computation would slip through.

Test:
  1. Buy TOKEN post-decay so the seller has inventory.
  2. Approve swapper for half the acquired TOKEN.
  3. Sell that half back for WETH (zeroForOne flips with
     tokenIsCurrency0).
  4. Assert wethOut > 0.

PASSES against real Base mainnet. All 4 hook delta cases now
exercised end-to-end across the fork suite.
Fifth mainnet-fork suite. Verifies the full Wedge Rail mechanism:

  test_rail_swap_wedge_for_token_succeeds:
    Bootstrap WEDGE → launch TOKEN with Rail extension → deal
    WEDGE to buyer → swap WEDGE→TOKEN through the hookless Rail
    pool. Asserts buyer's WEDGE fully consumed, got TOKEN out.

  test_rail_collectFees_routes_to_treasury:
    Same setup + swap, then permissionless
    WedgeRailLocker.collectFees(token). Asserts treasury's WEDGE
    balance increased (100% of Rail fees → treasury per Phase 0.4),
    no TOKEN balance change (only swapped WEDGE in).

Both pass against real Base mainnet at ~10M gas each (full
bootstrap + Rail seeding + swap + fee collect).
The Rail launches single-sided with zero WEDGE on the WEDGE side
(launchpad seeds only TOKEN at the band tickLowers). A TOKEN→WEDGE
swap therefore can only succeed after at least one WEDGE→TOKEN
buy has populated the pool's WEDGE side.

Test pattern:
  1. Buyer puts WEDGE in, gets TOKEN out (pool now has WEDGE).
  2. Seller (fresh addr) puts TOKEN in, gets WEDGE out.

PASS against real Base. Confirms the Rail mechanism works both
ways once the pool is seeded — important for arb between Mainline
and Rail (arb flows in both directions over time).
It only returns the selector constant — no state read or write.
Eliminates the persistent forge build warning that's been
spamming every CI run.
@defidough
defidough merged commit 88ef450 into main May 29, 2026
2 checks passed
@defidough
defidough deleted the test/swap-fork-coverage branch May 29, 2026 21:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant