The only slippage protection that is yours is a number you type into the calldata. I wanted to see exactly what it does, so I broke it on purpose.
Five dry-runs of the same 0.5 WETH → USDC swap on Base. Nothing is sent, nothing is signed; every result is an eth_call.

The setup

BlazePhoenix swaps carry a userMinOut in the calldata: an absolute amount of the output token. The Router checks it on what the recipient actually received, and reverts with RouterE(5) (slippage) otherwise. That is what the public source says, in the 2.0.0 Router:

uint256 recipBefore = BPC.balanceOf(tokenOut, recipient); BPC.safeTransfer(tokenOut, recipient, net); uint256 delivered = BPC.balanceOf(tokenOut, recipient) - recipBefore; // Slippage protection is enforced on the DELIVERED amount if (delivered < userMinOut) revert RouterE(5); 

Before that line, the Router computes an effective minimum as the strictest of three numbers: your userMinOut, the route's own singleOutFloor, and a protocol floor it recomputes on-chain. if (amountOut < effMin) revert RouterE(5). And on every entry point that moves value, userMinOut == 0 reverts with RouterE(10), so "no protection" is not something the 2.0.0 source lets you sign.

That is the source. The experiments below run against the 1.0.0 deployment that the SDK pins today for Base (2.0.0 is listed as pending in the deployments registry). So I report what that deployment did, not what the 2.0.0 source says it will do.

The script

import { getAddress, createPublicClient, http, keccak256, encodeAbiParameters, parseUnits, formatUnits, type Address, type Hex } from 'viem'; import { BlazePhoenix, decodeBlazeError, decodeSwapExactIn, encodeSwapExactIn } from '@blazephoenix/sdk'; const RPC = process.env.BASE_RPC_URL ?? 'https://mainnet.base.org'; const WETH: Address = '0x4200000000000000000000000000000000000006'; const me = getAddress('0x00000000000000000000000000000000000a11ce'); // holds nothing: its balance is faked inside the eth_call const blaze = new BlazePhoenix({ rpc: { base: RPC } }); const client = createPublicClient({ transport: http(RPC) }); // 1. a normal plan, 0.50 % slippage; then take its calldata apart const plan = await blaze.buildSwap({ chain: 'base', tokenIn: 'WETH', tokenOut: 'USDC', amount: '0.5', recipient: me, slippageBps: 50 }); const c = decodeSwapExactIn(plan.tx.data); const usdc = (n: bigint) => Number(formatUnits(n, 6)).toLocaleString('en', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); console.log('quote net', usdc(plan.quote.amountOut), '| minimum signed', usdc(c.userMinOut)); // 2. dry-run any calldata as `me`, with 1 WETH + approval written into the call's state (nothing is sent) const slot = (k: Address, s: bigint) => keccak256(encodeAbiParameters([{ type: 'address' }, { type: 'uint256' }], [k, s])); const word = (n: bigint) => ('0x' + n.toString(16).padStart(64, '0')) as Hex; const dryRun = (data: Hex) => client.call({ account: me, to: plan.tx.to, data, stateOverride: [{ address: WETH, stateDiff: [ { slot: slot(me, 3n), value: word(parseUnits('1', 18)) }, { slot: keccak256(encodeAbiParameters([{ type: 'address' }, { type: 'bytes32' }], [plan.tx.to, slot(me, 4n)])), value: word(parseUnits('1', 18)) } ] }] }); async function sim(label: string, route: typeof c.route, minOut: bigint) { try { const r = await dryRun(encodeSwapExactIn(route, c.amountIn, minOut, me, c.deadline)); console.log(label.padEnd(40), 'ok delivers', usdc(BigInt(r.data!))); } catch (e) { const d = decodeBlazeError(e); console.log(label.padEnd(40), 'REVERT', d ? `RouterE(${d.code}) ${d.reason.split(':')[0]}` : (e as Error).message.split('\n')[0].slice(0,90)); } } const net = plan.quote.amountOut; await sim('A honest route, minOut as built', c.route, c.userMinOut); await sim('B honest route, minOut = quote + 1 %', c.route, net + net / 100n); // C/D: the route is only calldata. Push the whole order into the smallest pool and rewrite every // number the route "attests" to 1: a hostile front-end could send exactly this. const hop = c.route.hops[0]; const small = [...hop.legs].sort((a, b) => Number(a.amountIn - b.amountIn))[0]; const bad = { ...c.route, singleOutFloor: 1n, totalOut: 1n, singleOut: 1n, hops: [{ ...hop, expectedOut: 1n, legs: [{ ...small, amountIn: hop.amountIn, expectedOut: 1n }] }] }; await sim('C bad route, minOut = 1 (dust)', bad, 1n); await sim('D same bad route, minOut as built', bad, c.userMinOut); await sim('E honest route, minOut = 0', c.route, 0n); 

Two notes on the trick: stateOverride writes a fake WETH balance and allowance into that single eth_call (WETH9 storage layout: balances in slot 3, allowances in slot 4), so a wallet that owns nothing can still rehearse. And it needs an RPC that supports state overrides; the public Base endpoint I used does.

What came back

quote net 1,168.54 | minimum signed 1,162.70 A honest route, minOut as built ok delivers 1,171.83 B honest route, minOut = quote + 1 % REVERT RouterE(5) slippage C bad route, minOut = 1 (dust) ok delivers 1,001.96 D same bad route, minOut as built REVERT RouterE(5) slippage E honest route, minOut = 0 ok delivers 1,171.83 

Reading it line by line:

  • A. The honest swap clears its minimum. (It delivers a little more than the quote's net figure; I haven't traced why on this deployment, so I make no claim about it.)
  • B. Ask for 1 % more than the quote and the swap reverts with RouterE(5) instead of filling short. That is the whole promise of minOut: it turns "a worse price" into "no transaction".
  • C. The uncomfortable one. I rewrote the route so the entire order goes through the smallest pool, and set every number the route attests to as 1. With minOut = 1 the dry-run succeeds and delivers 1,001.96 USDC, about 14 % below the quote's net of 1,168.54. The route is calldata. Whoever builds your calldata chooses it, and a dust minimum signs away your right to object.
  • D. Same hostile route, but with the minimum the SDK built for you: RouterE(5). One honest number was enough to stop it.
  • E. On the 1.0.0 deployment, minOut = 0 was accepted by the dry-run. The 2.0.0 source refuses it with RouterE(10). Either way, you should never be the one sending it.

What this means in code

The Router also enforces floors of its own (per-leg and per-route; see the iron floor article), and the site is explicit that they are a backstop, not the protection to rely on day to day. Experiment C is the practical version of that sentence. Two habits follow:

  1. Derive minOut from a quote you read yourself, as in the quote-by-eth_call pattern, and never accept calldata whose minimum you didn't compute. The SDK's buildSwap refuses a userMinOut below the quote net of slippage (min_implausible), and verifySwapExactIn compares decoded calldata to your intent.
  2. Dry-run before you sign. If the rehearsal doesn't pass, the real thing won't.

On execution surplus: in the 2.0.0 source, after the floor checks the recipient is paid the Router's measured output (the code comment is literally "Everything left over goes to the recipient"). The site's surplus doctrine page describes the surplus promise for the deployed generation; the SDK source notes the separate Surplus event is 1.x-only. Check which generation you integrate against.

Install: npm i @blazephoenix/sdk viem tsx, run as npx tsx minout.mts. SDK source: gitlab.com/blazephoenix-group/sdk; contracts: gitlab.com/blazephoenix-group/dex.

Status: BlazePhoenix is pre-launch and an independent external audit is scheduled. These are simulations of one block's state on one deployment generation, not a performance claim, and not advice.