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

# Contracts and ABIs

> How a SauceScript program calls a contract, for anyone whose program reads or changes another protocol's state.

A program calls a contract the way TypeScript calls a method: `UniswapV3.Factory.getPool(WETH, USDC, 500)`. The ABI comes from the registry, the compiler encodes the arguments and decodes the result, and the call is made from the Pot when the program runs.

Prefer these forms, in order:

1. A protocol namespace, for a contract the chain's registry records (this page's first half).
2. A registry ABI imported by path, for a contract the registry describes but the chain has no namespace for ([low level](#low-level-abi-imports-and-raw-calls)).
3. Your own JSON ABI beside the program, for a contract or function no registry describes ([low level](#low-level-abi-imports-and-raw-calls)).

## Calling by namespace

```ts factory.sauce.ts theme={null}
// Finds the chain's 0.05% WETH/USDC Uniswap V3 pool and returns its current sqrt price.
function main(): Uint256 {
  const pool = UniswapV3.Factory.getPool(WETH, USDC, 500);
  require(pool != 0, "no WETH/USDC 0.05% pool");
  const [sqrtPriceX96] = pool.slot0();
  require(pool.token0().decimals() > 0, "token0 has no decimals");
  return sqrtPriceX96;
}
```

* **The registry ABI types the call.** `getPool(tokenA: address, tokenB: address, fee: uint24)` is encoded from the three arguments, and its `pool: address` output is decoded. `sauce globals --chain base --protocol UniswapV3` prints every method of the namespace with its types.

* **A contract a call returns** is called directly where the registry records what it is. `getPool` returns a Uniswap V3 pool, so `pool.slot0()` is a pool call, and `pool.token0()` returns a token, so `.decimals()` is an ERC-20 call. A token reached this way takes the token methods; its `transfer`, `approve` and `transferFrom` are the checked ones that `Token(address)` uses. Keep such a result in a `const`, not a `let`.

* **An address the program is given** is called through the interface that describes it, with the address in the same expression:

  ```ts interface-call.sauce.ts theme={null}
  // Reads the price and liquidity of a Uniswap V3 pool passed as an argument.
  function main(pool: Address): Uint256 {
    const [sqrtPriceX96, tick] = UniswapV3.Pool(pool).slot0();
    require(UniswapV3.Pool(pool).liquidity() > 0, "the pool has no liquidity");
    return sqrtPriceX96;
  }
  ```

  A method called on a bare `Address` parameter, such as `pool.liquidity()` with `pool: Address`, does not compile, because the compiler does not know which ABI to use; write `UniswapV3.Pool(pool).liquidity()`, as above. Its message:

  ```text theme={null}
  this call does not name a function, builtin, or contract method in scope
  ```

* **`.at(address)` reaches another deployment of a named contract's ABI**: `AaveV3.Pool.at(pool).supply(…)` calls `supply` at `pool` with Aave V3 Pool's ABI.

### Results

| The method declares | The call returns |
| - | - |
| no outputs | the raw return bytes |
| one output | that output |
| several outputs | a record named after the ABI's outputs: `.amountOut`, `[0]`, or `const [a, b] = …` |

An `intN` output is an `Int256`. A `uintN`, `address` or `bool` output is an unsigned number. A read integer output is checked against its declared width: a reply word outside the range of `uint128`, `int24` or whichever type the ABI declares reverts the cook. Arrays keep the kind: an `intN[]` output is an `Int256[]`, and an `intN[]` parameter takes an `Int256[]`, never a `Uint256[]`.

### Results nothing reads

A result that nothing reads is not decoded and not checked. That holds for a discarded call and for a result kept in a variable that is never read. An empty or malformed reply then passes, and so does a call to an address with no code, which succeeds with empty data:

```ts unread.sauce.ts theme={null}
// Calls `liquidity()` on `target` and discards the result.
function main(target: Address): Uint256 {
  UniswapV3.Pool(target).liquidity();
  return 1;
}
```

```ts read.sauce.ts theme={null}
// Calls `liquidity()` on `target` and returns the result.
function main(target: Address): Uint256 {
  return UniswapV3.Pool(target).liquidity();
}
```

On a local fork of Base at block 52965000 with the current contracts, with an address that has no code as `target`, `unread.sauce.ts` returns 1 and `read.sauce.ts` reverts with no data.

* Reading any part of a result decodes and checks the whole declared result, sibling and nested fields included. Destructuring counts as a read.
* `abi.decode` is always checked.
* Where the target must be a contract, check `ctx.isContract(target)` first.

[Security considerations](https://docs.eco.com/sauce/security-considerations.md) lists what a program has to check itself.

### Struct arguments and several outputs

A struct parameter takes a record. Its fields are matched to the ABI by name, in any order. Destructuring takes the outputs in ABI order, and stops wherever you stop naming them:

```ts quote.sauce.ts theme={null}
// Quotes selling `amountIn` WETH for USDC in the 0.05% Uniswap V3 pool, and returns the
// output and the number of initialized ticks the swap would cross.
function main(amountIn: Uint256): Uint256 {
  const [amountOut, sqrtPriceX96After, ticksCrossed] = UniswapV3.QuoterV2.quoteExactInputSingle({
    tokenIn: WETH,
    tokenOut: USDC,
    amountIn: amountIn,
    fee: 500,
    sqrtPriceLimitX96: 0,
  });
  require(sqrtPriceX96After > 0, "no price after the swap");
  return amountOut * 1000 + ticksCrossed;
}
```

To read one field, name it on the result:

```ts quote-field.sauce.ts theme={null}
// The USDC out of selling `amountIn` WETH in the 0.05% pool.
function main(amountIn: Uint256): Uint256 {
  return UniswapV3.QuoterV2.quoteExactInputSingle({
    tokenIn: WETH,
    tokenOut: USDC,
    amountIn: amountIn,
    fee: 500,
    sqrtPriceLimitX96: 0,
  }).amountOut;
}
```

`const [sqrtPriceX96] = pool.slot0();` and `pool.slot0().sqrtPriceX96` both read the first field alone.

### Value and gas

A call through a namespace or an ABI binding sends no native value. The one way to attach value is `evm.call`, below.

`.gas(n)` gives one call at most `n` gas. Without it, a call is forwarded all the gas the EVM allows. `.gas(n)` and `.catch(…)` can be written in either order:

```ts gas.sauce.ts theme={null}
// Reads `pool`'s liquidity, giving the call at most `gasCap` gas, and returns 0 when the call fails.
function main(pool: Address, gasCap: Uint256): Uint256 {
  return UniswapV3.Pool(pool).liquidity().gas(gasCap).catch(() => {
    return 0;
  });
}
```

On a local fork of Base at block 52965000 with the current contracts, on the 0.05% WETH/USDC pool, a cap of 50000 returns the pool's liquidity and a cap of 100 returns 0. `.gas` exists only on EVM chains.

## Who receives what a call produces

Every call a program makes comes from the Pot: the Pot is `msg.sender`, the Pot holds the tokens a call spends, and the Pot is the owner of every allowance the program grants. `pot` is the Pot's address on an EVM chain.

* A call that credits its caller leaves the output in the Pot.
* A call that names a beneficiary, such as a lending market's `onBehalfOf`, a vault's `receiver`, a swap's `recipient` or a bridge's `to`, credits that address.

Make the beneficiary a `main` parameter so the caller decides: their wallet, or `pot` to keep the position in the Pot.

```ts supply.sauce.ts theme={null}
// Supplies `amount` of the Pot's USDC to Aave V3 for `onBehalfOf`, who receives the aTokens:
// a wallet, or the Pot itself (`pot`).
function main(onBehalfOf: Address, amount: Uint256) {
  USDC.approve(AaveV3.Pool, amount);
  AaveV3.Pool.supply(USDC, amount, onBehalfOf, 0);
}
```

```ts vault.sauce.ts theme={null}
// Deposits `assets` of an ERC-4626 vault's asset from the Pot; `receiver` gets the shares.
function main(vault: Address, assets: Uint256, receiver: Address): Uint256 {
  const asset = Erc4626.Vault(vault).asset();
  asset.approve(vault, assets);
  return Erc4626.Vault(vault).deposit(assets, receiver);
}
```

On EVM, `ctx.msgSender()` is the Pot's immediate caller. For `cookSigned`, that can be a third-party submitter, not the owner. Pass the intended payment recipient explicitly instead of using `ctx.msgSender()` as proof of ownership. See [Signed cooks](https://docs.eco.com/sauce/pot.md#signed-cooks).

## Low level: ABI imports and raw calls

> [!WARNING]
>
> Everything below is low level. A namespace call is shorter, is typed from an ABI the registry checks against the deployed contract, and uses the chain's own address. Use these forms only where no namespace exists for the contract or the function.

### A registry ABI, imported by path

Every protocol ABI the registry carries is served at `./protocols/<slug>/<Export>.json`. The protocol's page in [Protocols](https://docs.eco.com/sauce-sdk/reference/protocols.md) gives the import line. Bind it to an address with `.at(address)`:

```ts feed.sauce.ts theme={null}
// Reads a Chainlink price feed passed as an argument, and reverts when its answer is not positive
// or older than `maxAge` seconds.
import AggregatorV3ABI from "./protocols/chainlink/AggregatorV3ABI.json";

function main(feed: Address, maxAge: Uint256): Uint256 {
  const round = AggregatorV3ABI.at(feed).latestRoundData();
  require(round.answer > 0, "negative or zero answer");
  require(ctx.timestamp() - round.updatedAt <= maxAge, "stale answer");
  return Uint256(round.answer);
}
```

`answer` is an `int256` in the ABI, so `round.answer` is an `Int256` and `round.answer > 0` is a signed comparison. `Uint256(round.answer)` is a checked conversion: it reverts on a negative value, which the first `require` has already refused. On a local fork of Base at block 52965000 with the current contracts, with Base's ETH/USD feed and a `maxAge` of 86400, it returns 238671514307 (8 decimals: 2386.71514307).

### Your own ABI file

For a contract no registry describes, or a function its registry ABI does not declare, put a JSON ABI beside the program. It should hold only the functions the program calls, with the types from the deployed contract's verified source. The registry's `UniswapV3.Pool` interface has no `observe`, so this file declares it:

```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[]" }
    ]
  }
]
```

Import it by relative path as its default export, and bind it with `.at(address)`, the address a `main` parameter:

```ts twap.sauce.ts theme={null}
// Returns the average tick of a Uniswap V3 pool over the last `window` seconds.
import IOracle from "./IOracle.json";

function main(pool: Address, window: Uint256): Int256 {
  require(UniswapV3.Pool(pool).liquidity() > 0, "the pool has no liquidity");
  const [ticks] = IOracle.at(pool).observe([window, 0]);
  return (ticks[1] - ticks[0]) / Int256(window);
}
```

`ticks` is an `Int256[]`, so the subtraction and the division are signed. `window` is a `Uint256`, and mixing the two in one expression does not compile, so `Int256(window)` converts it. On a local fork of Base at block 52965000 with the current contracts, on the 0.05% WETH/USDC pool with a `window` of 1800, it returns -198543.

The `sauce` command reads the file from beside the program. Through the MCP server, pass it in the tool's `modules` argument. A binding has three call forms:

| Form | Call |
| - | - |
| `IOracle.at(address).method(…)` | a static call for `view` and `pure` methods; otherwise a call with no value |
| `IOracle.view(address).method(…)` | always a static call |
| `IOracle.lib(address).method(…)` | a delegatecall |

* Overloaded methods with the same argument count are not supported.
* A call to an address with no code succeeds with no return data. Reading a declared output of that empty reply reverts the cook; a result nothing reads passes ([Results nothing reads](#results-nothing-reads)).

A failed bound call can be caught with `.catch()`; [Builtins](https://docs.eco.com/sauce/builtins.md) covers it. A method with a narrow integer output, such as `liquidity()`'s `uint128`, can be refused in the statement form, which discards the result: `UniswapV3.Pool(pool).liquidity().catch(() => { ok = 0; });` fails with:

```text theme={null}
a `.catch()` on something other than an external contract call is not supported in SauceScript yet
```

Use the value form and read its result:

```ts catch-value.sauce.ts theme={null}
function main(pool: Address): Uint256 {
  const liquidity = UniswapV3.Pool(pool).liquidity().catch(() => {
    return 0;
  });
  return liquidity;
}
```

A `.catch()` covers the call's failure only. Decoding a successful reply is outside it, so a malformed reply reverts the cook; a multi-statement `try` block covers both.

### The raw ERC-20 ABI

`./artifacts/IERC20.json` is the raw ERC-20 ABI, for a call the token syntax does not offer. Import it under a name of its own:

```ts raw-erc20.sauce.ts theme={null}
// Reads `who`'s balance of `token` through the raw ERC-20 ABI.
import { IERC20 as Erc20 } from "./artifacts/IERC20.json";

function main(token: Address, who: Address): Uint256 {
  return Erc20.at(token).balanceOf(who);
}
```

Its writes are not `SafeERC20`:

* **Result discarded** (`Erc20.at(token).transfer(to, amount);`): the reply is not checked. A token that returns `false` and an address with no code both pass, with nothing moved.
* **Result read** (`const ok = Erc20.at(token).transfer(to, amount);`): the reply must decode as a `bool`. A token that returns nothing, such as USDT, reverts there, and a `false` passes unless the program checks `ok`.

On a local fork of Base at block 52965000 with the current contracts, with an address that has no code as `token`, the discarded transfer succeeds, and the read transfer and `Token(token).transfer(…)` both revert.

Never write a transfer or an approval through it. Use `USDC.transfer(…)`, `Token(token).transfer(…)` or `@sauce/token`'s `transfer(token, to, amount)`. These accept an empty reply or `true` from an address with code, and revert on anything else, as OpenZeppelin's `SafeERC20` does. See [`@sauce/token`](https://docs.eco.com/sauce-libraries/token).

### evm.call, evm.static, abi.encode, abi.decode

The `evm` and `abi` builtins call an address with bytes you build and return the raw reply. They are the last resort. A typed call encodes the arguments, decodes the result and names its fields. Do not build a selector by hand for a function a namespace, an interface or a registry ABI declares.

The one case a binding cannot cover is a call that sends native value:

```ts wrap.sauce.ts theme={null}
// Wraps `amount` wei of the Pot's ETH into WETH by calling WETH's payable deposit() with value,
// and returns the Pot's WETH balance.
function main(amount: Uint256): Uint256 {
  evm.call(WETH, amount, hex`d0e30db0`);
  return pot.WETH;
}
```

| Builtin | Does |
| - | - |
| `evm.call(address, value, data, gas?)` | a call with `value` wei; returns the reply's bytes |
| `evm.static(address, data, gas?)` | a static call; returns the reply's bytes |
| `evm.delegate(address, data, gas?)` | a delegatecall; returns the reply's bytes |
| `abi.encode(v1, …, vN)` | Solidity ABI encoding of the values as one argument list |
| `abi.decode(data, ["Uint256", "Address"])` | a record with unnamed fields, read by position or destructured |

The optional last argument of `evm.call`, `evm.static` and `evm.delegate` is the gas forwarded to the call, as `.gas(n)` is for a bound call. Shapes in `abi.decode` use SauceScript type names (`"Uint256"`, `"Int256"`, `"Address"`, `"bool"`, `"bytes"`, `"string"`, arrays with `[]`), written in place. An explicit `abi.decode` is checked: bytes that do not fit the shape revert the cook. The `evm` and `abi` builtins exist only on EVM chains. On Solana a program calls another program with `svm.call`; see [EVM and Solana](https://docs.eco.com/sauce/targets.md).

## Next steps

* [Names](https://docs.eco.com/sauce/names.md): token symbols, balances and protocol namespaces.
* [@sauce libraries](https://docs.eco.com/sauce-libraries/overview.md): `@sauce/token` and `@sauce/router`.
* [Builtins](https://docs.eco.com/sauce/builtins.md): `evm`, `abi`, `ctx` and `.catch()` in full.
* [Protocols](https://docs.eco.com/sauce-sdk/reference/protocols.md): every namespace, its contracts and its ABI import lines.


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