pub mod migration;
use crate::{
slot_range::SlotRange,
traits::{Auctioneer, Registrar},
};
use alloc::{vec, vec::Vec};
use codec::{Decode, Encode};
use frame_support::{
ensure,
pallet_prelude::{DispatchResult, Weight},
storage::{child, ChildTriePrefixIterator},
traits::{
Currency, Defensive,
ExistenceRequirement::{self, AllowDeath, KeepAlive},
Get, ReservableCurrency,
},
Identity, PalletId,
};
use frame_system::pallet_prelude::BlockNumberFor;
pub use pallet::*;
use polkadot_primitives::Id as ParaId;
use scale_info::TypeInfo;
use sp_runtime::{
traits::{
AccountIdConversion, CheckedAdd, Hash, IdentifyAccount, One, Saturating, Verify, Zero,
},
MultiSignature, MultiSigner, RuntimeDebug,
};
type CurrencyOf<T> = <<T as Config>::Auctioneer as Auctioneer<BlockNumberFor<T>>>::Currency;
type LeasePeriodOf<T> = <<T as Config>::Auctioneer as Auctioneer<BlockNumberFor<T>>>::LeasePeriod;
type BalanceOf<T> = <CurrencyOf<T> as Currency<<T as frame_system::Config>::AccountId>>::Balance;
type FundIndex = u32;
pub trait WeightInfo {
fn create() -> Weight;
fn contribute() -> Weight;
fn withdraw() -> Weight;
fn refund(k: u32) -> Weight;
fn dissolve() -> Weight;
fn edit() -> Weight;
fn add_memo() -> Weight;
fn on_initialize(n: u32) -> Weight;
fn poke() -> Weight;
}
pub struct TestWeightInfo;
impl WeightInfo for TestWeightInfo {
fn create() -> Weight {
Weight::zero()
}
fn contribute() -> Weight {
Weight::zero()
}
fn withdraw() -> Weight {
Weight::zero()
}
fn refund(_k: u32) -> Weight {
Weight::zero()
}
fn dissolve() -> Weight {
Weight::zero()
}
fn edit() -> Weight {
Weight::zero()
}
fn add_memo() -> Weight {
Weight::zero()
}
fn on_initialize(_n: u32) -> Weight {
Weight::zero()
}
fn poke() -> Weight {
Weight::zero()
}
}
#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub enum LastContribution<BlockNumber> {
Never,
PreEnding(u32),
Ending(BlockNumber),
}
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
#[codec(dumb_trait_bound)]
pub struct FundInfo<AccountId, Balance, BlockNumber, LeasePeriod> {
pub depositor: AccountId,
pub verifier: Option<MultiSigner>,
pub deposit: Balance,
pub raised: Balance,
pub end: BlockNumber,
pub cap: Balance,
pub last_contribution: LastContribution<BlockNumber>,
pub first_period: LeasePeriod,
pub last_period: LeasePeriod,
pub fund_index: FundIndex,
}
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::{ensure_root, ensure_signed, pallet_prelude::*};
const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);
#[pallet::pallet]
#[pallet::without_storage_info]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
#[pallet::constant]
type PalletId: Get<PalletId>;
type SubmissionDeposit: Get<BalanceOf<Self>>;
#[pallet::constant]
type MinContribution: Get<BalanceOf<Self>>;
#[pallet::constant]
type RemoveKeysLimit: Get<u32>;
type Registrar: Registrar<AccountId = Self::AccountId>;
type Auctioneer: Auctioneer<
BlockNumberFor<Self>,
AccountId = Self::AccountId,
LeasePeriod = BlockNumberFor<Self>,
>;
type MaxMemoLength: Get<u8>;
type WeightInfo: WeightInfo;
}
#[pallet::storage]
pub type Funds<T: Config> = StorageMap<
_,
Twox64Concat,
ParaId,
FundInfo<T::AccountId, BalanceOf<T>, BlockNumberFor<T>, LeasePeriodOf<T>>,
>;
#[pallet::storage]
pub type NewRaise<T> = StorageValue<_, Vec<ParaId>, ValueQuery>;
#[pallet::storage]
pub type EndingsCount<T> = StorageValue<_, u32, ValueQuery>;
#[pallet::storage]
pub type NextFundIndex<T> = StorageValue<_, u32, ValueQuery>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
Created { para_id: ParaId },
Contributed { who: T::AccountId, fund_index: ParaId, amount: BalanceOf<T> },
Withdrew { who: T::AccountId, fund_index: ParaId, amount: BalanceOf<T> },
PartiallyRefunded { para_id: ParaId },
AllRefunded { para_id: ParaId },
Dissolved { para_id: ParaId },
HandleBidResult { para_id: ParaId, result: DispatchResult },
Edited { para_id: ParaId },
MemoUpdated { who: T::AccountId, para_id: ParaId, memo: Vec<u8> },
AddedToNewRaise { para_id: ParaId },
}
#[pallet::error]
pub enum Error<T> {
FirstPeriodInPast,
FirstPeriodTooFarInFuture,
LastPeriodBeforeFirstPeriod,
LastPeriodTooFarInFuture,
CannotEndInPast,
EndTooFarInFuture,
Overflow,
ContributionTooSmall,
InvalidParaId,
CapExceeded,
ContributionPeriodOver,
InvalidOrigin,
NotParachain,
LeaseActive,
BidOrLeaseActive,
FundNotEnded,
NoContributions,
NotReadyToDissolve,
InvalidSignature,
MemoTooLarge,
AlreadyInNewRaise,
VrfDelayInProgress,
NoLeasePeriod,
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(num: BlockNumberFor<T>) -> frame_support::weights::Weight {
if let Some((sample, sub_sample)) = T::Auctioneer::auction_status(num).is_ending() {
if sample.is_zero() && sub_sample.is_zero() {
EndingsCount::<T>::mutate(|c| *c += 1);
}
let new_raise = NewRaise::<T>::take();
let new_raise_len = new_raise.len() as u32;
for (fund, para_id) in
new_raise.into_iter().filter_map(|i| Funds::<T>::get(i).map(|f| (f, i)))
{
let result = T::Auctioneer::place_bid(
Self::fund_account_id(fund.fund_index),
para_id,
fund.first_period,
fund.last_period,
fund.raised,
);
Self::deposit_event(Event::<T>::HandleBidResult { para_id, result });
}
T::WeightInfo::on_initialize(new_raise_len)
} else {
T::DbWeight::get().reads(1)
}
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::create())]
pub fn create(
origin: OriginFor<T>,
#[pallet::compact] index: ParaId,
#[pallet::compact] cap: BalanceOf<T>,
#[pallet::compact] first_period: LeasePeriodOf<T>,
#[pallet::compact] last_period: LeasePeriodOf<T>,
#[pallet::compact] end: BlockNumberFor<T>,
verifier: Option<MultiSigner>,
) -> DispatchResult {
let depositor = ensure_signed(origin)?;
let now = frame_system::Pallet::<T>::block_number();
ensure!(first_period <= last_period, Error::<T>::LastPeriodBeforeFirstPeriod);
let last_period_limit = first_period
.checked_add(&((SlotRange::LEASE_PERIODS_PER_SLOT as u32) - 1).into())
.ok_or(Error::<T>::FirstPeriodTooFarInFuture)?;
ensure!(last_period <= last_period_limit, Error::<T>::LastPeriodTooFarInFuture);
ensure!(end > now, Error::<T>::CannotEndInPast);
let (lease_period_at_end, is_first_block) =
T::Auctioneer::lease_period_index(end).ok_or(Error::<T>::NoLeasePeriod)?;
let adjusted_lease_period_at_end = if is_first_block {
lease_period_at_end.saturating_sub(One::one())
} else {
lease_period_at_end
};
ensure!(adjusted_lease_period_at_end <= first_period, Error::<T>::EndTooFarInFuture);
if let Some((current_lease_period, _)) = T::Auctioneer::lease_period_index(now) {
ensure!(first_period >= current_lease_period, Error::<T>::FirstPeriodInPast);
}
ensure!(!Funds::<T>::contains_key(index), Error::<T>::FundNotEnded);
let manager = T::Registrar::manager_of(index).ok_or(Error::<T>::InvalidParaId)?;
ensure!(depositor == manager, Error::<T>::InvalidOrigin);
ensure!(T::Registrar::is_registered(index), Error::<T>::InvalidParaId);
let fund_index = NextFundIndex::<T>::get();
let new_fund_index = fund_index.checked_add(1).ok_or(Error::<T>::Overflow)?;
let deposit = T::SubmissionDeposit::get();
frame_system::Pallet::<T>::inc_providers(&Self::fund_account_id(fund_index));
CurrencyOf::<T>::reserve(&depositor, deposit)?;
Funds::<T>::insert(
index,
FundInfo {
depositor,
verifier,
deposit,
raised: Zero::zero(),
end,
cap,
last_contribution: LastContribution::Never,
first_period,
last_period,
fund_index,
},
);
NextFundIndex::<T>::put(new_fund_index);
Self::deposit_event(Event::<T>::Created { para_id: index });
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::contribute())]
pub fn contribute(
origin: OriginFor<T>,
#[pallet::compact] index: ParaId,
#[pallet::compact] value: BalanceOf<T>,
signature: Option<MultiSignature>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
Self::do_contribute(who, index, value, signature, KeepAlive)
}
#[pallet::call_index(2)]
#[pallet::weight(T::WeightInfo::withdraw())]
pub fn withdraw(
origin: OriginFor<T>,
who: T::AccountId,
#[pallet::compact] index: ParaId,
) -> DispatchResult {
ensure_signed(origin)?;
let mut fund = Funds::<T>::get(index).ok_or(Error::<T>::InvalidParaId)?;
let now = frame_system::Pallet::<T>::block_number();
let fund_account = Self::fund_account_id(fund.fund_index);
Self::ensure_crowdloan_ended(now, &fund_account, &fund)?;
let (balance, _) = Self::contribution_get(fund.fund_index, &who);
ensure!(balance > Zero::zero(), Error::<T>::NoContributions);
CurrencyOf::<T>::transfer(&fund_account, &who, balance, AllowDeath)?;
CurrencyOf::<T>::reactivate(balance);
Self::contribution_kill(fund.fund_index, &who);
fund.raised = fund.raised.saturating_sub(balance);
Funds::<T>::insert(index, &fund);
Self::deposit_event(Event::<T>::Withdrew { who, fund_index: index, amount: balance });
Ok(())
}
#[pallet::call_index(3)]
#[pallet::weight(T::WeightInfo::refund(T::RemoveKeysLimit::get()))]
pub fn refund(
origin: OriginFor<T>,
#[pallet::compact] index: ParaId,
) -> DispatchResultWithPostInfo {
ensure_signed(origin)?;
let mut fund = Funds::<T>::get(index).ok_or(Error::<T>::InvalidParaId)?;
let now = frame_system::Pallet::<T>::block_number();
let fund_account = Self::fund_account_id(fund.fund_index);
Self::ensure_crowdloan_ended(now, &fund_account, &fund)?;
let mut refund_count = 0u32;
let contributions = Self::contribution_iterator(fund.fund_index);
let mut all_refunded = true;
for (who, (balance, _)) in contributions {
if refund_count >= T::RemoveKeysLimit::get() {
all_refunded = false;
break
}
CurrencyOf::<T>::transfer(&fund_account, &who, balance, AllowDeath)?;
CurrencyOf::<T>::reactivate(balance);
Self::contribution_kill(fund.fund_index, &who);
fund.raised = fund.raised.saturating_sub(balance);
refund_count += 1;
}
Funds::<T>::insert(index, &fund);
if all_refunded {
Self::deposit_event(Event::<T>::AllRefunded { para_id: index });
Ok(Some(T::WeightInfo::refund(refund_count)).into())
} else {
Self::deposit_event(Event::<T>::PartiallyRefunded { para_id: index });
Ok(().into())
}
}
#[pallet::call_index(4)]
#[pallet::weight(T::WeightInfo::dissolve())]
pub fn dissolve(origin: OriginFor<T>, #[pallet::compact] index: ParaId) -> DispatchResult {
let who = ensure_signed(origin)?;
let fund = Funds::<T>::get(index).ok_or(Error::<T>::InvalidParaId)?;
let pot = Self::fund_account_id(fund.fund_index);
let now = frame_system::Pallet::<T>::block_number();
let permitted = who == fund.depositor || now >= fund.end;
let can_dissolve = permitted && fund.raised.is_zero();
ensure!(can_dissolve, Error::<T>::NotReadyToDissolve);
debug_assert!(Self::contribution_iterator(fund.fund_index).count().is_zero());
let _imba = CurrencyOf::<T>::make_free_balance_be(&pot, Zero::zero());
let _ = frame_system::Pallet::<T>::dec_providers(&pot).defensive();
CurrencyOf::<T>::unreserve(&fund.depositor, fund.deposit);
Funds::<T>::remove(index);
Self::deposit_event(Event::<T>::Dissolved { para_id: index });
Ok(())
}
#[pallet::call_index(5)]
#[pallet::weight(T::WeightInfo::edit())]
pub fn edit(
origin: OriginFor<T>,
#[pallet::compact] index: ParaId,
#[pallet::compact] cap: BalanceOf<T>,
#[pallet::compact] first_period: LeasePeriodOf<T>,
#[pallet::compact] last_period: LeasePeriodOf<T>,
#[pallet::compact] end: BlockNumberFor<T>,
verifier: Option<MultiSigner>,
) -> DispatchResult {
ensure_root(origin)?;
let fund = Funds::<T>::get(index).ok_or(Error::<T>::InvalidParaId)?;
Funds::<T>::insert(
index,
FundInfo {
depositor: fund.depositor,
verifier,
deposit: fund.deposit,
raised: fund.raised,
end,
cap,
last_contribution: fund.last_contribution,
first_period,
last_period,
fund_index: fund.fund_index,
},
);
Self::deposit_event(Event::<T>::Edited { para_id: index });
Ok(())
}
#[pallet::call_index(6)]
#[pallet::weight(T::WeightInfo::add_memo())]
pub fn add_memo(origin: OriginFor<T>, index: ParaId, memo: Vec<u8>) -> DispatchResult {
let who = ensure_signed(origin)?;
ensure!(memo.len() <= T::MaxMemoLength::get().into(), Error::<T>::MemoTooLarge);
let fund = Funds::<T>::get(index).ok_or(Error::<T>::InvalidParaId)?;
let (balance, _) = Self::contribution_get(fund.fund_index, &who);
ensure!(balance > Zero::zero(), Error::<T>::NoContributions);
Self::contribution_put(fund.fund_index, &who, &balance, &memo);
Self::deposit_event(Event::<T>::MemoUpdated { who, para_id: index, memo });
Ok(())
}
#[pallet::call_index(7)]
#[pallet::weight(T::WeightInfo::poke())]
pub fn poke(origin: OriginFor<T>, index: ParaId) -> DispatchResult {
ensure_signed(origin)?;
let fund = Funds::<T>::get(index).ok_or(Error::<T>::InvalidParaId)?;
ensure!(!fund.raised.is_zero(), Error::<T>::NoContributions);
ensure!(!NewRaise::<T>::get().contains(&index), Error::<T>::AlreadyInNewRaise);
NewRaise::<T>::append(index);
Self::deposit_event(Event::<T>::AddedToNewRaise { para_id: index });
Ok(())
}
#[pallet::call_index(8)]
#[pallet::weight(T::WeightInfo::contribute())]
pub fn contribute_all(
origin: OriginFor<T>,
#[pallet::compact] index: ParaId,
signature: Option<MultiSignature>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let value = CurrencyOf::<T>::free_balance(&who);
Self::do_contribute(who, index, value, signature, AllowDeath)
}
}
}
impl<T: Config> Pallet<T> {
pub fn fund_account_id(index: FundIndex) -> T::AccountId {
T::PalletId::get().into_sub_account_truncating(index)
}
pub fn id_from_index(index: FundIndex) -> 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: FundIndex,
who: &T::AccountId,
balance: &BalanceOf<T>,
memo: &[u8],
) {
who.using_encoded(|b| child::put(&Self::id_from_index(index), b, &(balance, memo)));
}
pub fn contribution_get(index: FundIndex, who: &T::AccountId) -> (BalanceOf<T>, Vec<u8>) {
who.using_encoded(|b| {
child::get_or_default::<(BalanceOf<T>, Vec<u8>)>(&Self::id_from_index(index), b)
})
}
pub fn contribution_kill(index: FundIndex, who: &T::AccountId) {
who.using_encoded(|b| child::kill(&Self::id_from_index(index), b));
}
pub fn crowdloan_kill(index: FundIndex) -> child::KillStorageResult {
#[allow(deprecated)]
child::kill_storage(&Self::id_from_index(index), Some(T::RemoveKeysLimit::get()))
}
pub fn contribution_iterator(
index: FundIndex,
) -> ChildTriePrefixIterator<(T::AccountId, (BalanceOf<T>, Vec<u8>))> {
ChildTriePrefixIterator::<_>::with_prefix_over_key::<Identity>(
&Self::id_from_index(index),
&[],
)
}
fn ensure_crowdloan_ended(
now: BlockNumberFor<T>,
fund_account: &T::AccountId,
fund: &FundInfo<T::AccountId, BalanceOf<T>, BlockNumberFor<T>, LeasePeriodOf<T>>,
) -> sp_runtime::DispatchResult {
let (current_lease_period, _) =
T::Auctioneer::lease_period_index(now).ok_or(Error::<T>::NoLeasePeriod)?;
ensure!(
now >= fund.end || current_lease_period > fund.first_period,
Error::<T>::FundNotEnded
);
ensure!(
CurrencyOf::<T>::free_balance(&fund_account) >= fund.raised,
Error::<T>::BidOrLeaseActive
);
Ok(())
}
fn do_contribute(
who: T::AccountId,
index: ParaId,
value: BalanceOf<T>,
signature: Option<MultiSignature>,
existence: ExistenceRequirement,
) -> DispatchResult {
ensure!(value >= T::MinContribution::get(), Error::<T>::ContributionTooSmall);
let mut fund = Funds::<T>::get(index).ok_or(Error::<T>::InvalidParaId)?;
fund.raised = fund.raised.checked_add(&value).ok_or(Error::<T>::Overflow)?;
ensure!(fund.raised <= fund.cap, Error::<T>::CapExceeded);
let now = frame_system::Pallet::<T>::block_number();
ensure!(now < fund.end, Error::<T>::ContributionPeriodOver);
let now = frame_system::Pallet::<T>::block_number();
let (current_lease_period, _) =
T::Auctioneer::lease_period_index(now).ok_or(Error::<T>::NoLeasePeriod)?;
ensure!(current_lease_period <= fund.first_period, Error::<T>::ContributionPeriodOver);
let fund_account = Self::fund_account_id(fund.fund_index);
ensure!(
!T::Auctioneer::has_won_an_auction(index, &fund_account),
Error::<T>::BidOrLeaseActive
);
ensure!(!T::Auctioneer::auction_status(now).is_vrf(), Error::<T>::VrfDelayInProgress);
let (old_balance, memo) = Self::contribution_get(fund.fund_index, &who);
if let Some(ref verifier) = fund.verifier {
let signature = signature.ok_or(Error::<T>::InvalidSignature)?;
let payload = (index, &who, old_balance, value);
let valid = payload.using_encoded(|encoded| {
signature.verify(encoded, &verifier.clone().into_account())
});
ensure!(valid, Error::<T>::InvalidSignature);
}
CurrencyOf::<T>::transfer(&who, &fund_account, value, existence)?;
CurrencyOf::<T>::deactivate(value);
let balance = old_balance.saturating_add(value);
Self::contribution_put(fund.fund_index, &who, &balance, &memo);
if T::Auctioneer::auction_status(now).is_ending().is_some() {
match fund.last_contribution {
LastContribution::Ending(n) if n == now => {
},
_ => {
NewRaise::<T>::append(index);
fund.last_contribution = LastContribution::Ending(now);
},
}
} else {
let endings_count = EndingsCount::<T>::get();
match fund.last_contribution {
LastContribution::PreEnding(a) if a == endings_count => {
},
_ => {
NewRaise::<T>::append(index);
fund.last_contribution = LastContribution::PreEnding(endings_count);
},
}
}
Funds::<T>::insert(index, &fund);
Self::deposit_event(Event::<T>::Contributed { who, fund_index: index, amount: value });
Ok(())
}
}
impl<T: Config> crate::traits::OnSwap for Pallet<T> {
fn on_swap(one: ParaId, other: ParaId) {
Funds::<T>::mutate(one, |x| Funds::<T>::mutate(other, |y| core::mem::swap(x, y)))
}
}
#[cfg(any(feature = "runtime-benchmarks", test))]
mod crypto {
use alloc::vec::Vec;
use sp_core::ed25519;
use sp_io::crypto::{ed25519_generate, ed25519_sign};
use sp_runtime::{MultiSignature, MultiSigner};
pub fn create_ed25519_pubkey(seed: Vec<u8>) -> 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 frame_support::{
assert_noop, assert_ok, derive_impl, parameter_types,
traits::{OnFinalize, OnInitialize},
};
use polkadot_primitives::Id as ParaId;
use sp_core::H256;
use std::{cell::RefCell, collections::BTreeMap, sync::Arc};
use crate::{
crowdloan,
mock::TestRegistrar,
traits::{AuctionStatus, OnSwap},
};
use polkadot_primitives_test_helpers::{dummy_head_data, dummy_validation_code};
use sp_keystore::{testing::MemoryKeystore, KeystoreExt};
use sp_runtime::{
traits::{BlakeTwo256, IdentityLookup, TrailingZeroInput},
BuildStorage, DispatchResult,
};
type Block = frame_system::mocking::MockBlock<Test>;
frame_support::construct_runtime!(
pub enum Test
{
System: frame_system,
Balances: pallet_balances,
Crowdloan: crowdloan,
}
);
type BlockNumber = u64;
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for Test {
type BaseCallFilter = frame_support::traits::Everything;
type BlockWeights = ();
type BlockLength = ();
type DbWeight = ();
type RuntimeOrigin = RuntimeOrigin;
type RuntimeCall = RuntimeCall;
type Nonce = u64;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = u64;
type Lookup = IdentityLookup<Self::AccountId>;
type Block = Block;
type RuntimeEvent = RuntimeEvent;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = pallet_balances::AccountData<u64>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ();
type OnSetCode = ();
type MaxConsumers = frame_support::traits::ConstU32<16>;
}
#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)]
impl pallet_balances::Config for Test {
type AccountStore = System;
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
struct BidPlaced {
height: u64,
bidder: u64,
para: ParaId,
first_period: u64,
last_period: u64,
amount: u64,
}
thread_local! {
static AUCTION: RefCell<Option<(u64, u64)>> = RefCell::new(None);
static VRF_DELAY: RefCell<u64> = RefCell::new(0);
static ENDING_PERIOD: RefCell<u64> = RefCell::new(5);
static BIDS_PLACED: RefCell<Vec<BidPlaced>> = RefCell::new(Vec::new());
static HAS_WON: RefCell<BTreeMap<(ParaId, u64), bool>> = RefCell::new(BTreeMap::new());
}
#[allow(unused)]
fn set_ending_period(ending_period: u64) {
ENDING_PERIOD.with(|p| *p.borrow_mut() = ending_period);
}
fn auction() -> Option<(u64, u64)> {
AUCTION.with(|p| *p.borrow())
}
fn ending_period() -> u64 {
ENDING_PERIOD.with(|p| *p.borrow())
}
fn bids() -> Vec<BidPlaced> {
BIDS_PLACED.with(|p| p.borrow().clone())
}
fn vrf_delay() -> u64 {
VRF_DELAY.with(|p| *p.borrow())
}
fn set_vrf_delay(delay: u64) {
VRF_DELAY.with(|p| *p.borrow_mut() = delay);
}
fn set_winner(para: ParaId, who: u64, winner: bool) {
let fund = Funds::<Test>::get(para).unwrap();
let account_id = Crowdloan::fund_account_id(fund.fund_index);
if winner {
let ed: u64 = <Test as pallet_balances::Config>::ExistentialDeposit::get();
let free_balance = Balances::free_balance(&account_id);
Balances::reserve(&account_id, free_balance - ed)
.expect("should be able to reserve free balance minus ED");
} else {
let reserved_balance = Balances::reserved_balance(&account_id);
Balances::unreserve(&account_id, reserved_balance);
}
HAS_WON.with(|p| p.borrow_mut().insert((para, who), winner));
}
pub struct TestAuctioneer;
impl Auctioneer<u64> for TestAuctioneer {
type AccountId = u64;
type LeasePeriod = u64;
type Currency = Balances;
fn new_auction(duration: u64, lease_period_index: u64) -> DispatchResult {
let now = System::block_number();
let (current_lease_period, _) =
Self::lease_period_index(now).ok_or("no lease period yet")?;
assert!(lease_period_index >= current_lease_period);
let ending = System::block_number().saturating_add(duration);
AUCTION.with(|p| *p.borrow_mut() = Some((lease_period_index, ending)));
Ok(())
}
fn auction_status(now: u64) -> AuctionStatus<u64> {
let early_end = match auction() {
Some((_, early_end)) => early_end,
None => return AuctionStatus::NotStarted,
};
let after_early_end = match now.checked_sub(early_end) {
Some(after_early_end) => after_early_end,
None => return AuctionStatus::StartingPeriod,
};
let ending_period = ending_period();
if after_early_end < ending_period {
return AuctionStatus::EndingPeriod(after_early_end, 0)
} else {
let after_end = after_early_end - ending_period;
if after_end < vrf_delay() {
return AuctionStatus::VrfDelay(after_end)
} else {
return AuctionStatus::NotStarted
}
}
}
fn place_bid(
bidder: u64,
para: ParaId,
first_period: u64,
last_period: u64,
amount: u64,
) -> DispatchResult {
let height = System::block_number();
BIDS_PLACED.with(|p| {
p.borrow_mut().push(BidPlaced {
height,
bidder,
para,
first_period,
last_period,
amount,
})
});
Ok(())
}
fn lease_period_index(b: BlockNumber) -> Option<(u64, bool)> {
let (lease_period_length, offset) = Self::lease_period_length();
let b = b.checked_sub(offset)?;
let lease_period = b / lease_period_length;
let first_block = (b % lease_period_length).is_zero();
Some((lease_period, first_block))
}
fn lease_period_length() -> (u64, u64) {
(20, 0)
}
fn has_won_an_auction(para: ParaId, bidder: &u64) -> bool {
HAS_WON.with(|p| *p.borrow().get(&(para, *bidder)).unwrap_or(&false))
}
}
parameter_types! {
pub const SubmissionDeposit: u64 = 1;
pub const MinContribution: u64 = 10;
pub const CrowdloanPalletId: PalletId = PalletId(*b"py/cfund");
pub const RemoveKeysLimit: u32 = 10;
pub const MaxMemoLength: u8 = 32;
}
impl Config for Test {
type RuntimeEvent = RuntimeEvent;
type SubmissionDeposit = SubmissionDeposit;
type MinContribution = MinContribution;
type PalletId = CrowdloanPalletId;
type RemoveKeysLimit = RemoveKeysLimit;
type Registrar = TestRegistrar<Test>;
type Auctioneer = TestAuctioneer;
type MaxMemoLength = MaxMemoLength;
type WeightInfo = crate::crowdloan::TestWeightInfo;
}
use pallet_balances::Error as BalancesError;
pub fn new_test_ext() -> sp_io::TestExternalities {
let mut t = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
pallet_balances::GenesisConfig::<Test> {
balances: vec![(1, 1000), (2, 2000), (3, 3000), (4, 4000)],
}
.assimilate_storage(&mut t)
.unwrap();
let keystore = MemoryKeystore::new();
let mut t: sp_io::TestExternalities = t.into();
t.register_extension(KeystoreExt(Arc::new(keystore)));
t
}
fn new_para() -> ParaId {
for i in 0.. {
let para: ParaId = i.into();
if TestRegistrar::<Test>::is_registered(para) {
continue
}
assert_ok!(TestRegistrar::<Test>::register(
1,
para,
dummy_head_data(),
dummy_validation_code()
));
return para
}
unreachable!()
}
fn run_to_block(n: u64) {
while System::block_number() < n {
Crowdloan::on_finalize(System::block_number());
Balances::on_finalize(System::block_number());
System::on_finalize(System::block_number());
System::set_block_number(System::block_number() + 1);
System::on_initialize(System::block_number());
Balances::on_initialize(System::block_number());
Crowdloan::on_initialize(System::block_number());
}
}
fn last_event() -> RuntimeEvent {
System::events().pop().expect("RuntimeEvent expected").event
}
#[test]
fn basic_setup_works() {
new_test_ext().execute_with(|| {
assert_eq!(System::block_number(), 0);
assert_eq!(crowdloan::Funds::<Test>::get(ParaId::from(0)), None);
let empty: Vec<ParaId> = Vec::new();
assert_eq!(crowdloan::NewRaise::<Test>::get(), empty);
assert_eq!(Crowdloan::contribution_get(0u32, &1).0, 0);
assert_eq!(crowdloan::EndingsCount::<Test>::get(), 0);
assert_ok!(TestAuctioneer::new_auction(5, 0));
assert_eq!(bids(), vec![]);
assert_ok!(TestAuctioneer::place_bid(1, 2.into(), 0, 3, 6));
let b = BidPlaced {
height: 0,
bidder: 1,
para: 2.into(),
first_period: 0,
last_period: 3,
amount: 6,
};
assert_eq!(bids(), vec![b]);
assert_eq!(TestAuctioneer::auction_status(4), AuctionStatus::<u64>::StartingPeriod);
assert_eq!(TestAuctioneer::auction_status(5), AuctionStatus::<u64>::EndingPeriod(0, 0));
assert_eq!(TestAuctioneer::auction_status(9), AuctionStatus::<u64>::EndingPeriod(4, 0));
assert_eq!(TestAuctioneer::auction_status(11), AuctionStatus::<u64>::NotStarted);
});
}
#[test]
fn create_works() {
new_test_ext().execute_with(|| {
let para = new_para();
assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 4, 9, None));
let fund_info = FundInfo {
depositor: 1,
verifier: None,
deposit: 1,
raised: 0,
end: 9,
cap: 1000,
last_contribution: LastContribution::Never,
first_period: 1,
last_period: 4,
fund_index: 0,
};
assert_eq!(crowdloan::Funds::<Test>::get(para), Some(fund_info));
assert_eq!(Balances::free_balance(1), 999);
assert_eq!(Balances::reserved_balance(1), 1);
let empty: Vec<ParaId> = Vec::new();
assert_eq!(crowdloan::NewRaise::<Test>::get(), empty);
});
}
#[test]
fn create_with_verifier_works() {
new_test_ext().execute_with(|| {
let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec());
let para = new_para();
assert_ok!(Crowdloan::create(
RuntimeOrigin::signed(1),
para,
1000,
1,
4,
9,
Some(pubkey.clone())
));
let fund_info = FundInfo {
depositor: 1,
verifier: Some(pubkey),
deposit: 1,
raised: 0,
end: 9,
cap: 1000,
last_contribution: LastContribution::Never,
first_period: 1,
last_period: 4,
fund_index: 0,
};
assert_eq!(crowdloan::Funds::<Test>::get(ParaId::from(0)), Some(fund_info));
assert_eq!(Balances::free_balance(1), 999);
assert_eq!(Balances::reserved_balance(1), 1);
let empty: Vec<ParaId> = Vec::new();
assert_eq!(crowdloan::NewRaise::<Test>::get(), empty);
});
}
#[test]
fn create_handles_basic_errors() {
new_test_ext().execute_with(|| {
let para = new_para();
let e = Error::<Test>::InvalidParaId;
assert_noop!(
Crowdloan::create(RuntimeOrigin::signed(1), 1.into(), 1000, 1, 4, 9, None),
e
);
let e = Error::<Test>::LastPeriodBeforeFirstPeriod;
assert_noop!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 4, 1, 9, None), e);
let e = Error::<Test>::LastPeriodTooFarInFuture;
assert_noop!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 9, 9, None), e);
assert_ok!(TestRegistrar::<Test>::register(
1337,
ParaId::from(1234),
dummy_head_data(),
dummy_validation_code()
));
let e = BalancesError::<Test, _>::InsufficientBalance;
assert_noop!(
Crowdloan::create(
RuntimeOrigin::signed(1337),
ParaId::from(1234),
1000,
1,
3,
9,
None
),
e
);
assert_noop!(
Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 4, 41, None),
Error::<Test>::EndTooFarInFuture
);
});
}
#[test]
fn contribute_works() {
new_test_ext().execute_with(|| {
let para = new_para();
let index = NextFundIndex::<Test>::get();
assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 4, 9, None));
assert_eq!(Crowdloan::contribution_get(u32::from(para), &1).0, 0);
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(1), para, 49, None));
assert_eq!(Balances::free_balance(1), 950);
assert_eq!(Crowdloan::contribution_get(u32::from(para), &1).0, 49);
assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(index)), 49);
assert_eq!(crowdloan::NewRaise::<Test>::get(), vec![para]);
let fund = crowdloan::Funds::<Test>::get(para).unwrap();
assert_eq!(fund.last_contribution, LastContribution::PreEnding(0));
assert_eq!(fund.raised, 49);
});
}
#[test]
fn contribute_with_verifier_works() {
new_test_ext().execute_with(|| {
let para = new_para();
let index = NextFundIndex::<Test>::get();
let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec());
assert_ok!(Crowdloan::create(
RuntimeOrigin::signed(1),
para,
1000,
1,
4,
9,
Some(pubkey.clone())
));
assert_eq!(Crowdloan::contribution_get(u32::from(para), &1).0, 0);
assert_noop!(
Crowdloan::contribute(RuntimeOrigin::signed(1), para, 49, None),
Error::<Test>::InvalidSignature
);
let payload = (0u32, 1u64, 0u64, 49u64);
let valid_signature =
crypto::create_ed25519_signature(&payload.encode(), pubkey.clone());
let invalid_signature =
MultiSignature::decode(&mut TrailingZeroInput::zeroes()).unwrap();
assert_noop!(
Crowdloan::contribute(RuntimeOrigin::signed(1), para, 49, Some(invalid_signature)),
Error::<Test>::InvalidSignature
);
assert_noop!(
Crowdloan::contribute(
RuntimeOrigin::signed(1),
para,
50,
Some(valid_signature.clone())
),
Error::<Test>::InvalidSignature
);
assert_noop!(
Crowdloan::contribute(
RuntimeOrigin::signed(2),
para,
49,
Some(valid_signature.clone())
),
Error::<Test>::InvalidSignature
);
assert_ok!(Crowdloan::contribute(
RuntimeOrigin::signed(1),
para,
49,
Some(valid_signature.clone())
));
assert_noop!(
Crowdloan::contribute(RuntimeOrigin::signed(1), para, 49, Some(valid_signature)),
Error::<Test>::InvalidSignature
);
let payload_2 = (0u32, 1u64, 49u64, 10u64);
let valid_signature_2 = crypto::create_ed25519_signature(&payload_2.encode(), pubkey);
assert_ok!(Crowdloan::contribute(
RuntimeOrigin::signed(1),
para,
10,
Some(valid_signature_2)
));
assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(index)), 59);
let fund = crowdloan::Funds::<Test>::get(para).unwrap();
assert_eq!(fund.raised, 59);
});
}
#[test]
fn contribute_handles_basic_errors() {
new_test_ext().execute_with(|| {
let para = new_para();
assert_noop!(
Crowdloan::contribute(RuntimeOrigin::signed(1), para, 49, None),
Error::<Test>::InvalidParaId
);
assert_noop!(
Crowdloan::contribute(RuntimeOrigin::signed(1), para, 9, None),
Error::<Test>::ContributionTooSmall
);
assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 4, 9, None));
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(1), para, 101, None));
assert_noop!(
Crowdloan::contribute(RuntimeOrigin::signed(2), para, 900, None),
Error::<Test>::CapExceeded
);
run_to_block(10);
assert_noop!(
Crowdloan::contribute(RuntimeOrigin::signed(1), para, 49, None),
Error::<Test>::ContributionPeriodOver
);
let para_2 = new_para();
let index = NextFundIndex::<Test>::get();
assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_2, 1000, 1, 4, 40, None));
let crowdloan_account = Crowdloan::fund_account_id(index);
set_winner(para_2, crowdloan_account, true);
assert_noop!(
Crowdloan::contribute(RuntimeOrigin::signed(1), para_2, 49, None),
Error::<Test>::BidOrLeaseActive
);
let para_3 = new_para();
assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_3, 1000, 1, 4, 40, None));
run_to_block(40);
let now = System::block_number();
assert_eq!(TestAuctioneer::lease_period_index(now).unwrap().0, 2);
assert_noop!(
Crowdloan::contribute(RuntimeOrigin::signed(1), para_3, 49, None),
Error::<Test>::ContributionPeriodOver
);
});
}
#[test]
fn cannot_contribute_during_vrf() {
new_test_ext().execute_with(|| {
set_vrf_delay(5);
let para = new_para();
let first_period = 1;
let last_period = 4;
assert_ok!(TestAuctioneer::new_auction(5, 0));
assert_ok!(Crowdloan::create(
RuntimeOrigin::signed(1),
para,
1000,
first_period,
last_period,
20,
None
));
run_to_block(8);
assert!(TestAuctioneer::auction_status(System::block_number()).is_ending().is_some());
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 250, None));
run_to_block(10);
assert!(TestAuctioneer::auction_status(System::block_number()).is_vrf());
assert_noop!(
Crowdloan::contribute(RuntimeOrigin::signed(2), para, 250, None),
Error::<Test>::VrfDelayInProgress
);
run_to_block(15);
assert!(!TestAuctioneer::auction_status(System::block_number()).is_in_progress());
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 250, None));
})
}
#[test]
fn bidding_works() {
new_test_ext().execute_with(|| {
let para = new_para();
let index = NextFundIndex::<Test>::get();
let first_period = 1;
let last_period = 4;
assert_ok!(TestAuctioneer::new_auction(5, 0));
assert_ok!(Crowdloan::create(
RuntimeOrigin::signed(1),
para,
1000,
first_period,
last_period,
9,
None
));
let bidder = Crowdloan::fund_account_id(index);
run_to_block(1);
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None));
run_to_block(3);
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 150, None));
run_to_block(5);
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(4), para, 200, None));
run_to_block(8);
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 250, None));
run_to_block(10);
assert_eq!(
bids(),
vec![
BidPlaced { height: 5, amount: 250, bidder, para, first_period, last_period },
BidPlaced { height: 6, amount: 450, bidder, para, first_period, last_period },
BidPlaced { height: 9, amount: 700, bidder, para, first_period, last_period },
]
);
assert_eq!(crowdloan::EndingsCount::<Test>::get(), 1);
});
}
#[test]
fn withdraw_from_failed_works() {
new_test_ext().execute_with(|| {
let para = new_para();
let index = NextFundIndex::<Test>::get();
assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None));
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None));
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 50, None));
run_to_block(10);
let account_id = Crowdloan::fund_account_id(index);
assert_eq!(Balances::reserved_balance(&account_id), 0);
assert_eq!(Balances::free_balance(&account_id), 150);
assert_eq!(Balances::free_balance(2), 1900);
assert_eq!(Balances::free_balance(3), 2950);
assert_ok!(Crowdloan::withdraw(RuntimeOrigin::signed(2), 2, para));
assert_eq!(Balances::free_balance(&account_id), 50);
assert_eq!(Balances::free_balance(2), 2000);
assert_ok!(Crowdloan::withdraw(RuntimeOrigin::signed(2), 3, para));
assert_eq!(Balances::free_balance(&account_id), 0);
assert_eq!(Balances::free_balance(3), 3000);
});
}
#[test]
fn withdraw_cannot_be_griefed() {
new_test_ext().execute_with(|| {
let para = new_para();
let index = NextFundIndex::<Test>::get();
let issuance = Balances::total_issuance();
assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None));
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None));
run_to_block(10);
let account_id = Crowdloan::fund_account_id(index);
assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(1), account_id, 10));
assert_eq!(Balances::free_balance(&account_id), 110);
assert_eq!(Balances::free_balance(2), 1900);
assert_ok!(Crowdloan::withdraw(RuntimeOrigin::signed(2), 2, para));
assert_eq!(Balances::free_balance(2), 2000);
assert_eq!(Balances::free_balance(&account_id), 10);
assert_ok!(Crowdloan::dissolve(RuntimeOrigin::signed(1), para));
assert_eq!(Balances::free_balance(&account_id), 0);
assert_eq!(Balances::total_issuance(), issuance - 10);
});
}
#[test]
fn refund_works() {
new_test_ext().execute_with(|| {
let para = new_para();
let index = NextFundIndex::<Test>::get();
let account_id = Crowdloan::fund_account_id(index);
assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None));
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(1), para, 100, None));
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 200, None));
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 300, None));
assert_eq!(Balances::free_balance(account_id), 600);
assert_noop!(
Crowdloan::refund(RuntimeOrigin::signed(1337), para),
Error::<Test>::FundNotEnded,
);
run_to_block(10);
assert_ok!(Crowdloan::refund(RuntimeOrigin::signed(1337), para));
assert_eq!(Balances::free_balance(account_id), 0);
assert_eq!(Balances::free_balance(1), 1000 - 1);
assert_eq!(Balances::free_balance(2), 2000);
assert_eq!(Balances::free_balance(3), 3000);
});
}
#[test]
fn multiple_refund_works() {
new_test_ext().execute_with(|| {
let para = new_para();
let index = NextFundIndex::<Test>::get();
let account_id = Crowdloan::fund_account_id(index);
assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 100000, 1, 1, 9, None));
for i in 1..=RemoveKeysLimit::get() * 2 {
Balances::make_free_balance_be(&i.into(), (1000 * i).into());
assert_ok!(Crowdloan::contribute(
RuntimeOrigin::signed(i.into()),
para,
(i * 100).into(),
None
));
}
assert_eq!(Balances::free_balance(account_id), 21000);
run_to_block(10);
assert_ok!(Crowdloan::refund(RuntimeOrigin::signed(1337), para));
assert_eq!(
last_event(),
super::Event::<Test>::PartiallyRefunded { para_id: para }.into()
);
assert!(!Balances::free_balance(account_id).is_zero());
assert_ok!(Crowdloan::refund(RuntimeOrigin::signed(1337), para));
assert_eq!(last_event(), super::Event::<Test>::AllRefunded { para_id: para }.into());
assert_eq!(Balances::free_balance(account_id), 0);
for i in 1..=RemoveKeysLimit::get() * 2 {
assert_eq!(Balances::free_balance(&i.into()), i as u64 * 1000);
}
});
}
#[test]
fn refund_and_dissolve_works() {
new_test_ext().execute_with(|| {
let para = new_para();
let issuance = Balances::total_issuance();
assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None));
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None));
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 50, None));
run_to_block(10);
assert_ok!(Crowdloan::refund(RuntimeOrigin::signed(2), para));
assert_ok!(Crowdloan::dissolve(RuntimeOrigin::signed(1), para));
assert_eq!(Balances::free_balance(1), 1000);
assert_eq!(Balances::free_balance(2), 2000);
assert_eq!(Balances::free_balance(3), 3000);
assert_eq!(Balances::total_issuance(), issuance);
});
}
#[test]
fn dissolve_provider_refs_total_issuance_works() {
new_test_ext().execute_with(|| {
let para = new_para();
let issuance = Balances::total_issuance();
assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None));
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None));
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 50, None));
run_to_block(10);
{
let fund = crowdloan::Funds::<Test>::get(para).unwrap();
let pot = Crowdloan::fund_account_id(fund.fund_index);
System::dec_providers(&pot).unwrap();
assert_eq!(System::providers(&pot), 1);
}
assert_ok!(Crowdloan::refund(RuntimeOrigin::signed(2), para));
assert_ok!(Crowdloan::dissolve(RuntimeOrigin::signed(1), para));
assert_eq!(Balances::free_balance(1), 1000);
assert_eq!(Balances::free_balance(2), 2000);
assert_eq!(Balances::free_balance(3), 3000);
assert_eq!(Balances::total_issuance(), issuance);
});
}
#[test]
fn dissolve_works() {
new_test_ext().execute_with(|| {
let para = new_para();
let issuance = Balances::total_issuance();
assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None));
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None));
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 50, None));
assert_noop!(
Crowdloan::dissolve(RuntimeOrigin::signed(1), para),
Error::<Test>::NotReadyToDissolve
);
run_to_block(10);
set_winner(para, 1, true);
assert_noop!(
Crowdloan::dissolve(RuntimeOrigin::signed(1), para),
Error::<Test>::NotReadyToDissolve
);
set_winner(para, 1, false);
assert_noop!(
Crowdloan::dissolve(RuntimeOrigin::signed(1), para),
Error::<Test>::NotReadyToDissolve
);
assert_ok!(Crowdloan::refund(RuntimeOrigin::signed(2), para));
assert_ok!(Crowdloan::dissolve(RuntimeOrigin::signed(1), para));
assert_eq!(Balances::free_balance(1), 1000);
assert_eq!(Balances::free_balance(2), 2000);
assert_eq!(Balances::free_balance(3), 3000);
assert_eq!(Balances::total_issuance(), issuance);
});
}
#[test]
fn withdraw_from_finished_works() {
new_test_ext().execute_with(|| {
let ed: u64 = <Test as pallet_balances::Config>::ExistentialDeposit::get();
assert_eq!(ed, 1);
let para = new_para();
let index = NextFundIndex::<Test>::get();
let account_id = Crowdloan::fund_account_id(index);
assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None));
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None));
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 50, None));
assert_ok!(Balances::reserve(&account_id, 149));
run_to_block(19);
assert_noop!(
Crowdloan::withdraw(RuntimeOrigin::signed(2), 2, para),
Error::<Test>::BidOrLeaseActive
);
run_to_block(20);
Balances::unreserve(&account_id, 150);
assert_eq!(Balances::reserved_balance(&account_id), 0);
assert_eq!(Balances::free_balance(&account_id), 150);
assert_eq!(Balances::free_balance(2), 1900);
assert_eq!(Balances::free_balance(3), 2950);
assert_ok!(Crowdloan::withdraw(RuntimeOrigin::signed(2), 2, para));
assert_eq!(Balances::free_balance(&account_id), 50);
assert_eq!(Balances::free_balance(2), 2000);
assert_ok!(Crowdloan::withdraw(RuntimeOrigin::signed(2), 3, para));
assert_eq!(Balances::free_balance(&account_id), 0);
assert_eq!(Balances::free_balance(3), 3000);
});
}
#[test]
fn on_swap_works() {
new_test_ext().execute_with(|| {
let para_1 = new_para();
let para_2 = new_para();
assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_1, 1000, 1, 1, 9, None));
assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_2, 1000, 1, 1, 9, None));
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para_1, 100, None));
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para_2, 50, None));
assert_eq!(Funds::<Test>::get(para_1).unwrap().raised, 100);
assert_eq!(Funds::<Test>::get(para_2).unwrap().raised, 50);
Crowdloan::on_swap(para_1, para_2);
assert_eq!(Funds::<Test>::get(para_2).unwrap().raised, 100);
assert_eq!(Funds::<Test>::get(para_1).unwrap().raised, 50);
});
}
#[test]
fn cannot_create_fund_when_already_active() {
new_test_ext().execute_with(|| {
let para_1 = new_para();
assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_1, 1000, 1, 1, 9, None));
assert_noop!(
Crowdloan::create(RuntimeOrigin::signed(1), para_1, 1000, 1, 1, 9, None),
Error::<Test>::FundNotEnded,
);
});
}
#[test]
fn edit_works() {
new_test_ext().execute_with(|| {
let para_1 = new_para();
assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_1, 1000, 1, 1, 9, None));
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para_1, 100, None));
let old_crowdloan = crowdloan::Funds::<Test>::get(para_1).unwrap();
assert_ok!(Crowdloan::edit(RuntimeOrigin::root(), para_1, 1234, 2, 3, 4, None));
let new_crowdloan = crowdloan::Funds::<Test>::get(para_1).unwrap();
assert_eq!(old_crowdloan.depositor, new_crowdloan.depositor);
assert_eq!(old_crowdloan.deposit, new_crowdloan.deposit);
assert_eq!(old_crowdloan.raised, new_crowdloan.raised);
assert!(old_crowdloan.cap != new_crowdloan.cap);
assert!(old_crowdloan.first_period != new_crowdloan.first_period);
assert!(old_crowdloan.last_period != new_crowdloan.last_period);
});
}
#[test]
fn add_memo_works() {
new_test_ext().execute_with(|| {
let para_1 = new_para();
assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_1, 1000, 1, 1, 9, None));
assert_noop!(
Crowdloan::add_memo(RuntimeOrigin::signed(1), para_1, b"hello, world".to_vec()),
Error::<Test>::NoContributions,
);
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(1), para_1, 100, None));
assert_eq!(Crowdloan::contribution_get(0u32, &1), (100, vec![]));
assert_noop!(
Crowdloan::add_memo(RuntimeOrigin::signed(1), para_1, vec![123; 123]),
Error::<Test>::MemoTooLarge,
);
assert_ok!(Crowdloan::add_memo(
RuntimeOrigin::signed(1),
para_1,
b"hello, world".to_vec()
));
assert_eq!(Crowdloan::contribution_get(0u32, &1), (100, b"hello, world".to_vec()));
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(1), para_1, 100, None));
assert_eq!(Crowdloan::contribution_get(0u32, &1), (200, b"hello, world".to_vec()));
});
}
#[test]
fn poke_works() {
new_test_ext().execute_with(|| {
let para_1 = new_para();
assert_ok!(TestAuctioneer::new_auction(5, 0));
assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_1, 1000, 1, 1, 9, None));
assert_noop!(
Crowdloan::poke(RuntimeOrigin::signed(1), para_1),
Error::<Test>::NoContributions
);
assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para_1, 100, None));
run_to_block(6);
assert_ok!(Crowdloan::poke(RuntimeOrigin::signed(1), para_1));
assert_eq!(crowdloan::NewRaise::<Test>::get(), vec![para_1]);
assert_noop!(
Crowdloan::poke(RuntimeOrigin::signed(1), para_1),
Error::<Test>::AlreadyInNewRaise
);
});
}
}
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking {
use super::{Pallet as Crowdloan, *};
use frame_support::{assert_ok, traits::OnInitialize};
use frame_system::RawOrigin;
use polkadot_runtime_parachains::paras;
use sp_core::crypto::UncheckedFrom;
use sp_runtime::traits::{Bounded, CheckedSub};
use frame_benchmarking::{account, benchmarks, whitelisted_caller};
fn assert_last_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
let events = frame_system::Pallet::<T>::events();
let system_event: <T as frame_system::Config>::RuntimeEvent = generic_event.into();
let frame_system::EventRecord { event, .. } = &events[events.len() - 1];
assert_eq!(event, &system_event);
}
fn create_fund<T: Config + paras::Config>(id: u32, end: BlockNumberFor<T>) -> ParaId {
let cap = BalanceOf::<T>::max_value();
let (_, offset) = T::Auctioneer::lease_period_length();
frame_system::Pallet::<T>::set_block_number(offset);
let now = frame_system::Pallet::<T>::block_number();
let (lease_period_index, _) = T::Auctioneer::lease_period_index(now).unwrap_or_default();
let first_period = lease_period_index;
let last_period =
lease_period_index + ((SlotRange::LEASE_PERIODS_PER_SLOT as u32) - 1).into();
let para_id = id.into();
let caller = account("fund_creator", id, 0);
CurrencyOf::<T>::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec());
let head_data = T::Registrar::worst_head_data();
let validation_code = T::Registrar::worst_validation_code();
assert_ok!(T::Registrar::register(
caller.clone(),
para_id,
head_data,
validation_code.clone()
));
assert_ok!(paras::Pallet::<T>::add_trusted_validation_code(
frame_system::Origin::<T>::Root.into(),
validation_code,
));
T::Registrar::execute_pending_transitions();
assert_ok!(Crowdloan::<T>::create(
RawOrigin::Signed(caller).into(),
para_id,
cap,
first_period,
last_period,
end,
Some(pubkey)
));
para_id
}
fn contribute_fund<T: Config>(who: &T::AccountId, index: ParaId) {
CurrencyOf::<T>::make_free_balance_be(&who, BalanceOf::<T>::max_value());
let value = T::MinContribution::get();
let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec());
let payload = (index, &who, BalanceOf::<T>::default(), value);
let sig = crypto::create_ed25519_signature(&payload.encode(), pubkey);
assert_ok!(Crowdloan::<T>::contribute(
RawOrigin::Signed(who.clone()).into(),
index,
value,
Some(sig)
));
}
benchmarks! {
where_clause { where T: paras::Config }
create {
let para_id = ParaId::from(1_u32);
let cap = BalanceOf::<T>::max_value();
let first_period = 0u32.into();
let last_period = 3u32.into();
let (lpl, offset) = T::Auctioneer::lease_period_length();
let end = lpl + offset;
let caller: T::AccountId = whitelisted_caller();
let head_data = T::Registrar::worst_head_data();
let validation_code = T::Registrar::worst_validation_code();
let verifier = MultiSigner::unchecked_from(account::<[u8; 32]>("verifier", 0, 0));
CurrencyOf::<T>::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
T::Registrar::register(caller.clone(), para_id, head_data, validation_code.clone())?;
assert_ok!(paras::Pallet::<T>::add_trusted_validation_code(
frame_system::Origin::<T>::Root.into(),
validation_code,
));
T::Registrar::execute_pending_transitions();
}: _(RawOrigin::Signed(caller), para_id, cap, first_period, last_period, end, Some(verifier))
verify {
assert_last_event::<T>(Event::<T>::Created { para_id }.into())
}
contribute {
let (lpl, offset) = T::Auctioneer::lease_period_length();
let end = lpl + offset;
let fund_index = create_fund::<T>(1, end);
let caller: T::AccountId = whitelisted_caller();
let contribution = T::MinContribution::get();
CurrencyOf::<T>::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
assert!(NewRaise::<T>::get().is_empty());
let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec());
let payload = (fund_index, &caller, BalanceOf::<T>::default(), contribution);
let sig = crypto::create_ed25519_signature(&payload.encode(), pubkey);
}: _(RawOrigin::Signed(caller.clone()), fund_index, contribution, Some(sig))
verify {
assert!(!NewRaise::<T>::get().is_empty());
assert_last_event::<T>(Event::<T>::Contributed { who: caller, fund_index, amount: contribution }.into());
}
withdraw {
let (lpl, offset) = T::Auctioneer::lease_period_length();
let end = lpl + offset;
let fund_index = create_fund::<T>(1337, end);
let caller: T::AccountId = whitelisted_caller();
let contributor = account("contributor", 0, 0);
contribute_fund::<T>(&contributor, fund_index);
frame_system::Pallet::<T>::set_block_number(BlockNumberFor::<T>::max_value());
}: _(RawOrigin::Signed(caller), contributor.clone(), fund_index)
verify {
assert_last_event::<T>(Event::<T>::Withdrew { who: contributor, fund_index, amount: T::MinContribution::get() }.into());
}
#[skip_meta]
refund {
let k in 0 .. T::RemoveKeysLimit::get();
let (lpl, offset) = T::Auctioneer::lease_period_length();
let end = lpl + offset;
let fund_index = create_fund::<T>(1337, end);
for i in 0 .. k {
contribute_fund::<T>(&account("contributor", i, 0), fund_index);
}
let caller: T::AccountId = whitelisted_caller();
frame_system::Pallet::<T>::set_block_number(BlockNumberFor::<T>::max_value());
}: _(RawOrigin::Signed(caller), fund_index)
verify {
assert_last_event::<T>(Event::<T>::AllRefunded { para_id: fund_index }.into());
}
dissolve {
let (lpl, offset) = T::Auctioneer::lease_period_length();
let end = lpl + offset;
let fund_index = create_fund::<T>(1337, end);
let caller: T::AccountId = whitelisted_caller();
frame_system::Pallet::<T>::set_block_number(BlockNumberFor::<T>::max_value());
}: _(RawOrigin::Signed(caller.clone()), fund_index)
verify {
assert_last_event::<T>(Event::<T>::Dissolved { para_id: fund_index }.into());
}
edit {
let para_id = ParaId::from(1_u32);
let cap = BalanceOf::<T>::max_value();
let first_period = 0u32.into();
let last_period = 3u32.into();
let (lpl, offset) = T::Auctioneer::lease_period_length();
let end = lpl + offset;
let caller: T::AccountId = whitelisted_caller();
let head_data = T::Registrar::worst_head_data();
let validation_code = T::Registrar::worst_validation_code();
let verifier = MultiSigner::unchecked_from(account::<[u8; 32]>("verifier", 0, 0));
CurrencyOf::<T>::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
T::Registrar::register(caller.clone(), para_id, head_data, validation_code.clone())?;
assert_ok!(paras::Pallet::<T>::add_trusted_validation_code(
frame_system::Origin::<T>::Root.into(),
validation_code,
));
T::Registrar::execute_pending_transitions();
Crowdloan::<T>::create(
RawOrigin::Signed(caller).into(),
para_id, cap, first_period, last_period, end, Some(verifier.clone()),
)?;
}: _(RawOrigin::Root, para_id, cap, first_period, last_period, end, Some(verifier))
verify {
assert_last_event::<T>(Event::<T>::Edited { para_id }.into())
}
add_memo {
let (lpl, offset) = T::Auctioneer::lease_period_length();
let end = lpl + offset;
let fund_index = create_fund::<T>(1, end);
let caller: T::AccountId = whitelisted_caller();
contribute_fund::<T>(&caller, fund_index);
let worst_memo = vec![42; T::MaxMemoLength::get().into()];
}: _(RawOrigin::Signed(caller.clone()), fund_index, worst_memo.clone())
verify {
let fund = Funds::<T>::get(fund_index).expect("fund was created...");
assert_eq!(
Crowdloan::<T>::contribution_get(fund.fund_index, &caller),
(T::MinContribution::get(), worst_memo),
);
}
poke {
let (lpl, offset) = T::Auctioneer::lease_period_length();
let end = lpl + offset;
let fund_index = create_fund::<T>(1, end);
let caller: T::AccountId = whitelisted_caller();
contribute_fund::<T>(&caller, fund_index);
NewRaise::<T>::kill();
assert!(NewRaise::<T>::get().is_empty());
}: _(RawOrigin::Signed(caller), fund_index)
verify {
assert!(!NewRaise::<T>::get().is_empty());
assert_last_event::<T>(Event::<T>::AddedToNewRaise { para_id: fund_index }.into())
}
on_initialize {
let n in 2 .. 100;
let (lpl, offset) = T::Auctioneer::lease_period_length();
let end_block = lpl + offset - 1u32.into();
let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec());
for i in 0 .. n {
let fund_index = create_fund::<T>(i, end_block);
let contributor: T::AccountId = account("contributor", i, 0);
let contribution = T::MinContribution::get() * (i + 1).into();
let payload = (fund_index, &contributor, BalanceOf::<T>::default(), contribution);
let sig = crypto::create_ed25519_signature(&payload.encode(), pubkey.clone());
CurrencyOf::<T>::make_free_balance_be(&contributor, BalanceOf::<T>::max_value());
Crowdloan::<T>::contribute(RawOrigin::Signed(contributor).into(), fund_index, contribution, Some(sig))?;
}
let now = frame_system::Pallet::<T>::block_number();
let (lease_period_index, _) = T::Auctioneer::lease_period_index(now).unwrap_or_default();
let duration = end_block
.checked_sub(&frame_system::Pallet::<T>::block_number())
.ok_or("duration of auction less than zero")?;
T::Auctioneer::new_auction(duration, lease_period_index)?;
assert_eq!(T::Auctioneer::auction_status(end_block).is_ending(), Some((0u32.into(), 0u32.into())));
assert_eq!(NewRaise::<T>::get().len(), n as usize);
let old_endings_count = EndingsCount::<T>::get();
}: {
Crowdloan::<T>::on_initialize(end_block);
} verify {
assert_eq!(EndingsCount::<T>::get(), old_endings_count + 1);
assert_last_event::<T>(Event::<T>::HandleBidResult { para_id: (n - 1).into(), result: Ok(()) }.into());
}
impl_benchmark_test_suite!(
Crowdloan,
crate::integration_tests::new_test_ext_with_offset(10),
crate::integration_tests::Test,
);
}
}