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 onTokenDefinition::FungibleNewFungibleDefinitionWithAuthorityinstruction — create a token and set a mint authority at initializationSetAuthorityinstruction — rotate the authority to a new account, or revoke it permanently by setting it toNone- Updated
Mintinstruction — now enforces thatmint_authorityisSomebefore allowing minting - Fully backwards compatible — the existing
NewFungibleDefinitioninstruction still works, creating tokens withmint_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
- Rust (stable) — install via rustup
- RISC Zero toolchain:
curl -L https://risczero.com/install | bash rzup install - LEZ wallet and sequencer from logos-blockchain/logos-execution-zone
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.
Related Repos
youthisguy/logos-execution-zone— mirrored changes to the sequencer, wallet CLI, and guest binary. The wallet CLI gains two new commands:token new-with-authorityandtoken set-authority.
License
MIT