302 lines
12 KiB
Markdown
Raw Normal View History

# 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](../../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/<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:**
```bash
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:**
```bash
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:**
```bash
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:**
```bash
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:**
```bash
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:
```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/<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):**
```bash
# 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`](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