The quote is the fill.

Candor is a fully on-chain order router for Monad. Every venue is executed inside your transaction, the best result wins, and the winning venue is held to exactly that price. There is no off-chain quote anywhere in the price path, so there is nothing to spoof.

Monad mainnet · chain 143 unit tests 41/41 · fuzz 5×1024 · invariants 128k calls adversarial audit, 4 passes 23/23 fixed mainnet-fork tests 5/5 protocol fee 0.05% · cap 0.30% venues: UniV3 · PancakeV3 · UniV4 (hooks, native MON) · UniV2 · paths

Aggregator spoofing is a hidden tax

Documented on Solana aggregators, then on PropAMMs and Uniswap V4 hooks.

1. Quote tight

The aggregator asks venues for prices off-chain. A malicious maker answers with a very tight quote and wins the routing decision.

2. Fill wide

When the user's transaction reaches the venue, the venue can tell it is the real trade and charges a wide spread. Hooks have been observed flipping between 0% and 18%.

3. Slippage does not save you

A tight limit price makes the trade fail. A loose one, which is most users, pays the tax. Either way the tight quote was never real.

Every version of the trick works the same way: the venue can observe something that differs between being quoted and being filled. A storage counter, a transient counter, a staticcall, the gas stipend, tx.origin, block parity, who is calling. Each is a channel that separates "being looked at" from "being traded with." Candor closes all of them at once.

How Candor works

Probe by real, reverted execution. Then execute the winner through the identical frame.

User calls swapExactIn. Input tokens (or native MON) land in the router.
For each venue the router calls itself: probe() executes the swap for real, measures the router's balance delta, then reverts carrying the output.
The revert undoes everything the venue did: storage, transient storage, balances. The venue has no memory of being probed.
Best venue wins. With chunks > 1, the order is split greedily by marginal output, each marginal being another real reverted execution.
The winner runs through the same frame(): same gas stipend (pre-checked), sender, calldata shape and block. In split mode a leg that follows a filled leg is re-probed on current state; if it comes in below what it quoted at plan time it is not filled on the worse number, the remainder is re-planned. If a fill is below its probe, the leg reverts.
A venue that reneges is struck: bond slashed, quarantined in the public registry, dead for the rest of the tx. The remaining amount is re-planned over live venues. The user never sees the bad fill.

What the venue can observe

ObservableProbe frameExecute frame
msg.sender (the adapter)samesame
calldatasamesame
gas stipend at frame entryprobeGasprobeGas
tx.origin, block.*, chain statesamesame
its own storage / transient storageuntouched (reverted)untouched
its own token balancesuntouched (reverted)untouched
router token balancessame: each leg is re-probed right before executionsame
call depthrouter → router → adapter → venueidentical
"have I been called before in this tx?"no memory survivesno memory

A venue can still discriminate on who is trading (every Candor user, say) but not on when. That is a price, not a spoof: it is the price the user is shown and gets, and the venue simply does not win the flow.

Proof

The test suite reproduces every spoofing pattern and shows the router cannot be gamed by any of them.

Attack (mock venue mode)Naive quote-then-fillThrough Candor
Storage counter: 0% on first call in block, 18% aftertaxed 18%filled at 0%, held to it
Transient-storage countertaxed 18%filled at 0%
Lying quoter: view says 0%, swap charges 18%taxed 18%never selected, honest venue wins
Gas-stipend detectionsimulation ≠ executionsame stipend both frames, consistent
Block parity, tx.origin detectiontight for the bot, wide for yourouter sees the truth at execution
Gas bomb / always-revert venuedead after first failed probe; costs at most one stipend
Revert-data bomb (800 KB payload)never copied; costs nothing outside the stipend
Forged ProbeResult revert from a fake poolnormalised inside the frame; reads as no fill
Fill below the immediately-preceding probeleg reverts, venue struck + bond slashed, remainder re-planned
Split mode: tight at plan time, wide once the router holds outputfilled widedegraded re-probe is never filled; venue dropped, remainder re-planned
Split mode: venue moves a competitor's pool inside its own fill (intra-tx sandwich)later legs land worsesplit must beat best-single − 0.10% on-chain, else the tx and the manipulation revert
Degrading venue forcing many re-plan rounds (gas ×130)dead on first degrade; fixed per-tx frame budget; stipend burners count as no fill; two stipends max per venue per tx
Malicious token forging ProbeResult inside transfertoken calls are low-level too; reads as no fill, no strike
Registry spam, identity variants of one poolbond, canonical venue id + live-pool validation, 32 live entries per pair

Adversarial audit

An independent adversarial review ran four passes before deployment: 23 findings in total (6 high, 6 medium, 7 low, 4 info), all fixed, each with a regression test. Round one found that a venue could forge a probe result through a bubbling revert, that the old fallback path could fill a venue whose own planned leg then reneged and strike an honest venue, that revert-data bombs were copied into the outer frame, and that the registry allowed free identity variants, crowd-out and unbounded reads. Round two found that the first fix for the fallback path had itself opened a split-mode price spoof (tight at plan time, wide at the pre-leg re-probe) and that a malicious token could still forge a probe result through the router's own token calls. Round three found a free gas-amplification path through repeated re-plans and an intra-transaction sandwich between venues; both are closed by the frame budget and the on-chain best-single floor. The mechanism described on this page is the post-audit design. Full write-up in the whitepaper and DESIGN.md in the source.

On Monad mainnet state

Fork tests against 17 live venues (Uniswap V3, PancakeSwap V3, Uniswap V4 native-MON pools, and two-hop paths through USDC) at block height of 2026-09-14:

100 WMON → USDC   11 venues probed, best: Uniswap V4 native/USDC 0.05%
                  fill == best same-block probe (minus 0.05% fee)
2000 WMON → USDC  greedy split: 500 via V3, 1500 via V4 — beats best single venue
100 WMON → USDT0  a 2-hop path (V4 native→USDC, V3 USDC→USDT0 0.01%) beat all 3 direct pools
USDC → native MON, native MON → USDC   round-trip within 0.2%

On mainnet, for real

First swap through the live router on 2026-09-14: 0.5 MON → USDC, best of 11 registered WMON/USDC venues, filled through the Uniswap V3 adapter with the 0.05% fee accrued in-contract. Transaction 0x8c681dcda103bc06b9a8d69a74cbec2d1769766d189c1898c4358d41f874e07b.

Contracts

Monad mainnet, chain id 143. Deployed 2026-09-15. Router v1 was superseded the same day by v2 (identical logic, test hook removed from bytecode, ownership timelocked).

CandorRouter
0xd2e3efca153eb574eeed7c838efde93fe38b79bc
VenueRegistry
0xd7dc225c69afbf55f9c2d39c6053662e1ab8bcfc
TimelockController (owner, 24h delay)
0xb29713fb7492809c6267cffbf72dad9ee2099fc4
UniV3Adapter
0xc8f34962eda52c703cd9b3447da30f7091641586
UniV4Adapter
0x81a9e1b50a4adb490410222e5bf96dce9a7c9ef0
UniV2Adapter
0xb3fb04c6ba743941d242199cb8edc6abbe7798a0
PathAdapter
0x8057116fc2927bb9e30a9ff959681df51e6ed4ea
KuruAdapter (experimental)
0x8f5bdec951837c600a636424504d3ad53c759053
NadFunAdapter
0x89c868a9c2c83148a384273d9a4341a38f4ac9e1
NadFunLister (on-chain meme keeper)
0x242bc4967090dd35f475ad8a397ff0f06f8e40cb
Deployer / proposer / fee recipient
0x57190e1006bcdf4c3ed38c94b378c73fc38dba7a
Router v1 (superseded, do not use)
0x597a411f8ef8015cacd9bf0701fb21fc56f8ad4c

Machine-readable: deployments.json.

Governance

Ownership of the router and the registry is held by an OpenZeppelin TimelockController with a 24-hour delay. Every admin action (fee within the cap, per-venue gas stipend, registry pointer, adapter allow-list, moderation) must be scheduled publicly and can only execute a day later; anyone may execute a matured operation. The deployer key is the sole proposer today and is meant to be replaced by a multisig. The two-step ownership transfers were scheduled at deployment and take effect once the delay passes.

Seeded venues

76 venues across 22 pairs, every pool with live liquidity on Uniswap V3, PancakeSwap V3 and Uniswap V4 native-MON pools at seeding time. Native MON works on either side of any WMON pair.

PairVenues
WMON/USDC11 (Uniswap V3 ×4, PancakeSwap V3 ×3, Uniswap V4 native ×4)
WMON/USDT011 (Uniswap V3 ×3, PancakeSwap V3 ×3, Uniswap V4 native ×2, two-hop paths via USDC ×3)
WMON/AUSD, WMON/WETH7 each
WMON/shMON6
WMON/WBTC, WMON/sMON5 and 4
WMON/gMON4
WMON/aprMON, WMON/wstETH, WMON/USDX1 each
USDC/USDT0, USDC/AUSD4 each
USDC/WETH, USDC/WBTC, USDC/USDX2 each
USDC/sMON, USDC/aprMON, USDC/gMON, USDC/shMON1 each

Anyone can add more: any pool behind an allow-listed adapter, 1 MON bond. Live list: VenueRegistry.idsFor(tokenA, tokenB).

Fee

0.05% of routed output, taken after routing so it never changes which venue wins and never touches the probe-equals-fill invariant. Hard-capped in code at 0.30%; can be set to zero. Fees accrue in the router and are pulled with collectFees, so a fee transfer can never block a swap. Quotes report gross, fee and net.

Listing bond

Registering a venue costs a 1 MON bond, refundable after a 7-day minimum listing while the venue has no strikes, slashed on the first strike (or by the owner for junk). The adapter validates at registration that the params describe a live pool for the pair. Identity is canonical per pool, so a struck pool cannot come back under a different encoding. At most 32 live entries per pair.

Meme tokens, on-chain keeper

nad.fun tokens are venues too, on the bonding curve and after graduation, and a contract decides which ones get listed.

NadFunAdapter

Both nad.fun generations. V2 through the single router that serves the curve before graduation and the DEX pool after; V1 through whichever router the lens names. A listing survives graduation. Every fill is held to its own reverted probe like any other venue.

NadFunLister

Anyone calls list(token, version). The contract prices the token from nad.fun's own quote for 1 MON, converts with the WMON/USDC pool, and lists only if market cap clears $200k. It pays the 1 MON registry bond from its treasury and tips the caller. Bonds of clean listings recycle after 7 days.

Keeper trigger

The chain decides; an off-chain job only proposes. A 30-minute job reads nad.fun's market-cap ordering and calls the lister for anything not yet listed. Anyone can run it and collect the tips.

Seeded at launch, every nad.fun token above $200k on 2026-09-14: JAMES, BUSY, CHOG, MUSHROOM, emo, moncock, MOLANDAK. Paste any nad.fun token address on the swap page to trade it through the router.

Cross-chain

Probe-by-revert cannot reach across chains, so a bridge is never a Candor venue. Instead the bridge page bundles Wormhole Connect (Token Bridge, CCTP for USDC, NTT) so anyone can bring assets in from Ethereum, Solana, Arbitrum, Base and others with Monad as the destination, swap through Candor, and bridge back out. Same rails the WRAITH wallet routes on; Candor never holds bridged funds.

Use it

Wallets, bots, and other routers can call the router directly. Anyone can list a pool.

Swap

struct SwapParams {
  address tokenIn;      // address(0) = native MON
  address tokenOut;     // address(0) = native MON
  uint256 amountIn;
  uint256 minAmountOut; // on the net amount, after fee
  address recipient;
  uint256 deadline;
  uint8   chunks;       // 1 = best single venue; 2..8 = greedy split
  uint8   maxVenues;    // 0 = default (8)
  Venue[] venues;       // empty = use the registry; or bring your own list
}
function swapExactIn(SwapParams calldata p) external payable
  returns (uint256 amountOut, Fill[] memory fills);
function quote(SwapParams calldata p) external payable returns (Quote memory); // eth_call it

TypeScript (viem)

import { createPublicClient, http } from "viem";
import { createCandor, MONAD, NATIVE } from "@candor/sdk";

const client = createPublicClient({ transport: http("https://rpc.monad.xyz") });
const candor = createCandor({ client, router: "0xd2e3efca153eb574eeed7c838efde93fe38b79bc", registry: "0xd7dc225c69afbf55f9c2d39c6053662e1ab8bcfc" });

const req = { tokenIn: NATIVE, tokenOut: MONAD.USDC, amountIn: 10n ** 18n, minAmountOut: 0n, recipient: me, chunks: 2 };
const { amountOut } = await candor.simulate(req, me);          // exact fill, same code path as the tx
const tx = candor.buildSwap({ ...req, minAmountOut: amountOut * 995n / 1000n });
const gas = await candor.gasLimit(req, me);                     // Monad bills the gas limit

List a venue

// any pool, behind an allow-listed adapter; 1 MON bond (refundable while strike-free)
VenueRegistry.register{value: 1 ether}(tokenA, tokenB, UniV3Adapter, abi.encode(pool));
VenueRegistry.register{value: 1 ether}(WMON, USDC, UniV4Adapter, abi.encode(PoolKey(...), hookData));
VenueRegistry.withdraw(id);   // disable + refund, if never struck
// venues that fill below their own probe get struck, slashed and quarantined with exponential backoff
event Reneged(uint256 id, address adapter, uint256 expected, uint256 actual, uint32 strikes, uint64 quarantinedUntil);
event BondSlashed(uint256 id, uint256 amount);

Monad has no public mempool and charges the gas limit, not gas used. Size the limit from an estimate plus modest headroom; the SDK does this for you. chunks = 1 is the default and the always-available mode; split routing (chunks > 1) is best-effort and can revert where single-venue mode succeeds, so offer it only when a simulation with it succeeds.

Whitepaper

Candor: fully on-chain, spoof-proof order routing. v1.0, September 2026. Plain-text version.

1. The problem

Aggregators pick a venue off-chain from quotes, then send the user's trade to that venue on-chain. Those are two separate events, and a venue can tell them apart. So a malicious maker quotes tight to win the routing decision and fills wide when the real trade arrives. Slippage limits cap the damage for careful users; everyone else pays a hidden tax. This has been documented on Solana aggregators, with fees flipping to about 1%, and on PropAMMs and Uniswap V4 hooks, with fees alternating between 0% and 18%.

The tricks all reduce to one thing: the venue can observe something that differs between the quote and the fill. Storage call counters, transient counters, staticcall detection, gas stipend, tx.origin, block parity, the identity of the caller. Every one of them is a channel that distinguishes "being looked at" from "being traded with."

The promise of open systems is the elimination of middlemen and hidden taxes. Aggregator spoofing is a hidden tax, and it can only be removed by taking the routing decision on-chain.

2. The mechanism

Candor closes every channel at once by making the quote be the fill. For each candidate venue, inside the user's transaction, the router calls itself:

router.probe(venue, tokenIn, tokenOut, amountIn)   // external self-call, fixed gas stipend
  → transfer amountIn to the adapter
  → adapter executes the swap for real on the venue
  → router measures its own tokenOut balance delta
  → revert ProbeResult(delta)

The frame reverts, so every state change the venue made is undone: storage, transient storage, balances. The router decodes the output from the revert data. After probing all venues it picks the best (or, in split mode, greedily allocates chunks by marginal output, each marginal being another real reverted execution) and then runs the winner through router.execute(...): the same code path with the same gas stipend, same sender, same calldata shape, same block. execute keeps the fill only if out ≥ probedOut; otherwise it reverts the leg, the router records a strike in the registry, and re-routes that amount over the remaining venues with fresh probes.

3. Why the venue cannot tell

Between the probe frame and the execute frame, msg.sender, calldata, the gas stipend at frame entry, tx.origin, block values, chain state, call depth, the venue's own storage, transient storage and token balances are all identical. The venue cannot even count: a "first call tight, second call wide" policy sees exactly one call, and the router enforces the tight price. What a venue can still do is discriminate on who rather than when. It can quote everyone routed through Candor wide. That is a price, not a spoof: it is the same price the user is shown and gets, and the venue simply does not win the flow.

4. The residual case

The only way execute can deliver less than probe is if state changed in between. Within a single-venue route nothing can. In split mode, leg N executes before leg N+1, so two registry entries pointing at the same pool would see leg 2 come in short. The registry rejects exact duplicates, and if a venue reneges anyway the leg reverts, the entry is struck and quarantined with exponential backoff, and the amount is re-routed. The user never sees the reneged fill.

5. Guarantees

6. Multi-hop paths

A path adapter presents a fixed sequence of hops as a single venue. Because probing is real execution, a two-hop route is quoted and held to its price exactly like a single pool, and competes with direct pools on equal terms. On Monad, a WMON → USDC → USDT0 path through the V4 native pool and the 0.01% V3 stable pool beat all three direct WMON/USDT0 pools in the fork test.

7. Fee

A protocol fee of 0.05% is taken from the routed output after venue selection. It cannot change which venue wins and does not participate in the probe-equals-fill check. It is hard-capped in code at 0.30%, can be set to zero, and is reported by quote() as gross, fee and net so integrators can display exact numbers.

8. Why Monad

9. Trust and governance

Adapters are allow-listed by the registry owner so a registration can point at any pool but never at arbitrary code; pool registration itself is permissionless. Strikes are reported only by the router. The router owner can change the fee within the cap, the per-venue gas stipend and the registry pointer; the owner cannot touch user funds mid-swap, and the router holds nothing between transactions. A timelock or multisig in front of the owner is recommended before treating the registry as neutral infrastructure.

10. Limitations

Exact-output swaps are not supported in v1. The greedy split is monotone but not optimal. Paths are fixed sequences, not searched. Kuru's order books are solver-gated on-chain today, so the Kuru adapter never wins until Candor is registered as a solver.