1use crate::{
22	slot_range::SlotRange,
23	traits::{AuctionStatus, Auctioneer, LeaseError, Leaser, Registrar},
24};
25use alloc::{vec, vec::Vec};
26use codec::Decode;
27use core::mem::swap;
28use frame_support::{
29	dispatch::DispatchResult,
30	ensure,
31	traits::{Currency, Get, Randomness, ReservableCurrency},
32	weights::Weight,
33};
34use frame_system::pallet_prelude::BlockNumberFor;
35pub use pallet::*;
36use polkadot_primitives::Id as ParaId;
37use sp_runtime::traits::{CheckedSub, One, Saturating, Zero};
38
39type CurrencyOf<T> = <<T as Config>::Leaser as Leaser<BlockNumberFor<T>>>::Currency;
40type BalanceOf<T> = <<<T as Config>::Leaser as Leaser<BlockNumberFor<T>>>::Currency as Currency<
41	<T as frame_system::Config>::AccountId,
42>>::Balance;
43
44pub trait WeightInfo {
45	fn new_auction() -> Weight;
46	fn bid() -> Weight;
47	fn cancel_auction() -> Weight;
48	fn on_initialize() -> Weight;
49}
50
51pub struct TestWeightInfo;
52impl WeightInfo for TestWeightInfo {
53	fn new_auction() -> Weight {
54		Weight::zero()
55	}
56	fn bid() -> Weight {
57		Weight::zero()
58	}
59	fn cancel_auction() -> Weight {
60		Weight::zero()
61	}
62	fn on_initialize() -> Weight {
63		Weight::zero()
64	}
65}
66
67pub type AuctionIndex = u32;
69
70type LeasePeriodOf<T> = <<T as Config>::Leaser as Leaser<BlockNumberFor<T>>>::LeasePeriod;
71
72type WinningData<T> = [Option<(<T as frame_system::Config>::AccountId, ParaId, BalanceOf<T>)>;
74	SlotRange::SLOT_RANGE_COUNT];
75type WinnersData<T> =
78	Vec<(<T as frame_system::Config>::AccountId, ParaId, BalanceOf<T>, SlotRange)>;
79
80#[frame_support::pallet]
81pub mod pallet {
82	use super::*;
83	use frame_support::{dispatch::DispatchClass, pallet_prelude::*, traits::EnsureOrigin};
84	use frame_system::{ensure_root, ensure_signed, pallet_prelude::*};
85
86	#[pallet::pallet]
87	pub struct Pallet<T>(_);
88
89	#[pallet::config]
91	pub trait Config: frame_system::Config {
92		#[allow(deprecated)]
94		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
95
96		type Leaser: Leaser<
98			BlockNumberFor<Self>,
99			AccountId = Self::AccountId,
100			LeasePeriod = BlockNumberFor<Self>,
101		>;
102
103		type Registrar: Registrar<AccountId = Self::AccountId>;
105
106		#[pallet::constant]
108		type EndingPeriod: Get<BlockNumberFor<Self>>;
109
110		#[pallet::constant]
114		type SampleLength: Get<BlockNumberFor<Self>>;
115
116		type Randomness: Randomness<Self::Hash, BlockNumberFor<Self>>;
118
119		type InitiateOrigin: EnsureOrigin<Self::RuntimeOrigin>;
121
122		type WeightInfo: WeightInfo;
124	}
125
126	#[pallet::event]
127	#[pallet::generate_deposit(pub(super) fn deposit_event)]
128	pub enum Event<T: Config> {
129		AuctionStarted {
132			auction_index: AuctionIndex,
133			lease_period: LeasePeriodOf<T>,
134			ending: BlockNumberFor<T>,
135		},
136		AuctionClosed { auction_index: AuctionIndex },
138		Reserved { bidder: T::AccountId, extra_reserved: BalanceOf<T>, total_amount: BalanceOf<T> },
141		Unreserved { bidder: T::AccountId, amount: BalanceOf<T> },
143		ReserveConfiscated { para_id: ParaId, leaser: T::AccountId, amount: BalanceOf<T> },
146		BidAccepted {
148			bidder: T::AccountId,
149			para_id: ParaId,
150			amount: BalanceOf<T>,
151			first_slot: LeasePeriodOf<T>,
152			last_slot: LeasePeriodOf<T>,
153		},
154		WinningOffset { auction_index: AuctionIndex, block_number: BlockNumberFor<T> },
157	}
158
159	#[pallet::error]
160	pub enum Error<T> {
161		AuctionInProgress,
163		LeasePeriodInPast,
165		ParaNotRegistered,
167		NotCurrentAuction,
169		NotAuction,
171		AuctionEnded,
173		AlreadyLeasedOut,
175	}
176
177	#[pallet::storage]
179	pub type AuctionCounter<T> = StorageValue<_, AuctionIndex, ValueQuery>;
180
181	#[pallet::storage]
187	pub type AuctionInfo<T: Config> = StorageValue<_, (LeasePeriodOf<T>, BlockNumberFor<T>)>;
188
189	#[pallet::storage]
192	pub type ReservedAmounts<T: Config> =
193		StorageMap<_, Twox64Concat, (T::AccountId, ParaId), BalanceOf<T>>;
194
195	#[pallet::storage]
199	pub type Winning<T: Config> = StorageMap<_, Twox64Concat, BlockNumberFor<T>, WinningData<T>>;
200
201	#[pallet::extra_constants]
202	impl<T: Config> Pallet<T> {
203		#[pallet::constant_name(SlotRangeCount)]
204		fn slot_range_count() -> u32 {
205			SlotRange::SLOT_RANGE_COUNT as u32
206		}
207
208		#[pallet::constant_name(LeasePeriodsPerSlot)]
209		fn lease_periods_per_slot() -> u32 {
210			SlotRange::LEASE_PERIODS_PER_SLOT as u32
211		}
212	}
213
214	#[pallet::hooks]
215	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
216		fn on_initialize(n: BlockNumberFor<T>) -> Weight {
217			let mut weight = T::DbWeight::get().reads(1);
218
219			if let AuctionStatus::EndingPeriod(offset, _sub_sample) = Self::auction_status(n) {
223				weight = weight.saturating_add(T::DbWeight::get().reads(1));
224				if !Winning::<T>::contains_key(&offset) {
225					weight = weight.saturating_add(T::DbWeight::get().writes(1));
226					let winning_data = offset
227						.checked_sub(&One::one())
228						.and_then(Winning::<T>::get)
229						.unwrap_or([Self::EMPTY; SlotRange::SLOT_RANGE_COUNT]);
230					Winning::<T>::insert(offset, winning_data);
231				}
232			}
233
234			if let Some((winning_ranges, auction_lease_period_index)) = Self::check_auction_end(n) {
236				Self::manage_auction_end(auction_lease_period_index, winning_ranges);
239				weight = weight.saturating_add(T::WeightInfo::on_initialize());
240			}
241
242			weight
243		}
244	}
245
246	#[pallet::call]
247	impl<T: Config> Pallet<T> {
248		#[pallet::call_index(0)]
254		#[pallet::weight((T::WeightInfo::new_auction(), DispatchClass::Operational))]
255		pub fn new_auction(
256			origin: OriginFor<T>,
257			#[pallet::compact] duration: BlockNumberFor<T>,
258			#[pallet::compact] lease_period_index: LeasePeriodOf<T>,
259		) -> DispatchResult {
260			T::InitiateOrigin::ensure_origin(origin)?;
261			Self::do_new_auction(duration, lease_period_index)
262		}
263
264		#[pallet::call_index(1)]
281		#[pallet::weight(T::WeightInfo::bid())]
282		pub fn bid(
283			origin: OriginFor<T>,
284			#[pallet::compact] para: ParaId,
285			#[pallet::compact] auction_index: AuctionIndex,
286			#[pallet::compact] first_slot: LeasePeriodOf<T>,
287			#[pallet::compact] last_slot: LeasePeriodOf<T>,
288			#[pallet::compact] amount: BalanceOf<T>,
289		) -> DispatchResult {
290			let who = ensure_signed(origin)?;
291			Self::handle_bid(who, para, auction_index, first_slot, last_slot, amount)?;
292			Ok(())
293		}
294
295		#[pallet::call_index(2)]
299		#[pallet::weight(T::WeightInfo::cancel_auction())]
300		pub fn cancel_auction(origin: OriginFor<T>) -> DispatchResult {
301			ensure_root(origin)?;
302			for ((bidder, _), amount) in ReservedAmounts::<T>::drain() {
304				CurrencyOf::<T>::unreserve(&bidder, amount);
305			}
306			#[allow(deprecated)]
307			Winning::<T>::remove_all(None);
308			AuctionInfo::<T>::kill();
309			Ok(())
310		}
311	}
312}
313
314impl<T: Config> Auctioneer<BlockNumberFor<T>> for Pallet<T> {
315	type AccountId = T::AccountId;
316	type LeasePeriod = BlockNumberFor<T>;
317	type Currency = CurrencyOf<T>;
318
319	fn new_auction(
320		duration: BlockNumberFor<T>,
321		lease_period_index: LeasePeriodOf<T>,
322	) -> DispatchResult {
323		Self::do_new_auction(duration, lease_period_index)
324	}
325
326	fn auction_status(now: BlockNumberFor<T>) -> AuctionStatus<BlockNumberFor<T>> {
328		let early_end = match AuctionInfo::<T>::get() {
329			Some((_, early_end)) => early_end,
330			None => return AuctionStatus::NotStarted,
331		};
332
333		let after_early_end = match now.checked_sub(&early_end) {
334			Some(after_early_end) => after_early_end,
335			None => return AuctionStatus::StartingPeriod,
336		};
337
338		let ending_period = T::EndingPeriod::get();
339		if after_early_end < ending_period {
340			let sample_length = T::SampleLength::get().max(One::one());
341			let sample = after_early_end / sample_length;
342			let sub_sample = after_early_end % sample_length;
343			return AuctionStatus::EndingPeriod(sample, sub_sample)
344		} else {
345			return AuctionStatus::VrfDelay(after_early_end - ending_period)
347		}
348	}
349
350	fn place_bid(
351		bidder: T::AccountId,
352		para: ParaId,
353		first_slot: LeasePeriodOf<T>,
354		last_slot: LeasePeriodOf<T>,
355		amount: BalanceOf<T>,
356	) -> DispatchResult {
357		Self::handle_bid(bidder, para, AuctionCounter::<T>::get(), first_slot, last_slot, amount)
358	}
359
360	fn lease_period_index(b: BlockNumberFor<T>) -> Option<(Self::LeasePeriod, bool)> {
361		T::Leaser::lease_period_index(b)
362	}
363
364	#[cfg(any(feature = "runtime-benchmarks", test))]
365	fn lease_period_length() -> (BlockNumberFor<T>, BlockNumberFor<T>) {
366		T::Leaser::lease_period_length()
367	}
368
369	fn has_won_an_auction(para: ParaId, bidder: &T::AccountId) -> bool {
370		!T::Leaser::deposit_held(para, bidder).is_zero()
371	}
372}
373
374impl<T: Config> Pallet<T> {
375	const EMPTY: Option<(<T as frame_system::Config>::AccountId, ParaId, BalanceOf<T>)> = None;
377
378	fn do_new_auction(
384		duration: BlockNumberFor<T>,
385		lease_period_index: LeasePeriodOf<T>,
386	) -> DispatchResult {
387		let maybe_auction = AuctionInfo::<T>::get();
388		ensure!(maybe_auction.is_none(), Error::<T>::AuctionInProgress);
389		let now = frame_system::Pallet::<T>::block_number();
390		if let Some((current_lease_period, _)) = T::Leaser::lease_period_index(now) {
391			ensure!(lease_period_index >= current_lease_period, Error::<T>::LeasePeriodInPast);
393		}
394
395		let n = AuctionCounter::<T>::mutate(|n| {
397			*n += 1;
398			*n
399		});
400
401		let ending = frame_system::Pallet::<T>::block_number().saturating_add(duration);
403		AuctionInfo::<T>::put((lease_period_index, ending));
404
405		Self::deposit_event(Event::<T>::AuctionStarted {
406			auction_index: n,
407			lease_period: lease_period_index,
408			ending,
409		});
410		Ok(())
411	}
412
413	pub fn handle_bid(
422		bidder: T::AccountId,
423		para: ParaId,
424		auction_index: u32,
425		first_slot: LeasePeriodOf<T>,
426		last_slot: LeasePeriodOf<T>,
427		amount: BalanceOf<T>,
428	) -> DispatchResult {
429		ensure!(T::Registrar::is_registered(para), Error::<T>::ParaNotRegistered);
431		ensure!(auction_index == AuctionCounter::<T>::get(), Error::<T>::NotCurrentAuction);
433		let (first_lease_period, _) = AuctionInfo::<T>::get().ok_or(Error::<T>::NotAuction)?;
435
436		let auction_status = Self::auction_status(frame_system::Pallet::<T>::block_number());
439		let offset = match auction_status {
441			AuctionStatus::NotStarted => return Err(Error::<T>::AuctionEnded.into()),
442			AuctionStatus::StartingPeriod => Zero::zero(),
443			AuctionStatus::EndingPeriod(o, _) => o,
444			AuctionStatus::VrfDelay(_) => return Err(Error::<T>::AuctionEnded.into()),
445		};
446
447		ensure!(
449			!T::Leaser::already_leased(para, first_slot, last_slot),
450			Error::<T>::AlreadyLeasedOut
451		);
452
453		let range = SlotRange::new_bounded(first_lease_period, first_slot, last_slot)?;
455		let range_index = range as u8 as usize;
457
458		let mut current_winning = Winning::<T>::get(offset)
460			.or_else(|| offset.checked_sub(&One::one()).and_then(Winning::<T>::get))
461			.unwrap_or([Self::EMPTY; SlotRange::SLOT_RANGE_COUNT]);
462
463		if current_winning[range_index].as_ref().map_or(true, |last| amount > last.2) {
465			let existing_lease_deposit = T::Leaser::deposit_held(para, &bidder);
470			let reserve_required = amount.saturating_sub(existing_lease_deposit);
471
472			let bidder_para = (bidder.clone(), para);
474			let already_reserved = ReservedAmounts::<T>::get(&bidder_para).unwrap_or_default();
475
476			if let Some(additional) = reserve_required.checked_sub(&already_reserved) {
478				CurrencyOf::<T>::reserve(&bidder, additional)?;
481				ReservedAmounts::<T>::insert(&bidder_para, reserve_required);
483
484				Self::deposit_event(Event::<T>::Reserved {
485					bidder: bidder.clone(),
486					extra_reserved: additional,
487					total_amount: reserve_required,
488				});
489			}
490
491			let mut outgoing_winner = Some((bidder.clone(), para, amount));
494			swap(&mut current_winning[range_index], &mut outgoing_winner);
495			if let Some((who, para, _amount)) = outgoing_winner {
496				if auction_status.is_starting() &&
497					current_winning
498						.iter()
499						.filter_map(Option::as_ref)
500						.all(|&(ref other, other_para, _)| other != &who || other_para != para)
501				{
502					if let Some(amount) = ReservedAmounts::<T>::take(&(who.clone(), para)) {
504						let err_amt = CurrencyOf::<T>::unreserve(&who, amount);
506						debug_assert!(err_amt.is_zero());
507						Self::deposit_event(Event::<T>::Unreserved { bidder: who, amount });
508					}
509				}
510			}
511
512			Winning::<T>::insert(offset, ¤t_winning);
514			Self::deposit_event(Event::<T>::BidAccepted {
515				bidder,
516				para_id: para,
517				amount,
518				first_slot,
519				last_slot,
520			});
521		}
522		Ok(())
523	}
524
525	fn check_auction_end(now: BlockNumberFor<T>) -> Option<(WinningData<T>, LeasePeriodOf<T>)> {
532		if let Some((lease_period_index, early_end)) = AuctionInfo::<T>::get() {
533			let ending_period = T::EndingPeriod::get();
534			let late_end = early_end.saturating_add(ending_period);
535			let is_ended = now >= late_end;
536			if is_ended {
537				let (raw_offset, known_since) = T::Randomness::random(&b"para_auction"[..]);
540
541				if late_end <= known_since {
542					let raw_offset_block_number = <BlockNumberFor<T>>::decode(
544						&mut raw_offset.as_ref(),
545					)
546					.expect("secure hashes should always be bigger than the block number; qed");
547					let offset = (raw_offset_block_number % ending_period) /
548						T::SampleLength::get().max(One::one());
549
550					let auction_counter = AuctionCounter::<T>::get();
551					Self::deposit_event(Event::<T>::WinningOffset {
552						auction_index: auction_counter,
553						block_number: offset,
554					});
555					let res = Winning::<T>::get(offset)
556						.unwrap_or([Self::EMPTY; SlotRange::SLOT_RANGE_COUNT]);
557					#[allow(deprecated)]
561					Winning::<T>::remove_all(None);
562					AuctionInfo::<T>::kill();
563					return Some((res, lease_period_index))
564				}
565			}
566		}
567		None
568	}
569
570	fn manage_auction_end(
574		auction_lease_period_index: LeasePeriodOf<T>,
575		winning_ranges: WinningData<T>,
576	) {
577		for ((bidder, _), amount) in ReservedAmounts::<T>::drain() {
581			CurrencyOf::<T>::unreserve(&bidder, amount);
582		}
583
584		let winners = Self::calculate_winners(winning_ranges);
587
588		for (leaser, para, amount, range) in winners.into_iter() {
591			let begin_offset = LeasePeriodOf::<T>::from(range.as_pair().0 as u32);
592			let period_begin = auction_lease_period_index + begin_offset;
593			let period_count = LeasePeriodOf::<T>::from(range.len() as u32);
594
595			match T::Leaser::lease_out(para, &leaser, amount, period_begin, period_count) {
596				Err(LeaseError::ReserveFailed) |
597				Err(LeaseError::AlreadyEnded) |
598				Err(LeaseError::NoLeasePeriod) => {
599					},
602				Err(LeaseError::AlreadyLeased) => {
603					if CurrencyOf::<T>::reserve(&leaser, amount).is_ok() {
606						Self::deposit_event(Event::<T>::ReserveConfiscated {
607							para_id: para,
608							leaser,
609							amount,
610						});
611					}
612				},
613				Ok(()) => {}, }
615		}
616
617		Self::deposit_event(Event::<T>::AuctionClosed {
618			auction_index: AuctionCounter::<T>::get(),
619		});
620	}
621
622	fn calculate_winners(mut winning: WinningData<T>) -> WinnersData<T> {
627		let winning_ranges = {
628			let mut best_winners_ending_at: [(Vec<SlotRange>, BalanceOf<T>);
629				SlotRange::LEASE_PERIODS_PER_SLOT] = Default::default();
630			let best_bid = |range: SlotRange| {
631				winning[range as u8 as usize]
632					.as_ref()
633					.map(|(_, _, amount)| *amount * (range.len() as u32).into())
634			};
635			for i in 0..SlotRange::LEASE_PERIODS_PER_SLOT {
636				let r = SlotRange::new_bounded(0, 0, i as u32).expect("`i < LPPS`; qed");
637				if let Some(bid) = best_bid(r) {
638					best_winners_ending_at[i] = (vec![r], bid);
639				}
640				for j in 0..i {
641					let r = SlotRange::new_bounded(0, j as u32 + 1, i as u32)
642						.expect("`i < LPPS`; `j < i`; `j + 1 < LPPS`; qed");
643					if let Some(mut bid) = best_bid(r) {
644						bid += best_winners_ending_at[j].1;
645						if bid > best_winners_ending_at[i].1 {
646							let mut new_winners = best_winners_ending_at[j].0.clone();
647							new_winners.push(r);
648							best_winners_ending_at[i] = (new_winners, bid);
649						}
650					} else {
651						if best_winners_ending_at[j].1 > best_winners_ending_at[i].1 {
652							best_winners_ending_at[i] = best_winners_ending_at[j].clone();
653						}
654					}
655				}
656			}
657			best_winners_ending_at[SlotRange::LEASE_PERIODS_PER_SLOT - 1].0.clone()
658		};
659
660		winning_ranges
661			.into_iter()
662			.filter_map(|range| {
663				winning[range as u8 as usize]
664					.take()
665					.map(|(bidder, para, amount)| (bidder, para, amount, range))
666			})
667			.collect::<Vec<_>>()
668	}
669}
670
671#[cfg(test)]
672mod mock;
673
674#[cfg(test)]
675mod tests;
676
677#[cfg(feature = "runtime-benchmarks")]
678mod benchmarking;