How a name resolves
resolve.sauce.ts
sauce compile resolve.sauce.ts --chain base writes Base’s USDC address into the bytecode. Compiling the same file with --chain ethereum or --chain solana writes that chain’s USDC. One file serves every chain whose names it uses, and each compile produces its own bytecode.
- Resolution is per chain and happens at compile time. Nothing of the name is left in the bytecode; the engine on chain only reads addresses.
-
sauce compile --jsonlists what was applied.definesholds each token andchainIdwith its value, andcontractsholds each namespace contract with its address (addresses abbreviated here): -
A use of a name the chain lacks is a compile error at the use, naming the chain. The error gives the file, line and column. Nothing is compiled, so nothing can be sent. This program compiles for Ethereum, which registers
USDT:Base does not registermissing-token.sauce.tsUSDT, so--chain basefails like this atUSDT.transfer: -
A protocol contract the chain lacks is refused the same way, with the chains that have it. This program compiles for Base:
Compiled withmissing-contract.sauce.ts
--chain unichain, it fails:To use a name only on the chains that have it, test it: Using a name only where the chain has it.
sauce simulate proves a call works on the chain as it is now.
Chains, Protocols and Tokens list every name per chain. sauce globals --chain <chain> prints the same registries for one chain, with --tokens, --protocols, --protocol <Namespace> for a namespace’s methods, --stocks, and --search <text>. See sauce globals.
Token symbols
A token symbol is that chain’s token address. It is a value (balanceOf(USDC, account)) and the receiver of the token methods (USDC.transfer(…)).
-
Symbols are case-sensitive.
SPCXCis notSPCXc. -
Not every chain registers symbols. Only 19 of the 46 registry chains register any: Ethereum, Optimism, Unichain, Polygon, Monad, Sei, Mantle, Arc, Base, Plasma, Arbitrum, Ink, Berachain and Solana, and the testnets Arc Testnet, Base Sepolia, Ethereum Sepolia, Solana Devnet and Solana Testnet. Among those, the symbols differ: Base has
USDCandWETH, Ethereum addsUSDT, Unichain has onlyUSDT0. On a chain that does not register a symbol, take the address as amainparameter and useToken(address). -
Tokenized stocks are tokens. Each issuer’s own ticker is the symbol, with
.written as_(BRK.BxisBRK_Bx). They take the same methods, balances and swaps as any token:The chain’s stocks are not in itsstock.sauce.tssauce globalstoken list, which counts them.sauce globals --chain base --stockslists them and--search spacexfinds one by symbol, company or ticker. One company has one token per issuer, so the symbol depends on the chain and the issuer. -
A wrong case is refused, with the right one suggested. On Base,
SPCXC.transfer(to, amount)fails with this hint; writeSPCXc, asstock.sauce.tsdoes:
Balances
<holder>.<TOKEN> reads a holder’s balance of a token. It is TOKEN.balanceOf(holder) written short and compiles to the same bytes.
balances.sauce.ts
potis the Pot, andpot.USDCis the Pot’s USDC.- The native coin is
.balance.pot.balanceisctx.selfBalance(), andaccount.balanceisctx.balance(account). On Solanapot.balanceis the Pot’s lamports..<SYMBOL>reads only a token the chain registers. - A holder on an EVM chain is any address:
potorctx.self(), a variable or parameter, a protocol contract (UniswapV4.PoolManager), a contract at an address (UniswapV3.Pool(pool)), a call that returns an address, or a token contract (WETH.balanceis the ETH locked in WETH). - A holder on Solana must be known when the transaction is built, because its account is added to
main:pot,pot.owner, anAddressparameter ofmain, a token’s mint, or a 32-byte key written out. .balanceon a value that is not an address reads a field: a record, a list, a valueabi.decodereturns, or a struct a contract call returns. For the native coin of an address such a value holds, writectx.balance(x).
Token(address) has no short form: its balance is Token(address).balanceOf(holder).
The Pot’s owner and account names
pot.owner is the Pot’s owner on both targets. On EVM it reads the Pot’s owner(); a Shared Pot returns the zero address. On Solana it is the owner’s account, which signs the cook. pot.owner.USDC and pot.owner.balance read the owner’s token and native-coin balances.
On Solana, reading only pot.owner does not bind a Pot and permits --via engine. Use Account(key) for an account at a key, Account.pda(program, [seeds]) for a PDA, and Account.systemProgram, Account.tokenProgram, Account.token2022Program, Account.associatedTokenProgram or Account.memoProgram for standard programs. These accounts are read-only unless given a role: pot.owner.writable().signer().
An account’s .owner is its owning program. To read the program that owns the Pot account, write Account(pot.address).owner.
A declaration or import of pot or Account shadows that global in its scope; a compile-time binding also takes precedence. Account in a type position remains the account type. The Pot’s owner, balance and token-balance members are read-only; store values you need to change in your own locals. ctx.self(), ctx.balance(x) and ctx.selfBalance() remain valid.
Arc: one balance, two scales
On Arc the native coin is USDC, and the ERC-20USDC is the same balance at another scale: 18 decimals for the native coin, 6 for the token. pot.USDC reads the token and pot.balance the native coin:
arc-balances.sauce.ts
pot.USDC is the native balance divided by 10^12, rounded down, so less than 10^12 wei reads as 0. Convert one amount to the other’s scale before comparing them. EVM and Solana lists the native coins.
Token(address) and bound tokens
A token the registry does not carry isToken(address), with the address as a main parameter:
token-address.sauce.ts
Token(…) can be the receiver of a call, or be bound once to a local that is only ever called:
- the binding is declared once with
const, in the whole program; - it is never reassigned;
- it is used only as
t.method(…).
t to a function, as in show(t), fails with:
token-address.sauce.ts passes token, and bind Token(token) where it is called.
On Solana, Token(mint) takes the mint as an Address argument. Its token program is read from the cluster when the cook is built, and a mint whose program cannot be read is refused. See EVM and Solana.
Token methods
-
A token that returns nothing, such as USDT, works when the address has code. A
falsereturn, a malformed return, or an address with no code reverts the program. -
A write returns nothing, so its result cannot be used.
const ok = USDC.transfer(to, amount);fails with the message below; call the write as a statement, astoken-functions.sauce.tsdoes: - Approve the amount the next call spends and no more. An allowance that the program leaves standing outlives the program.
@sauce/token functions of the same names, which lists every function and what each one checks.
The same functions without a method
@sauce/token’s functions need no import. They take the token as the first argument:
token-functions.sauce.ts
Protocol namespaces
Every catalogued protocol has a namespace, in PascalCase (uniswap-v3 is UniswapV3), resolved for the compiled-for chain with its addresses and ABIs from the registry. A member is one of three kinds:
namespaces.sauce.ts
-
An interface is called with an address in the same expression:
UniswapV3.Pool(pool).slot0().UniswapV3.Pool.at(pool)is the same. A deployed contract’s name is never called with an address;.at(address)reaches another deployment of its ABI. -
A contract a call returns is called directly where the registry records what the returned address is:
found.liquidity()above, andErc4626.Vault(vault).asset().decimals(). Contracts and ABIs covers calls. -
A namespace’s default contract is called by the namespace alone when the namespace has exactly one deployed contract with an ABI.
AaveV3.supply(…)callsAaveV3.Pool.supply(…). A namespace with several contracts, such asUniswapV3, needs the contract’s name. -
A family alias groups a protocol’s versions.
UniswapcoversUniswapV2,UniswapV3andUniswapV4, andAavecoversAaveV2,AaveV3andAaveV4. An alias names a contract only when exactly one member has it on the chain.alias.sauce.tsUniswap.Factoryexists in V2 and V3, so it is ambiguous.return Uniswap.Factory;fails with the message below; name the version,UniswapV3.Factory, asnamespaces.sauce.tsdoes: -
A chain-qualified contract is another chain’s address and nothing more. A chain name qualifies the namespace:
The program still runs on the chain it is compiled for. A call through the qualified name is a call to that address on this chain, where it is some other contract or none, and the compiler does not stop it. Pass a qualified name to something that acts on the other chain; never call it. A token is never qualified: the symbol is always the compiled-for chain’s.qualified.sauce.ts
Using a name only where the chain has it
Every registry name is declared on every chain: each protocol namespace with every contract it has on any chain, family aliases included, and each token symbol some chain registers. Where the chain the program is compiled for lacks a contract or a token, the name is zero. A test of the name is decided when the program compiles, and the branch the test rules out is not compiled, so one program can name a venue or a token that some of its chains lack:presence.sauce.ts
- A test is the name in a condition:
if (X),!X,X != 0,X == 0,X ? a : b,X && …andX || ….Xis a contract (UniswapV3.Factory,Uniswap.UniversalRouter, or one on a named chain,Arbitrum.UniswapV3.Factory) or a token symbol (USDT). A guard that returns,if (!USDT) return 0;, leaves out the rest of its block whereUSDTis absent. - Any other use of a name the chain lacks is refused at the use, naming the chain: a call on it, passing it, returning it, or
const a = X. A zero never reaches the chain as a contract address.require(X != 0)on a chain withoutXis refused too, since it fails whatever the program does. - A decision is made only from registry names,
trueandfalse,!,&&,||and comparisons with0.X > 0is a use ofX, not a test. A decision held in a variable is live code: withconst has = USDT != 0; if (has) { USDT.transfer(to, amount); }, Base refuses the transfer. A helper function is checked as if it ran, so it tests the name itself. - Outside a condition,
X && YandX || Ygivetrueorfalse, and each side is a test:const both = USDT && UniswapV3.Factory;compiles on Base, whereUSDTis absent. On a contract they give a bool too:const factory = UniswapV3.Factory; return factory || 0;returnstrue, not the factory’s address. - A name no chain has is an error on every chain, tested or not, so a typo cannot drop a branch.
if (UniswapV9.Factory) { … }fails on Base with “UniswapV9is not a token or protocol on base. Did you meanUniswapV2orUniswapV3orUniswapV4? …”, and the same holds for a contract or method the registry does not have. - A program’s own declaration wins. A local, parameter or function named like a registry name is the program’s in the block it is declared in, from the declaration on. A type-only import (
import type { USDT }) declares nothing. - A program that declares its own function named
availablekeeps it. A registry name is tested by the name itself, never by a call.
import { USDT } from "@sauce/chain" fails with “@sauce/chain is the chain’s names, which a program reads with no import: write UniswapV3.Factory, USDC, Base.UniswapV3.Factory as they are.”
Chain names and chainId
- A chain name (
Base,Ethereum,Arc,Solana) qualifies another chain’s contract address, as above. Chains lists each name. chainIdis the id of the chain the program was compiled for. It is a constant folded into the bytecode.
chain-id.sauce.ts
ctx.chainId() reads the chain the program runs on. The two differ only when bytecode compiled for one chain is cooked on another.
Solana venue namespaces
Solana has no protocol namespaces. Each swap venue is a namespace of its own: its protocol, then its program.sauce globals --chain solana lists them under Venues.
orca.solana.sauce.ts
@sauce/router on Solana lists the venues.
Next steps
Contracts and ABIs
Call a namespace’s contracts, pass structs, destructure results.
@sauce libraries
@sauce/token and @sauce/router: every function and what it checks.EVM and Solana
Which names, builtins and accounts each target has.
Tokens reference
Every token symbol per chain.
