Skip to content

Commit

Permalink
Update fee destination address (#149)
Browse files Browse the repository at this point in the history
* Update fee destination address

* Update and test ownerless close destination
  • Loading branch information
danenbm authored Nov 1, 2024
1 parent 85514fe commit 2878f77
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 25 deletions.
2 changes: 1 addition & 1 deletion clients/js/test/close/fungible.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
2 changes: 1 addition & 1 deletion clients/js/test/close/nonFungible.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
1 change: 0 additions & 1 deletion programs/token-metadata/program/src/processor/close/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
9 changes: 8 additions & 1 deletion programs/token-metadata/program/src/processor/fee/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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());
Expand Down
5 changes: 3 additions & 2 deletions programs/token-metadata/program/src/state/fee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
112 changes: 93 additions & 19 deletions programs/token-metadata/program/tests/fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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![];
Expand All @@ -122,7 +145,7 @@ mod fees {

let fee_accounts: Vec<Pubkey> = 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()),
Expand All @@ -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);

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

0 comments on commit 2878f77

Please sign in to comment.