diff --git a/.editorconfig b/.editorconfig index 3dce414..6a906e5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,4 +6,7 @@ indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true -trim_trailing_whitespace = true \ No newline at end of file +trim_trailing_whitespace = true + +[*.rs] +indent_size = 4 diff --git a/js/packages/common/src/actions/auction.ts b/js/packages/common/src/actions/auction.ts index ee30828..bee58f5 100644 --- a/js/packages/common/src/actions/auction.ts +++ b/js/packages/common/src/actions/auction.ts @@ -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, diff --git a/rust/auction/program/src/instruction.rs b/rust/auction/program/src/instruction.rs index 8c79a79..bad5993 100644 --- a/rust/auction/program/src/instruction.rs +++ b/rust/auction/program/src/instruction.rs @@ -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. diff --git a/rust/auction/program/src/processor.rs b/rust/auction/program/src/processor.rs index a9a4dd8..286cfb8 100644 --- a/rust/auction/program/src/processor.rs +++ b/rust/auction/program/src/processor.rs @@ -370,6 +370,16 @@ impl AuctionData { self.bid_state.winner_at(idx) } + pub fn consider_instant_bid(&mut self, instant_sale_price: Option) { + // 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)] diff --git a/rust/auction/program/src/processor/claim_bid.rs b/rust/auction/program/src/processor/claim_bid.rs index 50bf852..a9865e2 100755 --- a/rust/auction/program/src/processor/claim_bid.rs +++ b/rust/auction/program/src/processor/claim_bid.rs @@ -132,36 +132,34 @@ pub fn claim_bid( return Err(AuctionError::InvalidState.into()); } - let mut auction_extended: Option = 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()), + _ => (), } } diff --git a/rust/metaplex/program/src/instruction.rs b/rust/metaplex/program/src/instruction.rs index 85fcb76..6d0e95b 100644 --- a/rust/metaplex/program/src/instruction.rs +++ b/rust/metaplex/program/src/instruction.rs @@ -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, diff --git a/rust/metaplex/program/src/utils.rs b/rust/metaplex/program/src/utils.rs index 09a2d94..1269872 100644 --- a/rust/metaplex/program/src/utils.rs +++ b/rust/metaplex/program/src/utils.rs @@ -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, +) -> 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;