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.
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.
swapExactIn. Input tokens (or native MON) land in the router.probe() executes the swap for real, measures the router's balance delta, then reverts carrying the output.chunks > 1, the order is split greedily by marginal output, each marginal being another real reverted execution.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.What the venue can observe
| Observable | Probe frame | Execute frame |
|---|---|---|
msg.sender (the adapter) | same | same |
| calldata | same | same |
| gas stipend at frame entry | probeGas | probeGas |
tx.origin, block.*, chain state | same | same |
| its own storage / transient storage | untouched (reverted) | untouched |
| its own token balances | untouched (reverted) | untouched |
| router token balances | same: each leg is re-probed right before execution | same |
| call depth | router → router → adapter → venue | identical |
| "have I been called before in this tx?" | no memory survives | no 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-fill | Through Candor |
|---|---|---|
| Storage counter: 0% on first call in block, 18% after | taxed 18% | filled at 0%, held to it |
| Transient-storage counter | taxed 18% | filled at 0% |
| Lying quoter: view says 0%, swap charges 18% | taxed 18% | never selected, honest venue wins |
| Gas-stipend detection | simulation ≠ execution | same stipend both frames, consistent |
Block parity, tx.origin detection | tight for the bot, wide for you | router sees the truth at execution |
| Gas bomb / always-revert venue | — | dead 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 pool | — | normalised inside the frame; reads as no fill |
| Fill below the immediately-preceding probe | — | leg reverts, venue struck + bond slashed, remainder re-planned |
| Split mode: tight at plan time, wide once the router holds output | filled wide | degraded 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 worse | split 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 transfer | — | token calls are low-level too; reads as no fill, no strike |
| Registry spam, identity variants of one pool | — | bond, 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).
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.
| Pair | Venues |
|---|---|
| WMON/USDC | 11 (Uniswap V3 ×4, PancakeSwap V3 ×3, Uniswap V4 native ×4) |
| WMON/USDT0 | 11 (Uniswap V3 ×3, PancakeSwap V3 ×3, Uniswap V4 native ×2, two-hop paths via USDC ×3) |
| WMON/AUSD, WMON/WETH | 7 each |
| WMON/shMON | 6 |
| WMON/WBTC, WMON/sMON | 5 and 4 |
| WMON/gMON | 4 |
| WMON/aprMON, WMON/wstETH, WMON/USDX | 1 each |
| USDC/USDT0, USDC/AUSD | 4 each |
| USDC/WETH, USDC/WBTC, USDC/USDX | 2 each |
| USDC/sMON, USDC/aprMON, USDC/gMON, USDC/shMON | 1 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
- Price = probe. Per leg, the user receives at least the output the venue produced for the identical call in the same transaction, or the transaction reverts. No off-chain quote exists anywhere in the price path.
- Best of the set. With one chunk the user gets the maximum over active venues at execution state. With more chunks they get a greedy split that is never worse than the best single venue.
- Bounded griefing. Each venue frame gets a fixed stipend. A venue that burns gas or reverts is skipped and costs the user at most one stipend.
- Public ledger. Every renege is an on-chain event with expected versus actual, so "who spoofs" becomes public data rather than an article that gets taken down.
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
- Cheap, plentiful compute. Routing cost is O(venues × chunks) full swap executions. Eleven WMON/USDC venues cost about 5M gas for a full quote plus swap, well under a cent at today's MON price. The same on Ethereum L1 would be prohibitive.
- No pre-declared account access. Which venues get touched is decided at runtime from the registry, including venues registered after the transaction was built. Access-list chains cannot do this.
- Try/catch with revert data and transient storage. The design is a reverted-frame trick; it needs revert data to bubble through an external self-call, and adapters use transient storage for callback authentication.
- No public mempool. Nobody sees the probes; nothing leaks before inclusion.
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.