Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3917e79
Add gas efficiency comparison page with Uniswap v4/v3 benchmarks
moodysalem Sep 16, 2026
130a0bc
Rework gas benchmarks: warmed steady-state, real-pool fork legs
moodysalem Sep 16, 2026
617eb5d
Correct gas page to cold-path singles with warmed scaling marginals
moodysalem Sep 16, 2026
1b77acd
Finalize gas efficiency page on steady-state benchmarks
moodysalem Sep 17, 2026
47d74e2
Correct EIP-7623 floor figures to total floor ~24-29k gas
moodysalem Sep 17, 2026
6cf5ebb
Measure mints through production position managers; revise mint section
moodysalem Sep 17, 2026
896bb7a
Attribute bare mints by frame, drop init costs, model capacity on routes
moodysalem Sep 17, 2026
b54026d
Simplify gas-efficiency page for a PM audience; move harness detail t…
moodysalem Sep 17, 2026
b5a0f9c
Measure pools-per-swap on mainnet and rebuild the chain-capacity blen…
moodysalem Sep 17, 2026
a57c5f3
Extend pools-per-swap to every mainnet AMM family and a 1,000-block w…
moodysalem Sep 17, 2026
1b0fed6
Measure Ekubo vs Uniswap swap gas on Base and Arbitrum with L1 data c…
moodysalem Sep 17, 2026
165960f
Add production-contract fresh-token gas bench and 30-day stride sampling
moodysalem Sep 17, 2026
7497f11
Rebuild gas-efficiency page on production-contract bench and a 30-day…
moodysalem Sep 17, 2026
8ce429a
Exclude benchmark harness dependency dirs from astro check; format en…
moodysalem Sep 17, 2026
63e6025
Note that pools-per-swap and swap gas share are lower bounds
moodysalem Sep 18, 2026
46e4b2b
Move the gas-efficiency page to the Reference section
moodysalem Sep 18, 2026
864d485
Write the sqrt-ratio formula as a code block; no math renderer is con…
moodysalem Sep 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
964 changes: 964 additions & 0 deletions benchmarks/README.md

Large diffs are not rendered by default.

286 changes: 286 additions & 0 deletions benchmarks/fork-l2/ForkL2Ekubo.t.sol
Original file line number Diff line number Diff line change
@@ -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 <base-archive-rpc> --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 <arbitrum-archive-rpc> --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";
}
}
14 changes: 14 additions & 0 deletions benchmarks/fork-l2/ForkL2EkuboArbitrumTest.json
Original file line number Diff line number Diff line change
@@ -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"
}
14 changes: 14 additions & 0 deletions benchmarks/fork-l2/ForkL2EkuboBaseTest.json
Original file line number Diff line number Diff line change
@@ -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"
}
Loading
Loading