diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..80c15e7ba --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true +[*] +indent_style=tab +indent_size=tab +tab_width=4 +end_of_line=lf +charset=utf-8 +trim_trailing_whitespace=true +max_line_length=120 +insert_final_newline=true + +[*.{yml,yaml}] +indent_style=space +indent_size=2 +tab_width=8 +end_of_line=lf diff --git a/flow/Cargo.toml b/flow/Cargo.toml index f6c94d038..cf6a44092 100644 --- a/flow/Cargo.toml +++ b/flow/Cargo.toml @@ -18,14 +18,21 @@ frame-system = { git = "https://github.com/paritytech/substrate", branch = "polk frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false, optional = true } orml-traits = { path = "../../orml/traits", default-features = false } -primitives = { package = "zero-primitives", path = "../../primitives", default-features = false } -support = { package = "gamedao-protocol-support", path = "../support", default-features = false } +zero-primitives = { package = "zero-primitives", path = "../../primitives", default-features = false } +gamedao-protocol-support = { package = "gamedao-protocol-support", path = "../support", default-features = false } [dev-dependencies] +frame-support-test = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } +pallet-timestamp = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } +pallet-balances = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } + +orml-tokens = { path = "../../orml/tokens", default-features = false } +orml-currencies = { path = "../../orml/currencies", default-features = false } + [features] default = ["std"] runtime-benchmarks = ["frame-benchmarking"] @@ -42,6 +49,8 @@ std = [ "sp-std/std", "sp-runtime/std", "orml-traits/std", - "support/std", + "orml-tokens/std", + "orml-currencies/std", + "gamedao-protocol-support/std", ] try-runtime = ["frame-support/try-runtime"] diff --git a/flow/src/lib.rs b/flow/src/lib.rs index c69b14ee5..4ab92a728 100644 --- a/flow/src/lib.rs +++ b/flow/src/lib.rs @@ -50,29 +50,32 @@ // 1. create campaigns with custom funding goal and runtime // 2. invest into open campaigns #![cfg_attr(not(feature = "std"), no_std)] -#[warn(unused_imports)] +// #[warn(unused_imports)] // pub use weights::WeightInfo; -pub use pallet::*; use frame_support::{ - codec::{Decode, Encode}, - dispatch::DispatchResult, - traits::{Randomness, UnixTime}, + transactional, + codec::{Decode, Encode}, + dispatch::DispatchResult, + traits::{Randomness, UnixTime} }; use scale_info::TypeInfo; use sp_std::{fmt::Debug, vec::Vec}; - -use codec::FullCodec; +use sp_runtime::Permill; use frame_support::traits::Get; -use sp_runtime::traits::AtLeast32BitUnsigned; use orml_traits::{MultiCurrency, MultiReservableCurrency}; -use primitives::{Balance, CurrencyId, Moment}; -use support::ControlPalletStorage; +use zero_primitives::{Balance, CurrencyId, Moment}; +use gamedao_protocol_support::{ControlPalletStorage, FlowState}; + -// TODO: tests -// TODO: pallet benchmarking +mod mock; +mod tests; + +pub use pallet::*; + +// TODO: after Control pallet will be merged // mod benchmarking; // TODO: weights @@ -84,914 +87,889 @@ use support::ControlPalletStorage; #[derive(Encode, Decode, Clone, PartialEq, Eq, PartialOrd, Ord, TypeInfo, Debug)] #[repr(u8)] pub enum FlowProtocol { - Grant = 0, - Raise = 1, - Lend = 2, - Loan = 3, - Share = 4, - Pool = 5, + Grant = 0, + Raise = 1, + Lend = 2, + Loan = 3, + Share = 4, + Pool = 5, } impl Default for FlowProtocol { - fn default() -> Self { - Self::Raise - } + fn default() -> Self { + Self::Raise + } } #[derive(Encode, Decode, Clone, PartialEq, Eq, PartialOrd, Ord, TypeInfo, Debug)] #[repr(u8)] pub enum FlowGovernance { - No = 0, // 100% unreserved upon completion - Yes = 1, // withdrawal votings + No = 0, // 100% unreserved upon completion + Yes = 1, // withdrawal votings } impl Default for FlowGovernance { - fn default() -> Self { - Self::No - } -} - -#[derive(Encode, Decode, Clone, PartialEq, Eq, PartialOrd, Ord, TypeInfo, Debug)] -#[repr(u8)] -pub enum FlowState { - Init = 0, - Active = 1, - Paused = 2, - Success = 3, - Failed = 4, - Locked = 5, -} - -impl Default for FlowState { - fn default() -> Self { - Self::Init - } + fn default() -> Self { + Self::No + } } // TODO: this can be decomposed to improve weight #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, TypeInfo)] pub struct Campaign { - // unique hash to identify campaign (generated) - id: Hash, - // hash of the overarching body from module-control - org: Hash, - // name - name: Vec, - - // controller account -> must match body controller - // during campaing runtime controller change is not allowed - // needs to be revised to avoid campaign attack by starting - // a campagin when dao wants to remove controller for reasons - owner: AccountId, - - // TODO: THIS NEEDS TO BE GAMEDAO COUNCIL - /// admin account of the campaign (operator) - admin: AccountId, - - // TODO: assets => GAME - /// campaign owners deposit - deposit: Balance, - - // TODO: /// campaign start block - // start: BlockNumber, - /// block until campaign has to reach cap - expiry: BlockNumber, - /// minimum amount of token to become a successful campaign - cap: Balance, - - /// protocol after successful raise - protocol: FlowProtocol, - /// governance after successful raise - governance: FlowGovernance, - - /// content storage - cid: Vec, - - // TODO: prepare for launchpad functionality - // token cap - // token_cap: u64, - // token_price - // token_price: u64, - // /// token symbol - token_symbol: Vec, - // /// token name - token_name: Vec, - - /// creation timestamp - created: Timestamp, + // unique hash to identify campaign (generated) + id: Hash, + // hash of the overarching body from module-control + org: Hash, + // name + name: Vec, + + // controller account -> must match body controller + // during campaing runtime controller change is not allowed + // needs to be revised to avoid campaign attack by starting + // a campagin when dao wants to remove controller for reasons + owner: AccountId, + + // TODO: THIS NEEDS TO BE GAMEDAO COUNCIL + /// admin account of the campaign (operator) + admin: AccountId, + + // TODO: assets => GAME + /// campaign owners deposit + deposit: Balance, + + // TODO: /// campaign start block + // start: BlockNumber, + /// block until campaign has to reach cap + expiry: BlockNumber, + /// minimum amount of token to become a successful campaign + cap: Balance, + + /// protocol after successful raise + protocol: FlowProtocol, + /// governance after successful raise + governance: FlowGovernance, + + /// content storage + cid: Vec, + + // TODO: prepare for launchpad functionality + // token cap + // token_cap: u64, + // token_price + // token_price: u64, + // /// token symbol + token_symbol: Vec, + // /// token name + token_name: Vec, + + /// creation timestamp + created: Timestamp, } #[frame_support::pallet] pub mod pallet { - use super::*; - use frame_support::pallet_prelude::*; - use frame_system::pallet_prelude::*; - - #[pallet::pallet] - #[pallet::generate_store(pub(super) trait Store)] - pub struct Pallet(_); - - #[pallet::config] - pub trait Config: frame_system::Config { - type WeightInfo: frame_system::weights::WeightInfo; - type Event: From> - + IsType<::Event> - + Into<::Event>; - - type Currency: MultiCurrency - + MultiReservableCurrency; - type UnixTime: UnixTime; - type Randomness: Randomness; - type Control: ControlPalletStorage; - - /// The origin that is allowed to make judgements. - type GameDAOAdminOrigin: EnsureOrigin; - type GameDAOTreasury: Get; - - #[pallet::constant] - type MinLength: Get; - #[pallet::constant] - type MaxLength: Get; - - #[pallet::constant] - type MaxCampaignsPerAddress: Get; - #[pallet::constant] - type MaxCampaignsPerBlock: Get; - #[pallet::constant] - type MaxContributionsPerBlock: Get; - - #[pallet::constant] - type MinDuration: Get; - #[pallet::constant] - type MaxDuration: Get; - #[pallet::constant] - type MinCreatorDeposit: Get; - #[pallet::constant] - type MinContribution: Get; - - #[pallet::constant] - type FundingCurrencyId: Get; - - // TODO: collect fees for treasury - // type CreationFee: Get>; - #[pallet::constant] - type CampaignFee: Get; - } - - /// Campaign - #[pallet::storage] - #[pallet::getter(fn campaign_by_id)] - pub(super) type Campaigns = StorageMap< - _, - Blake2_128Concat, - T::Hash, - Campaign< - T::Hash, - T::AccountId, - Balance, - T::BlockNumber, - Moment, - FlowProtocol, - FlowGovernance, - >, - ValueQuery, - >; - - /// Associated Body - #[pallet::storage] - #[pallet::getter(fn campaign_org)] - pub(super) type CampaignOrg = - StorageMap<_, Blake2_128Concat, T::Hash, T::Hash, ValueQuery>; - - /// Get Campaign Owner (body controller) by campaign id - #[pallet::storage] - #[pallet::getter(fn campaign_owner)] - pub(super) type CampaignOwner = - StorageMap<_, Blake2_128Concat, T::Hash, T::AccountId, OptionQuery>; - - /// Get Campaign Admin (supervision) by campaign id - #[pallet::storage] - #[pallet::getter(fn campaign_admin)] - pub(super) type CampaignAdmin = - StorageMap<_, Blake2_128Concat, T::Hash, T::AccountId, OptionQuery>; - - /// Campaign state - /// 0 init, 1 active, 2 paused, 3 complete success, 4 complete failed, 5 authority lock - #[pallet::storage] - #[pallet::getter(fn campaign_state)] - pub(super) type CampaignState = - StorageMap<_, Blake2_128Concat, T::Hash, FlowState, ValueQuery, GetDefault>; - - /// Get Campaigns for a certain state - #[pallet::storage] - #[pallet::getter(fn campaigns_by_state)] - pub(super) type CampaignsByState = - StorageMap<_, Blake2_128Concat, FlowState, Vec, ValueQuery>; - - /// Campaigns ending in block x - #[pallet::storage] - #[pallet::getter(fn campaigns_by_block)] - pub(super) type CampaignsByBlock = - StorageMap<_, Blake2_128Concat, T::BlockNumber, Vec, ValueQuery>; - - /// Total number of campaigns -> all campaigns - #[pallet::storage] - #[pallet::getter(fn campaigns_index)] - pub(super) type CampaignsArray = - StorageMap<_, Blake2_128Concat, u64, T::Hash, ValueQuery>; - #[pallet::storage] - #[pallet::getter(fn campaigns_count)] - pub type CampaignsCount = StorageValue<_, u64, ValueQuery>; - #[pallet::storage] - pub(super) type CampaignsIndex = - StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery>; - - // caller owned campaigns -> my campaigns - #[pallet::storage] - #[pallet::getter(fn campaigns_owned_index)] - pub(super) type CampaignsOwnedArray = - StorageMap<_, Blake2_128Concat, T::Hash, T::Hash, ValueQuery>; - #[pallet::storage] - #[pallet::getter(fn campaigns_owned_count)] - pub(super) type CampaignsOwnedCount = - StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery>; - #[pallet::storage] - pub(super) type CampaignsOwnedIndex = - StorageMap<_, Blake2_128Concat, (T::Hash, T::Hash), u64, ValueQuery>; - - /// campaigns contributed by accountid - #[pallet::storage] - #[pallet::getter(fn campaigns_contributed)] - pub(super) type CampaignsContributed = - StorageMap<_, Blake2_128Concat, T::AccountId, Vec, ValueQuery>; - - /// campaigns related to an organisation - #[pallet::storage] - #[pallet::getter(fn campaigns_by_body)] - pub(super) type CampaignsByBody = - StorageMap<_, Blake2_128Concat, T::Hash, Vec, ValueQuery>; - - // caller contributed campaigns -> contributed campaigns - #[pallet::storage] - #[pallet::getter(fn campaigns_contributed_index)] - pub(super) type CampaignsContributedArray = - StorageMap<_, Blake2_128Concat, (T::AccountId, u64), T::Hash, ValueQuery>; - #[pallet::storage] - #[pallet::getter(fn campaigns_contributed_count)] - pub(super) type CampaignsContributedCount = - StorageMap<_, Blake2_128Concat, T::AccountId, u64, ValueQuery>; - #[pallet::storage] - pub(super) type CampaignsContributedIndex = - StorageMap<_, Blake2_128Concat, (T::AccountId, T::Hash), u64, ValueQuery>; - - // Total contributions balance per campaign - #[pallet::storage] - #[pallet::getter(fn campaign_balance)] - pub(super) type CampaignBalance = - StorageMap<_, Blake2_128Concat, T::Hash, Balance, ValueQuery>; - - // Contributions per user - #[pallet::storage] - #[pallet::getter(fn campaign_contribution)] - pub(super) type CampaignContribution = - StorageMap<_, Blake2_128Concat, (T::Hash, T::AccountId), Balance, ValueQuery>; - - // Contributors - #[pallet::storage] - #[pallet::getter(fn campaign_contributors)] - pub(super) type CampaignContributors = - StorageMap<_, Blake2_128Concat, T::Hash, Vec, ValueQuery>; - #[pallet::storage] - #[pallet::getter(fn campaign_contributors_count)] - pub(super) type CampaignContributorsCount = - StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery>; - - // Max campaign block limit - // CampaignMaxDuration get(fn get_max_duration) config(): T::BlockNumber = T::BlockNumber::from(T::MaxDuration::get()); - - // Campaign nonce, increases per created campaign - - #[pallet::storage] - #[pallet::getter(fn nonce)] - pub type Nonce = StorageValue<_, u128, ValueQuery>; - - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - CampaignDestroyed(T::Hash), - CampaignCreated( - T::Hash, - T::AccountId, - T::AccountId, - Balance, - Balance, - T::BlockNumber, - Vec, - ), - CampaignContributed(T::Hash, T::AccountId, Balance, T::BlockNumber), - CampaignFinalized(T::Hash, Balance, T::BlockNumber, bool), - CampaignFailed(T::Hash, Balance, T::BlockNumber, bool), - CampaignUpdated(T::Hash, FlowState, T::BlockNumber), - Message(Vec), - } - - #[pallet::error] - pub enum Error { - // - // general - // - /// Must contribute at least the minimum amount of Campaigns - ContributionTooSmall, - /// Balance too low. - BalanceTooLow, - /// Treasury Balance Too Low - TreasuryBalanceTooLow, - /// The Campaign id specified does not exist - InvalidId, - /// The Campaign's contribution period has ended; no more contributions will be accepted - ContributionPeriodOver, - /// You may not withdraw or dispense Campaigns while the Campaign is still active - CampaignStillActive, - /// You cannot withdraw Campaigns because you have not contributed any - NoContribution, - /// You cannot dissolve a Campaign that has not yet completed its retirement period - CampaignNotRetired, - /// Campaign expired - CampaignExpired, - /// Cannot dispense Campaigns from an unsuccessful Campaign - UnsuccessfulCampaign, - - // - // create - // - /// Campaign must end after it starts - EndTooEarly, - /// Campaign expiry has be lower than the block number limit - EndTooLate, - /// Max contributions per block exceeded - ContributionsPerBlockExceeded, - /// Name too long - NameTooLong, - /// Name too short - NameTooShort, - /// Deposit exceeds the campaign target - DepositTooHigh, - /// Campaign id exists - IdExists, - - // - // mint - // - /// Overflow adding a new campaign to total fundings - AddCampaignOverflow, - /// Overflow adding a new owner - AddOwnedOverflow, - /// Overflow adding to the total number of contributors of a camapaign - UpdateContributorOverflow, - /// Overflow adding to the total number of contributions of a camapaign - AddContributionOverflow, - /// Campaign owner unknown - OwnerUnknown, - /// Campaign admin unknown - AdminUnknown, - /// Cannot contribute to owned campaign - NoContributionToOwnCampaign, - /// Guru Meditation - GuruMeditation, - /// Zou are not authorized for this call - AuthorizationError, - /// Contributions not allowed - NoContributionsAllowed, - /// Id Unknown - IdUnknown, - /// Transfer Error - TransferError, - } - - #[pallet::hooks] - impl Hooks> for Pallet { - /// Block finalization - fn on_finalize(_n: BlockNumberFor) { - // get all the campaigns ending in current block - let block_number = >::block_number(); - // which campaigns end in this block - let campaign_hashes = Self::campaigns_by_block(block_number); - - // iterate over campaigns ending in this block - for campaign_id in &campaign_hashes { - // get campaign struct - let campaign = Self::campaign_by_id(campaign_id); - let campaign_balance = Self::campaign_balance(campaign_id); - let dao = Self::campaign_org(&campaign_id); - let dao_treasury = T::Control::body_treasury(&dao); - - // check for cap reached - if campaign_balance >= campaign.cap { - // get campaign owner - // should be controller --- test? - let _owner = Self::campaign_owner(campaign_id); - - match _owner { - Some(owner) => { - // get all contributors - let contributors = Self::campaign_contributors(campaign_id); - let mut transaction_complete = true; - - // 1 iterate over contributors - // 2 unreserve contribution - // 3 transfer contribution to campaign treasury - 'inner: for contributor in &contributors { - // if contributor == campaign owner, skip - if contributor == &owner { - continue; - } - - // get amount from contributor - let contributor_balance = Self::campaign_contribution(( - *campaign_id, - contributor.clone(), - )); - - // unreserve the amount in contributor balance - let unreserve_amount = T::Currency::unreserve( - T::FundingCurrencyId::get(), - &contributor, - contributor_balance.clone(), - ); - - // transfer from contributor - let transfer_amount = T::Currency::transfer( - T::FundingCurrencyId::get(), - &contributor, - &dao_treasury, - contributor_balance.clone(), - // TODO: check how this impacts logic: - // ExistenceRequirement::AllowDeath - ); - - // success? - match transfer_amount { - Err(_e) => { - transaction_complete = false; - break 'inner; - } - Ok(_v) => {} - } - } - - // If all transactions are settled - // 1. reserve campaign balance - // 2. unreserve and send the commission to operator treasury - if transaction_complete { - // reserve campaign volume - let reserve_campaign_amount = T::Currency::reserve( - T::FundingCurrencyId::get(), - &dao_treasury, - campaign_balance.clone(), - ); - - // - // - - // calculate commission - - // -> pub const CampaignFee: Balance = 25 * CENTS; - // let fee = ::CampaignFee::get(); - - // -> CampaignBalance get(fn campaign_balance): map hasher(blake2_128_concat) T::Hash => T::Balance; - // let bal = campaign_balance.clone(); - - // let commission = U256::from( bal.into() ) - // .checked_div( U256::from( fee.into() ) ); - - // let commission = bal.checked_div(fee); - // let commission = U256::from(bal).checked_div(U256::from(fee)); - - // - // - - // let unreserve_commission = >::unreserve( - // &dao_treasury, - // commission.clone() - // ); - - // let transfer_commission = as Currency<_>>::transfer( - // &dao_treasury, - // &::GameDAOTreasury::get(), - // commission, - // ExistenceRequirement::AllowDeath - // ); - // match transfer_commission { - // Err(_e) => { }, //(Error::::TransferError) - // Ok(_v) => {} - // } - - Self::set_state(campaign.id.clone(), FlowState::Success); - - // finalized event - Self::deposit_event(Event::CampaignFinalized( - *campaign_id, - campaign_balance, - block_number, - true, - )); - } - } - None => continue, - } - - // campaign cap not reached - } else { - // campaign failed, revert all contributions - - let contributors = Self::campaign_contributors(campaign_id); - for account in contributors { - let contribution = - Self::campaign_contribution((*campaign_id, account.clone())); - T::Currency::unreserve(T::FundingCurrencyId::get(), &account, contribution); - } - - // update campaign state to failed - Self::set_state(campaign.id, FlowState::Failed); - - // unreserve DEPOSIT - - let unreserve_deposit = T::Currency::unreserve( - T::FundingCurrencyId::get(), - &dao_treasury, - campaign.deposit, - ); - - // failed event - Self::deposit_event(Event::CampaignFailed( - *campaign_id, - campaign_balance, - block_number, - false, - )); - } - } - } - } - - #[pallet::call] - impl Pallet { - #[pallet::weight(5_000_000)] - pub fn create( - origin: OriginFor, - org: T::Hash, - admin: T::AccountId, // supervision, should be dao provided! - name: Vec, - target: Balance, - deposit: Balance, - expiry: T::BlockNumber, - protocol: FlowProtocol, - governance: FlowGovernance, - cid: Vec, // content cid - token_symbol: Vec, // up to 5 - token_name: Vec, // cleartext - // token_curve_a: u8, // preset - // token_curve_b: Vec, // custom - ) -> DispatchResult { - let creator = ensure_signed(origin)?; - - let controller = T::Control::body_controller(&org); - - ensure!(creator == controller, Error::::AuthorizationError); - - // Get Treasury account for deposits and fees - - let treasury = T::Control::body_treasury(&org); - - let free_balance = T::Currency::free_balance(T::FundingCurrencyId::get(), &treasury); - ensure!(free_balance > deposit, Error::::TreasuryBalanceTooLow); - ensure!(deposit <= target, Error::::DepositTooHigh); - - // check name length boundary - ensure!( - (name.len() as u32) >= T::MinLength::get(), - Error::::NameTooShort - ); - ensure!( - (name.len() as u32) <= T::MaxLength::get(), - Error::::NameTooLong - ); - - let now = >::block_number(); - let timestamp = T::UnixTime::now().as_secs(); - - // ensure campaign expires after now - ensure!(expiry > now, Error::::EndTooEarly); - - let max_length = T::MaxDuration::get(); - let max_end_block = now + max_length; - ensure!(expiry <= max_end_block, Error::::EndTooLate); - - // generate the unique campaign id + ensure uniqueness - let phrase = b"crowdfunding_campaign"; // create from name? - let id = T::Randomness::random(phrase).0; - // ensure!(!>::exists(&id), Error::::IdExists ); // check for collision - - // check contribution limit per block - let contributions = Self::campaigns_by_block(expiry); - ensure!( - (contributions.len() as u32) < T::MaxCampaignsPerBlock::get(), - Error::::ContributionsPerBlockExceeded - ); - - // - // - // - - let new_campaign = Campaign { - id: id.clone(), - org: org.clone(), - name: name.clone(), - owner: creator.clone(), - admin: admin.clone(), - deposit: deposit.clone(), - expiry: expiry.clone(), - cap: target.clone(), - protocol: protocol.clone(), - governance: governance.clone(), - cid: cid.clone(), - token_symbol: token_symbol.clone(), - token_name: token_name.clone(), - created: timestamp, - }; - - // mint the campaign - Self::mint(new_campaign)?; - - // 0 init, 1 active, 2 paused, 3 complete success, 4 complete failed, 5 authority lock - Self::set_state(id.clone(), FlowState::Active); - - // deposit the event - Self::deposit_event(Event::CampaignCreated( - id, creator, admin, target, deposit, expiry, name, - )); - Ok(()) - - // 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::campaign_account_id(index), imb); - } - - #[pallet::weight(1_000_000)] - pub fn update_state( - origin: OriginFor, - campaign_id: T::Hash, - state: FlowState, - ) -> DispatchResult { - // access control - let sender = ensure_signed(origin)?; - - let owner = Self::campaign_owner(campaign_id).ok_or(Error::::OwnerUnknown)?; - let admin = Self::campaign_admin(campaign_id).ok_or(Error::::AdminUnknown)?; - ensure!(sender == admin, Error::::AuthorizationError); - - // expired? - let campaign = Self::campaign_by_id(&campaign_id); - let now = >::block_number(); - ensure!(now < campaign.expiry, Error::::CampaignExpired); - - // not finished or locked? - let current_state = Self::campaign_state(campaign_id); - ensure!( - current_state < FlowState::Success, - Error::::CampaignExpired - ); - - // set - Self::set_state(campaign_id.clone(), state.clone()); - - // dispatch status update event - Self::deposit_event(Event::CampaignUpdated(campaign_id, state, now)); - - Ok(()) - } - - /// contribute to project - #[pallet::weight(5_000_000)] - pub fn contribute( - origin: OriginFor, - campaign_id: T::Hash, - contribution: Balance, - ) -> DispatchResult { - // check - - let sender = ensure_signed(origin)?; - ensure!( - T::Currency::free_balance(T::FundingCurrencyId::get(), &sender) >= contribution, - Error::::BalanceTooLow - ); - let owner = Self::campaign_owner(campaign_id).ok_or(Error::::OwnerUnknown)?; - ensure!(owner != sender, Error::::NoContributionToOwnCampaign); - - ensure!( - Campaigns::::contains_key(campaign_id), - Error::::InvalidId - ); - let state = Self::campaign_state(campaign_id); - ensure!( - state == FlowState::Active, - Error::::NoContributionsAllowed - ); - let campaign = Self::campaign_by_id(&campaign_id); - ensure!( - >::block_number() < campaign.expiry, - Error::::CampaignExpired - ); - - // write - - Self::create_contribution(sender.clone(), campaign_id.clone(), contribution.clone())?; - - // event - - let now = >::block_number(); - Self::deposit_event(Event::CampaignContributed( - campaign_id, - sender, - contribution, - now, - )); - - Ok(()) - } - } + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + type WeightInfo: frame_system::weights::WeightInfo; + type Event: From> + + IsType<::Event> + + Into<::Event>; + + type Currency: MultiCurrency + + MultiReservableCurrency; + type UnixTime: UnixTime; + type Randomness: Randomness; + type Control: ControlPalletStorage; + + /// The origin that is allowed to make judgements. + type GameDAOAdminOrigin: EnsureOrigin; + type GameDAOTreasury: Get; + + #[pallet::constant] + type MinLength: Get; + #[pallet::constant] + type MaxLength: Get; + + #[pallet::constant] + type MaxCampaignsPerAddress: Get; + #[pallet::constant] + type MaxCampaignsPerBlock: Get; + #[pallet::constant] + type MaxContributionsPerBlock: Get; + + #[pallet::constant] + type MinDuration: Get; + #[pallet::constant] + type MaxDuration: Get; + #[pallet::constant] + type MinCreatorDeposit: Get; + #[pallet::constant] + type MinContribution: Get; + + #[pallet::constant] + type FundingCurrencyId: Get; + + // TODO: collect fees for treasury + #[pallet::constant] + type CampaignFee: Get; + } + + /// Campaign + #[pallet::storage] + #[pallet::getter(fn campaign_by_id)] + pub(super) type Campaigns = StorageMap< + _, + Blake2_128Concat, + T::Hash, + Campaign< + T::Hash, + T::AccountId, + Balance, + T::BlockNumber, + Moment, + FlowProtocol, + FlowGovernance, + >, + ValueQuery, + >; + + /// Associated Body + #[pallet::storage] + #[pallet::getter(fn campaign_org)] + pub(super) type CampaignOrg = + StorageMap<_, Blake2_128Concat, T::Hash, T::Hash, ValueQuery>; + + /// Get Campaign Owner (body controller) by campaign id + #[pallet::storage] + #[pallet::getter(fn campaign_owner)] + pub(super) type CampaignOwner = + StorageMap<_, Blake2_128Concat, T::Hash, T::AccountId, OptionQuery>; + + /// Get Campaign Admin (supervision) by campaign id + #[pallet::storage] + #[pallet::getter(fn campaign_admin)] + pub(super) type CampaignAdmin = + StorageMap<_, Blake2_128Concat, T::Hash, T::AccountId, OptionQuery>; + + /// Campaign state + /// 0 init, 1 active, 2 paused, 3 complete success, 4 complete failed, 5 authority lock + #[pallet::storage] + #[pallet::getter(fn campaign_state)] + pub(super) type CampaignState = + StorageMap<_, Blake2_128Concat, T::Hash, FlowState, ValueQuery, GetDefault>; + + /// Get Campaigns for a certain state + #[pallet::storage] + #[pallet::getter(fn campaigns_by_state)] + pub(super) type CampaignsByState = + StorageMap<_, Blake2_128Concat, FlowState, Vec, ValueQuery>; + + /// Campaigns ending in block x + #[pallet::storage] + #[pallet::getter(fn campaigns_by_block)] + pub(super) type CampaignsByBlock = + StorageMap<_, Blake2_128Concat, T::BlockNumber, Vec, ValueQuery>; + + /// Total number of campaigns -> all campaigns + #[pallet::storage] + #[pallet::getter(fn campaigns_index)] + pub(super) type CampaignsArray = + StorageMap<_, Blake2_128Concat, u64, T::Hash, ValueQuery>; + #[pallet::storage] + #[pallet::getter(fn campaigns_count)] + pub type CampaignsCount = StorageValue<_, u64, ValueQuery>; + #[pallet::storage] + pub(super) type CampaignsIndex = + StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery>; + + // caller owned campaigns -> my campaigns + #[pallet::storage] + #[pallet::getter(fn campaigns_owned_index)] + pub(super) type CampaignsOwnedArray = + StorageMap<_, Blake2_128Concat, T::Hash, T::Hash, ValueQuery>; + #[pallet::storage] + #[pallet::getter(fn campaigns_owned_count)] + pub(super) type CampaignsOwnedCount = + StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery>; + #[pallet::storage] + pub(super) type CampaignsOwnedIndex = + StorageMap<_, Blake2_128Concat, (T::Hash, T::Hash), u64, ValueQuery>; + + /// campaigns contributed by accountid + #[pallet::storage] + #[pallet::getter(fn campaigns_contributed)] + pub(super) type CampaignsContributed = + StorageMap<_, Blake2_128Concat, T::AccountId, Vec, ValueQuery>; + + /// campaigns related to an organisation + #[pallet::storage] + #[pallet::getter(fn campaigns_by_body)] + pub(super) type CampaignsByBody = + StorageMap<_, Blake2_128Concat, T::Hash, Vec, ValueQuery>; + + // caller contributed campaigns -> contributed campaigns + #[pallet::storage] + #[pallet::getter(fn campaigns_contributed_index)] + pub(super) type CampaignsContributedArray = + StorageMap<_, Blake2_128Concat, (T::AccountId, u64), T::Hash, ValueQuery>; + #[pallet::storage] + #[pallet::getter(fn campaigns_contributed_count)] + pub(super) type CampaignsContributedCount = + StorageMap<_, Blake2_128Concat, T::AccountId, u64, ValueQuery>; + #[pallet::storage] + pub(super) type CampaignsContributedIndex = + StorageMap<_, Blake2_128Concat, (T::AccountId, T::Hash), u64, ValueQuery>; + + // Total contributions balance per campaign + #[pallet::storage] + #[pallet::getter(fn campaign_balance)] + pub(super) type CampaignBalance = + StorageMap<_, Blake2_128Concat, T::Hash, Balance, ValueQuery>; + + // Contributions per user + #[pallet::storage] + #[pallet::getter(fn campaign_contribution)] + pub(super) type CampaignContribution = + StorageMap<_, Blake2_128Concat, (T::Hash, T::AccountId), Balance, ValueQuery>; + + // Contributors + #[pallet::storage] + #[pallet::getter(fn campaign_contributors)] + pub(super) type CampaignContributors = + StorageMap<_, Blake2_128Concat, T::Hash, Vec, ValueQuery>; + #[pallet::storage] + #[pallet::getter(fn campaign_contributors_count)] + pub(super) type CampaignContributorsCount = + StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery>; + + // Max campaign block limit + // CampaignMaxDuration get(fn get_max_duration) config(): T::BlockNumber = T::BlockNumber::from(T::MaxDuration::get()); + + // Campaign nonce, increases per created campaign + #[pallet::storage] + #[pallet::getter(fn nonce)] + pub type Nonce = StorageValue<_, u128, ValueQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + CampaignDestroyed{ + campaign_id: T::Hash + }, + CampaignCreated{ + campaign_id: T::Hash, + creator: T::AccountId, + admin: T::AccountId, + target: Balance, + deposit: Balance, + expiry: T::BlockNumber, + name: Vec, + }, + CampaignContributed{ + campaign_id: T::Hash, + sender: T::AccountId, + contribution: Balance, + block_number: T::BlockNumber + }, + CampaignFinalized{ + campaign_id: T::Hash, + campaign_balance: Balance, + block_number: T::BlockNumber, + success: bool + }, + CampaignFailed{ + campaign_id: T::Hash, + campaign_balance: Balance, + block_number: T::BlockNumber, + success: bool + }, + CampaignUpdated{ + campaign_id: T::Hash, + state: FlowState, + block_number: T::BlockNumber + }, + Message(Vec), + } + + #[pallet::error] + pub enum Error { + // + // general + // + /// Must contribute at least the minimum amount of Campaigns + ContributionTooSmall, + /// Balance too low. + BalanceTooLow, + /// Treasury Balance Too Low + TreasuryBalanceTooLow, + /// The Campaign id specified does not exist + InvalidId, + /// The Campaign's contribution period has ended; no more contributions will be accepted + ContributionPeriodOver, + /// You may not withdraw or dispense Campaigns while the Campaign is still active + CampaignStillActive, + /// You cannot withdraw Campaigns because you have not contributed any + NoContribution, + /// You cannot dissolve a Campaign that has not yet completed its retirement period + CampaignNotRetired, + /// Campaign expired + CampaignExpired, + /// Cannot dispense Campaigns from an unsuccessful Campaign + UnsuccessfulCampaign, + + // + // create + // + /// Campaign must end after it starts + EndTooEarly, + /// Campaign expiry has be lower than the block number limit + EndTooLate, + /// Max contributions per block exceeded + ContributionsPerBlockExceeded, + /// Name too long + NameTooLong, + /// Name too short + NameTooShort, + /// Deposit exceeds the campaign target + DepositTooHigh, + /// Campaign id exists + IdExists, + + // + // mint + // + /// Overflow adding a new campaign to total fundings + AddCampaignOverflow, + /// Overflow adding a new owner + AddOwnedOverflow, + /// Overflow adding to the total number of contributors of a camapaign + UpdateContributorOverflow, + /// Overflow adding to the total number of contributions of a camapaign + AddContributionOverflow, + /// Campaign owner unknown + OwnerUnknown, + /// Campaign admin unknown + AdminUnknown, + /// Cannot contribute to owned campaign + NoContributionToOwnCampaign, + /// Guru Meditation + GuruMeditation, + /// Zou are not authorized for this call + AuthorizationError, + /// Contributions not allowed + NoContributionsAllowed, + /// Id Unknown + IdUnknown, + /// Transfer Error + TransferError, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + /// Block finalization + fn on_finalize(_n: BlockNumberFor) { + // get all the campaigns ending in current block + let block_number = >::block_number(); + // which campaigns end in this block + let campaign_hashes = Self::campaigns_by_block(block_number); + + // iterate over campaigns ending in this block + for campaign_id in &campaign_hashes { + // get campaign struct + let campaign = Self::campaign_by_id(campaign_id); + let campaign_balance = Self::campaign_balance(campaign_id); + let dao = Self::campaign_org(&campaign_id); + let dao_treasury = T::Control::body_treasury(&dao); + + // check for cap reached + if campaign_balance >= campaign.cap { + // get campaign owner + // should be controller --- test? + let _owner = Self::campaign_owner(campaign_id); + + match _owner { + Some(owner) => { + // get all contributors + let contributors = Self::campaign_contributors(campaign_id); + let mut transaction_complete = true; + + // 1 iterate over contributors + // 2 unreserve contribution + // 3 transfer contribution to campaign treasury + 'inner: for contributor in &contributors { + // if contributor == campaign owner, skip + if contributor == &owner { + continue; + } + + // get amount from contributor + let contributor_balance = Self::campaign_contribution(( + *campaign_id, + contributor.clone(), + )); + + // unreserve the amount in contributor balance + T::Currency::unreserve( + T::FundingCurrencyId::get(), + &contributor, + contributor_balance.clone(), + ); + + // transfer from contributor + let transfer_amount = T::Currency::transfer( + T::FundingCurrencyId::get(), + &contributor, + &dao_treasury, + contributor_balance.clone(), + ); + + // success? + match transfer_amount { + Err(_e) => { + transaction_complete = false; + break 'inner; + } + Ok(_v) => {} + } + } + + // If all transactions are settled + // 1. reserve campaign balance + // 2. unreserve and send the commission to operator treasury + if transaction_complete { + // reserve campaign volume + let _reserve_campaign_amount = T::Currency::reserve( + T::FundingCurrencyId::get(), + &dao_treasury, + campaign_balance.clone(), + ); + + let fee = T::CampaignFee::get(); + let commission = fee.mul_floor(campaign_balance.clone()); + T::Currency::unreserve(T::FundingCurrencyId::get(), &dao_treasury, commission.clone()); + + let _transfer_commission = T::Currency::transfer( + T::FundingCurrencyId::get(), + &dao_treasury, + &T::GameDAOTreasury::get(), + commission, + ); + + // TODO: TransferError? + // match transfer_commission { + // Err(_e) => { }, //(Error::::TransferError) + // Ok(_v) => {} + // } + + Self::set_state(campaign.id.clone(), FlowState::Success); + + // finalized event + Self::deposit_event(Event::CampaignFinalized{ + campaign_id: *campaign_id, + campaign_balance, + block_number, + success: true, + }); + } + } + None => continue, + } + + // campaign cap not reached + } else { + // campaign failed, revert all contributions + + let contributors = Self::campaign_contributors(campaign_id); + for account in contributors { + let contribution = + Self::campaign_contribution((*campaign_id, account.clone())); + T::Currency::unreserve(T::FundingCurrencyId::get(), &account, contribution); + } + + // update campaign state to failed + Self::set_state(campaign.id, FlowState::Failed); + + // unreserve DEPOSIT + + T::Currency::unreserve( + T::FundingCurrencyId::get(), + &dao_treasury, + campaign.deposit, + ); + + // failed event + Self::deposit_event(Event::CampaignFailed{ + campaign_id: *campaign_id, + campaign_balance, + block_number, + success: false, + }); + } + } + } + } + + #[pallet::call] + impl Pallet { + #[pallet::weight(5_000_000)] + // Reason for using transactional is get_and_increment_nonce + #[transactional] + pub fn create( + origin: OriginFor, + org: T::Hash, + admin: T::AccountId, // supervision, should be dao provided! + name: Vec, + target: Balance, + deposit: Balance, + expiry: T::BlockNumber, + protocol: FlowProtocol, + governance: FlowGovernance, + cid: Vec, // content cid + token_symbol: Vec, // up to 5 + token_name: Vec, // cleartext + // token_curve_a: u8, // preset + // token_curve_b: Vec, // custom + ) -> DispatchResult { + let creator = ensure_signed(origin)?; + + let controller = T::Control::body_controller(&org); + + ensure!(creator == controller, Error::::AuthorizationError); + + // Get Treasury account for deposits and fees + + let treasury = T::Control::body_treasury(&org); + + let free_balance = T::Currency::free_balance(T::FundingCurrencyId::get(), &treasury); + ensure!(free_balance > deposit, Error::::TreasuryBalanceTooLow); + ensure!(deposit <= target, Error::::DepositTooHigh); + + // check name length boundary + ensure!( + (name.len() as u32) >= T::MinLength::get(), + Error::::NameTooShort + ); + ensure!( + (name.len() as u32) <= T::MaxLength::get(), + Error::::NameTooLong + ); + + let now = >::block_number(); + let timestamp = T::UnixTime::now().as_secs(); + + // ensure campaign expires after the current block + ensure!(expiry > now, Error::::EndTooEarly); + + let max_length = T::MaxDuration::get(); + let max_end_block = now + max_length; + ensure!(expiry <= max_end_block, Error::::EndTooLate); + + // generate the unique campaign id + ensure uniqueness + let nonce = Self::get_and_increment_nonce(); + let (id, _) = T::Randomness::random(&nonce); + // ensure!(!>::exists(&id), Error::::IdExists ); // check for collision + + // check contribution limit per block + let contributions = Self::campaigns_by_block(expiry); + ensure!( + (contributions.len() as u32) < T::MaxCampaignsPerBlock::get(), + Error::::ContributionsPerBlockExceeded + ); + + let new_campaign = Campaign { + id: id.clone(), + org: org.clone(), + name: name.clone(), + owner: creator.clone(), + admin: admin.clone(), + deposit: deposit.clone(), + expiry: expiry.clone(), + cap: target.clone(), + protocol: protocol.clone(), + governance: governance.clone(), + cid: cid.clone(), + token_symbol: token_symbol.clone(), + token_name: token_name.clone(), + created: timestamp, + }; + + // mint the campaign + Self::mint(new_campaign)?; + + // 0 init, 1 active, 2 paused, 3 complete success, 4 complete failed, 5 authority lock + Self::set_state(id.clone(), FlowState::Active); + + // deposit the event + Self::deposit_event(Event::CampaignCreated{ + campaign_id: id, creator, admin, target, deposit, expiry, name, + }); + Ok(()) + + // 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::campaign_account_id(index), imb); + } + + #[pallet::weight(1_000_000)] + pub fn update_state( + origin: OriginFor, + campaign_id: T::Hash, + state: FlowState, + ) -> DispatchResult { + // access control + let sender = ensure_signed(origin)?; + + Self::campaign_owner(campaign_id).ok_or(Error::::OwnerUnknown)?; + let admin = Self::campaign_admin(campaign_id).ok_or(Error::::AdminUnknown)?; + ensure!(sender == admin, Error::::AuthorizationError); + + // expired? + let campaign = Self::campaign_by_id(&campaign_id); + let now = >::block_number(); + ensure!(now < campaign.expiry, Error::::CampaignExpired); + + // not finished or locked? + let current_state = Self::campaign_state(campaign_id); + ensure!( + current_state < FlowState::Success, + Error::::CampaignExpired + ); + + Self::set_state(campaign_id.clone(), state.clone()); + + // dispatch status update event + Self::deposit_event(Event::CampaignUpdated{campaign_id, state, block_number: now}); + + Ok(()) + } + + /// contribute to project + #[pallet::weight(5_000_000)] + pub fn contribute( + origin: OriginFor, + campaign_id: T::Hash, + contribution: Balance, + ) -> DispatchResult { + // check + + let sender = ensure_signed(origin)?; + ensure!( + T::Currency::free_balance(T::FundingCurrencyId::get(), &sender) >= contribution, + Error::::BalanceTooLow + ); + let owner = Self::campaign_owner(campaign_id).ok_or(Error::::OwnerUnknown)?; + ensure!(owner != sender, Error::::NoContributionToOwnCampaign); + + ensure!( + Campaigns::::contains_key(campaign_id), + Error::::InvalidId + ); + let state = Self::campaign_state(campaign_id); + ensure!( + state == FlowState::Active, + Error::::NoContributionsAllowed + ); + let campaign = Self::campaign_by_id(&campaign_id); + ensure!( + >::block_number() < campaign.expiry, + Error::::CampaignExpired + ); + + // write + + Self::create_contribution(sender.clone(), campaign_id.clone(), contribution.clone())?; + + // event + + let now = >::block_number(); + Self::deposit_event(Event::CampaignContributed{ + campaign_id, + sender, + contribution, + block_number: now, + }); + + Ok(()) + } + } } impl Pallet { - fn set_state(id: T::Hash, state: FlowState) { - let current_state = Self::campaign_state(&id); - - // remove - let mut current_state_members = Self::campaigns_by_state(¤t_state); - match current_state_members.binary_search(&id) { - Ok(index) => { - current_state_members.remove(index); - CampaignsByState::::insert(¤t_state, current_state_members); - } - Err(_) => (), //(Error::::IdUnknown) - } - - // add - CampaignsByState::::mutate(&state, |campaigns| campaigns.push(id.clone())); - CampaignState::::insert(id, state); - } - - // campaign creator - // sender: T::AccountId, - // generated campaign id - // campaign_id: T::Hash, - // expiration blocktime - // example: desired lifetime == 30 days - // 30 days * 24h * 60m / 5s avg blocktime == - // 2592000s / 5s == 518400 blocks from now. - // expiry: T::BlockNumber, - // campaign creator deposit to invoke the campaign - // deposit: Balance, - // funding protocol - // 0 grant, 1 prepaid, 2 loan, 3 shares, 4 dao, 5 pool - // proper assignment of funds into the instrument - // happens after successful funding of the campaing - // protocol: u8, - // campaign object - pub fn mint( - campaign: Campaign< - T::Hash, - T::AccountId, - Balance, - T::BlockNumber, - Moment, - FlowProtocol, - FlowGovernance, - >, - ) -> DispatchResult { - // add campaign to campaigns - Campaigns::::insert(&campaign.id, campaign.clone()); - // add org to index - CampaignOrg::::insert(&campaign.id, campaign.org.clone()); - // Owner == DAO - CampaignOwner::::insert(&campaign.id, campaign.owner.clone()); - // TODO: Admin == Council - CampaignAdmin::::insert(&campaign.id, campaign.admin.clone()); - // add to campaigns by body - CampaignsByBody::::mutate(&campaign.org, |campaigns| campaigns.push(campaign.id)); - - // expiration - CampaignsByBlock::::mutate(&campaign.expiry, |campaigns| { - campaigns.push(campaign.id.clone()) - }); - - // global campaigns count - let campaigns_count = Self::campaigns_count(); - let update_campaigns_count = campaigns_count - .checked_add(1) - .ok_or(Error::::AddCampaignOverflow)?; - - // update global campaign count - CampaignsArray::::insert(&campaigns_count, campaign.id.clone()); - CampaignsCount::::put(update_campaigns_count); - CampaignsIndex::::insert(campaign.id.clone(), campaigns_count); - - // campaigns owned needs a refactor: - // CampaignsCreated( dao => map ) - // owned campaigns count - let campaigns_owned_count = Self::campaigns_owned_count(&campaign.org); - let update_campaigns_owned_count = campaigns_owned_count - .checked_add(1) - .ok_or(Error::::AddOwnedOverflow)?; - - // update owned campaigns for dao - CampaignsOwnedArray::::insert(&campaign.org, campaign.id.clone()); - CampaignsOwnedCount::::insert(&campaign.org, update_campaigns_count); - CampaignsOwnedIndex::::insert((&campaign.org, &campaign.id), campaigns_owned_count); - - // TODO: this should be a proper mechanism - // to reserve some of the staked GAME - let treasury = T::Control::body_treasury(&campaign.org); - - // let fundingCurrency = T::FundingCurrencyId::get(); - T::Currency::reserve( - T::FundingCurrencyId::get(), - &treasury, - campaign.deposit.clone(), - )?; - // let _ = >::reserve( - // &treasury, - // campaign.deposit.clone() - // ); - - // nonce ++ - Nonce::::mutate(|n| *n += 1); - - Ok(()) - } - - fn create_contribution( - sender: T::AccountId, - campaign_id: T::Hash, - contribution: Balance, - ) -> DispatchResult { - let campaign = Self::campaign_by_id(&campaign_id); - let returning_contributor = - CampaignContribution::::contains_key((&campaign_id, &sender)); - - // check if contributor exists - // if not, update metadata - if !returning_contributor { - // increase the number of contributors - let campaigns_contributed = Self::campaigns_contributed_count(&sender); - CampaignsContributedArray::::insert( - (sender.clone(), campaigns_contributed), - campaign_id, - ); - CampaignsContributedIndex::::insert( - (sender.clone(), campaign_id.clone()), - campaigns_contributed, - ); - - let update_campaigns_contributed = campaigns_contributed - .checked_add(1) - .ok_or(Error::::AddContributionOverflow)?; - CampaignsContributedCount::::insert(&sender, update_campaigns_contributed); - - // increase the number of contributors of the campaign - let contributors = CampaignContributorsCount::::get(&campaign_id); - let update_contributors = contributors - .checked_add(1) - .ok_or(Error::::UpdateContributorOverflow)?; - CampaignContributorsCount::::insert(campaign_id.clone(), update_contributors); - - // add contibutor to campaign contributors - CampaignContributors::::mutate(&campaign_id, |accounts| { - accounts.push(sender.clone()) - }); - } - - // check if campaign is in contributions map of contributor and add - let mut campaigns_contributed = Self::campaigns_contributed(&sender); - if !campaigns_contributed.contains(&campaign_id) { - campaigns_contributed.push(campaign_id.clone()); - CampaignsContributed::::insert(&sender, campaigns_contributed); - } - - // reserve contributed amount - T::Currency::reserve(T::FundingCurrencyId::get(), &sender, contribution)?; - - // update contributor balance for campaign - let total_contribution = Self::campaign_contribution((&campaign_id, &sender)); - let update_total_contribution = total_contribution + contribution; - CampaignContribution::::insert((&campaign_id, &sender), update_total_contribution); - - // update campaign balance - let total_campaign_balance = Self::campaign_balance(&campaign_id); - let update_campaign_balance = total_campaign_balance + contribution; - CampaignBalance::::insert(&campaign_id, update_campaign_balance); - - Ok(()) - } + fn set_state(id: T::Hash, state: FlowState) { + let current_state = Self::campaign_state(&id); + + // remove + let mut current_state_members = Self::campaigns_by_state(¤t_state); + match current_state_members.binary_search(&id) { + Ok(index) => { + current_state_members.remove(index); + CampaignsByState::::insert(¤t_state, current_state_members); + } + Err(_) => (), //(Error::::IdUnknown) + } + + // add + CampaignsByState::::mutate(&state, |campaigns| campaigns.push(id.clone())); + CampaignState::::insert(id, state); + } + + // campaign creator + // sender: T::AccountId, + // generated campaign id + // campaign_id: T::Hash, + // expiration blocktime + // example: desired lifetime == 30 days + // 30 days * 24h * 60m / 5s avg blocktime == + // 2592000s / 5s == 518400 blocks from now. + // expiry: T::BlockNumber, + // campaign creator deposit to invoke the campaign + // deposit: Balance, + // funding protocol + // 0 grant, 1 prepaid, 2 loan, 3 shares, 4 dao, 5 pool + // proper assignment of funds into the instrument + // happens after successful funding of the campaing + // protocol: u8, + // campaign object + pub fn mint( + campaign: Campaign< + T::Hash, + T::AccountId, + Balance, + T::BlockNumber, + Moment, + FlowProtocol, + FlowGovernance, + >, + ) -> DispatchResult { + // add campaign to campaigns + Campaigns::::insert(&campaign.id, campaign.clone()); + // add org to index + CampaignOrg::::insert(&campaign.id, campaign.org.clone()); + // Owner == DAO + CampaignOwner::::insert(&campaign.id, campaign.owner.clone()); + // TODO: Admin == Council + CampaignAdmin::::insert(&campaign.id, campaign.admin.clone()); + // add to campaigns by body + CampaignsByBody::::mutate(&campaign.org, |campaigns| campaigns.push(campaign.id)); + + // expiration + CampaignsByBlock::::mutate(&campaign.expiry, |campaigns| { + campaigns.push(campaign.id.clone()) + }); + + // global campaigns count + let campaigns_count = Self::campaigns_count(); + let update_campaigns_count = campaigns_count + .checked_add(1) + .ok_or(Error::::AddCampaignOverflow)?; + + // update global campaign count + CampaignsArray::::insert(&campaigns_count, campaign.id.clone()); + CampaignsCount::::put(update_campaigns_count); + CampaignsIndex::::insert(campaign.id.clone(), campaigns_count); + + // campaigns owned needs a refactor: + // CampaignsCreated( dao => map ) + // owned campaigns count + let campaigns_owned_count = Self::campaigns_owned_count(&campaign.org); + campaigns_owned_count + .checked_add(1) + .ok_or(Error::::AddOwnedOverflow)?; + + // update owned campaigns for dao + CampaignsOwnedArray::::insert(&campaign.org, campaign.id.clone()); + CampaignsOwnedCount::::insert(&campaign.org, update_campaigns_count); + CampaignsOwnedIndex::::insert((&campaign.org, &campaign.id), campaigns_owned_count); + + // TODO: this should be a proper mechanism + // to reserve some of the staked GAME + let treasury = T::Control::body_treasury(&campaign.org); + + T::Currency::reserve( + T::FundingCurrencyId::get(), + &treasury, + campaign.deposit.clone(), + )?; + + Ok(()) + } + + fn create_contribution( + sender: T::AccountId, + campaign_id: T::Hash, + contribution: Balance, + ) -> DispatchResult { + let returning_contributor = + CampaignContribution::::contains_key((&campaign_id, &sender)); + + // check if contributor exists + // if not, update metadata + if !returning_contributor { + // increase the number of contributors + let campaigns_contributed = Self::campaigns_contributed_count(&sender); + CampaignsContributedArray::::insert( + (sender.clone(), campaigns_contributed), + campaign_id, + ); + CampaignsContributedIndex::::insert( + (sender.clone(), campaign_id.clone()), + campaigns_contributed, + ); + + let update_campaigns_contributed = campaigns_contributed + .checked_add(1) + .ok_or(Error::::AddContributionOverflow)?; + CampaignsContributedCount::::insert(&sender, update_campaigns_contributed); + + // increase the number of contributors of the campaign + let contributors = CampaignContributorsCount::::get(&campaign_id); + let update_contributors = contributors + .checked_add(1) + .ok_or(Error::::UpdateContributorOverflow)?; + CampaignContributorsCount::::insert(campaign_id.clone(), update_contributors); + + // add contibutor to campaign contributors + CampaignContributors::::mutate(&campaign_id, |accounts| { + accounts.push(sender.clone()) + }); + } + + // check if campaign is in contributions map of contributor and add + let mut campaigns_contributed = Self::campaigns_contributed(&sender); + if !campaigns_contributed.contains(&campaign_id) { + campaigns_contributed.push(campaign_id.clone()); + CampaignsContributed::::insert(&sender, campaigns_contributed); + } + + // reserve contributed amount + T::Currency::reserve(T::FundingCurrencyId::get(), &sender, contribution)?; + + // update contributor balance for campaign + let total_contribution = Self::campaign_contribution((&campaign_id, &sender)); + let update_total_contribution = total_contribution + contribution; + CampaignContribution::::insert((&campaign_id, &sender), update_total_contribution); + + // update campaign balance + let total_campaign_balance = Self::campaign_balance(&campaign_id); + let update_campaign_balance = total_campaign_balance + contribution; + CampaignBalance::::insert(&campaign_id, update_campaign_balance); + + Ok(()) + } + + fn get_and_increment_nonce() -> Vec { + let nonce = Nonce::::get(); + Nonce::::put(nonce.wrapping_add(1)); + nonce.encode() + } } diff --git a/flow/src/mock.rs b/flow/src/mock.rs new file mode 100644 index 000000000..9a7960690 --- /dev/null +++ b/flow/src/mock.rs @@ -0,0 +1,238 @@ +#![cfg(test)] + +pub use super::*; +use frame_support::{ + construct_runtime, parameter_types, + traits::{Everything, Nothing, GenesisBuild} +}; +use frame_support_test::TestRandomness; +use frame_system::EnsureRoot; +use sp_core::H256; +use sp_runtime::{traits::{IdentityLookup},Permill}; + +use orml_traits::parameter_type_with_key; +use gamedao_protocol_support::{ControlPalletStorage, ControlMemberState, ControlState}; +use zero_primitives::{Amount, CurrencyId, TokenSymbol, Header}; + +pub type AccountId = u32; +pub type BlockNumber = u32; +pub type Hash = H256; +pub type Timestamp = u64; + + +// TODO: move it to constants------- +pub const MILLICENTS: Balance = 1_000_000_000; +pub const CENTS: Balance = 1_000 * MILLICENTS; +pub const DOLLARS: Balance = 100 * CENTS; + +pub const MILLISECS_PER_BLOCK: u64 = 6000; + +pub const MINUTES: BlockNumber = 60_000 / (MILLISECS_PER_BLOCK as BlockNumber); +pub const HOURS: BlockNumber = MINUTES * 60; +pub const DAYS: BlockNumber = HOURS * 24; +// --------------------------------- + +pub const ALICE: AccountId = 1; +pub const BOB: AccountId = 2; +pub const BOGDANA: AccountId = 3; +pub const TREASURY: AccountId = 4; +pub const GAMEDAO_TREASURY: AccountId = 5; + +pub const MAX_DURATION: BlockNumber = DAYS * 100; +pub const GAME_CURRENCY_ID: CurrencyId = TokenSymbol::GAME as u32; + + +mod pallet_flow { + pub use super::super::*; +} + +parameter_types! { + pub const BlockHashCount: u32 = 250; +} + +impl frame_system::Config for Test { + type Origin = Origin; + type Index = u64; + type BlockNumber = BlockNumber; + type Call = Call; + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type BlockWeights = (); + type BlockLength = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type DbWeight = (); + type BaseCallFilter = Everything; + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); +} + +parameter_type_with_key! { + pub ExistentialDeposits: |_currency_id: CurrencyId| -> Balance { + Default::default() + }; +} + +impl orml_tokens::Config for Test { + type Event = Event; + type Balance = Balance; + type Amount = Amount; + type CurrencyId = CurrencyId; + type WeightInfo = (); + type ExistentialDeposits = ExistentialDeposits; + type OnDust = (); + type MaxLocks = (); + type DustRemovalWhitelist = Nothing; +} + +parameter_types! { + pub const ExistentialDeposit: Balance = 1; +} + +impl pallet_balances::Config for Test { + type Balance = Balance; + type DustRemoval = (); + type Event = Event; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = frame_system::Pallet; + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); +} +pub type AdaptedBasicCurrency = orml_currencies::BasicCurrencyAdapter; + +impl orml_currencies::Config for Test { + type Event = Event; + type MultiCurrency = Tokens; + type NativeCurrency = AdaptedBasicCurrency; + type GetNativeCurrencyId = (); + type WeightInfo = (); +} + +parameter_types! { + pub const MinimumPeriod: Moment = 1000; +} + +impl pallet_timestamp::Config for Test { + type Moment = Moment; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +pub struct ControlPalletMock; + +impl ControlPalletStorage for ControlPalletMock { + fn body_controller(_org: &Hash) -> AccountId { BOB } + fn body_treasury(_org: &Hash) -> AccountId { TREASURY } + fn body_state(_hash: &Hash) -> ControlState { ControlState::Active } + fn body_member_state(_hash: &Hash, _account_id: &AccountId) -> ControlMemberState { ControlMemberState::Active } +} + +parameter_types! { + pub const MinLength: u32 = 2; + pub const MaxLength: u32 = 4; + pub const MaxCampaignsPerAddress: u32 = 3; + pub const MaxCampaignsPerBlock: u32 = 1; + pub const MaxContributionsPerBlock: u32 = 3; + pub const MinDuration: BlockNumber = 1 * DAYS; + pub const MaxDuration: BlockNumber = MAX_DURATION; + pub const MinCreatorDeposit: Balance = 1 * DOLLARS; + pub const MinContribution: Balance = 1 * DOLLARS; + pub CampaignFee: Permill = Permill::from_rational(1u32, 10u32); // 10% + // pub const CampaignFee: Balance = 25 * CENTS; + pub const GAMECurrencyId: CurrencyId = GAME_CURRENCY_ID; + pub const GameDAOTreasury: AccountId = GAMEDAO_TREASURY; +} + +impl Config for Test { + type WeightInfo = (); + type Event = Event; + type Currency = Currencies; + type FundingCurrencyId = GAMECurrencyId; + type UnixTime = PalletTimestamp; + type Randomness = TestRandomness; + + type Control = ControlPalletMock; + + // TODO: type GameDAOAdminOrigin = EnsureRootOrHalfCouncil + type GameDAOAdminOrigin = EnsureRoot; + type GameDAOTreasury = GameDAOTreasury; + + type MinLength = MinLength; + type MaxLength = MaxLength; + + type MaxCampaignsPerAddress = MaxCampaignsPerAddress; + type MaxCampaignsPerBlock = MaxCampaignsPerBlock; + type MaxContributionsPerBlock = MaxContributionsPerBlock; + + type MinDuration = MinDuration; + type MaxDuration = MaxDuration; + type MinCreatorDeposit = MinCreatorDeposit; + type MinContribution = MinContribution; + + type CampaignFee = CampaignFee; +} + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +impl Campaign { + pub fn new(campaign_id: Hash, expiry: BlockNumber) -> Campaign { + Campaign { + id: campaign_id, + org: H256::random(), + name: vec![1, 2], + owner: BOB, + admin: BOB, + deposit: 10, + expiry: expiry, + cap: 110, + protocol: FlowProtocol::Raise, + governance: FlowGovernance::No, + cid: vec![1, 2], + token_symbol: vec![1, 2], + token_name: vec![1, 2], + created: PalletTimestamp::now(), + } + } +} + +construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Storage, Config, Event}, + Currencies: orml_currencies::{Pallet, Call, Event}, + Tokens: orml_tokens::{Pallet, Storage, Event, Config}, + PalletBalances: pallet_balances::{Pallet, Call, Storage, Event}, + PalletTimestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent}, + Flow: pallet_flow, + } +); + +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + orml_tokens::GenesisConfig:: { + balances: vec![ + (ALICE, GAME_CURRENCY_ID, 100), + (BOB, GAME_CURRENCY_ID, 100), + (BOGDANA, GAME_CURRENCY_ID, 100), + (TREASURY, GAME_CURRENCY_ID, 100), + (GAMEDAO_TREASURY, GAME_CURRENCY_ID, 0), + ], + }.assimilate_storage(&mut t).unwrap(); + t.into() +} \ No newline at end of file diff --git a/flow/src/tests.rs b/flow/src/tests.rs new file mode 100644 index 000000000..6faad5f74 --- /dev/null +++ b/flow/src/tests.rs @@ -0,0 +1,470 @@ +#![cfg(test)] + +use super::*; +use codec::Encode; +use frame_support::{assert_noop, assert_ok}; +use frame_support::traits::Hooks; +use frame_system::{EventRecord, Phase}; +use mock::{ + new_test_ext, Flow, FlowProtocol, FlowGovernance, Event, Origin, Test, System, ALICE, + BOB, BOGDANA, TREASURY, MAX_DURATION, GAME_CURRENCY_ID, GAMEDAO_TREASURY +}; +use gamedao_protocol_support::{FlowState}; +use sp_core::H256; + +#[test] +fn flow_create_errors() { + new_test_ext().execute_with(|| { + let current_block = 3; + System::set_block_number(current_block); + // Check if creator is the controller of organization + // Error: AuthorizationError + let not_creator = ALICE; + assert_noop!( + Flow::create( + Origin::signed(not_creator), H256::random(), not_creator, vec![1, 2], 0, 0, 0, + FlowProtocol::Raise, FlowGovernance::No, vec![], vec![], vec![]), + Error::::AuthorizationError + ); + // Check if organization's treasury has enough deposit + // Error: TreasuryBalanceTooLow + let deposit_more_than_treasury = 1000; + assert_noop!( + Flow::create( + Origin::signed(BOB), H256::random(), BOB, vec![1, 2], 0, deposit_more_than_treasury, 0, + FlowProtocol::Raise, FlowGovernance::No, vec![], vec![], vec![]), + Error::::TreasuryBalanceTooLow + ); + // Check if deposit is not too high + // Error: DepositTooHigh + let target = 10; + let deposit_more_than_target = 20; + assert_noop!( + Flow::create( + Origin::signed(BOB), H256::random(), BOB, vec![1, 2], target, deposit_more_than_target, 0, + FlowProtocol::Raise, FlowGovernance::No, vec![], vec![], vec![]), + Error::::DepositTooHigh + ); + // Check Campaign name length + // Error: NameTooShort + let short_name = vec![1]; + assert_noop!( + Flow::create( + Origin::signed(BOB), H256::random(), BOB, short_name, 20, 10, 0, + FlowProtocol::Raise, FlowGovernance::No, vec![], vec![], vec![]), + Error::::NameTooShort + ); + // Error: NameTooLong + let long_name = vec![1, 2, 3, 4, 5]; + assert_noop!( + Flow::create( + Origin::signed(BOB), H256::random(), BOB, long_name, 20, 10, 0, + FlowProtocol::Raise, FlowGovernance::No, vec![], vec![], vec![]), + Error::::NameTooLong + ); + // Ensure campaign expires after the current block + // Error: EndTooEarly + let expiration_block = current_block - 1; + assert_noop!( + Flow::create( + Origin::signed(BOB), H256::random(), BOB, vec![1, 2], 20, 10, expiration_block, + FlowProtocol::Raise, FlowGovernance::No, vec![], vec![], vec![]), + Error::::EndTooEarly + ); + // Ensure campaign expires before expiration limit + // Error: EndTooLate + let expiration_block = MAX_DURATION + current_block + 1; + assert_noop!( + Flow::create( + Origin::signed(BOB), H256::random(), BOB, vec![1, 2], 20, 10, expiration_block, + FlowProtocol::Raise, FlowGovernance::No, vec![], vec![], vec![]), + Error::::EndTooLate + ); + // Check contribution limit per block + // Error: ContributionsPerBlockExceeded + CampaignsByBlock::::mutate(current_block + 1, |campaigns| { + campaigns.push(H256::random()) + }); + assert_noop!( + Flow::create( + Origin::signed(BOB), H256::random(), BOB, vec![1, 2], 20, 10, current_block + 1, + FlowProtocol::Raise, FlowGovernance::No, vec![1, 2], vec![], vec![]), + Error::::ContributionsPerBlockExceeded + ); + + }); +} + +#[test] +fn flow_create_success() { + new_test_ext().execute_with(|| { + let current_block = 3; + System::set_block_number(current_block); + + let nonce = Nonce::::get().encode(); + let id: H256 = ::Randomness::random(&nonce).0; + let org = H256::random(); + let expiry = current_block + 1; + let deposit = 10; + let target = 20; + let name = vec![1, 2]; + + assert_ok!( + Flow::create( + Origin::signed(BOB), org, BOB, name.clone(), target, deposit, expiry, + FlowProtocol::Raise, FlowGovernance::No, vec![1, 2], vec![], vec![]) + ); + + assert_eq!(Campaigns::::get(id).id, id); + assert_eq!(CampaignOrg::::get(id), org); + assert_eq!(CampaignOwner::::get(id), Some(BOB)); + assert_eq!(CampaignAdmin::::get(id), Some(BOB)); + assert_eq!(CampaignsByBody::::get(org), vec![id]); + assert_eq!(CampaignsByBlock::::get(expiry), vec![id]); + assert_eq!(CampaignsCount::::get(), 1); + assert_eq!(CampaignsArray::::get(0), id); + assert_eq!(CampaignsIndex::::get(id), 0); + assert_eq!(CampaignsOwnedArray::::get(org), id); + assert_eq!(CampaignsOwnedCount::::get(org), 1); + assert_eq!(CampaignsOwnedIndex::::get((org, id)), 0); + assert_eq!(Nonce::::get(), 1); + assert_eq!(CampaignsByState::::get(FlowState::Active), vec![id]); + assert_eq!(CampaignState::::get(id), FlowState::Active); + + // Events + assert_eq!( + System::events(), + vec![ + EventRecord { + phase: Phase::Initialization, + event: Event::Tokens(orml_tokens::Event::Reserved(GAME_CURRENCY_ID, TREASURY, deposit)), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: Event::Flow(crate::Event::CampaignCreated{ + campaign_id: id, creator: BOB, admin: BOB, target, deposit, expiry, name + }), + topics: vec![], + }, + ] + ); + + }); +} + +#[test] +fn flow_update_state_errors() { + new_test_ext().execute_with(|| { + let current_block = 3; + System::set_block_number(current_block); + let campaign_id: H256 = H256::random(); + + // Check if campaign has an owner + // Error: OwnerUnknown + assert_noop!( + Flow::update_state(Origin::signed(BOB), campaign_id, FlowState::Active), + Error::::OwnerUnknown + ); + // Check if caller is + // Error: AdminUnknown + CampaignOwner::::insert(campaign_id, BOB); + assert_noop!( + Flow::update_state(Origin::signed(BOB), campaign_id, FlowState::Active), + Error::::AdminUnknown + ); + // Check if caller is the controller of organization + // Error: AuthorizationError + CampaignAdmin::::insert(campaign_id, ALICE); + assert_noop!( + Flow::update_state(Origin::signed(BOB), campaign_id, FlowState::Active), + Error::::AuthorizationError + ); + // Check if campaign expires after the current block + // Error: CampaignExpired + let campaign = Campaign::new(campaign_id, current_block - 1); + CampaignAdmin::::insert(&campaign_id, &BOB); + Campaigns::::insert(&campaign_id, &campaign); + assert_noop!( + Flow::update_state(Origin::signed(BOB), campaign_id, FlowState::Active), + Error::::CampaignExpired + ); + // Ensure that campaign state is not Failed + // Error: CampaignExpired + let campaign = Campaign::new(campaign_id, current_block + 2); + Campaigns::::insert(&campaign_id, &campaign); + CampaignState::::insert(&campaign_id, FlowState::Failed); + assert_noop!( + Flow::update_state(Origin::signed(BOB), campaign_id, FlowState::Active), + Error::::CampaignExpired + ); + }); +} + +#[test] +fn flow_update_state_success() { + new_test_ext().execute_with(|| { + let current_block = 3; + System::set_block_number(current_block); + + let campaign_id: H256 = H256::random(); + let campaign = Campaign::new(campaign_id, current_block + 1); + + Campaigns::::insert(&campaign_id, &campaign); + CampaignOwner::::insert(campaign_id, BOB); + CampaignAdmin::::insert(campaign_id, BOB); + + assert_ok!(Flow::update_state(Origin::signed(BOB), campaign_id, FlowState::Paused)); + assert_eq!(CampaignsByState::::get(FlowState::Paused), vec![campaign_id]); + assert_eq!(CampaignState::::get(campaign_id), FlowState::Paused); + }); +} + +#[test] +fn flow_contribute_errors() { + new_test_ext().execute_with(|| { + let current_block = 3; + System::set_block_number(current_block); + + let campaign_id: H256 = H256::random(); + let campaign = Campaign::new(campaign_id, current_block + 2); + Campaigns::::insert(&campaign_id, &campaign); + + // Check if contributor has enough balance + // Error: BalanceTooLow + let more_than_balance = 110; + assert_noop!( + Flow::contribute(Origin::signed(BOB), campaign_id, more_than_balance), + Error::::BalanceTooLow + ); + // Check if owner exists for the campaign + // OwnerUnknown + assert_noop!( + Flow::contribute(Origin::signed(BOB), campaign_id, 50), + Error::::OwnerUnknown + ); + // Check that owner is not caller + // NoContributionToOwnCampaign + CampaignOwner::::insert(campaign_id, BOB); + assert_noop!( + Flow::contribute(Origin::signed(BOB), campaign_id, 50), + Error::::NoContributionToOwnCampaign + ); + // Check if campaign exists + // InvalidId + let new_campaign_id = H256::random(); + CampaignOwner::::insert(new_campaign_id, BOB); + assert_noop!( + Flow::contribute(Origin::signed(ALICE), new_campaign_id, 50), + Error::::InvalidId + ); + // Check if Campaign's state is Active + // NoContributionsAllowed + CampaignState::::insert(&campaign_id, FlowState::Paused); + assert_noop!( + Flow::contribute(Origin::signed(ALICE), campaign_id, 50), + Error::::NoContributionsAllowed + ); + // Check if campaign ends before the current block + // CampaignExpired + System::set_block_number(current_block + 2); + CampaignState::::insert(&campaign_id, FlowState::Active); + assert_noop!( + Flow::contribute(Origin::signed(ALICE), campaign_id, 50), + Error::::CampaignExpired + ); + }); +} + +#[test] +fn flow_contribute_success() { + new_test_ext().execute_with(|| { + let current_block = 3; + System::set_block_number(current_block); + + let campaign_id: H256 = H256::random(); + let campaign = Campaign::new(campaign_id, current_block + 2); + let contribution = 30; + Campaigns::::insert(&campaign_id, &campaign); + CampaignOwner::::insert(campaign_id, BOB); + CampaignState::::insert(&campaign_id, FlowState::Active); + + assert_ok!(Flow::contribute(Origin::signed(ALICE), campaign_id, contribution)); + + assert_eq!(CampaignsContributedArray::::get((ALICE, 0)), campaign_id); + assert_eq!(CampaignsContributedIndex::::get((ALICE, campaign_id)), 0); + assert_eq!(CampaignsContributedCount::::get(ALICE), 1); + assert_eq!(CampaignContributorsCount::::get(campaign_id), 1); + assert_eq!(CampaignContribution::::get((campaign_id, ALICE)), contribution); + assert_eq!(CampaignBalance::::get(campaign_id), contribution); + + // Events + assert_eq!( + System::events(), + vec![ + EventRecord { + phase: Phase::Initialization, + event: Event::Tokens(orml_tokens::Event::Reserved(GAME_CURRENCY_ID, ALICE, contribution)), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: Event::Flow(crate::Event::CampaignContributed{ + campaign_id, sender: ALICE, contribution, block_number: current_block + }), + topics: vec![], + }, + ] + ); + }); +} + +#[test] +fn flow_on_finalize_campaign_succeess() { + new_test_ext().execute_with(|| { + let current_block = 3; + System::set_block_number(current_block); + + let expiry = current_block + 1; + let deposit = 60; + let target = 100; + + // Create Campaign + let nonce = Nonce::::get().encode(); + let campaign_id: H256 = ::Randomness::random(&nonce).0; + assert_ok!( + Flow::create( + Origin::signed(BOB), H256::random(), BOB, vec![1, 2], target, deposit, expiry, + FlowProtocol::Raise, FlowGovernance::No, vec![1, 2], vec![], vec![]) + ); + // Contribute (60/100) + assert_ok!(Flow::contribute(Origin::signed(ALICE), campaign_id, deposit)); + // Contribute (120/100) + assert_ok!(Flow::contribute(Origin::signed(BOGDANA), campaign_id, deposit)); + + // deposit > capacity + System::set_block_number(expiry); + Flow::on_finalize(expiry); + + let commission = ::CampaignFee::get().mul_floor(deposit * 2); + assert_eq!( + ::Currency::total_balance(GAME_CURRENCY_ID, &TREASURY), + 100 + deposit * 2 - commission + ); + assert_eq!( + ::Currency::free_balance(GAME_CURRENCY_ID, &TREASURY), + 100 - deposit + ); + + assert_eq!( + // Skip events from create and contribute extrinsics + System::events()[6..], + vec![ + EventRecord { + phase: Phase::Initialization, + event: Event::Tokens(orml_tokens::Event::Unreserved(GAME_CURRENCY_ID, ALICE, deposit)), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: Event::Currencies(orml_currencies::Event::Transferred(GAME_CURRENCY_ID, ALICE, TREASURY, deposit)), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: Event::Tokens(orml_tokens::Event::Unreserved(GAME_CURRENCY_ID, BOGDANA, deposit)), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: Event::Currencies(orml_currencies::Event::Transferred(GAME_CURRENCY_ID, BOGDANA, TREASURY, deposit)), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: Event::Tokens(orml_tokens::Event::Reserved(GAME_CURRENCY_ID, TREASURY, deposit * 2)), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: Event::Tokens(orml_tokens::Event::Unreserved(GAME_CURRENCY_ID, TREASURY, commission)), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: Event::Currencies(orml_currencies::Event::Transferred(GAME_CURRENCY_ID, TREASURY, GAMEDAO_TREASURY, commission)), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: Event::Flow(crate::Event::CampaignFinalized{ + campaign_id, campaign_balance: deposit * 2, block_number: expiry, success: true + }), + topics: vec![], + }, + ] + ); + + }); +} + +#[test] +fn flow_on_finalize_campaign_failed() { + new_test_ext().execute_with(|| { + let current_block = 3; + System::set_block_number(current_block); + + let expiry = current_block + 1; + let deposit = 60; + let target = 100; + + // Create Campaign + let nonce = Nonce::::get().encode(); + let campaign_id: H256 = ::Randomness::random(&nonce).0; + assert_ok!( + Flow::create( + Origin::signed(BOB), H256::random(), BOB, vec![1, 2], target, deposit, expiry, + FlowProtocol::Raise, FlowGovernance::No, vec![1, 2], vec![], vec![]) + ); + // Contribute (60/100) + assert_ok!(Flow::contribute(Origin::signed(ALICE), campaign_id, deposit)); + + // deposit < capacity + System::set_block_number(expiry); + Flow::on_finalize(expiry); + + assert_eq!( + ::Currency::total_balance(GAME_CURRENCY_ID, &TREASURY), + 100 + ); + + assert_eq!( + ::Currency::free_balance(GAME_CURRENCY_ID, &TREASURY), + 100 + ); + + assert_eq!( + // Skip events from create and contribute extrinsics + System::events()[4..], + vec![ + EventRecord { + phase: Phase::Initialization, + event: Event::Tokens(orml_tokens::Event::Unreserved(GAME_CURRENCY_ID, ALICE, deposit)), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: Event::Tokens(orml_tokens::Event::Unreserved(GAME_CURRENCY_ID, TREASURY, deposit)), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: Event::Flow(crate::Event::CampaignFailed{ + campaign_id, campaign_balance: deposit, block_number: expiry, success: false + }), + topics: vec![], + }, + ] + ); + }); +} \ No newline at end of file diff --git a/sense/src/benchmarking.rs b/sense/src/benchmarking.rs index d38ece214..bce558e95 100644 --- a/sense/src/benchmarking.rs +++ b/sense/src/benchmarking.rs @@ -2,31 +2,30 @@ use super::*; #[allow(unused)] -use crate::Pallet as ZeroSense; -use frame_benchmarking::{benchmarks, impl_benchmark_test_suite, account}; +use crate::Pallet as Sense; +use frame_benchmarking::{account, benchmarks, impl_benchmark_test_suite}; use frame_system::RawOrigin; use sp_std::vec; +benchmarks! { -benchmarks!{ + create_entity {}: _(RawOrigin::Root, account("1", 0, 0), vec![1; 256]) - create_entity {}: _(RawOrigin::Root, account("1", 0, 0), vec![1; 256]) - - mod_xp { - let caller_origin = ::Origin::from(RawOrigin::Root); - ZeroSense::::create_entity(caller_origin, account("1", 0, 0), vec![1; 1])?; + mod_xp { + let caller_origin = ::Origin::from(RawOrigin::Root); + Sense::::create_entity(caller_origin, account("1", 0, 0), vec![1; 1])?; }: _(RawOrigin::Root, account("1", 0, 0), 255) - mod_rep { - let caller_origin = ::Origin::from(RawOrigin::Root); - ZeroSense::::create_entity(caller_origin, account("1", 0, 0), vec![1; 1])?; + mod_rep { + let caller_origin = ::Origin::from(RawOrigin::Root); + Sense::::create_entity(caller_origin, account("1", 0, 0), vec![1; 1])?; }: _(RawOrigin::Root, account("1", 0, 0), 255) - mod_trust { - let caller_origin = ::Origin::from(RawOrigin::Root); - ZeroSense::::create_entity(caller_origin, account("1", 0, 0), vec![1; 1])?; + mod_trust { + let caller_origin = ::Origin::from(RawOrigin::Root); + Sense::::create_entity(caller_origin, account("1", 0, 0), vec![1; 1])?; }: _(RawOrigin::Root, account("1", 0, 0), 255) } -impl_benchmark_test_suite!(ZeroSense, crate::mock::new_test_ext(), crate::mock::Test); +impl_benchmark_test_suite!(Sense, crate::mock::new_test_ext(), crate::mock::Test); diff --git a/sense/src/lib.rs b/sense/src/lib.rs old mode 100755 new mode 100644 index 19b178425..a78a0e47b --- a/sense/src/lib.rs +++ b/sense/src/lib.rs @@ -16,77 +16,74 @@ //! //! This pallet aggregates datapoints to reflect user experience and behaviour. #![cfg_attr(not(feature = "std"), no_std)] +#[warn(unused_imports)] +use frame_support::{dispatch::DispatchResult, pallet_prelude::*, traits::Get}; +use frame_system::pallet_prelude::*; +use scale_info::TypeInfo; +use sp_std::vec::Vec; pub use weights::WeightInfo; -pub use pallet::*; - -#[cfg(test)] -mod mock; - -#[cfg(test)] -mod tests; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; - +mod mock; +mod tests; pub mod weights; +pub use pallet::*; -#[frame_support::pallet] -pub mod pallet { - use frame_support::{dispatch::DispatchResult, pallet_prelude::*}; - use frame_system::pallet_prelude::*; - use sp_std::vec::Vec; - use scale_info::TypeInfo; - use super::*; +pub const MAX_STRING_FIELD_LENGTH: usize = 256; - pub const MAX_STRING_FIELD_LENGTH: usize = 256; +#[derive(Encode, Decode, Default, PartialEq, Eq, TypeInfo)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct Entity { + account: AccountId, + index: u128, + cid: Vec, + created: BlockNumber, + mutated: BlockNumber, +} - #[pallet::config] - pub trait Config: frame_system::Config { - type Event: From> + IsType<::Event> + Into<::Event>; - type ForceOrigin: EnsureOrigin; - type WeightInfo: WeightInfo; - } +#[derive(Encode, Decode, Default, PartialEq, Eq, TypeInfo)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct EntityProperty { + value: u64, + mutated: BlockNumber, +} - #[derive(Encode, Decode, Default, PartialEq, Eq, TypeInfo)] - #[cfg_attr(feature = "std", derive(Debug))] - pub struct Entity { +impl Entity { + pub fn new( account: AccountId, - index: u128, - cid: Vec, - created: BlockNumber, - mutated: BlockNumber, + block_number: BlockNumber, + index: u128, + cid: Vec, + ) -> Entity + where + BlockNumber: Clone, + { + Entity { account, index, cid, created: block_number.clone(), mutated: block_number } } +} - #[derive(Encode, Decode, Default, PartialEq, Eq, TypeInfo)] - #[cfg_attr(feature = "std", derive(Debug))] - pub struct EntityProperty { - value: u64, - mutated: BlockNumber, +impl EntityProperty { + pub fn new(value: u64, block_number: BlockNumber) -> EntityProperty { + EntityProperty { value, mutated: block_number } } +} - impl Entity { - pub fn new(account: AccountId, block_number: BlockNumber, index: u128, cid: Vec) - -> Entity where BlockNumber: Clone, { - Entity { - account: account, - index: index, - cid: cid, - created: block_number.clone(), - mutated: block_number, - } - } - } +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; - impl EntityProperty { - pub fn new(value: u64, block_number: BlockNumber) - -> EntityProperty { - EntityProperty { - value: value, - mutated: block_number, - } - } + #[pallet::config] + pub trait Config: frame_system::Config { + type Event: From> + + IsType<::Event> + + Into<::Event>; + type ForceOrigin: EnsureOrigin; + type WeightInfo: WeightInfo; } #[pallet::pallet] @@ -95,19 +92,28 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn entity)] - pub(super) type Sense = StorageMap<_, Blake2_128Concat, T::AccountId, Entity, ValueQuery>; + pub(super) type SenseEntity = StorageMap< + _, + Blake2_128Concat, + T::AccountId, + Entity, + ValueQuery, + >; #[pallet::storage] #[pallet::getter(fn xp)] - pub(super) type SenseXP = StorageMap<_, Blake2_128Concat, T::AccountId, EntityProperty, ValueQuery>; + pub(super) type SenseXP = + StorageMap<_, Blake2_128Concat, T::AccountId, EntityProperty, ValueQuery>; #[pallet::storage] #[pallet::getter(fn rep)] - pub(super) type SenseREP = StorageMap<_, Blake2_128Concat, T::AccountId, EntityProperty, ValueQuery>; + pub(super) type SenseREP = + StorageMap<_, Blake2_128Concat, T::AccountId, EntityProperty, ValueQuery>; #[pallet::storage] #[pallet::getter(fn trust)] - pub(super) type SenseTrust = StorageMap<_, Blake2_128Concat, T::AccountId, EntityProperty, ValueQuery>; + pub(super) type SenseTrust = + StorageMap<_, Blake2_128Concat, T::AccountId, EntityProperty, ValueQuery>; #[pallet::storage] #[pallet::getter(fn nonce)] @@ -134,19 +140,21 @@ pub mod pallet { /// Param Limit Exceed ParamLimitExceed, /// Invalid Param - InvalidParam + InvalidParam, } #[pallet::call] impl Pallet { - #[pallet::weight(::WeightInfo::create_entity())] - pub fn create_entity(origin: OriginFor, account: T::AccountId, cid: Vec) -> DispatchResult { - + pub fn create_entity( + origin: OriginFor, + account: T::AccountId, + cid: Vec, + ) -> DispatchResult { ensure_root(origin)?; ensure!(cid.len() > 0, Error::::InvalidParam); ensure!(cid.len() <= MAX_STRING_FIELD_LENGTH, Error::::ParamLimitExceed); - ensure!(!>::contains_key(&account), Error::::EntityExists); + ensure!(!>::contains_key(&account), Error::::EntityExists); let current_block = >::block_number(); let index = >::get(); @@ -156,18 +164,15 @@ pub mod pallet { let rep = EntityProperty { value: 0, mutated: current_block.clone() }; let trust = EntityProperty { value: 0, mutated: current_block.clone() }; - >::insert( account.clone(), xp ); - >::insert( account.clone(), rep ); - >::insert( account.clone(), trust ); - >::insert( account.clone(), entity ); + >::insert(account.clone(), xp); + >::insert(account.clone(), rep); + >::insert(account.clone(), trust); + >::insert(account.clone(), entity); // TODO: safe increment, checked_add >::mutate(|n| *n += 1); - Self::deposit_event( - Event::EntityInit(account, current_block) - ); + Self::deposit_event(Event::EntityInit(account, current_block)); Ok(()) - } // TODO: @@ -181,9 +186,8 @@ pub mod pallet { #[pallet::weight(::WeightInfo::mod_xp())] pub fn mod_xp(origin: OriginFor, account: T::AccountId, value: u8) -> DispatchResult { - ensure_root(origin)?; - ensure!( >::contains_key(&account), Error::::EntityUnknown ); + ensure!(>::contains_key(&account), Error::::EntityUnknown); let now = >::block_number(); let v = u64::from(value); @@ -191,23 +195,19 @@ pub mod pallet { let updated = EntityProperty { value: current.value.checked_add(v).ok_or(Error::::GuruMeditation)?, - mutated: now.clone() + mutated: now.clone(), }; - >::insert( account.clone(), updated ); + >::insert(account.clone(), updated); - Self::deposit_event( - Event::EntityMutateXP(account, now) - ); + Self::deposit_event(Event::EntityMutateXP(account, now)); Ok(()) - } #[pallet::weight(::WeightInfo::mod_rep())] pub fn mod_rep(origin: OriginFor, account: T::AccountId, value: u8) -> DispatchResult { - ensure_root(origin)?; - ensure!( >::contains_key(&account), Error::::EntityUnknown ); + ensure!(>::contains_key(&account), Error::::EntityUnknown); let now = >::block_number(); let v = u64::from(value); @@ -215,23 +215,19 @@ pub mod pallet { let updated = EntityProperty { value: current.value.checked_add(v).ok_or(Error::::GuruMeditation)?, - mutated: now.clone() + mutated: now.clone(), }; - >::insert( account.clone(), updated ); + >::insert(account.clone(), updated); - Self::deposit_event( - Event::EntityMutateREP(account, now) - ); + Self::deposit_event(Event::EntityMutateREP(account, now)); Ok(()) - } #[pallet::weight(::WeightInfo::mod_trust())] pub fn mod_trust(origin: OriginFor, account: T::AccountId, value: u8) -> DispatchResult { - ensure_root(origin)?; - ensure!( >::contains_key(&account), Error::::EntityUnknown ); + ensure!(>::contains_key(&account), Error::::EntityUnknown); let now = >::block_number(); let v = u64::from(value); @@ -239,20 +235,16 @@ pub mod pallet { let updated = EntityProperty { value: current.value.checked_add(v).ok_or(Error::::GuruMeditation)?, - mutated: now + mutated: now, }; - >::insert( account.clone(), updated ); + >::insert(account.clone(), updated); - Self::deposit_event( - Event::EntityMutateTrust(account, now) - ); + Self::deposit_event(Event::EntityMutateTrust(account, now)); Ok(()) - } // TODO: // generic mod for all properties - } } diff --git a/sense/src/mock.rs b/sense/src/mock.rs index 67c1e4140..387ae1431 100644 --- a/sense/src/mock.rs +++ b/sense/src/mock.rs @@ -1,4 +1,5 @@ -use crate as pallet_sense; +#![cfg(test)] + use frame_support::parameter_types; use frame_system as system; use sp_core::H256; @@ -18,10 +19,14 @@ frame_support::construct_runtime!( UncheckedExtrinsic = UncheckedExtrinsic, { System: frame_system::{Pallet, Call, Config, Storage, Event}, - ZeroSense: pallet_sense::{Pallet, Call, Storage, Event}, + Sense: pallet_sense::{Pallet, Call, Storage, Event}, } ); +mod pallet_sense { + pub use super::super::*; +} + parameter_types! { pub const BlockHashCount: u64 = 250; pub const SS58Prefix: u8 = 42; diff --git a/sense/src/tests.rs b/sense/src/tests.rs index 84f877ffe..e91c8ee73 100644 --- a/sense/src/tests.rs +++ b/sense/src/tests.rs @@ -1,14 +1,16 @@ -use super::{mock::*, Error, EntityProperty, Entity, Sense, SenseXP, SenseREP, SenseTrust, Event as ZeroEvent}; +#![cfg(test)] +use super::{ + mock::*, Entity, EntityProperty, Error, Event as SenseEvent, SenseEntity, SenseREP, SenseTrust, + SenseXP, +}; use frame_support::{assert_noop, assert_ok}; use frame_system::{EventRecord, Phase, RawOrigin}; use sp_runtime::traits::BadOrigin; - #[test] fn sense_create_entity() { new_test_ext().execute_with(|| { - - let cid = vec![1,2,3]; + let cid = vec![1, 2, 3]; let account = 1; let index = 0; @@ -16,60 +18,47 @@ fn sense_create_entity() { System::set_block_number(block_number); - assert_noop!(ZeroSense::create_entity( - RawOrigin::Root.into(), 1, vec![]), + assert_noop!( + Sense::create_entity(RawOrigin::Root.into(), 1, vec![]), Error::::InvalidParam ); - assert_noop!(ZeroSense::create_entity( - RawOrigin::Root.into(), 1, vec![1u8; 257]), + assert_noop!( + Sense::create_entity(RawOrigin::Root.into(), 1, vec![1u8; 257]), Error::::ParamLimitExceed ); - assert_ok!(ZeroSense::create_entity( - RawOrigin::Root.into(), account, cid.clone()) - ); - + assert_ok!(Sense::create_entity(RawOrigin::Root.into(), account, cid.clone())); + assert_eq!( Entity::new(account, block_number, index, cid.clone()), - ZeroSense::entity(account) - ); - assert_eq!( - EntityProperty::new(0, block_number), - ZeroSense::xp(account) - ); - assert_eq!( - EntityProperty::new(0, block_number), - ZeroSense::rep(account) - ); - assert_eq!( - EntityProperty::new(0, block_number), - ZeroSense::trust(account) + Sense::entity(account) ); + assert_eq!(EntityProperty::new(0, block_number), Sense::xp(account)); + assert_eq!(EntityProperty::new(0, block_number), Sense::rep(account)); + assert_eq!(EntityProperty::new(0, block_number), Sense::trust(account)); assert_eq!( System::events(), vec![EventRecord { phase: Phase::Initialization, - event: Event::ZeroSense(ZeroEvent::EntityInit(account, block_number)), + event: Event::Sense(SenseEvent::EntityInit(account, block_number)), topics: vec![], }] ); // TODO: Check Nonce value increased in storage as a result of successful extrinsic call. - - assert_noop!(ZeroSense::create_entity(Origin::signed(1), 1, vec![1u8]), BadOrigin); - assert_noop!(ZeroSense::create_entity( - RawOrigin::Root.into(), account, cid.clone()), + assert_noop!(Sense::create_entity(Origin::signed(1), 1, vec![1u8]), BadOrigin); + + assert_noop!( + Sense::create_entity(RawOrigin::Root.into(), account, cid.clone()), Error::::EntityExists ); - }); - } // TODO: 1. Test: StorageMap value updated after calling extrinsic (SenseXP etc.) -// 2. Tese: event is generated (EntityMutateXP etc.) +// 2. Test: event is generated (EntityMutateXP etc.) // 3. Add comments macro_rules! sense_mod_tests { ($($name:ident: $storage:tt, $extrinsic:path,)*) => { @@ -80,20 +69,20 @@ macro_rules! sense_mod_tests { let account = 1; let block_number = 3; System::set_block_number(block_number); - + assert_noop!($extrinsic(Origin::signed(1), 1, 1), BadOrigin); assert_noop!( $extrinsic(RawOrigin::Root.into(), 1, 1), Error::::EntityUnknown ); - - Sense::::insert( + + SenseEntity::::insert( account, Entity::new(account, block_number, 0, vec![1,2,3]) ); $storage::::insert( account, EntityProperty::new(account, block_number) ); - + assert_ok!($extrinsic( RawOrigin::Root.into(), account, 125) ); @@ -104,7 +93,7 @@ macro_rules! sense_mod_tests { } sense_mod_tests! { - sense_mod_xp: SenseXP, ZeroSense::mod_xp, - sense_mod_rep: SenseREP, ZeroSense::mod_rep, - sense_mod_trust: SenseTrust, ZeroSense::mod_trust, + sense_mod_xp: SenseXP, Sense::mod_xp, + sense_mod_rep: SenseREP, Sense::mod_rep, + sense_mod_trust: SenseTrust, Sense::mod_trust, } diff --git a/sense/src/weights.rs b/sense/src/weights.rs index 967c51460..d5269a970 100644 --- a/sense/src/weights.rs +++ b/sense/src/weights.rs @@ -35,11 +35,13 @@ // --output=./pallets/sense/src/weights.rs // --template=./.maintain/frame-weight-template.hbs - #![allow(unused_parens)] #![allow(unused_imports)] -use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use frame_support::{ + traits::Get, + weights::{constants::RocksDbWeight, Weight}, +}; use sp_std::marker::PhantomData; /// Weight functions needed for pallet_sense.