Skip to main content
@sauce/router swaps at a venue’s pool: one function per venue, imported inside a program. Every argument is a value read at run time, so a program can choose the pool, the direction and the amount while it runs, and spend one swap’s output in the next. Each function returns an output amount. How it obtains that amount depends on the venue: some use the pool’s reported output and others measure a balance change. If your program must verify the recipient’s actual receipt, check the recipient’s token balance before and after the swap.
swap.sauce.ts
The same file compiles for Base and Ethereum, each with its own factory, WETH and USDC. The swap actions of an actions document compile to calls into the same functions, so a JSON document and a hand-written program trade a venue through the same code. On Solana the same specifier holds a different set of functions, one per Solana venue (On Solana). The EVM and Solana implementations export different functions. Importing a function unavailable on your target produces a compile error.

Functions

Every function takes its arguments in one order, the pool, the two tokens, the amount, the venue’s own facts, then the floor and the recipient, and returns what the recipient received:
Each has a try form with the same arguments: tryUniswapV2, tryUniswapV3, tryUniswapV4, tryCurve, tryBalancerV2, tryDodoV2, tryTraderJoeLB, tryWoofi, tryMaverickV2, tryInfinityCL and tryInfinityBin (The try forms).

Common arguments

  • amountIn is what the Pot pays. Every swap is exact input; there is no exact-output form.
  • minOut is the minimum accepted output. A result below it raises a failure; an unhandled failure reverts the cook. 0 sets no floor, except for woofi, which requires a positive one. Where the venue enforces a floor itself (Curve’s min_dy, the Balancer Vault’s limit, WooPPV2’s minToAmount), minOut is passed as that floor; otherwise the function checks the amount the venue reports.
  • recipient receives the output. pot, the Pot the program runs on (Names), keeps it in the Pot, where a later step of the program, or a later cook, spends it.
  • The return value is what the recipient received, as the pool or the Router reports it. For Curve, whose older pools report nothing, it is the balance change.

Venue facts

How each venue is reached

The pay-first venues never call back. Each function either pushes the input to the pool first (V2, DODO, Trader Joe, WOOFi) or approves exactly the input for the pool or the Vault to pull (Curve, Balancer), which leaves a zero allowance. Such a swap consumes its whole input or reverts, and runs in any Pot. Uniswap V3 and its forks, Uniswap V4, Maverick V2 and PancakeSwap Infinity pools call their caller back during the swap to be paid. A program cannot answer a callback, so these functions call the Pot’s Router at the Pot’s own address. The Pot’s fallback delegatecalls the Router, which runs as the Pot and answers the callback. Each callback is authenticated to the pool being swapped, pays once, and pays only the input the pool asks for, never more than amountIn. Where a price limit stops the pool early, the unspent input stays in the Pot. Each Pot has its own Router. A new Pot starts on the Kitchen’s current Router, and after that only the Pot’s owner can point it at another one. A Router is identified by its address, not by a version number: Deployments gives the Router, which v12RouterAddress() returns in TypeScript. See Pot. Every token write in the library is @sauce/token’s, with OpenZeppelin SafeERC20 semantics: USDT works, and a false return reverts.

Flash swaps

@sauce/router has no flash function. The Pot’s Router has one, flash(callback, swap), which a program calls at the Pot’s own address through a JSON ABI that declares it:
  • swap is the calldata of one of the Router’s own swap methods: swapV3, swapV4, swapMaverickV2, swapInfinityCL or swapInfinityBin.
  • callback is a program: what program(...) returns, its code followed by its args. The venue sends the output first. The Router then runs callback inside the venue’s callback, before it pays the venue, and pays out of what the Pot holds by then. A callback that leaves the Pot short fails the payment, and the whole swap reverts.
  • The Router runs only the callback it was given. A swap whose venue never calls back, a swap of 0 included, reverts with CallbackNotRun().
  • It returns what the swap method returns: the input the Router paid, then the output.
  • The callback runs while the venue is in the middle of its swap. A Uniswap V3 pool is locked then, so a callback that swaps in the same pool reverts with LOK.
flash-swap.sauce.ts
SauceRouterFlash.json, beside it:
SauceRouterFlash.json
It compiles for Base and Ethereum. On a local fork of Base at block 53159700 with the current contracts, sauce simulate flash-swap.sauce.ts --chain base --owner <address> --assume-balance WETH:0.01 --args 10000000000000000 returned 23260395. The pool sent the Pot 23.260395 USDC, and the Pot paid it 0.01 WETH after the callback:
With amountIn 0 the cook reverts with CallbackNotRun().

Direction and orientation

Each function works out the side it trades from its own arguments while the program runs, and reverts rather than trade a side the arguments contradict:
  • a Uniswap V2 pair’s and a V4 pool’s direction is the sort order of tokenIn and tokenOut;
  • dodoV2 sells base when tokenIn is baseToken and quote when it is quoteToken, and reverts unless the two tokens are those two. The side cannot come from address order, and a wrong one would not revert at the pool;
  • traderJoeLB checks tokenX and tokenY the same way;
  • the library checks a V3 or Maverick pool’s tokens before the swap.
Take the facts from the pool: they are the caller’s, like the pool address.

The try forms

A try form can skip a swap and return 0 without moving tokens in the cases listed below. This lets the program continue to another venue. A concentrated-liquidity swap can also stop at its price limit after spending only part of the input; a positive return indicates a completed swap, not necessarily a full fill. A swap that ran returns at least 1, so 0 always means nothing moved. A failure a try form does not check for still reverts the program, as the plain form does.
  • tryUniswapV2, tryTraderJoeLB, tryDodoV2 and tryWoofi check before anything moves that the pool takes the whole input at the floor, from the pool’s own view (the V2 reserves, getSwapOut, querySellBase/querySellQuote, query). All four skip a pool with no code, such as the 0 a factory returns for a missing pair. tryDodoV2 and tryWoofi also skip a pool whose view reverts. tryUniswapV2 and tryTraderJoeLB read the view uncaught, so a contract that has code but is not a pair, such as a token, reverts the program. Once the check passes, the swap runs uncaught.
  • tryCurve and tryBalancerV2 skip a pool or Vault with no code. For one with code, they catch the pool’s or the Vault’s failure, such as an output below minOut, and reset the approval to zero.
  • tryUniswapV3, tryUniswapV4, tryMaverickV2, tryInfinityCL and tryInfinityBin catch the Router’s call. A pool with no code, a pool for another pair, a key no pool has, or a swap the pool or Router refuses is skipped. A swap that ran is held to minOut, and at least 1, on what the recipient’s balance gained, or the program reverts, because input the pool kept cannot be returned: an output below minOut reverts rather than skips. With no price limit, an empty pool costs a full tick walk before it is skipped; pass a sqrtPriceLimitX96 to bound it.
On a local fork of Base at block 53005062 with the current contracts:
  • Given 0x…dEaD, an address with no code, each of the eleven try forms returned 0. tryUniswapV2, tryTraderJoeLB, tryCurve and tryBalancerV2 also returned 0 for the zero address.
  • tryUniswapV2 and tryTraderJoeLB given Base’s USDC contract, which has code and is not a pair, reverted with no data.
  • tryUniswapV2 at the Uniswap V2 USDC/WETH pair, selling 1,000 USDC, returned 418305883082737258 (0.418 WETH) with a minOut of 1, and 0 with nothing moved with a minOut of 10^30.
  • tryUniswapV3 at the 0.05% USDC/WETH pool returned 419272799328827730 (0.419 WETH) with a minOut of 1, and with a minOut of 10^30, above its output, reverted.
On a local fork of Ethereum at block 26258727 with the current contracts:
  • tryUniswapV2, tryTraderJoeLB, tryCurve and tryBalancerV2 returned 0 for 0x…dEaD and for the zero address. tryUniswapV2 and tryTraderJoeLB given Ethereum’s USDC contract reverted with no data.
  • tryUniswapV2 at the Uniswap V2 USDC/WETH pair, selling 1,000 USDC, returned 419238855521904606 (0.419 WETH) with a minOut of 1, and 0 with a minOut of 10^30.
  • tryCurve at the 3pool, selling 1,000 USDC for USDT, returned 1000825422 (1,000.825422 USDT) with a minOut of 1, and 0 with a minOut of 2,000 USDT.
  • tryBalancerV2 at the BAL/WETH pool, selling 0.1 WETH, returned 1935915708649823758613 (1,935.9 BAL) with a minOut of 1, and 0 with a minOut of 10^30.
The registry has no Trader Joe LB deployment on Base or Ethereum, so tryTraderJoeLB was not run against an LB pair there. The try forms are EVM only: the Solana arm has none, and importing one for Solana fails at the import (“tryUniswapV3 is not exported by module …”).
swap-fallback.sauce.ts
When a V3 swap stops at a price limit, part of amountIn stays in the Pot. A program that must not leave it there sends back amountIn less what the swap took: the token balance before the swap minus the balance after. An atomic try block around a swap and its checks undoes the whole attempt instead, as the try-fallback swap does.

Price limits

sqrtPriceLimitX96 bounds the pool’s price after the swap. It is floor(sqrt(token1Units / token0Units) * 2^96), with token0 the lower of the two token addresses.
  • Selling token0 moves the price down, so the limit is a floor. Selling token1 moves it up, so the limit is a ceiling.
  • It must lie strictly between 4295128739 and 1461446703485210103287273052203988822378723970342, on the far side of the pool’s current price, or the pool reverts with SPL.
  • 0 means no limit, which leaves minOut as the only bound.
  • The fee comes on top: under one limit, the last unit a 1% pool fills costs about 1% more than the limit.
sauce price --chain base WETH USDC 2686.5 --offline turns a price of 2,686.5 USDC per WETH into the limit for each direction without reading the chain; without --offline it also prints a live pool’s current price. See sauce price. The library has no quote function. A program can read a quote itself before it swaps, as Quote, swap and supply does; from TypeScript, quoteAction builds the eth_call that prices a swap.

On Solana

On Solana a program calls a venue by its namespace, the venue’s protocol then its program. Each namespace has swap(pool, tokenIn, tokenOut, amountIn, priceLimit, minOut) and pool(tokenA, tokenB, key), which finds the venue’s pool for the pair by its tick spacing or fee: Meteora.DLMM has swap(pool, tokenIn, tokenOut, amountIn, minOut), an exact input with no price limit, and its pool is named by address.
orca-swap.solana.sauce.ts
  • swap returns what arrived. Where the pool cannot trade, or the venue has no pool at the key, it returns 0 having moved nothing. Below minOut the cook reverts.
  • priceLimit is the most one unit bought may cost, fee included: input base units per output base unit, times 10^18. The function reads the pool’s fee and gives the pool its own limit.
  • The key is written out, as a number or a loop variable over numbers written out, because the pool’s accounts are derived when the cook is built. Compiling adds the pool’s nine accounts and the Pot’s accounts to main.
  • What is bought lands in the Pot’s own token account for tokenOut. Forward it with the token methods, as WSOL.transfer(to, got) does.
  • A failed call into another program fails the whole Solana transaction and cannot be caught, so each function checks before anything moves and returns 0 for a pool it cannot trade: no liquidity at its price, already past the limit, swaps paused, or an input too small to buy one unit after the fee.
sauce pools --chain solana USDC WSOL prints each pool’s key, its price with its fee, and its price as a priceLimit. See sauce pools.

The venue functions by name

Each namespace call compiles to the venue’s @sauce/router function, keeping every check: the venue’s program, the pool’s owner, the pair and minOut. A program or code generator can import those functions by name. The venues whose swap stops at a price limit (Raydium CLMM, PancakeSwap CLMM, Byreal, Stabble CLMM, Orca Whirlpools, Cropper, DefiTuna, Crema and Phoenix) have four forms each: The functions at a price are raydiumClmm, pancakeswapClmm, byreal, stabbleClmm, orcaWhirlpool, cropper, defituna, crema and phoenix:
  • slot is the pool’s nine accounts, [program, pool, config, vaultA, vaultB, extra, ticks0, ticks1, ticks2], with the System program for any the venue does not take. sauce pools prints each pool’s slot for the direction paid.
  • trader is [authority, source, payMint, buyMint, tokenProgram, token2022, memo]: the authority that signs (the Pot, through the Kitchen), the token account paid from, the two mints, the two token programs and the memo program.
  • limit, in the AtLimit, Cpi and Data forms, is the pool’s own: the sqrt price its swap takes, or an order book’s price in its market’s units. No fee is read for it.
  • The eight concentrated-liquidity venues also have a <venue>Stopped form, the same swap with its limit held to a stop as well, which the sauce command compiles a swap to when it holds a pool short of its limit, skips it, or walks it on into its furthest tick array (Swaps at a price and compute units). The stop’s low 192 bits are a tighter price limit, bits 192 to 223 a start bound, and on Raydium’s code and Crema bits 224 to 255 a deep tick: the last initialized tick of the furthest tick array the pool’s accounts give, which the swap walks on to and checks in that array as it runs. A program no stop binds compiles to the plain function.
Invariant and OpenBook v2 have the AtLimit, Cpi and Data forms, and Manifest’s book has manifest, manifestCpi and manifestData. The other venues (Raydium CP-Swap, Meteora DLMM and DAMM, and more) swap exactly amountIn with no price limit, as <venue>(slot, trader, amountIn, minOut, recipient), <venue>Cpi and <venue>Data; the venue takes the whole input or the transaction fails. Some, such as PumpSwap, have only the Cpi and Data forms. @eco-incorp/sauce/svm lists each venue’s accounts and data in the order its instruction takes them, for a code generator that calls a venue itself: see Solana from TypeScript.

Next steps