A program that uses only what both targets have compiles for both. The calls into other contracts and the accounts a program touches are where the targets differ.
pay.sauce.ts
What compiles on each target
Something that exists on only one target is a compile error on the other, never a silent difference.
The error names the target and, where there is one, the replacement:
Builtins lists every builtin with its targets.
Accounts on Solana
A Solana transaction lists every account it reads or writes before it runs, each with a role: read-only, writable or signer. A program cannot reach an account the transaction did not list. When you compile for Solana, the compiler returns an account manifest: every account the program needs, in order, with its role and how to find it.sauce compile pay.sauce.ts --chain solana --json gives this manifest for the program above:
derive says where the account comes from: pot is the Pot, pot:USDC the Pot’s USDC account, @to:USDC the USDC account of main’s argument to, and the last slot is the SPL Token program. sauce run, sauce simulate and sauce tx derive every such slot from the owner, the Pot and main’s arguments, so the program runs with --args alone. An account given with --account for a slot derived from the Pot, the owner or the authority must be the one its derivation gives; another key is refused.
A program that does not use the Pot derives the accounts its token methods move through from authority instead (authority, authority:USDC): the Pot through the Kitchen, the signer in a direct cook on the engine. Reading pot, pot.USDC or pot.balance binds the program to the Pot, so --via engine cannot run it. Reading only pot.owner does not bind a Pot and works with --via engine.
A program gets its accounts three ways:
Derived accounts
derived.solana.sauce.ts
- A program-derived address (PDA) is an address a program computes from seeds, with no key behind it. Only that program can sign for it. The Pot is one, derived by the Kitchen.
- An associated token account (ATA) is the token account the token program derives for a holder and a mint. It is where that holder’s balance of that token lives.
- Every holder and seed must be known when the cook is built:
pot,pot.owner,ctx.self(), anAddressparameter ofmain, a key written out, or a program account. For an account computed while the program runs, take anAccountparameter instead. .writable()and.signer()give an account the role a call needs, and chain:pot.owner.writable().signer(). Account expressions are read-only unless given a role; uses of the same account combine their roles.
Account parameters
accounts.solana.sauce.ts
Accountis read-only;Account<Writable>andAccount<Signer>declare the role the transaction must grant. The manifest recordsreadonly,writable,readonly_signerorwritable_signerper slot. Roles are what you declare; the compiler does not infer them from a call.Accountparameters are not encoded as arguments.sauce compile --jsonlists them asaccountsToGive, and a cook missing one is refused before anything is read.- An
Accountexposes.address,.owner(the program that owns it) and.dataLen. Check.ownerbefore trusting an account’s bytes: anyone can create an account with any layout.pot.owneris not this property: it is the Pot’s owner, described underctx.self()andpotbelow. - An account passed as a parameter is used as attached. Nothing checks whose token account
destinationis; check it withownerOf(mint, destination)when it matters, or take anAddressand let the token method derive the account.
A program checks its accounts
A transaction built by someone else could attach a different account in a slot, so every derived account is checked on chain at the top ofmain, before the program’s own code. A wrong account fails the whole cook with a reason: wrong recipient account, wrong holder account, wrong program-derived address, wrong mint or wrong program. The Pot and the owner are pinned by their signatures.
Token programs
Every Solana token belongs to one of two token programs, and so does each of its token accounts:
The token methods pick the write by the mint’s program, so
USDC.transfer(…) and PYUSD.transfer(…) look the same in a program. Token(mint) reads the mint’s program from the cluster when the cook is built, and refuses a mint whose program cannot be read. Token-2022’s transfer fee is withheld from what arrives, and a mint whose transfer hook names a program is refused. @sauce/token lists both of its arms.
Protocol calls
On an EVM chain a program calls a contract through a protocol namespace or an ABI binding, typed by the ABI:UniswapV3.Factory.getPool(WETH, USDC, 500). See Contracts and ABIs.
Solana has no protocol namespaces. A program calls another program with svm.call(program, [accounts], data), a cross-program invocation (CPI) with accounts the transaction already lists. A failed svm.call fails the whole transaction; Solana has neither .catch() nor try/catch. Swaps use the venue namespaces, which compile to @sauce/router’s Solana functions and derive the pool’s accounts:
venue.solana.sauce.ts
ctx.self(), pot and pot.owner
On Solana only a Kitchen cook makes the Pot sign, so a use of
pot that signs as the Pot, such as a token write or pot.signer() in svm.call, fails with another account in its slot. pot.owner is the owner’s account, not the program that owns the Pot account. Write Account(pot.address).owner for that program.
On Solana the engine’s program id owns every Pot account, and other accounts too, so acct.owner == ctx.self() does not prove an account is the Pot. To bind a slot to the Pot, re-derive the Pot’s address with crypto.findPDA from the Kitchen’s program id, "pot", the owner and the salt, and require the slot to be it. The Kitchen’s program id is on Deployments; see also Pot.
Transaction size and staging
A Solana transaction holds at most 1,232 bytes. A cook carries the program, its arguments (32 bytes for eachUint256 or Address), the accounts, the signatures and the transaction’s setup. sauce compile --json --chain solana reports the program’s size, and sauce tx reports whether the cook fits in one transaction. Compiled for Solana:
A program too large for one transaction is staged: uploaded first, over several transactions, into a code buffer account of the Kitchen, then cooked from that buffer. The Kitchen runs only a buffer that was finalized, and finalizing checks the SHA-256 digest of the upload. After the cook the buffer is closed and its rent returned.
sauce run stages a program when it does not fit, and sauce tx returns the steps as a plan for another signer.
Every account a transaction lists takes 32 bytes. An address lookup table lets a transaction refer to an account with one byte. sauce run creates one when a cook’s accounts do not fit.
On EVM chains the program travels as the calldata of the cook; there is no staging.
A Solana cook is built for one release set’s engine and Kitchen: the engine’s program id and the Kitchen’s instruction format are part of it. Build every cook, staged or not, with the installed SDK, and never replay a serialized cook or staging instruction built by another SDK release.
Compute and memory on Solana
- Compute budget: a transaction has 1,400,000 compute units, and every instruction spends from it, the engine’s and each called program’s. A cook that runs out fails whole. A concentrated-liquidity swap spends units for every initialized tick it crosses, so a wide price limit on a deep pool can exhaust the budget.
sauceholds each Solana pool’s swap short of its limit where the budget would not cover it, and on Raydium’s code and Crema walks a swap on into the furthest tick array its accounts give (Swaps at a price and compute units). - Memory: a program has 64 KiB of working memory per run. Every cook requests a 256 KiB heap frame, larger than Solana’s default.
sauceand the TypeScript client add that request. A cook built by hand prependsbuildHeapFramePrepend()from@eco-incorp/sauce/svmonce.
CPI depth
Solana allows 4 levels of CPI below a transaction’s instruction, and 8 on a cluster where SIMD-0268 is active. Through the Kitchen, the Kitchen’s call into the engine uses one of them:
A direct cook (
--via engine) runs with no Pot. The program acts with the signer’s authority and spends the signer’s own token accounts, so a program that names pot is refused there. A program that calls an aggregator, which calls a pool, which calls a token program, can need the extra level.
Native coins
On an EVM chain only
evm.call(address, value, data) sends native value with a call; ABI bindings send none. On Solana a program moves lamports with a System Program transfer through svm.call.
On Arc the native coin and the ERC-20 USDC are one balance at two scales: 18 decimals natively, 6 for the token. Sending native value and calling USDC.transfer move the same money, and the gas and the Kitchen’s fee are paid from it too. pot.USDC reads the 6-decimal token and pot.balance the 18-decimal native coin:
arc-pay.sauce.ts
Next steps
- Names: what each name resolves to on each chain.
- @sauce libraries:
@sauce/tokenand@sauce/routeron both targets. - Kitchen and Pot: how a cook runs on each target.
- Chains: every chain, its target and its support.
