Skip to main content
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). 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:
signed-delta.sauce.ts
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. 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:
supply.sauce.ts
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.
rollback.sauce.ts
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.
catch-bytes.sauce.ts
With 1 USDC in the Pot on the fork: 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).
  • 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.
  • 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:
guarded-swap.sauce.ts
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):

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).
  • 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.

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 describes what is signed and what the Pot checks, and sauce sign signs one. A signed cook is a cheque: whoever holds it can cash it, once, until it expires.
  • 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).
  • 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()

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.

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:
deliver.sauce.ts
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.
  • 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.
  • 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.
arrived.sauce.ts

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.
unread-output.sauce.ts
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.
checked-read.sauce.ts
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:
codesize.sauce.ts
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.

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

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):
  • 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:
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