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:
- A protocol namespace, for a contract the chain’s registry records (this page’s first half).
- A registry ABI imported by path, for a contract the registry describes but the chain has no namespace for (low level).
- 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 itspool: addressoutput is decoded.sauce globals --chain base --protocol UniswapV3prints every method of the namespace with its types. -
A contract a call returns is called directly where the registry records what it is.
getPoolreturns a Uniswap V3 pool, sopool.slot0()is a pool call, andpool.token0()returns a token, so.decimals()is an ERC-20 call. A token reached this way takes the token methods; itstransfer,approveandtransferFromare the checked ones thatToken(address)uses. Keep such a result in aconst, not alet. -
An address the program is given is called through the interface that describes it, with the address in the same expression:
A method called on a bareinterface-call.sauce.ts
Addressparameter, such aspool.liquidity()withpool: Address, does not compile, because the compiler does not know which ABI to use; writeUniswapV3.Pool(pool).liquidity(), as above. Its message: -
.at(address)reaches another deployment of a named contract’s ABI:AaveV3.Pool.at(pool).supply(…)callssupplyatpoolwith 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
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.decodeis always checked.- Where the target must be a contract, check
ctx.isContract(target)first.
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
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 isevm.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
.gas exists only on EVM chains.
Who receives what a call produces
Every call a program makes comes from the Pot: the Pot ismsg.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’sreceiver, a swap’srecipientor a bridge’sto, credits that address.
main parameter so the caller decides: their wallet, or pot to keep the position in the Pot.
supply.sauce.ts
vault.sauce.ts
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
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’sUniswapV3.Pool interface has no observe, so this file declares it:
IOracle.json
.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).
.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:
catch-value.sauce.ts
.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
SafeERC20:
- Result discarded (
Erc20.at(token).transfer(to, amount);): the reply is not checked. A token that returnsfalseand 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 abool. A token that returns nothing, such as USDT, reverts there, and afalsepasses unless the program checksok.
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
Theevm 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/tokenand@sauce/router. - Builtins:
evm,abi,ctxand.catch()in full. - Protocols: every namespace, its contracts and its ABI import lines.
