diff --git a/integration_tests/tests/token.rs b/integration_tests/tests/token.rs index aed4d8a..9cc5404 100644 --- a/integration_tests/tests/token.rs +++ b/integration_tests/tests/token.rs @@ -36,6 +36,10 @@ impl Ids { token_methods::TOKEN_ID } + fn foreign_token_program() -> nssa_core::program::ProgramId { + [0xfeed_u32; 8] + } + fn token_definition() -> AccountId { AccountId::from(&PublicKey::new_from_private_key(&Keys::def_key())) } @@ -63,6 +67,19 @@ impl Accounts { } } + fn token_definition_foreign_owner() -> Account { + Account { + program_owner: Ids::foreign_token_program(), + balance: 0_u128, + data: Data::from(&TokenDefinition::Fungible { + name: String::from("Gold"), + total_supply: 1_000_000_u128, + metadata_id: None, + }), + nonce: Nonce(0), + } + } + fn holder_init() -> Account { Account { program_owner: Ids::token_program(), @@ -167,6 +184,78 @@ fn token_new_fungible_definition() { ); } +#[test] +fn token_initialize_account_succeeds_for_canonical_definition() { + let mut state = state_for_token_tests_without_recipient(); + + let instruction = token_core::Instruction::InitializeAccount; + + let message = public_transaction::Message::try_new( + Ids::token_program(), + vec![Ids::token_definition(), Ids::recipient()], + vec![Nonce(0)], + instruction, + ) + .unwrap(); + + let witness_set = + public_transaction::WitnessSet::for_message(&message, &[&Keys::recipient_key()]); + + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + + assert_eq!( + state.get_account_by_id(Ids::token_definition()), + Accounts::token_definition_init() + ); + assert_eq!( + state.get_account_by_id(Ids::recipient()), + Account { + program_owner: Ids::token_program(), + balance: 0_u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: Ids::token_definition(), + balance: 0_u128, + }), + nonce: Nonce(1), + } + ); +} + +#[test] +fn token_initialize_account_rejects_foreign_owned_definition() { + let mut state = state_for_token_tests_without_recipient(); + state.force_insert_account( + Ids::token_definition(), + Accounts::token_definition_foreign_owner(), + ); + + let instruction = token_core::Instruction::InitializeAccount; + + let message = public_transaction::Message::try_new( + Ids::token_program(), + vec![Ids::token_definition(), Ids::recipient()], + vec![Nonce(0)], + instruction, + ) + .unwrap(); + + let witness_set = + public_transaction::WitnessSet::for_message(&message, &[&Keys::recipient_key()]); + + let tx = PublicTransaction::new(message, witness_set); + assert!(state.transition_from_public_transaction(&tx, 0, 0).is_err()); + + assert_eq!( + state.get_account_by_id(Ids::token_definition()), + Accounts::token_definition_foreign_owner() + ); + assert_eq!( + state.get_account_by_id(Ids::recipient()), + Account::default() + ); +} + #[test] fn token_transfer() { let mut state = state_for_token_tests(); @@ -395,6 +484,44 @@ fn token_mint() { ); } +#[test] +fn token_mint_rejects_foreign_owned_definition() { + let mut state = state_for_token_tests_without_recipient(); + state.force_insert_account( + Ids::token_definition(), + Accounts::token_definition_foreign_owner(), + ); + + let instruction = token_core::Instruction::Mint { + amount_to_mint: 500_000_u128, + }; + + let message = public_transaction::Message::try_new( + Ids::token_program(), + vec![Ids::token_definition(), Ids::recipient()], + vec![Nonce(0), Nonce(0)], + instruction, + ) + .unwrap(); + + let witness_set = public_transaction::WitnessSet::for_message( + &message, + &[&Keys::def_key(), &Keys::recipient_key()], + ); + + let tx = PublicTransaction::new(message, witness_set); + assert!(state.transition_from_public_transaction(&tx, 0, 0).is_err()); + + assert_eq!( + state.get_account_by_id(Ids::token_definition()), + Accounts::token_definition_foreign_owner() + ); + assert_eq!( + state.get_account_by_id(Ids::recipient()), + Account::default() + ); +} + #[test] fn token_mint_fresh_public_recipient_requires_authorization() { let mut state = state_for_token_tests_without_recipient(); diff --git a/token/methods/guest/src/bin/token.rs b/token/methods/guest/src/bin/token.rs index 73ae46a..7a0cf0d 100644 --- a/token/methods/guest/src/bin/token.rs +++ b/token/methods/guest/src/bin/token.rs @@ -1,10 +1,126 @@ -#![no_main] +#![cfg_attr(not(test), no_main)] +use nssa_core::{ + account::AccountWithMetadata, + program::{AccountPostState, ProgramId}, +}; +#[cfg(not(test))] +use nssa_core::program::{ProgramInput, ProgramOutput}; +#[cfg(test)] use spel_framework::prelude::*; -use nssa_core::account::AccountWithMetadata; +use token_core::Instruction as TokenInstruction; +#[cfg(not(test))] risc0_zkvm::guest::entry!(main); +#[cfg(not(test))] +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + instruction, + }, + instruction_words, + ) = nssa_core::program::read_nssa_inputs::(); + let pre_states_clone = pre_states.clone(); + + let post_states = dispatch(instruction, pre_states, self_program_id); + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + pre_states_clone, + post_states, + ) + .write(); +} + +#[cfg_attr(test, allow(dead_code))] +fn dispatch( + instruction: TokenInstruction, + pre_states: Vec, + self_program_id: ProgramId, +) -> Vec { + match instruction { + TokenInstruction::Transfer { amount_to_transfer } => { + let [sender, recipient] = expect_accounts(pre_states); + token_program::transfer::transfer(sender, recipient, amount_to_transfer) + } + TokenInstruction::NewFungibleDefinition { name, total_supply } => { + let [definition_target_account, holding_target_account] = + expect_accounts(pre_states); + token_program::new_definition::new_fungible_definition( + definition_target_account, + holding_target_account, + name, + total_supply, + ) + } + TokenInstruction::NewDefinitionWithMetadata { + new_definition, + metadata, + } => { + let [ + definition_target_account, + holding_target_account, + metadata_target_account, + ] = expect_accounts(pre_states); + token_program::new_definition::new_definition_with_metadata( + definition_target_account, + holding_target_account, + metadata_target_account, + new_definition, + *metadata, + ) + } + TokenInstruction::InitializeAccount => { + let [definition_account, account_to_initialize] = expect_accounts(pre_states); + token_program::initialize::initialize_account( + definition_account, + account_to_initialize, + self_program_id, + ) + } + TokenInstruction::Burn { amount_to_burn } => { + let [definition_account, user_holding_account] = expect_accounts(pre_states); + token_program::burn::burn( + definition_account, + user_holding_account, + amount_to_burn, + ) + } + TokenInstruction::Mint { amount_to_mint } => { + let [definition_account, user_holding_account] = expect_accounts(pre_states); + token_program::mint::mint( + definition_account, + user_holding_account, + amount_to_mint, + self_program_id, + ) + } + TokenInstruction::PrintNft => { + let [master_account, printed_account] = expect_accounts(pre_states); + token_program::print_nft::print_nft(master_account, printed_account) + } + } +} + +#[cfg_attr(test, allow(dead_code))] +fn expect_accounts( + pre_states: Vec, +) -> [AccountWithMetadata; N] { + let actual = pre_states.len(); + pre_states.try_into().unwrap_or_else(|_| { + panic!("Account count mismatch: expected {N}, got {actual}"); + }) +} + +// Kept for `idl-gen`, which parses source annotations directly. +#[cfg(test)] +#[allow(dead_code)] #[lez_program(instruction = "token_core::Instruction")] mod token { #[allow(unused_imports)] @@ -12,6 +128,7 @@ mod token { /// Transfer tokens from sender to recipient. /// Fresh public recipients must be explicitly authorized in the same transaction. + #[allow(dead_code, clippy::boxed_local)] #[instruction] pub fn transfer( sender: AccountWithMetadata, @@ -27,6 +144,7 @@ mod token { /// Create a new fungible token definition without metadata. /// Definition and holding targets must be uninitialized and authorized. + #[allow(dead_code, clippy::boxed_local)] #[instruction] pub fn new_fungible_definition( definition_target_account: AccountWithMetadata, @@ -46,6 +164,7 @@ mod token { /// Create a new fungible or non-fungible token definition with metadata. /// Definition, holding, and metadata targets must be uninitialized and authorized. + #[allow(dead_code, clippy::boxed_local)] #[instruction] pub fn new_definition_with_metadata( definition_target_account: AccountWithMetadata, @@ -67,6 +186,7 @@ mod token { /// Initialize a token holding account for a given token definition. /// The holding target must be uninitialized and authorized. + #[allow(dead_code, clippy::boxed_local)] #[instruction] pub fn initialize_account( definition_account: AccountWithMetadata, @@ -76,11 +196,13 @@ mod token { token_program::initialize::initialize_account( definition_account, account_to_initialize, + nssa_core::program::DEFAULT_PROGRAM_ID, ), )) } /// Burn tokens from the holder's account. + #[allow(dead_code, clippy::boxed_local)] #[instruction] pub fn burn( definition_account: AccountWithMetadata, @@ -96,6 +218,7 @@ mod token { /// Mint new tokens to the holder's account. /// Fresh public holders must be explicitly authorized in the same transaction. + #[allow(dead_code, clippy::boxed_local)] #[instruction] pub fn mint( definition_account: AccountWithMetadata, @@ -106,11 +229,13 @@ mod token { definition_account, user_holding_account, amount_to_mint, + nssa_core::program::DEFAULT_PROGRAM_ID, ))) } /// Print a new NFT from the master copy. /// The printed copy target must be uninitialized and authorized. + #[allow(dead_code, clippy::boxed_local)] #[instruction] pub fn print_nft( master_account: AccountWithMetadata, diff --git a/token/src/initialize.rs b/token/src/initialize.rs index d8350d4..7f57e38 100644 --- a/token/src/initialize.rs +++ b/token/src/initialize.rs @@ -1,12 +1,13 @@ use nssa_core::{ account::{Account, AccountWithMetadata, Data}, - program::{AccountPostState, Claim}, + program::{AccountPostState, Claim, ProgramId}, }; use token_core::{TokenDefinition, TokenHolding}; pub fn initialize_account( definition_account: AccountWithMetadata, account_to_initialize: AccountWithMetadata, + token_program_id: ProgramId, ) -> Vec { assert_eq!( account_to_initialize.account, @@ -17,11 +18,11 @@ pub fn initialize_account( account_to_initialize.is_authorized, "Account to initialize must be authorized" ); + assert_eq!( + definition_account.account.program_owner, token_program_id, + "Token definition must be owned by token program" + ); - // TODO: #212 We should check that this is an account owned by the token program. - // This check can't be done here since the ID of the program is known only after compiling it - // - // Check definition account is valid let definition = TokenDefinition::try_from(&definition_account.account.data) .expect("Definition account must be valid"); let holding = diff --git a/token/src/mint.rs b/token/src/mint.rs index ff744d6..0c638d1 100644 --- a/token/src/mint.rs +++ b/token/src/mint.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata, Data}, - program::{AccountPostState, Claim}, + program::{AccountPostState, Claim, ProgramId}, }; use token_core::{TokenDefinition, TokenHolding}; @@ -8,11 +8,16 @@ pub fn mint( definition_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" + ); let mut definition = TokenDefinition::try_from(&definition_account.account.data) .expect("Token Definition account must be valid"); diff --git a/token/src/tests.rs b/token/src/tests.rs index 532787e..3c5494c 100644 --- a/token/src/tests.rs +++ b/token/src/tests.rs @@ -2,7 +2,7 @@ use nssa_core::{ account::{Account, AccountId, AccountWithMetadata, Data, Nonce}, - program::Claim, + program::{Claim, ProgramId}, }; use token_core::{ MetadataStandard, NewTokenDefinition, NewTokenMetadata, TokenDefinition, TokenHolding, @@ -25,11 +25,31 @@ struct IdForTests; struct AccountForTests; +const TOKEN_PROGRAM_ID: ProgramId = [5u32; 8]; +const FOREIGN_TOKEN_PROGRAM_ID: ProgramId = [6u32; 8]; + impl AccountForTests { fn definition_account_auth() -> AccountWithMetadata { AccountWithMetadata { account: Account { - program_owner: [5u32; 8], + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: Data::from(&TokenDefinition::Fungible { + name: String::from("test"), + total_supply: BalanceForTests::init_supply(), + metadata_id: None, + }), + nonce: Nonce(0), + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn definition_account_foreign_owner() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: FOREIGN_TOKEN_PROGRAM_ID, balance: 0u128, data: Data::from(&TokenDefinition::Fungible { name: String::from("test"), @@ -46,7 +66,7 @@ impl AccountForTests { fn definition_account_without_auth() -> AccountWithMetadata { AccountWithMetadata { account: Account { - program_owner: [5u32; 8], + program_owner: TOKEN_PROGRAM_ID, balance: 0u128, data: Data::from(&TokenDefinition::Fungible { name: String::from("test"), @@ -757,7 +777,7 @@ fn test_transfer_with_default_recipient_claims_recipient() { fn test_token_initialize_account_succeeds() { let definition_account = AccountForTests::definition_account_auth(); let holding_account = AccountForTests::holding_account_uninit_auth(); - let post_states = initialize_account(definition_account, holding_account); + let post_states = initialize_account(definition_account, holding_account, TOKEN_PROGRAM_ID); let [definition_post, holding_post] = post_states.try_into().unwrap(); assert_eq!( @@ -785,7 +805,15 @@ fn test_token_initialize_account_succeeds() { fn test_token_initialize_account_requires_authorization() { let definition_account = AccountForTests::definition_account_auth(); let holding_account = AccountForTests::holding_account_uninit(); - let _post_states = initialize_account(definition_account, holding_account); + let _post_states = initialize_account(definition_account, holding_account, TOKEN_PROGRAM_ID); +} + +#[test] +#[should_panic(expected = "Token definition must be owned by token program")] +fn test_token_initialize_account_rejects_foreign_owned_definition() { + let definition_account = AccountForTests::definition_account_foreign_owner(); + let holding_account = AccountForTests::holding_account_uninit_auth(); + let _post_states = initialize_account(definition_account, holding_account, TOKEN_PROGRAM_ID); } #[test] @@ -868,6 +896,7 @@ fn test_mint_not_valid_holding_account() { definition_account, holding_account, BalanceForTests::mint_success(), + TOKEN_PROGRAM_ID, ); } @@ -880,6 +909,7 @@ fn test_mint_not_valid_definition_account() { definition_account, holding_account, BalanceForTests::mint_success(), + TOKEN_PROGRAM_ID, ); } @@ -892,6 +922,20 @@ fn test_mint_missing_authorization() { definition_account, holding_account, BalanceForTests::mint_success(), + TOKEN_PROGRAM_ID, + ); +} + +#[test] +#[should_panic(expected = "Token definition must be owned by token program")] +fn test_mint_rejects_foreign_owned_definition() { + let definition_account = AccountForTests::definition_account_foreign_owner(); + let holding_account = AccountForTests::holding_account_uninit(); + let _post_states = mint( + definition_account, + holding_account, + BalanceForTests::mint_success(), + TOKEN_PROGRAM_ID, ); } @@ -904,6 +948,7 @@ fn test_mint_mismatched_token_definition() { definition_account, holding_account, BalanceForTests::mint_success(), + TOKEN_PROGRAM_ID, ); } @@ -915,6 +960,7 @@ fn test_mint_success() { definition_account, holding_account, BalanceForTests::mint_success(), + TOKEN_PROGRAM_ID, ); let [def_post, holding_post] = post_states.try_into().unwrap(); @@ -939,6 +985,7 @@ fn test_mint_uninit_holding_success() { definition_account, holding_account, BalanceForTests::mint_success(), + TOKEN_PROGRAM_ID, ); let [def_post, holding_post] = post_states.try_into().unwrap(); @@ -964,6 +1011,7 @@ fn test_mint_total_supply_overflow() { definition_account, holding_account, BalanceForTests::mint_overflow(), + TOKEN_PROGRAM_ID, ); } @@ -976,6 +1024,7 @@ fn test_mint_holding_account_overflow() { definition_account, holding_account, BalanceForTests::mint_overflow(), + TOKEN_PROGRAM_ID, ); } @@ -988,6 +1037,7 @@ fn test_mint_cannot_mint_unmintable_tokens() { definition_account, holding_account, BalanceForTests::mint_success(), + TOKEN_PROGRAM_ID, ); }