diff --git a/auctioneer/program/Cargo.lock b/auctioneer/program/Cargo.lock index 16b4ee4531..a238c041ac 100644 --- a/auctioneer/program/Cargo.lock +++ b/auctioneer/program/Cargo.lock @@ -2010,7 +2010,7 @@ dependencies = [ [[package]] name = "mpl-auction-house" -version = "1.3.5" +version = "1.4.0" dependencies = [ "anchor-lang", "anchor-spl", diff --git a/auctioneer/program/src/execute_sale/mod.rs b/auctioneer/program/src/execute_sale/mod.rs index 7d14f5495c..5c9ac304ad 100644 --- a/auctioneer/program/src/execute_sale/mod.rs +++ b/auctioneer/program/src/execute_sale/mod.rs @@ -211,6 +211,9 @@ pub fn auctioneer_execute_sale<'info>( cpi_account_metas.append(&mut ctx.remaining_accounts.to_vec().to_account_metas(None)); + let mut cpi_account_infos: Vec = cpi_accounts.to_account_infos(); + cpi_account_infos.append(&mut ctx.remaining_accounts.to_vec()); + let ix = solana_program::instruction::Instruction { program_id: cpi_program.key(), accounts: cpi_account_metas, @@ -228,7 +231,7 @@ pub fn auctioneer_execute_sale<'info>( &[auctioneer_authority_bump], ]; - invoke_signed(&ix, &cpi_accounts.to_account_infos(), &[&auctioneer_seeds])?; + invoke_signed(&ix, &cpi_account_infos, &[&auctioneer_seeds])?; // Close the Listing Config account. let listing_config = &ctx.accounts.listing_config.to_account_info(); diff --git a/auctioneer/program/tests/execute_sale.rs b/auctioneer/program/tests/execute_sale.rs index c67b432534..799a63d091 100644 --- a/auctioneer/program/tests/execute_sale.rs +++ b/auctioneer/program/tests/execute_sale.rs @@ -7,11 +7,16 @@ use utils::setup_functions::*; use anchor_lang::{InstructionData, ToAccountMetas}; use mpl_testing_utils::{solana::airdrop, utils::Metadata}; -use solana_sdk::{compute_budget::ComputeBudgetInstruction, signer::Signer}; +use solana_sdk::{ + account::Account as SolanaAccount, compute_budget::ComputeBudgetInstruction, signer::Signer, +}; use std::{assert_eq, time::SystemTime}; -use solana_program::{instruction::Instruction, system_program, sysvar}; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + system_program, sysvar, +}; use solana_program::program_pack::Pack; @@ -20,7 +25,8 @@ use mpl_auction_house::pda::{ find_trade_state_address, }; use mpl_auctioneer::pda::find_auctioneer_authority_seeds; -use solana_sdk::{signature::Keypair, transaction::Transaction}; +use mpl_token_metadata::state::Creator; +use solana_sdk::{pubkey::Pubkey, signature::Keypair, transaction::Transaction}; use spl_associated_token_account::get_associated_token_address; use spl_token::state::Account; @@ -782,3 +788,281 @@ async fn execute_sale_two_bids_failure() { assert_error!(result, NOT_HIGH_BIDDER) } + +#[tokio::test] +async fn execute_sale_one_creator() { + execute_sale_with_creators(vec![(Pubkey::new_unique(), 100)]).await; +} + +#[tokio::test] +async fn execute_sale_two_creator() { + execute_sale_with_creators(vec![(Pubkey::new_unique(), 25), (Pubkey::new_unique(), 75)]).await; +} + +async fn execute_sale_with_creators(metadata_creators: Vec<(Pubkey, u8)>) { + let mut context = auctioneer_program_test().start_with_context().await; + // Payer Wallet + let (ah, ahkey, authority) = existing_auction_house_test_context(&mut context) + .await + .unwrap(); + let test_metadata = Metadata::new(); + airdrop(&mut context, &test_metadata.token.pubkey(), 10_000_000_000) + .await + .unwrap(); + + for (creator, _) in &metadata_creators { + // airdrop 0.1 sol to ensure rent-exempt minimum + airdrop(&mut context, &creator, 100_000_000).await.unwrap(); + } + test_metadata + .create( + &mut context, + "Test".to_string(), + "TST".to_string(), + "uri".to_string(), + Some( + metadata_creators + .clone() + .iter() + .map(|(address, share)| Creator { + address: *address, + verified: false, + share: *share, + }) + .collect(), + ), + 1000, + false, + 1, + ) + .await + .unwrap(); + let ((sell_acc, listing_config_address), sell_tx) = sell( + &mut context, + &ahkey, + &ah, + &test_metadata, + (SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Time went backwards") + .as_secs() + - 60) as i64, + (SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Time went backwards") + .as_secs() + + 60) as i64, + None, + None, + None, + None, + None, + ); + context + .banks_client + .process_transaction(sell_tx) + .await + .unwrap(); + + let buyer = Keypair::new(); + airdrop(&mut context, &buyer.pubkey(), 10_000_000_000) + .await + .unwrap(); + let (bid_acc, buy_tx) = buy( + &mut context, + &ahkey, + &ah, + &test_metadata, + &test_metadata.token.pubkey(), + &buyer, + &sell_acc.wallet, + &listing_config_address, + 100_000_000, + ); + context + .banks_client + .process_transaction(buy_tx) + .await + .unwrap(); + let buyer_token_account = + get_associated_token_address(&buyer.pubkey(), &test_metadata.mint.pubkey()); + + context.warp_to_slot(120 * 400).unwrap(); + + let (auctioneer_authority, _aa_bump) = find_auctioneer_authority_seeds(&ahkey); + let (auctioneer_pda, _) = find_auctioneer_pda(&ahkey, &auctioneer_authority); + let mut accounts = mpl_auctioneer::accounts::AuctioneerExecuteSale { + auction_house_program: mpl_auction_house::id(), + listing_config: listing_config_address, + buyer: buyer.pubkey(), + seller: test_metadata.token.pubkey(), + authority: ah.authority, + auction_house: ahkey, + metadata: test_metadata.pubkey, + token_account: sell_acc.token_account, + seller_trade_state: sell_acc.seller_trade_state, + buyer_trade_state: bid_acc.buyer_trade_state, + token_program: spl_token::id(), + free_trade_state: sell_acc.free_seller_trade_state, + seller_payment_receipt_account: test_metadata.token.pubkey(), + buyer_receipt_token_account: buyer_token_account, + escrow_payment_account: bid_acc.escrow_payment_account, + token_mint: test_metadata.mint.pubkey(), + auction_house_fee_account: ah.auction_house_fee_account, + auction_house_treasury: ah.auction_house_treasury, + treasury_mint: ah.treasury_mint, + program_as_signer: sell_acc.program_as_signer, + system_program: system_program::id(), + ata_program: spl_associated_token_account::id(), + rent: sysvar::rent::id(), + auctioneer_authority, + ah_auctioneer_pda: auctioneer_pda, + } + .to_account_metas(None); + for (pubkey, _) in &metadata_creators { + accounts.push(AccountMeta { + pubkey: *pubkey, + is_signer: false, + is_writable: true, + }); + } + + let (_, free_sts_bump) = find_trade_state_address( + &test_metadata.token.pubkey(), + &ahkey, + &sell_acc.token_account, + &ah.treasury_mint, + &test_metadata.mint.pubkey(), + 0, + 1, + ); + let (_, escrow_bump) = find_escrow_payment_address(&ahkey, &buyer.pubkey()); + let (_, pas_bump) = find_program_as_signer_address(); + let (_, aa_bump) = find_auctioneer_authority_seeds(&ahkey); + + let instruction = Instruction { + program_id: mpl_auctioneer::id(), + data: mpl_auctioneer::instruction::ExecuteSale { + escrow_payment_bump: escrow_bump, + free_trade_state_bump: free_sts_bump, + program_as_signer_bump: pas_bump, + auctioneer_authority_bump: aa_bump, + token_size: 1, + buyer_price: 100_000_000, + } + .data(), + accounts, + }; + airdrop(&mut context, &ah.auction_house_fee_account, 10_000_000_000) + .await + .unwrap(); + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(350_000); + + let tx = Transaction::new_signed_with_payer( + &[compute_ix, instruction], + Some(&authority.pubkey()), + &[&authority], + context.last_blockhash, + ); + let seller_before = context + .banks_client + .get_account(test_metadata.token.pubkey()) + .await + .unwrap() + .unwrap(); + let mut metadata_creators_before: Vec = Vec::new(); + for (creator, _) in &metadata_creators { + metadata_creators_before.push( + context + .banks_client + .get_account(*creator) + .await + .unwrap() + .unwrap(), + ); + } + let buyer_token_before = &context + .banks_client + .get_account(buyer_token_account) + .await + .unwrap(); + assert!(buyer_token_before.is_none()); + + let listing_config_account = context + .banks_client + .get_account(listing_config_address) + .await + .unwrap() + .unwrap(); + + context.banks_client.process_transaction(tx).await.unwrap(); + + let seller_after = context + .banks_client + .get_account(test_metadata.token.pubkey()) + .await + .unwrap() + .unwrap(); + let mut metadata_creators_after: Vec = Vec::new(); + for (creator, _) in &metadata_creators { + metadata_creators_after.push( + context + .banks_client + .get_account(*creator) + .await + .unwrap() + .unwrap(), + ); + } + let buyer_token_after = Account::unpack_from_slice( + context + .banks_client + .get_account(buyer_token_account) + .await + .unwrap() + .unwrap() + .data + .as_slice(), + ) + .unwrap(); + + let royalty = (test_metadata + .get_data(&mut context) + .await + .data + .seller_fee_basis_points as u64 + * 100_000_000) + / 10000; + let fee_minus: u64 = + 100_000_000 - royalty - ((ah.seller_fee_basis_points as u64 * (100_000_000)) / 10000); + assert!(seller_before.lamports < seller_after.lamports); + assert_eq!(buyer_token_after.amount, 1); + + let rent = context.banks_client.get_rent().await.unwrap(); + let rent_exempt_min: u64 = rent.minimum_balance(listing_config_account.data.len()); + + for (((_, share), creator_before), creator_after) in metadata_creators + .iter() + .zip(metadata_creators_before.iter()) + .zip(metadata_creators_after.iter()) + { + assert_eq!( + creator_before.lamports + (royalty * (*share as u64)) / 100, + creator_after.lamports + ); + } + + assert_eq!( + seller_before.lamports + fee_minus + rent_exempt_min, + seller_after.lamports + ); + + let listing_config_closed = context + .banks_client + .get_account(listing_config_address) + .await + .unwrap(); + + assert!(listing_config_closed.is_none()); +}