Skip to main content
A builtin is a name the compiler provides: a statement such as require, a namespace such as ctx or Math, a method on a value, or a property of an account. This page lists each one with its signature, the value it returns, the targets it compiles for, and the message the compiler prints on a target that lacks it. It also lists the names the sauce command and routes.compileSauceProgram add before compiling. Those are not builtins: the compiler alone does not know them (Compiler or SDK).

Compiler or SDK

The sauce command, the MCP server, actions documents and routes.compileSauceProgram add names to a program before the compiler sees it: the chain’s tokens, protocols and chain id, the token functions and the libraries. routes.compileSauceRoute adds neither the chain id nor the token functions (Compiling programs). The published compiler, @eco-incorp/sauce-compiler, provides only the language. Each row below was compiled both ways: with the raw compile from @eco-incorp/sauce/compiler (the published compiler, re-exported, given no defines and no ambient, and a resolver that serves only the program), and with sauce compile --chain base or --chain solana. A host that compiles with the compiler alone supplies these itself: Compiling programs covers defines, ambient and the SDK’s resolvers.

How to read the tables

  • Targets. EVM is every EVM chain, Solana is Solana. An entry marked EVM or Solana alone fails to compile for the other target.
  • Types. A builtin takes and returns the types of Types. scalar is a number or a bool; bytes is a byte string. An integer is a 256-bit Uint256 (unsigned, the default) or Int256 (signed); Integer conversions moves a value between the two.
  • Refusals. Compiling an entry for a target that lacks it fails with a message of the form “ctx.txOrigin is not available on the svm target”, naming the entry and the target (svm or evm), and often followed by what to write instead. The tables do not repeat that message per row; Target refusals quotes real ones.
  • Examples. An example named *.solana.sauce.ts compiles for Solana only. An example named *-evm.sauce.ts, or one that uses what the tables mark EVM only (an entry, or log with topics), compiles for Base and Ethereum only: ambient-token-evm.sauce.ts uses the token functions’ EVM signatures, which take a token address where Solana’s take accounts. Every other example compiles for Base, Ethereum and Solana. bindings.sauce.ts and catch.sauce.ts import Counter.json, the ABI shown above bindings.sauce.ts, and compile only with it in the same directory. An address is a main parameter. A contract is normally called through its namespace (AaveV3.Pool.supply(...)); the raw evm.call and evm.static entries below are the low-level form, and Contracts and ABIs covers the normal spelling.
  • Results. A result stated for an example comes from sauce simulate on a local fork of Base at block 52965000 with the current contracts.

Statements

These are available on both targets. require, revert, throw and log are statements; eval returns a value. Their names are reserved: a function cannot be named require, revert, log or eval.
  • require, revert and throw revert the whole transaction, and so do a failing call and every checked operation: nothing that ran before them survives. On EVM, a try/catch block (below) or a call’s .catch() confines the rollback to its block. See Errors and reverts.
  • require is a statement: const ok = require(x) is rejected (“require aborts execution and produces no value — use it as a statement”).
  • On Solana log takes data alone. A program that needs an event discriminator concatenates it in front of the data.
  • eval runs the inner program read-only: it can read state and return a value, and a write in it reverts the whole cook. If the inner program reverts, so does the outer one. On EVM, inside the inner program ctx.msgSender() is the engine and ctx.self() is still the Pot; a storage write or a token transfer there reverts. On Solana the inner program can read accounts but cannot write them, and it keeps the transaction’s signer flags, so an account that signed the transaction is still a signer in a call the inner program makes.
guards.sauce.ts
eval.sauce.ts
inner runs read-only, whoever compiled it. On a local fork of Base at block 52992458 with the current contracts, an inner program that returned abi.encode(ctx.msgSender(), ctx.self()) returned the engine’s address and the Pot’s, where the outer program’s own ctx.msgSender() was the owner; an inner evm.sstore and an inner USDC.transfer each reverted the cook with no data. log topics exist on EVM only. A topic is a scalar, so an event signature is hashed first:
log-topics.sauce.ts

try/catch

On EVM, try { … } catch (error) { … } is a statement. A block of several statements is atomic: if any of it fails, its calls, storage writes and assignments are all undone, and then the handler runs. Unlike JavaScript, a variable assigned inside the block has its old value again in the handler. Work before the block stays. error is the revert data as raw bytes, at most the first 256 of them, and may be empty; check its length before decoding it. catch { … } without a name discards it. There is no finally (“a finally block is not supported in SauceScript”), and Solana has no try/catch. A block holding a single external call is that call’s .catch(), with its narrower boundary. Errors and reverts covers both forms.
try-catch.sauce.ts
With a Pot holding 1 USDC and limit 100: amount 50 returns 2 and moves 50 of USDC’s smallest units to to. amount 200 returns 1010: stage is 1 again, error is the 10 bytes of "over limit", and no USDC moves, because the transfer was undone with the block.

program

Run the result with eval(parcel.code.concat(parcel.args)). The arrow may read the enclosing function’s values. A value known at compile time folds into the closure’s code; a run-time number, address or bool becomes a captured argument. Captures are read-only: “a belongs to the program around this closure: the closure’s program reads it, and can neither assign it nor change what it holds”. Run-time bytes cannot be captured: “b is a run-time bytes of the program around this closure, which the closure’s program cannot read: it reads a number, an address or a bool, and the values the compiler knows”. An arrow function is accepted only here and in a .catch() handler.
program.sauce.ts
With delta −10 it returns the 32-byte word of −5.

ctx

ctx reads the context a program runs in. Every read is a call: ctx.self(), not ctx.self. A wrong argument count gives “ctx.timestamp takes 0 argument(s), but this call passes 1”, and an unknown name gives “ctx.origin is not a context builtin”.

Reads on both targets

On Solana the engine sees the Pot as one of its accounts, so ctx.self() is the engine’s program id and not the Pot; pot, and pot as a token holder (pot.USDC), are still the Pot. EVM and Solana lists the other differences.
ctx-both.sauce.ts
On Solana, vault is an attached account and ctx.balance(vault.address) reads its lamports. On EVM, an Account is an address, and the same call reads that address’s balance.

Reads on EVM only

  • ctx.txOrigin() is not the caller. It differs from ctx.msgSender() whenever a contract stands between the signer and the program, such as a smart wallet or a relayer. Authorize on ctx.msgSender(): a check of the origin passes for any contract the signer calls.
  • ctx.prevrandao() can be biased by whoever produces the block. Do not decide anything with it that they would profit from choosing.
  • ctx.blobBaseFee() is not implemented on every chain. On Arbitrum and Polygon, a cook that reaches the read halts, using up its gas and returning no revert data, although the program still compiles for those chains. Read it only on a chain that has it.
  • A bare name is refused. gasLeft() gives ”… is called but never declared; write ctx.gasLeft() instead”. A bare blockHash(n), blobHash(i) or blobBaseFee() is pointed to its ctx spelling the same way, and so are origin() and gasLimit(), to ctx.txOrigin() and ctx.blockGasLimit().
ctx-evm.sauce.ts

crypto

  • crypto.keccak256 takes bytes. A number gives “keccak256’s argument requires a bytes value”; hash a word with crypto.keccak256(x.toBytesBe(32)).
  • crypto.findPDA must be destructured: “crypto.findPDA produces two values — bind them by destructuring, as const [address, bump] = crypto.findPDA(programId, seeds)”. [a] and [, bump] are accepted. At most 15 seeds: more compile but fail when run.
  • An unknown name gives “crypto.sha256 is not a crypto intrinsic”.
crypto.sauce.ts

Math

These are SauceScript’s integer functions. They are not JavaScript’s Math: Math.min, Math.max, Math.abs, Math.pow and Math.floor give “Math.min is not a Math intrinsic”. Write an if or a ternary for a minimum, and ** for a power. Every Math function is available on EVM and Solana. A wrong argument count gives “Math.sqrt takes 1 argument(s), but this call passes 2”. sqrt, mulDiv, addMod and mulMod work on unsigned integers, and a negative Int256 argument reverts: Math.sqrt of an Int256 −4 reverts with no data, and of 16 returns 4. Math.signExtend(raw, 2) reads a 24-bit int24 word, such as a pool tick: with raw 16777206 (0xfffff6) it returns −10. Assigning a negative result to a Uint256 reverts. Signed arithmetic and comparisons need no function: the operators /, %, >>, <, >, <= and >= are signed on Int256 values.
math.sauce.ts

Integer conversions

An integer is a Uint256 from 0 through 2²⁵⁶ − 1 or an Int256 from −2²⁵⁵ through 2²⁵⁵ − 1. A negative literal is an Int256, and arithmetic that mixes the two is refused: “cannot combine Uint256 and Int256 in this expression; convert an operand with Int256(value) or Uint256(value) first (conversions check bounds)”. These four builtins convert, on EVM and Solana: Int256(…) and Uint256(…) keep the number and check that it fits; .fromBits keeps the bits and changes the number. Use .fromBits only to reinterpret a raw word, such as a signed value read from storage. A return type, a parameter type or an annotation converts a scalar with the same checks as Int256(…) and Uint256(…). Int256 and Uint256 are reserved names: “Int256 is a builtin constructor and cannot be bound — constructor calls resolve against the builtin before any binding is consulted, so pick another name”.
amount + delta, with amount a Uint256 and delta an Int256, is refused with the message above. Convert one operand first:
mixed-add.sauce.ts

abi

abi is EVM only. Program arguments still work on Solana; they are decoded separately from abi.
  • Shapes. A field type is a SauceScript name: "Uint256", "Int256", "Address", "bool", "bytes", "string", "number", "bigint", "boolean", any of them followed by [], and records such as "{Uint256, bool}". Solidity spellings are rejected: “uint256 is not an abi.decode field type — …”. Shapes held in a variable are rejected too: “abi.decode’s second argument must be an array literal of shape strings, e.g. [“Uint256”, “bytes[]”]”.
  • Fields. A "bool" field decodes as a bool, an "Address" field keeps only the low 20 bytes, an "Int256" field decodes a signed word, and every other single-value field decodes as an unsigned number. Read a field by position (t[0]) or by destructuring; .name on the result is rejected because the fields are unnamed.
  • Depth. Deeply nested values can exceed the engine’s execution limits when the program runs. Keep encoded structures shallow, and simulate the largest input the program accepts.
  • Errors: “abi.encode requires at least one value to encode”; “abi.encode takes at most 255 arguments, got 256”; “abi.decode takes exactly 2 arguments (data, shape), got N”; “abi.decode’s first argument must be bytes”.
abi.sauce.ts

evm

evm is EVM only. On Solana every function here gives “evm.<name> is not available on the svm target”, with a pointer to its svm equivalent. A call to another contract is normally written through its name, AaveV3.Pool.supply(...). The evm.call, evm.static and evm.delegate entries below are the raw calls such a call lowers to; use them when the callee has no registry entry and no ABI file.

Calls

The optional last argument gas caps the gas forwarded to the call. Without it, the call gets what the EVM allows; with 0 it gets none. A bound call takes the same cap as .gas(cap). Errors: “evm.call() expects 3 argument(s), or 4 with a gas cap, but this call passes 2”; “evm.call()’s calldata argument must be bytes”. All three accept .catch() (Contract bindings).
evm-calls.sauce.ts
d0e30db0 is the selector of deposit() and 18160ddd that of totalSupply(). Moving a token through its name (WETH.transfer) is the normal spelling; see Names.
static-gas.sauce.ts
On Base, cap 50000 returns 1; cap 100 and cap 0 return 0.

Storage

Storage belongs to the executing contract, the Pot. A top-level let in a program is not storage: it is scratch space, reset on every run.
evm-storage.sauce.ts
evm.sstore writes any slot of the Pot’s storage, the Pot’s own included: the bitmap of used and cancelled signed-cook nonces, and, on a Pot whose Router is not fixed, the Router it calls for pool callbacks. A Pot deployed with deployPot(owner, salt, true) keeps its Router in its code, out of a program’s reach. A slot taken from an argument, as here, can be one of those. Derive the slots a program owns by hashing a name of its own, and see Security considerations for what a write there changes.

Deployment

initcode is creation bytecode, not the deployed code, and must be bytes: “evm.create’s init code requires a bytes value”. An omitted value sends none, and the Pot pays any value given.
evm-deploy.sauce.ts
evm.create and evm.create3 take the same init code and an optional value: evm.create(initcode), evm.create3(initcode, salt, value).

svm

svm is Solana only. On EVM every function here gives “svm.<name> is not available on the evm target”, with a pointer to its evm equivalent. An unknown name gives “svm.rent is not an svm builtin; svm is a reserved namespace root”. An account is a main parameter, in the order the transaction attaches them, optionally with the roles it must have: Account, Account<Writable>, Account<Signer> or Account<Writable, Signer>. Refusals: a main account not declared writable gives “state is not declared writable, so svm.sstore may not write it — declare it Account<Writable> (or Account<Writable, Signer>)”, then “state is not declared a signer, so svm.sstore may not write it — … declare it Account<Writable, Signer>”. svm.remainingAccounts() used anywhere but an initializer gives “svm.remainingAccounts() can only initialize a let/const — bind it first (const rest = svm.remainingAccounts();), then use the binding”.
svm-data.solana.sauce.ts
svm-call.solana.sauce.ts
svm-remaining.solana.sauce.ts

Methods on values

These work on both targets, on a variable, an expression or a literal: hex`a9`.concat(d) compiles. A constant width outside 1 to 32 gives “a .toBytesBe/.toBytesLe width must be between 1 and 32 bytes, got 33”. A computed width above 32 reverts, and a computed 0 gives empty bytes.
methods.sauce.ts
length.sauce.ts

Account properties

An Account parameter has these properties. They are read-only: assigning one gives “.owner is read-only — …”. On Solana an account is not an ordinary value. Returning one gives “a return value can’t be an account — …”, and using one where a number is expected, as in arithmetic, gives ”…: expected a number, found an account”. Use account.address when you need its key. On EVM, account.owner has no meaning, because an address is its code: check it with ctx.isContract.
account-both.sauce.ts
account-solana.solana.sauce.ts

Constructors and unary minus

These work on both targets. With x a Uint256 parameter, return -x; returns 0 for 0 and reverts with no data for 5.
constructors.sauce.ts

Contract bindings

An ABI imported from a JSON file binds a name, here Counter. This is the low-level spelling: a contract the registry names is called through its namespace, and Contracts and ABIs covers both. Bindings are EVM only. A binding never sends value: use evm.call(address, value, data) for that. An ABI intN output is an Int256. Every other single-value output is an unsigned number, bool and address included, so a : bool function cannot return a call directly. A method with no outputs returns its raw return bytes, and one with several returns a record named after the ABI’s outputs. A result that nothing reads is not decoded or checked, even when it is assigned to an unused variable: an empty or malformed reply, or a call to an address with no code, then passes. Reading any part of a result decodes all of it. Security considerations covers what to check instead.
Counter.json
bindings.sauce.ts

.gas()

<call>.gas(cap) forwards at most cap gas to one external call: a bound method, or a protocol or token call written by name. The raw evm.call, evm.static and evm.delegate take the cap as their last argument instead. .gas() and .catch() combine in either order. On Solana it is refused: “a call's .gas() cap is not available on the svm target; an SVM cross-program invocation runs on its transaction’s one compute budget, which only the transaction’s ComputeBudget instruction sets; there is no per-call cap”.
gas-cap.sauce.ts
With the Pot holding 5 USDC, cap 50000 returns 5000000 and cap 100 returns 0.

.catch()

.catch() survives a failed external EVM call. It works on bound contract calls, calls written by name, and evm.call, evm.static and evm.delegate.
  • .catch() covers the call alone. Evaluating its arguments, and decoding a successful reply that the program reads, happen outside it: a call to an address with no code succeeds with empty return data, .catch() does not run, and decoding that empty reply reverts the cook. Check ctx.isContract(address) first when the address may have no code. To catch a decode as well, put the call and the read in a try/catch block.
  • error is untrusted data from the callee and may be empty or cut short: check its length before decoding it. A second handler parameter is refused: “a .catch() handler with more than one parameter (it takes the revert data) is not supported in SauceScript”.
  • Use a block, not () => value: “a concise-body .catch() handler (use a { ... } block body) is not supported in SauceScript”. A return or a break or continue leaving the block is not allowed in the statement form. The value form needs exactly return <constant>;, a constant that fits the declared ABI result width.
  • A handler, here or in a try/catch, compiles to at most 255 bytes: “a catch block or .catch() handler compiles to 320 bytes, over the 255-byte limit — shorten the handler”. Move a long handler’s work into a function and call it.
catch.sauce.ts

Ambient token functions

These functions are in scope with no import in every program the sauce command, the MCP server, actions documents and routes.compileSauceProgram compile. They are the functions of @sauce/token, which those add to the compile; the compiler alone does not declare them, and neither does routes.compileSauceRoute unless you pass ambient: ["@sauce/token"] (Compiling programs). A function a target lacks is refused there: “ownerOf is called but never declared”. Their arguments differ by target: on Solana they take the accounts they read or move through. @sauce/token gives each signature, its checks and its refusals. On EVM the writes have SafeERC20 semantics.
ambient-token-evm.sauce.ts
ambient-token.solana.sauce.ts

Names resolved at compile time

These names are not functions of the language: the SDK resolves them, before compiling, against the chain the program is compiled for. The compiler alone (@eco-incorp/sauce-compiler) refuses them (Compiler or SDK). Names covers them in full.
  • A test takes a contract or a token symbol. Any other use of a name the chain lacks is refused at the use, naming the chain. A name no chain has is an error, not false, so a typo cannot drop a branch. A name is tested by writing the name itself, never by a call.
  • A Token(address) bound to a name must be declared once, never reassigned, and used only as t.<method>(...).
  • pot.owner, pot.balance and pot.<TOKEN> are read-only members. Keep a value you need to change in a local variable.
  • A program’s declaration or import of pot or Account shadows that global in its scope. A compile-time binding of either name also takes precedence. Account remains the account type in type positions.
  • On Solana, Account(pot.address).owner reads the program that owns the Pot account, not the owner who signs the cook.
compile-time.sauce.ts

Target refusals

A builtin used on a target that lacks it fails at compile time, never as a silent difference. Each of these is refused, with the compiler’s message: Compile a program that uses a target’s builtins for that target only, or put each target’s code in its own arm (One module, an arm per target).

Gas costs

This page gives no gas figures. A program’s gas is what sauce simulate reports for it; cook() lists what a cook costs.

Next steps

Errors and reverts

require, throw, atomic try/catch and .catch() in the context of a whole program.

Contracts and ABIs

Calling a contract through its namespace, an ABI import or an address.

EVM and Solana

Every difference between the two targets, including accounts and ctx.

Names

Token symbols, Token(address), pot and protocol namespaces.