diff --git a/polkadot/runtime/src/crowdfund.rs b/polkadot/runtime/src/crowdfund.rs
new file mode 100644
index 0000000000..0209e99636
--- /dev/null
+++ b/polkadot/runtime/src/crowdfund.rs
@@ -0,0 +1,1230 @@
+// Copyright 2017-2019 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 srml_support::{
+ StorageValue, StorageMap, decl_module, decl_storage, decl_event, storage::child, ensure,
+ traits::{Currency, Get, OnUnbalanced, WithdrawReason, ExistenceRequirement}
+};
+use system::ensure_signed;
+use sr_primitives::{ModuleId, weights::SimpleDispatchInfo,
+ traits::{AccountIdConversion, Hash, Saturating, Zero, CheckedAdd}
+};
+use crate::slots;
+use codec::{Encode, Decode};
+use rstd::vec::Vec;
+use crate::parachains::ParachainRegistrar;
+use substrate_primitives::storage::well_known_keys::CHILD_STORAGE_KEY_PREFIX;
+
+const MODULE_ID: ModuleId = ModuleId(*b"py/cfund");
+
+pub type BalanceOf = <::Currency as Currency<::AccountId>>::Balance;
+pub type NegativeImbalanceOf = <::Currency as Currency<::AccountId>>::NegativeImbalance;
+pub type ParaIdOf = <::Parachains as ParachainRegistrar<::AccountId>>::ParaId;
+
+pub trait Trait: slots::Trait {
+ type Event: From> + Into<::Event>;
+
+ /// 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))]
+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 initial head data.
+ deploy_data: Option<(Hash, Vec)>,
+}
+
+decl_storage! {
+ trait Store for Module as Example {
+ /// Info on all of the funds.
+ Funds get(funds):
+ map FundIndex => Option, T::Hash, T::BlockNumber, ParaIdOf>>;
+
+ /// The total number of funds that have so far been allocated.
+ FundCount get(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(new_raise): Vec;
+
+ /// The number of auctions that have entered into their ending period so far.
+ EndingsCount get(endings_count): slots::AuctionIndex;
+ }
+}
+
+decl_event! {
+ pub enum Event where
+ ::AccountId,
+ Balance = BalanceOf,
+ ParaId = ParaIdOf,
+ {
+ Created(FundIndex),
+ Contributed(AccountId, FundIndex, Balance),
+ Withdrew(AccountId, FundIndex, Balance),
+ Retiring(FundIndex),
+ Dissolved(FundIndex),
+ DeployDataFixed(FundIndex),
+ Onboarded(FundIndex, ParaId),
+ }
+}
+
+decl_module! {
+ pub struct Module for enum Call where origin: T::Origin {
+ fn deposit_event() = default;
+
+ /// Create a new crowdfunding campaign for a parachain slot deposit for the current auction.
+ #[weight = SimpleDispatchInfo::FixedNormal(100_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, "last slot must be greater than first slot");
+ ensure!(last_slot <= first_slot + 3.into(), "last slot cannot be more then 3 more than first slot");
+ ensure!(end > >::block_number(), "end must be in the future");
+
+ let deposit = T::SubmissionDeposit::get();
+ let imb = T::Currency::withdraw(
+ &owner,
+ deposit,
+ WithdrawReason::Transfer,
+ ExistenceRequirement::AllowDeath,
+ )?;
+
+ let index = FundCount::get();
+ let next_index = index.checked_add(1).ok_or("overflow when adding fund")?;
+ 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
+ fn contribute(origin, #[compact] index: FundIndex, #[compact] value: BalanceOf) {
+ let who = ensure_signed(origin)?;
+
+ ensure!(value >= T::MinContribution::get(), "contribution too small");
+ let mut fund = Self::funds(index).ok_or("invalid fund index")?;
+ fund.raised = fund.raised.checked_add(&value).ok_or("overflow when adding new funds")?;
+ ensure!(fund.raised <= fund.cap, "contributions exceed cap");
+
+ // Make sure crowdfund has not ended
+ let now = >::block_number();
+ ensure!(fund.end > now, "contribution period ended");
+
+ T::Currency::transfer(&who, &Self::fund_account_id(index), value)?;
+
+ 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.
+ fn fix_deploy_data(origin,
+ #[compact] index: FundIndex,
+ code_hash: T::Hash,
+ initial_head_data: Vec
+ ) {
+ let who = ensure_signed(origin)?;
+
+ let mut fund = Self::funds(index).ok_or("invalid fund index")?;
+ ensure!(fund.owner == who, "origin must be fund owner");
+ ensure!(fund.deploy_data.is_none(), "deploy data already set");
+
+ fund.deploy_data = Some((code_hash, 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.
+ fn onboard(origin,
+ #[compact] index: FundIndex,
+ #[compact] para_id: ParaIdOf
+ ) {
+ let _ = ensure_signed(origin)?;
+
+ let mut fund = Self::funds(index).ok_or("invalid fund index")?;
+ let (code_hash, initial_head_data) = fund.clone().deploy_data.ok_or("deploy data not fixed")?;
+ ensure!(fund.parachain.is_none(), "fund already onboarded");
+ fund.parachain = Some(para_id);
+
+ let fund_origin = system::RawOrigin::Signed(Self::fund_account_id(index)).into();
+ >::fix_deploy_data(fund_origin, index, para_id, code_hash, 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.
+ fn begin_retirement(origin, #[compact] index: FundIndex) {
+ let _ = ensure_signed(origin)?;
+
+ let mut fund = Self::funds(index).ok_or("invalid fund index")?;
+ let parachain_id = fund.parachain.take().ok_or("fund has no parachain")?;
+ // No deposit information implies the parachain was off-boarded
+ ensure!(>::deposits(parachain_id).len() == 0, "parachain still has deposit");
+ 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, "funds not yet returned");
+
+ // 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.
+ fn withdraw(origin, #[compact] index: FundIndex) {
+ let who = ensure_signed(origin)?;
+
+ let mut fund = Self::funds(index).ok_or("invalid fund index")?;
+ ensure!(fund.parachain.is_none(), "fund has not retired");
+ let now = >::block_number();
+
+ // `fund.end` can represent the end of a failed crowdsale or the beginning of retirement
+ ensure!(now >= fund.end, "fund has not ended");
+
+ let balance = Self::contribution_get(index, &who);
+ ensure!(balance > Zero::zero(), "no contributions stored");
+
+ // Avoid using transfer to ensure we don't pay any fees.
+ let _ = T::Currency::resolve_into_existing(&who, T::Currency::withdraw(
+ &Self::fund_account_id(index),
+ balance,
+ WithdrawReason::Transfer,
+ ExistenceRequirement::AllowDeath
+ )?);
+
+ 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.
+ fn dissolve(origin, #[compact] index: FundIndex) {
+ let _ = ensure_signed(origin)?;
+
+ let fund = Self::funds(index).ok_or("invalid fund index")?;
+ ensure!(fund.parachain.is_none(), "cannot dissolve fund with active parachain");
+ let now = >::block_number();
+ ensure!(now >= fund.end + T::RetirementPeriod::get(), "retirement period not over");
+
+ let account = Self::fund_account_id(index);
+
+ // Avoid using transfer to ensure we don't pay any fees.
+ let _ = T::Currency::resolve_into_existing(&fund.owner, T::Currency::withdraw(
+ &account,
+ fund.deposit,
+ WithdrawReason::Transfer,
+ ExistenceRequirement::AllowDeath
+ )?);
+
+ T::OrphanedFunds::on_unbalanced(T::Currency::withdraw(
+ &account,
+ fund.raised,
+ WithdrawReason::Transfer,
+ ExistenceRequirement::AllowDeath
+ )?);
+
+ 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 {
+ MODULE_ID.into_sub_account(index)
+ }
+
+ pub fn id_from_index(index: FundIndex) -> Vec {
+ let mut buf = Vec::new();
+ buf.extend_from_slice(b"crowdfund");
+ buf.extend_from_slice(&index.to_le_bytes()[..]);
+
+ CHILD_STORAGE_KEY_PREFIX.into_iter()
+ .chain(b"default:")
+ .chain(T::Hashing::hash(&buf[..]).as_ref().into_iter())
+ .cloned()
+ .collect()
+ }
+
+ pub fn contribution_put(index: FundIndex, who: &T::AccountId, balance: &BalanceOf) {
+ let id = Self::id_from_index(index);
+ who.using_encoded(|b| child::put(id.as_ref(), b, balance));
+ }
+
+ pub fn contribution_get(index: FundIndex, who: &T::AccountId) -> BalanceOf {
+ let id = Self::id_from_index(index);
+ who.using_encoded(|b| child::get_or_default::>(id.as_ref(), b))
+ }
+
+ pub fn contribution_kill(index: FundIndex, who: &T::AccountId) {
+ let id = Self::id_from_index(index);
+ who.using_encoded(|b| child::kill(id.as_ref(), b));
+ }
+
+ pub fn crowdfund_kill(index: FundIndex) {
+ let id = Self::id_from_index(index);
+ child::kill_storage(id.as_ref());
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use std::{collections::HashMap, cell::RefCell};
+ use srml_support::{impl_outer_origin, assert_ok, assert_noop, parameter_types};
+ use sr_io::with_externalities;
+ use substrate_primitives::{H256, Blake2Hasher};
+ use primitives::parachain::Id as ParaId;
+ // The testing primitives are very useful for avoiding having to work with signatures
+ // or public keys. `u64` is used as the `AccountId` and no `Signature`s are requried.
+ use sr_primitives::{
+ Perbill, Permill, testing::Header,
+ traits::{BlakeTwo256, OnInitialize, OnFinalize, IdentityLookup, ConvertInto},
+ };
+
+ 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;
+ pub const MaximumBlockWeight: u32 = 4 * 1024 * 1024;
+ pub const MaximumBlockLength: u32 = 4 * 1024 * 1024;
+ pub const AvailableBlockRatio: Perbill = Perbill::from_percent(75);
+ }
+ impl system::Trait for Test {
+ 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 WeightMultiplierUpdate = ();
+ type Event = ();
+ type BlockHashCount = BlockHashCount;
+ type MaximumBlockWeight = MaximumBlockWeight;
+ type MaximumBlockLength = MaximumBlockLength;
+ type AvailableBlockRatio = AvailableBlockRatio;
+ type Version = ();
+ }
+ parameter_types! {
+ pub const ExistentialDeposit: u64 = 0;
+ // We want to make sure these fees are non zero, so we can check
+ // that our module correctly avoids these fees :)
+ pub const TransferFee: u64 = 10;
+ pub const CreationFee: u64 = 10;
+ pub const TransactionBaseFee: u64 = 0;
+ pub const TransactionByteFee: u64 = 0;
+ }
+ impl balances::Trait for Test {
+ type Balance = u64;
+ type OnFreeBalanceZero = ();
+ type OnNewAccount = ();
+ type Event = ();
+ type TransactionPayment = ();
+ type DustRemoval = ();
+ type TransferPayment = ();
+ type ExistentialDeposit = ExistentialDeposit;
+ type TransferFee = TransferFee;
+ type CreationFee = CreationFee;
+ type TransactionBaseFee = TransactionBaseFee;
+ type TransactionByteFee = TransactionByteFee;
+ type WeightToFee = ConvertInto;
+ }
+
+ 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);
+ }
+ impl treasury::Trait for Test {
+ type Currency = balances::Module;
+ type ApproveOrigin = system::EnsureRoot;
+ type RejectOrigin = system::EnsureRoot;
+ type Event = ();
+ type MintedForSpending = ();
+ type ProposalRejection = ();
+ type ProposalBond = ProposalBond;
+ type ProposalBondMinimum = ProposalBondMinimum;
+ type SpendPeriod = SpendPeriod;
+ type Burn = Burn;
+ }
+
+ thread_local! {
+ pub static PARACHAIN_COUNT: RefCell = RefCell::new(0);
+ pub static PARACHAINS:
+ RefCell, Vec)>> = RefCell::new(HashMap::new());
+ }
+
+ pub struct TestParachains;
+ impl ParachainRegistrar for TestParachains {
+ type ParaId = ParaId;
+ fn new_id() -> Self::ParaId {
+ PARACHAIN_COUNT.with(|p| {
+ *p.borrow_mut() += 1;
+ (*p.borrow() - 1).into()
+ })
+ }
+ fn register_parachain(
+ id: Self::ParaId,
+ code: Vec,
+ initial_head_data: Vec
+ ) -> Result<(), &'static str> {
+ PARACHAINS.with(|p| {
+ if p.borrow().contains_key(&id.into_inner()) {
+ panic!("ID already exists")
+ }
+ p.borrow_mut().insert(id.into_inner(), (code, initial_head_data));
+ Ok(())
+ })
+ }
+ fn deregister_parachain(id: Self::ParaId) -> Result<(), &'static str> {
+ PARACHAINS.with(|p| {
+ if !p.borrow().contains_key(&id.into_inner()) {
+ panic!("ID doesn't exist")
+ }
+ p.borrow_mut().remove(&id.into_inner());
+ Ok(())
+ })
+ }
+ }
+
+ parameter_types!{
+ pub const LeasePeriod: u64 = 10;
+ pub const EndingPeriod: u64 = 3;
+ }
+ impl slots::Trait for Test {
+ type Event = ();
+ type Currency = Balances;
+ type Parachains = TestParachains;
+ type LeasePeriod = LeasePeriod;
+ type EndingPeriod = EndingPeriod;
+ }
+ parameter_types! {
+ pub const SubmissionDeposit: u64 = 1;
+ pub const MinContribution: u64 = 10;
+ pub const RetirementPeriod: u64 = 5;
+ }
+ impl Trait for Test {
+ type Event = ();
+ type SubmissionDeposit = SubmissionDeposit;
+ type MinContribution = MinContribution;
+ type RetirementPeriod = RetirementPeriod;
+ type OrphanedFunds = Treasury;
+ }
+
+ type System = system::Module;
+ type Balances = balances::Module;
+ type Slots = slots::Module;
+ type Treasury = treasury::Module;
+ type Crowdfund = Module;
+
+ // This function basically just builds a genesis storage key/value store according to
+ // our desired mockup.
+ fn new_test_ext() -> sr_io::TestExternalities {
+ let mut t = system::GenesisConfig::default().build_storage::().unwrap();
+ balances::GenesisConfig::{
+ balances: vec![(1, 1000), (2, 2000), (3, 3000), (4, 4000)],
+ vesting: vec![],
+ }.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_finalize(System::block_number());
+ Crowdfund::on_initialize(System::block_number());
+ }
+ }
+
+ #[test]
+ fn basic_setup_works() {
+ with_externalities(&mut new_test_ext(), || {
+ assert_eq!(System::block_number(), 1);
+ 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() {
+ with_externalities(&mut new_test_ext(), || {
+ // 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() {
+ with_externalities(&mut new_test_ext(), || {
+ // Cannot create a crowdfund with bad slots
+ assert_noop!(Crowdfund::create(Origin::signed(1), 1000, 4, 1, 9), "last slot must be greater than first slot");
+ assert_noop!(Crowdfund::create(Origin::signed(1), 1000, 1, 5, 9), "last slot cannot be more then 3 more than first slot");
+
+ // Cannot create a crowdfund without some deposit funds
+ assert_noop!(Crowdfund::create(Origin::signed(1337), 1000, 1, 3, 9), "too few free funds in account");
+ });
+ }
+
+ #[test]
+ fn contribute_works() {
+ with_externalities(&mut new_test_ext(), || {
+ // 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), 940);
+ // 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() {
+ with_externalities(&mut new_test_ext(), || {
+ // Cannot contribute to non-existing fund
+ assert_noop!(Crowdfund::contribute(Origin::signed(1), 0, 49), "invalid fund index");
+ // Cannot contribute below minimum contribution
+ assert_noop!(Crowdfund::contribute(Origin::signed(1), 0, 9), "contribution too small");
+
+ // 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), "contributions exceed cap");
+
+ // Move past end date
+ run_to_block(10);
+
+ // Cannot contribute to ended fund
+ assert_noop!(Crowdfund::contribute(Origin::signed(1), 0, 49), "contribution period ended");
+ });
+ }
+
+ #[test]
+ fn fix_deploy_data_works() {
+ with_externalities(&mut new_test_ext(), || {
+ // 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(),
+ vec![0]
+ ));
+
+ let fund = Crowdfund::funds(0).unwrap();
+
+ // Confirm deploy data is stored correctly
+ assert_eq!(fund.deploy_data, Some((::Hash::default(), vec![0])));
+ });
+ }
+
+ #[test]
+ fn fix_deploy_data_handles_basic_errors() {
+ with_externalities(&mut new_test_ext(), || {
+ // 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(),
+ vec![0]),
+ "origin must be fund owner"
+ );
+
+ // Cannot set deploy data to an invalid index
+ assert_noop!(Crowdfund::fix_deploy_data(
+ Origin::signed(1),
+ 1,
+ ::Hash::default(),
+ vec![0]),
+ "invalid fund index"
+ );
+
+ // Cannot set deploy data after it already has been set
+ assert_ok!(Crowdfund::fix_deploy_data(
+ Origin::signed(1),
+ 0,
+ ::Hash::default(),
+ vec![0]
+ ));
+
+ assert_noop!(Crowdfund::fix_deploy_data(
+ Origin::signed(1),
+ 0,
+ ::Hash::default(),
+ vec![1]),
+ "deploy data already set"
+ );
+ });
+ }
+
+ #[test]
+ fn onboard_works() {
+ with_externalities(&mut new_test_ext(), || {
+ // 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(),
+ vec![0]
+ ));
+
+ // 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() {
+ with_externalities(&mut new_test_ext(), || {
+ // 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()), "invalid fund index");
+ // Cannot onboard crowdfund without deploy data
+ assert_noop!(Crowdfund::onboard(Origin::signed(1), 0, 0.into()), "deploy data not fixed");
+
+ // Add deploy data
+ assert_ok!(Crowdfund::fix_deploy_data(
+ Origin::signed(1),
+ 0,
+ ::Hash::default(),
+ vec![0]
+ ));
+
+ // Cannot onboard fund with incorrect parachain id
+ assert_noop!(Crowdfund::onboard(Origin::signed(1), 0, 1.into()), "parachain id not in onboarding");
+
+ // 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()), "fund already onboarded");
+ });
+ }
+
+ #[test]
+ fn begin_retirement_works() {
+ with_externalities(&mut new_test_ext(), || {
+ // 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(),
+ vec![0]
+ ));
+
+ // 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() {
+ with_externalities(&mut new_test_ext(), || {
+ // 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(),
+ vec![0]
+ ));
+
+ // 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), "fund has no parachain");
+
+ // 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), "parachain still has deposit");
+
+ run_to_block(50);
+
+ // Cannot retire invalid fund index
+ assert_noop!(Crowdfund::begin_retirement(Origin::signed(1), 1), "invalid fund index");
+
+ // Cannot retire twice
+ assert_ok!(Crowdfund::begin_retirement(Origin::signed(1), 0));
+ assert_noop!(Crowdfund::begin_retirement(Origin::signed(1), 0), "fund has no parachain");
+ });
+ }
+
+ #[test]
+ fn withdraw_works() {
+ with_externalities(&mut new_test_ext(), || {
+ // 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), 989);
+
+ assert_ok!(Crowdfund::withdraw(Origin::signed(2), 0));
+ assert_eq!(Balances::free_balance(2), 1990);
+
+ assert_ok!(Crowdfund::withdraw(Origin::signed(3), 0));
+ assert_eq!(Balances::free_balance(3), 2990);
+ });
+ }
+
+ #[test]
+ fn withdraw_handles_basic_errors() {
+ with_externalities(&mut new_test_ext(), || {
+ // 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), 940);
+
+ run_to_block(5);
+
+ // Cannot withdraw before fund ends
+ assert_noop!(Crowdfund::withdraw(Origin::signed(1), 0), "fund has not ended");
+
+ run_to_block(10);
+
+ // Cannot withdraw if they did not contribute
+ assert_noop!(Crowdfund::withdraw(Origin::signed(2), 0), "no contributions stored");
+ // Cannot withdraw from a non-existent fund
+ assert_noop!(Crowdfund::withdraw(Origin::signed(1), 1), "invalid fund index");
+ });
+ }
+
+ #[test]
+ fn dissolve_works() {
+ with_externalities(&mut new_test_ext(), || {
+ // 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 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), 890);
+ // 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() {
+ with_externalities(&mut new_test_ext(), || {
+ // 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), "invalid fund index");
+ // Cannot dissolve a fund in progress
+ assert_noop!(Crowdfund::dissolve(Origin::signed(1), 0), "retirement period not over");
+
+ run_to_block(10);
+
+ // Onboard fund
+ assert_ok!(Crowdfund::fix_deploy_data(
+ Origin::signed(1),
+ 0,
+ ::Hash::default(),
+ vec![0]
+ ));
+ assert_ok!(Crowdfund::onboard(Origin::signed(1), 0, 0.into()));
+
+ // Cannot dissolve an active fund
+ assert_noop!(Crowdfund::dissolve(Origin::signed(1), 0), "cannot dissolve fund with active parachain");
+ });
+ }
+
+ #[test]
+ fn fund_before_auction_works() {
+ with_externalities(&mut new_test_ext(), || {
+ // 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(),
+ vec![0]
+ ));
+ // 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() {
+ with_externalities(&mut new_test_ext(), || {
+ // 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(),
+ vec![0]
+ ));
+ assert_ok!(Crowdfund::fix_deploy_data(
+ Origin::signed(2),
+ 1,
+ ::Hash::default(),
+ vec![0]
+ ));
+
+ // 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()]);
+ });
+ }
+}
diff --git a/polkadot/runtime/src/lib.rs b/polkadot/runtime/src/lib.rs
index ed28b88c81..6c66b12b4f 100644
--- a/polkadot/runtime/src/lib.rs
+++ b/polkadot/runtime/src/lib.rs
@@ -25,6 +25,7 @@ mod claims;
mod parachains;
mod slot_range;
mod slots;
+mod crowdfund;
use rstd::prelude::*;
use codec::{Encode, Decode};
diff --git a/polkadot/runtime/src/slots.rs b/polkadot/runtime/src/slots.rs
index c7a4bbe985..cc5517b9ed 100644
--- a/polkadot/runtime/src/slots.rs
+++ b/polkadot/runtime/src/slots.rs
@@ -65,10 +65,10 @@ pub type AuctionIndex = u32;
#[cfg_attr(feature = "std", derive(Debug))]
pub struct NewBidder {
/// The bidder's account ID; this is the account that funds the bid.
- who: AccountId,
+ pub who: AccountId,
/// An additional ID to allow the same account ID (and funding source) to have multiple
/// logical bidders.
- sub: SubId,
+ pub sub: SubId,
}
/// The desired target of a bidder in an auction.
@@ -246,8 +246,7 @@ decl_module! {
/// called by the root origin. Accepts the `duration` of this auction and the
/// `lease_period_index` of the initial lease period of the four that are to be auctioned.
#[weight = SimpleDispatchInfo::FixedOperational(100_000)]
- fn new_auction(
- origin,
+ pub fn new_auction(origin,
#[compact] duration: T::BlockNumber,
#[compact] lease_period_index: LeasePeriodOf
) {
@@ -282,8 +281,7 @@ decl_module! {
/// - `amount` is the amount to bid to be held as deposit for the parachain should the
/// bid win. This amount is held throughout the range.
#[weight = SimpleDispatchInfo::FixedNormal(500_000)]
- fn bid(
- origin,
+ fn bid(origin,
#[compact] sub: SubId,
#[compact] auction_index: AuctionIndex,
#[compact] first_slot: LeasePeriodOf,
@@ -310,9 +308,8 @@ decl_module! {
/// absolute lease period index value, not an auction-specific offset.
/// - `amount` is the amount to bid to be held as deposit for the parachain should the
/// bid win. This amount is held throughout the range.
- #[weight = SimpleDispatchInfo::FixedNormal(500_000)]
- fn bid_renew(
- origin,
+ #[weight = SimpleDispatchInfo::FixedNormal(500_000)]
+ fn bid_renew(origin,
#[compact] auction_index: AuctionIndex,
#[compact] first_slot: LeasePeriodOf,
#[compact] last_slot: LeasePeriodOf,
@@ -346,9 +343,8 @@ decl_module! {
/// - `para_id` is the parachain ID allotted to the winning bidder.
/// - `code_hash` is the hash of the parachain's Wasm validation function.
/// - `initial_head_data` is the parachain's initial head data.
- #[weight = SimpleDispatchInfo::FixedNormal(500_000)]
- fn fix_deploy_data(
- origin,
+ #[weight = SimpleDispatchInfo::FixedNormal(500_000)]
+ pub fn fix_deploy_data(origin,
#[compact] sub: SubId,
#[compact] para_id: ParaIdOf,
code_hash: T::Hash,
@@ -408,13 +404,13 @@ impl Module {
}
/// True if an auction is in progress.
- fn is_in_progress() -> bool {
+ pub fn is_in_progress() -> bool {
>::exists()
}
/// Returns `Some(n)` if the now block is part of the ending period of an auction, where `n`
/// represents how far into the ending period this block is. Otherwise, returns `None`.
- fn is_ending(now: T::BlockNumber) -> Option {
+ pub fn is_ending(now: T::BlockNumber) -> Option {
if let Some((_, early_end)) = >::get() {
if let Some(after_early_end) = now.checked_sub(&early_end) {
if after_early_end < T::EndingPeriod::get() {
@@ -496,7 +492,7 @@ impl Module {
}
// Add para IDs of any chains that will be newly deployed to our set of managed
- // IDs
+ // IDs.
>::mutate(|m| m.push(para_id));
Self::deposit_event(RawEvent::WonDeploy(bidder.clone(), range, para_id, amount));
@@ -506,6 +502,8 @@ impl Module {
let begin_offset = >::from(range.as_pair().0 as u32);
let begin_lease_period = auction_lease_period_index + begin_offset;
>::mutate(begin_lease_period, |starts| starts.push(para_id));
+ // Add a default off-boarding account which matches the original bidder
+ >::insert(¶_id, &bidder.who);
let entry = (begin_lease_period, IncomingParachain::Unset(bidder));
>::insert(¶_id, entry);
}
@@ -645,7 +643,7 @@ impl Module {
/// - `first_slot`: The first lease period index of the range to be bid on.
/// - `last_slot`: The last lease period index of the range to be bid on (inclusive).
/// - `amount`: The total amount to be the bid for deposit over the range.
- fn handle_bid(
+ pub fn handle_bid(
bidder: Bidder>,
auction_index: u32,
first_slot: LeasePeriodOf,
@@ -1046,6 +1044,24 @@ mod tests {
#[test]
fn offboarding_works() {
+ with_externalities(&mut new_test_ext(), || {
+ run_to_block(1);
+ assert_ok!(Slots::new_auction(Origin::ROOT, 5, 1));
+ assert_ok!(Slots::bid(Origin::signed(1), 0, 1, 1, 4, 1));
+ assert_eq!(Balances::free_balance(&1), 9);
+
+ run_to_block(9);
+ assert_eq!(Slots::deposit_held(&0.into()), 1);
+ assert_eq!(Slots::deposits(&0.into())[0], 0);
+
+ run_to_block(50);
+ assert_eq!(Slots::deposit_held(&0.into()), 0);
+ assert_eq!(Balances::free_balance(&1), 10);
+ });
+ }
+
+ #[test]
+ fn set_offboarding_works() {
with_externalities(&mut new_test_ext(), || {
run_to_block(1);
assert_ok!(Slots::new_auction(Origin::ROOT, 5, 1));