1use crate::errors::AuctionError;
2use arrayref::array_ref;
3use borsh::{BorshDeserialize, BorshSerialize};
4use solana_program::{
5 account_info::AccountInfo, borsh::try_from_slice_unchecked, clock::UnixTimestamp,
6 entrypoint::ProgramResult, hash::Hash, msg, program_error::ProgramError, pubkey::Pubkey,
7};
8use std::{cell::Ref, cmp, mem};
9
10pub mod cancel_bid;
12pub mod claim_bid;
13pub mod create_auction;
14pub mod create_auction_v2;
15pub mod end_auction;
16pub mod place_bid;
17pub mod set_authority;
18pub mod start_auction;
19
20pub use cancel_bid::*;
22pub use claim_bid::*;
23pub use create_auction::*;
24pub use create_auction_v2::*;
25pub use end_auction::*;
26pub use place_bid::*;
27pub use set_authority::*;
28pub use start_auction::*;
29
30pub fn process_instruction(
31 program_id: &Pubkey,
32 accounts: &[AccountInfo],
33 input: &[u8],
34) -> ProgramResult {
35 use crate::instruction::AuctionInstruction;
36 match AuctionInstruction::try_from_slice(input)? {
37 AuctionInstruction::CancelBid(args) => cancel_bid(program_id, accounts, args),
38 AuctionInstruction::ClaimBid(args) => claim_bid(program_id, accounts, args),
39 AuctionInstruction::CreateAuction(args) => {
40 create_auction(program_id, accounts, args, None, None)
41 }
42 AuctionInstruction::CreateAuctionV2(args) => create_auction_v2(program_id, accounts, args),
43 AuctionInstruction::EndAuction(args) => end_auction(program_id, accounts, args),
44 AuctionInstruction::PlaceBid(args) => place_bid(program_id, accounts, args),
45 AuctionInstruction::SetAuthority => set_authority(program_id, accounts),
46 AuctionInstruction::StartAuction(args) => start_auction(program_id, accounts, args),
47 }
48}
49
50#[repr(C)]
52#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
53pub enum PriceFloor {
54 None([u8; 32]),
58 MinimumPrice([u64; 4]),
60 BlindedPrice(Hash),
62}
63
64pub const BASE_AUCTION_DATA_SIZE: usize = 32 + 32 + 9 + 9 + 9 + 9 + 1 + 32 + 1 + 8 + 8 + 8;
69pub const BID_LENGTH: usize = 32 + 8;
70#[repr(C)]
71#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
72pub struct AuctionData {
73 pub authority: Pubkey,
75 pub token_mint: Pubkey,
82 pub last_bid: Option<UnixTimestamp>,
84 pub ended_at: Option<UnixTimestamp>,
86 pub end_auction_at: Option<UnixTimestamp>,
88 pub end_auction_gap: Option<UnixTimestamp>,
90 pub price_floor: PriceFloor,
92 pub state: AuctionState,
94 pub bid_state: BidState,
96}
97
98pub type AuctionName = [u8; 32];
100
101pub const MAX_AUCTION_DATA_EXTENDED_SIZE: usize = 8 + 9 + 2 + 9 + 33 + 158;
102#[repr(C)]
106#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
107pub struct AuctionDataExtended {
108 pub total_uncancelled_bids: u64,
110 pub tick_size: Option<u64>,
113 pub gap_tick_size_percentage: Option<u8>,
115 pub instant_sale_price: Option<u64>,
117 pub name: Option<AuctionName>,
119}
120
121impl AuctionDataExtended {
122 pub fn from_account_info(a: &AccountInfo) -> Result<AuctionDataExtended, ProgramError> {
123 if a.data_len() != MAX_AUCTION_DATA_EXTENDED_SIZE {
124 return Err(AuctionError::DataTypeMismatch.into());
125 }
126
127 let auction_extended: AuctionDataExtended = try_from_slice_unchecked(&a.data.borrow_mut())?;
128
129 Ok(auction_extended)
130 }
131
132 pub fn get_instant_sale_price<'a>(data: &'a Ref<'a, &'a mut [u8]>) -> Option<u64> {
133 if let Some(idx) = Self::find_instant_sale_beginning(data) {
134 Some(u64::from_le_bytes(*array_ref![data, idx, 8]))
135 } else {
136 None
137 }
138 }
139
140 fn find_instant_sale_beginning<'a>(data: &'a Ref<'a, &'a mut [u8]>) -> Option<usize> {
141 let mut instant_sale_beginning = 8;
143
144 let gaps = [9, 2];
146
147 for gap in gaps.iter() {
148 if data[instant_sale_beginning] == 1 {
149 instant_sale_beginning += gap;
150 } else {
151 instant_sale_beginning += 1;
152 }
153 }
154
155 if data[instant_sale_beginning] == 1 {
157 Some(instant_sale_beginning + 1)
158 } else {
159 None
160 }
161 }
162}
163
164impl AuctionData {
165 pub fn get_token_mint(a: &AccountInfo) -> Pubkey {
168 let data = a.data.borrow();
169 let token_mint_data = array_ref![data, 32, 32];
170 Pubkey::new_from_array(*token_mint_data)
171 }
172
173 pub fn get_state(a: &AccountInfo) -> Result<AuctionState, ProgramError> {
174 let bid_state_beginning = AuctionData::find_bid_state_beginning(a) - 1 - 4 - 1;
177 match a.data.borrow()[bid_state_beginning] {
178 0 => Ok(AuctionState::Created),
179 1 => Ok(AuctionState::Started),
180 2 => Ok(AuctionState::Ended),
181 _ => Err(ProgramError::InvalidAccountData),
182 }
183 }
184
185 pub fn get_num_winners(a: &AccountInfo) -> usize {
186 let (bid_state_beginning, num_elements, max) = AuctionData::get_vec_info(a);
187 std::cmp::min(num_elements, max)
188 }
189
190 fn find_bid_state_beginning(a: &AccountInfo) -> usize {
191 let data = a.data.borrow();
192 let mut bid_state_beginning = 32 + 32;
193
194 for i in 0..4 {
195 if data[bid_state_beginning] == 1 {
197 bid_state_beginning += 9
198 } else {
199 bid_state_beginning += 1;
200 }
201 }
202
203 bid_state_beginning += 1 + 32 + 1 + 4 + 1;
206 return bid_state_beginning;
207 }
208
209 fn get_vec_info(a: &AccountInfo) -> (usize, usize, usize) {
210 let bid_state_beginning = AuctionData::find_bid_state_beginning(a);
211 let data = a.data.borrow();
212
213 let num_elements_data = array_ref![data, bid_state_beginning - 4, 4];
214 let num_elements = u32::from_le_bytes(*num_elements_data) as usize;
215 let max_data = array_ref![data, bid_state_beginning + BID_LENGTH * num_elements, 8];
216 let max = u64::from_le_bytes(*max_data) as usize;
217
218 (bid_state_beginning, num_elements, max)
219 }
220
221 pub fn get_is_winner(a: &AccountInfo, key: &Pubkey) -> Option<usize> {
222 let bid_state_beginning = AuctionData::find_bid_state_beginning(a);
223 let data = a.data.borrow();
224 let as_bytes = key.to_bytes();
225 let (bid_state_beginning, num_elements, max) = AuctionData::get_vec_info(a);
226 for idx in 0..std::cmp::min(num_elements, max) {
227 match AuctionData::get_winner_at_inner(
228 &a.data.borrow(),
229 idx,
230 bid_state_beginning,
231 num_elements,
232 max,
233 ) {
234 Some(bid_key) => {
235 let mut matching = true;
238 for bid_key_idx in 0..32 {
239 if bid_key[bid_key_idx] != as_bytes[bid_key_idx] {
240 matching = false;
241 break;
242 }
243 }
244 if matching {
245 return Some(idx as usize);
246 }
247 }
248 None => return None,
249 }
250 }
251 None
252 }
253
254 pub fn get_winner_at(a: &AccountInfo, idx: usize) -> Option<Pubkey> {
255 let (bid_state_beginning, num_elements, max) = AuctionData::get_vec_info(a);
256 match AuctionData::get_winner_at_inner(
257 &a.data.borrow(),
258 idx,
259 bid_state_beginning,
260 num_elements,
261 max,
262 ) {
263 Some(bid_key) => Some(Pubkey::new_from_array(*bid_key)),
264 None => None,
265 }
266 }
267
268 fn get_winner_at_inner<'a>(
269 data: &'a Ref<'a, &'a mut [u8]>,
270 idx: usize,
271 bid_state_beginning: usize,
272 num_elements: usize,
273 max: usize,
274 ) -> Option<&'a [u8; 32]> {
275 if idx + 1 > num_elements || idx + 1 > max {
276 return None;
277 }
278 Some(array_ref![
279 data,
280 bid_state_beginning + (num_elements - idx - 1) * BID_LENGTH,
281 32
282 ])
283 }
284
285 pub fn get_winner_bid_amount_at(a: &AccountInfo, idx: usize) -> Option<u64> {
286 let (bid_state_beginning, num_elements, max) = AuctionData::get_vec_info(a);
287 match AuctionData::get_winner_bid_amount_at_inner(
288 &a.data.borrow(),
289 idx,
290 bid_state_beginning,
291 num_elements,
292 max,
293 ) {
294 Some(bid_amount) => Some(bid_amount),
295 None => None,
296 }
297 }
298
299 fn get_winner_bid_amount_at_inner<'a>(
300 data: &'a Ref<'a, &'a mut [u8]>,
301 idx: usize,
302 bid_state_beginning: usize,
303 num_elements: usize,
304 max: usize,
305 ) -> Option<u64> {
306 if idx + 1 > num_elements || idx + 1 > max {
307 return None;
308 }
309 Some(u64::from_le_bytes(*array_ref![
310 data,
311 bid_state_beginning + (num_elements - idx - 1) * BID_LENGTH + 32,
312 8
313 ]))
314 }
315
316 pub fn from_account_info(a: &AccountInfo) -> Result<AuctionData, ProgramError> {
317 if (a.data_len() - BASE_AUCTION_DATA_SIZE) % mem::size_of::<Bid>() != 0 {
318 return Err(AuctionError::DataTypeMismatch.into());
319 }
320
321 let auction: AuctionData = try_from_slice_unchecked(&a.data.borrow_mut())?;
322
323 Ok(auction)
324 }
325
326 pub fn ended(&self, now: UnixTimestamp) -> Result<bool, ProgramError> {
327 return match (self.ended_at, self.end_auction_gap) {
329 (Some(end), Some(gap)) => {
333 if let Some(last) = self.last_bid {
335 let next_bid_time = last
336 .checked_add(gap)
337 .ok_or(AuctionError::NumericalOverflowError)?;
338
339 Ok(now > end && now > next_bid_time)
340 } else {
341 Ok(now > end)
342 }
343 }
344
345 (Some(end), None) => Ok(now > end),
347
348 _ => Ok(false),
350 };
351 }
352
353 pub fn is_winner(&self, key: &Pubkey) -> Option<usize> {
354 let minimum = match self.price_floor {
355 PriceFloor::MinimumPrice(min) => min[0],
356 _ => 0,
357 };
358 self.bid_state.is_winner(key, minimum)
359 }
360
361 pub fn num_winners(&self) -> u64 {
362 self.bid_state.num_winners()
363 }
364
365 pub fn num_possible_winners(&self) -> u64 {
366 self.bid_state.num_possible_winners()
367 }
368
369 pub fn winner_at(&self, idx: usize) -> Option<Pubkey> {
370 self.bid_state.winner_at(idx)
371 }
372
373 pub fn consider_instant_bid(&mut self, instant_sale_price: Option<u64>) {
374 if let Some(price) = instant_sale_price {
376 if self
377 .bid_state
378 .lowest_winning_bid_is_instant_bid_price(price)
379 {
380 msg!("All the lots were sold with instant_sale_price, auction is ended");
381 self.state = AuctionState::Ended;
382 }
383 }
384 }
385
386 pub fn place_bid(
387 &mut self,
388 bid: Bid,
389 tick_size: Option<u64>,
390 gap_tick_size_percentage: Option<u8>,
391 now: UnixTimestamp,
392 instant_sale_price: Option<u64>,
393 ) -> Result<(), ProgramError> {
394 let gap_val = match self.ended_at {
395 Some(end) => {
396 if now > end {
399 gap_tick_size_percentage
400 } else {
401 None
402 }
403 }
404 None => None,
405 };
406 let minimum = match self.price_floor {
407 PriceFloor::MinimumPrice(min) => min[0],
408 _ => 0,
409 };
410
411 self.bid_state.place_bid(
412 bid,
413 tick_size,
414 gap_val,
415 minimum,
416 instant_sale_price,
417 &mut self.state,
418 )?;
419
420 self.consider_instant_bid(instant_sale_price);
421
422 Ok(())
423 }
424}
425
426#[repr(C)]
428#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
429pub enum AuctionState {
430 Created,
431 Started,
432 Ended,
433}
434
435impl AuctionState {
436 pub fn create() -> Self {
437 AuctionState::Created
438 }
439
440 #[inline(always)]
441 pub fn start(self) -> Result<Self, ProgramError> {
442 match self {
443 AuctionState::Created => Ok(AuctionState::Started),
444 _ => Err(AuctionError::AuctionTransitionInvalid.into()),
445 }
446 }
447
448 #[inline(always)]
449 pub fn end(self) -> Result<Self, ProgramError> {
450 match self {
451 AuctionState::Started => Ok(AuctionState::Ended),
452 AuctionState::Created => Ok(AuctionState::Ended),
453 _ => Err(AuctionError::AuctionTransitionInvalid.into()),
454 }
455 }
456}
457
458#[repr(C)]
460#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
461pub struct Bid(pub Pubkey, pub u64);
462
463#[repr(C)]
466#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
467pub enum BidState {
468 EnglishAuction { bids: Vec<Bid>, max: usize },
469 OpenEdition { bids: Vec<Bid>, max: usize },
470}
471
472impl BidState {
480 pub fn new_english(n: usize) -> Self {
481 BidState::EnglishAuction {
482 bids: vec![],
483 max: n,
484 }
485 }
486
487 pub fn new_open_edition() -> Self {
488 BidState::OpenEdition {
489 bids: vec![],
490 max: 0,
491 }
492 }
493
494 pub fn max_array_size_for(n: usize) -> usize {
495 let mut real_max = n;
496 if real_max < 8 {
497 real_max = 8;
498 } else {
499 real_max = 2 * real_max
500 }
501 real_max
502 }
503
504 fn assert_valid_tick_size_bid(bid: &Bid, tick_size: Option<u64>) -> ProgramResult {
505 if let Some(tick) = tick_size {
506 if bid.1.checked_rem(tick) != Some(0) {
507 msg!(
508 "This bid {:?} is not a multiple of tick size {:?}, throw it out.",
509 bid.1,
510 tick_size
511 );
512 return Err(AuctionError::BidMustBeMultipleOfTickSize.into());
513 }
514 } else {
515 msg!("No tick size on this auction")
516 }
517
518 Ok(())
519 }
520
521 fn assert_valid_gap_insertion(
522 gap_tick: u8,
523 beaten_bid: &Bid,
524 beating_bid: &Bid,
525 ) -> ProgramResult {
526 let mut minimum_bid_amount: u128 = (beaten_bid.1 as u128)
529 .checked_mul((100 + gap_tick) as u128)
530 .ok_or(AuctionError::NumericalOverflowError)?;
531 minimum_bid_amount = minimum_bid_amount
532 .checked_div(100u128)
533 .ok_or(AuctionError::NumericalOverflowError)?;
534
535 if minimum_bid_amount > beating_bid.1 as u128 {
536 msg!("Rejecting inserting this bid due to gap tick size of {:?} which causes min bid of {:?} from {:?} which is the bid it is trying to beat", gap_tick, minimum_bid_amount.to_string(), beaten_bid.1);
537 return Err(AuctionError::GapBetweenBidsTooSmall.into());
538 }
539
540 Ok(())
541 }
542
543 pub fn place_bid(
546 &mut self,
547 bid: Bid,
548 tick_size: Option<u64>,
549 gap_tick_size_percentage: Option<u8>,
550 minimum: u64,
551 instant_sale_price: Option<u64>,
552 auction_state: &mut AuctionState,
553 ) -> Result<(), ProgramError> {
554 msg!("Placing bid {:?}", &bid.1.to_string());
555 BidState::assert_valid_tick_size_bid(&bid, tick_size)?;
556 if bid.1 < minimum {
557 return Err(AuctionError::BidTooSmall.into());
558 }
559
560 match self {
561 BidState::EnglishAuction { ref mut bids, max } => {
563 match bids.last() {
564 Some(top) => {
565 msg!("Looking to go over the loop, but check tick size first");
566
567 for i in (0..bids.len()).rev() {
568 msg!("Comparison of {:?} and {:?} for {:?}", bids[i].1, bid.1, i);
569 if bids[i].1 < bid.1 {
570 if let Some(gap_tick) = gap_tick_size_percentage {
571 BidState::assert_valid_gap_insertion(gap_tick, &bids[i], &bid)?
572 }
573
574 msg!("Ok we can do an insert");
575 if i + 1 < bids.len() {
576 msg!("Doing a normal insert");
577 bids.insert(i + 1, bid);
578 } else {
579 msg!("Doing an on the end insert");
580 bids.push(bid)
581 }
582 break;
583 } else if bids[i].1 == bid.1 {
584 if let Some(gap_tick) = gap_tick_size_percentage {
585 if gap_tick > 0 {
586 msg!("Rejecting same-bid insert due to gap tick size of {:?}", gap_tick);
587 return Err(AuctionError::GapBetweenBidsTooSmall.into());
588 }
589 }
590
591 msg!("Ok we can do an equivalent insert");
592 if i == 0 {
593 msg!("Doing a normal insert");
594 bids.insert(0, bid);
595 break;
596 } else {
597 if bids[i - 1].1 != bids[i].1 {
598 msg!("Doing an insert just before");
599 bids.insert(i, bid);
600 break;
601 }
602 msg!("More duplicates ahead...")
603 }
604 } else if i == 0 {
605 msg!("Inserting at 0");
606 bids.insert(0, bid);
607 break;
608 }
609 }
610
611 let max_size = BidState::max_array_size_for(*max);
612
613 if bids.len() > max_size {
614 bids.remove(0);
615 }
616 Ok(())
617 }
618 _ => {
619 msg!("Pushing bid onto stack");
620 bids.push(bid);
621 Ok(())
622 }
623 }
624 }
625
626 BidState::OpenEdition { bids, max } => Ok(()),
628 }
629 }
630
631 pub fn cancel_bid(&mut self, key: Pubkey) -> Result<(), ProgramError> {
634 match self {
635 BidState::EnglishAuction { ref mut bids, max } => {
636 bids.retain(|b| b.0 != key);
637 Ok(())
638 }
639
640 BidState::OpenEdition { bids, max } => Ok(()),
643 }
644 }
645
646 pub fn amount(&self, index: usize) -> u64 {
647 match self {
648 BidState::EnglishAuction { bids, max } => {
649 if index >= 0 as usize && index < bids.len() {
650 return bids[bids.len() - index - 1].1;
651 } else {
652 return 0;
653 }
654 }
655 BidState::OpenEdition { bids, max } => 0,
656 }
657 }
658
659 pub fn is_winner(&self, key: &Pubkey, min: u64) -> Option<usize> {
661 match self {
664 BidState::EnglishAuction { bids, max } => {
666 match bids.iter().position(|bid| &bid.0 == key && bid.1 >= min) {
667 Some(val) => {
668 let zero_based_index = bids.len() - val - 1;
669 if zero_based_index < *max {
670 Some(zero_based_index)
671 } else {
672 None
673 }
674 }
675 None => None,
676 }
677 }
678 BidState::OpenEdition { bids, max } => None,
681 }
682 }
683
684 pub fn num_winners(&self) -> u64 {
685 match self {
686 BidState::EnglishAuction { bids, max } => cmp::min(bids.len(), *max) as u64,
687 BidState::OpenEdition { bids, max } => 0,
688 }
689 }
690
691 pub fn num_possible_winners(&self) -> u64 {
692 match self {
693 BidState::EnglishAuction { bids, max } => *max as u64,
694 BidState::OpenEdition { bids, max } => 0,
695 }
696 }
697
698 pub fn winner_at(&self, index: usize) -> Option<Pubkey> {
700 match self {
701 BidState::EnglishAuction { bids, max } => {
702 if index < *max && index < bids.len() {
703 let bid = &bids[bids.len() - index - 1];
704 Some(bids[bids.len() - index - 1].0)
705 } else {
706 None
707 }
708 }
709 BidState::OpenEdition { bids, max } => None,
710 }
711 }
712
713 pub fn lowest_winning_bid_is_instant_bid_price(&self, instant_sale_amount: u64) -> bool {
714 match self {
715 BidState::EnglishAuction { bids, max } => {
717 bids.len() >= *max && bids[bids.len() - *max].1 >= instant_sale_amount
719 }
720 _ => false,
721 }
722 }
723}
724
725#[repr(C)]
726#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
727pub enum WinnerLimit {
728 Unlimited(usize),
729 Capped(usize),
730}
731
732pub const BIDDER_METADATA_LEN: usize = 32 + 32 + 8 + 8 + 1;
733#[repr(C)]
736#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
737pub struct BidderMetadata {
738 pub bidder_pubkey: Pubkey,
740 pub auction_pubkey: Pubkey,
742 pub last_bid: u64,
744 pub last_bid_timestamp: UnixTimestamp,
746 pub cancelled: bool,
749}
750
751impl BidderMetadata {
752 pub fn from_account_info(a: &AccountInfo) -> Result<BidderMetadata, ProgramError> {
753 if a.data_len() != BIDDER_METADATA_LEN {
754 return Err(AuctionError::DataTypeMismatch.into());
755 }
756
757 let bidder_meta: BidderMetadata = try_from_slice_unchecked(&a.data.borrow_mut())?;
758
759 Ok(bidder_meta)
760 }
761}
762
763#[repr(C)]
764#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq)]
765pub struct BidderPot {
766 pub bidder_pot: Pubkey,
768 pub bidder_act: Pubkey,
770 pub auction_act: Pubkey,
772 pub emptied: bool,
774}
775
776impl BidderPot {
777 pub fn from_account_info(a: &AccountInfo) -> Result<BidderPot, ProgramError> {
778 if a.data_len() != mem::size_of::<BidderPot>() {
779 return Err(AuctionError::DataTypeMismatch.into());
780 }
781
782 let bidder_pot: BidderPot = try_from_slice_unchecked(&a.data.borrow_mut())?;
783
784 Ok(bidder_pot)
785 }
786}