Newer
Older
// 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 <http://www.gnu.org/licenses/>.
//!
//! The point of this module is to allow parachain projects to offer the ability to help fund a
//! deposit for the parachain. When the crowdloan has ended, the funds are returned.
//! 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 can be dissolved.
//! 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 will get a refund of their contributions from completed funds before the crowdloan
//! can be dissolved.
//! Funds may accept contributions at any point before their success or end. 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.
//!
//! Successful funds remain tracked (in the `Funds` storage item and the associated child trie) as long as
//! the parachain remains active. Users can withdraw their funds once the slot is completed and funds are
//! returned to the crowdloan account.
decl_module, decl_storage, decl_event, decl_error, ensure, Identity,
storage::{child, ChildTriePrefixIterator},
Currency, ReservableCurrency, Get, ExistenceRequirement::AllowDeath
pallet_prelude::{Weight, DispatchResultWithPostInfo},
use frame_system::{ensure_signed, ensure_root};
ModuleId, DispatchResult, RuntimeDebug, MultiSignature, MultiSigner,
AccountIdConversion, Hash, Saturating, Zero, One, CheckedAdd, Verify, IdentifyAccount,
use crate::traits::{Registrar, Auctioneer};
use crate::slot_range::SlotRange;
use parity_scale_codec::{Encode, Decode};
use primitives::v1::Id as ParaId;
type CurrencyOf<T> = <<T as Config>::Auctioneer as Auctioneer>::Currency;
type LeasePeriodOf<T> = <<T as Config>::Auctioneer as Auctioneer>::LeasePeriod;
type BalanceOf<T> = <CurrencyOf<T> as Currency<<T as frame_system::Config>::AccountId>>::Balance;
type NegativeImbalanceOf<T> = <CurrencyOf<T> as Currency<<T as frame_system::Config>::AccountId>>::NegativeImbalance;
type TrieIndex = 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 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 refund(_k: u32, ) -> Weight { 0 }
fn dissolve() -> Weight { 0 }
fn edit() -> Weight { 0 }
fn add_memo() -> Weight { 0 }
fn on_initialize(_n: u32, ) -> Weight { 0 }
}
pub trait Config: frame_system::Config {
type Event: From<Event<Self>> + Into<<Self as frame_system::Config>::Event>;
/// ModuleID for the crowdloan module. An appropriate value could be ```ModuleId(*b"py/cfund")```
/// The amount to be held on deposit by the depositor of a crowdloan.
type SubmissionDeposit: Get<BalanceOf<Self>>;
/// The minimum amount that may be contributed into a crowdloan. Should almost certainly be at
/// least ExistentialDeposit.
type MinContribution: Get<BalanceOf<Self>>;
/// Max number of storage keys to remove per extrinsic call.
type RemoveKeysLimit: Get<u32>;
/// 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<AccountId=Self::AccountId>;
/// The type representing the auctioning system.
type Auctioneer: Auctioneer<
AccountId=Self::AccountId,
BlockNumber=Self::BlockNumber,
LeasePeriod=Self::BlockNumber,
>;
/// The maximum length for the memo attached to a crowdloan contribution.
type MaxMemoLength: Get<u8>;
/// Weight Information for the Extrinsics in the Pallet
type WeightInfo: WeightInfo;
}
#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug)]
pub enum LastContribution<BlockNumber> {
Never,
/// 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<AccountId, Balance, BlockNumber, LeasePeriod> {
/// The owning account who placed the deposit.
/// An optional verifier. If exists, contributions must be signed by verifier.
verifier: Option<MultiSigner>,
/// 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<BlockNumber>,
/// First lease period in range to bid on; it's actually a LeasePeriod, but that's the same type
/// as BlockNumber.
first_period: LeasePeriod,
/// Last lease period in range to bid on; it's actually a LeasePeriod, but that's the same type
/// as BlockNumber.
last_period: LeasePeriod,
/// Index used for the child trie of this fund
trie_index: TrieIndex,
trait Store for Module<T: Config> as Crowdloan {
map hasher(twox_64_concat) ParaId
=> Option<FundInfo<T::AccountId, BalanceOf<T>, T::BlockNumber, LeasePeriodOf<T>>>;
/// 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<ParaId>;
/// 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<T> where
<T as frame_system::Config>::AccountId,
/// Create a new crowdloaning campaign. [fund_index]
/// 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),
/// The loans in a fund have been partially dissolved, i.e. there are some left
/// over child keys that still need to be killed. [fund_index]
PartiallyRefunded(ParaId),
/// All loans in a fund have been refunded. [fund_index]
AllRefunded(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),
/// The configuration to a crowdloan has been edited. [fund_index]
Edited(ParaId),
/// A memo has been updated. [who, fund_index, memo]
MemoUpdated(AccountId, ParaId, Vec<u8>),
pub enum Error for Module<T: Config> {
/// The current lease period is more than the first lease period.
FirstPeriodInPast,
/// The first lease period needs to at least be less than 3 `max_value`.
FirstPeriodTooFarInFuture,
/// Last lease period must be greater than first lease period.
LastPeriodBeforeFirstPeriod,
/// The last lease period cannot be more then 3 periods after the first period.
LastPeriodTooFarInFuture,
/// The campaign ends before the current block number. The end must be in the future.
CannotEndInPast,
/// The end date for this crowdloan is not sensible.
EndTooFarInFuture,
/// There was an overflow.
Overflow,
/// The contribution was below the minimum, `MinContribution`.
ContributionTooSmall,
/// Invalid fund index.
/// 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.
/// 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,
/// There are no contributions stored in this crowdloan.
/// This crowdloan has an active parachain and cannot be dissolved.
/// The crowdloan is not ready to dissolve. Potentially still has a slot or in retirement period.
NotReadyToDissolve,
/// Invalid signature.
InvalidSignature,
/// The provided memo is too large.
MemoTooLarge,
pub struct Module<T: Config> for enum Call where origin: <T as frame_system::Config>::Origin {
type Error = Error<T>;
const ModuleId: ModuleId = T::ModuleId::get();
const MinContribution: BalanceOf<T> = T::MinContribution::get();
const RemoveKeysLimit: u32 = T::RemoveKeysLimit::get();
/// Create a new crowdloaning campaign for a parachain slot with the given lease period range.
#[weight = T::WeightInfo::create()]
pub fn create(origin,
#[compact] index: ParaId,
#[compact] first_period: LeasePeriodOf<T>,
#[compact] last_period: LeasePeriodOf<T>,
#[compact] end: T::BlockNumber,
verifier: Option<MultiSigner>,
let depositor = ensure_signed(origin)?;
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);
Shaun Wang
committed
ensure!(end > <frame_system::Pallet<T>>::block_number(), Error::<T>::CannotEndInPast);
let last_possible_win_date = (first_period.saturating_add(One::one())).saturating_mul(T::Auctioneer::lease_period());
ensure!(end <= last_possible_win_date, Error::<T>::EndTooFarInFuture);
ensure!(first_period >= T::Auctioneer::lease_period_index(), Error::<T>::FirstPeriodInPast);
// There should not be an existing fund.
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);
let trie_index = Self::next_trie_index();
let new_trie_index = trie_index.checked_add(1).ok_or(Error::<T>::Overflow)?;
CurrencyOf::<T>::reserve(&depositor, deposit)?;
Funds::<T>::insert(index, FundInfo {
depositor,
deposit,
raised: Zero::zero(),
end,
cap,
last_contribution: LastContribution::Never,
first_period,
last_period,
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 when the crowdloan has ended and the funds are unused.
#[weight = T::WeightInfo::contribute()]
pub fn contribute(origin,
#[compact] index: ParaId,
#[compact] value: BalanceOf<T>,
signature: Option<MultiSignature>,
ensure!(value >= T::MinContribution::get(), Error::<T>::ContributionTooSmall);
let mut fund = Self::funds(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);
Shaun Wang
committed
let now = <frame_system::Pallet<T>>::block_number();
ensure!(now < fund.end, Error::<T>::ContributionPeriodOver);
// Make sure crowdloan is in a valid lease period
let current_lease_period = T::Auctioneer::lease_period_index();
ensure!(current_lease_period <= fund.first_period, Error::<T>::ContributionPeriodOver);
// Make sure crowdloan has not already won.
let fund_account = Self::fund_account_id(index);
ensure!(!T::Auctioneer::has_won_an_auction(index, &fund_account), Error::<T>::BidOrLeaseActive);
let (old_balance, memo) = Self::contribution_get(fund.trie_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, AllowDeath)?;
let balance = old_balance.saturating_add(value);
Self::contribution_put(fund.trie_index, &who, &balance, &memo);
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
}
_ => {
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.
fund.last_contribution = LastContribution::PreEnding(endings_count);
}
}
}
Self::deposit_event(RawEvent::Contributed(who, index, value));
}
/// Withdraw full balance of a specific contributor.
/// Origin must be signed, but can come from anyone.
/// 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_period`.
///
/// 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) {
let mut fund = Self::funds(index).ok_or(Error::<T>::InvalidParaId)?;
Shaun Wang
committed
let now = frame_system::Pallet::<T>::block_number();
let fund_account = Self::fund_account_id(index);
Self::ensure_crowdloan_ended(now, &fund_account, &fund)?;
let (balance, _) = Self::contribution_get(fund.trie_index, &who);
ensure!(balance > Zero::zero(), Error::<T>::NoContributions);
CurrencyOf::<T>::transfer(&fund_account, &who, balance, AllowDeath)?;
Self::contribution_kill(fund.trie_index, &who);
fund.raised = fund.raised.saturating_sub(balance);
Self::deposit_event(RawEvent::Withdrew(who, index, balance));
}
/// Automatically refund contributors of an ended crowdloan.
/// Due to weight restrictions, this function may need to be called multiple
/// times to fully refund all users. We will refund `RemoveKeysLimit` users at a time.
/// Origin must be signed, but can come from anyone.
#[weight = T::WeightInfo::refund(T::RemoveKeysLimit::get())]
pub fn refund(origin, #[compact] index: ParaId) -> DispatchResultWithPostInfo {
ensure_signed(origin)?;
let mut fund = Self::funds(index).ok_or(Error::<T>::InvalidParaId)?;
Shaun Wang
committed
let now = frame_system::Pallet::<T>::block_number();
let fund_account = Self::fund_account_id(index);
Self::ensure_crowdloan_ended(now, &fund_account, &fund)?;
let mut refund_count = 0u32;
let contributions = Self::contribution_iterator(fund.trie_index);
// Assume everyone will be refunded.
let mut all_refunded = true;
for (who, (balance, _)) in contributions {
if refund_count >= T::RemoveKeysLimit::get() {
// Not everyone was able to be refunded this time around.
all_refunded = false;
break;
}
CurrencyOf::<T>::transfer(&fund_account, &who, balance, AllowDeath)?;
Self::contribution_kill(fund.trie_index, &who);
fund.raised = fund.raised.saturating_sub(balance);
refund_count += 1;
}
// Save the changes.
Funds::<T>::insert(index, &fund);
if all_refunded {
Self::deposit_event(RawEvent::AllRefunded(index));
// Refund for unused refund count.
Ok(Some(T::WeightInfo::refund(refund_count)).into())
} else {
Self::deposit_event(RawEvent::PartiallyRefunded(index));
// No weight to refund since we did not finish the loop.
Ok(().into())
}
}
/// Remove a fund after the retirement period has ended and all funds have been returned.
#[weight = T::WeightInfo::dissolve()]
pub fn dissolve(origin, #[compact] index: ParaId) -> DispatchResult {
let who = ensure_signed(origin)?;
let fund = Self::funds(index).ok_or(Error::<T>::InvalidParaId)?;
let now = frame_system::Pallet::<T>::block_number();
// Only allow dissolution when the raised funds goes to zero,
// and the caller is the fund creator or we are past the end date.
let permitted = who == fund.depositor || now >= fund.end;
let can_dissolve = permitted && fund.raised.is_zero();
ensure!(can_dissolve, Error::<T>::NotReadyToDissolve);
// Assuming state is not corrupted, the child trie should already be cleaned up
// and all funds in the crowdloan account have been returned. If not, governance
// can take care of that.
debug_assert!(Self::contribution_iterator(fund.trie_index).count().is_zero());
CurrencyOf::<T>::unreserve(&fund.depositor, fund.deposit);
Funds::<T>::remove(index);
Self::deposit_event(RawEvent::Dissolved(index));
Ok(())
/// Edit the configuration for an in-progress crowdloan.
///
/// Can only be called by Root origin.
#[weight = T::WeightInfo::edit()]
pub fn edit(origin,
#[compact] index: ParaId,
#[compact] cap: BalanceOf<T>,
#[compact] first_period: LeasePeriodOf<T>,
#[compact] last_period: LeasePeriodOf<T>,
#[compact] end: T::BlockNumber,
verifier: Option<MultiSigner>,
) {
ensure_root(origin)?;
let fund = Self::funds(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,
trie_index: fund.trie_index,
});
Self::deposit_event(RawEvent::Edited(index));
}
/// Add an optional memo to an existing crowdloan contribution.
///
/// Origin must be Signed, and the user must have contributed to the crowdloan.
#[weight = T::WeightInfo::add_memo()]
pub fn add_memo(origin, index: ParaId, memo: Vec<u8>) {
let who = ensure_signed(origin)?;
ensure!(memo.len() <= T::MaxMemoLength::get().into(), Error::<T>::MemoTooLarge);
let fund = Self::funds(index).ok_or(Error::<T>::InvalidParaId)?;
let (balance, _) = Self::contribution_get(fund.trie_index, &who);
ensure!(balance > Zero::zero(), Error::<T>::NoContributions);
Self::contribution_put(fund.trie_index, &who, &balance, &memo);
Self::deposit_event(RawEvent::MemoUpdated(who, index, memo));
}
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_period,
fund.last_period,
Self::deposit_event(RawEvent::HandleBidResult(para_id, result));
T::WeightInfo::on_initialize(new_raise_len)
} else {
T::DbWeight::get().reads(1)
impl<T: Config> Module<T> {
/// 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 {
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<T>, memo: &[u8]) {
who.using_encoded(|b| child::put(&Self::id_from_index(index), b, &(balance, memo)));
pub fn contribution_get(index: TrieIndex, who: &T::AccountId) -> (BalanceOf<T>, Vec<u8>) {
who.using_encoded(|b| child::get_or_default::<(BalanceOf<T>, Vec<u8>)>(
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()))
pub fn contribution_iterator(
index: TrieIndex
) -> ChildTriePrefixIterator<(T::AccountId, (BalanceOf<T>, Vec<u8>))> {
ChildTriePrefixIterator::<_>::with_prefix_over_key::<Identity>(&Self::id_from_index(index), &[])
}
/// This function checks all conditions which would qualify a crowdloan has ended.
/// * If we have reached the `fund.end` block OR the first lease period the fund is
/// trying to bid for has started already.
/// * And, if the fund has enough free funds to refund full raised amount.
fn ensure_crowdloan_ended(
now: T::BlockNumber,
fund_account: &T::AccountId,
fund: &FundInfo<T::AccountId, BalanceOf<T>, T::BlockNumber, LeasePeriodOf<T>>
) -> DispatchResult {
// `fund.end` can represent the end of a failed crowdloan or the beginning of retirement
// If the current lease period is past the first period they are trying to bid for, then
// it is already too late to win the bid.
let current_lease_period = T::Auctioneer::lease_period_index();
ensure!(now >= fund.end || current_lease_period > fund.first_period, Error::<T>::FundNotEnded);
// free balance must greater than or equal amount raised, otherwise funds are being used
// and a bid or lease must be active.
ensure!(CurrencyOf::<T>::free_balance(&fund_account) >= fund.raised, Error::<T>::BidOrLeaseActive);
Ok(())
}
impl<T: Config> crate::traits::OnSwap for Module<T> {
fn on_swap(one: ParaId, other: ParaId) {
Funds::<T>::mutate(one, |x|
Funds::<T>::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<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 std::{cell::RefCell, sync::Arc, collections::BTreeMap};
thiolliere
committed
use frame_support::{
assert_ok, assert_noop, parameter_types,
thiolliere
committed
traits::{OnInitialize, OnFinalize},
};
// 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.
testing::Header, traits::{BlakeTwo256, IdentityLookup},
};
use crate::{
mock::TestRegistrar,
traits::OnSwap,
crowdloan,
use sp_keystore::{KeystoreExt, testing::KeyStore};
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
type Block = frame_system::mocking::MockBlock<Test>;
frame_support::construct_runtime!(
pub enum Test where
Block = Block,
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{
Shaun Wang
committed
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
Crowdloan: crowdloan::{Pallet, Call, Storage, Event<T>},
);
parameter_types! {
pub const BlockHashCount: u32 = 250;
}
type BlockNumber = u64;
impl frame_system::Config for Test {
thiolliere
committed
type BaseCallFilter = ();
type BlockWeights = ();
type BlockLength = ();
type DbWeight = ();
type Call = Call;
type BlockNumber = BlockNumber;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = u64;
type Lookup = IdentityLookup<Self::AccountId>;
type Header = Header;
type BlockHashCount = BlockHashCount;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = pallet_balances::AccountData<u64>;
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ();
impl pallet_balances::Config for Test {
type ExistentialDeposit = ExistentialDeposit;
type MaxLocks = ();
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
struct BidPlaced {
height: u64,
bidder: u64,
para: ParaId,
first_period: u64,
last_period: u64,
static AUCTION: RefCell<Option<(u64, u64)>> = RefCell::new(None);
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().clone())
}
fn ending_period() -> u64 {
ENDING_PERIOD.with(|p| p.borrow().clone())
}
fn bids() -> Vec<BidPlaced> {
BIDS_PLACED.with(|p| p.borrow().clone())
// Emulate what would happen if we won an auction:
// balance is reserved and a deposit_held is recorded
fn set_winner(para: ParaId, who: u64, winner: bool) {
let account_id = Crowdloan::fund_account_id(para);
if winner {
let free_balance = Balances::free_balance(&account_id);
Balances::reserve(&account_id, free_balance).expect("should be able to reserve free balance");
} 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 for TestAuctioneer {
type AccountId = u64;
type BlockNumber = BlockNumber;
type LeasePeriod = u64;
type Currency = Balances;
fn new_auction(duration: u64, lease_period_index: u64) -> DispatchResult {
assert!(lease_period_index >= Self::lease_period_index());
let ending = System::block_number().saturating_add(duration);
AUCTION.with(|p| *p.borrow_mut() = Some((lease_period_index, ending)));
Ok(())
fn is_ending(now: u64) -> Option<u64> {
if let Some((_, early_end)) = auction() {
if let Some(after_early_end) = now.checked_sub(early_end) {
if after_early_end < ending_period() {
return Some(after_early_end)
}
}
}
None
fn place_bid(
bidder: u64,
para: ParaId,
first_period: u64,
last_period: u64,
let height = System::block_number();
BIDS_PLACED.with(|p| p.borrow_mut().push(BidPlaced { height, bidder, para, first_period, last_period, amount }));
System::block_number() / Self::lease_period()
}
fn lease_period() -> u64 {
20
}
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 CrowdloanModuleId: ModuleId = ModuleId(*b"py/cfund");
pub const RemoveKeysLimit: u32 = 10;
pub const MaxMemoLength: u8 = 32;
impl Config for Test {
type SubmissionDeposit = SubmissionDeposit;
type MinContribution = MinContribution;
type ModuleId = CrowdloanModuleId;
type RemoveKeysLimit = RemoveKeysLimit;
type Registrar = TestRegistrar<Test>;
type Auctioneer = TestAuctioneer;
type MaxMemoLength = MaxMemoLength;
type WeightInfo = crate::crowdloan::TestWeightInfo;
use pallet_balances::Error as BalancesError;
// This function basically just builds a genesis storage key/value store according to
// our desired mockup.
pub fn new_test_ext() -> sp_io::TestExternalities {
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
pallet_balances::GenesisConfig::<Test>{
balances: vec![(1, 1000), (2, 2000), (3, 3000), (4, 4000)],
}.assimilate_storage(&mut t).unwrap();
let keystore = KeyStore::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, Default::default(), Default::default()));
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() -> Event {
System::events().pop().expect("Event expected").event
}
new_test_ext().execute_with(|| {
assert_eq!(System::block_number(), 0);
assert_eq!(Crowdloan::funds(ParaId::from(0)), None);
let empty: Vec<ParaId> = Vec::new();
assert_eq!(Crowdloan::contribution_get(0u32, &1).0, 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::is_ending(4), None);
assert_eq!(TestAuctioneer::is_ending(5), Some(0));
assert_eq!(TestAuctioneer::is_ending(9), Some(4));
assert_eq!(TestAuctioneer::is_ending(11), None);
});
}
#[test]
fn create_works() {
new_test_ext().execute_with(|| {
assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 4, 9, None));
// This is what the initial `fund_info` should look like
let fund_info = FundInfo {
verifier: None,
deposit: 1,
raised: 0,
// 5 blocks length + 3 block ending period + 1 starting block
end: 9,
cap: 1000,
last_contribution: LastContribution::Never,
first_period: 1,
last_period: 4,
assert_eq!(Crowdloan::funds(para), Some(fund_info));
// User has deposit removed from their free balance
assert_eq!(Balances::free_balance(1), 999);
// Deposit is placed in reserved
assert_eq!(Balances::reserved_balance(1), 1);
// No new raise until first contribution
let empty: Vec<ParaId> = Vec::new();
assert_eq!(Crowdloan::new_raise(), empty);
});
}
#[test]
fn create_with_verifier_works() {
new_test_ext().execute_with(|| {
let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec());
// Now try to create a crowdloan campaign
assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 4, 9, Some(pubkey.clone())));
// This is what the initial `fund_info` should look like
let fund_info = FundInfo {