> ## 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.

# Sell WETH only near the Chainlink price

> One Ethereum transaction that checks Chainlink's ETH/USD price for age and against the Uniswap V3 WETH/USDC pool, then sells WETH with a price limit set from the oracle.

The program reads Chainlink's ETH/USD price and the Uniswap V3 WETH/USDC 0.05% pool's price, then sells up to `amount` WETH in that pool with a price limit derived from the oracle. The USDC and any WETH the limit leaves unsold go to `recipient`. The cook reverts when the oracle's price is older than `maxAge`, the pool's price is more than `maxDeviationBps` from it, the pool is already past the limit, or the recipient receives less than `minOut` USDC.

**Target chains:** Ethereum. The recorded simulation uses an Ethereum fork.

## 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 WETH 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 oracle-guarded-swap.sauce.ts theme={null}
// Sells up to `amount` of the Pot's WETH for USDC on the Uniswap V3 WETH/USDC 0.05% pool, only
// while Chainlink's ETH/USD price is fresh and the pool's price is near it. The swap's price limit
// keeps every unit sold, after the pool's fee, at or above the oracle's price less
// `maxDeviationBps`; the USDC and the WETH the pool did not take go to `recipient`. The cook
// reverts, and nothing moves, when the oracle's price is older than `maxAge`, the pool's price is
// more than `maxDeviationBps` from it, the pool is already past the limit, or `recipient` receives
// less than `minOut` USDC. The oracle's USD price is compared with USDC prices as if
// 1 USDC = 1 USD.
//
//   sauce simulate oracle-guarded-swap.sauce.ts --chain ethereum --owner <address> \
//     --assume-balance WETH:<amount>n \
//     --args <amount> <maxAge> <maxDeviationBps> <minOut> <recipient>
//   sauce run oracle-guarded-swap.sauce.ts --chain ethereum --wallet <name> \
//     --fund WETH:<amount>n --args <amount> <maxAge> <maxDeviationBps> <minOut> <recipient>
//
//   amount: Uint256           the most WETH to sell, in wei (18 decimals): 1000000000000000000
//                             is 1 WETH; the Pot must hold it
//   maxAge: Uint256           the oldest oracle price to act on, in seconds: 3600 is an hour
//   maxDeviationBps: Uint256  how far the pool's price may be from the oracle's, and how far
//                             below it a unit may sell, in basis points: 50 is 0.5%
//   minOut: Uint256           the least USDC `recipient` may receive, in USDC base units
//                             (6 decimals): 2300000000 is 2,300 USDC
//   recipient: Address        receives the USDC and the unsold WETH
//
// On Ethereum, sell 1 WETH within 0.5% of an oracle price at most an hour old, for at least
// 2,300 USDC, paid to <recipient>:
//
//   sauce run oracle-guarded-swap.sauce.ts --chain ethereum --wallet me --fund WETH:1 \
//     --args 1000000000000000000 3600 50 2300000000 <recipient>
import { uniswapV3 } from "@sauce/router";

function main(
  amount: Uint256,
  maxAge: Uint256,
  maxDeviationBps: Uint256,
  minOut: Uint256,
  recipient: Address,
): { oracle: Uint256; poolPrice: Uint256; sold: Uint256; received: Uint256 } {
  require(amount > 0, "amount is 0");
  require(recipient != 0, "no recipient");
  require(maxDeviationBps < 10000, "maxDeviationBps is 100% or more");
  const wethBefore = pot.WETH;
  require(wethBefore >= amount, "not enough WETH in the Pot");

  // The oracle: `answer` is an Int256 of USD per ETH with 8 decimals. After the positivity check,
  // Uint256() converts it without failing and /100 rescales it to 6 decimals. It is compared with
  // USDC prices as if 1 USDC = 1 USD.
  const round = Chainlink.EthUsdFeed.latestRoundData();
  require(round.answer > 0, "no oracle price");
  require(ctx.timestamp() - round.updatedAt <= maxAge, "the oracle price is stale");
  const oracle = Uint256(round.answer) / 100;

  // The pool's price in the same units, from its sqrt price (token1 per token0, times 2^96).
  const pool = UniswapV3.Factory.getPool(WETH, USDC, 500);
  require(pool != 0, "no WETH/USDC pool at 0.05%");
  require(UniswapV3.Pool(pool).liquidity() > 0, "the pool has no liquidity");
  const sqrtPriceX96 = UniswapV3.Pool(pool).slot0().sqrtPriceX96;
  const ratio = Math.mulDiv(sqrtPriceX96, sqrtPriceX96, 1 << 96);
  const wethIs0 = WETH < USDC;
  const poolPrice = wethIs0
    ? Math.mulDiv(ratio, 10 ** 18, 1 << 96)
    : Math.mulDiv(10 ** 18, 1 << 96, ratio);
  const gap = poolPrice > oracle ? poolPrice - oracle : oracle - poolPrice;
  require(gap * 10000 <= oracle * maxDeviationBps, "the pool price is too far from the oracle");

  // The floor every unit must fetch after the fee, as the pool's sqrt price moved by √(1 − fee).
  // Selling token0 pushes the price down and selling token1 pushes it up: the swap stops there.
  const floor = Math.mulDiv(oracle, 10000 - maxDeviationBps, 10000);
  const root = Math.sqrt((1000000 - UniswapV3.Pool(pool).fee()) * 10 ** 30); // √(1 − fee), × 1e18
  const limit = wethIs0
    ? Math.mulDiv(Math.sqrt(Math.mulDiv(floor, 1 << 192, 10 ** 18)), 10 ** 18, root)
    : Math.mulDiv(Math.sqrt(Math.mulDiv(1 << 192, 10 ** 18, floor)), root, 10 ** 18);
  require(wethIs0 ? sqrtPriceX96 > limit : sqrtPriceX96 < limit, "the pool is past the limit");

  const usdcBefore = recipient.USDC;
  uniswapV3(pool, WETH, USDC, amount, limit, 0, recipient);
  const received = recipient.USDC - usdcBefore;
  const sold = wethBefore - pot.WETH;
  require(received >= minOut, "received below minOut");
  require(received >= Math.mulDiv(sold, floor, 10 ** 18), "sold below the floor");

  // The WETH the limit left unsold goes back to the recipient; the Pot's other WETH stays.
  if (amount > sold) {
    WETH.transfer(recipient, amount - sold);
  }
  return { oracle, poolPrice, sold, received };
}
```

## How it works

* Two checks run before any swap: the feed's `updatedAt` must be within `maxAge` seconds of the block's timestamp, and the pool's price within `maxDeviationBps` of the oracle's. The second is written `gap * 10000 <= oracle * maxDeviationBps`, so no division rounds the bound. The feed's `answer` is an `Int256`; the program requires it above 0 and converts it with `Uint256(round.answer)`, a checked conversion, so the unsigned arithmetic after it never sees a negative price.
* The price limit comes from the oracle, not the pool. `floor` is the oracle's price less `maxDeviationBps`; as a sqrt price it is moved by √(1 − fee), with the fee read from the pool in the cook, so the last unit the pool fills fetches `floor` after the 0.05% fee. USDC is token0 on Ethereum, so selling WETH raises the pool's sqrt price and the limit is a ceiling.
* A limit on the wrong side of the pool's current price makes a Uniswap V3 pool revert with `SPL`; the program checks that first and reverts with `the pool is past the limit`.
* `received` is the recipient's USDC balance change and `sold` the Pot's WETH balance change. The average is held to the floor (`sold below the floor`), and only `amount - sold` WETH goes back to the recipient; other WETH in the Pot stays.

## Run the example

**1. Compile**

```bash theme={null}
sauce compile oracle-guarded-swap.sauce.ts --chain ethereum
```

**2. Simulate**

```bash theme={null}
sauce simulate oracle-guarded-swap.sauce.ts --chain ethereum --owner <your-address> \
  --assume-balance WETH:1 --args 1000000000000000000 3600 50 2300000000 <recipient>
```

`--assume-balance WETH:1` gives the Pot 1 WETH for this simulation only. The result lists the oracle's price, the pool's price, the WETH sold and the USDC received.

**3. Send**

```bash theme={null}
sauce run oracle-guarded-swap.sauce.ts --chain ethereum --wallet <name> \
  --fund WETH:1 --args 1000000000000000000 3600 50 2300000000 <recipient>
```

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

```bash theme={null}
sauce tx oracle-guarded-swap.sauce.ts --chain ethereum --owner <your-address> \
  --fund WETH:1 --args 1000000000000000000 3600 50 2300000000 <recipient>
```

## Simulation result

Simulated on a local fork of Ethereum at block 26278321 with the current contracts. Nothing was signed or sent; values change with the block.

```text theme={null}
The cook succeeds from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266: gas used 275555 for the cook, after the Pot's deploy (1. below), returns 0x000000000000000000000000000000000000000000000000000000008abfcb3d000000000000000000000000000000000000000000000000000000008b082d0e0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000008af5937e
main returns { oracle: Uint256, poolPrice: Uint256, sold: Uint256, received: Uint256 }: { oracle: 2327825213, poolPrice: 2332568846, sold: 1000000000000000000, received: 2331349886 }
...
What moved, in order:
...
  [3] 0x88e6…5640        -> 0x7099…79C8          2331.349886 USDC
  [4] Pot                -> 0x88e6…5640          1 WETH
Net balance changes:
  0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640: -2331.349886 USDC, +1 WETH
  0x70997970C51812dc3A010C7d01b50e0d17dc79C8: +2331.349886 USDC
  0xDcFCc75A3C9a0EC798CAf81B6C84D4c5d5988c49 (the Pot): -1 WETH
  0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (owner): -0.00003728943124295 ETH
...
```

The oracle's price was 2,327.825213 USD per ETH and the pool's 2,332.568846 USDC per WETH, about 0.2% apart at 1 USDC = 1 USD, inside the 0.5% allowed. The program sold the whole 1 WETH for 2,331.349886 USDC, paid to the recipient.

A larger order stops at the limit. On the same fork, selling 300 WETH with `maxDeviationBps` at 40:

```bash theme={null}
sauce simulate oracle-guarded-swap.sauce.ts --chain ethereum --owner <your-address> \
  --assume-balance WETH:300 \
  --args 300000000000000000000 3600 40 200000000000 <recipient>
```

```text theme={null}
The cook succeeds from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266: gas used 432051 for the cook, after the Pot's deploy (1. below), returns 0x000000000000000000000000000000000000000000000000000000008abfcb3d000000000000000000000000000000000000000000000000000000008b082d0e000000000000000000000000000000000000000000000006371d2e0fdae835c20000000000000000000000000000000000000000000000000000003e109c3584
main returns { oracle: Uint256, poolPrice: Uint256, sold: Uint256, received: Uint256 }: { oracle: 2327825213, poolPrice: 2332568846, sold: 114651845534299993538, received: 266566645124 }
...
What moved, in order:
...
  [3] 0x88e6…5640        -> 0x7099…79C8          266566.645124 USDC
  [4] Pot                -> 0x88e6…5640          114.651845534299993538 WETH
  [5] Pot                -> 0x7099…79C8          185.348154465700006462 WETH
Net balance changes:
  0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640: -266566.645124 USDC, +114.651845534299993538 WETH
  0x70997970C51812dc3A010C7d01b50e0d17dc79C8: +266566.645124 USDC, +185.348154465700006462 WETH
  0xDcFCc75A3C9a0EC798CAf81B6C84D4c5d5988c49 (the Pot): -300 WETH
  0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (owner): -0.00003728943124295 ETH
...
```

The program sold 114.651845534299993538 WETH for 266,566.645124 USDC and stopped at the price limit, then sent the other 185.348154465700006462 WETH to the recipient.

On the same fork, with `maxDeviationBps` at 0 the pool's price must equal the oracle's, and the cook reverts at that check:

```bash theme={null}
sauce simulate oracle-guarded-swap.sauce.ts --chain ethereum --owner <your-address> \
  --assume-balance WETH:1 \
  --args 1000000000000000000 3600 0 2300000000 <recipient> \
  --expect-revert "too far from the oracle"
```

```text theme={null}
As expected, the cook reverts: the program threw "the pool price is too far from the oracle"
The cook reverts: the program threw "the pool price is too far from the oracle"
...
```

## Next steps

* [Price survey](https://docs.eco.com/quickstarts/sauce/price-survey.md): the same reads with no swap, run with `sauce simulate`.
* [Multi-venue route](https://docs.eco.com/quickstarts/sauce/multi-venue-route.md): two legs on paid-first venues, each with its own minimum.
* [`sauce simulate`](https://docs.eco.com/sauce-sdk/cli/simulate), [`sauce price`](https://docs.eco.com/sauce-sdk/cli/price), [Built-ins](https://docs.eco.com/sauce/builtins.md), [Names](https://docs.eco.com/sauce/names.md)


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