// 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 Crowdfunding 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, storage::child, ensure, traits::{ Currency, Get, OnUnbalanced, WithdrawReasons, ExistenceRequirement::AllowDeath }, }; use frame_system::ensure_signed; use sp_runtime::{ModuleId, traits::{AccountIdConversion, Hash, Saturating, Zero, CheckedAdd} }; use crate::slots; use parity_scale_codec::{Encode, Decode}; use sp_std::vec::Vec; use primitives::v1::{Id as ParaId, HeadData}; pub type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; #[allow(dead_code)] pub type NegativeImbalanceOf = <::Currency as Currency<::AccountId>>::NegativeImbalance; pub trait Config: slots::Config { type Event: From> + Into<::Event>; /// ModuleID for the crowdfund module. An appropriate value could be ```ModuleId(*b"py/cfund")``` type ModuleId: Get; /// The amount to be held on deposit by the owner of a crowdfund. type SubmissionDeposit: Get>; /// The minimum amount that may be contributed into a crowdfund. Should almost certainly be at /// least ExistentialDeposit. type MinContribution: Get>; /// The period of time (in blocks) after an unsuccessful crowdfund 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>; } /// Simple index for identifying a fund. pub type FundIndex = u32; #[derive(Encode, Decode, Copy, Clone, PartialEq, Eq)] #[cfg_attr(feature = "std", derive(Debug))] pub enum LastContribution { Never, PreEnding(slots::AuctionIndex), Ending(BlockNumber), } #[derive(Encode, Decode, Clone, PartialEq, Eq)] #[cfg_attr(feature = "std", derive(Debug))] struct DeployData { code_hash: Hash, code_size: u32, initial_head_data: HeadData, } #[derive(Encode, Decode, Clone, PartialEq, Eq)] #[cfg_attr(feature = "std", derive(Debug))] #[codec(dumb_trait_bound)] pub struct FundInfo { /// The parachain that this fund has funded, if there is one. As long as this is `Some`, then /// the funds may not be withdrawn and the fund cannot be dissolved. parachain: Option, /// The owning account who placed the deposit. owner: AccountId, /// 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: BlockNumber, /// Last slot in range to bid on; it's actually a LeasePeriod, but that's the same type as /// BlockNumber. last_slot: BlockNumber, /// The deployment data associated with this fund, if any. Once set it may not be reset. First /// is the code hash, second is the code size, third is the initial head data. deploy_data: Option>, } decl_storage! { trait Store for Module as Crowdfund { /// Info on all of the funds. Funds get(fn funds): map hasher(twox_64_concat) FundIndex => Option, T::Hash, T::BlockNumber>>; /// The total number of funds that have so far been allocated. FundCount get(fn fund_count): FundIndex; /// 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): slots::AuctionIndex; } } decl_event! { pub enum Event where ::AccountId, Balance = BalanceOf, { /// Create a new crowdfunding campaign. [fund_index] Created(FundIndex), /// Contributed to a crowd sale. [who, fund_index, amount] Contributed(AccountId, FundIndex, Balance), /// Withdrew full balance of a contributor. [who, fund_index, amount] Withdrew(AccountId, FundIndex, Balance), /// Fund is placed into retirement. [fund_index] Retiring(FundIndex), /// Fund is dissolved. [fund_index] Dissolved(FundIndex), /// The deploy data of the funded parachain is setted. [fund_index] DeployDataFixed(FundIndex), /// Onboarding process for a winning parachain fund is completed. [find_index, parachain_id] Onboarded(FundIndex, ParaId), } } decl_error! { pub enum Error for Module { /// 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. InvalidFundIndex, /// Contributions exceed maximum amount. CapExceeded, /// The contribution period has already ended. ContributionPeriodOver, /// The origin of this call is invalid. InvalidOrigin, /// Deployment data for a fund can only be set once. The deployment data for this fund /// already exists. ExistingDeployData, /// Deployment data has not been set for this fund. UnsetDeployData, /// This fund has already been onboarded. AlreadyOnboard, /// This crowdfund does not correspond to a parachain. NotParachain, /// This parachain still has its deposit. Implies that it has already been offboarded. ParaHasDeposit, /// Funds have not yet been returned. FundsNotReturned, /// Fund has not yet retired. FundNotRetired, /// The crowdfund has not yet ended. FundNotEnded, /// There are no contributions stored in this crowdfund. NoContributions, /// This crowdfund has an active parachain and cannot be dissolved. HasActiveParachain, /// The retirement period has not ended. InRetirementPeriod, } } decl_module! { pub struct Module for enum Call where origin: T::Origin { type Error = Error; const ModuleId: ModuleId = T::ModuleId::get(); fn deposit_event() = default; /// Create a new crowdfunding campaign for a parachain slot deposit for the current auction. #[weight = 100_000_000] fn create(origin, #[compact] cap: BalanceOf, #[compact] first_slot: T::BlockNumber, #[compact] last_slot: T::BlockNumber, #[compact] end: T::BlockNumber ) { let owner = ensure_signed(origin)?; ensure!(first_slot < last_slot, Error::::LastSlotBeforeFirstSlot); ensure!(last_slot <= first_slot + 3u32.into(), Error::::LastSlotTooFarInFuture); ensure!(end > >::block_number(), Error::::CannotEndInPast); let deposit = T::SubmissionDeposit::get(); let transfer = WithdrawReasons::TRANSFER; let imb = T::Currency::withdraw(&owner, deposit, transfer, AllowDeath)?; let index = FundCount::get(); let next_index = index.checked_add(1).ok_or(Error::::Overflow)?; FundCount::put(next_index); // No fees are paid here if we need to create this account; that's why we don't just // use the stock `transfer`. T::Currency::resolve_creating(&Self::fund_account_id(index), imb); >::insert(index, FundInfo { parachain: None, owner, deposit, raised: Zero::zero(), end, cap, last_contribution: LastContribution::Never, first_slot, last_slot, deploy_data: None, }); 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 = 0] fn contribute(origin, #[compact] index: FundIndex, #[compact] value: BalanceOf) { let who = ensure_signed(origin)?; ensure!(value >= T::MinContribution::get(), Error::::ContributionTooSmall); let mut fund = Self::funds(index).ok_or(Error::::InvalidFundIndex)?; fund.raised = fund.raised.checked_add(&value).ok_or(Error::::Overflow)?; ensure!(fund.raised <= fund.cap, Error::::CapExceeded); // Make sure crowdfund has not ended let now = >::block_number(); ensure!(fund.end > now, Error::::ContributionPeriodOver); T::Currency::transfer(&who, &Self::fund_account_id(index), value, AllowDeath)?; let balance = Self::contribution_get(index, &who); let balance = balance.saturating_add(value); Self::contribution_put(index, &who, &balance); if >::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::mutate(|v| v.push(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::mutate(|v| v.push(index)); fund.last_contribution = LastContribution::PreEnding(endings_count); } } } >::insert(index, &fund); Self::deposit_event(RawEvent::Contributed(who, index, value)); } /// Set the deploy data of the funded parachain if not already set. Once set, this cannot /// be changed again. /// /// - `origin` must be the fund owner. /// - `index` is the fund index that `origin` owns and whose deploy data will be set. /// - `code_hash` is the hash of the parachain's Wasm validation function. /// - `initial_head_data` is the parachain's initial head data. #[weight = 0] fn fix_deploy_data(origin, #[compact] index: FundIndex, code_hash: T::Hash, code_size: u32, initial_head_data: HeadData, ) { let who = ensure_signed(origin)?; let mut fund = Self::funds(index).ok_or(Error::::InvalidFundIndex)?; ensure!(fund.owner == who, Error::::InvalidOrigin); // must be fund owner ensure!(fund.deploy_data.is_none(), Error::::ExistingDeployData); fund.deploy_data = Some(DeployData { code_hash, code_size, initial_head_data }); >::insert(index, &fund); Self::deposit_event(RawEvent::DeployDataFixed(index)); } /// Complete onboarding process for a winning parachain fund. This can be called once by /// any origin once a fund wins a slot and the fund has set its deploy data (using /// `fix_deploy_data`). /// /// - `index` is the fund index that `origin` owns and whose deploy data will be set. /// - `para_id` is the parachain index that this fund won. #[weight = 0] fn onboard(origin, #[compact] index: FundIndex, #[compact] para_id: ParaId ) { let _ = ensure_signed(origin)?; let mut fund = Self::funds(index).ok_or(Error::::InvalidFundIndex)?; let DeployData { code_hash, code_size, initial_head_data } = fund.clone().deploy_data.ok_or(Error::::UnsetDeployData)?; ensure!(fund.parachain.is_none(), Error::::AlreadyOnboard); fund.parachain = Some(para_id); let fund_origin = frame_system::RawOrigin::Signed(Self::fund_account_id(index)).into(); >::fix_deploy_data( fund_origin, index, para_id, code_hash, code_size, initial_head_data, )?; >::insert(index, &fund); Self::deposit_event(RawEvent::Onboarded(index, para_id)); } /// Note that a successful fund has lost its parachain slot, and place it into retirement. #[weight = 0] fn begin_retirement(origin, #[compact] index: FundIndex) { let _ = ensure_signed(origin)?; let mut fund = Self::funds(index).ok_or(Error::::InvalidFundIndex)?; let parachain_id = fund.parachain.take().ok_or(Error::::NotParachain)?; // No deposit information implies the parachain was off-boarded ensure!(>::deposits(parachain_id).len() == 0, Error::::ParaHasDeposit); let account = Self::fund_account_id(index); // Funds should be returned at the end of off-boarding ensure!(T::Currency::free_balance(&account) >= fund.raised, Error::::FundsNotReturned); // This fund just ended. Withdrawal period begins. let now = >::block_number(); fund.end = now; >::insert(index, &fund); Self::deposit_event(RawEvent::Retiring(index)); } /// Withdraw full balance of a contributor to an unsuccessful or off-boarded fund. #[weight = 0] fn withdraw(origin, #[compact] index: FundIndex) { let who = ensure_signed(origin)?; let mut fund = Self::funds(index).ok_or(Error::::InvalidFundIndex)?; ensure!(fund.parachain.is_none(), Error::::FundNotRetired); let now = >::block_number(); // `fund.end` can represent the end of a failed crowdsale or the beginning of retirement ensure!(now >= fund.end, Error::::FundNotEnded); let balance = Self::contribution_get(index, &who); ensure!(balance > Zero::zero(), Error::::NoContributions); // Avoid using transfer to ensure we don't pay any fees. let fund_account = &Self::fund_account_id(index); let transfer = WithdrawReasons::TRANSFER; let imbalance = T::Currency::withdraw(fund_account, balance, transfer, AllowDeath)?; let _ = T::Currency::resolve_into_existing(&who, imbalance); Self::contribution_kill(index, &who); fund.raised = fund.raised.saturating_sub(balance); >::insert(index, &fund); Self::deposit_event(RawEvent::Withdrew(who, index, balance)); } /// Remove a fund after either: it was unsuccessful and it timed out; or it was successful /// but it has been retired from its parachain slot. This places any deposits that were not /// withdrawn into the treasury. #[weight = 0] fn dissolve(origin, #[compact] index: FundIndex) { let _ = ensure_signed(origin)?; let fund = Self::funds(index).ok_or(Error::::InvalidFundIndex)?; ensure!(fund.parachain.is_none(), Error::::HasActiveParachain); let now = >::block_number(); ensure!( now >= fund.end.saturating_add(T::RetirementPeriod::get()), Error::::InRetirementPeriod ); let account = Self::fund_account_id(index); // Avoid using transfer to ensure we don't pay any fees. let transfer = WithdrawReasons::TRANSFER; let imbalance = T::Currency::withdraw(&account, fund.deposit, transfer, AllowDeath)?; let _ = T::Currency::resolve_into_existing(&fund.owner, imbalance); let imbalance = T::Currency::withdraw(&account, fund.raised, transfer, AllowDeath)?; T::OrphanedFunds::on_unbalanced(imbalance); Self::crowdfund_kill(index); >::remove(index); Self::deposit_event(RawEvent::Dissolved(index)); } fn on_finalize(n: T::BlockNumber) { if let Some(n) = >::is_ending(n) { let auction_index = >::auction_counter(); if n.is_zero() { // first block of ending period. EndingsCount::mutate(|c| *c += 1); } for (fund, index) in NewRaise::take().into_iter().filter_map(|i| Self::funds(i).map(|f| (f, i))) { let bidder = slots::Bidder::New(slots::NewBidder { who: Self::fund_account_id(index), /// FundIndex and slots::SubId happen to be the same type (u32). If this /// ever changes, then some sort of conversion will be needed here. sub: index, }); // Care needs to be taken by the crowdfund creator that this function will succeed given // the crowdfunding configuration. We do some checks ahead of time in crowdfund `create`. let _ = >::handle_bid( bidder, auction_index, fund.first_slot, fund.last_slot, fund.raised, ); } } } } } 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: FundIndex) -> T::AccountId { T::ModuleId::get().into_sub_account(index) } pub fn id_from_index(index: FundIndex) -> child::ChildInfo { let mut buf = Vec::new(); buf.extend_from_slice(b"crowdfund"); buf.extend_from_slice(&index.to_le_bytes()[..]); child::ChildInfo::new_default(T::Hashing::hash(&buf[..]).as_ref()) } pub fn contribution_put(index: FundIndex, who: &T::AccountId, balance: &BalanceOf) { who.using_encoded(|b| child::put(&Self::id_from_index(index), b, balance)); } pub fn contribution_get(index: FundIndex, who: &T::AccountId) -> BalanceOf { who.using_encoded(|b| child::get_or_default::>( &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 crowdfund_kill(index: FundIndex) { child::kill_storage(&Self::id_from_index(index), None); } } #[cfg(test)] mod tests { use super::*; use std::{collections::HashMap, cell::RefCell}; use frame_support::{ impl_outer_origin, assert_ok, assert_noop, parameter_types, traits::{OnInitialize, OnFinalize}, }; use sp_core::H256; use primitives::v1::{Id as ParaId, ValidationCode}; // 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::{ Permill, testing::Header, DispatchResult, traits::{BlakeTwo256, IdentityLookup}, }; use crate::slots::Registrar; impl_outer_origin! { pub enum Origin for Test {} } // For testing the module, we construct most of a mock runtime. This means // first constructing a configuration type (`Test`) which `impl`s each of the // configuration traits of modules we want to use. #[derive(Clone, Eq, PartialEq)] pub struct Test; 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 = (); type Index = u64; type BlockNumber = u64; type Hash = H256; type Hashing = BlakeTwo256; type AccountId = u64; type Lookup = IdentityLookup; type Header = Header; type Event = (); type BlockHashCount = BlockHashCount; type Version = (); type PalletInfo = (); type AccountData = pallet_balances::AccountData; type OnNewAccount = (); type OnKilledAccount = Balances; type SystemWeightInfo = (); type SS58Prefix = (); } parameter_types! { pub const ExistentialDeposit: u64 = 1; } impl pallet_balances::Config for Test { type Balance = u64; type Event = (); type DustRemoval = (); type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; type MaxLocks = (); type WeightInfo = (); } parameter_types! { pub const ProposalBond: Permill = Permill::from_percent(5); pub const ProposalBondMinimum: u64 = 1; pub const SpendPeriod: u64 = 2; pub const Burn: Permill = Permill::from_percent(50); pub const TreasuryModuleId: ModuleId = ModuleId(*b"py/trsry"); } impl pallet_treasury::Config for Test { type Currency = pallet_balances::Module; type ApproveOrigin = frame_system::EnsureRoot; type RejectOrigin = frame_system::EnsureRoot; type Event = (); type OnSlash = (); type ProposalBond = ProposalBond; type ProposalBondMinimum = ProposalBondMinimum; type SpendPeriod = SpendPeriod; type Burn = Burn; type BurnDestination = (); type ModuleId = TreasuryModuleId; type SpendFunds = (); type WeightInfo = (); } thread_local! { pub static PARACHAIN_COUNT: RefCell = RefCell::new(0); pub static PARACHAINS: RefCell> = RefCell::new(HashMap::new()); } const MAX_CODE_SIZE: u32 = 100; const MAX_HEAD_DATA_SIZE: u32 = 10; pub struct TestParachains; impl Registrar for TestParachains { fn new_id() -> ParaId { PARACHAIN_COUNT.with(|p| { *p.borrow_mut() += 1; (*p.borrow() - 1).into() }) } fn head_data_size_allowed(head_data_size: u32) -> bool { head_data_size <= MAX_HEAD_DATA_SIZE } fn code_size_allowed(code_size: u32) -> bool { code_size <= MAX_CODE_SIZE } fn register_para( id: ParaId, _parachain: bool, code: ValidationCode, initial_head_data: HeadData, ) -> DispatchResult { PARACHAINS.with(|p| { if p.borrow().contains_key(&id.into()) { panic!("ID already exists") } p.borrow_mut().insert(id.into(), (code, initial_head_data)); Ok(()) }) } fn deregister_para(id: ParaId) -> DispatchResult { PARACHAINS.with(|p| { if !p.borrow().contains_key(&id.into()) { panic!("ID doesn't exist") } p.borrow_mut().remove(&id.into()); Ok(()) }) } } parameter_types!{ pub const LeasePeriod: u64 = 10; pub const EndingPeriod: u64 = 3; } impl slots::Config for Test { type Event = (); type Currency = Balances; type Parachains = TestParachains; type LeasePeriod = LeasePeriod; type EndingPeriod = EndingPeriod; type Randomness = RandomnessCollectiveFlip; } parameter_types! { pub const SubmissionDeposit: u64 = 1; pub const MinContribution: u64 = 10; pub const RetirementPeriod: u64 = 5; pub const CrowdfundModuleId: ModuleId = ModuleId(*b"py/cfund"); } impl Config for Test { type Event = (); type SubmissionDeposit = SubmissionDeposit; type MinContribution = MinContribution; type RetirementPeriod = RetirementPeriod; type OrphanedFunds = Treasury; type ModuleId = CrowdfundModuleId; } type System = frame_system::Module; type Balances = pallet_balances::Module; type Slots = slots::Module; type Treasury = pallet_treasury::Module; type Crowdfund = Module; type RandomnessCollectiveFlip = pallet_randomness_collective_flip::Module; use pallet_balances::Error as BalancesError; use slots::Error as SlotsError; // This function basically just builds a genesis storage key/value store according to // our desired mockup. fn new_test_ext() -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); pallet_balances::GenesisConfig::{ balances: vec![(1, 1000), (2, 2000), (3, 3000), (4, 4000)], }.assimilate_storage(&mut t).unwrap(); t.into() } fn run_to_block(n: u64) { while System::block_number() < n { Crowdfund::on_finalize(System::block_number()); Treasury::on_finalize(System::block_number()); Slots::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()); Slots::on_initialize(System::block_number()); Treasury::on_initialize(System::block_number()); Crowdfund::on_initialize(System::block_number()); } } #[test] fn basic_setup_works() { new_test_ext().execute_with(|| { assert_eq!(System::block_number(), 0); assert_eq!(Crowdfund::fund_count(), 0); assert_eq!(Crowdfund::funds(0), None); let empty: Vec = Vec::new(); assert_eq!(Crowdfund::new_raise(), empty); assert_eq!(Crowdfund::contribution_get(0, &1), 0); assert_eq!(Crowdfund::endings_count(), 0); }); } #[test] fn create_works() { new_test_ext().execute_with(|| { // Now try to create a crowdfund campaign assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Crowdfund::fund_count(), 1); // This is what the initial `fund_info` should look like let fund_info = FundInfo { parachain: None, owner: 1, deposit: 1, raised: 0, // 5 blocks length + 3 block ending period + 1 starting block end: 9, cap: 1000, last_contribution: LastContribution::Never, first_slot: 1, last_slot: 4, deploy_data: None, }; assert_eq!(Crowdfund::funds(0), Some(fund_info)); // User has deposit removed from their free balance assert_eq!(Balances::free_balance(1), 999); // Deposit is placed in crowdfund free balance assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 1); // No new raise until first contribution let empty: Vec = Vec::new(); assert_eq!(Crowdfund::new_raise(), empty); }); } #[test] fn create_handles_basic_errors() { new_test_ext().execute_with(|| { // Cannot create a crowdfund with bad slots assert_noop!( Crowdfund::create(Origin::signed(1), 1000, 4, 1, 9), Error::::LastSlotBeforeFirstSlot ); assert_noop!( Crowdfund::create(Origin::signed(1), 1000, 1, 5, 9), Error::::LastSlotTooFarInFuture ); // Cannot create a crowdfund without some deposit funds assert_noop!( Crowdfund::create(Origin::signed(1337), 1000, 1, 3, 9), BalancesError::::InsufficientBalance ); }); } #[test] fn contribute_works() { new_test_ext().execute_with(|| { // Set up a crowdfund assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Balances::free_balance(1), 999); assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 1); // No contributions yet assert_eq!(Crowdfund::contribution_get(0, &1), 0); // User 1 contributes to their own crowdfund assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 49)); // User 1 has spent some funds to do this, transfer fees **are** taken assert_eq!(Balances::free_balance(1), 950); // Contributions are stored in the trie assert_eq!(Crowdfund::contribution_get(0, &1), 49); // Contributions appear in free balance of crowdfund assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 50); // Crowdfund is added to NewRaise assert_eq!(Crowdfund::new_raise(), vec![0]); let fund = Crowdfund::funds(0).unwrap(); // Last contribution time recorded assert_eq!(fund.last_contribution, LastContribution::PreEnding(0)); assert_eq!(fund.raised, 49); }); } #[test] fn contribute_handles_basic_errors() { new_test_ext().execute_with(|| { // Cannot contribute to non-existing fund assert_noop!(Crowdfund::contribute(Origin::signed(1), 0, 49), Error::::InvalidFundIndex); // Cannot contribute below minimum contribution assert_noop!(Crowdfund::contribute(Origin::signed(1), 0, 9), Error::::ContributionTooSmall); // Set up a crowdfund assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 101)); // Cannot contribute past the limit assert_noop!(Crowdfund::contribute(Origin::signed(2), 0, 900), Error::::CapExceeded); // Move past end date run_to_block(10); // Cannot contribute to ended fund assert_noop!(Crowdfund::contribute(Origin::signed(1), 0, 49), Error::::ContributionPeriodOver); }); } #[test] fn fix_deploy_data_works() { new_test_ext().execute_with(|| { // Set up a crowdfund assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Balances::free_balance(1), 999); // Add deploy data assert_ok!(Crowdfund::fix_deploy_data( Origin::signed(1), 0, ::Hash::default(), 0, vec![0].into() )); let fund = Crowdfund::funds(0).unwrap(); // Confirm deploy data is stored correctly assert_eq!( fund.deploy_data, Some(DeployData { code_hash: ::Hash::default(), code_size: 0, initial_head_data: vec![0].into(), }), ); }); } #[test] fn fix_deploy_data_handles_basic_errors() { new_test_ext().execute_with(|| { // Set up a crowdfund assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Balances::free_balance(1), 999); // Cannot set deploy data by non-owner assert_noop!(Crowdfund::fix_deploy_data( Origin::signed(2), 0, ::Hash::default(), 0, vec![0].into()), Error::::InvalidOrigin ); // Cannot set deploy data to an invalid index assert_noop!(Crowdfund::fix_deploy_data( Origin::signed(1), 1, ::Hash::default(), 0, vec![0].into()), Error::::InvalidFundIndex ); // Cannot set deploy data after it already has been set assert_ok!(Crowdfund::fix_deploy_data( Origin::signed(1), 0, ::Hash::default(), 0, vec![0].into(), )); assert_noop!(Crowdfund::fix_deploy_data( Origin::signed(1), 0, ::Hash::default(), 0, vec![1].into()), Error::::ExistingDeployData ); }); } #[test] fn onboard_works() { new_test_ext().execute_with(|| { // Set up a crowdfund assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Balances::free_balance(1), 999); // Add deploy data assert_ok!(Crowdfund::fix_deploy_data( Origin::signed(1), 0, ::Hash::default(), 0, vec![0].into(), )); // Fund crowdfund assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 1000)); run_to_block(10); // Endings count incremented assert_eq!(Crowdfund::endings_count(), 1); // Onboard crowdfund assert_ok!(Crowdfund::onboard(Origin::signed(1), 0, 0.into())); let fund = Crowdfund::funds(0).unwrap(); // Crowdfund is now assigned a parachain id assert_eq!(fund.parachain, Some(0.into())); // This parachain is managed by Slots assert_eq!(Slots::managed_ids(), vec![0.into()]); }); } #[test] fn onboard_handles_basic_errors() { new_test_ext().execute_with(|| { // Set up a crowdfund assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Balances::free_balance(1), 999); // Fund crowdfund assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 1000)); run_to_block(10); // Cannot onboard invalid fund index assert_noop!(Crowdfund::onboard(Origin::signed(1), 1, 0.into()), Error::::InvalidFundIndex); // Cannot onboard crowdfund without deploy data assert_noop!(Crowdfund::onboard(Origin::signed(1), 0, 0.into()), Error::::UnsetDeployData); // Add deploy data assert_ok!(Crowdfund::fix_deploy_data( Origin::signed(1), 0, ::Hash::default(), 0, vec![0].into(), )); // Cannot onboard fund with incorrect parachain id assert_noop!(Crowdfund::onboard(Origin::signed(1), 0, 1.into()), SlotsError::::ParaNotOnboarding); // Onboard crowdfund assert_ok!(Crowdfund::onboard(Origin::signed(1), 0, 0.into())); // Cannot onboard fund again assert_noop!(Crowdfund::onboard(Origin::signed(1), 0, 0.into()), Error::::AlreadyOnboard); }); } #[test] fn begin_retirement_works() { new_test_ext().execute_with(|| { // Set up a crowdfund assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Balances::free_balance(1), 999); // Add deploy data assert_ok!(Crowdfund::fix_deploy_data( Origin::signed(1), 0, ::Hash::default(), 0, vec![0].into(), )); // Fund crowdfund assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 1000)); run_to_block(10); // Onboard crowdfund assert_ok!(Crowdfund::onboard(Origin::signed(1), 0, 0.into())); // Fund is assigned a parachain id let fund = Crowdfund::funds(0).unwrap(); assert_eq!(fund.parachain, Some(0.into())); // Off-boarding is set to the crowdfund account assert_eq!(Slots::offboarding(ParaId::from(0)), Crowdfund::fund_account_id(0)); run_to_block(50); // Retire crowdfund to remove parachain id assert_ok!(Crowdfund::begin_retirement(Origin::signed(1), 0)); // Fund should no longer have parachain id let fund = Crowdfund::funds(0).unwrap(); assert_eq!(fund.parachain, None); }); } #[test] fn begin_retirement_handles_basic_errors() { new_test_ext().execute_with(|| { // Set up a crowdfund assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Balances::free_balance(1), 999); // Add deploy data assert_ok!(Crowdfund::fix_deploy_data( Origin::signed(1), 0, ::Hash::default(), 0, vec![0].into(), )); // Fund crowdfund assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 1000)); run_to_block(10); // Cannot retire fund that is not onboarded assert_noop!(Crowdfund::begin_retirement(Origin::signed(1), 0), Error::::NotParachain); // Onboard crowdfund assert_ok!(Crowdfund::onboard(Origin::signed(1), 0, 0.into())); // Fund is assigned a parachain id let fund = Crowdfund::funds(0).unwrap(); assert_eq!(fund.parachain, Some(0.into())); // Cannot retire fund whose deposit has not been returned assert_noop!(Crowdfund::begin_retirement(Origin::signed(1), 0), Error::::ParaHasDeposit); run_to_block(50); // Cannot retire invalid fund index assert_noop!(Crowdfund::begin_retirement(Origin::signed(1), 1), Error::::InvalidFundIndex); // Cannot retire twice assert_ok!(Crowdfund::begin_retirement(Origin::signed(1), 0)); assert_noop!(Crowdfund::begin_retirement(Origin::signed(1), 0), Error::::NotParachain); }); } #[test] fn withdraw_works() { new_test_ext().execute_with(|| { // Set up a crowdfund assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); // Transfer fee is taken here assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 100)); assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 200)); assert_ok!(Crowdfund::contribute(Origin::signed(3), 0, 300)); // Skip all the way to the end run_to_block(50); // User can withdraw their full balance without fees assert_ok!(Crowdfund::withdraw(Origin::signed(1), 0)); assert_eq!(Balances::free_balance(1), 999); assert_ok!(Crowdfund::withdraw(Origin::signed(2), 0)); assert_eq!(Balances::free_balance(2), 2000); assert_ok!(Crowdfund::withdraw(Origin::signed(3), 0)); assert_eq!(Balances::free_balance(3), 3000); }); } #[test] fn withdraw_handles_basic_errors() { new_test_ext().execute_with(|| { // Set up a crowdfund assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); // Transfer fee is taken here assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 49)); assert_eq!(Balances::free_balance(1), 950); run_to_block(5); // Cannot withdraw before fund ends assert_noop!(Crowdfund::withdraw(Origin::signed(1), 0), Error::::FundNotEnded); run_to_block(10); // Cannot withdraw if they did not contribute assert_noop!(Crowdfund::withdraw(Origin::signed(2), 0), Error::::NoContributions); // Cannot withdraw from a non-existent fund assert_noop!(Crowdfund::withdraw(Origin::signed(1), 1), Error::::InvalidFundIndex); }); } #[test] fn dissolve_works() { new_test_ext().execute_with(|| { // Set up a crowdfund assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); // Transfer fee is taken here assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 100)); assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 200)); assert_ok!(Crowdfund::contribute(Origin::signed(3), 0, 300)); // Skip all the way to the end run_to_block(50); // Check initiator's balance. assert_eq!(Balances::free_balance(1), 899); // Check current funds (contributions + deposit) assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 601); // Dissolve the crowdfund assert_ok!(Crowdfund::dissolve(Origin::signed(1), 0)); // Fund account is emptied assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 0); // Deposit is returned assert_eq!(Balances::free_balance(1), 900); // Treasury account is filled assert_eq!(Balances::free_balance(Treasury::account_id()), 600); // Storage trie is removed assert_eq!(Crowdfund::contribution_get(0,&0), 0); // Fund storage is removed assert_eq!(Crowdfund::funds(0), None); }); } #[test] fn dissolve_handles_basic_errors() { new_test_ext().execute_with(|| { // Set up a crowdfund assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); // Transfer fee is taken here assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 100)); assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 200)); assert_ok!(Crowdfund::contribute(Origin::signed(3), 0, 300)); // Cannot dissolve an invalid fund index assert_noop!(Crowdfund::dissolve(Origin::signed(1), 1), Error::::InvalidFundIndex); // Cannot dissolve a fund in progress assert_noop!(Crowdfund::dissolve(Origin::signed(1), 0), Error::::InRetirementPeriod); run_to_block(10); // Onboard fund assert_ok!(Crowdfund::fix_deploy_data( Origin::signed(1), 0, ::Hash::default(), 0, vec![0].into(), )); assert_ok!(Crowdfund::onboard(Origin::signed(1), 0, 0.into())); // Cannot dissolve an active fund assert_noop!(Crowdfund::dissolve(Origin::signed(1), 0), Error::::HasActiveParachain); }); } #[test] fn fund_before_auction_works() { new_test_ext().execute_with(|| { // Create a crowdfund before an auction is created assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); // Users can already contribute assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 49)); // Fund added to NewRaise assert_eq!(Crowdfund::new_raise(), vec![0]); // Some blocks later... run_to_block(2); // Create an auction assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); // Add deploy data assert_ok!(Crowdfund::fix_deploy_data( Origin::signed(1), 0, ::Hash::default(), 0, vec![0].into(), )); // Move to the end of auction... run_to_block(12); // Endings count incremented assert_eq!(Crowdfund::endings_count(), 1); // Onboard crowdfund assert_ok!(Crowdfund::onboard(Origin::signed(1), 0, 0.into())); let fund = Crowdfund::funds(0).unwrap(); // Crowdfund is now assigned a parachain id assert_eq!(fund.parachain, Some(0.into())); // This parachain is managed by Slots assert_eq!(Slots::managed_ids(), vec![0.into()]); }); } #[test] fn fund_across_multiple_auctions_works() { new_test_ext().execute_with(|| { // Create an auction assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); // Create two competing crowdfunds, with end dates across multiple auctions // Each crowdfund is competing for the same slots, so only one can win assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 30)); assert_ok!(Crowdfund::create(Origin::signed(2), 1000, 1, 4, 30)); // Contribute to all, but more money to 0, less to 1 assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 300)); assert_ok!(Crowdfund::contribute(Origin::signed(1), 1, 200)); // Add deploy data to all assert_ok!(Crowdfund::fix_deploy_data( Origin::signed(1), 0, ::Hash::default(), 0, vec![0].into(), )); assert_ok!(Crowdfund::fix_deploy_data( Origin::signed(2), 1, ::Hash::default(), 0, vec![0].into(), )); // End the current auction, fund 0 wins! run_to_block(10); assert_eq!(Crowdfund::endings_count(), 1); // Onboard crowdfund assert_ok!(Crowdfund::onboard(Origin::signed(1), 0, 0.into())); let fund = Crowdfund::funds(0).unwrap(); // Crowdfund is now assigned a parachain id assert_eq!(fund.parachain, Some(0.into())); // This parachain is managed by Slots assert_eq!(Slots::managed_ids(), vec![0.into()]); // Create a second auction assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); // Contribute to existing funds add to NewRaise assert_ok!(Crowdfund::contribute(Origin::signed(1), 1, 10)); // End the current auction, fund 1 wins! run_to_block(20); assert_eq!(Crowdfund::endings_count(), 2); // Onboard crowdfund assert_ok!(Crowdfund::onboard(Origin::signed(2), 1, 1.into())); let fund = Crowdfund::funds(1).unwrap(); // Crowdfund is now assigned a parachain id assert_eq!(fund.parachain, Some(1.into())); // This parachain is managed by Slots assert_eq!(Slots::managed_ids(), vec![0.into(), 1.into()]); }); } }