diff --git a/artifacts/amm-idl.json b/artifacts/amm-idl.json index a27829e..d65d53b 100644 --- a/artifacts/amm-idl.json +++ b/artifacts/amm-idl.json @@ -666,6 +666,12 @@ "type": { "option": "account_id" } + }, + { + "name": "authority", + "type": { + "defined": "Authority" + } } ] }, diff --git a/artifacts/ata-idl.json b/artifacts/ata-idl.json index 1222abe..5ccb597 100644 --- a/artifacts/ata-idl.json +++ b/artifacts/ata-idl.json @@ -120,6 +120,12 @@ "type": { "option": "account_id" } + }, + { + "name": "authority", + "type": { + "defined": "Authority" + } } ] }, diff --git a/artifacts/stablecoin-idl.json b/artifacts/stablecoin-idl.json index 2488061..c6c5f31 100644 --- a/artifacts/stablecoin-idl.json +++ b/artifacts/stablecoin-idl.json @@ -160,6 +160,12 @@ "type": { "option": "account_id" } + }, + { + "name": "authority", + "type": { + "defined": "Authority" + } } ] }, diff --git a/artifacts/token-idl.json b/artifacts/token-idl.json index 5174f95..0986ea2 100644 --- a/artifacts/token-idl.json +++ b/artifacts/token-idl.json @@ -49,6 +49,12 @@ { "name": "total_supply", "type": "u128" + }, + { + "name": "mint_authority", + "type": { + "option": "account_id" + } } ] }, @@ -139,12 +145,6 @@ "signer": true, "init": false }, - { - "name": "authority_account", - "writable": false, - "signer": false, - "init": false - }, { "name": "user_holding_account", "writable": true, @@ -159,42 +159,6 @@ } ] }, - { - "name": "new_fungible_definition_with_authority", - "accounts": [ - { - "name": "definition_target_account", - "writable": false, - "signer": false, - "init": false - }, - { - "name": "holding_target_account", - "writable": false, - "signer": false, - "init": false - } - ], - "args": [ - { - "name": "name", - "type": "string" - }, - { - "name": "initial_supply", - "type": "u128" - }, - { - "name": "mint_authority", - "type": { - "array": [ - "u8", - 32 - ] - } - } - ] - }, { "name": "set_authority", "accounts": [ @@ -203,24 +167,13 @@ "writable": false, "signer": false, "init": false - }, - { - "name": "authority_account", - "writable": false, - "signer": false, - "init": false } ], "args": [ { "name": "new_authority", "type": { - "option": { - "array": [ - "u8", - 32 - ] - } + "option": "account_id" } } ] @@ -268,14 +221,9 @@ } }, { - "name": "mint_authority", + "name": "authority", "type": { - "option": { - "array": [ - "u8", - 32 - ] - } + "defined": "Authority" } } ] diff --git a/lez-authority/Cargo.toml b/lez-authority/Cargo.toml index 96d90d8..7cef526 100644 --- a/lez-authority/Cargo.toml +++ b/lez-authority/Cargo.toml @@ -8,4 +8,5 @@ license = "MIT OR Apache-2.0" workspace = true [dependencies] -borsh = { workspace = true, features = ["derive"] } +borsh = { workspace = true } +serde = { workspace = true, features = ["derive"] } diff --git a/lez-authority/src/lib.rs b/lez-authority/src/lib.rs index 60df9b2..4efa7c5 100644 --- a/lez-authority/src/lib.rs +++ b/lez-authority/src/lib.rs @@ -1,40 +1,64 @@ -//! Agnostic mint authority library for LEZ programs. +//! Agnostic admin/mint authority library for LEZ programs. //! Implements the approval model defined in RFP-001. //! No dependency on any specific program or nssa_core. use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AuthorityError { + /// The authority slot is empty (renounced); the resource is permanently fixed. Revoked, + /// The signer does not match the current authority. Unauthorized, + /// Attempted to act on an already-renounced authority. AlreadyRevoked, } impl core::fmt::Display for AuthorityError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - Self::Revoked => write!(f, "mint authority has been revoked; supply is fixed"), - Self::Unauthorized => write!(f, "signer is not the current mint authority"), + Self::Revoked => write!(f, "authority has been revoked; resource is fixed"), + Self::Unauthorized => write!(f, "signer is not the current authority"), Self::AlreadyRevoked => write!(f, "authority already revoked; cannot set again"), } } } -/// A mint authority slot. None = permanently fixed supply. -#[derive(BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq)] -pub struct AuthoritySlot(pub Option<[u8; 32]>); +/// An ownership/authority slot. `None` = permanently renounced (no further changes +/// or privileged actions are possible). +#[derive( + BorshSerialize, BorshDeserialize, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, +)] +pub struct Authority(Option<[u8; 32]>); -impl AuthoritySlot { - pub fn new(authority: [u8; 32]) -> Self { - Self(Some(authority)) +impl Authority { + /// Create an authority owned by `owner`. + #[must_use] + pub fn new(owner: [u8; 32]) -> Self { + Self(Some(owner)) } - pub fn fixed() -> Self { + /// Create a permanently renounced authority (fixed resource). + #[must_use] + pub fn renounced() -> Self { Self(None) } - pub fn check(&self, signer: [u8; 32]) -> Result<(), AuthorityError> { + /// The current authority key, or `None` if renounced. + #[must_use] + pub fn authority(&self) -> Option<[u8; 32]> { + self.0 + } + + /// Returns `true` if the authority has been permanently renounced. + #[must_use] + pub fn is_renounced(&self) -> bool { + self.0.is_none() + } + + /// Require that `signer` is the current authority. + pub fn require(&self, signer: [u8; 32]) -> Result<(), AuthorityError> { match self.0 { None => Err(AuthorityError::Revoked), Some(auth) if auth != signer => Err(AuthorityError::Unauthorized), @@ -42,24 +66,50 @@ impl AuthoritySlot { } } - /// Rotate or revoke. Only mutates AFTER all checks pass. - pub fn set( + /// Rotate to a new authority, or renounce with `None`. + /// Only mutates AFTER all checks pass (atomic). + pub fn rotate( &mut self, signer: [u8; 32], - new_authority: Option<[u8; 32]>, + new: Option<[u8; 32]>, ) -> Result<(), AuthorityError> { match self.0 { None => Err(AuthorityError::AlreadyRevoked), Some(auth) if auth != signer => Err(AuthorityError::Unauthorized), Some(_) => { - self.0 = new_authority; + self.0 = new; Ok(()) } } } +} - pub fn is_revoked(&self) -> bool { - self.0.is_none() +/// A type that carries an [`Authority`] slot and can be guarded by it. +/// +/// Programs "inherit the owner slot" by embedding an [`Authority`] field in their +/// account type and implementing this trait; the default methods then provide the +/// standard require / transfer / renounce semantics. +pub trait Ownable { + fn authority(&self) -> &Authority; + fn authority_mut(&mut self) -> &mut Authority; + + /// Require that `signer` is the current owner. + fn require_owner(&self, signer: [u8; 32]) -> Result<(), AuthorityError> { + self.authority().require(signer) + } + + /// Transfer ownership to `new`, authorized by the current owner `signer`. + fn transfer_ownership( + &mut self, + signer: [u8; 32], + new: [u8; 32], + ) -> Result<(), AuthorityError> { + self.authority_mut().rotate(signer, Some(new)) + } + + /// Permanently renounce ownership, authorized by the current owner `signer`. + fn renounce_ownership(&mut self, signer: [u8; 32]) -> Result<(), AuthorityError> { + self.authority_mut().rotate(signer, None) } } @@ -71,55 +121,90 @@ mod tests { const BOB: [u8; 32] = [2u8; 32]; #[test] - fn check_succeeds_for_correct_signer() { - assert!(AuthoritySlot::new(ALICE).check(ALICE).is_ok()); + fn require_succeeds_for_correct_owner() { + assert!(Authority::new(ALICE).require(ALICE).is_ok()); } #[test] - fn check_fails_unauthorized() { + fn require_fails_unauthorized() { assert_eq!( - AuthoritySlot::new(ALICE).check(BOB), + Authority::new(ALICE).require(BOB), Err(AuthorityError::Unauthorized) ); } #[test] - fn check_fails_when_revoked() { + fn require_fails_when_renounced() { assert_eq!( - AuthoritySlot::fixed().check(ALICE), + Authority::renounced().require(ALICE), Err(AuthorityError::Revoked) ); } #[test] - fn set_rotates_authority() { - let mut slot = AuthoritySlot::new(ALICE); - slot.set(ALICE, Some(BOB)).unwrap(); - assert_eq!(slot.0, Some(BOB)); - assert_eq!(slot.check(ALICE), Err(AuthorityError::Unauthorized)); + fn rotate_transfers_authority() { + let mut auth = Authority::new(ALICE); + auth.rotate(ALICE, Some(BOB)).unwrap(); + assert_eq!(auth.authority(), Some(BOB)); + assert_eq!(auth.require(ALICE), Err(AuthorityError::Unauthorized)); } #[test] - fn set_revokes_permanently() { - let mut slot = AuthoritySlot::new(ALICE); - slot.set(ALICE, None).unwrap(); - assert!(slot.is_revoked()); + fn rotate_renounces_permanently() { + let mut auth = Authority::new(ALICE); + auth.rotate(ALICE, None).unwrap(); + assert!(auth.is_renounced()); assert_eq!( - slot.set(ALICE, Some(ALICE)), + auth.rotate(ALICE, Some(ALICE)), Err(AuthorityError::AlreadyRevoked) ); } #[test] - fn wrong_authority_cannot_rotate_and_state_unchanged() { - let mut slot = AuthoritySlot::new(ALICE); - assert_eq!(slot.set(BOB, Some(BOB)), Err(AuthorityError::Unauthorized)); - assert_eq!(slot.0, Some(ALICE)); // state unchanged + fn wrong_owner_cannot_rotate_and_state_unchanged() { + let mut auth = Authority::new(ALICE); + assert_eq!( + auth.rotate(BOB, Some(BOB)), + Err(AuthorityError::Unauthorized) + ); + assert_eq!(auth.authority(), Some(ALICE)); } #[test] - fn set_none_on_already_fixed_fails() { - let mut slot = AuthoritySlot::fixed(); - assert_eq!(slot.set(ALICE, None), Err(AuthorityError::AlreadyRevoked)); + fn renounce_on_already_renounced_fails() { + let mut auth = Authority::renounced(); + assert_eq!( + auth.rotate(ALICE, None), + Err(AuthorityError::AlreadyRevoked) + ); + } + + // Ownable trait via a tiny embedding type. + struct Resource { + owner: Authority, + } + impl Ownable for Resource { + fn authority(&self) -> &Authority { + &self.owner + } + + fn authority_mut(&mut self) -> &mut Authority { + &mut self.owner + } + } + + #[test] + fn ownable_require_transfer_renounce() { + let mut r = Resource { + owner: Authority::new(ALICE), + }; + assert!(r.require_owner(ALICE).is_ok()); + assert_eq!(r.require_owner(BOB), Err(AuthorityError::Unauthorized)); + + r.transfer_ownership(ALICE, BOB).unwrap(); + assert!(r.require_owner(BOB).is_ok()); + + r.renounce_ownership(BOB).unwrap(); + assert!(r.authority().is_renounced()); } } diff --git a/programs/amm/Cargo.toml b/programs/amm/Cargo.toml index e985d27..951d50c 100644 --- a/programs/amm/Cargo.toml +++ b/programs/amm/Cargo.toml @@ -12,3 +12,4 @@ clock_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.g amm_core = { path = "core" } token_core = { path = "../token/core" } twap_oracle_core = { path = "../twap_oracle/core" } +lez-authority = { path = "../../lez-authority" } diff --git a/programs/amm/src/new_definition.rs b/programs/amm/src/new_definition.rs index 45fbba1..a62eb4e 100644 --- a/programs/amm/src/new_definition.rs +++ b/programs/amm/src/new_definition.rs @@ -7,7 +7,9 @@ use amm_core::{ compute_vault_pda_seed, isqrt_product, spot_price_q64_64, AmmConfig, PoolDefinition, MINIMUM_LIQUIDITY, }; + use clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID; +use lez_authority::Authority; use nssa_core::{ account::{Account, AccountWithMetadata, Data}, program::{AccountPostState, ChainedCall, Claim, ProgramId}, @@ -193,6 +195,7 @@ pub fn new_definition( &token_core::Instruction::NewFungibleDefinition { name: String::from("LP Token"), total_supply: MINIMUM_LIQUIDITY, + mint_authority: Some(pool_definition_lp.account_id), }, ) .with_pda_seeds(vec![ @@ -206,9 +209,14 @@ pub fn new_definition( name: String::from("LP Token"), total_supply: MINIMUM_LIQUIDITY, metadata_id: None, - mint_authority: None, + authority: Authority::new( + pool_definition_lp + .account_id + .as_ref() + .try_into() + .expect("AccountId is always 32 bytes"), + ), }); - let call_token_lp_user = ChainedCall::new( token_program_id, vec![pool_lp_after_lock, user_holding_lp.clone()], diff --git a/programs/amm/src/tests.rs b/programs/amm/src/tests.rs index ec17213..cde63a5 100644 --- a/programs/amm/src/tests.rs +++ b/programs/amm/src/tests.rs @@ -538,10 +538,11 @@ impl ChainedCallForTests { ChainedCall::new( TOKEN_PROGRAM_ID, - vec![pool_lp_auth, lp_lock_holding_auth], + vec![pool_lp_auth.clone(), lp_lock_holding_auth], &token_core::Instruction::NewFungibleDefinition { name: String::from("LP Token"), total_supply: MINIMUM_LIQUIDITY, + mint_authority: Some(pool_lp_auth.account_id), }, ) .with_pda_seeds(vec![ @@ -872,7 +873,7 @@ impl AccountWithMetadataForTests { name: String::from("test"), total_supply: BalanceForTests::lp_supply_init(), metadata_id: None, - mint_authority: None, + authority: token_core::Authority::renounced(), }), nonce: Nonce(0), }, @@ -898,7 +899,7 @@ impl AccountWithMetadataForTests { name: String::from("LP Token"), total_supply: MINIMUM_LIQUIDITY, metadata_id: None, - mint_authority: None, + authority: token_core::Authority::renounced(), }), nonce: Nonce(0), }, @@ -916,7 +917,7 @@ impl AccountWithMetadataForTests { name: String::from("test"), total_supply: BalanceForTests::lp_supply_init(), metadata_id: None, - mint_authority: None, + authority: token_core::Authority::renounced(), }), nonce: Nonce(0), }, @@ -3266,6 +3267,7 @@ fn test_new_definition_lp_symmetric_amounts() { &token_core::Instruction::NewFungibleDefinition { name: String::from("LP Token"), total_supply: MINIMUM_LIQUIDITY, + mint_authority: Some(pool_lp_auth.account_id), }, ) .with_pda_seeds(vec![ @@ -3368,6 +3370,7 @@ fn test_minimum_liquidity_lock_and_remove_all_user_lp() { &token_core::Instruction::NewFungibleDefinition { name: String::from("LP Token"), total_supply: MINIMUM_LIQUIDITY, + mint_authority: Some(pool_lp_auth.account_id), }, ) .with_pda_seeds(vec![ diff --git a/programs/ata/src/tests.rs b/programs/ata/src/tests.rs index 5fe6db3..25de01c 100644 --- a/programs/ata/src/tests.rs +++ b/programs/ata/src/tests.rs @@ -41,7 +41,7 @@ fn definition_account() -> AccountWithMetadata { name: "TEST".to_string(), total_supply: 1000, metadata_id: None, - mint_authority: None, + authority: token_core::Authority::renounced(), }), nonce: nssa_core::account::Nonce(0), }, diff --git a/programs/integration_tests/tests/amm.rs b/programs/integration_tests/tests/amm.rs index 5d6dc87..549dc61 100644 --- a/programs/integration_tests/tests/amm.rs +++ b/programs/integration_tests/tests/amm.rs @@ -401,7 +401,7 @@ impl Accounts { name: String::from("test"), total_supply: Balances::token_a_supply(), metadata_id: None, - mint_authority: None, + authority: token_core::Authority::renounced(), }), nonce: Nonce(0), } @@ -415,7 +415,7 @@ impl Accounts { name: String::from("test"), total_supply: Balances::token_b_supply(), metadata_id: None, - mint_authority: None, + authority: token_core::Authority::renounced(), }), nonce: Nonce(0), } @@ -429,7 +429,12 @@ impl Accounts { name: String::from("LP Token"), total_supply: Balances::token_lp_supply(), metadata_id: None, - mint_authority: None, + authority: token_core::Authority::new( + Ids::token_lp_definition() + .as_ref() + .try_into() + .expect("AccountId is always 32 bytes"), + ), }), nonce: Nonce(0), } @@ -708,7 +713,12 @@ impl Accounts { name: String::from("LP Token"), total_supply: Balances::token_lp_supply_add(), metadata_id: None, - mint_authority: None, + authority: token_core::Authority::new( + Ids::token_lp_definition() + .as_ref() + .try_into() + .expect("AccountId is always 32 bytes"), + ), }), nonce: Nonce(0), } @@ -801,7 +811,12 @@ impl Accounts { name: String::from("LP Token"), total_supply: Balances::token_lp_supply_remove(), metadata_id: None, - mint_authority: None, + authority: token_core::Authority::new( + Ids::token_lp_definition() + .as_ref() + .try_into() + .expect("AccountId is always 32 bytes"), + ), }), nonce: Nonce(0), } @@ -815,7 +830,12 @@ impl Accounts { name: String::from("LP Token"), total_supply: 0, metadata_id: None, - mint_authority: None, + authority: token_core::Authority::new( + Ids::token_lp_definition() + .as_ref() + .try_into() + .expect("AccountId is always 32 bytes"), + ), }), nonce: Nonce(0), } @@ -908,7 +928,12 @@ impl Accounts { name: String::from("LP Token"), total_supply: Balances::lp_supply_init(), metadata_id: None, - mint_authority: None, + authority: token_core::Authority::new( + Ids::token_lp_definition() + .as_ref() + .try_into() + .expect("AccountId is always 32 bytes"), + ), }), nonce: Nonce(0), } @@ -1397,7 +1422,7 @@ fn fungible_total_supply(account: &Account) -> u128 { name: _, total_supply, metadata_id: _, - mint_authority: _, + authority: _, } = definition else { panic!("expected fungible token definition") diff --git a/programs/integration_tests/tests/ata.rs b/programs/integration_tests/tests/ata.rs index 1403522..50b09ed 100644 --- a/programs/integration_tests/tests/ata.rs +++ b/programs/integration_tests/tests/ata.rs @@ -84,7 +84,7 @@ impl Accounts { name: String::from("Gold"), total_supply: 1_000_000_u128, metadata_id: None, - mint_authority: None, + authority: token_core::Authority::renounced(), }), nonce: Nonce(0), } @@ -122,7 +122,7 @@ impl Accounts { name: String::from("Foreign Gold"), total_supply: 1_000_000_u128, metadata_id: None, - mint_authority: None, + authority: token_core::Authority::renounced(), }), nonce: Nonce(0), } @@ -497,7 +497,7 @@ fn ata_burn() { name: String::from("Gold"), total_supply: 700_000_u128, metadata_id: None, - mint_authority: None, + authority: token_core::Authority::renounced(), }), nonce: Nonce(0), } diff --git a/programs/integration_tests/tests/stablecoin.rs b/programs/integration_tests/tests/stablecoin.rs index cbc2a7e..e26cbe9 100644 --- a/programs/integration_tests/tests/stablecoin.rs +++ b/programs/integration_tests/tests/stablecoin.rs @@ -108,7 +108,7 @@ impl Accounts { name: String::from("Gold"), total_supply: Balances::user_holding_init(), metadata_id: None, - mint_authority: None, + authority: token_core::Authority::renounced(), }), nonce: Nonce(0), } @@ -134,7 +134,7 @@ impl Accounts { name: String::from("DAI"), total_supply: Balances::stablecoin_supply_init(), metadata_id: None, - mint_authority: None, + authority: token_core::Authority::renounced(), }), nonce: Nonce(0), } diff --git a/programs/integration_tests/tests/token.rs b/programs/integration_tests/tests/token.rs index 6418b50..f96b3ed 100644 --- a/programs/integration_tests/tests/token.rs +++ b/programs/integration_tests/tests/token.rs @@ -69,8 +69,8 @@ impl Accounts { name: String::from("Gold"), total_supply: 1_000_000_u128, metadata_id: None, - mint_authority: Some( - Ids::authority() + authority: token_core::Authority::new( + Ids::token_definition() .as_ref() .try_into() .expect("AccountId is always 32 bytes"), @@ -88,8 +88,8 @@ impl Accounts { name: String::from("Gold"), total_supply: 1_000_000_u128, metadata_id: None, - mint_authority: Some( - Ids::authority() + authority: token_core::Authority::new( + Ids::token_definition() .as_ref() .try_into() .expect("AccountId is always 32 bytes"), @@ -168,6 +168,7 @@ fn token_new_fungible_definition() { let instruction = token_core::Instruction::NewFungibleDefinition { name: String::from("Gold"), total_supply: 1_000_000_u128, + mint_authority: None, }; let message = public_transaction::Message::try_new( @@ -195,7 +196,7 @@ fn token_new_fungible_definition() { name: String::from("Gold"), total_supply: 1_000_000_u128, metadata_id: None, - mint_authority: None, + authority: token_core::Authority::renounced(), }), nonce: Nonce(1), } @@ -447,8 +448,8 @@ fn token_burn() { name: String::from("Gold"), total_supply: 800_000_u128, metadata_id: None, - mint_authority: Some( - Ids::authority() + authority: token_core::Authority::new( + Ids::token_definition() .as_ref() .try_into() .expect("AccountId is always 32 bytes") @@ -482,14 +483,13 @@ fn token_mint() { let message = public_transaction::Message::try_new( Ids::token_program(), - vec![Ids::token_definition(), Ids::authority(), Ids::holder()], + vec![Ids::token_definition(), Ids::holder()], vec![Nonce(0)], instruction, ) .unwrap(); - let witness_set = - public_transaction::WitnessSet::for_message(&message, &[&Keys::authority_key()]); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::def_key()]); let tx = PublicTransaction::new(message, witness_set); state.transition_from_public_transaction(&tx, 0, 0).unwrap(); @@ -503,14 +503,14 @@ fn token_mint() { name: String::from("Gold"), total_supply: 1_500_000_u128, metadata_id: None, - mint_authority: Some( - Ids::authority() + authority: token_core::Authority::new( + Ids::token_definition() .as_ref() .try_into() .expect("AccountId is always 32 bytes") ), }), - nonce: Nonce(0), + nonce: Nonce(1), } ); @@ -542,7 +542,7 @@ fn token_mint_rejects_foreign_owned_definition() { let message = public_transaction::Message::try_new( Ids::token_program(), - vec![Ids::token_definition(), Ids::authority(), Ids::recipient()], + vec![Ids::token_definition(), Ids::recipient()], vec![Nonce(0), Nonce(0)], instruction, ) @@ -550,7 +550,7 @@ fn token_mint_rejects_foreign_owned_definition() { let witness_set = public_transaction::WitnessSet::for_message( &message, - &[&Keys::authority_key(), &Keys::recipient_key()], + &[&Keys::def_key(), &Keys::recipient_key()], ); let tx = PublicTransaction::new(message, witness_set); @@ -576,14 +576,13 @@ fn token_mint_fresh_public_recipient_requires_authorization() { let message = public_transaction::Message::try_new( Ids::token_program(), - vec![Ids::token_definition(), Ids::authority(), Ids::recipient()], + vec![Ids::token_definition(), Ids::recipient()], vec![Nonce(0)], instruction, ) .unwrap(); - let witness_set = - public_transaction::WitnessSet::for_message(&message, &[&Keys::authority_key()]); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::def_key()]); let tx = PublicTransaction::new(message, witness_set); assert!(state.transition_from_public_transaction(&tx, 0, 0).is_err()); @@ -608,7 +607,7 @@ fn token_mint_fresh_authorized_public_recipient() { let message = public_transaction::Message::try_new( Ids::token_program(), - vec![Ids::token_definition(), Ids::authority(), Ids::recipient()], + vec![Ids::token_definition(), Ids::recipient()], vec![Nonce(0), Nonce(0)], instruction, ) @@ -616,7 +615,7 @@ fn token_mint_fresh_authorized_public_recipient() { let witness_set = public_transaction::WitnessSet::for_message( &message, - &[&Keys::authority_key(), &Keys::recipient_key()], + &[&Keys::def_key(), &Keys::recipient_key()], ); let tx = PublicTransaction::new(message, witness_set); @@ -631,14 +630,14 @@ fn token_mint_fresh_authorized_public_recipient() { name: String::from("Gold"), total_supply: 1_500_000_u128, metadata_id: None, - mint_authority: Some( - Ids::authority() + authority: token_core::Authority::new( + Ids::token_definition() .as_ref() .try_into() .expect("AccountId is always 32 bytes") ), }), - nonce: Nonce(0), + nonce: Nonce(1), } ); @@ -976,10 +975,10 @@ fn token_new_fungible_definition_with_authority() { .as_ref() .try_into() .expect("AccountId is always 32 bytes"); - let instruction = token_core::Instruction::NewFungibleDefinitionWithAuthority { + let instruction = token_core::Instruction::NewFungibleDefinition { name: String::from("AuthCoin"), - initial_supply: 1_000_000_u128, - mint_authority: authority_key, + total_supply: 1_000_000_u128, + mint_authority: Some(AccountId::new(authority_key)), }; let message = public_transaction::Message::try_new( Ids::token_program(), @@ -1003,7 +1002,7 @@ fn token_new_fungible_definition_with_authority() { name: String::from("AuthCoin"), total_supply: 1_000_000_u128, metadata_id: None, - mint_authority: Some(authority_key), + authority: token_core::Authority::new(authority_key), }), nonce: Nonce(1), } @@ -1014,15 +1013,15 @@ fn token_new_fungible_definition_with_authority() { fn token_set_authority_revoke() { let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); deploy_token(&mut state); - let authority_key: [u8; 32] = Ids::authority() + let authority_key: [u8; 32] = Ids::token_definition() .as_ref() .try_into() .expect("AccountId is always 32 bytes"); // Create token with authority - let instruction = token_core::Instruction::NewFungibleDefinitionWithAuthority { + let instruction = token_core::Instruction::NewFungibleDefinition { name: String::from("AuthCoin"), - initial_supply: 1_000_000_u128, - mint_authority: authority_key, + total_supply: 1_000_000_u128, + mint_authority: Some(AccountId::new(authority_key)), }; let message = public_transaction::Message::try_new( Ids::token_program(), @@ -1047,13 +1046,12 @@ fn token_set_authority_revoke() { }; let message = public_transaction::Message::try_new( Ids::token_program(), - vec![Ids::token_definition(), Ids::authority()], - vec![Nonce(0)], + vec![Ids::token_definition()], + vec![Nonce(1)], instruction, ) .unwrap(); - let witness_set = - public_transaction::WitnessSet::for_message(&message, &[&Keys::authority_key()]); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::def_key()]); let tx = PublicTransaction::new(message, witness_set); state.transition_from_public_transaction(&tx, 0, 0).unwrap(); assert_eq!( @@ -1065,9 +1063,9 @@ fn token_set_authority_revoke() { name: String::from("AuthCoin"), total_supply: 1_000_000_u128, metadata_id: None, - mint_authority: None, + authority: token_core::Authority::renounced(), }), - nonce: Nonce(1), + nonce: Nonce(2), } ); } diff --git a/programs/stablecoin/src/tests.rs b/programs/stablecoin/src/tests.rs index 69b706a..8f4ac0c 100644 --- a/programs/stablecoin/src/tests.rs +++ b/programs/stablecoin/src/tests.rs @@ -79,7 +79,7 @@ fn collateral_definition_account() -> AccountWithMetadata { name: "SNT".to_owned(), total_supply: 1_000_000, metadata_id: None, - mint_authority: None, + authority: token_core::Authority::renounced(), }), nonce: Nonce(0), }, @@ -157,7 +157,7 @@ fn stablecoin_definition_account() -> AccountWithMetadata { name: "DAI".to_owned(), total_supply: 1_000_000, metadata_id: None, - mint_authority: None, + authority: token_core::Authority::renounced(), }), nonce: Nonce(0), }, @@ -391,7 +391,7 @@ fn open_position_rejects_mismatched_token_definition() { name: "OTHER".to_owned(), total_supply: 1, metadata_id: None, - mint_authority: None, + authority: token_core::Authority::renounced(), }), nonce: Nonce(0), }, diff --git a/programs/token/core/Cargo.toml b/programs/token/core/Cargo.toml index e398324..76f4070 100644 --- a/programs/token/core/Cargo.toml +++ b/programs/token/core/Cargo.toml @@ -11,3 +11,4 @@ nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.gi spel-framework-macros = { git = "https://github.com/0x-r4bbit/spel.git", rev = "91023c9115bf88173b0d25d2e905f2a55ef0313b", package = "spel-framework-macros" } borsh = { version = "1.5", features = ["derive"] } serde = { version = "1.0", features = ["derive"] } +lez-authority = { path = "../../../lez-authority" } diff --git a/programs/token/core/src/lib.rs b/programs/token/core/src/lib.rs index 2b5add2..2236ddd 100644 --- a/programs/token/core/src/lib.rs +++ b/programs/token/core/src/lib.rs @@ -1,6 +1,7 @@ //! This crate contains core data structures and utilities for the Token Program. use borsh::{BorshDeserialize, BorshSerialize}; +pub use lez_authority::{Authority, Ownable}; use nssa_core::account::{AccountId, Data}; use serde::{Deserialize, Serialize}; use spel_framework_macros::account_type; @@ -18,10 +19,18 @@ pub enum Instruction { /// Create a new fungible token definition without metadata. /// + /// `mint_authority` decides the supply model: + /// - `Some(id)` — `id` may mint additional supply and rotate/renounce the authority, + /// - `None` — supply is permanently fixed at `total_supply`. + /// /// Required accounts: /// - Token Definition account (uninitialized, authorized), /// - Token Holding account (uninitialized, authorized). - NewFungibleDefinition { name: String, total_supply: u128 }, + NewFungibleDefinition { + name: String, + total_supply: u128, + mint_authority: Option, + }, /// Create a new fungible or non-fungible token definition with metadata. /// @@ -51,9 +60,13 @@ pub enum Instruction { /// Mint new tokens to the holder's account. /// + /// Minting is gated on the definition's mint authority: the Token Definition + /// account must be authorized in this transaction and its account id must match + /// the stored authority. A definition with no authority has a fixed supply and + /// rejects minting. + /// /// Required accounts: - /// - Token Definition account (initialized). - /// - Authority account: must sign and match the stored mint authority. + /// - Token Definition account (initialized, authorized as the current mint authority), /// - Token Holding account (uninitialized or authorized and initialized). Mint { amount_to_mint: u128 }, @@ -64,26 +77,12 @@ pub enum Instruction { /// - NFT Printed Copy Token Holding account (uninitialized, authorized). PrintNft, - /// Create a new fungible token definition with a mint authority. - /// Unlike NewFungibleDefinition, this allows minting additional tokens later. + /// Rotate or renounce the mint authority for a fungible token definition. + /// Pass `new_authority: None` to permanently renounce minting (fixed supply). /// /// Required accounts: - /// - Token Definition account (uninitialized, authorized), - /// - Token Holding account (uninitialized, authorized). - NewFungibleDefinitionWithAuthority { - name: String, - initial_supply: u128, - /// The initial mint authority. Can be rotated or revoked later via SetAuthority. - mint_authority: [u8; 32], - }, - - /// Set or rotate the mint authority for a fungible token definition. - /// Pass `new_authority: None` to permanently revoke minting (fixed supply). - /// - /// Required accounts: - /// - Token Definition account (initialized). - /// - Authority account: must sign and match the current mint authority. - SetAuthority { new_authority: Option<[u8; 32]> }, + /// - Token Definition account (initialized, authorized as the current mint authority). + SetAuthority { new_authority: Option }, } #[derive(Serialize, Deserialize)] @@ -105,9 +104,9 @@ pub enum TokenDefinition { name: String, total_supply: u128, metadata_id: Option, - /// Mint authority. `None` = supply is permanently fixed (no further minting allowed). - /// Added by LP-0013. - mint_authority: Option<[u8; 32]>, + /// Mint authority slot. `Some(id)` may mint and rotate/renounce; + /// `None` means the supply is permanently fixed. + authority: Authority, }, NonFungible { name: String, @@ -116,6 +115,26 @@ pub enum TokenDefinition { }, } +impl Ownable for TokenDefinition { + fn authority(&self) -> &Authority { + match self { + TokenDefinition::Fungible { authority, .. } => authority, + TokenDefinition::NonFungible { .. } => { + panic!("Authority is not supported for Non-Fungible Tokens") + } + } + } + + fn authority_mut(&mut self) -> &mut Authority { + match self { + TokenDefinition::Fungible { authority, .. } => authority, + TokenDefinition::NonFungible { .. } => { + panic!("Authority is not supported for Non-Fungible Tokens") + } + } + } +} + impl TryFrom<&Data> for TokenDefinition { type Error = std::io::Error; diff --git a/programs/token/methods/guest/src/bin/token.rs b/programs/token/methods/guest/src/bin/token.rs index 0490692..f756071 100644 --- a/programs/token/methods/guest/src/bin/token.rs +++ b/programs/token/methods/guest/src/bin/token.rs @@ -1,6 +1,6 @@ #![cfg_attr(not(test), no_main)] -use nssa_core::account::AccountWithMetadata; +use nssa_core::account::{AccountId, AccountWithMetadata}; use spel_framework::context::ProgramContext; use spel_framework::prelude::*; @@ -33,6 +33,7 @@ mod token { /// Create a new fungible token definition without metadata. /// Definition and holding targets must be uninitialized and authorized. + /// `mint_authority` is `Some(id)` for a mintable token or `None` for fixed supply. #[instruction] pub fn new_fungible_definition( #[account(init, signer)] @@ -41,6 +42,7 @@ mod token { holding_target_account: AccountWithMetadata, name: String, total_supply: u128, + mint_authority: Option, ) -> SpelResult { Ok(spel_framework::SpelOutput::execute( token_program::new_definition::new_fungible_definition( @@ -48,6 +50,7 @@ mod token { holding_target_account, name, total_supply, + mint_authority, ), vec![], )) @@ -117,20 +120,19 @@ mod token { } /// Mint new tokens to the holder's account. + /// The definition account must be authorized as the current mint authority. /// Fresh public holders must be explicitly authorized in the same transaction. #[instruction] pub fn mint( ctx: ProgramContext, #[account(mut, signer)] definition_account: AccountWithMetadata, - authority_account: AccountWithMetadata, user_holding_account: AccountWithMetadata, amount_to_mint: u128, ) -> SpelResult { Ok(spel_framework::SpelOutput::execute( token_program::mint::mint( definition_account, - authority_account, user_holding_account, amount_to_mint, ctx.self_program_id, @@ -139,42 +141,16 @@ mod token { )) } - /// Create a new fungible token definition with a mint authority. - /// Unlike NewFungibleDefinition, this allows minting additional tokens later. - #[instruction] - pub fn new_fungible_definition_with_authority( - definition_target_account: AccountWithMetadata, - holding_target_account: AccountWithMetadata, - name: String, - initial_supply: u128, - mint_authority: [u8; 32], - ) -> SpelResult { - Ok(spel_framework::SpelOutput::execute( - token_program::new_definition::new_fungible_definition_with_authority( - definition_target_account, - holding_target_account, - name, - initial_supply, - mint_authority, - ), - vec![], - )) - } - - /// Set or rotate the mint authority for a fungible token definition. - /// Pass `new_authority: None` to permanently revoke minting (fixed supply). + /// Rotate or renounce the mint authority for a fungible token definition. + /// Pass `new_authority: None` to permanently renounce minting (fixed supply). + /// The definition account must be authorized as the current mint authority. #[instruction] pub fn set_authority( definition_account: AccountWithMetadata, - authority_account: AccountWithMetadata, - new_authority: Option<[u8; 32]>, + new_authority: Option, ) -> SpelResult { Ok(spel_framework::SpelOutput::execute( - token_program::set_authority::set_authority( - definition_account, - authority_account, - new_authority, - ), + token_program::set_authority::set_authority(definition_account, new_authority), vec![], )) } diff --git a/programs/token/src/burn.rs b/programs/token/src/burn.rs index f0777f6..e984745 100644 --- a/programs/token/src/burn.rs +++ b/programs/token/src/burn.rs @@ -31,7 +31,7 @@ pub fn burn( name: _, metadata_id: _, total_supply, - mint_authority: _, + authority: _, }, TokenHolding::Fungible { definition_id: _, diff --git a/programs/token/src/mint.rs b/programs/token/src/mint.rs index 745a12b..d2fdc42 100644 --- a/programs/token/src/mint.rs +++ b/programs/token/src/mint.rs @@ -1,4 +1,4 @@ -use lez_authority::AuthoritySlot; +use lez_authority::Ownable; use nssa_core::{ account::{Account, AccountWithMetadata, Data}, program::{AccountPostState, Claim, ProgramId}, @@ -7,7 +7,6 @@ use token_core::{TokenDefinition, TokenHolding}; pub fn mint( definition_account: AccountWithMetadata, - authority_account: AccountWithMetadata, user_holding_account: AccountWithMetadata, amount_to_mint: u128, token_program_id: ProgramId, @@ -20,20 +19,24 @@ pub fn mint( let mut definition = TokenDefinition::try_from(&definition_account.account.data) .expect("Token Definition account must be valid"); - // LP-0013 / RFP-001: gate minting through lez-authority. The authority_account - // is the signer and must match the stored mint authority. - if let TokenDefinition::Fungible { mint_authority, .. } = &definition { + // Minting is gated on the definition's mint authority: the definition account + // must be authorized in this transaction and its id must match the stored + // authority. This holds for an external owner that signs the definition key, + // and for a program-controlled PDA authorized via its seeds (e.g. the AMM's + // pool definition minting LP tokens). + if let TokenDefinition::Fungible { .. } = &definition { assert!( - authority_account.is_authorized, - "Mint authority must sign the transaction" + definition_account.is_authorized, + "Mint authority must authorize the transaction" ); - let signer: [u8; 32] = authority_account + let signer: [u8; 32] = definition_account .account_id .as_ref() .try_into() .expect("AccountId is always 32 bytes"); - let slot = AuthoritySlot(*mint_authority); - slot.check(signer).expect("Mint authority check failed"); + definition + .require_owner(signer) + .expect("Mint authority check failed"); } let mut holding = if user_holding_account.account == Account::default() { @@ -55,7 +58,7 @@ pub fn mint( name: _, metadata_id: _, total_supply, - mint_authority: _, + authority: _, }, TokenHolding::Fungible { definition_id: _, @@ -87,7 +90,6 @@ pub fn mint( vec![ AccountPostState::new(definition_post), - AccountPostState::new(authority_account.account), AccountPostState::new_claimed_if_default(holding_post, Claim::Authorized), ] } diff --git a/programs/token/src/new_definition.rs b/programs/token/src/new_definition.rs index 4a6eae3..32bad97 100644 --- a/programs/token/src/new_definition.rs +++ b/programs/token/src/new_definition.rs @@ -1,16 +1,39 @@ +use lez_authority::Authority; use nssa_core::{ - account::{Account, AccountWithMetadata, Data}, + account::{Account, AccountId, AccountWithMetadata, Data}, program::{AccountPostState, Claim}, }; use token_core::{ NewTokenDefinition, NewTokenMetadata, TokenDefinition, TokenHolding, TokenMetadata, }; +/// Build the embedded [`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 authority_from(mint_authority: Option) -> Authority { + match mint_authority { + Some(id) => { + let key: [u8; 32] = id + .as_ref() + .try_into() + .expect("AccountId is always 32 bytes"); + assert!( + key != [0u8; 32], + "Mint authority must be a valid non-zero account ID" + ); + Authority::new(key) + } + None => Authority::renounced(), + } +} + pub fn new_fungible_definition( definition_target_account: AccountWithMetadata, holding_target_account: AccountWithMetadata, name: String, total_supply: u128, + mint_authority: Option, ) -> Vec { assert_eq!( definition_target_account.account, @@ -36,7 +59,7 @@ pub fn new_fungible_definition( name, total_supply, metadata_id: None, - mint_authority: None, + authority: authority_from(mint_authority), }; let token_holding = TokenHolding::Fungible { definition_id: definition_target_account.account_id, @@ -98,7 +121,7 @@ pub fn new_definition_with_metadata( name, total_supply, metadata_id: Some(metadata_target_account.account_id), - mint_authority: None, + authority: Authority::renounced(), }, TokenHolding::Fungible { definition_id: definition_target_account.account_id, @@ -126,7 +149,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(); @@ -144,56 +167,3 @@ pub fn new_definition_with_metadata( AccountPostState::new_claimed(metadata_target_account_post, Claim::Authorized), ] } - -pub fn new_fungible_definition_with_authority( - definition_target_account: AccountWithMetadata, - holding_target_account: AccountWithMetadata, - name: String, - initial_supply: u128, - mint_authority: [u8; 32], -) -> Vec { - assert_eq!( - definition_target_account.account, - Account::default(), - "Definition target account must have default values" - ); - assert_eq!( - holding_target_account.account, - Account::default(), - "Holding target account must have default values" - ); - assert!( - definition_target_account.is_authorized, - "Definition target account must be authorized" - ); - assert!( - holding_target_account.is_authorized, - "Holding target account must be authorized" - ); - assert!( - mint_authority != [0u8; 32], - "Mint authority must be a valid non-zero account ID" - ); - - let token_definition = TokenDefinition::Fungible { - name, - total_supply: initial_supply, - metadata_id: None, - mint_authority: Some(mint_authority), - }; - let token_holding = TokenHolding::Fungible { - definition_id: definition_target_account.account_id, - balance: initial_supply, - }; - - let mut definition_target_account_post = definition_target_account.account; - definition_target_account_post.data = Data::from(&token_definition); - - let mut holding_target_account_post = holding_target_account.account; - holding_target_account_post.data = Data::from(&token_holding); - - vec![ - AccountPostState::new_claimed(definition_target_account_post, Claim::Authorized), - AccountPostState::new_claimed(holding_target_account_post, Claim::Authorized), - ] -} diff --git a/programs/token/src/set_authority.rs b/programs/token/src/set_authority.rs index a7ce696..91b4b1b 100644 --- a/programs/token/src/set_authority.rs +++ b/programs/token/src/set_authority.rs @@ -1,40 +1,52 @@ -use lez_authority::AuthoritySlot; +use lez_authority::Ownable; use nssa_core::{ - account::{AccountWithMetadata, Data}, + account::{AccountId, AccountWithMetadata, Data}, program::AccountPostState, }; use token_core::TokenDefinition; pub fn set_authority( definition_account: AccountWithMetadata, - authority_account: AccountWithMetadata, - new_authority: Option<[u8; 32]>, + new_authority: Option, ) -> Vec { let mut definition = TokenDefinition::try_from(&definition_account.account.data) .expect("Token Definition account must be valid"); match &mut definition { - TokenDefinition::Fungible { mint_authority, .. } => { + TokenDefinition::Fungible { .. } => { + // The current mint authority must authorize this transaction: the + // definition account must be authorized and its id must match the + // stored authority. assert!( - authority_account.is_authorized, - "Mint authority must sign the transaction" + definition_account.is_authorized, + "Mint authority must authorize the transaction" ); - - if let Some(new_key) = new_authority { - assert!( - new_key != [0u8; 32], - "New mint authority must be a valid non-zero account ID" - ); - } - let signer: [u8; 32] = authority_account + let signer: [u8; 32] = definition_account .account_id .as_ref() .try_into() .expect("AccountId is always 32 bytes"); - let mut slot = AuthoritySlot(*mint_authority); - slot.set(signer, new_authority) - .expect("SetAuthority failed"); - *mint_authority = slot.0; + + match new_authority { + Some(new) => { + let new_key: [u8; 32] = new + .as_ref() + .try_into() + .expect("AccountId is always 32 bytes"); + assert!( + new_key != [0u8; 32], + "New mint authority must be a valid non-zero account ID" + ); + definition + .transfer_ownership(signer, new_key) + .expect("SetAuthority failed"); + } + None => { + definition + .renounce_ownership(signer) + .expect("SetAuthority failed"); + } + } } TokenDefinition::NonFungible { .. } => { panic!("SetAuthority is not supported for Non-Fungible Tokens"); @@ -44,8 +56,5 @@ pub fn set_authority( let mut definition_post = definition_account.account; definition_post.data = Data::from(&definition); - vec![ - AccountPostState::new(definition_post), - AccountPostState::new(authority_account.account), - ] + vec![AccountPostState::new(definition_post)] } diff --git a/programs/token/src/tests.rs b/programs/token/src/tests.rs index b31399e..502aa2b 100644 --- a/programs/token/src/tests.rs +++ b/programs/token/src/tests.rs @@ -42,7 +42,7 @@ impl AccountForTests { name: String::from("test"), total_supply: BalanceForTests::init_supply(), metadata_id: None, - mint_authority: Some([15_u8; 32]), + authority: lez_authority::Authority::new([15_u8; 32]), }), nonce: Nonce(0), }, @@ -51,16 +51,6 @@ impl AccountForTests { } } - /// A signed authority account whose ID matches the [15; 32] mint authority - /// used by definition_account_auth() / definition_account_mint(). - fn authority_account_auth() -> AccountWithMetadata { - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - fn definition_account_foreign_owner() -> AccountWithMetadata { AccountWithMetadata { account: Account { @@ -70,7 +60,7 @@ impl AccountForTests { name: String::from("test"), total_supply: BalanceForTests::init_supply(), metadata_id: None, - mint_authority: None, + authority: lez_authority::Authority::renounced(), }), nonce: Nonce(0), }, @@ -88,7 +78,7 @@ impl AccountForTests { name: String::from("test"), total_supply: BalanceForTests::init_supply(), metadata_id: None, - mint_authority: None, + authority: lez_authority::Authority::renounced(), }), nonce: Nonce(0), }, @@ -170,7 +160,7 @@ impl AccountForTests { name: String::from("test"), total_supply: BalanceForTests::init_supply_burned(), metadata_id: None, - mint_authority: Some([15_u8; 32]), + authority: lez_authority::Authority::new([15_u8; 32]), }), nonce: Nonce(0), }, @@ -252,7 +242,7 @@ impl AccountForTests { name: String::from("test"), total_supply: BalanceForTests::init_supply_mint(), metadata_id: None, - mint_authority: Some([15_u8; 32]), + authority: lez_authority::Authority::new([15_u8; 32]), }), nonce: Nonce(0), }, @@ -343,7 +333,7 @@ impl AccountForTests { name: String::from("test"), total_supply: BalanceForTests::init_supply(), metadata_id: None, - mint_authority: None, + authority: lez_authority::Authority::renounced(), }), nonce: Nonce(0), }, @@ -610,6 +600,7 @@ fn test_new_definition_non_default_first_account_should_fail() { holding_account, String::from("test"), 10, + None, ); } @@ -634,6 +625,7 @@ fn test_new_definition_non_default_second_account_should_fail() { holding_account, String::from("test"), 10, + None, ); } @@ -647,6 +639,7 @@ fn test_new_definition_requires_authorized_definition_target() { holding_account, String::from("test"), 10, + None, ); } @@ -660,6 +653,7 @@ fn test_new_definition_requires_authorized_holding_target() { holding_account, String::from("test"), 10, + None, ); } @@ -673,6 +667,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(); @@ -914,7 +909,6 @@ fn test_mint_not_valid_holding_account() { let holding_account = AccountForTests::definition_account_without_auth(); let _post_states = mint( definition_account, - AccountForTests::authority_account_auth(), holding_account, BalanceForTests::mint_success(), TOKEN_PROGRAM_ID, @@ -928,7 +922,6 @@ fn test_mint_not_valid_definition_account() { let holding_account = AccountForTests::holding_same_definition_without_authorization(); let _post_states = mint( definition_account, - AccountForTests::authority_account_auth(), holding_account, BalanceForTests::mint_success(), TOKEN_PROGRAM_ID, @@ -936,19 +929,14 @@ fn test_mint_not_valid_definition_account() { } #[test] -#[should_panic(expected = "Mint authority must sign the transaction")] +#[should_panic(expected = "Mint authority must authorize the transaction")] fn test_mint_missing_authorization() { - let definition_account = AccountForTests::definition_account_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(); - // authority account that is NOT signed - let unsigned_authority = AccountWithMetadata { - account: Account::default(), - is_authorized: false, - account_id: IdForTests::pool_definition_id(), - }; let _post_states = mint( definition_account, - unsigned_authority, holding_account, BalanceForTests::mint_success(), TOKEN_PROGRAM_ID, @@ -962,7 +950,6 @@ fn test_mint_rejects_foreign_owned_definition() { let holding_account = AccountForTests::holding_account_uninit(); let _post_states = mint( definition_account, - AccountForTests::authority_account_auth(), holding_account, BalanceForTests::mint_success(), TOKEN_PROGRAM_ID, @@ -977,7 +964,6 @@ fn test_mint_mismatched_token_definition() { let holding_account = AccountForTests::holding_different_definition(); let _post_states = mint( definition_account, - AccountForTests::authority_account_auth(), holding_account, BalanceForTests::mint_success(), TOKEN_PROGRAM_ID, @@ -990,13 +976,12 @@ fn test_mint_success() { let holding_account = AccountForTests::holding_same_definition_without_authorization(); let post_states = mint( definition_account, - AccountForTests::authority_account_auth(), holding_account, BalanceForTests::mint_success(), TOKEN_PROGRAM_ID, ); - let [def_post, _authority_post, holding_post] = post_states.try_into().unwrap(); + let [def_post, holding_post] = post_states.try_into().unwrap(); assert_eq!( *def_post.account(), @@ -1016,13 +1001,12 @@ fn test_mint_uninit_holding_success() { let holding_account = AccountForTests::holding_account_uninit(); let post_states = mint( definition_account, - AccountForTests::authority_account_auth(), holding_account, BalanceForTests::mint_success(), TOKEN_PROGRAM_ID, ); - let [def_post, _authority_post, holding_post] = post_states.try_into().unwrap(); + let [def_post, holding_post] = post_states.try_into().unwrap(); assert_eq!( *def_post.account(), @@ -1043,7 +1027,6 @@ fn test_mint_total_supply_overflow() { let holding_account = AccountForTests::holding_same_definition_without_authorization(); let _post_states = mint( definition_account, - AccountForTests::authority_account_auth(), holding_account, BalanceForTests::mint_overflow(), TOKEN_PROGRAM_ID, @@ -1057,7 +1040,6 @@ fn test_mint_holding_account_overflow() { let holding_account = AccountForTests::holding_same_definition_without_authorization_overflow(); let _post_states = mint( definition_account, - AccountForTests::authority_account_auth(), holding_account, BalanceForTests::mint_overflow(), TOKEN_PROGRAM_ID, @@ -1071,7 +1053,6 @@ fn test_mint_cannot_mint_unmintable_tokens() { let holding_account = AccountForTests::holding_account_master_nft(); let _post_states = mint( definition_account, - AccountForTests::authority_account_auth(), holding_account, BalanceForTests::mint_success(), TOKEN_PROGRAM_ID, @@ -1355,6 +1336,9 @@ mod authority_tests { 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 { @@ -1364,7 +1348,7 @@ mod authority_tests { name: String::from("test"), total_supply: 100_000_u128, metadata_id: None, - mint_authority: Some(AUTHORITY), + authority: lez_authority::Authority::new(AUTHORITY), }), nonce: 0_u128.into(), }, @@ -1373,6 +1357,7 @@ mod authority_tests { } } + /// A definition whose authority has been renounced (fixed supply). fn def_with_authority_revoked() -> AccountWithMetadata { AccountWithMetadata { account: Account { @@ -1382,7 +1367,7 @@ mod authority_tests { name: String::from("test"), total_supply: 100_000_u128, metadata_id: None, - mint_authority: None, + authority: lez_authority::Authority::renounced(), }), nonce: 0_u128.into(), }, @@ -1391,6 +1376,26 @@ mod authority_tests { } } + /// 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: lez_authority::Authority::new(AUTHORITY), + }), + nonce: 0_u128.into(), + }, + is_authorized: true, + account_id: AccountId::new([99; 32]), + } + } + fn holding_account() -> AccountWithMetadata { AccountWithMetadata { account: Account { @@ -1407,34 +1412,15 @@ mod authority_tests { } } - /// Signed authority matching the [15; 32] stored mint authority. - fn authority_signer() -> AccountWithMetadata { - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([15; 32]), - } - } - - /// A different signer (Bob) — NOT the current authority. - fn wrong_authority_signer() -> AccountWithMetadata { - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([99; 32]), - } - } - #[test] fn mint_with_authority_succeeds() { let post_states = mint( def_with_authority(), - authority_signer(), holding_account(), 50_000, TOKEN_PROGRAM_ID, ); - let [def_post, _authority_post, holding_post] = post_states.try_into().unwrap(); + 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(); @@ -1443,7 +1429,6 @@ mod authority_tests { def, TokenDefinition::Fungible { total_supply: 150_000, - mint_authority: Some(_), .. } )); @@ -1461,7 +1446,6 @@ mod authority_tests { fn mint_with_revoked_authority_fails() { let _ = mint( def_with_authority_revoked(), - authority_signer(), holding_account(), 50_000, TOKEN_PROGRAM_ID, @@ -1469,16 +1453,18 @@ mod authority_tests { } #[test] - #[should_panic(expected = "Mint authority must sign the transaction")] + #[should_panic(expected = "Mint authority must authorize the transaction")] fn mint_without_is_authorized_fails() { - let unsigned_authority = AccountWithMetadata { - account: Account::default(), - is_authorized: false, - account_id: AccountId::new([15; 32]), - }; + 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_with_authority(), - unsigned_authority, + def_wrong_authority(), holding_account(), 50_000, TOKEN_PROGRAM_ID, @@ -1488,47 +1474,34 @@ mod authority_tests { #[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(), authority_signer(), Some([0u8; 32])); + let _ = set_authority(def_with_authority(), Some(AccountId::new([0u8; 32]))); } #[test] fn set_authority_rotates_to_new_key() { - let new_key = [7_u8; 32]; - let post_states = set_authority(def_with_authority(), authority_signer(), Some(new_key)); - let [def_post, _authority_post] = post_states.try_into().unwrap(); + let new_key = AccountId::new([7_u8; 32]); + let post_states = set_authority(def_with_authority(), Some(new_key)); + let [def_post] = post_states.try_into().unwrap(); let def = TokenDefinition::try_from(&def_post.account().data).unwrap(); - assert!(matches!( - def, - TokenDefinition::Fungible { mint_authority: Some(k), .. } if k == new_key - )); - } - - #[test] - #[should_panic(expected = "Mint authority check failed")] - fn mint_with_wrong_signer_fails() { - let _ = mint( - def_with_authority(), - wrong_authority_signer(), - holding_account(), - 50_000, - TOKEN_PROGRAM_ID, - ); + let auth = match def { + TokenDefinition::Fungible { authority, .. } => authority.authority(), + _ => None, + }; + assert_eq!(auth, Some([7_u8; 32])); } #[test] fn set_authority_revokes_permanently() { - let post_states = set_authority(def_with_authority(), authority_signer(), None); - let [def_post, _authority_post] = post_states.try_into().unwrap(); + let post_states = set_authority(def_with_authority(), None); + let [def_post] = post_states.try_into().unwrap(); let def = TokenDefinition::try_from(&def_post.account().data).unwrap(); - assert!(matches!( - def, - TokenDefinition::Fungible { - mint_authority: None, - .. - } - )); + let renounced = match def { + TokenDefinition::Fungible { authority, .. } => authority.is_renounced(), + _ => false, + }; + assert!(renounced); } #[test] @@ -1536,60 +1509,37 @@ mod authority_tests { fn set_authority_on_revoked_fails() { let _ = set_authority( def_with_authority_revoked(), - authority_signer(), - Some([7_u8; 32]), + Some(AccountId::new([7_u8; 32])), ); } #[test] - #[should_panic(expected = "Mint authority must sign the transaction")] + #[should_panic(expected = "Mint authority must authorize the transaction")] fn set_authority_without_is_authorized_fails() { - let unsigned_authority = AccountWithMetadata { - account: Account::default(), - is_authorized: false, - account_id: AccountId::new([15; 32]), - }; - let _ = set_authority(def_with_authority(), unsigned_authority, Some([7_u8; 32])); + let mut def = def_with_authority(); + def.is_authorized = false; + let _ = set_authority(def, Some(AccountId::new([7_u8; 32]))); } #[test] #[should_panic(expected = "SetAuthority failed")] fn set_authority_wrong_signer_fails() { - let _ = set_authority( - def_with_authority(), - wrong_authority_signer(), - Some([7_u8; 32]), - ); - } - - #[should_panic(expected = "Mint authority must be a valid non-zero account ID")] - #[test] - fn test_new_fungible_definition_with_authority_rejects_zero_authority() { - let definition_account = AccountForTests::definition_account_uninit_auth(); - let holding_account = AccountForTests::holding_account_uninit_auth(); - let _post_states = crate::new_definition::new_fungible_definition_with_authority( - definition_account, - holding_account, - String::from("test"), - 1000, - [0u8; 32], - ); + let _ = set_authority(def_wrong_authority(), Some(AccountId::new([7_u8; 32]))); } #[test] fn set_authority_rotate_then_old_cannot_mint() { - let new_key = [7_u8; 32]; - let post_states = set_authority(def_with_authority(), authority_signer(), Some(new_key)); - let [def_post, _authority_post] = post_states.try_into().unwrap(); + let new_key = AccountId::new([7_u8; 32]); + let post_states = set_authority(def_with_authority(), Some(new_key)); + let [def_post] = post_states.try_into().unwrap(); let def = TokenDefinition::try_from(&def_post.account().data).unwrap(); - assert!(matches!( - def, - TokenDefinition::Fungible { mint_authority: Some(k), .. } if k == new_key - )); - assert!(!matches!( - def, - TokenDefinition::Fungible { mint_authority: Some(k), .. } if k == AUTHORITY - )); + let auth = match def { + TokenDefinition::Fungible { authority, .. } => authority.authority(), + _ => None, + }; + // Rotated to the new key; the old authority no longer controls it. + assert_eq!(auth, Some([7_u8; 32])); + assert_ne!(auth, Some(AUTHORITY)); } }