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

# Stop-loss and take-profit on an oracle price

> Check a stop-loss or take-profit threshold and sell the Pot's WETH on Ethereum when the condition is met.

Each run reads Chainlink's ETH/USD feed. When the answer is at or below `stop` or at or above `takeProfit`, the program sells the Pot's WETH for USDC on Uniswap V3's 0.05% pool, down to a floor of the oracle price less `maxSlippageBps`. Between the thresholds it returns without trading. The cook reverts on a feed answer older than `maxAge` seconds, a non-positive answer, or an average sale price below the floor.

**Target chains:** Ethereum. `Chainlink.EthUsdFeed` is not in Base's registry, and `sauce compile --chain base` refuses the file.

This program evaluates conditions only when called. To run it repeatedly, use an external scheduler or caller that submits an authorized transaction.

## 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 native coin for gas and the Kitchen fee. Fund the Pot once it is deployed, or use [`--fund`](https://docs.eco.com/sauce-sdk/cli/funding); the program sells WETH the Pot holds.

## Program

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

```ts stop-loss.sauce.ts theme={null}
// A stop-loss and take-profit on the Pot's WETH, meant to run on a schedule. It reads Chainlink's
// ETH/USD feed and, when the price is at or below `stop` or at or above `takeProfit`, sells the
// Pot's WETH for USDC on Uniswap V3's 0.05% WETH/USDC pool. The swap stops at the pool price where
// a unit, after the pool fee, fetches the oracle price less `maxSlippageBps`; WETH the pool cannot
// take at that price stays in the Pot. Between the two thresholds it sells nothing and returns
// normally. The cook reverts on a feed answer older than `maxAge` seconds, a non-positive answer,
// or an average sale price below that floor. The USDC stays in the Pot.
//
//   sauce simulate stop-loss.sauce.ts --chain ethereum --owner <address> \
//     --assume-balance WETH:<whole-tokens> --args <stop> <takeProfit> <maxSlippageBps> <maxAge>
//   sauce run stop-loss.sauce.ts --chain ethereum --wallet <name> \
//     --args <stop> <takeProfit> <maxSlippageBps> <maxAge>
//
//   stop: Uint256            sell at or below this ETH price, in the feed's units (USD, 8
//                            decimals): 240000000000 is 2,400 USD; 0 sets no stop
//   takeProfit: Uint256      sell at or above this ETH price, same units: 320000000000 is
//                            3,200 USD; 0 sets no take-profit
//   maxSlippageBps: Uint256  how far below the oracle price a unit may sell, fee included, in
//                            hundredths of a percent: 100 is 1%
//   maxAge: Uint256          the oldest feed answer to act on, in seconds: 3600 is one hour
//
// On Ethereum, sell below 2,400 USD or above 3,200 USD, within 1% of the oracle:
//
//   sauce run stop-loss.sauce.ts --chain ethereum --wallet me \
//     --args 240000000000 320000000000 100 3600
import { uniswapV3 } from "@sauce/router";

function main(
  stop: Uint256,
  takeProfit: Uint256,
  maxSlippageBps: Uint256,
  maxAge: Uint256,
): { price: Uint256; triggered: bool; sold: Uint256; received: Uint256 } {
  require(maxSlippageBps < 10000, "maxSlippageBps is 100% or more");
  const round = Chainlink.EthUsdFeed.latestRoundData();
  // The feed's answer is a signed Int256. Reject zero and negative answers, then convert once:
  // Uint256() is checked, and the unsigned math below (mulDiv) refuses a negative Int256.
  require(round.answer > 0, "no price");
  const price = Uint256(round.answer); // USD per ETH, 8 decimals
  require(ctx.timestamp() - round.updatedAt <= maxAge, "the price is stale");

  const triggered = (stop > 0 && price <= stop) || (takeProfit > 0 && price >= takeProfit);
  const held = pot.WETH;
  if (!triggered || held == 0) {
    return { price, triggered, sold: 0, received: 0 };
  }

  // The least one WETH may fetch, in USDC base units: the feed's 8 decimals become USDC's 6.
  const floor = Math.mulDiv(price, 10000 - maxSlippageBps, 10000 * 100);
  const pool = UniswapV3.Factory.getPool(WETH, USDC, 500);
  require(pool != 0, "no WETH/USDC pool at 0.05%");
  // `floor` as the pool's sqrt price (token1 per token0, x 2^96), moved by sqrt(1 - fee) so the
  // last unit sold fetches `floor` after the fee. Selling token0 pushes the price down, selling
  // token1 pushes it up: the swap stops at the limit either way. `root` is sqrt(1 - fee) x 1e18.
  const root = Math.sqrt((1000000 - UniswapV3.Pool(pool).fee()) * 10 ** 30);
  const wethIs0 = WETH < USDC;
  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);
  // A pool already past the limit has no unit to sell at the floor: nothing sells this run.
  const now = UniswapV3.Pool(pool).slot0().sqrtPriceX96;
  if (wethIs0 ? now <= limit : now >= limit) {
    return { price, triggered, sold: 0, received: 0 };
  }

  // The limit decides how much sells, so no minimum output is known before the swap. The
  // average is checked after it, from what the Pot's balances show moved.
  const usdcBefore = pot.USDC;
  uniswapV3(pool, WETH, USDC, held, limit, 0, pot);
  const sold = held - pot.WETH;
  const received = pot.USDC - usdcBefore;
  require(received >= Math.mulDiv(sold, floor, 10 ** 18), "sold below the floor");
  return { price, triggered, sold, received };
}
```

## How it works

* Chainlink declares `answer` as `int256`, so `round.answer` is an `Int256`. The program requires it to be positive, then converts it once with `Uint256(round.answer)`. The conversion is checked: a negative value reverts instead of becoming a large unsigned number. Every comparison and `Math.mulDiv` after it is unsigned.
* `ctx.timestamp() - round.updatedAt <= maxAge` rejects a feed answer older than `maxAge`. The program acts on the oracle's price, not the pool's, so a pool pushed away from the oracle within one block does not trigger a sale.
* The floor is in USDC base units per WETH: `price` has the feed's 8 decimals, and dividing by `10000 * 100` both applies `maxSlippageBps` and drops 2 decimals to USDC's 6.
* The swap's price limit is the floor's sqrt price moved by `sqrt(1 - fee)`, so the last unit sold still fetches the floor after the pool fee. When the pool is already past that limit, the program returns `sold: 0` instead of calling the swap.
* `sold` and `received` are the changes in the Pot's WETH and USDC balances, and the final `require` checks the average price from those.

> [!NOTE]
>
> The swap stops at the floor. In a falling market the pool can reach it before all the WETH sells; the rest stays in the Pot until a later run finds a price above the floor.

## Run the example

**1. Compile**

```bash theme={null}
sauce compile stop-loss.sauce.ts --chain ethereum
```

**2. Simulate**

```bash theme={null}
sauce simulate stop-loss.sauce.ts --chain ethereum --owner <your-address> \
  --assume-balance WETH:1 --args 1000000000000 0 100 3600
```

`--assume-balance WETH:1` gives the Pot 1 WETH for this simulation only. A `stop` of 10,000 USD, above the market, makes this run trigger.

**3. Send**

```bash theme={null}
sauce run stop-loss.sauce.ts --chain ethereum --wallet <name> \
  --args 240000000000 320000000000 100 3600
```

`sauce tx stop-loss.sauce.ts --chain ethereum --owner <your-address> --args 240000000000 320000000000 100 3600` prints the unsigned transactions instead, for your own signer or scheduler.

## Simulation result

Simulated on a local fork of Ethereum at block 26284400 with the current contracts, using the Simulate command above: `stop` 10,000 USD, no take-profit, 1% slippage, a one-hour `maxAge`. Nothing was signed or sent.

```text theme={null}
main returns { price: Uint256, triggered: bool, sold: Uint256, received: Uint256 }: { price: 232491817030, triggered: true, sold: 1000000000000000000, received: 2329029303 }
Left in the Pot, not in the owner's wallet:
  2329.029303 USDC
...
What moved, in order:
...
  [3] 0x88e6…5640        -> Pot                  2329.029303 USDC
  [4] Pot                -> 0x88e6…5640          1 WETH
```

The feed answered 2,324.91817030 USD. That is below the stop, so the run triggered and sold the whole 1 WETH for 2,329.029303 USDC, above the floor of 2,301.668988 USDC (the oracle price less 1%).

On the same fork, with a stop of 2,000 USD and a take-profit of 3,200 USD, the price is between the two and nothing sells:

```bash theme={null}
sauce simulate stop-loss.sauce.ts --chain ethereum --owner <your-address> \
  --assume-balance WETH:1 --args 200000000000 320000000000 100 3600
```

```text theme={null}
main returns { price: Uint256, triggered: bool, sold: Uint256, received: Uint256 }: { price: 232491817030, triggered: false, sold: 0, received: 0 }
```

With `maxAge` set to 1 second, the feed's latest answer is too old and the cook reverts:

```bash theme={null}
sauce simulate stop-loss.sauce.ts --chain ethereum --owner <your-address> \
  --assume-balance WETH:1 --args 1000000000000 0 100 1 \
  --expect-revert "the price is stale"
```

```text theme={null}
As expected, the cook reverts: the program threw "the price is stale"
```

## Next steps

* [Dollar-cost averaging](https://docs.eco.com/quickstarts/sauce/dca.md): build the WETH position one tranche at a time.
* [Buy the dip](https://docs.eco.com/quickstarts/sauce/buy-the-dip.md): buy WETH when the oracle price falls a set percentage.
* [Oracle-guarded swap](https://docs.eco.com/quickstarts/sauce/oracle-guarded-swap.md): a single swap held to an oracle price.
* [Sweep the Pot](https://docs.eco.com/quickstarts/sauce/sweep-pot.md): move the USDC from the Pot to a wallet.
* [`sauce simulate`](https://docs.eco.com/sauce-sdk/cli/simulate), [Names](https://docs.eco.com/sauce/names.md), [Protocols reference](https://docs.eco.com/sauce-sdk/reference/protocols.md).


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