// Copyright 2017-2020 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see .
//! # Parachain Crowdloaning module
//!
//! The point of this module is to allow parachain projects to offer the ability to help fund a
//! deposit for the parachain. When the parachain is retired, the funds may be returned.
//!
//! Contributing funds is permissionless. Each fund has a child-trie which stores all
//! contributors account IDs together with the amount they contributed; the root of this can then be
//! used by the parachain to allow contributors to prove that they made some particular contribution
//! to the project (e.g. to be rewarded through some token or badge). The trie is retained for later
//! (efficient) redistribution back to the contributors.
//!
//! Contributions must be of at least `MinContribution` (to account for the resources taken in
//! tracking contributions), and may never tally greater than the fund's `cap`, set and fixed at the
//! time of creation. The `create` call may be used to create a new fund. In order to do this, then
//! a deposit must be paid of the amount `SubmissionDeposit`. Substantial resources are taken on
//! the main trie in tracking a fund and this accounts for that.
//!
//! Funds may be set up during an auction period; their closing time is fixed at creation (as a
//! block number) and if the fund is not successful by the closing time, then it will become *retired*.
//! Funds may span multiple auctions, and even auctions that sell differing periods. However, for a
//! fund to be active in bidding for an auction, it *must* have had *at least one bid* since the end
//! of the last auction. Until a fund takes a further bid following the end of an auction, then it
//! will be inactive.
//!
//! Contributors may get a refund of their contributions from retired funds. After a period (`RetirementPeriod`)
//! the fund may be dissolved entirely. At this point any non-refunded contributions are considered
//! `orphaned` and are disposed of through the `OrphanedFunds` handler (which may e.g. place them
//! into the treasury).
//!
//! Funds may accept contributions at any point before their success or retirement. When a parachain
//! slot auction enters its ending period, then parachains will each place a bid; the bid will be
//! raised once per block if the parachain had additional funds contributed since the last bid.
//!
//! Funds may set their deploy data (the code hash and head data of their parachain) at any point.
//! It may only be done once and once set cannot be changed. Good procedure would be to set them
//! ahead of receiving any contributions in order that contributors may verify that their parachain
//! contains all expected functionality. However, this is not enforced and deploy data may happen
//! at any point, even after a slot has been successfully won or, indeed, never.
//!
//! Funds that are successful winners of a slot may have their slot claimed through the `onboard`
//! call. This may only be done once and must be after the deploy data has been fixed. Successful
//! funds remain tracked (in the `Funds` storage item and the associated child trie) as long as
//! the parachain remains active. Once it does not, it is up to the parachain to ensure that the
//! funds are returned to this module's fund sub-account in order that they be redistributed back to
//! contributors. *Retirement* may be initiated by any account (using the `begin_retirement` call)
//! once the parachain is removed from the its slot.
//!
//! @WARNING: For funds to be returned, it is imperative that this module's account is provided as
//! the offboarding account for the slot. In the case that a parachain supplemented these funds in
//! order to win a later auction, then it is the parachain's duty to ensure that the right amount of
//! funds ultimately end up in module's fund sub-account.
use frame_support::{
decl_module, decl_storage, decl_event, decl_error, ensure,
storage::child,
traits::{
Currency, ReservableCurrency, Get, OnUnbalanced, ExistenceRequirement::AllowDeath
},
pallet_prelude::{Weight, DispatchResultWithPostInfo},
};
use frame_system::ensure_signed;
use sp_runtime::{
ModuleId, DispatchResult, RuntimeDebug, MultiSignature, MultiSigner,
traits::{
AccountIdConversion, Hash, Saturating, Zero, CheckedAdd, Bounded, Verify, IdentifyAccount,
},
};
use crate::traits::{Registrar, Auctioneer};
use parity_scale_codec::{Encode, Decode};
use sp_std::vec::Vec;
use primitives::v1::Id as ParaId;
type CurrencyOf = <::Auctioneer as Auctioneer>::Currency;
type LeasePeriodOf = <::Auctioneer as Auctioneer>::LeasePeriod;
type BalanceOf = as Currency<::AccountId>>::Balance;
#[allow(dead_code)]
type NegativeImbalanceOf = as Currency<::AccountId>>::NegativeImbalance;
type TrieIndex = u32;
pub trait WeightInfo {
fn create() -> Weight;
fn contribute() -> Weight;
fn withdraw() -> Weight;
fn dissolve(k: u32, ) -> Weight;
fn on_initialize(n: u32, ) -> Weight;
}
pub struct TestWeightInfo;
impl WeightInfo for TestWeightInfo {
fn create() -> Weight { 0 }
fn contribute() -> Weight { 0 }
fn withdraw() -> Weight { 0 }
fn dissolve(_k: u32, ) -> Weight { 0 }
fn on_initialize(_n: u32, ) -> Weight { 0 }
}
pub trait Config: frame_system::Config {
type Event: From> + Into<::Event>;
/// ModuleID for the crowdloan module. An appropriate value could be ```ModuleId(*b"py/cfund")```
type ModuleId: Get;
/// The amount to be held on deposit by the depositor of a crowdloan.
type SubmissionDeposit: Get>;
/// The minimum amount that may be contributed into a crowdloan. Should almost certainly be at
/// least ExistentialDeposit.
type MinContribution: Get>;
/// The period of time (in blocks) after an unsuccessful crowdloan ending when
/// contributors are able to withdraw their funds. After this period, their funds are lost.
type RetirementPeriod: Get;
/// What to do with funds that were not withdrawn.
type OrphanedFunds: OnUnbalanced>;
/// Max number of storage keys to remove per extrinsic call.
type RemoveKeysLimit: Get;
/// The parachain registrar type. We jus use this to ensure that only the manager of a para is able to
/// start a crowdloan for its slot.
type Registrar: Registrar;
/// The type representing the auctioning system.
type Auctioneer: Auctioneer<
AccountId=Self::AccountId,
BlockNumber=Self::BlockNumber,
LeasePeriod=Self::BlockNumber,
>;
/// Weight Information for the Extrinsics in the Pallet
type WeightInfo: WeightInfo;
}
#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug)]
pub enum LastContribution {
Never,
PreEnding(u32),
Ending(BlockNumber),
}
/// Information on a funding effort for a pre-existing parachain. We assume that the parachain ID
/// is known as it's used for the key of the storage item for which this is the value (`Funds`).
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)]
#[codec(dumb_trait_bound)]
pub struct FundInfo {
/// True if the fund is being retired. This can only be set once and only when the current
/// lease period is greater than the `last_slot`.
retiring: bool,
/// The owning account who placed the deposit.
depositor: AccountId,
/// An optional verifier. If exists, contributions must be signed by verifier.
verifier: Option,
/// The amount of deposit placed.
deposit: Balance,
/// The total amount raised.
raised: Balance,
/// Block number after which the funding must have succeeded. If not successful at this number
/// then everyone may withdraw their funds.
end: BlockNumber,
/// A hard-cap on the amount that may be contributed.
cap: Balance,
/// The most recent block that this had a contribution. Determines if we make a bid or not.
/// If this is `Never`, this fund has never received a contribution.
/// If this is `PreEnding(n)`, this fund received a contribution sometime in auction
/// number `n` before the ending period.
/// If this is `Ending(n)`, this fund received a contribution during the current ending period,
/// where `n` is how far into the ending period the contribution was made.
last_contribution: LastContribution,
/// First slot in range to bid on; it's actually a LeasePeriod, but that's the same type as
/// BlockNumber.
first_slot: LeasePeriod,
/// Last slot in range to bid on; it's actually a LeasePeriod, but that's the same type as
/// BlockNumber.
last_slot: LeasePeriod,
/// Index used for the child trie of this fund
trie_index: TrieIndex,
}
decl_storage! {
trait Store for Module as Crowdloan {
/// Info on all of the funds.
Funds get(fn funds):
map hasher(twox_64_concat) ParaId
=> Option, T::BlockNumber, LeasePeriodOf>>;
/// The funds that have had additional contributions during the last block. This is used
/// in order to determine which funds should submit new or updated bids.
NewRaise get(fn new_raise): Vec;
/// The number of auctions that have entered into their ending period so far.
EndingsCount get(fn endings_count): u32;
/// Tracker for the next available trie index
NextTrieIndex get(fn next_trie_index): u32;
}
}
decl_event! {
pub enum Event where
::AccountId,
Balance = BalanceOf,
{
/// Create a new crowdloaning campaign. [fund_index]
Created(ParaId),
/// Contributed to a crowd sale. [who, fund_index, amount]
Contributed(AccountId, ParaId, Balance),
/// Withdrew full balance of a contributor. [who, fund_index, amount]
Withdrew(AccountId, ParaId, Balance),
/// Fund is placed into retirement. [fund_index]
Retiring(ParaId),
/// Fund is partially dissolved, i.e. there are some left over child
/// keys that still need to be killed. [fund_index]
PartiallyDissolved(ParaId),
/// Fund is dissolved. [fund_index]
Dissolved(ParaId),
/// The deploy data of the funded parachain is set. [fund_index]
DeployDataFixed(ParaId),
/// On-boarding process for a winning parachain fund is completed. [find_index, parachain_id]
Onboarded(ParaId, ParaId),
/// The result of trying to submit a new bid to the Slots pallet.
HandleBidResult(ParaId, DispatchResult),
}
}
decl_error! {
pub enum Error for Module {
/// The first slot needs to at least be less than 3 `max_value`.
FirstSlotTooFarInFuture,
/// Last slot must be greater than first slot.
LastSlotBeforeFirstSlot,
/// The last slot cannot be more then 3 slots after the first slot.
LastSlotTooFarInFuture,
/// The campaign ends before the current block number. The end must be in the future.
CannotEndInPast,
/// There was an overflow.
Overflow,
/// The contribution was below the minimum, `MinContribution`.
ContributionTooSmall,
/// Invalid fund index.
InvalidParaId,
/// Contributions exceed maximum amount.
CapExceeded,
/// The contribution period has already ended.
ContributionPeriodOver,
/// The origin of this call is invalid.
InvalidOrigin,
/// This crowdloan does not correspond to a parachain.
NotParachain,
/// This parachain lease is still active and retirement cannot yet begin.
LeaseActive,
/// This parachain's bid or lease is still active and withdraw cannot yet begin.
BidOrLeaseActive,
/// Funds have not yet been returned.
FundsNotReturned,
/// Fund has not yet retired.
FundNotRetired,
/// The crowdloan has not yet ended.
FundNotEnded,
/// There are no contributions stored in this crowdloan.
NoContributions,
/// This crowdloan has an active parachain and cannot be dissolved.
HasActiveParachain,
/// The crowdloan is not ready to dissolve. Potentially still has a slot or in retirement period.
NotReadyToDissolve,
/// Invalid signature.
InvalidSignature,
}
}
decl_module! {
pub struct Module for enum Call where origin: ::Origin {
type Error = Error;
const ModuleId: ModuleId = T::ModuleId::get();
const MinContribution: BalanceOf = T::MinContribution::get();
const RemoveKeysLimit: u32 = T::RemoveKeysLimit::get();
const RetirementPeriod: T::BlockNumber = T::RetirementPeriod::get();
fn deposit_event() = default;
/// Create a new crowdloaning campaign for a parachain slot deposit for the current auction.
#[weight = T::WeightInfo::create()]
pub fn create(origin,
#[compact] index: ParaId,
#[compact] cap: BalanceOf,
#[compact] first_slot: LeasePeriodOf,
#[compact] last_slot: LeasePeriodOf,
#[compact] end: T::BlockNumber,
verifier: Option,
) {
let depositor = ensure_signed(origin)?;
ensure!(first_slot <= last_slot, Error::::LastSlotBeforeFirstSlot);
let last_slot_limit = first_slot.checked_add(&3u32.into()).ok_or(Error::::FirstSlotTooFarInFuture)?;
ensure!(last_slot <= last_slot_limit, Error::::LastSlotTooFarInFuture);
ensure!(end > >::block_number(), Error::::CannotEndInPast);
// There should not be an existing fund.
ensure!(!Funds::::contains_key(index), Error::::FundNotEnded);
let manager = T::Registrar::manager_of(index).ok_or(Error::::InvalidParaId)?;
ensure!(depositor == manager, Error::::InvalidOrigin);
let trie_index = Self::next_trie_index();
let new_trie_index = trie_index.checked_add(1).ok_or(Error::::Overflow)?;
let deposit = T::SubmissionDeposit::get();
CurrencyOf::::reserve(&depositor, deposit)?;
Funds::::insert(index, FundInfo {
retiring: false,
depositor,
verifier,
deposit,
raised: Zero::zero(),
end,
cap,
last_contribution: LastContribution::Never,
first_slot,
last_slot,
trie_index,
});
NextTrieIndex::put(new_trie_index);
Self::deposit_event(RawEvent::Created(index));
}
/// Contribute to a crowd sale. This will transfer some balance over to fund a parachain
/// slot. It will be withdrawable in two instances: the parachain becomes retired; or the
/// slot is unable to be purchased and the timeout expires.
#[weight = T::WeightInfo::contribute()]
pub fn contribute(origin,
#[compact] index: ParaId,
#[compact] value: BalanceOf,
signature: Option
) {
let who = ensure_signed(origin)?;
ensure!(value >= T::MinContribution::get(), Error::::ContributionTooSmall);
let mut fund = Self::funds(index).ok_or(Error::::InvalidParaId)?;
fund.raised = fund.raised.checked_add(&value).ok_or(Error::::Overflow)?;
ensure!(fund.raised <= fund.cap, Error::::CapExceeded);
// Make sure crowdloan has not ended
let now = >::block_number();
ensure!(now < fund.end, Error::::ContributionPeriodOver);
let old_balance = Self::contribution_get(fund.trie_index, &who);
if let Some(ref verifier) = fund.verifier {
let signature = signature.ok_or(Error::::InvalidSignature)?;
let payload = (index, &who, old_balance, value);
let valid = payload.using_encoded(|encoded| signature.verify(encoded, &verifier.clone().into_account()));
ensure!(valid, Error::::InvalidSignature);
}
CurrencyOf::::transfer(&who, &Self::fund_account_id(index), value, AllowDeath)?;
let balance = old_balance.saturating_add(value);
Self::contribution_put(fund.trie_index, &who, &balance);
if T::Auctioneer::is_ending(now).is_some() {
match fund.last_contribution {
// In ending period; must ensure that we are in NewRaise.
LastContribution::Ending(n) if n == now => {
// do nothing - already in NewRaise
}
_ => {
NewRaise::append(index);
fund.last_contribution = LastContribution::Ending(now);
}
}
} else {
let endings_count = Self::endings_count();
match fund.last_contribution {
LastContribution::PreEnding(a) if a == endings_count => {
// Not in ending period and no auctions have ended ending since our
// previous bid which was also not in an ending period.
// `NewRaise` will contain our ID still: Do nothing.
}
_ => {
// Not in ending period; but an auction has been ending since our previous
// bid, or we never had one to begin with. Add bid.
NewRaise::append(index);
fund.last_contribution = LastContribution::PreEnding(endings_count);
}
}
}
Funds::::insert(index, &fund);
Self::deposit_event(RawEvent::Contributed(who, index, value));
}
/// Withdraw full balance of a contributor.
///
/// Origin must be signed.
///
/// The fund must be either in, or ready for, retirement. For a fund to be *in* retirement, then the retirement
/// flag must be set. For a fund to be ready for retirement, then:
/// - it must not already be in retirement;
/// - the amount of raised funds must be bigger than the _free_ balance of the account;
/// - and either:
/// - the block number must be at least `end`; or
/// - the current lease period must be greater than the fund's `last_slot`.
///
/// In this case, the fund's retirement flag is set and its `end` is reset to the current block
/// number.
///
/// - `who`: The account whose contribution should be withdrawn.
/// - `index`: The parachain to whose crowdloan the contribution was made.
#[weight = T::WeightInfo::withdraw()]
pub fn withdraw(origin, who: T::AccountId, #[compact] index: ParaId) {
ensure_signed(origin)?;
let mut fund = Self::funds(index).ok_or(Error::::InvalidParaId)?;
// `fund.end` can represent the end of a failed crowdsale or the beginning of retirement
let now = frame_system::Pallet::::block_number();
let current_lease_period = T::Auctioneer::lease_period_index();
ensure!(now >= fund.end || current_lease_period > fund.last_slot, Error::::FundNotEnded);
let fund_account = Self::fund_account_id(index);
// free balance must equal amount raised, otherwise a bid or lease must be active.
ensure!(CurrencyOf::::free_balance(&fund_account) == fund.raised, Error::::BidOrLeaseActive);
let balance = Self::contribution_get(fund.trie_index, &who);
ensure!(balance > Zero::zero(), Error::::NoContributions);
// Avoid using transfer to ensure we don't pay any fees.
CurrencyOf::::transfer(&fund_account, &who, balance, AllowDeath)?;
Self::contribution_kill(fund.trie_index, &who);
fund.raised = fund.raised.saturating_sub(balance);
if !fund.retiring {
fund.retiring = true;
fund.end = now;
}
Funds::::insert(index, &fund);
Self::deposit_event(RawEvent::Withdrew(who, index, balance));
}
/// Remove a fund after the retirement period has ended.
///
/// This places any deposits that were not withdrawn into the treasury.
#[weight = T::WeightInfo::dissolve(T::RemoveKeysLimit::get())]
pub fn dissolve(origin, #[compact] index: ParaId) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let fund = Self::funds(index).ok_or(Error::::InvalidParaId)?;
let now = frame_system::Pallet::::block_number();
let dissolution = fund.end.saturating_add(T::RetirementPeriod::get());
let can_dissolve = (fund.retiring && now >= dissolution) ||
(fund.raised.is_zero() && who == fund.depositor);
ensure!(can_dissolve, Error::::NotReadyToDissolve);
// Try killing the crowdloan child trie
match Self::crowdloan_kill(fund.trie_index) {
child::KillChildStorageResult::AllRemoved(num_removed) => {
CurrencyOf::::unreserve(&fund.depositor, fund.deposit);
// Remove all other balance from the account into orphaned funds.
let account = Self::fund_account_id(index);
let (imbalance, _) = CurrencyOf::::slash(&account, BalanceOf::::max_value());
T::OrphanedFunds::on_unbalanced(imbalance);
Funds::::remove(index);
Self::deposit_event(RawEvent::Dissolved(index));
Ok(Some(T::WeightInfo::dissolve(num_removed)).into())
},
child::KillChildStorageResult::SomeRemaining(num_removed) => {
Self::deposit_event(RawEvent::PartiallyDissolved(index));
Ok(Some(T::WeightInfo::dissolve(num_removed)).into())
}
}
}
fn on_initialize(n: T::BlockNumber) -> frame_support::weights::Weight {
if let Some(n) = T::Auctioneer::is_ending(n) {
if n.is_zero() {
// first block of ending period.
EndingsCount::mutate(|c| *c += 1);
}
let new_raise = NewRaise::take();
let new_raise_len = new_raise.len() as u32;
for (fund, para_id) in new_raise.into_iter().filter_map(|i| Self::funds(i).map(|f| (f, i))) {
// Care needs to be taken by the crowdloan creator that this function will succeed given
// the crowdloaning configuration. We do some checks ahead of time in crowdloan `create`.
let result = T::Auctioneer::place_bid(
Self::fund_account_id(para_id),
para_id,
fund.first_slot,
fund.last_slot,
fund.raised,
);
Self::deposit_event(RawEvent::HandleBidResult(para_id, result));
}
T::WeightInfo::on_initialize(new_raise_len)
} else {
T::DbWeight::get().reads(1)
}
}
}
}
impl Module {
/// The account ID of the fund pot.
///
/// This actually does computation. If you need to keep using it, then make sure you cache the
/// value and only call this once.
pub fn fund_account_id(index: ParaId) -> T::AccountId {
T::ModuleId::get().into_sub_account(index)
}
pub fn id_from_index(index: TrieIndex) -> child::ChildInfo {
let mut buf = Vec::new();
buf.extend_from_slice(b"crowdloan");
buf.extend_from_slice(&index.encode()[..]);
child::ChildInfo::new_default(T::Hashing::hash(&buf[..]).as_ref())
}
pub fn contribution_put(index: TrieIndex, who: &T::AccountId, balance: &BalanceOf) {
who.using_encoded(|b| child::put(&Self::id_from_index(index), b, balance));
}
pub fn contribution_get(index: TrieIndex, who: &T::AccountId) -> BalanceOf {
who.using_encoded(|b| child::get_or_default::>(
&Self::id_from_index(index),
b,
))
}
pub fn contribution_kill(index: TrieIndex, who: &T::AccountId) {
who.using_encoded(|b| child::kill(&Self::id_from_index(index), b));
}
pub fn crowdloan_kill(index: TrieIndex) -> child::KillChildStorageResult {
child::kill_storage(&Self::id_from_index(index), Some(T::RemoveKeysLimit::get()))
}
}
impl crate::traits::OnSwap for Module {
fn on_swap(one: ParaId, other: ParaId) {
Funds::::mutate(one, |x|
Funds::::mutate(other, |y|
sp_std::mem::swap(x, y)
)
)
}
}
#[cfg(any(feature = "runtime-benchmarks", test))]
mod crypto {
use sp_core::ed25519;
use sp_io::crypto::{ed25519_sign, ed25519_generate};
use sp_std::{
vec::Vec,
convert::TryFrom,
};
use sp_runtime::{MultiSigner, MultiSignature};
pub fn create_ed25519_pubkey(seed: Vec) -> MultiSigner {
ed25519_generate(0.into(), Some(seed)).into()
}
pub fn create_ed25519_signature(payload: &[u8], pubkey: MultiSigner) -> MultiSignature {
let edpubkey = ed25519::Public::try_from(pubkey).unwrap();
let edsig = ed25519_sign(0.into(), &edpubkey, payload).unwrap();
edsig.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{cell::RefCell, sync::Arc};
use frame_support::{
assert_ok, assert_noop, parameter_types,
traits::{OnInitialize, OnFinalize},
};
use sp_core::H256;
use primitives::v1::Id as ParaId;
// The testing primitives are very useful for avoiding having to work with signatures
// or public keys. `u64` is used as the `AccountId` and no `Signature`s are requried.
use sp_runtime::{
testing::Header, traits::{BlakeTwo256, IdentityLookup},
};
use crate::{
mock::TestRegistrar,
traits::OnSwap,
crowdloan,
};
use sp_keystore::{KeystoreExt, testing::KeyStore};
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic;
type Block = frame_system::mocking::MockBlock;
frame_support::construct_runtime!(
pub enum Test where
Block = Block,
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{
System: frame_system::{Pallet, Call, Config, Storage, Event},
Balances: pallet_balances::{Pallet, Call, Storage, Config, Event},
Crowdloan: crowdloan::{Pallet, Call, Storage, Event},
}
);
parameter_types! {
pub const BlockHashCount: u32 = 250;
}
impl frame_system::Config for Test {
type BaseCallFilter = ();
type BlockWeights = ();
type BlockLength = ();
type DbWeight = ();
type Origin = Origin;
type Call = Call;
type Index = u64;
type BlockNumber = u64;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = u64;
type Lookup = IdentityLookup;
type Header = Header;
type Event = Event;
type BlockHashCount = BlockHashCount;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = pallet_balances::AccountData;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ();
}
parameter_types! {
pub const ExistentialDeposit: u64 = 1;
}
impl pallet_balances::Config for Test {
type Balance = u64;
type Event = Event;
type DustRemoval = ();
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System;
type MaxLocks = ();
type WeightInfo = ();
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
struct BidPlaced {
height: u64,
bidder: u64,
para: ParaId,
first_slot: u64,
last_slot: u64,
amount: u64
}
thread_local! {
static AUCTION: RefCell