> ## Documentation Index
> Fetch the complete documentation index at: https://docs.eco.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Spread one limit buy over Base's pools

> One Base transaction that buys WETH with USDC across Aerodrome Slipstream, Uniswap V3, PancakeSwap V3 and hookless Uniswap V4 pools, with no unit above a price limit, fees included.

This program buys WETH with a USDC budget across several pools on Base. It visits pools in a fixed order and gives each the remaining budget, subject to a price limit that accounts for its fee. The purchased WETH and unspent USDC go to `recipient`.

The program checks the total WETH received against `minTokens`. It also checks the average price when `maxAvgPrice` is nonzero. Pools are not sorted by price, so this example does not promise the best available execution.

**Target chains:** Base. The program requires Aerodrome Slipstream's factories. Compiling for Ethereum fails (`` `AerodromeSlipstream.CLFactory` is not deployed on ethereum. ``).

## Prerequisites

Install the [Sauce CLI](https://docs.eco.com/sauce-sdk/installation.md) to compile and simulate. To send the transaction, configure a wallet with funds for gas and the Kitchen fee, and provide USDC in your Pot once it is deployed, or through [`--fund`](https://docs.eco.com/sauce-sdk/cli/funding).

## Program

`pot` is the Pot the program runs on, available without declaring it ([Names](https://docs.eco.com/sauce/names.md#balances)).

```ts distributed-swap.sauce.ts theme={null}
// Buys WETH on Base with up to `budget` of the Pot's USDC, spread over the pair's pools on
// Aerodrome Slipstream, Uniswap V3, PancakeSwap V3 and hookless Uniswap V4: a limit buy in which
// no WETH costs more than `priceLimit`, fees included. Each pool, in the loops' order and not in
// order of price, is offered the budget still unspent and stops at its own fee-adjusted limit;
// missing, empty and past-limit pools are skipped. The WETH and the unspent USDC go to
// `recipient`. The cook reverts, and nothing moves, when less than `minTokens` arrives or, with
// `maxAvgPrice` not 0, the average price paid is above it.
//
//   sauce simulate distributed-swap.sauce.ts --chain base --owner <address> \
//     --assume-balance USDC:<budget>n \
//     --args <budget> <priceLimit> <minTokens> <maxAvgPrice> <recipient>
//   sauce run distributed-swap.sauce.ts --chain base --wallet <name> \
//     --fund USDC:<budget>n --args <budget> <priceLimit> <minTokens> <maxAvgPrice> <recipient>
//
//   budget: Uint256       the most USDC to spend, in USDC base units (6 decimals): 10000000000
//                         is 10,000 USDC; the Pot must hold it
//   priceLimit: Uint256   the most one WETH may cost, fees included: USDC base units per WETH
//                         base unit, times 1e18, which is the price in USDC times 10^6:
//                         2333000000 is 2,333 USDC
//   minTokens: Uint256    the least WETH `recipient` may receive, in wei (18 decimals);
//                         budget * 1e18 / priceLimit, rounded up, is the least a full fill at
//                         the limit buys: 4286326618088298329 for 10,000 USDC at 2,333. It does
//                         not force a full fill: pools below the limit can reach it with part of
//                         the budget unspent
//   maxAvgPrice: Uint256  the most the average may cost, in priceLimit's units; 0 turns it off
//   recipient: Address    receives the WETH bought and the unspent USDC
//
// On Base, up to 10,000 USDC of WETH at no more than 2,333 USDC each and at least
// 4.286326618088298329 WETH, an average of at most 2,332.489272 (halfway from a simulated
// 2,331.978543 to priceLimit), for <recipient>:
//
//   sauce run distributed-swap.sauce.ts --chain base --wallet me --fund USDC:10000 \
//     --args 10000000000 2333000000 4286326618088298329 2332489272 \
//     <recipient>
import { uniswapV3, uniswapV4 } from "@sauce/router";

// One pool: skipped when missing, empty or already at or past its own limit; otherwise offered
// all of the budget still unspent, up to its own limit, with the output paid to `recipient`.
function trade(
  venue: Uint256, // 0 Uniswap V3, 1 Slipstream, 2 Uniswap V4, 3 PancakeSwap V3
  pool: Uint256, // the pool's address; a V4 pool has none, and this is its id, a 256-bit hash
  spacing: Uint256, // a V4 pool's tick spacing
  kept: Uint256,
  limit: Uint256,
  recipient: Address,
): void {
  const unspent = pot.USDC - kept;
  if (pool == 0 || unspent == 0) {
    return;
  }
  const paysToken0 = USDC < WETH;
  let liquidity = 0;
  let price = 0;
  let fee = 0;
  let lpFee = 0;
  if (venue == 0) {
    liquidity = UniswapV3.Pool(pool).liquidity();
    price = UniswapV3.Pool(pool).slot0().sqrtPriceX96;
    fee = UniswapV3.Pool(pool).fee();
  } else if (venue == 1) {
    // Slipstream's own binding: its slot0 has no feeProtocol, and its fee can change any block.
    liquidity = AerodromeSlipstream.Pool(pool).liquidity();
    price = AerodromeSlipstream.Pool(pool).slot0().sqrtPriceX96;
    fee = AerodromeSlipstream.Pool(pool).fee();
  } else if (venue == 3) {
    // PancakeSwap V3's own binding: its slot0 declares feeProtocol as a uint32, and a read
    // decodes and range-checks every declared output.
    liquidity = PancakeswapV3.Pool(pool).liquidity();
    price = PancakeswapV3.Pool(pool).slot0().sqrtPriceX96;
    fee = PancakeswapV3.Pool(pool).fee();
  } else {
    // A missing V4 pool reads as empty. Its fee is this direction's protocol fee (paying token0's
    // in the low 12 bits, token1's in the next 12), then the LP fee on the rest.
    liquidity = UniswapV4.StateView.getLiquidity(pool);
    const [sqrtPriceX96, , protocolFees, poolFee] = UniswapV4.StateView.getSlot0(pool);
    const protocolFee = (paysToken0 ? protocolFees : protocolFees >> 12) & 0xfff;
    price = sqrtPriceX96;
    lpFee = poolFee;
    fee = protocolFee + lpFee - (protocolFee * lpFee) / 1000000;
  }
  // The pool's own limit: the caller's moved by √(1 − fee), so that its last unit, fee paid,
  // costs exactly priceLimit. Paying token0 pushes the price down, token1 pushes it up.
  const root = Math.sqrt((1000000 - fee) * 10 ** 30); // √(1 − fee), times 1e18
  const own = paysToken0 ? Math.mulDiv(limit, 10 ** 18, root) : Math.mulDiv(limit, root, 10 ** 18);
  const better = paysToken0 ? price > own : price < own;
  if (liquidity == 0 || !better) {
    return;
  }
  if (venue == 2) {
    // A hookless pool's LP fee is its key's fee.
    uniswapV4(UniswapV4.PoolManager, USDC, WETH, unspent, lpFee, spacing, 0, own, 0, recipient);
    return;
  }
  // Slipstream pools call back uniswapV3SwapCallback and PancakeSwap V3 pools
  // pancakeV3SwapCallback. The Router answers both, so uniswapV3 swaps all three.
  uniswapV3(pool, USDC, WETH, unspent, own, 0, recipient);
}

function main(
  budget: Uint256,
  priceLimit: Uint256,
  minTokens: Uint256,
  maxAvgPrice: Uint256,
  recipient: Address,
): { spent: Uint256; bought: Uint256; unspent: Uint256 } {
  require(budget > 0, "budget is 0");
  require(priceLimit > 0, "priceLimit is 0");
  require(pot.USDC >= budget, "budget is not in the Pot");
  const kept = pot.USDC - budget; // the Pot's other USDC, never spent
  const boughtBefore = recipient.WETH;

  // priceLimit as a pool's sqrt price (token1 per token0, times 2^96): where a unit costs it.
  let limit = Math.sqrt(Math.mulDiv(priceLimit, 1 << 192, 10 ** 18));
  if (USDC < WETH) {
    limit = Math.sqrt(Math.mulDiv(1 << 192, 10 ** 18, priceLimit));
  }

  // The loops only find pools; trade() decides and swaps. Each walks the tick spacings, fee
  // tiers or keys `sauce pools --chain base USDC WETH` listed for the pair, CLFactory first.
  for (const spacing of [1, 10, 50, 100, 200, 2000]) {
    const pool = AerodromeSlipstream.CLFactory.getPool(USDC, WETH, spacing);
    trade(1, pool, 0, kept, limit, recipient);
  }
  for (const fee of [100, 200, 300, 400, 500, 3000, 10000]) {
    const pool = UniswapV3.Factory.getPool(USDC, WETH, fee);
    trade(0, pool, 0, kept, limit, recipient);
  }
  for (const fee of [100, 500, 2500, 10000]) {
    const pool = PancakeswapV3.Factory.getPool(USDC, WETH, fee);
    trade(3, pool, 0, kept, limit, recipient);
  }
  for (const spacing of [1, 10, 50]) {
    const pool = AerodromeSlipstream.CLFactoryGaugesV3.getPool(USDC, WETH, spacing);
    trade(1, pool, 0, kept, limit, recipient);
  }
  for (const spacing of [50, 500]) {
    const pool = AerodromeSlipstream.CLFactoryGaugeCaps.getPool(USDC, WETH, spacing);
    trade(1, pool, 0, kept, limit, recipient);
  }
  // Uniswap V4, hookless. A pool is its id, the hash of its key with the pair in address order:
  // 256 bits, so a Uint256, never an Address. The keys are the `Uniswap V4 key …` rows that
  // `sauce pools` listed with liquidity.
  const low = USDC < WETH ? USDC : WETH;
  const high = USDC < WETH ? WETH : USDC;
  for (const key of [
    { fee: 500, spacing: 10 },
    { fee: 3000, spacing: 60 },
  ]) {
    const id = crypto.keccak256(abi.encode(low, high, key.fee, key.spacing, 0));
    trade(2, id, key.spacing, kept, limit, recipient);
  }

  // Settle: what arrived at the recipient, the budget the limits left, and the two checks.
  const bought = recipient.WETH - boughtBefore;
  const unspent = pot.USDC - kept;
  const spent = budget - unspent;
  require(bought >= minTokens, "bought below minTokens");
  require(maxAvgPrice == 0 || spent * 10 ** 18 <= maxAvgPrice * bought, "average too high");
  if (unspent > 0) {
    USDC.transfer(recipient, unspent);
  }
  return { spent, bought, unspent };
}
```

## How it works

* No amount is split in advance. Each pool is offered `pot.USDC - kept`, all of the budget still unspent, and its own limit stops it; the Router pays a pool only what it asks for, so the rest stays in the Pot for the next pool. The loops only look pools up; a V4 pool is its id, `crypto.keccak256(abi.encode(...))` of its key, a `Uint256`. The two V4 keys are the `Uniswap V4 key …` rows `sauce pools` lists with liquidity; it lists the keys 100:1 and 10000:200 as "no liquidity at its price", and a pool with no liquidity is skipped.
* Pools fill in the loops' order, not in order of price. Each pool in turn trades up to its own limit, so a pool early in the loops can spend the budget at prices up to `priceLimit` while a cheaper pool later in them is offered nothing, and a looser `priceLimit` can buy less. On Base at block 52997580, 10,000 USDC bought 4.172728624863845599 WETH at a 2,397 limit and 4.172225574547365654 WETH at 2,420. At 2,420 the `CLFactory` 0.015% pool, fourth in the first loop at 2,397.10 USDC per WETH with its fee, took 9,690.865139 USDC, and the Uniswap V3 0.05% and PancakeSwap V3 0.01% pools, at 2,396.34 and 2,396.26, were offered nothing. A `priceLimit` close to the prices `sauce pools` lists keeps every pool near them. Putting the loops in the order of those prices would not make the fill follow price: the first pool still trades up to `priceLimit` before the next is offered anything, and the order is fixed in the program while prices move every block.
* Each pool's limit is the caller's sqrt price moved by √(1 − fee), with the fee read from the pool in the cook: a Slipstream pool's fee can change between blocks, and a V4 pool's is its LP fee combined with the direction's protocol fee. The last unit each pool fills costs `priceLimit` with its fee paid. `trade` skips a missing pool, an empty one and one already at or past its own limit, which would otherwise revert its swap.
* A PancakeSwap V3 pool is read through `PancakeswapV3.Pool`, whose `slot0` declares `feeProtocol` as a `uint32`. A read decodes and range-checks every declared output, so the same read through `UniswapV3.Pool` (a `uint8` there) reverts: on the fork the 0.01% pool's `feeProtocol` was 216272100.
* `bought` is the change in `recipient.WETH` and `spent` the change in the Pot's USDC. `minTokens` is checked on those totals after the loops, and so is the average when `maxAvgPrice` is not 0. `minTokens` at `budget * 1e18 / priceLimit`, rounded up, is the least a full fill at the limit would buy. It does not make the order fill-or-kill: pools that fill below the limit can reach it with part of the budget unspent, and that part is refunded. Only the unspent budget goes to `recipient`, not the Pot's whole USDC balance.
* To spend all of the budget or nothing, add `require(unspent == 0, "budget not filled");` before the refund. The program as shown leaves it out, so that a `minTokens` of 0 buys what fills within `priceLimit` and returns the rest.

## Run the example

**1. Read the pools and set the limit**

```bash theme={null}
sauce pools --chain base USDC WETH
sauce price --chain base WETH USDC 2333
```

`sauce pools` lists each pool's price per WETH with its fee. Its `Uniswap V4 key …` rows give the keys with liquidity, which the program's key loop lists. `sauce price` turns a price in USDC per WETH into the program's units: its "Buying WETH" block gives `priceLimit` `2333000000` for 2,333 USDC.

**2. Compile**

```bash theme={null}
sauce compile distributed-swap.sauce.ts --chain base
```

**3. Simulate**

```bash theme={null}
sauce simulate distributed-swap.sauce.ts --chain base --owner <your-address> \
  --assume-balance USDC:10000 \
  --args 10000000000 2333000000 4286326618088298329 0 <recipient>
```

Simulate with `maxAvgPrice` at 0 soon after reading the pools. Read the average from `main returns` as `spent * 1e18 / bought`, set `maxAvgPrice` halfway from it to `priceLimit`, rounded up, and simulate again with that value.

**4. Send**

```bash theme={null}
sauce run distributed-swap.sauce.ts --chain base --wallet <name> --fund USDC:10000 \
  --args 10000000000 2333000000 4286326618088298329 2332489272 <recipient>
```

`sauce tx` prints the unsigned transaction instead, for your own signer, with the same funding and arguments:

```bash theme={null}
sauce tx distributed-swap.sauce.ts --chain base --owner <your-address> --fund USDC:10000 \
  --args 10000000000 2333000000 4286326618088298329 2332489272 <recipient>
```

> [!NOTE]
>
> A `priceLimit` at or below every pool's price with its fee buys nothing and sends the whole budget back to `recipient` without reverting. Set `minTokens` above 0 so that case reverts.

## Simulation result

Simulated on a local fork of Base at block 53122981 with the current contracts. Nothing was signed or sent; values change with the block. The run is the Simulate step's command with `maxAvgPrice` `2332489272`. On the fork `sauce pools` listed 24 pools of the pair with liquidity, the V4 keys 500:10 and 3000:60 among them; the cheapest with their fee were a Slipstream pool at 2,317.58 USDC per WETH and a Uniswap V3 pool at 2,330.92.

```text theme={null}
The cook succeeds from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266: gas used 1128863 for the cook, after the Pot's deploy (1. below), returns 0x00000000000000000000000000000000000000000000000000000002540be4000000000000000000000000000000000000000000000000003b82c2ed8b80f61e0000000000000000000000000000000000000000000000000000000000000000
main returns { spent: Uint256, bought: Uint256, unspent: Uint256 }: { spent: 10000000000, bought: 4288204120713655838, unspent: 0 }
...
What moved, in order:
...
  [3]  0xdbc6…30f1        -> 0x7099…79C8          0.008058120797465883 WETH
  [4]  Pot                -> 0xdbc6…30f1          18.794876 USDC
  [5]  0xb150…4bdB        -> 0x7099…79C8          0.000113972793043994 WETH
  [6]  Pot                -> 0xb150…4bdB          0.265862 USDC
  [7]  0xAaD2…D517        -> 0x7099…79C8          0.000005229161965382 WETH
  [8]  Pot                -> 0xAaD2…D517          0.012161 USDC
  [9]  0xb2cc…DC59        -> 0x7099…79C8          4.280026797961180579 WETH
  [10] Pot                -> 0xb2cc…DC59          9980.927101 USDC
Net balance changes:
...
  0x70997970C51812dc3A010C7d01b50e0d17dc79C8: +4.288204120713655838 WETH
  0xDcFCc75A3C9a0EC798CAf81B6C84D4c5d5988c49 (the Pot): -10000 USDC
...
```

Four pools traded before the budget ran out, all four `CLFactory` pools of the first loop, the last the 0.015% pool (0xb2cc…DC59), which took 9,980.927101 USDC. The pools after it, the two V4 pools included, were looked up but not read, because no budget was left. The recipient received 4.288204120713655838 WETH for 10,000 USDC, an average of 2,331.978543 USDC per WETH. The same run with `maxAvgPrice` 0 returned the same three values; `2332489272` is halfway from that average to `priceLimit`, rounded up.

On the same fork, with `maxAvgPrice` at 2,331.00 (`2331000000`), below that average, the cook reverts:

```bash theme={null}
sauce simulate distributed-swap.sauce.ts --chain base --owner <your-address> \
  --assume-balance USDC:10000 \
  --args 10000000000 2333000000 4286326618088298329 2331000000 <recipient> \
  --expect-revert "average too high"
```

```text theme={null}
As expected, the cook reverts: the program threw "average too high"
The cook reverts: the program threw "average too high"
...
```

## Next steps

* [Try-fallback swap](https://docs.eco.com/quickstarts/sauce/try-fallback-swap.md): one pool, and a second one when the first fails, in an atomic `try` block.
* [Buy tokenized stock](https://docs.eco.com/quickstarts/sauce/buy-tokenized-stock.md): one pool chosen by price, on Ethereum.
* [Solana distributed swap](https://docs.eco.com/quickstarts/sauce/solana-distributed-swap.md): the same order over Solana's pools.
* [`sauce pools`](https://docs.eco.com/sauce-sdk/cli/pools), [`sauce price`](https://docs.eco.com/sauce-sdk/cli/price), [`sauce simulate`](https://docs.eco.com/sauce-sdk/cli/simulate), [Tokens](https://docs.eco.com/sauce-sdk/reference/tokens.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.