USDC.balanceOf(pot)), and any other ERC-20 as Token(<address>). In TypeScript the
same token is a property of the chain’s export, Base.USDC, with its address, decimals and
symbol. Amounts are integers in the token’s smallest units: nothing scales them by decimals.
On Solana the token methods are the same (USDC.transfer(to, amount)): each call’s token
accounts are added to main and derived when the cook is built, and the symbol alone is the
mint’s 32-byte key, a value for the @sauce/token functions (balanceOf(USDC, account)). Each
Solana mint’s token program is legacy SPL Token or Token-2022, whose mints move by the checked
writes.
Every other chain registers no token: a program there uses
Token(<address>).
Balances and writes
A symbol is that chain’s token address: a value (balanceOf(USDC, account)) or the target of its
methods. Any other ERC-20 is Token(address), or bound once: const t = Token(address); (declared
once, never reassigned, used only as t.<method>(...)).
A balance is <holder>.<TOKEN>: any address or contract, then a token’s symbol. pot is the
Pot, and pot.USDC the USDC it holds, recipient.TSLAx the TSLAx recipient holds,
UniswapV4.PoolManager.WETH the WETH in the PoolManager. It is USDC.balanceOf(holder) written
short, and compiles to the same bytes. .<SYMBOL> is always a registered token; the native coin is
.balance (wei on an EVM chain, lamports on Solana): pot.balance is ctx.selfBalance(),
recipient.balance is ctx.balance(recipient). On Arc, whose coin is USDC, pot.USDC is the
token and pot.balance the coin. The holder is:
- on an EVM chain, any address:
pot,pot.owner(the Pot’s owner, itsowner(); the zero address on a Shared Pot, which has no owner), a variable or parameter, an address written out, a protocol contract (Aave.Pool), a contract at an address (UniswapV3.Pool(pool)orUniswapV3.Pool.at(pool): the address is the holder), or a call that returns an address (UniswapV4.StateView.poolManager()); a program that declares its ownpotowns the name, itspot.ownertoo, and reads that variable; - a token’s own contract too:
WETH.balanceis the ETH locked in WETH,USDC.WETHthe WETH the USDC contract holds (on Solana,USDC.balanceis the mint’s lamports,USDC.WSOLits own WSOL account’s balance); - on Solana, one known when the transaction is built, whose account is added to
main:pot,pot.owner(the Pot’s owner), anAddressparameter ofmain, a token’s mint, or a 32-byte key written out (Solana).
usdc: is a fine field name). Nor is an interface with no
address (UniswapV3.Pool.WETH). .balance is the coin only where the program’s text shows an
address. It reads the field on a record or a list, on a contract call’s result (unless the
registry records that result as an address: UniswapV4.StateView.poolManager().balance is the
PoolManager’s coin), on abi.decode’s result, on a name imported from a module, and on a local
bound to or destructured from one of those: write ctx.balance(x) for the coin there, or bind the
value to a local declared Address first. Token(address) has no short form: its balance is
Token(address).balanceOf(holder).
- Writes are SafeERC20: a token that returns nothing (USDT) works when it has code; a
falsereturn, a malformed return or an address with no code reverts the program. A write returns nothing, soconst ok = USDT.transfer(...)does not compile. - Approve exactly what the next call spends: a standing allowance outlives the program.
- The same library as functions, token first, with no import:
transfer(token, to, amount),transferFrom(token, from, to, amount),approve(token, spender, amount),balanceOf(token, account),allowance(token, owner, spender),totalSupply(token),decimals(token), and the signature pullspermitandpermitTransferFrom. The methods compile to exactly these; this page ends with every signature. - On Solana the methods are the same,
Token(mint)takes a mint given as anAddressargument, and compiling adds the token accounts each call moves through tomain, derived and checked (Solana).
IERC20 is low level, and only imported. import { IERC20 as Erc20 } from "./artifacts/IERC20.json" binds the raw ERC-20 ABI, for a call the token syntax does not offer. Its
writes decode the declared bool, so they are not SafeERC20 (USDT’s transfer reverts there, and
a false return passes): never write a transfer or an approval through it. Import it under a name
of its own, as here, in a file that also uses the token syntax, which binds IERC20 itself.
Ethereum
Optimism
Unichain
Polygon
Monad
Sei
Mantle
Arc
The native coin is USDC (18 decimals), and
USDC
is its ERC-20 face (6 decimals): one balance, read at two scales.
Arc has what that changes.
Base
Plasma
Arbitrum One
Ink
Berachain
Base Sepolia
Arc Testnet
The native coin is USDC (18 decimals), and
USDC
is its ERC-20 face (6 decimals): one balance, read at two scales.
Arc Testnet has what that changes.
Ethereum Sepolia
Solana
Solana Devnet
Solana Testnet
Tokenized stocks
A tokenized stock is a token like any other, named by its symbol with no import:pot.SPCXc, SPCXc.transfer(to, amount), a value for @sauce/token’s
functions, a token a swap buys or sells.
- The symbol is the issuer’s own ticker, in its case: in a program symbols are case-sensitive
(
SPCXCis notSPCXc; thesaucecommand’s token flags, such as--fund, match one without regard to case), a.is written_(BRK.BxisBRK_Bx, Backpack’sNVDA.USisNVDA_US), and PreStocks’ symbol is the company’s name (SPACEX). - One company has one token per issuer: SpaceX is
SPCXxandSPCXonon Ethereum,SPCXcon Base, andSPCXx,SPCXon,SPCX_USandSPACEXon Solana. Find one by name or ticker withsauce globals --chain base --search spacex, list a chain’s with--stocks, or read the stock tokens of each chain. - Buying or selling one needs no account with its issuer, and any contract can hold one, the Pot included; only minting and redeeming with the issuer need KYC.
- An issuer can pause its token, and refuses a sanctioned or blocked address.
- Backed’s tokens rebase: a balance is shares times a multiplier the issuer moves, so it changes between transactions, never within one. A transfer can credit one unit less than sent, so paying one into a pool that checks what it received can fail; buying one is unaffected.
- Ondo’s keep their balance: dividends raise the shares each token stands for, so its price follows the stock with dividends reinvested.
- Coinbase’s are B20 tokens, built into the Base node: they run on Base and not in a local copy
of the chain. Simulate a program that moves one with
sauce simulateagainst Base itself; no assumed balance can give a Pot one. - Each issuer’s list is copied from its own published list, and every row is checked on chain. Backpack’s mints it has not opened yet, and Superstate’s (allowlisted holders only), are not registered.
On Solana
A stock on Solana is a Token-2022 mint with the same methods:SPACEX.transfer(to, amount) lowers
to the checked write, transferChecked, with the mint’s decimals, and its symbol alone is the
mint’s 32-byte key (balanceOf(TSLAx, account)). Token-2022 and the accounts it moves through are
Solana’s; what is particular to the stocks:
- Amounts are raw, in the mint’s decimals. The issuer’s multiplier, by which a wallet shows them, moves for splits and dividends, so a raw unit is not always one share.
- PreStocks’ mints charge a transfer fee: the recipient gets less than was sent. Measure what arrived; the stock tokens of each chain marks each mint that charges one.
- Every stock mint has a transfer hook that names no program, which is no hook.
- The issuer can pause its mint, and can hold a permanent delegate that can move or burn any balance. A new token account is usable, not frozen.
Stock tokens by issuer
Token functions
Every program can call these@sauce/token functions with no import. The EVM writes are
SafeERC20: a token that returns nothing works, and one that returns false reverts.
On EVM chains:
balanceOf(token: Address, account: Account): Uint256totalSupply(token: Address): Uint256decimals(token: Address): Uint256allowance(token: Address, owner: Address, spender: Address): Uint256transfer(token: Address, to: Address, amount: Uint256): voidtransferFrom(token: Address, from: Address, to: Address, amount: Uint256): voidapprove(token: Address, spender: Address, amount: Uint256): voidpermit(token: Address, owner: Address, spender: Address, value: Uint256, deadline: Uint256, v: Uint256, r: Uint256, s: Uint256): voidpermitTransferFrom(token: Address, owner: Address, amount: Uint256, nonce: Uint256, deadline: Uint256, signature: bytes, permit2: Address): void
balanceOf(token: Address, account: Account): Uint256ownerOf(token: Address, account: Account): AddresstotalSupply(token: Account): Uint256decimals(token: Account): Uint256allowance(token: Address, account: Account, spender: Address): Uint256transfer(token: Address, tokenProgram: Account, source: Account, dest: Account, authority: Account, amount: Uint256): voidtransferFrom(token: Address, tokenProgram: Account, source: Account, dest: Account, authority: Account, amount: Uint256): voidapprove(token: Address, tokenProgram: Account, source: Account, delegate: Account, authority: Account, amount: Uint256): voidtransferChecked(token: Address, tokenProgram: Account, source: Account, mint: Account, dest: Account, authority: Account, amount: Uint256, decimals: Uint256): voidapproveChecked(token: Address, tokenProgram: Account, source: Account, mint: Account, delegate: Account, authority: Account, amount: Uint256, decimals: Uint256): void
Next steps
- Protocols and tokens: token methods and balances in a program.
- Safety: what Sauce checks, and what it cannot.
- Glossary: the words these pages use, in plain terms.
