Skip to main content
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).
  3. Your own JSON ABI beside the program, for a contract or function no registry describes (low level).

Calling by namespace

factory.sauce.ts
  • 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:
    interface-call.sauce.ts
    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:
  • .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

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:
unread.sauce.ts
read.sauce.ts
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 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:
quote.sauce.ts
To read one field, name it on the result:
quote-field.sauce.ts
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:
gas.sauce.ts
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.
supply.sauce.ts
vault.sauce.ts
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.

Low level: ABI imports and raw calls

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 gives the import line. Bind it to an address with .at(address):
feed.sauce.ts
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:
IOracle.json
Import it by relative path as its default export, and bind it with .at(address), the address a main parameter:
twap.sauce.ts
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:
  • 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).
A failed bound call can be caught with .catch(); Builtins 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:
Use the value form and read its result:
catch-value.sauce.ts
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:
raw-erc20.sauce.ts
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.

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:
wrap.sauce.ts
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.

Next steps

  • Names: token symbols, balances and protocol namespaces.
  • @sauce libraries: @sauce/token and @sauce/router.
  • Builtins: evm, abi, ctx and .catch() in full.
  • Protocols: every namespace, its contracts and its ABI import lines.