Skip to main content
An actions document is an ordered list of protocol operations written as JSON: swap on a Uniswap V3 pool, approve a token, supply to Aave. @eco-incorp/sauce/actions validates a document and compiles it, for one chain, into one Sauce program that runs in a Pot in one transaction. Any language that writes JSON can produce one, and the package publishes the JSON Schema it is checked against. The same documents run from the command line with sauce actions, and sauce compile, simulate, tx and run take one wherever they take a program file.

The document

swap-supply.json
This takes an amount of USDC as a run-time argument, swaps it for WETH in the pool Uniswap V3’s factory keys at 0.05%, approves Aave’s pool for the WETH received, and supplies it with the Pot as the beneficiary. A $schema key is allowed and ignored. Every action runs in one transaction on the chain the document is compiled for; a bridge’s destination is one of the bridge action’s own fields.

Values

Each amount must also fit the field it becomes; a value too wide for it is an error.

Names and addresses

A name lowers to the program global of that name, and the compile for a chain resolves it there, as it resolves USDC in a hand-written program. A document with only names and no chainId therefore compiles for every chain that has those names, each time to that chain’s addresses. An address means the same address on every chain, which suits a contract deployed at one address everywhere, such as Permit2. A document may mix names and addresses. A swap may name its pool by its factory’s key instead of its address: uniswapV2Swap by factory, uniswapV3Swap by factory with fee (or tickSpacing for Aerodrome Slipstream), traderJoeLBSwap by factory with binStep. The program reads the pool from the factory when it runs and reverts before anything moves if there is none.

Chaining outputs

An action that spends an amount takes it from, in order:
  1. its own amount;
  2. amountRef, the name of an output an earlier action saved with saveOutputAs;
  3. otherwise, the previous action’s output.
Setting both amount and amountRef, naming an output no earlier action saved, or a first action with no amount is an error. Saving a second output under one name reassigns it. An action’s output is what it delivered to the Pot (received), the amount it spent (its amount), or, for balanceOf, nativeBalance and argument, the value read. When a later action spends an output, the action delivers it to the Pot whatever its recipient says; an output nothing spends goes to the action’s recipient. An argument action declares a run-time parameter: it becomes a Uint256 parameter of main, so one compiled program serves every amount, and the value travels as an ABI-v1 word after the bytecode.

parseProgram

Validates a document and returns a canonical copy.

Import

Usage

parse-errors.mts

Signature

Parameters

  • input: the document as JSON text, or an already-parsed value.

Returns

ActionProgram: a fresh copy with its keys in canonical order. It fills in nothing: an optional field left out stays out. Parsing checks how each name is spelled; whether a name exists is decided by the chain the document is compiled for.

Throws

ProgramError, listing every problem at once, each with its path.

serializeProgram

Writes a document as canonical JSON.

Import

Usage

serialize.mts

Signature

Parameters

  • program: the document.
  • space: indentation, as JSON.stringify takes it. Default none.

Returns

The canonical JSON text: two equal programs serialize to the same text, and parseProgram(serializeProgram(p)) equals p.

Throws

ProgramError when program is invalid; it is validated first.

programSchema

Returns the JSON Schema (draft 2020-12) every document satisfies.

Import

The same schema ships as a file: @eco-incorp/sauce/actions/action-program.schema.json.

Usage

schema.mts

Signature

Returns

The schema object. It is generated from the table the parser reads, so a document the schema rejects is always rejected by parseProgram. It marks which fields take a token ($defs/token, $defs/tokenOrNative), a protocol contract ($defs/contract) or either ($defs/tokenContract), and states each swap’s pool alternatives as a oneOf. The schema cannot express four rules that the parser and the compile enforce: EIP-55 checksums, exact numeric ranges (it bounds digit counts), the chaining rules, and whether a name exists on the chain being compiled for. Validate with the schema at the edge of your system, and parse before compiling.

compileProgram

Compiles a document for one chain into a program and its cook payload.

Import

Usage

compile-actions.mts
The swap is a call into @sauce/router, the same library a hand-written program calls. The lowered program names the Pot pot and its native balance pot.balance, as a hand-written one does; a split swap compiled with no chain writes ctx.self(). Every other action is an ABI-bound call, IBinding.at(address).method(...), with the registry’s ABI wherever the registry declares the exact function. Reading any part of a bound call’s result decodes all its declared outputs; a result the program discards is neither decoded nor checked. ERC-20 transfer and approve are checked writes with OpenZeppelin SafeERC20 semantics. A call that carries native value (wrapETH, the staking deposits, the bridge fees) is a raw evm.call.

Signature

Parameters

  • program: the document. It is validated again, so a typed document that skipped parseProgram is held to the same rules.
  • options.chain: the chain to compile for, a slug ("base"), a chain id (8453) or a CanonicalChain. Defaults to the document’s chainId; a document without one needs it.

Returns

CompiledProgram:
  • bytecode: the compiled program.
  • source: the SauceScript the document lowered to, with every name as written.
  • contracts: the ABI bindings source calls through.
  • parameters: the argument names, in the order main takes them.
  • entryEncoding: always "abi-v1", the only argument encoding.
  • payload(values?): the bytecode followed by the encoded arguments, by name: the program a Pot’s cook(engine, program) takes (Cook transactions).
  • argumentOffset(name): where argument name’s 32-byte big-endian word starts in the payload. The offset is fixed whatever the values, so a number measured later can be spliced into a payload built earlier.

Throws

ProgramError when the document is invalid, when chainId names another chain, or when a name does not exist on the chain, listing every such name with its path. The SDK refuses on Arc (chain 5042) what Arc cannot do: wrapETH, unwrapETH, lidoStake, rocketPoolStake, etherFiStake, balancerV3Swap with wethIsEth: true, and a nativeBalance spent as a token amount. Arc’s native coin is USDC, the same balance as its USDC ERC-20 (Arc).

ProgramError

The error every function on this page throws for a document that breaks a rule.

Import

Usage

The parseProgram example catches it twice.

Signature

Properties

  • issues: every problem found, in order, each located by a path into the document (actions[2].amountRef).
  • chain: set when the issues are the compiled chain’s, such as a name it lacks, rather than the document’s own.
  • message: a summary line, then one line per issue.

quoteAction

Builds the eth_call that prices a swap action at its own amount. It reads nothing; you send the call. @sauce/router has no quote function, so this is how TypeScript prices a swap before it builds a program. A program can also read a quote itself before it swaps, as Quote, swap and supply does.

Import

Usage

quote.mts
On Base at block 53157152, through a public endpoint:

Signature

Parameters

  • action: a swap action with its amount (a Carbon trade or a Balancer batch prices at the amounts it carries). Its pool must be given by address.
  • options.chain: the chain whose names the action’s tokens and contracts resolve to; required when the action uses a name.
  • options.router: for uniswapV3Swap and uniswapV4Swap, the quote Router at v12QuoteRouterAddress() from @eco-incorp/sauce/deployments (Deployments). The content-addressed execution Router at v12RouterAddress(), which a Pot swaps through, has no quote method.
  • options.maverickQuoter, options.ambientImpact, options.fluidDexResolver: the quoter for maverickV2Swap, ambientSwap and fluidDexT1Swap.
The other venues price on the contract the action names: a Uniswap V2 pair’s reserves, a Curve pool’s get_dy, a DODO pool’s querySellBase or querySellQuote, a Trader Joe pair’s getSwapOut (zero when the pair cannot take the whole input), WooPPV2’s query. Send a Balancer V3 quote from the zero address. No quote needs a balance or an allowance.

Returns

Quote: send data to to as an eth_call, and pass the result to decode for the output amount in tokenOut’s smallest unit.

Throws

For a pool given by key: read the pool from the factory first, then give pool. For an action that names a token or contract when options.chain is missing.

compileSplitSwap

Compiles one program that divides an amount into equal slices and gives each, in turn, to the candidate pool whose onchain quote pays the most for it, then executes every candidate’s share.

Import

Usage

split-swap.mts

Signature

Parameters

  • candidates: ordinary swap actions with no amount, amountRef or saveOutputAs, each with its pool by address (a uniswapV4Swap by its poolManager and key) and each delivering to the Pot. Each must have an onchain quote with a fixed amount offset: every venue but Balancer (V2, its batch swap and V3) and Carbon, which the split refuses (candidates[i] (balancerV2Swap) has no on-chain quote with a runtime amount, which a split needs). Uniswap V3 and V4 candidates quote through quoters.router, the quote Router at v12QuoteRouterAddress().
  • amount: the total input, a base-10 string divisible by buckets.
  • buckets: how many equal slices the allocation hands out; at least 1.
  • amountOutMin: the least total output, checked once after every leg. Required; "0" waives it.
  • approvals: approvals to run first, each with an explicit amount, for a candidate that pulls through a periphery contract. A @sauce/router swap needs none.
  • sweep: sends the total output of token to recipient at the end; omit it to keep the output in the Pot. recipient is an address or "pot".
  • quoters: the quoter contracts the candidates price through, as in quoteAction.
  • chain: the chain the names in the candidates, approvals and sweep resolve on; needed only when they use a name.

Returns

A CompiledProgram with no run-time arguments: its payload() is the bytecode.

Throws

ProgramError for an invalid candidate or a name the chain lacks; an error for a pool given by key.

Action reference

Every action type, generated from @eco-incorp/sauce/actions/action-program.schema.json. Every action that takes amount also takes amountRef, and every action takes saveOutputAs. Output is what a later action spends. Each field’s documentation is its description in the schema. A @sauce/router swap (the first nine rows) needs no approval: it pushes its input to the pool, approves exactly its input itself, or the Pot’s Router pays the pool. Any other venue that pulls from the Pot needs an approve first.

TypeScript types

Every type is the JSON it describes, with amounts as strings and no class instances.
  • ActionProgram is the document, Action the union of every action, and ActionType its type.
  • Each action has its own type (UniswapV3SwapAction, AaveV3SupplyAction, …), grouped as SwapAction (with RouterSwapAction, the nine @sauce/router swaps), BridgeAction, WrapAction, StakeAction, LendingAction, TokenAction and SourceAction.
  • A field’s type says what it takes: Token (a TokenSymbol or an address), TokenOrNative, Contract (a ContractName, "Namespace.Contract", or an address), TokenContract and Recipient (`0x${string}` | "pot").
  • A swap that names its pool by key is a union, so TypeScript refuses the mixes the parser refuses: UniswapV3SwapAction is a pool, or a factory with a fee, or a factory with a tick spacing.

Next steps