refactor: instant sale considered open edition auction

This commit is contained in:
Vecheslav Druzhbin 2021-07-29 13:05:30 +03:00
parent ff4ed1b18a
commit e18c4e0bf5
7 changed files with 104 additions and 62 deletions

View File

@ -6,4 +6,7 @@ indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
trim_trailing_whitespace = true
[*.rs]
indent_size = 4

View File

@ -15,7 +15,7 @@ import { findProgramAddress } from '../utils';
export const AUCTION_PREFIX = 'auction';
export const METADATA = 'metadata';
export const EXTENDED = 'extended';
export const MAX_AUCTION_DATA_EXTENDED_SIZE = 8 + 9 + 2 + 200;
export const MAX_AUCTION_DATA_EXTENDED_SIZE = 8 + 9 + 2 + 9 + 191;
export enum AuctionState {
Created = 0,

View File

@ -31,20 +31,11 @@ pub enum AuctionInstruction {
/// Create a new auction account bound to a resource, initially in a pending state.
/// 0. `[signer]` The account creating the auction, which is authorised to make changes.
/// 1. `[writable]` Uninitialized auction account.
/// 2. `[writable]` Auction extended data account.
/// 2. `[writable]` Auction extended data account (pda relative to auction of ['auction', program id, vault key, 'extended']).
/// 3. `[]` Rent sysvar
/// 4. `[]` System account
CreateAuction(CreateAuctionArgs),
/// Create a new auction account bound to a resource, initially in a pending state.
/// The only one difference with above instruction it's instant_sale_price parameter in CreateAuctionArgsV2
/// 0. `[signer]` The account creating the auction, which is authorised to make changes.
/// 1. `[writable]` Uninitialized auction account.
/// 2. `[writable]` Auction extended data account.
/// 3. `[]` Rent sysvar
/// 4. `[]` System account
CreateAuctionV2(CreateAuctionArgsV2),
/// Move SPL tokens from winning bid to the destination account.
/// 0. `[writable]` The destination account
/// 1. `[writable]` The bidder pot token account
@ -55,7 +46,7 @@ pub enum AuctionInstruction {
/// 6. `[]` Token mint of the auction
/// 7. `[]` Clock sysvar
/// 8. `[]` Token program
/// 9. `[]` Auction extended
/// 9. `[]` Auction extended (pda relative to auction of ['auction', program id, vault key, 'extended'])
ClaimBid(ClaimBidArgs),
/// Ends an auction, regardless of end timing conditions
@ -85,6 +76,15 @@ pub enum AuctionInstruction {
/// 11. `[]` System program
/// 12. `[]` SPL Token Program
PlaceBid(PlaceBidArgs),
/// Create a new auction account bound to a resource, initially in a pending state.
/// The only one difference with above instruction it's additional parameters in CreateAuctionArgsV2
/// 0. `[signer]` The account creating the auction, which is authorised to make changes.
/// 1. `[writable]` Uninitialized auction account.
/// 2. `[writable]` Auction extended data account (pda relative to auction of ['auction', program id, vault key, 'extended']).
/// 3. `[]` Rent sysvar
/// 4. `[]` System account
CreateAuctionV2(CreateAuctionArgsV2),
}
/// Creates an CreateAuction instruction.

View File

@ -370,6 +370,16 @@ impl AuctionData {
self.bid_state.winner_at(idx)
}
pub fn consider_instant_bid(&mut self, instant_sale_price: Option<u64>) {
// Check if all the lots were sold with instant_sale_price
if let Some(price) = instant_sale_price {
if self.bid_state.has_instant_bid(price) {
msg!("All the lots were sold with instant_sale_price, auction is ended");
self.state = AuctionState::Ended;
}
}
}
pub fn place_bid(
&mut self,
bid: Bid,
@ -394,6 +404,7 @@ impl AuctionData {
PriceFloor::MinimumPrice(min) => min[0],
_ => 0,
};
self.bid_state.place_bid(
bid,
tick_size,
@ -401,7 +412,11 @@ impl AuctionData {
minimum,
instant_sale_price,
&mut self.state,
)
)?;
self.consider_instant_bid(instant_sale_price);
Ok(())
}
}
@ -590,17 +605,6 @@ impl BidState {
}
}
// Check if all the lots were sold with instant_sale_price
if let Some(instant_sale_amount) = instant_sale_price {
// bids.len() - max = index of the last winner bid
if bids.len() >= *max
&& bids[bids.len() - *max].1 >= instant_sale_amount
{
msg!("All the lots were sold with instant_sale_price, auction is ended");
*auction_state = AuctionState::Ended;
}
}
let max_size = BidState::max_array_size_for(*max);
if bids.len() > max_size {
@ -702,6 +706,17 @@ impl BidState {
BidState::OpenEdition { bids, max } => None,
}
}
pub fn has_instant_bid(&self, instant_sale_amount: u64) -> bool {
match self {
// In a capped auction, track the limited number of winners.
BidState::EnglishAuction { bids, max } | BidState::OpenEdition { bids, max } => {
// bids.len() - max = index of the last winner bid
bids.len() >= *max && bids[bids.len() - *max].1 >= instant_sale_amount
}
_ => false,
}
}
}
#[repr(C)]

View File

@ -132,36 +132,34 @@ pub fn claim_bid(
return Err(AuctionError::InvalidState.into());
}
let mut auction_extended: Option<AuctionDataExtended> = None;
if let Some(auction_extended_info) = accounts.auction_extended {
let instant_sale_price = accounts.auction_extended.and_then(|info| {
assert_derivation(
program_id,
auction_extended_info,
info,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
args.resource.as_ref(),
EXTENDED.as_bytes(),
],
)?;
let auction_extended_data = AuctionDataExtended::from_account_info(auction_extended_info)?;
// add this check because in this instruction we use AuctionDataExtended only for instant_sale_price
if auction_extended_data.instant_sale_price.is_some() {
auction_extended = Some(auction_extended_data);
}
}
)
.ok()?;
AuctionDataExtended::from_account_info(info)
.ok()?
.instant_sale_price
});
// Auction either must have ended or bidder pay instant_sale_price
if !auction.ended(clock.unix_timestamp)? {
if let Some(auction_extended_data) = auction_extended {
// we can safely unwrap instant_sale_price because we checked it in if let instruction before
if auction.bid_state.amount(bid_index.unwrap())
< auction_extended_data.instant_sale_price.unwrap()
match instant_sale_price {
Some(instant_sale_price)
if auction.bid_state.amount(bid_index.unwrap()) < instant_sale_price =>
{
return Err(AuctionError::InvalidState.into());
return Err(AuctionError::InvalidState.into())
}
} else {
return Err(AuctionError::InvalidState.into());
None => return Err(AuctionError::InvalidState.into()),
_ => (),
}
}

View File

@ -59,7 +59,7 @@ pub enum MetaplexInstruction {
/// 4. `[signer]` Payer
/// 5. `[]` Accept payment account of same token mint as the auction for taking payment for open editions, owner should be auction manager key
/// 6. `[]` Store that this auction manager will belong to
/// 7. `[]` System sysvar
/// 7. `[]` System sysvar
/// 8. `[]` Rent sysvar
InitAuctionManager(AuctionManagerSettings),
@ -123,7 +123,7 @@ pub enum MetaplexInstruction {
/// 18. `[optional/writable]` Master edition (if Printing type of WinningConfig)
/// 19. `[optional/writable]` Reservation list PDA ['metadata', program id, master edition key, 'reservation', auction manager key]
/// relative to token metadata program (if Printing type of WinningConfig)
/// 20. `[]` Auction extended
/// 20. `[]` Auction extended (pda relative to auction of ['auction', program id, vault key, 'extended'])
DeprecatedRedeemBid,
/// Note: This requires that auction manager be in a Running state.
@ -160,7 +160,7 @@ pub enum MetaplexInstruction {
/// after this transaction. Otherwise this account will be ignored.
/// 19. `[]` PDA-based Transfer authority to move the tokens from the store to the destination seed ['vault', program_id, vault key]
/// but please note that this is a PDA relative to the Token Vault program, with the 'vault' prefix
/// 20. `[]` Auction extended
/// 20. `[]` Auction extended (pda relative to auction of ['auction', program id, vault key, 'extended'])
RedeemFullRightsTransferBid,
/// Note: This requires that auction manager be in a Running state.
@ -198,7 +198,7 @@ pub enum MetaplexInstruction {
/// 18. `[writable]` The accept payment account for the auction manager
/// 19. `[writable]` The token account you will potentially pay for the open edition bid with if necessary
/// 20. `[writable]` Participation NFT printing holding account (present on participation_state)
/// 21. `[]` Auction extended
/// 21. `[]` Auction extended (pda relative to auction of ['auction', program id, vault key, 'extended'])
DeprecatedRedeemParticipationBid,
/// If the auction manager is in Validated state, it can invoke the start command via calling this command here.
@ -410,7 +410,7 @@ pub enum MetaplexInstruction {
/// where edition_number is NOT the edition number you pass in args but actually edition_number = floor(edition/EDITION_MARKER_BIT_SIZE). PDA is relative to token metadata.
/// 23. `[signer]` Mint authority of new mint - THIS WILL TRANSFER AUTHORITY AWAY FROM THIS KEY
/// 24. `[]` Metadata account of token in vault
/// 25. `[]` Auction extended
/// 25. `[]` Auction extended (pda relative to auction of ['auction', program id, vault key, 'extended'])
RedeemPrintingV2Bid(RedeemPrintingV2BidArgs),
/// Permissionless call to redeem the master edition in a given safety deposit for a PrintingV2 winning config to the
@ -824,6 +824,7 @@ pub fn create_set_store_instruction(
}
}
#[allow(clippy::too_many_arguments)]
pub fn create_deprecated_populate_participation_printing_account_instruction(
program_id: Pubkey,
safety_deposit_token_store: Pubkey,

View File

@ -1,4 +1,5 @@
use solana_program::log::sol_log_compute_units;
use spl_auction::processor::BidderMetadata;
use {
crate::{
@ -176,6 +177,39 @@ pub fn assert_authority_correct(
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)]
@ -605,21 +639,12 @@ pub fn common_redeem_checks(
return Err(MetaplexError::AuctionManagerTokenMetadataProgramMismatch.into());
}
if AuctionData::get_state(auction_info)? != AuctionState::Ended {
if win_index.is_some() && auction_extended_info.is_some() {
if let Some(instant_sale_price) = AuctionDataExtended::get_instant_sale_price(&auction_extended_info.unwrap().data.borrow()) {
// we can safely do unwrap here such as existing win_index proves that we can get winner_bid_price
let winner_bid_price = AuctionData::get_winner_bid_amount_at(auction_info, win_index.unwrap()).unwrap();
if winner_bid_price < instant_sale_price {
return Err(MetaplexError::AuctionHasNotEnded.into());
}
} else {
return Err(MetaplexError::AuctionHasNotEnded.into());
}
} else {
return Err(MetaplexError::AuctionHasNotEnded.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;