mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 14:11:09 +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:
@@ -31,6 +31,7 @@ pub fn burn(
|
||||
name: _,
|
||||
metadata_id: _,
|
||||
total_supply,
|
||||
authority: _,
|
||||
},
|
||||
TokenHolding::Fungible {
|
||||
definition_id: _,
|
||||
|
||||
@@ -7,6 +7,7 @@ pub mod initialize;
|
||||
pub mod mint;
|
||||
pub mod new_definition;
|
||||
pub mod print_nft;
|
||||
pub mod set_authority;
|
||||
pub mod transfer;
|
||||
|
||||
mod tests;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
use nssa_core::{
|
||||
account::{Account, AccountWithMetadata, Data},
|
||||
account::{Account, AccountId, AccountWithMetadata, Data},
|
||||
program::{AccountPostState, Claim},
|
||||
};
|
||||
use token_core::{
|
||||
NewTokenDefinition, NewTokenMetadata, TokenDefinition, TokenHolding, TokenMetadata,
|
||||
};
|
||||
|
||||
/// Validate the mint authority for a freshly created fungible definition.
|
||||
///
|
||||
/// `Some(id)` makes the token mintable by `id`; `None` fixes the supply.
|
||||
/// An all-zero authority id is rejected as it cannot be a real signer.
|
||||
fn validate_mint_authority(mint_authority: Option<AccountId>) -> Option<AccountId> {
|
||||
if let Some(id) = &mint_authority {
|
||||
assert!(
|
||||
id.value() != &[0u8; 32],
|
||||
"Mint authority must be a valid non-zero account ID"
|
||||
);
|
||||
}
|
||||
mint_authority
|
||||
}
|
||||
|
||||
pub fn new_fungible_definition(
|
||||
definition_target_account: AccountWithMetadata,
|
||||
holding_target_account: AccountWithMetadata,
|
||||
name: String,
|
||||
total_supply: u128,
|
||||
mint_authority: Option<AccountId>,
|
||||
) -> Vec<AccountPostState> {
|
||||
assert_eq!(
|
||||
definition_target_account.account,
|
||||
@@ -36,6 +51,7 @@ pub fn new_fungible_definition(
|
||||
name,
|
||||
total_supply,
|
||||
metadata_id: None,
|
||||
authority: validate_mint_authority(mint_authority),
|
||||
};
|
||||
let token_holding = TokenHolding::Fungible {
|
||||
definition_id: definition_target_account.account_id,
|
||||
@@ -92,11 +108,16 @@ pub fn new_definition_with_metadata(
|
||||
);
|
||||
|
||||
let (token_definition, token_holding) = match new_definition {
|
||||
NewTokenDefinition::Fungible { name, total_supply } => (
|
||||
NewTokenDefinition::Fungible {
|
||||
name,
|
||||
total_supply,
|
||||
mint_authority,
|
||||
} => (
|
||||
TokenDefinition::Fungible {
|
||||
name,
|
||||
total_supply,
|
||||
metadata_id: Some(metadata_target_account.account_id),
|
||||
authority: validate_mint_authority(mint_authority),
|
||||
},
|
||||
TokenHolding::Fungible {
|
||||
definition_id: definition_target_account.account_id,
|
||||
@@ -124,7 +145,7 @@ pub fn new_definition_with_metadata(
|
||||
standard: metadata.standard,
|
||||
uri: metadata.uri,
|
||||
creators: metadata.creators,
|
||||
primary_sale_date: 0u64, // TODO #261: future works to implement this
|
||||
primary_sale_date: 0u64,
|
||||
};
|
||||
|
||||
let mut definition_target_account_post = definition_target_account.account.clone();
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
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
|
||||
}
|
||||
+445
-3
@@ -15,9 +15,10 @@ use token_core::{
|
||||
use crate::{
|
||||
burn::burn,
|
||||
initialize::initialize_account,
|
||||
mint::mint,
|
||||
mint::{mint, mint_with_authority},
|
||||
new_definition::{new_definition_with_metadata, new_fungible_definition},
|
||||
print_nft::print_nft,
|
||||
set_authority::{set_authority, set_authority_with_authority},
|
||||
transfer::transfer,
|
||||
};
|
||||
|
||||
@@ -42,6 +43,7 @@ impl AccountForTests {
|
||||
name: String::from("test"),
|
||||
total_supply: BalanceForTests::init_supply(),
|
||||
metadata_id: None,
|
||||
authority: Some(AccountId::new([15_u8; 32])),
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
},
|
||||
@@ -59,6 +61,7 @@ impl AccountForTests {
|
||||
name: String::from("test"),
|
||||
total_supply: BalanceForTests::init_supply(),
|
||||
metadata_id: None,
|
||||
authority: None,
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
},
|
||||
@@ -76,6 +79,7 @@ impl AccountForTests {
|
||||
name: String::from("test"),
|
||||
total_supply: BalanceForTests::init_supply(),
|
||||
metadata_id: None,
|
||||
authority: None,
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
},
|
||||
@@ -157,6 +161,7 @@ impl AccountForTests {
|
||||
name: String::from("test"),
|
||||
total_supply: BalanceForTests::init_supply_burned(),
|
||||
metadata_id: None,
|
||||
authority: Some(AccountId::new([15_u8; 32])),
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
},
|
||||
@@ -238,6 +243,7 @@ impl AccountForTests {
|
||||
name: String::from("test"),
|
||||
total_supply: BalanceForTests::init_supply_mint(),
|
||||
metadata_id: None,
|
||||
authority: Some(AccountId::new([15_u8; 32])),
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
},
|
||||
@@ -328,6 +334,7 @@ impl AccountForTests {
|
||||
name: String::from("test"),
|
||||
total_supply: BalanceForTests::init_supply(),
|
||||
metadata_id: None,
|
||||
authority: None,
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
},
|
||||
@@ -594,6 +601,7 @@ fn test_new_definition_non_default_first_account_should_fail() {
|
||||
holding_account,
|
||||
String::from("test"),
|
||||
10,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -618,6 +626,7 @@ fn test_new_definition_non_default_second_account_should_fail() {
|
||||
holding_account,
|
||||
String::from("test"),
|
||||
10,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -631,6 +640,7 @@ fn test_new_definition_requires_authorized_definition_target() {
|
||||
holding_account,
|
||||
String::from("test"),
|
||||
10,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -644,6 +654,7 @@ fn test_new_definition_requires_authorized_holding_target() {
|
||||
holding_account,
|
||||
String::from("test"),
|
||||
10,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -657,6 +668,7 @@ fn test_new_definition_with_valid_inputs_succeeds() {
|
||||
holding_account,
|
||||
String::from("test"),
|
||||
BalanceForTests::init_supply(),
|
||||
None,
|
||||
);
|
||||
|
||||
let [definition_account, holding_account] = post_states.try_into().unwrap();
|
||||
@@ -918,9 +930,11 @@ fn test_mint_not_valid_definition_account() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Definition authorization is missing")]
|
||||
#[should_panic(expected = "Mint authority must authorize the transaction")]
|
||||
fn test_mint_missing_authorization() {
|
||||
let definition_account = AccountForTests::definition_account_without_auth();
|
||||
// The definition account itself is the authority; mark it unauthorized.
|
||||
let mut definition_account = AccountForTests::definition_account_auth();
|
||||
definition_account.is_authorized = false;
|
||||
let holding_account = AccountForTests::holding_same_definition_without_authorization();
|
||||
let _post_states = mint(
|
||||
definition_account,
|
||||
@@ -943,9 +957,23 @@ fn test_mint_rejects_foreign_owned_definition() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Token definition must be owned by token program")]
|
||||
fn test_set_authority_rejects_foreign_owned_definition() {
|
||||
// A foreign-owned account carrying token-shaped data must not be able to
|
||||
// rotate or revoke its authority through the token program.
|
||||
let definition_account = AccountForTests::definition_account_foreign_owner();
|
||||
let _post_states = set_authority(
|
||||
definition_account,
|
||||
Some(AccountId::new([7_u8; 32])),
|
||||
TOKEN_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Mismatch Token Definition and Token Holding")]
|
||||
fn test_mint_mismatched_token_definition() {
|
||||
//
|
||||
let definition_account = AccountForTests::definition_account_auth();
|
||||
let holding_account = AccountForTests::holding_different_definition();
|
||||
let _post_states = mint(
|
||||
@@ -1053,6 +1081,7 @@ fn test_new_definition_with_metadata_success() {
|
||||
let new_definition = NewTokenDefinition::Fungible {
|
||||
name: String::from("test"),
|
||||
total_supply: 15u128,
|
||||
mint_authority: None,
|
||||
};
|
||||
let metadata = NewTokenMetadata {
|
||||
standard: MetadataStandard::Simple,
|
||||
@@ -1074,6 +1103,42 @@ fn test_new_definition_with_metadata_success() {
|
||||
assert_eq!(metadata_post.required_claim(), Some(Claim::Authorized));
|
||||
}
|
||||
|
||||
/// Comment #2: a metadata-backed fungible created with `mint_authority: Some(..)`
|
||||
/// carries a real, non-renounced authority and is therefore mintable — no longer
|
||||
/// force-fixed-supply the way the hardcoded `Authority::renounced()` made it.
|
||||
#[test]
|
||||
fn test_metadata_fungible_with_authority_is_mintable() {
|
||||
let definition_account = AccountForTests::definition_account_uninit_auth();
|
||||
let holding_account = AccountForTests::holding_account_uninit_auth();
|
||||
let metadata_account = AccountForTests::metadata_account_uninit_auth();
|
||||
let new_definition = NewTokenDefinition::Fungible {
|
||||
name: String::from("test"),
|
||||
total_supply: 15u128,
|
||||
mint_authority: Some(AccountId::new([15_u8; 32])),
|
||||
};
|
||||
let metadata = NewTokenMetadata {
|
||||
standard: MetadataStandard::Simple,
|
||||
uri: "test_uri".to_string(),
|
||||
creators: "test_creators".to_string(),
|
||||
};
|
||||
let post_states = new_definition_with_metadata(
|
||||
definition_account,
|
||||
holding_account,
|
||||
metadata_account,
|
||||
new_definition,
|
||||
metadata,
|
||||
);
|
||||
let [definition_post, _holding_post, _metadata_post] = post_states.try_into().unwrap();
|
||||
|
||||
// The stored authority must be the requested key, NOT renounced.
|
||||
let def = TokenDefinition::try_from(&definition_post.account().data).unwrap();
|
||||
let stored = match def {
|
||||
TokenDefinition::Fungible { authority, .. } => authority,
|
||||
_ => None,
|
||||
};
|
||||
assert_eq!(stored, Some(AccountId::new([15_u8; 32])));
|
||||
}
|
||||
|
||||
#[should_panic(expected = "Definition target account must be authorized")]
|
||||
#[test]
|
||||
fn test_call_new_definition_metadata_requires_authorized_definition() {
|
||||
@@ -1083,6 +1148,7 @@ fn test_call_new_definition_metadata_requires_authorized_definition() {
|
||||
let new_definition = NewTokenDefinition::Fungible {
|
||||
name: String::from("test"),
|
||||
total_supply: 15u128,
|
||||
mint_authority: None,
|
||||
};
|
||||
let metadata = NewTokenMetadata {
|
||||
standard: MetadataStandard::Simple,
|
||||
@@ -1107,6 +1173,7 @@ fn test_call_new_definition_metadata_requires_authorized_holding() {
|
||||
let new_definition = NewTokenDefinition::Fungible {
|
||||
name: String::from("test"),
|
||||
total_supply: 15u128,
|
||||
mint_authority: None,
|
||||
};
|
||||
let metadata = NewTokenMetadata {
|
||||
standard: MetadataStandard::Simple,
|
||||
@@ -1135,6 +1202,7 @@ fn test_call_new_definition_metadata_requires_authorized_metadata() {
|
||||
let new_definition = NewTokenDefinition::Fungible {
|
||||
name: String::from("test"),
|
||||
total_supply: 15u128,
|
||||
mint_authority: None,
|
||||
};
|
||||
let metadata = NewTokenMetadata {
|
||||
standard: MetadataStandard::Simple,
|
||||
@@ -1167,6 +1235,7 @@ fn test_call_new_definition_metadata_with_init_definition() {
|
||||
let new_definition = NewTokenDefinition::Fungible {
|
||||
name: String::from("test"),
|
||||
total_supply: 15u128,
|
||||
mint_authority: None,
|
||||
};
|
||||
let metadata = NewTokenMetadata {
|
||||
standard: MetadataStandard::Simple,
|
||||
@@ -1199,6 +1268,7 @@ fn test_call_new_definition_metadata_with_init_metadata() {
|
||||
let new_definition = NewTokenDefinition::Fungible {
|
||||
name: String::from("test"),
|
||||
total_supply: 15u128,
|
||||
mint_authority: None,
|
||||
};
|
||||
let metadata = NewTokenMetadata {
|
||||
standard: MetadataStandard::Simple,
|
||||
@@ -1231,6 +1301,7 @@ fn test_call_new_definition_metadata_with_init_holding() {
|
||||
let new_definition = NewTokenDefinition::Fungible {
|
||||
name: String::from("test"),
|
||||
total_supply: 15u128,
|
||||
mint_authority: None,
|
||||
};
|
||||
let metadata = NewTokenMetadata {
|
||||
standard: MetadataStandard::Simple,
|
||||
@@ -1313,3 +1384,374 @@ fn test_print_nft_success() {
|
||||
assert_eq!(post_master_nft.required_claim(), None);
|
||||
assert_eq!(post_printed.required_claim(), Some(Claim::Authorized));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod authority_tests {
|
||||
use super::*;
|
||||
use crate::{mint::mint, set_authority::set_authority};
|
||||
|
||||
const AUTHORITY: [u8; 32] = [15_u8; 32];
|
||||
const TOKEN_PROGRAM_ID: [u32; 8] = [5_u32; 8];
|
||||
|
||||
/// A fungible definition whose own account id ([15;32]) equals its stored
|
||||
/// mint authority, authorized in the transaction. This models both an external
|
||||
/// owner signing the definition key and a PDA authorized via its seeds.
|
||||
fn def_with_authority() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
program_owner: [5_u32; 8],
|
||||
balance: 0_u128,
|
||||
data: Data::from(&TokenDefinition::Fungible {
|
||||
name: String::from("test"),
|
||||
total_supply: 100_000_u128,
|
||||
metadata_id: None,
|
||||
authority: Some(AccountId::new(AUTHORITY)),
|
||||
}),
|
||||
nonce: 0_u128.into(),
|
||||
},
|
||||
is_authorized: true,
|
||||
account_id: AccountId::new([15; 32]),
|
||||
}
|
||||
}
|
||||
|
||||
/// A definition whose authority has been renounced (fixed supply).
|
||||
fn def_with_authority_revoked() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
program_owner: [5_u32; 8],
|
||||
balance: 0_u128,
|
||||
data: Data::from(&TokenDefinition::Fungible {
|
||||
name: String::from("test"),
|
||||
total_supply: 100_000_u128,
|
||||
metadata_id: None,
|
||||
authority: None,
|
||||
}),
|
||||
nonce: 0_u128.into(),
|
||||
},
|
||||
is_authorized: true,
|
||||
account_id: AccountId::new([15; 32]),
|
||||
}
|
||||
}
|
||||
|
||||
/// A definition whose account id ([99;32]) does NOT match its stored
|
||||
/// authority ([15;32]) — models a caller that isn't the current authority.
|
||||
fn def_wrong_authority() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
program_owner: [5_u32; 8],
|
||||
balance: 0_u128,
|
||||
data: Data::from(&TokenDefinition::Fungible {
|
||||
name: String::from("test"),
|
||||
total_supply: 100_000_u128,
|
||||
metadata_id: None,
|
||||
authority: Some(AccountId::new(AUTHORITY)),
|
||||
}),
|
||||
nonce: 0_u128.into(),
|
||||
},
|
||||
is_authorized: true,
|
||||
account_id: AccountId::new([99; 32]),
|
||||
}
|
||||
}
|
||||
|
||||
fn holding_account() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
program_owner: [5_u32; 8],
|
||||
balance: 0_u128,
|
||||
data: Data::from(&TokenHolding::Fungible {
|
||||
definition_id: AccountId::new([15; 32]),
|
||||
balance: 1_000_u128,
|
||||
}),
|
||||
nonce: 0_u128.into(),
|
||||
},
|
||||
is_authorized: false,
|
||||
account_id: AccountId::new([17; 32]),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mint_with_authority_succeeds() {
|
||||
let post_states = mint(
|
||||
def_with_authority(),
|
||||
holding_account(),
|
||||
50_000,
|
||||
TOKEN_PROGRAM_ID,
|
||||
);
|
||||
let [def_post, holding_post] = post_states.try_into().unwrap();
|
||||
|
||||
let def = TokenDefinition::try_from(&def_post.account().data).unwrap();
|
||||
let holding = TokenHolding::try_from(&holding_post.account().data).unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
def,
|
||||
TokenDefinition::Fungible {
|
||||
total_supply: 150_000,
|
||||
..
|
||||
}
|
||||
));
|
||||
assert!(matches!(
|
||||
holding,
|
||||
TokenHolding::Fungible {
|
||||
balance: 51_000,
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Mint authority check failed")]
|
||||
fn mint_with_revoked_authority_fails() {
|
||||
let _ = mint(
|
||||
def_with_authority_revoked(),
|
||||
holding_account(),
|
||||
50_000,
|
||||
TOKEN_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Mint authority must authorize the transaction")]
|
||||
fn mint_without_is_authorized_fails() {
|
||||
let mut def = def_with_authority();
|
||||
def.is_authorized = false;
|
||||
let _ = mint(def, holding_account(), 50_000, TOKEN_PROGRAM_ID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Mint authority check failed")]
|
||||
fn mint_with_wrong_signer_fails() {
|
||||
let _ = mint(
|
||||
def_wrong_authority(),
|
||||
holding_account(),
|
||||
50_000,
|
||||
TOKEN_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "New mint authority must be a valid non-zero account ID")]
|
||||
fn set_authority_rejects_zero_new_authority() {
|
||||
let _ = set_authority(
|
||||
def_with_authority(),
|
||||
Some(AccountId::new([0u8; 32])),
|
||||
TOKEN_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_authority_rotates_to_new_key() {
|
||||
let new_key = AccountId::new([7_u8; 32]);
|
||||
let post_states = set_authority(def_with_authority(), Some(new_key), TOKEN_PROGRAM_ID);
|
||||
let [def_post] = post_states.try_into().unwrap();
|
||||
|
||||
let def = TokenDefinition::try_from(&def_post.account().data).unwrap();
|
||||
let auth = match def {
|
||||
TokenDefinition::Fungible { authority, .. } => authority,
|
||||
_ => None,
|
||||
};
|
||||
assert_eq!(auth, Some(AccountId::new([7_u8; 32])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_authority_revokes_permanently() {
|
||||
let post_states = set_authority(def_with_authority(), None, TOKEN_PROGRAM_ID);
|
||||
let [def_post] = post_states.try_into().unwrap();
|
||||
|
||||
let def = TokenDefinition::try_from(&def_post.account().data).unwrap();
|
||||
let renounced = match def {
|
||||
TokenDefinition::Fungible { authority, .. } => authority.is_none(),
|
||||
_ => false,
|
||||
};
|
||||
assert!(renounced);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "SetAuthority failed")]
|
||||
fn set_authority_on_revoked_fails() {
|
||||
let _ = set_authority(
|
||||
def_with_authority_revoked(),
|
||||
Some(AccountId::new([7_u8; 32])),
|
||||
TOKEN_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Mint authority must authorize the transaction")]
|
||||
fn set_authority_without_is_authorized_fails() {
|
||||
let mut def = def_with_authority();
|
||||
def.is_authorized = false;
|
||||
let _ = set_authority(def, Some(AccountId::new([7_u8; 32])), TOKEN_PROGRAM_ID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "SetAuthority failed")]
|
||||
fn set_authority_wrong_signer_fails() {
|
||||
let _ = set_authority(
|
||||
def_wrong_authority(),
|
||||
Some(AccountId::new([7_u8; 32])),
|
||||
TOKEN_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
/// After rotating A ([15;32]) -> B ([7;32]) via self-authority, B can rotate
|
||||
/// again to C ([9;32]) by presenting itself as the external authority.
|
||||
#[test]
|
||||
fn set_authority_with_authority_rotates_again() {
|
||||
let rotate_post = set_authority(
|
||||
def_with_authority(),
|
||||
Some(AccountId::new([7_u8; 32])),
|
||||
TOKEN_PROGRAM_ID,
|
||||
);
|
||||
let [def_post] = rotate_post.try_into().unwrap();
|
||||
|
||||
let mut rotated_def = def_with_authority();
|
||||
rotated_def.account = def_post.account().clone();
|
||||
|
||||
// B ([7;32]) rotates to C ([9;32]) as the external authority.
|
||||
let post_states = set_authority_with_authority(
|
||||
rotated_def,
|
||||
new_authority_signer(),
|
||||
Some(AccountId::new([9_u8; 32])),
|
||||
TOKEN_PROGRAM_ID,
|
||||
);
|
||||
let [def_after, _auth] = post_states.try_into().unwrap();
|
||||
let auth = match TokenDefinition::try_from(&def_after.account().data).unwrap() {
|
||||
TokenDefinition::Fungible { authority, .. } => authority,
|
||||
_ => None,
|
||||
};
|
||||
assert_eq!(auth, Some(AccountId::new([9_u8; 32])));
|
||||
}
|
||||
|
||||
/// A rotated external authority B ([7;32]) can permanently revoke.
|
||||
#[test]
|
||||
fn set_authority_with_authority_revokes() {
|
||||
let rotate_post = set_authority(
|
||||
def_with_authority(),
|
||||
Some(AccountId::new([7_u8; 32])),
|
||||
TOKEN_PROGRAM_ID,
|
||||
);
|
||||
let [def_post] = rotate_post.try_into().unwrap();
|
||||
|
||||
let mut rotated_def = def_with_authority();
|
||||
rotated_def.account = def_post.account().clone();
|
||||
|
||||
let post_states = set_authority_with_authority(
|
||||
rotated_def,
|
||||
new_authority_signer(),
|
||||
None,
|
||||
TOKEN_PROGRAM_ID,
|
||||
);
|
||||
let [def_after, _auth] = post_states.try_into().unwrap();
|
||||
let renounced = match TokenDefinition::try_from(&def_after.account().data).unwrap() {
|
||||
TokenDefinition::Fungible { authority, .. } => authority.is_none(),
|
||||
_ => false,
|
||||
};
|
||||
assert!(renounced);
|
||||
}
|
||||
|
||||
/// An external account that is not the current authority cannot rotate/revoke.
|
||||
#[test]
|
||||
#[should_panic(expected = "SetAuthority failed: signer is not the current authority")]
|
||||
fn set_authority_with_authority_wrong_signer_fails() {
|
||||
// Stored authority is A ([15;32]); present a different authorized account.
|
||||
let wrong_authority = AccountWithMetadata {
|
||||
account: Account::default(),
|
||||
is_authorized: true,
|
||||
account_id: AccountId::new([8_u8; 32]),
|
||||
};
|
||||
let _ = set_authority_with_authority(
|
||||
def_with_authority(),
|
||||
wrong_authority,
|
||||
Some(AccountId::new([9_u8; 32])),
|
||||
TOKEN_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_authority_rotate_then_old_cannot_mint() {
|
||||
let new_key = AccountId::new([7_u8; 32]);
|
||||
let post_states = set_authority(def_with_authority(), Some(new_key), TOKEN_PROGRAM_ID);
|
||||
let [def_post] = post_states.try_into().unwrap();
|
||||
|
||||
let def = TokenDefinition::try_from(&def_post.account().data).unwrap();
|
||||
let auth = match def {
|
||||
TokenDefinition::Fungible { authority, .. } => authority,
|
||||
_ => None,
|
||||
};
|
||||
// Rotated to the new key; the old authority no longer controls it.
|
||||
assert_eq!(auth, Some(AccountId::new([7_u8; 32])));
|
||||
assert_ne!(auth, Some(AccountId::new(AUTHORITY)));
|
||||
}
|
||||
|
||||
/// Authority signer for the rotated key B ([7;32]), authorized.
|
||||
fn new_authority_signer() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account::default(),
|
||||
is_authorized: true,
|
||||
account_id: AccountId::new([7_u8; 32]),
|
||||
}
|
||||
}
|
||||
|
||||
/// RFP-001 end-to-end (comment #1): after rotating authority A -> B, the new
|
||||
/// authority B can actually mint by presenting itself in `authority_accounts`.
|
||||
#[test]
|
||||
fn rotated_authority_can_mint() {
|
||||
// Rotate A ([15;32]) -> B ([7;32]), signed by A via self-authority.
|
||||
let rotate_post = set_authority(
|
||||
def_with_authority(),
|
||||
Some(AccountId::new([7_u8; 32])),
|
||||
TOKEN_PROGRAM_ID,
|
||||
);
|
||||
let [def_post] = rotate_post.try_into().unwrap();
|
||||
|
||||
// Rebuild the definition carrying the rotated authority, re-authorized.
|
||||
let mut rotated_def = def_with_authority();
|
||||
rotated_def.account = def_post.account().clone();
|
||||
|
||||
// B mints by presenting itself as the external authority.
|
||||
let mint_post = mint_with_authority(
|
||||
rotated_def,
|
||||
holding_account(),
|
||||
new_authority_signer(),
|
||||
10_000,
|
||||
TOKEN_PROGRAM_ID,
|
||||
);
|
||||
let [def_after, holding_after, _auth] = mint_post.try_into().unwrap();
|
||||
let minted = TokenDefinition::try_from(&def_after.account().data).unwrap();
|
||||
assert!(matches!(
|
||||
minted,
|
||||
TokenDefinition::Fungible {
|
||||
total_supply: 110_000,
|
||||
..
|
||||
}
|
||||
));
|
||||
let holding = TokenHolding::try_from(&holding_after.account().data).unwrap();
|
||||
assert!(matches!(
|
||||
holding,
|
||||
TokenHolding::Fungible {
|
||||
balance: 11_000,
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
/// Comment #1 negative: after rotation to B, the OLD authority A can no
|
||||
/// longer mint. Here A attempts self-authority (empty `authority_accounts`),
|
||||
/// but the definition's own id no longer matches the stored authority B.
|
||||
#[test]
|
||||
#[should_panic(expected = "Mint authority check failed")]
|
||||
fn rotated_authority_old_key_cannot_mint() {
|
||||
let rotate_post = set_authority(
|
||||
def_with_authority(),
|
||||
Some(AccountId::new([7_u8; 32])),
|
||||
TOKEN_PROGRAM_ID,
|
||||
);
|
||||
let [def_post] = rotate_post.try_into().unwrap();
|
||||
|
||||
let mut rotated_def = def_with_authority();
|
||||
rotated_def.account = def_post.account().clone();
|
||||
|
||||
// A ([15;32]) is no longer the authority; self-authority must fail.
|
||||
let _ = mint(rotated_def, holding_account(), 10_000, TOKEN_PROGRAM_ID);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user