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;