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

# Security considerations

> What a Sauce program enforces and what it leaves to you. Read it before a program you write moves real funds.

Sauce checks program types, contract-call shapes and arithmetic. A private Pot also checks who is authorized to execute a program. These checks do not establish that the recipients, amounts, approvals or price limits match your intent; you must choose and verify those conditions.

Each section below states a hazard, what it costs, and the code or check that addresses it. Results stated at a block come from `sauce simulate` on a local fork of Base at block 53122981 with the current contracts.

## What a program enforces

Your program enforces the conditions you write, and no others.

* A wrong recipient, an amount in the wrong unit and a missing check all compile, and a simulation runs them as written.
* Every token amount is a `Uint256` in the token's smallest unit. USDC has 6 decimals, so `1000000` is 1 USDC and `1` is one millionth of a USDC.
* Arithmetic is checked: an overflow, an underflow or a division by zero reverts the cook, except in an element an array literal skips. A literal indexed at a literal position runs the element it takes and every element up to and including its last helper call, contract call or token read, in order; it skips the others. `[1 / zero, 7][1]` returns 7 for `zero` 0, `[1 / zero, pot.USDC][1]` reverts, and `[pot.USDC, 1 / zero][0]` returns the Pot's USDC ([Arrays](https://docs.eco.com/sauce/types.md#arrays)). A builtin such as `Math.mulDiv` is not a call: `[Math.mulDiv(1, 1, zero), 7][1]` returns 7.
* Use `require` for a check that must run, and a statement of its own for a call or write that must run.
* A use of a name the chain does not have fails the compile, naming the chain; only a test such as `if (UniswapV3.Factory)` may name it, and the branch it rules out is not compiled. A missing contract never becomes a zero address at run time.
* Take every amount, recipient and limit as a typed `main` parameter, passed with `--args`, never as a constant edited into the file between runs.

## Signed integers

`Int256` is the signed integer type. Negative literals, ABI `intN` outputs and `Math.signExtend` are `Int256`, and `/`, `%`, `>>`, `<`, `>`, `<=` and `>=` are signed on it. A value crossing between `Uint256` and `Int256` is where a sign goes wrong, so the compiler and the engine check every crossing:

* **Conversions are checked.** `Uint256(x)` reverts when `x` is negative, and `Int256(x)` reverts when `x` is 2²⁵⁵ or more. `Uint256(delta)` with `delta` = -5 reverted with no data on the fork.
* **Mixed arithmetic does not compile.** `amount + delta` with a `Uint256` and an `Int256` is refused; convert one operand first.
* **Unsigned negation reverts.** `-amount` on a `Uint256` compiles, and reverts at run time when `amount` is not 0. Write `-Int256(amount)`, which returned -5 for `amount` = 5 on the fork.
* **The signed minimum divided by -1 reverts.** `Int256.fromBits(2 ** 255) / -1` overflows; it does not wrap.
* **`.fromBits(x)` is not a conversion.** `Int256.fromBits` and `Uint256.fromBits` reinterpret the bits and check nothing. Use them only when you mean the bit pattern.
* **Narrow ABI integers are checked.** A narrow argument out of its type's range, and an `intN` or `uintN` output whose word is out of range, revert.

A failed conversion reverts with no data. Check the bound yourself first, so the revert names its cause:

```ts signed-delta.sauce.ts theme={null}
// Applies a signed `delta` to `balance` and returns the result.
//
//   sauce simulate signed-delta.sauce.ts --chain base --owner <address> --args <balance> <delta>
//
//   balance: Uint256   the starting amount, in a token's smallest unit
//   delta: Int256      the change, negative to subtract
function main(balance: Uint256, delta: Int256): Uint256 {
  const next = Int256(balance) + delta;
  require(next >= 0, "balance would go below zero");
  return Uint256(next);
}
```

With `--args 100 -30` it returned 70; with `--args 10 -30` the cook reverted with "balance would go below zero".

A `Uint256` and an `Int256` in one expression, such as `amount + delta`, are refused with "cannot combine Uint256 and Int256 in this expression; convert an operand with `Int256(value)` or `Uint256(value)` first (conversions check bounds)". Convert first, as `signed-delta.sauce.ts` does with `Int256(balance)`.

## Atomic reverts and what they do not undo

A program runs inside one transaction. An unhandled failure, such as a failed `require`, a reverting call or an arithmetic error, reverts the whole cook:

* no token moves;
* the Kitchen's fee is not paid, because it reverts with the rest;
* the gas used up to the failure is spent.

On an EVM chain a `try { … } catch` block or `.catch(() => { … })` on an external call continues after a failure: the failed work is undone, and the handler runs instead. The next section states what each one undoes. See [Errors and reverts](https://docs.eco.com/sauce/errors.md).

A revert undoes only the cook's own transaction. State from earlier transactions stays:

* An allowance your wallet granted earlier, to the Pot or to anyone else, is not revoked when a cook reverts.
* An approval the Pot made in an earlier cook stays in force.
* An approval made in a cook that succeeds outlives the cook. The spender can use what is left of it in any later transaction.

Approve exactly what the next call spends, and check that nothing is left:

```ts supply.sauce.ts theme={null}
// Supplies `amount` of the Pot's USDC to Aave V3. The aTokens are credited to the Pot.
//
//   sauce simulate supply.sauce.ts --chain base --owner <address> --assume-balance USDC:1 --args 1000000
//
//   amount: Uint256   USDC to supply, in its smallest unit (6 decimals): 1000000 is 1 USDC
function main(amount: Uint256) {
  require(pot.USDC >= amount, "not enough USDC");
  USDC.approve(AaveV3.Pool, amount);
  AaveV3.Pool.supply(USDC, amount, pot, 0);
  require(allowance(USDC, pot, AaveV3.Pool) == 0, "allowance left standing");
}
```

`USDC.approve` is a forceApprove. It reads the current allowance and resets a non-zero one to zero before it sets the new amount, so a token that refuses to change one non-zero allowance to another, as USDT does, still works.

## Catch boundaries

A caught failure does not revert the cook. The cook succeeds, the Kitchen's fee is paid, and the program goes on from the handler. What the handler can see, and what was undone, depends on the form.

### A `try` block rolls back all of it

A `try` block of more than one statement commits everything in it on success. On a failure it rolls back the block's calls, storage writes and variable assignments, unlike JavaScript, and then runs the handler. Work before the block remains.

```ts rollback.sauce.ts theme={null}
// Sends `amount` of the Pot's USDC to `to` inside a try block that then requires
// `amount < 100`. Returns 2 when the block stands, or 1000 times `stage` plus the length
// of the error when it fails.
//
//   sauce simulate rollback.sauce.ts --chain base --owner <address> --assume-balance USDC:1 --args <to> <amount>
//
//   to: Address       who receives the USDC
//   amount: Uint256   USDC to send, in its smallest unit (6 decimals)
function main(to: Address, amount: Uint256): Uint256 {
  let stage = 1;
  try {
    stage = 2;
    USDC.transfer(to, amount);
    require(amount < 100, "too much");
  } catch (error) {
    return stage * 1000 + error.length;
  }
  return stage;
}
```

On the fork, `amount` 50 returned 2 and moved 0.00005 USDC. `amount` 200 returned 1008 and moved no USDC: the transfer was undone, `stage` was back to 1, and the error was the 8 bytes of "too much". A handler that reads a variable assigned in the block reads its value from before the block.

### A single call's catch covers only the call

`try { call } catch`, with one external call alone in the block, and `<call>.catch(() => { … })` catch that call's failure only. Evaluating its arguments and decoding its successful return are outside the boundary. A decode that fails, such as a read from an address with no code, reverts the cook. To catch a decode or a check, put it in a block of more than one statement.

### Error bytes are untrusted

`catch (error)` and `.catch((error) => { … })` receive the failure's raw revert bytes, at most the first 256. They can be empty: a bare revert and many arithmetic checks give no data. They can be truncated, and the callee chooses them. Check the length before reading or decoding them. A failure inside the handler is not caught by that handler.

```ts catch-bytes.sauce.ts theme={null}
// Sends `amount` of the Pot's USDC to `to` only if the Pot keeps at least `reserve` after it.
// Returns the first 4 bytes of the failure, or 0 when the transfer stood.
//
//   sauce simulate catch-bytes.sauce.ts --chain base --owner <address> --args <to> <amount> <reserve>
//
//   to: Address        who receives the USDC
//   amount: Uint256    USDC to send, in its smallest unit (6 decimals)
//   reserve: Uint256   USDC the Pot must keep, in its smallest unit
function main(to: Address, amount: Uint256, reserve: Uint256): Uint256 {
  let code = 0;
  try {
    USDC.transfer(to, amount);
    require(pot.USDC >= reserve, "keep the reserve");
  } catch (error) {
    if (error.length >= 4) {
      code = error.slice(0, 4).toBe();
    }
  }
  return code;
}
```

With 1 USDC in the Pot on the fork:

| `amount`, `reserve` | Returned | Why |
| - | - | - |
| `500000`, `200000` | `0x00000000` | the block stood |
| `900000`, `200000` | `0x6b656570` | the program's own `require`: its message as raw bytes, "keep" |
| `2000000`, `0` | `0x08c379a0` | USDC's revert, ABI-encoded `Error(string)` |

A `require` message in a program is its revert data as raw bytes, not ABI-encoded. USDC's revert was ABI-encoded `Error(string)`. Decode by the shape you check, never by assumption.

### No `finally`, and nothing on Solana

There is no `finally` block: "a `finally` block is not supported in SauceScript". On Solana neither `try`/`catch` nor `.catch()` compiles: a program that contains a `try` block fails with "`try/catch` is not available on the svm target". Every failure there reverts the whole cook, so write the conditions as `require` checks before the call that can fail.

## Slippage and price limits

A simulation runs on the chain's state at one block. Prices move before the cook lands, so a swap carries its own bounds, checked inside the program.

* **`minOut` is the floor.** Every `@sauce/router` function takes `minOut`, the least the recipient may receive. Make it a `main` parameter. In a real run never pass `0`, which sets no floor. The library's floor check carries no reason string, so a simulation reports a revert with no data.
* **Below `minOut`, a plain function reverts the whole cook, and a `try` form may skip instead.** A `try` form returns `0` with nothing moved when the shortfall shows before anything moves: a quote below `minOut` (`tryUniswapV2`, `tryTraderJoeLB`, `tryDodoV2`, `tryWoofi`), or a pool that refuses the floor (`tryCurve`, `tryBalancerV2`). It also returns `0` for a pool, pair or Vault with no code. A `try` swap through the Router that ran and delivered less still reverts ([The try forms](https://docs.eco.com/sauce-libraries/router.md#the-try-forms)).
* **`minOut` is checked against the amount the venue reports, not the recipient's balance.** The plain functions compare it with the output the pool, the Vault or the Router reports. `curve` and `tryCurve` pass it to the pool's `exchange` as its own minimum, and measure the Pot's balance change only to return and forward what arrived, with no check of that change against `minOut`. A token that takes a fee on transfer then delivers less than `minOut` to the recipient. Only the `try` forms through the Router (`tryUniswapV3`, `tryUniswapV4`, `tryMaverickV2`, `tryInfinityCL`, `tryInfinityBin`) measure what the recipient's balance gained. Where the output token may take a fee, read the recipient's balance before and after the swap and `require` the difference.
* **`sqrtPriceLimitX96` bounds the pool's price after the swap, not the price you pay.** The pool's fee comes on top: under one limit, the last unit a 1% pool fills costs about 1% more than the limit. `sauce price` turns a price into a limit; see [the `sauce price` command](https://docs.eco.com/sauce-sdk/cli/price).
* **A limit of `0` means no limit.** `minOut` is then the only bound.
* **A limit on the wrong side of the pool's price fills nothing.** The pool reverts with `SPL`.
* **A price limit can leave input unspent.** A pool given more than it fills before its limit takes only what it fills, and the rest of the input stays in the Pot. Every swap leg sends that rest back.
* **Venues paid first take the whole input or revert.** Uniswap V2 and its forks, Curve, Balancer V2, DODO V2, Trader Joe LB and WOOFi refund nothing.

This is the guard for one Uniswap V3 leg:

```ts guarded-swap.sauce.ts theme={null}
// Sells up to `amountIn` of the Pot's WETH for USDC on the chain's Uniswap V3 pool at `fee`,
// never past `sqrtPriceLimitX96`, and reverts below `minOut`. The WETH the pool did not take
// goes to `recipient` with the USDC.
//
//   sauce run guarded-swap.sauce.ts --chain base --wallet <name> \
//     --args <amountIn> <fee> <sqrtPriceLimitX96> <minOut> <recipient>
//
//   amountIn: Uint256           WETH to sell, in its smallest unit (18 decimals)
//   fee: Uint256                the pool's fee tier in millionths: 500 is 0.05%
//   sqrtPriceLimitX96: Uint256  the pool price past which nothing more is sold (sauce price)
//   minOut: Uint256             the least USDC to accept, in its smallest unit (6 decimals)
//   recipient: Address          who receives the USDC and the unsold WETH
import { uniswapV3 } from "@sauce/router";

function main(
  amountIn: Uint256,
  fee: Uint256,
  sqrtPriceLimitX96: Uint256,
  minOut: Uint256,
  recipient: Address,
): Uint256 {
  const pool = UniswapV3.Factory.getPool(WETH, USDC, fee);
  require(pool != 0, "no pool at that fee tier");
  const before = pot.WETH;
  require(before >= amountIn, "amountIn is not in the Pot");

  const got = uniswapV3(pool, WETH, USDC, amountIn, sqrtPriceLimitX96, minOut, recipient);

  const unspent = amountIn - (before - pot.WETH);
  if (unspent > 0) {
    WETH.transfer(recipient, unspent);
  }
  return got;
}
```

The refund is `amountIn` less what the swap took, measured as the change in the Pot's balance. It is never the Pot's whole balance, which can hold more of the token for other uses.

On the fork the 0.05% pool's price was 2,331.62 USDC per WETH. Selling 2,000 WETH with the limit at 2,328 USDC per WETH filled 14.28 WETH and returned the rest (abridged):

```bash theme={null}
sauce price --chain base WETH USDC 2328        # the sell block's sqrtPriceLimitX96
sauce simulate guarded-swap.sauce.ts --chain base --owner <address> --assume-balance WETH:2000 \
  --args 2000000000000000000000 500 3822707527392096055986861 1000000000 <recipient>
```

```text theme={null}
The cook succeeds from 0x1111…1111: gas used 254355 for the cook, after the Pot's deploy (1. below), returns 0x…07bd5f5cd9
main returns Uint256: 33241914585
What moved, in order:
  [1] wallet 0x1111…1111 -> Pot 0x6c91…D3Fe      0.00000374478538635 ETH       the cook's value
  [2] Pot                -> Kitchen 0x1984…506b  0.00000374478538635 ETH       the Kitchen's fee
  [3] 0xd0b5…F224        -> 0x2222…2222          33241.914585 USDC
  [4] Pot                -> 0xd0b5…F224          14.275205622616013512 WETH
  [5] Pot                -> 0x2222…2222          1985.724794377383986488 WETH
```

## Pot ownership and authority

The table and the points after it describe EVM Pots. Solana has a different trust boundary: the Kitchen is an upgradeable program that signs for every Pot, so its upgrade authority can replace the program that decides who cooks on a Pot, and must be trusted with what every Pot holds. The engine having no upgrade authority does not change that ([Trust on Solana](https://docs.eco.com/sauce/kitchen.md#trust-on-solana)).

| Who | Can | Cannot |
| - | - | - |
| The Pot's owner | Cook any program on the Pot; on a Private Pot, sign a cook for another account to submit, and cancel such signatures; point a Pot whose Router is not fixed at another Router | Hand the Pot to another owner |
| Anyone else | Deploy a Pot for any owner; send a Pot tokens; submit a cook the owner signed | Cook on the Pot without the owner's signature, or move what it holds |
| The Kitchen's owner | Set the Kitchen fee, with no cap: a fee no cook can pay, or an upgrade whose fee collection reverts, stops every Pot's cooks, and a Pot has no withdraw function, so what Pots hold stays in them until cooks run again; set the Router new Pots start on; withdraw collected fees; transfer the Kitchen's ownership; upgrade the Kitchen | Change an existing Pot's code, owner or Router, or move what it holds |

* **The owner is fixed when the Pot is created.** A `cook` from any other address reverts with `NotOwner()`.
* **Deploying a Pot or funding it grants nothing.** The Pot belongs to its owner whoever sends `deployPot`, and tokens sent to it can leave only through a cook its owner sends or signs. A Pot has no withdraw function.
* **An EVM Pot's code never changes.** Nothing upgrades a deployed Pot.
* **The engine runs only inside a Pot.** On an EVM chain the engine runs only inside a Pot its Kitchen created; called directly, it refuses.
* **The fee can change between building and landing.** A raised fee fails the cook only if it exceeds the cook's value, or for a signed cook the smaller of the Pot's balance and the signed ceiling. The cook then reverts with `FeeNotPaid`, and nothing moves.

### The Pot's Router

When a pool calls back during a swap, the call reaches the Pot, and the Pot's fallback delegatecalls its Router. The Router therefore runs as the Pot, with the Pot's balances. `router()` on the Pot returns it.

* A new Pot starts on the Kitchen's current Router.
* `setRouter(address)` on the Pot changes it. Only the owner can call it; anyone else gets `NotOwner()`.
* A Pot created with its Router fixed refuses `setRouter` with `RouterFixed()`.
* Point a Pot only at the Router the Kitchen names (`router()` on the Kitchen). Any contract set as the Router runs with the Pot's balances.

### Shared Pots

A Shared Pot has no owner. Anyone can cook on it and spend what it holds, and its Router is always fixed. Fund a Shared Pot and empty it within the same transaction. See [Pot](https://docs.eco.com/sauce/pot.md).

## Signed cooks

A Pot created by the current Kitchen runs a cook its owner signed and another account submits, with `cookSigned`; the owner cancels signatures with `invalidateNonces`. [Pot](https://docs.eco.com/sauce/pot.md#signed-cooks) describes what is signed and what the Pot checks, and [`sauce sign`](https://docs.eco.com/sauce-sdk/cli/sign) signs one. A signed cook is a cheque: whoever holds it can cash it, once, until it expires.

```solidity theme={null}
function cookSigned(address engine, bytes program, uint256 maxExecutionFee, uint256 nonce, uint256 deadline, address submitter, bytes signature) payable returns (bytes)
function invalidateNonces(uint256 word, uint256 mask)
```

* **Do not build the signature by hand.** `sauce sign`, and `signedCookTypedData`, `encodeCookSigned` and `verifySignedCook` from `@eco-incorp/sauce/evm/engine`, build the EIP-712 data and the call; `checkSignedCookPot` refuses a Shared Pot, or a Pot the Kitchen did not create, before you sign.
* **A signature carries the owner's full authority.** The program it covers runs as the Pot and can move everything the Pot holds, as a cook the owner sends can. Sign only a program you compiled and simulated yourself, with the arguments it will run with.
* **A signature is public once it is sent.** It is in the transaction's calldata. A cook that reverts spends nothing, so anyone who holds the signature can send it again until its deadline. Hand it only to the account that should send it.
* **Name the submitter when one account should send it.** With no submitter, anyone holding the signature may send it, before the account you meant and at a moment of their choosing up to the deadline. The submitter is the Pot's immediate caller: through a bundler or a forwarding contract, it is that contract.
* **Sign short deadlines.** The deadline is the last block timestamp at which the cook runs, inclusive. An expired signature is worth nothing to anyone who copied it.
* **Cancel what you no longer want.** `sauce sign --cancel <nonce>` sends `invalidateNonces(word, mask)`, and no signature for that nonce runs after it, unless a later cook clears the bit. A cancellation that lands after the cook has run changes nothing.
* **A later cook can undo a cancellation, and move the Router.** The nonce bitmap is ordinary Pot storage, and a program writes the Pot's storage with `evm.sstore`. On a local fork of Base at block 52991146 with the current contracts, a cook of `evm.sstore(slot, 0)` at a cancelled nonce's word (`cookNonceSlot(word)`) cleared it, and the cancelled signature's cook then ran. On a Pot whose Router is not fixed, the Router slot is writable the same way: a cook that stored another address there changed what `router()` returns, which is where the Pot's fallback goes, without `setRouter`'s checks. A Pot deployed with `deployPot(owner, salt, true)` keeps its Router in its code, and the same write changed nothing there (on a local fork of Base at block 52997615 with the current contracts). Cook only programs you have read, and treat an `evm.sstore` whose slot comes from an argument as a write to any of the Pot's storage.
* **Sign a fee ceiling you are willing to pay.** The Kitchen's owner may raise the fee after you sign. The Pot pays at most the signed `maxExecutionFee`, from its own balance; above it, or above what the Pot holds, the cook reverts with `FeeNotPaid` and can be sent again once the fee is back under it.
* **A wallet's spending limits refuse to sign one.** What a signed cook moves is decided when someone else sends it, on the chain's state then, so a simulation at signing does not measure it. A wallet whose policy sets a `native` or `tokens` limit refuses `sauce sign`; `chains` and `to` still apply ([Wallets and spending policy](https://docs.eco.com/sauce-sdk/cli/wallets-and-policy.md)).
* **The Pot's errors for a refused signed cook** are `BadSignature()`, `Expired()`, `NonceUsed()`, `WrongSubmitter()` and `FeeNotPaid`. Its events are `SignedCook` and `NoncesInvalidated`.
* **A Shared Pot has no owner** and refuses every signed cook.

## Pots and engine pairing

A Pot runs only with the engines its Kitchen pairs with, and an engine refuses a Pot its paired Kitchen did not create:

* **A Pot's address follows from the Kitchen, the owner and the salt.** The same owner and salt give a different Pot under a different Kitchen.
* **Manage a Pot with the SDK, compiler and engine that pair with its Kitchen.** `sauce pot --chain <chain> --owner <address>` prints the Pot the installed SDK uses; deploy it (`sauce pot --chain <chain> --deploy` and your signer) before you move anything to it, then review that destination and simulate the move.
* **Recompile and rebuild.** A compiler's output runs only on the engines its release set records. Recompile every stored program, and rebuild every unsigned transaction you saved, with the installed SDK.

## Authorization: `ctx.msgSender()` and `ctx.txOrigin()`

| Builtin | On an EVM chain |
| - | - |
| `ctx.msgSender()` | The immediate caller: the owner for a `cook` on a private Pot, the submitter for a `cookSigned`. |
| `ctx.txOrigin()` | The account that signed the transaction. |

The two differ whenever a contract stands between the signer and the Pot. Authorize on `ctx.msgSender()`, never on `ctx.txOrigin()`: a check of the origin passes for any contract the signer calls. `ctx.msgSender()` is still not proof of who owns the Pot, because a signed cook's submitter can be a third party. A program that pays the owner takes the recipient as a `main` parameter. On Solana `ctx.txOrigin()` does not compile, and `ctx.msgSender()` is the first signer among the program's accounts. See [Builtins](https://docs.eco.com/sauce/builtins.md).

## Where results end

What a call credits to its caller is credited to the Pot. A swap with no other recipient, Aave's aTokens supplied for `pot` and a vault's shares minted to the Pot all stay in the Pot. They belong to the owner, a wallet app does not show them, and only another cook moves them out.

To deliver a result, name the wallet as the call's beneficiary (`onBehalfOf`, `receiver`, `recipient`, `to`) with a `main` parameter, or transfer it in the same program:

```ts deliver.sauce.ts theme={null}
// Supplies `amount` of the Pot's USDC to Aave V3. `onBehalfOf` receives the aTokens.
//
//   sauce run deliver.sauce.ts --chain base --wallet <name> --args <amount> <onBehalfOf>
//
//   amount: Uint256        USDC to supply, in its smallest unit (6 decimals): 1000000 is 1 USDC
//   onBehalfOf: Address    who receives the aTokens: the owner's wallet, or the Pot
function main(amount: Uint256, onBehalfOf: Address) {
  USDC.approve(AaveV3.Pool, amount);
  AaveV3.Pool.supply(USDC, amount, onBehalfOf, 0);
}
```

`sauce simulate` lists what a cook leaves in the Pot under "Left in the Pot". It counts tokens only: a position a protocol keeps in its own books, with no token for it, shows in no balance.

## Tokens with unusual behaviour

* **No-return tokens.** Every token write a program makes through the token syntax follows OpenZeppelin's SafeERC20 rules: `USDC.transfer`, `.approve`, `.transferFrom`, the same methods on `Token(address)`, the `@sauce/token` functions and the swaps of `@sauce/router`. A token that returns nothing, as USDT does, works. A `false` return, a malformed return or an address with no code reverts the cook. See [Names](https://docs.eco.com/sauce/names.md).
* **Raw ABI writes are not checked.** A write through a raw ERC-20 ABI binding is an ordinary bound call. Read, its declared `bool` is decoded: USDT's empty return reverts there, and a `false` return passes. Discarded, nothing is decoded or checked. Never move tokens that way. See [Contracts and ABIs](https://docs.eco.com/sauce/contracts-and-abis.md).
* **Fee-on-transfer and rebasing tokens.** A token that charges a transfer fee credits the recipient less than was sent. A rebasing token's balance changes between transactions, and a transfer of one can credit one unit less than sent. Measure what arrived as a balance change, and pass that on, not the nominal amount.
* **Issuer controls.** An issuer can pause its token and refuse a sanctioned or blocked address. On Solana a stock mint's issuer can hold a permanent delegate that can move or burn any balance. A simulation shows the token's state at its block, not later.

```ts arrived.sauce.ts theme={null}
// Pulls `amount` of `token` from `from` into the Pot, then forwards what arrived to `to`.
// `from` must have approved the Pot for `amount` beforehand.
//
//   sauce run arrived.sauce.ts --chain base --wallet <name> --args <token> <from> <amount> <to>
//
//   token: Address    the token, by address
//   from: Address     who pays
//   amount: Uint256   the amount pulled, in the token's smallest unit
//   to: Address       who receives what arrived
function main(token: Address, from: Address, amount: Uint256, to: Address): Uint256 {
  const t = Token(token);
  const before = t.balanceOf(pot);
  t.transferFrom(from, pot, amount);
  const arrived = t.balanceOf(pot) - before;
  t.transfer(to, arrived);
  return arrived;
}
```

## Unread call outputs

On an EVM chain, a bound call whose result nothing reads, discarded or kept in a variable never used, skips all decoding and output checks. An empty or malformed successful reply then passes, and so does a call to an address with no code, which succeeds with empty data. The program goes on as if the call had done its work.

```ts unread-output.sauce.ts theme={null}
// Calls liquidity() on `pool` and discards the result.
//
//   sauce simulate unread-output.sauce.ts --chain base --owner <address> --args <pool>
//
//   pool: Address   the address to call
function main(pool: Address): Uint256 {
  UniswapV3.Pool(pool).liquidity();
  return 1;
}
```

On the fork, with an address that has no code, this cook succeeded and returned 1. `return UniswapV3.Pool(pool).liquidity();` with the same address reverted with no data, because the empty reply failed to decode.

* **Read the result you depend on.** Reading any part of a result decodes the whole declared result and checks it. `abi.decode` is always checked.
* **Check for code where it matters.** `ctx.isContract(address)` or `ctx.codeSize(address) > 0` before a call whose target must be a contract.
* **Move tokens with the checked writes.** `USDC.transfer`, `Token(address).transfer` and the `@sauce/token` functions check the reply whether or not the program reads anything. A discarded raw ERC-20 write checks nothing.

```ts checked-read.sauce.ts theme={null}
// Returns the in-range liquidity of the Uniswap V3 pool at `pool`. Reverts when `pool` has no
// code or the pool has no liquidity in range.
//
//   sauce simulate checked-read.sauce.ts --chain base --owner <address> --args <pool>
//
//   pool: Address   a Uniswap V3 pool
function main(pool: Address): Uint256 {
  require(ctx.isContract(pool), "no contract at the pool address");
  const liquidity = UniswapV3.Pool(pool).liquidity();
  require(liquidity > 0, "no liquidity in range");
  return liquidity;
}
```

On the fork it reverted with "no contract at the pool address" for an address with no code, and returned 890150520006439031 for the WETH/USDC 0.05% pool.

## Calls to addresses with no code

`.catch` does not help with an address that has no code. The call succeeds, so there is nothing to catch, and a read result's decoding is outside the single call's catch: `UniswapV3.Pool(pool).liquidity().catch(() => { return 0; })` with an address that has no code reverted the cook with no data on the fork. Check `ctx.codeSize(address) > 0` before calling an address the program is given:

```ts codesize.sauce.ts theme={null}
// Returns the in-range liquidity of the Uniswap V3 pool at `pool`, or 0 when the read fails.
//
//   sauce simulate codesize.sauce.ts --chain base --owner <address> --args <pool>
//
//   pool: Address   a Uniswap V3 pool
function main(pool: Address): Uint256 {
  require(ctx.codeSize(pool) > 0, "no contract at the pool address");
  return UniswapV3.Pool(pool)
    .liquidity()
    .catch(() => {
      return 0;
    });
}
```

Here `.catch` is the value form: the constant replaces the result when the call fails. The statement form, a handler that assigns instead of returning, can be refused for a method with a narrow integer output, such as `liquidity()`, a `uint128`. `UniswapV3.Pool(pool).liquidity().catch(() => { liquid = 0; });` fails with "a `.catch()` on something other than an external contract call is not supported in SauceScript yet". Use the value form for those, as `codesize.sauce.ts` does, or a `try` block.

`ctx.codeSize` and `ctx.isContract` do not compile for Solana, where every account a program touches is listed before it runs.

## Names and recorded addresses

* **A registry record is not proof of code.** The registry records where a contract is deployed; that is not proof that code is at the address today. `sauce pot` reads whether the released engine and Kitchen are on a chain. Simulate the program to see a missing contract fail.
* **A chain-qualified name is an address, not a contract to call.** In a program compiled for Ethereum, `Base.UniswapV3.Factory` is the address of Base's factory. Calling it calls that address on Ethereum, and the compiler does not stop the call.
* **An interface calls whatever is at the address.** `UniswapV3.Pool(pool)` calls the contract at `pool`. Where a program pays the contract it calls, take its address from the protocol, as `UniswapV3.Factory.getPool` does in the guard above, rather than from an input.

## On Solana

* **Accounts are checked by the program.** A Solana transaction lists every account it uses. Someone else can build a transaction that puts a different account in a slot, so the compiled program checks each account against what it must be: a token account owned by the named holder, an address derived from its seeds, the expected program. A wrong account fails the whole cook with a reason such as `wrong holder account`.
* **Token-2022 transfer hooks are refused.** `@sauce/token` moves Token-2022 tokens with the checked transfer, and refuses a mint whose transfer hook names a program before anything moves. Transfer fees, pauses and frozen accounts are applied by the token program; measure what arrived.
* **Running out of compute fails the whole cook.** A transaction has a budget of 1,400,000 compute units. A cook that exceeds it fails, every swap in it with it. Swaps at a price limit on concentrated-liquidity pools spend the most: each pool's program charges for every tick the swap crosses. `sauce simulate` and `sauce run` stop such swaps short of the budget and check those stops in a simulation; `sauce tx` simulates nothing, so the cook it builds can differ.
* **The fee is capped by the cook.** A Solana cook carries the most it will pay as the execution fee, and a higher fee refuses the cook before anything runs.
* **Rebuild instructions with the installed SDK.** The engine's program id and the Kitchen's instruction format belong to a release set. Build cooks with the installed SDK; never replay a serialized cook built by another one.
* **`ctx.self()` is not the Pot.** On Solana `ctx.self()` is the engine's program id. `pot` is the Pot, and `pot.USDC` reads its balance. See [EVM and Solana](https://docs.eco.com/sauce/targets.md).

## Agents, wallets and keys

A key that the `sauce` command or the MCP server signs with is protected by your account's file permissions and by the password's source, and by nothing else on the machine.

### Where a key is

* A wallet made with `sauce wallet new` is a hot wallet: one file, `~/.sauce/wallets/<name>.json`, mode 0600 in a 0700 directory, holding the key encrypted under a password. An EVM wallet's file is a standard keystore, so `--keystore`, `cast wallet` and geth open it too. Whoever can read the file and obtain the password holds the key. Keep small balances in it.
* The password comes from the source a command names: an environment variable (`--password-env`), a file (`--password-file`, refused when its group or others can read or write it), a command's output (`--password-command`), or the terminal prompt.
* `sauce tx` prints a cook unsigned, for a hardware wallet or a key service to sign, so the key never reaches this machine. `sauce inspect` takes a cook's data apart against your program file.

### What an agent running as you can do

A coding agent's shell commands, and the MCP server it starts over stdio, run under your user account. Whatever your account can read, so can they. Such an agent can:

* **Read the key.** The wallet files and every password source your account reads (a password file, a `.env` file, an environment variable, what a secret manager's command prints) are, together, the key.
* **Sign without the policy.** The spending policy applies to `--wallet` and to the MCP server's wallet tools. `--keystore` with the same wallet file and password, `--private-key`, `cast wallet` and geth apply none.
* **Change the policy or the ledger.** `<name>.policy.json` and `<name>.ledger.jsonl` are your files. A loosened policy lifts its limits, and a deleted ledger counts nothing spent in the last 24 hours.
* **Read an exported key.** `sauce wallet export` refuses a process with no controlling terminal. A program that runs it in a pseudo-terminal answers its prompt and reads the key.
* **Enable the MCP server's wallets.** `sauce mcp` over stdio with no flags, the entry `sauce mcp setup` writes, serves the wallets in `~/.sauce/wallets`. When it was started without a password source, `sauce_wallet_enable` enables them from a source the agent names: a variable of the server's environment, a password file, a `.env` file, or a new password the server generates into a new file. `sauce_wallet_send` and `sauce_sign` then sign. The server reads any variable an agent names this way, and uses its value only as the password.

No MCP tool takes or returns a key or a password, and every command scrubs the keys and passwords it was given from what it prints. Those rules keep a key out of a tool's transcript; they do not keep it from an agent that reads your files.

### What the spending policy limits

* A send through `--wallet` or `sauce_wallet_send` is simulated before it is signed, and the policy is checked against what the simulation moved. On EVM chains the transaction is measured whole, with the gas and fees it is then signed with, and signed exactly as measured. Nothing on chain enforces the limits: the simulation shows the chain's head a moment before the send, not the block the transaction lands in. A program that reads that block (its time, its base fee, what another transaction changed first) may move something else there. A bound that must hold in any block is the program's own, such as a `require` on what it sends.
* `to` limits the address a transaction is sent to, and nothing else. A cook is a transaction to the Pot, so a policy that allows the Pot allows whatever its program pays out; `native` and `tokens` count what leaves the wallet and the Pot.
* An allowance a transaction leaves another key counts as sent, at the whole allowance, with what the transaction moves of the token: on EVM chains an ERC-20's allowance or one granted through Permit2, read as the chain holds it after the transaction, and on Solana a delegate set or raised. An allowance of a limited token to anyone but the wallet or its own Pot counts only where it is measured exactly, and is refused where it is not. A cook on a Pot whose owner cannot be read is refused where a `native` or `tokens` limit applies.
* An approval of Permit2 for a limited token is refused: it makes spendable again every Permit2 allowance of that token given before, which the transaction does not report. On a local fork of Base at block 53011315 with the current contracts, a wallet limited to 1 USDC ran a cook that approves Permit2 for the Pot's USDC and gives another account a 10 USDC Permit2 allowance. `sauce run` refused it, naming `tokens.USDC`, and sent nothing. Under such a limit, fund with `--fund-mode permit`, `allowance` or `transfer`.
* ERC-721 and ERC-1155 transfers and approvals, `setApprovalForAll` included, are not limited. Nor is a Token-2022 mint's permanent delegate that the wallet never held: it moves tokens with no approval from the wallet.
* On Windows the command does not check file permissions: a password file, a policy file or the wallet directory that others can read or write is not refused.

[Wallets, passwords and spending policy](https://docs.eco.com/sauce-sdk/cli/wallets-and-policy.md) documents the file formats and each limit.

### Keeping a key from an agent

* Only an account that cannot read the wallet directory and its password source is kept from the key.
* `sauce mcp --no-wallets` serves no wallet tools, and the server over HTTP never signs and has none.
* To keep a key off the agent's machine, sign elsewhere: hand `sauce tx`'s unsigned transactions to your own wallet, or let a wallet's own MCP server sign them ([Sign with a wallet's MCP server](https://docs.eco.com/sauce-sdk/mcp/external-wallets.md)).

## Simulate before you send

`sauce simulate` runs the cook from the Pot's owner on the chain's current state, or at `--block <number>`, and signs nothing. Simulate the file you will send, with the arguments you will send, from the owner who will send it. Read three things in the output: what leaves the wallet, what arrives where, and what stays in the Pot.

The supply program above, with 1 USDC assumed in the Pot (abridged):

```bash theme={null}
sauce simulate supply.sauce.ts --chain base --owner <address> --assume-balance USDC:1 --args 1000000
```

```text theme={null}
The cook succeeds from 0x1111…1111: gas used 270015 for the cook, after the Pot's deploy (1. below), returns 0x
Left in the Pot, not in the owner's wallet:
  0.999999 aBasUSDC
What moved, in order:
  [1] wallet 0x1111…1111 -> Pot 0x6c91…D3Fe       0.00000374478538635 ETH  the cook's value
  [2] Pot                -> Kitchen 0x1984…506b   0.00000374478538635 ETH  the Kitchen's fee
  [3] Pot                -> aBasUSDC 0x4e65…c0AB  1 USDC
  [4] mint               -> Pot                   0.999999 aBasUSDC
Approvals:
  0x6c91…D3Fe (the Pot) lets 0xA238…d1c5 (AaveV3.Pool) spend 1 USDC
Net balance changes:
  0x6c91…D3Fe (the Pot): -1 USDC, +0.999999 aBasUSDC
  0x1111…1111 (owner): -0.00000374478538635 ETH
```

* The first line gives the result, the gas and `main`'s return value, or the reason the cook reverts.
* "Left in the Pot" lists each token the cook leaves in the Pot. Here the aTokens stay there.
* "What moved, in order" lists every token transfer and native transfer, the Kitchen's fee included.
* "Approvals" lists every approval the cook emits. The `allowance` check in the program shows that none was left standing.
* The full report needs an endpoint that serves `eth_simulateV1`. Without it the command falls back to `eth_call`, which gives the result and the gas but no transfers.

`--expect-revert <text>` turns a simulation into a test of a guard: it exits 0 only when the cook reverts with a reason containing the text. The guard program above with a `minOut` of 3,000 USDC for 1 WETH:

```bash theme={null}
sauce simulate guarded-swap.sauce.ts --chain base --owner <address> --assume-balance WETH:1 \
  --args 1000000000000000000 500 3822707527392096055986861 3000000000 <recipient> \
  --expect-revert "no data"
```

```text theme={null}
As expected, the cook reverts: it reverted with no data (out of gas, or a revert with no reason)
```

The text is "no data" because the library's floor check reverts without a reason string. With a `minOut` of 2,300 USDC the same cook succeeded and returned 2330328573, 2,330.33 USDC, so the revert is the floor. A `require` you write with a message, such as `"amountIn is not in the Pot"`, is matched by its message.

## Next steps

* [Errors and reverts](https://docs.eco.com/sauce/errors.md): `require`, `throw`, `try` and `.catch`.
* [Types](https://docs.eco.com/sauce/types.md): `Uint256`, `Int256` and their conversions.
* [Pot](https://docs.eco.com/sauce/pot.md) and [Kitchen](https://docs.eco.com/sauce/kitchen.md): ownership, the Router and the fee.
* [The `sauce simulate` command](https://docs.eco.com/sauce-sdk/cli/simulate): every flag.
* [Glossary](https://docs.eco.com/sauce/glossary.md): the terms on this page.


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