Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 11 additions & 2 deletions contracts/monerium-forwarder/test/VortexForwarder.fork.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion docs/adr-0005-monerium-b2b-onramp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
Loading