diff --git a/astro.config.mjs b/astro.config.mjs index d3ec2f4..5a1d786 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -248,6 +248,7 @@ export default defineConfig({ label: "Price representation", slug: "reference/price-representation", }, + { label: "Gas efficiency", slug: "reference/gas-efficiency" }, { label: "Contract addresses", slug: "reference/contracts", diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 0000000..022e406 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,964 @@ +# Gas benchmark harnesses + +Foundry tests behind the numbers on the [Gas efficiency](/reference/gas-efficiency/) page. +Each protocol compiles under its own production toolchain settings, so the comparison +measures code and architecture rather than compiler tuning. + +## Layout + +- `lab/ekubo/` — Ekubo EVM swaps, mints and pool initialization (`EkuboGas.t.sol`, plus + `Flame.t.sol` for the single-call flamegraph capture) +- `lab/v4/` — Uniswap v4 (`V4Gas.t.sol`, `Flame.t.sol`) +- `lab/v3/` — Uniswap v3 (`V3Gas.t.sol`, `Flame.t.sol`) +- `lab/MockERC20.json` — the single shared token artifact deployed via `deployCode` + in all three lab harnesses, so ERC20 transfer costs cannot skew the comparison +- `prod/` — the headline bench: production Ekubo, Uniswap v3 and Uniswap v4 contracts on + a fork of the latest mainnet block with fresh ERC20 tokens (`test/Prod*.t.sol`, + `snapshots/*.json`, `calldata/*.hex` with the exact bytes of every measured call, + `encode-prod-routes.mjs` for the SDK routes, `run.sh`); see "Production-contract bench" + below +- `pools-per-swap/` — the pools-touched-per-swap-transaction measurement behind the + chain-wide section (`measure.mjs`, `results.json` for the 1,000-block window, + `results-30d.json` for the 30-day stride sample); see below +- `fork/` — mainnet-fork validation at pinned block `25991868` + (`ForkEkuboReal.t.sol`, `ForkV3.t.sol`, `ForkV4.t.sol` for swaps; + `ForkMintEkubo.t.sol`, `ForkMintV3.t.sol`, `ForkMintV4.t.sol` for mints through the + production position managers; `ForkProbe.t.sol` for the pool identifiers; + `calldata-*.hex` reference calldata, `encode-yul-route.mjs`) +- `fork-l2/` — Base and Arbitrum One fork swaps (`ForkL2Ekubo.t.sol`, `ForkL2V3.t.sol`, + `ForkL2V4.t.sol`, one snapshot JSON per test contract, `calldata/*.hex` with the exact + bytes of every measured call, `encode-l2-routes.mjs` for the SDK routes, + `l1-data-cost.mjs` and its raw output `l1-data-cost.json` for the L1 data component, + `quote-live-base.mjs`); see "L2 fork methodology" below + +`*.json` files next to each harness are the raw `snapshots/` output of a full run of +that test contract. `remappings.txt` files contain the original absolute paths and will +need adjusting; the dependency pins below are what matter for reproduction. + +## What a snapshot number is + +Every measured call runs under `forge-config: default.isolate = true`. Isolation gives +each top-level call from the test its own EVM context, so end-of-call refunds are +applied, and `vm.snapshotGasLastCall` reports **the execution gas of that call, net of +refunds**. It does not include the 21,000 transaction base cost or calldata: the +`Flame.t.sol` captures are not isolated, and the flamegraph frame for the same router +call (for example `Router.swapAllowPartialFill` at 87,416) equals the isolated snapshot +(`ekubo single erc20 steady-state`, 87,416) to the gas unit. Transaction totals on the +docs page are computed as snapshot + 21,000 + the calldata gas of a reference file. + +Refunds matter for one series. Isolated snapshots are post-refund, capped at 1/5 of gas +used (EIP-3529). The v3 harness records `gross before refund` and `refund granted` next to +every snapshot via `vm.lastCallGas`; every Ekubo and v4 measurement has zero refund, so +their snapshot, `lastCallGas().gasTotalUsed`, and a `gasleft()` delta around the call +agree to within the caller's own overhead (roughly 1–5k, the CALL and argument copying). + +## Production-contract bench (`prod/`) + +The page's headline and route-scaling numbers. Every contract that executes a measured +call is the deployed production one on Ethereum mainnet, read from a fork of the latest +block at run time (`run.sh`; no block pin, so the bench reproduces on any day — the +committed snapshots record the block they were taken at, `prod fork block number` +25999935, timestamp 1789680959, 2026-09-17 21:35:59 UTC). Only the tokens are new: + +- **Tokens.** Four fresh solmate `MockERC20` contracts (the lab artifact, + `artifacts/MockERC20.json`) etched at fixed, address-ordered addresses + `0x1111…1111` (A) `< 0x2222…2222` (B) `< 0x3333…3333` (C) `< 0x4444…4444` (D), asserted + codeless on the chain before etching. Fixed addresses make the SDK route calldata + reproducible; all-nonzero bytes make it pay the full 16 gas per address byte (a real + address has about one zero byte in 256, so this is marginally pessimistic for every + leg alike). Plain ERC20 transfers, no proxy, no blocklist, no fee-on-transfer: the + USDC/USDT fork numbers below keep the expensive-token case. +- **Contracts.** Ekubo Core `0x00000000000014aA86C5d3c41765bb24e11bd701`, Positions + `0x02D9876A21AF7545f8632C3af76eC90b5ad4b66D` (asserted `name() == "Ekubo Positions"`), + Yul router `0x7B2aA7Ecc0B5936b7C52E6259A19C3BA557d0748`; Uniswap v3 factory + `0x1F98431c8aD98523631AE4a59f267346ea31F984` and SwapRouter + `0xE592427A0AEce92De3Edee1F18E0157C05861564` (asserted `factory()` and `WETH9()`); + Uniswap v4 PoolManager `0x000000000004444c5dc75cB358380D2e3dE08A90`, Universal Router + `0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af` and Permit2 + `0x000000000022D473030F116dDEE9F6B43aC78BA3`. Every address is checked for code on the + fork; the Ekubo addresses are the ones in the docs' EVM contract reference. +- **Pools.** A/B, B/C, C/D and a native pool ETH/B on each protocol, 0.3% fee, Uniswap + tick spacing 60 (Ekubo spacing 6000; its ticks are 100x finer), initialized strictly + inside a tick (Uniswap tick 30, Ekubo tick 3050 = Uniswap 30.5, so Ekubo's pools sit + 0.005% higher and its outputs are 0.005% larger; irrelevant to gas). One full-range + position per pool of the liquidity that 1M tokens a side buys at that price + (v3 mints the same liquidity figure as the lab, 998501199320305883812938). Ekubo pools + are asserted uninitialized on the fork before creation; Uniswap pools are asserted + absent from the factory / PoolManager. Capitalization is unmeasured: Ekubo through the + production Positions manager (`mintAndDepositWithSalt` with explicit salts — the + deployed manager's `mintAndDeposit` derives its salt from prevrandao and remaining + gas, which back-to-back mints in one context share), v3 by a direct `pool.mint` with + the test as callback payer, v4 through v4-core's `PoolModifyLiquidityTest`. +- **Isolation and warmth.** Every measured call is a top-level call from the test under + `isolate = true`, so `vm.snapshotGasLastCall` is that call's execution gas net of + refunds, excluding the 21,000 base and calldata. One identical, unmeasured call + precedes every measurement, so every storage slot the swap touches is already nonzero + (pool state, both fee-growth accumulators, the recipient's output balance, which is + asserted nonzero before the measured call, and the router's approvals). Pool creation, + tick initialization, first-swap accumulator writes and NFT mints are therefore + excluded by design. Every measured swap is exact-input 1 token; the last pool's tick is + asserted to move by at most one Uniswap tick (measured: 0 on Uniswap, 2 fine ticks = + 0.02 Uniswap ticks on Ekubo), and the router's reported output is asserted equal to + the recipient's balance change. +- **Calldata.** The exact bytes of every measured call are written to `calldata/*.hex` + and recorded in the snapshot as byte count, EIP-2028 gas (4 per zero byte, 16 per + nonzero byte) and EIP-7623 floor tokens; the floor never binds here (execution is far + above it). Transaction gas on the page = snapshot + 21,000 + EIP-2028 calldata gas. +- **Paths.** Two tiers per protocol. Production routers: the Ekubo Yul router with + SDK-generated routes (`encode-prod-routes.mjs`, 184 / 273 / 362 bytes for 1 / 2 / 3 + pools), the v3 SwapRouter (`exactInputSingle`, `exactInput` with a packed path; the + native leg sends ETH to the router, which wraps it in flight), and the v4 Universal + Router (`execute` with one `V4_SWAP` command: `SWAP_EXACT_IN_SINGLE`, `SETTLE_ALL`, + `TAKE_ALL`, funded through Permit2 with a standing allowance granted in setup; the + `ExactInputSingleParams` layout is the 2025 deployment's, without `minHopPriceX36`, + confirmed by the call succeeding with the same output as the minimal locker to 1e-15). + Minimal single-lock routers: array-based N-hop lockers of the same shape on all three + sides (`MinimalEkuboRouter`: lock, swap each hop, pay once, withdraw once; + `V4MinimalRouter`: unlock, swap each hop, settle once, take once; `V3MinimalRouter`: + the SwapRouter's token flow with 2 transfers per pool). Minimal routers favor + Uniswap: its production routers cost more than the minimal figures, while Ekubo's + production Yul router is cheaper than its minimal locker. + +| Leg (execution gas) | 1 pool | 2 pools | 3 pools | Native ETH in, 1 pool | Calldata bytes (1 / 2 / 3 pools) | +| -------------------------------------- | ------- | ------- | ------- | --------------------- | -------------------------------- | +| Ekubo, production Yul router | 87,663 | 109,850 | 132,025 | 73,764 | 184 / 273 / 362 | +| Ekubo, minimal locker | 89,733 | 112,843 | 135,952 | 74,585 | 260 / 356 / 452 | +| Uniswap v4, Universal Router + Permit2 | 117,114 | — | — | — | 1,092 | +| Uniswap v4, minimal locker | 107,384 | 141,795 | 176,206 | 92,472 | 356 / 548 / 740 | +| Uniswap v3, SwapRouter | 100,299 | 165,719 | 230,478 | 103,504 (wraps ETH) | 260 / 324 / 324 | +| Uniswap v3, minimal router | 94,078 | 152,774 | 211,470 | — | 260 / 324 / 388 | + +Marginal execution gas per extra pool (2→3 pools): Ekubo Yul 22,175 (1→2: 22,187), +Ekubo minimal 23,109, v4 minimal 34,411, v3 minimal 58,696, v3 SwapRouter 64,759 +(1→2: 65,420). The v3 minimal-router figures equal the lab harness's to the gas unit +(94,078 / 152,774 / 211,470), which ties the two benches together. Outputs: Ekubo +1000042490223401887 / 1000084982120610903 / 1000127475691875278 wei for 1 / 2 / 3 +pools, Uniswap v3 999992341035506990 / 999984682148650249 / 999977023339428738, v4 +999992341035507518 / 999984682148651304 / 999977023339430321 (the 0.005% Ekubo +premium is the half-tick higher init price). + +Dependencies (`lib/`, git-ignored): forge-std, solady, `EkuboProtocol/evm-contracts` at +`v3.2.0` as `lib/ekubo` (with its own `lib/forge-std` and `lib/solady`), `Uniswap/v4-core` +(with its `lib/solmate`); `remappings.txt` lists the mapping. Foundry 1.8.3, solc +auto-detected per file (0.8.26 for the Uniswap-side tests, 0.8.33 for the Ekubo test), +`via_ir`, 1,000,000 optimizer runs, `evm_version = "osaka"`. + +## Lab methodology (all three harnesses) + +Each test runs on a fresh chain. Pools are capitalized in setup, and every measured call +is preceded by one identical unmeasured warm-up call, so the number is the steady state +of an established pool. A separate `first swap in fresh pool` measurement has no warm-up: +on v3 and v4 it costs exactly 17,100 more (one zero-to-nonzero SSTORE of the fee-growth +accumulator, 20,000 instead of 2,900; confirmed with `vm.startStateDiffRecording`, which +shows one fresh slot for v3 and v4 and none for Ekubo). Ekubo's `initializePool` writes +both fee-per-liquidity slots up front, so its first swap costs the same as any other +(87,384 against 87,416 in steady state). + +- Concentrated pools, 0.3% fee, one position covering the whole tick space, so no swap + crosses an initialized tick. +- Pools initialized strictly inside a tick (Ekubo tick 3050, Uniswap tick 30), never on a + tick-spacing multiple. +- Economically matched liquidity: 1M tokens a side on Ekubo and v4; v3 mints the + equivalent liquidity (998501199320305883812938) computed for the same amounts. +- 1-token exact-input swaps, both directions. Every recipient already holds the output + token, and every router delivers output to the calling user, so no measured call pays + a zero-to-nonzero balance write the others avoid. +- Each protocol's cheapest path an externally owned account can actually send: + - Ekubo: the production Solidity `Router`. Its single-swap entry + (`swapAllowPartialFill`, tests `test_gas_single_*`) and its multihop entry + (`multihopSwap`, tests `test_gas_route_*`; the 1-pool route is the same swap + through the multihop path). A true-minimal locker (`MinimalEkuboLocker`: lock, + swap, pay, withdraw; tests `test_gas_single_*_minimalLocker`) is recorded for a + like-for-like comparison with the v4 minimal locker. + - v4: a true-minimal locker (`MinimalSwapRouter`: unlock, swap, settle, take; tests + `test_gas_single_*`) and a single-lock multihop router (`MultiHopRouter`, tests + `test_gas_route_*`). v4-core's own `PoolSwapTest` helper is recorded for context + only. + - v3: a minimal router with the production SwapRouter's token flow (`V3MultiHopRouter`: + first hop paid straight from the user, intermediate outputs held by the router, + last hop delivered to the user; tests `test_gas_route_*`). A direct `pool.swap` + with the test contract as callback payer (tests `test_gas_single_*`) is recorded + as a lower bound; an EOA cannot be a v3 callback payer, so that path is not + sendable, and its figure moves by a few gas whenever the test contract's function + set changes (the callback runs in the test contract). +- v3 oracle: the observation is written only by a swap that changes the tick, at most + once per block. Two tests isolate it (`test_gas_single_tickChange_sameBlock`, 89,836, + write skipped; `test_gas_single_tickChange_nextBlock`, 93,712, observation written): + 3,876 gas for the write. +- Mints are new positions with both boundary ticks already initialized (different salt + on Ekubo and v4, different owner on v3); v3 also records adding to an existing position + (`test_gas_mint_samePosition`, 121,496 net). Every v3 mint earns a 2,800 refund. +- Pool initialization (`test_gas_initializePool`) is recorded in each harness's snapshot + file but is a one-time cost and is excluded from the docs page by design, as is every + cold-tick mint variant; the page's mint rows are subsequent mints into initialized + ticks only. Ekubo's `initializePool` pre-writes the two fee-accumulator slots (value 1), + which is why its first swap and its position snapshots cost the same in every pool. +- Mint attribution for the docs page's component table comes from `-vvvv` traces of the + measured mint call: Ekubo `Core.updatePosition` 106,407, settlement 2 × (5,914 + + 12,602 + 2,054) = 41,140, plumbing 31,473; v4 `PoolManager.modifyLiquidity` 63,358, + settlement 2 × (1,898 + 10,602 + 2,499) = 29,998, plumbing 68,832 (v4-core's + `PoolModifyLiquidityTest`, which also reads pool state, balances and deltas); v3 `mint` + frame 141,180 gross minus the callback (24,095) and four balance checks (6,100) = + 110,985 gross pool internals, 108,185 net of the 2,800 refund. A one-off probe (not in + the committed snapshots) that swaps once in each direction before the warm-up mint + measures Ekubo 179,020 (unchanged), v4 201,988 (`modifyLiquidity` 103,158), v3 178,224 + net / 181,024 gross: the Uniswap position fee-growth snapshots go zero-to-nonzero in a + traded pool, +39,800 each. +- `Flame.t.sol` files capture one steady-state call for the flamegraphs; their router + call frames equal the corresponding snapshots (Ekubo Router single 87,416, v4 minimal + locker single 103,533, v3 1-pool route 94,078). The root frame of each SVG also + includes the test's own call into the router. +- Native input: the Ekubo and v4 paths each make exactly two value-bearing calls (the + user's call into the router, then one internal call into Core or PoolManager). + +`vm.cool` was tried while building the harnesses and changed no figure; no committed +test uses it. Switching the v3 and v4 harnesses from `cancun` to `osaka` was checked and +changes none of their swap figures. + +## Fork methodology + +Same pair (USDT to USDC), two sizes (1000 and 100 USDT), warmed with one identical +unmeasured swap, negligible tick movement (0–1 ticks), pinned block `25991868` +(timestamp 1789583783, gas limit 59,999,943), every call isolated as above. The +recipient (the test contract) holds USDT via `deal`, holds USDC after the warm-up +(asserted), and has granted a max approval to the router under test (asserted). Output +amounts are recorded as snapshot values (USDC, 6 decimals): Ekubo 999176773 / 99920076, +v4 999169404 / 99917651, v3 999209400 / 99920949. + +| Leg | Pool | Fee | Spacing | Liquidity at block | Tick | +| ----- | --------------------------------------------------------------------------------------------------------------------------- | ------- | ------- | ---------------------- | ---- | +| Ekubo | USDC/USDT concentrated, no extension, id `0x6fde3244f6fa747ae318aba6e982a4281febad4d22e3b134aa299a83a951895d` | 0.0005% | 50 | 112,376,153,849,596 | 791 | +| v3 | `0x3416cF6C708Da44DB2624D63ea0AAef7113527C6` (deepest of fee 100/500/3000; cardinality 180) | 0.01% | 1 | 28,940,573,461,011,247 | 6 | +| v4 | id `0xe018f09af38956affdfeab72c2cefbcd4e6fee44d09df7525ec9dba3e51356a5`, hooks = `address(0)` (deepest of fee 100/500/3000) | 0.01% | 1 | 380,576,221,280,057 | 6 | + +Ekubo's config word `0x…53e2d6238da480000032` decodes as fee `0x53e2d6238da4` / 2^64 +(0.0005%), concentrated flag set, tick spacing 50 (Ekubo ticks are 100x finer than +Uniswap's). The v4 key with `hooks = address(0)` is confirmed initialized (nonzero +`sqrtPriceX96`), so the pool has no hook. + +- Ekubo: the live pool through the deployed production Yul router, with SDK-generated + calldata (`encode-yul-route.mjs`; `calldata-yul-route.hex` for 1000 USDT, + `calldata-yul-route-100.hex` for 100 USDT). Regenerating the 1000 USDT route from the + SDK reproduces the committed bytes exactly. Output matched the production quoter within + 0.02%. +- v3: the deployed SwapRouter `exactInputSingle` (128,611 at both sizes). +- v4: the deployed PoolManager with a minimal locker (the Universal Router would cost + more). USDT returns no returndata, so approvals and v4 settlement use low-level calls, + mirroring production routers. +- WETH: one `deposit` in steady state (the account already holds WETH), 27,938: what a + v3 user pays to wrap ETH before a native-input swap (`test_fork_weth_deposit`). + +### Fork mints through the production position managers + +`ForkMintEkubo.t.sol`, `ForkMintV4.t.sol` and `ForkMintV3.t.sol` mint a new full-range +position on the same three pools through each protocol's deployed position manager, the +path a liquidity provider actually sends. Same block, same `isolate` discipline; the +snapshot is the execution gas of the measured call. + +- Position: each pool's widest range aligned to its tick spacing (Uniswap spacing 1: + `-887272..887272`, the full tick range; Ekubo spacing 50: `-88722800..88722800`, the + widest multiples of 50 inside `MIN_TICK/MAX_TICK = ±88722835`). Max amounts 10,000 + USDC and 10,000 USDT (`deal`ed 100,000 of each in setup); the v4 leg passes the + liquidity computed from those amounts and the pool's current price + (`LiquidityAmounts.getLiquidityForAmounts`), since `MINT_POSITION` takes liquidity, with + the same amounts as `amount0Max/amount1Max`. Recipient/owner is the test contract; + `deadline = block.timestamp`; no receiver hook is invoked on any leg (plain `_mint`). +- Payment: Ekubo Positions pulls via `transferFrom` from `msg.sender` (ERC20 approval to + Positions); v3 NPM pulls in the pool callback (approval to NPM); v4 PositionManager + pulls through Permit2 (ERC20 approval to Permit2 plus a `Permit2.approve` allowance to + the manager). All approvals are granted in setup and never measured. USDT returns no + returndata, so its approvals are low-level calls. +- Warm-up: one identical, unmeasured mint first. Every mint creates a new NFT, so the + warm-up is a separate position that initializes the boundary ticks if they were cold + and makes the minter's NFT balance nonzero; the measured mint is then a new position + with initialized boundary ticks, the lab condition. On the Ekubo leg the warm-up uses + `mintAndDepositWithSalt` with an explicit salt: `mint()` derives its salt from + `prevrandao` and remaining gas, which two identical isolated calls share, so a plain + repeat reverts with `TokenAlreadyExists`. The measured Ekubo call is the plain + user-facing `mintAndDeposit`. The v4 tokenId is `nextTokenId()` read before the call. +- Boundary-tick state, checked on-chain before the warm-up: both Uniswap pools already had + their full-range ticks initialized (`ticks(...).initialized` on v3, + `getTickLiquidity` on v4); the Ekubo pool had no position at `±88722800` + (`Core.nextInitializedTick`), so its warm-up figure includes tick initialization and + the first NFT for the minter. A second test per leg (`*_coldBoundaryTicks`) mints, after + the same full-range warm-up, a range whose two boundary ticks are asserted + uninitialized (`±500001` on Uniswap, `±88722750` on Ekubo) and asserted initialized + afterwards; its figures, like the warm-up figures, are one-time initialization costs, + recorded in the snapshot files but excluded from the docs page by design. +- Proof of execution, asserted after the measured mint: `ownerOf(id)` is the minter, the + manager's recorded position liquidity equals the returned liquidity (Ekubo + `getPositionFeesAndLiquidity`, v4 `getPositionLiquidity`, v3 `positions`), the pool's + active liquidity equals its value before plus both mints (Ekubo: Core's packed pool + word; v4 `getLiquidity`; v3 `liquidity()`), and the minter's USDC and USDT balances fall + by exactly the amounts pulled. The v4 leg also asserts the manager already stored this + pool key (no first-use pool-key write in the measurement). +- Manager identity, asserted on the fork: v3 NPM `name()` = `Uniswap V3 Positions NFT-V1` + and `factory()` = the v3 factory; v4 PositionManager `name()` = + `Uniswap v4 Positions NFT`, `poolManager()` = the PoolManager, `permit2()` = Permit2; + Ekubo Positions `name()` = `Ekubo Positions`. The Ekubo pool id is re-derived as + `keccak256(abi.encode(USDC, USDT, config))` and asserted equal to the swap leg's id, and + Core's packed state at that slot is asserted initialized; the v4 pool id is re-derived + from the key and asserted equal to the swap leg's id. + +| Leg (execution gas) | Measured (warm, ticks initialized) | Liquidity | Amount0 USDC | Amount1 USDT | +| ---------------------------------- | ---------------------------------- | ------------- | ------------- | -------------- | +| Ekubo Positions `mintAndDeposit` | 244,513 | 9,996,041,392 | 9,992,084,352 | 10,000,000,000 | +| v4 PositionManager `MINT_POSITION` | 297,390 | 9,996,510,484 | 9,993,022,187 | 10,000,000,000 | +| v3 NPM `mint` | 381,653 | 9,996,546,576 | 9,993,094,345 | 10,000,000,000 | + +Ekubo tokenId `25083100237766864067710388918595849694396003226932398993912489470024538291565` +(salt-derived), v4 tokenId 405710, v3 tokenId 1365900. The Ekubo figures are not +comparable to the lab Positions NFT figure (213,104): the fork runs against the real USDC +proxy and USDT contracts, not the shared mock token. + +Calldata reference files (exact byte counts verified by decoding each file): + +| Router call | File | Bytes | Zero / nonzero | Calldata gas | EIP-7623 floor | +| -------------------------------- | -------------------------------------- | ----- | -------------- | ------------ | -------------- | +| Ekubo Yul router route | `calldata-yul-route.hex` | 184 | 68 / 116 | 2,128 | 5,320 | +| Ekubo Solidity `Router` single | `calldata-ekubo-solidity-router.hex` | 132 | 76 / 56 | 1,200 | 3,000 | +| SwapRouter `exactInputSingle` | `calldata-swaprouter-exactinput.hex` | 260 | 188 / 72 | 1,904 | 4,760 | +| Universal Router single V3 swap | `calldata-universal-router-v3swap.hex` | 548 | 464 / 84 | 3,200 | 8,000 | +| Minimal v4 locker (fork harness) | `calldata-v4-minimal-locker.hex` | 292 | 175 / 117 | 2,572 | 6,430 | + +The Yul route and the two Uniswap files are captured production calldata; the Solidity +`Router` and minimal-locker files are the ABI encoding of the same 1000 USDT swap +(`swapAllowPartialFill` on the Ekubo fork pool key, and `ForkSwapRouter.swap` with the +fork test's arguments) and decode back to those arguments with `cast calldata-decode`. +Calldata gas is 4 per zero byte and 16 per nonzero byte; the floor is 10 gas per token +(one per zero byte, four per nonzero byte) and applies only when it exceeds the whole +transaction's regular gas, which it never does here. + +## Lab result tables (archived from the docs page) + +Steady state unless noted (one identical unmeasured warm-up call first). +Execution gas, net of refunds; v3 rows that earn refunds show net (gross, refund). + +Single swaps, cheapest path per protocol (Ekubo production Solidity `Router`, +v4 minimal locker, v3 direct `pool.swap` as an unsendable floor): + +| Swap | Ekubo | Uniswap v4 | Uniswap v3 | +| --------------------------- | ------ | ---------- | ---------------------------------------------- | +| ERC20 to ERC20 | 87,416 | 103,533 | 87,641 | +| ERC20 to ERC20, reverse | 86,334 | 103,358 | 87,040 | +| Native ETH to ERC20 | 72,580 | 88,864 | n/a (pools hold WETH; one `deposit` is 27,938) | +| ERC20 to ERC20, bare locker | 87,077 | 103,533 | n/a (an EOA cannot pay a v3 callback) | + +First swap in a fresh pool, no warm-up (one-time fee-accumulator write on v3/v4): +Ekubo 87,384, v4 120,633, v3 104,741 — exactly 17,100 more on both Uniswap +versions (20,000 gas instead of 2,900 for one zero-to-nonzero write). + +Routes through 1/2/3 pools in one transaction (Ekubo production `Router` +multihop entry, single-lock v4 router, SwapRouter-shaped v3 router): + +| Pools | Ekubo | Uniswap v4 | Uniswap v3, net (gross, refund) | +| ----- | ------- | ---------- | ------------------------------- | +| 1 | 94,904 | 107,360 | 94,078 (94,078, 0) | +| 2 | 119,333 | 141,771 | 152,774 (172,674, 19,900) | +| 3 | 143,774 | 176,182 | 211,470 (251,270, 39,800) | + +Marginals per extra pool: 24,429 / 24,441 (Ekubo), 34,411 (v4), 58,696 net +(78,596 gross) on v3. + +Swap frame attribution, steady-state single (transfers excluded from the core +frame): Ekubo `Core.swap` 20,363 (23%), settlement 37,680 (43%), lock plumbing +29,373 (34%); v4 `PoolManager.swap` 28,884 (28%), settlement 38,024 (37%), +plumbing 36,625 (35%); v3 pool frame 66,430 with internals ~36,800 (39%), +settlement ~29,600 (31%), plumbing 27,648 (29%). + +Bare-tier mints (new position, boundary ticks initialized): Ekubo 179,020, v4 +162,188, v3 138,380 net (141,180 gross, 2,800 refund); Ekubo Positions NFT +213,104; v3 same-position top-up 121,496 net. + +## Pools per swap transaction (mainnet, JSON-RPC) + +The docs page's chain-wide blend uses a measured average of pools touched per swap +transaction. Dune was the intended source (a query over `ethereum.logs`) but no Dune API +key or account was available to this repository, so the measurement is taken from public +JSON-RPC endpoints instead (`eth_getBlockReceipts` and `eth_getBlockByNumber` with full +transactions, read per block). The page uses the **30-day stride sample** below +(`results-30d.json`): every 10th block from `25783810` to `25999800`, 21,600 blocks +read out of the 215,991 spanned (2026-08-18 18:36:47 to 2026-09-17 21:08:59 UTC, tip +pinned 12 blocks behind head at start), so every hour of the month, every day of the +week and every gas regime in it is sampled uniformly. The earlier 1,000-consecutive-block +window `25997046`–`25998045` (`results.json`, 2026-09-17 11:55:11 to 15:16:11 UTC) and a +200-block run before it (Uniswap v2/v3/v4 and Curve only, 1.657 pools per swap) are kept +as the short-window cross-checks; the 30-day sample supersedes both. + +```sh +cd benchmarks/pools-per-swap +node measure.mjs --blocks 21600 --stride 10 --to 25999800 --conc 6 --out results-30d.json # the 30-day sample +node measure.mjs --blocks 1000 --to 25998045 # the 1,000-block window +node measure.mjs --blocks 21600 --stride 10 # a fresh month ending 12 blocks behind head +``` + +Endpoints for the 30-day run (rotated per request on any failure; `--rpc` overrides): +`https://eth.drpc.org`, `https://mainnet.gateway.tenderly.co`, `https://eth.merkle.io`, +`https://eth.rpc.blxrbdn.com`. `ethereum-rpc.publicnode.com`, used for the 1,000-block +window, returns `null` receipts for blocks older than a few days and cannot serve a +month. Every block is accepted only when the receipt list has exactly one entry per +transaction in the block, in order, with matching hashes; otherwise both calls are +retried on the next endpoint. The run made 54,013 RPC calls, 10,803 of them retries +(rate limiting and one endpoint intermittently answering with empty or partial receipt +lists, which the consistency check rejects), at 6 concurrent blocks in about 100 minutes. + +Two measurements are taken over the same blocks: + +1. **Receipts (all flow).** A transaction counts as a swap if it succeeded and emitted at + least one pool-level swap event; its pool count is the number of such events. This + covers routers, aggregators, MEV bundles and direct pool calls alike. Logs exist only + for successful transactions, so failed fills are excluded by construction (the script + keeps a sanity counter for failed transactions with logs; it read 0). +2. **Calldata (router flow only).** For successful transactions sent to the routers + below, the hop count is decoded from calldata: Universal Router `execute` commands + (`V3_SWAP_EXACT_IN/OUT` from the `20 + 23n`-byte path, `V2_SWAP_EXACT_IN/OUT` from + `path.length − 1`, `V4_SWAP` from the single/multi swap actions and `PathKey[]` + length), SwapRouter02 and classic SwapRouter `exactInput/exactOutput` paths and their + `Single` variants (1 pool), including inside `multicall`, Uniswap v2 Router02 `swap*` + paths, and 1inch v6 `unoswap/unoswap2/unoswap3` (1/2/3 pools by construction). 1inch's + generic `swap(executor, desc, data)` carries its route inside opaque executor data and + is reported as "opaque"; the remaining undecoded 1inch selectors were limit-order calls + (`cancelOrder` 0xb68fb020 alone is 239 of the 323 undecoded transactions, then + `fillContractOrder(Args)`, `permitAndCall`), not swaps. Every decoded router transaction + is cross-checked against its own receipt (see the router table). + +### Event set + +Every signature was read from the protocol's own source file (path in the table) and +hashed with `cast keccak` (Foundry 1.8.3); nothing was taken from memory. + +| Family | Source | Event | topic0 | +| ------------------------ | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------- | +| Uniswap v3 | Uniswap/v3-core `IUniswapV3PoolEvents.sol` | `Swap(address,address,int256,int256,uint160,uint128,int24)` | `0xc42079f9…` | +| Uniswap v2 family | Uniswap/v2-core `IUniswapV2Pair.sol` | `Swap(address,uint256,uint256,uint256,uint256,address)` | `0xd78ad95f…` | +| Uniswap v4 | Uniswap/v4-core `IPoolManager.sol` | `Swap(bytes32,address,int128,int128,uint160,uint128,int24,uint24)` | `0x40e9cecb…` | +| PancakeSwap v3 | pancakeswap/pancake-v3-contracts `IPancakeV3PoolEvents.sol` | `Swap(address,address,int256,int256,uint160,uint128,int24,uint128,uint128)` | `0x19b47279…` | +| Curve (stable) | curvefi/stableswap-ng `CurveStableSwapNG.vy` (same as the classic pools) | `TokenExchange(address,int128,uint256,int128,uint256)` | `0x8b3e96f2…` | +| Curve (stable, meta) | same | `TokenExchangeUnderlying(address,int128,uint256,int128,uint256)` | `0xd013ca23…` | +| Curve (crypto v2) | curvefi/curve-crypto-contract `CurveCryptoSwap2ETH.vy` | `TokenExchange(address,uint256,uint256,uint256,uint256)` | `0xb2e76ae9…` | +| Curve (tricrypto/two-ng) | curvefi/tricrypto-ng `CurveTricryptoOptimizedWETH.vy`, twocrypto-ng `Twocrypto.vy` | `TokenExchange(address,uint256,uint256,uint256,uint256,uint256,uint256)` | `0x143f1f8e…` | +| Balancer v2 | balancer/balancer-v2-monorepo `vault/IVault.sol` | `Swap(bytes32,address,address,uint256,uint256)` | `0x2170c741…` | +| Balancer v3 | balancer/balancer-v3-monorepo `vault/IVaultEvents.sol` | `Swap(address,address,address,uint256,uint256,uint256,uint256)` | `0x0874b2d5…` | +| Maverick v1 | maverickprotocol/maverick-v1-interfaces `IPool.sol` | `Swap(address,address,bool,bool,uint256,uint256,int32)` | `0x3b841dc9…` | +| Maverick v2 | maverickprotocol/v2-common `IMaverickV2Pool.sol` | `PoolSwap(address,address,(uint256,bool,bool,int32),uint256,uint256)` | `0x103ed084…` | +| Fluid DEX | Instadapp/fluid-contracts-public `dex/poolT1/coreModule/events.sol` | `Swap(bool,uint256,uint256,address)` | `0xdc004dbc…` | +| DODO v2 (DVM/DPP/DSP) | DODOEX/contractV2 `DVMTrader.sol`, `DPPTrader.sol`, `DSPTrader.sol` | `DODOSwap(address,address,uint256,uint256,address,address)` | `0xc2c0245e…` | +| DODO v1 | DODOEX/dodo-smart-contract `impl/Trader.sol` | `SellBaseToken(address,uint256,uint256)` / `BuyBaseToken(address,uint256,uint256)` | `0xd8648b6a…` / `0xe93ad760…` | +| Bancor v3 | bancorprotocol/contracts-v3 `network/BancorNetwork.sol` | `TokensTraded(bytes32,address,address,uint256,uint256,uint256,uint256,uint256,address)` | `0x5c02c2bb…` | +| Bancor Carbon | bancorprotocol/carbon-contracts `carbon/Strategies.sol` | `TokensTraded(address,address,address,uint256,uint256,uint128,bool)` | `0x95f3b013…` | +| Bancor v2.1 | bancorprotocol/contracts-solidity `converter/interfaces/IConverter.sol` | `Conversion(address,address,address,uint256,uint256,int256)` | `0x276856b3…` | +| Solidly-style pairs | aerodrome-finance/contracts `interfaces/IPool.sol` | `Swap(address,address,uint256,uint256,uint256,uint256)` | `0xb3e27736…` | +| Ekubo | EkuboProtocol/evm-contracts v3.2.0 `src/Core.sol` | `log0`, no topics, 116 bytes (locker, poolId, balanceUpdate, stateAfter) | none | + +Full hashes are in `measure.mjs` and `results.json`. Notes on folding and coverage: + +- The v2 signature is shared by Uniswap v2, SushiSwap, PancakeSwap v2, Fraxswap and every + other v2 fork, so those are one "v2 family" and cannot be separated by event alone. + The v3 signature is likewise shared by SushiSwap v3 and KyberSwap Elastic (identical + parameter types), so those land in the v3 row. Solidly-style pairs (Aerodrome, + Velodrome) hash to a different topic and were added, but emitted nothing on mainnet in + the window (they live on Base and Optimism). +- Curve's four exchange events are folded into one family. PancakeSwap v3 is a separate + row because its `Swap` carries two extra protocol-fee words and therefore a different + topic. +- Ekubo Core writes its swap record with `log0` and no topics (`Core.sol`, + `log0(o, 116)`), so topic filtering cannot see it. It is counted instead as "a + topic-less log from the Core address with exactly 116 bytes of data"; every other + Core event is a normal topic-carrying `emit`, and the sanity counter for topic-less + Core logs of any other length read 0. This counts every Ekubo swap regardless of + caller. The production Yul router (`0x7B2aA7Ec…`, bytecode verified to embed the Core + address) received 1 transaction in the window; Ekubo flow arrives through 1inch, 0x, + Relay, Kyber and other aggregators. Hop counts are not decoded from the Yul router's + packed calldata. The legacy Ekubo v2 core (`0xe0e0e08A…`) is not counted. +- Within the Balancer v2 family, 58 of the 243 transactions came from a second + `Vault` deployment (`0xd315a9c3…`, Sourcify exact match to `contracts/Vault.sol`), a + Balancer-v2-architecture fork; it is left in the family. + +Not counted, and why: + +- Order-flow and RFQ settlement that is not a pool: CoW Protocol, UniswapX, 1inch and + KyberSwap limit orders (`DSLOProtocol`), 0x RFQ fills. Their transactions are counted + only when a leg lands in one of the pools above. The KyberSwap router transactions + without any counted event (51 in the first 200 blocks) were limit-order fills. +- Long-tail AMMs outside the set (Kyber Classic/DMM, Smardex, Integral, Shell, Uniswap v1, + DODO v3): no signature was verified from source for them within the time box. +- One unidentified contract (`0xf2e3dcb8…`) that emits topic-less logs (29 transactions, + 5.2M gas in the first 200 blocks, no verified source on Sourcify). +- Everything the discovery scan identified as non-AMM: ERC-4337 EntryPoint and + paymasters, Aave, Morpho, bridges (Relay, LiFi, Optimism/Base messengers), the Aztec + rollup, Seaport, Chainlink VRF, batch-transfer and address-poisoning contracts. + +Router addresses were not taken from memory: at startup the script fetches each +contract's bytecode and aborts unless it contains the listed function selectors (or, +for the selector-less Yul router, the Core address). + +| Router | Address | Verified in bytecode | +| ---------------------------------- | -------------------------------------------- | ---------------------------------------------------- | +| Universal Router (v2, 2025) | `0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af` | `execute` `0x3593564c` | +| Universal Router (v1.2, 2023) | `0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD` | `execute` `0x3593564c` | +| Universal Router (2026 deployment) | `0x4c82d1FBfe28c977cbb58d8c7ff8FcF9f70a2cca` | `execute` `0x3593564c` (Sourcify: `UniversalRouter`) | +| SwapRouter02 | `0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45` | `exactInput` `0xb858183f`, `multicall` `0x5ae401dc` | +| SwapRouter (v3 classic) | `0xE592427A0AEce92De3Edee1F18E0157C05861564` | `exactInput` `0xc04b8d59` | +| Uniswap v2 Router02 | `0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D` | `swapExactTokensForTokens` `0x38ed1739` | +| 1inch AggregationRouter v6 | `0x111111125421cA6dc452d289314280a0f8842A65` | `swap` `0x07ed2379`, `unoswap` `0x83800a8e` | +| Ekubo Yul router | `0x7B2aA7Ecc0B5936b7C52E6259A19C3BA557d0748` | Core address `0x…14aA86C5d3c41765bb24e11bd701` | + +### Results (blocks 25997046–25998045) + +All flow, from receipts: 321,702 transactions in the window (317,617 successful), of +which 32,417 are swap transactions with 54,407 pool swap events, +**1.678 pools per swap transaction** (54,407 / 32,417 = 1.6783). + +| Pools touched | Transactions | Share | +| ------------- | ------------ | ------ | +| 1 | 23,358 | 72.05% | +| 2 | 4,801 | 14.81% | +| 3 | 2,027 | 6.25% | +| 4 or more | 2,231 | 6.88% | + +Per family (a transaction touching two families is counted in both; "gas" is the gas of +every transaction touching the family, so it also overlaps): + +| Family | Transactions | Events | Events of this family per tx | All pools per tx touching it | Gas of those txs | Emitting contracts | +| ----------------- | ------------ | ------ | ---------------------------- | ---------------------------- | ---------------- | ------------------ | +| Uniswap v3 | 15,900 | 21,418 | 1.347 | 2.024 | 5,672,233,165 | 813 | +| Uniswap v4 | 11,748 | 17,467 | 1.487 | 2.354 | 4,741,053,798 | 1 (PoolManager) | +| Uniswap v2 family | 7,528 | 8,963 | 1.191 | 2.128 | 2,467,266,054 | 1,267 | +| Curve | 1,652 | 2,250 | 1.362 | 4.079 | 1,286,127,611 | 219 | +| PancakeSwap v3 | 1,307 | 1,433 | 1.096 | 4.111 | 858,951,131 | 35 | +| Ekubo | 943 | 1,079 | 1.144 | 3.538 | 673,692,276 | 1 (Core) | +| Fluid DEX | 551 | 587 | 1.065 | 3.316 | 435,123,007 | 19 | +| DODO | 421 | 448 | 1.064 | 4.496 | 399,393,841 | 56 | +| Balancer v2 | 243 | 259 | 1.066 | 4.066 | 160,357,147 | 2 (Vault + fork) | +| Balancer v3 | 202 | 226 | 1.119 | 5.119 | 204,734,020 | 2 | +| Maverick v2 | 118 | 123 | 1.042 | 5.127 | 117,418,379 | 7 | +| Bancor v2.1 | 54 | 107 | 1.981 | 6.130 | 42,174,657 | 19 | +| Bancor v3 | 23 | 23 | 1.000 | 5.348 | 17,867,787 | 1 | +| Maverick v1 | 17 | 18 | 1.059 | 6.059 | 12,229,328 | 7 | +| Bancor Carbon | 6 | 6 | 1.000 | 2.333 | 3,788,965 | 1 | +| Solidly-style | 0 | 0 | — | — | — | 0 | + +Reading the right-hand columns: the families added in this pass are mostly reached +through aggregators (a transaction touching Balancer v3 averages 5.1 pools in total, +Maverick v2 5.1, Curve 4.1), while the four families of the first run are where the +single-pool retail flow lives. Adding them therefore raised the average from 1.657 to +1.678, not by more. + +Gas: the swap transactions used 10,039,217,633 of the window's 30,729,852,504 gas, +**32.67%**. This is the gas of whole transactions that contain a swap event (an +aggregator's or MEV bot's overhead included), so it is an upper bound on swap gas and a +direct check on the router-address-only burn share (4.2%), which it comfortably exceeds. + +Router flow only, from calldata: 4,432 decoded transactions, 5,687 pools, +**1.283 pools per swap**; 444 opaque (443 1inch generic `swap`, 1 Ekubo Yul +router), 323 undecoded (limit-order and liquidity calls, see above), 284 failed +(excluded). + +| Pools touched | Transactions | Share | +| ------------- | ------------ | ------ | +| 1 | 3,712 | 83.75% | +| 2 | 473 | 10.67% | +| 3 | 130 | 2.93% | +| 4 or more | 117 | 2.64% | + +| Router | Decoded txs | Avg pools | 1 / 2 / 3 / 4+ | Calldata = events | +| ---------------------------------- | ----------- | --------- | -------------------- | ----------------- | +| SwapRouter02 | 1,168 | 1.076 | 1093 / 71 / 0 / 4 | 1,168 / 1,168 | +| Universal Router (v2, 2025) | 1,153 | 1.207 | 974 / 147 / 18 / 14 | 1,150 / 1,153 | +| Universal Router (2026 deployment) | 1,070 | 1.837 | 635 / 224 / 112 / 99 | 1,056 / 1,070 | +| Uniswap v2 Router02 | 658 | 1.011 | 651 / 7 / 0 / 0 | 650 / 658 | +| SwapRouter (v3 classic) | 281 | 1.036 | 271 / 10 / 0 / 0 | 281 / 281 | +| Universal Router (v1.2, 2023) | 70 | 1.000 | 70 / 0 / 0 / 0 | 70 / 70 | +| 1inch v6 `unoswapN` | 32 | 1.438 | 18 / 14 / 0 / 0 | 32 / 32 | + +In 4,407 of the 4,432 decoded transactions the calldata hop count equals the event count. +The 25 exceptions all have more events than hops, never fewer: Universal Router +transactions whose Uniswap v4 legs went through hook pools that perform a nested +`PoolManager` swap (17, e.g. 1 hop decoded / 2 `Swap` events, 11 / 16, 3 / 8), and v2 +Router02 `swapExactTokensForTokensSupportingFeeOnTransferTokens` calls on tax tokens +that sell their fee through the same pair inside the transfer (8, each 1 hop / 2 +events on one pair). Both are real extra pool swaps, so the receipts measurement keeps +them; note that it counts swap events, not distinct pools, so a pool swapped twice in +one transaction counts twice. + +The gap between 1.68 (all flow) and 1.28 (router flow) is the multi-pool weight of +aggregators, solvers, MEV bundles and other contract callers. The docs page uses the +all-flow figure as its headline blend and shows the router-only figure as context. + +Caveats: 1,000 blocks is about three and a half hours of one day, not a 7-day window; +the distribution should be re-run over a longer window (the script accepts `--blocks`) +before being quoted as a long-run figure. The event set now covers every AMM family +found among the window's gas consumers; what remains uncounted is listed under "Not +counted" above, so a transaction whose only swap is an order-book fill, a legacy Ekubo +v2 swap, or a long-tail AMM is not counted. + +### Results, 30-day stride sample (every 10th block, 25783810–25999800) + +All flow, from receipts: 5,724,722 transactions in the sampled blocks (5,643,974 +successful), of which 664,726 are swap transactions with 1,161,778 pool swap events, +**1.748 pools per swap transaction** (1,161,778 / 664,726 = 1.7478). The swap transactions used +32.68% of all gas in the sampled blocks (the ratio of summed `gasUsed`, so it is the +share of block space, not of fee burn). + +| Pools touched | Transactions | Share | +| ------------- | ------------ | ------ | +| 1 | 459,356 | 69.10% | +| 2 | 106,197 | 15.98% | +| 3 | 49,247 | 7.41% | +| 4 or more | 49,926 | 7.51% | + +Per family (a transaction touching two families is counted in both; "gas share" is the +gas of every transaction touching the family over all gas in the sampled blocks, so it +also overlaps): + +| Family | Transactions | Events of this family per tx | All pools per tx touching it | Gas share of window | +| ----------------- | ------------ | ---------------------------- | ---------------------------- | ------------------- | +| Uniswap v3 | 327,953 | 1.327 | 2.101 | 18.30% | +| Uniswap v4 | 237,383 | 1.507 | 2.493 | 15.51% | +| Uniswap v2 family | 178,611 | 1.241 | 2.212 | 9.50% | +| Curve | 37,104 | 1.391 | 4.086 | 4.61% | +| PancakeSwap v3 | 30,611 | 1.140 | 4.299 | 3.23% | +| Ekubo | 21,419 | 1.128 | 3.346 | 2.22% | +| Fluid DEX | 8,789 | 1.079 | 3.940 | 1.16% | +| DODO | 7,846 | 1.071 | 4.457 | 1.08% | +| Balancer v2 | 5,584 | 1.063 | 4.230 | 0.61% | +| Balancer v3 | 3,987 | 1.080 | 4.385 | 0.53% | +| Maverick v2 | 2,591 | 1.045 | 4.116 | 0.33% | +| Bancor v2.1 | 2,359 | 1.848 | 5.242 | 0.25% | +| Maverick v1 | 563 | 1.046 | 4.417 | 0.08% | +| Bancor v3 | 531 | 1.047 | 5.633 | 0.06% | +| Bancor Carbon | 218 | 1.009 | 2.844 | 0.02% | +| Solidly-style | 3 | 1.000 | 1.667 | 0.00% | + +Every catalogued family had at least one event in the month; none is missing. The +sanity counters read 0 failed transactions with swap logs and 0 topic-less Core logs of +a length other than 116 bytes. + +Per UTC day (`daily` in `results-30d.json`, 716–718 sampled blocks per full day; the +first and last days are partial): the average ranged from 1.640 (August 26) to 1.865 +(August 19) pools per swap transaction, and the swap-transaction gas share from 20.25% +(the 162-block partial first day) and 24.82% (August 29) to 41.03% (August 23). The +1,000-block window on September 17 (1.678, 32.67%) sits inside both ranges. + +Router flow, from calldata: 135,167 successful transactions to the verified routers were +decoded (8,303 opaque 1inch `swap` calls and 4,224 non-swap selectors excluded) at +**1.219 pools per router transaction**; cross-checked against their own receipts, +134,178 agree exactly, 989 have more pool events than the calldata declares (aggregator +executors adding hops) and none fewer. Direct router calls are lighter than the all-flow +average because aggregators, MEV bundles and other contracts carry most of the multi-pool +weight. + +Reading the histogram for the page: 69% of swap transactions touch one pool, but the +31% that touch more carry enough pools to lift the mean to 1.75, and the daily mean +never left the 1.64–1.87 band in a month that spanned a 2x range of swap gas share. + +Both the pools-per-swap average and the gas share are lower bounds. A transaction is +counted, and its pools are counted, only through swap events of the catalogued +families; any AMM outside the catalogue (long-tail forks with their own event +signatures, order-book fills that never touch a pool, the legacy Ekubo v2 core) +contributes uncounted transactions, uncounted pools inside counted transactions, and +uncounted gas. The chain-wide capacity figures on the page therefore understate the +effect of moving swap flow to Ekubo. + +## L2 fork methodology (Base, Arbitrum One) + +Same discipline as the mainnet fork (pinned block, one identical unmeasured warm-up swap, +every measured call isolated, snapshot = execution gas of the measured call net of refunds, +excluding the 21,000 base and calldata), on two L2s whose fee structure differs from +mainnet: cheap L2 execution plus a separate charge for the L1 data the sequencer posts. +Harness, snapshots, exact calldata and the fee-oracle readings are in `fork-l2/`. + +### Deployment check + +Chain IDs and code presence were read from the public RPCs before anything was pinned +(`cast chain-id`, `cast code`, September 17, 2026). Both chains answer `8453` and `42161` +respectively. Every address below has code on both chains at the pinned blocks; nothing +listed here was taken from memory. + +| Contract | Base (8453) | Arbitrum One (42161) | Source | +| -------------------------------------------------------- | -------------------------------------------- | -------------------------------------------- | --------------------------------------- | +| Ekubo Core | `0x00000000000014aA86C5d3c41765bb24e11bd701` | same | docs `reference/contracts/evm-v3` | +| Ekubo Yul router (production, v0.7.1) | `0x7B2aA7Ecc0B5936b7C52E6259A19C3BA557d0748` | same | `yul-router/broadcast/deployments.json` | +| Ekubo Positions (original generation, canonical on both) | `0x02D9876A21AF7545f8632C3af76eC90b5ad4b66D` | same | docs `reference/contracts/evm-v3` | +| Ekubo Positions (192-bit generation, also deployed) | `0xA2971E0C37cFdb13aE8440A0C94Ef1A1af39e326` | same | docs `reference/contracts/evm-v3` | +| Ekubo CoreDataFetcher / QuoteDataFetcher | `0xF68F25CA…` / `0x5a3F0F1d…` | same | docs `reference/contracts/evm-v3` | +| Uniswap v3 factory | `0x33128a8fC17869897dcE68Ed026d694621f6FDfD` | `0x1F98431c8aD98523631AE4a59f267346ea31F984` | docs.uniswap.org v3 deployments | +| Uniswap v3 SwapRouter (classic) | not listed by Uniswap for Base | `0xE592427A0AEce92De3Edee1F18E0157C05861564` | docs.uniswap.org v3 deployments | +| Uniswap v3 SwapRouter02 | `0x2626664c2603336E57B271c5C0b26F421741e481` | `0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45` | docs.uniswap.org v3 deployments | +| Uniswap v4 PoolManager | `0x498581fF718922c3f8e6A244956aF099B2652b2b` | `0x360E68faCcca8cA495c1B759Fd9EEe466db9FB32` | docs.uniswap.org v4 deployments | +| Uniswap v4 StateView | `0xA3c0c9b65baD0b08107Aa264b0f3dB444b867A71` | `0x76Fd297e2D437cd7f76d50F01AfE6160f86e9990` | docs.uniswap.org v4 deployments | +| WETH | `0x4200000000000000000000000000000000000006` | `0x82aF49447D8a07e3bd95BD0d56f35241523fBab1` | `symbol()`/`decimals()` read on-chain | +| USDC (native) | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | `0xaf88d065e77c8cC2239327C5EDb3A432268e5831` | `symbol()`/`decimals()` read on-chain | +| USDT (Base) / USD₮0 (Arbitrum) | `0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2` | `0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9` | `symbol()`/`decimals()` read on-chain | + +The mainnet Ekubo `Router` (Solidity) has no listed L2 address in the docs (it is +deployment-specific), so the Ekubo leg is the Yul router only, which is also what the +mainnet headline uses. The address `0xE592427A…` on Base holds 4,221 bytes of unrelated +code and is not the Uniswap SwapRouter; Base's v3 leg therefore goes through SwapRouter02 +(`exactInputSingle` without the `deadline` field, 228 bytes of calldata against 260). On +Arbitrum the classic SwapRouter is used for parity with mainnet and SwapRouter02 is +recorded as well (`ForkL2V3ArbitrumRouter02Test`). + +### Endpoints and blocks + +`https://base-rpc.publicnode.com` and `https://arbitrum-rpc.publicnode.com` served the chain +IDs, heads and code checks but refuse any historical state with `Archive requests require +a personal token` (Base already about 1,000 blocks behind head, Arbitrum at 100 blocks), so +the forks, the pool probes and the fee-oracle reads ran against +`https://base-mainnet.public.blastapi.io` and `https://arbitrum-one.public.blastapi.io`. +The L1-fee readings were cross-checked against `https://mainnet.base.org` and +`https://arb1.arbitrum.io/rpc` (identical to the wei, `l1-data-cost.json`). + +| Chain | Block | Hash | Timestamp | L2 base fee (wei) | Note | +| ------------ | ----------- | ------------- | --------------------------- | ----------------- | ---------------------------------------------------------------------------- | +| Base | `51439900` | `0x54e5275d…` | 1789669147 (2026-09-17 UTC) | 5,000,000 | L1 origin block 25998943; Jovian active (`isJovian()` true) | +| Arbitrum One | `506178800` | `0x24768c22…` | 1789669181 (2026-09-17 UTC) | 20,074,000 | L1 block 25998959; `block.number` on the fork is that L1 number (Nitro rule) | + +Because Foundry reports Arbitrum's `block.number` as the L1 block, the harness pins the +block by timestamp on both chains. Foundry 1.8.3 also selects its `optimism` network family +for chain 8453, and that family disables `CLZ` (EIP-7939) even at `evm_version = "osaka"`: +every Ekubo swap reverted with `NotActivated` until the Base run was given +`--network ethereum`. The opcode is live on the real chain (a Yul-router `quote` against the +live Base ETH/USDC pool executes through `eth_call`, `quote-live-base.mjs`), so the flag +restores the chain's actual EVM; it changes no opcode gas. Uniswap legs measure the same with +or without it. + +### Pair choice: WETH/USDC, fresh Ekubo pool + +The rule was USDC/USDT on a live Ekubo pool, else WETH/USDC with real Uniswap pools and a +freshly capitalized Ekubo pool. Neither chain has a live Ekubo pool that can absorb the +swap at the pinned blocks: + +- Ekubo's own quoter (`prod-api-quoter.ekubo.org`) returns 107 USDC for 1000 USDT on Base + (two USDC/USDT pools, both far off-price and near-empty) and `insufficient_liquidity` + for every other pair tried on Base and for every pair on Arbitrum, including 10 USDC. +- The Ekubo API's `poolKeys` and `overview/tvl` for the two chains show total Core balances + of about 0.002 ETH, 120 USDC and 120 USDT on Base and 0.002 ETH, 6,634 USDC (plus ARB) + on Arbitrum. The one populated Base ETH/USDC pool (fee 0.05%, spacing 1000, liquidity + 1,498,307,833,298) would move tens of Uniswap ticks on a 100 USDC swap. + +So the Ekubo leg on each chain is a pool created on the fork in the test's setup: +concentrated, no extension, the deepest live v3 WETH/USDC pool's fee tier and 100x its +tick spacing, initialized at the fine tick matching that pool's `sqrtPriceX96`, and +capitalized through a bare locker (`BareMintRouter`, never measured) with **exactly that +v3 pool's active liquidity** in a single position spanning ±60,000 fine ticks (about ±6% +in price). Both tokens are the real WETH and USDC contracts of the chain; Core already held +a nonzero balance of each before setup (2 wei of WETH and the USDC above), and holds the +minted amounts afterwards, so no measured swap pays a zero-to-nonzero balance write. The +Yul router calldata is generated by the SDK (`encode-l2-routes.mjs`) with the same config +word the test asserts against `createConcentratedPoolConfig`; the bytes the test sent are +written to `calldata/ekubo-yul-*.hex` and equal the SDK output (case-insensitive hex). + +| Chain | Leg | Pool | Fee | Spacing | Liquidity at block | Tick at block | +| -------- | ------------- | ------------------------------------------------------------------------------------- | ----- | ------- | -------------------------- | ------------- | +| Base | Ekubo (fresh) | id `keccak(WETH, USDC, 0x…c49ba5e353f7ce80001770)`, fee `55340232221128654 / 2^64` | 0.3% | 6000 | 37,026,042,371,903,555,946 | -19821136 | +| Base | v3 | `0x6c561B446416E1A00E8E93E221854d6eA4171372` (deepest of 100/500/3000/10000) | 0.3% | 60 | 37,026,042,371,903,555,946 | -198222 | +| Base | v4 | id `0x1d8c55f3…5a718f54`, hooks `address(0)` (deepest hookless of 100/500/3000/10000) | 0.3% | 60 | 31,809,972,076,514,118 | -198217 | +| Arbitrum | Ekubo (fresh) | id `keccak(WETH, USDC, 0x…20c49ba5e353f7800003e8)`, fee `9223372036854775 / 2^64` | 0.05% | 1000 | 3,696,581,650,313,474,097 | -19822971 | +| Arbitrum | v3 | `0xC6962004f452bE9203591991D15f6b388e09E8D0` (deepest of 100/500/3000/10000) | 0.05% | 10 | 3,696,581,650,313,474,097 | -198240 | +| Arbitrum | v4 | id `0xfc7b3ad1…57028653`, hooks `address(0)` (deepest hookless of 100/500/3000/10000) | 0.05% | 10 | 52,128,164,974,525,471 | -198242 | + +Other v3/v4 tiers probed at the pinned blocks (`getPool` + `liquidity()`, StateView +`getLiquidity`): Base v3 100/500/10000 = 7.68e16 / 1.48e18 / 6.15e16, v4 WETH/USDC +100/500/10000 = 0 / 3.72e14 / 0; Arbitrum v3 100/500/3000/10000 = 5.63e16 / 3.70e18 / +4.81e17 / 6.28e15, v4 WETH/USDC 100/3000/10000 = 5e9 / 1.64e11 / 0. The native +ETH/USDC v4 pools are deeper on both chains (8.14e17 at 0.3% on Base, 4.08e17 at 0.05% on +Arbitrum) but are a different pair (native ETH settlement) and were not used. The v4 pools +on both chains carry a nonzero protocol fee (`protocolFee` 2048500 on Base, 512125 on +Arbitrum) which the swap pays inside `PoolManager.swap`; that is production state, not a +harness choice. + +### Sizes and tick movement + +Swaps are USDC exact-input to WETH at 50, 100 and 1000 USDC. The measured swap's tick +movement is read from pool state before and after and cross-checked against the pool's +`Swap` event (v3 `slot0`/`Swap`, v4 StateView/`Swap`, Ekubo Core's 116-byte `log0` record). +At 100 USDC no leg moves more than one tick and no leg crosses an initialized tick, so 100 +USDC is the size the docs page quotes; gas is identical at 50 USDC. At 1000 USDC the +shallow v4 WETH/USDC pools move 13 ticks on Base (no initialized tick crossed, gas +unchanged at 126,991) and 8 ticks on Arbitrum, crossing an initialized spacing-10 tick: +156,321 instead of 135,006. That row is kept in the snapshot as a reminder of why the +size was capped. Ekubo's fresh pool moves 0–1 fine ticks (1/100 of a Uniswap tick) at 100 +USDC and 11 at 1000. + +Outputs (wei of WETH for 100 USDC): Base Ekubo 40,448,346,569,108,551, v3 +40,448,339,546,831,424, v4 40,400,300,715,083,769 (spread 0.12%, the v4 pool is 0.03% off +the v3 price); Arbitrum Ekubo 40,624,188,992,664,839, v3 40,624,162,908,750,166 (both +routers), v4 40,620,850,698,508,208 (spread 0.008%). The Ekubo and v3 legs agree to 7 +significant figures because the fresh pool mirrors the v3 pool's price, fee and liquidity. + +### Results + +Execution gas is the isolated snapshot. Calldata bytes are the exact bytes of the measured +call (`calldata/*.hex`); "L2 calldata gas" is the 16/4 per byte intrinsic charge every L2 +still levies inside its own gas (EIP-7623's floor never binds here). "L1 data" is what the +chain's own fee logic charges for posting the transaction, read at the pinned block (see +below), and its gas-equivalent is that wei amount divided by the block's L2 base fee. + +| Chain | Leg (100 USDC) | Execution | Calldata bytes (zero / nonzero) | L2 calldata gas | L1 data (wei) | L1 data, L2-gas equivalent | L2 gas incl. 21,000 | Total gas-equivalent | +| -------- | ------------------------------ | --------- | ------------------------------- | --------------- | -------------- | -------------------------- | ------------------- | -------------------- | +| Base | Ekubo Yul router | 105,084 | 184 (99 / 85) | 1,756 | 1,655,831,768 | 331 | 127,840 | 128,171 | +| Base | v3 SwapRouter02 | 121,803 | 228 (177 / 51) | 1,524 | 1,596,015,122 | 319 | 144,327 | 144,646 | +| Base | v4 PoolManager, minimal locker | 126,959 | 292 (192 / 100) | 2,368 | 1,835,281,704 | 367 | 150,327 | 150,694 | +| Arbitrum | Ekubo Yul router | 113,121 | 184 (63 / 121) | 2,188 | 11,220,000,000 | 561 | 136,309 | 136,870 | +| Arbitrum | v3 SwapRouter (classic) | 130,896 | 260 (187 / 73) | 1,916 | 12,060,000,000 | 603 | 153,812 | 154,415 | +| Arbitrum | v3 SwapRouter02 | 130,705 | 228 (159 / 69) | 1,740 | 11,400,000,000 | 570 | 153,445 | 154,015 | +| Arbitrum | v4 PoolManager, minimal locker | 135,006 | 292 (174 / 118) | 2,584 | 12,880,000,000 | 644 | 158,590 | 159,234 | + +Base additionally tracks a Jovian "DA footprint" per transaction (block-space accounting, +not a fee): `max(100, (intercept + fastlzCoef × fastlzSize) / 1e6) × daFootprintGasScalar`, +with scalar 148 at the block: Ekubo 20,424, v3 19,684, v4 22,644 gas of DA footprint. + +Ekubo against v3 / v4 at 100 USDC: execution 13.7% / 17.2% lower on Base and 13.6% / 16.2% +on Arbitrum (mainnet fork, 1000 USDT: 14.0% / 16.4%); whole transaction 11.4% / 14.9% on +Base and 11.4% / 14.0% on Arbitrum (mainnet: 11.7% / 14.2%). The L1 data component is +0.22–0.41% of every leg's total at these blocks, and Ekubo's is not the smallest on Base: +Fjord's FastLZ estimator prices the dense 184-byte route at 138.9 estimated compressed +bytes against 133.9 for the zero-padded 228-byte SwapRouter02 call. The same effect shows +in the L2 calldata gas column (1,756 against 1,524), because 16/4 pricing also rewards +zero bytes. On Arbitrum (Brotli) the Yul route is the cheapest to post, by 9–42 gas. + +Per-leg execution gas differs from the mainnet figures because the pair and the token +contracts differ (WETH/USDC instead of USDT/USDC; Arbitrum's WETH and both chains' USDC +are proxies), and the Ekubo pool is fresh rather than live; the ratios between legs are +what carries over. + +### Fee predeploys and formulas + +Base (OP Stack; `GasPriceOracle` `0x420000000000000000000000000000000000000F` version +1.6.0, `isFjord`, `isIsthmus`, `isJovian` all true at the block; `L1Block` +`0x4200000000000000000000000000000000000015`). Values at block 51439900: + +| Parameter | Value | Read from | +| ---------------------- | ----------- | -------------------------------- | +| `l1BaseFee` | 118,337,311 | oracle / `L1Block.basefee()` | +| `blobBaseFee` | 7,219,357 | oracle / `L1Block.blobBaseFee()` | +| `baseFeeScalar` | 2,269 | oracle / `L1Block` | +| `blobBaseFeeScalar` | 1,055,762 | oracle / `L1Block` | +| `operatorFeeScalar` | 0 | `L1Block` | +| `operatorFeeConstant` | 0 | `L1Block` | +| `daFootprintGasScalar` | 148 | `L1Block` | + +Fjord L1 cost (`specs/protocol/fjord/exec-engine.md`, implemented in `GasPriceOracle`): +`l1FeeScaled = baseFeeScalar × l1BaseFee × 16 + blobBaseFeeScalar × blobBaseFee`; +`estimatedSizeScaled = max(100 × 1e6, −42,585,600 + 836,500 × fastlzSize)` where +`fastlzSize` is the FastLZ-compressed length of the signed transaction (the oracle +compresses the unsigned RLP and adds 68 bytes for the signature); `l1Fee = +estimatedSizeScaled × l1FeeScaled / 1e12`. `l1-data-cost.mjs` builds the unsigned EIP-1559 +transaction for each leg (chain id, nonce 100, priority fee 0.001 gwei, max fee 0.01 gwei, +gas limit 200,000, value 0, the leg's `to` and exact calldata; 228 / 273 / 338 bytes for +the three Base legs) and calls `getL1Fee(bytes)` and `getL1GasUsed(bytes)` on the oracle at +the pinned block; the estimated compressed size in the results is the oracle's own answer +divided back through the formula. The operator fee is zero at the block, so it adds +nothing. + +Arbitrum One (Nitro; `ArbGasInfo` precompile `0x6C`, `NodeInterface` virtual contract +`0xC8`). Values at block 506178800: `getPricesInWei()` = (perL2Tx 4,317,615,680; per L1 +calldata byte 30,840,112; per storage allocation 401,480,000,000; per ArbGas base +20,000,000; congestion 74,000; total 20,074,000), `getL1BaseFeeEstimate()` = 1,927,507 +wei per L1 gas (so 30,840,112 = 16 × 1,927,507). The chain's poster charge +(`nitro/arbos/l1pricing/l1pricing.go`) is `pricePerUnit × units` with `units = 16 × +brotli-compressed bytes of the signed transaction`; in estimation the units are padded by +256 and 1%. `l1-data-cost.mjs` calls `NodeInterface.gasEstimateL1Component(to, false, +data)` with `from` = the test contract at the pinned block; it returns the L1 component +already denominated in L2 gas (`gasEstimateForL1`, the table's gas-equivalent), the L2 base +fee it used (20,000,000, the block's minimum, against 20,074,000 in the header), and the L1 +base fee estimate. The wei column is `gasEstimateForL1 × 20,000,000`. The precompiles are +not executable on the Foundry fork, so both chains' fee readings come from `eth_call` +against the archive endpoints, not from inside the tests. + +### Reproducing the L2 numbers + +```sh +# from a checkout with lib/ekubo (evm-contracts 1f5be49), lib/v4-core (46c6834, submodules), +# lib/forge-std (77041d2), lib/solady (65e87c7); foundry.toml and remappings.txt as in fork-l2/ +forge test --fork-url https://base-mainnet.public.blastapi.io --fork-block-number 51439900 \ + --network ethereum --fork-retries 10 --fork-retry-backoff 2000 \ + --match-contract 'ForkL2EkuboBaseTest|ForkL2V3BaseTest|ForkL2V4BaseTest' -vv + +forge test --fork-url https://arbitrum-one.public.blastapi.io --fork-block-number 506178800 \ + --fork-retries 10 --fork-retry-backoff 2000 \ + --match-contract 'ForkL2EkuboArbitrumTest|ForkL2V3ArbitrumTest|ForkL2V3ArbitrumRouter02Test|ForkL2V4ArbitrumTest' -vv + +bun run encode-l2-routes.mjs # regenerates the SDK routes (yul-router SDK checkout path inside) +bun run l1-data-cost.mjs > l1-data-cost.json # fee-oracle readings (viem from the SDK's node_modules) +``` + +Foundry 1.8.3, `evm_version = "osaka"`, `via_ir`, `optimizer_runs = 1000000`, solc +auto-detected per file (0.8.26 Uniswap drivers, 0.8.33 Ekubo sources). `isolate = true` is +set per test. + +## Dependency pins + +- Ekubo `evm-contracts` worktree at commit `1f5be49` (plus the harness test files + in this directory); `forge-std` `77041d2`, `solady` `65e87c7`; solc `0.8.33`, + `optimizer_runs = 9999999`, `via_ir`, `evm_version = "osaka"`. +- Uniswap `v4-core` commit `46c6834`; solc `0.8.26`, + `optimizer_runs = 44444444`, `via_ir`, `evm_version = "cancun"` + (v4-core's own `foundry.toml`). +- Uniswap `v3-core` `1.0.1` canonical mainnet bytecode + (`@uniswap/v3-core` npm artifacts, `UniswapV3Factory.json`); the harness driver + compiles with solc `0.8.26`, `evm_version = "cancun"`. +- Fork harness: `evm_version = "osaka"` for all three legs, solc auto-detected per file. +- Shared token: solmate `MockERC20` bytecode in `lab/MockERC20.json`. +- Yul router SDK (`ekubo/yul-router`) for the fork calldata generation. +- Foundry `1.8.3`. + +## Reproducing the lab numbers + +```sh +# v4 (from a checkout of Uniswap/v4-core@46c6834 with submodules initialized) +forge test --match-contract V4GasTest + +# ekubo (remap ekubo/, forge-std/, solady/ at the pins above, deployCode the token) +forge test --offline --match-contract EkuboGasTest + +# v3 (needs UniswapV3Factory.json next to MockERC20.json under artifacts/) +forge test --match-contract V3GasTest + +# flamegraphs (one per harness) +forge test --match-contract FlameTest --flamegraph --no-open +``` + +Run a whole test contract at a time: Foundry rewrites `snapshots/.json` with +only the tests that ran. + +## Reproducing the fork numbers + +```sh +forge test --fork-url --fork-block-number 25991868 \ + --match-contract 'ForkEkuboRealTest|ForkV3Test|ForkV4Test|ForkProbeTest' + +# mints through the production position managers (one contract at a time keeps +# each snapshot file complete; add retries/backoff for rate-limited public RPCs) +forge test --fork-url --fork-block-number 25991868 \ + --fork-retries 10 --fork-retry-backoff 2000 \ + --match-contract 'ForkMintEkuboTest|ForkMintV3Test|ForkMintV4Test' -vv +``` + +The block needs an archive-capable endpoint. The swap legs were captured through +`https://ethereum-rpc.publicnode.com` while the block was inside its recent-state window; +the mint legs were run later against `https://eth-mainnet.public.blastapi.io` (publicnode +had by then started refusing the block as an archive request). Both serve the same +canonical state, which the assertions on pool identity, price and manager wiring confirm. + +Deployed addresses used (all verified on-chain, none memorized): + +| Contract | Address | +| ----------------------------------------------------------------------------------- | -------------------------------------------- | +| Ekubo Core | `0x00000000000014aA86C5d3c41765bb24e11bd701` | +| Ekubo Yul router (production) | `0x7B2aA7Ecc0B5936b7C52E6259A19C3BA557d0748` | +| Uniswap v4 PoolManager | `0x000000000004444c5dc75cB358380D2e3dE08A90` | +| Uniswap v3 SwapRouter (classic) | `0xE592427A0AEce92De3Edee1F18E0157C05861564` | +| Uniswap v3 factory | `0x1F98431c8aD98523631AE4a59f267346ea31F984` | +| Ekubo Positions (canonical, original generation; docs contracts reference) | `0x02D9876A21AF7545f8632C3af76eC90b5ad4b66D` | +| Uniswap v4 PositionManager (docs.uniswap.org/contracts/v4/deployments, Ethereum: 1) | `0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e` | +| Permit2 | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | +| Uniswap v3 NonfungiblePositionManager | `0xC36442b4a4522E871399CD717aBDD847Ab11FE88` | +| WETH | `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` | +| USDC | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | +| USDT | `0xdAC17F958D2ee523a2206206994597C13D831ec7` | + +The v3/v4 fork pools (USDT/USDC fee-100, deepest at the pinned block) were selected by +on-chain probing (`ForkProbe.t.sol`, which also reads the Ekubo pool state directly from +Core storage); the Ekubo pool came from the production quoter. diff --git a/benchmarks/fork-l2/ForkL2Ekubo.t.sol b/benchmarks/fork-l2/ForkL2Ekubo.t.sol new file mode 100644 index 0000000..d388830 --- /dev/null +++ b/benchmarks/fork-l2/ForkL2Ekubo.t.sol @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.33; + +import {Test, Vm} from "forge-std/Test.sol"; +import {ICore} from "ekubo/src/interfaces/ICore.sol"; +import {PoolKey, toPoolId} from "ekubo/src/types/poolKey.sol"; +import {PoolId} from "ekubo/src/types/poolId.sol"; +import {PoolConfig, createConcentratedPoolConfig} from "ekubo/src/types/poolConfig.sol"; +import {PoolState} from "ekubo/src/types/poolState.sol"; +import {SqrtRatio} from "ekubo/src/types/sqrtRatio.sol"; +import {createPositionId} from "ekubo/src/types/positionId.sol"; +import {PoolBalanceUpdate} from "ekubo/src/types/poolBalanceUpdate.sol"; +import {BaseLocker} from "ekubo/src/base/BaseLocker.sol"; +import {FlashAccountantLib} from "ekubo/src/libraries/FlashAccountantLib.sol"; +import {CoreLib} from "ekubo/src/libraries/CoreLib.sol"; + +interface IERC20Like { + function approve(address spender, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); +} + +/// @notice Bare core-level liquidity provision used only to capitalize the fresh pool in +/// setup (lock, updatePosition, settle net). Never measured. +contract BareMintRouter is BaseLocker { + using FlashAccountantLib for *; + + constructor(ICore core) BaseLocker(core) {} + + function mint(PoolKey memory key, bytes24 salt, int32 tickLower, int32 tickUpper, int128 liquidityDelta, address payer) + external + returns (PoolBalanceUpdate balanceUpdate) + { + balanceUpdate = abi.decode( + lock(abi.encode(key, salt, tickLower, tickUpper, liquidityDelta, payer, msg.sender)), (PoolBalanceUpdate) + ); + } + + function handleLockData(uint256, bytes memory data) internal override returns (bytes memory result) { + ( + PoolKey memory key, + bytes24 salt, + int32 tickLower, + int32 tickUpper, + int128 liquidityDelta, + address payer, + address recipient + ) = abi.decode(data, (PoolKey, bytes24, int32, int32, int128, address, address)); + PoolBalanceUpdate balanceUpdate = ICore(payable(address(ACCOUNTANT))).updatePosition( + key, createPositionId(salt, tickLower, tickUpper), liquidityDelta + ); + if (balanceUpdate.delta0() > 0) { + ACCOUNTANT.payFrom(payer, key.token0, uint128(balanceUpdate.delta0())); + } else if (balanceUpdate.delta0() < 0) { + ACCOUNTANT.withdraw(key.token0, recipient, uint128(-balanceUpdate.delta0())); + } + if (balanceUpdate.delta1() > 0) { + ACCOUNTANT.payFrom(payer, key.token1, uint128(balanceUpdate.delta1())); + } else if (balanceUpdate.delta1() < 0) { + ACCOUNTANT.withdraw(key.token1, recipient, uint128(-balanceUpdate.delta1())); + } + result = abi.encode(balanceUpdate); + } +} + +/// @notice L2-fork measurement of the production Ekubo Yul router on Base / Arbitrum One. +/// No live Ekubo WETH/USDC (or USDC/USDT) pool on either chain can absorb the swap +/// at the pinned block (see benchmarks/README.md), so the pool is created fresh on +/// the fork at the deepest Uniswap v3 pool's price, fee tier and (100x finer) +/// tick spacing, and capitalized with exactly that pool's active liquidity through +/// a bare locker in setup. The measured call is the real deployed Yul router with +/// SDK-generated calldata (`encode-l2-routes.mjs`), USDC exact-input to WETH, +/// warmed with one identical unmeasured swap; every call is isolated so the snapshot +/// is the execution gas of the measured swap (net of refunds, excluding the 21,000 +/// base and calldata). +abstract contract ForkL2EkuboBase is Test { + using CoreLib for ICore; + + struct Cfg { + string name; + uint256 chainId; + uint256 blockNumber; + uint256 timestamp; + address weth; + address usdc; + uint64 fee; + uint32 spacing; + int32 initTick; + int32 lower; + int32 upper; + uint128 liquidity; + bytes32 config; + } + + ICore constant CORE = ICore(payable(0x00000000000014aA86C5d3c41765bb24e11bd701)); + address constant YUL_ROUTER = 0x7B2aA7Ecc0B5936b7C52E6259A19C3BA557d0748; + address constant RECIPIENT = 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496; + + PoolKey internal key; + PoolId internal poolId; + BareMintRouter internal minter; + + function _cfg() internal pure virtual returns (Cfg memory); + function _route(uint256 usdc) internal pure virtual returns (bytes memory); + + function _setup() internal { + Cfg memory c = _cfg(); + assertEq(block.chainid, c.chainId, "chain id"); + // Arbitrum forks report the L1 block number as `block.number`; the timestamp pins the block on both chains. + assertEq(block.timestamp, c.timestamp, "pinned block timestamp"); + assertEq(address(this), RECIPIENT, "route recipient is this contract"); + assertGt(address(CORE).code.length, 0, "Core deployed"); + assertGt(YUL_ROUTER.code.length, 0, "Yul router deployed"); + + key = PoolKey({token0: c.weth, token1: c.usdc, config: createConcentratedPoolConfig(c.fee, c.spacing, address(0))}); + assertEq(PoolConfig.unwrap(key.config), c.config, "config word equals the one encoded into the SDK route"); + poolId = toPoolId(key); + assertFalse(CORE.poolState(poolId).isInitialized(), "pool does not exist yet on this chain"); + CORE.initializePool(key, c.initTick); + + minter = new BareMintRouter(CORE); + deal(c.weth, address(this), 100_000 ether); + deal(c.usdc, address(this), 1_000_000_000_000_000); // 1e9 USDC + IERC20Like(c.weth).approve(address(minter), type(uint256).max); + IERC20Like(c.usdc).approve(address(minter), type(uint256).max); + PoolBalanceUpdate u = minter.mint(key, bytes24(0), c.lower, c.upper, int128(c.liquidity), address(this)); + emit log_named_int("mint amount0 (WETH wei)", u.delta0()); + emit log_named_int("mint amount1 (USDC units)", u.delta1()); + (, int32 tick, uint128 liq) = CORE.poolState(poolId).parse(); + assertEq(liq, c.liquidity, "active liquidity equals the v3 pool's"); + assertEq(tick, c.initTick, "pool at init tick"); + assertGt(IERC20Like(c.weth).balanceOf(address(CORE)), 0, "Core holds WETH"); + assertGt(IERC20Like(c.usdc).balanceOf(address(CORE)), 0, "Core holds USDC"); + + IERC20Like(c.usdc).approve(YUL_ROUTER, type(uint256).max); + assertEq(IERC20Like(c.usdc).allowance(address(this), YUL_ROUTER), type(uint256).max, "max approval"); + } + + function _swap(bytes memory route, string memory snapshotName) internal returns (uint256 out) { + Cfg memory c = _cfg(); + (, int32 tickBefore,) = CORE.poolState(poolId).parse(); + uint256 wethBefore = IERC20Like(c.weth).balanceOf(address(this)); + uint256 usdcBefore = IERC20Like(c.usdc).balanceOf(address(this)); + vm.recordLogs(); + (bool ok, bytes memory ret) = YUL_ROUTER.call(route); + if (bytes(snapshotName).length > 0) vm.snapshotGasLastCall(snapshotName); + assertTrue(ok, "swap"); + (address specifiedToken, address calculatedToken, int256 specified, int256 calculated) = + abi.decode(ret, (address, address, int256, int256)); + assertEq(specifiedToken, c.usdc); + assertEq(calculatedToken, c.weth); + assertGt(calculated, 0); + out = IERC20Like(c.weth).balanceOf(address(this)) - wethBefore; + assertEq(out, uint256(calculated), "output equals router-reported calculated amount"); + assertEq(usdcBefore - IERC20Like(c.usdc).balanceOf(address(this)), uint256(specified), "input pulled"); + (, int32 tickAfter,) = CORE.poolState(poolId).parse(); + // Core's swap record: log0 with 116 bytes (locker, poolId, balanceUpdate, stateAfter) + Vm.Log[] memory logs = vm.getRecordedLogs(); + bool seen; + for (uint256 i = 0; i < logs.length; i++) { + if (logs[i].emitter == address(CORE) && logs[i].topics.length == 0 && logs[i].data.length == 116) { + bytes memory d = logs[i].data; + bytes32 stateAfter; + assembly ("memory-safe") { + stateAfter := mload(add(d, 116)) + } + (, int32 eventTick,) = PoolState.wrap(stateAfter).parse(); + assertEq(eventTick, tickAfter, "swap log stateAfter tick equals pool tick"); + seen = true; + } + } + assertTrue(seen, "Core swap record found"); + if (bytes(snapshotName).length > 0) { + emit log_named_int("ekubo tick before", tickBefore); + emit log_named_int("ekubo tick after", tickAfter); + int256 moved = int256(tickAfter) - int256(tickBefore); + vm.snapshotValue( + string.concat(snapshotName, " | Ekubo fine ticks moved (1/100 of a Uniswap tick)"), + uint256(moved < 0 ? -moved : moved) + ); + // no initialized tick between the position bounds: the swap must stay inside them + assertGt(tickAfter, c.lower); + assertLt(tickAfter, c.upper); + } + } + + function _run(uint256 usdc, uint256 minOut, uint256 maxOut) internal { + _setup(); + bytes memory route = _route(usdc); + _swap(route, ""); // warm-up: steady state, unmeasured + Cfg memory c = _cfg(); + assertGt(IERC20Like(c.weth).balanceOf(address(this)), 0, "recipient WETH balance nonzero before measured swap"); + string memory size = vm.toString(usdc / 1_000_000); + string memory name = string.concat("fork-l2 ", c.name, " ekubo Yul router ", size, " USDC->WETH fresh pool"); + uint256 out = _swap(route, name); + vm.snapshotValue(string.concat("fork-l2 ", c.name, " ekubo output ", size, " USDC->WETH (wei)"), out); + vm.snapshotValue(string.concat("fork-l2 ", c.name, " ekubo calldata bytes ", size, " USDC"), route.length); + vm.writeFile(string.concat("calldata/ekubo-yul-", c.name, "-", size, ".hex"), vm.toString(route)); + emit log_named_uint("ekubo measured output (WETH wei)", out); + assertGe(out, minOut); + assertLe(out, maxOut); + } + + /// forge-config: default.isolate = true + function test_fork_l2_ekubo_yulRouter_1000() public { + _run(1_000_000_000, 0.38 ether, 0.43 ether); + } + + /// forge-config: default.isolate = true + function test_fork_l2_ekubo_yulRouter_100() public { + _run(100_000_000, 0.038 ether, 0.043 ether); + } + + /// forge-config: default.isolate = true + function test_fork_l2_ekubo_yulRouter_50() public { + _run(50_000_000, 0.019 ether, 0.0215 ether); + } +} + +/// Run with: --fork-url --fork-block-number 51439900 +contract ForkL2EkuboBaseTest is ForkL2EkuboBase { + function _cfg() internal pure override returns (Cfg memory) { + return Cfg({ + name: "base", + chainId: 8453, + blockNumber: 51439900, + timestamp: 1789669147, + weth: 0x4200000000000000000000000000000000000006, + usdc: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, + fee: 55340232221128654, // floor(0.003 * 2^64): 0.3%, the deepest v3 WETH/USDC tier on Base + spacing: 6000, // v3 spacing 60 x 100 + initTick: -19821136, // floor(ln(price) / ln(1.000001)) of the v3 fee-3000 pool's sqrtPriceX96 + lower: -19884000, + upper: -19758000, + liquidity: 37026042371903555946, // v3 fee-3000 pool `liquidity()` at the pinned block + config: 0x000000000000000000000000000000000000000000c49ba5e353f7ce80001770 + }); + } + + function _route(uint256 usdc) internal pure override returns (bytes memory) { + if (usdc == 1_000_000_000) { + return + hex"0100833589fCD6eDb6E08f4c7C32D4f71b54bdA029134200000000000000000000000000000000000006000000000000000005113442e2ec20b97FA9385bE102ac3EAc297483Dd6233D62b3e14960000000000000000000000003b9aca0000004200000000000000000000000000000000000006833589fCD6eDb6E08f4c7C32D4f71b54bdA02913000000000000000000000000000000000000000000c49ba5e353f7ce8000177000000000000000000000000000000000"; + } + if (usdc == 100_000_000) { + return + hex"0100833589fCD6eDb6E08f4c7C32D4f71b54bdA02913420000000000000000000000000000000000000600000000000000000081b86d16b1367a7FA9385bE102ac3EAc297483Dd6233D62b3e149600000000000000000000000005f5e10000004200000000000000000000000000000000000006833589fCD6eDb6E08f4c7C32D4f71b54bdA02913000000000000000000000000000000000000000000c49ba5e353f7ce8000177000000000000000000000000000000000"; + } + return + hex"0100833589fCD6eDb6E08f4c7C32D4f71b54bdA02913420000000000000000000000000000000000000600000000000000000040dc368b589b3d7FA9385bE102ac3EAc297483Dd6233D62b3e149600000000000000000000000002faf08000004200000000000000000000000000000000000006833589fCD6eDb6E08f4c7C32D4f71b54bdA02913000000000000000000000000000000000000000000c49ba5e353f7ce8000177000000000000000000000000000000000"; + } +} + +/// Run with: --fork-url --fork-block-number 506178800 +contract ForkL2EkuboArbitrumTest is ForkL2EkuboBase { + function _cfg() internal pure override returns (Cfg memory) { + return Cfg({ + name: "arbitrum", + chainId: 42161, + blockNumber: 506178800, + timestamp: 1789669181, + weth: 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1, + usdc: 0xaf88d065e77c8cC2239327C5EDb3A432268e5831, + fee: 9223372036854775, // floor(0.0005 * 2^64): 0.05%, the deepest v3 WETH/USDC tier on Arbitrum + spacing: 1000, // v3 spacing 10 x 100 + initTick: -19822971, + lower: -19883000, + upper: -19762000, + liquidity: 3696581650313474097, // v3 fee-500 pool `liquidity()` at the pinned block + config: 0x00000000000000000000000000000000000000000020c49ba5e353f7800003e8 + }); + } + + function _route(uint256 usdc) internal pure override returns (bytes memory) { + if (usdc == 1_000_000_000) { + return + hex"0100af88d065e77c8cC2239327C5EDb3A432268e583182aF49447D8a07e3bd95BD0d56f35241523fBab100000000000000000513960a07707c937FA9385bE102ac3EAc297483Dd6233D62b3e14960000000000000000000000003b9aca00000082aF49447D8a07e3bd95BD0d56f35241523fBab1af88d065e77c8cC2239327C5EDb3A432268e583100000000000000000000000000000000000000000020c49ba5e353f7800003e800000000000000000000000000000000"; + } + if (usdc == 100_000_000) { + return + hex"0100af88d065e77c8cC2239327C5EDb3A432268e583182aF49447D8a07e3bd95BD0d56f35241523fBab100000000000000000081f5676724d9447FA9385bE102ac3EAc297483Dd6233D62b3e149600000000000000000000000005f5e100000082aF49447D8a07e3bd95BD0d56f35241523fBab1af88d065e77c8cC2239327C5EDb3A432268e583100000000000000000000000000000000000000000020c49ba5e353f7800003e800000000000000000000000000000000"; + } + return + hex"0100af88d065e77c8cC2239327C5EDb3A432268e583182aF49447D8a07e3bd95BD0d56f35241523fBab100000000000000000040fab3b3926ca27FA9385bE102ac3EAc297483Dd6233D62b3e149600000000000000000000000002faf080000082aF49447D8a07e3bd95BD0d56f35241523fBab1af88d065e77c8cC2239327C5EDb3A432268e583100000000000000000000000000000000000000000020c49ba5e353f7800003e800000000000000000000000000000000"; + } +} diff --git a/benchmarks/fork-l2/ForkL2EkuboArbitrumTest.json b/benchmarks/fork-l2/ForkL2EkuboArbitrumTest.json new file mode 100644 index 0000000..5fcbb3e --- /dev/null +++ b/benchmarks/fork-l2/ForkL2EkuboArbitrumTest.json @@ -0,0 +1,14 @@ +{ + "fork-l2 arbitrum ekubo Yul router 100 USDC->WETH fresh pool": "113121", + "fork-l2 arbitrum ekubo Yul router 100 USDC->WETH fresh pool | Ekubo fine ticks moved (1/100 of a Uniswap tick)": "1", + "fork-l2 arbitrum ekubo Yul router 1000 USDC->WETH fresh pool": "113133", + "fork-l2 arbitrum ekubo Yul router 1000 USDC->WETH fresh pool | Ekubo fine ticks moved (1/100 of a Uniswap tick)": "11", + "fork-l2 arbitrum ekubo Yul router 50 USDC->WETH fresh pool": "113133", + "fork-l2 arbitrum ekubo Yul router 50 USDC->WETH fresh pool | Ekubo fine ticks moved (1/100 of a Uniswap tick)": "1", + "fork-l2 arbitrum ekubo calldata bytes 100 USDC": "184", + "fork-l2 arbitrum ekubo calldata bytes 1000 USDC": "184", + "fork-l2 arbitrum ekubo calldata bytes 50 USDC": "184", + "fork-l2 arbitrum ekubo output 100 USDC->WETH (wei)": "40624188992664839", + "fork-l2 arbitrum ekubo output 1000 USDC->WETH (wei)": "406235910949341733", + "fork-l2 arbitrum ekubo output 50 USDC->WETH (wei)": "20312111104803398" +} diff --git a/benchmarks/fork-l2/ForkL2EkuboBaseTest.json b/benchmarks/fork-l2/ForkL2EkuboBaseTest.json new file mode 100644 index 0000000..5156eb4 --- /dev/null +++ b/benchmarks/fork-l2/ForkL2EkuboBaseTest.json @@ -0,0 +1,14 @@ +{ + "fork-l2 base ekubo Yul router 100 USDC->WETH fresh pool": "105084", + "fork-l2 base ekubo Yul router 100 USDC->WETH fresh pool | Ekubo fine ticks moved (1/100 of a Uniswap tick)": "0", + "fork-l2 base ekubo Yul router 1000 USDC->WETH fresh pool": "105096", + "fork-l2 base ekubo Yul router 1000 USDC->WETH fresh pool | Ekubo fine ticks moved (1/100 of a Uniswap tick)": "1", + "fork-l2 base ekubo Yul router 50 USDC->WETH fresh pool": "105096", + "fork-l2 base ekubo Yul router 50 USDC->WETH fresh pool | Ekubo fine ticks moved (1/100 of a Uniswap tick)": "0", + "fork-l2 base ekubo calldata bytes 100 USDC": "184", + "fork-l2 base ekubo calldata bytes 1000 USDC": "184", + "fork-l2 base ekubo calldata bytes 50 USDC": "184", + "fork-l2 base ekubo output 100 USDC->WETH (wei)": "40448346569108551", + "fork-l2 base ekubo output 1000 USDC->WETH (wei)": "404482873373385029", + "fork-l2 base ekubo output 50 USDC->WETH (wei)": "20224174929883199" +} diff --git a/benchmarks/fork-l2/ForkL2V3.t.sol b/benchmarks/fork-l2/ForkL2V3.t.sol new file mode 100644 index 0000000..8e40ecd --- /dev/null +++ b/benchmarks/fork-l2/ForkL2V3.t.sol @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.26; + +import {Test, Vm} from "forge-std/Test.sol"; + +/// Classic SwapRouter (Arbitrum One, same contract and address as mainnet): params carry a deadline. +interface ISwapRouter { + struct ExactInputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + uint160 sqrtPriceLimitX96; + } + + function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); +} + +/// SwapRouter02 (the only Uniswap-listed v3 router on Base): no deadline field. +interface ISwapRouter02 { + struct ExactInputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 amountIn; + uint256 amountOutMinimum; + uint160 sqrtPriceLimitX96; + } + + function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); +} + +interface IUniswapV3PoolLike { + function slot0() + external + view + returns (uint160 sqrtPriceX96, int24 tick, uint16, uint16, uint16, uint8, bool); + function liquidity() external view returns (uint128); + function fee() external view returns (uint24); + function token0() external view returns (address); + function token1() external view returns (address); +} + +interface IERC20Like { + function approve(address spender, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); +} + +/// @notice L2-fork measurement: the deployed Uniswap v3 router + the deepest live WETH/USDC +/// pool on the chain, USDC exact-input to WETH, warmed with one identical unmeasured +/// swap; every call isolated, snapshot = execution gas of the measured call (net of +/// refunds, excluding the 21,000 base and calldata). Ticks are read from `slot0` and +/// cross-checked against the pool's `Swap` event. +abstract contract ForkL2V3Base is Test { + bytes32 constant SWAP_TOPIC = keccak256("Swap(address,address,int256,int256,uint160,uint128,int24)"); + + struct Cfg { + string name; + uint256 chainId; + uint256 blockNumber; + uint256 timestamp; + address router; + bool classic; // true: SwapRouter (deadline); false: SwapRouter02 + address pool; + address weth; + address usdc; + uint24 fee; + } + + function _cfg() internal pure virtual returns (Cfg memory); + + function _setup() internal { + Cfg memory c = _cfg(); + assertEq(block.chainid, c.chainId, "chain id"); + // Arbitrum forks report the L1 block number as `block.number`; the timestamp pins the block on both chains. + assertEq(block.timestamp, c.timestamp, "pinned block timestamp"); + assertGt(c.router.code.length, 0, "router deployed"); + assertEq(IUniswapV3PoolLike(c.pool).token0(), c.weth); + assertEq(IUniswapV3PoolLike(c.pool).token1(), c.usdc); + assertEq(IUniswapV3PoolLike(c.pool).fee(), c.fee); + vm.snapshotValue(string.concat("fork-l2 ", c.name, " v3 pool liquidity"), IUniswapV3PoolLike(c.pool).liquidity()); + deal(c.usdc, address(this), 1_000_000_000_000); // 1e6 USDC + IERC20Like(c.usdc).approve(c.router, type(uint256).max); + assertEq(IERC20Like(c.usdc).allowance(address(this), c.router), type(uint256).max, "max approval"); + } + + function _calldata(uint256 amountIn) internal view returns (bytes memory) { + Cfg memory c = _cfg(); + if (c.classic) { + return abi.encodeCall( + ISwapRouter.exactInputSingle, + ( + ISwapRouter.ExactInputSingleParams({ + tokenIn: c.usdc, + tokenOut: c.weth, + fee: c.fee, + recipient: address(this), + deadline: block.timestamp, + amountIn: amountIn, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }) + ) + ); + } + return abi.encodeCall( + ISwapRouter02.exactInputSingle, + ( + ISwapRouter02.ExactInputSingleParams({ + tokenIn: c.usdc, + tokenOut: c.weth, + fee: c.fee, + recipient: address(this), + amountIn: amountIn, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }) + ) + ); + } + + function _swap(uint256 amountIn, string memory snapshotName) internal returns (uint256 out, bytes memory cd) { + Cfg memory c = _cfg(); + cd = _calldata(amountIn); + (, int24 tickBefore,,,,,) = IUniswapV3PoolLike(c.pool).slot0(); + uint256 wethBefore = IERC20Like(c.weth).balanceOf(address(this)); + vm.recordLogs(); + (bool ok, bytes memory ret) = c.router.call(cd); + if (bytes(snapshotName).length > 0) vm.snapshotGasLastCall(snapshotName); + assertTrue(ok, "swap"); + out = abi.decode(ret, (uint256)); + assertEq(IERC20Like(c.weth).balanceOf(address(this)) - wethBefore, out, "output delivered"); + (, int24 tickAfter,,,,,) = IUniswapV3PoolLike(c.pool).slot0(); + Vm.Log[] memory logs = vm.getRecordedLogs(); + bool seen; + for (uint256 i = 0; i < logs.length; i++) { + if (logs[i].emitter == c.pool && logs[i].topics[0] == SWAP_TOPIC) { + (,,,, int24 eventTick) = abi.decode(logs[i].data, (int256, int256, uint160, uint128, int24)); + assertEq(eventTick, tickAfter, "Swap event tick equals slot0 tick"); + seen = true; + } + } + assertTrue(seen, "Swap event found"); + if (bytes(snapshotName).length > 0) { + emit log_named_int("v3 tick before", tickBefore); + emit log_named_int("v3 tick after", tickAfter); + int256 moved = int256(tickAfter) - int256(tickBefore); + vm.snapshotValue(string.concat(snapshotName, " | ticks moved"), uint256(moved < 0 ? -moved : moved)); + } + } + + function _run(uint256 usdc, uint256 minOut, uint256 maxOut) internal { + _setup(); + Cfg memory c = _cfg(); + _swap(usdc, ""); // warm-up, unmeasured + assertGt(IERC20Like(c.weth).balanceOf(address(this)), 0, "recipient WETH balance nonzero before measured swap"); + string memory size = vm.toString(usdc / 1_000_000); + string memory routerName = c.classic ? "SwapRouter" : "SwapRouter02"; + string memory name = string.concat( + "fork-l2 ", c.name, " v3 ", routerName, " ", size, " USDC->WETH fee-", vm.toString(uint256(c.fee)) + ); + (uint256 out, bytes memory cd) = _swap(usdc, name); + vm.snapshotValue(string.concat("fork-l2 ", c.name, " v3 ", routerName, " output ", size, " USDC->WETH (wei)"), out); + vm.snapshotValue(string.concat("fork-l2 ", c.name, " v3 ", routerName, " calldata bytes ", size, " USDC"), cd.length); + vm.writeFile(string.concat("calldata/v3-", routerName, "-", c.name, "-", size, ".hex"), vm.toString(cd)); + emit log_named_uint("v3 measured output (WETH wei)", out); + assertGe(out, minOut); + assertLe(out, maxOut); + } + + /// forge-config: default.isolate = true + function test_fork_l2_v3_exactInputSingle_1000() public { + _run(1_000_000_000, 0.38 ether, 0.43 ether); + } + + /// forge-config: default.isolate = true + function test_fork_l2_v3_exactInputSingle_100() public { + _run(100_000_000, 0.038 ether, 0.043 ether); + } + + /// forge-config: default.isolate = true + function test_fork_l2_v3_exactInputSingle_50() public { + _run(50_000_000, 0.019 ether, 0.0215 ether); + } +} + +/// Run with: --fork-url --fork-block-number 51439900 +contract ForkL2V3BaseTest is ForkL2V3Base { + function _cfg() internal pure override returns (Cfg memory) { + return Cfg({ + name: "base", + chainId: 8453, + blockNumber: 51439900, + timestamp: 1789669147, + router: 0x2626664c2603336E57B271c5C0b26F421741e481, // SwapRouter02 (docs.uniswap.org Base deployments) + classic: false, + pool: 0x6c561B446416E1A00E8E93E221854d6eA4171372, // WETH/USDC fee 3000, deepest of 100/500/3000/10000 + weth: 0x4200000000000000000000000000000000000006, + usdc: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, + fee: 3000 + }); + } +} + +/// Run with: --fork-url --fork-block-number 506178800 +contract ForkL2V3ArbitrumTest is ForkL2V3Base { + function _cfg() internal pure override returns (Cfg memory) { + return Cfg({ + name: "arbitrum", + chainId: 42161, + blockNumber: 506178800, + timestamp: 1789669181, + router: 0xE592427A0AEce92De3Edee1F18E0157C05861564, // classic SwapRouter (same as mainnet) + classic: true, + pool: 0xC6962004f452bE9203591991D15f6b388e09E8D0, // WETH/USDC fee 500, deepest of 100/500/3000/10000 + weth: 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1, + usdc: 0xaf88d065e77c8cC2239327C5EDb3A432268e5831, + fee: 500 + }); + } +} + +/// Same pool through SwapRouter02, for a like-for-like router with the Base leg. +contract ForkL2V3ArbitrumRouter02Test is ForkL2V3Base { + function _cfg() internal pure override returns (Cfg memory) { + return Cfg({ + name: "arbitrum", + chainId: 42161, + blockNumber: 506178800, + timestamp: 1789669181, + router: 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45, // SwapRouter02 + classic: false, + pool: 0xC6962004f452bE9203591991D15f6b388e09E8D0, + weth: 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1, + usdc: 0xaf88d065e77c8cC2239327C5EDb3A432268e5831, + fee: 500 + }); + } +} diff --git a/benchmarks/fork-l2/ForkL2V3ArbitrumRouter02Test.json b/benchmarks/fork-l2/ForkL2V3ArbitrumRouter02Test.json new file mode 100644 index 0000000..7a4056e --- /dev/null +++ b/benchmarks/fork-l2/ForkL2V3ArbitrumRouter02Test.json @@ -0,0 +1,15 @@ +{ + "fork-l2 arbitrum v3 SwapRouter02 100 USDC->WETH fee-500": "130705", + "fork-l2 arbitrum v3 SwapRouter02 100 USDC->WETH fee-500 | ticks moved": "0", + "fork-l2 arbitrum v3 SwapRouter02 1000 USDC->WETH fee-500": "130705", + "fork-l2 arbitrum v3 SwapRouter02 1000 USDC->WETH fee-500 | ticks moved": "0", + "fork-l2 arbitrum v3 SwapRouter02 50 USDC->WETH fee-500": "130717", + "fork-l2 arbitrum v3 SwapRouter02 50 USDC->WETH fee-500 | ticks moved": "0", + "fork-l2 arbitrum v3 SwapRouter02 calldata bytes 100 USDC": "228", + "fork-l2 arbitrum v3 SwapRouter02 calldata bytes 1000 USDC": "228", + "fork-l2 arbitrum v3 SwapRouter02 calldata bytes 50 USDC": "228", + "fork-l2 arbitrum v3 SwapRouter02 output 100 USDC->WETH (wei)": "40624162908750166", + "fork-l2 arbitrum v3 SwapRouter02 output 1000 USDC->WETH (wei)": "406235650115953443", + "fork-l2 arbitrum v3 SwapRouter02 output 50 USDC->WETH (wei)": "20312098062830065", + "fork-l2 arbitrum v3 pool liquidity": "3696581650313474097" +} diff --git a/benchmarks/fork-l2/ForkL2V3ArbitrumTest.json b/benchmarks/fork-l2/ForkL2V3ArbitrumTest.json new file mode 100644 index 0000000..6a245bf --- /dev/null +++ b/benchmarks/fork-l2/ForkL2V3ArbitrumTest.json @@ -0,0 +1,15 @@ +{ + "fork-l2 arbitrum v3 SwapRouter 100 USDC->WETH fee-500": "130896", + "fork-l2 arbitrum v3 SwapRouter 100 USDC->WETH fee-500 | ticks moved": "0", + "fork-l2 arbitrum v3 SwapRouter 1000 USDC->WETH fee-500": "130896", + "fork-l2 arbitrum v3 SwapRouter 1000 USDC->WETH fee-500 | ticks moved": "0", + "fork-l2 arbitrum v3 SwapRouter 50 USDC->WETH fee-500": "130908", + "fork-l2 arbitrum v3 SwapRouter 50 USDC->WETH fee-500 | ticks moved": "0", + "fork-l2 arbitrum v3 SwapRouter calldata bytes 100 USDC": "260", + "fork-l2 arbitrum v3 SwapRouter calldata bytes 1000 USDC": "260", + "fork-l2 arbitrum v3 SwapRouter calldata bytes 50 USDC": "260", + "fork-l2 arbitrum v3 SwapRouter output 100 USDC->WETH (wei)": "40624162908750166", + "fork-l2 arbitrum v3 SwapRouter output 1000 USDC->WETH (wei)": "406235650115953443", + "fork-l2 arbitrum v3 SwapRouter output 50 USDC->WETH (wei)": "20312098062830065", + "fork-l2 arbitrum v3 pool liquidity": "3696581650313474097" +} diff --git a/benchmarks/fork-l2/ForkL2V3BaseTest.json b/benchmarks/fork-l2/ForkL2V3BaseTest.json new file mode 100644 index 0000000..331b928 --- /dev/null +++ b/benchmarks/fork-l2/ForkL2V3BaseTest.json @@ -0,0 +1,15 @@ +{ + "fork-l2 base v3 SwapRouter02 100 USDC->WETH fee-3000": "121803", + "fork-l2 base v3 SwapRouter02 100 USDC->WETH fee-3000 | ticks moved": "0", + "fork-l2 base v3 SwapRouter02 1000 USDC->WETH fee-3000": "121803", + "fork-l2 base v3 SwapRouter02 1000 USDC->WETH fee-3000 | ticks moved": "0", + "fork-l2 base v3 SwapRouter02 50 USDC->WETH fee-3000": "121815", + "fork-l2 base v3 SwapRouter02 50 USDC->WETH fee-3000 | ticks moved": "0", + "fork-l2 base v3 SwapRouter02 calldata bytes 100 USDC": "228", + "fork-l2 base v3 SwapRouter02 calldata bytes 1000 USDC": "228", + "fork-l2 base v3 SwapRouter02 calldata bytes 50 USDC": "228", + "fork-l2 base v3 SwapRouter02 output 100 USDC->WETH (wei)": "40448339546831424", + "fork-l2 base v3 SwapRouter02 output 1000 USDC->WETH (wei)": "404482803150768010", + "fork-l2 base v3 SwapRouter02 output 50 USDC->WETH (wei)": "20224171418744207", + "fork-l2 base v3 pool liquidity": "37026042371903555946" +} diff --git a/benchmarks/fork-l2/ForkL2V4.t.sol b/benchmarks/fork-l2/ForkL2V4.t.sol new file mode 100644 index 0000000..5f5dd97 --- /dev/null +++ b/benchmarks/fork-l2/ForkL2V4.t.sol @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.26; + +import {Test, Vm} from "forge-std/Test.sol"; +import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "v4-core/src/types/PoolKey.sol"; +import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; +import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol"; +import {SwapParams} from "v4-core/src/types/PoolOperation.sol"; +import {IHooks} from "v4-core/src/interfaces/IHooks.sol"; +import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol"; +import {TransientStateLibrary} from "v4-core/src/libraries/TransientStateLibrary.sol"; +import {CurrencySettler} from "v4-core/test/utils/CurrencySettler.sol"; +import {TickMath} from "v4-core/src/libraries/TickMath.sol"; + +interface IERC20Like { + function approve(address spender, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); +} + +interface IStateViewLike { + function getSlot0(PoolId poolId) + external + view + returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee); + function getLiquidity(PoolId poolId) external view returns (uint128 liquidity); +} + +/// @notice Minimal single-lock exact-input swapper over a live v4 pool (same contract as the +/// mainnet fork harness). The production Universal Router would cost more on top. +contract ForkSwapRouter is IUnlockCallback { + using CurrencyLibrary for Currency; + using CurrencySettler for Currency; + using TransientStateLibrary for IPoolManager; + + IPoolManager public immutable manager; + + constructor(IPoolManager _manager) { + manager = _manager; + } + + function swap(PoolKey memory key, bool zeroForOne, int256 amountIn, uint160 limit, address payer) + external + payable + returns (uint256 amountOut) + { + bytes memory result = manager.unlock(abi.encode(key, zeroForOne, amountIn, limit, payer, msg.sender)); + amountOut = abi.decode(result, (uint256)); + } + + function unlockCallback(bytes calldata rawData) external returns (bytes memory) { + require(msg.sender == address(manager)); + (PoolKey memory key, bool zeroForOne, int256 amountIn, uint160 limit, address payer, address recipient) = + abi.decode(rawData, (PoolKey, bool, int256, uint160, address, address)); + manager.swap(key, SwapParams({zeroForOne: zeroForOne, amountSpecified: amountIn, sqrtPriceLimitX96: limit}), ""); + Currency currencyIn = zeroForOne ? key.currency0 : key.currency1; + Currency currencyOut = zeroForOne ? key.currency1 : key.currency0; + int256 inDelta = manager.currencyDelta(address(this), currencyIn); + int256 outDelta = manager.currencyDelta(address(this), currencyOut); + if (inDelta < 0) { + manager.sync(currencyIn); + (bool ok,) = Currency.unwrap(currencyIn).call( + abi.encodeWithSignature( + "transferFrom(address,address,uint256)", payer, address(manager), uint256(-inDelta) + ) + ); + require(ok, "transferFrom failed"); + manager.settle(); + } + if (outDelta > 0) currencyOut.take(manager, recipient, uint256(outDelta), false); + return abi.encode(uint256(outDelta)); + } +} + +/// @notice L2-fork measurement: the deployed v4 PoolManager + the deepest live WETH/USDC pool +/// (hooks = address(0)) through the minimal locker above, USDC exact-input to WETH, +/// warmed with one identical unmeasured swap; every call isolated, snapshot = +/// execution gas of the measured call (net of refunds, excluding the 21,000 base and +/// calldata). Ticks are read from StateView and cross-checked against the `Swap` event. +abstract contract ForkL2V4Base is Test { + using PoolIdLibrary for PoolKey; + + bytes32 constant SWAP_TOPIC = keccak256("Swap(bytes32,address,int128,int128,uint160,uint128,int24,uint24)"); + + struct Cfg { + string name; + uint256 chainId; + uint256 blockNumber; + uint256 timestamp; + address manager; + address stateView; + address weth; + address usdc; + uint24 fee; + int24 spacing; + bytes32 expectedPoolId; + } + + ForkSwapRouter internal router; + PoolKey internal key; + PoolId internal poolId; + + function _cfg() internal pure virtual returns (Cfg memory); + + function _setup() internal { + Cfg memory c = _cfg(); + assertEq(block.chainid, c.chainId, "chain id"); + // Arbitrum forks report the L1 block number as `block.number`; the timestamp pins the block on both chains. + assertEq(block.timestamp, c.timestamp, "pinned block timestamp"); + assertGt(c.manager.code.length, 0, "PoolManager deployed"); + router = new ForkSwapRouter(IPoolManager(c.manager)); + // WETH < USDC on both chains: USDC is currency1, so USDC->WETH is oneForZero + key = PoolKey({ + currency0: Currency.wrap(c.weth), + currency1: Currency.wrap(c.usdc), + fee: c.fee, + tickSpacing: c.spacing, + hooks: IHooks(address(0)) + }); + poolId = key.toId(); + assertEq(PoolId.unwrap(poolId), c.expectedPoolId, "pool id"); + (uint160 sqrtPriceX96,,, uint24 lpFee) = IStateViewLike(c.stateView).getSlot0(poolId); + assertGt(sqrtPriceX96, 0, "pool initialized (no hook)"); + assertEq(lpFee, c.fee); + vm.snapshotValue(string.concat("fork-l2 ", c.name, " v4 pool liquidity"), IStateViewLike(c.stateView).getLiquidity(poolId)); + deal(c.usdc, address(this), 1_000_000_000_000); // 1e6 USDC + IERC20Like(c.usdc).approve(address(router), type(uint256).max); + assertEq(IERC20Like(c.usdc).allowance(address(this), address(router)), type(uint256).max, "max approval"); + } + + function _calldata(int256 amountIn) internal view returns (bytes memory) { + return abi.encodeCall(router.swap, (key, false, amountIn, TickMath.MAX_SQRT_PRICE - 1, address(this))); + } + + function _swap(int256 amountIn, string memory snapshotName) internal returns (uint256 out, bytes memory cd) { + Cfg memory c = _cfg(); + cd = _calldata(amountIn); + (, int24 tickBefore,,) = IStateViewLike(c.stateView).getSlot0(poolId); + uint256 wethBefore = IERC20Like(c.weth).balanceOf(address(this)); + vm.recordLogs(); + (bool ok, bytes memory ret) = address(router).call(cd); + if (bytes(snapshotName).length > 0) vm.snapshotGasLastCall(snapshotName); + assertTrue(ok, "swap"); + out = abi.decode(ret, (uint256)); + assertEq(IERC20Like(c.weth).balanceOf(address(this)) - wethBefore, out, "output delivered"); + (, int24 tickAfter,,) = IStateViewLike(c.stateView).getSlot0(poolId); + Vm.Log[] memory logs = vm.getRecordedLogs(); + bool seen; + for (uint256 i = 0; i < logs.length; i++) { + if (logs[i].emitter == c.manager && logs[i].topics.length > 0 && logs[i].topics[0] == SWAP_TOPIC) { + assertEq(logs[i].topics[1], PoolId.unwrap(poolId), "Swap event for this pool"); + (,,,, int24 eventTick,) = abi.decode(logs[i].data, (int128, int128, uint160, uint128, int24, uint24)); + assertEq(eventTick, tickAfter, "Swap event tick equals StateView tick"); + seen = true; + } + } + assertTrue(seen, "Swap event found"); + if (bytes(snapshotName).length > 0) { + emit log_named_int("v4 tick before", tickBefore); + emit log_named_int("v4 tick after", tickAfter); + int256 moved = int256(tickAfter) - int256(tickBefore); + vm.snapshotValue(string.concat(snapshotName, " | ticks moved"), uint256(moved < 0 ? -moved : moved)); + } + } + + function _run(uint256 usdc, uint256 minOut, uint256 maxOut) internal { + _setup(); + Cfg memory c = _cfg(); + _swap(-int256(usdc), ""); // warm-up, unmeasured + assertGt(IERC20Like(c.weth).balanceOf(address(this)), 0, "recipient WETH balance nonzero before measured swap"); + string memory size = vm.toString(usdc / 1_000_000); + string memory name = string.concat( + "fork-l2 ", c.name, " v4 PoolManager minimal locker ", size, " USDC->WETH fee-", vm.toString(uint256(c.fee)) + ); + (uint256 out, bytes memory cd) = _swap(-int256(usdc), name); + vm.snapshotValue(string.concat("fork-l2 ", c.name, " v4 output ", size, " USDC->WETH (wei)"), out); + vm.snapshotValue(string.concat("fork-l2 ", c.name, " v4 calldata bytes ", size, " USDC"), cd.length); + vm.writeFile(string.concat("calldata/v4-minimal-locker-", c.name, "-", size, ".hex"), vm.toString(cd)); + emit log_named_uint("v4 measured output (WETH wei)", out); + assertGe(out, minOut); + assertLe(out, maxOut); + } + + /// forge-config: default.isolate = true + function test_fork_l2_v4_minimalLocker_1000() public { + _run(1_000_000_000, 0.38 ether, 0.43 ether); + } + + /// forge-config: default.isolate = true + function test_fork_l2_v4_minimalLocker_100() public { + _run(100_000_000, 0.038 ether, 0.043 ether); + } + + /// forge-config: default.isolate = true + function test_fork_l2_v4_minimalLocker_50() public { + _run(50_000_000, 0.019 ether, 0.0215 ether); + } +} + +/// Run with: --fork-url --fork-block-number 51439900 +contract ForkL2V4BaseTest is ForkL2V4Base { + function _cfg() internal pure override returns (Cfg memory) { + return Cfg({ + name: "base", + chainId: 8453, + blockNumber: 51439900, + timestamp: 1789669147, + manager: 0x498581fF718922c3f8e6A244956aF099B2652b2b, // docs.uniswap.org v4 deployments, Base + stateView: 0xA3c0c9b65baD0b08107Aa264b0f3dB444b867A71, + weth: 0x4200000000000000000000000000000000000006, + usdc: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, + fee: 3000, // deepest WETH/USDC (hookless) tier of 100/500/3000/10000 + spacing: 60, + expectedPoolId: 0x1d8c55f347727c0fb4f5e1b65cdb93639e0c7102580a7d345e1144cd5a718f54 + }); + } +} + +/// Run with: --fork-url --fork-block-number 506178800 +contract ForkL2V4ArbitrumTest is ForkL2V4Base { + function _cfg() internal pure override returns (Cfg memory) { + return Cfg({ + name: "arbitrum", + chainId: 42161, + blockNumber: 506178800, + timestamp: 1789669181, + manager: 0x360E68faCcca8cA495c1B759Fd9EEe466db9FB32, // docs.uniswap.org v4 deployments, Arbitrum One + stateView: 0x76Fd297e2D437cd7f76d50F01AfE6160f86e9990, + weth: 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1, + usdc: 0xaf88d065e77c8cC2239327C5EDb3A432268e5831, + fee: 500, // deepest WETH/USDC (hookless) tier of 100/500/3000/10000 + spacing: 10, + expectedPoolId: 0xfc7b3ad139daaf1e9c3637ed921c154d1b04286f8a82b805a6c352da57028653 + }); + } +} diff --git a/benchmarks/fork-l2/ForkL2V4ArbitrumTest.json b/benchmarks/fork-l2/ForkL2V4ArbitrumTest.json new file mode 100644 index 0000000..f1f66ef --- /dev/null +++ b/benchmarks/fork-l2/ForkL2V4ArbitrumTest.json @@ -0,0 +1,15 @@ +{ + "fork-l2 arbitrum v4 PoolManager minimal locker 100 USDC->WETH fee-500": "135006", + "fork-l2 arbitrum v4 PoolManager minimal locker 100 USDC->WETH fee-500 | ticks moved": "1", + "fork-l2 arbitrum v4 PoolManager minimal locker 1000 USDC->WETH fee-500": "156321", + "fork-l2 arbitrum v4 PoolManager minimal locker 1000 USDC->WETH fee-500 | ticks moved": "8", + "fork-l2 arbitrum v4 PoolManager minimal locker 50 USDC->WETH fee-500": "134922", + "fork-l2 arbitrum v4 PoolManager minimal locker 50 USDC->WETH fee-500 | ticks moved": "0", + "fork-l2 arbitrum v4 calldata bytes 100 USDC": "292", + "fork-l2 arbitrum v4 calldata bytes 1000 USDC": "292", + "fork-l2 arbitrum v4 calldata bytes 50 USDC": "292", + "fork-l2 arbitrum v4 output 100 USDC->WETH (wei)": "40620850698508208", + "fork-l2 arbitrum v4 output 1000 USDC->WETH (wei)": "405784743605143050", + "fork-l2 arbitrum v4 output 50 USDC->WETH (wei)": "20311602937263111", + "fork-l2 arbitrum v4 pool liquidity": "52128164974525471" +} diff --git a/benchmarks/fork-l2/ForkL2V4BaseTest.json b/benchmarks/fork-l2/ForkL2V4BaseTest.json new file mode 100644 index 0000000..a2f2ad5 --- /dev/null +++ b/benchmarks/fork-l2/ForkL2V4BaseTest.json @@ -0,0 +1,15 @@ +{ + "fork-l2 base v4 PoolManager minimal locker 100 USDC->WETH fee-3000": "126959", + "fork-l2 base v4 PoolManager minimal locker 100 USDC->WETH fee-3000 | ticks moved": "1", + "fork-l2 base v4 PoolManager minimal locker 1000 USDC->WETH fee-3000": "126991", + "fork-l2 base v4 PoolManager minimal locker 1000 USDC->WETH fee-3000 | ticks moved": "13", + "fork-l2 base v4 PoolManager minimal locker 50 USDC->WETH fee-3000": "126971", + "fork-l2 base v4 PoolManager minimal locker 50 USDC->WETH fee-3000 | ticks moved": "1", + "fork-l2 base v4 calldata bytes 100 USDC": "292", + "fork-l2 base v4 calldata bytes 1000 USDC": "292", + "fork-l2 base v4 calldata bytes 50 USDC": "292", + "fork-l2 base v4 output 100 USDC->WETH (wei)": "40400300715083769", + "fork-l2 base v4 output 1000 USDC->WETH (wei)": "403315880591944557", + "fork-l2 base v4 output 50 USDC->WETH (wei)": "20202061711569369", + "fork-l2 base v4 pool liquidity": "31809972076514118" +} diff --git a/benchmarks/fork-l2/calldata/ekubo-yul-arbitrum-100.hex b/benchmarks/fork-l2/calldata/ekubo-yul-arbitrum-100.hex new file mode 100644 index 0000000..2f157c8 --- /dev/null +++ b/benchmarks/fork-l2/calldata/ekubo-yul-arbitrum-100.hex @@ -0,0 +1 @@ +0x0100af88d065e77c8cc2239327c5edb3a432268e583182af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000081f5676724d9447fa9385be102ac3eac297483dd6233d62b3e149600000000000000000000000005f5e100000082af49447d8a07e3bd95bd0d56f35241523fbab1af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000000000000000000000020c49ba5e353f7800003e800000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/fork-l2/calldata/ekubo-yul-arbitrum-1000.hex b/benchmarks/fork-l2/calldata/ekubo-yul-arbitrum-1000.hex new file mode 100644 index 0000000..099f02c --- /dev/null +++ b/benchmarks/fork-l2/calldata/ekubo-yul-arbitrum-1000.hex @@ -0,0 +1 @@ +0x0100af88d065e77c8cc2239327c5edb3a432268e583182af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000513960a07707c937fa9385be102ac3eac297483dd6233d62b3e14960000000000000000000000003b9aca00000082af49447d8a07e3bd95bd0d56f35241523fbab1af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000000000000000000000020c49ba5e353f7800003e800000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/fork-l2/calldata/ekubo-yul-arbitrum-50.hex b/benchmarks/fork-l2/calldata/ekubo-yul-arbitrum-50.hex new file mode 100644 index 0000000..821b0ce --- /dev/null +++ b/benchmarks/fork-l2/calldata/ekubo-yul-arbitrum-50.hex @@ -0,0 +1 @@ +0x0100af88d065e77c8cc2239327c5edb3a432268e583182af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000040fab3b3926ca27fa9385be102ac3eac297483dd6233d62b3e149600000000000000000000000002faf080000082af49447d8a07e3bd95bd0d56f35241523fbab1af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000000000000000000000020c49ba5e353f7800003e800000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/fork-l2/calldata/ekubo-yul-base-100.hex b/benchmarks/fork-l2/calldata/ekubo-yul-base-100.hex new file mode 100644 index 0000000..6f0f6f8 --- /dev/null +++ b/benchmarks/fork-l2/calldata/ekubo-yul-base-100.hex @@ -0,0 +1 @@ +0x0100833589fcd6edb6e08f4c7c32d4f71b54bda02913420000000000000000000000000000000000000600000000000000000081b86d16b1367a7fa9385be102ac3eac297483dd6233d62b3e149600000000000000000000000005f5e10000004200000000000000000000000000000000000006833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000000000000000000000c49ba5e353f7ce8000177000000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/fork-l2/calldata/ekubo-yul-base-1000.hex b/benchmarks/fork-l2/calldata/ekubo-yul-base-1000.hex new file mode 100644 index 0000000..900b2b9 --- /dev/null +++ b/benchmarks/fork-l2/calldata/ekubo-yul-base-1000.hex @@ -0,0 +1 @@ +0x0100833589fcd6edb6e08f4c7c32d4f71b54bda029134200000000000000000000000000000000000006000000000000000005113442e2ec20b97fa9385be102ac3eac297483dd6233d62b3e14960000000000000000000000003b9aca0000004200000000000000000000000000000000000006833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000000000000000000000c49ba5e353f7ce8000177000000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/fork-l2/calldata/ekubo-yul-base-50.hex b/benchmarks/fork-l2/calldata/ekubo-yul-base-50.hex new file mode 100644 index 0000000..a833c00 --- /dev/null +++ b/benchmarks/fork-l2/calldata/ekubo-yul-base-50.hex @@ -0,0 +1 @@ +0x0100833589fcd6edb6e08f4c7c32d4f71b54bda02913420000000000000000000000000000000000000600000000000000000040dc368b589b3d7fa9385be102ac3eac297483dd6233d62b3e149600000000000000000000000002faf08000004200000000000000000000000000000000000006833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000000000000000000000c49ba5e353f7ce8000177000000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/fork-l2/calldata/v3-SwapRouter-arbitrum-100.hex b/benchmarks/fork-l2/calldata/v3-SwapRouter-arbitrum-100.hex new file mode 100644 index 0000000..4a7e578 --- /dev/null +++ b/benchmarks/fork-l2/calldata/v3-SwapRouter-arbitrum-100.hex @@ -0,0 +1 @@ +0x414bf389000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000001f40000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000006aac2f3d0000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/fork-l2/calldata/v3-SwapRouter-arbitrum-1000.hex b/benchmarks/fork-l2/calldata/v3-SwapRouter-arbitrum-1000.hex new file mode 100644 index 0000000..4f1ea00 --- /dev/null +++ b/benchmarks/fork-l2/calldata/v3-SwapRouter-arbitrum-1000.hex @@ -0,0 +1 @@ +0x414bf389000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000001f40000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000006aac2f3d000000000000000000000000000000000000000000000000000000003b9aca0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/fork-l2/calldata/v3-SwapRouter-arbitrum-50.hex b/benchmarks/fork-l2/calldata/v3-SwapRouter-arbitrum-50.hex new file mode 100644 index 0000000..f925f20 --- /dev/null +++ b/benchmarks/fork-l2/calldata/v3-SwapRouter-arbitrum-50.hex @@ -0,0 +1 @@ +0x414bf389000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000001f40000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000006aac2f3d0000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/fork-l2/calldata/v3-SwapRouter02-arbitrum-100.hex b/benchmarks/fork-l2/calldata/v3-SwapRouter02-arbitrum-100.hex new file mode 100644 index 0000000..af0d9ec --- /dev/null +++ b/benchmarks/fork-l2/calldata/v3-SwapRouter02-arbitrum-100.hex @@ -0,0 +1 @@ +0x04e45aaf000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000001f40000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e14960000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/fork-l2/calldata/v3-SwapRouter02-arbitrum-1000.hex b/benchmarks/fork-l2/calldata/v3-SwapRouter02-arbitrum-1000.hex new file mode 100644 index 0000000..d447762 --- /dev/null +++ b/benchmarks/fork-l2/calldata/v3-SwapRouter02-arbitrum-1000.hex @@ -0,0 +1 @@ +0x04e45aaf000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000001f40000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000003b9aca0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/fork-l2/calldata/v3-SwapRouter02-arbitrum-50.hex b/benchmarks/fork-l2/calldata/v3-SwapRouter02-arbitrum-50.hex new file mode 100644 index 0000000..b931b64 --- /dev/null +++ b/benchmarks/fork-l2/calldata/v3-SwapRouter02-arbitrum-50.hex @@ -0,0 +1 @@ +0x04e45aaf000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000001f40000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e14960000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/fork-l2/calldata/v3-SwapRouter02-base-100.hex b/benchmarks/fork-l2/calldata/v3-SwapRouter02-base-100.hex new file mode 100644 index 0000000..bc0172a --- /dev/null +++ b/benchmarks/fork-l2/calldata/v3-SwapRouter02-base-100.hex @@ -0,0 +1 @@ +0x04e45aaf000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e14960000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/fork-l2/calldata/v3-SwapRouter02-base-1000.hex b/benchmarks/fork-l2/calldata/v3-SwapRouter02-base-1000.hex new file mode 100644 index 0000000..135a6c2 --- /dev/null +++ b/benchmarks/fork-l2/calldata/v3-SwapRouter02-base-1000.hex @@ -0,0 +1 @@ +0x04e45aaf000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000003b9aca0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/fork-l2/calldata/v3-SwapRouter02-base-50.hex b/benchmarks/fork-l2/calldata/v3-SwapRouter02-base-50.hex new file mode 100644 index 0000000..ded5f06 --- /dev/null +++ b/benchmarks/fork-l2/calldata/v3-SwapRouter02-base-50.hex @@ -0,0 +1 @@ +0x04e45aaf000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e14960000000000000000000000000000000000000000000000000000000002faf08000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/fork-l2/calldata/v4-minimal-locker-arbitrum-100.hex b/benchmarks/fork-l2/calldata/v4-minimal-locker-arbitrum-100.hex new file mode 100644 index 0000000..0ee1114 --- /dev/null +++ b/benchmarks/fork-l2/calldata/v4-minimal-locker-arbitrum-100.hex @@ -0,0 +1 @@ +0x6e8c9dbf00000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000000000000000000000000000000000000000001f4000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0a1f00000000000000000000000000fffd8963efd1fc6a506488495d951d5263988d250000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496 \ No newline at end of file diff --git a/benchmarks/fork-l2/calldata/v4-minimal-locker-arbitrum-1000.hex b/benchmarks/fork-l2/calldata/v4-minimal-locker-arbitrum-1000.hex new file mode 100644 index 0000000..401f6f1 --- /dev/null +++ b/benchmarks/fork-l2/calldata/v4-minimal-locker-arbitrum-1000.hex @@ -0,0 +1 @@ +0x6e8c9dbf00000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000000000000000000000000000000000000000001f4000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffc4653600000000000000000000000000fffd8963efd1fc6a506488495d951d5263988d250000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496 \ No newline at end of file diff --git a/benchmarks/fork-l2/calldata/v4-minimal-locker-arbitrum-50.hex b/benchmarks/fork-l2/calldata/v4-minimal-locker-arbitrum-50.hex new file mode 100644 index 0000000..790637d --- /dev/null +++ b/benchmarks/fork-l2/calldata/v4-minimal-locker-arbitrum-50.hex @@ -0,0 +1 @@ +0x6e8c9dbf00000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000000000000000000000000000000000000000001f4000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffd050f80000000000000000000000000fffd8963efd1fc6a506488495d951d5263988d250000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496 \ No newline at end of file diff --git a/benchmarks/fork-l2/calldata/v4-minimal-locker-base-100.hex b/benchmarks/fork-l2/calldata/v4-minimal-locker-base-100.hex new file mode 100644 index 0000000..538f98f --- /dev/null +++ b/benchmarks/fork-l2/calldata/v4-minimal-locker-base-100.hex @@ -0,0 +1 @@ +0x6e8c9dbf0000000000000000000000004200000000000000000000000000000000000006000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0a1f00000000000000000000000000fffd8963efd1fc6a506488495d951d5263988d250000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496 \ No newline at end of file diff --git a/benchmarks/fork-l2/calldata/v4-minimal-locker-base-1000.hex b/benchmarks/fork-l2/calldata/v4-minimal-locker-base-1000.hex new file mode 100644 index 0000000..e49bdc3 --- /dev/null +++ b/benchmarks/fork-l2/calldata/v4-minimal-locker-base-1000.hex @@ -0,0 +1 @@ +0x6e8c9dbf0000000000000000000000004200000000000000000000000000000000000006000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffc4653600000000000000000000000000fffd8963efd1fc6a506488495d951d5263988d250000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496 \ No newline at end of file diff --git a/benchmarks/fork-l2/calldata/v4-minimal-locker-base-50.hex b/benchmarks/fork-l2/calldata/v4-minimal-locker-base-50.hex new file mode 100644 index 0000000..e68eca7 --- /dev/null +++ b/benchmarks/fork-l2/calldata/v4-minimal-locker-base-50.hex @@ -0,0 +1 @@ +0x6e8c9dbf0000000000000000000000004200000000000000000000000000000000000006000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffd050f80000000000000000000000000fffd8963efd1fc6a506488495d951d5263988d250000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496 \ No newline at end of file diff --git a/benchmarks/fork-l2/encode-l2-routes.mjs b/benchmarks/fork-l2/encode-l2-routes.mjs new file mode 100644 index 0000000..415e6a8 --- /dev/null +++ b/benchmarks/fork-l2/encode-l2-routes.mjs @@ -0,0 +1,59 @@ +// Generates Yul-router calldata for the L2 fresh-pool WETH/USDC legs with the yul-router SDK. +import { encodeRoute } from "/home/sendmoodz/Work/ekubo/yul-router/sdk/src/index.ts"; +import { writeFileSync } from "node:fs"; +const RECIPIENT = "0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496"; // forge test contract address (asserted in the test) +function config(feeQ64, spacing) { + // extension (20 bytes, zero) | fee (8 bytes) | 0x80000000 | spacing (4 bytes) + const word = (BigInt(feeQ64) << 32n) | (0x80000000n | BigInt(spacing)); + return "0x" + word.toString(16).padStart(64, "0"); +} +const chains = { + base: { + weth: "0x4200000000000000000000000000000000000006", + usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + fee: 55340232221128654n, + spacing: 6000, + priceWeiPerUnit: 1 / 2.464872103401631e-9, + }, + arb: { + weth: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + usdc: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + fee: 9223372036854775n, + spacing: 1000, + priceWeiPerUnit: 1 / 2.4603543652989017e-9, + }, +}; +for (const [name, c] of Object.entries(chains)) { + const cfg = config(c.fee, c.spacing); + console.log(name, "config", cfg); + for (const usdc of [50_000_000n, 100_000_000n, 1_000_000_000n]) { + const expectedWei = BigInt(Math.floor(Number(usdc) * c.priceWeiPerUnit)); + const threshold = (expectedWei * 90n) / 100n; + const data = encodeRoute({ + specifiedToken: c.usdc, + calculatedToken: c.weth, + specifiedAmount: usdc, + calculatedAmountThreshold: threshold, + recipient: RECIPIENT, + hops: [ + { + type: "core", + poolKey: { token0: c.weth, token1: c.usdc, config: cfg }, + }, + ], + }); + const f = `/tmp/l2bench/calldata/yul-route-${name}-${usdc / 1_000_000n}.hex`; + writeFileSync(f, data + "\n"); + console.log( + name, + `${usdc / 1_000_000n} USDC`, + "expectedWei", + expectedWei.toString(), + "threshold", + threshold.toString(), + "bytes", + (data.length - 2) / 2, + ); + console.log(data); + } +} diff --git a/benchmarks/fork-l2/foundry.toml b/benchmarks/fork-l2/foundry.toml new file mode 100644 index 0000000..176101d --- /dev/null +++ b/benchmarks/fork-l2/foundry.toml @@ -0,0 +1,12 @@ +[profile.default] +src = "src" +test = "test" +out = "out" +libs = ["lib"] +# No `solc` pin: foundry auto-detects per file (0.8.26 driver files, =0.8.33 ekubo sources). +optimizer = true +optimizer_runs = 1000000 +via_ir = true +evm_version = "osaka" +verbosity = 2 +fs_permissions = [{ access = "read-write", path = "./" }] diff --git a/benchmarks/fork-l2/l1-data-cost.json b/benchmarks/fork-l2/l1-data-cost.json new file mode 100644 index 0000000..6bf3eb1 --- /dev/null +++ b/benchmarks/fork-l2/l1-data-cost.json @@ -0,0 +1,295 @@ +{ + "base": { + "chainId": 8453, + "block": 51439900, + "blockHash": "0x54e5275d7f99214f0b17e01f415226b85915235c95385d30bf2dcb26d70cbf2f", + "timestamp": 1789669147, + "l2BaseFeeWei": "5000000", + "rpc": "https://base-mainnet.public.blastapi.io", + "crossCheckRpc": "https://mainnet.base.org", + "legs": { + "ekubo Yul router 50 USDC": { + "to": "0x7B2aA7Ecc0B5936b7C52E6259A19C3BA557d0748", + "calldataBytes": 184, + "calldataGasL1Rule": 1768, + "unsignedTxBytes": 228, + "l1FeeWei": "1665801209", + "l1FeeWeiCrossCheck": "1665801209", + "l1GasUsed": "2236", + "estimatedCompressedBytes": 139.771399, + "daFootprintGas": 20572, + "l1FeeInL2GasUnits": 333.1602418 + }, + "ekubo Yul router 100 USDC": { + "to": "0x7B2aA7Ecc0B5936b7C52E6259A19C3BA557d0748", + "calldataBytes": 184, + "calldataGasL1Rule": 1756, + "unsignedTxBytes": 228, + "l1FeeWei": "1655831768", + "l1FeeWeiCrossCheck": "1655831768", + "l1GasUsed": "2222", + "estimatedCompressedBytes": 138.934899, + "daFootprintGas": 20424, + "l1FeeInL2GasUnits": 331.1663536 + }, + "ekubo Yul router 1000 USDC": { + "to": "0x7B2aA7Ecc0B5936b7C52E6259A19C3BA557d0748", + "calldataBytes": 184, + "calldataGasL1Rule": 1768, + "unsignedTxBytes": 228, + "l1FeeWei": "1645862327", + "l1FeeWeiCrossCheck": "1645862327", + "l1GasUsed": "2209", + "estimatedCompressedBytes": 138.098399, + "daFootprintGas": 20424, + "l1FeeInL2GasUnits": 329.1724654 + }, + "v3 SwapRouter02 50 USDC": { + "to": "0x2626664c2603336E57B271c5C0b26F421741e481", + "calldataBytes": 228, + "calldataGasL1Rule": 1536, + "unsignedTxBytes": 273, + "l1FeeWei": "1605984563", + "l1FeeWeiCrossCheck": "1605984563", + "l1GasUsed": "2156", + "estimatedCompressedBytes": 134.752399, + "daFootprintGas": 19832, + "l1FeeInL2GasUnits": 321.1969126 + }, + "v3 SwapRouter02 100 USDC": { + "to": "0x2626664c2603336E57B271c5C0b26F421741e481", + "calldataBytes": 228, + "calldataGasL1Rule": 1524, + "unsignedTxBytes": 273, + "l1FeeWei": "1596015122", + "l1FeeWeiCrossCheck": "1596015122", + "l1GasUsed": "2142", + "estimatedCompressedBytes": 133.915899, + "daFootprintGas": 19684, + "l1FeeInL2GasUnits": 319.2030244 + }, + "v3 SwapRouter02 1000 USDC": { + "to": "0x2626664c2603336E57B271c5C0b26F421741e481", + "calldataBytes": 228, + "calldataGasL1Rule": 1524, + "unsignedTxBytes": 273, + "l1FeeWei": "1596015122", + "l1FeeWeiCrossCheck": "1596015122", + "l1GasUsed": "2142", + "estimatedCompressedBytes": 133.915899, + "daFootprintGas": 19684, + "l1FeeInL2GasUnits": 319.2030244 + }, + "v4 minimal locker 50 USDC": { + "to": "0x2e234DAe75C793f67A35089C9d99245E1C58470b", + "calldataBytes": 292, + "calldataGasL1Rule": 2380, + "unsignedTxBytes": 338, + "l1FeeWei": "1845251145", + "l1FeeWeiCrossCheck": "1845251145", + "l1GasUsed": "2477", + "estimatedCompressedBytes": 154.828399, + "daFootprintGas": 22792, + "l1FeeInL2GasUnits": 369.050229 + }, + "v4 minimal locker 100 USDC": { + "to": "0x2e234DAe75C793f67A35089C9d99245E1C58470b", + "calldataBytes": 292, + "calldataGasL1Rule": 2368, + "unsignedTxBytes": 338, + "l1FeeWei": "1835281704", + "l1FeeWeiCrossCheck": "1835281704", + "l1GasUsed": "2463", + "estimatedCompressedBytes": 153.991899, + "daFootprintGas": 22644, + "l1FeeInL2GasUnits": 367.0563408 + }, + "v4 minimal locker 1000 USDC": { + "to": "0x2e234DAe75C793f67A35089C9d99245E1C58470b", + "calldataBytes": 292, + "calldataGasL1Rule": 2368, + "unsignedTxBytes": 338, + "l1FeeWei": "1835281704", + "l1FeeWeiCrossCheck": "1835281704", + "l1GasUsed": "2463", + "estimatedCompressedBytes": 153.991899, + "daFootprintGas": 22644, + "l1FeeInL2GasUnits": 367.0563408 + } + }, + "oracle": { + "isFjord": true, + "isJovian": true, + "l1BaseFee": "118337311", + "blobBaseFee": "7219357", + "baseFeeScalar": 2269, + "blobBaseFeeScalar": 1055762, + "daFootprintGasScalar": 148, + "l1OriginBlock": 25998943 + } + }, + "arbitrum": { + "chainId": 42161, + "block": 506178800, + "blockHash": "0x24768c22e96c6e6fec640926ab1526e513d49fdcb70ed8572109c803fe032204", + "timestamp": 1789669181, + "l2BaseFeeWei": "20074000", + "rpc": "https://arbitrum-one.public.blastapi.io", + "crossCheckRpc": "https://arb1.arbitrum.io/rpc", + "legs": { + "ekubo Yul router 50 USDC": { + "to": "0x7B2aA7Ecc0B5936b7C52E6259A19C3BA557d0748", + "calldataBytes": 184, + "calldataGasL1Rule": 2200, + "unsignedTxBytes": 228, + "gasEstimateForL1": "561", + "gasEstimateForL1CrossCheck": "561", + "l2BaseFeeFromNode": "20000000", + "l1BaseFeeEstimate": "1927507", + "posterCostWei": "11220000000", + "paddedL1GasUnits": 5821 + }, + "ekubo Yul router 100 USDC": { + "to": "0x7B2aA7Ecc0B5936b7C52E6259A19C3BA557d0748", + "calldataBytes": 184, + "calldataGasL1Rule": 2188, + "unsignedTxBytes": 228, + "gasEstimateForL1": "561", + "gasEstimateForL1CrossCheck": "561", + "l2BaseFeeFromNode": "20000000", + "l1BaseFeeEstimate": "1927507", + "posterCostWei": "11220000000", + "paddedL1GasUnits": 5821 + }, + "ekubo Yul router 1000 USDC": { + "to": "0x7B2aA7Ecc0B5936b7C52E6259A19C3BA557d0748", + "calldataBytes": 184, + "calldataGasL1Rule": 2200, + "unsignedTxBytes": 228, + "gasEstimateForL1": "561", + "gasEstimateForL1CrossCheck": "561", + "l2BaseFeeFromNode": "20000000", + "l1BaseFeeEstimate": "1927507", + "posterCostWei": "11220000000", + "paddedL1GasUnits": 5821 + }, + "v3 SwapRouter 50 USDC": { + "to": "0xE592427A0AEce92De3Edee1F18E0157C05861564", + "calldataBytes": 260, + "calldataGasL1Rule": 1928, + "unsignedTxBytes": 306, + "gasEstimateForL1": "601", + "gasEstimateForL1CrossCheck": "601", + "l2BaseFeeFromNode": "20000000", + "l1BaseFeeEstimate": "1927507", + "posterCostWei": "12020000000", + "paddedL1GasUnits": 6236 + }, + "v3 SwapRouter 100 USDC": { + "to": "0xE592427A0AEce92De3Edee1F18E0157C05861564", + "calldataBytes": 260, + "calldataGasL1Rule": 1916, + "unsignedTxBytes": 306, + "gasEstimateForL1": "603", + "gasEstimateForL1CrossCheck": "603", + "l2BaseFeeFromNode": "20000000", + "l1BaseFeeEstimate": "1927507", + "posterCostWei": "12060000000", + "paddedL1GasUnits": 6257 + }, + "v3 SwapRouter 1000 USDC": { + "to": "0xE592427A0AEce92De3Edee1F18E0157C05861564", + "calldataBytes": 260, + "calldataGasL1Rule": 1916, + "unsignedTxBytes": 306, + "gasEstimateForL1": "604", + "gasEstimateForL1CrossCheck": "604", + "l2BaseFeeFromNode": "20000000", + "l1BaseFeeEstimate": "1927507", + "posterCostWei": "12080000000", + "paddedL1GasUnits": 6267 + }, + "v3 SwapRouter02 50 USDC": { + "to": "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", + "calldataBytes": 228, + "calldataGasL1Rule": 1752, + "unsignedTxBytes": 273, + "gasEstimateForL1": "568", + "gasEstimateForL1CrossCheck": "568", + "l2BaseFeeFromNode": "20000000", + "l1BaseFeeEstimate": "1927507", + "posterCostWei": "11360000000", + "paddedL1GasUnits": 5894 + }, + "v3 SwapRouter02 100 USDC": { + "to": "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", + "calldataBytes": 228, + "calldataGasL1Rule": 1740, + "unsignedTxBytes": 273, + "gasEstimateForL1": "570", + "gasEstimateForL1CrossCheck": "570", + "l2BaseFeeFromNode": "20000000", + "l1BaseFeeEstimate": "1927507", + "posterCostWei": "11400000000", + "paddedL1GasUnits": 5914 + }, + "v3 SwapRouter02 1000 USDC": { + "to": "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", + "calldataBytes": 228, + "calldataGasL1Rule": 1740, + "unsignedTxBytes": 273, + "gasEstimateForL1": "572", + "gasEstimateForL1CrossCheck": "572", + "l2BaseFeeFromNode": "20000000", + "l1BaseFeeEstimate": "1927507", + "posterCostWei": "11440000000", + "paddedL1GasUnits": 5935 + }, + "v4 minimal locker 50 USDC": { + "to": "0x2e234DAe75C793f67A35089C9d99245E1C58470b", + "calldataBytes": 292, + "calldataGasL1Rule": 2596, + "unsignedTxBytes": 338, + "gasEstimateForL1": "645", + "gasEstimateForL1CrossCheck": "645", + "l2BaseFeeFromNode": "20000000", + "l1BaseFeeEstimate": "1927507", + "posterCostWei": "12900000000", + "paddedL1GasUnits": 6693 + }, + "v4 minimal locker 100 USDC": { + "to": "0x2e234DAe75C793f67A35089C9d99245E1C58470b", + "calldataBytes": 292, + "calldataGasL1Rule": 2584, + "unsignedTxBytes": 338, + "gasEstimateForL1": "644", + "gasEstimateForL1CrossCheck": "644", + "l2BaseFeeFromNode": "20000000", + "l1BaseFeeEstimate": "1927507", + "posterCostWei": "12880000000", + "paddedL1GasUnits": 6682 + }, + "v4 minimal locker 1000 USDC": { + "to": "0x2e234DAe75C793f67A35089C9d99245E1C58470b", + "calldataBytes": 292, + "calldataGasL1Rule": 2584, + "unsignedTxBytes": 338, + "gasEstimateForL1": "640", + "gasEstimateForL1CrossCheck": "640", + "l2BaseFeeFromNode": "20000000", + "l1BaseFeeEstimate": "1927507", + "posterCostWei": "12800000000", + "paddedL1GasUnits": 6641 + } + }, + "arbGasInfo": { + "perL2Tx": "4317615680", + "weiPerL1CalldataByte": "30840112", + "weiPerStorageAllocation": "401480000000", + "perArbGasBase": "20000000", + "perArbGasCongestion": "74000", + "perArbGasTotal": "20074000", + "l1BaseFeeEstimate": "1927507" + } + } +} diff --git a/benchmarks/fork-l2/l1-data-cost.mjs b/benchmarks/fork-l2/l1-data-cost.mjs new file mode 100644 index 0000000..4354128 --- /dev/null +++ b/benchmarks/fork-l2/l1-data-cost.mjs @@ -0,0 +1,274 @@ +// Measures the L1 data component each L2 charges for the exact calldata of every measured leg, +// by asking the chain's own fee logic at the pinned block: +// Base (OP Stack, Fjord+ estimator): GasPriceOracle.getL1Fee(unsignedTx) and getL1GasUsed(unsignedTx) +// on the 0x42..0F predeploy, plus the L1Block scalars it reads (0x42..15), so the Fjord formula +// can be re-derived by hand from the same inputs. +// Arbitrum One (Nitro): NodeInterface.gasEstimateL1Component(to, false, data) (virtual contract +// 0xc8, eth_call only) and ArbGasInfo.getPricesInWei / getL1BaseFeeEstimate (precompile 0x6C). +// Usage: bun run l1-data-cost.mjs > l1-data-cost.json +import { readFileSync } from "node:fs"; +import { + serializeTransaction, + encodeFunctionData, + decodeFunctionResult, + parseAbi, +} from "/home/sendmoodz/Work/ekubo/yul-router/sdk/node_modules/viem/_esm/index.js"; + +const SENDER = "0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496"; // the forge test contract, recipient in every route +const CHAINS = { + base: { + chainId: 8453, + block: 51439900, + rpcs: [ + "https://base-mainnet.public.blastapi.io", + "https://mainnet.base.org", + ], + yulRouter: "0x7B2aA7Ecc0B5936b7C52E6259A19C3BA557d0748", + v3Router: "0x2626664c2603336E57B271c5C0b26F421741e481", // SwapRouter02 + v4Locker: "0x2e234DAe75C793f67A35089C9d99245E1C58470b", // ForkSwapRouter deployed by the test (address only affects the tx `to` bytes) + legs: [ + ["ekubo Yul router", "ekubo-yul-base", "yulRouter"], + ["v3 SwapRouter02", "v3-SwapRouter02-base", "v3Router"], + ["v4 minimal locker", "v4-minimal-locker-base", "v4Locker"], + ], + }, + arbitrum: { + chainId: 42161, + block: 506178800, + rpcs: [ + "https://arbitrum-one.public.blastapi.io", + "https://arb1.arbitrum.io/rpc", + ], + yulRouter: "0x7B2aA7Ecc0B5936b7C52E6259A19C3BA557d0748", + v3Router: "0xE592427A0AEce92De3Edee1F18E0157C05861564", // classic SwapRouter + v3Router02: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", + v4Locker: "0x2e234DAe75C793f67A35089C9d99245E1C58470b", + legs: [ + ["ekubo Yul router", "ekubo-yul-arbitrum", "yulRouter"], + ["v3 SwapRouter", "v3-SwapRouter-arbitrum", "v3Router"], + ["v3 SwapRouter02", "v3-SwapRouter02-arbitrum", "v3Router02"], + ["v4 minimal locker", "v4-minimal-locker-arbitrum", "v4Locker"], + ], + }, +}; +const SIZES = [50, 100, 1000]; +// Realistic envelope for the unsigned EIP-1559 transaction whose bytes the estimators compress. +const TX = { + nonce: 100, + maxPriorityFeePerGas: 1_000_000n, + maxFeePerGas: 10_000_000n, + gas: 200_000n, + value: 0n, +}; + +const oracleAbi = parseAbi([ + "function getL1Fee(bytes data) view returns (uint256)", + "function getL1GasUsed(bytes data) view returns (uint256)", + "function l1BaseFee() view returns (uint256)", + "function blobBaseFee() view returns (uint256)", + "function baseFeeScalar() view returns (uint32)", + "function blobBaseFeeScalar() view returns (uint32)", + "function isFjord() view returns (bool)", + "function isJovian() view returns (bool)", +]); +const l1BlockAbi = parseAbi([ + "function daFootprintGasScalar() view returns (uint16)", + "function number() view returns (uint64)", +]); +const nodeInterfaceAbi = parseAbi([ + "function gasEstimateL1Component(address to, bool contractCreation, bytes data) view returns (uint64 gasEstimateForL1, uint256 baseFee, uint256 l1BaseFeeEstimate)", +]); +const arbGasInfoAbi = parseAbi([ + "function getPricesInWei() view returns (uint256,uint256,uint256,uint256,uint256,uint256)", + "function getL1BaseFeeEstimate() view returns (uint256)", +]); + +let id = 1; +async function rpc(url, method, params) { + for (let attempt = 0; ; attempt++) { + const res = await fetch(url, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ jsonrpc: "2.0", id: id++, method, params }), + }); + const j = await res.json(); + if (!j.error) return j.result; + if ( + attempt >= 8 || + !/rate limit|429|too many/i.test(JSON.stringify(j.error)) + ) + throw new Error(`${method} on ${url}: ${JSON.stringify(j.error)}`); + await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt)); // backoff on public-endpoint rate limits + } +} +async function call(url, block, to, data, from) { + const tx = { to, data }; + if (from) tx.from = from; + return rpc(url, "eth_call", [tx, "0x" + block.toString(16)]); +} +async function read(url, block, to, abi, functionName, args = [], from) { + const out = await call( + url, + block, + to, + encodeFunctionData({ abi, functionName, args }), + from, + ); + return decodeFunctionResult({ abi, functionName, data: out }); +} +const hex = (f) => readFileSync(`calldata/${f}.hex`, "utf8").trim(); +const bytesOf = (h) => (h.length - 2) / 2; +const calldataGas = (h) => { + let g = 0; + for (let i = 2; i < h.length; i += 2) + g += h.slice(i, i + 2) === "00" ? 4 : 16; + return g; +}; + +const out = {}; +for (const [name, c] of Object.entries(CHAINS)) { + const url = c.rpcs[0]; + const blockHex = "0x" + c.block.toString(16); + const blk = await rpc(url, "eth_getBlockByNumber", [blockHex, false]); + const l2BaseFee = BigInt(blk.baseFeePerGas); + const chain = { + chainId: c.chainId, + block: c.block, + blockHash: blk.hash, + timestamp: Number(blk.timestamp), + l2BaseFeeWei: l2BaseFee.toString(), + rpc: url, + crossCheckRpc: c.rpcs[1], + legs: {}, + }; + if (name === "base") { + const O = "0x420000000000000000000000000000000000000F", + L = "0x4200000000000000000000000000000000000015"; + chain.oracle = { + isFjord: await read(url, c.block, O, oracleAbi, "isFjord"), + isJovian: await read(url, c.block, O, oracleAbi, "isJovian"), + l1BaseFee: ( + await read(url, c.block, O, oracleAbi, "l1BaseFee") + ).toString(), + blobBaseFee: ( + await read(url, c.block, O, oracleAbi, "blobBaseFee") + ).toString(), + baseFeeScalar: Number( + await read(url, c.block, O, oracleAbi, "baseFeeScalar"), + ), + blobBaseFeeScalar: Number( + await read(url, c.block, O, oracleAbi, "blobBaseFeeScalar"), + ), + daFootprintGasScalar: Number( + await read(url, c.block, L, l1BlockAbi, "daFootprintGasScalar"), + ), + l1OriginBlock: Number(await read(url, c.block, L, l1BlockAbi, "number")), + }; + } else { + const G = "0x000000000000000000000000000000000000006C"; + const p = await read(url, c.block, G, arbGasInfoAbi, "getPricesInWei"); + chain.arbGasInfo = { + perL2Tx: p[0].toString(), + weiPerL1CalldataByte: p[1].toString(), + weiPerStorageAllocation: p[2].toString(), + perArbGasBase: p[3].toString(), + perArbGasCongestion: p[4].toString(), + perArbGasTotal: p[5].toString(), + l1BaseFeeEstimate: ( + await read(url, c.block, G, arbGasInfoAbi, "getL1BaseFeeEstimate") + ).toString(), + }; + } + for (const [legName, file, toKey] of c.legs) { + for (const size of SIZES) { + const data = hex(`${file}-${size}`); + const to = c[toKey]; + const unsigned = serializeTransaction({ + type: "eip1559", + chainId: c.chainId, + to, + data, + ...TX, + }); + const leg = { + to, + calldataBytes: bytesOf(data), + calldataGasL1Rule: calldataGas(data), + unsignedTxBytes: bytesOf(unsigned), + }; + if (name === "base") { + const O = "0x420000000000000000000000000000000000000F"; + const l1Fee = await read(url, c.block, O, oracleAbi, "getL1Fee", [ + unsigned, + ]); + const l1GasUsed = await read( + url, + c.block, + O, + oracleAbi, + "getL1GasUsed", + [unsigned], + ); + const l1Fee2 = await read( + c.rpcs[1], + c.block, + O, + oracleAbi, + "getL1Fee", + [unsigned], + ); + const o = chain.oracle; + const feeScaled = + BigInt(o.baseFeeScalar) * 16n * BigInt(o.l1BaseFee) + + BigInt(o.blobBaseFeeScalar) * BigInt(o.blobBaseFee); + // l1Fee = estimatedSizeScaled * feeScaled / 1e12 => estimatedSize bytes = l1Fee * 1e12 / feeScaled / 1e6 + const estimatedSizeScaled = (l1Fee * 10n ** 12n) / feeScaled; + Object.assign(leg, { + l1FeeWei: l1Fee.toString(), + l1FeeWeiCrossCheck: l1Fee2.toString(), + l1GasUsed: l1GasUsed.toString(), + estimatedCompressedBytes: Number(estimatedSizeScaled) / 1e6, + daFootprintGas: + Math.floor(Number(estimatedSizeScaled) / 1e6) * + o.daFootprintGasScalar, + l1FeeInL2GasUnits: Number(l1Fee) / Number(l2BaseFee), + }); + } else { + const N = "0x00000000000000000000000000000000000000C8"; + const r = await read( + url, + c.block, + N, + nodeInterfaceAbi, + "gasEstimateL1Component", + [to, false, data], + SENDER, + ); + const r2 = await read( + c.rpcs[1], + c.block, + N, + nodeInterfaceAbi, + "gasEstimateL1Component", + [to, false, data], + SENDER, + ); + const gasForL1 = BigInt(r[0]); + const posterCostWei = gasForL1 * BigInt(r[1]); + // units = 16 * brotli-compressed bytes of the (fake-signed) tx; gas estimation pads them by 256 units and 1% (nitro arbos/l1pricing) + const paddedUnits = Number(posterCostWei) / Number(r[2]); + Object.assign(leg, { + gasEstimateForL1: gasForL1.toString(), + gasEstimateForL1CrossCheck: r2[0].toString(), + l2BaseFeeFromNode: r[1].toString(), + l1BaseFeeEstimate: r[2].toString(), + posterCostWei: posterCostWei.toString(), + paddedL1GasUnits: Math.round(paddedUnits), + }); + } + chain.legs[`${legName} ${size} USDC`] = leg; + } + } + out[name] = chain; +} +console.log(JSON.stringify(out, null, 2)); diff --git a/benchmarks/fork-l2/quote-live-base.mjs b/benchmarks/fork-l2/quote-live-base.mjs new file mode 100644 index 0000000..89976c5 --- /dev/null +++ b/benchmarks/fork-l2/quote-live-base.mjs @@ -0,0 +1,24 @@ +import { + encodeRoute, + encodeQuoteCalldata, +} from "/home/sendmoodz/Work/ekubo/yul-router/sdk/src/index.ts"; +// live Base pool from the Ekubo API poolKeys listing: native ETH / USDC, fee 0x20c49ba5e353f7 (0.05%), spacing 1000 +const route = encodeRoute({ + specifiedToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + calculatedToken: "0x0000000000000000000000000000000000000000", + specifiedAmount: 1_000_000n, + calculatedAmountThreshold: false, + recipient: "0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496", + hops: [ + { + type: "core", + poolKey: { + token0: "0x0000000000000000000000000000000000000000", + token1: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + config: + "0x00000000000000000000000000000000000000000020c49ba5e353f7800003e8", + }, + }, + ], +}); +console.log(encodeQuoteCalldata(route)); diff --git a/benchmarks/fork-l2/remappings.txt b/benchmarks/fork-l2/remappings.txt new file mode 100644 index 0000000..28caac7 --- /dev/null +++ b/benchmarks/fork-l2/remappings.txt @@ -0,0 +1,6 @@ +forge-std/=lib/forge-std/src/ +solmate/=lib/v4-core/lib/solmate/ +solady/=lib/solady/src/ +v4-core/=lib/v4-core/ +ekubo/=lib/ekubo/ +ekubo-test/=lib/ekubo/test/ diff --git a/benchmarks/fork/ForkEkuboReal.t.sol b/benchmarks/fork/ForkEkuboReal.t.sol new file mode 100644 index 0000000..adae89e --- /dev/null +++ b/benchmarks/fork/ForkEkuboReal.t.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.26; + +import {Test} from "forge-std/Test.sol"; + +interface IERC20Like { + function approve(address spender, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); +} + +/// @notice Mainnet-fork validation against a REAL live Ekubo pool (USDC/USDT, +/// concentrated, spacing 50, core-only extension) through the REAL +/// production Yul router, with SDK-generated calldata (see below). +/// One unmeasured warm-up swap precedes the measured swap (steady state); +/// every call runs isolated in its own EVM context, so the snapshot is the +/// execution gas of the measured swap (net of refunds, excluding the 21,000 +/// base and calldata) with cold access at the start. +/// +/// Calldata generated with the yul-router SDK (`encode-yul-route.mjs`): +/// encodeRoute({ specifiedToken: USDT, calculatedToken: USDC, +/// specifiedAmount, calculatedAmountThreshold = 99% of it, +/// recipient = this test contract, one core hop on the pool key below }) +/// Run with: --fork-url --fork-block-number 25991868 +contract ForkEkuboRealTest is Test { + address constant YUL_ROUTER = 0x7B2aA7Ecc0B5936b7C52E6259A19C3BA557d0748; + address constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + + // Pool: token0 USDC, token1 USDT, concentrated spacing 50, no extension, + // fee 0x53e2d6238da4 / 2^64 (0.0005%). + // Config: 0x0000000000000000000000000000000000000000000053e2d6238da480000032 + bytes constant ROUTE_1000 = hex"0100dAC17F958D2ee523a2206206994597C13D831ec7A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480000000000000000000000003b0233807FA9385bE102ac3EAc297483Dd6233D62b3e14960000000000000000000000003b9aca000000A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48dAC17F958D2ee523a2206206994597C13D831ec70000000000000000000000000000000000000000000053e2d6238da48000003200000000000000000000000000000000"; + bytes constant ROUTE_100 = hex"0100dAC17F958D2ee523a2206206994597C13D831ec7A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB4800000000000000000000000005e69ec07FA9385bE102ac3EAc297483Dd6233D62b3e149600000000000000000000000005f5e1000000A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48dAC17F958D2ee523a2206206994597C13D831ec70000000000000000000000000000000000000000000053e2d6238da48000003200000000000000000000000000000000"; + + function _setup() internal { + assertEq(address(this), 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496, "route recipient is this contract"); + deal(USDT, address(this), 1000000000000); + // USDT returns no returndata: approve via low-level call, not a + // bool-returning interface (which would revert on decoding). + (bool approved,) = USDT.call( + abi.encodeWithSignature("approve(address,uint256)", YUL_ROUTER, type(uint256).max) + ); + assertTrue(approved, "usdt approve"); + assertEq(IERC20Like(USDT).allowance(address(this), YUL_ROUTER), type(uint256).max, "max approval"); + } + + function _swap(bytes memory route, string memory snapshotName) internal returns (uint256 out) { + uint256 usdcBefore = IERC20Like(USDC).balanceOf(address(this)); + (bool ok, bytes memory ret) = YUL_ROUTER.call(route); + if (bytes(snapshotName).length > 0) vm.snapshotGasLastCall(snapshotName); + assertTrue(ok, "swap"); + (, , , int256 calculated) = abi.decode(ret, (address, address, int256, int256)); + assertGt(calculated, 0); + out = IERC20Like(USDC).balanceOf(address(this)) - usdcBefore; + } + + /// forge-config: default.isolate = true + function test_fork_realPool_yulRouter_1000() public { + _setup(); + uint256 warm = _swap(ROUTE_1000, ""); // warm-up: steady state, unmeasured + assertGt(IERC20Like(USDC).balanceOf(address(this)), 0, "recipient USDC balance nonzero before measured swap"); + uint256 out = _swap(ROUTE_1000, "fork ekubo Yul router 1000 USDT->USDC live pool"); + vm.snapshotValue("fork ekubo output 1000 USDT->USDC (USDC, 6 decimals)", out); + emit log_named_uint("ekubo warm-up output (USDC)", warm); + emit log_named_uint("ekubo measured output (USDC)", out); + // production quoter quoted ~999.36 USDC for this size; stay within 1% + assertGe(out, 989000000); + assertLe(out, 1009000000); + } + + /// forge-config: default.isolate = true + function test_fork_realPool_yulRouter_100() public { + _setup(); + _swap(ROUTE_100, ""); + uint256 out = _swap(ROUTE_100, "fork ekubo Yul router 100 USDT->USDC live pool"); + vm.snapshotValue("fork ekubo output 100 USDT->USDC (USDC, 6 decimals)", out); + emit log_named_uint("ekubo measured output (USDC)", out); + assertGe(out, 98900000); + assertLe(out, 100900000); + } +} diff --git a/benchmarks/fork/ForkEkuboRealTest.json b/benchmarks/fork/ForkEkuboRealTest.json new file mode 100644 index 0000000..14d06f2 --- /dev/null +++ b/benchmarks/fork/ForkEkuboRealTest.json @@ -0,0 +1,6 @@ +{ + "fork ekubo Yul router 100 USDT->USDC live pool": "110591", + "fork ekubo Yul router 1000 USDT->USDC live pool": "110622", + "fork ekubo output 100 USDT->USDC (USDC, 6 decimals)": "99920076", + "fork ekubo output 1000 USDT->USDC (USDC, 6 decimals)": "999176773" +} \ No newline at end of file diff --git a/benchmarks/fork/ForkMintEkubo.t.sol b/benchmarks/fork/ForkMintEkubo.t.sol new file mode 100644 index 0000000..5b57245 --- /dev/null +++ b/benchmarks/fork/ForkMintEkubo.t.sol @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.26; + +import {Test} from "forge-std/Test.sol"; + +/// @dev Minimal surface of the deployed canonical Ekubo Positions (evm-contracts v3.1.1 generation). +/// PoolKey is (address token0, address token1, bytes32 config); mintAndDeposit returns the new +/// NFT id, the liquidity added and the amounts pulled from msg.sender. +interface IEkuboPositions { + struct PoolKey { + address token0; + address token1; + bytes32 config; + } + + function mintAndDeposit( + PoolKey memory poolKey, + int32 tickLower, + int32 tickUpper, + uint128 maxAmount0, + uint128 maxAmount1, + uint128 minLiquidity + ) external payable returns (uint256 id, uint128 liquidity, uint128 amount0, uint128 amount1); + function mintAndDepositWithSalt( + bytes32 salt, + PoolKey memory poolKey, + int32 tickLower, + int32 tickUpper, + uint128 maxAmount0, + uint128 maxAmount1, + uint128 minLiquidity + ) external payable returns (uint256 id, uint128 liquidity, uint128 amount0, uint128 amount1); + function getPositionFeesAndLiquidity(uint256 id, PoolKey memory poolKey, int32 tickLower, int32 tickUpper) + external + view + returns (uint128 liquidity, uint128 principal0, uint128 principal1, uint128 fees0, uint128 fees1); + function ownerOf(uint256 id) external view returns (address); + function name() external view returns (string memory); +} + +interface IEkuboCoreLike { + function nextInitializedTick(bytes32 poolId, int32 fromTick, uint32 tickSpacing, uint256 skipAhead) + external + view + returns (int32 tick, bool isInitialized); +} + +interface IERC20Like { + function approve(address spender, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); +} + +/// @notice Mainnet-fork mint through the REAL deployed canonical Ekubo Positions NFT contract +/// (address from the docs contracts reference, verified on the fork: code + name) on the REAL +/// live USDC/USDT concentrated pool used by the fork swap leg (spacing 50, no extension). +/// Pool id re-derived as keccak256(token0, token1, config) and asserted against the id used +/// by the swap leg, and the pool's packed state in Core is asserted initialized. +/// Widest position aligned to spacing 50 (+-88,722,800; MIN/MAX_TICK are +-88,722,835), +/// max amounts 10,000 USDC + 10,000 USDT, NFT to msg.sender (this contract). Tokens are +/// pulled by Positions via transferFrom (ERC20 approval to Positions in setup, not measured). +/// One identical unmeasured mint first (separate NFT; it uses mintAndDepositWithSalt with an +/// explicit salt because mint() derives its salt from prevrandao and remaining gas, which two +/// identical isolated calls share, so a plain repeat would collide on the same NFT id). The +/// measured call is the plain user-facing mintAndDeposit. Every call is its own transaction +/// (`isolate`). USDT returns no returndata: approve via low-level call. +/// Run with: --fork-url --fork-block-number 25991868 +contract ForkMintEkuboTest is Test { + IEkuboPositions constant POSITIONS = IEkuboPositions(0x02D9876A21AF7545f8632C3af76eC90b5ad4b66D); + address constant CORE = 0x00000000000014aA86C5d3c41765bb24e11bd701; + address constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + bytes32 constant CONFIG = 0x0000000000000000000000000000000000000000000053e2d6238da480000032; + bytes32 constant EXPECTED_POOL_ID = 0x6fde3244f6fa747ae318aba6e982a4281febad4d22e3b134aa299a83a951895d; + + int32 constant TICK_LOWER = -88722800; // -1774456 * 50 (MIN_TICK is -88722835) + int32 constant TICK_UPPER = 88722800; + uint128 constant AMOUNT = 10_000_000_000; // 10,000 (6 decimals) per side + + IEkuboPositions.PoolKey key; + + function _setup() internal { + assertGt(address(POSITIONS).code.length, 0, "Positions has code on fork"); + assertGt(CORE.code.length, 0, "Core has code on fork"); + assertEq(POSITIONS.name(), "Ekubo Positions", "positions name"); + + key = IEkuboPositions.PoolKey({token0: USDC, token1: USDT, config: CONFIG}); + bytes32 poolId = keccak256(abi.encode(USDC, USDT, CONFIG)); + assertEq(poolId, EXPECTED_POOL_ID, "pool id matches fork swap leg"); + // Core packs pool state at slot poolId: sqrtRatio (96 bits) | tick (32) | liquidity (128) + bytes32 state = vm.load(CORE, poolId); + assertGt(uint256(state) >> 160, 0, "pool initialized (sqrtRatio != 0)"); + assertEq(uint256(CONFIG) & 0x7fffffff, 50, "tick spacing 50"); + assertTrue((uint256(CONFIG) & 0x80000000) != 0, "concentrated"); + + deal(USDC, address(this), 100_000_000_000); + deal(USDT, address(this), 100_000_000_000); + assertTrue(IERC20Like(USDC).approve(address(POSITIONS), type(uint256).max), "usdc approve"); + (bool ok,) = + USDT.call(abi.encodeWithSignature("approve(address,uint256)", address(POSITIONS), type(uint256).max)); + assertTrue(ok, "usdt approve"); + assertEq(IERC20Like(USDC).allowance(address(this), address(POSITIONS)), type(uint256).max); + assertEq(IERC20Like(USDT).allowance(address(this), address(POSITIONS)), type(uint256).max); + } + + function _mint() internal returns (uint256 id, uint128 liquidity, uint128 amount0, uint128 amount1) { + return POSITIONS.mintAndDeposit(key, TICK_LOWER, TICK_UPPER, AMOUNT, AMOUNT, 0); + } + + /// @dev Whether `tick` is an initialized tick of the pool, via Core's bitmap walk from the + /// previous spacing multiple. + function _tickInitialized(bytes32 poolId, int32 tick) internal view returns (bool) { + (int32 found, bool init) = IEkuboCoreLike(CORE).nextInitializedTick(poolId, tick - 50, 50, 0); + return init && found == tick; + } + + /// forge-config: default.isolate = true + function test_fork_mint_ekubo_positions_fullRange() public { + _setup(); + bytes32 poolId = keccak256(abi.encode(USDC, USDT, CONFIG)); + uint128 poolLiqBefore = uint128(uint256(vm.load(CORE, poolId))); + emit log_named_string( + "ekubo boundary ticks initialized before warm-up", + _tickInitialized(poolId, TICK_LOWER) && _tickInitialized(poolId, TICK_UPPER) ? "yes" : "no" + ); + + // warm-up: a separate position (explicit salt -> distinct NFT id); unmeasured for the headline, + // recorded for context + (uint256 warmId, uint128 warmLiq,,) = + POSITIONS.mintAndDepositWithSalt(bytes32(uint256(1)), key, TICK_LOWER, TICK_UPPER, AMOUNT, AMOUNT, 0); + vm.snapshotGasLastCall("fork ekubo Positions mintAndDepositWithSalt full-range USDC/USDT (first, warm-up)"); + assertGt(warmLiq, 0); + assertEq(POSITIONS.ownerOf(warmId), address(this)); + assertTrue( + _tickInitialized(poolId, TICK_LOWER) && _tickInitialized(poolId, TICK_UPPER), + "boundary ticks initialized before measured mint" + ); + + uint256 usdcBefore = IERC20Like(USDC).balanceOf(address(this)); + uint256 usdtBefore = IERC20Like(USDT).balanceOf(address(this)); + + (uint256 id, uint128 liquidity, uint128 amount0, uint128 amount1) = _mint(); + vm.snapshotGasLastCall("fork ekubo Positions mintAndDeposit full-range USDC/USDT (measured)"); + + // proof of execution + assertTrue(id != warmId, "new NFT"); + assertGt(liquidity, 0, "liquidity > 0"); + assertEq(POSITIONS.ownerOf(id), address(this), "NFT owned by minter"); + (uint128 posLiq,,,,) = POSITIONS.getPositionFeesAndLiquidity(id, key, TICK_LOWER, TICK_UPPER); + assertEq(posLiq, liquidity, "position liquidity recorded in Core"); + assertEq(IERC20Like(USDC).balanceOf(address(this)), usdcBefore - amount0, "USDC moved"); + assertEq(IERC20Like(USDT).balanceOf(address(this)), usdtBefore - amount1, "USDT moved"); + assertGt(amount0, 0); + assertGt(amount1, 0); + assertLe(amount0, AMOUNT); + assertLe(amount1, AMOUNT); + uint128 poolLiqAfter = uint128(uint256(vm.load(CORE, poolId))); + assertEq(poolLiqAfter, poolLiqBefore + warmLiq + liquidity, "pool active liquidity increased"); + + vm.snapshotValue("fork ekubo Positions mint id", id); + vm.snapshotValue("fork ekubo Positions mint liquidity", liquidity); + vm.snapshotValue("fork ekubo Positions mint amount0 (USDC, 6 decimals)", amount0); + vm.snapshotValue("fork ekubo Positions mint amount1 (USDT, 6 decimals)", amount1); + emit log_named_uint("ekubo id", id); + emit log_named_uint("ekubo liquidity", liquidity); + emit log_named_uint("ekubo amount0 USDC", amount0); + emit log_named_uint("ekubo amount1 USDT", amount1); + } + + /// @dev Same mint after the same full-range warm-up, but on a range whose boundary ticks are + /// NOT initialized on-chain (asserted), so the figure includes Ekubo tick initialization. + /// forge-config: default.isolate = true + function test_fork_mint_ekubo_positions_coldBoundaryTicks() public { + _setup(); + bytes32 poolId = keccak256(abi.encode(USDC, USDT, CONFIG)); + int32 lower = -88_722_750; // -1774455 * 50 + int32 upper = 88_722_750; + assertTrue(!_tickInitialized(poolId, lower) && !_tickInitialized(poolId, upper), "boundary ticks cold"); + + (uint256 warmId,,,) = + POSITIONS.mintAndDepositWithSalt(bytes32(uint256(1)), key, TICK_LOWER, TICK_UPPER, AMOUNT, AMOUNT, 0); + assertEq(POSITIONS.ownerOf(warmId), address(this)); + + (uint256 id, uint128 liquidity,,) = POSITIONS.mintAndDeposit(key, lower, upper, AMOUNT, AMOUNT, 0); + vm.snapshotGasLastCall("fork ekubo Positions mintAndDeposit USDC/USDT, cold boundary ticks"); + assertGt(liquidity, 0); + assertEq(POSITIONS.ownerOf(id), address(this)); + assertTrue(_tickInitialized(poolId, lower) && _tickInitialized(poolId, upper), "ticks initialized by mint"); + } +} diff --git a/benchmarks/fork/ForkMintEkuboTest.json b/benchmarks/fork/ForkMintEkuboTest.json new file mode 100644 index 0000000..4c78397 --- /dev/null +++ b/benchmarks/fork/ForkMintEkuboTest.json @@ -0,0 +1,9 @@ +{ + "fork ekubo Positions mint amount0 (USDC, 6 decimals)": "9992084352", + "fork ekubo Positions mint amount1 (USDT, 6 decimals)": "10000000000", + "fork ekubo Positions mint id": "25083100237766864067710388918595849694396003226932398993912489470024538291565", + "fork ekubo Positions mint liquidity": "9996041392", + "fork ekubo Positions mintAndDeposit USDC/USDT, cold boundary ticks": "369791", + "fork ekubo Positions mintAndDeposit full-range USDC/USDT (measured)": "244513", + "fork ekubo Positions mintAndDepositWithSalt full-range USDC/USDT (first, warm-up)": "420636" +} \ No newline at end of file diff --git a/benchmarks/fork/ForkMintV3.t.sol b/benchmarks/fork/ForkMintV3.t.sol new file mode 100644 index 0000000..1d346f5 --- /dev/null +++ b/benchmarks/fork/ForkMintV3.t.sol @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.26; + +import {Test} from "forge-std/Test.sol"; + +interface INonfungiblePositionManager { + struct MintParams { + address token0; + address token1; + uint24 fee; + int24 tickLower; + int24 tickUpper; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + address recipient; + uint256 deadline; + } + + function mint(MintParams calldata params) + external + payable + returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1); + function positions(uint256 tokenId) + external + view + returns ( + uint96 nonce, + address operator, + address token0, + address token1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ); + function ownerOf(uint256 tokenId) external view returns (address); + function factory() external view returns (address); + function name() external view returns (string memory); +} + +interface IUniswapV3PoolLike { + function tickSpacing() external view returns (int24); + function fee() external view returns (uint24); + function token0() external view returns (address); + function token1() external view returns (address); + function liquidity() external view returns (uint128); + function ticks(int24 tick) + external + view + returns ( + uint128 liquidityGross, + int128 liquidityNet, + uint256 feeGrowthOutside0X128, + uint256 feeGrowthOutside1X128, + int56 tickCumulativeOutside, + uint160 secondsPerLiquidityOutsideX128, + uint32 secondsOutside, + bool initialized + ); +} + +interface IERC20Like { + function approve(address spender, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); +} + +/// @notice Mainnet-fork mint through the REAL deployed Uniswap v3 NonfungiblePositionManager +/// on the REAL USDT/USDC fee-100 pool (the same pool as the fork swap legs). +/// Full-range position (tick spacing 1: MIN_TICK..MAX_TICK), ~10,000 USDC + ~10,000 USDT, +/// recipient = this contract, deadline = block.timestamp. One identical unmeasured +/// mint first (a separate NFT / separate position: warm-up initializes the boundary +/// ticks if they were cold and warms the manager's per-pool state); every call is its +/// own transaction (`isolate`), so the snapshot is the execution gas of the measured mint. +/// USDT returns no returndata: approve via low-level call. +/// Run with: --fork-url --fork-block-number 25991868 +contract ForkMintV3Test is Test { + INonfungiblePositionManager constant NPM = INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); + IUniswapV3PoolLike constant POOL = IUniswapV3PoolLike(0x3416cF6C708Da44DB2624D63ea0AAef7113527C6); + address constant FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + address constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + + int24 constant TICK_LOWER = -887272; // TickMath.MIN_TICK, usable at spacing 1 + int24 constant TICK_UPPER = 887272; // TickMath.MAX_TICK + uint256 constant AMOUNT = 10_000_000_000; // 10,000 (6 decimals) per side + + function _setup() internal { + assertGt(address(NPM).code.length, 0, "NPM has code on fork"); + assertEq(NPM.factory(), FACTORY, "NPM factory"); + assertEq(NPM.name(), "Uniswap V3 Positions NFT-V1", "NPM name"); + assertEq(POOL.token0(), USDC, "pool token0"); + assertEq(POOL.token1(), USDT, "pool token1"); + assertEq(POOL.fee(), 100, "pool fee"); + assertEq(POOL.tickSpacing(), 1, "pool spacing"); + + deal(USDC, address(this), 100_000_000_000); + deal(USDT, address(this), 100_000_000_000); + assertTrue(IERC20Like(USDC).approve(address(NPM), type(uint256).max), "usdc approve"); + (bool ok,) = USDT.call(abi.encodeWithSignature("approve(address,uint256)", address(NPM), type(uint256).max)); + assertTrue(ok, "usdt approve"); + assertEq(IERC20Like(USDC).allowance(address(this), address(NPM)), type(uint256).max); + assertEq(IERC20Like(USDT).allowance(address(this), address(NPM)), type(uint256).max); + } + + function _mint() internal returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) { + return _mintRange(TICK_LOWER, TICK_UPPER); + } + + function _mintRange(int24 lower, int24 upper) + internal + returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) + { + return NPM.mint( + INonfungiblePositionManager.MintParams({ + token0: USDC, + token1: USDT, + fee: 100, + tickLower: lower, + tickUpper: upper, + amount0Desired: AMOUNT, + amount1Desired: AMOUNT, + amount0Min: 0, + amount1Min: 0, + recipient: address(this), + deadline: block.timestamp + }) + ); + } + + /// forge-config: default.isolate = true + function test_fork_mint_v3_npm_fullRange() public { + _setup(); + (,,,,,,, bool lowerInit) = POOL.ticks(TICK_LOWER); + (,,,,,,, bool upperInit) = POOL.ticks(TICK_UPPER); + emit log_named_string("v3 boundary ticks initialized before warm-up", lowerInit && upperInit ? "yes" : "no"); + uint128 poolLiqBefore = POOL.liquidity(); + + // warm-up: a separate position (new tokenId); unmeasured for the headline, recorded for context + (uint256 warmId, uint128 warmLiq,,) = _mint(); + vm.snapshotGasLastCall("fork v3 NPM mint full-range USDC/USDT fee-100 (first, warm-up)"); + assertGt(warmLiq, 0); + assertEq(NPM.ownerOf(warmId), address(this)); + + (,,,,,,, lowerInit) = POOL.ticks(TICK_LOWER); + (,,,,,,, upperInit) = POOL.ticks(TICK_UPPER); + assertTrue(lowerInit && upperInit, "boundary ticks initialized before measured mint"); + + uint256 usdcBefore = IERC20Like(USDC).balanceOf(address(this)); + uint256 usdtBefore = IERC20Like(USDT).balanceOf(address(this)); + + (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) = _mint(); + vm.snapshotGasLastCall("fork v3 NPM mint full-range USDC/USDT fee-100 (measured)"); + + // proof of execution + assertTrue(tokenId != warmId, "new NFT"); + assertGt(liquidity, 0, "liquidity > 0"); + assertEq(NPM.ownerOf(tokenId), address(this), "NFT owned by recipient"); + (,,,,,,, uint128 posLiq,,,,) = NPM.positions(tokenId); + assertEq(posLiq, liquidity, "position liquidity recorded"); + assertEq(IERC20Like(USDC).balanceOf(address(this)), usdcBefore - amount0, "USDC moved"); + assertEq(IERC20Like(USDT).balanceOf(address(this)), usdtBefore - amount1, "USDT moved"); + assertGt(amount0, 0); + assertGt(amount1, 0); + assertEq(POOL.liquidity(), poolLiqBefore + warmLiq + liquidity, "pool active liquidity increased"); + + vm.snapshotValue("fork v3 NPM mint tokenId", tokenId); + vm.snapshotValue("fork v3 NPM mint liquidity", liquidity); + vm.snapshotValue("fork v3 NPM mint amount0 (USDC, 6 decimals)", amount0); + vm.snapshotValue("fork v3 NPM mint amount1 (USDT, 6 decimals)", amount1); + emit log_named_uint("v3 tokenId", tokenId); + emit log_named_uint("v3 liquidity", liquidity); + emit log_named_uint("v3 amount0 USDC", amount0); + emit log_named_uint("v3 amount1 USDT", amount1); + } + + /// @dev Same mint after the same full-range warm-up, but on a range whose boundary ticks are + /// NOT initialized on-chain (asserted), so the figure includes v3 tick initialization. + /// forge-config: default.isolate = true + function test_fork_mint_v3_npm_coldBoundaryTicks() public { + _setup(); + int24 lower = -500_001; + int24 upper = 500_001; + (,,,,,,, bool lowerInit) = POOL.ticks(lower); + (,,,,,,, bool upperInit) = POOL.ticks(upper); + assertTrue(!lowerInit && !upperInit, "boundary ticks cold before mint"); + + (uint256 warmId,,,) = _mint(); // full-range warm-up, as in the headline test + assertEq(NPM.ownerOf(warmId), address(this)); + + (uint256 tokenId, uint128 liquidity,,) = _mintRange(lower, upper); + vm.snapshotGasLastCall("fork v3 NPM mint USDC/USDT fee-100, cold boundary ticks"); + assertGt(liquidity, 0); + assertEq(NPM.ownerOf(tokenId), address(this)); + (,,,,,,, lowerInit) = POOL.ticks(lower); + (,,,,,,, upperInit) = POOL.ticks(upper); + assertTrue(lowerInit && upperInit, "boundary ticks initialized by the mint"); + } +} diff --git a/benchmarks/fork/ForkMintV3Test.json b/benchmarks/fork/ForkMintV3Test.json new file mode 100644 index 0000000..2573493 --- /dev/null +++ b/benchmarks/fork/ForkMintV3Test.json @@ -0,0 +1,9 @@ +{ + "fork v3 NPM mint USDC/USDT fee-100, cold boundary ticks": "522450", + "fork v3 NPM mint amount0 (USDC, 6 decimals)": "9993094345", + "fork v3 NPM mint amount1 (USDT, 6 decimals)": "10000000000", + "fork v3 NPM mint full-range USDC/USDT fee-100 (first, warm-up)": "416642", + "fork v3 NPM mint full-range USDC/USDT fee-100 (measured)": "381653", + "fork v3 NPM mint liquidity": "9996546576", + "fork v3 NPM mint tokenId": "1365900" +} \ No newline at end of file diff --git a/benchmarks/fork/ForkMintV4.t.sol b/benchmarks/fork/ForkMintV4.t.sol new file mode 100644 index 0000000..c85aa60 --- /dev/null +++ b/benchmarks/fork/ForkMintV4.t.sol @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.26; + +import {Test} from "forge-std/Test.sol"; +import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "v4-core/src/types/PoolKey.sol"; +import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; +import {Currency} from "v4-core/src/types/Currency.sol"; +import {IHooks} from "v4-core/src/interfaces/IHooks.sol"; +import {TickMath} from "v4-core/src/libraries/TickMath.sol"; +import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol"; +import {LiquidityAmounts} from "v4-core/test/utils/LiquidityAmounts.sol"; + +/// @dev Minimal surface of the deployed v4 PositionManager (v4-periphery). +interface IV4PositionManager { + function modifyLiquidities(bytes calldata unlockData, uint256 deadline) external payable; + function nextTokenId() external view returns (uint256); + function ownerOf(uint256 tokenId) external view returns (address); + function getPositionLiquidity(uint256 tokenId) external view returns (uint128); + function poolKeys(bytes25 poolId) external view returns (Currency, Currency, uint24, int24, IHooks); + function poolManager() external view returns (address); + function permit2() external view returns (address); + function name() external view returns (string memory); +} + +interface IPermit2Like { + function approve(address token, address spender, uint160 amount, uint48 expiration) external; + function allowance(address user, address token, address spender) + external + view + returns (uint160 amount, uint48 expiration, uint48 nonce); +} + +interface IERC20Like { + function approve(address spender, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); +} + +/// @notice Mainnet-fork mint through the REAL deployed Uniswap v4 PositionManager +/// (canonical mainnet address per docs.uniswap.org/contracts/v4/deployments, verified on the +/// fork: name, poolManager(), permit2()) on the REAL USDC/USDT fee-100 hookless pool used by +/// the fork swap leg. Full-range position (tick spacing 1: MIN_TICK..MAX_TICK), liquidity +/// sized to ~10,000 USDC + ~10,000 USDT, owner = this contract, deadline = block.timestamp. +/// Actions: MINT_POSITION (0x02) + SETTLE_PAIR (0x0d); tokens are pulled through Permit2, as +/// for every PositionManager user (ERC20 approval to Permit2 + Permit2 allowance to the manager, +/// both granted in setup and not measured). One identical unmeasured mint first (separate NFT); +/// every call is its own transaction (`isolate`). +/// Run with: --fork-url --fork-block-number 25991868 +contract ForkMintV4Test is Test { + using StateLibrary for IPoolManager; + using PoolIdLibrary for PoolKey; + + IPoolManager constant MANAGER = IPoolManager(0x000000000004444c5dc75cB358380D2e3dE08A90); + IV4PositionManager constant POSM = IV4PositionManager(0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e); + IPermit2Like constant PERMIT2 = IPermit2Like(0x000000000022D473030F116dDEE9F6B43aC78BA3); + address constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + bytes32 constant EXPECTED_POOL_ID = 0xe018f09af38956affdfeab72c2cefbcd4e6fee44d09df7525ec9dba3e51356a5; + + uint8 constant MINT_POSITION = 0x02; + uint8 constant SETTLE_PAIR = 0x0d; + int24 constant TICK_LOWER = -887272; // TickMath.MIN_TICK, usable at spacing 1 + int24 constant TICK_UPPER = 887272; + uint256 constant AMOUNT = 10_000_000_000; // 10,000 (6 decimals) per side + + PoolKey key; + + function _setup() internal { + assertGt(address(POSM).code.length, 0, "PositionManager has code on fork"); + assertEq(POSM.poolManager(), address(MANAGER), "posm.poolManager"); + assertEq(POSM.permit2(), address(PERMIT2), "posm.permit2"); + assertEq(POSM.name(), "Uniswap v4 Positions NFT", "posm name"); + + key = PoolKey({ + currency0: Currency.wrap(USDC), + currency1: Currency.wrap(USDT), + fee: 100, + tickSpacing: 1, + hooks: IHooks(address(0)) + }); + assertEq(PoolId.unwrap(key.toId()), EXPECTED_POOL_ID, "pool id matches fork swap leg"); + (uint160 sqrtP,,,) = MANAGER.getSlot0(key.toId()); + assertGt(sqrtP, 0, "pool initialized"); + + deal(USDC, address(this), 100_000_000_000); + deal(USDT, address(this), 100_000_000_000); + assertTrue(IERC20Like(USDC).approve(address(PERMIT2), type(uint256).max), "usdc approve permit2"); + (bool ok,) = USDT.call(abi.encodeWithSignature("approve(address,uint256)", address(PERMIT2), type(uint256).max)); + assertTrue(ok, "usdt approve permit2"); + PERMIT2.approve(USDC, address(POSM), type(uint160).max, type(uint48).max); + PERMIT2.approve(USDT, address(POSM), type(uint160).max, type(uint48).max); + (uint160 a0,,) = PERMIT2.allowance(address(this), USDC, address(POSM)); + (uint160 a1,,) = PERMIT2.allowance(address(this), USDT, address(POSM)); + assertEq(a0, type(uint160).max); + assertEq(a1, type(uint160).max); + } + + function _liquidityForAmounts() internal view returns (uint128) { + (uint160 sqrtP,,,) = MANAGER.getSlot0(key.toId()); + return LiquidityAmounts.getLiquidityForAmounts( + sqrtP, TickMath.getSqrtPriceAtTick(TICK_LOWER), TickMath.getSqrtPriceAtTick(TICK_UPPER), AMOUNT, AMOUNT + ); + } + + function _mint(uint128 liquidity) internal returns (uint256 tokenId) { + return _mintRange(TICK_LOWER, TICK_UPPER, liquidity); + } + + function _mintRange(int24 lower, int24 upper, uint128 liquidity) internal returns (uint256 tokenId) { + tokenId = POSM.nextTokenId(); + bytes memory actions = abi.encodePacked(MINT_POSITION, SETTLE_PAIR); + bytes[] memory params = new bytes[](2); + params[0] = abi.encode( + key, lower, upper, uint256(liquidity), uint128(AMOUNT), uint128(AMOUNT), address(this), bytes("") + ); + params[1] = abi.encode(key.currency0, key.currency1); + POSM.modifyLiquidities(abi.encode(actions, params), block.timestamp); + } + + /// forge-config: default.isolate = true + function test_fork_mint_v4_posm_fullRange() public { + _setup(); + bytes25 shortId = bytes25(PoolId.unwrap(key.toId())); + (Currency c0, Currency c1,,,) = POSM.poolKeys(shortId); + emit log_named_string( + "v4 PositionManager already knows this pool key before warm-up", + Currency.unwrap(c0) == USDC && Currency.unwrap(c1) == USDT ? "yes" : "no" + ); + (uint128 lowerGross,) = MANAGER.getTickLiquidity(key.toId(), TICK_LOWER); + (uint128 upperGross,) = MANAGER.getTickLiquidity(key.toId(), TICK_UPPER); + emit log_named_string( + "v4 boundary ticks initialized before warm-up", lowerGross > 0 && upperGross > 0 ? "yes" : "no" + ); + uint128 poolLiqBefore = MANAGER.getLiquidity(key.toId()); + uint128 liquidity = _liquidityForAmounts(); + assertGt(liquidity, 0); + + // warm-up: a separate position (new tokenId); unmeasured for the headline, recorded for context + uint256 warmId = _mint(liquidity); + vm.snapshotGasLastCall("fork v4 PositionManager mint full-range USDC/USDT fee-100 (first, warm-up)"); + assertEq(POSM.ownerOf(warmId), address(this)); + assertEq(POSM.getPositionLiquidity(warmId), liquidity); + + (c0, c1,,,) = POSM.poolKeys(shortId); + assertTrue(Currency.unwrap(c0) == USDC && Currency.unwrap(c1) == USDT, "pool key stored before measured mint"); + (lowerGross,) = MANAGER.getTickLiquidity(key.toId(), TICK_LOWER); + (upperGross,) = MANAGER.getTickLiquidity(key.toId(), TICK_UPPER); + assertTrue(lowerGross > 0 && upperGross > 0, "boundary ticks initialized before measured mint"); + + uint256 usdcBefore = IERC20Like(USDC).balanceOf(address(this)); + uint256 usdtBefore = IERC20Like(USDT).balanceOf(address(this)); + + uint256 tokenId = _mint(liquidity); + vm.snapshotGasLastCall("fork v4 PositionManager mint full-range USDC/USDT fee-100 (measured)"); + + // proof of execution + assertEq(tokenId, warmId + 1, "new NFT"); + assertEq(POSM.ownerOf(tokenId), address(this), "NFT owned by recipient"); + assertEq(POSM.getPositionLiquidity(tokenId), liquidity, "position liquidity recorded"); + uint256 amount0 = usdcBefore - IERC20Like(USDC).balanceOf(address(this)); + uint256 amount1 = usdtBefore - IERC20Like(USDT).balanceOf(address(this)); + assertGt(amount0, 0, "USDC moved"); + assertGt(amount1, 0, "USDT moved"); + assertLe(amount0, AMOUNT); + assertLe(amount1, AMOUNT); + assertEq(MANAGER.getLiquidity(key.toId()), poolLiqBefore + 2 * liquidity, "pool active liquidity increased"); + + vm.snapshotValue("fork v4 PositionManager mint tokenId", tokenId); + vm.snapshotValue("fork v4 PositionManager mint liquidity", liquidity); + vm.snapshotValue("fork v4 PositionManager mint amount0 (USDC, 6 decimals)", amount0); + vm.snapshotValue("fork v4 PositionManager mint amount1 (USDT, 6 decimals)", amount1); + emit log_named_uint("v4 tokenId", tokenId); + emit log_named_uint("v4 liquidity", liquidity); + emit log_named_uint("v4 amount0 USDC", amount0); + emit log_named_uint("v4 amount1 USDT", amount1); + } + + /// @dev Same mint after the same full-range warm-up, but on a range whose boundary ticks are + /// NOT initialized on-chain (asserted), so the figure includes v4 tick initialization. + /// forge-config: default.isolate = true + function test_fork_mint_v4_posm_coldBoundaryTicks() public { + _setup(); + int24 lower = -500_001; + int24 upper = 500_001; + (uint128 lowerGross,) = MANAGER.getTickLiquidity(key.toId(), lower); + (uint128 upperGross,) = MANAGER.getTickLiquidity(key.toId(), upper); + assertTrue(lowerGross == 0 && upperGross == 0, "boundary ticks cold before mint"); + + uint128 liquidity = _liquidityForAmounts(); + uint256 warmId = _mint(liquidity); // full-range warm-up, as in the headline test + assertEq(POSM.ownerOf(warmId), address(this)); + + (uint160 sqrtP,,,) = MANAGER.getSlot0(key.toId()); + uint128 liq = LiquidityAmounts.getLiquidityForAmounts( + sqrtP, TickMath.getSqrtPriceAtTick(lower), TickMath.getSqrtPriceAtTick(upper), AMOUNT, AMOUNT + ); + uint256 tokenId = _mintRange(lower, upper, liq); + vm.snapshotGasLastCall("fork v4 PositionManager mint USDC/USDT fee-100, cold boundary ticks"); + assertEq(POSM.ownerOf(tokenId), address(this)); + assertEq(POSM.getPositionLiquidity(tokenId), liq); + (lowerGross,) = MANAGER.getTickLiquidity(key.toId(), lower); + (upperGross,) = MANAGER.getTickLiquidity(key.toId(), upper); + assertTrue(lowerGross > 0 && upperGross > 0, "boundary ticks initialized by the mint"); + } +} diff --git a/benchmarks/fork/ForkMintV4Test.json b/benchmarks/fork/ForkMintV4Test.json new file mode 100644 index 0000000..6d3025c --- /dev/null +++ b/benchmarks/fork/ForkMintV4Test.json @@ -0,0 +1,9 @@ +{ + "fork v4 PositionManager mint USDC/USDT fee-100, cold boundary ticks": "376984", + "fork v4 PositionManager mint amount0 (USDC, 6 decimals)": "9993022187", + "fork v4 PositionManager mint amount1 (USDT, 6 decimals)": "10000000000", + "fork v4 PositionManager mint full-range USDC/USDT fee-100 (first, warm-up)": "314490", + "fork v4 PositionManager mint full-range USDC/USDT fee-100 (measured)": "297390", + "fork v4 PositionManager mint liquidity": "9996510484", + "fork v4 PositionManager mint tokenId": "405710" +} \ No newline at end of file diff --git a/benchmarks/fork/ForkProbe.t.sol b/benchmarks/fork/ForkProbe.t.sol new file mode 100644 index 0000000..858bbe4 --- /dev/null +++ b/benchmarks/fork/ForkProbe.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.26; + +import {Test, console2} from "forge-std/Test.sol"; +import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "v4-core/src/types/PoolKey.sol"; +import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; +import {Currency} from "v4-core/src/types/Currency.sol"; +import {IHooks} from "v4-core/src/interfaces/IHooks.sol"; +import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol"; + +interface IUniswapV3FactoryLike { + function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool); +} + +interface IUniswapV3PoolLike { + function liquidity() external view returns (uint128); + function slot0() + external + view + returns ( + uint160 sqrtPriceX96, + int24 tick, + uint16 observationIndex, + uint16 observationCardinality, + uint16 observationCardinalityNext, + uint8 feeProtocol, + bool unlocked + ); +} + +/// @notice Identifies the three fork pools at the pinned block: address / id, fee, +/// spacing, hooks (v4), in-range liquidity and current tick, so the fork numbers +/// can be tied to concrete on-chain state. Read-only. +/// Run with: --fork-url --fork-block-number 25991868 +contract ForkProbeTest is Test { + using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; + + IPoolManager constant MANAGER = IPoolManager(0x000000000004444c5dc75cB358380D2e3dE08A90); + IUniswapV3FactoryLike constant V3_FACTORY = IUniswapV3FactoryLike(0x1F98431c8aD98523631AE4a59f267346ea31F984); + address constant EKUBO_CORE = 0x00000000000014aA86C5d3c41765bb24e11bd701; + address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + bytes32 constant EKUBO_CONFIG = 0x0000000000000000000000000000000000000000000053e2d6238da480000032; + + function test_probe_block() public view { + console2.log("block", block.number); + console2.log("timestamp", block.timestamp); + } + + function test_probe_usdt_usdc_pools_v4() public view { + uint24[3] memory fees = [uint24(100), uint24(500), uint24(3000)]; + int24[3] memory spacings = [int24(1), int24(10), int24(60)]; + for (uint256 i = 0; i < 3; i++) { + PoolKey memory key = PoolKey({ + currency0: Currency.wrap(USDC), + currency1: Currency.wrap(USDT), + fee: fees[i], + tickSpacing: spacings[i], + hooks: IHooks(address(0)) + }); + PoolId id = key.toId(); + uint128 liq = MANAGER.getLiquidity(id); + (uint160 sqrtP, int24 tick,,) = MANAGER.getSlot0(id); + console2.log("v4 fee", fees[i]); + console2.logBytes32(PoolId.unwrap(id)); + console2.log("v4 initialized (sqrtPrice != 0, hooks = 0)", sqrtP != 0); + console2.log("v4 liquidity", liq); + console2.log("v4 tick", tick); + } + } + + function test_probe_usdt_usdc_pools_v3() public view { + uint24[3] memory fees = [uint24(100), uint24(500), uint24(3000)]; + for (uint256 i = 0; i < 3; i++) { + address pool = V3_FACTORY.getPool(USDC, USDT, fees[i]); + console2.log("v3 fee", fees[i]); + console2.log("v3 pool", pool); + if (pool == address(0)) continue; + (, int24 tick,, uint16 cardinality,,,) = IUniswapV3PoolLike(pool).slot0(); + console2.log("v3 liquidity", IUniswapV3PoolLike(pool).liquidity()); + console2.log("v3 tick", tick); + console2.log("v3 observation cardinality", cardinality); + } + } + + /// @dev Core stores the packed pool state at slot `poolId = keccak256(token0, token1, config)`: + /// sqrtRatio (96 bits) | tick (32 bits) | liquidity (128 bits). + function test_probe_usdc_usdt_pool_ekubo() public view { + bytes32 poolId = keccak256(abi.encode(USDC, USDT, EKUBO_CONFIG)); + bytes32 state = vm.load(EKUBO_CORE, poolId); + uint256 sqrtRatio = uint256(state) >> 160; + int32 tick = int32(uint32((uint256(state) >> 128) & 0xffffffff)); + uint128 liq = uint128(uint256(state)); + uint64 fee = uint64((uint256(EKUBO_CONFIG) >> 32) & 0xffffffffffffffff); + console2.log("ekubo config fee (Q64)", fee); + console2.log("ekubo config tick spacing", uint256(EKUBO_CONFIG) & 0x7fffffff); + console2.log("ekubo config concentrated", (uint256(EKUBO_CONFIG) & 0x80000000) != 0); + console2.logBytes32(poolId); + console2.log("ekubo sqrtRatio raw (96-bit)", sqrtRatio); + console2.log("ekubo tick", tick); + console2.log("ekubo liquidity", liq); + assertTrue(state != bytes32(0), "pool initialized"); + } +} diff --git a/benchmarks/fork/ForkV3.t.sol b/benchmarks/fork/ForkV3.t.sol new file mode 100644 index 0000000..65baa75 --- /dev/null +++ b/benchmarks/fork/ForkV3.t.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.26; + +import {Test} from "forge-std/Test.sol"; + +interface ISwapRouter { + struct ExactInputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + uint160 sqrtPriceLimitX96; + } + + function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); +} + +interface IERC20Like { + function balanceOf(address account) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); +} + +interface IWETH9 { + function deposit() external payable; + function balanceOf(address account) external view returns (uint256); +} + +/// @notice Mainnet-fork validation: REAL SwapRouter + REAL USDT/USDC fee-100 pool +/// (deepest at the pinned block), exact input, warmed with one identical +/// unmeasured swap (steady state); every call runs isolated in its own EVM +/// context, so the snapshot is the execution gas of the measured swap (net of +/// refunds, excluding the 21,000 base and calldata). +/// USDT returns no returndata: approve via low-level call. Run with: --fork-url --fork-block-number 25991868 +contract ForkV3Test is Test { + ISwapRouter constant ROUTER = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); + address constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + IWETH9 constant WETH = IWETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + function _setup() internal { + deal(USDT, address(this), 1000000000000); + (bool approved,) = + USDT.call(abi.encodeWithSignature("approve(address,uint256)", address(ROUTER), type(uint256).max)); + assertTrue(approved, "usdt approve"); + assertEq(IERC20Like(USDT).allowance(address(this), address(ROUTER)), type(uint256).max, "max approval"); + } + + function _swap(uint256 amountIn) internal returns (uint256) { + return ROUTER.exactInputSingle( + ISwapRouter.ExactInputSingleParams({ + tokenIn: USDT, + tokenOut: USDC, + fee: 100, + recipient: address(this), + deadline: block.timestamp, + amountIn: amountIn, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }) + ); + } + + /// forge-config: default.isolate = true + function test_fork_single_exactInput_usdt_1000() public { + _setup(); + _swap(1000000000); // warm-up, unmeasured + assertGt(IERC20Like(USDC).balanceOf(address(this)), 0, "recipient USDC balance nonzero before measured swap"); + uint256 out = _swap(1000000000); // measured + vm.snapshotGasLastCall("fork v3 SwapRouter 1000 USDT->USDC fee-100"); + vm.snapshotValue("fork v3 output 1000 USDT->USDC (USDC, 6 decimals)", out); + emit log_named_uint("v3 measured output (USDC)", out); + // sanity: stable pair near parity (1000 USDT in, ~1000 USDC out) + assertGe(out, 990000000); + assertLe(out, 1005000000); + } + + /// forge-config: default.isolate = true + function test_fork_single_exactInput_usdt_100() public { + _setup(); + _swap(100000000); + uint256 out = _swap(100000000); + vm.snapshotGasLastCall("fork v3 SwapRouter 100 USDT->USDC fee-100"); + vm.snapshotValue("fork v3 output 100 USDT->USDC (USDC, 6 decimals)", out); + emit log_named_uint("v3 measured output (USDC)", out); + assertGe(out, 99000000); + assertLe(out, 100500000); + } + + /// @dev What a v3 user pays to wrap ETH before a native-input swap (v3 pools hold + /// WETH, not ETH). Steady state: the account already holds WETH (this address + /// does at the pinned block), so the balance write is nonzero -> nonzero. + /// forge-config: default.isolate = true + function test_fork_weth_deposit() public { + vm.deal(address(this), 10 ether); + uint256 before = WETH.balanceOf(address(this)); + assertGt(before, 0, "already holds WETH"); + WETH.deposit{value: 1 ether}(); // warm-up, unmeasured + WETH.deposit{value: 1 ether}(); + vm.snapshotGasLastCall("fork WETH deposit steady-state"); + assertEq(WETH.balanceOf(address(this)), before + 2 ether); + } +} diff --git a/benchmarks/fork/ForkV3Test.json b/benchmarks/fork/ForkV3Test.json new file mode 100644 index 0000000..6951aba --- /dev/null +++ b/benchmarks/fork/ForkV3Test.json @@ -0,0 +1,7 @@ +{ + "fork WETH deposit steady-state": "27938", + "fork v3 SwapRouter 100 USDT->USDC fee-100": "128611", + "fork v3 SwapRouter 1000 USDT->USDC fee-100": "128611", + "fork v3 output 100 USDT->USDC (USDC, 6 decimals)": "99920949", + "fork v3 output 1000 USDT->USDC (USDC, 6 decimals)": "999209400" +} \ No newline at end of file diff --git a/benchmarks/fork/ForkV4.t.sol b/benchmarks/fork/ForkV4.t.sol new file mode 100644 index 0000000..ac903cf --- /dev/null +++ b/benchmarks/fork/ForkV4.t.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.26; + +import {Test} from "forge-std/Test.sol"; +import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "v4-core/src/types/PoolKey.sol"; +import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol"; +import {SwapParams} from "v4-core/src/types/PoolOperation.sol"; +import {IHooks} from "v4-core/src/interfaces/IHooks.sol"; +import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol"; +import {TransientStateLibrary} from "v4-core/src/libraries/TransientStateLibrary.sol"; +import {CurrencySettler} from "v4-core/test/utils/CurrencySettler.sol"; +import {TickMath} from "v4-core/src/libraries/TickMath.sol"; + +interface IERC20Like { + function balanceOf(address account) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); +} + +/// @notice Minimal single-lock exact-input swapper over a live v4 pool. +/// Production Universal Router calldata would add overhead on top. +contract ForkSwapRouter is IUnlockCallback { + using CurrencyLibrary for Currency; + using CurrencySettler for Currency; + using TransientStateLibrary for IPoolManager; + + IPoolManager public immutable manager; + + constructor(IPoolManager _manager) { + manager = _manager; + } + + function swap(PoolKey memory key, bool zeroForOne, int256 amountIn, uint160 limit, address payer) + external + payable + returns (uint256 amountOut) + { + bytes memory result = manager.unlock(abi.encode(key, zeroForOne, amountIn, limit, payer, msg.sender)); + amountOut = abi.decode(result, (uint256)); + } + + function unlockCallback(bytes calldata rawData) external returns (bytes memory) { + require(msg.sender == address(manager)); + (PoolKey memory key, bool zeroForOne, int256 amountIn, uint160 limit, address payer, address recipient) = + abi.decode(rawData, (PoolKey, bool, int256, uint160, address, address)); + manager.swap(key, SwapParams({zeroForOne: zeroForOne, amountSpecified: amountIn, sqrtPriceLimitX96: limit}), ""); + Currency currencyIn = zeroForOne ? key.currency0 : key.currency1; + Currency currencyOut = zeroForOne ? key.currency1 : key.currency0; + int256 inDelta = manager.currencyDelta(address(this), currencyIn); + int256 outDelta = manager.currencyDelta(address(this), currencyOut); + if (inDelta < 0) { + // tolerant settlement: low-level transferFrom so non-standard + // tokens (e.g. USDT, no returndata) work like on production routers + manager.sync(currencyIn); + (bool ok,) = Currency.unwrap(currencyIn).call( + abi.encodeWithSignature( + "transferFrom(address,address,uint256)", payer, address(manager), uint256(-inDelta) + ) + ); + require(ok, "transferFrom failed"); + manager.settle(); + } + if (outDelta > 0) currencyOut.take(manager, recipient, uint256(outDelta), false); + return abi.encode(uint256(outDelta)); + } +} + +/// @notice Mainnet-fork validation: REAL PoolManager + REAL USDT/USDC fee-100 pool +/// (deepest at the pinned block, hooks = address(0)), exact input, warmed with +/// one identical unmeasured swap (steady state); every call runs isolated in its +/// own EVM context, so the snapshot is the execution gas of the measured swap +/// (net of refunds, excluding the 21,000 base and calldata). +/// Run with: --fork-url --fork-block-number 25991868 +contract ForkV4Test is Test { + IPoolManager constant MANAGER = IPoolManager(0x000000000004444c5dc75cB358380D2e3dE08A90); + address constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + + ForkSwapRouter router; + PoolKey key; + + function _setup() internal { + router = new ForkSwapRouter(MANAGER); + // USDT is currency1 (USDC < USDT): USDT->USDC is oneForZero + key = PoolKey({ + currency0: Currency.wrap(USDC), + currency1: Currency.wrap(USDT), + fee: 100, + tickSpacing: 1, + hooks: IHooks(address(0)) + }); + deal(USDT, address(this), 1000000000000); + (bool approved,) = + USDT.call(abi.encodeWithSignature("approve(address,uint256)", address(router), type(uint256).max)); + assertTrue(approved, "usdt approve"); + assertEq(IERC20Like(USDT).allowance(address(this), address(router)), type(uint256).max, "max approval"); + } + + function _swap(int256 amountIn) internal returns (uint256) { + return router.swap(key, false, amountIn, TickMath.MAX_SQRT_PRICE - 1, address(this)); + } + + /// forge-config: default.isolate = true + function test_fork_single_exactInput_usdt_1000() public { + _setup(); + _swap(-1000000000); // warm-up + assertGt(IERC20Like(USDC).balanceOf(address(this)), 0, "recipient USDC balance nonzero before measured swap"); + uint256 out = _swap(-1000000000); // measured + vm.snapshotGasLastCall("fork v4 PoolManager 1000 USDT->USDC fee-100"); + vm.snapshotValue("fork v4 output 1000 USDT->USDC (USDC, 6 decimals)", out); + emit log_named_uint("v4 measured output (USDC)", out); + assertGe(out, 990000000); + assertLe(out, 1005000000); + } + + /// forge-config: default.isolate = true + function test_fork_single_exactInput_usdt_100() public { + _setup(); + _swap(-100000000); + uint256 out = _swap(-100000000); + vm.snapshotGasLastCall("fork v4 PoolManager 100 USDT->USDC fee-100"); + vm.snapshotValue("fork v4 output 100 USDT->USDC (USDC, 6 decimals)", out); + emit log_named_uint("v4 measured output (USDC)", out); + assertGe(out, 99000000); + assertLe(out, 100500000); + } +} diff --git a/benchmarks/fork/ForkV4Test.json b/benchmarks/fork/ForkV4Test.json new file mode 100644 index 0000000..266425d --- /dev/null +++ b/benchmarks/fork/ForkV4Test.json @@ -0,0 +1,6 @@ +{ + "fork v4 PoolManager 100 USDT->USDC fee-100": "132424", + "fork v4 PoolManager 1000 USDT->USDC fee-100": "132388", + "fork v4 output 100 USDT->USDC (USDC, 6 decimals)": "99917651", + "fork v4 output 1000 USDT->USDC (USDC, 6 decimals)": "999169404" +} \ No newline at end of file diff --git a/benchmarks/fork/calldata-ekubo-solidity-router.hex b/benchmarks/fork/calldata-ekubo-solidity-router.hex new file mode 100644 index 0000000..42625f2 --- /dev/null +++ b/benchmarks/fork/calldata-ekubo-solidity-router.hex @@ -0,0 +1 @@ +0x2f9deda5000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000053e2d6238da4800000320000000000000000000000000000000000000000000000003b9aca0080000000 diff --git a/benchmarks/fork/calldata-swaprouter-exactinput.hex b/benchmarks/fork/calldata-swaprouter-exactinput.hex new file mode 100644 index 0000000..fe9ad73 --- /dev/null +++ b/benchmarks/fork/calldata-swaprouter-exactinput.hex @@ -0,0 +1 @@ +0x414bf389000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000000640000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e14960000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000003b8b87c00000000000000000000000000000000000000000000000000000000000000000 diff --git a/benchmarks/fork/calldata-universal-router-v3swap.hex b/benchmarks/fork/calldata-universal-router-v3swap.hex new file mode 100644 index 0000000..b57f387 --- /dev/null +++ b/benchmarks/fork/calldata-universal-router-v3swap.hex @@ -0,0 +1 @@ +0x24856bc300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001400000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000003b8b87c000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000002bdac17f958d2ee523a2206206994597c13d831ec7000064a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 diff --git a/benchmarks/fork/calldata-v4-minimal-locker.hex b/benchmarks/fork/calldata-v4-minimal-locker.hex new file mode 100644 index 0000000..d57ba21 --- /dev/null +++ b/benchmarks/fork/calldata-v4-minimal-locker.hex @@ -0,0 +1 @@ +0x6e8c9dbf000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffc4653600000000000000000000000000fffd8963efd1fc6a506488495d951d5263988d250000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496 diff --git a/benchmarks/fork/calldata-yul-route-100.hex b/benchmarks/fork/calldata-yul-route-100.hex new file mode 100644 index 0000000..d1c3db4 --- /dev/null +++ b/benchmarks/fork/calldata-yul-route-100.hex @@ -0,0 +1 @@ +0x0100dAC17F958D2ee523a2206206994597C13D831ec7A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB4800000000000000000000000005e69ec07FA9385bE102ac3EAc297483Dd6233D62b3e149600000000000000000000000005f5e1000000A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48dAC17F958D2ee523a2206206994597C13D831ec70000000000000000000000000000000000000000000053e2d6238da48000003200000000000000000000000000000000 diff --git a/benchmarks/fork/calldata-yul-route.hex b/benchmarks/fork/calldata-yul-route.hex new file mode 100644 index 0000000..57470a0 --- /dev/null +++ b/benchmarks/fork/calldata-yul-route.hex @@ -0,0 +1 @@ +0x0100dAC17F958D2ee523a2206206994597C13D831ec7A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480000000000000000000000003b0233807FA9385bE102ac3EAc297483Dd6233D62b3e14960000000000000000000000003b9aca000000A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48dAC17F958D2ee523a2206206994597C13D831ec70000000000000000000000000000000000000000000053e2d6238da48000003200000000000000000000000000000000 diff --git a/benchmarks/fork/encode-yul-route.mjs b/benchmarks/fork/encode-yul-route.mjs new file mode 100644 index 0000000..0cd6488 --- /dev/null +++ b/benchmarks/fork/encode-yul-route.mjs @@ -0,0 +1,20 @@ +import { encodeRoute } from "/home/sendmoodz/Work/ekubo/yul-router/sdk/src/index.ts"; +const data = encodeRoute({ + specifiedToken: "0xdAC17F958D2ee523a2206206994597C13D831ec7", + calculatedToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + specifiedAmount: 1000000000n, + calculatedAmountThreshold: 990000000n, + recipient: "0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496", + hops: [ + { + type: "core", + poolKey: { + token0: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + token1: "0xdAC17F958D2ee523a2206206994597C13D831ec7", + config: + "0x0000000000000000000000000000000000000000000053e2d6238da480000032", + }, + }, + ], +}); +console.log(data); diff --git a/benchmarks/fork/foundry.toml b/benchmarks/fork/foundry.toml new file mode 100644 index 0000000..ae3e9f1 --- /dev/null +++ b/benchmarks/fork/foundry.toml @@ -0,0 +1,12 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +# No `solc` pin: foundry auto-detects per file (0.8.26 driver files, +# =0.8.33 ekubo sources). A pin would force one version on everything. +optimizer = true +optimizer_runs = 1000000 +via_ir = true +evm_version = "osaka" +verbosity = 2 +fs_permissions = [{ access = "read", path = "./" }] diff --git a/benchmarks/fork/remappings.txt b/benchmarks/fork/remappings.txt new file mode 100644 index 0000000..c00f85a --- /dev/null +++ b/benchmarks/fork/remappings.txt @@ -0,0 +1,5 @@ +forge-std/=lib/forge-std/src/ +solmate/=lib/v4-core/lib/solmate/ +v4-core/=lib/v4-core/ +ekubo/=lib/ekubo/ +ekubo-test/=lib/ekubo/test/ diff --git a/benchmarks/lab/MockERC20.json b/benchmarks/lab/MockERC20.json new file mode 100644 index 0000000..860972c --- /dev/null +++ b/benchmarks/lab/MockERC20.json @@ -0,0 +1 @@ +{"abi":[{"type":"constructor","inputs":[{"name":"_name","type":"string","internalType":"string"},{"name":"_symbol","type":"string","internalType":"string"},{"name":"_decimals","type":"uint8","internalType":"uint8"}],"stateMutability":"nonpayable"},{"type":"function","name":"DOMAIN_SEPARATOR","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"allowance","inputs":[{"name":"","type":"address","internalType":"address"},{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"approve","inputs":[{"name":"spender","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"type":"function","name":"balanceOf","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"burn","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"decimals","inputs":[],"outputs":[{"name":"","type":"uint8","internalType":"uint8"}],"stateMutability":"view"},{"type":"function","name":"mint","inputs":[{"name":"to","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"name","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"nonces","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"permit","inputs":[{"name":"owner","type":"address","internalType":"address"},{"name":"spender","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"symbol","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"totalSupply","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"transfer","inputs":[{"name":"to","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"type":"function","name":"transferFrom","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"to","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"type":"event","name":"Approval","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"spender","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Transfer","inputs":[{"name":"from","type":"address","indexed":true,"internalType":"address"},{"name":"to","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false}],"bytecode":{"object":"0x60e080604052346104065761134e803803809161001c828561040a565b83398101906060818303126104065780516001600160401b038111610406578261004791830161042d565b60208201519092906001600160401b0381116104065760409161006b91840161042d565b91015160ff811681036104065782516001600160401b038111610337576100925f54610482565b601f81116103b7575b506020601f821160011461035657819293945f9261034b575b50508160011b915f199060031b1c1916175f555b81516001600160401b038111610337576100e3600154610482565b601f81116102d4575b50602092601f821160011461027357928192935f92610268575b50508160011b915f199060031b1c1916176001555b6080524660a0526040515f905f54918161013484610482565b9182825260208201946001811690815f1461024c5750600114610202575b61015e9250038261040a565b51902060405160208101917f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f835260408201527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc660608201524660808201523060a082015260a081526101d260c08261040a565b51902060c052604051610e7390816104bb823960805181610865015260a05181610ca8015260c05181610cce0152f35b505f80805290915f8051602061132e8339815191525b81831061023057505090602061015e92820101610152565b6020919350806001915483858801015201910190918392610218565b60ff191686525061015e92151560051b82016020019050610152565b015190505f80610106565b601f1982169360015f52805f20915f5b8681106102bc57508360019596106102a4575b505050811b0160015561011b565b01515f1960f88460031b161c191690555f8080610296565b91926020600181928685015181550194019201610283565b60015f527fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6601f830160051c8101916020841061032d575b601f0160051c01905b81811061032257506100ec565b5f8155600101610315565b909150819061030c565b634e487b7160e01b5f52604160045260245ffd5b015190505f806100b4565b601f198216905f8052805f20915f5b81811061039f57509583600195969710610387575b505050811b015f556100c8565b01515f1960f88460031b161c191690555f808061037a565b9192602060018192868b015181550194019201610365565b5f80525f8051602061132e833981519152601f830160051c810191602084106103fc575b601f0160051c01905b8181106103f1575061009b565b5f81556001016103e4565b90915081906103db565b5f80fd5b601f909101601f19168101906001600160401b0382119082101761033757604052565b81601f82011215610406578051906001600160401b0382116103375760405192610461601f8401601f19166020018561040a565b8284526020838301011161040657815f9260208093018386015e8301015290565b90600182811c921680156104b0575b602083101461049c57565b634e487b7160e01b5f52602260045260245ffd5b91607f169161049156fe60806040526004361015610011575f80fd5b5f3560e01c806306fdde0314610a8b578063095ea7b3146109e857806318160ddd146109ad57806323b872dd14610889578063313ce5671461082e5780633644e515146107ee57806340c10f191461071f57806370a08231146106bc5780637ecebe001461065957806395d89b41146105435780639dc29fac146104a4578063a9059cbb146103f0578063d505accf146101435763dd62ed3e146100b3575f80fd5b3461013f5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576100ea610c52565b73ffffffffffffffffffffffffffffffffffffffff610107610c75565b91165f52600460205273ffffffffffffffffffffffffffffffffffffffff60405f2091165f52602052602060405f2054604051908152f35b5f80fd5b3461013f5760e07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f5761017a610c52565b610182610c75565b6044356064359260843560ff811680910361013f574285106103925760805f9160209373ffffffffffffffffffffffffffffffffffffffff6101c2610ca5565b91169687855260058652604085209889549960018b0190556040519073ffffffffffffffffffffffffffffffffffffffff888301937f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c985528b6040850152169a8b6060840152898784015260a083015260c082015260c0815261024660e082610b9c565b51902060405190868201927f19010000000000000000000000000000000000000000000000000000000000008452602283015260428201526042815261028d606282610b9c565b519020906040519182528482015260a435604082015260c435606082015282805260015afa156103875773ffffffffffffffffffffffffffffffffffffffff5f51168015158061037e575b15610320577f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925916020915f526004825260405f20855f5282528060405f2055604051908152a3005b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152600e60248201527f494e56414c49445f5349474e45520000000000000000000000000000000000006044820152fd5b508281146102d8565b6040513d5f823e3d90fd5b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f5045524d49545f444541444c494e455f455850495245440000000000000000006044820152fd5b3461013f5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f57610427610c52565b73ffffffffffffffffffffffffffffffffffffffff60243591335f52600360205260405f20610457848254610c98565b90551690815f52600360205260405f208181540190556040519081527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60203392a3602060405160018152f35b3461013f5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f575f6104dc610c52565b7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef602073ffffffffffffffffffffffffffffffffffffffff6024359316928385526003825260408520610530828254610c98565b90558060025403600255604051908152a3005b3461013f575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576040515f60015461058181610b4b565b808452906001811690811561061757506001146105b9575b6105b5836105a981850382610b9c565b60405191829182610c0a565b0390f35b60015f9081527fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6939250905b8082106105fd575090915081016020016105a9610599565b9192600181602092548385880101520191019092916105e5565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660208086019190915291151560051b840190910191506105a99050610599565b3461013f5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f5773ffffffffffffffffffffffffffffffffffffffff6106a5610c52565b165f526005602052602060405f2054604051908152f35b3461013f5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f5773ffffffffffffffffffffffffffffffffffffffff610708610c52565b165f526003602052602060405f2054604051908152f35b3461013f5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f57610756610c52565b602435906002548281018091116107c157602073ffffffffffffffffffffffffffffffffffffffff5f937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9360025516938484526003825260408420818154019055604051908152a3005b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b3461013f575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576020610826610ca5565b604051908152f35b3461013f575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f57602060405160ff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b3461013f5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576108c0610c52565b6108c8610c75565b7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef602073ffffffffffffffffffffffffffffffffffffffff80604435951693845f526004835260405f208233165f52835260405f2054867fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203610986575b5050845f526003835260405f2061095f878254610c98565b90551693845f526003825260405f20818154019055604051908152a3602060405160018152f35b61098f91610c98565b855f526004845260405f208333165f52845260405f20558686610947565b3461013f575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576020600254604051908152f35b3461013f5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f57610a1f610c52565b73ffffffffffffffffffffffffffffffffffffffff60243591335f52600460205260405f208282165f526020528260405f205560405192835216907f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560203392a3602060405160018152f35b3461013f575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576040515f8054610ac881610b4b565b80845290600181169081156106175750600114610aef576105b5836105a981850382610b9c565b5f8080527f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563939250905b808210610b31575090915081016020016105a9610599565b919260018160209254838588010152019101909291610b19565b90600182811c92168015610b92575b6020831014610b6557565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b91607f1691610b5a565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117610bdd57604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f602060409481855280519182918282880152018686015e5f8582860101520116010190565b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361013f57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361013f57565b919082039182116107c157565b467f000000000000000000000000000000000000000000000000000000000000000003610cf0577f000000000000000000000000000000000000000000000000000000000000000090565b6040515f905f549181610d0284610b4b565b9182825260208201946001811690815f14610e035750600114610da6575b610d2c92500382610b9c565b51902060405160208101917f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f835260408201527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc660608201524660808201523060a082015260a08152610da060c082610b9c565b51902090565b505f80805290917f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e5635b818310610de7575050906020610d2c92820101610d20565b6020919350806001915483858801015201910190918392610dcf565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016865250610d2c92151560051b82016020019050610d2056fea26469706673582212200e4e3600233109bb884b081d524b13dac8383267fd5a07dfc715d3d22497968a64736f6c634300081a0033290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563","sourceMap":"118:368:20:-:0;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;-1:-1:-1;;;;;118:368:20;;;;;;;;;;:::i;:::-;;;;;;;;-1:-1:-1;;;;;118:368:20;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;-1:-1:-1;;;;;118:368:20;;;;;-1:-1:-1;118:368:20;;:::i;:::-;;;;;;-1:-1:-1;118:368:20;;;;;;;;;;;;;-1:-1:-1;118:368:20;;;;;;;;;;;;;;;;;;;-1:-1:-1;118:368:20;;;;-1:-1:-1;;;;;118:368:20;;;;;2116:16:21;118:368:20;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;118:368:20;;;;;;;2116:16:21;118:368:20;;;;;;;;;;;2116:16:21;118:368:20;;2142:20:21;;2192:13;2173:32;;118:368:20;;-1:-1:-1;118:368:20;-1:-1:-1;118:368:20;;;;;;:::i;:::-;;;;;;;;;2116:16:21;118:368:20;;2116:16:21;;118:368:20;2116:16:21;;;118:368:20;;;;;;;;;;;;:::i;:::-;;5789:22:21;;118:368:20;;;5640:295:21;;118:368:20;5672:95:21;118:368:20;;;;;;5833:14:21;118:368:20;;;;2192:13:21;2142:20;118:368:20;;;5912:4:21;2173:32;118:368:20;;;2173:32:21;5640:295;;;;;;:::i;:::-;118:368:20;5613:336:21;;5640:295;2215:51;118:368:20;;;;;;;;2142:20:21;118:368:20;;;;;2173:32:21;118:368:20;;;;;5640:295:21;118:368:20;;;;;;;-1:-1:-1;;118:368:20;;;;;-1:-1:-1;;;;;;;;;;;118:368:20;;;;;;;;;;;;;;;;;;;;;;;2116:16:21;118:368:20;;;;;;;;;;;;;;;;;;-1:-1:-1;;118:368:20;;;-1:-1:-1;118:368:20;;;;;;;;;;;-1:-1:-1;118:368:20;;;;;;-1:-1:-1;118:368:20;;;;;;;;;;2116:16:21;-1:-1:-1;118:368:20;;-1:-1:-1;118:368:20;;-1:-1:-1;118:368:20;;;;;;;;2116:16:21;118:368:20;;;;;;;;;;;;2116:16:21;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;2116:16:21;118:368:20;;;;;;;;;;;;;;;;2116:16:21;-1:-1:-1;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;118:368:20;;2116:16:21;118:368:20;;;;;;-1:-1:-1;118:368:20;;;;;;;;-1:-1:-1;118:368:20;;;;;-1:-1:-1;118:368:20;;;;;-1:-1:-1;118:368:20;;;;;;;;;;-1:-1:-1;118:368:20;;;-1:-1:-1;118:368:20;;-1:-1:-1;118:368:20;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;118:368:20;;-1:-1:-1;;;;;;;;;;;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;118:368:20;;;;;;;;;-1:-1:-1;118:368:20;;;;;-1:-1:-1;118:368:20;;;;;;;-1:-1:-1;;118:368:20;;;;-1:-1:-1;;;;;118:368:20;;;;;;;;;;:::o;:::-;;;;;;;;;;;;-1:-1:-1;;;;;118:368:20;;;;;;;;;;;-1:-1:-1;;118:368:20;;;;;:::i;:::-;;;;;;;;;;;;;-1:-1:-1;118:368:20;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x60806040526004361015610011575f80fd5b5f3560e01c806306fdde0314610a8b578063095ea7b3146109e857806318160ddd146109ad57806323b872dd14610889578063313ce5671461082e5780633644e515146107ee57806340c10f191461071f57806370a08231146106bc5780637ecebe001461065957806395d89b41146105435780639dc29fac146104a4578063a9059cbb146103f0578063d505accf146101435763dd62ed3e146100b3575f80fd5b3461013f5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576100ea610c52565b73ffffffffffffffffffffffffffffffffffffffff610107610c75565b91165f52600460205273ffffffffffffffffffffffffffffffffffffffff60405f2091165f52602052602060405f2054604051908152f35b5f80fd5b3461013f5760e07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f5761017a610c52565b610182610c75565b6044356064359260843560ff811680910361013f574285106103925760805f9160209373ffffffffffffffffffffffffffffffffffffffff6101c2610ca5565b91169687855260058652604085209889549960018b0190556040519073ffffffffffffffffffffffffffffffffffffffff888301937f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c985528b6040850152169a8b6060840152898784015260a083015260c082015260c0815261024660e082610b9c565b51902060405190868201927f19010000000000000000000000000000000000000000000000000000000000008452602283015260428201526042815261028d606282610b9c565b519020906040519182528482015260a435604082015260c435606082015282805260015afa156103875773ffffffffffffffffffffffffffffffffffffffff5f51168015158061037e575b15610320577f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925916020915f526004825260405f20855f5282528060405f2055604051908152a3005b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152600e60248201527f494e56414c49445f5349474e45520000000000000000000000000000000000006044820152fd5b508281146102d8565b6040513d5f823e3d90fd5b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f5045524d49545f444541444c494e455f455850495245440000000000000000006044820152fd5b3461013f5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f57610427610c52565b73ffffffffffffffffffffffffffffffffffffffff60243591335f52600360205260405f20610457848254610c98565b90551690815f52600360205260405f208181540190556040519081527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60203392a3602060405160018152f35b3461013f5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f575f6104dc610c52565b7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef602073ffffffffffffffffffffffffffffffffffffffff6024359316928385526003825260408520610530828254610c98565b90558060025403600255604051908152a3005b3461013f575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576040515f60015461058181610b4b565b808452906001811690811561061757506001146105b9575b6105b5836105a981850382610b9c565b60405191829182610c0a565b0390f35b60015f9081527fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6939250905b8082106105fd575090915081016020016105a9610599565b9192600181602092548385880101520191019092916105e5565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660208086019190915291151560051b840190910191506105a99050610599565b3461013f5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f5773ffffffffffffffffffffffffffffffffffffffff6106a5610c52565b165f526005602052602060405f2054604051908152f35b3461013f5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f5773ffffffffffffffffffffffffffffffffffffffff610708610c52565b165f526003602052602060405f2054604051908152f35b3461013f5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f57610756610c52565b602435906002548281018091116107c157602073ffffffffffffffffffffffffffffffffffffffff5f937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9360025516938484526003825260408420818154019055604051908152a3005b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b3461013f575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576020610826610ca5565b604051908152f35b3461013f575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f57602060405160ff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b3461013f5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576108c0610c52565b6108c8610c75565b7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef602073ffffffffffffffffffffffffffffffffffffffff80604435951693845f526004835260405f208233165f52835260405f2054867fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203610986575b5050845f526003835260405f2061095f878254610c98565b90551693845f526003825260405f20818154019055604051908152a3602060405160018152f35b61098f91610c98565b855f526004845260405f208333165f52845260405f20558686610947565b3461013f575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576020600254604051908152f35b3461013f5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f57610a1f610c52565b73ffffffffffffffffffffffffffffffffffffffff60243591335f52600460205260405f208282165f526020528260405f205560405192835216907f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560203392a3602060405160018152f35b3461013f575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576040515f8054610ac881610b4b565b80845290600181169081156106175750600114610aef576105b5836105a981850382610b9c565b5f8080527f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563939250905b808210610b31575090915081016020016105a9610599565b919260018160209254838588010152019101909291610b19565b90600182811c92168015610b92575b6020831014610b6557565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b91607f1691610b5a565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117610bdd57604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f602060409481855280519182918282880152018686015e5f8582860101520116010190565b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361013f57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361013f57565b919082039182116107c157565b467f000000000000000000000000000000000000000000000000000000000000000003610cf0577f000000000000000000000000000000000000000000000000000000000000000090565b6040515f905f549181610d0284610b4b565b9182825260208201946001811690815f14610e035750600114610da6575b610d2c92500382610b9c565b51902060405160208101917f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f835260408201527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc660608201524660808201523060a082015260a08152610da060c082610b9c565b51902090565b505f80805290917f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e5635b818310610de7575050906020610d2c92820101610d20565b6020919350806001915483858801015201910190918392610dcf565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016865250610d2c92151560051b82016020019050610d2056fea26469706673582212200e4e3600233109bb884b081d524b13dac8383267fd5a07dfc715d3d22497968a64736f6c634300081a0033","sourceMap":"118:368:20:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;:::i;:::-;;;;;;;;;;;;1388:64:21;118:368:20;-1:-1:-1;118:368:20;;;;;-1:-1:-1;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;:::i;:::-;;;;;;;;;;;;;;;;4057:15:21;4045:27;;118:368:20;;;;4428:18:21;118:368:20;4428:18:21;118:368:20;4428:18:21;;:::i;:::-;118:368:20;;;;;;4873:6:21;118:368:20;;;;;;;;;;;;;;;;4511:449:21;118:368:20;4511:449:21;;;118:368:20;4555:165:21;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;;4511:449:21;;;118:368:20;4511:449:21;;:::i;:::-;118:368:20;4472:514:21;;118:368:20;;4350:658:21;;;;118:368:20;;;;;;;;;;;;;4350:658:21;;;;;;:::i;:::-;118:368:20;4319:707:21;;118:368:20;;;;;;;;;;;;;;;;;;;;;;4292:805:21;;;118:368:20;4292:805:21;;;;;118:368:20;;4292:805:21;118:368:20;5120:30:21;;;:59;;;118:368:20;;;;5283:31:21;118:368:20;;;;;;;;;;;;-1:-1:-1;118:368:20;;;;;-1:-1:-1;118:368:20;;;;;;;5283:31:21;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;5120:59:21;5154:25;;;;5120:59;;4292:805;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;2774:10:21;;118:368:20;;2764:9:21;118:368:20;;;;;2764:31:21;118:368:20;;;2764:31:21;:::i;:::-;118:368:20;;;;;;;2764:9:21;118:368:20;;;;;;;;;;;;;;;;2990:32:21;118:368:20;2774:10:21;2990:32;;118:368:20;;;;;;;;;;;;;;;;;;;;;:::i;:::-;6768:34:21;118:368:20;;;;;;;;;;6553:9:21;118:368:20;;;;;6553:25:21;118:368:20;;;6553:25:21;:::i;:::-;118:368:20;;;6721:21:21;118:368:20;;6721:21:21;118:368:20;;;;;;6768:34:21;118:368:20;;;;;;;;;;;;;;;1056:20:21;118:368:20;;;;:::i;:::-;;;;;1056:20:21;118:368:20;;;1056:20:21;;;;118:368:20;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;1056:20:21;118:368:20;;;;;;;-1:-1:-1;118:368:20;;;;;;;-1:-1:-1;118:368:20;;-1:-1:-1;118:368:20;;;;;;;;;;1056:20:21;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;118:368:20;;-1:-1:-1;118:368:20;;;;;;;;;;;;;;;;:::i;:::-;;;;1751:41:21;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;1337:44:21;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;6220:21:21;118:368:20;;;;;;;;;;;;;6436:32:21;118:368:20;6220:21:21;118:368:20;;;;;;6387:9:21;118:368:20;;;;;;;;;;;;;;;;6436:32:21;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;1083:31:21;118:368:20;;;;;;;;;;;;;;;;;:::i;:::-;;;:::i;:::-;3594:26:21;118:368:20;;;;;;;;;;;;;;;;;3225:10:21;;118:368:20;-1:-1:-1;118:368:20;;;;-1:-1:-1;118:368:20;;3287:28:21;3298:17;3287:28;;3283:80;;118:368:20;;;;;;3374:9:21;118:368:20;;;;;3374:25:21;118:368:20;;;3374:25:21;:::i;:::-;118:368:20;;;;;;;3374:9:21;118:368:20;;;;;;;;;;;;;;;;3594:26:21;118:368:20;;;;;;;3283:80:21;3347:16;;;:::i;:::-;118:368:20;;;;;;;;;3225:10:21;;118:368:20;-1:-1:-1;118:368:20;;;;-1:-1:-1;118:368:20;;3283:80:21;;;;118:368:20;;;;;;;;;;;;1304:26:21;118:368:20;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;2561:10:21;;118:368:20;;;;;;;;;;;-1:-1:-1;118:368:20;;;;;-1:-1:-1;118:368:20;;;;;;;;2561:10:21;2606:37;118:368:20;2561:10:21;2606:37;;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;-1:-1:-1;118:368:20;;;;;;;-1:-1:-1;118:368:20;;-1:-1:-1;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;-1:-1:-1;118:368:20;;;;;-1:-1:-1;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;:::o;:::-;;;;;;;;;;;:::o;:::-;;;;;;;;;;:::o;5327:177:21:-;5410:13;5427:16;5410:33;5427:16;;5446:24;5327:177;:::o;5410:87::-;118:368:20;;-1:-1:-1;118:368:20;-1:-1:-1;118:368:20;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;5789:22:21;;118:368:20;;;5640:295:21;;118:368:20;5672:95:21;118:368:20;;;;;;5833:14:21;118:368:20;;;;5410:13:21;118:368:20;;;;5912:4:21;118:368:20;;;;;5640:295:21;;;;;;:::i;:::-;118:368:20;5613:336:21;;5327:177;:::o;118:368:20:-;-1:-1:-1;;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;118:368:20;;;;;;;;;;;-1:-1:-1;118:368:20;","linkReferences":{},"immutableReferences":{"42052":[{"start":2149,"length":32}],"42066":[{"start":3240,"length":32}],"42068":[{"start":3278,"length":32}]}},"methodIdentifiers":{"DOMAIN_SEPARATOR()":"3644e515","allowance(address,address)":"dd62ed3e","approve(address,uint256)":"095ea7b3","balanceOf(address)":"70a08231","burn(address,uint256)":"9dc29fac","decimals()":"313ce567","mint(address,uint256)":"40c10f19","name()":"06fdde03","nonces(address)":"7ecebe00","permit(address,address,uint256,uint256,uint8,bytes32,bytes32)":"d505accf","symbol()":"95d89b41","totalSupply()":"18160ddd","transfer(address,uint256)":"a9059cbb","transferFrom(address,address,uint256)":"23b872dd"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.26+commit.8a97fa7a\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"string\",\"name\":\"_name\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"_symbol\",\"type\":\"string\"},{\"internalType\":\"uint8\",\"name\":\"_decimals\",\"type\":\"uint8\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"DOMAIN_SEPARATOR\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"burn\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"decimals\",\"outputs\":[{\"internalType\":\"uint8\",\"name\":\"\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"mint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"name\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"nonces\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"},{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"permit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"symbol\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"lib/v4-core/lib/solmate/src/test/utils/mocks/MockERC20.sol\":\"MockERC20\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":true,\"runs\":44444444},\"remappings\":[\"/tmp/opencode/gasbench/lib/v4-core/lib/openzeppelin-contracts/lib/forge-std/:ds-test/=lib/v4-core/lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/\",\"/tmp/opencode/gasbench/lib/v4-core/lib/solmate/:ds-test/=lib/v4-core/lib/solmate/lib/ds-test/src/\",\":@ensdomains/=lib/v4-core/node_modules/@ensdomains/\",\":@openzeppelin/=lib/v4-core/lib/openzeppelin-contracts/\",\":ds-test/=lib/v4-core/lib/forge-std/lib/ds-test/src/\",\":erc4626-tests/=lib/v4-core/lib/openzeppelin-contracts/lib/erc4626-tests/\",\":forge-std/=lib/forge-std/src/\",\":hardhat/=lib/v4-core/node_modules/hardhat/\",\":openzeppelin-contracts/=lib/v4-core/lib/openzeppelin-contracts/\",\":solady/=lib/solady/src/\",\":solmate/=lib/v4-core/lib/solmate/\",\":v4-core/=lib/v4-core/\",\":v4-src/=lib/v4-core/src/\",\"lib/v4-core/lib/openzeppelin-contracts/lib/forge-std/:ds-test/=lib/v4-core/lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/\",\"lib/v4-core/lib/solmate/:ds-test/=lib/v4-core/lib/solmate/lib/ds-test/src/\"],\"viaIR\":true},\"sources\":{\"lib/v4-core/lib/solmate/src/test/utils/mocks/MockERC20.sol\":{\"keccak256\":\"0x525129ccb4103c9ecb8f235618d2ff4a8ffde4b8654f7c17766ad175d10ef466\",\"license\":\"AGPL-3.0-only\",\"urls\":[\"bzz-raw://45692a21807ba815c0cf0bfc7d633a14a6cb9ba863ab53f18fc639375a72125e\",\"dweb:/ipfs/QmSrqxyodjDtSU1Dc6G3ScnNc9CFjMnAeHpK8a8guM3nJu\"]},\"lib/v4-core/lib/solmate/src/tokens/ERC20.sol\":{\"keccak256\":\"0xcdfd8db76b2a3415620e4d18cc5545f3d50de792dbf2c3dd5adb40cbe6f94b10\",\"license\":\"AGPL-3.0-only\",\"urls\":[\"bzz-raw://57b3ab70cde374af1cf2c9888636e8de6cf660f087b1c9abd805e9271e19fa35\",\"dweb:/ipfs/QmNrLDBAHYFjpjSd12jerm1AdBkDqEYUUaXgnT854BUZ97\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.26+commit.8a97fa7a"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"string","name":"_name","type":"string"},{"internalType":"string","name":"_symbol","type":"string"},{"internalType":"uint8","name":"_decimals","type":"uint8"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"owner","type":"address","indexed":true},{"internalType":"address","name":"spender","type":"address","indexed":true},{"internalType":"uint256","name":"amount","type":"uint256","indexed":false}],"type":"event","name":"Approval","anonymous":false},{"inputs":[{"internalType":"address","name":"from","type":"address","indexed":true},{"internalType":"address","name":"to","type":"address","indexed":true},{"internalType":"uint256","name":"amount","type":"uint256","indexed":false}],"type":"event","name":"Transfer","anonymous":false},{"inputs":[],"stateMutability":"view","type":"function","name":"DOMAIN_SEPARATOR","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}]},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function","name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}]},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"stateMutability":"nonpayable","type":"function","name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}]},{"inputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function","name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}]},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"stateMutability":"nonpayable","type":"function","name":"burn"},{"inputs":[],"stateMutability":"view","type":"function","name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}]},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"stateMutability":"nonpayable","type":"function","name":"mint"},{"inputs":[],"stateMutability":"view","type":"function","name":"name","outputs":[{"internalType":"string","name":"","type":"string"}]},{"inputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function","name":"nonces","outputs":[{"internalType":"uint256","name":"","type":"uint256"}]},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"stateMutability":"nonpayable","type":"function","name":"permit"},{"inputs":[],"stateMutability":"view","type":"function","name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}]},{"inputs":[],"stateMutability":"view","type":"function","name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}]},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"stateMutability":"nonpayable","type":"function","name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}]},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"stateMutability":"nonpayable","type":"function","name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}]}],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["/tmp/opencode/gasbench/lib/v4-core/lib/openzeppelin-contracts/lib/forge-std/:ds-test/=lib/v4-core/lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/","/tmp/opencode/gasbench/lib/v4-core/lib/solmate/:ds-test/=lib/v4-core/lib/solmate/lib/ds-test/src/","@ensdomains/=lib/v4-core/node_modules/@ensdomains/","@openzeppelin/=lib/v4-core/lib/openzeppelin-contracts/","ds-test/=lib/v4-core/lib/forge-std/lib/ds-test/src/","erc4626-tests/=lib/v4-core/lib/openzeppelin-contracts/lib/erc4626-tests/","forge-std/=lib/forge-std/src/","hardhat/=lib/v4-core/node_modules/hardhat/","openzeppelin-contracts/=lib/v4-core/lib/openzeppelin-contracts/","solady/=lib/solady/src/","solmate/=lib/v4-core/lib/solmate/","v4-core/=lib/v4-core/","v4-src/=lib/v4-core/src/","lib/v4-core/lib/openzeppelin-contracts/lib/forge-std/:ds-test/=lib/v4-core/lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/","lib/v4-core/lib/solmate/:ds-test/=lib/v4-core/lib/solmate/lib/ds-test/src/"],"optimizer":{"enabled":true,"runs":44444444},"metadata":{"bytecodeHash":"ipfs"},"compilationTarget":{"lib/v4-core/lib/solmate/src/test/utils/mocks/MockERC20.sol":"MockERC20"},"evmVersion":"cancun","libraries":{},"viaIR":true},"sources":{"lib/v4-core/lib/solmate/src/test/utils/mocks/MockERC20.sol":{"keccak256":"0x525129ccb4103c9ecb8f235618d2ff4a8ffde4b8654f7c17766ad175d10ef466","urls":["bzz-raw://45692a21807ba815c0cf0bfc7d633a14a6cb9ba863ab53f18fc639375a72125e","dweb:/ipfs/QmSrqxyodjDtSU1Dc6G3ScnNc9CFjMnAeHpK8a8guM3nJu"],"license":"AGPL-3.0-only"},"lib/v4-core/lib/solmate/src/tokens/ERC20.sol":{"keccak256":"0xcdfd8db76b2a3415620e4d18cc5545f3d50de792dbf2c3dd5adb40cbe6f94b10","urls":["bzz-raw://57b3ab70cde374af1cf2c9888636e8de6cf660f087b1c9abd805e9271e19fa35","dweb:/ipfs/QmNrLDBAHYFjpjSd12jerm1AdBkDqEYUUaXgnT854BUZ97"],"license":"AGPL-3.0-only"}},"version":1},"id":20} \ No newline at end of file diff --git a/benchmarks/lab/ekubo/EkuboGas.t.sol b/benchmarks/lab/ekubo/EkuboGas.t.sol new file mode 100644 index 0000000..e6d6c18 --- /dev/null +++ b/benchmarks/lab/ekubo/EkuboGas.t.sol @@ -0,0 +1,312 @@ +// SPDX-License-Identifier: ekubo-license-v1.eth +pragma solidity =0.8.33; + +import {Test} from "forge-std/Test.sol"; +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; +import {Core} from "ekubo/src/Core.sol"; +import {Positions} from "ekubo/src/Positions.sol"; +import {Router} from "ekubo/src/Router.sol"; +import {PoolKey} from "ekubo/src/types/poolKey.sol"; +import {createConcentratedPoolConfig} from "ekubo/src/types/poolConfig.sol"; +import {SqrtRatio, MIN_SQRT_RATIO} from "ekubo/src/types/sqrtRatio.sol"; +import {NATIVE_TOKEN_ADDRESS} from "ekubo/src/math/constants.sol"; +import {RouteNode, TokenAmount, Swap} from "ekubo/src/base/BaseRouter.sol"; +import {SwapParameters, createSwapParameters} from "ekubo/src/types/swapParameters.sol"; +import {CoreLib} from "ekubo/src/libraries/CoreLib.sol"; +import {BaseLocker} from "ekubo/src/base/BaseLocker.sol"; +import {ICore} from "ekubo/src/interfaces/ICore.sol"; +import {FlashAccountantLib} from "ekubo/src/libraries/FlashAccountantLib.sol"; +import {createPositionId} from "ekubo/src/types/positionId.sol"; +import {PoolBalanceUpdate} from "ekubo/src/types/poolBalanceUpdate.sol"; + +/// @notice Bare core-level liquidity provision: lock, updatePosition, settle net. +/// No NFT, no protocol fees — the same tier as the v4/v3 direct mints. +contract BareMintRouter is BaseLocker { + using FlashAccountantLib for *; + + constructor(ICore core) BaseLocker(core) {} + + function mint(PoolKey memory key, bytes24 salt, int32 tickLower, int32 tickUpper, int128 liquidityDelta, address payer) + external + returns (PoolBalanceUpdate balanceUpdate) + { + balanceUpdate = + abi.decode(lock(abi.encode(key, salt, tickLower, tickUpper, liquidityDelta, payer, msg.sender)), (PoolBalanceUpdate)); + } + + function handleLockData(uint256, bytes memory data) internal override returns (bytes memory result) { + (PoolKey memory key, bytes24 salt, int32 tickLower, int32 tickUpper, int128 liquidityDelta, address payer, address recipient) = + abi.decode(data, (PoolKey, bytes24, int32, int32, int128, address, address)); + PoolBalanceUpdate balanceUpdate = ICore(payable(address(ACCOUNTANT))).updatePosition( + key, createPositionId(salt, tickLower, tickUpper), liquidityDelta + ); + if (balanceUpdate.delta0() > 0) { + ACCOUNTANT.payFrom(payer, key.token0, uint128(balanceUpdate.delta0())); + } else if (balanceUpdate.delta0() < 0) { + ACCOUNTANT.withdraw(key.token0, recipient, uint128(-balanceUpdate.delta0())); + } + if (balanceUpdate.delta1() > 0) { + ACCOUNTANT.payFrom(payer, key.token1, uint128(balanceUpdate.delta1())); + } else if (balanceUpdate.delta1() < 0) { + ACCOUNTANT.withdraw(key.token1, recipient, uint128(-balanceUpdate.delta1())); + } + result = abi.encode(balanceUpdate); + } +} + +/// @notice True-minimal single-hop exact-input locker, the Ekubo analogue of the v4 +/// MinimalSwapRouter: lock, swap, pay the input from the payer, withdraw the +/// output to the caller. Native input is forwarded as call value to Core. +contract MinimalEkuboLocker is BaseLocker { + using FlashAccountantLib for *; + using CoreLib for ICore; + + constructor(ICore core) BaseLocker(core) {} + + function swapExactIn(PoolKey memory key, SwapParameters params, address payer) + external + payable + returns (PoolBalanceUpdate balanceUpdate) + { + balanceUpdate = + abi.decode(lock(abi.encode(key, params, payer, msg.sender, msg.value)), (PoolBalanceUpdate)); + } + + function handleLockData(uint256, bytes memory data) internal override returns (bytes memory result) { + (PoolKey memory key, SwapParameters params, address payer, address recipient, uint256 value) = + abi.decode(data, (PoolKey, SwapParameters, address, address, uint256)); + (PoolBalanceUpdate balanceUpdate,) = ICore(payable(address(ACCOUNTANT))).swap(value, key, params); + if (balanceUpdate.delta0() > 0) { + if (key.token0 != NATIVE_TOKEN_ADDRESS) ACCOUNTANT.payFrom(payer, key.token0, uint128(balanceUpdate.delta0())); + } else if (balanceUpdate.delta0() < 0) { + ACCOUNTANT.withdraw(key.token0, recipient, uint128(-balanceUpdate.delta0())); + } + if (balanceUpdate.delta1() > 0) { + ACCOUNTANT.payFrom(payer, key.token1, uint128(balanceUpdate.delta1())); + } else if (balanceUpdate.delta1() < 0) { + ACCOUNTANT.withdraw(key.token1, recipient, uint128(-balanceUpdate.delta1())); + } + result = abi.encode(balanceUpdate); + } +} + +contract EkuboGasTest is Test { + Core core; + Positions positions; + Router router; + BareMintRouter bareMint; + MinimalEkuboLocker minLocker; + + MockERC20 tokenA; + MockERC20 tokenB; + MockERC20 tokenC; + MockERC20 tokenD; + + PoolKey keyAB; + PoolKey keyBC; + PoolKey keyCD; + PoolKey keyNative; + + // 0.3% fee in Q64 (matches Uniswap's 3000 fee tier) + uint64 constant FEE_0_3 = 55340232221128655; + // Concentrated code path (tick bitmap active), MIN..MAX position so swaps + // never cross an initialized tick. Spacing 6000 fine-ticks ~= Uniswap spacing 60. + uint32 constant TICK_SPACING = 6000; + int32 constant RANGE_LOWER = -88722000; // -14787 * spacing + int32 constant RANGE_UPPER = 88722000; // +14787 * spacing + int32 constant INIT_TICK = 3050; // strictly inside a tick, not on a spacing multiple + uint128 constant LIQUIDITY_AMOUNT = 1_000_000 ether; + int128 constant SWAP_AMOUNT = 1 ether; + + function setUp() public virtual { + core = new Core(); + positions = new Positions(core, address(this), 0, 1); + router = new Router(core, address(0), address(0)); + bareMint = new BareMintRouter(core); + minLocker = new MinimalEkuboLocker(core); + + // One identical token artifact (solmate MockERC20, deployed via deployCode) + // in every harness, so transfer costs cannot skew the comparison. + tokenA = MockERC20(vm.deployCode("artifacts/MockERC20.json", abi.encode("TokenA", "A", 18))); + tokenB = MockERC20(vm.deployCode("artifacts/MockERC20.json", abi.encode("TokenB", "B", 18))); + tokenC = MockERC20(vm.deployCode("artifacts/MockERC20.json", abi.encode("TokenC", "C", 18))); + tokenD = MockERC20(vm.deployCode("artifacts/MockERC20.json", abi.encode("TokenD", "D", 18))); + if (address(tokenA) > address(tokenB)) (tokenA, tokenB) = (tokenB, tokenA); + if (address(tokenC) > address(tokenD)) (tokenC, tokenD) = (tokenD, tokenC); + if (address(tokenB) > address(tokenC)) (tokenB, tokenC) = (tokenC, tokenB); + if (address(tokenA) > address(tokenB)) (tokenA, tokenB) = (tokenB, tokenA); + if (address(tokenC) > address(tokenD)) (tokenC, tokenD) = (tokenD, tokenC); + tokenA.mint(address(this), 10_000_000 ether); + tokenB.mint(address(this), 10_000_000 ether); + tokenC.mint(address(this), 10_000_000 ether); + tokenD.mint(address(this), 10_000_000 ether); + vm.deal(address(this), 10_000_000 ether); + + keyAB = _concentratedKey(address(tokenA), address(tokenB)); + keyBC = _concentratedKey(address(tokenB), address(tokenC)); + keyCD = _concentratedKey(address(tokenC), address(tokenD)); + keyNative = _concentratedKey(NATIVE_TOKEN_ADDRESS, address(tokenB)); + + core.initializePool(keyAB, INIT_TICK); + core.initializePool(keyBC, INIT_TICK); + core.initializePool(keyCD, INIT_TICK); + core.initializePool(keyNative, INIT_TICK); + + tokenA.approve(address(positions), type(uint256).max); + tokenB.approve(address(positions), type(uint256).max); + tokenC.approve(address(positions), type(uint256).max); + tokenD.approve(address(positions), type(uint256).max); + tokenA.approve(address(router), type(uint256).max); + tokenB.approve(address(router), type(uint256).max); + tokenC.approve(address(router), type(uint256).max); + tokenA.approve(address(bareMint), type(uint256).max); + tokenB.approve(address(bareMint), type(uint256).max); + tokenA.approve(address(minLocker), type(uint256).max); + + positions.mintAndDeposit(keyAB, RANGE_LOWER, RANGE_UPPER, LIQUIDITY_AMOUNT, LIQUIDITY_AMOUNT, 0); + positions.mintAndDeposit(keyBC, RANGE_LOWER, RANGE_UPPER, LIQUIDITY_AMOUNT, LIQUIDITY_AMOUNT, 0); + positions.mintAndDeposit(keyCD, RANGE_LOWER, RANGE_UPPER, LIQUIDITY_AMOUNT, LIQUIDITY_AMOUNT, 0); + positions.mintAndDeposit{value: LIQUIDITY_AMOUNT}( + keyNative, RANGE_LOWER, RANGE_UPPER, LIQUIDITY_AMOUNT, LIQUIDITY_AMOUNT, 0 + ); + } + + function _concentratedKey(address t0, address t1) internal pure returns (PoolKey memory) { + return PoolKey({token0: t0, token1: t1, config: createConcentratedPoolConfig(FEE_0_3, TICK_SPACING, address(0))}); + } + + function _route(PoolKey[] memory keys) internal pure returns (RouteNode[] memory route) { + route = new RouteNode[](keys.length); + for (uint256 i = 0; i < keys.length; i++) { + route[i] = RouteNode(keys[i], SqrtRatio.wrap(0), 0); + } + } + + function _swapAB() internal { + router.swapAllowPartialFill(keyAB, createSwapParameters(SqrtRatio.wrap(0), SWAP_AMOUNT, false, 0)); + } + + function _multihop(uint256 n) internal { + PoolKey[] memory keys = new PoolKey[](n); + keys[0] = keyAB; + if (n > 1) keys[1] = keyBC; + if (n > 2) keys[2] = keyCD; + router.multihopSwap( + Swap(_route(keys), TokenAmount({token: address(tokenA), amount: SWAP_AMOUNT})), type(int256).min + ); + } + + // ---- single swaps through the Solidity Router ---- + + /// forge-config: default.isolate = true + function test_gas_single_steady_erc20() public { + _swapAB(); // warm-up, unmeasured + _swapAB(); + vm.snapshotGasLastCall("ekubo single erc20 steady-state"); + } + + /// forge-config: default.isolate = true + function test_gas_single_steady_erc20_reverse() public { + router.swapAllowPartialFill(keyAB, createSwapParameters(SqrtRatio.wrap(0), SWAP_AMOUNT, true, 0)); + router.swapAllowPartialFill(keyAB, createSwapParameters(SqrtRatio.wrap(0), SWAP_AMOUNT, true, 0)); + vm.snapshotGasLastCall("ekubo single erc20 reverse steady-state"); + } + + /// forge-config: default.isolate = true + function test_gas_single_steady_native() public { + router.swapAllowPartialFill{value: uint128(SWAP_AMOUNT)}( + keyNative, createSwapParameters(SqrtRatio.wrap(0), SWAP_AMOUNT, false, 0) + ); + router.swapAllowPartialFill{value: uint128(SWAP_AMOUNT)}( + keyNative, createSwapParameters(SqrtRatio.wrap(0), SWAP_AMOUNT, false, 0) + ); + vm.snapshotGasLastCall("ekubo single native steady-state"); + } + + /// @dev No warm-up: the very first swap in a freshly capitalized pool. Core pre-writes + /// the fee-per-liquidity accumulator slots in `initializePool`, so this should + /// cost the same as steady state. + /// forge-config: default.isolate = true + function test_gas_single_firstSwap_erc20() public { + _swapAB(); + vm.snapshotGasLastCall("ekubo single erc20 first swap in fresh pool"); + } + + // ---- single swaps through the minimal locker (same shape as the v4 minimal locker) ---- + + // The Router substitutes the extreme limit for a zero limit; Core itself requires an + // explicit one, so the minimal locker passes MIN_SQRT_RATIO (token0 in, price decreasing). + function _minSwapAB() internal { + minLocker.swapExactIn(keyAB, createSwapParameters(MIN_SQRT_RATIO, SWAP_AMOUNT, false, 0), address(this)); + } + + /// forge-config: default.isolate = true + function test_gas_single_steady_erc20_minimalLocker() public { + _minSwapAB(); + _minSwapAB(); + vm.snapshotGasLastCall("ekubo single erc20 steady-state minimal locker"); + } + + /// forge-config: default.isolate = true + function test_gas_single_steady_native_minimalLocker() public { + minLocker.swapExactIn{value: uint128(SWAP_AMOUNT)}( + keyNative, createSwapParameters(MIN_SQRT_RATIO, SWAP_AMOUNT, false, 0), address(this) + ); + minLocker.swapExactIn{value: uint128(SWAP_AMOUNT)}( + keyNative, createSwapParameters(MIN_SQRT_RATIO, SWAP_AMOUNT, false, 0), address(this) + ); + vm.snapshotGasLastCall("ekubo single native steady-state minimal locker"); + } + + // ---- routes through the Router's single-lock multihop path, warmed ---- + + /// forge-config: default.isolate = true + function test_gas_route_1pool() public { + _multihop(1); + _multihop(1); + vm.snapshotGasLastCall("ekubo route 1 pool steady-state"); + } + + /// forge-config: default.isolate = true + function test_gas_route_2pools() public { + _multihop(2); + _multihop(2); + vm.snapshotGasLastCall("ekubo route 2 pools steady-state"); + } + + /// forge-config: default.isolate = true + function test_gas_route_3pools() public { + _multihop(3); + _multihop(3); + vm.snapshotGasLastCall("ekubo route 3 pools steady-state"); + } + + // ---- liquidity provision ---- + + /// @dev Warm-up mint under one salt, measured mint under another: a brand-new position + /// with both boundary ticks already initialized (bare core path, no NFT). + /// forge-config: default.isolate = true + function test_gas_mint_newPosition_bare() public { + bareMint.mint(keyAB, bytes24(uint192(776)), RANGE_LOWER, RANGE_UPPER, 1e24, address(this)); + bareMint.mint(keyAB, bytes24(uint192(777)), RANGE_LOWER, RANGE_UPPER, 1e24, address(this)); + vm.snapshotGasLastCall("ekubo mint new position bare"); + } + + /// @dev The user-facing path: a new NFT position through the Positions contract. + /// forge-config: default.isolate = true + function test_gas_mint_newPosition_positionsNft() public { + positions.mintAndDeposit(keyAB, RANGE_LOWER, RANGE_UPPER, LIQUIDITY_AMOUNT, LIQUIDITY_AMOUNT, 0); + positions.mintAndDeposit(keyAB, RANGE_LOWER, RANGE_UPPER, LIQUIDITY_AMOUNT, LIQUIDITY_AMOUNT, 0); + vm.snapshotGasLastCall("ekubo mint new position Positions NFT"); + } + + // ---- pool creation ---- + + /// forge-config: default.isolate = true + function test_gas_initializePool() public { + core.initializePool(_concentratedKey(address(tokenA), address(tokenD)), INIT_TICK); + vm.snapshotGasLastCall("ekubo initialize pool"); + } + + receive() external payable {} +} diff --git a/benchmarks/lab/ekubo/EkuboGasTest.json b/benchmarks/lab/ekubo/EkuboGasTest.json new file mode 100644 index 0000000..2f423d6 --- /dev/null +++ b/benchmarks/lab/ekubo/EkuboGasTest.json @@ -0,0 +1,14 @@ +{ + "ekubo initialize pool": "92935", + "ekubo mint new position Positions NFT": "213104", + "ekubo mint new position bare": "179020", + "ekubo route 1 pool steady-state": "94904", + "ekubo route 2 pools steady-state": "119333", + "ekubo route 3 pools steady-state": "143774", + "ekubo single erc20 first swap in fresh pool": "87384", + "ekubo single erc20 reverse steady-state": "86334", + "ekubo single erc20 steady-state": "87416", + "ekubo single erc20 steady-state minimal locker": "87077", + "ekubo single native steady-state": "72580", + "ekubo single native steady-state minimal locker": "72164" +} \ No newline at end of file diff --git a/benchmarks/lab/ekubo/Flame.t.sol b/benchmarks/lab/ekubo/Flame.t.sol new file mode 100644 index 0000000..9c65494 --- /dev/null +++ b/benchmarks/lab/ekubo/Flame.t.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: ekubo-license-v1.eth +pragma solidity =0.8.33; + +import {EkuboGasTest} from "./EkuboGas.t.sol"; + +/// @notice Steady-state single-call capture for flamegraphs: setUp performs the same +/// warm-up swap as the snapshot test, the test itself makes exactly one swap, so +/// the flamegraph total matches "ekubo single erc20 steady-state". +contract FlameTest is EkuboGasTest { + function setUp() public override { + super.setUp(); + _swapAB(); + } + + function test_flame_single() public { + _swapAB(); + } +} diff --git a/benchmarks/lab/ekubo/foundry.toml b/benchmarks/lab/ekubo/foundry.toml new file mode 100644 index 0000000..ee3c7a4 --- /dev/null +++ b/benchmarks/lab/ekubo/foundry.toml @@ -0,0 +1,11 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +solc = "0.8.33" +optimizer = true +optimizer_runs = 9999999 +via_ir = true +evm_version = "osaka" +verbosity = 2 +fs_permissions = [{ access = "read", path = "./" }] diff --git a/benchmarks/lab/ekubo/remappings.txt b/benchmarks/lab/ekubo/remappings.txt new file mode 100644 index 0000000..6767287 --- /dev/null +++ b/benchmarks/lab/ekubo/remappings.txt @@ -0,0 +1,5 @@ +forge-std/=lib/forge-std/src/ +solady/=lib/solady/src/ +solmate/=/tmp/opencode/gasbench/lib/v4-core/lib/solmate/ +ekubo/=lib/ekubo/ +ekubo-test/=lib/ekubo/test/ diff --git a/benchmarks/lab/v3/Flame.t.sol b/benchmarks/lab/v3/Flame.t.sol new file mode 100644 index 0000000..9196aad --- /dev/null +++ b/benchmarks/lab/v3/Flame.t.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.26; + +import {IUniswapV3Pool, V3GasTest} from "./V3Gas.t.sol"; + +/// @notice Steady-state single-call capture for flamegraphs: setUp performs the same +/// warm-up as the snapshot test, the test itself makes exactly one 1-pool route +/// through the SwapRouter-shaped minimal router, so the flamegraph total matches +/// "v3 route 1 pool steady-state" (the cheapest path an EOA can send). +contract FlameTest is V3GasTest { + IUniswapV3Pool[] pools; + address[] tokensIn; + + function setUp() public override { + super.setUp(); + (IUniswapV3Pool[] memory p, address[] memory t) = _pools(1); + pools = p; + tokensIn = t; + multihopRouter.multiHop(pools, tokensIn, SWAP_AMOUNT, address(this)); + } + + function test_flame_single() public { + multihopRouter.multiHop(pools, tokensIn, SWAP_AMOUNT, address(this)); + } +} diff --git a/benchmarks/lab/v3/V3Gas.t.sol b/benchmarks/lab/v3/V3Gas.t.sol new file mode 100644 index 0000000..4854255 --- /dev/null +++ b/benchmarks/lab/v3/V3Gas.t.sol @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.26; + +import {Test} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; + +interface IUniswapV3Factory { + function createPool(address tokenA, address tokenB, uint24 fee) external returns (address pool); +} + +interface IUniswapV3Pool { + function token0() external view returns (address); + function token1() external view returns (address); + function initialize(uint160 sqrtPriceX96) external; + function mint(address recipient, int24 tickLower, int24 tickUpper, uint128 amount, bytes calldata data) + external + returns (uint256 amount0, uint256 amount1); + function swap( + address recipient, + bool zeroForOne, + int256 amountSpecified, + uint160 sqrtPriceLimitX96, + bytes calldata data + ) external returns (int256 amount0, int256 amount1); +} + +/// @notice Interface to the shared token artifact (solmate MockERC20 bytecode, +/// deployed via deployCode): the identical token contract runs in all harnesses. +interface SharedToken { + function mint(address to, uint256 amount) external; + function approve(address spender, uint256 amount) external returns (bool); + function transfer(address to, uint256 amount) external returns (bool); + function transferFrom(address from, address to, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); +} + +/// @notice Minimal N-hop exact-input router over v3 pools with the same token flow as +/// the production SwapRouter's `exactInput`: the first hop is paid straight from +/// the user (transferFrom user -> pool), intermediate outputs land on the router +/// and are paid forward (transfer router -> next pool), and the last hop delivers +/// to the recipient. That is 2 transfers per pool, and every intermediate router +/// balance goes 0 -> x -> 0 inside the transaction (refund-eligible, see tests). +contract V3MultiHopRouter { + uint160 constant MIN_LIMIT = 4295128740; // MIN_SQRT_RATIO + 1 + + function multiHop(IUniswapV3Pool[] memory pools, address[] memory tokensIn, int256 amountIn, address recipient) + external + returns (int256 out) + { + int256 amount = amountIn; + address payer = msg.sender; + for (uint256 i = 0; i < pools.length; i++) { + bool last = i == pools.length - 1; + (, int256 a1) = + pools[i].swap(last ? recipient : address(this), true, amount, MIN_LIMIT, abi.encode(tokensIn[i], payer)); + amount = -a1; + require(amount > 0, "no hop output"); + payer = address(this); + } + out = amount; + } + + function uniswapV3SwapCallback(int256 amount0Delta, int256, bytes calldata data) external { + (address tokenIn, address payer) = abi.decode(data, (address, address)); + uint256 owed = uint256(amount0Delta); // every hop is zeroForOne + if (payer == address(this)) SharedToken(tokenIn).transfer(msg.sender, owed); + else SharedToken(tokenIn).transferFrom(payer, msg.sender, owed); + } +} + +/// @notice Uniswap v3 gas harness deploying canonical mainnet bytecode (v3-core 1.0.1 +/// artifacts) and driving it with a minimal callback payer. Direct pool calls +/// make the single-hop number a lower bound for production-router (SwapRouter) gas. +contract V3GasTest is Test { + IUniswapV3Factory factory; + IUniswapV3Pool poolAB; + IUniswapV3Pool poolBC; + IUniswapV3Pool poolCD; + V3MultiHopRouter multihopRouter; + + SharedToken tokenA; + SharedToken tokenB; + SharedToken tokenC; + SharedToken tokenD; + + int24 constant TICK_LOWER = -887220; + int24 constant TICK_UPPER = 887220; + uint160 constant INIT_SQRT_PRICE = 79347087983665984109010747392; // 1.0001**(30/2) * 2**96: tick 30, strictly inside (spacing 60) + uint160 constant MIN_LIMIT = 4295128740; // MIN_SQRT_RATIO + 1 + uint160 constant MAX_LIMIT = 1461446703485210103287273052203988822378723970341; // MAX_SQRT_RATIO - 1 + uint128 constant LIQUIDITY = 998501199320305883812938; // liquidity for 1M/1M full-range at the init price (matches the v4/ekubo harnesses) + int256 constant SWAP_AMOUNT = 1e18; // exact input + + function setUp() public virtual { + factory = IUniswapV3Factory(vm.deployCode("artifacts/UniswapV3Factory.json")); + + // One identical token artifact (solmate MockERC20) in every harness. + tokenA = SharedToken(vm.deployCode("artifacts/MockERC20.json", abi.encode("TokenA", "A", 18))); + tokenB = SharedToken(vm.deployCode("artifacts/MockERC20.json", abi.encode("TokenB", "B", 18))); + tokenC = SharedToken(vm.deployCode("artifacts/MockERC20.json", abi.encode("TokenC", "C", 18))); + tokenD = SharedToken(vm.deployCode("artifacts/MockERC20.json", abi.encode("TokenD", "D", 18))); + if (address(tokenA) > address(tokenB)) (tokenA, tokenB) = (tokenB, tokenA); + if (address(tokenC) > address(tokenD)) (tokenC, tokenD) = (tokenD, tokenC); + if (address(tokenB) > address(tokenC)) (tokenB, tokenC) = (tokenC, tokenB); + if (address(tokenA) > address(tokenB)) (tokenA, tokenB) = (tokenB, tokenA); + if (address(tokenC) > address(tokenD)) (tokenC, tokenD) = (tokenD, tokenC); + tokenA.mint(address(this), 1e45); + tokenB.mint(address(this), 1e45); + tokenC.mint(address(this), 1e45); + tokenD.mint(address(this), 1e45); + + poolAB = IUniswapV3Pool(factory.createPool(address(tokenA), address(tokenB), 3000)); + poolBC = IUniswapV3Pool(factory.createPool(address(tokenB), address(tokenC), 3000)); + poolCD = IUniswapV3Pool(factory.createPool(address(tokenC), address(tokenD), 3000)); + poolAB.initialize(INIT_SQRT_PRICE); + poolBC.initialize(INIT_SQRT_PRICE); + poolCD.initialize(INIT_SQRT_PRICE); + poolAB.mint(address(this), TICK_LOWER, TICK_UPPER, LIQUIDITY, ""); + poolBC.mint(address(this), TICK_LOWER, TICK_UPPER, LIQUIDITY, ""); + poolCD.mint(address(this), TICK_LOWER, TICK_UPPER, LIQUIDITY, ""); + multihopRouter = new V3MultiHopRouter(); + tokenA.approve(address(multihopRouter), type(uint256).max); + } + + function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external { + if (amount0Owed > 0) SharedToken(IUniswapV3Pool(msg.sender).token0()).transfer(msg.sender, amount0Owed); + if (amount1Owed > 0) SharedToken(IUniswapV3Pool(msg.sender).token1()).transfer(msg.sender, amount1Owed); + } + + function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external { + if (amount0Delta > 0) SharedToken(IUniswapV3Pool(msg.sender).token0()).transfer(msg.sender, uint256(amount0Delta)); + if (amount1Delta > 0) SharedToken(IUniswapV3Pool(msg.sender).token1()).transfer(msg.sender, uint256(amount1Delta)); + } + + function _pools(uint256 n) internal view returns (IUniswapV3Pool[] memory pools, address[] memory tokensIn) { + pools = new IUniswapV3Pool[](n); + tokensIn = new address[](n); + pools[0] = poolAB; + tokensIn[0] = address(tokenA); + if (n > 1) { + pools[1] = poolBC; + tokensIn[1] = address(tokenB); + } + if (n > 2) { + pools[2] = poolCD; + tokensIn[2] = address(tokenC); + } + } + + /// @dev Isolated calls get their own EVM context, so `snapshotGasLastCall` reports the + /// call's execution gas net of the EIP-3529 refund (capped at 1/5 of gas used), + /// excluding the 21,000 base and calldata. The gross figure before refunds and the + /// refund actually granted are recorded alongside. + function _snapshotWithRefund(string memory name) internal { + Vm.Gas memory g = vm.lastCallGas(); + vm.snapshotGasLastCall(name); + vm.snapshotValue(string.concat(name, " gross before refund"), g.gasTotalUsed); + vm.snapshotValue(string.concat(name, " refund granted"), uint256(uint64(g.gasRefunded))); + } + + // ---- single swaps: direct pool call, the test contract is the callback payer ---- + // A lower bound only: an externally owned account cannot be a v3 callback payer, so + // the cheapest path a user can actually send is the 1-pool route below. The callback + // runs in this test contract, so these direct-call figures move by a few gas when the + // contract's function set (its dispatcher) changes. + + /// forge-config: default.isolate = true + function test_gas_single_steady_erc20() public { + poolAB.swap(address(this), true, SWAP_AMOUNT, MIN_LIMIT, ""); // warm-up, unmeasured + poolAB.swap(address(this), true, SWAP_AMOUNT, MIN_LIMIT, ""); + _snapshotWithRefund("v3 single erc20 steady-state"); + } + + /// forge-config: default.isolate = true + function test_gas_single_steady_erc20_reverse() public { + poolAB.swap(address(this), false, SWAP_AMOUNT, MAX_LIMIT, ""); + poolAB.swap(address(this), false, SWAP_AMOUNT, MAX_LIMIT, ""); + _snapshotWithRefund("v3 single erc20 reverse steady-state"); + } + + /// @dev The oracle observation is written only by swaps that change the tick, and at + /// most once per block (`Oracle.write` returns early on a repeated timestamp). Two + /// tests with identical swaps isolate that write. Warm-ups: one reverse swap (so the + /// token1 fee accumulator is already nonzero) and one forward swap (tick 30 -> 29); + /// the measured swap buys 2 tokens back (tick 29 -> 30), once in the same block + /// (write skipped) and once in a later block (observation written, cardinality 1). + function _tickChangeWarmUp() internal { + poolAB.swap(address(this), false, SWAP_AMOUNT, MAX_LIMIT, ""); + poolAB.swap(address(this), true, SWAP_AMOUNT, MIN_LIMIT, ""); + } + + /// forge-config: default.isolate = true + function test_gas_single_tickChange_sameBlock() public { + _tickChangeWarmUp(); + poolAB.swap(address(this), false, 2 * SWAP_AMOUNT, MAX_LIMIT, ""); + _snapshotWithRefund("v3 single tick-changing swap same block (oracle write skipped)"); + } + + /// forge-config: default.isolate = true + function test_gas_single_tickChange_nextBlock() public { + _tickChangeWarmUp(); + vm.warp(block.timestamp + 12); + vm.roll(block.number + 1); + poolAB.swap(address(this), false, 2 * SWAP_AMOUNT, MAX_LIMIT, ""); + _snapshotWithRefund("v3 single tick-changing swap next block (oracle written)"); + } + + /// @dev No warm-up: the very first swap in a freshly capitalized pool, which writes the + /// fee-growth accumulator from zero (SSTORE 20,000 instead of 2,900). + /// forge-config: default.isolate = true + function test_gas_single_firstSwap_erc20() public { + poolAB.swap(address(this), true, SWAP_AMOUNT, MIN_LIMIT, ""); + _snapshotWithRefund("v3 single erc20 first swap in fresh pool"); + } + + // ---- routes through the SwapRouter-shaped minimal router, warmed ---- + + /// forge-config: default.isolate = true + function test_gas_route_1pool() public { + (IUniswapV3Pool[] memory pools, address[] memory tokensIn) = _pools(1); + multihopRouter.multiHop(pools, tokensIn, SWAP_AMOUNT, address(this)); + multihopRouter.multiHop(pools, tokensIn, SWAP_AMOUNT, address(this)); + _snapshotWithRefund("v3 route 1 pool steady-state"); + } + + /// forge-config: default.isolate = true + function test_gas_route_2pools() public { + (IUniswapV3Pool[] memory pools, address[] memory tokensIn) = _pools(2); + multihopRouter.multiHop(pools, tokensIn, SWAP_AMOUNT, address(this)); + multihopRouter.multiHop(pools, tokensIn, SWAP_AMOUNT, address(this)); + _snapshotWithRefund("v3 route 2 pools steady-state"); + } + + /// forge-config: default.isolate = true + function test_gas_route_3pools() public { + (IUniswapV3Pool[] memory pools, address[] memory tokensIn) = _pools(3); + multihopRouter.multiHop(pools, tokensIn, SWAP_AMOUNT, address(this)); + multihopRouter.multiHop(pools, tokensIn, SWAP_AMOUNT, address(this)); + _snapshotWithRefund("v3 route 3 pools steady-state"); + } + + // ---- liquidity provision, direct to the pool ---- + + /// @dev Warm-up mint to one owner, measured mint to a different owner: a brand-new + /// position (fresh position slots) with both boundary ticks already initialized, + /// the same shape as the Ekubo and v4 new-position mints. + /// forge-config: default.isolate = true + function test_gas_mint_newPosition() public { + poolAB.mint(address(this), TICK_LOWER, TICK_UPPER, LIQUIDITY, ""); + poolAB.mint(address(0xBEEF), TICK_LOWER, TICK_UPPER, LIQUIDITY, ""); + _snapshotWithRefund("v3 mint new position"); + } + + /// @dev Adding liquidity to an existing position (same owner and ticks): cheaper, because + /// the position slots are already nonzero. + /// forge-config: default.isolate = true + function test_gas_mint_samePosition() public { + poolAB.mint(address(this), TICK_LOWER, TICK_UPPER, LIQUIDITY, ""); + poolAB.mint(address(this), TICK_LOWER, TICK_UPPER, LIQUIDITY, ""); + _snapshotWithRefund("v3 mint same position"); + } + + // ---- pool creation ---- + + /// @dev v3 pools are separate contracts: `createPool` deploys one (CREATE2 of the full + /// pool bytecode), then `initialize` sets the price and the first observation. + /// forge-config: default.isolate = true + function test_gas_initializePool() public { + IUniswapV3Pool poolAD = IUniswapV3Pool(factory.createPool(address(tokenA), address(tokenD), 3000)); + uint256 create = vm.snapshotGasLastCall("v3 createPool"); + poolAD.initialize(INIT_SQRT_PRICE); + uint256 init = vm.snapshotGasLastCall("v3 initialize"); + vm.snapshotValue("v3 createPool + initialize", create + init); + } +} diff --git a/benchmarks/lab/v3/V3GasTest.json b/benchmarks/lab/v3/V3GasTest.json new file mode 100644 index 0000000..e4a4c03 --- /dev/null +++ b/benchmarks/lab/v3/V3GasTest.json @@ -0,0 +1,35 @@ +{ + "v3 createPool": "4558970", + "v3 createPool + initialize": "4629298", + "v3 initialize": "70328", + "v3 mint new position": "138380", + "v3 mint new position gross before refund": "141180", + "v3 mint new position refund granted": "2800", + "v3 mint same position": "121496", + "v3 mint same position gross before refund": "124296", + "v3 mint same position refund granted": "2800", + "v3 route 1 pool steady-state": "94078", + "v3 route 1 pool steady-state gross before refund": "94078", + "v3 route 1 pool steady-state refund granted": "0", + "v3 route 2 pools steady-state": "152774", + "v3 route 2 pools steady-state gross before refund": "172674", + "v3 route 2 pools steady-state refund granted": "19900", + "v3 route 3 pools steady-state": "211470", + "v3 route 3 pools steady-state gross before refund": "251270", + "v3 route 3 pools steady-state refund granted": "39800", + "v3 single erc20 first swap in fresh pool": "104741", + "v3 single erc20 first swap in fresh pool gross before refund": "104741", + "v3 single erc20 first swap in fresh pool refund granted": "0", + "v3 single erc20 reverse steady-state": "87040", + "v3 single erc20 reverse steady-state gross before refund": "87040", + "v3 single erc20 reverse steady-state refund granted": "0", + "v3 single erc20 steady-state": "87641", + "v3 single erc20 steady-state gross before refund": "87641", + "v3 single erc20 steady-state refund granted": "0", + "v3 single tick-changing swap next block (oracle written)": "93712", + "v3 single tick-changing swap next block (oracle written) gross before refund": "93712", + "v3 single tick-changing swap next block (oracle written) refund granted": "0", + "v3 single tick-changing swap same block (oracle write skipped)": "89836", + "v3 single tick-changing swap same block (oracle write skipped) gross before refund": "89836", + "v3 single tick-changing swap same block (oracle write skipped) refund granted": "0" +} \ No newline at end of file diff --git a/benchmarks/lab/v3/foundry.toml b/benchmarks/lab/v3/foundry.toml new file mode 100644 index 0000000..abdece5 --- /dev/null +++ b/benchmarks/lab/v3/foundry.toml @@ -0,0 +1,11 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +solc = "0.8.26" +optimizer = true +optimizer_runs = 1000000 +via_ir = true +evm_version = "cancun" +verbosity = 2 +fs_permissions = [{ access = "read", path = "./artifacts" }] diff --git a/benchmarks/lab/v4/Flame.t.sol b/benchmarks/lab/v4/Flame.t.sol new file mode 100644 index 0000000..7c72f36 --- /dev/null +++ b/benchmarks/lab/v4/Flame.t.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.26; + +import {V4GasTest} from "./V4Gas.t.sol"; + +/// @notice Steady-state single-call capture for flamegraphs: setUp performs the same +/// warm-up swap as the snapshot test, the test itself makes exactly one swap, so +/// the flamegraph total matches "v4 single erc20 steady-state". +contract FlameTest is V4GasTest { + function setUp() public override { + super.setUp(); + _swapAB(); + } + + function test_flame_single() public { + assertGt(_swapAB(), 0); + } +} diff --git a/benchmarks/lab/v4/V4Gas.t.sol b/benchmarks/lab/v4/V4Gas.t.sol new file mode 100644 index 0000000..29ae610 --- /dev/null +++ b/benchmarks/lab/v4/V4Gas.t.sol @@ -0,0 +1,346 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.26; + +import {Test} from "forge-std/Test.sol"; +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; +import {PoolManager} from "v4-core/src/PoolManager.sol"; +import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "v4-core/src/types/PoolKey.sol"; +import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; +import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol"; +import {SwapParams, ModifyLiquidityParams} from "v4-core/src/types/PoolOperation.sol"; +import {IHooks} from "v4-core/src/interfaces/IHooks.sol"; +import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol"; +import {TickMath} from "v4-core/src/libraries/TickMath.sol"; +import {TransientStateLibrary} from "v4-core/src/libraries/TransientStateLibrary.sol"; +import {CurrencySettler} from "v4-core/test/utils/CurrencySettler.sol"; +import {LiquidityAmounts} from "v4-core/test/utils/LiquidityAmounts.sol"; +import {PoolSwapTest} from "v4-core/src/test/PoolSwapTest.sol"; +import {PoolModifyLiquidityTest} from "v4-core/src/test/PoolModifyLiquidityTest.sol"; + +/// @notice True-minimal single-hop exact-input locker: unlock, swap, settle net. +/// No balance assertions, no test settings — the floor for v4 router cost. +/// Native input is settled with the value forwarded by the caller. +contract MinimalSwapRouter is IUnlockCallback { + using CurrencyLibrary for Currency; + using CurrencySettler for Currency; + using TransientStateLibrary for IPoolManager; + + IPoolManager public immutable manager; + + constructor(IPoolManager _manager) { + manager = _manager; + } + + function swapExactIn(PoolKey memory key, bool zeroForOne, int256 amountIn, uint160 limit, address payer) + external + payable + returns (uint256 amountOut) + { + bytes memory result = manager.unlock(abi.encode(key, zeroForOne, amountIn, limit, payer, msg.sender)); + amountOut = abi.decode(result, (uint256)); + } + + function unlockCallback(bytes calldata rawData) external returns (bytes memory) { + require(msg.sender == address(manager)); + (PoolKey memory key, bool zeroForOne, int256 amountIn, uint160 limit, address payer, address recipient) = + abi.decode(rawData, (PoolKey, bool, int256, uint160, address, address)); + manager.swap(key, SwapParams({zeroForOne: zeroForOne, amountSpecified: amountIn, sqrtPriceLimitX96: limit}), ""); + Currency currencyIn = zeroForOne ? key.currency0 : key.currency1; + Currency currencyOut = zeroForOne ? key.currency1 : key.currency0; + int256 inDelta = manager.currencyDelta(address(this), currencyIn); + int256 outDelta = manager.currencyDelta(address(this), currencyOut); + if (inDelta < 0) currencyIn.settle(manager, payer, uint256(-inDelta), false); + if (outDelta > 0) currencyOut.take(manager, recipient, uint256(outDelta), false); + return abi.encode(uint256(outDelta)); + } +} + +/// @notice Minimal single-lock N-hop exact-input router (e.g. A->B->C->D), mirroring +/// what a production v4 router does inside one `unlock` with net settlement. +contract MultiHopRouter is IUnlockCallback { + using CurrencyLibrary for Currency; + using CurrencySettler for Currency; + using TransientStateLibrary for IPoolManager; + + IPoolManager public immutable manager; + + constructor(IPoolManager _manager) { + manager = _manager; + } + + struct Hop { + PoolKey key; + uint160 sqrtPriceLimitX96; + } + + function multiHop(Hop[] memory hops, Currency currencyIn, Currency currencyOut, int256 amountIn) + external + payable + returns (uint256 amountOut) + { + bytes memory result = manager.unlock( + abi.encode(hops, currencyIn, currencyOut, amountIn, msg.sender, msg.sender) + ); + amountOut = abi.decode(result, (uint256)); + uint256 ethBalance = address(this).balance; + if (ethBalance > 0) CurrencyLibrary.ADDRESS_ZERO.transfer(msg.sender, ethBalance); + } + + function unlockCallback(bytes calldata rawData) external returns (bytes memory) { + require(msg.sender == address(manager)); + (Hop[] memory hops, Currency currencyIn, Currency currencyOut, int256 amountIn, address payer, address recipient) + = abi.decode(rawData, (Hop[], Currency, Currency, int256, address, address)); + + Currency currencyOwed = currencyIn; + int256 amountOwed = amountIn; + for (uint256 i = 0; i < hops.length; i++) { + bool zeroForOne = currencyOwed == hops[i].key.currency0; + manager.swap( + hops[i].key, + SwapParams({zeroForOne: zeroForOne, amountSpecified: amountOwed, sqrtPriceLimitX96: hops[i].sqrtPriceLimitX96}), + "" + ); + currencyOwed = zeroForOne ? hops[i].key.currency1 : hops[i].key.currency0; + amountOwed = -manager.currencyDelta(address(this), currencyOwed); + require(amountOwed < 0, "no hop output"); + } + + int256 inDelta = manager.currencyDelta(address(this), currencyIn); + int256 outDelta = manager.currencyDelta(address(this), currencyOut); + require(inDelta <= 0, "inDelta positive"); + require(outDelta >= 0, "outDelta negative"); + if (inDelta < 0) currencyIn.settle(manager, payer, uint256(-inDelta), false); + if (outDelta > 0) currencyOut.take(manager, recipient, uint256(outDelta), false); + return abi.encode(uint256(outDelta)); + } +} + +contract V4GasTest is Test { + using PoolIdLibrary for PoolKey; + using CurrencyLibrary for Currency; + + PoolManager manager; + PoolSwapTest swapRouter; + MinimalSwapRouter minRouter; + PoolModifyLiquidityTest mintRouter; + MultiHopRouter multiHopRouter; + + MockERC20 tokenA; + MockERC20 tokenB; + MockERC20 tokenC; + MockERC20 tokenD; + + PoolKey keyAB; + PoolKey keyBC; + PoolKey keyCD; + PoolKey keyNative; + + int24 constant TICK_LOWER = -887220; + int24 constant TICK_UPPER = 887220; + uint256 constant LIQUIDITY_TOKEN_AMOUNT = 1_000_000 ether; + int256 constant SWAP_AMOUNT = -1 ether; + + function setUp() public virtual { + manager = new PoolManager(address(this)); + swapRouter = new PoolSwapTest(manager); + minRouter = new MinimalSwapRouter(manager); + mintRouter = new PoolModifyLiquidityTest(manager); + multiHopRouter = new MultiHopRouter(manager); + + // One identical token artifact (solmate MockERC20, deployed via deployCode) + // in every harness, so transfer costs cannot skew the comparison. + tokenA = MockERC20(vm.deployCode("artifacts/MockERC20.json", abi.encode("TokenA", "A", 18))); + tokenB = MockERC20(vm.deployCode("artifacts/MockERC20.json", abi.encode("TokenB", "B", 18))); + tokenC = MockERC20(vm.deployCode("artifacts/MockERC20.json", abi.encode("TokenC", "C", 18))); + tokenD = MockERC20(vm.deployCode("artifacts/MockERC20.json", abi.encode("TokenD", "D", 18))); + // sort A < B < C < D by address for valid v4 currency ordering + if (address(tokenA) > address(tokenB)) (tokenA, tokenB) = (tokenB, tokenA); + if (address(tokenC) > address(tokenD)) (tokenC, tokenD) = (tokenD, tokenC); + if (address(tokenB) > address(tokenC)) (tokenB, tokenC) = (tokenC, tokenB); + if (address(tokenA) > address(tokenB)) (tokenA, tokenB) = (tokenB, tokenA); + if (address(tokenC) > address(tokenD)) (tokenC, tokenD) = (tokenD, tokenC); + + tokenA.mint(address(this), 10_000_000 ether); + tokenB.mint(address(this), 10_000_000 ether); + tokenC.mint(address(this), 10_000_000 ether); + tokenD.mint(address(this), 10_000_000 ether); + vm.deal(address(this), 10_000_000 ether); + + tokenA.approve(address(swapRouter), type(uint256).max); + tokenB.approve(address(swapRouter), type(uint256).max); + tokenC.approve(address(swapRouter), type(uint256).max); + tokenA.approve(address(mintRouter), type(uint256).max); + tokenB.approve(address(mintRouter), type(uint256).max); + tokenC.approve(address(mintRouter), type(uint256).max); + tokenD.approve(address(mintRouter), type(uint256).max); + tokenA.approve(address(multiHopRouter), type(uint256).max); + tokenA.approve(address(minRouter), type(uint256).max); + tokenB.approve(address(minRouter), type(uint256).max); + + keyAB = _key(address(tokenA), address(tokenB)); + keyBC = _key(address(tokenB), address(tokenC)); + keyCD = _key(address(tokenC), address(tokenD)); + + manager.initialize(keyAB, TickMath.getSqrtPriceAtTick(30)); + manager.initialize(keyBC, TickMath.getSqrtPriceAtTick(30)); + manager.initialize(keyCD, TickMath.getSqrtPriceAtTick(30)); + _addFullRangeLiquidity(keyAB); + _addFullRangeLiquidity(keyBC); + _addFullRangeLiquidity(keyCD); + + // native (ETH) / tokenB pool for the native-input scenario + keyNative = _key(address(0), address(tokenB)); + manager.initialize(keyNative, TickMath.getSqrtPriceAtTick(30)); + mintRouter.modifyLiquidity{value: LIQUIDITY_TOKEN_AMOUNT}( + keyNative, + ModifyLiquidityParams({tickLower: TICK_LOWER, tickUpper: TICK_UPPER, liquidityDelta: int256(uint256(_fullRangeLiquidity(LIQUIDITY_TOKEN_AMOUNT, LIQUIDITY_TOKEN_AMOUNT))), salt: bytes32(0)}), + "" + ); + } + + function _key(address c0, address c1) internal pure returns (PoolKey memory) { + return PoolKey({ + currency0: Currency.wrap(c0), + currency1: Currency.wrap(c1), + fee: 3000, + tickSpacing: 60, + hooks: IHooks(address(0)) + }); + } + + function _fullRangeLiquidity(uint256 amount0, uint256 amount1) internal pure returns (uint128) { + uint160 sqrtA = TickMath.getSqrtPriceAtTick(TICK_LOWER); + uint160 sqrtB = TickMath.getSqrtPriceAtTick(TICK_UPPER); + return LiquidityAmounts.getLiquidityForAmounts(TickMath.getSqrtPriceAtTick(30), sqrtA, sqrtB, amount0, amount1); + } + + function _addFullRangeLiquidity(PoolKey memory key) internal { + uint128 liq = _fullRangeLiquidity(LIQUIDITY_TOKEN_AMOUNT, LIQUIDITY_TOKEN_AMOUNT); + mintRouter.modifyLiquidity( + key, + ModifyLiquidityParams({tickLower: TICK_LOWER, tickUpper: TICK_UPPER, liquidityDelta: int256(uint256(liq)), salt: bytes32(0)}), + "" + ); + } + + function _v4Hops(PoolKey[] memory keys) internal pure returns (MultiHopRouter.Hop[] memory hops) { + hops = new MultiHopRouter.Hop[](keys.length); + for (uint256 i = 0; i < keys.length; i++) { + hops[i] = MultiHopRouter.Hop({key: keys[i], sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1}); + } + } + + function _swapAB() internal returns (uint256) { + return minRouter.swapExactIn(keyAB, true, SWAP_AMOUNT, TickMath.MIN_SQRT_PRICE + 1, address(this)); + } + + // ---- single swaps through the minimal locker ---- + + /// forge-config: default.isolate = true + function test_gas_single_steady_erc20() public { + _swapAB(); // warm-up, unmeasured + uint256 out = _swapAB(); + assertGt(out, 0); + vm.snapshotGasLastCall("v4 single erc20 steady-state"); + } + + /// forge-config: default.isolate = true + function test_gas_single_steady_erc20_reverse() public { + minRouter.swapExactIn(keyAB, false, SWAP_AMOUNT, TickMath.MAX_SQRT_PRICE - 1, address(this)); + uint256 out = minRouter.swapExactIn(keyAB, false, SWAP_AMOUNT, TickMath.MAX_SQRT_PRICE - 1, address(this)); + assertGt(out, 0); + vm.snapshotGasLastCall("v4 single erc20 reverse steady-state"); + } + + /// forge-config: default.isolate = true + function test_gas_single_steady_native() public { + minRouter.swapExactIn{value: 1 ether}(keyNative, true, SWAP_AMOUNT, TickMath.MIN_SQRT_PRICE + 1, address(this)); + uint256 out = minRouter.swapExactIn{value: 1 ether}(keyNative, true, SWAP_AMOUNT, TickMath.MIN_SQRT_PRICE + 1, address(this)); + assertGt(out, 0); + vm.snapshotGasLastCall("v4 single native steady-state"); + } + + /// @dev No warm-up: the very first swap in a freshly capitalized pool, which writes the + /// fee-growth accumulator from zero (SSTORE 20,000 instead of 2,900). + /// forge-config: default.isolate = true + function test_gas_single_firstSwap_erc20() public { + uint256 out = _swapAB(); + assertGt(out, 0); + vm.snapshotGasLastCall("v4 single erc20 first swap in fresh pool"); + } + + /// @dev Context only: v4-core's own PoolSwapTest helper (balance assertions and test + /// settings) on the same swap, to show the minimal locker is the floor. + /// forge-config: default.isolate = true + function test_gas_single_steady_erc20_poolSwapTest() public { + SwapParams memory params = + SwapParams({zeroForOne: true, amountSpecified: SWAP_AMOUNT, sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1}); + PoolSwapTest.TestSettings memory settings = PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); + swapRouter.swap(keyAB, params, settings, ""); + swapRouter.swap(keyAB, params, settings, ""); + vm.snapshotGasLastCall("v4 single erc20 steady-state PoolSwapTest helper"); + } + + // ---- routes through the single-lock multihop router, warmed ---- + + function _route(uint256 n) internal returns (uint256 out) { + PoolKey[] memory keys = new PoolKey[](n); + keys[0] = keyAB; + if (n > 1) keys[1] = keyBC; + if (n > 2) keys[2] = keyCD; + Currency currencyOut = Currency.wrap(address(n == 1 ? tokenB : n == 2 ? tokenC : tokenD)); + out = multiHopRouter.multiHop(_v4Hops(keys), Currency.wrap(address(tokenA)), currencyOut, SWAP_AMOUNT); + } + + /// forge-config: default.isolate = true + function test_gas_route_1pool() public { + _route(1); + assertGt(_route(1), 0); + vm.snapshotGasLastCall("v4 route 1 pool steady-state"); + } + + /// forge-config: default.isolate = true + function test_gas_route_2pools() public { + _route(2); + assertGt(_route(2), 0); + vm.snapshotGasLastCall("v4 route 2 pools steady-state"); + } + + /// forge-config: default.isolate = true + function test_gas_route_3pools() public { + _route(3); + assertGt(_route(3), 0); + vm.snapshotGasLastCall("v4 route 3 pools steady-state"); + } + + // ---- liquidity provision through v4-core's PoolModifyLiquidityTest ---- + + /// @dev Warm-up mint under one salt, measured mint under another: a brand-new position + /// with both boundary ticks already initialized. + /// forge-config: default.isolate = true + function test_gas_mint_newPosition() public { + uint128 liq = _fullRangeLiquidity(LIQUIDITY_TOKEN_AMOUNT, LIQUIDITY_TOKEN_AMOUNT); + mintRouter.modifyLiquidity( + keyAB, + ModifyLiquidityParams({tickLower: TICK_LOWER, tickUpper: TICK_UPPER, liquidityDelta: int256(uint256(liq)), salt: bytes32(uint256(2))}), + "" + ); + mintRouter.modifyLiquidity( + keyAB, + ModifyLiquidityParams({tickLower: TICK_LOWER, tickUpper: TICK_UPPER, liquidityDelta: int256(uint256(liq)), salt: bytes32(uint256(1))}), + "" + ); + vm.snapshotGasLastCall("v4 mint new position"); + } + + // ---- pool creation ---- + + /// forge-config: default.isolate = true + function test_gas_initializePool() public { + manager.initialize(_key(address(tokenA), address(tokenD)), TickMath.getSqrtPriceAtTick(30)); + vm.snapshotGasLastCall("v4 initialize pool"); + } + + receive() external payable {} +} diff --git a/benchmarks/lab/v4/V4GasTest.json b/benchmarks/lab/v4/V4GasTest.json new file mode 100644 index 0000000..1096672 --- /dev/null +++ b/benchmarks/lab/v4/V4GasTest.json @@ -0,0 +1,12 @@ +{ + "v4 initialize pool": "51812", + "v4 mint new position": "162188", + "v4 route 1 pool steady-state": "107360", + "v4 route 2 pools steady-state": "141771", + "v4 route 3 pools steady-state": "176182", + "v4 single erc20 first swap in fresh pool": "120633", + "v4 single erc20 reverse steady-state": "103358", + "v4 single erc20 steady-state": "103533", + "v4 single erc20 steady-state PoolSwapTest helper": "116827", + "v4 single native steady-state": "88864" +} \ No newline at end of file diff --git a/benchmarks/lab/v4/foundry.toml b/benchmarks/lab/v4/foundry.toml new file mode 100644 index 0000000..e9b0a10 --- /dev/null +++ b/benchmarks/lab/v4/foundry.toml @@ -0,0 +1,11 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +solc = "0.8.26" +optimizer = true +optimizer_runs = 44444444 +via_ir = true +evm_version = "cancun" +verbosity = 2 +fs_permissions = [{ access = "read", path = "./" }] diff --git a/benchmarks/lab/v4/remappings.txt b/benchmarks/lab/v4/remappings.txt new file mode 100644 index 0000000..5f3d97e --- /dev/null +++ b/benchmarks/lab/v4/remappings.txt @@ -0,0 +1,6 @@ +forge-std/=lib/forge-std/src/ +solady/=lib/solady/src/ +solmate/=lib/v4-core/lib/solmate/ +v4-core/=lib/v4-core/ + +v4-src/=lib/v4-core/src/ diff --git a/benchmarks/page-numbers.mjs b/benchmarks/page-numbers.mjs new file mode 100644 index 0000000..2808cd0 --- /dev/null +++ b/benchmarks/page-numbers.mjs @@ -0,0 +1,207 @@ +#!/usr/bin/env node +// Derives every figure on the Gas efficiency page from the raw snapshot and measurement +// files in this directory, so each percentage can be re-checked without a calculator. +// Run: node page-numbers.mjs +import { readFileSync } from "node:fs"; +const j = (p) => JSON.parse(readFileSync(new URL(p, import.meta.url))); +const ek = j("./prod/snapshots/ProdEkuboTest.json"); +const v3 = j("./prod/snapshots/ProdV3Test.json"); +const v4 = j("./prod/snapshots/ProdV4Test.json"); +const month = j(process.env.MONTH ?? "./pools-per-swap/results-30d.json"); +const BASE = 21000; +const BLOCK_GAS = 60_000_000; +const tx = (snap, name) => + Number(snap[name]) + + BASE + + Number(snap[`${name} | calldata gas (4/16 per byte)`]); +const pct = (a, b) => ((100 * (b - a)) / b).toFixed(1); +const fmt = (n) => Math.round(n).toLocaleString("en-US"); + +// --- production routers and minimal lockers, 1 / 2 / 3 pools, fresh ERC20 pair +const legs = { + "Ekubo, Yul router": [1, 2, 3].map((n) => + tx( + ek, + `prod ekubo Yul router ${n} pool${n > 1 ? "s" : ""} A->B${n > 1 ? "->C" : ""}${n > 2 ? "->D" : ""}`, + ), + ), + "Ekubo, minimal locker": [1, 2, 3].map((n) => + tx( + ek, + `prod ekubo minimal locker ${n} pool${n > 1 ? "s" : ""} A->B${n > 1 ? "->C" : ""}${n > 2 ? "->D" : ""}`, + ), + ), + "v4, Universal Router": [ + tx(v4, "prod v4 Universal Router V4_SWAP 1 pool A->B (Permit2)"), + ], + "v4, minimal locker": [1, 2, 3].map((n) => + tx( + v4, + `prod v4 minimal locker ${n} pool${n > 1 ? "s" : ""} A->B${n > 1 ? "->C" : ""}${n > 2 ? "->D" : ""}`, + ), + ), + "v3, SwapRouter": [ + tx(v3, "prod v3 SwapRouter exactInputSingle 1 pool A->B"), + tx(v3, "prod v3 SwapRouter exactInput 2 pools A->B->C"), + tx(v3, "prod v3 SwapRouter exactInput 3 pools A->B->C->D"), + ], + "v3, minimal router": [1, 2, 3].map((n) => + tx( + v3, + `prod v3 minimal router ${n} pool${n > 1 ? "s" : ""} A->B${n > 1 ? "->C" : ""}${n > 2 ? "->D" : ""}`, + ), + ), +}; +console.log( + "Transaction gas (execution + 21,000 + calldata), fresh ERC20 pair, block", + ek["prod fork block number"], +); +for (const [k, v] of Object.entries(legs)) + console.log( + ` ${k.padEnd(24)} ${v.map(fmt).join(" / ")} routes per 60M block: ${v.map((g) => Math.floor(BLOCK_GAS / g)).join(" / ")}`, + ); +const E = legs["Ekubo, Yul router"][0], + V3 = legs["v3, SwapRouter"][0], + V4 = legs["v4, Universal Router"][0], + V4m = legs["v4, minimal locker"][0]; +console.log( + ` headline production: Ekubo ${fmt(E)} vs v3 ${fmt(V3)} (${pct(E, V3)}% less) vs v4 UR ${fmt(V4)} (${pct(E, V4)}% less); vs v4 minimal ${fmt(V4m)} (${pct(E, V4m)}% less)`, +); +const Em = legs["Ekubo, minimal locker"][0], + V3m = legs["v3, minimal router"][0]; +console.log( + ` like-for-like minimal: Ekubo ${fmt(Em)} vs v4 ${fmt(V4m)} (${pct(Em, V4m)}%) vs v3 ${fmt(V3m)} (${pct(Em, V3m)}%)`, +); + +// --- native ETH input +const nat = { + ekubo: tx(ek, "prod ekubo Yul router 1 pool ETH->B"), + v4: tx(v4, "prod v4 minimal locker 1 pool ETH->B"), + v3: tx( + v3, + "prod v3 SwapRouter exactInputSingle 1 pool ETH(wrapped in flight)->B", + ), +}; +console.log( + ` native ETH in: Ekubo ${fmt(nat.ekubo)} vs v4 minimal ${fmt(nat.v4)} (${pct(nat.ekubo, nat.v4)}%) vs v3 SwapRouter ${fmt(nat.v3)} (${pct(nat.ekubo, nat.v3)}%)`, +); + +// --- marginal cost per extra pool (minimal routers, transaction gas incl. calldata growth) and blend +const marg = Object.fromEntries( + Object.entries(legs) + .filter(([, v]) => v.length === 3) + .map(([k, v]) => [k, (v[2] - v[0]) / 2]), +); +console.log( + "Marginal transaction gas per extra pool:", + Object.entries(marg) + .map(([k, v]) => `${k} ${fmt(v)}`) + .join("; "), +); +const execMarg = { + ekuboYul: + (ek["prod ekubo Yul router 3 pools A->B->C->D"] - + ek["prod ekubo Yul router 1 pool A->B"]) / + 2, + ekuboMin: + (ek["prod ekubo minimal locker 3 pools A->B->C->D"] - + ek["prod ekubo minimal locker 1 pool A->B"]) / + 2, + v4Min: + (v4["prod v4 minimal locker 3 pools A->B->C->D"] - + v4["prod v4 minimal locker 1 pool A->B"]) / + 2, + v3Min: + (v3["prod v3 minimal router 3 pools A->B->C->D"] - + v3["prod v3 minimal router 1 pool A->B"]) / + 2, + v3SR: + (v3["prod v3 SwapRouter exactInput 3 pools A->B->C->D"] - + v3["prod v3 SwapRouter exactInputSingle 1 pool A->B"]) / + 2, +}; +console.log( + "Marginal execution gas per extra pool:", + Object.entries(execMarg) + .map(([k, v]) => `${k} ${fmt(v)}`) + .join("; "), +); + +const avg = month.receipts.avgPoolsPerSwapTx; +const blend = (p) => ({ + ekubo: + legs["Ekubo, minimal locker"][0] + (p - 1) * marg["Ekubo, minimal locker"], + v4: legs["v4, minimal locker"][0] + (p - 1) * marg["v4, minimal locker"], + v3: legs["v3, minimal router"][0] + (p - 1) * marg["v3, minimal router"], +}); +console.log( + `Blend (minimal routers), pools per swap -> Ekubo / v4 / v3 / saving vs v4 / vs v3`, +); +const rows = [avg, 1.0, 1.5, 2.0]; +const savings = {}; +for (const p of rows) { + const b = blend(p); + savings[p] = { v4: (b.v4 - b.ekubo) / b.v4, v3: (b.v3 - b.ekubo) / b.v3 }; + console.log( + ` ${p}${p === avg ? " (measured)" : ""}: ${fmt(b.ekubo)} / ${fmt(b.v4)} / ${fmt(b.v3)} / ${pct(b.ekubo, b.v4)}% / ${pct(b.ekubo, b.v3)}%`, + ); +} + +// --- pools per swap, 30-day sample +const r = month.receipts; +console.log( + `30-day sample: ${month.window.blocksRead} blocks, stride ${month.window.stride}, ${month.window.fromIso} .. ${month.window.toIso}, blocks ${month.window.fromBlock}..${month.window.toBlock}`, +); +console.log( + ` swap txs ${r.swapTxs.toLocaleString("en-US")}, avg ${avg} pools/swap tx, histogram`, + Object.entries(r.histogram) + .map(([k, v]) => `${k}: ${v.pct}%`) + .join(", "), + `gas share ${r.gasShare.pct}%`, +); +console.log( + " per family (txs / avg events of family per tx / avg total pools in those txs / gas share of window):", +); +for (const [f, s] of Object.entries(r.perFamily)) + console.log( + ` ${f.padEnd(14)} ${String(s.txs).padStart(7)} ${s.avgEventsOfThisFamilyPerTx} ${s.avgTotalPoolsPerTxTouchingFamily} ${((100 * Number(s.gasOfTxsTouchingFamily)) / Number(r.gasShare.allTxGas)).toFixed(2)}%`, + ); +if (month.daily) { + const d = Object.values(month.daily); + console.log( + ` daily avg pools/swap range ${Math.min(...d.map((x) => x.avgPoolsPerSwapTx))}..${Math.max(...d.map((x) => x.avgPoolsPerSwapTx))}, daily gas share range ${Math.min(...d.map((x) => x.swapTxGasSharePct))}%..${Math.max(...d.map((x) => x.swapTxGasSharePct))}%`, + ); +} +if (month.calldata) + console.log( + ` router-only (calldata-decoded) txs ${month.calldata.decodedTxs}, avg ${month.calldata.avgPoolsPerSwapTx}`, + ); + +// --- chain-wide scenarios: share of block gas freed = share x saving +const shares = [ + ["4.2%, measured DEX-router burn, lower bound", 0.042], + ["19.4%, ultrasound 'defi' bracket", 0.194], + [ + `${r.gasShare.pct}%, swap-event txs in the 30-day sample`, + r.gasShare.pct / 100, + ], + ["70%, stipulated AMM-trading upside (not measured)", 0.7], +]; +const cols = [ + ["headline production 1 pool", { v3: (V3 - E) / V3, v4: (V4 - E) / V4 }], + [`measured ${avg} pools`, savings[avg]], + ["2.0 pools", savings[2]], +]; +console.log( + "Scenarios (range = v3 saving .. v4 saving, share of block gas freed):", +); +for (const [label, sh] of shares) + console.log( + ` ${label.padEnd(52)} ` + + cols + .map( + ([c, s]) => + `${c}: ${(100 * sh * Math.min(s.v3, s.v4)).toFixed(1)}–${(100 * sh * Math.max(s.v3, s.v4)).toFixed(1)}%`, + ) + .join(" | "), + ); diff --git a/benchmarks/pools-per-swap/measure.mjs b/benchmarks/pools-per-swap/measure.mjs new file mode 100644 index 0000000..a469329 --- /dev/null +++ b/benchmarks/pools-per-swap/measure.mjs @@ -0,0 +1,747 @@ +#!/usr/bin/env node +// Measures how many pools a mainnet swap transaction touches. +// +// Two independent measurements over the same block window, both from a public +// JSON-RPC endpoint (no Dune / indexer needed): +// +// 1. receipts — eth_getBlockReceipts per block. Counts pool-level swap events +// per successful transaction (topic0 in the families below, plus +// Ekubo Core's topic-less 116-byte swap log). This covers every +// flow: routers, aggregators, MEV bundles, direct pool calls. Logs +// exist only for successful transactions, so failed fills are +// excluded by construction. +// 2. calldata — eth_getBlockByNumber(full txs). For transactions sent to the +// verified router contracts below, decodes the hop count from the +// calldata (Universal Router commands, SwapRouter02 / SwapRouter +// exactInput paths, 1inch unoswapN). Router flow only. +// +// Usage: node measure.mjs [--blocks N] [--stride S] [--to BLOCK] [--rpc URL[,URL...]] [--conc C] [--out FILE] +// +// --blocks N number of blocks to read (default 200) +// --stride S read every S-th block, so the N blocks span N*S blocks ending at --to +// (default 1 = consecutive). A one-month window at 12 s/block is 216,000 +// blocks; --blocks 21600 --stride 10 reads a uniform 10% sample of it. +// --rpc comma-separated JSON-RPC endpoints; requests rotate across them on +// failure. Endpoints must serve eth_getBlockReceipts for the whole window +// (publicnode returns null for blocks older than a few days). + +import { writeFileSync } from "node:fs"; + +const args = Object.fromEntries( + process.argv + .slice(2) + .map((a, i, arr) => (a.startsWith("--") ? [a.slice(2), arr[i + 1]] : null)) + .filter(Boolean), +); +const RPCS = ( + args.rpc ?? + "https://eth.drpc.org,https://mainnet.gateway.tenderly.co,https://eth.merkle.io,https://eth.rpc.blxrbdn.com" +).split(","); +const BLOCKS = Number(args.blocks ?? 200); +const STRIDE = Number(args.stride ?? 1); +const CONC = Number(args.conc ?? 4); +const OUT = args.out ?? "results.json"; + +// Event signatures. Every signature was read from the protocol's own source (repo path in +// the comment) and hashed with `cast keccak` (Foundry 1.8.3); none is memorized. +const FAMILIES = { + // Uniswap v3-core IUniswapV3PoolEvents: Swap(address,address,int256,int256,uint160,uint128,int24). + // Shared by SushiSwap v3 and KyberSwap Elastic (identical parameter types), so those land here too. + "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67": "univ3", + // Uniswap v2-core: Swap(address,uint256,uint256,uint256,uint256,address) — v2 and every fork (Sushi, PancakeSwap v2, Fraxswap, ...) + "0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822": + "univ2-family", + // curvefi/stableswap-ng CurveStableSwapNG.vy (and the classic plain/meta pools): TokenExchange(address,int128,uint256,int128,uint256) + "0x8b3e96f2b889fa771c53c981b40daf005f63f637f1869f707052d15a3dd97140": "curve", + // same, TokenExchangeUnderlying(address,int128,uint256,int128,uint256) + "0xd013ca23e77a65003c2c659c5442c00c805371b7fc1ebd4c206c41d1536bd90b": "curve", + // curvefi/curve-crypto-contract CurveCryptoSwap2ETH.vy (crypto v2 pools): TokenExchange(address,uint256,uint256,uint256,uint256) + "0xb2e76ae99761dc136e598d4a629bb347eccb9532a5f8bbd72e18467c3c34cc98": "curve", + // curvefi/tricrypto-ng CurveTricryptoOptimizedWETH.vy and twocrypto-ng Twocrypto.vy: + // TokenExchange(address,uint256,uint256,uint256,uint256,uint256,uint256) + "0x143f1f8e861fbdeddd5b46e844b7d3ac7b86a122f36e8c463859ee6811b1f29c": "curve", + // Uniswap v4-core IPoolManager: Swap(bytes32,address,int128,int128,uint160,uint128,int24,uint24) + "0x40e9cecb9f5f1f1c5b9c97dec2917b7ee92e57ba5563708daca94dd84ad7112f": "univ4", + // balancer-v2-monorepo pkg/interfaces/contracts/vault/IVault.sol: Swap(bytes32,address,address,uint256,uint256) + "0x2170c741c41531aec20e7c107c24eecfdd15e69c9bb0a8dd37b1840b9e0b207b": + "balancer-v2", + // balancer-v3-monorepo pkg/interfaces/contracts/vault/IVaultEvents.sol: + // Swap(address,address,address,uint256,uint256,uint256,uint256) + "0x0874b2d545cb271cdbda4e093020c452328b24af12382ed62c4d00f5c26709db": + "balancer-v3", + // maverickprotocol/maverick-v1-interfaces IPool.sol: Swap(address,address,bool,bool,uint256,uint256,int32) + "0x3b841dc9ab51e3104bda4f61b41e4271192d22cd19da5ee6e292dc8e2744f713": + "maverick-v1", + // maverickprotocol/v2-common IMaverickV2Pool.sol: PoolSwap(address,address,SwapParams,uint256,uint256) + // with SwapParams = (uint256 amount, bool tokenAIn, bool exactOutput, int32 tickLimit) + "0x103ed084e94a44c8f5f6ba8e3011507c41063177e29949083c439777d8d63f60": + "maverick-v2", + // pancakeswap/pancake-v3-contracts IPancakeV3PoolEvents.sol: + // Swap(address,address,int256,int256,uint160,uint128,int24,uint128,uint128) — differs from v3 by the two protocol-fee words + "0x19b47279256b2a23a1665c810c8d55a1758940ee09377d4f8d26497a3577dc83": + "pancake-v3", + // DODOEX/contractV2 DVMTrader.sol / DPPTrader.sol / DSPTrader.sol: DODOSwap(address,address,uint256,uint256,address,address) + "0xc2c0245e056d5fb095f04cd6373bc770802ebd1e6c918eb78fdef843cdb37b0f": "dodo", + // DODOEX/dodo-smart-contract Trader.sol (v1): SellBaseToken(address,uint256,uint256) + "0xd8648b6ac54162763c86fd54bf2005af8ecd2f9cb273a5775921fd7f91e17b2d": "dodo", + // same (v1): BuyBaseToken(address,uint256,uint256) + "0xe93ad76094f247c0dafc1c61adc2187de1ac2738f7a3b49cb20b2263420251a3": "dodo", + // bancorprotocol/contracts-v3 BancorNetwork.sol: + // TokensTraded(bytes32,address,address,uint256,uint256,uint256,uint256,uint256,address), one per hop + "0x5c02c2bb2d1d082317eb23916ca27b3e7c294398b60061a2ad54f1c3c018c318": + "bancor-v3", + // bancorprotocol/carbon-contracts Strategies.sol: TokensTraded(address,address,address,uint256,uint256,uint128,bool) + "0x95f3b01351225fea0e69a46f68b164c9dea10284f12cd4a907ce66510ab7af6a": + "bancor-carbon", + // bancorprotocol/contracts-solidity converter/interfaces/IConverter.sol (v2.1 converters): + // Conversion(address,address,address,uint256,uint256,int256) + "0x276856b36cbc45526a0ba64f44611557a2a8b68662c5388e9fe6d72e86e1c8cb": + "bancor-v2", + // Instadapp/fluid-contracts-public protocols/dex/poolT1/coreModule/events.sol: Swap(bool,uint256,uint256,address) + "0xdc004dbca4ef9c966218431ee5d9133d337ad018dd5b5c5493722803f75c64f7": "fluid", + // aerodrome-finance/contracts IPool.sol (Solidly-style pairs: Aerodrome, Velodrome, Solidly forks): + // Swap(address,address,uint256,uint256,uint256,uint256) — a different topic from the v2 family + "0xb3e2773606abfd36b5bd91394b3a54d1398336c65005baf7bf7a05efeffaf75b": + "solidly-style", +}; + +// Ekubo Core emits its swap record with `log0` (no topics) and exactly 116 bytes of data: +// 20-byte locker, 32-byte poolId, 32-byte balance update, 32-byte pool state +// (EkuboProtocol/evm-contracts v3.2.0 src/Core.sol, `log0(o, 116)`). Every other Core +// event is a normal topic-carrying `emit`, so "zero topics + 116 bytes from Core" is exact. +const EKUBO_CORE = "0x00000000000014aa86c5d3c41765bb24e11bd701"; +const EKUBO_SWAP_DATA_HEX_LEN = 2 + 116 * 2; + +// Router contracts. Each address is verified at startup: the deployed bytecode must +// contain the 4-byte selectors listed (a memorized address that fails this check aborts the run). +const ROUTERS = { + "0x66a9893cc07d91d95644aedd05d03f95e1dba8af": { + name: "Universal Router (v2, 2025)", + kind: "ur", + selectors: ["3593564c"], + }, + "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad": { + name: "Universal Router (v1.2, 2023)", + kind: "ur", + selectors: ["3593564c"], + }, + "0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45": { + name: "SwapRouter02", + kind: "sr02", + selectors: ["b858183f", "5ae401dc"], + }, + "0xe592427a0aece92de3edee1f18e0157c05861564": { + name: "SwapRouter (v3 classic)", + kind: "sr", + selectors: ["c04b8d59"], + }, + "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": { + name: "Uniswap v2 Router02", + kind: "v2r", + selectors: ["38ed1739"], + }, + "0x111111125421ca6dc452d289314280a0f8842a65": { + name: "1inch AggregationRouter v6", + kind: "oneinch", + selectors: ["07ed2379", "83800a8e"], + }, + // Third Universal Router deployment seen as a top gas consumer in the window; Sourcify + // full-matches it to UniswapRouter contracts/UniversalRouter.sol. + "0x4c82d1fbfe28c977cbb58d8c7ff8fcf9f70a2cca": { + name: "Universal Router (2026 deployment)", + kind: "ur", + selectors: ["3593564c"], + }, + // Ekubo's production Yul router has no ABI selectors (calldata is packed route data), + // so it is verified by the Core address embedded in its bytecode instead. Hop counts are + // not decoded from its calldata; Ekubo pools are counted from Core's own swap logs. + "0x7b2aa7ecc0b5936b7c52e6259a19c3ba557d0748": { + name: "Ekubo Yul router", + kind: "ekubo-yul", + selectors: [], + bytecodeContains: [EKUBO_CORE.slice(2)], + }, +}; + +let rpcCalls = 0; +let rpcRetries = 0; +let rpcSeq = 0; +async function rpc(method, params) { + const seed = rpcSeq++; + for (let attempt = 0; ; attempt++) { + const url = RPCS[(seed + attempt) % RPCS.length]; + try { + rpcCalls++; + if (attempt) rpcRetries++; + const res = await fetch(url, { + signal: AbortSignal.timeout(30000), + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }), + }); + const body = await res.json(); + if (body.error) + throw new Error(`${method}: ${JSON.stringify(body.error)}`); + if (body.result == null) throw new Error(`${method}: null result`); + return body.result; + } catch (e) { + if (attempt >= 8) throw e; + const wait = 500 * 2 ** attempt; + await new Promise((r) => setTimeout(r, wait)); + } + } +} + +// ---- minimal ABI helpers ----------------------------------------------------- +const word = (hex, i) => hex.slice(i * 64, i * 64 + 64); +const num = (hex, i) => Number(BigInt("0x" + word(hex, i))); +// dynamic `bytes` or `bytes[]` element at word index i inside `hex` (already stripped of selector) +function dynBytes(hex, base, i) { + const off = num(hex.slice(base * 2), i); + const start = base + off; + const len = Number(BigInt("0x" + hex.slice(start * 2, start * 2 + 64))); + return hex.slice(start * 2 + 64, start * 2 + 64 + len * 2); +} +function dynArray(hex, base, i) { + const off = num(hex.slice(base * 2), i); + const start = base + off; + const len = Number(BigInt("0x" + hex.slice(start * 2, start * 2 + 64))); + return { start: start + 32, len }; +} +function bytesArray(hex, base, i) { + const { start, len } = dynArray(hex, base, i); + const out = []; + for (let k = 0; k < len; k++) out.push(dynBytes(hex, start, k)); + return out; +} +const v3PathHops = (path) => (path.length / 2 - 20) / 23; // 20 + 23n bytes + +// Universal Router command bytes (Commands.sol), low 6 bits. +const UR = { + V3_SWAP_EXACT_IN: 0x00, + V3_SWAP_EXACT_OUT: 0x01, + V2_SWAP_EXACT_IN: 0x08, + V2_SWAP_EXACT_OUT: 0x09, + V4_SWAP: 0x10, +}; +// v4 router actions (Actions.sol) +const V4 = { + SWAP_EXACT_IN_SINGLE: 0x06, + SWAP_EXACT_IN: 0x07, + SWAP_EXACT_OUT_SINGLE: 0x08, + SWAP_EXACT_OUT: 0x09, +}; + +// Pools touched by one Universal Router command; null when the command is not a swap. +function urCommandPools(cmd, inp) { + if (cmd === UR.V3_SWAP_EXACT_IN || cmd === UR.V3_SWAP_EXACT_OUT) { + // (address recipient, uint256 amount, uint256 amountLimit, bytes path, bool payerIsUser) + return v3PathHops(dynBytes(inp, 0, 3)); + } + if (cmd === UR.V2_SWAP_EXACT_IN || cmd === UR.V2_SWAP_EXACT_OUT) { + // (address recipient, uint256 amount, uint256 amountLimit, address[] path, bool payerIsUser) + return dynArray(inp, 0, 3).len - 1; + } + if (cmd === UR.V4_SWAP) return v4SwapPools(inp); + return null; +} + +// V4_SWAP input: (bytes actions, bytes[] params) +function v4SwapPools(inp) { + const actions = dynBytes(inp, 0, 0); + const params = bytesArray(inp, 0, 1); + let pools = null; + for (let a = 0; a < actions.length / 2; a++) { + const n = v4ActionPools( + parseInt(actions.slice(a * 2, a * 2 + 2), 16), + params[a], + ); + if (n != null) pools = (pools ?? 0) + n; + } + return pools; +} + +function v4ActionPools(act, p) { + if (act === V4.SWAP_EXACT_IN_SINGLE || act === V4.SWAP_EXACT_OUT_SINGLE) + return 1; + if (act === V4.SWAP_EXACT_IN || act === V4.SWAP_EXACT_OUT) { + // abi.encode(ExactInputParams): one head word (offset) then struct: currency, path offset, amount, amountMin + const structStart = num(p, 0); + const pathOff = num(p.slice(structStart * 2), 1); + return num(p.slice((structStart + pathOff) * 2), 0); // PathKey[] length + } + return null; +} + +function decodeUR(input) { + const sel = input.slice(2, 10); + if (sel !== "3593564c" && sel !== "24856bc3") return null; + const hex = input.slice(10); + const commands = dynBytes(hex, 0, 0); + const inputs = bytesArray(hex, 0, 1); + let pools = null; + for (let k = 0; k < commands.length / 2; k++) { + const cmd = parseInt(commands.slice(k * 2, k * 2 + 2), 16) & 0x3f; + const n = urCommandPools(cmd, inputs[k]); + if (n != null) pools = (pools ?? 0) + n; + } + return pools; +} + +// SwapRouter02 / classic SwapRouter selectors -> how to read the pool count. +const SR_PATH = new Set([ + "b858183f", // SwapRouter02 exactInput((bytes path,address,uint256,uint256)) + "09b81346", // SwapRouter02 exactOutput + "c04b8d59", // SwapRouter exactInput((bytes,address,uint256,uint256,uint256)) + "f28c0498", // SwapRouter exactOutput +]); +const SR_SINGLE = new Set([ + "04e45aaf", // SwapRouter02 exactInputSingle + "5023b4df", // SwapRouter02 exactOutputSingle + "414bf389", // SwapRouter exactInputSingle + "db3e2198", // SwapRouter exactOutputSingle +]); +const SR_V2 = new Set([ + "472b43f3", // SwapRouter02 swapExactTokensForTokens(uint256,uint256,address[],address) + "42712a67", // SwapRouter02 swapTokensForExactTokens +]); + +function decodeSwapRouterCall(callHex) { + const sel = callHex.slice(0, 8); + const hex = callHex.slice(8); + if (SR_PATH.has(sel)) return v3PathHops(dynBytes(hex, num(hex, 0), 0)); + if (SR_SINGLE.has(sel)) return 1; + if (SR_V2.has(sel)) return dynArray(hex, 0, 2).len - 1; + return null; +} + +function decodeSwapRouter(input) { + const sel = input.slice(2, 10); + const hex = input.slice(10); + let calls; + if (sel === "5ae401dc" || sel === "1f0464d1") + calls = bytesArray(hex, 0, 1); // multicall(uint256|bytes32, bytes[]) + else if (sel === "ac9650d8") + calls = bytesArray(hex, 0, 0); // multicall(bytes[]) + else calls = [input.slice(2)]; + let pools = 0; + let found = false; + for (const c of calls) { + const n = decodeSwapRouterCall(c); + if (n != null) { + pools += n; + found = true; + } + } + return found ? pools : null; +} + +function decodeV2Router(input) { + const sel = input.slice(2, 10); + const hex = input.slice(10); + // swapExact*/swap*ForExact* on Router02: path is arg index 2 (amountIn, amountOutMin, path, to, deadline) + // or index 1 for the payable ETH-input variants (amountOutMin, path, to, deadline) + const idx2 = new Set([ + "38ed1739", + "8803dbee", + "18cbafe5", + "4a25d94a", + "5c11d795", + "791ac947", + ]); + const idx1 = new Set(["7ff36ab5", "fb3bdb41", "b6f9de95"]); + if (idx2.has(sel)) return dynArray(hex, 0, 2).len - 1; + if (idx1.has(sel)) return dynArray(hex, 0, 1).len - 1; + return null; +} + +function decodeOneInch(input) { + const sel = input.slice(2, 10); + const table = { + "83800a8e": 1, + "8770ba91": 2, + 19367472: 3, + a76dfc3b: 1, + "89af926a": 2, + "188ac35d": 3, + }; + if (sel in table) return table[sel]; + if (sel === "07ed2379") return "opaque"; // generic swap(executor, desc, data): hop count is inside executor data + return null; +} + +function decodeRouterTx(kind, input) { + try { + if (kind === "ekubo-yul") return "opaque"; // packed route calldata, not decoded here + if (kind === "ur") return decodeUR(input); + if (kind === "sr02" || kind === "sr") return decodeSwapRouter(input); + if (kind === "v2r") return decodeV2Router(input); + if (kind === "oneinch") return decodeOneInch(input); + } catch { + return null; + } + return null; +} + +// ---- main --------------------------------------------------------------------- +const latest = Number(BigInt(await rpc("eth_blockNumber", []))); +const toBlock = Number(args.to ?? latest - 12); // stay behind the head to avoid reorgs +const fromBlock = toBlock - (BLOCKS - 1) * STRIDE; + +for (const [addr, r] of Object.entries(ROUTERS)) { + const code = await rpc("eth_getCode", [addr, "latest"]); + for (const s of [...r.selectors, ...(r.bytecodeContains ?? [])]) { + if (!code.includes(s)) + throw new Error(`${r.name} at ${addr}: bytecode lacks ${s}`); + } + r.codeBytes = (code.length - 2) / 2; +} +const ekuboCoreCodeBytes = + ((await rpc("eth_getCode", [EKUBO_CORE, "latest"])).length - 2) / 2; +if (!ekuboCoreCodeBytes) throw new Error("Ekubo Core has no code"); + +const receipts = { + txs: 0, + failedSwapTxs: 0, + hist: {}, + perFamily: {}, + gasUsedSwapTxs: 0n, + gasUsedAll: 0n, + sumPools: 0, + ekuboCoreOtherTopiclessLogs: 0, // sanity: Core topic-less logs that are not 116 bytes (expected 0) + allTxs: 0, + successfulTxs: 0, +}; +const calldata = { + txs: 0, + hist: {}, + perRouter: {}, + sumPools: 0, + opaque: 0, + undecoded: 0, + failed: 0, + crossCheck: { compared: 0, equal: 0, calldataLower: 0, calldataHigher: 0 }, + undecodedSelectors: {}, +}; +const bump = (h, k) => (h[k] = (h[k] ?? 0) + 1); +const fam = (name) => + (receipts.perFamily[name] ??= { + txs: 0, + events: 0, + poolsInThoseTxs: 0, + gas: 0n, + emitters: {}, + }); +const routerStat = (name) => + (calldata.perRouter[name] ??= { + txs: 0, + sumPools: 0, + hist: {}, + opaque: 0, + undecoded: 0, + failed: 0, + crossCheck: { compared: 0, equal: 0, calldataLower: 0, calldataHigher: 0 }, + mismatchSamples: [], + }); + +let firstTs, lastTs; +const blocks = Array.from({ length: BLOCKS }, (_, i) => fromBlock + i * STRIDE); +// Per-UTC-day buckets, to show how the averages move across the window's gas and market regimes. +const daily = {}; +const dayStat = (ts) => + (daily[new Date(ts * 1000).toISOString().slice(0, 10)] ??= { + blocks: 0, + swapTxs: 0, + sumPools: 0, + gasSwapTxs: 0n, + gasAll: 0n, + }); +let next = 0; +// Count pool-level swap events in one receipt; returns { counts by family, total }. +function swapEventsIn(receipt) { + const counts = {}; + const emitters = {}; + let total = 0; + for (const log of receipt.logs) { + let f; + if (log.topics.length === 0) { + if (log.address.toLowerCase() !== EKUBO_CORE) continue; + if (log.data.length !== EKUBO_SWAP_DATA_HEX_LEN) { + receipts.ekuboCoreOtherTopiclessLogs++; + continue; + } + f = "ekubo"; + } else { + f = FAMILIES[log.topics[0]]; + if (!f) continue; + } + counts[f] = (counts[f] ?? 0) + 1; + (emitters[f] ??= {})[log.address.toLowerCase()] = 1; + total++; + } + return { counts, emitters, total }; +} + +// Measurement 1: tally one block's receipts. Returns tx hash -> pool count for the cross-check. +function tallyReceipts(rcpts, day) { + const eventPools = new Map(); + day.blocks++; + for (const r of rcpts) { + receipts.gasUsedAll += BigInt(r.gasUsed); + day.gasAll += BigInt(r.gasUsed); + receipts.allTxs++; + if (r.status === "0x1") receipts.successfulTxs++; + const { counts, emitters, total } = swapEventsIn(r); + if (!total) continue; + if (r.status !== "0x1") { + receipts.failedSwapTxs++; // cannot happen (failed txs emit no logs) — kept as a sanity counter + continue; + } + receipts.txs++; + receipts.sumPools += total; + eventPools.set(r.transactionHash, total); + receipts.gasUsedSwapTxs += BigInt(r.gasUsed); + day.swapTxs++; + day.sumPools += total; + day.gasSwapTxs += BigInt(r.gasUsed); + bump(receipts.hist, Math.min(total, 4)); + for (const [f, c] of Object.entries(counts)) { + const s = fam(f); + s.txs++; + s.events += c; + s.poolsInThoseTxs += total; + s.gas += BigInt(r.gasUsed); + for (const a of Object.keys(emitters[f])) bump(s.emitters, a); + } + } + return eventPools; +} + +function crossCheck(n, ev, rs, hash) { + if (ev == null) return; + for (const cc of [calldata.crossCheck, rs.crossCheck]) { + cc.compared++; + if (ev === n) cc.equal++; + else if (n < ev) cc.calldataLower++; + else cc.calldataHigher++; + } + if (ev !== n && rs.mismatchSamples.length < 5) + rs.mismatchSamples.push({ hash, calldata: n, events: ev }); +} + +// Measurement 2: one successful transaction sent to a verified router. +function tallyRouterTx(router, tx, eventPools) { + const rs = routerStat(router.name); + const n = decodeRouterTx(router.kind, tx.input); + if (n === "opaque") { + calldata.opaque++; + rs.opaque++; + return; + } + if (n == null || n <= 0) { + calldata.undecoded++; + rs.undecoded++; + bump( + calldata.undecodedSelectors, + `${router.name}:${tx.input.slice(0, 10)}`, + ); + return; + } + calldata.txs++; + calldata.sumPools += n; + bump(calldata.hist, Math.min(n, 4)); + rs.txs++; + rs.sumPools += n; + bump(rs.hist, Math.min(n, 4)); + crossCheck(n, eventPools.get(tx.hash), rs, tx.hash); +} + +function tallyBlock(block, rcpts) { + const status = new Map( + rcpts.map((r) => [r.transactionHash, r.status === "0x1"]), + ); + const eventPools = tallyReceipts( + rcpts, + dayStat(Number(BigInt(block.timestamp))), + ); + for (const tx of block.transactions) { + const router = ROUTERS[(tx.to ?? "").toLowerCase()]; + if (!router) continue; + if (!status.get(tx.hash)) { + // keep the same population as the receipts measurement: successful txs only + calldata.failed++; + routerStat(router.name).failed++; + continue; + } + tallyRouterTx(router, tx, eventPools); + } +} + +async function worker() { + while (next < blocks.length) { + const bn = blocks[next++]; + const tag = "0x" + bn.toString(16); + let block, rcpts; + for (let attempt = 0; ; attempt++) { + [block, rcpts] = await Promise.all([ + rpc("eth_getBlockByNumber", [tag, true]), + rpc("eth_getBlockReceipts", [tag]), + ]); + // an endpoint that serves a block but not its receipts (or a partial set) is rejected + if ( + Array.isArray(rcpts) && + rcpts.length === block.transactions.length && + rcpts.every((r, i) => r.transactionHash === block.transactions[i].hash) + ) + break; + if (attempt >= 8) + throw new Error(`block ${bn}: receipts do not match block`); + rpcRetries++; + await new Promise((r) => setTimeout(r, 500 * 2 ** attempt)); + } + const ts = Number(BigInt(block.timestamp)); + if (bn === fromBlock) firstTs = ts; + if (bn === toBlock) lastTs = ts; + tallyBlock(block, rcpts); + const idx = (bn - fromBlock) / STRIDE; + if (idx % 200 === 0) + process.stderr.write(`block ${bn} (${idx + 1}/${BLOCKS})\n`); + } +} +await Promise.all(Array.from({ length: CONC }, worker)); + +const pct = (n, d) => (d ? +((100 * n) / d).toFixed(2) : 0); +const histShare = (h, n) => + Object.fromEntries( + [1, 2, 3, 4].map((k) => [ + k === 4 ? "4+" : String(k), + { txs: h[k] ?? 0, pct: pct(h[k] ?? 0, n) }, + ]), + ); + +const out = { + rpc: RPCS, + window: { + fromBlock, + toBlock, + stride: STRIDE, + blocksRead: BLOCKS, + blocksSpanned: toBlock - fromBlock + 1, + blocks: BLOCKS, + fromTimestamp: firstTs, + toTimestamp: lastTs, + fromIso: new Date(firstTs * 1000).toISOString(), + toIso: new Date(lastTs * 1000).toISOString(), + }, + rpcCalls, + rpcRetries, + families: FAMILIES, + daily: Object.fromEntries( + Object.entries(daily) + .sort() + .map(([d, s]) => [ + d, + { + blocks: s.blocks, + swapTxs: s.swapTxs, + avgPoolsPerSwapTx: +(s.sumPools / (s.swapTxs || 1)).toFixed(3), + swapTxGasSharePct: s.gasAll + ? +((10000n * s.gasSwapTxs) / s.gasAll).toString() / 100 + : 0, + }, + ]), + ), + ekuboCore: { + address: EKUBO_CORE, + codeBytes: ekuboCoreCodeBytes, + swapLog: + "log0, no topics, 116 bytes of data (locker, poolId, balanceUpdate, stateAfter)", + }, + routers: Object.fromEntries( + Object.entries(ROUTERS).map(([a, r]) => [ + a, + { + name: r.name, + kind: r.kind, + verifiedSelectors: r.selectors, + verifiedBytecodeContains: r.bytecodeContains ?? [], + codeBytes: r.codeBytes, + }, + ]), + ), + receipts: { + method: + "eth_getBlockReceipts; successful txs with >=1 pool-level swap event; pools = number of such events in the tx", + swapTxs: receipts.txs, + sumPools: receipts.sumPools, + failedSwapTxsSeen: receipts.failedSwapTxs, + avgPoolsPerSwapTx: +(receipts.sumPools / receipts.txs).toFixed(3), + histogram: histShare(receipts.hist, receipts.txs), + ekuboCoreOtherTopiclessLogs: receipts.ekuboCoreOtherTopiclessLogs, + allTxs: receipts.allTxs, + successfulTxs: receipts.successfulTxs, + perFamily: Object.fromEntries( + Object.entries(receipts.perFamily) + .sort((a, b) => b[1].txs - a[1].txs) + .map(([f, s]) => [ + f, + { + txs: s.txs, + events: s.events, + avgEventsOfThisFamilyPerTx: +(s.events / s.txs).toFixed(3), + avgTotalPoolsPerTxTouchingFamily: +( + s.poolsInThoseTxs / s.txs + ).toFixed(3), + gasOfTxsTouchingFamily: s.gas.toString(), + // distinct emitting contracts and the busiest ones (txs in which each emitted) + emitters: Object.keys(s.emitters).length, + topEmitters: Object.fromEntries( + Object.entries(s.emitters) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5), + ), + }, + ]), + ), + familiesWithNoEvents: [...new Set(Object.values(FAMILIES)), "ekubo"].filter( + (f) => !receipts.perFamily[f], + ), + gasShare: { + swapTxGas: receipts.gasUsedSwapTxs.toString(), + allTxGas: receipts.gasUsedAll.toString(), + pct: pct(Number(receipts.gasUsedSwapTxs), Number(receipts.gasUsedAll)), + }, + }, + calldata: { + method: + "eth_getBlockByNumber(full); successful txs to the verified routers; pools decoded from calldata", + decodedTxs: calldata.txs, + sumPools: calldata.sumPools, + avgPoolsPerSwapTx: +(calldata.sumPools / calldata.txs).toFixed(3), + histogram: histShare(calldata.hist, calldata.txs), + opaqueTxs: calldata.opaque, + undecodedTxs: calldata.undecoded, + failedTxs: calldata.failed, + crossCheckAgainstReceipts: calldata.crossCheck, + undecodedSelectors: Object.fromEntries( + Object.entries(calldata.undecodedSelectors).sort((a, b) => b[1] - a[1]), + ), + perRouter: Object.fromEntries( + Object.entries(calldata.perRouter).map(([n, s]) => [ + n, + { + decodedTxs: s.txs, + avgPools: s.txs ? +(s.sumPools / s.txs).toFixed(3) : null, + histogram: histShare(s.hist, s.txs), + opaque: s.opaque, + undecoded: s.undecoded, + failed: s.failed, + crossCheckAgainstReceipts: s.crossCheck, + mismatchSamples: s.mismatchSamples, + }, + ]), + ), + }, +}; +writeFileSync(OUT, JSON.stringify(out, null, 2) + "\n"); +console.log(JSON.stringify(out, null, 2)); diff --git a/benchmarks/pools-per-swap/results-30d.json b/benchmarks/pools-per-swap/results-30d.json new file mode 100644 index 0000000..91fe4e2 --- /dev/null +++ b/benchmarks/pools-per-swap/results-30d.json @@ -0,0 +1,972 @@ +{ + "rpc": [ + "https://eth.drpc.org", + "https://mainnet.gateway.tenderly.co", + "https://eth.merkle.io", + "https://eth.rpc.blxrbdn.com" + ], + "window": { + "fromBlock": 25783810, + "toBlock": 25999800, + "stride": 10, + "blocksRead": 21600, + "blocksSpanned": 215991, + "blocks": 21600, + "fromTimestamp": 1787078207, + "toTimestamp": 1789679339, + "fromIso": "2026-08-18T18:36:47.000Z", + "toIso": "2026-09-17T21:08:59.000Z" + }, + "rpcCalls": 54013, + "rpcRetries": 10803, + "families": { + "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67": "univ3", + "0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822": "univ2-family", + "0x8b3e96f2b889fa771c53c981b40daf005f63f637f1869f707052d15a3dd97140": "curve", + "0xd013ca23e77a65003c2c659c5442c00c805371b7fc1ebd4c206c41d1536bd90b": "curve", + "0xb2e76ae99761dc136e598d4a629bb347eccb9532a5f8bbd72e18467c3c34cc98": "curve", + "0x143f1f8e861fbdeddd5b46e844b7d3ac7b86a122f36e8c463859ee6811b1f29c": "curve", + "0x40e9cecb9f5f1f1c5b9c97dec2917b7ee92e57ba5563708daca94dd84ad7112f": "univ4", + "0x2170c741c41531aec20e7c107c24eecfdd15e69c9bb0a8dd37b1840b9e0b207b": "balancer-v2", + "0x0874b2d545cb271cdbda4e093020c452328b24af12382ed62c4d00f5c26709db": "balancer-v3", + "0x3b841dc9ab51e3104bda4f61b41e4271192d22cd19da5ee6e292dc8e2744f713": "maverick-v1", + "0x103ed084e94a44c8f5f6ba8e3011507c41063177e29949083c439777d8d63f60": "maverick-v2", + "0x19b47279256b2a23a1665c810c8d55a1758940ee09377d4f8d26497a3577dc83": "pancake-v3", + "0xc2c0245e056d5fb095f04cd6373bc770802ebd1e6c918eb78fdef843cdb37b0f": "dodo", + "0xd8648b6ac54162763c86fd54bf2005af8ecd2f9cb273a5775921fd7f91e17b2d": "dodo", + "0xe93ad76094f247c0dafc1c61adc2187de1ac2738f7a3b49cb20b2263420251a3": "dodo", + "0x5c02c2bb2d1d082317eb23916ca27b3e7c294398b60061a2ad54f1c3c018c318": "bancor-v3", + "0x95f3b01351225fea0e69a46f68b164c9dea10284f12cd4a907ce66510ab7af6a": "bancor-carbon", + "0x276856b36cbc45526a0ba64f44611557a2a8b68662c5388e9fe6d72e86e1c8cb": "bancor-v2", + "0xdc004dbca4ef9c966218431ee5d9133d337ad018dd5b5c5493722803f75c64f7": "fluid", + "0xb3e2773606abfd36b5bd91394b3a54d1398336c65005baf7bf7a05efeffaf75b": "solidly-style" + }, + "daily": { + "2026-08-18": { + "blocks": 162, + "swapTxs": 3093, + "avgPoolsPerSwapTx": 1.716, + "swapTxGasSharePct": 20.25 + }, + "2026-08-19": { + "blocks": 718, + "swapTxs": 21436, + "avgPoolsPerSwapTx": 1.865, + "swapTxGasSharePct": 32.76 + }, + "2026-08-20": { + "blocks": 717, + "swapTxs": 23867, + "avgPoolsPerSwapTx": 1.801, + "swapTxGasSharePct": 34.02 + }, + "2026-08-21": { + "blocks": 718, + "swapTxs": 27266, + "avgPoolsPerSwapTx": 1.801, + "swapTxGasSharePct": 39.43 + }, + "2026-08-22": { + "blocks": 717, + "swapTxs": 25728, + "avgPoolsPerSwapTx": 1.809, + "swapTxGasSharePct": 38.11 + }, + "2026-08-23": { + "blocks": 718, + "swapTxs": 28203, + "avgPoolsPerSwapTx": 1.831, + "swapTxGasSharePct": 41.03 + }, + "2026-08-24": { + "blocks": 718, + "swapTxs": 23088, + "avgPoolsPerSwapTx": 1.785, + "swapTxGasSharePct": 34.81 + }, + "2026-08-25": { + "blocks": 717, + "swapTxs": 25345, + "avgPoolsPerSwapTx": 1.719, + "swapTxGasSharePct": 34.51 + }, + "2026-08-26": { + "blocks": 717, + "swapTxs": 25078, + "avgPoolsPerSwapTx": 1.64, + "swapTxGasSharePct": 33.7 + }, + "2026-08-27": { + "blocks": 718, + "swapTxs": 23151, + "avgPoolsPerSwapTx": 1.68, + "swapTxGasSharePct": 32.74 + }, + "2026-08-28": { + "blocks": 718, + "swapTxs": 23946, + "avgPoolsPerSwapTx": 1.692, + "swapTxGasSharePct": 32.87 + }, + "2026-08-29": { + "blocks": 717, + "swapTxs": 17916, + "avgPoolsPerSwapTx": 1.671, + "swapTxGasSharePct": 24.82 + }, + "2026-08-30": { + "blocks": 718, + "swapTxs": 23422, + "avgPoolsPerSwapTx": 1.69, + "swapTxGasSharePct": 33.03 + }, + "2026-08-31": { + "blocks": 717, + "swapTxs": 21310, + "avgPoolsPerSwapTx": 1.666, + "swapTxGasSharePct": 38.55 + }, + "2026-09-01": { + "blocks": 717, + "swapTxs": 23334, + "avgPoolsPerSwapTx": 1.672, + "swapTxGasSharePct": 32.45 + }, + "2026-09-02": { + "blocks": 718, + "swapTxs": 21248, + "avgPoolsPerSwapTx": 1.713, + "swapTxGasSharePct": 31.01 + }, + "2026-09-03": { + "blocks": 717, + "swapTxs": 20996, + "avgPoolsPerSwapTx": 1.779, + "swapTxGasSharePct": 31.4 + }, + "2026-09-04": { + "blocks": 717, + "swapTxs": 21678, + "avgPoolsPerSwapTx": 1.746, + "swapTxGasSharePct": 32.78 + }, + "2026-09-05": { + "blocks": 718, + "swapTxs": 20022, + "avgPoolsPerSwapTx": 1.755, + "swapTxGasSharePct": 29.55 + }, + "2026-09-06": { + "blocks": 718, + "swapTxs": 21348, + "avgPoolsPerSwapTx": 1.779, + "swapTxGasSharePct": 32.22 + }, + "2026-09-07": { + "blocks": 716, + "swapTxs": 21942, + "avgPoolsPerSwapTx": 1.795, + "swapTxGasSharePct": 34.59 + }, + "2026-09-08": { + "blocks": 717, + "swapTxs": 21679, + "avgPoolsPerSwapTx": 1.731, + "swapTxGasSharePct": 31.85 + }, + "2026-09-09": { + "blocks": 718, + "swapTxs": 20183, + "avgPoolsPerSwapTx": 1.724, + "swapTxGasSharePct": 31.21 + }, + "2026-09-10": { + "blocks": 717, + "swapTxs": 19255, + "avgPoolsPerSwapTx": 1.691, + "swapTxGasSharePct": 29.16 + }, + "2026-09-11": { + "blocks": 717, + "swapTxs": 20262, + "avgPoolsPerSwapTx": 1.835, + "swapTxGasSharePct": 31.73 + }, + "2026-09-12": { + "blocks": 718, + "swapTxs": 19723, + "avgPoolsPerSwapTx": 1.831, + "swapTxGasSharePct": 32.61 + }, + "2026-09-13": { + "blocks": 718, + "swapTxs": 18625, + "avgPoolsPerSwapTx": 1.774, + "swapTxGasSharePct": 29.16 + }, + "2026-09-14": { + "blocks": 717, + "swapTxs": 19998, + "avgPoolsPerSwapTx": 1.701, + "swapTxGasSharePct": 28.48 + }, + "2026-09-15": { + "blocks": 717, + "swapTxs": 21488, + "avgPoolsPerSwapTx": 1.787, + "swapTxGasSharePct": 32.66 + }, + "2026-09-16": { + "blocks": 718, + "swapTxs": 21706, + "avgPoolsPerSwapTx": 1.703, + "swapTxGasSharePct": 30.78 + }, + "2026-09-17": { + "blocks": 632, + "swapTxs": 18390, + "avgPoolsPerSwapTx": 1.75, + "swapTxGasSharePct": 30.33 + } + }, + "ekuboCore": { + "address": "0x00000000000014aa86c5d3c41765bb24e11bd701", + "codeBytes": 18797, + "swapLog": "log0, no topics, 116 bytes of data (locker, poolId, balanceUpdate, stateAfter)" + }, + "routers": { + "0x66a9893cc07d91d95644aedd05d03f95e1dba8af": { + "name": "Universal Router (v2, 2025)", + "kind": "ur", + "verifiedSelectors": [ + "3593564c" + ], + "verifiedBytecodeContains": [], + "codeBytes": 19499 + }, + "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad": { + "name": "Universal Router (v1.2, 2023)", + "kind": "ur", + "verifiedSelectors": [ + "3593564c" + ], + "verifiedBytecodeContains": [], + "codeBytes": 17958 + }, + "0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45": { + "name": "SwapRouter02", + "kind": "sr02", + "verifiedSelectors": [ + "b858183f", + "5ae401dc" + ], + "verifiedBytecodeContains": [], + "codeBytes": 24497 + }, + "0xe592427a0aece92de3edee1f18e0157c05861564": { + "name": "SwapRouter (v3 classic)", + "kind": "sr", + "verifiedSelectors": [ + "c04b8d59" + ], + "verifiedBytecodeContains": [], + "codeBytes": 12070 + }, + "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": { + "name": "Uniswap v2 Router02", + "kind": "v2r", + "verifiedSelectors": [ + "38ed1739" + ], + "verifiedBytecodeContains": [], + "codeBytes": 21943 + }, + "0x111111125421ca6dc452d289314280a0f8842a65": { + "name": "1inch AggregationRouter v6", + "kind": "oneinch", + "verifiedSelectors": [ + "07ed2379", + "83800a8e" + ], + "verifiedBytecodeContains": [], + "codeBytes": 24294 + }, + "0x4c82d1fbfe28c977cbb58d8c7ff8fcf9f70a2cca": { + "name": "Universal Router (2026 deployment)", + "kind": "ur", + "verifiedSelectors": [ + "3593564c" + ], + "verifiedBytecodeContains": [], + "codeBytes": 24546 + }, + "0x7b2aa7ecc0b5936b7c52e6259a19c3ba557d0748": { + "name": "Ekubo Yul router", + "kind": "ekubo-yul", + "verifiedSelectors": [], + "verifiedBytecodeContains": [ + "00000000000014aa86c5d3c41765bb24e11bd701" + ], + "codeBytes": 2878 + } + }, + "receipts": { + "method": "eth_getBlockReceipts; successful txs with >=1 pool-level swap event; pools = number of such events in the tx", + "swapTxs": 664726, + "sumPools": 1161778, + "failedSwapTxsSeen": 0, + "avgPoolsPerSwapTx": 1.748, + "histogram": { + "1": { + "txs": 459356, + "pct": 69.1 + }, + "2": { + "txs": 106197, + "pct": 15.98 + }, + "3": { + "txs": 49247, + "pct": 7.41 + }, + "4+": { + "txs": 49926, + "pct": 7.51 + } + }, + "ekuboCoreOtherTopiclessLogs": 0, + "allTxs": 5724722, + "successfulTxs": 5643974, + "perFamily": { + "univ3": { + "txs": 327953, + "events": 435259, + "avgEventsOfThisFamilyPerTx": 1.327, + "avgTotalPoolsPerTxTouchingFamily": 2.101, + "gasOfTxsTouchingFamily": "119065061543", + "emitters": 5232, + "topEmitters": { + "0xe0554a476a092703abdb3ef35c80e0d76d32939f": 61019, + "0xc7bbec68d12a0d1830360f8ec58fa599ba1b0e9b": 37879, + "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640": 21193, + "0x11b815efb8f581194ae79006d24e0d814b7697f6": 10039, + "0xfaa318479b7755b2dbfdd34dc306cb28b420ad12": 8245 + } + }, + "univ4": { + "txs": 237383, + "events": 357639, + "avgEventsOfThisFamilyPerTx": 1.507, + "avgTotalPoolsPerTxTouchingFamily": 2.493, + "gasOfTxsTouchingFamily": "100861794702", + "emitters": 1, + "topEmitters": { + "0x000000000004444c5dc75cb358380d2e3de08a90": 237383 + } + }, + "univ2-family": { + "txs": 178611, + "events": 221663, + "avgEventsOfThisFamilyPerTx": 1.241, + "avgTotalPoolsPerTxTouchingFamily": 2.212, + "gasOfTxsTouchingFamily": "61822705206", + "emitters": 12930, + "topEmitters": { + "0x76a411f14a704099ba476ce8dffc288a53295218": 4028, + "0xc56c9d4aee66e16aa4a31a710a6d2fc720a626b6": 3114, + "0x69c7bd26512f52bf6f76fab834140d13dda673ca": 2939, + "0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc": 2903, + "0x52c77b0cb827afbad022e6d6caf2c44452edbc39": 2751 + } + }, + "curve": { + "txs": 37104, + "events": 51606, + "avgEventsOfThisFamilyPerTx": 1.391, + "avgTotalPoolsPerTxTouchingFamily": 4.086, + "gasOfTxsTouchingFamily": "29977793714", + "emitters": 583, + "topEmitters": { + "0x35c9a4dae1ff05788f24b5b32721d89340cbb636": 6071, + "0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14": 5317, + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b": 3494, + "0xd51a44d3fae010294c616388b506acda1bfaae46": 3293, + "0x4f493b7de8aac7d55f71853688b1f7c8f0243c85": 1376 + } + }, + "pancake-v3": { + "txs": 30611, + "events": 34890, + "avgEventsOfThisFamilyPerTx": 1.14, + "avgTotalPoolsPerTxTouchingFamily": 4.299, + "gasOfTxsTouchingFamily": "20991198643", + "emitters": 200, + "topEmitters": { + "0x1445f32d1a74872ba41f3d8cf4022e9996120b31": 13953, + "0xacdb27b266142223e1e676841c1e809255fc6d07": 8512, + "0x6ca298d2983ab03aa1da7679389d955a4efee15c": 3579, + "0x1ac1a8feaaea1900c4166deeed0c11cc10669d36": 2408, + "0xbabe4490da9bcf0bbf062c40112aea2109d6ba7f": 1025 + } + }, + "ekubo": { + "txs": 21419, + "events": 24161, + "avgEventsOfThisFamilyPerTx": 1.128, + "avgTotalPoolsPerTxTouchingFamily": 3.346, + "gasOfTxsTouchingFamily": "14424619848", + "emitters": 1, + "topEmitters": { + "0x00000000000014aa86c5d3c41765bb24e11bd701": 21419 + } + }, + "fluid": { + "txs": 8789, + "events": 9481, + "avgEventsOfThisFamilyPerTx": 1.079, + "avgTotalPoolsPerTxTouchingFamily": 3.94, + "gasOfTxsTouchingFamily": "7517421114", + "emitters": 35, + "topEmitters": { + "0x667701e51b4d1ca244f17c78f7ab8744b4c99f9b": 3812, + "0x836951eb21f3df98273517b7249dceff270d34bf": 2588, + "0xea734b615888c669667038d11950f44b177f15c0": 748, + "0x3c0441b42195f4ad6aa9a0978e06096ea616cda7": 652, + "0x0b1a513ee24972daef112bc777a5610d4325c9e7": 211 + } + }, + "dodo": { + "txs": 7846, + "events": 8405, + "avgEventsOfThisFamilyPerTx": 1.071, + "avgTotalPoolsPerTxTouchingFamily": 4.457, + "gasOfTxsTouchingFamily": "7053994957", + "emitters": 506, + "topEmitters": { + "0x04571c32a4e1c5f39bc3a238cb95b215058c432c": 3836, + "0x3058ef90929cb8180174d74c507176cca6835d73": 924, + "0xc9f93163c99695c6526b799ebca2207fdf7d61ad": 409, + "0x201939f5c74f5fd3beef1cf03a9cfcc75beba9d9": 155, + "0x46cf01f14ff6f8c7ef4d6a6b139b1c3d45af9289": 90 + } + }, + "balancer-v2": { + "txs": 5584, + "events": 5934, + "avgEventsOfThisFamilyPerTx": 1.063, + "avgTotalPoolsPerTxTouchingFamily": 4.23, + "gasOfTxsTouchingFamily": "3988451309", + "emitters": 2, + "topEmitters": { + "0xba12222222228d8ba445958a75a0704d566bf2c8": 4492, + "0xd315a9c38ec871068fec378e4ce78af528c76293": 1094 + } + }, + "balancer-v3": { + "txs": 3987, + "events": 4306, + "avgEventsOfThisFamilyPerTx": 1.08, + "avgTotalPoolsPerTxTouchingFamily": 4.385, + "gasOfTxsTouchingFamily": "3438367528", + "emitters": 3, + "topEmitters": { + "0xba1333333333a1ba1108e8412f11850a5c319ba9": 3863, + "0x955244edc797a1c1b04134b600f819ac23c76081": 121, + "0x21e64c2a1c953a4202433ad584fe8eef283b56dd": 6 + } + }, + "maverick-v2": { + "txs": 2591, + "events": 2707, + "avgEventsOfThisFamilyPerTx": 1.045, + "avgTotalPoolsPerTxTouchingFamily": 4.116, + "gasOfTxsTouchingFamily": "2176138301", + "emitters": 18, + "topEmitters": { + "0x025a13752ec1fb6c0e441e5d7b1678bb5f9ca62f": 1093, + "0x6061cf96aa9f02aa072b77211a863989ca070c78": 507, + "0xdf9c440111a17eafbecf5db37dd04c948e878878": 475, + "0x31373595f40ea48a7aab6cbcb0d377c6066e2dca": 212, + "0x5b6a0771c752e35b2ca2aff4f22a66b1598a2bc5": 156 + } + }, + "bancor-v2": { + "txs": 2359, + "events": 4359, + "avgEventsOfThisFamilyPerTx": 1.848, + "avgTotalPoolsPerTxTouchingFamily": 5.242, + "gasOfTxsTouchingFamily": "1609101112", + "emitters": 135, + "topEmitters": { + "0xe331821bc94187c2649e932810a60204699d45cb": 1278, + "0xd4d5298136b52ec439ebfb23fda2ec500de69c71": 327, + "0x93334ab8a64b24df7b2fd2132e0e087239a21b63": 314, + "0xcba47689202d31575bfa204efe70e2c83d29da4d": 294, + "0x079ca3f710599739a22673c2856202f90d3a8806": 250 + } + }, + "maverick-v1": { + "txs": 563, + "events": 589, + "avgEventsOfThisFamilyPerTx": 1.046, + "avgTotalPoolsPerTxTouchingFamily": 4.417, + "gasOfTxsTouchingFamily": "539029948", + "emitters": 13, + "topEmitters": { + "0x11a653ddfbb61e0feff5484919f06d9d254bf65f": 245, + "0x352b186090068eb35d532428676ce510e17ab581": 167, + "0xd0b2f5018b5d22759724af6d4281ac0b13266360": 49, + "0x6c6fc818b25df89a8ada8da5a43669023bad1f4c": 30, + "0xab1e2d29699146a2e07c3a1c8ad8cb914a536f7a": 29 + } + }, + "bancor-v3": { + "txs": 531, + "events": 556, + "avgEventsOfThisFamilyPerTx": 1.047, + "avgTotalPoolsPerTxTouchingFamily": 5.633, + "gasOfTxsTouchingFamily": "400607619", + "emitters": 1, + "topEmitters": { + "0xeef417e1d5cc832e619ae18d2f140de2999dd4fb": 531 + } + }, + "bancor-carbon": { + "txs": 218, + "events": 220, + "avgEventsOfThisFamilyPerTx": 1.009, + "avgTotalPoolsPerTxTouchingFamily": 2.844, + "gasOfTxsTouchingFamily": "136615766", + "emitters": 1, + "topEmitters": { + "0xc537e898cd774e2dcba3b14ea6f34c93d5ea45e1": 218 + } + }, + "solidly-style": { + "txs": 3, + "events": 3, + "avgEventsOfThisFamilyPerTx": 1, + "avgTotalPoolsPerTxTouchingFamily": 1.667, + "gasOfTxsTouchingFamily": "1207060", + "emitters": 2, + "topEmitters": { + "0xfe0c14b4e614f40bf58845b4b04ced17fd116532": 2, + "0x7565756feaec1cbf51dfe71e04720b13f1c5104d": 1 + } + } + }, + "familiesWithNoEvents": [], + "gasShare": { + "swapTxGas": "212552446940", + "allTxGas": "650468728416", + "pct": 32.68 + } + }, + "calldata": { + "method": "eth_getBlockByNumber(full); successful txs to the verified routers; pools decoded from calldata", + "decodedTxs": 135167, + "sumPools": 164766, + "avgPoolsPerSwapTx": 1.219, + "histogram": { + "1": { + "txs": 116216, + "pct": 85.98 + }, + "2": { + "txs": 14123, + "pct": 10.45 + }, + "3": { + "txs": 2440, + "pct": 1.81 + }, + "4+": { + "txs": 2388, + "pct": 1.77 + } + }, + "opaqueTxs": 8303, + "undecodedTxs": 4224, + "failedTxs": 8023, + "crossCheckAgainstReceipts": { + "compared": 135167, + "equal": 134178, + "calldataLower": 989, + "calldataHigher": 0 + }, + "undecodedSelectors": { + "1inch AggregationRouter v6:0xb68fb020": 2725, + "1inch AggregationRouter v6:0xc3cf8043": 336, + "Uniswap v2 Router02:0xf305d719": 306, + "Uniswap v2 Router02:0x02751cec": 207, + "Uniswap v2 Router02:0xbaa2abde": 105, + "Uniswap v2 Router02:0xaf2979eb": 102, + "Universal Router (v2, 2025):0x3593564c": 98, + "Universal Router (v2, 2025):0x24856bc3": 75, + "Uniswap v2 Router02:0xe8e33700": 73, + "1inch AggregationRouter v6:0x89e7c650": 50, + "SwapRouter02:0xac9650d8": 36, + "1inch AggregationRouter v6:0x0d2c7c16": 34, + "1inch AggregationRouter v6:0x5816d723": 26, + "1inch AggregationRouter v6:0xea76dddf": 11, + "1inch AggregationRouter v6:0xcc713a04": 10, + "1inch AggregationRouter v6:0xe2c95c82": 6, + "Universal Router (v1.2, 2023):0x3593564c": 5, + "1inch AggregationRouter v6:0x9fda64bd": 4, + "1inch AggregationRouter v6:0x56a75868": 4, + "Universal Router (2026 deployment):0x3593564c": 4, + "Uniswap v2 Router02:0xded9382a": 2, + "Uniswap v2 Router02:0x5b0d5984": 2, + "SwapRouter02:0xdf2ab5bb": 1, + "1inch AggregationRouter v6:0xf497df75": 1, + "Uniswap v2 Router02:0x2195995c": 1 + }, + "perRouter": { + "SwapRouter02": { + "decodedTxs": 57070, + "avgPools": 1.071, + "histogram": { + "1": { + "txs": 53099, + "pct": 93.04 + }, + "2": { + "txs": 3906, + "pct": 6.84 + }, + "3": { + "txs": 47, + "pct": 0.08 + }, + "4+": { + "txs": 18, + "pct": 0.03 + } + }, + "opaque": 0, + "undecoded": 37, + "failed": 922, + "crossCheckAgainstReceipts": { + "compared": 57070, + "equal": 57070, + "calldataLower": 0, + "calldataHigher": 0 + }, + "mismatchSamples": [] + }, + "Universal Router (2026 deployment)": { + "decodedTxs": 20867, + "avgPools": 1.884, + "histogram": { + "1": { + "txs": 11869, + "pct": 56.88 + }, + "2": { + "txs": 4827, + "pct": 23.13 + }, + "3": { + "txs": 2031, + "pct": 9.73 + }, + "4+": { + "txs": 2140, + "pct": 10.26 + } + }, + "opaque": 0, + "undecoded": 4, + "failed": 750, + "crossCheckAgainstReceipts": { + "compared": 20867, + "equal": 20611, + "calldataLower": 256, + "calldataHigher": 0 + }, + "mismatchSamples": [ + { + "hash": "0x36348e4fb73d8a5b8832bca0da4a328955f45f6295a47b7af518363ceadd5cc0", + "calldata": 1, + "events": 2 + }, + { + "hash": "0x2d27bc4fb07a45227c171977c3bb92fd3f9c4ae492ff2f8ce8af324d79196580", + "calldata": 1, + "events": 2 + }, + { + "hash": "0x8ea6b0103d540f3ab103a4c3b8c52d2461a3fbe7629a1236b57c772d5a4fbd18", + "calldata": 1, + "events": 2 + }, + { + "hash": "0xea16c255284227d90b6a2dbc61191d5440ad4f4d7e97bbee330e876948b9269a", + "calldata": 2, + "events": 3 + }, + { + "hash": "0xe3c0310d80dcc0655dc6c4d90536c05c9830b97f2a343df897701a3baa3d217f", + "calldata": 1, + "events": 2 + } + ] + }, + "Universal Router (v2, 2025)": { + "decodedTxs": 31349, + "avgPools": 1.201, + "histogram": { + "1": { + "txs": 26103, + "pct": 83.27 + }, + "2": { + "txs": 4670, + "pct": 14.9 + }, + "3": { + "txs": 354, + "pct": 1.13 + }, + "4+": { + "txs": 222, + "pct": 0.71 + } + }, + "opaque": 0, + "undecoded": 173, + "failed": 4141, + "crossCheckAgainstReceipts": { + "compared": 31349, + "equal": 30795, + "calldataLower": 554, + "calldataHigher": 0 + }, + "mismatchSamples": [ + { + "hash": "0xf8e95975c298b42f0f6bb6e2158b718c05408f6f827032e317d37895e492559a", + "calldata": 1, + "events": 2 + }, + { + "hash": "0xa969b9330082242ea578b710df784036453541a7a0084493d22352b02a9e40ee", + "calldata": 1, + "events": 2 + }, + { + "hash": "0x513d5dc0bd4e41278200be5b18fbc2a855f5698c350449bf77d66bb0a25e9408", + "calldata": 1, + "events": 2 + }, + { + "hash": "0x5892102ad1c51701aa6d664e5ff0f155a4a2b36a7977819c32e4d2bdd7a46f19", + "calldata": 1, + "events": 3 + }, + { + "hash": "0x22af68dfad3d3ec0a792a40e47d722200862e1d03b239ff8756c5dfb539048a0", + "calldata": 1, + "events": 3 + } + ] + }, + "Uniswap v2 Router02": { + "decodedTxs": 19072, + "avgPools": 1.028, + "histogram": { + "1": { + "txs": 18534, + "pct": 97.18 + }, + "2": { + "txs": 536, + "pct": 2.81 + }, + "3": { + "txs": 2, + "pct": 0.01 + }, + "4+": { + "txs": 0, + "pct": 0 + } + }, + "opaque": 0, + "undecoded": 798, + "failed": 1021, + "crossCheckAgainstReceipts": { + "compared": 19072, + "equal": 18895, + "calldataLower": 177, + "calldataHigher": 0 + }, + "mismatchSamples": [ + { + "hash": "0xa36abe4510265aa77dbe3d49149c5e695d942f4f60f5a18bc258b5a6589b399b", + "calldata": 1, + "events": 2 + }, + { + "hash": "0xd622bf435c0803810878dcc98d4e08fa72570ade3180b4f4050812e755512b90", + "calldata": 1, + "events": 2 + }, + { + "hash": "0x1f2c205eec27c70573b2bc5cdf67b1c3718dd81226a1a2376cc12d3268497166", + "calldata": 1, + "events": 2 + }, + { + "hash": "0xdbfa8169f999378e81956b728c21452994f1391ba92249f6f3188256a95dbacd", + "calldata": 1, + "events": 2 + }, + { + "hash": "0x01271e9b0653a84f8bacdd0c62a3564e9c23075763fe0e51dd375ef3cc383a86", + "calldata": 1, + "events": 2 + } + ] + }, + "1inch AggregationRouter v6": { + "decodedTxs": 453, + "avgPools": 1.245, + "histogram": { + "1": { + "txs": 345, + "pct": 76.16 + }, + "2": { + "txs": 105, + "pct": 23.18 + }, + "3": { + "txs": 3, + "pct": 0.66 + }, + "4+": { + "txs": 0, + "pct": 0 + } + }, + "opaque": 8283, + "undecoded": 3207, + "failed": 941, + "crossCheckAgainstReceipts": { + "compared": 453, + "equal": 453, + "calldataLower": 0, + "calldataHigher": 0 + }, + "mismatchSamples": [] + }, + "SwapRouter (v3 classic)": { + "decodedTxs": 4813, + "avgPools": 1.017, + "histogram": { + "1": { + "txs": 4732, + "pct": 98.32 + }, + "2": { + "txs": 78, + "pct": 1.62 + }, + "3": { + "txs": 3, + "pct": 0.06 + }, + "4+": { + "txs": 0, + "pct": 0 + } + }, + "opaque": 0, + "undecoded": 0, + "failed": 234, + "crossCheckAgainstReceipts": { + "compared": 4813, + "equal": 4813, + "calldataLower": 0, + "calldataHigher": 0 + }, + "mismatchSamples": [] + }, + "Universal Router (v1.2, 2023)": { + "decodedTxs": 1543, + "avgPools": 1.022, + "histogram": { + "1": { + "txs": 1534, + "pct": 99.42 + }, + "2": { + "txs": 1, + "pct": 0.06 + }, + "3": { + "txs": 0, + "pct": 0 + }, + "4+": { + "txs": 8, + "pct": 0.52 + } + }, + "opaque": 0, + "undecoded": 5, + "failed": 14, + "crossCheckAgainstReceipts": { + "compared": 1543, + "equal": 1541, + "calldataLower": 2, + "calldataHigher": 0 + }, + "mismatchSamples": [ + { + "hash": "0xcca4964ec899dfdd66316107236306c224cb784a572c3ddf484ed92ef67fb5c8", + "calldata": 1, + "events": 2 + }, + { + "hash": "0xdd819743acf4517182d5f8270f5f7c4d3999ac7e823cc20f7fb2193091c02fd6", + "calldata": 1, + "events": 2 + } + ] + }, + "Ekubo Yul router": { + "decodedTxs": 0, + "avgPools": null, + "histogram": { + "1": { + "txs": 0, + "pct": 0 + }, + "2": { + "txs": 0, + "pct": 0 + }, + "3": { + "txs": 0, + "pct": 0 + }, + "4+": { + "txs": 0, + "pct": 0 + } + }, + "opaque": 20, + "undecoded": 0, + "failed": 0, + "crossCheckAgainstReceipts": { + "compared": 0, + "equal": 0, + "calldataLower": 0, + "calldataHigher": 0 + }, + "mismatchSamples": [] + } + } + } +} diff --git a/benchmarks/pools-per-swap/results.json b/benchmarks/pools-per-swap/results.json new file mode 100644 index 0000000..d86f782 --- /dev/null +++ b/benchmarks/pools-per-swap/results.json @@ -0,0 +1,712 @@ +{ + "rpc": "https://ethereum-rpc.publicnode.com", + "window": { + "fromBlock": 25997046, + "toBlock": 25998045, + "blocks": 1000, + "fromTimestamp": 1789646111, + "toTimestamp": 1789658171, + "fromIso": "2026-09-17T11:55:11.000Z", + "toIso": "2026-09-17T15:16:11.000Z" + }, + "rpcCalls": 2174, + "families": { + "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67": "univ3", + "0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822": "univ2-family", + "0x8b3e96f2b889fa771c53c981b40daf005f63f637f1869f707052d15a3dd97140": "curve", + "0xd013ca23e77a65003c2c659c5442c00c805371b7fc1ebd4c206c41d1536bd90b": "curve", + "0xb2e76ae99761dc136e598d4a629bb347eccb9532a5f8bbd72e18467c3c34cc98": "curve", + "0x143f1f8e861fbdeddd5b46e844b7d3ac7b86a122f36e8c463859ee6811b1f29c": "curve", + "0x40e9cecb9f5f1f1c5b9c97dec2917b7ee92e57ba5563708daca94dd84ad7112f": "univ4", + "0x2170c741c41531aec20e7c107c24eecfdd15e69c9bb0a8dd37b1840b9e0b207b": "balancer-v2", + "0x0874b2d545cb271cdbda4e093020c452328b24af12382ed62c4d00f5c26709db": "balancer-v3", + "0x3b841dc9ab51e3104bda4f61b41e4271192d22cd19da5ee6e292dc8e2744f713": "maverick-v1", + "0x103ed084e94a44c8f5f6ba8e3011507c41063177e29949083c439777d8d63f60": "maverick-v2", + "0x19b47279256b2a23a1665c810c8d55a1758940ee09377d4f8d26497a3577dc83": "pancake-v3", + "0xc2c0245e056d5fb095f04cd6373bc770802ebd1e6c918eb78fdef843cdb37b0f": "dodo", + "0xd8648b6ac54162763c86fd54bf2005af8ecd2f9cb273a5775921fd7f91e17b2d": "dodo", + "0xe93ad76094f247c0dafc1c61adc2187de1ac2738f7a3b49cb20b2263420251a3": "dodo", + "0x5c02c2bb2d1d082317eb23916ca27b3e7c294398b60061a2ad54f1c3c018c318": "bancor-v3", + "0x95f3b01351225fea0e69a46f68b164c9dea10284f12cd4a907ce66510ab7af6a": "bancor-carbon", + "0x276856b36cbc45526a0ba64f44611557a2a8b68662c5388e9fe6d72e86e1c8cb": "bancor-v2", + "0xdc004dbca4ef9c966218431ee5d9133d337ad018dd5b5c5493722803f75c64f7": "fluid", + "0xb3e2773606abfd36b5bd91394b3a54d1398336c65005baf7bf7a05efeffaf75b": "solidly-style" + }, + "ekuboCore": { + "address": "0x00000000000014aa86c5d3c41765bb24e11bd701", + "codeBytes": 18797, + "swapLog": "log0, no topics, 116 bytes of data (locker, poolId, balanceUpdate, stateAfter)" + }, + "routers": { + "0x66a9893cc07d91d95644aedd05d03f95e1dba8af": { + "name": "Universal Router (v2, 2025)", + "kind": "ur", + "verifiedSelectors": ["3593564c"], + "verifiedBytecodeContains": [], + "codeBytes": 19499 + }, + "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad": { + "name": "Universal Router (v1.2, 2023)", + "kind": "ur", + "verifiedSelectors": ["3593564c"], + "verifiedBytecodeContains": [], + "codeBytes": 17958 + }, + "0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45": { + "name": "SwapRouter02", + "kind": "sr02", + "verifiedSelectors": ["b858183f", "5ae401dc"], + "verifiedBytecodeContains": [], + "codeBytes": 24497 + }, + "0xe592427a0aece92de3edee1f18e0157c05861564": { + "name": "SwapRouter (v3 classic)", + "kind": "sr", + "verifiedSelectors": ["c04b8d59"], + "verifiedBytecodeContains": [], + "codeBytes": 12070 + }, + "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": { + "name": "Uniswap v2 Router02", + "kind": "v2r", + "verifiedSelectors": ["38ed1739"], + "verifiedBytecodeContains": [], + "codeBytes": 21943 + }, + "0x111111125421ca6dc452d289314280a0f8842a65": { + "name": "1inch AggregationRouter v6", + "kind": "oneinch", + "verifiedSelectors": ["07ed2379", "83800a8e"], + "verifiedBytecodeContains": [], + "codeBytes": 24294 + }, + "0x4c82d1fbfe28c977cbb58d8c7ff8fcf9f70a2cca": { + "name": "Universal Router (2026 deployment)", + "kind": "ur", + "verifiedSelectors": ["3593564c"], + "verifiedBytecodeContains": [], + "codeBytes": 24546 + }, + "0x7b2aa7ecc0b5936b7c52e6259a19c3ba557d0748": { + "name": "Ekubo Yul router", + "kind": "ekubo-yul", + "verifiedSelectors": [], + "verifiedBytecodeContains": ["00000000000014aa86c5d3c41765bb24e11bd701"], + "codeBytes": 2878 + } + }, + "receipts": { + "method": "eth_getBlockReceipts; successful txs with >=1 pool-level swap event; pools = number of such events in the tx", + "swapTxs": 32417, + "sumPools": 54407, + "failedSwapTxsSeen": 0, + "avgPoolsPerSwapTx": 1.678, + "histogram": { + "1": { + "txs": 23358, + "pct": 72.05 + }, + "2": { + "txs": 4801, + "pct": 14.81 + }, + "3": { + "txs": 2027, + "pct": 6.25 + }, + "4+": { + "txs": 2231, + "pct": 6.88 + } + }, + "ekuboCoreOtherTopiclessLogs": 0, + "allTxs": 321702, + "successfulTxs": 317617, + "perFamily": { + "univ3": { + "txs": 15900, + "events": 21418, + "avgEventsOfThisFamilyPerTx": 1.347, + "avgTotalPoolsPerTxTouchingFamily": 2.024, + "gasOfTxsTouchingFamily": "5672233165", + "emitters": 813, + "topEmitters": { + "0xe0554a476a092703abdb3ef35c80e0d76d32939f": 3058, + "0xc7bbec68d12a0d1830360f8ec58fa599ba1b0e9b": 1870, + "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640": 1059, + "0x99ac8ca7087fa4a2a1fb6357269965a2014abc35": 914, + "0x11b815efb8f581194ae79006d24e0d814b7697f6": 662 + } + }, + "univ4": { + "txs": 11748, + "events": 17467, + "avgEventsOfThisFamilyPerTx": 1.487, + "avgTotalPoolsPerTxTouchingFamily": 2.354, + "gasOfTxsTouchingFamily": "4741053798", + "emitters": 1, + "topEmitters": { + "0x000000000004444c5dc75cb358380d2e3de08a90": 11748 + } + }, + "univ2-family": { + "txs": 7528, + "events": 8963, + "avgEventsOfThisFamilyPerTx": 1.191, + "avgTotalPoolsPerTxTouchingFamily": 2.128, + "gasOfTxsTouchingFamily": "2467266054", + "emitters": 1267, + "topEmitters": { + "0xad1a21f61d653c6101b92c335f61140459c81c79": 233, + "0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc": 222, + "0x4a86c01d67965f8cb3d0aaa2c655705e64097c31": 181, + "0xd3d2e2692501a5c9ca623199d38826e513033a17": 172, + "0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852": 172 + } + }, + "curve": { + "txs": 1652, + "events": 2250, + "avgEventsOfThisFamilyPerTx": 1.362, + "avgTotalPoolsPerTxTouchingFamily": 4.079, + "gasOfTxsTouchingFamily": "1286127611", + "emitters": 219, + "topEmitters": { + "0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14": 194, + "0x35c9a4dae1ff05788f24b5b32721d89340cbb636": 178, + "0xd51a44d3fae010294c616388b506acda1bfaae46": 129, + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b": 105, + "0xf5f5b97624542d72a9e06f04804bf81baa15e2b4": 81 + } + }, + "pancake-v3": { + "txs": 1307, + "events": 1433, + "avgEventsOfThisFamilyPerTx": 1.096, + "avgTotalPoolsPerTxTouchingFamily": 4.111, + "gasOfTxsTouchingFamily": "858951131", + "emitters": 35, + "topEmitters": { + "0x1445f32d1a74872ba41f3d8cf4022e9996120b31": 575, + "0xacdb27b266142223e1e676841c1e809255fc6d07": 372, + "0x1ac1a8feaaea1900c4166deeed0c11cc10669d36": 116, + "0x6ca298d2983ab03aa1da7679389d955a4efee15c": 103, + "0xbf542ecaa30d23f651e07720f9ca74d6109d4e3a": 62 + } + }, + "ekubo": { + "txs": 943, + "events": 1079, + "avgEventsOfThisFamilyPerTx": 1.144, + "avgTotalPoolsPerTxTouchingFamily": 3.538, + "gasOfTxsTouchingFamily": "673692276", + "emitters": 1, + "topEmitters": { + "0x00000000000014aa86c5d3c41765bb24e11bd701": 943 + } + }, + "fluid": { + "txs": 551, + "events": 587, + "avgEventsOfThisFamilyPerTx": 1.065, + "avgTotalPoolsPerTxTouchingFamily": 3.316, + "gasOfTxsTouchingFamily": "435123007", + "emitters": 19, + "topEmitters": { + "0x667701e51b4d1ca244f17c78f7ab8744b4c99f9b": 224, + "0x836951eb21f3df98273517b7249dceff270d34bf": 194, + "0x3c0441b42195f4ad6aa9a0978e06096ea616cda7": 76, + "0xde632c3a214d5f14c1d8ddf0b92f8bcd188fee45": 20, + "0x97f32e5091d0e6aee24beb398e6592ae111dee9c": 8 + } + }, + "dodo": { + "txs": 421, + "events": 448, + "avgEventsOfThisFamilyPerTx": 1.064, + "avgTotalPoolsPerTxTouchingFamily": 4.496, + "gasOfTxsTouchingFamily": "399393841", + "emitters": 56, + "topEmitters": { + "0x04571c32a4e1c5f39bc3a238cb95b215058c432c": 243, + "0x3058ef90929cb8180174d74c507176cca6835d73": 40, + "0xc9f93163c99695c6526b799ebca2207fdf7d61ad": 23, + "0x201939f5c74f5fd3beef1cf03a9cfcc75beba9d9": 13, + "0x46cf01f14ff6f8c7ef4d6a6b139b1c3d45af9289": 12 + } + }, + "balancer-v2": { + "txs": 243, + "events": 259, + "avgEventsOfThisFamilyPerTx": 1.066, + "avgTotalPoolsPerTxTouchingFamily": 4.066, + "gasOfTxsTouchingFamily": "160357147", + "emitters": 2, + "topEmitters": { + "0xba12222222228d8ba445958a75a0704d566bf2c8": 185, + "0xd315a9c38ec871068fec378e4ce78af528c76293": 58 + } + }, + "balancer-v3": { + "txs": 202, + "events": 226, + "avgEventsOfThisFamilyPerTx": 1.119, + "avgTotalPoolsPerTxTouchingFamily": 5.119, + "gasOfTxsTouchingFamily": "204734020", + "emitters": 2, + "topEmitters": { + "0xba1333333333a1ba1108e8412f11850a5c319ba9": 189, + "0x955244edc797a1c1b04134b600f819ac23c76081": 13 + } + }, + "maverick-v2": { + "txs": 118, + "events": 123, + "avgEventsOfThisFamilyPerTx": 1.042, + "avgTotalPoolsPerTxTouchingFamily": 5.127, + "gasOfTxsTouchingFamily": "117418379", + "emitters": 7, + "topEmitters": { + "0x6061cf96aa9f02aa072b77211a863989ca070c78": 51, + "0x025a13752ec1fb6c0e441e5d7b1678bb5f9ca62f": 27, + "0x5b6a0771c752e35b2ca2aff4f22a66b1598a2bc5": 18, + "0xdf9c440111a17eafbecf5db37dd04c948e878878": 12, + "0x31373595f40ea48a7aab6cbcb0d377c6066e2dca": 10 + } + }, + "bancor-v2": { + "txs": 54, + "events": 107, + "avgEventsOfThisFamilyPerTx": 1.981, + "avgTotalPoolsPerTxTouchingFamily": 6.13, + "gasOfTxsTouchingFamily": "42174657", + "emitters": 19, + "topEmitters": { + "0x079ca3f710599739a22673c2856202f90d3a8806": 22, + "0xe331821bc94187c2649e932810a60204699d45cb": 19, + "0xcba47689202d31575bfa204efe70e2c83d29da4d": 11, + "0xd4d5298136b52ec439ebfb23fda2ec500de69c71": 9, + "0x987caf390fc5100da6db7e8ec4b651cd3b61bc32": 6 + } + }, + "bancor-v3": { + "txs": 23, + "events": 23, + "avgEventsOfThisFamilyPerTx": 1, + "avgTotalPoolsPerTxTouchingFamily": 5.348, + "gasOfTxsTouchingFamily": "17867787", + "emitters": 1, + "topEmitters": { + "0xeef417e1d5cc832e619ae18d2f140de2999dd4fb": 23 + } + }, + "maverick-v1": { + "txs": 17, + "events": 18, + "avgEventsOfThisFamilyPerTx": 1.059, + "avgTotalPoolsPerTxTouchingFamily": 6.059, + "gasOfTxsTouchingFamily": "12229328", + "emitters": 7, + "topEmitters": { + "0x11a653ddfbb61e0feff5484919f06d9d254bf65f": 4, + "0xa6634fc4ed29950e85d97d48580a1a9d5c14ca7c": 4, + "0x6c6fc818b25df89a8ada8da5a43669023bad1f4c": 3, + "0x352b186090068eb35d532428676ce510e17ab581": 3, + "0x8dd893b9ec19d89b66c01794cf5e04e30d284e53": 2 + } + }, + "bancor-carbon": { + "txs": 6, + "events": 6, + "avgEventsOfThisFamilyPerTx": 1, + "avgTotalPoolsPerTxTouchingFamily": 2.333, + "gasOfTxsTouchingFamily": "3788965", + "emitters": 1, + "topEmitters": { + "0xc537e898cd774e2dcba3b14ea6f34c93d5ea45e1": 6 + } + } + }, + "familiesWithNoEvents": ["solidly-style"], + "gasShare": { + "swapTxGas": "10039217633", + "allTxGas": "30729852504", + "pct": 32.67 + } + }, + "calldata": { + "method": "eth_getBlockByNumber(full); successful txs to the verified routers; pools decoded from calldata", + "decodedTxs": 4432, + "sumPools": 5687, + "avgPoolsPerSwapTx": 1.283, + "histogram": { + "1": { + "txs": 3712, + "pct": 83.75 + }, + "2": { + "txs": 473, + "pct": 10.67 + }, + "3": { + "txs": 130, + "pct": 2.93 + }, + "4+": { + "txs": 117, + "pct": 2.64 + } + }, + "opaqueTxs": 444, + "undecodedTxs": 323, + "failedTxs": 284, + "crossCheckAgainstReceipts": { + "compared": 4432, + "equal": 4407, + "calldataLower": 25, + "calldataHigher": 0 + }, + "undecodedSelectors": { + "1inch AggregationRouter v6:0xb68fb020": 239, + "1inch AggregationRouter v6:0xc3cf8043": 34, + "1inch AggregationRouter v6:0x89e7c650": 13, + "Uniswap v2 Router02:0xe8e33700": 8, + "Uniswap v2 Router02:0x02751cec": 6, + "Universal Router (v2, 2025):0x24856bc3": 5, + "Uniswap v2 Router02:0xf305d719": 5, + "Uniswap v2 Router02:0xaf2979eb": 5, + "1inch AggregationRouter v6:0x56a75868": 2, + "Uniswap v2 Router02:0xbaa2abde": 2, + "Universal Router (v2, 2025):0x3593564c": 1, + "1inch AggregationRouter v6:0xea76dddf": 1, + "1inch AggregationRouter v6:0xcc713a04": 1, + "1inch AggregationRouter v6:0x5816d723": 1 + }, + "perRouter": { + "SwapRouter02": { + "decodedTxs": 1168, + "avgPools": 1.076, + "histogram": { + "1": { + "txs": 1093, + "pct": 93.58 + }, + "2": { + "txs": 71, + "pct": 6.08 + }, + "3": { + "txs": 0, + "pct": 0 + }, + "4+": { + "txs": 4, + "pct": 0.34 + } + }, + "opaque": 0, + "undecoded": 0, + "failed": 39, + "crossCheckAgainstReceipts": { + "compared": 1168, + "equal": 1168, + "calldataLower": 0, + "calldataHigher": 0 + }, + "mismatchSamples": [] + }, + "Universal Router (2026 deployment)": { + "decodedTxs": 1070, + "avgPools": 1.837, + "histogram": { + "1": { + "txs": 635, + "pct": 59.35 + }, + "2": { + "txs": 224, + "pct": 20.93 + }, + "3": { + "txs": 112, + "pct": 10.47 + }, + "4+": { + "txs": 99, + "pct": 9.25 + } + }, + "opaque": 0, + "undecoded": 0, + "failed": 56, + "crossCheckAgainstReceipts": { + "compared": 1070, + "equal": 1056, + "calldataLower": 14, + "calldataHigher": 0 + }, + "mismatchSamples": [ + { + "hash": "0x11ca7590c89ee5e282d79c94e6a4d2324b2c715038c048ff7beabfd2a9d2cf0a", + "calldata": 1, + "events": 2 + }, + { + "hash": "0xb874723278b4f54e8d520c7f7c2dfcc8c51a3003f01bd77efa55c10628002214", + "calldata": 11, + "events": 16 + }, + { + "hash": "0xa214808bf64c3608d17bfc0fb6d20e29e5f209dbb0160d73677d7af92d59d445", + "calldata": 10, + "events": 15 + }, + { + "hash": "0xf9a1267117fb3703a6d23a0fd00b8d2c7f7285dead677988e5242b1509ce14b8", + "calldata": 3, + "events": 8 + }, + { + "hash": "0xf5247e389078d1c7e9f0b95648c29de1b1f48685ab8b881746abe4607538bb22", + "calldata": 4, + "events": 9 + } + ] + }, + "Universal Router (v2, 2025)": { + "decodedTxs": 1153, + "avgPools": 1.207, + "histogram": { + "1": { + "txs": 974, + "pct": 84.48 + }, + "2": { + "txs": 147, + "pct": 12.75 + }, + "3": { + "txs": 18, + "pct": 1.56 + }, + "4+": { + "txs": 14, + "pct": 1.21 + } + }, + "opaque": 0, + "undecoded": 6, + "failed": 84, + "crossCheckAgainstReceipts": { + "compared": 1153, + "equal": 1150, + "calldataLower": 3, + "calldataHigher": 0 + }, + "mismatchSamples": [ + { + "hash": "0x1a0f9590d96739def7529481e2473fee20aeea31641e67d86ae008624b34db35", + "calldata": 1, + "events": 2 + }, + { + "hash": "0xa65e91f36e9dff915f2f4e8c3b53ca646868fdf4f02bd99be8981db7bcf25763", + "calldata": 1, + "events": 2 + }, + { + "hash": "0x0dd68372de80d8ca4877135f5e3cefb4723e953c30dc7d8545683fc9092520e9", + "calldata": 1, + "events": 2 + } + ] + }, + "1inch AggregationRouter v6": { + "decodedTxs": 32, + "avgPools": 1.438, + "histogram": { + "1": { + "txs": 18, + "pct": 56.25 + }, + "2": { + "txs": 14, + "pct": 43.75 + }, + "3": { + "txs": 0, + "pct": 0 + }, + "4+": { + "txs": 0, + "pct": 0 + } + }, + "opaque": 443, + "undecoded": 291, + "failed": 77, + "crossCheckAgainstReceipts": { + "compared": 32, + "equal": 32, + "calldataLower": 0, + "calldataHigher": 0 + }, + "mismatchSamples": [] + }, + "Uniswap v2 Router02": { + "decodedTxs": 658, + "avgPools": 1.011, + "histogram": { + "1": { + "txs": 651, + "pct": 98.94 + }, + "2": { + "txs": 7, + "pct": 1.06 + }, + "3": { + "txs": 0, + "pct": 0 + }, + "4+": { + "txs": 0, + "pct": 0 + } + }, + "opaque": 0, + "undecoded": 26, + "failed": 19, + "crossCheckAgainstReceipts": { + "compared": 658, + "equal": 650, + "calldataLower": 8, + "calldataHigher": 0 + }, + "mismatchSamples": [ + { + "hash": "0x10d260c15faef3956f49a27deafcc922aa6571239a3bce2bb303ae30617f21cc", + "calldata": 1, + "events": 2 + }, + { + "hash": "0x7e9460abd09a72a555b5263f6c9bcbfba452ac567a33fd7a42efd5b8130f133f", + "calldata": 1, + "events": 2 + }, + { + "hash": "0xdac01c00222fcc79c0655a297c1787b452557f159fcd804e1ff8c470983a00eb", + "calldata": 1, + "events": 2 + }, + { + "hash": "0x0ebb14652926f33c82cc43d8d209647ca77ab098afc6d4cd98d412c67d1e0490", + "calldata": 1, + "events": 2 + }, + { + "hash": "0xef721bef53ae97252361b1e7abb4d54f166ad46fe9cf76152923890f62874226", + "calldata": 1, + "events": 2 + } + ] + }, + "SwapRouter (v3 classic)": { + "decodedTxs": 281, + "avgPools": 1.036, + "histogram": { + "1": { + "txs": 271, + "pct": 96.44 + }, + "2": { + "txs": 10, + "pct": 3.56 + }, + "3": { + "txs": 0, + "pct": 0 + }, + "4+": { + "txs": 0, + "pct": 0 + } + }, + "opaque": 0, + "undecoded": 0, + "failed": 8, + "crossCheckAgainstReceipts": { + "compared": 281, + "equal": 281, + "calldataLower": 0, + "calldataHigher": 0 + }, + "mismatchSamples": [] + }, + "Universal Router (v1.2, 2023)": { + "decodedTxs": 70, + "avgPools": 1, + "histogram": { + "1": { + "txs": 70, + "pct": 100 + }, + "2": { + "txs": 0, + "pct": 0 + }, + "3": { + "txs": 0, + "pct": 0 + }, + "4+": { + "txs": 0, + "pct": 0 + } + }, + "opaque": 0, + "undecoded": 0, + "failed": 1, + "crossCheckAgainstReceipts": { + "compared": 70, + "equal": 70, + "calldataLower": 0, + "calldataHigher": 0 + }, + "mismatchSamples": [] + }, + "Ekubo Yul router": { + "decodedTxs": 0, + "avgPools": null, + "histogram": { + "1": { + "txs": 0, + "pct": 0 + }, + "2": { + "txs": 0, + "pct": 0 + }, + "3": { + "txs": 0, + "pct": 0 + }, + "4+": { + "txs": 0, + "pct": 0 + } + }, + "opaque": 1, + "undecoded": 0, + "failed": 0, + "crossCheckAgainstReceipts": { + "compared": 0, + "equal": 0, + "calldataLower": 0, + "calldataHigher": 0 + }, + "mismatchSamples": [] + } + } + } +} diff --git a/benchmarks/prod/.gitignore b/benchmarks/prod/.gitignore new file mode 100644 index 0000000..182be85 --- /dev/null +++ b/benchmarks/prod/.gitignore @@ -0,0 +1,3 @@ +out/ +cache/ +lib/ diff --git a/benchmarks/prod/artifacts/MockERC20.json b/benchmarks/prod/artifacts/MockERC20.json new file mode 100644 index 0000000..860972c --- /dev/null +++ b/benchmarks/prod/artifacts/MockERC20.json @@ -0,0 +1 @@ +{"abi":[{"type":"constructor","inputs":[{"name":"_name","type":"string","internalType":"string"},{"name":"_symbol","type":"string","internalType":"string"},{"name":"_decimals","type":"uint8","internalType":"uint8"}],"stateMutability":"nonpayable"},{"type":"function","name":"DOMAIN_SEPARATOR","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"allowance","inputs":[{"name":"","type":"address","internalType":"address"},{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"approve","inputs":[{"name":"spender","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"type":"function","name":"balanceOf","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"burn","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"decimals","inputs":[],"outputs":[{"name":"","type":"uint8","internalType":"uint8"}],"stateMutability":"view"},{"type":"function","name":"mint","inputs":[{"name":"to","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"name","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"nonces","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"permit","inputs":[{"name":"owner","type":"address","internalType":"address"},{"name":"spender","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"symbol","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"totalSupply","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"transfer","inputs":[{"name":"to","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"type":"function","name":"transferFrom","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"to","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"type":"event","name":"Approval","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"spender","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Transfer","inputs":[{"name":"from","type":"address","indexed":true,"internalType":"address"},{"name":"to","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false}],"bytecode":{"object":"0x60e080604052346104065761134e803803809161001c828561040a565b83398101906060818303126104065780516001600160401b038111610406578261004791830161042d565b60208201519092906001600160401b0381116104065760409161006b91840161042d565b91015160ff811681036104065782516001600160401b038111610337576100925f54610482565b601f81116103b7575b506020601f821160011461035657819293945f9261034b575b50508160011b915f199060031b1c1916175f555b81516001600160401b038111610337576100e3600154610482565b601f81116102d4575b50602092601f821160011461027357928192935f92610268575b50508160011b915f199060031b1c1916176001555b6080524660a0526040515f905f54918161013484610482565b9182825260208201946001811690815f1461024c5750600114610202575b61015e9250038261040a565b51902060405160208101917f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f835260408201527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc660608201524660808201523060a082015260a081526101d260c08261040a565b51902060c052604051610e7390816104bb823960805181610865015260a05181610ca8015260c05181610cce0152f35b505f80805290915f8051602061132e8339815191525b81831061023057505090602061015e92820101610152565b6020919350806001915483858801015201910190918392610218565b60ff191686525061015e92151560051b82016020019050610152565b015190505f80610106565b601f1982169360015f52805f20915f5b8681106102bc57508360019596106102a4575b505050811b0160015561011b565b01515f1960f88460031b161c191690555f8080610296565b91926020600181928685015181550194019201610283565b60015f527fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6601f830160051c8101916020841061032d575b601f0160051c01905b81811061032257506100ec565b5f8155600101610315565b909150819061030c565b634e487b7160e01b5f52604160045260245ffd5b015190505f806100b4565b601f198216905f8052805f20915f5b81811061039f57509583600195969710610387575b505050811b015f556100c8565b01515f1960f88460031b161c191690555f808061037a565b9192602060018192868b015181550194019201610365565b5f80525f8051602061132e833981519152601f830160051c810191602084106103fc575b601f0160051c01905b8181106103f1575061009b565b5f81556001016103e4565b90915081906103db565b5f80fd5b601f909101601f19168101906001600160401b0382119082101761033757604052565b81601f82011215610406578051906001600160401b0382116103375760405192610461601f8401601f19166020018561040a565b8284526020838301011161040657815f9260208093018386015e8301015290565b90600182811c921680156104b0575b602083101461049c57565b634e487b7160e01b5f52602260045260245ffd5b91607f169161049156fe60806040526004361015610011575f80fd5b5f3560e01c806306fdde0314610a8b578063095ea7b3146109e857806318160ddd146109ad57806323b872dd14610889578063313ce5671461082e5780633644e515146107ee57806340c10f191461071f57806370a08231146106bc5780637ecebe001461065957806395d89b41146105435780639dc29fac146104a4578063a9059cbb146103f0578063d505accf146101435763dd62ed3e146100b3575f80fd5b3461013f5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576100ea610c52565b73ffffffffffffffffffffffffffffffffffffffff610107610c75565b91165f52600460205273ffffffffffffffffffffffffffffffffffffffff60405f2091165f52602052602060405f2054604051908152f35b5f80fd5b3461013f5760e07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f5761017a610c52565b610182610c75565b6044356064359260843560ff811680910361013f574285106103925760805f9160209373ffffffffffffffffffffffffffffffffffffffff6101c2610ca5565b91169687855260058652604085209889549960018b0190556040519073ffffffffffffffffffffffffffffffffffffffff888301937f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c985528b6040850152169a8b6060840152898784015260a083015260c082015260c0815261024660e082610b9c565b51902060405190868201927f19010000000000000000000000000000000000000000000000000000000000008452602283015260428201526042815261028d606282610b9c565b519020906040519182528482015260a435604082015260c435606082015282805260015afa156103875773ffffffffffffffffffffffffffffffffffffffff5f51168015158061037e575b15610320577f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925916020915f526004825260405f20855f5282528060405f2055604051908152a3005b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152600e60248201527f494e56414c49445f5349474e45520000000000000000000000000000000000006044820152fd5b508281146102d8565b6040513d5f823e3d90fd5b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f5045524d49545f444541444c494e455f455850495245440000000000000000006044820152fd5b3461013f5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f57610427610c52565b73ffffffffffffffffffffffffffffffffffffffff60243591335f52600360205260405f20610457848254610c98565b90551690815f52600360205260405f208181540190556040519081527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60203392a3602060405160018152f35b3461013f5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f575f6104dc610c52565b7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef602073ffffffffffffffffffffffffffffffffffffffff6024359316928385526003825260408520610530828254610c98565b90558060025403600255604051908152a3005b3461013f575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576040515f60015461058181610b4b565b808452906001811690811561061757506001146105b9575b6105b5836105a981850382610b9c565b60405191829182610c0a565b0390f35b60015f9081527fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6939250905b8082106105fd575090915081016020016105a9610599565b9192600181602092548385880101520191019092916105e5565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660208086019190915291151560051b840190910191506105a99050610599565b3461013f5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f5773ffffffffffffffffffffffffffffffffffffffff6106a5610c52565b165f526005602052602060405f2054604051908152f35b3461013f5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f5773ffffffffffffffffffffffffffffffffffffffff610708610c52565b165f526003602052602060405f2054604051908152f35b3461013f5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f57610756610c52565b602435906002548281018091116107c157602073ffffffffffffffffffffffffffffffffffffffff5f937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9360025516938484526003825260408420818154019055604051908152a3005b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b3461013f575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576020610826610ca5565b604051908152f35b3461013f575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f57602060405160ff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b3461013f5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576108c0610c52565b6108c8610c75565b7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef602073ffffffffffffffffffffffffffffffffffffffff80604435951693845f526004835260405f208233165f52835260405f2054867fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203610986575b5050845f526003835260405f2061095f878254610c98565b90551693845f526003825260405f20818154019055604051908152a3602060405160018152f35b61098f91610c98565b855f526004845260405f208333165f52845260405f20558686610947565b3461013f575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576020600254604051908152f35b3461013f5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f57610a1f610c52565b73ffffffffffffffffffffffffffffffffffffffff60243591335f52600460205260405f208282165f526020528260405f205560405192835216907f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560203392a3602060405160018152f35b3461013f575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576040515f8054610ac881610b4b565b80845290600181169081156106175750600114610aef576105b5836105a981850382610b9c565b5f8080527f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563939250905b808210610b31575090915081016020016105a9610599565b919260018160209254838588010152019101909291610b19565b90600182811c92168015610b92575b6020831014610b6557565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b91607f1691610b5a565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117610bdd57604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f602060409481855280519182918282880152018686015e5f8582860101520116010190565b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361013f57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361013f57565b919082039182116107c157565b467f000000000000000000000000000000000000000000000000000000000000000003610cf0577f000000000000000000000000000000000000000000000000000000000000000090565b6040515f905f549181610d0284610b4b565b9182825260208201946001811690815f14610e035750600114610da6575b610d2c92500382610b9c565b51902060405160208101917f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f835260408201527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc660608201524660808201523060a082015260a08152610da060c082610b9c565b51902090565b505f80805290917f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e5635b818310610de7575050906020610d2c92820101610d20565b6020919350806001915483858801015201910190918392610dcf565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016865250610d2c92151560051b82016020019050610d2056fea26469706673582212200e4e3600233109bb884b081d524b13dac8383267fd5a07dfc715d3d22497968a64736f6c634300081a0033290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563","sourceMap":"118:368:20:-:0;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;-1:-1:-1;;;;;118:368:20;;;;;;;;;;:::i;:::-;;;;;;;;-1:-1:-1;;;;;118:368:20;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;-1:-1:-1;;;;;118:368:20;;;;;-1:-1:-1;118:368:20;;:::i;:::-;;;;;;-1:-1:-1;118:368:20;;;;;;;;;;;;;-1:-1:-1;118:368:20;;;;;;;;;;;;;;;;;;;-1:-1:-1;118:368:20;;;;-1:-1:-1;;;;;118:368:20;;;;;2116:16:21;118:368:20;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;118:368:20;;;;;;;2116:16:21;118:368:20;;;;;;;;;;;2116:16:21;118:368:20;;2142:20:21;;2192:13;2173:32;;118:368:20;;-1:-1:-1;118:368:20;-1:-1:-1;118:368:20;;;;;;:::i;:::-;;;;;;;;;2116:16:21;118:368:20;;2116:16:21;;118:368:20;2116:16:21;;;118:368:20;;;;;;;;;;;;:::i;:::-;;5789:22:21;;118:368:20;;;5640:295:21;;118:368:20;5672:95:21;118:368:20;;;;;;5833:14:21;118:368:20;;;;2192:13:21;2142:20;118:368:20;;;5912:4:21;2173:32;118:368:20;;;2173:32:21;5640:295;;;;;;:::i;:::-;118:368:20;5613:336:21;;5640:295;2215:51;118:368:20;;;;;;;;2142:20:21;118:368:20;;;;;2173:32:21;118:368:20;;;;;5640:295:21;118:368:20;;;;;;;-1:-1:-1;;118:368:20;;;;;-1:-1:-1;;;;;;;;;;;118:368:20;;;;;;;;;;;;;;;;;;;;;;;2116:16:21;118:368:20;;;;;;;;;;;;;;;;;;-1:-1:-1;;118:368:20;;;-1:-1:-1;118:368:20;;;;;;;;;;;-1:-1:-1;118:368:20;;;;;;-1:-1:-1;118:368:20;;;;;;;;;;2116:16:21;-1:-1:-1;118:368:20;;-1:-1:-1;118:368:20;;-1:-1:-1;118:368:20;;;;;;;;2116:16:21;118:368:20;;;;;;;;;;;;2116:16:21;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;2116:16:21;118:368:20;;;;;;;;;;;;;;;;2116:16:21;-1:-1:-1;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;118:368:20;;2116:16:21;118:368:20;;;;;;-1:-1:-1;118:368:20;;;;;;;;-1:-1:-1;118:368:20;;;;;-1:-1:-1;118:368:20;;;;;-1:-1:-1;118:368:20;;;;;;;;;;-1:-1:-1;118:368:20;;;-1:-1:-1;118:368:20;;-1:-1:-1;118:368:20;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;118:368:20;;-1:-1:-1;;;;;;;;;;;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;118:368:20;;;;;;;;;-1:-1:-1;118:368:20;;;;;-1:-1:-1;118:368:20;;;;;;;-1:-1:-1;;118:368:20;;;;-1:-1:-1;;;;;118:368:20;;;;;;;;;;:::o;:::-;;;;;;;;;;;;-1:-1:-1;;;;;118:368:20;;;;;;;;;;;-1:-1:-1;;118:368:20;;;;;:::i;:::-;;;;;;;;;;;;;-1:-1:-1;118:368:20;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x60806040526004361015610011575f80fd5b5f3560e01c806306fdde0314610a8b578063095ea7b3146109e857806318160ddd146109ad57806323b872dd14610889578063313ce5671461082e5780633644e515146107ee57806340c10f191461071f57806370a08231146106bc5780637ecebe001461065957806395d89b41146105435780639dc29fac146104a4578063a9059cbb146103f0578063d505accf146101435763dd62ed3e146100b3575f80fd5b3461013f5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576100ea610c52565b73ffffffffffffffffffffffffffffffffffffffff610107610c75565b91165f52600460205273ffffffffffffffffffffffffffffffffffffffff60405f2091165f52602052602060405f2054604051908152f35b5f80fd5b3461013f5760e07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f5761017a610c52565b610182610c75565b6044356064359260843560ff811680910361013f574285106103925760805f9160209373ffffffffffffffffffffffffffffffffffffffff6101c2610ca5565b91169687855260058652604085209889549960018b0190556040519073ffffffffffffffffffffffffffffffffffffffff888301937f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c985528b6040850152169a8b6060840152898784015260a083015260c082015260c0815261024660e082610b9c565b51902060405190868201927f19010000000000000000000000000000000000000000000000000000000000008452602283015260428201526042815261028d606282610b9c565b519020906040519182528482015260a435604082015260c435606082015282805260015afa156103875773ffffffffffffffffffffffffffffffffffffffff5f51168015158061037e575b15610320577f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925916020915f526004825260405f20855f5282528060405f2055604051908152a3005b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152600e60248201527f494e56414c49445f5349474e45520000000000000000000000000000000000006044820152fd5b508281146102d8565b6040513d5f823e3d90fd5b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f5045524d49545f444541444c494e455f455850495245440000000000000000006044820152fd5b3461013f5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f57610427610c52565b73ffffffffffffffffffffffffffffffffffffffff60243591335f52600360205260405f20610457848254610c98565b90551690815f52600360205260405f208181540190556040519081527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60203392a3602060405160018152f35b3461013f5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f575f6104dc610c52565b7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef602073ffffffffffffffffffffffffffffffffffffffff6024359316928385526003825260408520610530828254610c98565b90558060025403600255604051908152a3005b3461013f575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576040515f60015461058181610b4b565b808452906001811690811561061757506001146105b9575b6105b5836105a981850382610b9c565b60405191829182610c0a565b0390f35b60015f9081527fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6939250905b8082106105fd575090915081016020016105a9610599565b9192600181602092548385880101520191019092916105e5565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660208086019190915291151560051b840190910191506105a99050610599565b3461013f5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f5773ffffffffffffffffffffffffffffffffffffffff6106a5610c52565b165f526005602052602060405f2054604051908152f35b3461013f5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f5773ffffffffffffffffffffffffffffffffffffffff610708610c52565b165f526003602052602060405f2054604051908152f35b3461013f5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f57610756610c52565b602435906002548281018091116107c157602073ffffffffffffffffffffffffffffffffffffffff5f937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9360025516938484526003825260408420818154019055604051908152a3005b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b3461013f575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576020610826610ca5565b604051908152f35b3461013f575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f57602060405160ff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b3461013f5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576108c0610c52565b6108c8610c75565b7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef602073ffffffffffffffffffffffffffffffffffffffff80604435951693845f526004835260405f208233165f52835260405f2054867fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203610986575b5050845f526003835260405f2061095f878254610c98565b90551693845f526003825260405f20818154019055604051908152a3602060405160018152f35b61098f91610c98565b855f526004845260405f208333165f52845260405f20558686610947565b3461013f575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576020600254604051908152f35b3461013f5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f57610a1f610c52565b73ffffffffffffffffffffffffffffffffffffffff60243591335f52600460205260405f208282165f526020528260405f205560405192835216907f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560203392a3602060405160018152f35b3461013f575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013f576040515f8054610ac881610b4b565b80845290600181169081156106175750600114610aef576105b5836105a981850382610b9c565b5f8080527f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563939250905b808210610b31575090915081016020016105a9610599565b919260018160209254838588010152019101909291610b19565b90600182811c92168015610b92575b6020831014610b6557565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b91607f1691610b5a565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117610bdd57604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f602060409481855280519182918282880152018686015e5f8582860101520116010190565b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361013f57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361013f57565b919082039182116107c157565b467f000000000000000000000000000000000000000000000000000000000000000003610cf0577f000000000000000000000000000000000000000000000000000000000000000090565b6040515f905f549181610d0284610b4b565b9182825260208201946001811690815f14610e035750600114610da6575b610d2c92500382610b9c565b51902060405160208101917f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f835260408201527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc660608201524660808201523060a082015260a08152610da060c082610b9c565b51902090565b505f80805290917f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e5635b818310610de7575050906020610d2c92820101610d20565b6020919350806001915483858801015201910190918392610dcf565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016865250610d2c92151560051b82016020019050610d2056fea26469706673582212200e4e3600233109bb884b081d524b13dac8383267fd5a07dfc715d3d22497968a64736f6c634300081a0033","sourceMap":"118:368:20:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;:::i;:::-;;;;;;;;;;;;1388:64:21;118:368:20;-1:-1:-1;118:368:20;;;;;-1:-1:-1;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;:::i;:::-;;;;;;;;;;;;;;;;4057:15:21;4045:27;;118:368:20;;;;4428:18:21;118:368:20;4428:18:21;118:368:20;4428:18:21;;:::i;:::-;118:368:20;;;;;;4873:6:21;118:368:20;;;;;;;;;;;;;;;;4511:449:21;118:368:20;4511:449:21;;;118:368:20;4555:165:21;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;;4511:449:21;;;118:368:20;4511:449:21;;:::i;:::-;118:368:20;4472:514:21;;118:368:20;;4350:658:21;;;;118:368:20;;;;;;;;;;;;;4350:658:21;;;;;;:::i;:::-;118:368:20;4319:707:21;;118:368:20;;;;;;;;;;;;;;;;;;;;;;4292:805:21;;;118:368:20;4292:805:21;;;;;118:368:20;;4292:805:21;118:368:20;5120:30:21;;;:59;;;118:368:20;;;;5283:31:21;118:368:20;;;;;;;;;;;;-1:-1:-1;118:368:20;;;;;-1:-1:-1;118:368:20;;;;;;;5283:31:21;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;5120:59:21;5154:25;;;;5120:59;;4292:805;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;2774:10:21;;118:368:20;;2764:9:21;118:368:20;;;;;2764:31:21;118:368:20;;;2764:31:21;:::i;:::-;118:368:20;;;;;;;2764:9:21;118:368:20;;;;;;;;;;;;;;;;2990:32:21;118:368:20;2774:10:21;2990:32;;118:368:20;;;;;;;;;;;;;;;;;;;;;:::i;:::-;6768:34:21;118:368:20;;;;;;;;;;6553:9:21;118:368:20;;;;;6553:25:21;118:368:20;;;6553:25:21;:::i;:::-;118:368:20;;;6721:21:21;118:368:20;;6721:21:21;118:368:20;;;;;;6768:34:21;118:368:20;;;;;;;;;;;;;;;1056:20:21;118:368:20;;;;:::i;:::-;;;;;1056:20:21;118:368:20;;;1056:20:21;;;;118:368:20;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;1056:20:21;118:368:20;;;;;;;-1:-1:-1;118:368:20;;;;;;;-1:-1:-1;118:368:20;;-1:-1:-1;118:368:20;;;;;;;;;;1056:20:21;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;118:368:20;;-1:-1:-1;118:368:20;;;;;;;;;;;;;;;;:::i;:::-;;;;1751:41:21;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;1337:44:21;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;6220:21:21;118:368:20;;;;;;;;;;;;;6436:32:21;118:368:20;6220:21:21;118:368:20;;;;;;6387:9:21;118:368:20;;;;;;;;;;;;;;;;6436:32:21;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;1083:31:21;118:368:20;;;;;;;;;;;;;;;;;:::i;:::-;;;:::i;:::-;3594:26:21;118:368:20;;;;;;;;;;;;;;;;;3225:10:21;;118:368:20;-1:-1:-1;118:368:20;;;;-1:-1:-1;118:368:20;;3287:28:21;3298:17;3287:28;;3283:80;;118:368:20;;;;;;3374:9:21;118:368:20;;;;;3374:25:21;118:368:20;;;3374:25:21;:::i;:::-;118:368:20;;;;;;;3374:9:21;118:368:20;;;;;;;;;;;;;;;;3594:26:21;118:368:20;;;;;;;3283:80:21;3347:16;;;:::i;:::-;118:368:20;;;;;;;;;3225:10:21;;118:368:20;-1:-1:-1;118:368:20;;;;-1:-1:-1;118:368:20;;3283:80:21;;;;118:368:20;;;;;;;;;;;;1304:26:21;118:368:20;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;2561:10:21;;118:368:20;;;;;;;;;;;-1:-1:-1;118:368:20;;;;;-1:-1:-1;118:368:20;;;;;;;;2561:10:21;2606:37;118:368:20;2561:10:21;2606:37;;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;-1:-1:-1;118:368:20;;;;;;;-1:-1:-1;118:368:20;;-1:-1:-1;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;-1:-1:-1;118:368:20;;;;;-1:-1:-1;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;:::o;:::-;;;;;;;;;;;:::o;:::-;;;;;;;;;;:::o;5327:177:21:-;5410:13;5427:16;5410:33;5427:16;;5446:24;5327:177;:::o;5410:87::-;118:368:20;;-1:-1:-1;118:368:20;-1:-1:-1;118:368:20;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;5789:22:21;;118:368:20;;;5640:295:21;;118:368:20;5672:95:21;118:368:20;;;;;;5833:14:21;118:368:20;;;;5410:13:21;118:368:20;;;;5912:4:21;118:368:20;;;;;5640:295:21;;;;;;:::i;:::-;118:368:20;5613:336:21;;5327:177;:::o;118:368:20:-;-1:-1:-1;;118:368:20;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;118:368:20;;;;;;;;;;;-1:-1:-1;118:368:20;","linkReferences":{},"immutableReferences":{"42052":[{"start":2149,"length":32}],"42066":[{"start":3240,"length":32}],"42068":[{"start":3278,"length":32}]}},"methodIdentifiers":{"DOMAIN_SEPARATOR()":"3644e515","allowance(address,address)":"dd62ed3e","approve(address,uint256)":"095ea7b3","balanceOf(address)":"70a08231","burn(address,uint256)":"9dc29fac","decimals()":"313ce567","mint(address,uint256)":"40c10f19","name()":"06fdde03","nonces(address)":"7ecebe00","permit(address,address,uint256,uint256,uint8,bytes32,bytes32)":"d505accf","symbol()":"95d89b41","totalSupply()":"18160ddd","transfer(address,uint256)":"a9059cbb","transferFrom(address,address,uint256)":"23b872dd"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.26+commit.8a97fa7a\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"string\",\"name\":\"_name\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"_symbol\",\"type\":\"string\"},{\"internalType\":\"uint8\",\"name\":\"_decimals\",\"type\":\"uint8\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"DOMAIN_SEPARATOR\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"burn\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"decimals\",\"outputs\":[{\"internalType\":\"uint8\",\"name\":\"\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"mint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"name\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"nonces\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"},{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"permit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"symbol\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"lib/v4-core/lib/solmate/src/test/utils/mocks/MockERC20.sol\":\"MockERC20\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":true,\"runs\":44444444},\"remappings\":[\"/tmp/opencode/gasbench/lib/v4-core/lib/openzeppelin-contracts/lib/forge-std/:ds-test/=lib/v4-core/lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/\",\"/tmp/opencode/gasbench/lib/v4-core/lib/solmate/:ds-test/=lib/v4-core/lib/solmate/lib/ds-test/src/\",\":@ensdomains/=lib/v4-core/node_modules/@ensdomains/\",\":@openzeppelin/=lib/v4-core/lib/openzeppelin-contracts/\",\":ds-test/=lib/v4-core/lib/forge-std/lib/ds-test/src/\",\":erc4626-tests/=lib/v4-core/lib/openzeppelin-contracts/lib/erc4626-tests/\",\":forge-std/=lib/forge-std/src/\",\":hardhat/=lib/v4-core/node_modules/hardhat/\",\":openzeppelin-contracts/=lib/v4-core/lib/openzeppelin-contracts/\",\":solady/=lib/solady/src/\",\":solmate/=lib/v4-core/lib/solmate/\",\":v4-core/=lib/v4-core/\",\":v4-src/=lib/v4-core/src/\",\"lib/v4-core/lib/openzeppelin-contracts/lib/forge-std/:ds-test/=lib/v4-core/lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/\",\"lib/v4-core/lib/solmate/:ds-test/=lib/v4-core/lib/solmate/lib/ds-test/src/\"],\"viaIR\":true},\"sources\":{\"lib/v4-core/lib/solmate/src/test/utils/mocks/MockERC20.sol\":{\"keccak256\":\"0x525129ccb4103c9ecb8f235618d2ff4a8ffde4b8654f7c17766ad175d10ef466\",\"license\":\"AGPL-3.0-only\",\"urls\":[\"bzz-raw://45692a21807ba815c0cf0bfc7d633a14a6cb9ba863ab53f18fc639375a72125e\",\"dweb:/ipfs/QmSrqxyodjDtSU1Dc6G3ScnNc9CFjMnAeHpK8a8guM3nJu\"]},\"lib/v4-core/lib/solmate/src/tokens/ERC20.sol\":{\"keccak256\":\"0xcdfd8db76b2a3415620e4d18cc5545f3d50de792dbf2c3dd5adb40cbe6f94b10\",\"license\":\"AGPL-3.0-only\",\"urls\":[\"bzz-raw://57b3ab70cde374af1cf2c9888636e8de6cf660f087b1c9abd805e9271e19fa35\",\"dweb:/ipfs/QmNrLDBAHYFjpjSd12jerm1AdBkDqEYUUaXgnT854BUZ97\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.26+commit.8a97fa7a"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"string","name":"_name","type":"string"},{"internalType":"string","name":"_symbol","type":"string"},{"internalType":"uint8","name":"_decimals","type":"uint8"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"owner","type":"address","indexed":true},{"internalType":"address","name":"spender","type":"address","indexed":true},{"internalType":"uint256","name":"amount","type":"uint256","indexed":false}],"type":"event","name":"Approval","anonymous":false},{"inputs":[{"internalType":"address","name":"from","type":"address","indexed":true},{"internalType":"address","name":"to","type":"address","indexed":true},{"internalType":"uint256","name":"amount","type":"uint256","indexed":false}],"type":"event","name":"Transfer","anonymous":false},{"inputs":[],"stateMutability":"view","type":"function","name":"DOMAIN_SEPARATOR","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}]},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function","name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}]},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"stateMutability":"nonpayable","type":"function","name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}]},{"inputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function","name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}]},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"stateMutability":"nonpayable","type":"function","name":"burn"},{"inputs":[],"stateMutability":"view","type":"function","name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}]},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"stateMutability":"nonpayable","type":"function","name":"mint"},{"inputs":[],"stateMutability":"view","type":"function","name":"name","outputs":[{"internalType":"string","name":"","type":"string"}]},{"inputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function","name":"nonces","outputs":[{"internalType":"uint256","name":"","type":"uint256"}]},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"stateMutability":"nonpayable","type":"function","name":"permit"},{"inputs":[],"stateMutability":"view","type":"function","name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}]},{"inputs":[],"stateMutability":"view","type":"function","name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}]},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"stateMutability":"nonpayable","type":"function","name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}]},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"stateMutability":"nonpayable","type":"function","name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}]}],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["/tmp/opencode/gasbench/lib/v4-core/lib/openzeppelin-contracts/lib/forge-std/:ds-test/=lib/v4-core/lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/","/tmp/opencode/gasbench/lib/v4-core/lib/solmate/:ds-test/=lib/v4-core/lib/solmate/lib/ds-test/src/","@ensdomains/=lib/v4-core/node_modules/@ensdomains/","@openzeppelin/=lib/v4-core/lib/openzeppelin-contracts/","ds-test/=lib/v4-core/lib/forge-std/lib/ds-test/src/","erc4626-tests/=lib/v4-core/lib/openzeppelin-contracts/lib/erc4626-tests/","forge-std/=lib/forge-std/src/","hardhat/=lib/v4-core/node_modules/hardhat/","openzeppelin-contracts/=lib/v4-core/lib/openzeppelin-contracts/","solady/=lib/solady/src/","solmate/=lib/v4-core/lib/solmate/","v4-core/=lib/v4-core/","v4-src/=lib/v4-core/src/","lib/v4-core/lib/openzeppelin-contracts/lib/forge-std/:ds-test/=lib/v4-core/lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/","lib/v4-core/lib/solmate/:ds-test/=lib/v4-core/lib/solmate/lib/ds-test/src/"],"optimizer":{"enabled":true,"runs":44444444},"metadata":{"bytecodeHash":"ipfs"},"compilationTarget":{"lib/v4-core/lib/solmate/src/test/utils/mocks/MockERC20.sol":"MockERC20"},"evmVersion":"cancun","libraries":{},"viaIR":true},"sources":{"lib/v4-core/lib/solmate/src/test/utils/mocks/MockERC20.sol":{"keccak256":"0x525129ccb4103c9ecb8f235618d2ff4a8ffde4b8654f7c17766ad175d10ef466","urls":["bzz-raw://45692a21807ba815c0cf0bfc7d633a14a6cb9ba863ab53f18fc639375a72125e","dweb:/ipfs/QmSrqxyodjDtSU1Dc6G3ScnNc9CFjMnAeHpK8a8guM3nJu"],"license":"AGPL-3.0-only"},"lib/v4-core/lib/solmate/src/tokens/ERC20.sol":{"keccak256":"0xcdfd8db76b2a3415620e4d18cc5545f3d50de792dbf2c3dd5adb40cbe6f94b10","urls":["bzz-raw://57b3ab70cde374af1cf2c9888636e8de6cf660f087b1c9abd805e9271e19fa35","dweb:/ipfs/QmNrLDBAHYFjpjSd12jerm1AdBkDqEYUUaXgnT854BUZ97"],"license":"AGPL-3.0-only"}},"version":1},"id":20} \ No newline at end of file diff --git a/benchmarks/prod/calldata/ekubo-minimal-1pool.hex b/benchmarks/prod/calldata/ekubo-minimal-1pool.hex new file mode 100644 index 0000000..0c2bd17 --- /dev/null +++ b/benchmarks/prod/calldata/ekubo-minimal-1pool.hex @@ -0,0 +1 @@ +0xb2887f8a000000000000000000000000000000000000000000000000000000000000008000000000000000000000000011111111111111111111111111111111111111110000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000000000000100000000000000000000000011111111111111111111111111111111111111110000000000000000000000002222222222222222222222222222222222222222000000000000000000000000000000000000000000c49ba5e353f7ce80001770 \ No newline at end of file diff --git a/benchmarks/prod/calldata/ekubo-minimal-2pools.hex b/benchmarks/prod/calldata/ekubo-minimal-2pools.hex new file mode 100644 index 0000000..cfd3cf9 --- /dev/null +++ b/benchmarks/prod/calldata/ekubo-minimal-2pools.hex @@ -0,0 +1 @@ +0xb2887f8a000000000000000000000000000000000000000000000000000000000000008000000000000000000000000011111111111111111111111111111111111111110000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000000000000200000000000000000000000011111111111111111111111111111111111111110000000000000000000000002222222222222222222222222222222222222222000000000000000000000000000000000000000000c49ba5e353f7ce8000177000000000000000000000000022222222222222222222222222222222222222220000000000000000000000003333333333333333333333333333333333333333000000000000000000000000000000000000000000c49ba5e353f7ce80001770 \ No newline at end of file diff --git a/benchmarks/prod/calldata/ekubo-minimal-3pools.hex b/benchmarks/prod/calldata/ekubo-minimal-3pools.hex new file mode 100644 index 0000000..e6e9a76 --- /dev/null +++ b/benchmarks/prod/calldata/ekubo-minimal-3pools.hex @@ -0,0 +1 @@ +0xb2887f8a000000000000000000000000000000000000000000000000000000000000008000000000000000000000000011111111111111111111111111111111111111110000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000000000000300000000000000000000000011111111111111111111111111111111111111110000000000000000000000002222222222222222222222222222222222222222000000000000000000000000000000000000000000c49ba5e353f7ce8000177000000000000000000000000022222222222222222222222222222222222222220000000000000000000000003333333333333333333333333333333333333333000000000000000000000000000000000000000000c49ba5e353f7ce8000177000000000000000000000000033333333333333333333333333333333333333330000000000000000000000004444444444444444444444444444444444444444000000000000000000000000000000000000000000c49ba5e353f7ce80001770 \ No newline at end of file diff --git a/benchmarks/prod/calldata/ekubo-minimal-native.hex b/benchmarks/prod/calldata/ekubo-minimal-native.hex new file mode 100644 index 0000000..b6dadc5 --- /dev/null +++ b/benchmarks/prod/calldata/ekubo-minimal-native.hex @@ -0,0 +1 @@ +0xb2887f8a000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000002222222222222222222222222222222222222222000000000000000000000000000000000000000000c49ba5e353f7ce80001770 \ No newline at end of file diff --git a/benchmarks/prod/calldata/ekubo-yul-1pool.hex b/benchmarks/prod/calldata/ekubo-yul-1pool.hex new file mode 100644 index 0000000..dc74bec --- /dev/null +++ b/benchmarks/prod/calldata/ekubo-yul-1pool.hex @@ -0,0 +1 @@ +0x01001111111111111111111111111111111111111111222222222222222222222222222222222222222200000000000000000c7d713b49da00007fa9385be102ac3eac297483dd6233d62b3e149600000000000000000de0b6b3a7640000000011111111111111111111111111111111111111112222222222222222222222222222222222222222000000000000000000000000000000000000000000c49ba5e353f7ce8000177000000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/prod/calldata/ekubo-yul-2pools.hex b/benchmarks/prod/calldata/ekubo-yul-2pools.hex new file mode 100644 index 0000000..343704e --- /dev/null +++ b/benchmarks/prod/calldata/ekubo-yul-2pools.hex @@ -0,0 +1 @@ +0x01001111111111111111111111111111111111111111333333333333333333333333333333333333333300000000000000000c7d713b49da00007fa9385be102ac3eac297483dd6233d62b3e149600000000000000000de0b6b3a7640000010011111111111111111111111111111111111111112222222222222222222222222222222222222222000000000000000000000000000000000000000000c49ba5e353f7ce80001770000000000000000000000000000000000022222222222222222222222222222222222222223333333333333333333333333333333333333333000000000000000000000000000000000000000000c49ba5e353f7ce8000177000000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/prod/calldata/ekubo-yul-3pools.hex b/benchmarks/prod/calldata/ekubo-yul-3pools.hex new file mode 100644 index 0000000..5145863 --- /dev/null +++ b/benchmarks/prod/calldata/ekubo-yul-3pools.hex @@ -0,0 +1 @@ +0x01001111111111111111111111111111111111111111444444444444444444444444444444444444444400000000000000000c7d713b49da00007fa9385be102ac3eac297483dd6233d62b3e149600000000000000000de0b6b3a7640000020011111111111111111111111111111111111111112222222222222222222222222222222222222222000000000000000000000000000000000000000000c49ba5e353f7ce80001770000000000000000000000000000000000022222222222222222222222222222222222222223333333333333333333333333333333333333333000000000000000000000000000000000000000000c49ba5e353f7ce80001770000000000000000000000000000000000033333333333333333333333333333333333333334444444444444444444444444444444444444444000000000000000000000000000000000000000000c49ba5e353f7ce8000177000000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/prod/calldata/ekubo-yul-native.hex b/benchmarks/prod/calldata/ekubo-yul-native.hex new file mode 100644 index 0000000..7dcb58e --- /dev/null +++ b/benchmarks/prod/calldata/ekubo-yul-native.hex @@ -0,0 +1 @@ +0x01000000000000000000000000000000000000000000222222222222222222222222222222222222222200000000000000000c7d713b49da00007fa9385be102ac3eac297483dd6233d62b3e149600000000000000000de0b6b3a7640000000000000000000000000000000000000000000000002222222222222222222222222222222222222222000000000000000000000000000000000000000000c49ba5e353f7ce8000177000000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/prod/calldata/v3-minimal-1pool.hex b/benchmarks/prod/calldata/v3-minimal-1pool.hex new file mode 100644 index 0000000..eeef9af --- /dev/null +++ b/benchmarks/prod/calldata/v3-minimal-1pool.hex @@ -0,0 +1 @@ +0x0b1d6cab000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e14960000000000000000000000000000000000000000000000000000000000000001000000000000000000000000550e26c349515b8c85306c603a1a0bd6a124712600000000000000000000000000000000000000000000000000000000000000010000000000000000000000001111111111111111111111111111111111111111 \ No newline at end of file diff --git a/benchmarks/prod/calldata/v3-minimal-2pools.hex b/benchmarks/prod/calldata/v3-minimal-2pools.hex new file mode 100644 index 0000000..0748c45 --- /dev/null +++ b/benchmarks/prod/calldata/v3-minimal-2pools.hex @@ -0,0 +1 @@ +0x0b1d6cab000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e14960000000000000000000000000000000000000000000000000000000000000002000000000000000000000000550e26c349515b8c85306c603a1a0bd6a12471260000000000000000000000007a3b5ec5c068ecbf8a87e30b3ed57243b888f2dc000000000000000000000000000000000000000000000000000000000000000200000000000000000000000011111111111111111111111111111111111111110000000000000000000000002222222222222222222222222222222222222222 \ No newline at end of file diff --git a/benchmarks/prod/calldata/v3-minimal-3pools.hex b/benchmarks/prod/calldata/v3-minimal-3pools.hex new file mode 100644 index 0000000..705440a --- /dev/null +++ b/benchmarks/prod/calldata/v3-minimal-3pools.hex @@ -0,0 +1 @@ +0x0b1d6cab000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e14960000000000000000000000000000000000000000000000000000000000000003000000000000000000000000550e26c349515b8c85306c603a1a0bd6a12471260000000000000000000000007a3b5ec5c068ecbf8a87e30b3ed57243b888f2dc00000000000000000000000091df1e818bc3ebcab7840fa87ea7517d2a6729b90000000000000000000000000000000000000000000000000000000000000003000000000000000000000000111111111111111111111111111111111111111100000000000000000000000022222222222222222222222222222222222222220000000000000000000000003333333333333333333333333333333333333333 \ No newline at end of file diff --git a/benchmarks/prod/calldata/v3-swaprouter-1pool.hex b/benchmarks/prod/calldata/v3-swaprouter-1pool.hex new file mode 100644 index 0000000..c4c27f1 --- /dev/null +++ b/benchmarks/prod/calldata/v3-swaprouter-1pool.hex @@ -0,0 +1 @@ +0x414bf389000000000000000000000000111111111111111111111111111111111111111100000000000000000000000022222222222222222222222222222222222222220000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/prod/calldata/v3-swaprouter-2pools.hex b/benchmarks/prod/calldata/v3-swaprouter-2pools.hex new file mode 100644 index 0000000..053b217 --- /dev/null +++ b/benchmarks/prod/calldata/v3-swaprouter-2pools.hex @@ -0,0 +1 @@ +0xc04b8d59000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000421111111111111111111111111111111111111111000bb82222222222222222222222222222222222222222000bb83333333333333333333333333333333333333333000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/prod/calldata/v3-swaprouter-3pools.hex b/benchmarks/prod/calldata/v3-swaprouter-3pools.hex new file mode 100644 index 0000000..51e9a94 --- /dev/null +++ b/benchmarks/prod/calldata/v3-swaprouter-3pools.hex @@ -0,0 +1 @@ +0xc04b8d59000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000591111111111111111111111111111111111111111000bb82222222222222222222222222222222222222222000bb83333333333333333333333333333333333333333000bb8444444444444444444444444444444444444444400000000000000 \ No newline at end of file diff --git a/benchmarks/prod/calldata/v3-swaprouter-native.hex b/benchmarks/prod/calldata/v3-swaprouter-native.hex new file mode 100644 index 0000000..b81cf6c --- /dev/null +++ b/benchmarks/prod/calldata/v3-swaprouter-native.hex @@ -0,0 +1 @@ +0x414bf389000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000022222222222222222222222222222222222222220000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/prod/calldata/v4-minimal-1pool.hex b/benchmarks/prod/calldata/v4-minimal-1pool.hex new file mode 100644 index 0000000..1fbc5de --- /dev/null +++ b/benchmarks/prod/calldata/v4-minimal-1pool.hex @@ -0,0 +1 @@ +0x19305657000000000000000000000000000000000000000000000000000000000000008000000000000000000000000011111111111111111111111111111111111111110000000000000000000000002222222222222222222222222222222222222222fffffffffffffffffffffffffffffffffffffffffffffffff21f494c589c00000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000111111111111111111111111111111111111111100000000000000000000000022222222222222222222222222222222222222220000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000276a4 \ No newline at end of file diff --git a/benchmarks/prod/calldata/v4-minimal-2pools.hex b/benchmarks/prod/calldata/v4-minimal-2pools.hex new file mode 100644 index 0000000..b20da50 --- /dev/null +++ b/benchmarks/prod/calldata/v4-minimal-2pools.hex @@ -0,0 +1 @@ +0x19305657000000000000000000000000000000000000000000000000000000000000008000000000000000000000000011111111111111111111111111111111111111110000000000000000000000003333333333333333333333333333333333333333fffffffffffffffffffffffffffffffffffffffffffffffff21f494c589c00000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000111111111111111111111111111111111111111100000000000000000000000022222222222222222222222222222222222222220000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000276a4000000000000000000000000222222222222222222222222222222222222222200000000000000000000000033333333333333333333333333333333333333330000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000276a4 \ No newline at end of file diff --git a/benchmarks/prod/calldata/v4-minimal-3pools.hex b/benchmarks/prod/calldata/v4-minimal-3pools.hex new file mode 100644 index 0000000..5a86032 --- /dev/null +++ b/benchmarks/prod/calldata/v4-minimal-3pools.hex @@ -0,0 +1 @@ +0x19305657000000000000000000000000000000000000000000000000000000000000008000000000000000000000000011111111111111111111111111111111111111110000000000000000000000004444444444444444444444444444444444444444fffffffffffffffffffffffffffffffffffffffffffffffff21f494c589c00000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000111111111111111111111111111111111111111100000000000000000000000022222222222222222222222222222222222222220000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000276a4000000000000000000000000222222222222222222222222222222222222222200000000000000000000000033333333333333333333333333333333333333330000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000276a4000000000000000000000000333333333333333333333333333333333333333300000000000000000000000044444444444444444444444444444444444444440000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000276a4 \ No newline at end of file diff --git a/benchmarks/prod/calldata/v4-minimal-native.hex b/benchmarks/prod/calldata/v4-minimal-native.hex new file mode 100644 index 0000000..6ecf1c0 --- /dev/null +++ b/benchmarks/prod/calldata/v4-minimal-native.hex @@ -0,0 +1 @@ +0x19305657000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002222222222222222222222222222222222222222fffffffffffffffffffffffffffffffffffffffffffffffff21f494c589c00000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022222222222222222222222222222222222222220000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000276a4 \ No newline at end of file diff --git a/benchmarks/prod/calldata/v4-universal-router-1pool.hex b/benchmarks/prod/calldata/v4-universal-router-1pool.hex new file mode 100644 index 0000000..2c18b30 --- /dev/null +++ b/benchmarks/prod/calldata/v4-universal-router-1pool.hex @@ -0,0 +1 @@ +0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000340000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003060c0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111111111111111111111111111111111111100000000000000000000000022222222222222222222222222222222222222220000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000011111111111111111111111111111111111111110000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000022222222222222222222222222222222222222220000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/benchmarks/prod/encode-prod-routes.mjs b/benchmarks/prod/encode-prod-routes.mjs new file mode 100644 index 0000000..d9b611c --- /dev/null +++ b/benchmarks/prod/encode-prod-routes.mjs @@ -0,0 +1,52 @@ +// Generates the Yul-router calldata for ProdEkubo.t.sol with the yul-router SDK. +// Token addresses and the pool config are the constants in test/ProdCommon.sol and +// test/ProdEkubo.t.sol; the recipient is the forge test contract address. +// Run: bun encode-prod-routes.mjs (writes calldata/ekubo-yul-*.hex) +import { writeFileSync } from "node:fs"; +import { encodeRoute } from "/home/sendmoodz/Work/ekubo/yul-router/sdk/src/index.ts"; + +const A = "0x1111111111111111111111111111111111111111"; +const B = "0x2222222222222222222222222222222222222222"; +const C = "0x3333333333333333333333333333333333333333"; +const D = "0x4444444444444444444444444444444444444444"; +const NATIVE = "0x0000000000000000000000000000000000000000"; +const config = + "0x000000000000000000000000000000000000000000c49ba5e353f7ce80001770"; // 0.3%, spacing 6000, no extension +const recipient = "0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496"; +const hop = (token0, token1) => ({ + type: "core", + poolKey: { token0, token1, config }, +}); + +const routes = { + "ekubo-yul-1pool": { + specifiedToken: A, + calculatedToken: B, + hops: [hop(A, B)], + }, + "ekubo-yul-2pools": { + specifiedToken: A, + calculatedToken: C, + hops: [hop(A, B), hop(B, C)], + }, + "ekubo-yul-3pools": { + specifiedToken: A, + calculatedToken: D, + hops: [hop(A, B), hop(B, C), hop(C, D)], + }, + "ekubo-yul-native": { + specifiedToken: NATIVE, + calculatedToken: B, + hops: [hop(NATIVE, B)], + }, +}; +for (const [file, r] of Object.entries(routes)) { + const data = encodeRoute({ + ...r, + specifiedAmount: 1000000000000000000n, + calculatedAmountThreshold: 900000000000000000n, // 90% of input: 0.3% fee per hop cannot trip it + recipient, + }); + writeFileSync(`calldata/${file}.hex`, data); + console.log(file, (data.length - 2) / 2, "bytes"); +} diff --git a/benchmarks/prod/foundry.toml b/benchmarks/prod/foundry.toml new file mode 100644 index 0000000..d9af8b5 --- /dev/null +++ b/benchmarks/prod/foundry.toml @@ -0,0 +1,16 @@ +[profile.default] +src = "src" +test = "test" +out = "out" +libs = ["lib"] +# No `solc` pin: foundry auto-detects per file (=0.8.26 for the Uniswap-side files, +# =0.8.33 for the Ekubo sources). A pin would force one version on everything. +optimizer = true +optimizer_runs = 1000000 +via_ir = true +evm_version = "osaka" +verbosity = 2 +fs_permissions = [ + { access = "read", path = "./artifacts" }, + { access = "read-write", path = "./calldata" }, +] diff --git a/benchmarks/prod/remappings.txt b/benchmarks/prod/remappings.txt new file mode 100644 index 0000000..4272ddc --- /dev/null +++ b/benchmarks/prod/remappings.txt @@ -0,0 +1,5 @@ +forge-std/=lib/forge-std/src/ +solady/=lib/solady/src/ +solmate/=lib/v4-core/lib/solmate/ +v4-core/=lib/v4-core/ +ekubo/=lib/ekubo/ diff --git a/benchmarks/prod/run.sh b/benchmarks/prod/run.sh new file mode 100755 index 0000000..30acd5e --- /dev/null +++ b/benchmarks/prod/run.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh +# Runs the production-contract bench on a fork of the latest mainnet block. +# RPC=https://eth.drpc.org ./run.sh # latest block (default) +# RPC=... BLOCK=25999935 ./run.sh # reproduce a recorded run +# Dependencies (lib/, git-ignored): forge-std, solady, EkuboProtocol/evm-contracts at +# v3.2.0 (as lib/ekubo, with its lib/forge-std and lib/solady), Uniswap/v4-core (with its +# lib/solmate). Routes: `bun encode-prod-routes.mjs` regenerates calldata/ekubo-yul-*.hex. +set -eu +RPC="${RPC:-https://eth.drpc.org}" +BLOCK="${BLOCK:-$(cast block-number -r "$RPC")}" +echo "fork block $BLOCK" +forge test --fork-url "$RPC" --fork-block-number "$BLOCK" -vv "$@" diff --git a/benchmarks/prod/snapshots/ProdEkuboTest.json b/benchmarks/prod/snapshots/ProdEkuboTest.json new file mode 100644 index 0000000..cac0388 --- /dev/null +++ b/benchmarks/prod/snapshots/ProdEkuboTest.json @@ -0,0 +1,52 @@ +{ + "prod ekubo Yul router 1 pool A->B": "87663", + "prod ekubo Yul router 1 pool A->B | calldata bytes": "184", + "prod ekubo Yul router 1 pool A->B | calldata floor gas (EIP-7623, 10 per token)": "5530", + "prod ekubo Yul router 1 pool A->B | calldata gas (4/16 per byte)": "2212", + "prod ekubo Yul router 1 pool A->B | fine ticks moved": "2", + "prod ekubo Yul router 1 pool A->B | output (wei)": "1000042490223401887", + "prod ekubo Yul router 1 pool ETH->B": "73764", + "prod ekubo Yul router 1 pool ETH->B | calldata bytes": "184", + "prod ekubo Yul router 1 pool ETH->B | calldata floor gas (EIP-7623, 10 per token)": "4330", + "prod ekubo Yul router 1 pool ETH->B | calldata gas (4/16 per byte)": "1732", + "prod ekubo Yul router 1 pool ETH->B | fine ticks moved": "2", + "prod ekubo Yul router 1 pool ETH->B | output (wei)": "1000042490223401887", + "prod ekubo Yul router 2 pools A->B->C": "109850", + "prod ekubo Yul router 2 pools A->B->C | calldata bytes": "273", + "prod ekubo Yul router 2 pools A->B->C | calldata floor gas (EIP-7623, 10 per token)": "7950", + "prod ekubo Yul router 2 pools A->B->C | calldata gas (4/16 per byte)": "3180", + "prod ekubo Yul router 2 pools A->B->C | fine ticks moved": "2", + "prod ekubo Yul router 2 pools A->B->C | output (wei)": "1000084982120610903", + "prod ekubo Yul router 3 pools A->B->C->D": "132025", + "prod ekubo Yul router 3 pools A->B->C->D | calldata bytes": "362", + "prod ekubo Yul router 3 pools A->B->C->D | calldata floor gas (EIP-7623, 10 per token)": "10340", + "prod ekubo Yul router 3 pools A->B->C->D | calldata gas (4/16 per byte)": "4136", + "prod ekubo Yul router 3 pools A->B->C->D | fine ticks moved": "2", + "prod ekubo Yul router 3 pools A->B->C->D | output (wei)": "1000127475691875278", + "prod ekubo minimal locker 1 pool A->B": "89733", + "prod ekubo minimal locker 1 pool A->B | calldata bytes": "260", + "prod ekubo minimal locker 1 pool A->B | calldata floor gas (EIP-7623, 10 per token)": "5660", + "prod ekubo minimal locker 1 pool A->B | calldata gas (4/16 per byte)": "2264", + "prod ekubo minimal locker 1 pool A->B | fine ticks moved": "2", + "prod ekubo minimal locker 1 pool A->B | output (wei)": "1000042490223401887", + "prod ekubo minimal locker 1 pool ETH->B": "74585", + "prod ekubo minimal locker 1 pool ETH->B | calldata bytes": "260", + "prod ekubo minimal locker 1 pool ETH->B | calldata floor gas (EIP-7623, 10 per token)": "4460", + "prod ekubo minimal locker 1 pool ETH->B | calldata gas (4/16 per byte)": "1784", + "prod ekubo minimal locker 1 pool ETH->B | fine ticks moved": "2", + "prod ekubo minimal locker 1 pool ETH->B | output (wei)": "1000042490223401887", + "prod ekubo minimal locker 2 pools A->B->C": "112843", + "prod ekubo minimal locker 2 pools A->B->C | calldata bytes": "356", + "prod ekubo minimal locker 2 pools A->B->C | calldata floor gas (EIP-7623, 10 per token)": "8120", + "prod ekubo minimal locker 2 pools A->B->C | calldata gas (4/16 per byte)": "3248", + "prod ekubo minimal locker 2 pools A->B->C | fine ticks moved": "2", + "prod ekubo minimal locker 2 pools A->B->C | output (wei)": "1000084982120610903", + "prod ekubo minimal locker 3 pools A->B->C->D": "135952", + "prod ekubo minimal locker 3 pools A->B->C->D | calldata bytes": "452", + "prod ekubo minimal locker 3 pools A->B->C->D | calldata floor gas (EIP-7623, 10 per token)": "10580", + "prod ekubo minimal locker 3 pools A->B->C->D | calldata gas (4/16 per byte)": "4232", + "prod ekubo minimal locker 3 pools A->B->C->D | fine ticks moved": "2", + "prod ekubo minimal locker 3 pools A->B->C->D | output (wei)": "1000127475691875278", + "prod fork block number": "25999935", + "prod fork block timestamp": "1789680959" +} \ No newline at end of file diff --git a/benchmarks/prod/snapshots/ProdV3Test.json b/benchmarks/prod/snapshots/ProdV3Test.json new file mode 100644 index 0000000..17fdc7b --- /dev/null +++ b/benchmarks/prod/snapshots/ProdV3Test.json @@ -0,0 +1,46 @@ +{ + "prod fork block number": "25999935", + "prod fork block timestamp": "1789680959", + "prod v3 SwapRouter exactInput 2 pools A->B->C": "165719", + "prod v3 SwapRouter exactInput 2 pools A->B->C | calldata bytes": "324", + "prod v3 SwapRouter exactInput 2 pools A->B->C | calldata floor gas (EIP-7623, 10 per token)": "7110", + "prod v3 SwapRouter exactInput 2 pools A->B->C | calldata gas (4/16 per byte)": "2844", + "prod v3 SwapRouter exactInput 2 pools A->B->C | output (wei)": "999984682148650249", + "prod v3 SwapRouter exactInput 2 pools A->B->C | ticks moved": "0", + "prod v3 SwapRouter exactInput 3 pools A->B->C->D": "230478", + "prod v3 SwapRouter exactInput 3 pools A->B->C->D | calldata bytes": "324", + "prod v3 SwapRouter exactInput 3 pools A->B->C->D | calldata floor gas (EIP-7623, 10 per token)": "7770", + "prod v3 SwapRouter exactInput 3 pools A->B->C->D | calldata gas (4/16 per byte)": "3108", + "prod v3 SwapRouter exactInput 3 pools A->B->C->D | output (wei)": "999977023339428738", + "prod v3 SwapRouter exactInput 3 pools A->B->C->D | ticks moved": "0", + "prod v3 SwapRouter exactInputSingle 1 pool A->B": "100299", + "prod v3 SwapRouter exactInputSingle 1 pool A->B | calldata bytes": "260", + "prod v3 SwapRouter exactInputSingle 1 pool A->B | calldata floor gas (EIP-7623, 10 per token)": "5720", + "prod v3 SwapRouter exactInputSingle 1 pool A->B | calldata gas (4/16 per byte)": "2288", + "prod v3 SwapRouter exactInputSingle 1 pool A->B | output (wei)": "999992341035506990", + "prod v3 SwapRouter exactInputSingle 1 pool A->B | ticks moved": "0", + "prod v3 SwapRouter exactInputSingle 1 pool ETH(wrapped in flight)->B": "103504", + "prod v3 SwapRouter exactInputSingle 1 pool ETH(wrapped in flight)->B | calldata bytes": "260", + "prod v3 SwapRouter exactInputSingle 1 pool ETH(wrapped in flight)->B | calldata floor gas (EIP-7623, 10 per token)": "5720", + "prod v3 SwapRouter exactInputSingle 1 pool ETH(wrapped in flight)->B | calldata gas (4/16 per byte)": "2288", + "prod v3 SwapRouter exactInputSingle 1 pool ETH(wrapped in flight)->B | output (wei)": "994010658021103223", + "prod v3 SwapRouter exactInputSingle 1 pool ETH(wrapped in flight)->B | ticks moved": "0", + "prod v3 minimal router 1 pool A->B": "94078", + "prod v3 minimal router 1 pool A->B | calldata bytes": "260", + "prod v3 minimal router 1 pool A->B | calldata floor gas (EIP-7623, 10 per token)": "4820", + "prod v3 minimal router 1 pool A->B | calldata gas (4/16 per byte)": "1928", + "prod v3 minimal router 1 pool A->B | output (wei)": "999992341035506990", + "prod v3 minimal router 1 pool A->B | ticks moved": "0", + "prod v3 minimal router 2 pools A->B->C": "152774", + "prod v3 minimal router 2 pools A->B->C | calldata bytes": "324", + "prod v3 minimal router 2 pools A->B->C | calldata floor gas (EIP-7623, 10 per token)": "6660", + "prod v3 minimal router 2 pools A->B->C | calldata gas (4/16 per byte)": "2664", + "prod v3 minimal router 2 pools A->B->C | output (wei)": "999984682148650249", + "prod v3 minimal router 2 pools A->B->C | ticks moved": "0", + "prod v3 minimal router 3 pools A->B->C->D": "211470", + "prod v3 minimal router 3 pools A->B->C->D | calldata bytes": "388", + "prod v3 minimal router 3 pools A->B->C->D | calldata floor gas (EIP-7623, 10 per token)": "8500", + "prod v3 minimal router 3 pools A->B->C->D | calldata gas (4/16 per byte)": "3400", + "prod v3 minimal router 3 pools A->B->C->D | output (wei)": "999977023339428738", + "prod v3 minimal router 3 pools A->B->C->D | ticks moved": "0" +} \ No newline at end of file diff --git a/benchmarks/prod/snapshots/ProdV4Test.json b/benchmarks/prod/snapshots/ProdV4Test.json new file mode 100644 index 0000000..7f74f4d --- /dev/null +++ b/benchmarks/prod/snapshots/ProdV4Test.json @@ -0,0 +1,34 @@ +{ + "prod fork block number": "25999935", + "prod fork block timestamp": "1789680959", + "prod v4 Universal Router V4_SWAP 1 pool A->B (Permit2)": "117114", + "prod v4 Universal Router V4_SWAP 1 pool A->B (Permit2) | calldata bytes": "1092", + "prod v4 Universal Router V4_SWAP 1 pool A->B (Permit2) | calldata floor gas (EIP-7623, 10 per token)": "15690", + "prod v4 Universal Router V4_SWAP 1 pool A->B (Permit2) | calldata gas (4/16 per byte)": "6276", + "prod v4 Universal Router V4_SWAP 1 pool A->B (Permit2) | output (wei)": "999992341035507518", + "prod v4 Universal Router V4_SWAP 1 pool A->B (Permit2) | ticks moved": "0", + "prod v4 minimal locker 1 pool A->B": "107384", + "prod v4 minimal locker 1 pool A->B | calldata bytes": "356", + "prod v4 minimal locker 1 pool A->B | calldata floor gas (EIP-7623, 10 per token)": "7250", + "prod v4 minimal locker 1 pool A->B | calldata gas (4/16 per byte)": "2900", + "prod v4 minimal locker 1 pool A->B | output (wei)": "999992341035507518", + "prod v4 minimal locker 1 pool A->B | ticks moved": "0", + "prod v4 minimal locker 1 pool ETH->B": "92472", + "prod v4 minimal locker 1 pool ETH->B | calldata bytes": "356", + "prod v4 minimal locker 1 pool ETH->B | calldata floor gas (EIP-7623, 10 per token)": "6050", + "prod v4 minimal locker 1 pool ETH->B | calldata gas (4/16 per byte)": "2420", + "prod v4 minimal locker 1 pool ETH->B | output (wei)": "999992341035507518", + "prod v4 minimal locker 1 pool ETH->B | ticks moved": "0", + "prod v4 minimal locker 2 pools A->B->C": "141795", + "prod v4 minimal locker 2 pools A->B->C | calldata bytes": "548", + "prod v4 minimal locker 2 pools A->B->C | calldata floor gas (EIP-7623, 10 per token)": "10580", + "prod v4 minimal locker 2 pools A->B->C | calldata gas (4/16 per byte)": "4232", + "prod v4 minimal locker 2 pools A->B->C | output (wei)": "999984682148651304", + "prod v4 minimal locker 2 pools A->B->C | ticks moved": "0", + "prod v4 minimal locker 3 pools A->B->C->D": "176206", + "prod v4 minimal locker 3 pools A->B->C->D | calldata bytes": "740", + "prod v4 minimal locker 3 pools A->B->C->D | calldata floor gas (EIP-7623, 10 per token)": "13910", + "prod v4 minimal locker 3 pools A->B->C->D | calldata gas (4/16 per byte)": "5564", + "prod v4 minimal locker 3 pools A->B->C->D | output (wei)": "999977023339430321", + "prod v4 minimal locker 3 pools A->B->C->D | ticks moved": "0" +} \ No newline at end of file diff --git a/benchmarks/prod/test/ProdCommon.sol b/benchmarks/prod/test/ProdCommon.sol new file mode 100644 index 0000000..ef9c474 --- /dev/null +++ b/benchmarks/prod/test/ProdCommon.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.26; + +import {Test} from "forge-std/Test.sol"; + +interface IFreshToken { + function mint(address to, uint256 amount) external; + function approve(address spender, uint256 amount) external returns (bool); + function transfer(address to, uint256 amount) external returns (bool); + function transferFrom(address from, address to, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); +} + +/// @notice Shared fixture for the production-contract benches: four fresh ERC20 tokens +/// (the same solmate MockERC20 artifact as the lab harnesses) etched at fixed, +/// address-ordered, all-nonzero-byte addresses on a fork of the latest block, so +/// every run is reproducible without a block pin and the SDK-generated router +/// calldata can embed the token addresses. Nothing in this file is measured. +abstract contract ProdCommon is Test { + // Ordered A < B < C < D. All bytes nonzero, so calldata that carries these addresses + // pays the same 16 gas per byte as a real token address would (a real address has + // about one zero byte in 256; these have none, which is marginally pessimistic). + address constant TOKEN_A = 0x1111111111111111111111111111111111111111; + address constant TOKEN_B = 0x2222222222222222222222222222222222222222; + address constant TOKEN_C = 0x3333333333333333333333333333333333333333; + address constant TOKEN_D = 0x4444444444444444444444444444444444444444; + address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + uint256 constant LIQ_AMOUNT = 1_000_000 ether; // per side, per pool + uint256 constant SWAP_AMOUNT = 1 ether; // exact input, every measured swap + + function _deployTokens() internal { + address[4] memory ts = [TOKEN_A, TOKEN_B, TOKEN_C, TOKEN_D]; + string[4] memory names = ["TokenA", "TokenB", "TokenC", "TokenD"]; + string[4] memory syms = ["A", "B", "C", "D"]; + for (uint256 i = 0; i < 4; i++) { + assertEq(ts[i].code.length, 0, "fresh token address already has code on this chain"); + deployCodeTo("artifacts/MockERC20.json", abi.encode(names[i], syms[i], uint8(18)), ts[i]); + IFreshToken(ts[i]).mint(address(this), 10_000_000 ether); + } + vm.deal(address(this), 10_000_000 ether); + vm.snapshotValue("prod fork block number", block.number); + vm.snapshotValue("prod fork block timestamp", block.timestamp); + } + + /// @dev Records the exact bytes of a measured call next to its gas: byte count, the + /// EIP-2028 calldata gas (4 per zero byte, 16 per nonzero byte) and the EIP-7623 + /// floor tokens, so the page can add calldata to execution gas without modeling. + function _recordCalldata(string memory name, string memory file, bytes memory data) internal { + uint256 zeros; + for (uint256 i = 0; i < data.length; i++) { + if (data[i] == 0) zeros++; + } + uint256 nonzeros = data.length - zeros; + vm.snapshotValue(string.concat(name, " | calldata bytes"), data.length); + vm.snapshotValue(string.concat(name, " | calldata gas (4/16 per byte)"), 4 * zeros + 16 * nonzeros); + vm.snapshotValue(string.concat(name, " | calldata floor gas (EIP-7623, 10 per token)"), 10 * (zeros + 4 * nonzeros)); + vm.writeFile(string.concat("calldata/", file, ".hex"), vm.toString(data)); + } + + receive() external payable {} +} diff --git a/benchmarks/prod/test/ProdEkubo.t.sol b/benchmarks/prod/test/ProdEkubo.t.sol new file mode 100644 index 0000000..64f9d1c --- /dev/null +++ b/benchmarks/prod/test/ProdEkubo.t.sol @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.33; + +import {Vm} from "forge-std/Test.sol"; +import {ProdCommon, IFreshToken} from "./ProdCommon.sol"; +import {ICore} from "ekubo/src/interfaces/ICore.sol"; +import {PoolKey, toPoolId} from "ekubo/src/types/poolKey.sol"; +import {PoolId} from "ekubo/src/types/poolId.sol"; +import {PoolConfig, createConcentratedPoolConfig} from "ekubo/src/types/poolConfig.sol"; +import {PoolState} from "ekubo/src/types/poolState.sol"; +import {SqrtRatio, MIN_SQRT_RATIO, MAX_SQRT_RATIO} from "ekubo/src/types/sqrtRatio.sol"; +import {NATIVE_TOKEN_ADDRESS} from "ekubo/src/math/constants.sol"; +import {SwapParameters, createSwapParameters} from "ekubo/src/types/swapParameters.sol"; +import {PoolBalanceUpdate} from "ekubo/src/types/poolBalanceUpdate.sol"; +import {BaseLocker} from "ekubo/src/base/BaseLocker.sol"; +import {FlashAccountantLib} from "ekubo/src/libraries/FlashAccountantLib.sol"; +import {CoreLib} from "ekubo/src/libraries/CoreLib.sol"; + +interface IPositionsLike { + function name() external view returns (string memory); + function mintAndDepositWithSalt( + bytes32 salt, + PoolKey memory poolKey, + int32 tickLower, + int32 tickUpper, + uint128 maxAmount0, + uint128 maxAmount1, + uint128 minLiquidity + ) external payable returns (uint256 id, uint128 liquidity, uint128 amount0, uint128 amount1); +} + +/// @notice True-minimal single-lock exact-input locker over the production Core, the same +/// shape as the v4 minimal locker: lock, swap each hop, pay the input once, withdraw +/// the output once (net settlement). Native input rides on msg.value. +contract MinimalEkuboRouter is BaseLocker { + using FlashAccountantLib for *; + using CoreLib for ICore; + + constructor(ICore core) BaseLocker(core) {} + + function swapExactIn(PoolKey[] memory keys, address tokenIn, int128 amountIn, address payer) + external + payable + returns (int128 amountOut) + { + amountOut = abi.decode(lock(abi.encode(keys, tokenIn, amountIn, payer, msg.sender, msg.value)), (int128)); + } + + function handleLockData(uint256, bytes memory data) internal override returns (bytes memory result) { + (PoolKey[] memory keys, address tokenIn, int128 amount, address payer, address recipient, uint256 value) = + abi.decode(data, (PoolKey[], address, int128, address, address, uint256)); + ICore core = ICore(payable(address(ACCOUNTANT))); + address firstToken = tokenIn; + int128 firstAmount = amount; + for (uint256 i = 0; i < keys.length; i++) { + bool isToken1 = tokenIn == keys[i].token1; + require(isToken1 || tokenIn == keys[i].token0, "hop token"); + SwapParameters params = + createSwapParameters(isToken1 ? MAX_SQRT_RATIO : MIN_SQRT_RATIO, amount, isToken1, 0); + (PoolBalanceUpdate bu,) = core.swap(i == 0 ? value : 0, keys[i], params); + if (isToken1) { + require(bu.delta1() == amount && bu.delta0() < 0, "hop"); + amount = -bu.delta0(); + tokenIn = keys[i].token0; + } else { + require(bu.delta0() == amount && bu.delta1() < 0, "hop"); + amount = -bu.delta1(); + tokenIn = keys[i].token1; + } + } + if (firstToken != NATIVE_TOKEN_ADDRESS) ACCOUNTANT.payFrom(payer, firstToken, uint128(firstAmount)); + ACCOUNTANT.withdraw(tokenIn, recipient, uint128(amount)); + result = abi.encode(amount); + } +} + +/// @notice Production Ekubo contracts on a fork of the latest mainnet block, fresh tokens: +/// Core (all pools), Positions (capitalizes the pools in setup, unmeasured) and the +/// deployed Yul router with SDK-generated calldata (`encode-prod-routes.mjs`). The +/// minimal locker above is the like-for-like counterpart of the v3/v4 minimal +/// routers. One identical unmeasured call precedes every measurement; every call is +/// isolated, so the snapshot is the execution gas of the measured call net of +/// refunds, excluding the 21,000 base and the calldata recorded next to it. +contract ProdEkuboTest is ProdCommon { + using CoreLib for ICore; + + ICore constant CORE = ICore(payable(0x00000000000014aA86C5d3c41765bb24e11bd701)); + IPositionsLike constant POSITIONS = IPositionsLike(0x02D9876A21AF7545f8632C3af76eC90b5ad4b66D); + address constant YUL_ROUTER = 0x7B2aA7Ecc0B5936b7C52E6259A19C3BA557d0748; + address constant RECIPIENT = 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496; // this test contract, embedded in the routes + + uint64 constant FEE_0_3 = 55340232221128654; // floor(0.003 * 2^64), the Uniswap 3000 tier + uint32 constant TICK_SPACING = 6000; // Uniswap spacing 60, Ekubo ticks are 100x finer + int32 constant RANGE_LOWER = -88722000; + int32 constant RANGE_UPPER = 88722000; + int32 constant INIT_TICK = 3050; // strictly inside a tick (Uniswap tick 30.5), never on a spacing multiple + bytes32 constant CONFIG = 0x000000000000000000000000000000000000000000c49ba5e353f7ce80001770; + + PoolKey keyAB; + PoolKey keyBC; + PoolKey keyCD; + PoolKey keyNative; + MinimalEkuboRouter minimal; + + function _key(address t0, address t1) internal pure returns (PoolKey memory k) { + k = PoolKey({token0: t0, token1: t1, config: createConcentratedPoolConfig(FEE_0_3, TICK_SPACING, address(0))}); + require(PoolConfig.unwrap(k.config) == CONFIG, "config word differs from the one in the SDK routes"); + } + + function _setup() internal { + assertEq(block.chainid, 1, "mainnet fork"); + assertEq(address(this), RECIPIENT, "route recipient is this contract"); + assertGt(address(CORE).code.length, 0, "Core deployed"); + assertGt(YUL_ROUTER.code.length, 0, "Yul router deployed"); + assertEq(POSITIONS.name(), "Ekubo Positions", "production Positions"); + _deployTokens(); + + keyAB = _key(TOKEN_A, TOKEN_B); + keyBC = _key(TOKEN_B, TOKEN_C); + keyCD = _key(TOKEN_C, TOKEN_D); + keyNative = _key(NATIVE_TOKEN_ADDRESS, TOKEN_B); + PoolKey[4] memory keys = [keyAB, keyBC, keyCD, keyNative]; + for (uint256 i = 0; i < 4; i++) { + assertFalse(CORE.poolState(toPoolId(keys[i])).isInitialized(), "pool must not exist yet"); + CORE.initializePool(keys[i], INIT_TICK); + } + address[4] memory ts = [TOKEN_A, TOKEN_B, TOKEN_C, TOKEN_D]; + for (uint256 i = 0; i < 4; i++) { + IFreshToken(ts[i]).approve(address(POSITIONS), type(uint256).max); + } + // Capitalize through the production Positions manager (unmeasured): one full-range + // position per pool, 1M tokens a side, so no measured swap crosses an initialized tick. + // Explicit salts: the deployed manager's `mintAndDeposit` derives its salt from + // prevrandao and remaining gas, which back-to-back mints in one context share. + for (uint256 i = 0; i < 3; i++) { + (, uint128 liq,,) = POSITIONS.mintAndDepositWithSalt( + bytes32(i + 1), keys[i], RANGE_LOWER, RANGE_UPPER, uint128(LIQ_AMOUNT), uint128(LIQ_AMOUNT), 0 + ); + assertGt(liq, 0); + } + POSITIONS.mintAndDepositWithSalt{value: LIQ_AMOUNT}( + bytes32(uint256(4)), keyNative, RANGE_LOWER, RANGE_UPPER, uint128(LIQ_AMOUNT), uint128(LIQ_AMOUNT), 0 + ); + + minimal = new MinimalEkuboRouter(CORE); + IFreshToken(TOKEN_A).approve(YUL_ROUTER, type(uint256).max); + IFreshToken(TOKEN_A).approve(address(minimal), type(uint256).max); + assertEq(IFreshToken(TOKEN_A).allowance(address(this), YUL_ROUTER), type(uint256).max, "max approval"); + } + + function _tick(PoolKey memory k) internal view returns (int32 tick) { + (, tick,) = CORE.poolState(toPoolId(k)).parse(); + } + + /// @dev Calls `to` with `data` (and `value`), snapshots the call when `name` is set, and + /// checks the swap moved the last pool by at most one Uniswap tick (100 fine ticks). + function _call(string memory name, address to, uint256 value, bytes memory data, PoolKey memory lastPool, address tokenOut) + internal + returns (uint256 out, bytes memory ret) + { + int32 before = _tick(lastPool); + uint256 balBefore = IFreshToken(tokenOut).balanceOf(address(this)); + assertGt(balBefore, 0, "recipient already holds the output token (nonzero -> nonzero balance write)"); + bool ok; + (ok, ret) = to.call{value: value}(data); + if (bytes(name).length > 0) vm.snapshotGasLastCall(name); + assertTrue(ok, "call reverted"); + out = IFreshToken(tokenOut).balanceOf(address(this)) - balBefore; + int256 moved = int256(_tick(lastPool)) - int256(before); + if (moved < 0) moved = -moved; + assertLe(uint256(moved), 100, "moved more than one Uniswap tick"); + if (bytes(name).length > 0) { + vm.snapshotValue(string.concat(name, " | output (wei)"), out); + vm.snapshotValue(string.concat(name, " | fine ticks moved"), uint256(moved)); + } + } + + function _route(string memory file) internal view returns (bytes memory) { + return vm.parseBytes(vm.readFile(string.concat("calldata/", file, ".hex"))); + } + + // ---- production Yul router, SDK calldata ---- + + function _lastPool(uint256 n) internal view returns (PoolKey memory) { + // resolved after _setup(): the keys are storage, so they must not be read before it + return n == 0 ? keyNative : (n == 1 ? keyAB : (n == 2 ? keyBC : keyCD)); + } + + function _yul(string memory file, string memory name, uint256 value, uint256 n, address tokenOut, uint256 minOut) internal { + _setup(); + PoolKey memory lastPool = _lastPool(n); + bytes memory route = _route(file); + _call("", YUL_ROUTER, value, route, lastPool, tokenOut); // warm-up, unmeasured + (uint256 out, bytes memory ret) = _call(name, YUL_ROUTER, value, route, lastPool, tokenOut); + (,, int256 specified, int256 calculated) = abi.decode(ret, (address, address, int256, int256)); + assertEq(uint256(specified), SWAP_AMOUNT, "exact input"); + assertEq(uint256(calculated), out, "router-reported output equals balance change"); + assertGe(out, minOut, "output sanity"); + assertLe(out, (SWAP_AMOUNT * 101) / 100, "output sanity"); // pools sit at tick 30.5, so 1 A buys ~1.003 B before the 0.3% fee + _recordCalldata(name, file, route); + } + + /// forge-config: default.isolate = true + function test_prod_ekubo_yul_1pool() public { + _yul("ekubo-yul-1pool", "prod ekubo Yul router 1 pool A->B", 0, 1, TOKEN_B, 0.99e18); + } + + /// forge-config: default.isolate = true + function test_prod_ekubo_yul_2pools() public { + _yul("ekubo-yul-2pools", "prod ekubo Yul router 2 pools A->B->C", 0, 2, TOKEN_C, 0.98e18); + } + + /// forge-config: default.isolate = true + function test_prod_ekubo_yul_3pools() public { + _yul("ekubo-yul-3pools", "prod ekubo Yul router 3 pools A->B->C->D", 0, 3, TOKEN_D, 0.97e18); + } + + /// forge-config: default.isolate = true + function test_prod_ekubo_yul_native() public { + _yul("ekubo-yul-native", "prod ekubo Yul router 1 pool ETH->B", SWAP_AMOUNT, 0, TOKEN_B, 0.99e18); + } + + // ---- minimal single-lock locker (like-for-like with the v4 minimal locker) ---- + + function _min(uint256 n, string memory name, address tokenIn, uint256 value, address tokenOut, uint256 minOut) internal { + _setup(); + PoolKey[] memory keys = new PoolKey[](n); + PoolKey memory last; + if (tokenIn == NATIVE_TOKEN_ADDRESS) { + keys[0] = keyNative; + last = keyNative; + } else { + keys[0] = keyAB; + if (n > 1) keys[1] = keyBC; + if (n > 2) keys[2] = keyCD; + last = keys[n - 1]; + } + bytes memory data = abi.encodeCall(minimal.swapExactIn, (keys, tokenIn, int128(int256(SWAP_AMOUNT)), address(this))); + _call("", address(minimal), value, data, last, tokenOut); + (uint256 out,) = _call(name, address(minimal), value, data, last, tokenOut); + assertGe(out, minOut, "output sanity"); + _recordCalldata(name, tokenIn == NATIVE_TOKEN_ADDRESS ? "ekubo-minimal-native" : string.concat("ekubo-minimal-", vm.toString(n), n == 1 ? "pool" : "pools"), data); + } + + /// forge-config: default.isolate = true + function test_prod_ekubo_minimal_1pool() public { + _min(1, "prod ekubo minimal locker 1 pool A->B", TOKEN_A, 0, TOKEN_B, 0.99e18); + } + + /// forge-config: default.isolate = true + function test_prod_ekubo_minimal_2pools() public { + _min(2, "prod ekubo minimal locker 2 pools A->B->C", TOKEN_A, 0, TOKEN_C, 0.98e18); + } + + /// forge-config: default.isolate = true + function test_prod_ekubo_minimal_3pools() public { + _min(3, "prod ekubo minimal locker 3 pools A->B->C->D", TOKEN_A, 0, TOKEN_D, 0.97e18); + } + + /// forge-config: default.isolate = true + function test_prod_ekubo_minimal_native() public { + _min(1, "prod ekubo minimal locker 1 pool ETH->B", NATIVE_TOKEN_ADDRESS, SWAP_AMOUNT, TOKEN_B, 0.99e18); + } +} diff --git a/benchmarks/prod/test/ProdV3.t.sol b/benchmarks/prod/test/ProdV3.t.sol new file mode 100644 index 0000000..9f68f46 --- /dev/null +++ b/benchmarks/prod/test/ProdV3.t.sol @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.26; + +import {ProdCommon, IFreshToken} from "./ProdCommon.sol"; + +interface IUniswapV3Factory { + function createPool(address tokenA, address tokenB, uint24 fee) external returns (address pool); + function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool); +} + +interface IUniswapV3Pool { + function token0() external view returns (address); + function token1() external view returns (address); + function initialize(uint160 sqrtPriceX96) external; + function mint(address recipient, int24 tickLower, int24 tickUpper, uint128 amount, bytes calldata data) + external + returns (uint256 amount0, uint256 amount1); + function swap(address recipient, bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96, bytes calldata data) + external + returns (int256 amount0, int256 amount1); + function slot0() + external + view + returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked); + function liquidity() external view returns (uint128); +} + +interface ISwapRouter { + struct ExactInputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + uint160 sqrtPriceLimitX96; + } + + struct ExactInputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + } + + function factory() external view returns (address); + function WETH9() external view returns (address); + function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); + function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); +} + +/// @notice Minimal N-hop exact-input router over v3 pools with the production SwapRouter's +/// token flow (first hop paid straight from the user, intermediate outputs held by +/// the router and paid forward, last hop delivered to the recipient): 2 transfers +/// per pool. Same contract as in the lab harness. +contract V3MinimalRouter { + uint160 constant MIN_LIMIT = 4295128740; // MIN_SQRT_RATIO + 1 + + function multiHop(IUniswapV3Pool[] memory pools, address[] memory tokensIn, int256 amountIn, address recipient) + external + returns (int256 out) + { + int256 amount = amountIn; + address payer = msg.sender; + for (uint256 i = 0; i < pools.length; i++) { + bool last = i == pools.length - 1; + (, int256 a1) = + pools[i].swap(last ? recipient : address(this), true, amount, MIN_LIMIT, abi.encode(tokensIn[i], payer)); + amount = -a1; + require(amount > 0, "no hop output"); + payer = address(this); + } + out = amount; + } + + function uniswapV3SwapCallback(int256 amount0Delta, int256, bytes calldata data) external { + (address tokenIn, address payer) = abi.decode(data, (address, address)); + uint256 owed = uint256(amount0Delta); // every hop is zeroForOne + if (payer == address(this)) IFreshToken(tokenIn).transfer(msg.sender, owed); + else IFreshToken(tokenIn).transferFrom(payer, msg.sender, owed); + } +} + +/// @notice Production Uniswap v3 on a fork of the latest mainnet block, fresh tokens: the +/// deployed factory creates the pools, the deployed SwapRouter is the production +/// path, the minimal router above is the lab counterpart. Full-range positions of +/// the liquidity that 1M/1M tokens buy at the init price (matching the other two +/// harnesses), minted directly in setup (unmeasured). One identical unmeasured call +/// precedes every measurement; every call is isolated (execution gas net of +/// refunds, excluding the 21,000 base and the calldata recorded next to it). +contract ProdV3Test is ProdCommon { + IUniswapV3Factory constant FACTORY = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984); + ISwapRouter constant ROUTER = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); + + uint24 constant FEE = 3000; + int24 constant TICK_LOWER = -887220; + int24 constant TICK_UPPER = 887220; + uint160 constant INIT_SQRT_PRICE = 79347087983665984109010747392; // tick 30, strictly inside (spacing 60) + uint128 constant LIQUIDITY = 998501199320305883812938; // 1M/1M full-range at the init price + + IUniswapV3Pool poolAB; + IUniswapV3Pool poolBC; + IUniswapV3Pool poolCD; + IUniswapV3Pool poolWethB; + V3MinimalRouter minimal; + + function _setup() internal { + assertEq(block.chainid, 1, "mainnet fork"); + assertEq(ROUTER.factory(), address(FACTORY), "production SwapRouter and factory"); + assertEq(ROUTER.WETH9(), WETH, "WETH9"); + _deployTokens(); + deal(WETH, address(this), 10_000_000 ether); + + poolAB = _pool(TOKEN_A, TOKEN_B); + poolBC = _pool(TOKEN_B, TOKEN_C); + poolCD = _pool(TOKEN_C, TOKEN_D); + poolWethB = _pool(TOKEN_B, WETH); // TOKEN_B < WETH by address + minimal = new V3MinimalRouter(); + IFreshToken(TOKEN_A).approve(address(ROUTER), type(uint256).max); + IFreshToken(TOKEN_A).approve(address(minimal), type(uint256).max); + assertEq(IFreshToken(TOKEN_A).allowance(address(this), address(ROUTER)), type(uint256).max, "max approval"); + } + + function _pool(address t0, address t1) internal returns (IUniswapV3Pool pool) { + assertLt(uint160(t0), uint160(t1), "ordered"); + assertEq(FACTORY.getPool(t0, t1, FEE), address(0), "pool must not exist yet"); + pool = IUniswapV3Pool(FACTORY.createPool(t0, t1, FEE)); + pool.initialize(INIT_SQRT_PRICE); + pool.mint(address(this), TICK_LOWER, TICK_UPPER, LIQUIDITY, ""); + assertEq(pool.liquidity(), LIQUIDITY); + } + + function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external { + if (amount0Owed > 0) IFreshToken(IUniswapV3Pool(msg.sender).token0()).transfer(msg.sender, amount0Owed); + if (amount1Owed > 0) IFreshToken(IUniswapV3Pool(msg.sender).token1()).transfer(msg.sender, amount1Owed); + } + + function _tick(IUniswapV3Pool pool) internal view returns (int24 tick) { + (, tick,,,,,) = pool.slot0(); + } + + function _call(string memory name, address to, uint256 value, bytes memory data, IUniswapV3Pool lastPool, address tokenOut) + internal + returns (uint256 out) + { + int24 before = _tick(lastPool); + uint256 balBefore = IFreshToken(tokenOut).balanceOf(address(this)); + assertGt(balBefore, 0, "recipient already holds the output token"); + (bool ok, bytes memory ret) = to.call{value: value}(data); + if (bytes(name).length > 0) vm.snapshotGasLastCall(name); + assertTrue(ok, "call reverted"); + out = IFreshToken(tokenOut).balanceOf(address(this)) - balBefore; + if (ret.length == 32) assertEq(abi.decode(ret, (uint256)), out, "router-reported output equals balance change"); + int256 moved = int256(_tick(lastPool)) - int256(before); + if (moved < 0) moved = -moved; + assertLe(uint256(moved), 1, "moved more than one tick"); + if (bytes(name).length > 0) { + vm.snapshotValue(string.concat(name, " | output (wei)"), out); + vm.snapshotValue(string.concat(name, " | ticks moved"), uint256(moved)); + } + } + + // ---- production SwapRouter ---- + + function _single(address tokenIn, address tokenOut, uint256 value) internal pure returns (bytes memory) { + return abi.encodeCall( + ISwapRouter.exactInputSingle, + ( + ISwapRouter.ExactInputSingleParams({ + tokenIn: tokenIn, + tokenOut: tokenOut, + fee: FEE, + recipient: 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496, + deadline: type(uint256).max, + amountIn: value == 0 ? SWAP_AMOUNT : value, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }) + ) + ); + } + + function _path(uint256 n) internal pure returns (bytes memory path) { + path = abi.encodePacked(TOKEN_A, FEE, TOKEN_B); + if (n > 1) path = abi.encodePacked(path, FEE, TOKEN_C); + if (n > 2) path = abi.encodePacked(path, FEE, TOKEN_D); + } + + function _router(string memory file, string memory name, bytes memory data, uint256 value, uint256 n, address tokenOut, uint256 minOut) + internal + { + _setup(); + // resolved after _setup(): the pools are storage, so they must not be read before it + IUniswapV3Pool last = n == 0 ? poolWethB : (n == 1 ? poolAB : (n == 2 ? poolBC : poolCD)); + _call("", address(ROUTER), value, data, last, tokenOut); // warm-up, unmeasured + uint256 out = _call(name, address(ROUTER), value, data, last, tokenOut); + assertGe(out, minOut, "output sanity"); + assertLe(out, (SWAP_AMOUNT * 101) / 100, "output sanity"); // pools sit at tick 30.5, so 1 A buys ~1.003 B before the 0.3% fee + _recordCalldata(name, file, data); + } + + /// forge-config: default.isolate = true + function test_prod_v3_swapRouter_1pool() public { + _router("v3-swaprouter-1pool", "prod v3 SwapRouter exactInputSingle 1 pool A->B", _single(TOKEN_A, TOKEN_B, 0), 0, 1, TOKEN_B, 0.99e18); + } + + /// forge-config: default.isolate = true + function test_prod_v3_swapRouter_2pools() public { + bytes memory data = abi.encodeCall( + ISwapRouter.exactInput, + (ISwapRouter.ExactInputParams({path: _path(2), recipient: address(this), deadline: type(uint256).max, amountIn: SWAP_AMOUNT, amountOutMinimum: 0})) + ); + _router("v3-swaprouter-2pools", "prod v3 SwapRouter exactInput 2 pools A->B->C", data, 0, 2, TOKEN_C, 0.98e18); + } + + /// forge-config: default.isolate = true + function test_prod_v3_swapRouter_3pools() public { + bytes memory data = abi.encodeCall( + ISwapRouter.exactInput, + (ISwapRouter.ExactInputParams({path: _path(3), recipient: address(this), deadline: type(uint256).max, amountIn: SWAP_AMOUNT, amountOutMinimum: 0})) + ); + _router("v3-swaprouter-3pools", "prod v3 SwapRouter exactInput 3 pools A->B->C->D", data, 0, 3, TOKEN_D, 0.97e18); + } + + /// @dev v3 pools hold WETH, so a native-ETH user sends value to the SwapRouter, which + /// wraps it in flight (`pay`: tokenIn == WETH9 and the value covers the input). + /// forge-config: default.isolate = true + function test_prod_v3_swapRouter_native() public { + _router("v3-swaprouter-native", "prod v3 SwapRouter exactInputSingle 1 pool ETH(wrapped in flight)->B", _single(WETH, TOKEN_B, SWAP_AMOUNT), SWAP_AMOUNT, 0, TOKEN_B, 0.99e18); + } + + // ---- minimal router ---- + + function _min(uint256 n, string memory name, address tokenOut, uint256 minOut) internal { + _setup(); + IUniswapV3Pool[] memory pools = new IUniswapV3Pool[](n); + address[] memory tokensIn = new address[](n); + pools[0] = poolAB; + tokensIn[0] = TOKEN_A; + if (n > 1) { + pools[1] = poolBC; + tokensIn[1] = TOKEN_B; + } + if (n > 2) { + pools[2] = poolCD; + tokensIn[2] = TOKEN_C; + } + bytes memory data = abi.encodeCall(minimal.multiHop, (pools, tokensIn, int256(SWAP_AMOUNT), address(this))); + _call("", address(minimal), 0, data, pools[n - 1], tokenOut); + uint256 out = _call(name, address(minimal), 0, data, pools[n - 1], tokenOut); + assertGe(out, minOut, "output sanity"); + _recordCalldata(name, string.concat("v3-minimal-", vm.toString(n), n == 1 ? "pool" : "pools"), data); + } + + /// forge-config: default.isolate = true + function test_prod_v3_minimal_1pool() public { + _min(1, "prod v3 minimal router 1 pool A->B", TOKEN_B, 0.99e18); + } + + /// forge-config: default.isolate = true + function test_prod_v3_minimal_2pools() public { + _min(2, "prod v3 minimal router 2 pools A->B->C", TOKEN_C, 0.98e18); + } + + /// forge-config: default.isolate = true + function test_prod_v3_minimal_3pools() public { + _min(3, "prod v3 minimal router 3 pools A->B->C->D", TOKEN_D, 0.97e18); + } +} diff --git a/benchmarks/prod/test/ProdV4.t.sol b/benchmarks/prod/test/ProdV4.t.sol new file mode 100644 index 0000000..b55d4ea --- /dev/null +++ b/benchmarks/prod/test/ProdV4.t.sol @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.26; + +import {ProdCommon, IFreshToken} from "./ProdCommon.sol"; +import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "v4-core/src/types/PoolKey.sol"; +import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; +import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol"; +import {SwapParams, ModifyLiquidityParams} from "v4-core/src/types/PoolOperation.sol"; +import {IHooks} from "v4-core/src/interfaces/IHooks.sol"; +import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol"; +import {TickMath} from "v4-core/src/libraries/TickMath.sol"; +import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol"; +import {TransientStateLibrary} from "v4-core/src/libraries/TransientStateLibrary.sol"; +import {CurrencySettler} from "v4-core/test/utils/CurrencySettler.sol"; +import {LiquidityAmounts} from "v4-core/test/utils/LiquidityAmounts.sol"; +import {PoolModifyLiquidityTest} from "v4-core/src/test/PoolModifyLiquidityTest.sol"; + +interface IPermit2 { + function approve(address token, address spender, uint160 amount, uint48 expiration) external; +} + +interface IUniversalRouter { + function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline) external payable; +} + +/// @notice Minimal single-lock N-hop exact-input router (e.g. A->B->C->D) with net +/// settlement: unlock, swap each hop, settle the input once, take the output once. +/// Same contract as in the lab harness; the like-for-like counterpart of the +/// Ekubo minimal locker. +contract V4MinimalRouter is IUnlockCallback { + using CurrencyLibrary for Currency; + using CurrencySettler for Currency; + using TransientStateLibrary for IPoolManager; + + IPoolManager public immutable manager; + + constructor(IPoolManager _manager) { + manager = _manager; + } + + struct Hop { + PoolKey key; + uint160 sqrtPriceLimitX96; + } + + function multiHop(Hop[] memory hops, Currency currencyIn, Currency currencyOut, int256 amountIn) + external + payable + returns (uint256 amountOut) + { + bytes memory result = manager.unlock(abi.encode(hops, currencyIn, currencyOut, amountIn, msg.sender, msg.sender)); + amountOut = abi.decode(result, (uint256)); + uint256 ethBalance = address(this).balance; + if (ethBalance > 0) CurrencyLibrary.ADDRESS_ZERO.transfer(msg.sender, ethBalance); + } + + function unlockCallback(bytes calldata rawData) external returns (bytes memory) { + require(msg.sender == address(manager)); + (Hop[] memory hops, Currency currencyIn, Currency currencyOut, int256 amountIn, address payer, address recipient) = + abi.decode(rawData, (Hop[], Currency, Currency, int256, address, address)); + + Currency currencyOwed = currencyIn; + int256 amountOwed = amountIn; + for (uint256 i = 0; i < hops.length; i++) { + bool zeroForOne = currencyOwed == hops[i].key.currency0; + manager.swap( + hops[i].key, + SwapParams({zeroForOne: zeroForOne, amountSpecified: amountOwed, sqrtPriceLimitX96: hops[i].sqrtPriceLimitX96}), + "" + ); + currencyOwed = zeroForOne ? hops[i].key.currency1 : hops[i].key.currency0; + amountOwed = -manager.currencyDelta(address(this), currencyOwed); + require(amountOwed < 0, "no hop output"); + } + + int256 inDelta = manager.currencyDelta(address(this), currencyIn); + int256 outDelta = manager.currencyDelta(address(this), currencyOut); + require(inDelta <= 0, "inDelta positive"); + require(outDelta >= 0, "outDelta negative"); + if (inDelta < 0) currencyIn.settle(manager, payer, uint256(-inDelta), false); + if (outDelta > 0) currencyOut.take(manager, recipient, uint256(outDelta), false); + return abi.encode(uint256(outDelta)); + } +} + +/// @notice Production Uniswap v4 on a fork of the latest mainnet block, fresh tokens: the +/// deployed PoolManager holds the pools, the deployed Universal Router (funded +/// through Permit2, the path the Uniswap interface sends) is the production path, +/// the minimal router above is the lab counterpart. Full-range positions of 1M/1M +/// tokens per pool, added through v4-core's PoolModifyLiquidityTest in setup +/// (unmeasured). One identical unmeasured call precedes every measurement; every +/// call is isolated (execution gas net of refunds, excluding the 21,000 base and +/// the calldata recorded next to it). +contract ProdV4Test is ProdCommon { + using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; + + IPoolManager constant MANAGER = IPoolManager(0x000000000004444c5dc75cB358380D2e3dE08A90); + IUniversalRouter constant UR = IUniversalRouter(0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af); + IPermit2 constant PERMIT2 = IPermit2(0x000000000022D473030F116dDEE9F6B43aC78BA3); + + int24 constant TICK_LOWER = -887220; + int24 constant TICK_UPPER = 887220; + int24 constant INIT_TICK = 30; + + // Universal Router command / v4 action bytes (universal-router Commands.sol, v4-periphery Actions.sol) + bytes1 constant CMD_V4_SWAP = 0x10; + uint8 constant ACT_SWAP_EXACT_IN_SINGLE = 0x06; + uint8 constant ACT_SETTLE_ALL = 0x0c; + uint8 constant ACT_TAKE_ALL = 0x0f; + + // IV4Router.ExactInputSingleParams as deployed in the 2025 Universal Router (no minHopPriceX36) + struct ExactInputSingleParams { + PoolKey poolKey; + bool zeroForOne; + uint128 amountIn; + uint128 amountOutMinimum; + bytes hookData; + } + + PoolKey keyAB; + PoolKey keyBC; + PoolKey keyCD; + PoolKey keyNative; + V4MinimalRouter minimal; + PoolModifyLiquidityTest mintRouter; + + function _key(address c0, address c1) internal pure returns (PoolKey memory) { + return PoolKey({currency0: Currency.wrap(c0), currency1: Currency.wrap(c1), fee: 3000, tickSpacing: 60, hooks: IHooks(address(0))}); + } + + function _setup() internal { + assertEq(block.chainid, 1, "mainnet fork"); + assertGt(address(MANAGER).code.length, 0, "PoolManager deployed"); + assertGt(address(UR).code.length, 0, "Universal Router deployed"); + _deployTokens(); + + mintRouter = new PoolModifyLiquidityTest(MANAGER); + minimal = new V4MinimalRouter(MANAGER); + address[4] memory ts = [TOKEN_A, TOKEN_B, TOKEN_C, TOKEN_D]; + for (uint256 i = 0; i < 4; i++) { + IFreshToken(ts[i]).approve(address(mintRouter), type(uint256).max); + } + keyAB = _key(TOKEN_A, TOKEN_B); + keyBC = _key(TOKEN_B, TOKEN_C); + keyCD = _key(TOKEN_C, TOKEN_D); + keyNative = _key(address(0), TOKEN_B); + _init(keyAB, 0); + _init(keyBC, 0); + _init(keyCD, 0); + _init(keyNative, LIQ_AMOUNT); + + IFreshToken(TOKEN_A).approve(address(minimal), type(uint256).max); + IFreshToken(TOKEN_A).approve(address(PERMIT2), type(uint256).max); + PERMIT2.approve(TOKEN_A, address(UR), type(uint160).max, uint48(block.timestamp + 1 days)); + assertEq(IFreshToken(TOKEN_A).allowance(address(this), address(minimal)), type(uint256).max, "max approval"); + } + + function _init(PoolKey memory key, uint256 value) internal { + (uint160 sqrtPrice,,,) = MANAGER.getSlot0(key.toId()); + assertEq(sqrtPrice, 0, "pool must not exist yet"); + MANAGER.initialize(key, TickMath.getSqrtPriceAtTick(INIT_TICK)); + uint128 liq = LiquidityAmounts.getLiquidityForAmounts( + TickMath.getSqrtPriceAtTick(INIT_TICK), + TickMath.getSqrtPriceAtTick(TICK_LOWER), + TickMath.getSqrtPriceAtTick(TICK_UPPER), + LIQ_AMOUNT, + LIQ_AMOUNT + ); + mintRouter.modifyLiquidity{value: value}( + key, ModifyLiquidityParams({tickLower: TICK_LOWER, tickUpper: TICK_UPPER, liquidityDelta: int256(uint256(liq)), salt: bytes32(0)}), "" + ); + assertEq(MANAGER.getLiquidity(key.toId()), liq); + } + + function _tick(PoolKey memory key) internal view returns (int24 tick) { + (, tick,,) = MANAGER.getSlot0(key.toId()); + } + + function _call(string memory name, address to, uint256 value, bytes memory data, PoolKey memory lastPool, address tokenOut) + internal + returns (uint256 out) + { + int24 before = _tick(lastPool); + uint256 balBefore = IFreshToken(tokenOut).balanceOf(address(this)); + assertGt(balBefore, 0, "recipient already holds the output token"); + (bool ok, bytes memory ret) = to.call{value: value}(data); + if (bytes(name).length > 0) vm.snapshotGasLastCall(name); + assertTrue(ok, "call reverted"); + out = IFreshToken(tokenOut).balanceOf(address(this)) - balBefore; + if (ret.length == 32) assertEq(abi.decode(ret, (uint256)), out, "router-reported output equals balance change"); + int256 moved = int256(_tick(lastPool)) - int256(before); + if (moved < 0) moved = -moved; + assertLe(uint256(moved), 1, "moved more than one tick"); + if (bytes(name).length > 0) { + vm.snapshotValue(string.concat(name, " | output (wei)"), out); + vm.snapshotValue(string.concat(name, " | ticks moved"), uint256(moved)); + } + } + + // ---- minimal single-lock router ---- + + function _min(uint256 n, string memory name, address tokenIn, uint256 value, address tokenOut, uint256 minOut) internal { + _setup(); + V4MinimalRouter.Hop[] memory hops = new V4MinimalRouter.Hop[](n); + PoolKey memory last; + if (tokenIn == address(0)) { + hops[0] = V4MinimalRouter.Hop(keyNative, TickMath.MIN_SQRT_PRICE + 1); + last = keyNative; + } else { + hops[0] = V4MinimalRouter.Hop(keyAB, TickMath.MIN_SQRT_PRICE + 1); + if (n > 1) hops[1] = V4MinimalRouter.Hop(keyBC, TickMath.MIN_SQRT_PRICE + 1); + if (n > 2) hops[2] = V4MinimalRouter.Hop(keyCD, TickMath.MIN_SQRT_PRICE + 1); + last = hops[n - 1].key; + } + bytes memory data = abi.encodeCall( + minimal.multiHop, (hops, Currency.wrap(tokenIn), Currency.wrap(tokenOut), -int256(SWAP_AMOUNT)) + ); + _call("", address(minimal), value, data, last, tokenOut); + uint256 out = _call(name, address(minimal), value, data, last, tokenOut); + assertGe(out, minOut, "output sanity"); + _recordCalldata(name, tokenIn == address(0) ? "v4-minimal-native" : string.concat("v4-minimal-", vm.toString(n), n == 1 ? "pool" : "pools"), data); + } + + /// forge-config: default.isolate = true + function test_prod_v4_minimal_1pool() public { + _min(1, "prod v4 minimal locker 1 pool A->B", TOKEN_A, 0, TOKEN_B, 0.99e18); + } + + /// forge-config: default.isolate = true + function test_prod_v4_minimal_2pools() public { + _min(2, "prod v4 minimal locker 2 pools A->B->C", TOKEN_A, 0, TOKEN_C, 0.98e18); + } + + /// forge-config: default.isolate = true + function test_prod_v4_minimal_3pools() public { + _min(3, "prod v4 minimal locker 3 pools A->B->C->D", TOKEN_A, 0, TOKEN_D, 0.97e18); + } + + /// forge-config: default.isolate = true + function test_prod_v4_minimal_native() public { + _min(1, "prod v4 minimal locker 1 pool ETH->B", address(0), SWAP_AMOUNT, TOKEN_B, 0.99e18); + } + + // ---- production Universal Router, Permit2-funded ---- + + /// forge-config: default.isolate = true + function test_prod_v4_universalRouter_1pool() public { + _setup(); + bytes memory actions = abi.encodePacked(ACT_SWAP_EXACT_IN_SINGLE, ACT_SETTLE_ALL, ACT_TAKE_ALL); + bytes[] memory params = new bytes[](3); + params[0] = abi.encode( + ExactInputSingleParams({poolKey: keyAB, zeroForOne: true, amountIn: uint128(SWAP_AMOUNT), amountOutMinimum: 0, hookData: ""}) + ); + params[1] = abi.encode(Currency.wrap(TOKEN_A), SWAP_AMOUNT); + params[2] = abi.encode(Currency.wrap(TOKEN_B), uint256(0)); + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(actions, params); + bytes memory data = abi.encodeCall(IUniversalRouter.execute, (abi.encodePacked(CMD_V4_SWAP), inputs, type(uint256).max)); + string memory name = "prod v4 Universal Router V4_SWAP 1 pool A->B (Permit2)"; + _call("", address(UR), 0, data, keyAB, TOKEN_B); + uint256 out = _call(name, address(UR), 0, data, keyAB, TOKEN_B); + assertGe(out, 0.99e18, "output sanity"); + assertLe(out, (SWAP_AMOUNT * 101) / 100, "output sanity"); // pools sit at tick 30.5, so 1 A buys ~1.003 B before the 0.3% fee + _recordCalldata(name, "v4-universal-router-1pool", data); + } +} diff --git a/public/gas/flamegraph-ekubo-single.svg b/public/gas/flamegraph-ekubo-single.svg new file mode 100644 index 0000000..290960e --- /dev/null +++ b/public/gas/flamegraph-ekubo-single.svg @@ -0,0 +1,491 @@ +flamegraph FlameTest::test_flame_single Reset ZoomSearch Core::_updatePairDebtWithNative(uint256,address,address,int256,int256) (936 gas, 0.94%)FlashAccountant::_updatePairDebt(uint256,address,address,int256,int256) (876 gas, 0.88%)FixedPointMathLib::fullMulDivN(uint256,uint256,uint8) (86 gas, 0.09%)FlashAccountant::_requireLocker() (196 gas, 0.20%)FlashAccountant::_getLocker() (142 gas, 0.14%)CoreLib::swap (20,623 gas, 20.79%)CoreLib::swapCore.swap_6269342730() (20,363 gas, 20.52%)Core.swap_6269342730()SafeCastLib::toInt128(int256) (60 gas, 0.06%)FlashAccountant::_getLocker() (142 gas, 0.14%)Core.completePayments() (2,054 gas, 2.07%)C..MockERC20.balanceOf(address) (525 gas, 0.53%)Core.startPayments() (5,914 gas, 5.96%)Core.sta..MockERC20.balanceOf(address) (2,525 gas, 2.54%)Mo..FlashAccountantLib::payFrom (21,088 gas, 21.25%)FlashAccountantLib::payFromMockERC20.transferFrom(address,address,uint256) (12,602 gas, 12.70%)MockERC20.transferF..FlashAccountant::_requireLocker() (196 gas, 0.20%)FlashAccountant::_getLocker() (142 gas, 0.14%)all (99,218 gas, 100%)FlameTest.test_flame_single() (99,218 gas, 100.00%)FlameTest.test_flame_single()EkuboGasTest::_swapAB() (98,810 gas, 99.59%)EkuboGasTest::_swapAB()Router.swapAllowPartialFill((address,address,bytes32),bytes32) (87,416 gas, 88.10%)Router.swapAllowPartialFill((address,address,bytes32),bytes32)BaseLocker::lock(bytes) (64,273 gas, 64.78%)BaseLocker::lock(bytes)Core.lock() (61,453 gas, 61.94%)Core.lock()Router.locked_6416899205(uint256) (60,235 gas, 60.71%)Router.locked_6416899205(uint256)FlashAccountantLib::withdraw (16,592 gas, 16.72%)FlashAccountantLib::withdr..Core.0x3ccfd60b (16,351 gas, 16.48%)Core.0x3ccfd60bMockERC20.transfer(address,uint256) (12,452 gas, 12.55%)MockERC20.transfer(.. \ No newline at end of file diff --git a/public/gas/flamegraph-v3-single.svg b/public/gas/flamegraph-v3-single.svg new file mode 100644 index 0000000..417e7e8 --- /dev/null +++ b/public/gas/flamegraph-v3-single.svg @@ -0,0 +1,491 @@ +flamegraph FlameTest::test_flame_single Reset ZoomSearch V3MultiHopRouter.uniswapV3SwapCallback(int256,int256,bytes) (13,528 gas, 12.49%)V3MultiHopRouter.un..transferFrom(address,address,uint256) (12,602 gas, 11.63%)transferFrom(addr..balanceOf(address) (3,050 gas, 2.82%)ba..all (108,327 gas, 100%)FlameTest.test_flame_single() (108,327 gas, 100.00%)FlameTest.test_flame_single()V3MultiHopRouter.multiHop(address[],address[],int256,address) (94,078 gas, 86.85%)V3MultiHopRouter.multiHop(address[],address[],int256,address)swap(address,bool,int256,uint160,bytes) (66,430 gas, 61.32%)swap(address,bool,int256,uint160,bytes)transfer(address,uint256) (12,452 gas, 11.49%)transfer(address,.. \ No newline at end of file diff --git a/public/gas/flamegraph-v4-single.svg b/public/gas/flamegraph-v4-single.svg new file mode 100644 index 0000000..23fa807 --- /dev/null +++ b/public/gas/flamegraph-v4-single.svg @@ -0,0 +1,491 @@ +flamegraph FlameTest::test_flame_single Reset ZoomSearch StdAssertions::assertGt(uint256,uint256) (39 gas, 0.03%)MockERC20.transferFrom(address,address,uint256) (12,602 gas, 10.92%)MockERC20.transf..CurrencyLibrary::balanceOfSelf (934 gas, 0.81%)MockERC20.balanceOf(address) (525 gas, 0.45%)PoolManager::_accountDelta (689 gas, 0.60%)PoolManager.settle() (2,499 gas, 2.16%)P..SafeCast::toInt128(uint256) (53 gas, 0.05%)Hooks::afterSwap (223 gas, 0.19%)Hooks::beforeSwap (112 gas, 0.10%)NoDelegateCall::checkNotDelegateCall() (44 gas, 0.04%)Pool::checkPoolInitialized (2,139 gas, 1.85%)P..FullMath::mulDiv(uint256,uint256,uint256) (146 gas, 0.13%)SafeCast::toInt128(int256) (106 gas, 0.09%)SqrtPriceMath::getAmount0Delta(uint160,uint160,uint128,bool) (672 gas, 0.58%)FullMath::mulDivRoundingUp(uint256,uint256,uint256) (506 gas, 0.44%)FullMath::mulDiv(uint256,uint256,uint256) (389 gas, 0.34%)SqrtPriceMath::getAmount1Delta(uint160,uint160,uint128,bool) (228 gas, 0.20%)SqrtPriceMath::getNextSqrtPriceFromAmount0RoundingUp(uint160,uint128,uint256,bool) (712 gas, 0.62%)FullMath::mulDivRoundingUp(uint256,uint256,uint256) (506 gas, 0.44%)FullMath::mulDiv(uint256,uint256,uint256) (389 gas, 0.34%)TickMath::getSqrtPriceAtTick(int24) (571 gas, 0.49%)BitMath::mostSignificantBit(uint256) (180 gas, 0.16%)Pool::swap (19,288 gas, 16.71%)Pool::swapTickMath::getTickAtSqrtPrice(uint160) (1,829 gas, 1.58%)TickMath::getSqrtPriceAtTick(int24) (719 gas, 0.62%)PoolManager.swap((address,address,uint24,int24,address),(bool,int256,uint160),bytes) (28,884 gas, 25.02%)PoolManager.swap((address,address,uint24..PoolManager::_accountPoolBalanceDelta (1,504 gas, 1.30%)PoolManager::_accountDelta (1,408 gas, 1.22%)PoolManager.sync(address) (6,398 gas, 5.54%)PoolMan..CurrencyLibrary::balanceOfSelf (5,434 gas, 4.71%)Curre..MockERC20.balanceOf(address) (2,525 gas, 2.19%)M..CurrencyLibrary::transfer (15,292 gas, 13.25%)CurrencyLibrary::tra..MockERC20.transfer(address,uint256) (12,452 gas, 10.79%)MockERC20.transf..PoolManager::_accountDelta (689 gas, 0.60%)PoolManager.take(address,address,uint256) (16,525 gas, 14.31%)PoolManager.take(addre..SafeCast::toInt128(uint256) (53 gas, 0.05%)all (115,439 gas, 100%)FlameTest.test_flame_single() (115,439 gas, 100.00%)FlameTest.test_flame_single()V4GasTest::_swapAB() (115,051 gas, 99.66%)V4GasTest::_swapAB()MinimalSwapRouter.swapExactIn((address,address,uint24,int24,address),bool,int256,uint160,address) (103,533 gas, 89.69%)MinimalSwapRouter.swapExactIn((address,address,uint24,int24,address),bool,int256,uint160,address)PoolManager.unlock(bytes) (75,707 gas, 65.58%)PoolManager.unlock(bytes)MinimalSwapRouter.unlockCallback(bytes) (73,697 gas, 63.84%)MinimalSwapRouter.unlockCallback(bytes)TransientStateLibrary::currencyDelta (2,626 gas, 2.27%)T..PoolManager.exttload(bytes32) (1,718 gas, 1.49%) \ No newline at end of file diff --git a/src/content/docs/about-ekubo/features.md b/src/content/docs/about-ekubo/features.md index c92ee14..aed4074 100644 --- a/src/content/docs/about-ekubo/features.md +++ b/src/content/docs/about-ekubo/features.md @@ -11,7 +11,7 @@ The core contracts are ownerless and permissionless. They are deployed to the sa Ekubo uses the ["till" pattern](/concepts/architecture/) and a singleton design to provide the cheapest trades across many pools. All pools live in a single contract, and token transfers are deferred until the end of the transaction. Advanced users don't have to transfer tokens at all: balances can be saved inside Ekubo for later use, avoiding repeated token transfers entirely. -The result is that you can execute many actions across many pools while making only the minimum number of token transfers. Combined with contracts optimized down to the storage-slot and calldata level (see [Price representation](/reference/price-representation/)), this keeps the gas cost of a trade low, which matters most for routes that touch several pools. +The result is that you can execute many actions across many pools while making only the minimum number of token transfers. Combined with contracts optimized down to the storage-slot and calldata level (see [Price representation](/reference/price-representation/)), this keeps the gas cost of a trade low, which matters most for routes that touch several pools. Measured numbers against Uniswap v4 and v3 are on the [Gas efficiency](/reference/gas-efficiency/) page. ## Concentrated liquidity diff --git a/src/content/docs/reference/gas-efficiency.md b/src/content/docs/reference/gas-efficiency.md new file mode 100644 index 0000000..37050d8 --- /dev/null +++ b/src/content/docs/reference/gas-efficiency.md @@ -0,0 +1,182 @@ +--- +description: Measured gas costs for Ekubo swaps against Uniswap v4 and v3, and what they mean for chain throughput +title: "Gas efficiency" +--- + +Ekubo is designed to make trading cheap: one singleton contract holds every pool, +and flash accounting settles only the net difference at the end of a transaction. +This page measures what that means in practice — what a swap costs, how many +swaps fit in a block, and what it would mean chain-wide — against Uniswap v4 and +Uniswap v3. Every figure runs the deployed production contracts of all three +protocols on a fork of the latest mainnet block, with fresh plain ERC20 tokens so +token quirks do not blur the comparison. Harnesses, raw snapshots, exact calldata +and the full method live in `benchmarks/` in the docs repository. + +## Bottom line + +One swap of 1 token through a single 0.3% pool of a fresh ERC20 pair, warmed, +moving the price by at most one tick, at block 25999935 (September 17, 2026). +Transaction gas is execution plus the 21,000 base plus the measured calldata: + +| Transaction, 1 pool | Ekubo | Uniswap v4 | Uniswap v3 | +| ------------------------------------------------------------------------- | ----------- | ---------- | ---------- | +| Production router (Yul router; Universal Router with Permit2; SwapRouter) | **110,875** | 144,390 | 123,587 | +| Minimal single-lock router on every side (favors Uniswap) | **112,997** | 131,284 | 117,006 | +| Native ETH in (Yul router; minimal locker; SwapRouter wrapping in flight) | **96,496** | 115,892 | 126,792 | + +Through the production routers, Ekubo's transaction costs 23.2% less than v4's +and 10.3% less than v3's. With every router stripped to the same minimal +lock-swap-settle shape the gap is 13.9% to v4 and 3.4% to v3: at one pool v3's +pool is nearly as cheap, and the difference is in the router and beyond the first +pool. With native ETH in, Ekubo is 16.7% below v4 and 23.9% below v3, which must +wrap. All legs receive the same output within 0.005%, so the gap is overhead, not +price. At the expensive end, 1000 USDT to USDC on the live mainnet pools (proxy +tokens, blocklists) costs 133,750 on Ekubo against 155,960 on v4 and 151,515 on +v3, 14.2% and 11.7% less: costly tokens compress the gap but do not close it. + +## Throughput per block + +Mainnet's block gas limit is 60 million. Real swap flow is routes, not single +pools: every extra pool costs Ekubo about 24k of transaction gas against v4's 36k +and v3's 59k, so the gap widens with route length. Dividing the limit by measured +route costs gives each protocol's ceiling on routes per block if a block held +nothing else (minimal routers on every side, which favors Uniswap): + +| Routes per 60M block, if a block held nothing else | Ekubo | Uniswap v4 | Uniswap v3 | +| -------------------------------------------------- | ----------------- | ------------- | ------------- | +| 1 pool, production routers | 110,875 → **541** | 144,390 → 415 | 123,587 → 485 | +| 1 pool, minimal routers | 112,997 → **530** | 131,284 → 457 | 117,006 → 512 | +| 2 pools, minimal routers | 137,091 → **437** | 167,027 → 359 | 176,438 → 340 | +| 3 pools, minimal routers | 161,184 → **372** | 202,770 → 295 | 235,870 → 254 | + +At three pools an Ekubo block holds 26% more routes than a v4 block and 46% more +than a v3 block. + +## What this means for the chain + +**Pools per swap, measured over a month.** Every 10th mainnet block from +August 18, 2026, 18:36 UTC to September 17, 2026, 21:08 UTC (21,600 blocks, 25783810 to 25999800) was +read through public JSON-RPC, and every successful transaction that emitted a +pool-level swap event was counted by the number of such events. The event set +covers Uniswap v2 and its forks, v3, v4, PancakeSwap v3, Curve (all generations), +Balancer v2 and v3, Maverick v1 and v2, Fluid, DODO, Bancor v2.1, v3 and Carbon, +Solidly-style pairs, and Ekubo: + +| Pools touched in the transaction | Share of swap transactions | +| -------------------------------- | -------------------------- | +| 1 | 69.1% | +| 2 | 16.0% | +| 3 | 7.4% | +| 4 or more | 7.5% | + +That is 664,726 swap transactions at an average of **1.75 pools per swap** +(daily averages 1.64 to 1.87; a 1,000-block window on September 17 gave +1.68). Those transactions used 32.7% of all gas in the sampled blocks +(daily 20% to 41%). Transactions sent straight to the Uniswap and +1inch routers average 1.22 pools; the rest is aggregators, MEV and other +contracts. Failed transactions emit no logs and are excluded by construction. +Both figures are lower bounds: the count only sees swap events in the catalogued +families, so any AMM outside the catalogue adds uncounted transactions and +uncounted pools, and the swap-transaction gas share can only be higher. The +chain-wide capacity gains below inherit that bias and are underestimates. + +A swap's transaction gas at _p_ pools is the one-pool minimal-router transaction +plus (_p_ − 1) times the measured marginal per extra pool: + +| Average pools per swap | Ekubo | Uniswap v4 | Uniswap v3 | Ekubo saving vs v4 / v3 | +| ---------------------- | ----------- | ---------- | ---------- | ----------------------- | +| **1.75 (measured)** | **131,019** | 158,020 | 161,461 | **17.1% / 18.9%** | +| 1.0 | 112,997 | 131,284 | 117,006 | 13.9% / 3.4% | +| 1.5 | 125,044 | 149,156 | 146,722 | 16.2% / 14.8% | +| 2.0 | 137,091 | 167,027 | 176,438 | 17.9% / 22.3% | + +Block-space shares: over the 30 days ending September 17, 2026, mainnet burned +1,255.7 ETH of base fees (ultrasound.money), and transactions to known DEX +routers were about 4.2% of it, a lower bound that misses aggregators, MEV and +direct pool calls; ultrasound's coarser "defi" category was 19.4%. The +32.7% measured above counts every transaction that touched a pool, +including MEV and aggregator flow whose gas is mostly not swapping. The 70% share +was stipulated for this page as an "all AMM trading" upside case; it is an +assumption, not a measurement. If a swap's share of the burn equals its share of +gas and the saving applies to all of it, moving that slice to Ekubo frees the +share times the saving (each cell spans the saving against v3 and against v4): + +| Block-space share given to swaps | Measured 1.75 pools (v3 18.9%, v4 17.1%) | 1 pool, production routers (v3 10.3%, v4 23.2%) | 2.0 pools (v3 22.3%, v4 17.9%) | +| ------------------------------------------------- | ---------------------------------------- | ----------------------------------------------- | ------------------------------ | +| 4.2%, measured DEX-router burn, lower bound | 0.7–0.8% | 0.4–1.0% | 0.8–0.9% | +| 19.4%, "defi" category bracket | 3.3–3.7% | 2.0–4.5% | 3.5–4.3% | +| 32.7%, swap-event transactions, measured | 5.6–6.2% | 3.4–7.6% | 5.9–7.3% | +| 70%, stipulated AMM-trading upside (not measured) | 12.0–13.2% | 7.2–16.2% | 12.5–15.6% | + +Induced demand would fill freed space. Read these as an order of magnitude, not a +forecast. + +## On Base and Arbitrum + +L2 fees are cheap L2 execution plus a charge for the L1 data the sequencer posts. +Both were measured on Base (block 51439900) and Arbitrum One (block 506178800) +for 100 USDC to WETH through each protocol's deployed contracts, warmed, on the +deepest live Uniswap WETH/USDC pool of each chain; no live Ekubo pool there could +absorb the swap, so the Ekubo leg runs on a pool created on the fork with the v3 +pool's price, fee and liquidity. The L1 data component is what each chain's own +fee oracle charges for the exact transaction, in gas at the block's L2 base fee: + +| Chain | Leg (100 USDC to WETH) | Execution gas | Calldata bytes | L1 data (gas-equivalent) | Total, incl. 21,000 base and calldata gas | +| -------- | -------------------------- | ------------- | -------------- | ------------------------ | ----------------------------------------- | +| Base | Ekubo, Yul router | **105,084** | 184 | 331 | **128,171** | +| Base | Uniswap v3, SwapRouter02 | 121,803 | 228 | 319 | 144,646 | +| Base | Uniswap v4, minimal locker | 126,959 | 292 | 367 | 150,694 | +| Arbitrum | Ekubo, Yul router | **113,121** | 184 | 561 | **136,870** | +| Arbitrum | Uniswap v3, SwapRouter | 130,896 | 260 | 603 | 154,415 | +| Arbitrum | Uniswap v4, minimal locker | 135,006 | 292 | 644 | 159,234 | + +The mainnet pattern carries over: the whole transaction is 14.9% and 11.4% +cheaper than v4 and v3 on Base, 14.0% and 11.4% on Arbitrum, since both chains +run the same EVM at the same opcode prices. The data component decides nothing +at these blocks: 0.2% to 0.4% of every leg, and on Base not even smallest for +Ekubo, whose dense route compresses worse than Uniswap's zero-padded calldata. +Compact calldata buys nothing measurable while L1 data is this cheap. + +## Why it is cheaper + +- **Cheaper pool core.** One packed storage word and Q64 fixed-point math cost + about 20k gas per swap, against 29k for v4's multi-slot state and 37k for v3's + slot0, fee-growth and tick bookkeeping (lab flamegraphs in `benchmarks/`). +- **Net settlement.** Ekubo and v4 settle once per transaction (2 token transfers + for a route of any length); v3 pays 2 per pool. On the production contracts + each extra pool costs about 22k of execution on Ekubo, 34k on v4, 59k on v3. +- **Compact calldata.** The Yul router packs a single-pool route into 184 bytes; + Uniswap's Universal Router needs 1,092 for a single v4 swap with its Permit2 + funding, v3's SwapRouter 260. + +## Measured honestly + +Providing liquidity is measured where providers actually touch it: production +position managers on the live USDC/USDT pools. Minting a new ~10,000 USDC/USDT +position costs 244,513 on Ekubo against 297,390 on v4 (17.8% less) and 381,653 on +v3 (35.9% less). One-time costs (pool creation, tick initialization, first-swap +accumulator writes) are excluded by design everywhere on this page, and the v4 +Universal Router was measured only at one pool. Scope: EVM only, concentrated +pools, no hooks or extensions on any side — any v4 hook or Ekubo extension changes +every number here. + +## How it was measured + +- Production Ekubo Core, Positions and Yul router; Uniswap v3 factory and + SwapRouter; v4 PoolManager and Universal Router, on a fork of the latest + mainnet block at run time, four fresh ERC20 tokens, 0.3% pools of matched + liquidity, no tick crossings. +- Warmed, isolated steady state: one identical unmeasured call first, every + touched slot already nonzero, each measured call its own transaction, so the + snapshot is execution gas net of refunds; exact calldata recorded per call. +- Minimal single-lock routers of the same shape on all sides for route scaling, + which favors Uniswap: its production routers cost more than the minimal + figures, Ekubo's costs less. +- L2s: Base and Arbitrum One forks pinned by block, deployed Uniswap routers and + pools, the deployed Ekubo Yul router on a fresh pool mirroring the v3 pool; L1 + data read from each chain's fee oracle, not modeled. +- Pools per swap: a uniform 10% sample of a month of mainnet block receipts over + public JSON-RPC, every event signature hashed from its protocol's source. Not + counted: order-book and RFQ fills unless they land in a pool, the legacy Ekubo + v2 core, long-tail AMMs. Sources, snapshots, calldata, pool identifiers and + reproduction commands: `benchmarks/` in the docs repository. diff --git a/src/content/docs/reference/price-representation.md b/src/content/docs/reference/price-representation.md index 5336e2f..5d2ccb2 100644 --- a/src/content/docs/reference/price-representation.md +++ b/src/content/docs/reference/price-representation.md @@ -7,9 +7,9 @@ title: "Price representation" Every Ekubo pool tracks its current price as a **square root ratio**: the square root of the price of `token1` in terms of `token0`. -$$ -sqrt\_ratio = \sqrt{token1/token0} -$$ +``` +sqrt_ratio = sqrt(token1 / token0) +``` Working with the square root of the price (rather than the price itself) makes the concentrated liquidity math cheaper and more precise — see the [Math 1-pager](/reference/pool-math/) for why. Prices are supported over the enormous range `[2^-128, 2^128]`, so the sqrt ratio always lies in `[2^-64, 2^64]`. diff --git a/tsconfig.json b/tsconfig.json index 8bf91d3..60cddd6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,10 @@ { "extends": "astro/tsconfigs/strict", "include": [".astro/types.d.ts", "**/*"], - "exclude": ["dist"] + "exclude": [ + "dist", + "benchmarks/**/lib", + "benchmarks/**/out", + "benchmarks/**/cache" + ] }