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

# Low-level calls: raw calldata and your own ABI

> A low-level Base program that wraps ETH with a raw evm.call, reads a Uniswap V3 pool through its own ABI file, and keeps a failing read from reverting the cook with an atomic try block.

Use this recipe when you need raw calldata or a contract interface from your own ABI file. The program wraps ETH into WETH, then reads a Uniswap V3 pool's price observations through `IOracle.json`.

The observation read has a 200,000 gas limit and runs inside a `try` block. If that read fails, the program reports `observed: false` and keeps the successful wrap. Invalid inputs, an unavailable pool or an incorrect wrapped amount revert the whole cook.

**Target chains:** Base. It names `WETH`, `USDC` and `UniswapV3.Factory`; the pool's address comes from the factory at run time.

## 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 ETH for the program and native coin for gas and the Kitchen fee. Keep `IOracle.json` in the same directory as the program. See [Installation](https://docs.eco.com/sauce-sdk/installation.md).

## Program

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

```ts low-level-call.sauce.ts theme={null}
// Low level: raw calls and the program's own ABI, where the other cookbook programs use names.
// Wraps `wrapAmount` of the Pot's native ETH into WETH with a raw evm.call to WETH's deposit(),
// then reads the Uniswap V3 WETH/USDC 0.05% pool's tick cumulatives `window` seconds ago and now
// through observe(), which the registry's UniswapV3.Pool ABI does not declare: IOracle.json,
// beside this file, declares it. The read runs in a try block, capped at 200000 gas: when it fails
// (the pool keeps no observation that old), observed is false and the wrap still happens. The
// cook reverts when `wrapAmount` is 0 or more than the Pot's ETH, `window` is 0 or 2^32 or more,
// the factory has no WETH/USDC 0.05% pool, or the deposit credits a different amount of WETH.
//
//   sauce simulate low-level-call.sauce.ts --chain base --owner <address> \
//     --assume-balance ETH:<wrapAmount>n --args <wrapAmount> <window>
//   sauce run low-level-call.sauce.ts --chain base --wallet <name> \
//     --fund ETH:<wrapAmount>n --args <wrapAmount> <window>
//
//   wrapAmount: Uint256  the native ETH to wrap, in wei: 10000000000000000 is 0.01 ETH
//   window: Uint256      how far back to read the pool, in seconds, below 2^32: 1800 is 30 minutes
//
// On Base, wrap 0.01 ETH and read the pool's mean tick over the last 30 minutes:
//
//   sauce run low-level-call.sauce.ts --chain base --wallet me --fund ETH:0.01 \
//     --args 10000000000000000 1800
import IOracle from "./IOracle.json";

function main(
  wrapAmount: Uint256,
  window: Uint256,
): { wrapped: Uint256; observed: bool; meanTick: Int256 } {
  require(wrapAmount > 0, "nothing to wrap");
  require(window > 0 && window <= 4294967295, "window out of range");
  require(pot.balance >= wrapAmount, "not enough ETH in the Pot");

  // 1. A call that sends value. A bound call never does, so this is raw calldata: deposit().
  const wethBefore = pot.WETH;
  evm.call(WETH, wrapAmount, hex`d0e30db0`);
  const wrapped = pot.WETH - wethBefore;
  require(wrapped == wrapAmount, "deposit credited a different amount");

  // 2. A read through the program's own ABI, bound to the pool's address with .at().
  const pool = UniswapV3.Factory.getPool(WETH, USDC, 500);
  require(pool != 0, "no WETH/USDC 0.05% pool");

  // 3. A read that may fail, in one atomic try block: the call, its decode and the arithmetic.
  //    If any of them fails, the block's assignments are undone and the handler runs; the wrap
  //    above stays. .gas() caps what the read may use.
  let observed = false;
  let meanTick: Int256 = 0;
  try {
    const [ticks] = IOracle.at(pool).observe([window, 0]).gas(200000);
    // int56 outputs arrive as Int256; the difference is signed and divides toward zero.
    meanTick = (ticks[1] - ticks[0]) / Int256(window);
    observed = true;
  } catch {
    // observe reverts when the pool keeps no observation `window` seconds old.
  }

  return { wrapped, observed, meanTick };
}
```

The ABI file sits beside the program and declares the one function the program calls, with the pool's deployed types:

```json IOracle.json theme={null}
[
  {
    "type": "function",
    "name": "observe",
    "stateMutability": "view",
    "inputs": [{ "name": "secondsAgos", "type": "uint32[]" }],
    "outputs": [
      { "name": "tickCumulatives", "type": "int56[]" },
      { "name": "secondsPerLiquidityCumulativeX128s", "type": "uint160[]" }
    ]
  }
]
```

## How it works

* ``evm.call(WETH, wrapAmount, hex`d0e30db0`)`` is the only way to send value with a call: a bound call never sends any. `d0e30db0` is the selector of `deposit()`, and the call has no arguments. The program measures the WETH it received rather than reading a return value.
* `observe` is not in the registry's `UniswapV3.Pool` ABI, so `import IOracle from "./IOracle.json"` binds it and `IOracle.at(pool)` calls it at the address the factory returned. Anything bound this way is checked only against the ABI you supply.
* The `try` block holds more than one statement, so it is atomic: if the call, the decode of its `int56[]` result or the division fails, the block's assignments to `observed` and `meanTick` roll back and the handler runs. Work before the block, the wrap, remains. A `.catch()` on the call alone would not cover a failure while decoding a successful return.
* `.gas(200000)` caps the gas forwarded to `observe`, not the transaction's limit. A read that needs more fails inside the `try` and leaves the rest of the cook its gas.
* `int56` outputs arrive as `Int256`, so `ticks[1] - ticks[0]` is a signed difference. Mixed arithmetic needs a conversion, hence `Int256(window)`; signed division truncates toward zero.

## Run the example

**1. Compile**

```bash theme={null}
sauce compile low-level-call.sauce.ts --chain base
```

**2. Simulate**

```bash theme={null}
sauce simulate low-level-call.sauce.ts --chain base --owner <your-address> \
  --assume-balance ETH:0.01 --args 10000000000000000 1800
```

`--assume-balance ETH:0.01` gives the Pot 0.01 ETH for this simulation only. If your Pot is not deployed yet, the simulation deploys it first.

**3. Send**

```bash theme={null}
sauce run low-level-call.sauce.ts --chain base --wallet <name> \
  --fund ETH:0.01 --args 10000000000000000 1800
```

`--fund ETH:0.01` adds 0.01 ETH to the cook's value, which lands in the Pot. To sign elsewhere, build the unsigned transactions instead:

```bash theme={null}
sauce tx low-level-call.sauce.ts --chain base --owner <your-address> \
  --fund ETH:0.01 --args 10000000000000000 1800
```

## Simulation result

Simulated on a local fork of Base at block 52965104 with the current contracts. Nothing was signed or sent.

```text theme={null}
main returns { wrapped: Uint256, observed: bool, meanTick: Int256 }: { wrapped: 10000000000000000, observed: true, meanTick: -198546 }
Left in the Pot, not in the owner's wallet:
  0.01 WETH
...
What moved, in order:
...
  [3] Pot                -> WETH 0x4200…0006     0.01 ETH
  [4] mint               -> Pot                  0.01 WETH                wrapped
```

The Pot sent 0.01 ETH to WETH and received 0.01 WETH. The mean tick over the last 30 minutes is -198,546, the signed tick change divided by `window` and truncated toward zero.

On the same fork, a `window` of 4,000,000,000 seconds is older than any observation the pool keeps, so `observe` reverts inside the `try` block and the cook still succeeds:

```bash theme={null}
sauce simulate low-level-call.sauce.ts --chain base --owner <your-address> \
  --assume-balance ETH:0.01 --args 10000000000000000 4000000000
```

```text theme={null}
main returns { wrapped: Uint256, observed: bool, meanTick: Int256 }: { wrapped: 10000000000000000, observed: false, meanTick: 0 }
```

The wrap stands and `observed` and `meanTick` keep their initial values. On the same fork, with less ETH in the Pot than `wrapAmount` (`--assume-balance ETH:0.005`), the guard reverts the cook:

```text theme={null}
As expected, the cook reverts: the program threw "not enough ETH in the Pot"
```

## Next steps

* [Swap and supply as an actions document](https://docs.eco.com/quickstarts/sauce/actions-swap-supply.md): the opposite end, protocol calls written as JSON data.
* [Quote two pools, swap on the better one, supply to Aave](https://docs.eco.com/quickstarts/sauce/quote-swap-supply.md): the same pools called by name.
* [Builtins](https://docs.eco.com/sauce/builtins.md), [Names](https://docs.eco.com/sauce/names.md), [`sauce simulate`](https://docs.eco.com/sauce-sdk/cli/simulate).


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