Skip to main content
A program compiled for a chain names that chain’s registered tokens by symbol, with no import (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, its owner(); 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) or UniswapV3.Pool.at(pool): the address is the holder), or a call that returns an address (UniswapV4.StateView.poolManager()); a program that declares its own pot owns the name, its pot.owner too, and reads that variable;
  • a token’s own contract too: WETH.balance is the ETH locked in WETH, USDC.WETH the WETH the USDC contract holds (on Solana, USDC.balance is the mint’s lamports, USDC.WSOL its 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), an Address parameter of main, a token’s mint, or a 32-byte key written out (Solana).
A record or a list is not a holder: on one, a property named like a token is refused, since the name is reserved for the shorthand (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 false return, a malformed return or an address with no code reverts the program. A write returns nothing, so const 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 pulls permit and permitTransferFrom. 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 an Address argument, and compiling adds the token accounts each call moves through to main, 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 (SPCXC is not SPCXc; the sauce command’s token flags, such as --fund, match one without regard to case), a . is written _ (BRK.Bx is BRK_Bx, Backpack’s NVDA.US is NVDA_US), and PreStocks’ symbol is the company’s name (SPACEX).
  • One company has one token per issuer: SpaceX is SPCXx and SPCXon on Ethereum, SPCXc on Base, and SPCXx, SPCXon, SPCX_US and SPACEX on Solana. Find one by name or ticker with sauce 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 simulate against 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): Uint256
  • totalSupply(token: Address): Uint256
  • decimals(token: Address): Uint256
  • allowance(token: Address, owner: Address, spender: Address): Uint256
  • transfer(token: Address, to: Address, amount: Uint256): void
  • transferFrom(token: Address, from: Address, to: Address, amount: Uint256): void
  • approve(token: Address, spender: Address, amount: Uint256): void
  • permit(token: Address, owner: Address, spender: Address, value: Uint256, deadline: Uint256, v: Uint256, r: Uint256, s: Uint256): void
  • permitTransferFrom(token: Address, owner: Address, amount: Uint256, nonce: Uint256, deadline: Uint256, signature: bytes, permit2: Address): void
On Solana:
  • balanceOf(token: Address, account: Account): Uint256
  • ownerOf(token: Address, account: Account): Address
  • totalSupply(token: Account): Uint256
  • decimals(token: Account): Uint256
  • allowance(token: Address, account: Account, spender: Address): Uint256
  • transfer(token: Address, tokenProgram: Account, source: Account, dest: Account, authority: Account, amount: Uint256): void
  • transferFrom(token: Address, tokenProgram: Account, source: Account, dest: Account, authority: Account, amount: Uint256): void
  • approve(token: Address, tokenProgram: Account, source: Account, delegate: Account, authority: Account, amount: Uint256): void
  • transferChecked(token: Address, tokenProgram: Account, source: Account, mint: Account, dest: Account, authority: Account, amount: Uint256, decimals: Uint256): void
  • approveChecked(token: Address, tokenProgram: Account, source: Account, mint: Account, delegate: Account, authority: Account, amount: Uint256, decimals: Uint256): void

Next steps