mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 22:21:16 +00:00
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.
This commit is contained in:
@@ -4,16 +4,56 @@ use nssa_core::{
|
||||
};
|
||||
use token_core::{TokenDefinition, TokenHolding};
|
||||
|
||||
/// Mint additional supply under **self/PDA authority**: the definition account
|
||||
/// itself is the current mint authority and proves it by being authorized in
|
||||
/// this transaction (a signer, or a PDA authorized under its seeds — e.g. the
|
||||
/// AMM minting its own LP token via a chained call).
|
||||
pub fn mint(
|
||||
definition_account: AccountWithMetadata,
|
||||
user_holding_account: AccountWithMetadata,
|
||||
amount_to_mint: u128,
|
||||
token_program_id: ProgramId,
|
||||
) -> Vec<AccountPostState> {
|
||||
assert!(
|
||||
definition_account.is_authorized,
|
||||
"Definition authorization is missing"
|
||||
);
|
||||
mint_inner(
|
||||
definition_account,
|
||||
user_holding_account,
|
||||
None,
|
||||
amount_to_mint,
|
||||
token_program_id,
|
||||
)
|
||||
}
|
||||
|
||||
/// Mint additional supply under an **external authority**: a distinct account
|
||||
/// (e.g. an owner key the authority was rotated to) proves authority by signing.
|
||||
/// The definition account is still mutated but does not authorize the mint,
|
||||
/// which is what lets a rotated authority mint without the definition's key.
|
||||
pub fn mint_with_authority(
|
||||
definition_account: AccountWithMetadata,
|
||||
user_holding_account: AccountWithMetadata,
|
||||
authority_account: AccountWithMetadata,
|
||||
amount_to_mint: u128,
|
||||
token_program_id: ProgramId,
|
||||
) -> Vec<AccountPostState> {
|
||||
mint_inner(
|
||||
definition_account,
|
||||
user_holding_account,
|
||||
Some(authority_account),
|
||||
amount_to_mint,
|
||||
token_program_id,
|
||||
)
|
||||
}
|
||||
|
||||
/// Shared minting 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). Post-state order mirrors the
|
||||
/// pre-state account order for each mode.
|
||||
fn mint_inner(
|
||||
definition_account: AccountWithMetadata,
|
||||
user_holding_account: AccountWithMetadata,
|
||||
authority_account: Option<AccountWithMetadata>,
|
||||
amount_to_mint: u128,
|
||||
token_program_id: ProgramId,
|
||||
) -> Vec<AccountPostState> {
|
||||
assert_eq!(
|
||||
definition_account.account.program_owner, token_program_id,
|
||||
"Token definition must be owned by token program"
|
||||
@@ -21,6 +61,26 @@ pub fn mint(
|
||||
|
||||
let mut definition = TokenDefinition::try_from(&definition_account.account.data)
|
||||
.expect("Token Definition account must be valid");
|
||||
|
||||
// Minting is gated on the definition's stored mint 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 (self/PDA authority).
|
||||
if let TokenDefinition::Fungible { authority, .. } = &definition {
|
||||
// `None` means the supply is permanently fixed (renounced) — minting is rejected.
|
||||
let mint_authority =
|
||||
authority.expect("Mint authority check failed: authority revoked, supply is fixed");
|
||||
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, mint_authority,
|
||||
"Mint authority check failed: signer is not the current authority"
|
||||
);
|
||||
}
|
||||
|
||||
let mut holding = if user_holding_account.account == Account::default() {
|
||||
TokenHolding::zeroized_from_definition(definition_account.account_id, &definition)
|
||||
} else {
|
||||
@@ -40,6 +100,7 @@ pub fn mint(
|
||||
name: _,
|
||||
metadata_id: _,
|
||||
total_supply,
|
||||
authority: _,
|
||||
},
|
||||
TokenHolding::Fungible {
|
||||
definition_id: _,
|
||||
@@ -69,8 +130,16 @@ pub fn mint(
|
||||
let mut holding_post = user_holding_account.account;
|
||||
holding_post.data = Data::from(&holding);
|
||||
|
||||
vec![
|
||||
AccountPostState::new(definition_post),
|
||||
AccountPostState::new_claimed_if_default(holding_post, Claim::Authorized),
|
||||
]
|
||||
// Post-states must match pre-state order and count: [definition, holding]
|
||||
// for self authority, plus the read-only authority account when external.
|
||||
let mut post_states = Vec::with_capacity(3);
|
||||
post_states.push(AccountPostState::new(definition_post));
|
||||
post_states.push(AccountPostState::new_claimed_if_default(
|
||||
holding_post,
|
||||
Claim::Authorized,
|
||||
));
|
||||
if let Some(authority) = authority_account {
|
||||
post_states.push(AccountPostState::new(authority.account));
|
||||
}
|
||||
post_states
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user