pezkuwi_runtime_common/auctions/
mod.rs

1// Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute
2// This file is part of Pezkuwi.
3
4// Pezkuwi is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Pezkuwi is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Pezkuwi.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Auctioning system to determine the set of Teyrchains in operation. This includes logic for the
18//! auctioning mechanism and for reserving balance as part of the "payment". Unreserving the balance
19//! happens elsewhere.
20
21use 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
67/// An auction index. We count auctions in this type.
68pub type AuctionIndex = u32;
69
70type LeasePeriodOf<T> = <<T as Config>::Leaser as Leaser<BlockNumberFor<T>>>::LeasePeriod;
71
72// Winning data type. This encodes the top bidders of each range together with their bid.
73type WinningData<T> = [Option<(<T as pezframe_system::Config>::AccountId, ParaId, BalanceOf<T>)>;
74	SlotRange::SLOT_RANGE_COUNT];
75// Winners data type. This encodes each of the final winners of a teyrchain auction, the teyrchain
76// index assigned to them, their winning bid and the range that they won.
77type 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	/// The module's configuration trait.
90	#[pezpallet::config]
91	pub trait Config: pezframe_system::Config {
92		/// The overarching event type.
93		#[allow(deprecated)]
94		type RuntimeEvent: From<Event<Self>>
95			+ IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
96
97		/// The type representing the leasing system.
98		type Leaser: Leaser<
99			BlockNumberFor<Self>,
100			AccountId = Self::AccountId,
101			LeasePeriod = BlockNumberFor<Self>,
102		>;
103
104		/// The teyrchain registrar type.
105		type Registrar: Registrar<AccountId = Self::AccountId>;
106
107		/// The number of blocks over which an auction may be retroactively ended.
108		#[pezpallet::constant]
109		type EndingPeriod: Get<BlockNumberFor<Self>>;
110
111		/// The length of each sample to take during the ending period.
112		///
113		/// `EndingPeriod` / `SampleLength` = Total # of Samples
114		#[pezpallet::constant]
115		type SampleLength: Get<BlockNumberFor<Self>>;
116
117		/// Something that provides randomness in the runtime.
118		type Randomness: Randomness<Self::Hash, BlockNumberFor<Self>>;
119
120		/// The origin which may initiate auctions.
121		type InitiateOrigin: EnsureOrigin<Self::RuntimeOrigin>;
122
123		/// Weight Information for the Extrinsics in the Pezpallet
124		type WeightInfo: WeightInfo;
125	}
126
127	#[pezpallet::event]
128	#[pezpallet::generate_deposit(pub(super) fn deposit_event)]
129	pub enum Event<T: Config> {
130		/// An auction started. Provides its index and the block number where it will begin to
131		/// close and the first lease period of the quadruplet that is auctioned.
132		AuctionStarted {
133			auction_index: AuctionIndex,
134			lease_period: LeasePeriodOf<T>,
135			ending: BlockNumberFor<T>,
136		},
137		/// An auction ended. All funds become unreserved.
138		AuctionClosed { auction_index: AuctionIndex },
139		/// Funds were reserved for a winning bid. First balance is the extra amount reserved.
140		/// Second is the total.
141		Reserved { bidder: T::AccountId, extra_reserved: BalanceOf<T>, total_amount: BalanceOf<T> },
142		/// Funds were unreserved since bidder is no longer active. `[bidder, amount]`
143		Unreserved { bidder: T::AccountId, amount: BalanceOf<T> },
144		/// Someone attempted to lease the same slot twice for a teyrchain. The amount is held in
145		/// reserve but no teyrchain slot has been leased.
146		ReserveConfiscated { para_id: ParaId, leaser: T::AccountId, amount: BalanceOf<T> },
147		/// A new bid has been accepted as the current winner.
148		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		/// The winning offset was chosen for an auction. This will map into the `Winning` storage
156		/// map.
157		WinningOffset { auction_index: AuctionIndex, block_number: BlockNumberFor<T> },
158	}
159
160	#[pezpallet::error]
161	pub enum Error<T> {
162		/// This auction is already in progress.
163		AuctionInProgress,
164		/// The lease period is in the past.
165		LeasePeriodInPast,
166		/// Para is not registered
167		ParaNotRegistered,
168		/// Not a current auction.
169		NotCurrentAuction,
170		/// Not an auction.
171		NotAuction,
172		/// Auction has already ended.
173		AuctionEnded,
174		/// The para is already leased out for part of this range.
175		AlreadyLeasedOut,
176	}
177
178	/// Number of auctions started so far.
179	#[pezpallet::storage]
180	pub type AuctionCounter<T> = StorageValue<_, AuctionIndex, ValueQuery>;
181
182	/// Information relating to the current auction, if there is one.
183	///
184	/// The first item in the tuple is the lease period index that the first of the four
185	/// contiguous lease periods on auction is for. The second is the block number when the
186	/// auction will "begin to end", i.e. the first block of the Ending Period of the auction.
187	#[pezpallet::storage]
188	pub type AuctionInfo<T: Config> = StorageValue<_, (LeasePeriodOf<T>, BlockNumberFor<T>)>;
189
190	/// Amounts currently reserved in the accounts of the bidders currently winning
191	/// (sub-)ranges.
192	#[pezpallet::storage]
193	pub type ReservedAmounts<T: Config> =
194		StorageMap<_, Twox64Concat, (T::AccountId, ParaId), BalanceOf<T>>;
195
196	/// The winning bids for each of the 10 ranges at each sample in the final Ending Period of
197	/// the current auction. The map's key is the 0-based index into the Sample Size. The
198	/// first sample of the ending period is 0; the last is `Sample Size - 1`.
199	#[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 the current auction was in its ending period last block, then ensure that the
221			// (sub-)range winner information is duplicated from the previous block in case no bids
222			// happened in the last block.
223			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			// Check to see if an auction just ended.
236			if let Some((winning_ranges, auction_lease_period_index)) = Self::check_auction_end(n) {
237				// Auction is ended now. We have the winning ranges and the lease period index which
238				// acts as the offset. Handle it.
239				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		/// Create a new auction.
250		///
251		/// This can only happen when there isn't already an auction in progress and may only be
252		/// called by the root origin. Accepts the `duration` of this auction and the
253		/// `lease_period_index` of the initial lease period of the four that are to be auctioned.
254		#[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		/// Make a new bid from an account (including a teyrchain account) for deploying a new
266		/// teyrchain.
267		///
268		/// Multiple simultaneous bids from the same bidder are allowed only as long as all active
269		/// bids overlap each other (i.e. are mutually exclusive). Bids cannot be redacted.
270		///
271		/// - `sub` is the sub-bidder ID, allowing for multiple competing bids to be made by (and
272		/// funded by) the same account.
273		/// - `auction_index` is the index of the auction to bid on. Should just be the present
274		/// value of `AuctionCounter`.
275		/// - `first_slot` is the first lease period index of the range to bid on. This is the
276		/// absolute lease period index value, not an auction-specific offset.
277		/// - `last_slot` is the last lease period index of the range to bid on. This is the
278		/// absolute lease period index value, not an auction-specific offset.
279		/// - `amount` is the amount to bid to be held as deposit for the teyrchain should the
280		/// bid win. This amount is held throughout the range.
281		#[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		/// Cancel an in-progress auction.
297		///
298		/// Can only be called by Root origin.
299		#[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			// Unreserve all bids.
304			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	// Returns the status of the auction given the current block number.
328	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			// This is safe because of the comparison operator above
347			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	// A trick to allow me to initialize large arrays with nothing in them.
377	const EMPTY: Option<(<T as pezframe_system::Config>::AccountId, ParaId, BalanceOf<T>)> = None;
378
379	/// Create a new auction.
380	///
381	/// This can only happen when there isn't already an auction in progress. Accepts the `duration`
382	/// of this auction and the `lease_period_index` of the initial lease period of the four that
383	/// are to be auctioned.
384	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			// If there is no active lease period, then we don't need to make this check.
393			ensure!(lease_period_index >= current_lease_period, Error::<T>::LeasePeriodInPast);
394		}
395
396		// Bump the counter.
397		let n = AuctionCounter::<T>::mutate(|n| {
398			*n += 1;
399			*n
400		});
401
402		// Set the information.
403		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	/// Actually place a bid in the current auction.
415	///
416	/// - `bidder`: The account that will be funding this bid.
417	/// - `auction_index`: The auction index of the bid. For this to succeed, must equal
418	/// the current value of `AuctionCounter`.
419	/// - `first_slot`: The first lease period index of the range to be bid on.
420	/// - `last_slot`: The last lease period index of the range to be bid on (inclusive).
421	/// - `amount`: The total amount to be the bid for deposit over the range.
422	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 para is registered before placing a bid on it.
431		ensure!(T::Registrar::is_registered(para), Error::<T>::ParaNotRegistered);
432		// Bidding on latest auction.
433		ensure!(auction_index == AuctionCounter::<T>::get(), Error::<T>::NotCurrentAuction);
434		// Assume it's actually an auction (this should never fail because of above).
435		let (first_lease_period, _) = AuctionInfo::<T>::get().ok_or(Error::<T>::NotAuction)?;
436
437		// Get the auction status and the current sample block. For the starting period, the sample
438		// block is zero.
439		let auction_status = Self::auction_status(pezframe_system::Pezpallet::<T>::block_number());
440		// The offset into the ending samples of the auction.
441		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		// We also make sure that the bid is not for any existing leases the para already has.
449		ensure!(
450			!T::Leaser::already_leased(para, first_slot, last_slot),
451			Error::<T>::AlreadyLeasedOut
452		);
453
454		// Our range.
455		let range = SlotRange::new_bounded(first_lease_period, first_slot, last_slot)?;
456		// Range as an array index.
457		let range_index = range as u8 as usize;
458
459		// The current winning ranges.
460		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 this bid beat the previous winner of our range.
465		if current_winning[range_index].as_ref().map_or(true, |last| amount > last.2) {
466			// Ok; we are the new winner of this range - reserve the additional amount and record.
467
468			// Get the amount already held on deposit if this is a renewal bid (i.e. there's
469			// an existing lease on the same para by the same leaser).
470			let existing_lease_deposit = T::Leaser::deposit_held(para, &bidder);
471			let reserve_required = amount.saturating_sub(existing_lease_deposit);
472
473			// Get the amount already reserved in any prior and still active bids by us.
474			let bidder_para = (bidder.clone(), para);
475			let already_reserved = ReservedAmounts::<T>::get(&bidder_para).unwrap_or_default();
476
477			// If these don't already cover the bid...
478			if let Some(additional) = reserve_required.checked_sub(&already_reserved) {
479				// ...then reserve some more funds from their account, failing if there's not
480				// enough funds.
481				CurrencyOf::<T>::reserve(&bidder, additional)?;
482				// ...and record the amount reserved.
483				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			// Return any funds reserved for the previous winner if we are not in the ending period
493			// and they no longer have any active bids.
494			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					// Previous bidder is no longer winning any ranges: unreserve their funds.
504					if let Some(amount) = ReservedAmounts::<T>::take(&(who.clone(), para)) {
505						// It really should be reserved; there's not much we can do here on fail.
506						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			// Update the range winner.
514			Winning::<T>::insert(offset, &current_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	/// Some when the auction's end is known (with the end block number). None if it is unknown.
527	/// If `Some` then the block number must be at most the previous block and at least the
528	/// previous block minus `T::EndingPeriod::get()`.
529	///
530	/// This mutates the state, cleaning up `AuctionInfo` and `Winning` in the case of an auction
531	/// ending. An immediately subsequent call with the same argument will always return `None`.
532	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				// auction definitely ended.
539				// check to see if we can determine the actual ending point.
540				let (raw_offset, known_since) = T::Randomness::random(&b"para_auction"[..]);
541
542				if late_end <= known_since {
543					// Our random seed was known only after the auction ended. Good to use.
544					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					// This `remove_all` statement should remove at most `EndingPeriod` /
559					// `SampleLength` items, which should be bounded and sensibly configured in the
560					// runtime.
561					#[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	/// Auction just ended. We have the current lease period, the auction's lease period (which
572	/// is guaranteed to be at least the current period) and the bidders that were winning each
573	/// range at the time of the auction's close.
574	fn manage_auction_end(
575		auction_lease_period_index: LeasePeriodOf<T>,
576		winning_ranges: WinningData<T>,
577	) {
578		// First, unreserve all amounts that were reserved for the bids. We will later re-reserve
579		// the amounts from the bidders that ended up being assigned the slot so there's no need to
580		// special-case them here.
581		for ((bidder, _), amount) in ReservedAmounts::<T>::drain() {
582			CurrencyOf::<T>::unreserve(&bidder, amount);
583		}
584
585		// Next, calculate the winning combination of slots and thus the final winners of the
586		// auction.
587		let winners = Self::calculate_winners(winning_ranges);
588
589		// Go through those winners and re-reserve their bid, updating our table of deposits
590		// accordingly.
591		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					// Should never happen since we just unreserved this amount (and our offset is
601					// from the present period). But if it does, there's not much we can do.
602				},
603				Err(LeaseError::AlreadyLeased) => {
604					// The leaser attempted to get a second lease on the same para ID, possibly
605					// griefing us. Let's keep the amount reserved and let governance sort it out.
606					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(()) => {}, // Nothing to report.
615			}
616		}
617
618		Self::deposit_event(Event::<T>::AuctionClosed {
619			auction_index: AuctionCounter::<T>::get(),
620		});
621	}
622
623	/// Calculate the final winners from the winning slots.
624	///
625	/// This is a simple dynamic programming algorithm designed by Al, the original code is at:
626	/// `https://github.com/w3f/consensus/blob/master/NPoS/auctiondynamicthing.py`
627	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;