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 pezframe_support::{
29 dispatch::DispatchResult,
30 ensure,
31 traits::{Currency, Get, Randomness, ReservableCurrency},
32 weights::Weight,
33};
34use pezframe_system::pezpallet_prelude::BlockNumberFor;
35use pezkuwi_primitives::Id as ParaId;
36pub use pezpallet::*;
37use pezsp_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 pezframe_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 pezframe_system::Config>::AccountId, ParaId, BalanceOf<T>)>;
74 SlotRange::SLOT_RANGE_COUNT];
75type WinnersData<T> =
78 Vec<(<T as pezframe_system::Config>::AccountId, ParaId, BalanceOf<T>, SlotRange)>;
79
80#[pezframe_support::pezpallet]
81pub mod pezpallet {
82 use super::*;
83 use pezframe_support::{dispatch::DispatchClass, pezpallet_prelude::*, traits::EnsureOrigin};
84 use pezframe_system::{ensure_root, ensure_signed, pezpallet_prelude::*};
85
86 #[pezpallet::pezpallet]
87 pub struct Pezpallet<T>(_);
88
89 #[pezpallet::config]
91 pub trait Config: pezframe_system::Config {
92 #[allow(deprecated)]
94 type RuntimeEvent: From<Event<Self>>
95 + IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
96
97 type Leaser: Leaser<
99 BlockNumberFor<Self>,
100 AccountId = Self::AccountId,
101 LeasePeriod = BlockNumberFor<Self>,
102 >;
103
104 type Registrar: Registrar<AccountId = Self::AccountId>;
106
107 #[pezpallet::constant]
109 type EndingPeriod: Get<BlockNumberFor<Self>>;
110
111 #[pezpallet::constant]
115 type SampleLength: Get<BlockNumberFor<Self>>;
116
117 type Randomness: Randomness<Self::Hash, BlockNumberFor<Self>>;
119
120 type InitiateOrigin: EnsureOrigin<Self::RuntimeOrigin>;
122
123 type WeightInfo: WeightInfo;
125 }
126
127 #[pezpallet::event]
128 #[pezpallet::generate_deposit(pub(super) fn deposit_event)]
129 pub enum Event<T: Config> {
130 AuctionStarted {
133 auction_index: AuctionIndex,
134 lease_period: LeasePeriodOf<T>,
135 ending: BlockNumberFor<T>,
136 },
137 AuctionClosed { auction_index: AuctionIndex },
139 Reserved { bidder: T::AccountId, extra_reserved: BalanceOf<T>, total_amount: BalanceOf<T> },
142 Unreserved { bidder: T::AccountId, amount: BalanceOf<T> },
144 ReserveConfiscated { para_id: ParaId, leaser: T::AccountId, amount: BalanceOf<T> },
147 BidAccepted {
149 bidder: T::AccountId,
150 para_id: ParaId,
151 amount: BalanceOf<T>,
152 first_slot: LeasePeriodOf<T>,
153 last_slot: LeasePeriodOf<T>,
154 },
155 WinningOffset { auction_index: AuctionIndex, block_number: BlockNumberFor<T> },
158 }
159
160 #[pezpallet::error]
161 pub enum Error<T> {
162 AuctionInProgress,
164 LeasePeriodInPast,
166 ParaNotRegistered,
168 NotCurrentAuction,
170 NotAuction,
172 AuctionEnded,
174 AlreadyLeasedOut,
176 }
177
178 #[pezpallet::storage]
180 pub type AuctionCounter<T> = StorageValue<_, AuctionIndex, ValueQuery>;
181
182 #[pezpallet::storage]
188 pub type AuctionInfo<T: Config> = StorageValue<_, (LeasePeriodOf<T>, BlockNumberFor<T>)>;
189
190 #[pezpallet::storage]
193 pub type ReservedAmounts<T: Config> =
194 StorageMap<_, Twox64Concat, (T::AccountId, ParaId), BalanceOf<T>>;
195
196 #[pezpallet::storage]
200 pub type Winning<T: Config> = StorageMap<_, Twox64Concat, BlockNumberFor<T>, WinningData<T>>;
201
202 #[pezpallet::extra_constants]
203 impl<T: Config> Pezpallet<T> {
204 #[pezpallet::constant_name(SlotRangeCount)]
205 fn slot_range_count() -> u32 {
206 SlotRange::SLOT_RANGE_COUNT as u32
207 }
208
209 #[pezpallet::constant_name(LeasePeriodsPerSlot)]
210 fn lease_periods_per_slot() -> u32 {
211 SlotRange::LEASE_PERIODS_PER_SLOT as u32
212 }
213 }
214
215 #[pezpallet::hooks]
216 impl<T: Config> Hooks<BlockNumberFor<T>> for Pezpallet<T> {
217 fn on_initialize(n: BlockNumberFor<T>) -> Weight {
218 let mut weight = T::DbWeight::get().reads(1);
219
220 if let AuctionStatus::EndingPeriod(offset, _sub_sample) = Self::auction_status(n) {
224 weight = weight.saturating_add(T::DbWeight::get().reads(1));
225 if !Winning::<T>::contains_key(&offset) {
226 weight = weight.saturating_add(T::DbWeight::get().writes(1));
227 let winning_data = offset
228 .checked_sub(&One::one())
229 .and_then(Winning::<T>::get)
230 .unwrap_or([Self::EMPTY; SlotRange::SLOT_RANGE_COUNT]);
231 Winning::<T>::insert(offset, winning_data);
232 }
233 }
234
235 if let Some((winning_ranges, auction_lease_period_index)) = Self::check_auction_end(n) {
237 Self::manage_auction_end(auction_lease_period_index, winning_ranges);
240 weight = weight.saturating_add(T::WeightInfo::on_initialize());
241 }
242
243 weight
244 }
245 }
246
247 #[pezpallet::call]
248 impl<T: Config> Pezpallet<T> {
249 #[pezpallet::call_index(0)]
255 #[pezpallet::weight((T::WeightInfo::new_auction(), DispatchClass::Operational))]
256 pub fn new_auction(
257 origin: OriginFor<T>,
258 #[pezpallet::compact] duration: BlockNumberFor<T>,
259 #[pezpallet::compact] lease_period_index: LeasePeriodOf<T>,
260 ) -> DispatchResult {
261 T::InitiateOrigin::ensure_origin(origin)?;
262 Self::do_new_auction(duration, lease_period_index)
263 }
264
265 #[pezpallet::call_index(1)]
282 #[pezpallet::weight(T::WeightInfo::bid())]
283 pub fn bid(
284 origin: OriginFor<T>,
285 #[pezpallet::compact] para: ParaId,
286 #[pezpallet::compact] auction_index: AuctionIndex,
287 #[pezpallet::compact] first_slot: LeasePeriodOf<T>,
288 #[pezpallet::compact] last_slot: LeasePeriodOf<T>,
289 #[pezpallet::compact] amount: BalanceOf<T>,
290 ) -> DispatchResult {
291 let who = ensure_signed(origin)?;
292 Self::handle_bid(who, para, auction_index, first_slot, last_slot, amount)?;
293 Ok(())
294 }
295
296 #[pezpallet::call_index(2)]
300 #[pezpallet::weight(T::WeightInfo::cancel_auction())]
301 pub fn cancel_auction(origin: OriginFor<T>) -> DispatchResult {
302 ensure_root(origin)?;
303 for ((bidder, _), amount) in ReservedAmounts::<T>::drain() {
305 CurrencyOf::<T>::unreserve(&bidder, amount);
306 }
307 #[allow(deprecated)]
308 Winning::<T>::remove_all(None);
309 AuctionInfo::<T>::kill();
310 Ok(())
311 }
312 }
313}
314
315impl<T: Config> Auctioneer<BlockNumberFor<T>> for Pezpallet<T> {
316 type AccountId = T::AccountId;
317 type LeasePeriod = BlockNumberFor<T>;
318 type Currency = CurrencyOf<T>;
319
320 fn new_auction(
321 duration: BlockNumberFor<T>,
322 lease_period_index: LeasePeriodOf<T>,
323 ) -> DispatchResult {
324 Self::do_new_auction(duration, lease_period_index)
325 }
326
327 fn auction_status(now: BlockNumberFor<T>) -> AuctionStatus<BlockNumberFor<T>> {
329 let early_end = match AuctionInfo::<T>::get() {
330 Some((_, early_end)) => early_end,
331 None => return AuctionStatus::NotStarted,
332 };
333
334 let after_early_end = match now.checked_sub(&early_end) {
335 Some(after_early_end) => after_early_end,
336 None => return AuctionStatus::StartingPeriod,
337 };
338
339 let ending_period = T::EndingPeriod::get();
340 if after_early_end < ending_period {
341 let sample_length = T::SampleLength::get().max(One::one());
342 let sample = after_early_end / sample_length;
343 let sub_sample = after_early_end % sample_length;
344 return AuctionStatus::EndingPeriod(sample, sub_sample);
345 } else {
346 return AuctionStatus::VrfDelay(after_early_end - ending_period);
348 }
349 }
350
351 fn place_bid(
352 bidder: T::AccountId,
353 para: ParaId,
354 first_slot: LeasePeriodOf<T>,
355 last_slot: LeasePeriodOf<T>,
356 amount: BalanceOf<T>,
357 ) -> DispatchResult {
358 Self::handle_bid(bidder, para, AuctionCounter::<T>::get(), first_slot, last_slot, amount)
359 }
360
361 fn lease_period_index(b: BlockNumberFor<T>) -> Option<(Self::LeasePeriod, bool)> {
362 T::Leaser::lease_period_index(b)
363 }
364
365 #[cfg(any(feature = "runtime-benchmarks", test))]
366 fn lease_period_length() -> (BlockNumberFor<T>, BlockNumberFor<T>) {
367 T::Leaser::lease_period_length()
368 }
369
370 fn has_won_an_auction(para: ParaId, bidder: &T::AccountId) -> bool {
371 !T::Leaser::deposit_held(para, bidder).is_zero()
372 }
373}
374
375impl<T: Config> Pezpallet<T> {
376 const EMPTY: Option<(<T as pezframe_system::Config>::AccountId, ParaId, BalanceOf<T>)> = None;
378
379 fn do_new_auction(
385 duration: BlockNumberFor<T>,
386 lease_period_index: LeasePeriodOf<T>,
387 ) -> DispatchResult {
388 let maybe_auction = AuctionInfo::<T>::get();
389 ensure!(maybe_auction.is_none(), Error::<T>::AuctionInProgress);
390 let now = pezframe_system::Pezpallet::<T>::block_number();
391 if let Some((current_lease_period, _)) = T::Leaser::lease_period_index(now) {
392 ensure!(lease_period_index >= current_lease_period, Error::<T>::LeasePeriodInPast);
394 }
395
396 let n = AuctionCounter::<T>::mutate(|n| {
398 *n += 1;
399 *n
400 });
401
402 let ending = pezframe_system::Pezpallet::<T>::block_number().saturating_add(duration);
404 AuctionInfo::<T>::put((lease_period_index, ending));
405
406 Self::deposit_event(Event::<T>::AuctionStarted {
407 auction_index: n,
408 lease_period: lease_period_index,
409 ending,
410 });
411 Ok(())
412 }
413
414 pub fn handle_bid(
423 bidder: T::AccountId,
424 para: ParaId,
425 auction_index: u32,
426 first_slot: LeasePeriodOf<T>,
427 last_slot: LeasePeriodOf<T>,
428 amount: BalanceOf<T>,
429 ) -> DispatchResult {
430 ensure!(T::Registrar::is_registered(para), Error::<T>::ParaNotRegistered);
432 ensure!(auction_index == AuctionCounter::<T>::get(), Error::<T>::NotCurrentAuction);
434 let (first_lease_period, _) = AuctionInfo::<T>::get().ok_or(Error::<T>::NotAuction)?;
436
437 let auction_status = Self::auction_status(pezframe_system::Pezpallet::<T>::block_number());
440 let offset = match auction_status {
442 AuctionStatus::NotStarted => return Err(Error::<T>::AuctionEnded.into()),
443 AuctionStatus::StartingPeriod => Zero::zero(),
444 AuctionStatus::EndingPeriod(o, _) => o,
445 AuctionStatus::VrfDelay(_) => return Err(Error::<T>::AuctionEnded.into()),
446 };
447
448 ensure!(
450 !T::Leaser::already_leased(para, first_slot, last_slot),
451 Error::<T>::AlreadyLeasedOut
452 );
453
454 let range = SlotRange::new_bounded(first_lease_period, first_slot, last_slot)?;
456 let range_index = range as u8 as usize;
458
459 let mut current_winning = Winning::<T>::get(offset)
461 .or_else(|| offset.checked_sub(&One::one()).and_then(Winning::<T>::get))
462 .unwrap_or([Self::EMPTY; SlotRange::SLOT_RANGE_COUNT]);
463
464 if current_winning[range_index].as_ref().map_or(true, |last| amount > last.2) {
466 let existing_lease_deposit = T::Leaser::deposit_held(para, &bidder);
471 let reserve_required = amount.saturating_sub(existing_lease_deposit);
472
473 let bidder_para = (bidder.clone(), para);
475 let already_reserved = ReservedAmounts::<T>::get(&bidder_para).unwrap_or_default();
476
477 if let Some(additional) = reserve_required.checked_sub(&already_reserved) {
479 CurrencyOf::<T>::reserve(&bidder, additional)?;
482 ReservedAmounts::<T>::insert(&bidder_para, reserve_required);
484
485 Self::deposit_event(Event::<T>::Reserved {
486 bidder: bidder.clone(),
487 extra_reserved: additional,
488 total_amount: reserve_required,
489 });
490 }
491
492 let mut outgoing_winner = Some((bidder.clone(), para, amount));
495 swap(&mut current_winning[range_index], &mut outgoing_winner);
496 if let Some((who, para, _amount)) = outgoing_winner {
497 if auction_status.is_starting()
498 && current_winning
499 .iter()
500 .filter_map(Option::as_ref)
501 .all(|&(ref other, other_para, _)| other != &who || other_para != para)
502 {
503 if let Some(amount) = ReservedAmounts::<T>::take(&(who.clone(), para)) {
505 let err_amt = CurrencyOf::<T>::unreserve(&who, amount);
507 debug_assert!(err_amt.is_zero());
508 Self::deposit_event(Event::<T>::Unreserved { bidder: who, amount });
509 }
510 }
511 }
512
513 Winning::<T>::insert(offset, ¤t_winning);
515 Self::deposit_event(Event::<T>::BidAccepted {
516 bidder,
517 para_id: para,
518 amount,
519 first_slot,
520 last_slot,
521 });
522 }
523 Ok(())
524 }
525
526 fn check_auction_end(now: BlockNumberFor<T>) -> Option<(WinningData<T>, LeasePeriodOf<T>)> {
533 if let Some((lease_period_index, early_end)) = AuctionInfo::<T>::get() {
534 let ending_period = T::EndingPeriod::get();
535 let late_end = early_end.saturating_add(ending_period);
536 let is_ended = now >= late_end;
537 if is_ended {
538 let (raw_offset, known_since) = T::Randomness::random(&b"para_auction"[..]);
541
542 if late_end <= known_since {
543 let raw_offset_block_number = <BlockNumberFor<T>>::decode(
545 &mut raw_offset.as_ref(),
546 )
547 .expect("secure hashes should always be bigger than the block number; qed");
548 let offset = (raw_offset_block_number % ending_period)
549 / T::SampleLength::get().max(One::one());
550
551 let auction_counter = AuctionCounter::<T>::get();
552 Self::deposit_event(Event::<T>::WinningOffset {
553 auction_index: auction_counter,
554 block_number: offset,
555 });
556 let res = Winning::<T>::get(offset)
557 .unwrap_or([Self::EMPTY; SlotRange::SLOT_RANGE_COUNT]);
558 #[allow(deprecated)]
562 Winning::<T>::remove_all(None);
563 AuctionInfo::<T>::kill();
564 return Some((res, lease_period_index));
565 }
566 }
567 }
568 None
569 }
570
571 fn manage_auction_end(
575 auction_lease_period_index: LeasePeriodOf<T>,
576 winning_ranges: WinningData<T>,
577 ) {
578 for ((bidder, _), amount) in ReservedAmounts::<T>::drain() {
582 CurrencyOf::<T>::unreserve(&bidder, amount);
583 }
584
585 let winners = Self::calculate_winners(winning_ranges);
588
589 for (leaser, para, amount, range) in winners.into_iter() {
592 let begin_offset = LeasePeriodOf::<T>::from(range.as_pair().0 as u32);
593 let period_begin = auction_lease_period_index + begin_offset;
594 let period_count = LeasePeriodOf::<T>::from(range.len() as u32);
595
596 match T::Leaser::lease_out(para, &leaser, amount, period_begin, period_count) {
597 Err(LeaseError::ReserveFailed)
598 | Err(LeaseError::AlreadyEnded)
599 | Err(LeaseError::NoLeasePeriod) => {
600 },
603 Err(LeaseError::AlreadyLeased) => {
604 if CurrencyOf::<T>::reserve(&leaser, amount).is_ok() {
607 Self::deposit_event(Event::<T>::ReserveConfiscated {
608 para_id: para,
609 leaser,
610 amount,
611 });
612 }
613 },
614 Ok(()) => {}, }
616 }
617
618 Self::deposit_event(Event::<T>::AuctionClosed {
619 auction_index: AuctionCounter::<T>::get(),
620 });
621 }
622
623 fn calculate_winners(mut winning: WinningData<T>) -> WinnersData<T> {
628 let winning_ranges = {
629 let mut best_winners_ending_at: [(Vec<SlotRange>, BalanceOf<T>);
630 SlotRange::LEASE_PERIODS_PER_SLOT] = Default::default();
631 let best_bid = |range: SlotRange| {
632 winning[range as u8 as usize]
633 .as_ref()
634 .map(|(_, _, amount)| *amount * (range.len() as u32).into())
635 };
636 for i in 0..SlotRange::LEASE_PERIODS_PER_SLOT {
637 let r = SlotRange::new_bounded(0, 0, i as u32).expect("`i < LPPS`; qed");
638 if let Some(bid) = best_bid(r) {
639 best_winners_ending_at[i] = (vec![r], bid);
640 }
641 for j in 0..i {
642 let r = SlotRange::new_bounded(0, j as u32 + 1, i as u32)
643 .expect("`i < LPPS`; `j < i`; `j + 1 < LPPS`; qed");
644 if let Some(mut bid) = best_bid(r) {
645 bid += best_winners_ending_at[j].1;
646 if bid > best_winners_ending_at[i].1 {
647 let mut new_winners = best_winners_ending_at[j].0.clone();
648 new_winners.push(r);
649 best_winners_ending_at[i] = (new_winners, bid);
650 }
651 } else {
652 if best_winners_ending_at[j].1 > best_winners_ending_at[i].1 {
653 best_winners_ending_at[i] = best_winners_ending_at[j].clone();
654 }
655 }
656 }
657 }
658 best_winners_ending_at[SlotRange::LEASE_PERIODS_PER_SLOT - 1].0.clone()
659 };
660
661 winning_ranges
662 .into_iter()
663 .filter_map(|range| {
664 winning[range as u8 as usize]
665 .take()
666 .map(|(bidder, para, amount)| (bidder, para, amount, range))
667 })
668 .collect::<Vec<_>>()
669 }
670}
671
672#[cfg(test)]
673mod mock;
674
675#[cfg(test)]
676mod tests;
677
678#[cfg(feature = "runtime-benchmarks")]
679mod benchmarking;