@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
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:try form with the same arguments: tryUniswapV2, tryUniswapV3, tryUniswapV4, tryCurve, tryBalancerV2, tryDodoV2, tryTraderJoeLB, tryWoofi, tryMaverickV2, tryInfinityCL and tryInfinityBin (The try forms).
Common arguments
amountInis what the Pot pays. Every swap is exact input; there is no exact-output form.minOutis the minimum accepted output. A result below it raises a failure; an unhandled failure reverts the cook.0sets no floor, except forwoofi, which requires a positive one. Where the venue enforces a floor itself (Curve’smin_dy, the Balancer Vault’slimit, WooPPV2’sminToAmount),minOutis passed as that floor; otherwise the function checks the amount the venue reports.recipientreceives 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:
swapis the calldata of one of the Router’s own swap methods:swapV3,swapV4,swapMaverickV2,swapInfinityCLorswapInfinityBin.callbackis a program: whatprogram(...)returns, itscodefollowed by itsargs. The venue sends the output first. The Router then runscallbackinside 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
callbackit was given. A swap whose venue never calls back, a swap of 0 included, reverts withCallbackNotRun(). - 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
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:
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
tokenInandtokenOut; dodoV2sells base whentokenInisbaseTokenand quote when it isquoteToken, 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;traderJoeLBcheckstokenXandtokenYthe same way;- the library checks a V3 or Maverick pool’s tokens before the swap.
The try forms
Atry 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,tryDodoV2andtryWooficheck 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 the0a factory returns for a missing pair.tryDodoV2andtryWoofialso skip a pool whose view reverts.tryUniswapV2andtryTraderJoeLBread 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.tryCurveandtryBalancerV2skip 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 belowminOut, and reset the approval to zero.tryUniswapV3,tryUniswapV4,tryMaverickV2,tryInfinityCLandtryInfinityBincatch 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 tominOut, 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 belowminOutreverts rather than skips. With no price limit, an empty pool costs a full tick walk before it is skipped; pass asqrtPriceLimitX96to bound it.
- Given
0x…dEaD, an address with no code, each of the eleventryforms returned0.tryUniswapV2,tryTraderJoeLB,tryCurveandtryBalancerV2also returned0for the zero address. tryUniswapV2andtryTraderJoeLBgiven Base’s USDC contract, which has code and is not a pair, reverted with no data.tryUniswapV2at the Uniswap V2 USDC/WETH pair, selling 1,000 USDC, returned 418305883082737258 (0.418 WETH) with aminOutof 1, and0with nothing moved with aminOutof 10^30.tryUniswapV3at the 0.05% USDC/WETH pool returned 419272799328827730 (0.419 WETH) with aminOutof 1, and with aminOutof 10^30, above its output, reverted.
tryUniswapV2,tryTraderJoeLB,tryCurveandtryBalancerV2returned0for0x…dEaDand for the zero address.tryUniswapV2andtryTraderJoeLBgiven Ethereum’s USDC contract reverted with no data.tryUniswapV2at the Uniswap V2 USDC/WETH pair, selling 1,000 USDC, returned 419238855521904606 (0.419 WETH) with aminOutof 1, and0with aminOutof 10^30.tryCurveat the 3pool, selling 1,000 USDC for USDT, returned 1000825422 (1,000.825422 USDT) with aminOutof 1, and0with aminOutof 2,000 USDT.tryBalancerV2at the BAL/WETH pool, selling 0.1 WETH, returned 1935915708649823758613 (1,935.9 BAL) with aminOutof 1, and0with aminOutof 10^30.
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
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
4295128739and1461446703485210103287273052203988822378723970342, on the far side of the pool’s current price, or the pool reverts withSPL. 0means no limit, which leavesminOutas 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 hasswap(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
swapreturns what arrived. Where the pool cannot trade, or the venue has no pool at the key, it returns0having moved nothing. BelowminOutthe cook reverts.priceLimitis 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, asWSOL.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
0for 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:
slotis 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 poolsprints each pool’s slot for the direction paid.traderis[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 theAtLimit,CpiandDataforms, 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>Stoppedform, the same swap with its limit held to astopas well, which thesaucecommand 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.
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
- @sauce/token: the token writes every swap uses.
- Distributed swap: one order spread over several EVM pools with these functions.
- Distributed swap on Solana: the same over Solana pools.
- Compiling programs: compile a program that imports the library from TypeScript.
- Pot: the Pot and its Router.
