From 0a96dbcfaaf17c0b004c466be285c2606d714259 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 12 Oct 2026 16:32:48 +0200 Subject: [PATCH] fix(repo): move the mainnet eure/eurc hop to the 1 bp pool The planned initial route ran EURe to EURC on the 5 bps tier, but by block 26176624 that pool held no liquidity in range: its LPs moved to the 1 bp pool. The fork swap tests reverted on the old path, while EURe to EURC on 1 bp, then EURC to USDC on 5 bps, quoted 9 to 12 bps below Chainlink for 1k to 25k EURe. The pool check now also requires liquidity in range at the fork block, because a drained pool keeps a dust balance and passed the old token-balance check. --- .../test/VortexForwarder.fork.t.sol | 13 +++++++++++-- docs/adr-0005-monerium-b2b-onramp.md | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/contracts/monerium-forwarder/test/VortexForwarder.fork.t.sol b/contracts/monerium-forwarder/test/VortexForwarder.fork.t.sol index fe65b9594..ac915b487 100644 --- a/contracts/monerium-forwarder/test/VortexForwarder.fork.t.sol +++ b/contracts/monerium-forwarder/test/VortexForwarder.fork.t.sol @@ -11,6 +11,10 @@ interface IUniswapV3Factory { function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address); } +interface IUniswapV3Pool { + function liquidity() external view returns (uint128); +} + interface IERC20Meta { function balanceOf(address) external view returns (uint256); function decimals() external view returns (uint8); @@ -29,8 +33,10 @@ contract VortexForwarderForkTest is Test { address constant UNIV3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; // Chainlink EUR/USD proxy — verify against data.chain.link before deploy (registry P8). address constant CHAINLINK_EUR_USD = 0xb49f677943BC038e9857d61E7d053CaA2C1734C1; - // Initial whitelisted route: EURe -> EURC -> USDC on the 5 bps tiers (registry P10). - uint24 constant POOL_FEE_EURE_EURC = 500; + // Initial whitelisted route: EURe -> EURC on the 1 bp tier, EURC -> USDC on the 5 bps + // tier (registry P10). Re-verified 2026-10-12 at block 26176624: the EURe/EURC liquidity + // moved from the 5 bps pool, which holds none in range any more, to the 1 bp pool. + uint24 constant POOL_FEE_EURE_EURC = 100; uint24 constant POOL_FEE_EURC_USDC = 500; VortexForwarderFactory factory; @@ -116,6 +122,9 @@ contract VortexForwarderForkTest is Test { assertTrue(hop2 != address(0), "EURC/USDC pool missing at pinned fee tier"); // The V2 pool must actually hold V2 tokens (stale-pool trap check). assertGt(IERC20Meta(EURE_V2).balanceOf(hop1), 0, "pinned hop1 pool holds no V2 EURe"); + // A drained pool keeps dust balances, so require liquidity in range at the fork block. + assertGt(IUniswapV3Pool(hop1).liquidity(), 0, "pinned hop1 pool has no liquidity in range"); + assertGt(IUniswapV3Pool(hop2).liquidity(), 0, "pinned hop2 pool has no liquidity in range"); } function test_fork_swapThenForward_executesWithinOracleBounds() public onlyForked { diff --git a/docs/adr-0005-monerium-b2b-onramp.md b/docs/adr-0005-monerium-b2b-onramp.md index bb92e518e..21766dc90 100644 --- a/docs/adr-0005-monerium-b2b-onramp.md +++ b/docs/adr-0005-monerium-b2b-onramp.md @@ -304,7 +304,7 @@ the promised window. Decisions: | P7 | `perSwapCap` | operational **€10k** / ceiling €50k (amended 2026-09-29; €25k before; re-measure liquidity at the deploy block before raising) | | P8 | `MAX_ORACLE_AGE` | **52 h** (observed Chainlink EUR/USD weekend gaps up to 48 h; applied to configs 2026-08-26) | | P9 | Notification confirmation depth | 32 blocks (implemented) | -| P10 | Router pin and routes | SwapRouter02 immutable; routes are a guardian-managed, on-chain validated whitelist (EURe/EURC/USDC, four tiers, ≤ 2 hops); initial route EURe→EURC→USDC at the 5 bps tiers, re-verify at the deploy block (amended 2026-09-15) | +| P10 | Router pin and routes | SwapRouter02 immutable; routes are a guardian-managed, on-chain validated whitelist (EURe/EURC/USDC, four tiers, ≤ 2 hops); initial route **EURe→EURC on the 1 bp tier, EURC→USDC on the 5 bps tier**, re-verify at the deploy block (amended 2026-09-15; re-verified 2026-10-12 at block 26176624: the EURe/EURC liquidity moved from the 5 bps pool, which no longer holds any in range, to the 1 bp pool; the new path quoted 9 to 12 bps below Chainlink for €1k to €25k, and the 5 bps path no longer quoted at all) | | P11 | Fee adjustability | Guardian `setFeePolicy(target, floor)` within `MAX_FEE_PPM`; raising either value is announced and applies after 24 h, lowering is immediate (amended 2026-09-15) | | P12 | Reference rate | **Coinbase Exchange EURC-USDC bid/ask midpoint read just before the swap, deferring on a spread above 50 bps** (amended 2026-09-18; from 2026-09-15 a five-minute VWAP over one-minute candles widened to 60 min on no volume), keeper-computed per swap; `MAX_REFERENCE_DEVIATION_BPS` **100** (immutable, to confirm before deploy: must tolerate a weekend Chainlink gap); permissionless path uses Chainlink (2026-09-15). The floor on the net binds first: with `floorPpm` 15 bps and `SLIPPAGE_BPS` 40 bps, a reference more than `SLIPPAGE_BPS − floorPpm` ≈ 25 bps below Chainlink makes every normal fill (fee band or subsidized) revert on chain and defer off chain, so ~25 bps is the working downside margin against a stale round; the 100 bps band is the outlier ceiling for a keeper-supplied value, not the operating tolerance (2026-09-16) | | P14 | Subsidy ladder | **`MONERIUM_B2B_SUBSIDY_LADDER` = `0:0,360:10,480:20,600:30,720:40,840:50,960:100`** (2026-09-18; keeper policy, tunable from deferral logs); per-chunk clock; the vault's per-swap cap must be at least the ladder's top |