Skip to main content
On an EVM chain a private Pot is a contract the Kitchen creates for one owner. Every program runs inside a Pot: each call the program makes comes from the Pot, pot.USDC is the Pot’s USDC balance, and what a call pays to its caller lands in the Pot. Only the owner can cook on it, or authorize a cook that another account submits (Signed cooks). The owner is fixed when the Pot is created. A program reads it as pot.owner, the address the Pot’s owner() returns. A Shared Pot has no owner: there pot.owner is the zero address. On Solana a Pot is an account the Kitchen derives and signs for. A Pot on Solana covers the differences.

Its address

A Pot’s address follows from the Kitchen, the owner and a 32-byte salt. For a Pot that deployPot(owner, salt) creates, the kind the sauce command and the SDK’s helpers use:
A Pot whose Router is fixed, created with deployPot(owner, salt, true), hashes abi.encode(owner, salt, true) instead, so the same owner and salt give a different address; a Shared Pot hashes abi.encode(address(0), salt, true). The Kitchen’s predictPot(owner, salt, true) and predictSharedPot(salt) return them. For the owner 0x1111…1111 and the default salt, on a local fork of Base at block 52992458 with the current contracts, predictPot returned 0x6c91…D3Fe and 0xF993…7a27, and the formula gives the same two addresses.
  • On EVM, it is known before the Pot exists, as the prediction of the Kitchen the chain runs. Tokens sent to a predicted address that no Pot is deployed at are out of reach of every cook. Deploy the Pot before you send it funds, with sauce pot --chain <chain> --deploy and the owner’s signer, or with sauce run --fund, which deploys the Pot before it funds it (Funding a Pot). sauce tx --fund plans the deployment first only when its endpoint shows the Pot has no code; with --offline it cannot check, so deploy first. Without --offline, sauce pot also reports whether the chain has the Kitchen, and refuses the deployment on a chain the sauce command does not send on.
  • A deployed Pot’s address never changes.
  • The Kitchen has the same address on every EVM chain it is on, so where each chain runs the same Kitchen release, an owner’s Pot under one salt has the same address on each of them.
For the owner 0x1111…1111, both print the same Pot, 0x0828…Ec99, under the salt 0xcbd8…d8ac. --offline derives the address without reading a chain, so it is not checked against the deployed Kitchen, and the command says so.

Salts

An owner has one Pot per salt.
  • The default salt is zero: the owner’s default Pot. In TypeScript it is DEFAULT_POT_SALT.
  • --salt <label> names another Pot: its salt is the keccak256 of the label’s UTF-8 bytes. In TypeScript, potSalt(label).
  • --salt-hex <bytes32> gives the salt exactly.
A salt is not a namespace. Whoever sends deployPot(owner, salt) creates the same Pot, owned by owner, and no one can create a different contract at that address.
v12PredictPot reads no chain.

Deploying it

The Kitchen’s deployPot(owner, salt) deploys a Pot (Kitchen). Any account may send it, the Pot is the owner’s whoever sends it, and a second send returns the Pot already there. It carries no fee. On a local fork of Base at block 52965000 with the current contracts, a simulated deployment used 667,003 gas. You rarely send it yourself:
  • sauce run deploys the Pot before the first cook that needs it, as a transaction of its own, after asking (or with --yes).
  • sauce tx prints a plan when the Pot has no code yet: deployPot first, marked deploysPot, then the cook.
  • sauce simulate deploys the Pot inside the simulation first.
  • sauce pot --deploy sends the deployment alone.

What it holds

On EVM, a deployed Pot is funded like any address: send it tokens or native coin from any wallet once its deployment has landed (Its address), or let sauce run --fund, which deploys the Pot first, pull them from the wallet in the same transaction as the cook. Offline, sauce tx --fund cannot check deployment and adds no deployment step. What a cook produces lands where the program sends it, and a call that credits its caller credits the Pot. A swap with no other recipient, a lending market’s receipt tokens supplied for pot (Aave’s aTokens with onBehalfOf the Pot), and vault shares minted to the caller all stay in the Pot. They are the owner’s, but a wallet app does not show them, and only a later cook moves them. To deliver a result to a wallet, the program names the wallet as the recipient or beneficiary, from a main parameter:
supply.sauce.ts
Simulated on a local fork of Base at block 52965000 with the current contracts, with the owner’s address as onBehalfOf, the aUSDC was minted to the owner’s wallet and the 100 USDC left the Pot. Keeping a position in the Pot is a choice too: a later cook can withdraw it or borrow against it without the wallet moving anything back first.

No withdraw function

A Pot has no function that withdraws or transfers what it holds. What a Pot holds leaves only through a cook: a program that sends it.
sweep.sauce.ts
To give a Pot’s holdings to someone else, cook a program that transfers them to that person or to their own Pot. The Pot itself cannot change hands.

Who can do what

The table describes EVM Pots. On Solana the Kitchen’s upgrade authority can replace the program that signs for every Pot, and must be trusted with what Pots hold (Trust on Solana). An EVM Pot’s code never changes once it is deployed: nothing upgrades it.

Signed cooks

A Pot created by the current Kitchen has a second way to run a program. The owner signs a cook off-chain, and any account submits it with cookSigned: a relayer that pays the gas, a service that sends it later, a second account of the owner’s. The Pot runs it as it runs the owner’s own cook, if and only if the owner signed that engine, that program with its arguments, that value, that fee ceiling, that nonce, that deadline and that submitter, for that Pot, on that chain.
What is signed. The owner signs an EIP-712 Cook under the domain { name: "Sauce Pot", version: "1", chainId, verifyingContract: <the Pot> }, so one signature is good for one Pot on one chain: Who submits when submitter is zero, when it is submitted up to the deadline, the gas and the transaction’s place in its block are not signed. The program sees the submitter as ctx.msgSender(), so it can pay whoever submitted it. The fee. A signed cook pays the Kitchen’s fee once, as a cook does, but the Pot pays it from its own balance, which includes the call’s value, and never more than maxExecutionFee. The Kitchen’s owner sets the fee and may raise it after the owner signs. A fee above the smaller of the Pot’s balance and the ceiling fails with FeeNotPaid. The submitter sends exactly the signed value and pays the gas. Nonces. Nonces are unordered: a used nonce sets bit nonce & 255 of word nonce >> 8 in the Pot’s storage, in any order, before the program runs. A second submission fails with NonceUsed(). A cook that fails reverts everything, its nonce included, so the same signature can be submitted again until its deadline. invalidateNonces(word, mask) sets bits to cancel authorizations not yet used, and emits NoncesInvalidated(uint256 indexed word, uint256 mask). The owner can call it, and so can the Pot itself, so a signed cook can cancel others. A signed cook emits SignedCook(uint256 indexed nonce, address indexed submitter). Whose signature counts. The owner’s: a 65-byte signature that ecrecover recovers to the owner, or, when the owner has code, whatever the owner’s ERC-1271 isValidSignature accepts. A Safe or an EIP-7702 account signs this way. Anything else fails with BadSignature(). Which Pots take it. Private Pots created by the current Kitchen, with a fixed Router or not. A Shared Pot has no owner to sign and refuses every signed cook. The SDK builds each part: sauce sign signs a cook with a wallet or prints the typed data for one kept elsewhere, and cancels one; the MCP server has sauce_signed_cook and, over stdio, sauce_sign; and TypeScript has signedCookTypedData, encodeCookSigned, encodeInvalidateNonces, verifySignedCook and checkSignedCookPot from @eco-incorp/sauce/evm/engine (Cook transactions). Security considerations covers what to watch for.

The Pot’s interface

v12PotAbi from @eco-incorp/sauce/evm/engine is the complete ABI. The parts a developer uses: The ABI also declares the event SignedCook(nonce, submitter) and the errors BadSignature(), Expired(), NonceUsed(), WrongSubmitter(), EmptyProgram(), EngineHasNoCode(), FeeTransferFailed(), NotInFlightEngine(), RouterFixed(), RouterHasNoCode(), RouterUnset() and SharedRouterNotFixed(). sauce simulate prints a Pot’s or the Kitchen’s revert by its error’s name and arguments, such as FeeNotPaid(1234, 0).

The Pot’s Router

Some pools call their caller back in the middle of a swap to be paid: Uniswap V3 and its forks, Uniswap V4, Maverick V2 and PancakeSwap Infinity. A program cannot answer a callback. The Pot answers it: a call the Pot does not recognize goes to its fallback, which delegatecalls the Pot’s Router, and the Router pays the pool from the Pot exactly what the pool asks for, up to the swap’s input amount. The @sauce/router functions for those venues call the Router through the Pot. Venues that are paid first, Uniswap V2, Curve, Balancer V2, DODO V2, Trader Joe LB and WOOFi, are called directly by the program and do not use the Router.
  • A new Pot starts on the Router the Kitchen names in router() when the Pot is created.
  • After that, only the Pot’s owner can change it, with the Pot’s setRouter. A call from anyone else reverts with NotOwner().
  • A Pot created with deployPot(owner, salt, true) keeps its Router for good, and so does every Shared Pot.
  • The Kitchen’s owner changes the Router that Pots created afterwards start on, and nothing else. A Router change moves no Pot address.
The Router is content-addressed: its address follows from its code, so a Router release with other code has another address. v12RouterAddress() from @eco-incorp/sauce/deployments returns the current Router; compare a Pot’s router() with it by address. After a new Router is released, potRouterUpdate(client, pot) from @eco-incorp/sauce/evm/engine reads the Pot’s Router and the Kitchen’s through your client. It returns the setRouter call for the owner to sign, or nothing when the Pot is current. It sends nothing, and it throws for a Pot that the current Kitchen did not create.

A Pot on Solana

  • Address. A Pot is a program-derived address of the Kitchen program, from the seeds "pot", the owner and a 32-byte salt. It is known before the Pot exists, and the default salt is zero, as on EVM chains.
  • Creation. The Kitchen’s create_pot creates it. The owner signs that instruction and pays the rent, Solana’s deposit for storing an account. sauce run asks before creating it, and sauce tx puts it in the setup it prints.
  • Storage. A Pot keeps 10,240 bytes of storage for programs, fixed when it is created. In a simulation in the sauce command’s local copy (LiteSVM) of the cluster at slot 460261665, its rent was 52,669,440 lamports, and the rent of the Pot’s USDC token account 1,488,440 lamports.
  • Cook instructions. The Kitchen’s cook and cook_from_account carry the Pot’s salt and then its bump, the byte that makes the Pot’s address a valid program-derived address. The SDK’s builders and the sauce command derive the bump. An instruction serialized for the Kitchen before the current one has the earlier format and names the earlier engine: build cooks with the installed SDK.
  • Signing. The current Kitchen requires the owner to sign each cook, and signs for the Pot, so the program can spend tokens whose authority is the Pot. The Kitchen program is upgradeable: its upgrade authority can replace that check, and must be trusted with what the Pot holds (Trust on Solana).
  • Token balances. The Pot’s tokens sit in its token accounts, one per mint, at the associated token account under the mint’s token program. sauce pot --chain solana lists them.
  • pot and ctx.self(). In a program on Solana, ctx.self() is the engine program’s id, not the Pot. pot is the Pot: its key, its signer slot where an Account is taken, and the holder in pot.USDC. pot.owner is the owner’s account; pot.owner.writable().signer() gives it both roles. Account(pot.address).owner reads the program that owns the Pot account instead.
  • No Pot. A cook sent directly to the engine runs with no Pot, and has only the signers its transaction gives it. A program that uses pot, pot.balance or pot.<TOKEN> is refused there; one that reads only pot.owner is not.

Next steps