youthisguy 7fe3393179 feat: add mint authority model for LP-0013
- Added mint_authority: Option<AccountId> to TokenDefinition::Fungible
- NewFungibleDefinitionWithAuthority instruction
- SetAuthority instruction (rotate or revoke to None)
- Updated Mint with early authority guard
- Fully backwards compatible
- token_core SDK + regenerated SPEL IDL
- End-to-end demo script + tests
2026-06-19 09:53:43 +01:00

12 KiB

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<AccountId> 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<AccountId> 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):

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 — a standalone, reusable admin-authority crate (see RFP-001) - Any LEZ program needing the same single-admin pattern can depend on it directly:

[dependencies]
lez-authority = { path = "../../crates/lez-authority" }
use lez_authority::Authority;

let auth = Authority::from_option(definition.mint_authority);
auth.require(is_authorized)?; // Unauthorized or Renounced
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

Build & Test

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:

cd logos-execution-zone/bedrock
docker compose up

Terminal 2 — Sequencer (after bedrock shows "proposed block"):

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:

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:

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:

./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:

SEQUENCER_URL=http://127.0.0.1:3040 ./target/release/wallet token new-with-authority \
    --definition-account-id "Public/<DEF_ID>" \
    --supply-account-id "Public/<SUPPLY_ID>" \
    --name "Gold" \
    --total-supply 1000000 \
    --mint-authority "<DEF_ID>"

3. Verify on-chain — mint_authority is set:

SEQUENCER_URL=http://127.0.0.1:3040 ./target/release/wallet account get \
    --account-id "Public/<DEF_ID>"
# {"Fungible":{"name":"Gold","total_supply":1000000,"metadata_id":null,"mint_authority":"<DEF_ID>"}}

4. Mint additional tokens:

SEQUENCER_URL=http://127.0.0.1:3040 ./target/release/wallet token mint \
    --definition "Public/<DEF_ID>" \
    --holder "Public/<SUPPLY_ID>" \
    --amount 500000

5. Rotate authority to a new account:

SEQUENCER_URL=http://127.0.0.1:3040 ./target/release/wallet token set-authority \
    --definition-account-id "Public/<DEF_ID>" \
    --new-authority "<NEW_AUTHORITY_ID>"

6. Revoke authority permanently:

SEQUENCER_URL=http://127.0.0.1:3040 ./target/release/wallet token set-authority \
    --definition-account-id "Public/<DEF_ID>" \
    --new-authority "none"

7. Verify final state — mint_authority is null, total_supply updated:

SEQUENCER_URL=http://127.0.0.1:3040 ./target/release/wallet account get \
    --account-id "Public/<DEF_ID>"
# {"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:

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):

# 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/<DEF_ID>" \
    --supply-account-id "Public/<SUPPLY_ID>" \
    --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):

# Create with authority
SEQUENCER_URL=http://127.0.0.1:3040 ./target/release/wallet token new-with-authority \
    --definition-account-id "Public/<DEF_ID>" \
    --supply-account-id "Public/<SUPPLY_ID>" \
    --name "GovToken" \
    --total-supply 1000000 \
    --mint-authority "<DEF_ID>"

# Mint more later
SEQUENCER_URL=http://127.0.0.1:3040 ./target/release/wallet token mint \
    --definition "Public/<DEF_ID>" \
    --holder "Public/<SUPPLY_ID>" \
    --amount 250000

# Lock supply when ready
SEQUENCER_URL=http://127.0.0.1:3040 ./target/release/wallet token set-authority \
    --definition-account-id "Public/<DEF_ID>" \
    --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<AccountId> 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.

  • 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