982 lines
32 KiB
Rust
982 lines
32 KiB
Rust
use solana_program::log::sol_log_compute_units;
|
|
use spl_auction::processor::BidderMetadata;
|
|
|
|
use {
|
|
crate::{
|
|
error::MetaplexError,
|
|
state::{
|
|
AuctionManager, AuctionManagerStatus, Key, OriginalAuthorityLookup, Store,
|
|
WhitelistedCreator, WinningConfigItem, MAX_BID_REDEMPTION_TICKET_SIZE, PREFIX,
|
|
},
|
|
},
|
|
arrayref::{array_mut_ref, array_ref, mut_array_refs},
|
|
borsh::BorshDeserialize,
|
|
solana_program::{
|
|
account_info::AccountInfo,
|
|
borsh::try_from_slice_unchecked,
|
|
entrypoint::ProgramResult,
|
|
msg,
|
|
program::{invoke, invoke_signed},
|
|
program_error::ProgramError,
|
|
program_pack::{IsInitialized, Pack},
|
|
pubkey::Pubkey,
|
|
system_instruction,
|
|
sysvar::{rent::Rent, Sysvar},
|
|
},
|
|
spl_auction::{
|
|
instruction::end_auction_instruction,
|
|
processor::{end_auction::EndAuctionArgs, AuctionData, AuctionDataExtended, AuctionState},
|
|
},
|
|
spl_token::instruction::{set_authority, AuthorityType},
|
|
spl_token_metadata::{
|
|
instruction::update_metadata_accounts,
|
|
state::{Metadata, EDITION},
|
|
},
|
|
spl_token_vault::{instruction::create_withdraw_tokens_instruction, state::SafetyDepositBox},
|
|
std::{convert::TryInto, str::FromStr},
|
|
};
|
|
|
|
/// Cheap method to just grab amount from token account, instead of deserializing entire thing
|
|
pub fn get_amount_from_token_account(
|
|
token_account_info: &AccountInfo,
|
|
) -> Result<u64, ProgramError> {
|
|
// TokeAccount layout: mint(32), owner(32), ...
|
|
let data = token_account_info.try_borrow_data()?;
|
|
let amount_data = array_ref![data, 64, 8];
|
|
Ok(u64::from_le_bytes(*amount_data))
|
|
}
|
|
|
|
/// assert initialized account
|
|
pub fn assert_initialized<T: Pack + IsInitialized>(
|
|
account_info: &AccountInfo,
|
|
) -> Result<T, ProgramError> {
|
|
let account: T = T::unpack_unchecked(&account_info.data.borrow())?;
|
|
if !account.is_initialized() {
|
|
Err(MetaplexError::Uninitialized.into())
|
|
} else {
|
|
Ok(account)
|
|
}
|
|
}
|
|
|
|
pub fn assert_rent_exempt(rent: &Rent, account_info: &AccountInfo) -> ProgramResult {
|
|
if !rent.is_exempt(account_info.lamports(), account_info.data_len()) {
|
|
Err(MetaplexError::NotRentExempt.into())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub fn assert_owned_by(account: &AccountInfo, owner: &Pubkey) -> ProgramResult {
|
|
if account.owner != owner {
|
|
Err(MetaplexError::IncorrectOwner.into())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub fn assert_signer(account_info: &AccountInfo) -> ProgramResult {
|
|
if !account_info.is_signer {
|
|
Err(ProgramError::MissingRequiredSignature)
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub fn assert_store_safety_vault_manager_match(
|
|
auction_manager: &AuctionManager,
|
|
safety_deposit_info: &AccountInfo,
|
|
vault_info: &AccountInfo,
|
|
token_vault_program: &Pubkey,
|
|
) -> ProgramResult {
|
|
if auction_manager.vault != *vault_info.key {
|
|
return Err(MetaplexError::AuctionManagerVaultMismatch.into());
|
|
}
|
|
|
|
let data = safety_deposit_info.data.borrow();
|
|
let vault_key = Pubkey::new_from_array(*array_ref![data, 1, 32]);
|
|
let token_mint_key = Pubkey::new_from_array(*array_ref![data, 33, 32]);
|
|
|
|
assert_derivation(
|
|
&token_vault_program,
|
|
safety_deposit_info,
|
|
&[
|
|
spl_token_vault::state::PREFIX.as_bytes(),
|
|
vault_info.key.as_ref(),
|
|
token_mint_key.as_ref(),
|
|
],
|
|
)?;
|
|
|
|
if *vault_info.key != vault_key {
|
|
return Err(MetaplexError::SafetyDepositBoxVaultMismatch.into());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn assert_at_least_one_creator_matches_or_store_public_and_all_verified(
|
|
program_id: &Pubkey,
|
|
auction_manager: &AuctionManager,
|
|
metadata: &Metadata,
|
|
whitelisted_creator_info: &AccountInfo,
|
|
store_info: &AccountInfo,
|
|
) -> ProgramResult {
|
|
let store = Store::from_account_info(store_info)?;
|
|
if store.public {
|
|
return Ok(());
|
|
}
|
|
if let Some(creators) = &metadata.data.creators {
|
|
// does it exist? It better!
|
|
let existing_whitelist_creator: WhitelistedCreator =
|
|
match WhitelistedCreator::from_account_info(whitelisted_creator_info) {
|
|
Ok(val) => val,
|
|
Err(_) => return Err(MetaplexError::InvalidWhitelistedCreator.into()),
|
|
};
|
|
|
|
if !existing_whitelist_creator.activated {
|
|
return Err(MetaplexError::WhitelistedCreatorInactive.into());
|
|
}
|
|
|
|
let mut found = false;
|
|
for creator in creators {
|
|
// Now find at least one creator that can make this pda in the list
|
|
let (key, _) = Pubkey::find_program_address(
|
|
&[
|
|
PREFIX.as_bytes(),
|
|
program_id.as_ref(),
|
|
auction_manager.store.as_ref(),
|
|
creator.address.as_ref(),
|
|
],
|
|
program_id,
|
|
);
|
|
|
|
if key == *whitelisted_creator_info.key {
|
|
found = true;
|
|
}
|
|
|
|
if !creator.verified {
|
|
return Err(MetaplexError::CreatorHasNotVerifiedMetadata.into());
|
|
}
|
|
}
|
|
|
|
if found {
|
|
return Ok(());
|
|
}
|
|
}
|
|
Err(MetaplexError::InvalidWhitelistedCreator.into())
|
|
}
|
|
|
|
pub fn assert_authority_correct(
|
|
auction_manager: &AuctionManager,
|
|
authority_info: &AccountInfo,
|
|
) -> ProgramResult {
|
|
if auction_manager.authority != *authority_info.key {
|
|
return Err(MetaplexError::AuctionManagerAuthorityMismatch.into());
|
|
}
|
|
|
|
assert_signer(authority_info)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn assert_auction_is_ended_or_valid_instant_sale(
|
|
auction_info: &AccountInfo,
|
|
auction_extended_info: Option<&AccountInfo>,
|
|
bidder_metadata_info: &AccountInfo,
|
|
win_index: Option<usize>,
|
|
) -> ProgramResult {
|
|
if AuctionData::get_state(auction_info)? == AuctionState::Ended {
|
|
return Ok(());
|
|
}
|
|
|
|
let instant_sale_price = auction_extended_info
|
|
.and_then(|info| AuctionDataExtended::get_instant_sale_price(&info.data.borrow()));
|
|
|
|
match instant_sale_price {
|
|
Some(instant_sale_price) => {
|
|
let winner_bid_price = if let Some(win_index) = win_index {
|
|
AuctionData::get_winner_bid_amount_at(auction_info, win_index).unwrap()
|
|
} else {
|
|
// Possible case in an open auction
|
|
BidderMetadata::from_account_info(bidder_metadata_info)?.last_bid
|
|
};
|
|
|
|
if winner_bid_price < instant_sale_price {
|
|
return Err(MetaplexError::AuctionHasNotEnded.into());
|
|
}
|
|
}
|
|
None => return Err(MetaplexError::AuctionHasNotEnded.into()),
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Create account almost from scratch, lifted from
|
|
/// https://github.com/solana-labs/solana-program-library/blob/7d4873c61721aca25464d42cc5ef651a7923ca79/associated-token-account/program/src/processor.rs#L51-L98
|
|
#[inline(always)]
|
|
pub fn create_or_allocate_account_raw<'a>(
|
|
program_id: Pubkey,
|
|
new_account_info: &AccountInfo<'a>,
|
|
rent_sysvar_info: &AccountInfo<'a>,
|
|
system_program_info: &AccountInfo<'a>,
|
|
payer_info: &AccountInfo<'a>,
|
|
size: usize,
|
|
signer_seeds: &[&[u8]],
|
|
) -> Result<(), ProgramError> {
|
|
let rent = &Rent::from_account_info(rent_sysvar_info)?;
|
|
let required_lamports = rent
|
|
.minimum_balance(size)
|
|
.max(1)
|
|
.saturating_sub(new_account_info.lamports());
|
|
|
|
if required_lamports > 0 {
|
|
msg!("Transfer {} lamports to the new account", required_lamports);
|
|
invoke(
|
|
&system_instruction::transfer(&payer_info.key, new_account_info.key, required_lamports),
|
|
&[
|
|
payer_info.clone(),
|
|
new_account_info.clone(),
|
|
system_program_info.clone(),
|
|
],
|
|
)?;
|
|
}
|
|
|
|
let accounts = &[new_account_info.clone(), system_program_info.clone()];
|
|
|
|
msg!("Allocate space for the account");
|
|
invoke_signed(
|
|
&system_instruction::allocate(new_account_info.key, size.try_into().unwrap()),
|
|
accounts,
|
|
&[&signer_seeds],
|
|
)?;
|
|
|
|
msg!("Assign the account to the owning program");
|
|
invoke_signed(
|
|
&system_instruction::assign(new_account_info.key, &program_id),
|
|
accounts,
|
|
&[&signer_seeds],
|
|
)?;
|
|
msg!("Completed assignation!");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn transfer_safety_deposit_box_items<'a>(
|
|
token_vault_program: AccountInfo<'a>,
|
|
destination: AccountInfo<'a>,
|
|
safety_deposit_box: AccountInfo<'a>,
|
|
safety_deposit_token_store: AccountInfo<'a>,
|
|
vault: AccountInfo<'a>,
|
|
fraction_mint: AccountInfo<'a>,
|
|
vault_authority: AccountInfo<'a>,
|
|
transfer_authority: AccountInfo<'a>,
|
|
rent: AccountInfo<'a>,
|
|
amount: u64,
|
|
signer_seeds: &[&[u8]],
|
|
) -> ProgramResult {
|
|
invoke_signed(
|
|
&create_withdraw_tokens_instruction(
|
|
*token_vault_program.key,
|
|
*destination.key,
|
|
*safety_deposit_box.key,
|
|
*safety_deposit_token_store.key,
|
|
*vault.key,
|
|
*fraction_mint.key,
|
|
*vault_authority.key,
|
|
*transfer_authority.key,
|
|
amount,
|
|
),
|
|
&[
|
|
token_vault_program,
|
|
destination,
|
|
safety_deposit_box,
|
|
safety_deposit_token_store,
|
|
vault,
|
|
fraction_mint,
|
|
vault_authority,
|
|
transfer_authority,
|
|
rent,
|
|
],
|
|
&[&signer_seeds],
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn transfer_metadata_ownership<'a>(
|
|
token_metadata_program: AccountInfo<'a>,
|
|
metadata_info: AccountInfo<'a>,
|
|
update_authority: AccountInfo<'a>,
|
|
new_update_authority: AccountInfo<'a>,
|
|
signer_seeds: &[&[u8]],
|
|
) -> ProgramResult {
|
|
invoke_signed(
|
|
&update_metadata_accounts(
|
|
*token_metadata_program.key,
|
|
*metadata_info.key,
|
|
*update_authority.key,
|
|
Some(*new_update_authority.key),
|
|
None,
|
|
Some(true),
|
|
),
|
|
&[
|
|
update_authority,
|
|
new_update_authority,
|
|
metadata_info,
|
|
token_metadata_program,
|
|
],
|
|
&[&signer_seeds],
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn transfer_mint_authority<'a>(
|
|
new_authority_seeds: &[&[u8]],
|
|
new_authority_key: &Pubkey,
|
|
new_authority_info: &AccountInfo<'a>,
|
|
mint_info: &AccountInfo<'a>,
|
|
mint_authority_info: &AccountInfo<'a>,
|
|
token_program_info: &AccountInfo<'a>,
|
|
) -> ProgramResult {
|
|
msg!("Setting mint authority");
|
|
invoke_signed(
|
|
&set_authority(
|
|
token_program_info.key,
|
|
mint_info.key,
|
|
Some(new_authority_key),
|
|
AuthorityType::MintTokens,
|
|
mint_authority_info.key,
|
|
&[&mint_authority_info.key],
|
|
)
|
|
.unwrap(),
|
|
&[
|
|
mint_authority_info.clone(),
|
|
mint_info.clone(),
|
|
token_program_info.clone(),
|
|
new_authority_info.clone(),
|
|
],
|
|
&[new_authority_seeds],
|
|
)?;
|
|
msg!("Setting freeze authority");
|
|
invoke_signed(
|
|
&set_authority(
|
|
token_program_info.key,
|
|
mint_info.key,
|
|
Some(&new_authority_key),
|
|
AuthorityType::FreezeAccount,
|
|
mint_authority_info.key,
|
|
&[&mint_authority_info.key],
|
|
)
|
|
.unwrap(),
|
|
&[
|
|
mint_authority_info.clone(),
|
|
mint_info.clone(),
|
|
token_program_info.clone(),
|
|
new_authority_info.clone(),
|
|
],
|
|
&[new_authority_seeds],
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub struct CommonRedeemReturn {
|
|
pub redemption_bump_seed: u8,
|
|
pub auction_manager: AuctionManager,
|
|
pub cancelled: bool,
|
|
pub rent: Rent,
|
|
pub win_index: Option<usize>,
|
|
pub token_metadata_program: Pubkey,
|
|
}
|
|
|
|
pub struct CommonRedeemCheckArgs<'a> {
|
|
pub program_id: &'a Pubkey,
|
|
pub auction_manager_info: &'a AccountInfo<'a>,
|
|
pub safety_deposit_token_store_info: &'a AccountInfo<'a>,
|
|
pub destination_info: &'a AccountInfo<'a>,
|
|
pub bid_redemption_info: &'a AccountInfo<'a>,
|
|
pub safety_deposit_info: &'a AccountInfo<'a>,
|
|
pub vault_info: &'a AccountInfo<'a>,
|
|
pub auction_info: &'a AccountInfo<'a>,
|
|
pub auction_extended_info: Option<&'a AccountInfo<'a>>,
|
|
pub bidder_metadata_info: &'a AccountInfo<'a>,
|
|
pub bidder_info: &'a AccountInfo<'a>,
|
|
pub token_program_info: &'a AccountInfo<'a>,
|
|
pub token_vault_program_info: &'a AccountInfo<'a>,
|
|
pub token_metadata_program_info: &'a AccountInfo<'a>,
|
|
pub store_info: &'a AccountInfo<'a>,
|
|
pub rent_info: &'a AccountInfo<'a>,
|
|
pub is_participation: bool,
|
|
// If this is being called by the auctioneer to pull prizes out they overwrite the win index
|
|
// they would normally get if they themselves bid for whatever win index they choose.
|
|
pub overwrite_win_index: Option<usize>,
|
|
// In newer endpoints, to conserve CPU and make way for 10,000 person auctions,
|
|
// client must specify win index and then we simply check if the address matches for O(1) lookup vs O(n)
|
|
// scan. This is an option so older actions which rely on the O(n) lookup because we can't change their call structure
|
|
// can continue to work.
|
|
pub user_provided_win_index: Option<Option<usize>>,
|
|
pub assert_bidder_signer: bool,
|
|
// For printing v2, the edition pda is what essentially forms a backstop for bad bidders. We do not need this additional
|
|
// check which isn't accurate anyway when one winning config item has an amount > 1.
|
|
pub ignore_bid_redeemed_item_check: bool,
|
|
}
|
|
|
|
fn calculate_win_index(
|
|
bidder_info: &AccountInfo,
|
|
auction_info: &AccountInfo,
|
|
user_provided_win_index: Option<Option<usize>>,
|
|
overwrite_win_index: Option<usize>,
|
|
) -> Result<Option<usize>, ProgramError> {
|
|
let mut win_index: Option<usize>;
|
|
// User provided us with an option of an option telling us what if anything they won. We need to validate.
|
|
if let Some(up_win_index) = user_provided_win_index {
|
|
// check that this person is the winner they say they are. Only if not doing an override of win index,
|
|
// which we know likely wont match bidder info and is simply checking below that you arent stealing a prize.
|
|
|
|
if overwrite_win_index.is_none() {
|
|
if let Some(up_win_index_unwrapped) = up_win_index {
|
|
let winner = AuctionData::get_winner_at(auction_info, up_win_index_unwrapped);
|
|
if let Some(winner_key) = winner {
|
|
if winner_key != *bidder_info.key {
|
|
return Err(MetaplexError::WinnerIndexMismatch.into());
|
|
}
|
|
} else {
|
|
return Err(MetaplexError::WinnerIndexMismatch.into());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Notice if overwrite win index is some, this gets wiped anyway in the if statement below.
|
|
// If not, it becomes the win index going forward as we have validated the user is either
|
|
// saying they won nothing (Participation redemption) or they won something
|
|
// and they weren't lying.
|
|
win_index = up_win_index;
|
|
} else {
|
|
// Legacy system where we O(n) scan the bid index to find the winner index. CPU intensive.
|
|
win_index = AuctionData::get_is_winner(auction_info, bidder_info.key);
|
|
}
|
|
|
|
// This means auctioneer is attempting to pull goods out of the system, and is attempting to set
|
|
// the win index for themselves. Has a different field because it has different logic - mainly
|
|
// just checking to make sure you arent claiming from someone who won. Supersedes normal user provided
|
|
// logic.
|
|
if let Some(index) = overwrite_win_index {
|
|
let winner_at = AuctionData::get_winner_at(auction_info, index);
|
|
if winner_at.is_some() {
|
|
return Err(MetaplexError::AuctioneerCantClaimWonPrize.into());
|
|
} else {
|
|
win_index = overwrite_win_index
|
|
}
|
|
}
|
|
|
|
Ok(win_index)
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn common_redeem_checks(
|
|
args: CommonRedeemCheckArgs,
|
|
) -> Result<CommonRedeemReturn, ProgramError> {
|
|
let CommonRedeemCheckArgs {
|
|
program_id,
|
|
auction_manager_info,
|
|
safety_deposit_token_store_info,
|
|
destination_info,
|
|
bid_redemption_info,
|
|
safety_deposit_info,
|
|
vault_info,
|
|
auction_info,
|
|
auction_extended_info,
|
|
bidder_metadata_info,
|
|
bidder_info,
|
|
token_program_info,
|
|
token_vault_program_info,
|
|
token_metadata_program_info,
|
|
rent_info,
|
|
store_info,
|
|
is_participation,
|
|
overwrite_win_index,
|
|
user_provided_win_index,
|
|
assert_bidder_signer,
|
|
ignore_bid_redeemed_item_check,
|
|
} = args;
|
|
|
|
let rent = &Rent::from_account_info(&rent_info)?;
|
|
|
|
let mut auction_manager: AuctionManager =
|
|
AuctionManager::from_account_info(auction_manager_info)?;
|
|
let store_data = store_info.data.borrow();
|
|
let cancelled: bool;
|
|
|
|
let auction_program = Pubkey::new_from_array(*array_ref![store_data, 2, 32]);
|
|
let token_vault_program = Pubkey::new_from_array(*array_ref![store_data, 34, 32]);
|
|
let token_metadata_program = Pubkey::new_from_array(*array_ref![store_data, 66, 32]);
|
|
let token_program = Pubkey::new_from_array(*array_ref![store_data, 98, 32]);
|
|
|
|
let mut redemption_bump_seed: u8 = 0;
|
|
if overwrite_win_index.is_some() {
|
|
cancelled = false;
|
|
|
|
if *bidder_info.key != auction_manager.authority {
|
|
return Err(MetaplexError::MustBeAuctioneer.into());
|
|
}
|
|
} else {
|
|
let bidder_metadata_data = bidder_metadata_info.data.borrow();
|
|
if bidder_metadata_data[80] == 0 {
|
|
cancelled = false
|
|
} else {
|
|
cancelled = true;
|
|
}
|
|
assert_owned_by(bidder_metadata_info, &auction_program)?;
|
|
assert_derivation(
|
|
&auction_program,
|
|
bidder_metadata_info,
|
|
&[
|
|
spl_auction::PREFIX.as_bytes(),
|
|
auction_program.as_ref(),
|
|
auction_info.key.as_ref(),
|
|
bidder_info.key.as_ref(),
|
|
"metadata".as_bytes(),
|
|
],
|
|
)?;
|
|
|
|
let bidder_pubkey = Pubkey::new_from_array(*array_ref![bidder_metadata_data, 0, 32]);
|
|
if bidder_pubkey != *bidder_info.key {
|
|
return Err(MetaplexError::BidderMetadataBidderMismatch.into());
|
|
}
|
|
let redemption_path = [
|
|
PREFIX.as_bytes(),
|
|
auction_manager.auction.as_ref(),
|
|
bidder_metadata_info.key.as_ref(),
|
|
];
|
|
let (redemption_key, actual_redemption_bump_seed) =
|
|
Pubkey::find_program_address(&redemption_path, &program_id);
|
|
|
|
redemption_bump_seed = actual_redemption_bump_seed;
|
|
if redemption_key != *bid_redemption_info.key {
|
|
return Err(MetaplexError::BidRedemptionMismatch.into());
|
|
}
|
|
}
|
|
|
|
let win_index = calculate_win_index(
|
|
bidder_info,
|
|
auction_info,
|
|
user_provided_win_index,
|
|
overwrite_win_index,
|
|
)?;
|
|
|
|
if !bid_redemption_info.data_is_empty() && overwrite_win_index.is_none() {
|
|
let bid_redemption_data = bid_redemption_info.data.borrow();
|
|
|
|
if bid_redemption_data[0] != Key::BidRedemptionTicketV1 as u8 {
|
|
return Err(MetaplexError::DataTypeMismatch.into());
|
|
}
|
|
|
|
let mut participation_redeemed = false;
|
|
if bid_redemption_data[1] == 1 {
|
|
participation_redeemed = true;
|
|
}
|
|
let items_redeemed = bid_redemption_data[2];
|
|
msg!(
|
|
"Items redeemed is {} and participation redemption is {}",
|
|
items_redeemed,
|
|
participation_redeemed
|
|
);
|
|
let possible_items_to_redeem = match win_index {
|
|
Some(val) => auction_manager.settings.winning_configs[val].items.len(),
|
|
None => 0,
|
|
};
|
|
if (is_participation && participation_redeemed)
|
|
|| (!is_participation
|
|
&& !ignore_bid_redeemed_item_check
|
|
&& items_redeemed == possible_items_to_redeem as u8)
|
|
{
|
|
return Err(MetaplexError::BidAlreadyRedeemed.into());
|
|
}
|
|
}
|
|
|
|
if assert_bidder_signer {
|
|
assert_signer(bidder_info)?;
|
|
}
|
|
|
|
assert_owned_by(&destination_info, token_program_info.key)?;
|
|
assert_owned_by(&auction_manager_info, &program_id)?;
|
|
assert_owned_by(safety_deposit_token_store_info, token_program_info.key)?;
|
|
if !bid_redemption_info.data_is_empty() {
|
|
assert_owned_by(bid_redemption_info, &program_id)?;
|
|
}
|
|
assert_owned_by(safety_deposit_info, &token_vault_program)?;
|
|
assert_owned_by(vault_info, &token_vault_program)?;
|
|
assert_owned_by(auction_info, &auction_program)?;
|
|
assert_owned_by(store_info, &program_id)?;
|
|
|
|
assert_store_safety_vault_manager_match(
|
|
&auction_manager,
|
|
&safety_deposit_info,
|
|
&vault_info,
|
|
&token_vault_program,
|
|
)?;
|
|
// looking out for you!
|
|
assert_rent_exempt(rent, &destination_info)?;
|
|
|
|
if auction_manager.auction != *auction_info.key {
|
|
return Err(MetaplexError::AuctionManagerAuctionMismatch.into());
|
|
}
|
|
|
|
if *store_info.key != auction_manager.store {
|
|
return Err(MetaplexError::AuctionManagerStoreMismatch.into());
|
|
}
|
|
|
|
if token_program != *token_program_info.key {
|
|
return Err(MetaplexError::AuctionManagerTokenProgramMismatch.into());
|
|
}
|
|
|
|
if token_vault_program != *token_vault_program_info.key {
|
|
return Err(MetaplexError::AuctionManagerTokenVaultProgramMismatch.into());
|
|
}
|
|
|
|
if token_metadata_program != *token_metadata_program_info.key {
|
|
return Err(MetaplexError::AuctionManagerTokenMetadataProgramMismatch.into());
|
|
}
|
|
|
|
assert_auction_is_ended_or_valid_instant_sale(
|
|
auction_info,
|
|
auction_extended_info,
|
|
bidder_metadata_info,
|
|
win_index,
|
|
)?;
|
|
|
|
// No-op if already set.
|
|
auction_manager.state.status = AuctionManagerStatus::Disbursing;
|
|
|
|
Ok(CommonRedeemReturn {
|
|
redemption_bump_seed,
|
|
auction_manager,
|
|
cancelled,
|
|
rent: *rent,
|
|
win_index,
|
|
token_metadata_program,
|
|
})
|
|
}
|
|
|
|
pub struct CommonRedeemFinishArgs<'a> {
|
|
pub program_id: &'a Pubkey,
|
|
pub auction_manager: AuctionManager,
|
|
pub auction_manager_info: &'a AccountInfo<'a>,
|
|
pub bidder_metadata_info: &'a AccountInfo<'a>,
|
|
pub rent_info: &'a AccountInfo<'a>,
|
|
pub system_info: &'a AccountInfo<'a>,
|
|
pub payer_info: &'a AccountInfo<'a>,
|
|
pub bid_redemption_info: &'a AccountInfo<'a>,
|
|
pub winning_index: Option<usize>,
|
|
pub redemption_bump_seed: u8,
|
|
pub bid_redeemed: bool,
|
|
pub participation_redeemed: bool,
|
|
pub winning_item_index: Option<usize>,
|
|
pub overwrite_win_index: Option<usize>,
|
|
}
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn common_redeem_finish(args: CommonRedeemFinishArgs) -> ProgramResult {
|
|
let CommonRedeemFinishArgs {
|
|
program_id,
|
|
auction_manager,
|
|
auction_manager_info,
|
|
bidder_metadata_info,
|
|
rent_info,
|
|
system_info,
|
|
payer_info,
|
|
bid_redemption_info,
|
|
winning_index,
|
|
redemption_bump_seed,
|
|
bid_redeemed,
|
|
participation_redeemed,
|
|
winning_item_index,
|
|
overwrite_win_index,
|
|
} = args;
|
|
|
|
if (bid_redeemed || participation_redeemed) && overwrite_win_index.is_none() {
|
|
let redemption_seeds = &[
|
|
PREFIX.as_bytes(),
|
|
auction_manager.auction.as_ref(),
|
|
bidder_metadata_info.key.as_ref(),
|
|
&[redemption_bump_seed],
|
|
];
|
|
|
|
if bid_redemption_info.data_is_empty() {
|
|
create_or_allocate_account_raw(
|
|
*program_id,
|
|
&bid_redemption_info,
|
|
&rent_info,
|
|
&system_info,
|
|
&payer_info,
|
|
MAX_BID_REDEMPTION_TICKET_SIZE,
|
|
redemption_seeds,
|
|
)?;
|
|
}
|
|
// Saving on CPU in these large actions by avoiding borsh
|
|
let data = &mut bid_redemption_info.data.borrow_mut();
|
|
let output = array_mut_ref![data, 0, MAX_BID_REDEMPTION_TICKET_SIZE];
|
|
|
|
let (key, participation_redeemed_ptr, items_redeemed_ptr) =
|
|
mut_array_refs![output, 1, 1, 1];
|
|
|
|
*key = [Key::BidRedemptionTicketV1 as u8];
|
|
|
|
let curr_items_redeemed = u8::from_le_bytes(*items_redeemed_ptr);
|
|
|
|
if participation_redeemed {
|
|
*participation_redeemed_ptr = [1];
|
|
} else if bid_redeemed {
|
|
*items_redeemed_ptr = curr_items_redeemed
|
|
.checked_add(1)
|
|
.ok_or(MetaplexError::NumericalOverflowError)?
|
|
.to_le_bytes();
|
|
}
|
|
}
|
|
|
|
msg!("About to pass through the eye of the needle");
|
|
sol_log_compute_units();
|
|
|
|
if bid_redeemed {
|
|
if let Some(index) = winning_index {
|
|
if let Some(item_index) = winning_item_index {
|
|
AuctionManager::set_claimed_and_status(
|
|
auction_manager_info,
|
|
auction_manager.state.status,
|
|
index,
|
|
item_index,
|
|
auction_manager.straight_shot_optimization,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub struct CommonWinningConfigCheckReturn {
|
|
pub winning_config_item: WinningConfigItem,
|
|
pub winning_item_index: Option<usize>,
|
|
}
|
|
|
|
pub fn common_winning_config_checks(
|
|
auction_manager: &AuctionManager,
|
|
safety_deposit_info: &AccountInfo,
|
|
winning_index: usize,
|
|
ignore_claim: bool,
|
|
) -> Result<CommonWinningConfigCheckReturn, ProgramError> {
|
|
let winning_config = &auction_manager.settings.winning_configs[winning_index];
|
|
let winning_config_state = &auction_manager.state.winning_config_states[winning_index];
|
|
|
|
let mut winning_item_index = None;
|
|
for i in 0..winning_config.items.len() {
|
|
if winning_config.items[i].safety_deposit_box_index
|
|
== SafetyDepositBox::get_order(safety_deposit_info)
|
|
{
|
|
winning_item_index = Some(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
let winning_config_item = match winning_item_index {
|
|
Some(index) => winning_config.items[index],
|
|
None => return Err(MetaplexError::SafetyDepositBoxNotUsedInAuction.into()),
|
|
};
|
|
|
|
let winning_config_state_item = match winning_item_index {
|
|
Some(index) => winning_config_state.items[index],
|
|
None => return Err(MetaplexError::SafetyDepositBoxNotUsedInAuction.into()),
|
|
};
|
|
|
|
// For printing v2, we may call many times for different editions and the edition PDA check makes sure it cant
|
|
// be claimed over-much. This would be 1 time, we need n times.
|
|
if winning_config_state_item.claimed && !ignore_claim {
|
|
return Err(MetaplexError::PrizeAlreadyClaimed.into());
|
|
}
|
|
|
|
Ok(CommonWinningConfigCheckReturn {
|
|
winning_config_item,
|
|
winning_item_index,
|
|
})
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn shift_authority_back_to_originating_user<'a>(
|
|
program_id: &Pubkey,
|
|
auction_manager: &AuctionManager,
|
|
auction_manager_info: &AccountInfo<'a>,
|
|
master_metadata_info: &AccountInfo<'a>,
|
|
original_authority: &AccountInfo<'a>,
|
|
original_authority_lookup_info: &AccountInfo<'a>,
|
|
printing_mint_info: &AccountInfo<'a>,
|
|
token_program_info: &AccountInfo<'a>,
|
|
authority_seeds: &[&[u8]],
|
|
) -> ProgramResult {
|
|
let original_authority_lookup_seeds = &[
|
|
PREFIX.as_bytes(),
|
|
&auction_manager.auction.as_ref(),
|
|
master_metadata_info.key.as_ref(),
|
|
];
|
|
|
|
let (expected_key, _) =
|
|
Pubkey::find_program_address(original_authority_lookup_seeds, &program_id);
|
|
|
|
if expected_key != *original_authority_lookup_info.key {
|
|
return Err(MetaplexError::OriginalAuthorityLookupKeyMismatch.into());
|
|
}
|
|
|
|
let original_authority_lookup: OriginalAuthorityLookup =
|
|
OriginalAuthorityLookup::from_account_info(original_authority_lookup_info)?;
|
|
if original_authority_lookup.original_authority != *original_authority.key {
|
|
return Err(MetaplexError::OriginalAuthorityMismatch.into());
|
|
}
|
|
transfer_mint_authority(
|
|
authority_seeds,
|
|
original_authority.key,
|
|
original_authority,
|
|
printing_mint_info,
|
|
auction_manager_info,
|
|
token_program_info,
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// TODO due to a weird stack access violation bug we had to remove the args struct from this method
|
|
// to get redemptions working again after integrating new Auctions program. Try to bring it back one day
|
|
#[inline(always)]
|
|
pub fn spl_token_transfer<'a: 'b, 'b>(
|
|
source: AccountInfo<'a>,
|
|
destination: AccountInfo<'a>,
|
|
amount: u64,
|
|
authority: AccountInfo<'a>,
|
|
authority_signer_seeds: &'b [&'b [u8]],
|
|
token_program: AccountInfo<'a>,
|
|
) -> ProgramResult {
|
|
let result = invoke_signed(
|
|
&spl_token::instruction::transfer(
|
|
token_program.key,
|
|
source.key,
|
|
destination.key,
|
|
authority.key,
|
|
&[],
|
|
amount,
|
|
)?,
|
|
&[source, destination, authority, token_program],
|
|
&[authority_signer_seeds],
|
|
);
|
|
|
|
result.map_err(|_| MetaplexError::TokenTransferFailed.into())
|
|
}
|
|
|
|
pub fn assert_edition_valid(
|
|
program_id: &Pubkey,
|
|
mint: &Pubkey,
|
|
edition_account_info: &AccountInfo,
|
|
) -> ProgramResult {
|
|
let edition_seeds = &[
|
|
spl_token_metadata::state::PREFIX.as_bytes(),
|
|
program_id.as_ref(),
|
|
&mint.as_ref(),
|
|
EDITION.as_bytes(),
|
|
];
|
|
let (edition_key, _) = Pubkey::find_program_address(edition_seeds, program_id);
|
|
if edition_key != *edition_account_info.key {
|
|
return Err(MetaplexError::InvalidEditionKey.into());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// TODO due to a weird stack access violation bug we had to remove the args struct from this method
|
|
// to get redemptions working again after integrating new Auctions program. Try to bring it back one day.
|
|
pub fn spl_token_mint_to<'a: 'b, 'b>(
|
|
mint: AccountInfo<'a>,
|
|
destination: AccountInfo<'a>,
|
|
amount: u64,
|
|
authority: AccountInfo<'a>,
|
|
authority_signer_seeds: &'b [&'b [u8]],
|
|
token_program: AccountInfo<'a>,
|
|
) -> ProgramResult {
|
|
let result = invoke_signed(
|
|
&spl_token::instruction::mint_to(
|
|
token_program.key,
|
|
mint.key,
|
|
destination.key,
|
|
authority.key,
|
|
&[],
|
|
amount,
|
|
)?,
|
|
&[mint, destination, authority, token_program],
|
|
&[authority_signer_seeds],
|
|
);
|
|
result.map_err(|_| MetaplexError::TokenMintToFailed.into())
|
|
}
|
|
|
|
pub fn assert_derivation(
|
|
program_id: &Pubkey,
|
|
account: &AccountInfo,
|
|
path: &[&[u8]],
|
|
) -> Result<u8, ProgramError> {
|
|
let (key, bump) = Pubkey::find_program_address(&path, program_id);
|
|
if key != *account.key {
|
|
return Err(MetaplexError::DerivedKeyInvalid.into());
|
|
}
|
|
Ok(bump)
|
|
}
|
|
|
|
pub fn try_from_slice_checked<T: BorshDeserialize>(
|
|
data: &[u8],
|
|
data_type: Key,
|
|
data_size: usize,
|
|
) -> Result<T, ProgramError> {
|
|
if (data[0] != data_type as u8 && data[0] != Key::Uninitialized as u8)
|
|
|| data.len() != data_size
|
|
{
|
|
return Err(MetaplexError::DataTypeMismatch.into());
|
|
}
|
|
|
|
let result: T = try_from_slice_unchecked(data)?;
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
pub fn end_auction<'a: 'b, 'b>(
|
|
resource: Pubkey,
|
|
auction: AccountInfo<'a>,
|
|
authority: AccountInfo<'a>,
|
|
auction_program: AccountInfo<'a>,
|
|
clock: AccountInfo<'a>,
|
|
authority_signer_seeds: &'b [&'b [u8]],
|
|
) -> ProgramResult {
|
|
invoke_signed(
|
|
&end_auction_instruction(
|
|
*auction_program.key,
|
|
*authority.key,
|
|
EndAuctionArgs {
|
|
resource,
|
|
reveal: None,
|
|
},
|
|
),
|
|
&[auction, authority, auction_program, clock],
|
|
&[authority_signer_seeds],
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn assert_is_ata(
|
|
account: &AccountInfo,
|
|
wallet: &Pubkey,
|
|
token_program: &Pubkey,
|
|
mint: &Pubkey,
|
|
) -> ProgramResult {
|
|
assert_derivation(
|
|
&Pubkey::from_str("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL").unwrap(),
|
|
account,
|
|
&[wallet.as_ref(), token_program.as_ref(), mint.as_ref()],
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|