lez-programs/programs/token/src/set_authority.rs
bristinWild fe4c7a96da feat(token): add mint authority model to token program
Add an optional mint authority to fungible tokens for controlled supply:
create with a designated minter, mint additional supply, rotate the
authority to a new key, or permanently revoke it to fix the supply.

The authority is stored inline on `TokenDefinition::Fungible` as
`authority: Option<AccountId>` (`Some(id)` = mintable by `id`, `None` =
fixed supply). Keeping it a plain `Option<AccountId>` rather than a custom
wrapper type leaves account state decodable by `spel inspect`; the
require/rotate/revoke guard logic lives inline in the handlers.

LEZ rejects a transaction that lists the same account id twice, so one
instruction cannot statically express both "the definition account is the
authority and signs" (self/PDA authority) and "a distinct rotated account
signs" (external authority) — they need opposite signer markers. Each
privileged operation is therefore split into a self and an external
variant:

- `Mint` / `SetAuthority` — the definition account is the signer.
- `MintWithAuthority` / `SetAuthorityWithAuthority` — a distinct authority
  account is the signer; the definition account does not sign.

Creation via `NewFungibleDefinition { mint_authority, .. }`; an all-zero
authority id is rejected. The AMM's LP token uses self/PDA authority — its
stored authority is the LP definition PDA, minted only by the pool via
chained calls.

Covered by token unit tests and zkVM integration tests: creation with and
without an authority, self- and external-authority mint, rotation, and
external rotate/revoke. IDLs regenerated.
2026-07-02 19:19:23 +02:00

98 lines
4.1 KiB
Rust

use nssa_core::{
account::{AccountId, AccountWithMetadata, Data},
program::{AccountPostState, ProgramId},
};
use token_core::TokenDefinition;
/// Rotate or revoke the mint authority under **self/PDA authority**: the definition
/// account itself is the current authority and proves it by being authorized in this
/// transaction (a signer, or a PDA authorized under its seeds).
pub fn set_authority(
definition_account: AccountWithMetadata,
new_authority: Option<AccountId>,
token_program_id: ProgramId,
) -> Vec<AccountPostState> {
set_authority_inner(definition_account, None, new_authority, token_program_id)
}
/// Rotate or revoke the mint authority under an **external authority**: a distinct
/// account (the account the authority was previously rotated to) proves authority by
/// signing, so a rotated authority can rotate or revoke again without the definition's
/// key. The definition account is mutated but does not authorize the change.
pub fn set_authority_with_authority(
definition_account: AccountWithMetadata,
authority_account: AccountWithMetadata,
new_authority: Option<AccountId>,
token_program_id: ProgramId,
) -> Vec<AccountPostState> {
set_authority_inner(
definition_account,
Some(authority_account),
new_authority,
token_program_id,
)
}
/// Shared rotation/revocation core for both authority modes. `authority_account` is
/// the external authority when `Some`; when `None` the definition account itself is
/// treated as the authority (self/PDA authority). Only mutates state after all checks
/// pass, so a rejected call leaves the prior authority intact. Post-state order mirrors
/// the pre-state account order for each mode.
fn set_authority_inner(
definition_account: AccountWithMetadata,
authority_account: Option<AccountWithMetadata>,
new_authority: Option<AccountId>,
token_program_id: ProgramId,
) -> Vec<AccountPostState> {
assert_eq!(
definition_account.account.program_owner, token_program_id,
"Token definition must be owned by token program"
);
let mut definition = TokenDefinition::try_from(&definition_account.account.data)
.expect("Token Definition account must be valid");
match &mut definition {
TokenDefinition::Fungible { authority, .. } => {
// The account that proves authority must be authorized AND its id must
// match the stored authority. That account is the explicit external
// authority when present, otherwise the definition account itself.
// `None` means the authority was renounced and can no longer be set.
let current = authority.expect("SetAuthority failed: authority already revoked");
let authority_ref = authority_account.as_ref().unwrap_or(&definition_account);
assert!(
authority_ref.is_authorized,
"Mint authority must authorize the transaction"
);
assert_eq!(
authority_ref.account_id, current,
"SetAuthority failed: signer is not the current authority"
);
if let Some(new) = &new_authority {
assert!(
new.value() != &[0u8; 32],
"New mint authority must be a valid non-zero account ID"
);
}
// Rotate to the new authority, or renounce with `None`.
*authority = new_authority;
}
TokenDefinition::NonFungible { .. } => {
panic!("SetAuthority is not supported for Non-Fungible Tokens");
}
}
let mut definition_post = definition_account.account;
definition_post.data = Data::from(&definition);
// Post-states must match pre-state order and count: [definition] for self
// authority, plus the read-only authority account when external.
let mut post_states = Vec::with_capacity(2);
post_states.push(AccountPostState::new(definition_post));
if let Some(authority) = authority_account {
post_states.push(AccountPostState::new(authority.account));
}
post_states
}