# AMM on testnet — spel runbook End-to-end steps to deploy the programs, initialize the AMM, and create a pool using `spel`, in the order they must happen. Follow top to bottom; nothing here can be skipped. > **Golden rule:** every time you **recompile** a guest, its **ProgramId changes**, and > **every PDA derived from that ProgramId changes too** (config, pool, vaults, LP def, lp > lock, current tick). If you rebuild the AMM, you must recompute all AMM PDAs and > re-run `initialize`. Treat ProgramIds and PDAs as deployment-specific — re-derive them, > never reuse old values. --- ## Contents - [0. Prerequisites](#0-prerequisites) - [Argument formats](#argument-formats-used-throughout) - [1. Build & deploy the programs](#1-build--deploy-the-programs) - [Record the ProgramIds](#record-the-programids) - [2. Wallet CLI basics](#2-wallet-cli-basics) - [3. Create two token definitions](#3-create-two-token-definitions) - [(Optional) Transfer tokens](#optional-transfer-tokens) - [4. Derive the AMM PDAs](#4-derive-the-amm-pdas) - [All PDA helpers](#all-pda-helpers) - [5. Initialize the AMM](#5-initialize-the-amm) - [6. Create a pool (new-definition)](#6-create-a-pool-new-definition) - [7. Verify](#7-verify) - [8. (Optional) Create a TWAP price-observations account](#8-optional-create-a-twap-price-observations-account) - [9. Swap](#9-swap) - [10. Record a tick](#10-record-a-tick) - [11. Create the oracle price account](#11-create-the-oracle-price-account) - [12. Publish a price](#12-publish-a-price) - [Gotchas](#gotchas-we-hit-and-how-to-avoid-them) --- ## 0. Prerequisites - **Docker running** (guest builds cross-compile through it). - **`spel` / `wallet`** built from the [`refactor/lez-v020-compat`](https://github.com/0x-r4bbit/spel/tree/refactor/lez-v020-compat) branch (`github.com/0x-r4bbit/spel`). This is required for: the `program-id` command, the `--` subcommand separator, the public-tx signing scheme, and the `account_id` argument fix. - **Wallet home** exported in every shell you use (deploy *and* spel must point at the same wallet/network): ```bash export LEE_WALLET_HOME_DIR="$HOME/.lee/wallet" ``` - Wallet configured to reach your sequencer (`wallet_config.json`), with the accounts you need created (`wallet account ...`). ```bash wallet config set sequencer_addr https://testnet.lez.logos.co/ ``` ### Argument formats (used throughout) | Kind | Accepted forms | |---|---| | **account id** (PDAs, holdings, authority) | base58 (e.g. `9qbX…`) **or** `0x`-prefixed 32-byte hex. **No** `account_id( … )` wrapper. | | **program id** | 8 comma-separated u32 limbs, a bare 64-char ImageID hex, a `0x`-prefixed ImageID hex, **or** a base58 ImageID. (`spel program-id` prints the limbs and the bare hex; base58 is computed by the `*_pdas` helpers / yourself.) | --- ## 1. Build & deploy the programs **Note: Only do this if you want to deploy the protocols yourself!** Build and deploy **token**, **twap_oracle**, and **amm** (order doesn't matter for deploy, but you need all three before initializing the AMM): ```bash # repeat for: token, twap_oracle, amm cargo risczero build --manifest-path programs//methods/guest/Cargo.toml wallet deploy-program programs//methods/guest/target/riscv32im-risc0-zkvm-elf/docker/.bin ``` Binary path convention: `programs//methods/guest/target/riscv32im-risc0-zkvm-elf/docker/.bin` ### Record the ProgramIds ```bash spel -- program-id programs//methods/guest/target/riscv32im-risc0-zkvm-elf/docker/.bin ``` Record each ProgramId in whatever form you'll pass it — the forms are interchangeable (see the arg-format table above). `spel program-id` prints both the **decimal limbs** and the **64-char ImageID hex**; both are accepted by spel and the `*_pdas` helpers, and the ImageID hex is usually the easiest to copy. **base58 is also accepted as input**, but nothing in the toolchain prints it — base58-encode the 32 ImageID bytes yourself if you want that form. Fill in: | program | ProgramId (limbs, ImageID hex, or base58) | |---|---| | token | `` | | twap_oracle | `` | | amm | `` | ## 2. Wallet CLI basics The `wallet` CLI manages the accounts you'll pass to `spel` (definition targets, holdings, authority, LP holding). Every command needs `LEE_WALLET_HOME_DIR` set (see Prerequisites). Create a **public** account, giving it a `--label` you can recognize later: ```bash wallet account new public --label token-a-def ``` List the accounts the wallet owns (`ls` is an alias; add `-l`/`--long` for full details): ```bash wallet account list ``` The listing shows each account's label and base58 id. Use those ids as the ``, ``, ``, and `` arguments in the steps below. ## 3. Create two token definitions You need **two different** fungible tokens. For each, create two wallet accounts: ```bash wallet account new public --label "Token A Definition" wallet account new public --label "Token A Holding" wallet account new public --label "Token B Definition" wallet account new public --label "Token B Holding" ``` Confirm with (your addresses will be different): ```bash wallet account list /1 Public/4T69U868K6UzX8zbesU5wyr36gxaU7wb91Q45yedP4Rb [Token A Holding] /0 Public/CER21z16YgmWr3aN8FEHsrmfm2iRfQiwZTac3FQa21US [Token A Definition] /0/0 Public/EW2eoxcQyRDffrr94LkmuyByhaXx8emNzfDSAp9q29m5 [Token B Definition] /2 Public/EcWWrBekMaER4JRAzzP4rpB8TFD2eHvyYJxScQPwzmpE [Token B Holding] ``` Next, create the token definitions and holdings: ```bash spel --idl artifacts/token-idl.json \ --program programs/token/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/token.bin \ -- new-fungible-definition \ --name "TOKEN A" --total-supply 1000000000000000000000 \ --definition-target-account \ --holding-target-account ``` - `` becomes the token-definition account; `` receives `total_supply`. - Repeat for token B (``, ``). - These `` ids are what feed all the AMM pool PDAs. Inspect to confirm / find a holding's definition id: ```bash spel --idl artifacts/token-idl.json inspect --type TokenHolding spel --idl artifacts/token-idl.json inspect --type TokenDefinition ``` ### (Optional) Transfer tokens Move fungible tokens between holdings with the **token** program's `transfer`. Standalone — usable any time, independent of the AMM. spel signs the **sender** (you must own its key) and credits the **recipient**; both must be holdings of the **same** token definition. The recipient must be an **already-initialized** holding of that token. (`transfer` *can* credit an empty account by claiming a fresh holding, but that claim needs the recipient's signature, which `transfer` doesn't collect — so initialize the holding first.) Create one with `initialize-account` (the new holding is a **signer** — your wallet must hold its key): ```bash spel --idl artifacts/token-idl.json \ --program programs/token/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/token.bin \ -- initialize-account \ --definition-account \ --account-to-initialize ``` Then transfer (`amount-to-transfer` is in **base units** — e.g. `1000000000000000000` = one whole token at 18 decimals): ```bash spel --idl artifacts/token-idl.json \ --program programs/token/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/token.bin \ -- transfer \ --sender \ --recipient \ --amount-to-transfer ``` - `` — a holding you own (spel signs it; balance must be ≥ ``). - `` — an existing holding of the **same** `` (initialize it first, above). - Both holdings must share the same definition, or the transfer aborts with `Sender and recipient definition id mismatch`. Verify the recipient's new balance: ```bash spel --idl artifacts/token-idl.json inspect --type TokenHolding ``` ## 4. Derive the AMM PDAs Program PDAs use a **SHA-256 seed scheme** (in each `*_core`), which is **not** what `spel pda` computes (that pads raw bytes) — so `spel pda` will give wrong addresses. Each program ships a committed `examples/*_pdas` helper that derives its PDAs with its own `*_core` functions, guaranteeing they match the guest. They take ProgramIds as comma- separated u32 limbs, a 64-char ImageID hex, or a base58 ImageID (any form `spel program-id` prints, plus base58), and account ids as base58. ```bash # config only (for step 5): cargo run -q -p amm_program --example amm_pdas -- "" # config + all pool PDAs (for step 6): cargo run -q -p amm_program --example amm_pdas -- \ "" "" "" "" ``` The fixed **clock** account never changes: `4BdcjoXkq786TMWcBGGHqcxeLYMZmn17rL4eM9ZyRWNU` (`/LEZ/ClockProgramAccount/0000001`). ### All PDA helpers | program | command | prints | |---|---|---| | amm | `cargo run -q -p amm_program --example amm_pdas -- [ ]` | config; + pool, vault_a/b, pool_definition_lp, lp_lock_holding, current_tick_account | | twap_oracle | `cargo run -q -p twap_oracle_program --example twap_oracle_pdas -- []` | current_tick_account; + price_observations, oracle_price_account (with a window) | | ata | `cargo run -q -p ata_program --example ata_pdas -- ` | the ATA address | | stablecoin | `cargo run -q -p stablecoin_program --example stablecoin_pdas -- ` | position, position_vault | (The token program has no PDAs.) ## 5. Initialize the AMM Pick an `` account you control (the admin who may later `update_config`). `config` is the PDA from step 4; `token`/`twap` are the ProgramIds from step 1. ```bash spel --idl artifacts/amm-idl.json \ --program programs/amm/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/amm.bin \ -- initialize \ --config \ --token-program-id \ --twap-oracle-program-id \ --authority ``` > `config` is `init` but **not** a signer — spel just lists it; the guest claims it as a PDA. > Run once per deployment. (Add `--dry-run` to preview.) ## 6. Create a pool (`new-definition`) Provide three holding accounts you own: - `` / `` — your holdings of token A / B (**signers**; must hold ≥ the deposit amounts). - `` — a **fresh wallet account whose key you hold** (now a **signer**; it must be a normal keypair account, **not** an ATA). Receives the LP tokens. Amounts must satisfy `isqrt(token_a_amount * token_b_amount) > 1000` (MINIMUM_LIQUIDITY). `fees` ∈ {1, 5, 30, 100} (bps). `deadline` is a future ms timestamp (or `u64::MAX` = `18446744073709551615` to ignore). ```bash spel --idl artifacts/amm-idl.json \ --program programs/amm/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/amm.bin \ -- new-definition \ --config \ --pool \ --vault-a \ --vault-b \ --pool-definition-lp \ --lp-lock-holding \ --user-holding-a \ --user-holding-b \ --user-holding-lp \ --current-tick-account \ --clock 4BdcjoXkq786TMWcBGGHqcxeLYMZmn17rL4eM9ZyRWNU \ --token-a-amount \ --token-b-amount \ --fees 1 \ --deadline 18446744073709551615 ``` > spel signs `user_holding_a`, `user_holding_b`, and `user_holding_lp` (the IDL `signer` > accounts) — your wallet must hold all three keys. No `--bin-*` is needed: the chained > token/oracle calls are resolved on-chain from the ProgramIds stored in `config`. > Pair `` with ``/`` consistently (and B with B). ## 7. Verify ```bash spel --idl artifacts/amm-idl.json inspect --type PoolDefinition ``` Check `reserve_a`/`reserve_b`, `liquidity_pool_supply`, and `fees`. ## 8. (Optional) Create a TWAP price-observations account A TWAP feed for the pool over a time window. `window_duration` is in **milliseconds** (e.g. 24h = `86400000`). The observations account is a PDA of `(twap_pid, pool, window)`, so each window has its own account. Derive it (the `current_tick_account` is the same one from the pool): ```bash cargo run -q -p twap_oracle_program --example twap_oracle_pdas -- \ "" # prints current_tick_account, price_observations, oracle_price_account ``` ```bash spel --idl artifacts/amm-idl.json \ --program programs/amm/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/amm.bin \ -- create-price-observations \ --config \ --pool \ --current-tick-account \ --price-observations \ --clock 4BdcjoXkq786TMWcBGGHqcxeLYMZmn17rL4eM9ZyRWNU \ --window-duration ``` > **No signers** — this instruction is permissionless: the AMM authorizes the new > observations PDA via the pool's PDA seed in a chained call to the TWAP oracle, so spel > submits an empty witness set. Prereqs: the pool and its `current_tick_account` must > already exist (the latter was created during `new_definition`); the oracle seeds the first > observation from `current_tick_account`, so the price can't be forged. No `--bin-*` needed > (the TWAP ProgramId is read from `config`). The `price_observations` PDA must not already > exist (`init`). Verify: ```bash spel --idl artifacts/twap_oracle-idl.json inspect --type PriceObservations ``` ## 9. Swap Swap exact input → output. Uses the pool PDAs from step 4 plus your two token holdings. `--token-definition-id-in` picks the direction (pass the definition id of the token you're spending). ```bash spel --idl artifacts/amm-idl.json \ --program programs/amm/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/amm.bin \ -- swap-exact-input \ --config \ --pool \ --vault-a \ --vault-b \ --user-holding-a \ --user-holding-b \ --current-tick-account \ --clock 4BdcjoXkq786TMWcBGGHqcxeLYMZmn17rL4eM9ZyRWNU \ --swap-amount-in \ --min-amount-out \ --token-definition-id-in \ --deadline 18446744073709551615 ``` - **`--token-definition-id-in`**: `` ⇒ A→B; `` ⇒ B→A. - **`--swap-amount-in`** must be ≤ the input holding's balance; **`--min-amount-out`** is the slippage floor (`1` accepts any nonzero output). - spel signs **both** `user_holding_a` and `user_holding_b` (both are `signer` in the IDL — the input side is dynamic, so both are marked; see the gotcha below). Pass both even though only the input side is debited. - No `--bin-*` needed (token program id read from `config`). `swap-exact-output` is the same account set with `--exact-amount-out` / `--max-amount-in` instead. Verify (input reserve up by the input, output reserve down by the output): ```bash spel --idl artifacts/amm-idl.json inspect --type PoolDefinition ``` ## 10. Record a tick `record_tick` appends an observation to a window's `price_observations` account from the current tick. It's a **direct TWAP oracle call** (not AMM-chained), so it uses the twap_oracle binary + IDL. **No signers** — it's permissionless: it reads `current_tick_account` + `clock` and writes `price_observations`. Run a swap (step 9) first so `current_tick_account` holds a fresh tick, then: ```bash spel --idl artifacts/twap_oracle-idl.json \ --program programs/twap_oracle/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/twap_oracle.bin \ -- record-tick \ --price-observations \ --current-tick-account \ --clock 4BdcjoXkq786TMWcBGGHqcxeLYMZmn17rL4eM9ZyRWNU \ --price-source-id \ --window-duration ``` - `--price-source-id` is the **pool** (an `account_id` arg, not a listed account). - `--window-duration` must **match** the value the `price_observations` account was created with (step 8) — it's part of that PDA's seeds, so a mismatch points at a different/missing account. - Prereqs: the observations account exists (step 8) and `current_tick_account` holds a fresh tick (run step 9 first). Verify: ```bash spel --idl artifacts/twap_oracle-idl.json inspect --type PriceObservations ``` ## 11. Create the oracle price account `publish_price` (step 12) writes into an `oracle_price_account` that must **already exist**, so create it first. The oracle's own `create_oracle_price_account` requires the `price_source` (the pool) to **sign**, which a PDA can't do — so the **AMM** creates it on the pool's behalf via a chained, PDA-authorized call. **Permissionless** (no signers) and `init`, so run it **once** per `(pool, window)`. Derive `oracle_price_account` (same twap helper as step 8, with the window): ```bash cargo run -q -p twap_oracle_program --example twap_oracle_pdas -- \ "" # prints current_tick_account, price_observations, oracle_price_account ``` ```bash spel --idl artifacts/amm-idl.json \ --program programs/amm/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/amm.bin \ -- create-oracle-price-account \ --config \ --pool \ --oracle-price-account \ --clock 4BdcjoXkq786TMWcBGGHqcxeLYMZmn17rL4eM9ZyRWNU \ --window-duration ``` > `--window-duration` must match the observations account's window (step 8) — it seeds the > `oracle_price_account` PDA. `init`, so skip this step if the account already exists. ## 12. Publish a price Computes the TWAP from the observations ring buffer (extended to *now* using the current tick) and writes it to the `oracle_price_account`. **Direct TWAP oracle call, permissionless.** > **Needs at least two observations.** With fewer than two, `publish_price` is a silent > **no-op** — it returns every account unchanged and writes nothing. The first observation is > seeded when you create the observations account (step 8); the second comes from `record_tick` > (step 10). That's why we swap (step 9) to move the tick and then record it before publishing. ```bash spel --idl artifacts/twap_oracle-idl.json \ --program programs/twap_oracle/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/twap_oracle.bin \ -- publish-price \ --price-observations \ --oracle-price-account \ --current-tick-account \ --clock 4BdcjoXkq786TMWcBGGHqcxeLYMZmn17rL4eM9ZyRWNU \ --price-source-id \ --window-duration ``` - `--price-source-id` is the **pool** (an `account_id` arg, not a listed account). - Re-runnable any time to refresh the published TWAP (unlike step 11's one-time `init`). - `--window-duration` must match steps 8 and 11 — it seeds all three related PDAs. Verify (check `price` and the publication timestamp): ```bash spel --idl artifacts/twap_oracle-idl.json inspect --type OraclePriceAccount ``` --- ## Gotchas we hit (and how to avoid them) - **Recompile ⇒ new ProgramId ⇒ new PDAs.** Redo steps 4–6 after any AMM rebuild. The config/pool addresses are functions of the AMM ProgramId. - **`account_id` args must be bare base58/`0x`-hex** — strip the wallet's `account_id( … )` display wrapper. - **`spel pda` does NOT match these PDAs.** It uses a padded-bytes seed scheme; the `*_core` crates hash with SHA-256. Always use the committed `examples/*_pdas` helpers (step 4). - **`user_holding_lp` is a signer** for `new_definition` (its LP-token definition is created in the same tx, so the holding must be a fresh signed keypair, not an ATA). For `add_liquidity` the LP definition already exists, so an existing/pre-created holding works without a signature. - **Both swap holdings are signers.** `swap_exact_input`/`swap_exact_output` mark *both* `user_holding_a` and `user_holding_b` `signer`, because the input side (a token `Transfer` sender, which must be authorized) is chosen at runtime via `--token-definition-id-in`. So pass and sign both even though only the input is debited. (Re-creating a pool also needs a **fresh** `user_holding_lp` — the previous one is bound to the old pool's LP token.) - **Same `LEE_WALLET_HOME_DIR` everywhere.** Deploying with one wallet home and running spel with another points them at different networks/keys.