refactor(authority): embed Authority type in TokenDefinition; fix AMM LP minting

Addresses @0x-r4bbit's review:

- lez-authority now provides an Authority(Option<[u8;32]>) newtype and an
  Ownable trait (require_owner / transfer_ownership / renounce_ownership);
  programs embed the authority slot in their account type instead of calling
  a wrapper. Replaces the old AuthoritySlot.
- TokenDefinition::Fungible embeds authority: Authority; TokenDefinition
  implements Ownable.
- Fold mint authority into NewFungibleDefinition { mint_authority: Option<AccountId> };
  remove the separate NewFungibleDefinitionWithAuthority instruction.
- mint/set_authority authorize against the definition account itself (its id
  must match the stored authority and be authorized in the tx), restoring the
  2-account mint shape and supporting PDA authorities.
- Fix AMM: the pool-definition PDA is now the LP token's mint authority, so the
  AMM mints LP at creation and on add-liquidity (was permanently revoked).
- Instruction params use AccountId; remove LP-0013-specific comments.
- Regenerate token/amm/ata/stablecoin IDLs.

Tests: lez-authority 8, token unit 56, token/amm/stablecoin/ata integration all
green under RISC0_DEV_MODE=1; fmt + clippy clean.
This commit is contained in:
bristinWild 2026-06-06 03:15:30 +05:30
parent c2a7d753d7
commit 83df2037ef
23 changed files with 463 additions and 449 deletions

View File

@ -666,6 +666,12 @@
"type": {
"option": "account_id"
}
},
{
"name": "authority",
"type": {
"defined": "Authority"
}
}
]
},

View File

@ -120,6 +120,12 @@
"type": {
"option": "account_id"
}
},
{
"name": "authority",
"type": {
"defined": "Authority"
}
}
]
},

View File

@ -160,6 +160,12 @@
"type": {
"option": "account_id"
}
},
{
"name": "authority",
"type": {
"defined": "Authority"
}
}
]
},

View File

@ -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"
}
}
]

View File

@ -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"] }

View File

@ -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());
}
}

View File

@ -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" }

View File

@ -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()],

View File

@ -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![

View File

@ -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),
},

View File

@ -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")

View File

@ -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),
}

View File

@ -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),
}

View File

@ -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),
}
);
}

View File

@ -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),
},

View File

@ -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" }

View File

@ -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<AccountId>,
},
/// 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<AccountId> },
}
#[derive(Serialize, Deserialize)]
@ -105,9 +104,9 @@ pub enum TokenDefinition {
name: String,
total_supply: u128,
metadata_id: Option<AccountId>,
/// 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;

View File

@ -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<AccountId>,
) -> 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<AccountId>,
) -> 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![],
))
}

View File

@ -31,7 +31,7 @@ pub fn burn(
name: _,
metadata_id: _,
total_supply,
mint_authority: _,
authority: _,
},
TokenHolding::Fungible {
definition_id: _,

View File

@ -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),
]
}

View File

@ -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<AccountId>) -> 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<AccountId>,
) -> Vec<AccountPostState> {
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<AccountPostState> {
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),
]
}

View File

@ -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<AccountId>,
) -> Vec<AccountPostState> {
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)]
}

View File

@ -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));
}
}