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

# Buy the dip: buy WETH when ETH falls below a reference

> An Ethereum cook that reads Chainlink's ETH/USD feed, computes the signed change from a reference price, and buys WETH on Uniswap V3 only after a set fall.

The program reads Chainlink's ETH/USD feed and computes how far the price has moved from `reference`, as a signed number of basis points. When the change is at or below `dipBps`, a negative threshold such as `-300`, it spends `tranche` of the Pot's USDC on WETH in Uniswap V3's 0.05% pool, under a price limit. Otherwise it buys nothing and returns normally. The cook reverts on a stale or non-positive feed answer, a positive `dipBps`, or an average price paid above `priceLimit`.

**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); it needs at least `tranche` USDC when the program buys.

## 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 buy-the-dip.sauce.ts theme={null}
// Buys WETH with `tranche` of the Pot's USDC when ETH has fallen at least `dipBps` from
// `reference`. It reads Chainlink's ETH/USD feed and computes the signed change from `reference`
// in basis points. At or below `dipBps` it buys on Uniswap V3's 0.05% WETH/USDC pool, stopping at
// the pool price where one WETH, fee included, costs `priceLimit`. Otherwise it buys nothing and
// returns normally, as it does when the pool is above `priceLimit` or the Pot holds less than a
// tranche. The cook reverts on a feed answer older than `maxAge` seconds, a non-positive answer,
// a missing pool, or an average price paid above `priceLimit`. The WETH stays in the Pot.
//
//   sauce simulate buy-the-dip.sauce.ts --chain ethereum --owner <address> \
//     --assume-balance USDC:<tranche>n --args <reference> <dipBps> <maxAge> <tranche> <priceLimit>
//   sauce run buy-the-dip.sauce.ts --chain ethereum --wallet <name> \
//     --args <reference> <dipBps> <maxAge> <tranche> <priceLimit>
//
//   reference: Uint256   the ETH price to measure the dip from, in the feed's units (USD, 8
//                        decimals): 250000000000 is 2,500 USD
//   dipBps: Int256       the change that triggers a buy, in hundredths of a percent, negative:
//                        -300 buys once ETH is 3% or more below `reference`
//   maxAge: Uint256      the oldest feed answer to act on, in seconds: 3600 is one hour
//   tranche: Uint256     the USDC to spend, in USDC base units (6 decimals): 250000000 is 250 USDC
//   priceLimit: Uint256  the most one WETH may cost, fee included, in USDC base units:
//                        2500000000 is 2,500 USDC
//
// On Ethereum, spend 250 USDC once ETH is 3% below 2,500 USD, paying at most 2,500 USDC per WETH:
//
//   sauce run buy-the-dip.sauce.ts --chain ethereum --wallet me \
//     --args 250000000000 -300 3600 250000000 2500000000
import { uniswapV3 } from "@sauce/router";

function main(
  reference: Uint256,
  dipBps: Int256,
  maxAge: Uint256,
  tranche: Uint256,
  priceLimit: Uint256,
): { change: Int256; spent: Uint256; bought: Uint256 } {
  require(reference > 0 && tranche > 0 && priceLimit > 0, "set reference, tranche, priceLimit");
  require(dipBps < 0, "dipBps must be negative");
  const round = Chainlink.EthUsdFeed.latestRoundData();
  require(round.answer > 0, "no price");
  require(ctx.timestamp() - round.updatedAt <= maxAge, "the price is stale");

  // answer is Int256 and reference is Uint256: Int256(reference) is a checked conversion, and the
  // subtraction, multiplication and division are signed. Both prices have 8 decimals, so the
  // change is in basis points; division truncates toward zero.
  const ref = Int256(reference);
  const change = ((round.answer - ref) * 10000) / ref;
  if (change > dipBps) {
    return { change, spent: 0, bought: 0 };
  }

  const pool = UniswapV3.Factory.getPool(WETH, USDC, 500);
  require(pool != 0, "no WETH/USDC pool at 0.05%");
  const fee = UniswapV3.Pool(pool).fee();
  // The pool's price in USDC base units per WETH, from its sqrt price (token1 per token0, x 2^96).
  const sqrtPriceX96 = UniswapV3.Pool(pool).slot0().sqrtPriceX96;
  const ratio = Math.mulDiv(sqrtPriceX96, sqrtPriceX96, 1 << 96);
  const price =
    WETH < USDC ? Math.mulDiv(ratio, 10 ** 18, 1 << 96) : Math.mulDiv(10 ** 18, 1 << 96, ratio);
  const usdcBefore = pot.USDC;
  const wethBefore = pot.WETH;
  if (Math.mulDiv(price, 1000000, 1000000 - fee) > priceLimit || usdcBefore < tranche) {
    return { change, spent: 0, bought: 0 };
  }

  // The pool price at which WETH, fee included, costs exactly priceLimit, as a sqrt price.
  const atLimit = Math.mulDiv(priceLimit, 1000000 - fee, 1000000);
  const limit =
    WETH < USDC
      ? Math.sqrt(Math.mulDiv(atLimit, 1 << 192, 10 ** 18))
      : Math.sqrt(Math.mulDiv(1 << 192, 10 ** 18, atLimit));
  uniswapV3(pool, USDC, WETH, tranche, limit, 0, pot);
  const spent = usdcBefore - pot.USDC;
  const bought = pot.WETH - wethBefore;
  require(Math.mulDiv(bought, priceLimit, 10 ** 18) >= spent, "paid more than priceLimit");
  return { change, spent, bought };
}
```

## How it works

* `change` is an `Int256` because the price can sit above or below `reference`. `round.answer` is already `Int256`, since Chainlink declares it `int256`. `Int256(reference)` converts the unsigned parameter with a check: a value above 2²⁵⁵ − 1 reverts. The subtraction, multiplication and division are then signed, and the division truncates toward zero.
* Mixing the two kinds does not compile. `round.answer - reference` without the conversion is rejected with "cannot combine Int256 and Uint256 in this expression; convert an operand with `Int256(value)` or `Uint256(value)` first (conversions check bounds)".
* `dipBps` is an `Int256` parameter, so it can be negative. A negative literal is an `Int256`; negating a nonzero `Uint256` (`-amount`) fails, so a signed negative from an unsigned value is written `-Int256(amount)`.
* The swap half is the same as in [Dollar-cost averaging](https://docs.eco.com/quickstarts/sauce/dca.md): the price limit stops the pool at `priceLimit`, and `spent` and `bought` are the Pot's balance changes, checked against `priceLimit` after the swap.

## Run the example

**1. Compile**

```bash theme={null}
sauce compile buy-the-dip.sauce.ts --chain ethereum
```

**2. Simulate**

```bash theme={null}
sauce simulate buy-the-dip.sauce.ts --chain ethereum --owner <your-address> \
  --assume-balance USDC:250 --args 250000000000 -300 3600 250000000 2500000000
```

`--assume-balance USDC:250` gives the Pot 250 USDC for this simulation only. `--args` reads `-300` as a negative integer for the `Int256` parameter.

**3. Send**

```bash theme={null}
sauce run buy-the-dip.sauce.ts --chain ethereum --wallet <name> \
  --args 250000000000 -300 3600 250000000 2500000000
```

`sauce tx buy-the-dip.sauce.ts --chain ethereum --owner <your-address> --args 250000000000 -300 3600 250000000 2500000000` 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: a reference of 2,500 USD, above the market. Nothing was signed or sent.

```text theme={null}
main returns { change: Int256, spent: Uint256, bought: Uint256 }: { change: -700, spent: 250000000, bought: 107230659717509590 }
Left in the Pot, not in the owner's wallet:
  0.10723065971750959 WETH
...
What moved, in order:
...
  [3] 0x88e6…5640        -> Pot                  0.10723065971750959 WETH
  [4] Pot                -> 0x88e6…5640          250 USDC
```

The feed answered 2,324.91817030 USD, 7.0033% below the reference. The division truncates that to `change: -700`, which is at or below `-300`, so the program spent 250 USDC for 0.10723065971750959 WETH, an average of about 2,331.42 USDC per WETH with the fee included.

On the same fork, with a reference of 2,300 USD, below the market, the change is positive and nothing is bought:

```bash theme={null}
sauce simulate buy-the-dip.sauce.ts --chain ethereum --owner <your-address> \
  --assume-balance USDC:250 --args 230000000000 -300 3600 250000000 2500000000
```

```text theme={null}
main returns { change: Int256, spent: Uint256, bought: Uint256 }: { change: 108, spent: 0, bought: 0 }
```

A positive `dipBps` is refused:

```bash theme={null}
sauce simulate buy-the-dip.sauce.ts --chain ethereum --owner <your-address> \
  --assume-balance USDC:250 --args 250000000000 300 3600 250000000 2500000000 \
  --expect-revert "dipBps must be negative"
```

```text theme={null}
As expected, the cook reverts: the program threw "dipBps must be negative"
```

## Next steps

* [Dollar-cost averaging](https://docs.eco.com/quickstarts/sauce/dca.md): buy a tranche on every run, under the same kind of price limit.
* [Stop-loss and take-profit](https://docs.eco.com/quickstarts/sauce/stop-loss.md): sell on the same feed when the price crosses a threshold.
* [Oracle-guarded swap](https://docs.eco.com/quickstarts/sauce/oracle-guarded-swap.md): a single swap held to an oracle price.
* [Built-ins](https://docs.eco.com/sauce/builtins.md), [`sauce simulate`](https://docs.eco.com/sauce-sdk/cli/simulate), [`sauce tx`](https://docs.eco.com/sauce-sdk/cli/tx).


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