diff --git a/clients/js/test/close/fungible.test.ts b/clients/js/test/close/fungible.test.ts index 97fca1fd..77a42417 100644 --- a/clients/js/test/close/fungible.test.ts +++ b/clients/js/test/close/fungible.test.ts @@ -22,7 +22,7 @@ import { } from '../../src'; const closeDestination = publicKey( - 'Levytx9LLPzAtDJJD7q813Zsm8zg9e1pb53mGxTKpD7' + 'GxCXYtrnaU6JXeAza8Ugn4EE6QiFinpfn8t3Lo4UkBDX' ); test.skip('it can close ownerless metadata for a fungible with zero supply and no mint authority', async (t) => { diff --git a/clients/js/test/close/nonFungible.test.ts b/clients/js/test/close/nonFungible.test.ts index 319c9002..2afdc2cd 100644 --- a/clients/js/test/close/nonFungible.test.ts +++ b/clients/js/test/close/nonFungible.test.ts @@ -20,7 +20,7 @@ import { } from '../../src'; const closeDestination = publicKey( - 'Levytx9LLPzAtDJJD7q813Zsm8zg9e1pb53mGxTKpD7' + 'GxCXYtrnaU6JXeAza8Ugn4EE6QiFinpfn8t3Lo4UkBDX' ); test.skip('it can close ownerless metadata for a non-fungible with zero supply', async (t) => { diff --git a/programs/token-metadata/program/src/processor/close/mod.rs b/programs/token-metadata/program/src/processor/close/mod.rs index 6b1bdfc2..6f83177a 100644 --- a/programs/token-metadata/program/src/processor/close/mod.rs +++ b/programs/token-metadata/program/src/processor/close/mod.rs @@ -34,7 +34,6 @@ pub(crate) fn process_close_accounts<'a>( } // Assert the correct destination is set. - // TODO: This should be replaced by destination address. if *ctx.accounts.destination_info.key != OWNERLESS_CLOSE_DESTINATION { return Err(MetadataError::InvalidFeeAccount.into()); } diff --git a/programs/token-metadata/program/src/processor/fee/mod.rs b/programs/token-metadata/program/src/processor/fee/mod.rs index f91b2488..f1cfc3af 100644 --- a/programs/token-metadata/program/src/processor/fee/mod.rs +++ b/programs/token-metadata/program/src/processor/fee/mod.rs @@ -3,7 +3,10 @@ use num_traits::FromPrimitive; use solana_program::{account_info::next_account_info, rent::Rent, system_program, sysvar::Sysvar}; use super::*; -use crate::{state::fee::FEE_AUTHORITY, utils::fee::clear_fee_flag}; +use crate::{ + state::fee::{FEE_AUTHORITY, FEE_DESTINATION}, + utils::fee::clear_fee_flag, +}; pub(crate) fn process_collect_fees(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); @@ -18,6 +21,10 @@ pub(crate) fn process_collect_fees(program_id: &Pubkey, accounts: &[AccountInfo] let recipient_info = next_account_info(account_info_iter)?; + if *recipient_info.key != FEE_DESTINATION { + return Err(MetadataError::InvalidFeeAccount.into()); + } + for account_info in account_info_iter { if account_info.owner != program_id { return Err(MetadataError::InvalidFeeAccount.into()); diff --git a/programs/token-metadata/program/src/state/fee.rs b/programs/token-metadata/program/src/state/fee.rs index 07e7f76b..ed50c20b 100644 --- a/programs/token-metadata/program/src/state/fee.rs +++ b/programs/token-metadata/program/src/state/fee.rs @@ -2,10 +2,11 @@ use super::*; use solana_program::{rent::Rent, sysvar::Sysvar}; pub(crate) const FEE_AUTHORITY: Pubkey = pubkey!("Levytx9LLPzAtDJJD7q813Zsm8zg9e1pb53mGxTKpD7"); +pub const FEE_DESTINATION: Pubkey = pubkey!("2fb1TjRrJQLy9BkYfBjcYgibV7LUsr9cf6QxvyRZyuXn"); pub(crate) const OWNERLESS_CLOSE_AUTHORITY: Pubkey = pubkey!("C1oseLQExhuEzeBhsVbLtseSpVgvpHDbBj3PTevBCEBh"); -pub(crate) const OWNERLESS_CLOSE_DESTINATION: Pubkey = - pubkey!("E4ZJX8hYhz5tDbFsUo1DinxHqt33aUsFQpe8dYjASm2F"); +pub const OWNERLESS_CLOSE_DESTINATION: Pubkey = + pubkey!("GxCXYtrnaU6JXeAza8Ugn4EE6QiFinpfn8t3Lo4UkBDX"); pub(crate) const RESIZE_AUTHORITY: Pubkey = pubkey!("ResizebfwTEZTLbHbctTByvXYECKTJQXnMWG8g9XLix"); pub(crate) const RESIZE_DESTINATION: Pubkey = pubkey!("46mjNQBwXLCDCM7YiDQSPVdNZ4dLdZf79tTPRkT1wkF6"); diff --git a/programs/token-metadata/program/tests/fees.rs b/programs/token-metadata/program/tests/fees.rs index a9aefdd9..54e00971 100644 --- a/programs/token-metadata/program/tests/fees.rs +++ b/programs/token-metadata/program/tests/fees.rs @@ -5,19 +5,43 @@ use solana_program_test::*; use utils::*; mod fees { + use num_traits::FromPrimitive; use solana_program::{native_token::LAMPORTS_PER_SOL, pubkey::Pubkey}; use solana_sdk::{ + instruction::InstructionError, + pubkey, signature::{read_keypair_file, Keypair}, signer::Signer, transaction::Transaction, + transaction::TransactionError, }; use token_metadata::{ + error::MetadataError, instruction::{collect_fees, BurnArgs, UpdateArgs}, - state::{FEE_FLAG_CLEARED, METADATA_FEE_FLAG_OFFSET}, + state::{ + FEE_DESTINATION, FEE_FLAG_CLEARED, FEE_FLAG_SET, METADATA_FEE_FLAG_OFFSET, + OWNERLESS_CLOSE_DESTINATION, + }, }; use super::*; + #[tokio::test] + async fn fee_manager_pdas_are_correct() { + let fee_manager = pubkey!("mgrfTeJh5VgHt67LQQVZ7U2gPY88En94QMWz64cV2AY"); + + // Fee destination is correct PDA of the fee-manager program. + let (derived_fee_dest, _) = + Pubkey::find_program_address(&["fee_manager_treasury".as_bytes()], &fee_manager); + assert_eq!(derived_fee_dest, FEE_DESTINATION); + + // Ownerless close destination is correct PDA of the fee-manager program. + + let (derived_ownerless_close_dest, _) = + Pubkey::find_program_address(&["fee_manager_close_treasury".as_bytes()], &fee_manager); + assert_eq!(derived_ownerless_close_dest, OWNERLESS_CLOSE_DESTINATION); + } + #[tokio::test] async fn charge_create_metadata_v3() { let mut context = program_test().start_with_context().await; @@ -94,16 +118,15 @@ mod fees { let authority_funding = 10 * LAMPORTS_PER_SOL; - let authority = - read_keypair_file("/media/veracrypt1/Levytx9LLPzAtDJJD7q813Zsm8zg9e1pb53mGxTKpD7.json") - .unwrap(); + let authority = read_keypair_file( + "/home/danenbm/keypairs/Levytx9LLPzAtDJJD7q813Zsm8zg9e1pb53mGxTKpD7.json", + ) + .unwrap(); authority .airdrop(&mut context, authority_funding) .await .unwrap(); - let recipient = Keypair::new(); - let num_accounts = 25; let mut nfts = vec![]; @@ -122,7 +145,7 @@ mod fees { let fee_accounts: Vec = nfts.iter().map(|nft| nft.metadata).collect(); - let ix = collect_fees(recipient.pubkey(), fee_accounts.clone()); + let ix = collect_fees(FEE_DESTINATION, fee_accounts.clone()); let tx = Transaction::new_signed_with_payer( &[ix], Some(&authority.pubkey()), @@ -134,9 +157,7 @@ mod fees { let expected_balance = num_accounts * SOLANA_CREATE_FEE; - let recipient_balance = get_account(&mut context, &recipient.pubkey()) - .await - .lamports; + let recipient_balance = get_account(&mut context, &FEE_DESTINATION).await.lamports; assert_eq!(recipient_balance, expected_balance); @@ -162,16 +183,15 @@ mod fees { let fee_authority_funding = LAMPORTS_PER_SOL; - let fee_authority = - read_keypair_file("/media/veracrypt1/Levytx9LLPzAtDJJD7q813Zsm8zg9e1pb53mGxTKpD7.json") - .unwrap(); + let fee_authority = read_keypair_file( + "/home/danenbm/keypairs/Levytx9LLPzAtDJJD7q813Zsm8zg9e1pb53mGxTKpD7.json", + ) + .unwrap(); fee_authority .airdrop(&mut context, fee_authority_funding) .await .unwrap(); - let recipient = Keypair::new(); - let mut nft = DigitalAsset::new(); nft.create_and_mint( &mut context, @@ -197,7 +217,7 @@ mod fees { .await .unwrap(); - let ix = collect_fees(recipient.pubkey(), vec![nft.metadata]); + let ix = collect_fees(FEE_DESTINATION, vec![nft.metadata]); let tx = Transaction::new_signed_with_payer( &[ix], Some(&fee_authority.pubkey()), @@ -208,10 +228,64 @@ mod fees { let expected_balance = SOLANA_CREATE_FEE; - let recipient_balance = get_account(&mut context, &recipient.pubkey()) - .await - .lamports; + let recipient_balance = get_account(&mut context, &FEE_DESTINATION).await.lamports; assert_eq!(recipient_balance, expected_balance); } + + #[test_case::test_case(spl_token::id() ; "Token Program")] + #[test_case::test_case(spl_token_2022::id() ; "Token-2022 Program")] + #[tokio::test] + // Used for local QA testing and requires a keypair so excluded from CI. + #[ignore] + async fn cannot_collect_fees_using_wrong_fee_destination(spl_token_program: Pubkey) { + // Create NFTs and then collect the fees from the metadata accounts. + let mut context = program_test().start_with_context().await; + + let authority_funding = 10 * LAMPORTS_PER_SOL; + + let authority = read_keypair_file( + "/home/danenbm/keypairs/Levytx9LLPzAtDJJD7q813Zsm8zg9e1pb53mGxTKpD7.json", + ) + .unwrap(); + authority + .airdrop(&mut context, authority_funding) + .await + .unwrap(); + + // Fee destination is meant to be a specific PDA of the fee-manager program. + let wrong_fee_destination = Keypair::new().pubkey(); + + let mut nft = DigitalAsset::new(); + nft.create( + &mut context, + token_metadata::state::TokenStandard::NonFungible, + None, + spl_token_program, + ) + .await + .unwrap(); + + let before_lamports = get_account(&mut context, &nft.metadata).await.lamports; + + let ix = collect_fees(wrong_fee_destination, vec![nft.metadata]); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&authority.pubkey()), + &[&authority], + context.last_blockhash, + ); + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + assert_custom_error!(err, MetadataError::InvalidFeeAccount); + + let account = get_account(&mut context, &nft.metadata).await; + let last_byte = account.data.len() - METADATA_FEE_FLAG_OFFSET; + assert_eq!(account.data[last_byte], FEE_FLAG_SET); + assert_eq!(account.lamports, before_lamports); + } }