DEX aggregators: routes, reverts, and MEV
An aggregator quote can offer more tokens and still leave me worse off after an extra approval or a more expensive route. For a $500 swap, I want to see the minimum output and total cost before choosing it.
This is the quote record I would keep, followed by a read-only check of the exact calldata. I also check how the transaction will be submitted, because routing alone does not protect it from public-mempool MEV.
Pools, intermediate tokens, and split routes
An aggregator can split a swap across pools, use intermediate assets, or select a single pool. Uniswap’s Smart Order Router, for example, searches routes while accounting for split paths and gas costs. Its source repository documents that trade-off. A route that returns more tokens before gas can still be worse once its extra hops are paid for.
There are two execution models worth separating:
- Transaction routers prepare calldata for an on-chain transaction. The signed transaction normally enters the public mempool unless the wallet or RPC provides private submission. 0x exposes aggregated liquidity and returns transaction data for its Swap API integration. 0x’s documentation is a useful reference for the quote-to-transaction flow.
- Intent or auction systems accept a signed order and have solvers compete to settle it. CoW Protocol uses a fair combinatorial auction: solvers submit candidate solutions and the protocol selects winners under its objective and fairness constraints. The auction specification explains that model. The trade-off is that fill timing and reliability are different from an immediate router transaction.
For the same $500 USDC order, I care about this quantity rather than the headline quote:
net output = quoted output - gas cost valued in buy token - explicit integrator fee
Do not subtract a guessed gas cost from a number displayed in a different token without converting it at the same block. More importantly, compare the minimum received field too. That is the amount your slippage setting permits, and it is the relevant worst case if the transaction executes.
A minimal quote record and preflight simulation
The following JavaScript records the fields I want from any aggregator response. It deliberately does not sign or broadcast. Populate quote with a response from the router you are evaluating, then run an eth_call against the exact to, data, and value before trusting a wallet confirmation screen.
import { JsonRpcProvider, formatUnits } from 'ethers';
const provider = new JsonRpcProvider(process.env.RPC_URL);
async function inspectQuote(quote, sellDecimals, buyDecimals) {
// Keep raw integer amounts; JavaScript Number loses precision for token units.
console.table({
sellAmount: formatUnits(quote.sellAmount, sellDecimals),
quotedBuyAmount: formatUnits(quote.buyAmount, buyDecimals),
minBuyAmount: formatUnits(quote.minBuyAmount, buyDecimals),
gas: quote.gas ?? 'not supplied',
allowanceTarget: quote.allowanceTarget ?? 'none',
to: quote.transaction.to,
});
// This reproduces EVM execution without sending a transaction or paying gas.
// Set `from` to the address that will actually submit the swap.
try {
await provider.call({
from: process.env.TRADER_ADDRESS,
to: quote.transaction.to,
data: quote.transaction.data,
value: BigInt(quote.transaction.value ?? '0'),
});
console.log('Simulation passed at the latest RPC state.');
} catch (error) {
console.error('Simulation reverted:', error.shortMessage ?? error.message);
throw error;
}
}
// Example shape only: use the unmodified transaction object from your aggregator.
const quote = {
sellAmount: '500000000', // 500 USDC, 6 decimals
buyAmount: '180000000000000000',
minBuyAmount: '178200000000000000',
gas: '210000',
allowanceTarget: '0x...',
transaction: { to: '0x...', data: '0x...', value: '0' },
};
inspectQuote(quote, 6, 18);
An eth_call is not a guarantee. State, pool balances, and gas conditions can change before inclusion. It does catch failures such as insufficient allowance, insufficient balance, an unsupported fee-on-transfer token, a stale route, or a contract-level revert. Run it again after creating an approval; the approval itself changes the state the swap depends on.
Revert reasons I check before resubmitting
Allowance target mismatch. An approval to the visible brand is not necessarily an approval to the spender in the calldata. Read the exact spender from the quote, approve only that address for the required amount, and revoke or reduce it afterwards if this is not a recurring route.
Minimum-output failure. A Too little received-style revert means the pool state moved beyond your price limit. Do not solve it by repeatedly widening slippage. Re-quote, verify the token and chain again, then reduce the trade size or wait for liquidity to recover.
Expired or stale transaction data. Quotes are snapshots. A route can be valid when quoted and invalid one block later, especially around volatile pairs. Request fresh calldata rather than changing the nonce or gas price on old calldata.
Token behaviour the router cannot safely model. Transfer-tax, rebasing, paused, or blacklistable tokens make estimates fragile. A successful simulation is useful evidence, but it does not turn a token with hostile transfer logic into a normal ERC-20.
MEV protection is not the same as better routing
A public router transaction exposes its path and slippage limit to searchers before it is included. The relevant defense is how the order is submitted, not whether the route has two hops or five. An auction-based intent can avoid exposing a conventional public-mempool swap, but it introduces solver and settlement-liveness dependencies. CoW’s solver competition is documented as an optimization over a batch, not a promise of an immediate fill. Read its auction model before treating it as a drop-in router.
For a $100–$500 liquid-pair swap, I would use a normal router only with a tight, realistic minimum received and a private-submission option if the wallet supports one; compare it against an intent quote when execution is not urgent. I do not add multi-hop complexity for a one or two dollar quote improvement unless the gas-adjusted difference remains positive.
Risk analysis
- Technical risk. Aggregator calldata often grants a third-party spender access to the sell token. Verify chain ID, token addresses, recipient, spender, and
toaddress before signing. A simulation cannot detect a malicious token contract that changes behaviour later. - Economic risk. Quotes decay. Pool price movement, route splitting, gas, explicit fees, and your slippage ceiling determine the realized result. A route that wins for $50 can lose for $800 because price impact and gas scale differently.
- MEV risk. A tight slippage limit narrows what a sandwich can extract but may increase reverts. Private submission and intent settlement reduce public-mempool exposure, yet each adds an operator or solver dependency.
- Operational risk. Approving first and swapping later creates two transactions and two failure points. Keep the approval amount bounded, confirm it landed, then obtain a fresh swap quote. Never resubmit a failed route blindly with a wider limit.
Keep the quote and preflight result together
- Save both the quoted output and minimum output, then compare routes after gas and fees—not by the largest headline number.
- Simulate the exact quote calldata from the submitting address before signing. If it reverts, get a new quote and diagnose the reason instead of increasing slippage.
- Check the actual allowance target and limit the approval. It is one of the few route details a wallet UI can obscure.
- Treat routing and MEV protection as independent decisions. For small, liquid swaps, a simple route with a realistic limit and private submission is easier for me to inspect than a path with several unfamiliar contracts.