# LP-0013: Token Program - Mint Authority This branch adds a mint authority model to the LEZ token program, enabling variable supply tokens, permissioned issuance, and the standard "revoke to fix supply" pattern expected by wallets and DeFi protocols. ## What Changed The existing token program supported creating tokens with a fixed total supply but had no mechanism to control who could mint additional tokens after creation. This PR adds: - `mint_authority: Option` field on `TokenDefinition::Fungible` - `NewFungibleDefinitionWithAuthority` instruction — create a token and set a mint authority at initialization - `SetAuthority` instruction — rotate the authority to a new account, or revoke it permanently by setting it to `None` - Updated `Mint` instruction — now enforces that `mint_authority` is `Some` before allowing minting - Fully backwards compatible — the existing `NewFungibleDefinition` instruction still works, creating tokens with `mint_authority: None` (fixed supply) The design follows Solana's SPL Token authority model: a single `Option` field simultaneously encodes who the authority is and whether minting is possible. `None` is self-describing — no authority, no minting, ever. ## Files Changed | File | Change | |---|---| | `programs/token/core/src/lib.rs` | Added `mint_authority` field to `TokenDefinition::Fungible`; added `NewFungibleDefinitionWithAuthority` and `SetAuthority` instruction variants | | `programs/token/src/new_definition.rs` | Added `new_fungible_definition_with_authority()` function | | `programs/token/src/set_authority.rs` | New file — implements authority rotation and revocation | | `programs/token/src/mint.rs` | Added authority check before minting | | `programs/token/src/lib.rs` | Exposed `set_authority` module | | `programs/token/methods/guest/src/bin/token.rs` | Wired both new instructions into the SPEL guest dispatch | | `programs/token/src/tests.rs` | 8 new unit tests covering every authority state transition | | `programs/integration_tests/tests/token.rs` | 3 new integration tests running against a real `V03State` | ## Authority Lifecycle ``` NewFungibleDefinitionWithAuthority mint_authority: Some(A) ──▶ minting allowed (only A can mint) │ ├── SetAuthority(Some(B)) ──▶ authority transferred to B │ └── SetAuthority(None) ──▶ supply permanently fixed │ └── Mint ──▶ PANIC: "Mint authority has been revoked; supply is fixed" ``` ### Atomicity Authority rotation and revocation are atomic. The RISC Zero zkVM either commits the full output state or panics — there is no partial write. A failed `SetAuthority` call leaves the authority unchanged. ### Error Codes Authority-related panics now originate from `lez-authority`'s `AuthorityError` (see [docs/authority-model.md](../../docs/authority-model.md#admin-authority-library-rfp-001)): | Panic message | Condition | |---|---| | `"Mint authority has been revoked; supply is fixed"` | `Mint` called when `mint_authority` is `None` (wraps `AuthorityError::Renounced`) | | `"Renounced: authority has been permanently revoked"` | `SetAuthority` called when authority already `None` | | `"Unauthorized: caller is not the current authority"` | `SetAuthority` or `Mint` called without the correct signature | | `"Cannot set mint authority on a Non-Fungible Token definition"` | `SetAuthority` called on an NFT definition | ## SDK The SDK is `token_core` — the same crate modified in this PR. Downstream consumers import `token_core::Instruction` and get both new variants automatically. No separate SDK crate is needed; this follows the same pattern as `amm_core`, `stablecoin_core`, and `ata_core`. ## Authority Enforcement Authorization checks in `mint.rs` and `set_authority.rs` are implemented via [`lez-authority`](../../crates/lez-authority) — a standalone, reusable admin-authority crate (see [RFP-001](https://github.com/logos-co/rfp/blob/master/RFPs/RFP-001-admin-authority-lib.md)) - Any LEZ program needing the same single-admin pattern can depend on it directly: ```toml [dependencies] lez-authority = { path = "../../crates/lez-authority" } ``` ```rust use lez_authority::Authority; let auth = Authority::from_option(definition.mint_authority); auth.require(is_authorized)?; // Unauthorized or Renounced ``` ```rust use token_core::Instruction; // Create a token with mint authority let ix = Instruction::NewFungibleDefinitionWithAuthority { name: String::from("Gold"), total_supply: 1_000_000, mint_authority: Some(authority_account_id), }; // Rotate authority let ix = Instruction::SetAuthority { new_authority: Some(new_authority_id), }; // Revoke authority permanently let ix = Instruction::SetAuthority { new_authority: None, }; ``` ## Prerequisites - Rust (stable) — install via [rustup](https://rustup.rs/) - RISC Zero toolchain: ```bash curl -L https://risczero.com/install | bash rzup install ``` - LEZ wallet and sequencer from [logos-blockchain/logos-execution-zone](https://github.com/youthisguy/logos-execution-zone) ## Build & Test ```bash git clone https://github.com/youthisguy/lez-programs.git cd lez-programs # Run all tests (skips ZK proof generation) RISC0_DEV_MODE=1 cargo test --release # Run token-specific unit tests RISC0_DEV_MODE=1 cargo test --release -p token_program # Run token integration tests RISC0_DEV_MODE=1 cargo test --release -p integration_tests --test token ``` All 245+ tests pass. The 8 new unit tests and 3 new integration tests are included in the count. ## End-to-End Demo ### Prerequisites Start all three services in separate terminals: **Terminal 1 — Bedrock:** ```bash cd logos-execution-zone/bedrock docker compose up ``` **Terminal 2 — Sequencer (after bedrock shows "proposed block"):** ```bash cd logos-execution-zone/lez/sequencer/service RUST_LOG=info RISC0_DEV_MODE=1 cargo run --release -p sequencer_service configs/debug/sequencer_config.json ``` **Terminal 3 — Indexer:** ```bash cd logos-execution-zone/lez/indexer/service RUST_LOG=info RISC0_DEV_MODE=1 cargo run --release -p indexer_service configs/indexer_config.json ``` **Terminal 4 — Wallet commands:** ```bash export LEZ_WALLET_HOME_DIR=logos-execution-zone/lez/wallet/configs/debug cd logos-execution-zone SEQUENCER_URL=http://127.0.0.1:3040 ./target/release/wallet check-health # ✅ All looks good! ``` ### Demo Walkthrough **1. Create accounts:** ```bash ./target/release/wallet account new public --label "token-def" ./target/release/wallet account new public --label "token-supply" ./target/release/wallet account new public --label "new-authority" ``` **2. Create a token WITH mint authority:** ```bash SEQUENCER_URL=http://127.0.0.1:3040 ./target/release/wallet token new-with-authority \ --definition-account-id "Public/" \ --supply-account-id "Public/" \ --name "Gold" \ --total-supply 1000000 \ --mint-authority "" ``` **3. Verify on-chain — mint_authority is set:** ```bash SEQUENCER_URL=http://127.0.0.1:3040 ./target/release/wallet account get \ --account-id "Public/" # {"Fungible":{"name":"Gold","total_supply":1000000,"metadata_id":null,"mint_authority":""}} ``` **4. Mint additional tokens:** ```bash SEQUENCER_URL=http://127.0.0.1:3040 ./target/release/wallet token mint \ --definition "Public/" \ --holder "Public/" \ --amount 500000 ``` **5. Rotate authority to a new account:** ```bash SEQUENCER_URL=http://127.0.0.1:3040 ./target/release/wallet token set-authority \ --definition-account-id "Public/" \ --new-authority "" ``` **6. Revoke authority permanently:** ```bash SEQUENCER_URL=http://127.0.0.1:3040 ./target/release/wallet token set-authority \ --definition-account-id "Public/" \ --new-authority "none" ``` **7. Verify final state — mint_authority is null, total_supply updated:** ```bash SEQUENCER_URL=http://127.0.0.1:3040 ./target/release/wallet account get \ --account-id "Public/" # {"Fungible":{"name":"Gold","total_supply":1500000,"metadata_id":null,"mint_authority":null}} ``` ## Running the e2e Demo Script After setting up all three services (bedrock, sequencer, indexer), run: ```bash RISC0_DEV_MODE=0 \ WALLET_BIN=/path/to/logos-execution-zone/target/release/wallet \ LEZ_WALLET_HOME_DIR=/path/to/logos-execution-zone/lez/wallet/configs/debug \ bash scripts/demo.sh ``` ## Example Integrations Two example Rust programs are in `examples/program_deployment/src/bin/`: **Fixed supply token (authority revoked at creation):** ```bash # Create token with no authority — supply is fixed immediately SEQUENCER_URL=http://127.0.0.1:3040 ./target/release/wallet token new-with-authority \ --definition-account-id "Public/" \ --supply-account-id "Public/" \ --name "FixedCoin" \ --total-supply 21000000 \ --mint-authority "none" # mint_authority: null from the start — nobody can ever mint more ``` **Variable supply token (authority set, then used):** ```bash # Create with authority SEQUENCER_URL=http://127.0.0.1:3040 ./target/release/wallet token new-with-authority \ --definition-account-id "Public/" \ --supply-account-id "Public/" \ --name "GovToken" \ --total-supply 1000000 \ --mint-authority "" # Mint more later SEQUENCER_URL=http://127.0.0.1:3040 ./target/release/wallet token mint \ --definition "Public/" \ --holder "Public/" \ --amount 250000 # Lock supply when ready SEQUENCER_URL=http://127.0.0.1:3040 ./target/release/wallet token set-authority \ --definition-account-id "Public/" \ --new-authority "none" ``` ## Compute Unit Costs Measured on LEZ devnet (local sequencer standalone mode — devnet == localnet). Run with `RISC0_DEV_MODE=0` — real ZK proofs generated. Reproducible: clone repo, run `scripts/demo.sh` with `RISC0_DEV_MODE=0`, observe `execution time:` lines in sequencer logs. | Operation | Tx Hash | Block | Execution Time (RISC0_DEV_MODE=0) | |---|---|---|---| | `NewFungibleDefinitionWithAuthority` | `14197f9113ff000e81b7545c671942b286ef19bae7122ba280a0a620b8e01ca1` | 410 | 15.92ms | | `Mint` (authority active) | `99f00dbe40600d0c8bb745b74980c2241f1e7a6daa1291f5cef6b9ea27c82bd9` | 411 | 19.29ms | | `SetAuthority` (rotate) | `d865e26dfb5f82a5528aa9a0882307a73b00ffc4fa7825f0e7b5d0888d5c87fc` | 414 | 13.40ms | | `SetAuthority` (revoke to None) | `9408ef7ffd3efdbafbe2dd5bf243da32edd1a4d52f9709b5cfc92cb696b8956e` | 415 | 15.74ms | | `Mint` (rejected — authority revoked) | `5228cc62094a91e479b86a3aee067809f18674465ac72d8623d1ed770ab496de` | 416 | 9.84ms | Rejected operations cost ~38% less than successful ones because execution halts at the authority guard before any account writes — confirming rejection is via the correct code path (`"Mint authority has been revoked; supply is fixed"`). ## Design Decisions **Why `Option` and not a separate `is_fixed_supply: bool`?** One field encodes both who the authority is and whether minting is possible. `None` is self-describing — no authority, no minting, ever. Separate fields risk inconsistent state (`is_fixed_supply: false` but `mint_authority: None`). **Why is `SetAuthority` a separate instruction and not part of `NewDefinition`?** Separation of concerns. Creation and authority management are different operations with different signers and different lifecycle stages. This also matches SPL Token's design. **Why does `mint.rs` check `is_none()` rather than comparing account IDs?** The LEZ authorization model sets `is_authorized: true` on an account when the transaction includes a valid signature for that account. The program trusts the protocol's authorization flag rather than re-implementing signature verification. This is the correct pattern for all LEZ programs. **Why does `Authority::require()` check revocation before authorization?** If `mint_authority` is `None`, there is no authorized caller possible — the clearer error is `Renounced` rather than `Unauthorized`. This ordering lives in `lez-authority` itself, not in the token program, so every consumer of the library gets the same precedence for free. ## Related Repos - [`youthisguy/logos-execution-zone`](https://github.com/youthisguy/logos-execution-zone) — mirrored changes to the sequencer, wallet CLI, and guest binary. The wallet CLI gains two new commands: `token new-with-authority` and `token set-authority`. ## License MIT