# lez-authority A reusable single-admin authority library for LEZ programs, satisfying [RFP-001: Admin Authority Library](https://github.com/logos-co/rfp/blob/master/RFPs/RFP-001-admin-authority-lib.md). Provides standardised access control for LEZ programs where privileged functions can only be called by a designated admin authority. The authority can transfer control to a new signer or permanently renounce it. There can only be one admin authority at a time. ## Why Without a shared library, every LEZ program that needs "only the admin can call this" logic ends up re-implementing it slightly differently — inconsistent error messages, inconsistent revocation semantics, inconsistent edge-case handling. `lez-authority` gives every program the same primitive, the same error types, and the same tested behavior. ## Install Add to your program's `Cargo.toml`: ```toml [dependencies] lez-authority = { path = "../../crates/lez-authority" } ``` (Adjust the relative path to wherever your program lives relative to `crates/lez-authority`.) ## Core type ```rust pub struct Authority(Option); ``` `Authority` wraps `Option` — the same representation you'd store on-chain in an account's data: - `Some(id)` — authority is active; only `id` may call privileged instructions. - `None` — authority has been permanently renounced. This state is terminal. There's no separate `is_revoked: bool` to keep in sync — the `Option` *is* the state. ## API | Method | Description | |---|---| | `Authority::new(id)` | Construct an active authority owned by `id` | | `Authority::renounced()` | Construct an already-revoked authority | | `Authority::from_option(opt)` | Build from on-chain `Option` storage | | `.into_option()` | Convert back to `Option` for on-chain storage | | `.is_active()` / `.is_renounced()` | Query current state | | `.account_id()` | Get the current authority's `AccountId`, if active | | `.require(is_authorized)` | Gate a privileged call. Errors if renounced or unauthorized | | `.rotate(new_id, is_authorized)` | Transfer authority to `new_id`. Errors if renounced or unauthorized | | `.revoke(is_authorized)` | Permanently renounce. Errors if already renounced or unauthorized | ## Errors ```rust pub enum AuthorityError { Unauthorized, // caller is not the current authority Renounced, // authority has been permanently revoked } ``` Both variants implement `Display`, so `panic!("{e}")` in a guest program produces a clear, deterministic message that the sequencer surfaces as the transaction's rejection reason. ## Usage example This is the actual pattern used by the LEZ token program to gate minting: ```rust use lez_authority::Authority; pub fn mint( definition_account: AccountWithMetadata, /* ... */ ) -> Vec { let mut definition = TokenDefinition::try_from(&definition_account.account.data) .expect("Definition account must be valid"); match &definition { TokenDefinition::Fungible { mint_authority, .. } => { let auth = Authority::from_option(*mint_authority); auth.require(definition_account.is_authorized) .unwrap_or_else(|e| panic!("{e}")); } TokenDefinition::NonFungible { .. } => { panic!("Cannot mint additional supply for Non-Fungible Tokens"); } } // ... proceed with minting } ``` And gating authority rotation/revocation: ```rust use lez_authority::Authority; pub fn set_authority( definition_account: AccountWithMetadata, new_authority: Option, ) -> Vec { let mut definition = TokenDefinition::try_from(&definition_account.account.data) .expect("Definition account must be valid"); match &mut definition { TokenDefinition::Fungible { mint_authority, .. } => { let mut auth = Authority::from_option(*mint_authority); match new_authority { Some(new_id) => auth .rotate(new_id, definition_account.is_authorized) .unwrap_or_else(|e| panic!("{e}")), None => auth .revoke(definition_account.is_authorized) .unwrap_or_else(|e| panic!("{e}")), } *mint_authority = auth.into_option(); } TokenDefinition::NonFungible { .. } => { panic!("Cannot set mint authority on a Non-Fungible Token definition"); } } // ... write post-state } ``` ## The `require_authority!` macro For guest programs that prefer macro-style gating over manual `match`/`unwrap_or_else`: ```rust use lez_authority::{Authority, require_authority}; let auth = Authority::from_option(stored_authority); require_authority!(auth, is_authorized); // continues only if authorized; panics with a clear message otherwise ``` ## Reference integration The LEZ token program (`programs/token/src/mint.rs` and `programs/token/src/set_authority.rs`) is the reference consumer of this library. Read those two files for a complete, tested, production integration — including how the on-chain `Option` field round-trips through `Authority::from_option` / `.into_option()`. ## Design notes **Why a single `Authority` type instead of free functions?** Bundling the `Option` state with its operations means `rotate`/`revoke` can enforce their own preconditions (`require()` runs first) without the caller having to remember to check first. Misuse is harder. **Why does `require()` check "renounced" before "unauthorized"?** If authority has been renounced, there is no valid signer who could possibly satisfy the check — `Renounced` is the more informative error regardless of the `is_authorized` flag's value. This ordering is centralized here so every consumer gets it for free, rather than each program deciding independently. **Why is revocation irreversible?** This mirrors Solana's SPL Token `set_authority(None)` semantics. A revoked authority cannot be "re-granted" by anyone, including the original holder — this is what makes "fixed supply" or "config locked forever" a credible, verifiable claim rather than a soft convention. **Why no multisig support?** Out of scope for this library — `Authority` models exactly one signer. Programs needing shared governance over a privileged action should have a multisig program *be* the authority (i.e., the `AccountId` held by `Authority` is itself a multisig program's PDA), rather than `lez-authority` reimplementing multisig internally. ## Overhead `Authority` is a zero-cost wrapper around `Option` — identical in memory layout to the field it wraps. There are no additional accounts, no additional instruction fields, and no serialization overhead introduced by routing a check through this library instead of writing the equivalent `match` inline. ## Tests ```bash cargo test -p lez-authority ``` 16 unit tests cover every method in isolation: construction, state queries, `require`/`rotate`/`revoke` success and failure paths, and a full lifecycle test (init → rotate → revoke → confirm no further action possible).