From 175c9d256c1533276b0bd9a2067622378dccf16d Mon Sep 17 00:00:00 2001 From: bristinWild Date: Wed, 3 Jun 2026 01:10:08 +0530 Subject: [PATCH] refactor: gate mint/set_authority via lez-authority with explicit signer account --- Cargo.lock | 7 - artifacts/token-idl.json | 12 ++ programs/integration_tests/tests/token.rs | 67 ++++++--- programs/token/Cargo.toml | 1 + programs/token/methods/guest/src/bin/token.rs | 51 ++++--- programs/token/src/mint.rs | 32 ++-- programs/token/src/set_authority.rs | 40 +++-- programs/token/src/tests.rs | 138 +++++++++++++----- scripts/demo-full-flow.sh | 33 ++++- 9 files changed, 250 insertions(+), 131 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0cd90e..161af19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2251,13 +2251,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "lez-authority" -version = "0.1.0" -dependencies = [ - "borsh", -] - [[package]] name = "libc" version = "0.2.186" diff --git a/artifacts/token-idl.json b/artifacts/token-idl.json index 1867037..5174f95 100644 --- a/artifacts/token-idl.json +++ b/artifacts/token-idl.json @@ -139,6 +139,12 @@ "signer": true, "init": false }, + { + "name": "authority_account", + "writable": false, + "signer": false, + "init": false + }, { "name": "user_holding_account", "writable": true, @@ -197,6 +203,12 @@ "writable": false, "signer": false, "init": false + }, + { + "name": "authority_account", + "writable": false, + "signer": false, + "init": false } ], "args": [ diff --git a/programs/integration_tests/tests/token.rs b/programs/integration_tests/tests/token.rs index 61ad335..6418b50 100644 --- a/programs/integration_tests/tests/token.rs +++ b/programs/integration_tests/tests/token.rs @@ -28,6 +28,10 @@ impl Keys { fn recipient_key() -> PrivateKey { PrivateKey::try_new([12; 32]).expect("valid private key") } + + fn authority_key() -> PrivateKey { + PrivateKey::try_new([13; 32]).expect("valid private key") + } } impl Ids { @@ -50,6 +54,10 @@ impl Ids { fn recipient() -> AccountId { AccountId::from(&PublicKey::new_from_private_key(&Keys::recipient_key())) } + + fn authority() -> AccountId { + AccountId::from(&PublicKey::new_from_private_key(&Keys::authority_key())) + } } impl Accounts { @@ -62,7 +70,7 @@ impl Accounts { total_supply: 1_000_000_u128, metadata_id: None, mint_authority: Some( - Ids::token_definition() + Ids::authority() .as_ref() .try_into() .expect("AccountId is always 32 bytes"), @@ -81,7 +89,7 @@ impl Accounts { total_supply: 1_000_000_u128, metadata_id: None, mint_authority: Some( - Ids::token_definition() + Ids::authority() .as_ref() .try_into() .expect("AccountId is always 32 bytes"), @@ -114,6 +122,15 @@ impl Accounts { nonce: Nonce(0), } } + + fn authority_init() -> Account { + Account { + program_owner: Ids::token_program(), + balance: 0_u128, + data: Data::default(), + nonce: Nonce(0), + } + } } fn deploy_token(state: &mut V03State) { @@ -130,6 +147,7 @@ fn state_for_token_tests() -> V03State { state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init()); state.force_insert_account(Ids::holder(), Accounts::holder_init()); state.force_insert_account(Ids::recipient(), Accounts::recipient_init()); + state.force_insert_account(Ids::authority(), Accounts::authority_init()); state } @@ -138,6 +156,7 @@ fn state_for_token_tests_without_recipient() -> V03State { deploy_token(&mut state); state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init()); state.force_insert_account(Ids::holder(), Accounts::holder_init()); + state.force_insert_account(Ids::authority(), Accounts::authority_init()); state } @@ -429,7 +448,7 @@ fn token_burn() { total_supply: 800_000_u128, metadata_id: None, mint_authority: Some( - Ids::token_definition() + Ids::authority() .as_ref() .try_into() .expect("AccountId is always 32 bytes") @@ -463,13 +482,14 @@ fn token_mint() { let message = public_transaction::Message::try_new( Ids::token_program(), - vec![Ids::token_definition(), Ids::holder()], + vec![Ids::token_definition(), Ids::authority(), Ids::holder()], vec![Nonce(0)], instruction, ) .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::def_key()]); + let witness_set = + public_transaction::WitnessSet::for_message(&message, &[&Keys::authority_key()]); let tx = PublicTransaction::new(message, witness_set); state.transition_from_public_transaction(&tx, 0, 0).unwrap(); @@ -484,13 +504,13 @@ fn token_mint() { total_supply: 1_500_000_u128, metadata_id: None, mint_authority: Some( - Ids::token_definition() + Ids::authority() .as_ref() .try_into() .expect("AccountId is always 32 bytes") ), }), - nonce: Nonce(1), + nonce: Nonce(0), } ); @@ -522,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::recipient()], + vec![Ids::token_definition(), Ids::authority(), Ids::recipient()], vec![Nonce(0), Nonce(0)], instruction, ) @@ -530,7 +550,7 @@ fn token_mint_rejects_foreign_owned_definition() { let witness_set = public_transaction::WitnessSet::for_message( &message, - &[&Keys::def_key(), &Keys::recipient_key()], + &[&Keys::authority_key(), &Keys::recipient_key()], ); let tx = PublicTransaction::new(message, witness_set); @@ -556,13 +576,14 @@ fn token_mint_fresh_public_recipient_requires_authorization() { let message = public_transaction::Message::try_new( Ids::token_program(), - vec![Ids::token_definition(), Ids::recipient()], + vec![Ids::token_definition(), Ids::authority(), Ids::recipient()], vec![Nonce(0)], instruction, ) .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::def_key()]); + let witness_set = + public_transaction::WitnessSet::for_message(&message, &[&Keys::authority_key()]); let tx = PublicTransaction::new(message, witness_set); assert!(state.transition_from_public_transaction(&tx, 0, 0).is_err()); @@ -587,7 +608,7 @@ fn token_mint_fresh_authorized_public_recipient() { let message = public_transaction::Message::try_new( Ids::token_program(), - vec![Ids::token_definition(), Ids::recipient()], + vec![Ids::token_definition(), Ids::authority(), Ids::recipient()], vec![Nonce(0), Nonce(0)], instruction, ) @@ -595,7 +616,7 @@ fn token_mint_fresh_authorized_public_recipient() { let witness_set = public_transaction::WitnessSet::for_message( &message, - &[&Keys::def_key(), &Keys::recipient_key()], + &[&Keys::authority_key(), &Keys::recipient_key()], ); let tx = PublicTransaction::new(message, witness_set); @@ -611,13 +632,13 @@ fn token_mint_fresh_authorized_public_recipient() { total_supply: 1_500_000_u128, metadata_id: None, mint_authority: Some( - Ids::token_definition() + Ids::authority() .as_ref() .try_into() .expect("AccountId is always 32 bytes") ), }), - nonce: Nonce(1), + nonce: Nonce(0), } ); @@ -993,11 +1014,10 @@ 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::token_definition() + let authority_key: [u8; 32] = Ids::authority() .as_ref() .try_into() .expect("AccountId is always 32 bytes"); - // Create token with authority let instruction = token_core::Instruction::NewFungibleDefinitionWithAuthority { name: String::from("AuthCoin"), @@ -1017,18 +1037,23 @@ fn token_set_authority_revoke() { ); let tx = PublicTransaction::new(message, witness_set); state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + + // Seed the authority account so it can sign the revoke + state.force_insert_account(Ids::authority(), Accounts::authority_init()); + // Revoke authority let instruction = token_core::Instruction::SetAuthority { new_authority: None, }; let message = public_transaction::Message::try_new( Ids::token_program(), - vec![Ids::token_definition()], - vec![Nonce(1)], + vec![Ids::token_definition(), Ids::authority()], + vec![Nonce(0)], instruction, ) .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::def_key()]); + let witness_set = + public_transaction::WitnessSet::for_message(&message, &[&Keys::authority_key()]); let tx = PublicTransaction::new(message, witness_set); state.transition_from_public_transaction(&tx, 0, 0).unwrap(); assert_eq!( @@ -1042,7 +1067,7 @@ fn token_set_authority_revoke() { metadata_id: None, mint_authority: None, }), - nonce: Nonce(2), + nonce: Nonce(1), } ); } diff --git a/programs/token/Cargo.toml b/programs/token/Cargo.toml index ee016ac..f96aafc 100644 --- a/programs/token/Cargo.toml +++ b/programs/token/Cargo.toml @@ -9,3 +9,4 @@ workspace = true [dependencies] nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc6", features = ["host"], package = "lee_core" } token_core = { path = "core" } +lez-authority = { path = "../../lez-authority" } diff --git a/programs/token/methods/guest/src/bin/token.rs b/programs/token/methods/guest/src/bin/token.rs index c5f524d..0490692 100644 --- a/programs/token/methods/guest/src/bin/token.rs +++ b/programs/token/methods/guest/src/bin/token.rs @@ -1,8 +1,8 @@ #![cfg_attr(not(test), no_main)] -use spel_framework::prelude::*; -use spel_framework::context::ProgramContext; use nssa_core::account::AccountWithMetadata; +use spel_framework::context::ProgramContext; +use spel_framework::prelude::*; #[cfg(not(test))] risc0_zkvm::guest::entry!(main); @@ -25,11 +25,10 @@ mod token { recipient: AccountWithMetadata, amount_to_transfer: u128, ) -> SpelResult { - Ok(spel_framework::SpelOutput::execute(token_program::transfer::transfer( - sender, - recipient, - amount_to_transfer, - ), vec![])) + Ok(spel_framework::SpelOutput::execute( + token_program::transfer::transfer(sender, recipient, amount_to_transfer), + vec![], + )) } /// Create a new fungible token definition without metadata. @@ -111,11 +110,10 @@ mod token { user_holding_account: AccountWithMetadata, amount_to_burn: u128, ) -> SpelResult { - Ok(spel_framework::SpelOutput::execute(token_program::burn::burn( - definition_account, - user_holding_account, - amount_to_burn, - ), vec![])) + Ok(spel_framework::SpelOutput::execute( + token_program::burn::burn(definition_account, user_holding_account, amount_to_burn), + vec![], + )) } /// Mint new tokens to the holder's account. @@ -125,19 +123,22 @@ mod token { ctx: ProgramContext, #[account(mut, signer)] definition_account: AccountWithMetadata, - #[account(mut)] + authority_account: AccountWithMetadata, user_holding_account: AccountWithMetadata, amount_to_mint: u128, ) -> SpelResult { - Ok(spel_framework::SpelOutput::execute(token_program::mint::mint( - definition_account, - user_holding_account, - amount_to_mint, - ctx.self_program_id, - ), vec![])) + Ok(spel_framework::SpelOutput::execute( + token_program::mint::mint( + definition_account, + authority_account, + user_holding_account, + amount_to_mint, + ctx.self_program_id, + ), + vec![], + )) } - /// Create a new fungible token definition with a mint authority. /// Unlike NewFungibleDefinition, this allows minting additional tokens later. #[instruction] @@ -165,11 +166,13 @@ mod token { #[instruction] pub fn set_authority( definition_account: AccountWithMetadata, + authority_account: AccountWithMetadata, new_authority: Option<[u8; 32]>, ) -> SpelResult { Ok(spel_framework::SpelOutput::execute( token_program::set_authority::set_authority( definition_account, + authority_account, new_authority, ), vec![], @@ -185,9 +188,9 @@ mod token { #[account(init, signer)] printed_account: AccountWithMetadata, ) -> SpelResult { - Ok(spel_framework::SpelOutput::execute(token_program::print_nft::print_nft( - master_account, - printed_account, - ), vec![])) + Ok(spel_framework::SpelOutput::execute( + token_program::print_nft::print_nft(master_account, printed_account), + vec![], + )) } } diff --git a/programs/token/src/mint.rs b/programs/token/src/mint.rs index 2b21359..745a12b 100644 --- a/programs/token/src/mint.rs +++ b/programs/token/src/mint.rs @@ -1,3 +1,4 @@ +use lez_authority::AuthoritySlot; use nssa_core::{ account::{Account, AccountWithMetadata, Data}, program::{AccountPostState, Claim, ProgramId}, @@ -6,14 +7,11 @@ 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, ) -> Vec { - assert!( - definition_account.is_authorized, - "Definition authorization is missing" - ); assert_eq!( definition_account.account.program_owner, token_program_id, "Token definition must be owned by token program" @@ -22,19 +20,22 @@ pub fn mint( let mut definition = TokenDefinition::try_from(&definition_account.account.data) .expect("Token Definition account must be valid"); - // LP-0013: enforce mint authority — minting is only allowed if mint_authority is Some. + // 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 { - match mint_authority { - None => panic!("Mint authority has been revoked; this token has a fixed supply"), - Some(authority_key) => { - assert_eq!( - definition_account.account_id.as_ref(), - authority_key, - "Signer is not the mint authority" - ); - } - } + assert!( + authority_account.is_authorized, + "Mint authority must sign the transaction" + ); + let signer: [u8; 32] = authority_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"); } + let mut holding = if user_holding_account.account == Account::default() { TokenHolding::zeroized_from_definition(definition_account.account_id, &definition) } else { @@ -86,6 +87,7 @@ 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/set_authority.rs b/programs/token/src/set_authority.rs index b324a6f..5b664d5 100644 --- a/programs/token/src/set_authority.rs +++ b/programs/token/src/set_authority.rs @@ -1,38 +1,33 @@ +use lez_authority::AuthoritySlot; use nssa_core::{ account::{AccountWithMetadata, Data}, program::AccountPostState, }; use token_core::TokenDefinition; -#[must_use] pub fn set_authority( definition_account: AccountWithMetadata, + authority_account: AccountWithMetadata, new_authority: Option<[u8; 32]>, ) -> Vec { - assert!( - definition_account.is_authorized, - "Definition account authorization is missing; only the mint authority can call SetAuthority" - ); - let mut definition = TokenDefinition::try_from(&definition_account.account.data) .expect("Token Definition account must be valid"); match &mut definition { TokenDefinition::Fungible { mint_authority, .. } => { - match mint_authority { - None => { - panic!("Mint authority already revoked; supply is permanently fixed"); - } - Some(authority_key) => { - // Validate caller matches the stored mint authority key - assert_eq!( - definition_account.account_id.as_ref(), - authority_key.as_ref(), - "Signer does not match the stored mint authority" - ); - *mint_authority = new_authority; - } - } + assert!( + authority_account.is_authorized, + "Mint authority must sign the transaction" + ); + let signer: [u8; 32] = authority_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; } TokenDefinition::NonFungible { .. } => { panic!("SetAuthority is not supported for Non-Fungible Tokens"); @@ -42,5 +37,8 @@ pub fn set_authority( let mut definition_post = definition_account.account; definition_post.data = Data::from(&definition); - vec![AccountPostState::new(definition_post)] + vec![ + AccountPostState::new(definition_post), + AccountPostState::new(authority_account.account), + ] } diff --git a/programs/token/src/tests.rs b/programs/token/src/tests.rs index 01c56c9..552f03d 100644 --- a/programs/token/src/tests.rs +++ b/programs/token/src/tests.rs @@ -51,6 +51,16 @@ 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 { @@ -904,6 +914,7 @@ 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, @@ -917,6 +928,7 @@ 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, @@ -924,12 +936,19 @@ fn test_mint_not_valid_definition_account() { } #[test] -#[should_panic(expected = "Definition authorization is missing")] +#[should_panic(expected = "Mint authority must sign the transaction")] fn test_mint_missing_authorization() { - let definition_account = AccountForTests::definition_account_without_auth(); + let definition_account = AccountForTests::definition_account_auth(); 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, @@ -943,6 +962,7 @@ 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, @@ -952,10 +972,12 @@ fn test_mint_rejects_foreign_owned_definition() { #[test] #[should_panic(expected = "Mismatch Token Definition and Token Holding")] fn test_mint_mismatched_token_definition() { + // let definition_account = AccountForTests::definition_account_auth(); let holding_account = AccountForTests::holding_different_definition(); let _post_states = mint( definition_account, + AccountForTests::authority_account_auth(), holding_account, BalanceForTests::mint_success(), TOKEN_PROGRAM_ID, @@ -968,12 +990,13 @@ 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, holding_post] = post_states.try_into().unwrap(); + let [def_post, _authority_post, holding_post] = post_states.try_into().unwrap(); assert_eq!( *def_post.account(), @@ -993,12 +1016,13 @@ 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, holding_post] = post_states.try_into().unwrap(); + let [def_post, _authority_post, holding_post] = post_states.try_into().unwrap(); assert_eq!( *def_post.account(), @@ -1019,6 +1043,7 @@ 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, @@ -1032,6 +1057,7 @@ 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, @@ -1045,6 +1071,7 @@ 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, @@ -1364,24 +1391,6 @@ mod authority_tests { } } - fn def_without_auth_flag() -> 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, - mint_authority: Some(AUTHORITY), - }), - nonce: 0_u128.into(), - }, - is_authorized: false, - account_id: AccountId::new([15; 32]), - } - } - fn holding_account() -> AccountWithMetadata { AccountWithMetadata { account: Account { @@ -1398,15 +1407,34 @@ 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, holding_post] = post_states.try_into().unwrap(); + let [def_post, _authority_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(); @@ -1429,10 +1457,11 @@ mod authority_tests { } #[test] - #[should_panic(expected = "Mint authority has been revoked; this token has a fixed supply")] + #[should_panic(expected = "Mint authority check failed")] fn mint_with_revoked_authority_fails() { let _ = mint( def_with_authority_revoked(), + authority_signer(), holding_account(), 50_000, TOKEN_PROGRAM_ID, @@ -1440,10 +1469,16 @@ mod authority_tests { } #[test] - #[should_panic(expected = "Definition authorization is missing")] + #[should_panic(expected = "Mint authority must sign 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 _ = mint( - def_without_auth_flag(), + def_with_authority(), + unsigned_authority, holding_account(), 50_000, TOKEN_PROGRAM_ID, @@ -1453,8 +1488,8 @@ mod authority_tests { #[test] fn set_authority_rotates_to_new_key() { let new_key = [7_u8; 32]; - let post_states = set_authority(def_with_authority(), Some(new_key)); - let [def_post] = post_states.try_into().unwrap(); + let post_states = set_authority(def_with_authority(), authority_signer(), Some(new_key)); + let [def_post, _authority_post] = post_states.try_into().unwrap(); let def = TokenDefinition::try_from(&def_post.account().data).unwrap(); assert!(matches!( @@ -1463,10 +1498,22 @@ mod authority_tests { )); } + #[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, + ); + } + #[test] fn set_authority_revokes_permanently() { - let post_states = set_authority(def_with_authority(), None); - let [def_post] = post_states.try_into().unwrap(); + let post_states = set_authority(def_with_authority(), authority_signer(), None); + let [def_post, _authority_post] = post_states.try_into().unwrap(); let def = TokenDefinition::try_from(&def_post.account().data).unwrap(); assert!(matches!( @@ -1479,22 +1526,41 @@ mod authority_tests { } #[test] - #[should_panic(expected = "Mint authority already revoked; supply is permanently fixed")] + #[should_panic(expected = "SetAuthority failed")] fn set_authority_on_revoked_fails() { - let _ = set_authority(def_with_authority_revoked(), Some([7_u8; 32])); + let _ = set_authority( + def_with_authority_revoked(), + authority_signer(), + Some([7_u8; 32]), + ); } #[test] - #[should_panic(expected = "Definition account authorization is missing")] + #[should_panic(expected = "Mint authority must sign the transaction")] fn set_authority_without_is_authorized_fails() { - let _ = set_authority(def_without_auth_flag(), Some([7_u8; 32])); + 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])); + } + + #[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]), + ); } #[test] fn set_authority_rotate_then_old_cannot_mint() { let new_key = [7_u8; 32]; - let post_states = set_authority(def_with_authority(), Some(new_key)); - let [def_post] = post_states.try_into().unwrap(); + let post_states = set_authority(def_with_authority(), authority_signer(), Some(new_key)); + let [def_post, _authority_post] = post_states.try_into().unwrap(); let def = TokenDefinition::try_from(&def_post.account().data).unwrap(); assert!(matches!( diff --git a/scripts/demo-full-flow.sh b/scripts/demo-full-flow.sh index 6a250ee..300ea84 100755 --- a/scripts/demo-full-flow.sh +++ b/scripts/demo-full-flow.sh @@ -46,6 +46,22 @@ TOKEN_BIN="${TOKEN_BIN:-$LEZ_PROGRAMS/target/riscv-guest/token-methods/token-gue DEMO_DIR="${DEMO_DIR:-$(pwd)}" WALLET_DIR="${WALLET_DIR:-$DEMO_DIR/.scaffold/wallet}" +# Convert a base58 "Public/..." account_id to the 64-char hex form +# that SPEL expects for [u8; 32] args (e.g. --mint-authority). +b58_to_hex() { + local id="${1#Public/}" # strip the Public/ prefix + id="${id#Private/}" # strip Private/ if present + python3 -c " +import sys +s = sys.argv[1] +alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' +num = 0 +for c in s: + num = num * 58 + alphabet.index(c) +print(num.to_bytes(32, 'big').hex()) +" "$id" +} + echo "================================================================" echo " LP-0013: Token Program Mint Authority — End-to-End Demo" echo " RISC0_DEV_MODE=${RISC0_DEV_MODE:-not set}" @@ -66,17 +82,18 @@ lgs wallet topup 2>&1 | grep -E "complete|funded|Address" || true echo " Wallet funded." echo "[3/7] Creating token accounts..." -DEF_RESULT=$(lgs wallet -- account new --public 2>&1) -DEF_ID=$(echo "$DEF_RESULT" | grep -oE '[0-9a-f]{64}' | head -1) -SUPPLY_RESULT=$(lgs wallet -- account new --public 2>&1) -SUPPLY_ID=$(echo "$SUPPLY_RESULT" | grep -oE '[0-9a-f]{64}' | head -1) -RECIPIENT_RESULT=$(lgs wallet -- account new --public 2>&1) -RECIPIENT_ID=$(echo "$RECIPIENT_RESULT" | grep -oE '[0-9a-f]{64}' | head -1) +DEF_RESULT=$(lgs wallet -- account new public 2>&1) +DEF_ID=$(echo "$DEF_RESULT" | grep -oE 'account_id [^ ]+' | awk '{print $2}') +SUPPLY_RESULT=$(lgs wallet -- account new public 2>&1) +SUPPLY_ID=$(echo "$SUPPLY_RESULT" | grep -oE 'account_id [^ ]+' | awk '{print $2}') +RECIPIENT_RESULT=$(lgs wallet -- account new public 2>&1) +RECIPIENT_ID=$(echo "$RECIPIENT_RESULT" | grep -oE 'account_id [^ ]+' | awk '{print $2}') echo " Definition account: $DEF_ID" echo " Supply account: $SUPPLY_ID" echo " Recipient account: $RECIPIENT_ID" echo "[4/7] Creating token with mint authority..." +DEF_ID_HEX=$(b58_to_hex "$DEF_ID") NSSA_WALLET_HOME_DIR="$WALLET_DIR" \ ${TIMEOUT:+$TIMEOUT 30} "$SPEL" --idl "$IDL" --program "$TOKEN_BIN" \ -- new-fungible-definition-with-authority \ @@ -84,7 +101,7 @@ ${TIMEOUT:+$TIMEOUT 30} "$SPEL" --idl "$IDL" --program "$TOKEN_BIN" \ --holding-target-account "$SUPPLY_ID" \ --name "DemoCoin" \ --initial-supply 1000000 \ - --mint-authority "$DEF_ID" + --mint-authority "$DEF_ID_HEX" echo " Token 'DemoCoin' submitted. Initial supply: 1,000,000" sleep 2 @@ -94,6 +111,7 @@ NSSA_WALLET_HOME_DIR="$WALLET_DIR" \ ${TIMEOUT:+$TIMEOUT 30} "$SPEL" --idl "$IDL" --program "$TOKEN_BIN" \ -- mint \ --definition-account "$DEF_ID" \ + --authority-account "$DEF_ID" \ --user-holding-account "$RECIPIENT_ID" \ --amount-to-mint 500000 echo " Mint transaction submitted. New total supply: 1,500,000" @@ -105,6 +123,7 @@ NSSA_WALLET_HOME_DIR="$WALLET_DIR" \ ${TIMEOUT:+$TIMEOUT 30} "$SPEL" --idl "$IDL" --program "$TOKEN_BIN" \ -- set-authority \ --definition-account "$DEF_ID" \ + --authority-account "$DEF_ID" \ --new-authority none echo " Authority revoked. Supply permanently fixed at 1,500,000"