From 60ce4f538041cd7d93a2355bf5dd0242938a5bb5 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Fri, 5 Jul 2019 13:40:22 +0200 Subject: [PATCH 01/42] Initial crowdfund stuff --- network/src/validation.rs | 3 +-- runtime/src/lib.rs | 1 + runtime/src/parachains.rs | 10 +++++----- runtime/src/slots.rs | 6 +++--- validation/src/lib.rs | 39 +++++++++++++++++++-------------------- 5 files changed, 29 insertions(+), 30 deletions(-) diff --git a/network/src/validation.rs b/network/src/validation.rs index 8914b417fa54..69891913b489 100644 --- a/network/src/validation.rs +++ b/network/src/validation.rs @@ -330,7 +330,6 @@ impl ParachainNetwork for ValidationNetwork where &self, table: Arc, authorities: &[ValidatorId], - exit: exit_future::Exit, ) -> Self::BuildTableRouter { let parent_hash = *table.consensus_parent_hash(); let local_session_key = table.session_key(); @@ -355,7 +354,7 @@ impl ParachainNetwork for ValidationNetwork where let table_router_clone = table_router.clone(); let work = table_router.checked_statements() .for_each(move |msg| { table_router_clone.import_statement(msg); Ok(()) }); - executor.spawn(work.select(exit).map(|_| ()).map_err(|_| ())); + executor.spawn(work); table_router }); diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 6ff24d2751fc..74579424ba88 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -25,6 +25,7 @@ mod parachains; mod claims; mod slot_range; mod slots; +mod crowdfund; use rstd::prelude::*; use substrate_primitives::u32_trait::{_1, _2, _3, _4}; diff --git a/runtime/src/parachains.rs b/runtime/src/parachains.rs index 8f450a3a351a..9f8378797c08 100644 --- a/runtime/src/parachains.rs +++ b/runtime/src/parachains.rs @@ -22,7 +22,9 @@ use parity_codec::{Decode, HasCompact}; use srml_support::{decl_storage, decl_module, fail, ensure}; use bitvec::{bitvec, BigEndian}; -use sr_primitives::traits::{Hash as HashT, BlakeTwo256, Member, CheckedConversion, Saturating, One}; +use sr_primitives::traits::{ + Hash as HashT, BlakeTwo256, Member, CheckedConversion, Saturating, One, Zero, +}; use primitives::{Hash, Balance, parachain::{ self, Id as ParaId, Chain, DutyRoster, AttestedCandidate, Statement, AccountIdConversion, ParachainDispatchOrigin, UpwardMessage, BlockIngressRoots, @@ -241,11 +243,9 @@ decl_storage! { config(parachains): Vec<(ParaId, Vec, Vec)>; config(_phdata): PhantomData; build(|storage: &mut StorageOverlay, _: &mut ChildrenStorageOverlay, config: &GenesisConfig| { - use sr_primitives::traits::Zero; - let mut p = config.parachains.clone(); - p.sort_unstable_by_key(|&(ref id, _, _)| *id); - p.dedup_by_key(|&mut (ref id, _, _)| *id); + p.sort_unstable_by_key(|&(ref id, _, _)| id.clone()); + p.dedup_by_key(|&mut (ref id, _, _)| id.clone()); let only_ids: Vec<_> = p.iter().map(|&(ref id, _, _)| id).cloned().collect(); diff --git a/runtime/src/slots.rs b/runtime/src/slots.rs index ea1151a919ca..b1b2d5b74a79 100644 --- a/runtime/src/slots.rs +++ b/runtime/src/slots.rs @@ -335,7 +335,7 @@ 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. - fn fix_deploy_data(origin, + pub fn fix_deploy_data(origin, #[compact] sub: SubId, #[compact] para_id: ParaIdOf, code_hash: T::Hash, @@ -482,7 +482,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)); @@ -631,7 +631,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, diff --git a/validation/src/lib.rs b/validation/src/lib.rs index 652d98f64502..bd946f4bbe2c 100644 --- a/validation/src/lib.rs +++ b/validation/src/lib.rs @@ -147,7 +147,6 @@ pub trait Network { &self, table: Arc, authorities: &[SessionKey], - exit: exit_future::Exit, ) -> Self::BuildTableRouter; } @@ -314,14 +313,11 @@ impl ParachainValidation where let (group_info, local_duty) = make_group_info( duty_roster, &authorities, - sign_with.public(), + sign_with.public().into(), )?; - info!( - "Starting parachain attestation session on top of parent {:?}. Local parachain duty is {:?}", - parent_hash, - local_duty.validation, - ); + info!("Starting parachain attestation session on top of parent {:?}. Local parachain duty is {:?}", + parent_hash, local_duty.validation); let active_parachains = self.client.runtime_api().active_parachains(&id)?; @@ -335,23 +331,25 @@ impl ParachainValidation where self.extrinsic_store.clone(), max_block_data_size, )); - - let (_drop_signal, exit) = exit_future::signal(); - let router = self.network.communication_for( table.clone(), &authorities, - exit.clone(), ); - if let Chain::Parachain(id) = local_duty.validation { - self.launch_work(parent_hash, id, router, max_block_data_size, exit); - } + let drop_signal = match local_duty.validation { + Chain::Parachain(id) => Some(self.launch_work( + parent_hash, + id, + router, + max_block_data_size, + )), + Chain::Relay => None, + }; let tracker = Arc::new(AttestationTracker { table, started: Instant::now(), - _drop_signal, + _drop_signal: drop_signal }); live_instances.insert(parent_hash, tracker.clone()); @@ -371,10 +369,10 @@ impl ParachainValidation where validation_para: ParaId, build_router: N::BuildTableRouter, max_block_data_size: Option, - exit: exit_future::Exit, - ) { + ) -> exit_future::Signal { use extrinsic_store::Data; + let (signal, exit) = exit_future::signal(); let (collators, client) = (self.collators.clone(), self.client.clone()); let extrinsic_store = self.extrinsic_store.clone(); @@ -430,15 +428,16 @@ impl ParachainValidation where .then(|_| Ok(())); // spawn onto thread pool. - if self.handle.execute(Box::new(cancellable_work)).is_err() { + if let Err(_) = self.handle.execute(Box::new(cancellable_work)) { error!("Failed to spawn cancellable work task"); } + signal } } /// Parachain validation for a single block. struct AttestationTracker { - _drop_signal: exit_future::Signal, + _drop_signal: Option, table: Arc, started: Instant, } @@ -545,7 +544,7 @@ impl consensus::Environment for ProposerFactory Date: Fri, 5 Jul 2019 11:42:57 +0200 Subject: [PATCH 02/42] Make `communication_for` exit when we end a round (#313) * Make `communication_for` exit when we end a round * Fix compilation --- network/src/validation.rs | 3 ++- runtime/src/parachains.rs | 10 +++++----- validation/src/lib.rs | 39 ++++++++++++++++++++------------------- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/network/src/validation.rs b/network/src/validation.rs index 69891913b489..8914b417fa54 100644 --- a/network/src/validation.rs +++ b/network/src/validation.rs @@ -330,6 +330,7 @@ impl ParachainNetwork for ValidationNetwork where &self, table: Arc, authorities: &[ValidatorId], + exit: exit_future::Exit, ) -> Self::BuildTableRouter { let parent_hash = *table.consensus_parent_hash(); let local_session_key = table.session_key(); @@ -354,7 +355,7 @@ impl ParachainNetwork for ValidationNetwork where let table_router_clone = table_router.clone(); let work = table_router.checked_statements() .for_each(move |msg| { table_router_clone.import_statement(msg); Ok(()) }); - executor.spawn(work); + executor.spawn(work.select(exit).map(|_| ()).map_err(|_| ())); table_router }); diff --git a/runtime/src/parachains.rs b/runtime/src/parachains.rs index 9f8378797c08..8f450a3a351a 100644 --- a/runtime/src/parachains.rs +++ b/runtime/src/parachains.rs @@ -22,9 +22,7 @@ use parity_codec::{Decode, HasCompact}; use srml_support::{decl_storage, decl_module, fail, ensure}; use bitvec::{bitvec, BigEndian}; -use sr_primitives::traits::{ - Hash as HashT, BlakeTwo256, Member, CheckedConversion, Saturating, One, Zero, -}; +use sr_primitives::traits::{Hash as HashT, BlakeTwo256, Member, CheckedConversion, Saturating, One}; use primitives::{Hash, Balance, parachain::{ self, Id as ParaId, Chain, DutyRoster, AttestedCandidate, Statement, AccountIdConversion, ParachainDispatchOrigin, UpwardMessage, BlockIngressRoots, @@ -243,9 +241,11 @@ decl_storage! { config(parachains): Vec<(ParaId, Vec, Vec)>; config(_phdata): PhantomData; build(|storage: &mut StorageOverlay, _: &mut ChildrenStorageOverlay, config: &GenesisConfig| { + use sr_primitives::traits::Zero; + let mut p = config.parachains.clone(); - p.sort_unstable_by_key(|&(ref id, _, _)| id.clone()); - p.dedup_by_key(|&mut (ref id, _, _)| id.clone()); + p.sort_unstable_by_key(|&(ref id, _, _)| *id); + p.dedup_by_key(|&mut (ref id, _, _)| *id); let only_ids: Vec<_> = p.iter().map(|&(ref id, _, _)| id).cloned().collect(); diff --git a/validation/src/lib.rs b/validation/src/lib.rs index bd946f4bbe2c..652d98f64502 100644 --- a/validation/src/lib.rs +++ b/validation/src/lib.rs @@ -147,6 +147,7 @@ pub trait Network { &self, table: Arc, authorities: &[SessionKey], + exit: exit_future::Exit, ) -> Self::BuildTableRouter; } @@ -313,11 +314,14 @@ impl ParachainValidation where let (group_info, local_duty) = make_group_info( duty_roster, &authorities, - sign_with.public().into(), + sign_with.public(), )?; - info!("Starting parachain attestation session on top of parent {:?}. Local parachain duty is {:?}", - parent_hash, local_duty.validation); + info!( + "Starting parachain attestation session on top of parent {:?}. Local parachain duty is {:?}", + parent_hash, + local_duty.validation, + ); let active_parachains = self.client.runtime_api().active_parachains(&id)?; @@ -331,25 +335,23 @@ impl ParachainValidation where self.extrinsic_store.clone(), max_block_data_size, )); + + let (_drop_signal, exit) = exit_future::signal(); + let router = self.network.communication_for( table.clone(), &authorities, + exit.clone(), ); - let drop_signal = match local_duty.validation { - Chain::Parachain(id) => Some(self.launch_work( - parent_hash, - id, - router, - max_block_data_size, - )), - Chain::Relay => None, - }; + if let Chain::Parachain(id) = local_duty.validation { + self.launch_work(parent_hash, id, router, max_block_data_size, exit); + } let tracker = Arc::new(AttestationTracker { table, started: Instant::now(), - _drop_signal: drop_signal + _drop_signal, }); live_instances.insert(parent_hash, tracker.clone()); @@ -369,10 +371,10 @@ impl ParachainValidation where validation_para: ParaId, build_router: N::BuildTableRouter, max_block_data_size: Option, - ) -> exit_future::Signal { + exit: exit_future::Exit, + ) { use extrinsic_store::Data; - let (signal, exit) = exit_future::signal(); let (collators, client) = (self.collators.clone(), self.client.clone()); let extrinsic_store = self.extrinsic_store.clone(); @@ -428,16 +430,15 @@ impl ParachainValidation where .then(|_| Ok(())); // spawn onto thread pool. - if let Err(_) = self.handle.execute(Box::new(cancellable_work)) { + if self.handle.execute(Box::new(cancellable_work)).is_err() { error!("Failed to spawn cancellable work task"); } - signal } } /// Parachain validation for a single block. struct AttestationTracker { - _drop_signal: Option, + _drop_signal: exit_future::Signal, table: Arc, started: Instant, } @@ -544,7 +545,7 @@ impl consensus::Environment for ProposerFactory Date: Fri, 5 Jul 2019 13:45:09 +0200 Subject: [PATCH 03/42] Add file --- runtime/src/crowdfund.rs | 441 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 441 insertions(+) create mode 100644 runtime/src/crowdfund.rs diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs new file mode 100644 index 000000000000..13c08f525dc1 --- /dev/null +++ b/runtime/src/crowdfund.rs @@ -0,0 +1,441 @@ +// Copyright 2017-2019 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate 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. + +// Substrate 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 Substrate. If not, see . + +//! # Parachain Crowdfunding module + +use srml_support::{ + StorageValue, dispatch::Result, decl_module, decl_storage, decl_event, storage::child, + traits::{LockableCurrency, ReservableCurrency, Currency} +}; +use system::ensure_signed; +use sr_primitives::weights::TransactionWeight; +use primitives::parachain::Id as ParaId; +use crate::slots; + +const MODULE_ID: ModuleId = ModuleId(*b"py/cfund"); + +type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; +type NegativeImbalanceOf = <::Currency as Currency<::AccountId>>::NegativeImbalance; + +pub trait Trait: slots::Trait { + type Currency: LockableCurrency + ReservableCurrency; + + /// The overarching event type. + type Event: From> + Into<::Event>; + + /// The amount to be held on deposit by the owner of a crowdfund. + const SUBMISSION_DEPOSIT: BalanceOf; + + /// The minimum amount that may be contributed into a crowdfund. Should almost certainly be at + /// least ExistentialDeposit. + const MIN_CONTRIBUTION: BalanceOf; + + /// The period of time (in blocks) between after an unsuccessful crowdfund ending where + /// contributors are able to withdraw their funds. After this period, their funds are lost. + const WITHDRAW_PERIOD: Self::BlockNumber; + + /// What to do with funds that were not withdrawn. + type OrphanedFunds: OnUnbalanced>; +} + +pub type FundIndex = u32; + +pub struct FundInfo { + /// The id of the child-trie. + id: Hash, + /// 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 disolved. + 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 + /// the 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. + last_contribution: BlockNumber, + /// 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 pub(funds): + map FundIndex => Option, T::Hash, T::BlockNumber>>; + + /// The total number of ffunds that have so far been allocated. + FundCount pub(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 updated bids. + NewRaise: Vec; + } +} + +decl_event!( + pub enum Event { + Nothing, + } +); + +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. + #[weight = TransactionWeight::Basic(100_000, 10)] + fn create(origin, + #[compact] end: T::BlockNumber, + #[compact] cap: BalanceOf, + #[compact] first_slot: T::BlockNumber, + #[compact] last_slot: T::BlockNumber, + ) { + let owner = ensure_signed(origin)?; + let deposit = T::SUBMISSION_DEPOSIT; + + let imb = T::Currency::withdraw( + &owner, + deposit, + WithdrawReason::Transfer, + ExistenceRequirement::AllowDeath + )?; + // 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::account_id(), imb); + + let index = >::mutate(|c| { let r = *c; *c += 1; r }); + let id = Self::id_from_index(index); + >::insert(index, FundInfo { + raised: Zero::zero(), + last_contribution: Zero::zero(), + deploy_data: None, + .. + }); + } + + /// 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: T::Balance) { + let who = ensure_signed(origin)?; + + ensure!(value >= T::MIN_CONTRIBUTION, "contribution too small"); + + let mut fund = Self::funds(index).ok_or("invalid fund index")?; + fund.raised += value; + ensure!(fund.raised <= fund.cap, "contributions exceed cap"); + let now = >::block_number(); + ensure!(fund.end > now, "contribution period ended"); + + T::Currency::transfer(&who, &Self::account_id(), value)?; + + let balance = child::get_or_default::(&origin_contract.fund.id, &who); + let balance = balance.saturating_add(&value); + child::put(&origin_contract.fund.id, &owner, balance); + + if fund.last_contribution != now { + fund.last_contribution = now; + NewRaise::mutate(|v| v.push(index)); + } + + >::insert(index, &fund); + } + + /// Withdraw full balance of a contributer to an unsuccessful fund. + fn withdraw(origin, #[compact] index: FundIndex) { + let who = ensure_signed(origin)?; + + let mut fund = Self::funds(index).ok_or("invalid fund index")?; + let now = >::block_number(); + ensure!(now >= fund.end, "contribution period not over"); + + let balance = child::get::(&origin_contract.fund.id, &who) + .ok_or("not a contributor")?; + + // Avoid using transfer to ensure we don't pay any fees. + T::Currency::resolve_into_existing(&who, T::Currency::withdraw( + &Self::account_id(), + balance, + WithdrawReason::Transfer, + ExistenceRequirement::AllowDeath + )?); + + child::kill(&origin_contract.fund.id, &who); + fund.raised = fund.raised.saturating_sub(&balance); + + >::insert(index, &fund); + } + + /// Note that a successful fund has lost its parachain slot, and place it into retirement. + fn begin_retirement(_, #[compact] index: FundIndex) { + // origin unimportant. + + let mut fund = Self::funds(index).ok_or("invalid fund index")?; + + let parachain_id = fund.parachain.take().ok_or("fund has no parachain")?; + + // TODO: check that parachain_id has been retired. + + // This fund just ended. Withdrawal period begins. + let now = >::block_number(); + fund.end = now; + + >::insert(index, &fund); + } + + /// 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 unwithdrawn deposits + /// into the treasury. + fn dissolve(_, #[compact] index: FundIndex) { + // origin unimportant. + + let fund = Self::funds(index).ok_or("invalid fund index")?; + ensure!(fund.parachain.is_none(), "cannot disolve fund with active parachain"); + let now = >::block_number(); + ensure!(now >= fund.end + T::WITHDRAWAL_PERIOD, "withdrawal period not over"); + + // Avoid using transfer to ensure we don't pay any fees. + T::Currency::resolve_into_existing(&fund.owner, T::Currency::withdraw( + &Self::account_id(), + fund.deposit, + WithdrawReason::Transfer, + ExistenceRequirement::AllowDeath + )?); + + T::OrphanedFunds::on_unbalanced(T::Currency::withdraw( + &Self::account_id(), + fund.raised, + WithdrawReason::Transfer, + ExistenceRequirement::AllowDeath + )?); + + child::kill_storage(&origin_contract.fund.id); + >::kill(index); + } + + /// Set the deploy information for a successful bid to deploy a new parachain. + /// + /// - `origin` must be the successful bidder account. + /// - `sub` is the sub-bidder ID of the bidder. + /// - `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. + fn fix_deploy_data(origin, + #[compact] sub: SubId, + #[compact] para_id: ParaIdOf, + code_hash: T::Hash, + initial_head_data: Vec + ) { + let who = ensure_signed(origin)?; + let (starts, details) = >::get(¶_id) + .ok_or("parachain id not in onboarding")?; + if let IncomingParachain::Unset(ref nb) = details { + ensure!(nb.who == who && nb.sub == sub, "parachain not registered by origin"); + } else { + return Err("already registered") + } + let item = (starts, IncomingParachain::Fixed{code_hash, initial_head_data}); + >::insert(¶_id, item); + } + + /// 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); + } + + /// 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(_, + #[compact] index: FundIndex, + #[compact] para_id: ParaIdOf + ) { + let mut fund = Self::funds(index).ok_or("invalid fund index")?; + let (code_hash, initial_head_data) = fund.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::account_id()).into(); + slots::fix_deploy_data(fund_origin, index, para_id, code_hash, initial_head_data)?; + + >::insert(index, &fund); + } + + fn on_finalize(n: T::BlockNumber) { + if >::is_ending() { + for fund in NewRaise::take().into_iter().filter_map(Self::funds) { + if fund.last_contribution == n { + let bidder = slots::Bidder::New(slots::NewBidder { + who: Self::account_id(), + /// FundIndex and slots::SubId happen to be the same type (u32). If this + /// ever changes, then some sort of coversion will be needed here. + sub: fund.index, + }); + >::handle_bid( + bidder, + auction_index: >::auction_counter(), + first_slot: fund.first_slot, + last_slot: fund.last_slot, + amount: fund.raised, + ) + } + } + } + } + } +} + +impl Module { + /// The account ID of the treasury 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 account_id() -> T::AccountId { + MODULE_ID.into_account() + } + + pub fn id_from_index(index: FundIndex) -> T::Hash { + T::Hasher::hash_of((b"crowdfund", index)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use srml_support::{impl_outer_origin, assert_ok}; + use sr_io::with_externalities; + use substrate_primitives::{H256, Blake2Hasher}; + // 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::{ + BuildStorage, traits::{BlakeTwo256, OnInitialize, OnFinalize, IdentityLookup}, + testing::Header + }; + + 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; + impl system::Trait for Test { + type Origin = Origin; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = (); + } + impl balances::Trait for Test { + type Balance = u64; + type OnFreeBalanceZero = (); + type OnNewAccount = (); + type Event = (); + type TransactionPayment = (); + type TransferPayment = (); + type DustRemoval = (); + } + impl Trait for Test { + type Event = (); + } + type Example = 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().0; + // We use default for brevity, but you can configure as desired if needed. + t.extend(balances::GenesisConfig::::default().build_storage().unwrap().0); + t.extend(GenesisConfig::{ + dummy: 42, + // we configure the map with (key, value) pairs. + bar: vec![(1, 2), (2, 3)], + foo: 24, + }.build_storage().unwrap().0); + t.into() + } + + #[test] + fn it_works_for_optional_value() { + with_externalities(&mut new_test_ext(), || { + // Check that GenesisBuilder works properly. + assert_eq!(Example::dummy(), Some(42)); + + // Check that accumulate works when we have Some value in Dummy already. + assert_ok!(Example::accumulate_dummy(Origin::signed(1), 27)); + assert_eq!(Example::dummy(), Some(69)); + + // Check that finalizing the block removes Dummy from storage. + >::on_finalize(1); + assert_eq!(Example::dummy(), None); + + // Check that accumulate works when we Dummy has None in it. + >::on_initialize(2); + assert_ok!(Example::accumulate_dummy(Origin::signed(1), 42)); + assert_eq!(Example::dummy(), Some(42)); + }); + } + + #[test] + fn it_works_for_default_value() { + with_externalities(&mut new_test_ext(), || { + assert_eq!(Example::foo(), 24); + assert_ok!(Example::accumulate_foo(Origin::signed(1), 1)); + assert_eq!(Example::foo(), 25); + }); + } +} From 1433a13167ffbc8362ffa95eecec43c75eec1f5d Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Fri, 5 Jul 2019 15:01:22 +0200 Subject: [PATCH 04/42] Rest of logic. --- runtime/src/crowdfund.rs | 143 ++++++++++++++++++++++++++++++--------- 1 file changed, 112 insertions(+), 31 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 13c08f525dc1..d6a0c564c3ce 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -15,6 +15,50 @@ // along with Substrate. If not, see . //! # Parachain Crowdfunding module +//! +//! The point of this 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). +//! +//! Contributions must be of at least `MIN_CONTRIBUTION` (to account for the resouces taken in +//! tracking contributions), and may never tally greater than the fund's `cap`, set and fixed at the +//! time of creation. In order to create a new fund, then a deposit must be paid of the amount +//! `SUBMISSION_DEPOSIT`. Substantial resources are taken on the main trie in tracking a fund and +//! this accounts for that. +//! +//! Funds may be set up at any time; 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*. Contributors +//! may get a refund of their contributions from retired funds. After a period (`RETIREMENT_PERIOD`) +//! the fund may be dissolved entirely. At this point any unrefunded 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. If the funds do not arrive, then use srml_support::{ StorageValue, dispatch::Result, decl_module, decl_storage, decl_event, storage::child, @@ -45,7 +89,7 @@ pub trait Trait: slots::Trait { /// The period of time (in blocks) between after an unsuccessful crowdfund ending where /// contributors are able to withdraw their funds. After this period, their funds are lost. - const WITHDRAW_PERIOD: Self::BlockNumber; + const RETIREMENT_PERIOD: Self::BlockNumber; /// What to do with funds that were not withdrawn. type OrphanedFunds: OnUnbalanced>; @@ -54,8 +98,6 @@ pub trait Trait: slots::Trait { pub type FundIndex = u32; pub struct FundInfo { - /// The id of the child-trie. - id: Hash, /// 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 disolved. parachain: Option, @@ -71,7 +113,8 @@ pub struct FundInfo { /// 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. - last_contribution: BlockNumber, + /// If this is `None`, then the last contribution was made outside of the ending period. + last_contribution: Option, /// First slot in range to bid on; it's actually a LeasePeriod, but that's the same type as /// BlockNumber. first_slot: BlockNumber, @@ -95,6 +138,9 @@ decl_storage! { /// The funds that have had additional contributions during the last block. This is used /// in order to determine which funds should submit updated bids. NewRaise: Vec; + + /// True if the fund was ending at the last block. + WasEnding: bool; } } @@ -125,15 +171,18 @@ decl_module! { WithdrawReason::Transfer, ExistenceRequirement::AllowDeath )?; + + let index = >::mutate(|c| { let r = *c; *c += 1; r }); + // 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::account_id(), imb); + T::Currency::resolve_creating(Self::fund_account_id(index), imb); - let index = >::mutate(|c| { let r = *c; *c += 1; r }); - let id = Self::id_from_index(index); >::insert(index, FundInfo { raised: Zero::zero(), - last_contribution: Zero::zero(), + // Ensure it's Some, so that the first contribution causes it to be inserted into + // `NewRaise`. + last_contribution: Some(Zero::zero()), deploy_data: None, .. }); @@ -153,16 +202,45 @@ decl_module! { let now = >::block_number(); ensure!(fund.end > now, "contribution period ended"); - T::Currency::transfer(&who, &Self::account_id(), value)?; + T::Currency::transfer(&who, &Self::fund_account_id(index), value)?; - let balance = child::get_or_default::(&origin_contract.fund.id, &who); + let id = Self::id_from_index(index); + let balance = child::get_or_default::(&id, &who); let balance = balance.saturating_add(&value); - child::put(&origin_contract.fund.id, &owner, balance); - - if fund.last_contribution != now { - fund.last_contribution = now; + child::put(&id, &owner, balance); + + let is_ending = >::is_ending(); + let (maybe_last, push) = match (is_ending, fund.last_contribution) { + // Now in end period; last was at earlier time in end period: reset last and insert + (true, Some(c)) if c != now => (Some(Some(now)), true), + // Now in end period; last was either same block or before period: reset last (don't + // insert because it's already in). + (true, _) => (Some(Some(now)), false), + // Now outside end period; last was inside period end period: reset last to None and + // insert (because it will have been removed at end of last block). + (false, Some(_)) => (Some(None), true), + // Now outside end period; last was also outside end period. Don't do anything. + (false, _) => (None, false), + }; + if push { NewRaise::mutate(|v| v.push(index)); } + if let Some(last) = maybe_last { + fund.last_contribution = last; + } + + if >::is_ending() { + if let Some(c) = fund.last_contribution { + if c != now { + } + } + fund.last_contribution = Some(now); + } else { + if fund.last_contribution.is_some() { + fund.last_contribution = None; + NewRaise::mutate(|v| v.push(index)); + } + } >::insert(index, &fund); } @@ -175,18 +253,20 @@ decl_module! { let now = >::block_number(); ensure!(now >= fund.end, "contribution period not over"); - let balance = child::get::(&origin_contract.fund.id, &who) + let id = Self::id_from_index(index); + let balance = child::get::(&id, &who) .ok_or("not a contributor")?; // Avoid using transfer to ensure we don't pay any fees. T::Currency::resolve_into_existing(&who, T::Currency::withdraw( - &Self::account_id(), + &Self::fund_account_id(index), balance, WithdrawReason::Transfer, ExistenceRequirement::AllowDeath )?); - child::kill(&origin_contract.fund.id, &who); + let id = Self::id_from_index(index); + child::kill(&id, &who); fund.raised = fund.raised.saturating_sub(&balance); >::insert(index, &fund); @@ -195,12 +275,10 @@ decl_module! { /// Note that a successful fund has lost its parachain slot, and place it into retirement. fn begin_retirement(_, #[compact] index: FundIndex) { // origin unimportant. - let mut fund = Self::funds(index).ok_or("invalid fund index")?; - let parachain_id = fund.parachain.take().ok_or("fund has no parachain")?; - - // TODO: check that parachain_id has been retired. + let account = Self::fund_account_id(fund.index); + ensure!(T::Currency::free_balance(&account) >= fund.raised, "funds not yet returned"); // This fund just ended. Withdrawal period begins. let now = >::block_number(); @@ -218,24 +296,27 @@ decl_module! { let fund = Self::funds(index).ok_or("invalid fund index")?; ensure!(fund.parachain.is_none(), "cannot disolve fund with active parachain"); let now = >::block_number(); - ensure!(now >= fund.end + T::WITHDRAWAL_PERIOD, "withdrawal period not over"); + ensure!(now >= fund.end + T::RETIREMENT_PERIOD, "retirement period not over"); + + let account = Self::fund_account_id(index); // Avoid using transfer to ensure we don't pay any fees. T::Currency::resolve_into_existing(&fund.owner, T::Currency::withdraw( - &Self::account_id(), + &account, fund.deposit, WithdrawReason::Transfer, ExistenceRequirement::AllowDeath )?); T::OrphanedFunds::on_unbalanced(T::Currency::withdraw( - &Self::account_id(), + &account, fund.raised, WithdrawReason::Transfer, ExistenceRequirement::AllowDeath )?); - child::kill_storage(&origin_contract.fund.id); + let id = Self::id_from_index(index); + child::kill_storage(&id); >::kill(index); } @@ -302,7 +383,7 @@ decl_module! { ensure!(fund.parachain.is_none(), "fund already onboarded"); fund.parachain = Some(para_id); - let fund_origin = system::RawOrigin::Signed(Self::account_id()).into(); + let fund_origin = system::RawOrigin::Signed(Self::fund_account_id(index)).into(); slots::fix_deploy_data(fund_origin, index, para_id, code_hash, initial_head_data)?; >::insert(index, &fund); @@ -313,10 +394,10 @@ decl_module! { for fund in NewRaise::take().into_iter().filter_map(Self::funds) { if fund.last_contribution == n { let bidder = slots::Bidder::New(slots::NewBidder { - who: Self::account_id(), + who: Self::fund_account_id(fund.index), /// FundIndex and slots::SubId happen to be the same type (u32). If this /// ever changes, then some sort of coversion will be needed here. - sub: fund.index, + sub: 0, }); >::handle_bid( bidder, @@ -333,12 +414,12 @@ decl_module! { } impl Module { - /// The account ID of the treasury pot. + /// 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 account_id() -> T::AccountId { - MODULE_ID.into_account() + pub fn fund_account_id(index: FundIndex) -> T::AccountId { + MODULE_ID.into_sub_account(index) } pub fn id_from_index(index: FundIndex) -> T::Hash { From 08f671f91c349ffcb690760e985116bdf1219b2c Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Fri, 5 Jul 2019 15:10:25 +0200 Subject: [PATCH 05/42] Consts to Getters --- runtime/src/crowdfund.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index d6a0c564c3ce..766fcb44189f 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -22,17 +22,18 @@ //! 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). +//! 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 `MIN_CONTRIBUTION` (to account for the resouces taken in +//! Contributions must be of at least `MinContribution` (to account for the resouces taken in //! tracking contributions), and may never tally greater than the fund's `cap`, set and fixed at the -//! time of creation. In order to create a new fund, then a deposit must be paid of the amount -//! `SUBMISSION_DEPOSIT`. Substantial resources are taken on the main trie in tracking a fund and -//! this accounts for that. +//! 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 at any time; 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*. Contributors -//! may get a refund of their contributions from retired funds. After a period (`RETIREMENT_PERIOD`) +//! may get a refund of their contributions from retired funds. After a period (`RetirementPeriod`) //! the fund may be dissolved entirely. At this point any unrefunded contributions are considered //! `orphaned` and are disposed of through the `OrphanedFunds` handler (which may e.g. place them //! into the treasury). @@ -81,15 +82,15 @@ pub trait Trait: slots::Trait { type Event: From> + Into<::Event>; /// The amount to be held on deposit by the owner of a crowdfund. - const SUBMISSION_DEPOSIT: BalanceOf; + type SubmissionDeposit: Get>; /// The minimum amount that may be contributed into a crowdfund. Should almost certainly be at /// least ExistentialDeposit. - const MIN_CONTRIBUTION: BalanceOf; + type MinContribution: Get>; /// The period of time (in blocks) between after an unsuccessful crowdfund ending where /// contributors are able to withdraw their funds. After this period, their funds are lost. - const RETIREMENT_PERIOD: Self::BlockNumber; + type RetirementPeriod: Get; /// What to do with funds that were not withdrawn. type OrphanedFunds: OnUnbalanced>; @@ -163,7 +164,7 @@ decl_module! { #[compact] last_slot: T::BlockNumber, ) { let owner = ensure_signed(origin)?; - let deposit = T::SUBMISSION_DEPOSIT; + let deposit = T::SubmissionDeposit::get(); let imb = T::Currency::withdraw( &owner, @@ -194,7 +195,7 @@ decl_module! { fn contribute(origin, #[compact] index: FundIndex, #[compact] value: T::Balance) { let who = ensure_signed(origin)?; - ensure!(value >= T::MIN_CONTRIBUTION, "contribution too small"); + ensure!(value >= T::MinContribution::get(), "contribution too small"); let mut fund = Self::funds(index).ok_or("invalid fund index")?; fund.raised += value; @@ -296,7 +297,7 @@ decl_module! { let fund = Self::funds(index).ok_or("invalid fund index")?; ensure!(fund.parachain.is_none(), "cannot disolve fund with active parachain"); let now = >::block_number(); - ensure!(now >= fund.end + T::RETIREMENT_PERIOD, "retirement period not over"); + ensure!(now >= fund.end + T::RetirementPeriod::get(), "retirement period not over"); let account = Self::fund_account_id(index); From 7ee6b272964e29501299d8a24eaa12b0472b454a Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Fri, 5 Jul 2019 15:13:58 +0200 Subject: [PATCH 06/42] Cleanups --- runtime/src/crowdfund.rs | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 766fcb44189f..9faa41aae28f 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -210,34 +210,19 @@ decl_module! { let balance = balance.saturating_add(&value); child::put(&id, &owner, balance); - let is_ending = >::is_ending(); - let (maybe_last, push) = match (is_ending, fund.last_contribution) { - // Now in end period; last was at earlier time in end period: reset last and insert - (true, Some(c)) if c != now => (Some(Some(now)), true), - // Now in end period; last was either same block or before period: reset last (don't - // insert because it's already in). - (true, _) => (Some(Some(now)), false), - // Now outside end period; last was inside period end period: reset last to None and - // insert (because it will have been removed at end of last block). - (false, Some(_)) => (Some(None), true), - // Now outside end period; last was also outside end period. Don't do anything. - (false, _) => (None, false), - }; - if push { - NewRaise::mutate(|v| v.push(index)); - } - if let Some(last) = maybe_last { - fund.last_contribution = last; - } - if >::is_ending() { + // Now in end period; record it + fund.last_contribution = Some(now); if let Some(c) = fund.last_contribution { if c != now { + // last contribution was at earlier time; re-insert into `NewRaise` + NewRaise::mutate(|v| v.push(index)); } } - fund.last_contribution = Some(now); } else { if fund.last_contribution.is_some() { + // Now outside end period and last was also inside end period: reset last to + // None and insert (because it will have been removed at end of last block). fund.last_contribution = None; NewRaise::mutate(|v| v.push(index)); } From 70d11ce74f53a850e16a8353bd8a82d39b3a562e Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Tue, 16 Jul 2019 02:35:14 +0200 Subject: [PATCH 07/42] Trying to get things to compile --- runtime/src/crowdfund.rs | 75 +++++++++++++++++++++++++++------------- runtime/src/slots.rs | 2 +- 2 files changed, 52 insertions(+), 25 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 9faa41aae28f..1fd7cc2ae7b8 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -62,13 +62,17 @@ //! funds ultimately end up in module's fund sub-account. If the funds do not arrive, then use srml_support::{ - StorageValue, dispatch::Result, decl_module, decl_storage, decl_event, storage::child, - traits::{LockableCurrency, ReservableCurrency, Currency} + StorageValue, StorageMap, dispatch::Result, decl_module, decl_storage, decl_event, storage::child, ensure, + traits::{LockableCurrency, ReservableCurrency, Currency, Get, OnUnbalanced} }; use system::ensure_signed; -use sr_primitives::weights::TransactionWeight; +use sr_primitives::{ModuleId, weights::TransactionWeight, + traits::{AccountIdConversion, Hash, Saturating} +}; use primitives::parachain::Id as ParaId; use crate::slots; +use parity_codec::{Encode, Decode}; +use rstd::vec::Vec; const MODULE_ID: ModuleId = ModuleId(*b"py/cfund"); @@ -76,9 +80,10 @@ type BalanceOf = <::Currency as Currency<::Ac type NegativeImbalanceOf = <::Currency as Currency<::AccountId>>::NegativeImbalance; pub trait Trait: slots::Trait { - type Currency: LockableCurrency + ReservableCurrency; - - /// The overarching event type. + type Currency: + LockableCurrency + + ReservableCurrency; + type Event: From> + Into<::Event>; /// The amount to be held on deposit by the owner of a crowdfund. @@ -93,11 +98,13 @@ pub trait Trait: slots::Trait { type RetirementPeriod: Get; /// What to do with funds that were not withdrawn. - type OrphanedFunds: OnUnbalanced>; + type OrphanedFunds: OnUnbalanced>; } pub type FundIndex = u32; +#[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 disolved. @@ -130,15 +137,15 @@ pub struct FundInfo { decl_storage! { trait Store for Module as Example { /// Info on all of the funds. - Funds pub(funds): + Funds get(funds): map FundIndex => Option, T::Hash, T::BlockNumber>>; - /// The total number of ffunds that have so far been allocated. - FundCount pub(fund_count): FundIndex; + /// 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 updated bids. - NewRaise: Vec; + NewRaise: Vec; /// True if the fund was ending at the last block. WasEnding: bool; @@ -146,18 +153,20 @@ decl_storage! { } decl_event!( - pub enum Event { - Nothing, + pub enum Event where ::AccountId { + TODO(AccountId), } ); 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. #[weight = TransactionWeight::Basic(100_000, 10)] - fn create(origin, + fn create( + origin, #[compact] end: T::BlockNumber, #[compact] cap: BalanceOf, #[compact] first_slot: T::BlockNumber, @@ -180,19 +189,27 @@ decl_module! { T::Currency::resolve_creating(Self::fund_account_id(index), imb); >::insert(index, FundInfo { + parachain: None, + owner: owner, + deposit: deposit, raised: Zero::zero(), + end: end, + cap: cap, // Ensure it's Some, so that the first contribution causes it to be inserted into // `NewRaise`. last_contribution: Some(Zero::zero()), + first_slot: first_slot, + last_slot: last_slot, deploy_data: None, - .. }); } + */ + /// 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: T::Balance) { + fn contribute(origin, #[compact] index: FundIndex, #[compact] value: BalanceOf) { let who = ensure_signed(origin)?; ensure!(value >= T::MinContribution::get(), "contribution too small"); @@ -203,14 +220,14 @@ decl_module! { let now = >::block_number(); ensure!(fund.end > now, "contribution period ended"); - T::Currency::transfer(&who, &Self::fund_account_id(index), value)?; + ::Currency::transfer(&who, &Self::fund_account_id(index), value)?; let id = Self::id_from_index(index); - let balance = child::get_or_default::(&id, &who); - let balance = balance.saturating_add(&value); - child::put(&id, &owner, balance); + let balance = child::get_or_default::>(id.as_ref(), who); + let balance = balance.saturating_add(value); + child::put(id.as_ref(), who.as_ref(), &balance); - if >::is_ending() { + if >::is_ending(now).is_some() { // Now in end period; record it fund.last_contribution = Some(now); if let Some(c) = fund.last_contribution { @@ -231,6 +248,7 @@ decl_module! { >::insert(index, &fund); } + /* /// Withdraw full balance of a contributer to an unsuccessful fund. fn withdraw(origin, #[compact] index: FundIndex) { let who = ensure_signed(origin)?; @@ -396,6 +414,7 @@ decl_module! { } } } + */ } } @@ -405,14 +424,17 @@ impl Module { /// 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) + // TODO: use `into_sub_account(index)` when `polkadot-master` is updated + MODULE_ID.into_account() } pub fn id_from_index(index: FundIndex) -> T::Hash { - T::Hasher::hash_of((b"crowdfund", index)) + // TODO: This feels really dumb + (b"crowdfun", b"d", index).using_encoded(::Hashing::hash) } } +/* #[cfg(test)] mod tests { use super::*; @@ -458,6 +480,10 @@ mod tests { } impl Trait for Test { type Event = (); + type SubmissionDeposit: 1; + type MinContribution: 10; + type RetirementPeriod: 5; + type OrphanedFunds: (); } type Example = Module; @@ -506,3 +532,4 @@ mod tests { }); } } +*/ diff --git a/runtime/src/slots.rs b/runtime/src/slots.rs index b1b2d5b74a79..e8a6f15a1a41 100644 --- a/runtime/src/slots.rs +++ b/runtime/src/slots.rs @@ -400,7 +400,7 @@ impl Module { /// 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() { From 18feb1c8bc62e3ba56cdd20220678bc1e81a4497 Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Tue, 16 Jul 2019 03:34:01 +0200 Subject: [PATCH 08/42] More patchwork --- runtime/src/crowdfund.rs | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 1fd7cc2ae7b8..ed95a6351adb 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -63,7 +63,7 @@ use srml_support::{ StorageValue, StorageMap, dispatch::Result, decl_module, decl_storage, decl_event, storage::child, ensure, - traits::{LockableCurrency, ReservableCurrency, Currency, Get, OnUnbalanced} + traits::{LockableCurrency, ReservableCurrency, Currency, Get, OnUnbalanced, WithdrawReason, ExistenceRequirement} }; use system::ensure_signed; use sr_primitives::{ModuleId, weights::TransactionWeight, @@ -209,7 +209,8 @@ decl_module! { /// 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) { + fn contribute(origin, #[compact] index: FundIndex, #[compact] value: BalanceOf) + { let who = ensure_signed(origin)?; ensure!(value >= T::MinContribution::get(), "contribution too small"); @@ -223,9 +224,9 @@ decl_module! { ::Currency::transfer(&who, &Self::fund_account_id(index), value)?; let id = Self::id_from_index(index); - let balance = child::get_or_default::>(id.as_ref(), who); + let balance = who.using_encoded(|b| child::get_or_default::>(id.as_ref(), b)); let balance = balance.saturating_add(value); - child::put(id.as_ref(), who.as_ref(), &balance); + who.using_encoded(|b| child::put(id.as_ref(), b, &balance)); if >::is_ending(now).is_some() { // Now in end period; record it @@ -248,8 +249,8 @@ decl_module! { >::insert(index, &fund); } - /* - /// Withdraw full balance of a contributer to an unsuccessful fund. + + /// Withdraw full balance of a contributor to an unsuccessful fund. fn withdraw(origin, #[compact] index: FundIndex) { let who = ensure_signed(origin)?; @@ -258,11 +259,11 @@ decl_module! { ensure!(now >= fund.end, "contribution period not over"); let id = Self::id_from_index(index); - let balance = child::get::(&id, &who) + let balance = who.using_encoded(|b| child::get::>(id.as_ref(), b)) .ok_or("not a contributor")?; // Avoid using transfer to ensure we don't pay any fees. - T::Currency::resolve_into_existing(&who, T::Currency::withdraw( + ::Currency::resolve_into_existing(&who, ::Currency::withdraw( &Self::fund_account_id(index), balance, WithdrawReason::Transfer, @@ -270,19 +271,19 @@ decl_module! { )?); let id = Self::id_from_index(index); - child::kill(&id, &who); - fund.raised = fund.raised.saturating_sub(&balance); + who.using_encoded(|b| child::kill(id.as_ref(), b)); + fund.raised = fund.raised.saturating_sub(balance); >::insert(index, &fund); } - + /// Note that a successful fund has lost its parachain slot, and place it into retirement. - fn begin_retirement(_, #[compact] index: FundIndex) { + fn begin_retirement(origin, #[compact] index: FundIndex) { // origin unimportant. let mut fund = Self::funds(index).ok_or("invalid fund index")?; let parachain_id = fund.parachain.take().ok_or("fund has no parachain")?; - let account = Self::fund_account_id(fund.index); - ensure!(T::Currency::free_balance(&account) >= fund.raised, "funds not yet returned"); + let account = Self::fund_account_id(index); + ensure!(::Currency::free_balance(&account) >= fund.raised, "funds not yet returned"); // This fund just ended. Withdrawal period begins. let now = >::block_number(); @@ -290,7 +291,7 @@ decl_module! { >::insert(index, &fund); } - + /* /// 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 unwithdrawn deposits /// into the treasury. From a1b03e8b9c730bc4031f0814e24a853f90646d3b Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Tue, 16 Jul 2019 03:42:36 +0200 Subject: [PATCH 09/42] Patch dissolve --- runtime/src/crowdfund.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index ed95a6351adb..dbef800b96a7 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -278,7 +278,7 @@ decl_module! { } /// Note that a successful fund has lost its parachain slot, and place it into retirement. - fn begin_retirement(origin, #[compact] index: FundIndex) { + fn begin_retirement(_origin, #[compact] index: FundIndex) { // origin unimportant. let mut fund = Self::funds(index).ok_or("invalid fund index")?; let parachain_id = fund.parachain.take().ok_or("fund has no parachain")?; @@ -291,11 +291,11 @@ decl_module! { >::insert(index, &fund); } - /* + /// 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 unwithdrawn deposits /// into the treasury. - fn dissolve(_, #[compact] index: FundIndex) { + fn dissolve(_origin, #[compact] index: FundIndex) { // origin unimportant. let fund = Self::funds(index).ok_or("invalid fund index")?; @@ -306,14 +306,14 @@ decl_module! { let account = Self::fund_account_id(index); // Avoid using transfer to ensure we don't pay any fees. - T::Currency::resolve_into_existing(&fund.owner, T::Currency::withdraw( + ::Currency::resolve_into_existing(&fund.owner, ::Currency::withdraw( &account, fund.deposit, WithdrawReason::Transfer, ExistenceRequirement::AllowDeath )?); - T::OrphanedFunds::on_unbalanced(T::Currency::withdraw( + T::OrphanedFunds::on_unbalanced(::Currency::withdraw( &account, fund.raised, WithdrawReason::Transfer, @@ -321,10 +321,10 @@ decl_module! { )?); let id = Self::id_from_index(index); - child::kill_storage(&id); - >::kill(index); + child::kill_storage(id.as_ref()); + >::remove(index); } - + /* /// Set the deploy information for a successful bid to deploy a new parachain. /// /// - `origin` must be the successful bidder account. From bc742a34bb4f058e0bf86c1e9d4ba8848216b27a Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Tue, 16 Jul 2019 03:59:22 +0200 Subject: [PATCH 10/42] Patch `fix_deploy_data`, add getters to `NewBidder` --- runtime/src/crowdfund.rs | 10 ++++++---- runtime/src/slots.rs | 12 ++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index dbef800b96a7..723cee9aca0e 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -70,14 +70,16 @@ use sr_primitives::{ModuleId, weights::TransactionWeight, traits::{AccountIdConversion, Hash, Saturating} }; use primitives::parachain::Id as ParaId; -use crate::slots; +use crate::slots::{self, IncomingParachain, SubId, Onboarding}; use parity_codec::{Encode, Decode}; use rstd::vec::Vec; +use crate::parachains::ParachainRegistrar; const MODULE_ID: ModuleId = ModuleId(*b"py/cfund"); type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; type NegativeImbalanceOf = <::Currency as Currency<::AccountId>>::NegativeImbalance; +type ParaIdOf = <::Parachains as ParachainRegistrar<::AccountId>>::ParaId; pub trait Trait: slots::Trait { type Currency: @@ -324,7 +326,7 @@ decl_module! { child::kill_storage(id.as_ref()); >::remove(index); } - /* + /// Set the deploy information for a successful bid to deploy a new parachain. /// /// - `origin` must be the successful bidder account. @@ -342,14 +344,14 @@ decl_module! { let (starts, details) = >::get(¶_id) .ok_or("parachain id not in onboarding")?; if let IncomingParachain::Unset(ref nb) = details { - ensure!(nb.who == who && nb.sub == sub, "parachain not registered by origin"); + ensure!(nb.who() == who && nb.sub() == sub, "parachain not registered by origin"); } else { return Err("already registered") } let item = (starts, IncomingParachain::Fixed{code_hash, initial_head_data}); >::insert(¶_id, item); } - + /* /// Set the deploy data of the funded parachain if not already set. Once set, this cannot /// be changed again. /// diff --git a/runtime/src/slots.rs b/runtime/src/slots.rs index e8a6f15a1a41..d6e0b7db005f 100644 --- a/runtime/src/slots.rs +++ b/runtime/src/slots.rs @@ -68,6 +68,18 @@ pub struct NewBidder { sub: SubId, } +impl NewBidder { + /// Get the bidder's account ID; this is the account that funds the bid. + pub fn who(&self) -> AccountId { + self.who.clone() + } + + /// Get the additional ID allowing the same account ID to have multiple bidders. + pub fn sub(&self) -> SubId { + self.sub + } +} + /// The desired target of a bidder in an auction. #[derive(Clone, Eq, PartialEq, Encode, Decode)] #[cfg_attr(feature = "std", derive(Debug))] From 9704ea54ec6ef93b3a147cc5e4fb05511e144ebb Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Tue, 16 Jul 2019 14:02:30 +0200 Subject: [PATCH 11/42] Dispatchable functions compile... with warnings --- runtime/src/crowdfund.rs | 101 ++++++++++++++------------------------- runtime/src/slots.rs | 16 +------ 2 files changed, 38 insertions(+), 79 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 723cee9aca0e..0543b2228694 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -62,30 +62,25 @@ //! funds ultimately end up in module's fund sub-account. If the funds do not arrive, then use srml_support::{ - StorageValue, StorageMap, dispatch::Result, decl_module, decl_storage, decl_event, storage::child, ensure, - traits::{LockableCurrency, ReservableCurrency, Currency, Get, OnUnbalanced, WithdrawReason, ExistenceRequirement} + 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::TransactionWeight, - traits::{AccountIdConversion, Hash, Saturating} + traits::{AccountIdConversion, Hash, Saturating, Zero} }; -use primitives::parachain::Id as ParaId; -use crate::slots::{self, IncomingParachain, SubId, Onboarding}; +use crate::slots; use parity_codec::{Encode, Decode}; use rstd::vec::Vec; use crate::parachains::ParachainRegistrar; const MODULE_ID: ModuleId = ModuleId(*b"py/cfund"); -type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; -type NegativeImbalanceOf = <::Currency as Currency<::AccountId>>::NegativeImbalance; +type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; +type NegativeImbalanceOf = <::Currency as Currency<::AccountId>>::NegativeImbalance; type ParaIdOf = <::Parachains as ParachainRegistrar<::AccountId>>::ParaId; pub trait Trait: slots::Trait { - type Currency: - LockableCurrency - + ReservableCurrency; - type Event: From> + Into<::Event>; /// The amount to be held on deposit by the owner of a crowdfund. @@ -107,7 +102,7 @@ pub type FundIndex = u32; #[derive(Encode, Decode, Clone, PartialEq, Eq)] #[cfg_attr(feature = "std", derive(Debug))] -pub struct FundInfo { +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 disolved. parachain: Option, @@ -140,7 +135,7 @@ decl_storage! { trait Store for Module as Example { /// Info on all of the funds. Funds get(funds): - map FundIndex => Option, T::Hash, T::BlockNumber>>; + map FundIndex => Option, T::Hash, T::BlockNumber, ParaIdOf>>; /// The total number of funds that have so far been allocated. FundCount get(fund_count): FundIndex; @@ -164,7 +159,6 @@ 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. #[weight = TransactionWeight::Basic(100_000, 10)] fn create( @@ -172,7 +166,7 @@ decl_module! { #[compact] end: T::BlockNumber, #[compact] cap: BalanceOf, #[compact] first_slot: T::BlockNumber, - #[compact] last_slot: T::BlockNumber, + #[compact] last_slot: T::BlockNumber ) { let owner = ensure_signed(origin)?; let deposit = T::SubmissionDeposit::get(); @@ -181,14 +175,14 @@ decl_module! { &owner, deposit, WithdrawReason::Transfer, - ExistenceRequirement::AllowDeath + ExistenceRequirement::AllowDeath, )?; - let index = >::mutate(|c| { let r = *c; *c += 1; r }); + let index = FundCount::mutate(|c| { let r = *c; *c += 1; r }); // 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); + T::Currency::resolve_creating(&Self::fund_account_id(index), imb); >::insert(index, FundInfo { parachain: None, @@ -205,7 +199,7 @@ decl_module! { deploy_data: None, }); } - */ + /// Contribute to a crowd sale. This will transfer some balance over to fund a parachain @@ -223,7 +217,7 @@ decl_module! { let now = >::block_number(); ensure!(fund.end > now, "contribution period ended"); - ::Currency::transfer(&who, &Self::fund_account_id(index), value)?; + T::Currency::transfer(&who, &Self::fund_account_id(index), value)?; let id = Self::id_from_index(index); let balance = who.using_encoded(|b| child::get_or_default::>(id.as_ref(), b)); @@ -265,7 +259,7 @@ decl_module! { .ok_or("not a contributor")?; // Avoid using transfer to ensure we don't pay any fees. - ::Currency::resolve_into_existing(&who, ::Currency::withdraw( + T::Currency::resolve_into_existing(&who, T::Currency::withdraw( &Self::fund_account_id(index), balance, WithdrawReason::Transfer, @@ -285,7 +279,7 @@ decl_module! { let mut fund = Self::funds(index).ok_or("invalid fund index")?; let parachain_id = fund.parachain.take().ok_or("fund has no parachain")?; let account = Self::fund_account_id(index); - ensure!(::Currency::free_balance(&account) >= fund.raised, "funds not yet returned"); + ensure!(T::Currency::free_balance(&account) >= fund.raised, "funds not yet returned"); // This fund just ended. Withdrawal period begins. let now = >::block_number(); @@ -308,14 +302,14 @@ decl_module! { let account = Self::fund_account_id(index); // Avoid using transfer to ensure we don't pay any fees. - ::Currency::resolve_into_existing(&fund.owner, ::Currency::withdraw( + T::Currency::resolve_into_existing(&fund.owner, T::Currency::withdraw( &account, fund.deposit, WithdrawReason::Transfer, ExistenceRequirement::AllowDeath )?); - T::OrphanedFunds::on_unbalanced(::Currency::withdraw( + T::OrphanedFunds::on_unbalanced(T::Currency::withdraw( &account, fund.raised, WithdrawReason::Transfer, @@ -327,31 +321,7 @@ decl_module! { >::remove(index); } - /// Set the deploy information for a successful bid to deploy a new parachain. - /// - /// - `origin` must be the successful bidder account. - /// - `sub` is the sub-bidder ID of the bidder. - /// - `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. - fn fix_deploy_data(origin, - #[compact] sub: SubId, - #[compact] para_id: ParaIdOf, - code_hash: T::Hash, - initial_head_data: Vec - ) { - let who = ensure_signed(origin)?; - let (starts, details) = >::get(¶_id) - .ok_or("parachain id not in onboarding")?; - if let IncomingParachain::Unset(ref nb) = details { - ensure!(nb.who() == who && nb.sub() == sub, "parachain not registered by origin"); - } else { - return Err("already registered") - } - let item = (starts, IncomingParachain::Fixed{code_hash, initial_head_data}); - >::insert(¶_id, item); - } - /* + /// Set the deploy data of the funded parachain if not already set. Once set, this cannot /// be changed again. /// @@ -374,50 +344,51 @@ decl_module! { >::insert(index, &fund); } - + /// 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(_, + fn onboard( + origin, #[compact] index: FundIndex, #[compact] para_id: ParaIdOf ) { let mut fund = Self::funds(index).ok_or("invalid fund index")?; - let (code_hash, initial_head_data) = fund.deploy_data.ok_or("deploy data not fixed")?; + 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(); - slots::fix_deploy_data(fund_origin, index, para_id, code_hash, initial_head_data)?; + >::fix_deploy_data(fund_origin, index, para_id, code_hash, initial_head_data)?; >::insert(index, &fund); } - + fn on_finalize(n: T::BlockNumber) { - if >::is_ending() { - for fund in NewRaise::take().into_iter().filter_map(Self::funds) { - if fund.last_contribution == n { + if >::is_ending(n).is_some() { + for (fund, index) in NewRaise::take().into_iter().filter_map(|i| Self::funds(i).map(|f| (f, i))) + { + if fund.last_contribution == Some(n) { let bidder = slots::Bidder::New(slots::NewBidder { - who: Self::fund_account_id(fund.index), + 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 coversion will be needed here. + /// ever changes, then some sort of conversion will be needed here. sub: 0, }); >::handle_bid( bidder, - auction_index: >::auction_counter(), - first_slot: fund.first_slot, - last_slot: fund.last_slot, - amount: fund.raised, - ) + >::auction_counter(), + fund.first_slot, + fund.last_slot, + fund.raised, + ); } } } } - */ } } diff --git a/runtime/src/slots.rs b/runtime/src/slots.rs index d6e0b7db005f..6fc74b896940 100644 --- a/runtime/src/slots.rs +++ b/runtime/src/slots.rs @@ -62,22 +62,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, -} - -impl NewBidder { - /// Get the bidder's account ID; this is the account that funds the bid. - pub fn who(&self) -> AccountId { - self.who.clone() - } - - /// Get the additional ID allowing the same account ID to have multiple bidders. - pub fn sub(&self) -> SubId { - self.sub - } + pub sub: SubId, } /// The desired target of a bidder in an auction. From cb4ae9eaffbaf7c66ba1ec4da3b4a8b3d29acb28 Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Tue, 16 Jul 2019 14:47:31 +0200 Subject: [PATCH 12/42] Fix some warnings and typos --- runtime/src/crowdfund.rs | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 0543b2228694..eeed08bc5b1a 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -25,7 +25,7 @@ //! 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 resouces taken in +//! 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 @@ -34,7 +34,7 @@ //! Funds may be set up at any time; 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*. 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 unrefunded contributions are considered +//! 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). //! @@ -104,7 +104,7 @@ pub type FundIndex = u32; #[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 disolved. + /// the funds may not be withdrawn and the fund cannot be dissolved. parachain: Option, /// The owning account who placed the deposit. owner: AccountId, @@ -259,7 +259,7 @@ decl_module! { .ok_or("not a contributor")?; // Avoid using transfer to ensure we don't pay any fees. - T::Currency::resolve_into_existing(&who, T::Currency::withdraw( + let _ = T::Currency::resolve_into_existing(&who, T::Currency::withdraw( &Self::fund_account_id(index), balance, WithdrawReason::Transfer, @@ -277,7 +277,7 @@ decl_module! { fn begin_retirement(_origin, #[compact] index: FundIndex) { // origin unimportant. let mut fund = Self::funds(index).ok_or("invalid fund index")?; - let parachain_id = fund.parachain.take().ok_or("fund has no parachain")?; + let _parachain_id = fund.parachain.take().ok_or("fund has no parachain")?; let account = Self::fund_account_id(index); ensure!(T::Currency::free_balance(&account) >= fund.raised, "funds not yet returned"); @@ -289,20 +289,20 @@ decl_module! { } /// 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 unwithdrawn deposits - /// into the treasury. + /// 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) { // origin unimportant. let fund = Self::funds(index).ok_or("invalid fund index")?; - ensure!(fund.parachain.is_none(), "cannot disolve fund with active parachain"); + 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. - T::Currency::resolve_into_existing(&fund.owner, T::Currency::withdraw( + let _ = T::Currency::resolve_into_existing(&fund.owner, T::Currency::withdraw( &account, fund.deposit, WithdrawReason::Transfer, @@ -329,7 +329,8 @@ decl_module! { /// - `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, + fn fix_deploy_data( + origin, #[compact] index: FundIndex, code_hash: T::Hash, initial_head_data: Vec @@ -352,7 +353,7 @@ decl_module! { /// - `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, + _origin, #[compact] index: FundIndex, #[compact] para_id: ParaIdOf ) { @@ -378,7 +379,8 @@ decl_module! { /// ever changes, then some sort of conversion will be needed here. sub: 0, }); - >::handle_bid( + // TODO for review: Can this fail? Anything we can do to handle possible errors? + let _ = >::handle_bid( bidder, >::auction_counter(), fund.first_slot, @@ -403,8 +405,8 @@ impl Module { } pub fn id_from_index(index: FundIndex) -> T::Hash { - // TODO: This feels really dumb - (b"crowdfun", b"d", index).using_encoded(::Hashing::hash) + // TODO: Fix with https://github.com/paritytech/parity-scale-codec/issues/128 + (&b"crowdfund"[..], index).using_encoded(::Hashing::hash) } } From d16bc321b223a62f7c0483581d3b78de702f8ed7 Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Tue, 16 Jul 2019 14:57:23 +0200 Subject: [PATCH 13/42] Whitespace to Tabs --- runtime/src/slots.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/slots.rs b/runtime/src/slots.rs index d2ebf112904e..adefce60dc8f 100644 --- a/runtime/src/slots.rs +++ b/runtime/src/slots.rs @@ -340,7 +340,7 @@ decl_module! { /// - `code_hash` is the hash of the parachain's Wasm validation function. /// - `initial_head_data` is the parachain's initial head data. pub fn fix_deploy_data( - origin, + origin, #[compact] sub: SubId, #[compact] para_id: ParaIdOf, code_hash: T::Hash, From 0a744a48d794f0db077061fa5b82b5f347c3f3ef Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Fri, 19 Jul 2019 18:59:16 +0200 Subject: [PATCH 14/42] Update to use `into_sub_account` --- runtime/src/crowdfund.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index eeed08bc5b1a..0e1ca39efd58 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -400,8 +400,7 @@ impl Module { /// 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 { - // TODO: use `into_sub_account(index)` when `polkadot-master` is updated - MODULE_ID.into_account() + MODULE_ID.into_sub_account(index) } pub fn id_from_index(index: FundIndex) -> T::Hash { From c1d60c2d08cecbeac6b11fe0701876a95de1631d Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Sat, 20 Jul 2019 17:20:09 +0200 Subject: [PATCH 15/42] Add events --- runtime/src/crowdfund.rs | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 0e1ca39efd58..b9394783f2ac 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -150,8 +150,19 @@ decl_storage! { } decl_event!( - pub enum Event where ::AccountId { - TODO(AccountId), + 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), } ); @@ -198,6 +209,9 @@ decl_module! { last_slot: last_slot, deploy_data: None, }); + + Self::deposit_event(RawEvent::Created(index)); + } @@ -243,6 +257,8 @@ decl_module! { } >::insert(index, &fund); + + Self::deposit_event(RawEvent::Contributed(who, index, value)); } @@ -271,6 +287,8 @@ decl_module! { fund.raised = fund.raised.saturating_sub(balance); >::insert(index, &fund); + + Self::deposit_event(RawEvent::Withdrew(who, index, balance)); } /// Note that a successful fund has lost its parachain slot, and place it into retirement. @@ -286,6 +304,9 @@ decl_module! { fund.end = now; >::insert(index, &fund); + + Self::deposit_event(RawEvent::Retiring(index)); + } /// Remove a fund after either: it was unsuccessful and it timed out; or it was successful @@ -319,6 +340,9 @@ decl_module! { let id = Self::id_from_index(index); child::kill_storage(id.as_ref()); >::remove(index); + + Self::deposit_event(RawEvent::Dissolved(index)); + } @@ -344,6 +368,9 @@ decl_module! { 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 @@ -366,6 +393,8 @@ decl_module! { >::fix_deploy_data(fund_origin, index, para_id, code_hash, initial_head_data)?; >::insert(index, &fund); + + Self::deposit_event(RawEvent::Onboarded(index, para_id)); } fn on_finalize(n: T::BlockNumber) { From bc76652dc6bc42162db4f1fbf72edf8d5c14fb0c Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Sun, 21 Jul 2019 15:04:34 +0200 Subject: [PATCH 16/42] Basic fixes to runtime logic and checking --- runtime/src/crowdfund.rs | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index b9394783f2ac..4590bfee8652 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -67,7 +67,7 @@ use srml_support::{ }; use system::ensure_signed; use sr_primitives::{ModuleId, weights::TransactionWeight, - traits::{AccountIdConversion, Hash, Saturating, Zero} + traits::{AccountIdConversion, Hash, Saturating, Zero, CheckedAdd} }; use crate::slots; use parity_codec::{Encode, Decode}; @@ -180,6 +180,9 @@ decl_module! { #[compact] last_slot: T::BlockNumber ) { let owner = ensure_signed(origin)?; + let now = >::block_number(); + ensure!(end > now, "crowdfunding period must end in the future"); + ensure!(last_slot > first_slot, "last slot must be greater than first slot"); let deposit = T::SubmissionDeposit::get(); let imb = T::Currency::withdraw( @@ -189,7 +192,9 @@ decl_module! { ExistenceRequirement::AllowDeath, )?; - let index = FundCount::mutate(|c| { let r = *c; *c += 1; r }); + 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`. @@ -211,7 +216,6 @@ decl_module! { }); Self::deposit_event(RawEvent::Created(index)); - } @@ -219,14 +223,13 @@ decl_module! { /// 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) - { + 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 += value; + fund.raised = fund.raised.checked_add(&value).ok_or("overflow when adding new funds")?; ensure!(fund.raised <= fund.cap, "contributions exceed cap"); let now = >::block_number(); ensure!(fund.end > now, "contribution period ended"); @@ -282,7 +285,6 @@ decl_module! { ExistenceRequirement::AllowDeath )?); - let id = Self::id_from_index(index); who.using_encoded(|b| child::kill(id.as_ref(), b)); fund.raised = fund.raised.saturating_sub(balance); @@ -292,8 +294,9 @@ decl_module! { } /// Note that a successful fund has lost its parachain slot, and place it into retirement. - fn begin_retirement(_origin, #[compact] index: FundIndex) { - // origin unimportant. + 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")?; let account = Self::fund_account_id(index); @@ -306,14 +309,13 @@ decl_module! { >::insert(index, &fund); Self::deposit_event(RawEvent::Retiring(index)); - } /// 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) { - // origin unimportant. + 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"); @@ -380,10 +382,13 @@ decl_module! { /// - `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, + origin, #[compact] index: FundIndex, #[compact] para_id: ParaIdOf ) { + // Origin can be anything except none + 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"); From 606d6f9866fece8cfb890f0bdaa64529c73de438 Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Sun, 21 Jul 2019 19:51:59 +0200 Subject: [PATCH 17/42] Check that auction in progress when creating --- runtime/src/crowdfund.rs | 18 +++++++++++------- runtime/src/slots.rs | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 4590bfee8652..d0ae7f6b8885 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -16,8 +16,8 @@ //! # Parachain Crowdfunding module //! -//! The point of this 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. +//! 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 @@ -31,9 +31,9 @@ //! 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 at any time; 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*. Contributors -//! may get a refund of their contributions from retired funds. After a period (`RetirementPeriod`) +//! 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*. +//! 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). @@ -170,7 +170,7 @@ 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. + /// Create a new crowdfunding campaign for a parachain slot deposit for the current auction. #[weight = TransactionWeight::Basic(100_000, 10)] fn create( origin, @@ -180,6 +180,8 @@ decl_module! { #[compact] last_slot: T::BlockNumber ) { let owner = ensure_signed(origin)?; + + ensure!(>::is_in_progress(), "no auction in progress"); let now = >::block_number(); ensure!(end > now, "crowdfunding period must end in the future"); ensure!(last_slot > first_slot, "last slot must be greater than first slot"); @@ -413,7 +415,9 @@ decl_module! { /// ever changes, then some sort of conversion will be needed here. sub: 0, }); - // TODO for review: Can this fail? Anything we can do to handle possible errors? + + // Care needs to be taken by the crowdfund creator that this function will succeed given + // the crowdfunding configuration. We do basic checks ahead of time in crowdfund `create` let _ = >::handle_bid( bidder, >::auction_counter(), diff --git a/runtime/src/slots.rs b/runtime/src/slots.rs index 6e15a7361452..5a184c9ec6a2 100644 --- a/runtime/src/slots.rs +++ b/runtime/src/slots.rs @@ -399,7 +399,7 @@ impl Module { } /// True if an auction is in progress. - fn is_in_progress() -> bool { + pub fn is_in_progress() -> bool { >::exists() } From 612b834aa4cec8409cf8521a5669b6df906969d2 Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Sun, 21 Jul 2019 20:52:21 +0200 Subject: [PATCH 18/42] Automatically assign end for crowdfund --- runtime/src/crowdfund.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index d0ae7f6b8885..339a3d34434e 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -174,17 +174,18 @@ decl_module! { #[weight = TransactionWeight::Basic(100_000, 10)] fn create( origin, - #[compact] end: T::BlockNumber, #[compact] cap: BalanceOf, #[compact] first_slot: T::BlockNumber, #[compact] last_slot: T::BlockNumber ) { let owner = ensure_signed(origin)?; - ensure!(>::is_in_progress(), "no auction in progress"); - let now = >::block_number(); - ensure!(end > now, "crowdfunding period must end in the future"); ensure!(last_slot > first_slot, "last slot must be greater than first slot"); + // Check an auction is in progress, and extract the `early_end` block + let (_, early_end) = >::auction_info().ok_or("no auction in progress")?; + + // End of the crowdfund will be the last possible block for the ongoing auction + let end = early_end + T::EndingPeriod::get(); let deposit = T::SubmissionDeposit::get(); let imb = T::Currency::withdraw( @@ -229,10 +230,10 @@ decl_module! { 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"); + ensure!(>::is_in_progress(), "no auction in progress"); let now = >::block_number(); ensure!(fund.end > now, "contribution period ended"); @@ -346,7 +347,6 @@ decl_module! { >::remove(index); Self::deposit_event(RawEvent::Dissolved(index)); - } @@ -374,7 +374,6 @@ decl_module! { >::insert(index, &fund); Self::deposit_event(RawEvent::DeployDataFixed(index)); - } /// Complete onboarding process for a winning parachain fund. This can be called once by From 6122c675518b353700858a3b5a09f8260c89cc6c Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Mon, 22 Jul 2019 16:27:52 +0200 Subject: [PATCH 19/42] Update runtime/src/crowdfund.rs Co-Authored-By: Amar Singh --- runtime/src/crowdfund.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 339a3d34434e..85459a8f8f92 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -113,7 +113,7 @@ pub struct FundInfo { /// The total amount raised. raised: Balance, /// Block number after which the funding must have succeeded. If not successful at this number - /// the everyone may withdraw their funds. + /// then everyone may withdraw their funds. end: BlockNumber, /// A hard-cap on the amount that may be contributed. cap: Balance, From 899c6cfe78646764454945127a21e853fba694b6 Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Mon, 22 Jul 2019 16:28:01 +0200 Subject: [PATCH 20/42] Update runtime/src/crowdfund.rs Co-Authored-By: Amar Singh --- runtime/src/crowdfund.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 85459a8f8f92..32e8117964ac 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -90,7 +90,7 @@ pub trait Trait: slots::Trait { /// least ExistentialDeposit. type MinContribution: Get>; - /// The period of time (in blocks) between after an unsuccessful crowdfund ending where + /// 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; From c7d586e8060508b2303769ab6ca93e15025a1338 Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Mon, 22 Jul 2019 16:28:12 +0200 Subject: [PATCH 21/42] Update runtime/src/crowdfund.rs Co-Authored-By: Amar Singh --- runtime/src/crowdfund.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 32e8117964ac..54aa8773676b 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -59,7 +59,7 @@ //! @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. If the funds do not arrive, then +//! funds ultimately end up in module's fund sub-account. use srml_support::{ StorageValue, StorageMap, decl_module, decl_storage, decl_event, storage::child, ensure, From 1dc4b05a90e893d3aae145e71a1233b1104a13d2 Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Mon, 22 Jul 2019 17:04:06 +0200 Subject: [PATCH 22/42] Update crowdfund.rs --- runtime/src/crowdfund.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 339a3d34434e..f348d33097ac 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -59,7 +59,7 @@ //! @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. If the funds do not arrive, then +//! funds ultimately end up in module's fund sub-account. use srml_support::{ StorageValue, StorageMap, decl_module, decl_storage, decl_event, storage::child, ensure, @@ -90,7 +90,7 @@ pub trait Trait: slots::Trait { /// least ExistentialDeposit. type MinContribution: Get>; - /// The period of time (in blocks) between after an unsuccessful crowdfund ending where + /// 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; @@ -113,7 +113,7 @@ pub struct FundInfo { /// The total amount raised. raised: Balance, /// Block number after which the funding must have succeeded. If not successful at this number - /// the everyone may withdraw their funds. + /// then everyone may withdraw their funds. end: BlockNumber, /// A hard-cap on the amount that may be contributed. cap: Balance, @@ -180,14 +180,15 @@ decl_module! { ) { let owner = ensure_signed(origin)?; - ensure!(last_slot > first_slot, "last slot must be greater than first slot"); + ensure!(first_slot < last_slot, "last slot must be greater than first slot"); + ensure!(last_slot <= first_slot + 3, "last slot cannot be more then 3 more than first slot"); // Check an auction is in progress, and extract the `early_end` block let (_, early_end) = >::auction_info().ok_or("no auction in progress")?; // End of the crowdfund will be the last possible block for the ongoing auction let end = early_end + T::EndingPeriod::get(); - let deposit = T::SubmissionDeposit::get(); + let deposit = T::SubmissionDeposit::get(); let imb = T::Currency::withdraw( &owner, deposit, @@ -233,9 +234,11 @@ decl_module! { 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"); - ensure!(>::is_in_progress(), "no auction in progress"); + + // Make sure crowdfund has not ended and auction has not "ended early" (it is still in progress). let now = >::block_number(); ensure!(fund.end > now, "contribution period ended"); + ensure!(>::is_in_progress(), "no auction in progress"); T::Currency::transfer(&who, &Self::fund_account_id(index), value)?; @@ -246,11 +249,11 @@ decl_module! { if >::is_ending(now).is_some() { // Now in end period; record it - fund.last_contribution = Some(now); if let Some(c) = fund.last_contribution { if c != now { // last contribution was at earlier time; re-insert into `NewRaise` NewRaise::mutate(|v| v.push(index)); + fund.last_contribution = Some(now); } } } else { @@ -387,7 +390,6 @@ decl_module! { #[compact] index: FundIndex, #[compact] para_id: ParaIdOf ) { - // Origin can be anything except none let _ = ensure_signed(origin)?; let mut fund = Self::funds(index).ok_or("invalid fund index")?; @@ -446,7 +448,6 @@ impl Module { } } -/* #[cfg(test)] mod tests { use super::*; @@ -544,4 +545,3 @@ mod tests { }); } } -*/ From d85dadb1cb863e8ff952e6659da8f9fd6c906920 Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Mon, 22 Jul 2019 17:17:50 +0200 Subject: [PATCH 23/42] Patch `NewRaise` logic --- runtime/src/crowdfund.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index f348d33097ac..ec6ee7d49852 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -141,7 +141,7 @@ decl_storage! { 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 updated bids. + /// in order to determine which funds should submit new or updated bids. NewRaise: Vec; /// True if the fund was ending at the last block. @@ -181,7 +181,7 @@ decl_module! { let owner = ensure_signed(origin)?; ensure!(first_slot < last_slot, "last slot must be greater than first slot"); - ensure!(last_slot <= first_slot + 3, "last slot cannot be more then 3 more than first slot"); + ensure!(last_slot <= first_slot + 3.into(), "last slot cannot be more then 3 more than first slot"); // Check an auction is in progress, and extract the `early_end` block let (_, early_end) = >::auction_info().ok_or("no auction in progress")?; @@ -211,9 +211,7 @@ decl_module! { raised: Zero::zero(), end: end, cap: cap, - // Ensure it's Some, so that the first contribution causes it to be inserted into - // `NewRaise`. - last_contribution: Some(Zero::zero()), + last_contribution: None, first_slot: first_slot, last_slot: last_slot, deploy_data: None, @@ -257,11 +255,10 @@ decl_module! { } } } else { - if fund.last_contribution.is_some() { - // Now outside end period and last was also inside end period: reset last to - // None and insert (because it will have been removed at end of last block). - fund.last_contribution = None; + // First contribution to the fund should add it to `NewRaise` + if fund.last_contribution.is_none() { NewRaise::mutate(|v| v.push(index)); + fund.last_contribution = Some(now); } } @@ -448,6 +445,7 @@ impl Module { } } +/* #[cfg(test)] mod tests { use super::*; @@ -545,3 +543,4 @@ mod tests { }); } } +*/ From 7585660f80379904ec9ed1e04765aff2b5142521 Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Mon, 22 Jul 2019 18:45:05 +0200 Subject: [PATCH 24/42] Test compiles --- runtime/src/crowdfund.rs | 142 +++++++++++++++++++++++++++------------ 1 file changed, 99 insertions(+), 43 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index ec6ee7d49852..fd4d2c651310 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -445,14 +445,15 @@ impl Module { } } -/* #[cfg(test)] mod tests { use super::*; - use srml_support::{impl_outer_origin, assert_ok}; + use std::{collections::HashMap, cell::RefCell}; + use srml_support::{impl_outer_origin, assert_ok, 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::{ @@ -469,6 +470,9 @@ mod tests { // configuration traits of modules we want to use. #[derive(Clone, Eq, PartialEq)] pub struct Test; + parameter_types! { + pub const BlockHashCount: u64 = 250; + } impl system::Trait for Test { type Origin = Origin; type Index = u64; @@ -479,6 +483,14 @@ mod tests { type Lookup = IdentityLookup; type Header = Header; type Event = (); + type BlockHashCount = BlockHashCount; + } + parameter_types! { + pub const ExistentialDeposit: u64 = 0; + pub const TransferFee: u64 = 0; + pub const CreationFee: u64 = 0; + pub const TransactionBaseFee: u64 = 0; + pub const TransactionByteFee: u64 = 0; } impl balances::Trait for Test { type Balance = u64; @@ -486,61 +498,105 @@ mod tests { type OnNewAccount = (); type Event = (); type TransactionPayment = (); - type TransferPayment = (); type DustRemoval = (); + type TransferPayment = (); + type ExistentialDeposit = ExistentialDeposit; + type TransferFee = TransferFee; + type CreationFee = CreationFee; + type TransactionBaseFee = TransactionBaseFee; + type TransactionByteFee = TransactionByteFee; + } + + 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(()) + }) + } + } + + fn reset_count() { + PARACHAIN_COUNT.with(|p| *p.borrow_mut() = 0); + } + + fn with_parachains(f: impl FnOnce(&HashMap, Vec)>) -> T) -> T { + PARACHAINS.with(|p| f(&*p.borrow())) + } + + 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: 1; - type MinContribution: 10; - type RetirementPeriod: 5; - type OrphanedFunds: (); + type SubmissionDeposit = SubmissionDeposit; + type MinContribution = MinContribution; + type RetirementPeriod = RetirementPeriod; + type OrphanedFunds = (); } - type Example = Module; + + type System = system::Module; + type Balances = balances::Module; + type Slots = slots::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().0; - // We use default for brevity, but you can configure as desired if needed. - t.extend(balances::GenesisConfig::::default().build_storage().unwrap().0); - t.extend(GenesisConfig::{ - dummy: 42, - // we configure the map with (key, value) pairs. - bar: vec![(1, 2), (2, 3)], - foo: 24, + let mut t = system::GenesisConfig::default().build_storage::().unwrap().0; + t.extend(balances::GenesisConfig::{ + balances: vec![(1, 10), (2, 20), (3, 30), (4, 40), (5, 50), (6, 60)], + vesting: vec![], }.build_storage().unwrap().0); t.into() } #[test] - fn it_works_for_optional_value() { - with_externalities(&mut new_test_ext(), || { - // Check that GenesisBuilder works properly. - assert_eq!(Example::dummy(), Some(42)); - - // Check that accumulate works when we have Some value in Dummy already. - assert_ok!(Example::accumulate_dummy(Origin::signed(1), 27)); - assert_eq!(Example::dummy(), Some(69)); - - // Check that finalizing the block removes Dummy from storage. - >::on_finalize(1); - assert_eq!(Example::dummy(), None); - - // Check that accumulate works when we Dummy has None in it. - >::on_initialize(2); - assert_ok!(Example::accumulate_dummy(Origin::signed(1), 42)); - assert_eq!(Example::dummy(), Some(42)); - }); - } - - #[test] - fn it_works_for_default_value() { + fn it_works() { with_externalities(&mut new_test_ext(), || { - assert_eq!(Example::foo(), 24); - assert_ok!(Example::accumulate_foo(Origin::signed(1), 1)); - assert_eq!(Example::foo(), 25); - }); + }) } } -*/ From 3fdc25b22e93dfd5bd6fa13a768333fde731025e Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Mon, 22 Jul 2019 18:54:42 +0200 Subject: [PATCH 25/42] Make `NewRaised` logic even better --- runtime/src/crowdfund.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index fd4d2c651310..54b7f30656e5 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -245,23 +245,23 @@ decl_module! { let balance = balance.saturating_add(value); who.using_encoded(|b| child::put(id.as_ref(), b, &balance)); - if >::is_ending(now).is_some() { - // Now in end period; record it - if let Some(c) = fund.last_contribution { - if c != now { - // last contribution was at earlier time; re-insert into `NewRaise` - NewRaise::mutate(|v| v.push(index)); - fund.last_contribution = Some(now); - } - } + // First contribution to a fund should add it to `NewRaise` so initial bid is made + if fund.last_contribution.is_none() { + NewRaise::mutate(|v| v.push(index)); } else { - // First contribution to the fund should add it to `NewRaise` - if fund.last_contribution.is_none() { - NewRaise::mutate(|v| v.push(index)); - fund.last_contribution = Some(now); + // Any contributions that happen during the ending period should + // cause another bid to be placed with updated value + if >::is_ending(now).is_some() { + // Only add to `NewRaised` if it hasn't already been added this block + if let Some(c) = fund.last_contribution { + if c != now { + NewRaise::mutate(|v| v.push(index)); + } + } } } - + + fund.last_contribution = Some(now); >::insert(index, &fund); Self::deposit_event(RawEvent::Contributed(who, index, value)); From cb8b6b6c3cdf11af706fe139350c684992c6b9f7 Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Tue, 23 Jul 2019 11:12:47 +0200 Subject: [PATCH 26/42] Fix trie id generation, start to add some tests --- runtime/src/crowdfund.rs | 123 ++++++++++++++++++++++++++++++++++----- runtime/src/slots.rs | 2 +- 2 files changed, 110 insertions(+), 15 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 54b7f30656e5..a53c468b68e9 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -73,6 +73,8 @@ use crate::slots; use parity_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"); @@ -142,10 +144,7 @@ decl_storage! { /// 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: Vec; - - /// True if the fund was ending at the last block. - WasEnding: bool; + NewRaise get(new_raise): Vec; } } @@ -241,9 +240,13 @@ decl_module! { T::Currency::transfer(&who, &Self::fund_account_id(index), value)?; let id = Self::id_from_index(index); + sr_io::print("before get"); let balance = who.using_encoded(|b| child::get_or_default::>(id.as_ref(), b)); + sr_io::print("after get"); + let balance = balance.saturating_add(value); who.using_encoded(|b| child::put(id.as_ref(), b, &balance)); + sr_io::print("after put"); // First contribution to a fund should add it to `NewRaise` so initial bid is made if fund.last_contribution.is_none() { @@ -415,7 +418,7 @@ decl_module! { }); // Care needs to be taken by the crowdfund creator that this function will succeed given - // the crowdfunding configuration. We do basic checks ahead of time in crowdfund `create` + // the crowdfunding configuration. We do some checks ahead of time in crowdfund `create`. let _ = >::handle_bid( bidder, >::auction_counter(), @@ -439,9 +442,16 @@ impl Module { MODULE_ID.into_sub_account(index) } - pub fn id_from_index(index: FundIndex) -> T::Hash { - // TODO: Fix with https://github.com/paritytech/parity-scale-codec/issues/128 - (&b"crowdfund"[..], index).using_encoded(::Hashing::hash) + 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.iter() + .chain(b"default:") + .chain(T::Hashing::hash(&buf[..]).as_ref().iter()) + .cloned() + .collect() } } @@ -450,7 +460,7 @@ mod tests { use super::*; use std::{collections::HashMap, cell::RefCell}; - use srml_support::{impl_outer_origin, assert_ok, parameter_types}; + 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; @@ -487,8 +497,10 @@ mod tests { } parameter_types! { pub const ExistentialDeposit: u64 = 0; - pub const TransferFee: u64 = 0; - pub const CreationFee: 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; } @@ -588,15 +600,98 @@ mod tests { fn new_test_ext() -> sr_io::TestExternalities { let mut t = system::GenesisConfig::default().build_storage::().unwrap().0; t.extend(balances::GenesisConfig::{ - balances: vec![(1, 10), (2, 20), (3, 30), (4, 40), (5, 50), (6, 60)], + balances: vec![(1, 1000), (2, 2000), (3, 3000), (4, 4000)], vesting: vec![], }.build_storage().unwrap().0); t.into() } #[test] - fn it_works() { + 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); + }); + } + + #[test] + fn create_crowdfund_works() { + with_externalities(&mut new_test_ext(), || { + // Set up an auction + assert_ok!(Slots::new_auction(Origin::ROOT, 5, 1)); + // Now try to create a crowdfund campaign + assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4)); + 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: None, + 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); + // No new raise until first contribution + let empty: Vec = Vec::new(); + assert_eq!(Crowdfund::new_raise(), empty); + }); + } + + #[test] + fn create_crowdfund_handles_basic_errors() { with_externalities(&mut new_test_ext(), || { - }) + // Cannot create crowdfund without ongoing auction + assert_noop!(Crowdfund::create(Origin::signed(1), 1000, 1, 4), "no auction in progress"); + + // Set up an auction + assert_ok!(Slots::new_auction(Origin::ROOT, 5, 1)); + // Cannot create a crowdfund with bad slots + assert_noop!(Crowdfund::create(Origin::signed(1), 1000, 4, 1), "last slot must be greater than first slot"); + assert_noop!(Crowdfund::create(Origin::signed(1), 1000, 1, 5), "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), "too few free funds in account"); + }); + } + + #[test] + fn contribute_crowdfund_works() { + with_externalities(&mut new_test_ext(), || { + // Set up an crowdfund + assert_ok!(Slots::new_auction(Origin::ROOT, 5, 1)); + assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4)); + assert_eq!(Balances::free_balance(1), 999); + + // 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); + + + + }); + } + + #[test] + fn contribute_crowdfund_handles_basic_errors() { + with_externalities(&mut new_test_ext(), || { + // Set up an crowdfund + assert_ok!(Slots::new_auction(Origin::ROOT, 5, 1)); + assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4)); + + + + }); } } diff --git a/runtime/src/slots.rs b/runtime/src/slots.rs index 5a184c9ec6a2..39d4841a765c 100644 --- a/runtime/src/slots.rs +++ b/runtime/src/slots.rs @@ -242,7 +242,7 @@ decl_module! { /// This can only happen when there isn't already an auction in progress and may only be /// 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. - fn new_auction( + pub fn new_auction( origin, #[compact] duration: T::BlockNumber, #[compact] lease_period_index: LeasePeriodOf From 43bf6cd52d40b6f9610aa3d7860d9f1c937d8c4b Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Tue, 23 Jul 2019 15:18:34 +0200 Subject: [PATCH 27/42] More tests --- runtime/src/crowdfund.rs | 375 +++++++++++++++++++++++++++++---------- 1 file changed, 281 insertions(+), 94 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index a53c468b68e9..c3bacc6a8797 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -239,14 +239,9 @@ decl_module! { T::Currency::transfer(&who, &Self::fund_account_id(index), value)?; - let id = Self::id_from_index(index); - sr_io::print("before get"); - let balance = who.using_encoded(|b| child::get_or_default::>(id.as_ref(), b)); - sr_io::print("after get"); - + let balance = Self::contribution_get(index, &who); let balance = balance.saturating_add(value); - who.using_encoded(|b| child::put(id.as_ref(), b, &balance)); - sr_io::print("after put"); + Self::contribution_put(index, &who, &balance); // First contribution to a fund should add it to `NewRaise` so initial bid is made if fund.last_contribution.is_none() { @@ -270,33 +265,56 @@ decl_module! { Self::deposit_event(RawEvent::Contributed(who, index, value)); } - - /// Withdraw full balance of a contributor to an unsuccessful fund. - fn withdraw(origin, #[compact] index: FundIndex) { + /// 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")?; - let now = >::block_number(); - ensure!(now >= fund.end, "contribution period not over"); + ensure!(fund.owner == who, "origin must be fund owner"); + ensure!(fund.deploy_data.is_none(), "deploy data already set"); - let id = Self::id_from_index(index); - let balance = who.using_encoded(|b| child::get::>(id.as_ref(), b)) - .ok_or("not a contributor")?; + fund.deploy_data = Some((code_hash, initial_head_data)); - // 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 - )?); + >::insert(index, &fund); - who.using_encoded(|b| child::kill(id.as_ref(), b)); - fund.raised = fund.raised.saturating_sub(balance); + 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::Withdrew(who, index, balance)); + Self::deposit_event(RawEvent::Onboarded(index, para_id)); } /// Note that a successful fund has lost its parachain slot, and place it into retirement. @@ -304,17 +322,44 @@ decl_module! { 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")?; + 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. + // Update fund to remove its parachain id + >::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")?; let now = >::block_number(); - fund.end = now; + ensure!(now >= fund.end, "contribution period not over"); + + let balance = Self::contribution_get(index, &who); + ensure!(balance > 0.into(), "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::Retiring(index)); + Self::deposit_event(RawEvent::Withdrew(who, index, balance)); } /// Remove a fund after either: it was unsuccessful and it timed out; or it was successful @@ -345,66 +390,12 @@ decl_module! { ExistenceRequirement::AllowDeath )?); - let id = Self::id_from_index(index); - child::kill_storage(id.as_ref()); + Self::crowdfund_kill(index); >::remove(index); Self::deposit_event(RawEvent::Dissolved(index)); } - - /// 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)); - } - fn on_finalize(n: T::BlockNumber) { if >::is_ending(n).is_some() { for (fund, index) in NewRaise::take().into_iter().filter_map(|i| Self::funds(i).map(|f| (f, i))) @@ -453,6 +444,26 @@ impl Module { .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)] @@ -606,6 +617,20 @@ mod tests { t.into() } + fn run_to_block(n: u64) { + while System::block_number() < n { + Crowdfund::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()); + Crowdfund::on_initialize(System::block_number()); + } + } + #[test] fn basic_setup_works() { with_externalities(&mut new_test_ext(), || { @@ -614,11 +639,12 @@ mod tests { 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); }); } #[test] - fn create_crowdfund_works() { + fn create_works() { with_externalities(&mut new_test_ext(), || { // Set up an auction assert_ok!(Slots::new_auction(Origin::ROOT, 5, 1)); @@ -649,7 +675,7 @@ mod tests { } #[test] - fn create_crowdfund_handles_basic_errors() { + fn create_handles_basic_errors() { with_externalities(&mut new_test_ext(), || { // Cannot create crowdfund without ongoing auction assert_noop!(Crowdfund::create(Origin::signed(1), 1000, 1, 4), "no auction in progress"); @@ -666,32 +692,193 @@ mod tests { } #[test] - fn contribute_crowdfund_works() { + fn contribute_works() { with_externalities(&mut new_test_ext(), || { - // Set up an crowdfund + // Set up a crowdfund assert_ok!(Slots::new_auction(Origin::ROOT, 5, 1)); assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4)); assert_eq!(Balances::free_balance(1), 999); + // 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); + // 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, Some(1)); + 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!(Slots::new_auction(Origin::ROOT, 5, 1)); + assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4)); + 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 contribute_crowdfund_handles_basic_errors() { + fn fix_deploy_data_works() { with_externalities(&mut new_test_ext(), || { - // Set up an crowdfund + // Set up a crowdfund assert_ok!(Slots::new_auction(Origin::ROOT, 5, 1)); assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4)); + 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!(Slots::new_auction(Origin::ROOT, 5, 1)); + assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4)); + 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 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)); + // Transfer fee is taken here + assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 49)); + assert_eq!(Balances::free_balance(1), 940); + + run_to_block(10); + + // User can withdraw their full balance without fees + assert_ok!(Crowdfund::withdraw(Origin::signed(1), 0)); + assert_eq!(Balances::free_balance(1), 989); + }); + } + + #[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)); + // 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), "contribution period not over"); + + 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 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)); + // 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), "contribution period not over"); + + 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 create_multiple_crowdfunds_works() { + with_externalities(&mut new_test_ext(), || { - - }); } } From 43fe30f5f7562460c6889f93cb67bcdb7606e086 Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Tue, 23 Jul 2019 17:19:14 +0200 Subject: [PATCH 28/42] Add more tests --- runtime/src/crowdfund.rs | 111 ++++++++++++++++++++++++++++++++------- 1 file changed, 91 insertions(+), 20 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index c3bacc6a8797..5a7a3cdbe274 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -219,8 +219,6 @@ decl_module! { 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 @@ -400,24 +398,22 @@ decl_module! { if >::is_ending(n).is_some() { for (fund, index) in NewRaise::take().into_iter().filter_map(|i| Self::funds(i).map(|f| (f, i))) { - if fund.last_contribution == Some(n) { - 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: 0, - }); - - // 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_counter(), - fund.first_slot, - fund.last_slot, - fund.raised, - ); - } + 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_counter(), + fund.first_slot, + fund.last_slot, + fund.raised, + ); } } } @@ -668,6 +664,8 @@ mod tests { 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); @@ -698,6 +696,8 @@ mod tests { assert_ok!(Slots::new_auction(Origin::ROOT, 5, 1)); assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4)); 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); @@ -707,6 +707,8 @@ mod tests { 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]); @@ -810,6 +812,75 @@ mod tests { } #[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)); + 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())); + + 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)); + 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 withdraw_works() { with_externalities(&mut new_test_ext(), || { // Set up a crowdfund From 79a1c8d0df283742ff1831bbfb215dcb006d4e9a Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Tue, 23 Jul 2019 23:51:14 +0200 Subject: [PATCH 29/42] Finish tests --- runtime/src/crowdfund.rs | 223 ++++++++++++++++++++++++++++++++------- runtime/src/slots.rs | 20 ++++ 2 files changed, 206 insertions(+), 37 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 5a7a3cdbe274..8070fc9bd726 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -75,12 +75,11 @@ 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"); -type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; -type NegativeImbalanceOf = <::Currency as Currency<::AccountId>>::NegativeImbalance; -type ParaIdOf = <::Parachains as ParachainRegistrar<::AccountId>>::ParaId; +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>; @@ -327,7 +326,10 @@ decl_module! { // Funds should be returned at the end of off-boarding ensure!(T::Currency::free_balance(&account) >= fund.raised, "funds not yet returned"); - // Update fund to remove its parachain id + // This fund just ended. Withdrawal period begins. + let now = >::block_number(); + fund.end = now; + >::insert(index, &fund); Self::deposit_event(RawEvent::Retiring(index)); @@ -338,11 +340,14 @@ decl_module! { 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(); - ensure!(now >= fund.end, "contribution period not over"); + + // `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 > 0.into(), "no contributions stored"); + 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( @@ -474,8 +479,8 @@ mod tests { // 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::{ - BuildStorage, traits::{BlakeTwo256, OnInitialize, OnFinalize, IdentityLookup}, - testing::Header + Permill, testing::Header, + traits::{BlakeTwo256, OnInitialize, OnFinalize, IdentityLookup}, }; impl_outer_origin! { @@ -526,6 +531,25 @@ mod tests { type TransactionByteFee = TransactionByteFee; } + 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: @@ -565,14 +589,6 @@ mod tests { } } - fn reset_count() { - PARACHAIN_COUNT.with(|p| *p.borrow_mut() = 0); - } - - fn with_parachains(f: impl FnOnce(&HashMap, Vec)>) -> T) -> T { - PARACHAINS.with(|p| f(&*p.borrow())) - } - parameter_types!{ pub const LeasePeriod: u64 = 10; pub const EndingPeriod: u64 = 3; @@ -594,12 +610,13 @@ mod tests { type SubmissionDeposit = SubmissionDeposit; type MinContribution = MinContribution; type RetirementPeriod = RetirementPeriod; - type OrphanedFunds = (); + 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 @@ -616,6 +633,7 @@ mod tests { 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()); @@ -623,6 +641,7 @@ mod tests { 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()); } } @@ -880,21 +899,115 @@ mod tests { }); } - //#[test] + #[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)); + 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)); + 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)); // Transfer fee is taken here - assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 49)); - assert_eq!(Balances::free_balance(1), 940); + 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)); - run_to_block(10); + // 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); }); } @@ -911,7 +1024,7 @@ mod tests { run_to_block(5); // Cannot withdraw before fund ends - assert_noop!(Crowdfund::withdraw(Origin::signed(1), 0), "contribution period not over"); + assert_noop!(Crowdfund::withdraw(Origin::signed(1), 0), "fund has not ended"); run_to_block(10); @@ -923,33 +1036,69 @@ mod tests { } #[test] - fn retirement_works() { + 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)); // 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); + 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 withdraw before fund ends - assert_noop!(Crowdfund::withdraw(Origin::signed(1), 0), "contribution period not over"); - - run_to_block(10); + // 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); - // 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 create_multiple_crowdfunds_works() { + 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)); + // 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"); }); } } diff --git a/runtime/src/slots.rs b/runtime/src/slots.rs index 39d4841a765c..2ef2b38257bd 100644 --- a/runtime/src/slots.rs +++ b/runtime/src/slots.rs @@ -497,6 +497,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); } @@ -1025,6 +1027,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)); From d5f18844cef2f2a0c218c7778a81b4328c16f40e Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Wed, 24 Jul 2019 23:12:40 +0200 Subject: [PATCH 30/42] Formatting nits --- runtime/src/crowdfund.rs | 18 +++++++----------- runtime/src/slots.rs | 12 ++++-------- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 8070fc9bd726..f3498c0ac9f1 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -148,11 +148,10 @@ decl_storage! { } decl_event!( - pub enum Event - where - ::AccountId, - Balance = BalanceOf, - ParaId = ParaIdOf, + pub enum Event where + ::AccountId, + Balance = BalanceOf, + ParaId = ParaIdOf, { Created(FundIndex), Contributed(AccountId, FundIndex, Balance), @@ -170,8 +169,7 @@ decl_module! { /// Create a new crowdfunding campaign for a parachain slot deposit for the current auction. #[weight = TransactionWeight::Basic(100_000, 10)] - fn create( - origin, + fn create(origin, #[compact] cap: BalanceOf, #[compact] first_slot: T::BlockNumber, #[compact] last_slot: T::BlockNumber @@ -269,8 +267,7 @@ decl_module! { /// - `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, + fn fix_deploy_data(origin, #[compact] index: FundIndex, code_hash: T::Hash, initial_head_data: Vec @@ -294,8 +291,7 @@ decl_module! { /// /// - `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, + fn onboard(origin, #[compact] index: FundIndex, #[compact] para_id: ParaIdOf ) { diff --git a/runtime/src/slots.rs b/runtime/src/slots.rs index 2ef2b38257bd..8fc9ccceed69 100644 --- a/runtime/src/slots.rs +++ b/runtime/src/slots.rs @@ -242,8 +242,7 @@ decl_module! { /// This can only happen when there isn't already an auction in progress and may only be /// 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. - pub fn new_auction( - origin, + pub fn new_auction(origin, #[compact] duration: T::BlockNumber, #[compact] lease_period_index: LeasePeriodOf ) { @@ -277,8 +276,7 @@ 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. - fn bid( - origin, + fn bid(origin, #[compact] sub: SubId, #[compact] auction_index: AuctionIndex, #[compact] first_slot: LeasePeriodOf, @@ -305,8 +303,7 @@ 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. - fn bid_renew( - origin, + fn bid_renew(origin, #[compact] auction_index: AuctionIndex, #[compact] first_slot: LeasePeriodOf, #[compact] last_slot: LeasePeriodOf, @@ -339,8 +336,7 @@ 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. - pub fn fix_deploy_data( - origin, + pub fn fix_deploy_data(origin, #[compact] sub: SubId, #[compact] para_id: ParaIdOf, code_hash: T::Hash, From 2914538cf352a07029088f5fc644a57984219d15 Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Thu, 25 Jul 2019 14:48:52 +0200 Subject: [PATCH 31/42] Use `into_iter` --- runtime/src/crowdfund.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index f3498c0ac9f1..6de1eace7050 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -435,9 +435,9 @@ impl Module { buf.extend_from_slice(b"crowdfund"); buf.extend_from_slice(&index.to_le_bytes()[..]); - CHILD_STORAGE_KEY_PREFIX.iter() + CHILD_STORAGE_KEY_PREFIX.into_iter() .chain(b"default:") - .chain(T::Hashing::hash(&buf[..]).as_ref().iter()) + .chain(T::Hashing::hash(&buf[..]).as_ref().into_iter()) .cloned() .collect() } From a3ff9be10c701cac56ee9347f97c256d02ca186a Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Wed, 14 Aug 2019 14:22:40 +0200 Subject: [PATCH 32/42] Fix for latest Substrate updates --- runtime/src/crowdfund.rs | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 6de1eace7050..1e6524dfb5ae 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -66,11 +66,11 @@ use srml_support::{ traits::{Currency, Get, OnUnbalanced, WithdrawReason, ExistenceRequirement} }; use system::ensure_signed; -use sr_primitives::{ModuleId, weights::TransactionWeight, +use sr_primitives::{ModuleId, weights::SimpleDispatchInfo, traits::{AccountIdConversion, Hash, Saturating, Zero, CheckedAdd} }; use crate::slots; -use parity_codec::{Encode, Decode}; +use codec::{Encode, Decode}; use rstd::vec::Vec; use crate::parachains::ParachainRegistrar; use substrate_primitives::storage::well_known_keys::CHILD_STORAGE_KEY_PREFIX; @@ -168,7 +168,7 @@ decl_module! { fn deposit_event() = default; /// Create a new crowdfunding campaign for a parachain slot deposit for the current auction. - #[weight = TransactionWeight::Basic(100_000, 10)] + #[weight = SimpleDispatchInfo::FixedNormal(100_000)] fn create(origin, #[compact] cap: BalanceOf, #[compact] first_slot: T::BlockNumber, @@ -475,8 +475,8 @@ mod tests { // 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::{ - Permill, testing::Header, - traits::{BlakeTwo256, OnInitialize, OnFinalize, IdentityLookup}, + Perbill, Permill, testing::Header, + traits::{BlakeTwo256, OnInitialize, OnFinalize, IdentityLookup, ConvertInto}, }; impl_outer_origin! { @@ -489,10 +489,14 @@ mod tests { #[derive(Clone, Eq, PartialEq)] pub struct Test; parameter_types! { - pub const BlockHashCount: u64 = 250; + 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; @@ -500,8 +504,12 @@ mod tests { 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; } parameter_types! { pub const ExistentialDeposit: u64 = 0; @@ -525,6 +533,8 @@ mod tests { type CreationFee = CreationFee; type TransactionBaseFee = TransactionBaseFee; type TransactionByteFee = TransactionByteFee; + type WeightToFee = ConvertInto; + } parameter_types! { @@ -618,11 +628,11 @@ mod tests { // 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().0; - t.extend(balances::GenesisConfig::{ + let mut t = system::GenesisConfig::default().build_storage::().unwrap(); + balances::GenesisConfig::{ balances: vec![(1, 1000), (2, 2000), (3, 3000), (4, 4000)], vesting: vec![], - }.build_storage().unwrap().0); + }.assimilate_storage(&mut t).unwrap(); t.into() } From 7c4b105b86271cb8a0ad8a9c7472cf9f753a956b Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Fri, 13 Sep 2019 19:02:21 +0200 Subject: [PATCH 33/42] Cleanup --- runtime/src/crowdfund.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 1e6524dfb5ae..94735a417788 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -202,14 +202,14 @@ decl_module! { >::insert(index, FundInfo { parachain: None, - owner: owner, - deposit: deposit, + owner, + deposit, raised: Zero::zero(), - end: end, - cap: cap, + end, + cap, last_contribution: None, - first_slot: first_slot, - last_slot: last_slot, + first_slot, + last_slot, deploy_data: None, }); From cfb41f6866337a32751931c5c269f17a6f0d5137 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Fri, 13 Sep 2019 21:35:00 +0200 Subject: [PATCH 34/42] Fixes. --- runtime/src/crowdfund.rs | 80 ++++++++++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 94735a417788..562f4738d0a2 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -99,8 +99,17 @@ pub trait Trait: slots::Trait { 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 { @@ -120,7 +129,7 @@ pub struct FundInfo { cap: Balance, /// The most recent block that this had a contribution. Determines if we make a bid or not. /// If this is `None`, then the last contribution was made outside of the ending period. - last_contribution: Option, + 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, @@ -144,10 +153,13 @@ decl_storage! { /// 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!( +decl_event! { pub enum Event where ::AccountId, Balance = BalanceOf, @@ -161,28 +173,25 @@ decl_event!( DeployDataFixed(FundIndex), Onboarded(FundIndex, ParaId), } -); +} decl_module! { pub struct Module for enum Call where origin: T::Origin { - fn deposit_event() = default; + 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] 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"); - // Check an auction is in progress, and extract the `early_end` block - let (_, early_end) = >::auction_info().ok_or("no auction in progress")?; - - // End of the crowdfund will be the last possible block for the ongoing auction - let end = early_end + T::EndingPeriod::get(); + ensure!(end > >::block_number(), "end must be in the future"); let deposit = T::SubmissionDeposit::get(); let imb = T::Currency::withdraw( @@ -207,7 +216,7 @@ decl_module! { raised: Zero::zero(), end, cap, - last_contribution: None, + last_contribution: LastContribution::Never, first_slot, last_slot, deploy_data: None, @@ -238,23 +247,34 @@ decl_module! { let balance = balance.saturating_add(value); Self::contribution_put(index, &who, &balance); - // First contribution to a fund should add it to `NewRaise` so initial bid is made - if fund.last_contribution.is_none() { - NewRaise::mutate(|v| v.push(index)); + 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 { - // Any contributions that happen during the ending period should - // cause another bid to be placed with updated value - if >::is_ending(now).is_some() { - // Only add to `NewRaised` if it hasn't already been added this block - if let Some(c) = fund.last_contribution { - if c != now { - NewRaise::mutate(|v| v.push(index)); - } + 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); } } } - - fund.last_contribution = Some(now); + >::insert(index, &fund); Self::deposit_event(RawEvent::Contributed(who, index, value)); @@ -396,9 +416,13 @@ decl_module! { } fn on_finalize(n: T::BlockNumber) { - if >::is_ending(n).is_some() { - for (fund, index) in NewRaise::take().into_iter().filter_map(|i| Self::funds(i).map(|f| (f, i))) - { + 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 @@ -410,7 +434,7 @@ decl_module! { // the crowdfunding configuration. We do some checks ahead of time in crowdfund `create`. let _ = >::handle_bid( bidder, - >::auction_counter(), + auction_index, fund.first_slot, fund.last_slot, fund.raised, From f82611220831d3964ff0de7d3b150956cf9b67c1 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Fri, 13 Sep 2019 21:36:00 +0200 Subject: [PATCH 35/42] Copyright header. --- runtime/src/crowdfund.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 562f4738d0a2..6b82b061516f 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -1,18 +1,18 @@ // Copyright 2017-2019 Parity Technologies (UK) Ltd. -// This file is part of Substrate. +// This file is part of Polkadot. -// Substrate is free software: you can redistribute it and/or modify +// 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. -// Substrate is distributed in the hope that it will be useful, +// 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 Substrate. If not, see . +// along with Polkadot. If not, see . //! # Parachain Crowdfunding module //! From c1979d3e2e1b0868e6d858d33d7d2851e9b75d8a Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Fri, 13 Sep 2019 21:37:57 +0200 Subject: [PATCH 36/42] Remove dead code. --- runtime/src/crowdfund.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 6b82b061516f..012cca8882f7 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -78,7 +78,6 @@ 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 { From 8cb756686f23b800c26d057eebb2d9c062e5655d Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Fri, 13 Sep 2019 21:38:25 +0200 Subject: [PATCH 37/42] Reinstate actually alive code. --- runtime/src/crowdfund.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 012cca8882f7..6b82b061516f 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -78,6 +78,7 @@ 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 { From 49643baff18912d55ecb4576070aeddb73d4ee0c Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Sat, 14 Sep 2019 01:19:12 +0200 Subject: [PATCH 38/42] Fix tests Still have to write some new follow up tests though --- runtime/src/crowdfund.rs | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 6b82b061516f..fce224c811de 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -534,6 +534,7 @@ mod tests { type MaximumBlockWeight = MaximumBlockWeight; type MaximumBlockLength = MaximumBlockLength; type AvailableBlockRatio = AvailableBlockRatio; + type Version = (); } parameter_types! { pub const ExistentialDeposit: u64 = 0; @@ -694,7 +695,7 @@ mod tests { // Set up an auction assert_ok!(Slots::new_auction(Origin::ROOT, 5, 1)); // Now try to create a crowdfund campaign - assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4)); + 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 { @@ -705,7 +706,7 @@ mod tests { // 5 blocks length + 3 block ending period + 1 starting block end: 9, cap: 1000, - last_contribution: None, + last_contribution: LastContribution::Never, first_slot: 1, last_slot: 4, deploy_data: None, @@ -724,17 +725,14 @@ mod tests { #[test] fn create_handles_basic_errors() { with_externalities(&mut new_test_ext(), || { - // Cannot create crowdfund without ongoing auction - assert_noop!(Crowdfund::create(Origin::signed(1), 1000, 1, 4), "no auction in progress"); - // Set up an auction assert_ok!(Slots::new_auction(Origin::ROOT, 5, 1)); // Cannot create a crowdfund with bad slots - assert_noop!(Crowdfund::create(Origin::signed(1), 1000, 4, 1), "last slot must be greater than first slot"); - assert_noop!(Crowdfund::create(Origin::signed(1), 1000, 1, 5), "last slot cannot be more then 3 more than first slot"); + 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), "too few free funds in account"); + assert_noop!(Crowdfund::create(Origin::signed(1337), 1000, 1, 3, 9), "too few free funds in account"); }); } @@ -743,7 +741,7 @@ mod tests { 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)); + 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); @@ -764,7 +762,7 @@ mod tests { let fund = Crowdfund::funds(0).unwrap(); // Last contribution time recorded - assert_eq!(fund.last_contribution, Some(1)); + assert_eq!(fund.last_contribution, LastContribution::PreEnding(0)); assert_eq!(fund.raised, 49); }); } @@ -779,7 +777,7 @@ mod tests { // Set up a crowdfund assert_ok!(Slots::new_auction(Origin::ROOT, 5, 1)); - assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4)); + 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 @@ -798,7 +796,7 @@ mod tests { 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)); + assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Balances::free_balance(1), 999); // Add deploy data @@ -821,7 +819,7 @@ mod tests { 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)); + 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 @@ -865,7 +863,7 @@ mod tests { 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)); + assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Balances::free_balance(1), 999); // Add deploy data @@ -897,7 +895,7 @@ mod tests { 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)); + assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Balances::free_balance(1), 999); // Fund crowdfund @@ -934,7 +932,7 @@ mod tests { 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)); + assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Balances::free_balance(1), 999); // Add deploy data @@ -976,7 +974,7 @@ mod tests { 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)); + assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Balances::free_balance(1), 999); // Add deploy data @@ -1020,7 +1018,7 @@ mod tests { 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)); + 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)); @@ -1046,7 +1044,7 @@ mod tests { 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)); + 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); @@ -1070,7 +1068,7 @@ mod tests { 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)); + 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)); @@ -1105,7 +1103,7 @@ mod tests { 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)); + 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)); From 166ec2ca9f9f29338dd9eabee652596a19cee3cb Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Sat, 14 Sep 2019 02:16:17 +0200 Subject: [PATCH 39/42] Make funds work before auction --- runtime/src/crowdfund.rs | 53 ++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index fce224c811de..5dc77506e29d 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -236,10 +236,9 @@ decl_module! { 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 and auction has not "ended early" (it is still in progress). + // Make sure crowdfund has not ended let now = >::block_number(); ensure!(fund.end > now, "contribution period ended"); - ensure!(>::is_in_progress(), "no auction in progress"); T::Currency::transfer(&who, &Self::fund_account_id(index), value)?; @@ -686,14 +685,13 @@ mod tests { 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(), || { - // Set up an auction - assert_ok!(Slots::new_auction(Origin::ROOT, 5, 1)); // Now try to create a crowdfund campaign assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Crowdfund::fund_count(), 1); @@ -725,8 +723,6 @@ mod tests { #[test] fn create_handles_basic_errors() { with_externalities(&mut new_test_ext(), || { - // Set up an auction - assert_ok!(Slots::new_auction(Origin::ROOT, 5, 1)); // 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"); @@ -740,7 +736,6 @@ mod tests { fn contribute_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); assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 1); @@ -776,7 +771,6 @@ mod tests { assert_noop!(Crowdfund::contribute(Origin::signed(1), 0, 9), "contribution too small"); // 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_ok!(Crowdfund::contribute(Origin::signed(1), 0, 101)); @@ -795,7 +789,6 @@ mod tests { fn fix_deploy_data_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); @@ -818,7 +811,6 @@ mod tests { fn fix_deploy_data_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); @@ -879,6 +871,9 @@ mod tests { 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())); @@ -1129,4 +1124,42 @@ mod tests { 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()]); + }); + } } From 7a7079e1af1d26e88ed757d961d287911ca409f0 Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Sun, 15 Sep 2019 02:20:32 +0200 Subject: [PATCH 40/42] Test a fund which spans 2 auctions. --- runtime/src/crowdfund.rs | 57 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 5dc77506e29d..a9daae580def 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -1162,4 +1162,61 @@ mod tests { 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()]); + }); + } } From 4037522a429046a300f58b732c8b89d674b0ae10 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Tue, 17 Sep 2019 17:29:04 +0800 Subject: [PATCH 41/42] Docs. --- runtime/src/crowdfund.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 6b82b061516f..b8cc5d909943 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -33,6 +33,11 @@ //! //! 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 @@ -558,7 +563,6 @@ mod tests { type TransactionBaseFee = TransactionBaseFee; type TransactionByteFee = TransactionByteFee; type WeightToFee = ConvertInto; - } parameter_types! { From aebce487370f54a75504051ca4b0347b641d7228 Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Tue, 17 Sep 2019 12:07:49 +0200 Subject: [PATCH 42/42] Update doc --- runtime/src/crowdfund.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/runtime/src/crowdfund.rs b/runtime/src/crowdfund.rs index 1880bd55e3f0..0209e996368f 100644 --- a/runtime/src/crowdfund.rs +++ b/runtime/src/crowdfund.rs @@ -133,7 +133,11 @@ pub struct FundInfo { /// 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 `None`, then the last contribution was made outside of the ending period. + /// 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.