# Candor: fully on-chain, spoof-proof order routing

Version 1.0 — September 2026 — https://candor.dagentx.com

## Abstract

Aggregators choose a venue off-chain from quotes and then send the user's trade to that venue
on-chain. A venue can tell those two events apart, so a malicious maker quotes tight to win the
routing decision and fills wide when the real trade arrives. Candor removes the gap by taking the
routing decision on-chain and making the quote *be* the fill: every candidate venue is executed
inside the user's transaction in a frame that is then reverted, the best result is selected, and
the winner is executed through an identical frame and held to at least its probed output. Nothing
observable to a venue differs between the probe and the execution, so there is no channel left to
spoof through. The mechanism needs cheap compute, runtime-determined account access, revert data
that bubbles through self-calls, and transient storage. Monad has all four.

## 1. The problem

Documented on Solana aggregators (fees flipping to about 1%) and on PropAMMs and Uniswap V4 hooks
(fees alternating between 0% and 18%), aggregator spoofing is a hidden tax on users whose slippage
settings are loose, and a failed trade for users whose settings are tight. In both cases the tight
quote was never real.

Every variant relies on the venue observing something that differs between being quoted and being
filled:

- a storage call counter (first call in the block is tight, later calls are wide);
- a transient-storage counter;
- staticcall detection (a view quoter says tight, the swap charges wide);
- the gas stipend (simulation gas differs from execution gas);
- `tx.origin` (the aggregator's simulation bot versus a user);
- block parity or time (alternate tight and wide);
- caller identity.

## 2. Mechanism

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. In split mode it allocates the order in chunks, each chunk going to the venue with the
largest marginal output given what it already holds; every marginal is another real, reverted
execution. It then runs each winning leg 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 the leg reverts, the router records a strike in the registry, and the
amount is re-routed over the remaining venues with fresh probes.

## 3. Indistinguishability

| 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 |
| venue storage / transient storage | untouched (reverted) | untouched |
| venue token balances | untouched (reverted) | untouched |
| call depth | router → router → adapter → venue | identical |
| "have I been called before in this tx?" | no memory survives | no memory |

The venue cannot even count. A "first call tight, second call wide" policy sees exactly one call,
and the router enforces the tight price. A venue can still discriminate on *who* is trading (for
example, everyone routed through Candor) but not on *when*. That is a price, not a spoof: it is the
price the user is shown and receives, and the venue simply does not win the flow.

## 4. The residual case

The only way an execution can deliver less than its probe is if state changed in between, or the
venue behaves differently in the two frames. Candor removes the first: probe and execution share
one self-call entry point that is byte-identical up to and including the venue call, and a gas
pre-check guarantees both frames start with exactly the same stipend. In split mode, a leg that
follows a filled leg is re-probed on the now-current state; if that probe is below what the venue
quoted at plan time, the venue is not filled on the worse number: it is dropped for the
transaction without a strike and the remainder is re-planned over the venues still alive. Split
mode also carries an on-chain floor: every live venue is probed for the full amount first, and the
split must deliver at least the best single venue minus 0.10% or the whole transaction reverts,
so a venue that moves a competitor's pool inside its own fill reverts its manipulation along with
the trade and earns nothing. Gas is bounded by a fixed per-transaction frame budget; a venue that
fails or degrades is dead after one frame, and a probe that burns most of its stipend counts as no
fill. What remains is a venue that
behaves differently between a probe and its immediately following execution on identical state;
then the leg reverts, nothing it did is kept, the venue is struck (its registration bond slashed,
and it is quarantined with exponential backoff), it is dead for the rest of the transaction, and
the remaining amount is re-planned over the venues still alive. Legs already filled are kept. The
user never sees the reneged fill.

Nothing a venue or a token emits reaches the router as data. The adapter and the token calls are
made low-level inside the frame with no return data copied and any failure becomes the router's
own error, so neither a venue nor a malicious token can forge a probe result; and the router only
ever copies fixed-size return data from its self-calls, so nobody can make the user pay for a
revert-data bomb. A venue whose probe fails is not probed again in that transaction.

## 5. Guarantees

1. **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.
2. **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
   minus 0.10%, enforced on-chain.
3. **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.
4. **Public, bonded ledger.** Every renege is an on-chain event with expected versus actual
   output. It slashes the registrant's bond and quarantines the venue. Identity is canonical
   (`keccak(adapter, adapter.venueId(params))`), so one pool cannot shed strikes by re-registering
   under different bytes.

## 6. Multi-hop paths

A path adapter presents a fixed sequence of hops as one 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 Uniswap V4 native pool and
the 0.01% Uniswap V3 stable pool beat all three direct WMON/USDT0 pools in the mainnet-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.

## 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.
- **No pre-declared account access.** Which venues get touched is decided at runtime from the
  registry, including venues registered after the transaction was built.
- **Try/catch with revert data and transient storage.** The design is a reverted-frame trick.
- **No public mempool.** Nobody sees the probes; nothing leaks before inclusion.

## 9. Trust and governance

Adapter *code* is allow-listed by the registry owner; the pool an adapter is pointed at is
arbitrary and untrusted by design, and none of the guarantees above depend on it behaving. Pool
registration is permissionless and bonded (1 MON, refundable after a 7-day minimum listing while
strike-free, 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; each pair holds at most 32 live
entries so every registry read is bounded. 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, fees are
accounted separately from anything in flight, and the router holds nothing else between
transactions. Ownership transfers are two-step. A timelock or multisig in front of the owner is
recommended before treating the registry as neutral infrastructure.

## 9a. Audit

An adversarial review ran four passes before deployment: 21 findings in total (6 high, 5 medium,
6 low, 4 informational), all addressed, each with a regression test. Round one: a venue could
forge a probe result through a bubbling revert; the 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. Round two: the
first fix for the fallback path had opened a split-mode price spoof (tight at plan time, wide at
the pre-leg re-probe, filled wide without a strike), and a malicious token could still forge a
probe result through the router's own token calls; plus refundable bonds with an append-only pair
list allowed free list inflation, and identities proved shape rather than existence. Round three:
a degrading venue could force up to eight re-plan rounds and multiply swap gas about 130 times for
free, and a venue could move a competitor's pool inside its own fill so the user's later legs
landed worse. The mechanism in sections 2 through 4 and the registry in section 9 are the
post-audit design.

## 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 order books are solver-gated on-chain today, so the Kuru
adapter never wins until Candor is registered as a solver. Users supplying their own venue list
take on the trust of those adapters; the registry path is the audited one.

## 11. Verification

- Unit suite: 40 tests, including a mock venue that implements every spoofing pattern above,
  forged probe results from venues and from tokens, revert-data bombs, gas bombs, stipend burners,
  reverts, partial fills, the split-mode degrade re-plan, an intra-transaction sandwich that must
  revert, and the renege, re-plan and bond-slash paths.
- Monad mainnet-fork suite: 5 tests against 17 live venues (Uniswap V3, PancakeSwap V3, Uniswap V4
  native-MON pools, two-hop paths).
- Deployment addresses and the seeded venue set are listed at https://candor.dagentx.com.
