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 35/35 adversarial audit 12/12 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 each leg is re-probed on current state right before it executes. If the fill is below that 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
Registry spam, identity variants of one poolbond, canonical venue id, 32 entries per pair

Adversarial audit

An independent adversarial review preceded deployment: 12 findings (4 high, 3 medium, 4 low, 1 info), all fixed, each with a regression test. The high findings: a venue could forge a probe result through a bubbling revert; the old fallback path could fill a venue whose own planned leg then reneged, striking an honest venue; revert-data bombs were copied into the outer frame; the registry allowed free identity variants, crowd-out and unbounded reads. 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%

Contracts

Monad mainnet, chain id 143. Deployed pending — deployment in progress.

CandorRouter
pending
VenueRegistry
pending
UniV3Adapter
pending
UniV4Adapter
pending
UniV2Adapter
pending
PathAdapter
pending
KuruAdapter (experimental)
pending
Owner / fee recipient
pending

Seeded venues

WMON/USDC: Uniswap V3 0.01 / 0.05 / 0.3 / 1%, PancakeSwap V3 0.01 / 0.05 / 0.25%, Uniswap V4 native-MON/USDC 0.01 / 0.05 / 0.3 / 1%. USDC/USDT0: Uniswap V3 0.01 / 0.05 / 0.3%. WMON/USDT0: Uniswap V3 0.01 / 0.05 / 0.3% plus two-hop paths through USDC in both directions. Anyone can add more.

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, refunded on withdrawal while the venue has no strikes and slashed on the first strike. Identity is canonical per pool, so a struck pool cannot come back under a different encoding. At most 32 live entries per pair.

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: "pending", registry: "pending" });

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.

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.