From e60150783f76d7b6719cf678dcee29b172b19ce7 Mon Sep 17 00:00:00 2001 From: Volodymyr Brazhnyk Date: Fri, 18 Feb 2022 05:40:30 +0200 Subject: [PATCH 01/10] Add pallet-flow, support module --- flow/Cargo.toml | 47 +++ flow/README.md | 21 + flow/src/lib.rs | 943 +++++++++++++++++++++++++++++++++++++++++++++ support/Cargo.toml | 15 + support/src/lib.rs | 9 + 5 files changed, 1035 insertions(+) create mode 100644 flow/Cargo.toml create mode 100755 flow/README.md create mode 100644 flow/src/lib.rs create mode 100644 support/Cargo.toml create mode 100644 support/src/lib.rs diff --git a/flow/Cargo.toml b/flow/Cargo.toml new file mode 100644 index 000000000..f6c94d038 --- /dev/null +++ b/flow/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "pallet-flow" +version = "1.0.1-dev" +authors = ["zero.io","gamedao.co"] +edition = "2018" +license = "GPL-3.0-or-later" +description = "Simple Crowdfunding module, supporting multiple campaigns, which are all settled with the platform currency." +repository = "https://github.com/gamedaoco/gamedao-protocol" + +[dependencies] +serde = { version = "1.0.124", optional = true } +codec = { package = "parity-scale-codec", version = "2.3.1", default-features = false } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +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 } + +[dev-dependencies] +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" } + +[features] +default = ["std"] +runtime-benchmarks = ["frame-benchmarking"] +std = [ + "codec/std", + "serde/std", + "scale-info/std", + + "frame-support/std", + "frame-system/std", + "frame-benchmarking/std", + + "sp-core/std", + "sp-std/std", + "sp-runtime/std", + "orml-traits/std", + "support/std", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/flow/README.md b/flow/README.md new file mode 100755 index 000000000..52e628e20 --- /dev/null +++ b/flow/README.md @@ -0,0 +1,21 @@ +# Module Flow + +Simple Crowdfunding module, supporting multiple campaigns, which are all settled with the platform currency. + +This pallet provides a simple on-chain crowdfunding mechanism: +- creator can create a campaign with individual length and amount of funds in PLAY to raise +- investor can invest his funds into one of the running campaigns and become an investor + +Upon finalization: +- creator can request allocation of funds +- investors can collectively approve allocation of funds + +TODO: +- supervisor can lock, cancel campaigns +... + +1. create campaigns with custom funding goal and runtime +2. invest into open campaigns + +3. request withdrawal (unreserve) as creator from successful campaign +4. approve withdrawals (unreserve) as investor from successfully funded campaigns diff --git a/flow/src/lib.rs b/flow/src/lib.rs new file mode 100644 index 000000000..85dc63002 --- /dev/null +++ b/flow/src/lib.rs @@ -0,0 +1,943 @@ +// +// _______________________________ ________ +// \____ /\_ _____/\______ \\_____ \ +// / / | __)_ | _/ / | \ +// / /_ | \ | | \/ | \ +// /_______ \/_______ / |____|_ /\_______ / +// \/ \/ \/ \/ +// Z E R O . I O N E T W O R K +// © C O P Y R I O T 2 0 7 5 @ Z E R O . I O + +// This file is part of ZERO Network. +// Copyright (C) 2010-2020 ZERO Technologies. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Crowdfunding Campaign Factory + Treasury +//! +//! Run `cargo doc --package module-crowdfunding --open` to view this pallet's documentation. +//! +//! ## Overview +//! +//! This pallet provides a simple on-chain crowdfunding mechanism: +//! +//! - creator can create a campaign with individual length and +//! amount of funds in PLAY to raise +//! +//! - investor can invest his funds into one of the running campaigns +//! and become an investor +//! +//! Upon finalization: +//! +//! - creator can request allocation of funds +//! - investors can collectively approve allocation of funds +//! +//! TODO: +//! - supervisor can lock, cancel campaigns +//! + +// 1. create campaigns with custom funding goal and runtime +// 2. invest into open campaigns +#![cfg_attr(not(feature = "std"), no_std)] +#[warn(unused_imports)] + +// pub use weights::WeightInfo; +pub use pallet::*; + +use scale_info::TypeInfo; +use frame_support::{codec::{Decode, Encode}, dispatch::DispatchResult, traits::{UnixTime, Randomness}}; +use sp_std::{vec::Vec, fmt::Debug}; + +use codec::FullCodec; +use frame_support::traits::Get; +use sp_runtime::traits::{AtLeast32BitUnsigned}; + +use orml_traits::{MultiCurrency, MultiReservableCurrency}; +use support::ControlPalletStorage; +use primitives::{Balance, Moment, CurrencyId}; + +// TODO: tests + +// TODO: pallet benchmarking +// mod benchmarking; + +// TODO: weights +// mod default_weights; + +// TODO: externalise error messages +// mod errors; + +#[derive(Encode, Decode, Clone, PartialEq, Eq, PartialOrd, Ord, TypeInfo)] +#[derive(Debug)] +#[repr(u8)] +pub enum FlowProtocol { + Grant = 0, + Raise = 1, + Lend = 2, + Loan = 3, + Share = 4, + Pool = 5 +} + +impl Default for FlowProtocol { + fn default() -> Self { Self::Raise } +} + +#[derive(Encode, Decode, Clone, PartialEq, Eq, PartialOrd, Ord, TypeInfo)] +#[derive(Debug)] +#[repr(u8)] +pub enum FlowGovernance { + 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)] +#[derive(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 } +} + +// 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, +} + +#[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, 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.clone()); + + ensure!( creator == controller, Error::::AuthorizationError ); + + // Get Treasury account for deposits and fees + + let treasury = T::Control::body_treasury(org.clone()); + + 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(()) + } + + } + +} + +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 + ) -> 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.clone()); + + // 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(()) + } +} diff --git a/support/Cargo.toml b/support/Cargo.toml new file mode 100644 index 000000000..dbdd909e0 --- /dev/null +++ b/support/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "gamedao-protocol-support" +version = "1.0.1-dev" +authors = ["zero.io","gamedao.co"] +edition = "2018" +license = "GPL-3.0-or-later" +description = "" +repository = "https://github.com/gamedaoco/gamedao-protocol" + + +[features] +default = ["std"] +std = [ + +] diff --git a/support/src/lib.rs b/support/src/lib.rs new file mode 100644 index 000000000..596fcb5af --- /dev/null +++ b/support/src/lib.rs @@ -0,0 +1,9 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +pub trait ControlPalletStorage { + + fn body_controller(org: Hash) -> AccountId; + + fn body_treasury(org: Hash) -> AccountId; + +} From 29541fceb764b2910180ef5ebdca777f3140d8ad Mon Sep 17 00:00:00 2001 From: Volodymyr Brazhnyk Date: Fri, 18 Feb 2022 06:46:45 +0200 Subject: [PATCH 02/10] rustfmt --- flow/src/lib.rs | 1624 +++++++++++++++++++------------------ sense/src/benchmarking.rs | 11 +- sense/src/lib.rs | 481 ++++++----- sense/src/mock.rs | 87 +- sense/src/tests.rs | 119 +-- sense/src/weights.rs | 96 +-- support/src/lib.rs | 6 +- 7 files changed, 1258 insertions(+), 1166 deletions(-) diff --git a/flow/src/lib.rs b/flow/src/lib.rs index 85dc63002..0417d187c 100644 --- a/flow/src/lib.rs +++ b/flow/src/lib.rs @@ -51,21 +51,24 @@ // 2. invest into open campaigns #![cfg_attr(not(feature = "std"), no_std)] #[warn(unused_imports)] - // pub use weights::WeightInfo; pub use pallet::*; +use frame_support::{ + codec::{Decode, Encode}, + dispatch::DispatchResult, + traits::{Randomness, UnixTime}, +}; use scale_info::TypeInfo; -use frame_support::{codec::{Decode, Encode}, dispatch::DispatchResult, traits::{UnixTime, Randomness}}; -use sp_std::{vec::Vec, fmt::Debug}; +use sp_std::{fmt::Debug, vec::Vec}; use codec::FullCodec; use frame_support::traits::Get; -use sp_runtime::traits::{AtLeast32BitUnsigned}; +use sp_runtime::traits::AtLeast32BitUnsigned; use orml_traits::{MultiCurrency, MultiReservableCurrency}; +use primitives::{Balance, CurrencyId, Moment}; use support::ControlPalletStorage; -use primitives::{Balance, Moment, CurrencyId}; // TODO: tests @@ -78,267 +81,314 @@ use primitives::{Balance, Moment, CurrencyId}; // TODO: externalise error messages // mod errors; -#[derive(Encode, Decode, Clone, PartialEq, Eq, PartialOrd, Ord, TypeInfo)] -#[derive(Debug)] +#[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)] -#[derive(Debug)] +#[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 } + fn default() -> Self { + Self::No + } } -#[derive(Encode, Decode, Clone, PartialEq, Eq, PartialOrd, Ord, TypeInfo)] -#[derive(Debug)] +#[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 + 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::Init + } } // 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, +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, } #[frame_support::pallet] pub mod pallet { use super::*; - use frame_support::pallet_prelude::*; - use frame_system::pallet_prelude::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; #[pallet::pallet] - #[pallet::generate_store(pub(super) trait Store)] - pub struct 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>; + #[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, 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>; + + 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>; + #[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::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), + 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), @@ -346,263 +396,247 @@ pub mod pallet { 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::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 { - + impl Pallet { #[pallet::weight(5_000_000)] pub fn create( origin: OriginFor, @@ -614,330 +648,350 @@ pub mod pallet { 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 + 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.clone()); - - ensure!( creator == controller, Error::::AuthorizationError ); - - // Get Treasury account for deposits and fees - - let treasury = T::Control::body_treasury(org.clone()); - - 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)] + let creator = ensure_signed(origin)?; + + let controller = T::Control::body_controller(org.clone()); + + ensure!(creator == controller, Error::::AuthorizationError); + + // Get Treasury account for deposits and fees + + let treasury = T::Control::body_treasury(org.clone()); + + 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 + 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(()) - } + // 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 ( + #[pallet::weight(5_000_000)] + pub fn contribute( origin: OriginFor, campaign_id: T::Hash, - contribution: Balance + 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(()) - } - + // 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(()) + } } - } 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.clone()); + + // 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 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 - ) -> 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.clone()); - - // 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 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(()) + } } diff --git a/sense/src/benchmarking.rs b/sense/src/benchmarking.rs index d38ece214..2019cf7a9 100644 --- a/sense/src/benchmarking.rs +++ b/sense/src/benchmarking.rs @@ -3,29 +3,28 @@ use super::*; #[allow(unused)] use crate::Pallet as ZeroSense; -use frame_benchmarking::{benchmarks, impl_benchmark_test_suite, account}; +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]) mod_xp { let caller_origin = ::Origin::from(RawOrigin::Root); ZeroSense::::create_entity(caller_origin, account("1", 0, 0), vec![1; 1])?; - }: _(RawOrigin::Root, account("1", 0, 0), 255) + }: _(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])?; - }: _(RawOrigin::Root, account("1", 0, 0), 255) + }: _(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])?; - }: _(RawOrigin::Root, account("1", 0, 0), 255) + }: _(RawOrigin::Root, account("1", 0, 0), 255) } diff --git a/sense/src/lib.rs b/sense/src/lib.rs index 19b178425..70bb3ab87 100755 --- a/sense/src/lib.rs +++ b/sense/src/lib.rs @@ -17,8 +17,8 @@ //! This pallet aggregates datapoints to reflect user experience and behaviour. #![cfg_attr(not(feature = "std"), no_std)] -pub use weights::WeightInfo; pub use pallet::*; +pub use weights::WeightInfo; #[cfg(test)] mod mock; @@ -31,228 +31,263 @@ mod benchmarking; pub mod weights; - #[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; - - #[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 Entity { - account: AccountId, - index: u128, - cid: Vec, - created: BlockNumber, - mutated: BlockNumber, - } - - #[derive(Encode, Decode, Default, PartialEq, Eq, TypeInfo)] - #[cfg_attr(feature = "std", derive(Debug))] - pub struct EntityProperty { - value: u64, - mutated: BlockNumber, - } - - 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, - } - } - } - - impl EntityProperty { - pub fn new(value: u64, block_number: BlockNumber) - -> EntityProperty { - EntityProperty { - value: value, - mutated: block_number, - } - } - } - - #[pallet::pallet] - #[pallet::generate_store(pub(super) trait Store)] - pub struct Pallet(_); - - #[pallet::storage] - #[pallet::getter(fn entity)] - pub(super) type Sense = StorageMap<_, Blake2_128Concat, T::AccountId, Entity, ValueQuery>; - - #[pallet::storage] - #[pallet::getter(fn xp)] - 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>; - - #[pallet::storage] - #[pallet::getter(fn trust)] - pub(super) type SenseTrust = StorageMap<_, Blake2_128Concat, T::AccountId, EntityProperty, ValueQuery>; - - #[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 { - EntityInit(T::AccountId, T::BlockNumber), - EntityMutateXP(T::AccountId, T::BlockNumber), - EntityMutateREP(T::AccountId, T::BlockNumber), - EntityMutateTrust(T::AccountId, T::BlockNumber), - } - - // Errors inform users that something went wrong. - #[pallet::error] - pub enum Error { - /// Entity Exists - EntityExists, - /// Entity Unknown - EntityUnknown, - /// Guru Meditation - GuruMeditation, - /// Param Limit Exceed - ParamLimitExceed, - /// Invalid Param - InvalidParam - } - - #[pallet::call] - impl Pallet { - - #[pallet::weight(::WeightInfo::create_entity())] - 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); - - let current_block = >::block_number(); - let index = >::get(); - - let entity = Entity::new(account.clone(), current_block, index, cid.clone()); - let xp = EntityProperty { value: 0, mutated: current_block.clone() }; - 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 ); - // TODO: safe increment, checked_add - >::mutate(|n| *n += 1); - - Self::deposit_event( - Event::EntityInit(account, current_block) - ); - Ok(()) - - } - - // TODO: - // mutation of values should be restricted - // certain roles are allowed to mutate values - // xp: realm - // rep: social - // trust: id - // all: governance - // sudo ( until its removal ) - - #[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 ); - - let now = >::block_number(); - let v = u64::from(value); - let current = Self::xp(&account); - - let updated = EntityProperty { - value: current.value.checked_add(v).ok_or(Error::::GuruMeditation)?, - mutated: now.clone() - }; - - >::insert( account.clone(), updated ); - - 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 ); - - let now = >::block_number(); - let v = u64::from(value); - let current = Self::rep(&account); - - let updated = EntityProperty { - value: current.value.checked_add(v).ok_or(Error::::GuruMeditation)?, - mutated: now.clone() - }; - - >::insert( account.clone(), updated ); - - 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 ); - - let now = >::block_number(); - let v = u64::from(value); - let current = Self::trust(&account); - - let updated = EntityProperty { - value: current.value.checked_add(v).ok_or(Error::::GuruMeditation)?, - mutated: now - }; - - >::insert( account.clone(), updated ); - - Self::deposit_event( - Event::EntityMutateTrust(account, now) - ); - Ok(()) - - } - - // TODO: - // generic mod for all properties - - } + use super::*; + use frame_support::{dispatch::DispatchResult, pallet_prelude::*}; + use frame_system::pallet_prelude::*; + use scale_info::TypeInfo; + use sp_std::vec::Vec; + + pub const MAX_STRING_FIELD_LENGTH: usize = 256; + + #[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 Entity { + account: AccountId, + index: u128, + cid: Vec, + created: BlockNumber, + mutated: BlockNumber, + } + + #[derive(Encode, Decode, Default, PartialEq, Eq, TypeInfo)] + #[cfg_attr(feature = "std", derive(Debug))] + pub struct EntityProperty { + value: u64, + mutated: BlockNumber, + } + + 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, + } + } + } + + impl EntityProperty { + pub fn new(value: u64, block_number: BlockNumber) -> EntityProperty { + EntityProperty { + value: value, + mutated: block_number, + } + } + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::storage] + #[pallet::getter(fn entity)] + pub(super) type Sense = StorageMap< + _, + Blake2_128Concat, + T::AccountId, + Entity, + ValueQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn xp)] + 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>; + + #[pallet::storage] + #[pallet::getter(fn trust)] + pub(super) type SenseTrust = + StorageMap<_, Blake2_128Concat, T::AccountId, EntityProperty, ValueQuery>; + + #[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 { + EntityInit(T::AccountId, T::BlockNumber), + EntityMutateXP(T::AccountId, T::BlockNumber), + EntityMutateREP(T::AccountId, T::BlockNumber), + EntityMutateTrust(T::AccountId, T::BlockNumber), + } + + // Errors inform users that something went wrong. + #[pallet::error] + pub enum Error { + /// Entity Exists + EntityExists, + /// Entity Unknown + EntityUnknown, + /// Guru Meditation + GuruMeditation, + /// Param Limit Exceed + ParamLimitExceed, + /// Invalid Param + InvalidParam, + } + + #[pallet::call] + impl Pallet { + #[pallet::weight(::WeightInfo::create_entity())] + 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 + ); + + let current_block = >::block_number(); + let index = >::get(); + + let entity = Entity::new(account.clone(), current_block, index, cid.clone()); + let xp = EntityProperty { + value: 0, + mutated: current_block.clone(), + }; + 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); + // TODO: safe increment, checked_add + >::mutate(|n| *n += 1); + + Self::deposit_event(Event::EntityInit(account, current_block)); + Ok(()) + } + + // TODO: + // mutation of values should be restricted + // certain roles are allowed to mutate values + // xp: realm + // rep: social + // trust: id + // all: governance + // sudo ( until its removal ) + + #[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 + ); + + let now = >::block_number(); + let v = u64::from(value); + let current = Self::xp(&account); + + let updated = EntityProperty { + value: current + .value + .checked_add(v) + .ok_or(Error::::GuruMeditation)?, + mutated: now.clone(), + }; + + >::insert(account.clone(), updated); + + 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 + ); + + let now = >::block_number(); + let v = u64::from(value); + let current = Self::rep(&account); + + let updated = EntityProperty { + value: current + .value + .checked_add(v) + .ok_or(Error::::GuruMeditation)?, + mutated: now.clone(), + }; + + >::insert(account.clone(), updated); + + 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 + ); + + let now = >::block_number(); + let v = u64::from(value); + let current = Self::trust(&account); + + let updated = EntityProperty { + value: current + .value + .checked_add(v) + .ok_or(Error::::GuruMeditation)?, + mutated: now, + }; + + >::insert(account.clone(), updated); + + 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..7090a3e86 100644 --- a/sense/src/mock.rs +++ b/sense/src/mock.rs @@ -3,8 +3,8 @@ use frame_support::parameter_types; use frame_system as system; use sp_core::H256; use sp_runtime::{ - testing::Header, - traits::{BlakeTwo256, IdentityLookup}, + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, }; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; @@ -12,57 +12,60 @@ type Block = frame_system::mocking::MockBlock; // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( - pub enum Test where - Block = Block, - NodeBlock = Block, - UncheckedExtrinsic = UncheckedExtrinsic, - { - System: frame_system::{Pallet, Call, Config, Storage, Event}, - ZeroSense: pallet_sense::{Pallet, Call, Storage, Event}, - } + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + ZeroSense: pallet_sense::{Pallet, Call, Storage, Event}, + } ); parameter_types! { - pub const BlockHashCount: u64 = 250; - pub const SS58Prefix: u8 = 42; - pub BlockWeights: frame_system::limits::BlockWeights = - frame_system::limits::BlockWeights::simple_max(1024); - pub static ExistentialDeposit: u64 = 0; + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(1024); + pub static ExistentialDeposit: u64 = 0; } impl system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type Origin = Origin; - type Call = Call; - type Index = u64; - type BlockNumber = u64; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type Header = Header; - type Event = Event; - type BlockHashCount = BlockHashCount; - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = (); - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = SS58Prefix; - type OnSetCode = (); + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); } impl pallet_sense::Config for Test { - type Event = Event; - type ForceOrigin = frame_system::EnsureRoot; - type WeightInfo = (); + type Event = Event; + type ForceOrigin = frame_system::EnsureRoot; + type WeightInfo = (); } // Build genesis storage according to the mock runtime. pub fn new_test_ext() -> sp_io::TestExternalities { - system::GenesisConfig::default().build_storage::().unwrap().into() + system::GenesisConfig::default() + .build_storage::() + .unwrap() + .into() } diff --git a/sense/src/tests.rs b/sense/src/tests.rs index 84f877ffe..6df633439 100644 --- a/sense/src/tests.rs +++ b/sense/src/tests.rs @@ -1,71 +1,72 @@ -use super::{mock::*, Error, EntityProperty, Entity, Sense, SenseXP, SenseREP, SenseTrust, Event as ZeroEvent}; +use super::{ + mock::*, Entity, EntityProperty, Error, Event as ZeroEvent, Sense, 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]; + new_test_ext().execute_with(|| { + let cid = vec![1, 2, 3]; - let account = 1; - let index = 0; - let block_number = 3; + let account = 1; + let index = 0; + let block_number = 3; - System::set_block_number(block_number); + System::set_block_number(block_number); - assert_noop!(ZeroSense::create_entity( - RawOrigin::Root.into(), 1, vec![]), - Error::::InvalidParam - ); - assert_noop!(ZeroSense::create_entity( - RawOrigin::Root.into(), 1, vec![1u8; 257]), - Error::::ParamLimitExceed - ); + assert_noop!( + ZeroSense::create_entity(RawOrigin::Root.into(), 1, vec![]), + Error::::InvalidParam + ); + assert_noop!( + ZeroSense::create_entity(RawOrigin::Root.into(), 1, vec![1u8; 257]), + Error::::ParamLimitExceed + ); - assert_ok!(ZeroSense::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) - ); + assert_ok!(ZeroSense::create_entity( + RawOrigin::Root.into(), + account, + cid.clone() + )); - assert_eq!( - System::events(), - vec![EventRecord { - phase: Phase::Initialization, - event: Event::ZeroSense(ZeroEvent::EntityInit(account, block_number)), - topics: vec![], - }] - ); + 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) + ); - // 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_eq!( + System::events(), + vec![EventRecord { + phase: Phase::Initialization, + event: Event::ZeroSense(ZeroEvent::EntityInit(account, block_number)), + topics: vec![], + }] + ); - assert_noop!(ZeroSense::create_entity( - RawOrigin::Root.into(), account, cid.clone()), - Error::::EntityExists - ); + // 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()), + Error::::EntityExists + ); + }); } // TODO: 1. Test: StorageMap value updated after calling extrinsic (SenseXP etc.) @@ -80,20 +81,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( 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 +105,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, ZeroSense::mod_xp, + sense_mod_rep: SenseREP, ZeroSense::mod_rep, + sense_mod_trust: SenseTrust, ZeroSense::mod_trust, } diff --git a/sense/src/weights.rs b/sense/src/weights.rs index 967c51460..69ce70f7a 100644 --- a/sense/src/weights.rs +++ b/sense/src/weights.rs @@ -35,66 +35,68 @@ // --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. pub trait WeightInfo { - fn create_entity() -> Weight; - fn mod_xp() -> Weight; - fn mod_rep() -> Weight; - fn mod_trust() -> Weight; + fn create_entity() -> Weight; + fn mod_xp() -> Weight; + fn mod_rep() -> Weight; + fn mod_trust() -> Weight; } /// Weights for pallet_sense using the Substrate node and recommended hardware. pub struct SubstrateWeight(PhantomData); impl WeightInfo for SubstrateWeight { - fn create_entity() -> Weight { - (23_000_000 as Weight) - .saturating_add(T::DbWeight::get().reads(2 as Weight)) - .saturating_add(T::DbWeight::get().writes(5 as Weight)) - } - fn mod_xp() -> Weight { - (18_000_000 as Weight) - .saturating_add(T::DbWeight::get().reads(2 as Weight)) - .saturating_add(T::DbWeight::get().writes(1 as Weight)) - } - fn mod_rep() -> Weight { - (17_000_000 as Weight) - .saturating_add(T::DbWeight::get().reads(2 as Weight)) - .saturating_add(T::DbWeight::get().writes(1 as Weight)) - } - fn mod_trust() -> Weight { - (17_000_000 as Weight) - .saturating_add(T::DbWeight::get().reads(2 as Weight)) - .saturating_add(T::DbWeight::get().writes(1 as Weight)) - } + fn create_entity() -> Weight { + (23_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(5 as Weight)) + } + fn mod_xp() -> Weight { + (18_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + fn mod_rep() -> Weight { + (17_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + fn mod_trust() -> Weight { + (17_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } } // For backwards compatibility and tests impl WeightInfo for () { - fn create_entity() -> Weight { - (23_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(2 as Weight)) - .saturating_add(RocksDbWeight::get().writes(5 as Weight)) - } - fn mod_xp() -> Weight { - (18_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(2 as Weight)) - .saturating_add(RocksDbWeight::get().writes(1 as Weight)) - } - fn mod_rep() -> Weight { - (17_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(2 as Weight)) - .saturating_add(RocksDbWeight::get().writes(1 as Weight)) - } - fn mod_trust() -> Weight { - (17_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(2 as Weight)) - .saturating_add(RocksDbWeight::get().writes(1 as Weight)) - } -} \ No newline at end of file + fn create_entity() -> Weight { + (23_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(5 as Weight)) + } + fn mod_xp() -> Weight { + (18_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + fn mod_rep() -> Weight { + (17_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + fn mod_trust() -> Weight { + (17_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } +} diff --git a/support/src/lib.rs b/support/src/lib.rs index 596fcb5af..8dbf67408 100644 --- a/support/src/lib.rs +++ b/support/src/lib.rs @@ -1,9 +1,7 @@ #![cfg_attr(not(feature = "std"), no_std)] pub trait ControlPalletStorage { + fn body_controller(org: Hash) -> AccountId; - fn body_controller(org: Hash) -> AccountId; - - fn body_treasury(org: Hash) -> AccountId; - + fn body_treasury(org: Hash) -> AccountId; } From ff39da237b680697508d87fa9bf59fb88456bd53 Mon Sep 17 00:00:00 2001 From: vasylenko-yevhen Date: Thu, 24 Feb 2022 12:42:03 +0200 Subject: [PATCH 03/10] Signal pallet intermediate state --- signal/Cargo.toml | 63 +++ signal/src/lib.rs | 805 +++++++++++++++++++++++++++++++++++ signal/src/mock.rs | 116 +++++ signal/src/tests.rs | 304 +++++++++++++ signal/src/traits.rs | 26 ++ signal/src/voting_enums.rs | 45 ++ signal/src/voting_structs.rs | 21 + support/src/lib.rs | 23 +- 8 files changed, 1401 insertions(+), 2 deletions(-) create mode 100644 signal/Cargo.toml create mode 100644 signal/src/lib.rs create mode 100644 signal/src/mock.rs create mode 100644 signal/src/tests.rs create mode 100644 signal/src/traits.rs create mode 100644 signal/src/voting_enums.rs create mode 100644 signal/src/voting_structs.rs diff --git a/signal/Cargo.toml b/signal/Cargo.toml new file mode 100644 index 000000000..ad54c1d6a --- /dev/null +++ b/signal/Cargo.toml @@ -0,0 +1,63 @@ +# ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ +# ███░▄▄▄█░▄▄▀█░▄▀▄░█░▄▄█░▄▀█░▄▄▀█▀▄▄▀██ +# ███░█▄▀█░▀▀░█░█▄█░█░▄▄█░█░█░▀▀░█░██░██ +# ███▄▄▄▄█▄██▄█▄███▄█▄▄▄█▄▄██▄██▄██▄▄███ +# ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + +[package] +name = 'pallet-signal' +version = '0.1.0' +authors = ['zero.io','gamedao.co'] +repository = 'https://github.com/gamedaoco/gamedao-protocol' +edition = '2018' +license = 'GPL-3.0-or-later' +description = 'Signal pallet' + +[package.metadata.substrate] +categories = [ + 'zero', + 'core', + 'pallet' +] + +[dependencies] +serde = { version = "1.0.136", optional = true } +codec = { package = "parity-scale-codec", version = "2.3.1", default-features = false } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +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 } + +[dev-dependencies] +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" } +sp-io = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.13" } +pallet-randomness-collective-flip = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.13" } +orml-currencies = { path = "../../orml/currencies", default-features = false } + +[features] +default = ['std'] +runtime-benchmarks = ['frame-benchmarking'] +std = [ + 'codec/std', + 'serde/std', + 'scale-info/std', + + 'frame-support/std', + 'frame-system/std', + 'frame-benchmarking/std', + + 'sp-core/std', + 'sp-std/std', + 'sp-runtime/std', + + "orml-traits/std", + + "support/std" +] +try-runtime = ['frame-support/try-runtime'] \ No newline at end of file diff --git a/signal/src/lib.rs b/signal/src/lib.rs new file mode 100644 index 000000000..4a5e64f86 --- /dev/null +++ b/signal/src/lib.rs @@ -0,0 +1,805 @@ +// +// _______________________________ ________ +// \____ /\_ _____/\______ \\_____ \ +// / / | __)_ | _/ / | \ +// / /_ | \ | | \/ | \ +// /_______ \/_______ / |____|_ /\_______ / +// \/ \/ \/ \/ +// Z E R O . I O N E T W O R K +// © C O P Y R I O T 2 0 7 5 @ Z E R O . I O + +// This file is part of ZERO Network. +// Copyright (C) 2010-2020 ZERO Labs. +// SPDX-License-Identifier: Apache-2.0 + +#![cfg_attr(not(feature = "std"), no_std)] + +pub mod voting_enums; +pub mod voting_structs; +pub mod traits; + +#[cfg(test)] +pub mod mock; +#[cfg(test)] +mod tests; + +pub use pallet::*; + + +#[frame_support::pallet] +pub mod pallet { + use frame_system::{ + ensure_signed, + pallet_prelude::{OriginFor, BlockNumberFor}, + WeightInfo + }; + use frame_support::{ + dispatch::DispatchResult, + traits::{Randomness}, + pallet_prelude::* + }; + use sp_std::vec::Vec; + + // use orml_traits::{MultiCurrency, MultiReservableCurrency}; + use primitives::{Balance, CurrencyId}; + use support::{ControlPalletStorage, ControlState, ControlMemberState}; + use super::*; + use voting_enums::{ProposalState, ProposalType, VotingType}; + use voting_structs::{Proposal, ProposalMetadata}; + use traits::*; + + + // type Balance = <::Currency as Currency<::AccountId>>::Balance; + + #[pallet::config] + pub trait Config: frame_system::Config { + type Event: From> + IsType<::Event> + Into<::Event>; + // type Currency: MultiCurrency + // + MultiReservableCurrency; + type Randomness: Randomness; + type Control: ControlPalletStorage; + type Flow: Flow; + type ForceOrigin: EnsureOrigin; + type WeightInfo: WeightInfo; + + #[pallet::constant] + type MaxProposalsPerBlock: Get; // 3 + + #[pallet::constant] + type MaxProposalDuration: Get; // 864000, 60 * 60 * 24 * 30 / 3 + + #[pallet::constant] + type FundingCurrencyId: Get; + } + + + #[pallet::pallet] + pub struct Pallet(_); + + + /// Global status + #[pallet::storage] + pub(super) type Proposals = StorageMap<_, Blake2_128Concat, T::Hash, Proposal, ValueQuery>; + + #[pallet::storage] + pub(super) type Metadata = StorageMap<_, Blake2_128Concat, T::Hash, ProposalMetadata, ValueQuery>; + + #[pallet::storage] + pub(super) type Owners = StorageMap<_, Blake2_128Concat, T::Hash, T::AccountId, OptionQuery>; + + #[pallet::storage] + pub(super) type ProposalStates = StorageMap<_, Blake2_128Concat, T::Hash, ProposalState, ValueQuery, GetDefault>; + + /// Maximum time limit for a proposal + #[pallet::type_value] + pub(super) fn ProposalTimeLimitDefault() -> T::BlockNumber { T::BlockNumber::from(T::MaxProposalDuration::get()) } + #[pallet::storage] + pub(super) type ProposalTimeLimit = StorageValue <_, T::BlockNumber, ValueQuery, ProposalTimeLimitDefault>; + + /// All proposals + #[pallet::storage] + pub(super) type ProposalsArray = StorageMap<_, Blake2_128Concat, u64, T::Hash, ValueQuery>; + + #[pallet::storage] + pub(super) type ProposalsCount = StorageValue<_, u64, ValueQuery>; + + #[pallet::storage] + pub(super) type ProposalsIndex = StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery>; + + /// Proposals by campaign / org + #[pallet::storage] + pub(super) type ProposalsByContextArray = StorageMap<_, Blake2_128Concat, (T::Hash, u64), T::Hash, ValueQuery>; + + #[pallet::storage] + pub(super) type ProposalsByContextCount = StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery>; + + #[pallet::storage] + pub(super) type ProposalsByContextIndex = StorageMap<_, Blake2_128Concat, (T::Hash, T::Hash), u64, ValueQuery>; + + /// all proposals for a given context + #[pallet::storage] + pub(super) type ProposalsByContext = StorageMap<_, Blake2_128Concat, T::Hash, Vec, ValueQuery>; + + /// Proposals by owner + #[pallet::storage] + pub(super) type ProposalsByOwnerArray = StorageMap<_, Blake2_128Concat, (T::AccountId, u64), T::Hash, ValueQuery>; + + #[pallet::storage] + pub(super) type ProposalsByOwnerCount = StorageMap<_, Blake2_128Concat, T::AccountId, u64, ValueQuery>; + + #[pallet::storage] + pub(super) type ProposalsByOwnerIndex = StorageMap<_, Blake2_128Concat, (T::AccountId, T::Hash), u64, ValueQuery>; + + /// Proposals where voter participated + #[pallet::storage] + pub(super) type ProposalsByVoter = StorageMap<_, Blake2_128Concat, T::AccountId, Vec<(T::Hash, bool)>, ValueQuery>; + + /// Proposal voters and votes by proposal + #[pallet::storage] + pub(super) type ProposalVotesByVoters = StorageMap<_, Blake2_128Concat, T::Hash, Vec<(T::AccountId, bool)>, ValueQuery>; + + /// Total proposals voted on by voter + #[pallet::storage] + pub(super) type ProposalsByVoterCount = StorageMap<_, Blake2_128Concat, T::AccountId, u64, ValueQuery>; + + /// Proposals ending in a block + #[pallet::storage] + pub(super) type ProposalsByBlock = StorageMap<_, Blake2_128Concat, T::BlockNumber, Vec, ValueQuery>; + + /// The amount of currency that a project has used + #[pallet::storage] + pub(super) type CampaignBalanceUsed = StorageMap<_, Blake2_128Concat, T::Hash, Balance, ValueQuery>; + + /// The number of people who approve a proposal + #[pallet::storage] + pub(super) type ProposalApprovers = StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery, GetDefault>; + + /// The number of people who deny a proposal + #[pallet::storage] + pub(super) type ProposalDeniers = StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery, GetDefault>; + + /// Voters per proposal + #[pallet::storage] + pub(super) type ProposalVoters = StorageMap<_, Blake2_128Concat, T::Hash, Vec, ValueQuery>; + + /// Voter count per proposal + #[pallet::storage] + pub(super) type ProposalVotes = StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery, GetDefault>; + + /// Ack vs Nack + #[pallet::storage] + pub(super) type ProposalSimpleVotes = StorageMap<_, Blake2_128Concat, T::Hash, (u64, u64), ValueQuery, GetDefault>; + + /// User has voted on a proposal + #[pallet::storage] + pub(super) type VotedBefore = StorageMap<_, Blake2_128Concat, (T::AccountId, T::Hash), bool, ValueQuery, GetDefault>; + + // TODO: ProposalTotalEligibleVoters + // TODO: ProposalApproversWeight + // TODO: ProposalDeniersWeight + // TODO: ProposalTotalEligibleWeight + + /// The total number of proposals + #[pallet::storage] + pub(super) type Nonce = StorageValue<_, u128, ValueQuery>; + + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + Proposal(T::AccountId, T::Hash), + ProposalCreated(T::AccountId, T::Hash, T::Hash, Balance, T::BlockNumber), + ProposalVoted(T::AccountId, T::Hash, bool), + ProposalFinalized(T::Hash, u8), + ProposalApproved(T::Hash), + ProposalRejected(T::Hash), + ProposalExpired(T::Hash), + ProposalAborted(T::Hash), + ProposalError(T::Hash, Vec), + WithdrawalGranted(T::Hash, T::Hash, T::Hash), + } + + #[pallet::error] + pub enum Error { + /// Proposal Ended + ProposalEnded, + /// Proposal Exists + ProposalExists, + /// Proposal Expired + ProposalExpired, + /// Already Voted + AlreadyVoted, + /// Proposal Unknown + ProposalUnknown, + /// DAO Inactive + DAOInactive, + /// Authorization Error + AuthorizationError, + /// Tangram Creation Failed + TangramCreationError, + /// Out Of Bounds Error + OutOfBounds, + /// Unknown Error + UnknownError, + ///MemberExists + MemberExists, + /// Unknown Campaign + CampaignUnknown, + /// Campaign Failed + CampaignFailed, + /// Balance Too Low + BalanceInsufficient, + /// Hash Collision + HashCollision, + /// Unknown Account + UnknownAccount, + /// Too Many Proposals for block + TooManyProposals, + /// Overflow Error + OverflowError, + /// Division Error + DivisionError + } + + #[pallet::call] + impl Pallet { + + #[pallet::weight(10_000)] + pub fn simple_one(origin: OriginFor) -> DispatchResult { + Ok(()) + } + + // TODO: general proposal for a DAO + #[pallet::weight(5_000_000)] + pub fn general_proposal( + origin: OriginFor, + context_id: T::Hash, + title: Vec, + cid: Vec, + start: T::BlockNumber, + expiry: T::BlockNumber + ) -> DispatchResult { + + let sender = ensure_signed(origin)?; + + // active/existing dao? + ensure!(T::Control::body_state(&context_id) == ControlState::Active, Error::::DAOInactive); + + // member of body? + let member = T::Control::body_member_state(&context_id, &sender); + ensure!(member == ControlMemberState::Active, Error::::AuthorizationError); + + // ensure that start and expiry are in bounds + let current_block = >::block_number(); + // ensure!(start > current_block, Error::::OutOfBounds ); + ensure!(expiry > current_block, Error::::OutOfBounds ); + ensure!(expiry <= current_block + >::get(), Error::::OutOfBounds ); + + // ensure that number of proposals + // ending in target block + // do not exceed the maximum + let proposals = >::get(expiry); + // ensure!(proposals.len() as u32 < T::MaxProposalsPerBlock::get(), "Maximum number of proposals is reached for the target block, try another block"); + ensure!((proposals.len() as u32) < T::MaxProposalsPerBlock::get(), Error::::TooManyProposals); // todo: was error generated manually on purpose? + + let proposal_type = ProposalType::General; + let proposal_state = ProposalState::Active; + let voting_type = VotingType::Simple; + // let nonce = >::get(); + + // generate unique id + let phrase = b"just another proposal"; + let proposal_id = ::random(phrase).0; + // ensure!(!>::contains_key(&context_id), "Proposal id already exists"); + ensure!(!>::contains_key(&context_id), Error::::ProposalExists); // todo: was error generated manually on purpose? + + // proposal + + let new_proposal = Proposal { + proposal_id: proposal_id.clone(), + context_id: context_id.clone(), + proposal_type, + voting_type, + start, + expiry, + }; + + // metadata + + let metadata = ProposalMetadata { + title: title, + cid: cid, + amount: 0 + }; + + // + // + // + + // check add + let proposals_count = >::get(); + let updated_proposals_count = proposals_count.checked_add(1).ok_or( Error::::OverflowError)?; + let proposals_by_campaign_count = >::get(&context_id); + let updated_proposals_by_campaign_count = proposals_by_campaign_count.checked_add(1).ok_or( Error::::OverflowError )?; + let proposals_by_owner_count = >::get(&sender); + let updated_proposals_by_owner_count = proposals_by_owner_count.checked_add(1).ok_or( Error::::OverflowError )?; + + // insert proposals + >::insert(proposal_id.clone(), new_proposal.clone()); + >::insert(proposal_id.clone(), metadata.clone()); + >::insert(proposal_id.clone(), sender.clone()); + >::insert(proposal_id.clone(), proposal_state); + // update max per block + >::mutate(expiry, |proposals| proposals.push(proposal_id.clone())); + // update proposal map + >::insert(&proposals_count, proposal_id.clone()); + >::put(updated_proposals_count); + >::insert(proposal_id.clone(), proposals_count); + // update campaign map + >::insert((context_id.clone(), proposals_by_campaign_count.clone()), proposal_id.clone()); + >::insert(context_id.clone(), updated_proposals_by_campaign_count); + >::insert((context_id.clone(), proposal_id.clone()), proposals_by_campaign_count); + >::mutate( context_id.clone(), |proposals| proposals.push(proposal_id.clone()) ); + // update owner map + >::insert((sender.clone(), proposals_by_owner_count.clone()), proposal_id.clone()); + >::insert(sender.clone(), updated_proposals_by_owner_count); + >::insert((sender.clone(), proposal_id.clone()), proposals_by_owner_count); + // init votes + >::insert(context_id, (0,0)); + + // + // + // + + // nonce++ + >::mutate(|n| *n += 1); // todo: use safe addition + + // deposit event + Self::deposit_event(Event::::Proposal(sender,proposal_id)); + Ok(()) + } + + + // TODO: membership proposal for a DAO + + #[pallet::weight(5_000_000)] + pub fn membership_proposal( + origin: OriginFor, + context: T::Hash, + _member: T::Hash, + _action: u8, + _start: T::BlockNumber, + _expiry: T::BlockNumber + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + // ensure active + // ensure member + // match action + // action + // deposit event + Self::deposit_event(Event::::Proposal(sender, context)); + Ok(()) + } + + + // create a withdrawal proposal + // origin must be controller of the campaign == controller of the dao + // beneficiary must be the treasury of the dao + + #[pallet::weight(5_000_000)] + pub fn withdraw_proposal( + origin: OriginFor, + context_id: T::Hash, + title: Vec, + cid: Vec, + amount: Balance, + start: T::BlockNumber, + expiry: T::BlockNumber, + ) -> DispatchResult { + + let sender = ensure_signed(origin)?; + + // A C C E S S + + // ensure!( T::Flow::campaign_by_id(&context_id), Error::::CampaignUnknown ); + let state = T::Flow::campaign_state(&context_id); + ensure!( state == FlowState::Success, Error::::CampaignFailed ); + // let owner = flow::Module::::campaign_owner(&context_id); + // ensure!( sender == owner, Error::::AuthorizationError ); + + // B O U N D S + + // let current_block = >::block_number(); + // ensure!(start > current_block, Error::::OutOfBounds ); + // ensure!(expiry > start, Error::::OutOfBounds ); + // ensure!(expiry <= current_block + Self::proposal_time_limit(), Error::::OutOfBounds ); + + // B A L A N C E + + let used_balance = >::get(&context_id); + let total_balance = T::Flow::campaign_balance(&context_id); + let remaining_balance = total_balance - used_balance; + ensure!(remaining_balance >= amount, Error::::BalanceInsufficient ); + + // T R A F F I C + + let proposals = >::get(expiry); + ensure!((proposals.len() as u32) < T::MaxProposalsPerBlock::get(), Error::::TooManyProposals ); + + // C O N F I G + + let proposal_type = ProposalType::Withdrawal; // treasury + let voting_type = VotingType::Simple; // votes + // let nonce = >::get(); + let phrase = b"just another withdrawal"; + + let proposal_id = ::Randomness::random(phrase).0; + ensure!(!>::contains_key(&context_id), Error::::HashCollision ); + + let proposal = Proposal { + proposal_id: proposal_id.clone(), + context_id: context_id.clone(), + proposal_type, + voting_type, + start, + expiry + }; + + let metadata = ProposalMetadata { + title, + cid, + amount, + }; + + // C O U N T S + + let proposals_count = >::get(); + let updated_proposals_count = proposals_count.checked_add(1).ok_or(Error::::OverflowError)?; + let proposals_by_campaign_count = >::get(&context_id); + let updated_proposals_by_campaign_count = proposals_by_campaign_count.checked_add(1).ok_or(Error::::OverflowError)?; + let proposals_by_owner_count = >::get(&sender); + let updated_proposals_by_owner_count = proposals_by_owner_count.checked_add(1).ok_or(Error::::OverflowError)?; + + // W R I T E + + Proposals::::insert(&proposal_id, proposal.clone()); + >::insert(&proposal_id, metadata.clone()); + >::insert(&proposal_id, sender.clone()); + >::insert(proposal_id.clone(), ProposalState::Active); + + >::mutate(expiry, |proposals| proposals.push(proposal_id.clone())); + >::insert(&proposals_count, proposal_id.clone()); + >::put(updated_proposals_count); + >::insert(proposal_id.clone(), proposals_count); + >::insert((context_id.clone(), proposals_by_campaign_count.clone()), proposal_id.clone()); + >::insert(context_id.clone(), updated_proposals_by_campaign_count); + >::insert((context_id.clone(), proposal_id.clone()), proposals_by_campaign_count); + >::insert((sender.clone(), proposals_by_owner_count.clone()), proposal_id.clone()); + >::insert(sender.clone(), updated_proposals_by_owner_count); + >::insert((sender.clone(), proposal_id.clone()), proposals_by_owner_count); + >::mutate( context_id.clone(), |proposals| proposals.push(proposal_id.clone()) ); + + // ++ + + >::mutate(|n| *n += 1); // todo: safe nonce increase + + // E V E N T + + Self::deposit_event( + Event::::ProposalCreated(sender, context_id, proposal_id, amount, expiry) + ); + Ok(()) + + } + + // TODO: + // voting vs staking, e.g. + // 1. token weighted and democratic voting require yes/no + // 2. conviction voting requires ongoing staking + // 3. quadratic voting + + #[pallet::weight(5_000_000)] + pub fn simple_vote( + origin: OriginFor, + proposal_id: T::Hash, + vote: bool + ) -> DispatchResult { + + let sender = ensure_signed(origin)?; + + // Ensure the proposal exists + ensure!(>::contains_key(&proposal_id), Error::::ProposalUnknown); + + // Ensure the proposal has not ended + let proposal_state = >::get(&proposal_id); + ensure!(proposal_state == ProposalState::Active, Error::::ProposalEnded); + + // Ensure the contributor did not vote before + ensure!(!>::get((sender.clone(), proposal_id.clone())), Error::::AlreadyVoted); + + // Get the proposal + let proposal = >::get(&proposal_id); + // Ensure the proposal is not expired + ensure!(>::block_number() < proposal.expiry, Error::::ProposalExpired); + + // TODO: + // ensure origin is one of: + // a. member when the proposal is general + // b. contributor when the proposal is a withdrawal request + // let sender_balance = >::campaign_contribution(proposal.campaign_id, sender.clone()); + // ensure!( sender_balance > T::Balance::from(0), "You are not a contributor of this Campaign"); + + match &proposal.proposal_type { + // DAO Democratic Proposal + // simply one member one vote yes / no, + // TODO: ratio definable, now > 50% majority wins + ProposalType::General => { + + let (mut yes, mut no) = >::get(&proposal_id); + + match vote { + true => { + yes = yes.checked_add(1).ok_or(Error::::OverflowError)?; + let proposal_approvers = >::get(&proposal_id); + let updated_proposal_approvers = proposal_approvers.checked_add(1).ok_or(Error::::OverflowError)?; + >::insert( + proposal_id.clone(), + updated_proposal_approvers.clone() + ); + }, + false => { + no = no.checked_add(1).ok_or(Error::::OverflowError)?; + let proposal_deniers = >::get(&proposal_id); + let updated_proposal_deniers = proposal_deniers.checked_add(1).ok_or(Error::::OverflowError)?; + >::insert( + proposal_id.clone(), + updated_proposal_deniers.clone() + ); + } + } + + >::insert( + proposal_id.clone(), + (yes,no) + ); + + }, + // 50% majority over total number of campaign contributors + ProposalType::Withdrawal => { + + let (mut yes, mut no) = >::get(&proposal_id); + + match vote { + true => { + yes = yes.checked_add(1).ok_or(Error::::OverflowError)?; + + let current_approvers = >::get(&proposal_id); + let updated_approvers = current_approvers.checked_add(1).ok_or(Error::::OverflowError)?; + >::insert(proposal_id.clone(), updated_approvers.clone()); + + // TODO: make this variable + let contributors = T::Flow::campaign_contributors_count(&proposal.context_id); + let threshold = contributors.checked_div(2).ok_or(Error::::DivisionError)?; + if updated_approvers > threshold { + Self::unlock_balance(proposal_id, updated_approvers)?; + } + // remove + let proposal_approvers = >::get(&proposal_id); + let updated_proposal_approvers = proposal_approvers.checked_add(1).ok_or(Error::::OverflowError)?; + >::insert( + proposal_id.clone(), + updated_proposal_approvers.clone() + ); + + }, + false => { + no = no.checked_add(1).ok_or(Error::::OverflowError)?; + // remove + let proposal_deniers = >::get(&proposal_id); + let updated_proposal_deniers = proposal_deniers.checked_add(1).ok_or(Error::::OverflowError)?; + >::insert( + proposal_id.clone(), + updated_proposal_deniers.clone() + ); + } + } + + ProposalSimpleVotes::::insert( + proposal_id.clone(), + (yes,no) + ); + + + }, + + // Campaign Token Weighted Proposal + // total token balance yes vs no + // TODO: ratio definable, now > 50% majority wins + // ProposalType:: => { + // }, + + // Membership Voting + // simply one token one vote yes / no, + // TODO: ratio definable, now simple majority wins + ProposalType::Member => { + // approve + // deny + // kick + // ban + }, + // default + _ => { + }, + } + + VotedBefore::::insert( ( &sender, proposal_id.clone() ), true ); + ProposalsByVoterCount::::mutate( &sender, |v| *v +=1 ); + ProposalVotesByVoters::::mutate(&proposal_id, |votings| votings.push(( sender.clone(), vote.clone() )) ); + ProposalsByVoter::::mutate( &sender, |votings| votings.push((proposal_id.clone(), vote))); + + let mut voters = ProposalVoters::::get(&proposal_id); + match voters.binary_search(&sender) { + Ok(_) => {}, // should never happen + Err(index) => { + voters.insert(index, sender.clone()); + ProposalVoters::::insert( &proposal_id, voters ); + } + } + + // dispatch vote event + Self::deposit_event( + Event::::ProposalVoted( + sender, + proposal_id.clone(), + vote + ) + ); + Ok(()) + + } + + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_finalize(_n: T::BlockNumber) { + + // i'm still jenny from the block + let block_number = _n.clone(); + let proposal_hashes = >::get(block_number); + + for proposal_id in &proposal_hashes { + + let mut proposal_state = >::get(&proposal_id); + if proposal_state != ProposalState::Active { continue }; + + let proposal = >::get(&proposal_id); + + // TODO: + // a. result( accepted, rejected ) + // b. result( accepted, rejected, total_allowed ) + // c. result( required_majority, staked_accept, staked_reject, slash_amount ) + // d. threshold reached + // e. conviction + + match &proposal.proposal_type { + ProposalType::General => { + // simple vote + let (yes,no) = >::get(&proposal_id); + if yes > no { proposal_state = ProposalState::Accepted; } + if yes < no { proposal_state = ProposalState::Rejected; } + if yes == 0 && no == 0 { proposal_state = ProposalState::Expired; } + }, + ProposalType::Withdrawal => { + // treasury + // 50% majority of eligible voters + let (yes,_no) = >::get(&proposal_id); + let context = proposal.context_id.clone(); + let contributors = T::Flow::campaign_contributors_count(&context); + // TODO: dynamic threshold + let threshold = contributors.checked_div(2).ok_or(Error::::DivisionError); + match threshold { + Ok(t) => { + if yes > t { + proposal_state = ProposalState::Accepted; + Self::unlock_balance( proposal.proposal_id, yes ); + } else { + proposal_state = ProposalState::Rejected; + } + }, + Err(_err) => { } + } + }, + ProposalType::Member => { + // membership + // + }, + _ => { + // no result - fail + proposal_state = ProposalState::Expired; + } + } + + >::insert(&proposal_id, proposal_state.clone()); + + match proposal_state { + ProposalState::Accepted => { + Self::deposit_event( + Event::::ProposalApproved(proposal_id.clone()) + ); + }, + ProposalState::Rejected => { + Self::deposit_event( + Event::::ProposalRejected(proposal_id.clone()) + ); + }, + ProposalState::Expired => { + Self::deposit_event( + Event::::ProposalExpired(proposal_id.clone()) + ); + }, + _ => {} + } + + } + + } + } + + impl Pallet { + + // TODO: DISCUSSION + // withdrawal proposals are accepted + // when the number of approvals is higher + // than the number of rejections + // accepted / denied >= 1 + fn unlock_balance( + proposal_id: T::Hash, + _supported_count: u64 + ) -> DispatchResult { + + // Get proposal and metadata + let proposal = >::get(proposal_id.clone()); + let metadata = >::get(proposal_id.clone()); + + // Ensure sufficient balance + let proposal_balance = metadata.amount; + let total_balance = T::Flow::campaign_balance(&proposal.context_id); + + // let used_balance = Self::balance_used(proposal.context_id); + let used_balance = >::get(proposal.context_id); + let available_balance = total_balance - used_balance.clone(); + ensure!(available_balance >= proposal_balance, Error::::BalanceInsufficient ); + + // Get the owner of the campaign + let _owner = >::get(&proposal_id).ok_or("No owner for proposal")?; + + // get treasury account for related body and unlock balance + let body = T::Flow::campaign_org(&proposal.context_id); + let treasury_account = T::Control::body_treasury(&body); + // T::Currency::unreserve( + // T::FundingCurrencyId::get(), + // &treasury_account, + // proposal_balance + // ); + + // Change the used amount + let new_used_balance = used_balance + proposal_balance; + >::insert(proposal.context_id, new_used_balance); + + // proposal completed + let proposal_state = ProposalState::Finalized; + >::insert(proposal_id.clone(), proposal_state); + + >::insert(proposal_id.clone(), proposal.clone()); + + Self::deposit_event( + Event::::WithdrawalGranted(proposal_id, proposal.context_id, body) + ); + Ok(()) + + } + } +} + +// todo: Check storage fields and remove generices from those, who don't use Config trait \ No newline at end of file diff --git a/signal/src/mock.rs b/signal/src/mock.rs new file mode 100644 index 000000000..c545d0b8c --- /dev/null +++ b/signal/src/mock.rs @@ -0,0 +1,116 @@ +#[cfg(test)] + +use crate as pallet_signal; +use frame_support::parameter_types; +use frame_system as system; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + // BuildStorage, +}; +// use orml_currencies::{Currency}; +use crate::traits::{Flow, FlowState}; +use support::{ControlPalletStorage, ControlState, ControlMemberState}; +use primitives::{Balance, CurrencyId, Hash, TokenSymbol}; + +type AccountId = u64; +pub const ACC1: AccountId = 1; +pub const ACC2: AccountId = 2; + +pub struct ControlMock; +// impl ControlPalletStorage for ControlMock { +impl ControlPalletStorage for ControlMock { + + fn body_controller(org: &Hash) -> AccountId { ACC1 } + fn body_treasury(org: &Hash) -> AccountId { ACC2 } + fn body_member_state(hash: &Hash, account_id: &AccountId) -> ControlMemberState { ControlMemberState::Active } + fn body_state(hash: &Hash) -> ControlState { ControlState::Active } +} + +pub struct FlowMock; +impl Flow for FlowMock { + fn campaign_balance(hash: &Hash) -> Balance { Default::default() } + fn campaign_state(hash: &Hash) -> FlowState { FlowState::Active } + fn campaign_contributors_count(hash: &Hash) -> u64 { 0 } + fn campaign_org(hash: &Hash) -> Hash { Default::default() } +} + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Signal: pallet_signal::{Pallet, Call, Storage, Event}, + RandomnessCollectiveFlip: pallet_randomness_collective_flip::{Pallet, Storage}, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(1024); + pub static ExistentialDeposit: u64 = 0; + + pub const MaxProposalsPerBlock: u32 = 1; + pub const MaxProposalDuration: u32 = 20; + pub const FundingCurrencyId: CurrencyId = TokenSymbol::GAME as u32; +} + +impl pallet_randomness_collective_flip::Config for Test {} + +impl system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = Hash; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); +} + +impl pallet_signal::Config for Test { + type Event = Event; + type ForceOrigin = frame_system::EnsureRoot; + type WeightInfo = (); + type Control = ControlMock; + type Flow = FlowMock; + type MaxProposalsPerBlock = MaxProposalsPerBlock; + type MaxProposalDuration = MaxProposalDuration; + type FundingCurrencyId = FundingCurrencyId; + type Randomness = RandomnessCollectiveFlip; + // type Currency = Currency; +} + +#[derive(Default)] +pub struct ExtBuilder; +impl ExtBuilder { + pub fn build(self) -> sp_io::TestExternalities { + let t = system::GenesisConfig::default().build_storage::().unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} diff --git a/signal/src/tests.rs b/signal/src/tests.rs new file mode 100644 index 000000000..dbac63edf --- /dev/null +++ b/signal/src/tests.rs @@ -0,0 +1,304 @@ +#[cfg(test)] + +use super::{ + Pallet as SignalPallet, + Error, + Event, + mock::{ACC1, ACC2, ExtBuilder, System, Origin, Test} +}; +// use mock::*; +use sp_core::H256; +use frame_support::assert_ok; + + +#[test] +fn general_proposal_ok() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(3); + // assert_eq!(System::block_number(), 1); + assert_ok!(SignalPallet::::general_proposal(Origin::signed(ACC1), H256::random(), vec![1,2,3], vec![1,2,3], 3, 15)); + let event = >::events().pop() + .expect("Expected at least one EventRecord to be found").event; + }); +} + +// #[test] +// fn create_works() { +// new_test_ext().execute_with(|| { +// // Now try to create a crowdfund campaign +// assert_ok!(Crowdfund::create(Origin::signed(1), 2, 1000, 9)); +// assert_eq!(Crowdfund::fund_count(), 1); +// // This is what the initial `fund_info` should look like +// let fund_info = FundInfo { +// beneficiary: 2, +// deposit: 1, +// raised: 0, +// // 5 blocks length + 3 block ending period + 1 starting block +// end: 9, +// goal: 1000, +// }; +// 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); +// }); +// } + +// #[test] +// fn create_handles_insufficient_balance() { +// new_test_ext().execute_with(|| { +// assert_noop!( +// Crowdfund::create(Origin::signed(1337), 2, 1000, 9), +// BalancesError::::InsufficientBalance +// ); +// }); +// } + +// #[test] +// fn contribute_works() { +// new_test_ext().execute_with(|| { +// // Set up a crowdfund +// assert_ok!(Crowdfund::create(Origin::signed(1), 2, 1000, 9)); +// assert_eq!(Balances::free_balance(1), 999); +// assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 1); + +// // No contributions yet +// assert_eq!(Crowdfund::contribution_get(0, &1), 0); + +// // User 1 contributes to their own crowdfund +// assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 49)); +// // User 1 has spent some funds to do this, transfer fees **are** taken +// assert_eq!(Balances::free_balance(1), 950); +// // Contributions are stored in the trie +// assert_eq!(Crowdfund::contribution_get(0, &1), 49); +// // Contributions appear in free balance of crowdfund +// assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 50); +// // Last contribution time recorded +// assert_eq!(Crowdfund::funds(0).unwrap().raised, 49); +// }); +// } + +// #[test] +// fn contribute_handles_basic_errors() { +// new_test_ext().execute_with(|| { +// // Cannot contribute to non-existing fund +// assert_noop!( +// Crowdfund::contribute(Origin::signed(1), 0, 49), +// Error::::InvalidIndex +// ); +// // Cannot contribute below minimum contribution +// assert_noop!( +// Crowdfund::contribute(Origin::signed(1), 0, 9), +// Error::::ContributionTooSmall +// ); + +// // Set up a crowdfund +// assert_ok!(Crowdfund::create(Origin::signed(1), 2, 1000, 9)); +// assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 101)); + +// // Move past end date +// run_to_block(10); + +// // Cannot contribute to ended fund +// assert_noop!( +// Crowdfund::contribute(Origin::signed(1), 0, 49), +// Error::::ContributionPeriodOver +// ); +// }); +// } + +// #[test] +// fn withdraw_works() { +// new_test_ext().execute_with(|| { +// // Set up a crowdfund +// assert_ok!(Crowdfund::create(Origin::signed(1), 2, 1000, 9)); +// // Transfer fees are taken here +// assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 100)); +// assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 200)); +// assert_ok!(Crowdfund::contribute(Origin::signed(3), 0, 300)); + +// // Skip all the way to the end +// // Crowdfund is unsuccessful 100 + 200 + 300 < 1000 +// run_to_block(50); + +// // User can withdraw their full balance without fees +// assert_ok!(Crowdfund::withdraw(Origin::signed(1), 0)); +// assert_eq!(Balances::free_balance(1), 999); + +// assert_ok!(Crowdfund::withdraw(Origin::signed(2), 0)); +// assert_eq!(Balances::free_balance(2), 2000); + +// assert_ok!(Crowdfund::withdraw(Origin::signed(3), 0)); +// assert_eq!(Balances::free_balance(3), 3000); +// }); +// } + +// #[test] +// fn withdraw_handles_basic_errors() { +// new_test_ext().execute_with(|| { +// // Set up a crowdfund +// assert_ok!(Crowdfund::create(Origin::signed(1), 2, 1000, 9)); +// // Transfer fee is taken here +// assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 49)); +// assert_eq!(Balances::free_balance(1), 950); + +// run_to_block(5); + +// // Cannot withdraw before fund ends +// assert_noop!( +// Crowdfund::withdraw(Origin::signed(1), 0), +// Error::::FundStillActive +// ); + +// // Skip to the retirement period +// // Crowdfund is unsuccessful 100 + 200 + 300 < 1000 +// run_to_block(10); + +// // Cannot withdraw if they did not contribute +// assert_noop!( +// Crowdfund::withdraw(Origin::signed(2), 0), +// Error::::NoContribution +// ); +// // Cannot withdraw from a non-existent fund +// assert_noop!( +// Crowdfund::withdraw(Origin::signed(1), 1), +// Error::::InvalidIndex +// ); +// }); +// } + +// #[test] +// fn dissolve_works() { +// new_test_ext().execute_with(|| { +// // Set up a crowdfund +// assert_ok!(Crowdfund::create(Origin::signed(1), 2, 1000, 9)); +// // Transfer fee is taken here +// assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 100)); +// assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 200)); +// assert_ok!(Crowdfund::contribute(Origin::signed(3), 0, 300)); + +// // Skip all the way to the end +// // Crowdfund is unsuccessful 100 + 200 + 300 < 1000 +// run_to_block(50); + +// // Check initiator's balance. +// assert_eq!(Balances::free_balance(1), 899); +// // Check current funds (contributions + deposit) +// assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 601); + +// // Account 7 dissolves the crowdfund claiming the remaining funds +// assert_ok!(Crowdfund::dissolve(Origin::signed(7), 0)); + +// // Fund account is emptied +// assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 0); +// // Dissolver account is rewarded +// assert_eq!(Balances::free_balance(7), 601); + +// // Storage trie is removed +// assert_eq!(Crowdfund::contribution_get(0, &0), 0); +// // Fund storage is removed +// assert_eq!(Crowdfund::funds(0), None); +// }); +// } + +// #[test] +// fn dissolve_handles_basic_errors() { +// new_test_ext().execute_with(|| { +// // Set up a crowdfund +// assert_ok!(Crowdfund::create(Origin::signed(1), 2, 1000, 9)); +// // Transfer fee is taken here +// assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 100)); +// assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 200)); +// assert_ok!(Crowdfund::contribute(Origin::signed(3), 0, 300)); + +// // Cannot dissolve an invalid fund index +// assert_noop!( +// Crowdfund::dissolve(Origin::signed(1), 1), +// Error::::InvalidIndex +// ); +// // Cannot dissolve an active fund +// assert_noop!( +// Crowdfund::dissolve(Origin::signed(1), 0), +// Error::::FundNotRetired +// ); + +// run_to_block(10); + +// // Cannot disolve an ended but not yet retired fund +// assert_noop!( +// Crowdfund::dissolve(Origin::signed(1), 0), +// Error::::FundNotRetired +// ); +// }); +// } + +// #[test] +// fn dispense_works() { +// new_test_ext().execute_with(|| { +// // Set up a crowdfund +// assert_ok!(Crowdfund::create(Origin::signed(1), 20, 1000, 9)); +// // Transfer fee is taken here +// assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 100)); +// assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 200)); +// assert_ok!(Crowdfund::contribute(Origin::signed(3), 0, 300)); +// assert_ok!(Crowdfund::contribute(Origin::signed(3), 0, 400)); + +// // Skip to the retirement period +// // Crowdfund is successful 100 + 200 + 300 + 400 >= 1000 +// run_to_block(10); + +// // Check initiator's balance. +// assert_eq!(Balances::free_balance(1), 899); +// // Check current funds (contributions + deposit) +// assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 1001); + +// // Account 7 dispenses the crowdfund +// assert_ok!(Crowdfund::dispense(Origin::signed(7), 0)); + +// // Fund account is emptied +// assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 0); +// // Beneficiary account is funded +// assert_eq!(Balances::free_balance(20), 1000); +// // Dispensor account is rewarded deposit +// assert_eq!(Balances::free_balance(7), 1); + +// // Storage trie is removed +// assert_eq!(Crowdfund::contribution_get(0, &0), 0); +// // Fund storage is removed +// assert_eq!(Crowdfund::funds(0), None); +// }); +// } + +// #[test] +// fn dispense_handles_basic_errors() { +// new_test_ext().execute_with(|| { +// // Set up a crowdfund +// assert_ok!(Crowdfund::create(Origin::signed(1), 2, 1000, 9)); +// // Transfer fee is taken here +// assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 100)); +// assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 200)); +// assert_ok!(Crowdfund::contribute(Origin::signed(3), 0, 300)); + +// // Cannot dispense an invalid fund index +// assert_noop!( +// Crowdfund::dispense(Origin::signed(1), 1), +// Error::::InvalidIndex +// ); +// // Cannot dispense an active fund +// assert_noop!( +// Crowdfund::dispense(Origin::signed(1), 0), +// Error::::FundStillActive +// ); + +// // Skip to the retirement period +// // Crowdfund is unsuccessful 100 + 200 + 300 < 1000 +// run_to_block(10); + +// // Cannot disopens an ended but unsuccessful fund +// assert_noop!( +// Crowdfund::dispense(Origin::signed(1), 0), +// Error::::UnsuccessfulFund +// ); +// }); +// } diff --git a/signal/src/traits.rs b/signal/src/traits.rs new file mode 100644 index 000000000..0328bad1c --- /dev/null +++ b/signal/src/traits.rs @@ -0,0 +1,26 @@ +pub trait Flow { + fn campaign_balance(hash: &Hash) -> Balance; + fn campaign_state(hash: &Hash) -> FlowState; + fn campaign_contributors_count(hash: &Hash) -> u64; + fn campaign_org(hash: &Hash) -> Hash; +} + +#[derive(PartialEq)] +pub enum FlowState { + Init = 0, + Active = 1, + Paused = 2, + Success = 3, + Failed = 4, + Locked = 5, +} + + +pub struct ImplPlaceholder; + +impl Flow for ImplPlaceholder { + fn campaign_balance(_hash: &Hash) -> Balance { Default::default() } + fn campaign_state(_hash: &Hash) -> FlowState { FlowState::Failed } + fn campaign_contributors_count(_hash: &Hash) -> u64 { Default::default() } + fn campaign_org(_hash: &Hash) -> Hash { Default::default() } +} \ No newline at end of file diff --git a/signal/src/voting_enums.rs b/signal/src/voting_enums.rs new file mode 100644 index 000000000..29915fbd4 --- /dev/null +++ b/signal/src/voting_enums.rs @@ -0,0 +1,45 @@ +use frame_support::pallet_prelude::{Encode, Decode}; +use scale_info::TypeInfo; + +// #[derive(Encode, Decode, Clone, PartialEq, Default, Eq, PartialOrd, Ord, TypeInfo)] +#[derive(Encode, Decode, PartialEq, Clone, TypeInfo)] +pub enum ProposalState { + Init = 0, // waiting for start block + Active = 1, // voting is active + Accepted = 2, // voters did approve + Rejected = 3, // voters did not approve + Expired = 4, // ended without votes + Aborted = 5, // sudo abort + Finalized = 6, // accepted withdrawal proposal is processed +} + +#[derive(Encode, Decode, PartialEq, Clone, TypeInfo)] +pub enum ProposalType { + General = 0, + Multiple = 1, + Member = 2, + Withdrawal = 3, + Spending = 4 +} + +#[derive(Encode, Decode, PartialEq, Clone, TypeInfo)] +pub enum VotingType { + Simple = 0, // votes across participating votes + Token = 1, // weight across participating votes + Absolute = 2, // votes vs all eligible voters + Quadratic = 3, + Ranked = 4, + Conviction = 5 +} + +impl Default for ProposalState { + fn default() -> Self { ProposalState::Init } +} + +impl Default for ProposalType { + fn default() -> Self { ProposalType::General } +} + +impl Default for VotingType { + fn default() -> Self { VotingType::Simple } +} \ No newline at end of file diff --git a/signal/src/voting_structs.rs b/signal/src/voting_structs.rs new file mode 100644 index 000000000..d3c8b554c --- /dev/null +++ b/signal/src/voting_structs.rs @@ -0,0 +1,21 @@ +use frame_support::pallet_prelude::{Encode, Decode}; +use sp_std::vec::Vec; +use scale_info::TypeInfo; + + +#[derive(Encode, Decode, Default, Clone, PartialEq, TypeInfo)] +pub struct Proposal { + pub proposal_id: Hash, + pub context_id: Hash, + pub proposal_type: ProposalType, + pub voting_type: VotingType, + pub start: BlockNumber, + pub expiry: BlockNumber +} + +#[derive(Encode, Decode, Default, Clone, PartialEq, TypeInfo)] +pub struct ProposalMetadata { + pub title: Vec, + pub cid: Vec, + pub amount: Balance, +} \ No newline at end of file diff --git a/support/src/lib.rs b/support/src/lib.rs index 8dbf67408..1843f732c 100644 --- a/support/src/lib.rs +++ b/support/src/lib.rs @@ -1,7 +1,26 @@ #![cfg_attr(not(feature = "std"), no_std)] +#[derive(PartialEq)] +pub enum ControlMemberState { + Inactive = 0, // eg inactive after threshold period + Active = 1, // active + Pending = 2, // application voting pending + Kicked = 3, + Banned = 4, + Exited = 5, +} + +#[derive(PartialEq)] +pub enum ControlState { + Inactive = 0, + Active = 1, + Locked = 2, +} + pub trait ControlPalletStorage { - fn body_controller(org: Hash) -> AccountId; - fn body_treasury(org: Hash) -> AccountId; + fn body_controller(org: &Hash) -> AccountId; + fn body_treasury(org: &Hash) -> AccountId; + fn body_member_state(hash: &Hash, account_id: &AccountId) -> ControlMemberState; + fn body_state(hash: &Hash) -> ControlState; } From 31499611ac714cdb30471fb78d31affa2c05ac1c Mon Sep 17 00:00:00 2001 From: vasylenko-yevhen Date: Wed, 2 Mar 2022 19:44:39 +0200 Subject: [PATCH 04/10] Tests for `general_proposal` extrinsic --- signal/Cargo.toml | 1 + signal/src/lib.rs | 57 ++-- signal/src/mock.rs | 60 ++++- signal/src/tests.rs | 507 +++++++++++++++-------------------- signal/src/voting_enums.rs | 3 + signal/src/voting_structs.rs | 2 + 6 files changed, 305 insertions(+), 325 deletions(-) diff --git a/signal/Cargo.toml b/signal/Cargo.toml index ad54c1d6a..3dd24a11a 100644 --- a/signal/Cargo.toml +++ b/signal/Cargo.toml @@ -38,6 +38,7 @@ sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkad sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } sp-io = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.13" } pallet-randomness-collective-flip = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.13" } +frame-support-test = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.13" } orml-currencies = { path = "../../orml/currencies", default-features = false } [features] diff --git a/signal/src/lib.rs b/signal/src/lib.rs index 4a5e64f86..788e5b218 100644 --- a/signal/src/lib.rs +++ b/signal/src/lib.rs @@ -36,7 +36,8 @@ pub mod pallet { use frame_support::{ dispatch::DispatchResult, traits::{Randomness}, - pallet_prelude::* + pallet_prelude::*, + transactional }; use sp_std::vec::Vec; @@ -187,7 +188,10 @@ pub mod pallet { #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - Proposal(T::AccountId, T::Hash), + Proposal { + sender_id: T::AccountId, + proposal_id: T::Hash + }, ProposalCreated(T::AccountId, T::Hash, T::Hash, Balance, T::BlockNumber), ProposalVoted(T::AccountId, T::Hash, bool), ProposalFinalized(T::Hash, u8), @@ -244,13 +248,10 @@ pub mod pallet { #[pallet::call] impl Pallet { - #[pallet::weight(10_000)] - pub fn simple_one(origin: OriginFor) -> DispatchResult { - Ok(()) - } // TODO: general proposal for a DAO #[pallet::weight(5_000_000)] + #[transactional] pub fn general_proposal( origin: OriginFor, context_id: T::Hash, @@ -285,16 +286,22 @@ pub mod pallet { let proposal_type = ProposalType::General; let proposal_state = ProposalState::Active; let voting_type = VotingType::Simple; - // let nonce = >::get(); - - // generate unique id - let phrase = b"just another proposal"; - let proposal_id = ::random(phrase).0; // ensure!(!>::contains_key(&context_id), "Proposal id already exists"); ensure!(!>::contains_key(&context_id), Error::::ProposalExists); // todo: was error generated manually on purpose? + + // check add + let proposals_count = >::get(); + let updated_proposals_count = proposals_count.checked_add(1).ok_or( Error::::OverflowError)?; + let proposals_by_campaign_count = >::get(&context_id); + let updated_proposals_by_campaign_count = proposals_by_campaign_count.checked_add(1).ok_or( Error::::OverflowError )?; + let proposals_by_owner_count = >::get(&sender); + let updated_proposals_by_owner_count = proposals_by_owner_count.checked_add(1).ok_or( Error::::OverflowError )?; + // proposal + let nonce = Self::get_and_increment_nonce(); + let (proposal_id, _) = ::random(&nonce); let new_proposal = Proposal { proposal_id: proposal_id.clone(), context_id: context_id.clone(), @@ -316,14 +323,6 @@ pub mod pallet { // // - // check add - let proposals_count = >::get(); - let updated_proposals_count = proposals_count.checked_add(1).ok_or( Error::::OverflowError)?; - let proposals_by_campaign_count = >::get(&context_id); - let updated_proposals_by_campaign_count = proposals_by_campaign_count.checked_add(1).ok_or( Error::::OverflowError )?; - let proposals_by_owner_count = >::get(&sender); - let updated_proposals_by_owner_count = proposals_by_owner_count.checked_add(1).ok_or( Error::::OverflowError )?; - // insert proposals >::insert(proposal_id.clone(), new_proposal.clone()); >::insert(proposal_id.clone(), metadata.clone()); @@ -352,10 +351,10 @@ pub mod pallet { // // nonce++ - >::mutate(|n| *n += 1); // todo: use safe addition + // >::mutate(|n| *n += 1); // todo: use safe addition // deposit event - Self::deposit_event(Event::::Proposal(sender,proposal_id)); + Self::deposit_event(Event::::Proposal{sender_id: sender, proposal_id}); Ok(()) } @@ -377,7 +376,7 @@ pub mod pallet { // match action // action // deposit event - Self::deposit_event(Event::::Proposal(sender, context)); + Self::deposit_event(Event::::Proposal{sender_id: sender, proposal_id: context}); Ok(()) } @@ -430,10 +429,10 @@ pub mod pallet { let proposal_type = ProposalType::Withdrawal; // treasury let voting_type = VotingType::Simple; // votes - // let nonce = >::get(); - let phrase = b"just another withdrawal"; + let nonce = Self::get_and_increment_nonce(); + // let phrase = b"just another withdrawal"; - let proposal_id = ::Randomness::random(phrase).0; + let (proposal_id, _) = ::Randomness::random(&nonce.encode()); ensure!(!>::contains_key(&context_id), Error::::HashCollision ); let proposal = Proposal { @@ -481,7 +480,7 @@ pub mod pallet { // ++ - >::mutate(|n| *n += 1); // todo: safe nonce increase + // >::mutate(|n| *n += 1); // todo: safe nonce increase // E V E N T @@ -799,6 +798,12 @@ pub mod pallet { Ok(()) } + + fn get_and_increment_nonce() -> Vec { + let nonce = Nonce::::get(); + Nonce::::put(nonce.wrapping_add(1)); + nonce.encode() + } } } diff --git a/signal/src/mock.rs b/signal/src/mock.rs index c545d0b8c..dd0dd4d42 100644 --- a/signal/src/mock.rs +++ b/signal/src/mock.rs @@ -3,6 +3,8 @@ use crate as pallet_signal; use frame_support::parameter_types; use frame_system as system; +use frame_support_test::TestRandomness; +use sp_std::cell::RefCell; use sp_runtime::{ testing::Header, traits::{BlakeTwo256, IdentityLookup}, @@ -17,22 +19,55 @@ type AccountId = u64; pub const ACC1: AccountId = 1; pub const ACC2: AccountId = 2; +pub struct ControlFixture { + pub body_controller: AccountId, + pub body_treasury: AccountId, + pub body_member_state: ControlMemberState, + pub body_state: ControlState +} +impl ControlFixture { + pub fn body_controller(&mut self, value: AccountId) { + self.body_controller = value; + } + pub fn body_state(&mut self, value: ControlState) { + self.body_state = value; + } + pub fn body_member_state(&mut self, value: ControlMemberState) { + self.body_member_state = value; + } +} +thread_local!( + pub static control_fixture: RefCell = RefCell::new(ControlFixture { + body_controller: ACC1, + body_treasury: ACC2, + body_member_state: ControlMemberState::Active, + body_state: ControlState::Active + }); +); + + pub struct ControlMock; -// impl ControlPalletStorage for ControlMock { impl ControlPalletStorage for ControlMock { - fn body_controller(org: &Hash) -> AccountId { ACC1 } - fn body_treasury(org: &Hash) -> AccountId { ACC2 } - fn body_member_state(hash: &Hash, account_id: &AccountId) -> ControlMemberState { ControlMemberState::Active } - fn body_state(hash: &Hash) -> ControlState { ControlState::Active } + fn body_controller(_org: &Hash) -> AccountId { control_fixture.with(|v| v.borrow().body_controller.clone()) } + fn body_treasury(_org: &Hash) -> AccountId { control_fixture.with(|v| v.borrow().body_treasury.clone()) } + fn body_member_state(_hash: &Hash, _account_id: &AccountId) -> ControlMemberState { control_fixture.with(|v| v.borrow().body_member_state.clone()) } + fn body_state(_hash: &Hash) -> ControlState { control_fixture.with(|v| v.borrow().body_state.clone()) } } +// impl ControlPalletStorage for ControlMock { + +// fn body_controller(_org: &Hash) -> AccountId { ACC1 } +// fn body_treasury(_org: &Hash) -> AccountId { ACC2 } +// fn body_member_state(_hash: &Hash, _account_id: &AccountId) -> ControlMemberState { ControlMemberState::Active } +// fn body_state(_hash: &Hash) -> ControlState { ControlState::Active } +// } pub struct FlowMock; impl Flow for FlowMock { - fn campaign_balance(hash: &Hash) -> Balance { Default::default() } - fn campaign_state(hash: &Hash) -> FlowState { FlowState::Active } - fn campaign_contributors_count(hash: &Hash) -> u64 { 0 } - fn campaign_org(hash: &Hash) -> Hash { Default::default() } + fn campaign_balance(_hash: &Hash) -> Balance { Default::default() } + fn campaign_state(_hash: &Hash) -> FlowState { FlowState::Active } + fn campaign_contributors_count(_hash: &Hash) -> u64 { 0 } + fn campaign_org(_hash: &Hash) -> Hash { Default::default() } } type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; @@ -47,7 +82,6 @@ frame_support::construct_runtime!( { System: frame_system::{Pallet, Call, Config, Storage, Event}, Signal: pallet_signal::{Pallet, Call, Storage, Event}, - RandomnessCollectiveFlip: pallet_randomness_collective_flip::{Pallet, Storage}, } ); @@ -58,12 +92,12 @@ parameter_types! { frame_system::limits::BlockWeights::simple_max(1024); pub static ExistentialDeposit: u64 = 0; - pub const MaxProposalsPerBlock: u32 = 1; + pub const MaxProposalsPerBlock: u32 = 2; pub const MaxProposalDuration: u32 = 20; pub const FundingCurrencyId: CurrencyId = TokenSymbol::GAME as u32; } -impl pallet_randomness_collective_flip::Config for Test {} +// impl pallet_randomness_collective_flip::Config for Test {} impl system::Config for Test { type BaseCallFilter = frame_support::traits::Everything; @@ -100,7 +134,7 @@ impl pallet_signal::Config for Test { type MaxProposalsPerBlock = MaxProposalsPerBlock; type MaxProposalDuration = MaxProposalDuration; type FundingCurrencyId = FundingCurrencyId; - type Randomness = RandomnessCollectiveFlip; + type Randomness = TestRandomness; // type Currency = Currency; } diff --git a/signal/src/tests.rs b/signal/src/tests.rs index dbac63edf..16e370161 100644 --- a/signal/src/tests.rs +++ b/signal/src/tests.rs @@ -2,303 +2,238 @@ use super::{ Pallet as SignalPallet, + Proposals, + Metadata, + Owners, + ProposalsByBlock, + ProposalStates, + ProposalsCount, + ProposalsIndex, + ProposalsArray, + ProposalsByContextArray, + ProposalsByContextCount, + ProposalsByContextIndex, + ProposalsByOwnerArray, + ProposalsByOwnerCount, + ProposalsByOwnerIndex, + ProposalsByContext, + ProposalTimeLimit, + Nonce, + Config, Error, - Event, - mock::{ACC1, ACC2, ExtBuilder, System, Origin, Test} + Event as SignalEvent, + voting_structs::{Proposal, ProposalMetadata}, + voting_enums::{VotingType, ProposalType, ProposalState}, + mock::{ACC1, ACC2, ExtBuilder, System, Origin, Test, Event, control_fixture} }; -// use mock::*; +use support::{ControlPalletStorage, ControlState, ControlMemberState}; +use sp_runtime::traits::BadOrigin; use sp_core::H256; -use frame_support::assert_ok; +use frame_support::{assert_ok, assert_noop, traits::{Randomness}}; +use frame_system; + #[test] -fn general_proposal_ok() { +fn general_proposal_success() { ExtBuilder::default().build().execute_with(|| { System::set_block_number(3); - // assert_eq!(System::block_number(), 1); - assert_ok!(SignalPallet::::general_proposal(Origin::signed(ACC1), H256::random(), vec![1,2,3], vec![1,2,3], 3, 15)); + let nonce = vec![0]; + let (proposal_id, _): (H256, _) = ::Randomness::random(&nonce); + let ctx_id = H256::random(); + assert_ok!( + SignalPallet::::general_proposal( + Origin::signed(ACC1), + ctx_id, // context id + vec![1,2,3], // title + vec![1,2,3], // cid + 3, // start + 15 // expiry + ) + ); let event = >::events().pop() - .expect("Expected at least one EventRecord to be found").event; + .expect("No event generated").event; + assert_eq!( + event, + Event::from( + SignalEvent::Proposal { + sender_id: ACC1, + proposal_id: proposal_id + } + ) + ); + assert_eq!( + >::get(&proposal_id), + Proposal { + proposal_id, + context_id: ctx_id, + proposal_type: ProposalType::General, + voting_type: VotingType::Simple, + start: 3, + expiry: 15 + } + ); + assert_eq!( + >::get(&proposal_id), + ProposalMetadata { + title: vec![1,2,3], + cid: vec![1,2,3], + amount: 0 + } + ); + assert_eq!(>::get(&proposal_id), Some(ACC1)); + assert_eq!(>::get(&proposal_id), ProposalState::Active); + assert_eq!(>::get(15), vec![proposal_id.clone()]); + assert_eq!(>::get(0), proposal_id); + assert_eq!(>::get(), 1); + assert_eq!(>::get(&proposal_id), 0); + assert_eq!(>::get((ctx_id.clone(), 0)), proposal_id); + assert_eq!(>::get(ctx_id), 1); + assert_eq!(>::get((ctx_id, proposal_id)), 0); + assert_eq!(>::get((ACC1, 0)), proposal_id); + assert_eq!(>::get(ACC1), 1); + assert_eq!(>::get((ACC1, proposal_id)), 0); + assert_eq!(>::get(ctx_id), vec![proposal_id.clone()]); + assert_eq!(>::get(), 1); + + + let nonce = vec![1]; + let (new_proposal_id, _): (H256, _) = ::Randomness::random(&nonce); + assert_ok!( + SignalPallet::::general_proposal( + Origin::signed(ACC1), + ctx_id, // context id + vec![2,3,4], // title + vec![2,3,4], // cid + 3, // start + 15 // expiry + ) + ); + assert_eq!( + >::get(15), + vec![proposal_id.clone(), new_proposal_id.clone()] + ); + assert_eq!(>::get(1), new_proposal_id); + assert_eq!(>::get(), 2); + assert_eq!(>::get(&new_proposal_id), 1); + assert_eq!(>::get((ctx_id.clone(), 1)), new_proposal_id); + assert_eq!(>::get(ctx_id), 2); + assert_eq!(>::get((ctx_id, new_proposal_id)), 1); + assert_eq!(>::get((ACC1, 1)), new_proposal_id); + assert_eq!(>::get(ACC1), 2); + assert_eq!(>::get((ACC1, new_proposal_id)), 1); + assert_eq!( + >::get(ctx_id), + vec![proposal_id.clone(), new_proposal_id.clone()] + ); + assert_eq!(>::get(), 2); }); } -// #[test] -// fn create_works() { -// new_test_ext().execute_with(|| { -// // Now try to create a crowdfund campaign -// assert_ok!(Crowdfund::create(Origin::signed(1), 2, 1000, 9)); -// assert_eq!(Crowdfund::fund_count(), 1); -// // This is what the initial `fund_info` should look like -// let fund_info = FundInfo { -// beneficiary: 2, -// deposit: 1, -// raised: 0, -// // 5 blocks length + 3 block ending period + 1 starting block -// end: 9, -// goal: 1000, -// }; -// 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); -// }); -// } - -// #[test] -// fn create_handles_insufficient_balance() { -// new_test_ext().execute_with(|| { -// assert_noop!( -// Crowdfund::create(Origin::signed(1337), 2, 1000, 9), -// BalancesError::::InsufficientBalance -// ); -// }); -// } - -// #[test] -// fn contribute_works() { -// new_test_ext().execute_with(|| { -// // Set up a crowdfund -// assert_ok!(Crowdfund::create(Origin::signed(1), 2, 1000, 9)); -// assert_eq!(Balances::free_balance(1), 999); -// assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 1); - -// // No contributions yet -// assert_eq!(Crowdfund::contribution_get(0, &1), 0); - -// // User 1 contributes to their own crowdfund -// assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 49)); -// // User 1 has spent some funds to do this, transfer fees **are** taken -// assert_eq!(Balances::free_balance(1), 950); -// // Contributions are stored in the trie -// assert_eq!(Crowdfund::contribution_get(0, &1), 49); -// // Contributions appear in free balance of crowdfund -// assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 50); -// // Last contribution time recorded -// assert_eq!(Crowdfund::funds(0).unwrap().raised, 49); -// }); -// } - -// #[test] -// fn contribute_handles_basic_errors() { -// new_test_ext().execute_with(|| { -// // Cannot contribute to non-existing fund -// assert_noop!( -// Crowdfund::contribute(Origin::signed(1), 0, 49), -// Error::::InvalidIndex -// ); -// // Cannot contribute below minimum contribution -// assert_noop!( -// Crowdfund::contribute(Origin::signed(1), 0, 9), -// Error::::ContributionTooSmall -// ); - -// // Set up a crowdfund -// assert_ok!(Crowdfund::create(Origin::signed(1), 2, 1000, 9)); -// assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 101)); - -// // Move past end date -// run_to_block(10); - -// // Cannot contribute to ended fund -// assert_noop!( -// Crowdfund::contribute(Origin::signed(1), 0, 49), -// Error::::ContributionPeriodOver -// ); -// }); -// } - -// #[test] -// fn withdraw_works() { -// new_test_ext().execute_with(|| { -// // Set up a crowdfund -// assert_ok!(Crowdfund::create(Origin::signed(1), 2, 1000, 9)); -// // Transfer fees are taken here -// assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 100)); -// assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 200)); -// assert_ok!(Crowdfund::contribute(Origin::signed(3), 0, 300)); - -// // Skip all the way to the end -// // Crowdfund is unsuccessful 100 + 200 + 300 < 1000 -// run_to_block(50); - -// // User can withdraw their full balance without fees -// assert_ok!(Crowdfund::withdraw(Origin::signed(1), 0)); -// assert_eq!(Balances::free_balance(1), 999); - -// assert_ok!(Crowdfund::withdraw(Origin::signed(2), 0)); -// assert_eq!(Balances::free_balance(2), 2000); - -// assert_ok!(Crowdfund::withdraw(Origin::signed(3), 0)); -// assert_eq!(Balances::free_balance(3), 3000); -// }); -// } - -// #[test] -// fn withdraw_handles_basic_errors() { -// new_test_ext().execute_with(|| { -// // Set up a crowdfund -// assert_ok!(Crowdfund::create(Origin::signed(1), 2, 1000, 9)); -// // Transfer fee is taken here -// assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 49)); -// assert_eq!(Balances::free_balance(1), 950); - -// run_to_block(5); - -// // Cannot withdraw before fund ends -// assert_noop!( -// Crowdfund::withdraw(Origin::signed(1), 0), -// Error::::FundStillActive -// ); - -// // Skip to the retirement period -// // Crowdfund is unsuccessful 100 + 200 + 300 < 1000 -// run_to_block(10); - -// // Cannot withdraw if they did not contribute -// assert_noop!( -// Crowdfund::withdraw(Origin::signed(2), 0), -// Error::::NoContribution -// ); -// // Cannot withdraw from a non-existent fund -// assert_noop!( -// Crowdfund::withdraw(Origin::signed(1), 1), -// Error::::InvalidIndex -// ); -// }); -// } - -// #[test] -// fn dissolve_works() { -// new_test_ext().execute_with(|| { -// // Set up a crowdfund -// assert_ok!(Crowdfund::create(Origin::signed(1), 2, 1000, 9)); -// // Transfer fee is taken here -// assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 100)); -// assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 200)); -// assert_ok!(Crowdfund::contribute(Origin::signed(3), 0, 300)); - -// // Skip all the way to the end -// // Crowdfund is unsuccessful 100 + 200 + 300 < 1000 -// run_to_block(50); - -// // Check initiator's balance. -// assert_eq!(Balances::free_balance(1), 899); -// // Check current funds (contributions + deposit) -// assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 601); - -// // Account 7 dissolves the crowdfund claiming the remaining funds -// assert_ok!(Crowdfund::dissolve(Origin::signed(7), 0)); - -// // Fund account is emptied -// assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 0); -// // Dissolver account is rewarded -// assert_eq!(Balances::free_balance(7), 601); - -// // Storage trie is removed -// assert_eq!(Crowdfund::contribution_get(0, &0), 0); -// // Fund storage is removed -// assert_eq!(Crowdfund::funds(0), None); -// }); -// } - -// #[test] -// fn dissolve_handles_basic_errors() { -// new_test_ext().execute_with(|| { -// // Set up a crowdfund -// assert_ok!(Crowdfund::create(Origin::signed(1), 2, 1000, 9)); -// // Transfer fee is taken here -// assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 100)); -// assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 200)); -// assert_ok!(Crowdfund::contribute(Origin::signed(3), 0, 300)); - -// // Cannot dissolve an invalid fund index -// assert_noop!( -// Crowdfund::dissolve(Origin::signed(1), 1), -// Error::::InvalidIndex -// ); -// // Cannot dissolve an active fund -// assert_noop!( -// Crowdfund::dissolve(Origin::signed(1), 0), -// Error::::FundNotRetired -// ); - -// run_to_block(10); - -// // Cannot disolve an ended but not yet retired fund -// assert_noop!( -// Crowdfund::dissolve(Origin::signed(1), 0), -// Error::::FundNotRetired -// ); -// }); -// } - -// #[test] -// fn dispense_works() { -// new_test_ext().execute_with(|| { -// // Set up a crowdfund -// assert_ok!(Crowdfund::create(Origin::signed(1), 20, 1000, 9)); -// // Transfer fee is taken here -// assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 100)); -// assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 200)); -// assert_ok!(Crowdfund::contribute(Origin::signed(3), 0, 300)); -// assert_ok!(Crowdfund::contribute(Origin::signed(3), 0, 400)); - -// // Skip to the retirement period -// // Crowdfund is successful 100 + 200 + 300 + 400 >= 1000 -// run_to_block(10); - -// // Check initiator's balance. -// assert_eq!(Balances::free_balance(1), 899); -// // Check current funds (contributions + deposit) -// assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 1001); - -// // Account 7 dispenses the crowdfund -// assert_ok!(Crowdfund::dispense(Origin::signed(7), 0)); - -// // Fund account is emptied -// assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 0); -// // Beneficiary account is funded -// assert_eq!(Balances::free_balance(20), 1000); -// // Dispensor account is rewarded deposit -// assert_eq!(Balances::free_balance(7), 1); - -// // Storage trie is removed -// assert_eq!(Crowdfund::contribution_get(0, &0), 0); -// // Fund storage is removed -// assert_eq!(Crowdfund::funds(0), None); -// }); -// } - -// #[test] -// fn dispense_handles_basic_errors() { -// new_test_ext().execute_with(|| { -// // Set up a crowdfund -// assert_ok!(Crowdfund::create(Origin::signed(1), 2, 1000, 9)); -// // Transfer fee is taken here -// assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 100)); -// assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 200)); -// assert_ok!(Crowdfund::contribute(Origin::signed(3), 0, 300)); - -// // Cannot dispense an invalid fund index -// assert_noop!( -// Crowdfund::dispense(Origin::signed(1), 1), -// Error::::InvalidIndex -// ); -// // Cannot dispense an active fund -// assert_noop!( -// Crowdfund::dispense(Origin::signed(1), 0), -// Error::::FundStillActive -// ); +#[test] +fn general_proposal_error() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(3); + let nonce = vec![0]; + let (proposal_id, _): (H256, _) = ::Randomness::random(&nonce); + let ctx_id = H256::random(); + + >::insert(ctx_id, Proposal { + proposal_id: proposal_id, + context_id: ctx_id, + proposal_type: ProposalType::General, + voting_type: VotingType::Simple, + start: 2, + expiry: 13 + }); + assert_noop!( + SignalPallet::::general_proposal( + Origin::signed(ACC1), + ctx_id, // context id + vec![1,2,3], // title + vec![1,2,3], // cid + 3, // start + 15 // expiry + ), + Error::::ProposalExists + ); + + + let proposal_ids = vec![H256::random(), H256::random()]; + >::insert(15, proposal_ids); + assert_noop!( + SignalPallet::::general_proposal( + Origin::signed(ACC1), + ctx_id, // context id + vec![1,2,3], // title + vec![1,2,3], // cid + 3, // start + 15 // expiry + ), + Error::::TooManyProposals + ); + + assert_noop!( + SignalPallet::::general_proposal( + Origin::signed(ACC1), + ctx_id, // context id + vec![1,2,3], // title + vec![1,2,3], // cid + 3, // start + System::block_number() + >::get() + 1 + ), + Error::::OutOfBounds + ); + assert_noop!( + SignalPallet::::general_proposal( + Origin::signed(ACC1), + ctx_id, // context id + vec![1,2,3], // title + vec![1,2,3], // cid + System::block_number(), // start + System::block_number() // expiry + ), + Error::::OutOfBounds + ); + + control_fixture.with(|val|val.borrow_mut().body_member_state(ControlMemberState::Inactive)); + assert_noop!( + SignalPallet::::general_proposal( + Origin::signed(ACC1), + ctx_id, // context id + vec![1,2,3], // title + vec![1,2,3], // cid + 3, // start + 15 // expiry + ), + Error::::AuthorizationError + ); + + control_fixture.with(|val|val.borrow_mut().body_state(ControlState::Inactive)); + assert_noop!( + SignalPallet::::general_proposal( + Origin::signed(ACC1), + ctx_id, // context id + vec![1,2,3], // title + vec![1,2,3], // cid + 3, // start + 15 // expiry + ), + Error::::DAOInactive + ); + + assert_noop!( + SignalPallet::::general_proposal( + Origin::none(), + ctx_id, // context id + vec![1,2,3], // title + vec![1,2,3], // cid + 3, // start + 15 // expiry + ), + BadOrigin + ); -// // Skip to the retirement period -// // Crowdfund is unsuccessful 100 + 200 + 300 < 1000 -// run_to_block(10); -// // Cannot disopens an ended but unsuccessful fund -// assert_noop!( -// Crowdfund::dispense(Origin::signed(1), 0), -// Error::::UnsuccessfulFund -// ); -// }); -// } + }); +} diff --git a/signal/src/voting_enums.rs b/signal/src/voting_enums.rs index 29915fbd4..cc69d6e03 100644 --- a/signal/src/voting_enums.rs +++ b/signal/src/voting_enums.rs @@ -3,6 +3,7 @@ use scale_info::TypeInfo; // #[derive(Encode, Decode, Clone, PartialEq, Default, Eq, PartialOrd, Ord, TypeInfo)] #[derive(Encode, Decode, PartialEq, Clone, TypeInfo)] +#[cfg_attr(feature = "std", derive(Debug))] pub enum ProposalState { Init = 0, // waiting for start block Active = 1, // voting is active @@ -14,6 +15,7 @@ pub enum ProposalState { } #[derive(Encode, Decode, PartialEq, Clone, TypeInfo)] +#[cfg_attr(feature = "std", derive(Debug))] pub enum ProposalType { General = 0, Multiple = 1, @@ -23,6 +25,7 @@ pub enum ProposalType { } #[derive(Encode, Decode, PartialEq, Clone, TypeInfo)] +#[cfg_attr(feature = "std", derive(Debug))] pub enum VotingType { Simple = 0, // votes across participating votes Token = 1, // weight across participating votes diff --git a/signal/src/voting_structs.rs b/signal/src/voting_structs.rs index d3c8b554c..b0b108088 100644 --- a/signal/src/voting_structs.rs +++ b/signal/src/voting_structs.rs @@ -4,6 +4,7 @@ use scale_info::TypeInfo; #[derive(Encode, Decode, Default, Clone, PartialEq, TypeInfo)] +#[cfg_attr(feature = "std", derive(Debug))] pub struct Proposal { pub proposal_id: Hash, pub context_id: Hash, @@ -14,6 +15,7 @@ pub struct Proposal { } #[derive(Encode, Decode, Default, Clone, PartialEq, TypeInfo)] +#[cfg_attr(feature = "std", derive(Debug))] pub struct ProposalMetadata { pub title: Vec, pub cid: Vec, From 508bc350b6231e7ddaf12676542f959b653b8820 Mon Sep 17 00:00:00 2001 From: vasylenko-yevhen Date: Thu, 3 Mar 2022 23:11:42 +0200 Subject: [PATCH 05/10] Tests for all extrinsics --- signal/src/lib.rs | 86 ++++---- signal/src/mock.rs | 56 +++--- signal/src/tests.rs | 470 ++++++++++++++++++++++++++++++++++++++++--- signal/src/traits.rs | 26 --- support/Cargo.toml | 13 +- support/src/lib.rs | 38 +++- 6 files changed, 572 insertions(+), 117 deletions(-) delete mode 100644 signal/src/traits.rs diff --git a/signal/src/lib.rs b/signal/src/lib.rs index 788e5b218..10d001f7b 100644 --- a/signal/src/lib.rs +++ b/signal/src/lib.rs @@ -16,7 +16,6 @@ pub mod voting_enums; pub mod voting_structs; -pub mod traits; #[cfg(test)] pub mod mock; @@ -43,11 +42,13 @@ pub mod pallet { // use orml_traits::{MultiCurrency, MultiReservableCurrency}; use primitives::{Balance, CurrencyId}; - use support::{ControlPalletStorage, ControlState, ControlMemberState}; + use support::{ + ControlPalletStorage, ControlState, ControlMemberState, + FlowPalletStorage, FlowState + }; use super::*; use voting_enums::{ProposalState, ProposalType, VotingType}; use voting_structs::{Proposal, ProposalMetadata}; - use traits::*; // type Balance = <::Currency as Currency<::AccountId>>::Balance; @@ -59,7 +60,7 @@ pub mod pallet { // + MultiReservableCurrency; type Randomness: Randomness; type Control: ControlPalletStorage; - type Flow: Flow; + type Flow: FlowPalletStorage; type ForceOrigin: EnsureOrigin; type WeightInfo: WeightInfo; @@ -75,6 +76,7 @@ pub mod pallet { #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] pub struct Pallet(_); @@ -192,8 +194,18 @@ pub mod pallet { sender_id: T::AccountId, proposal_id: T::Hash }, - ProposalCreated(T::AccountId, T::Hash, T::Hash, Balance, T::BlockNumber), - ProposalVoted(T::AccountId, T::Hash, bool), + ProposalCreated { + sender_id: T::AccountId, + context_id: T::Hash, + proposal_id: T::Hash, + amount: Balance, + expiry: T::BlockNumber, + }, + ProposalVoted { + sender_id: T::AccountId, + proposal_id: T::Hash, + vote: bool + }, ProposalFinalized(T::Hash, u8), ProposalApproved(T::Hash), ProposalRejected(T::Hash), @@ -403,11 +415,13 @@ pub mod pallet { // ensure!( T::Flow::campaign_by_id(&context_id), Error::::CampaignUnknown ); let state = T::Flow::campaign_state(&context_id); ensure!( state == FlowState::Success, Error::::CampaignFailed ); - // let owner = flow::Module::::campaign_owner(&context_id); + // todo: should this checks be performed? + // let owner = T::Flow::campaign_owner(&context_id); // ensure!( sender == owner, Error::::AuthorizationError ); // B O U N D S + // todo: should this checks be performed or not? // let current_block = >::block_number(); // ensure!(start > current_block, Error::::OutOfBounds ); // ensure!(expiry > start, Error::::OutOfBounds ); @@ -417,23 +431,31 @@ pub mod pallet { let used_balance = >::get(&context_id); let total_balance = T::Flow::campaign_balance(&context_id); - let remaining_balance = total_balance - used_balance; + let remaining_balance = total_balance.checked_sub(used_balance).ok_or(Error::::BalanceInsufficient)? ; ensure!(remaining_balance >= amount, Error::::BalanceInsufficient ); // T R A F F I C let proposals = >::get(expiry); - ensure!((proposals.len() as u32) < T::MaxProposalsPerBlock::get(), Error::::TooManyProposals ); + ensure!((proposals.len() as u32) < T::MaxProposalsPerBlock::get(), Error::::TooManyProposals); + ensure!(!>::contains_key(&context_id), Error::::ProposalExists); + + // C O U N T S + + let proposals_count = >::get(); + let updated_proposals_count = proposals_count.checked_add(1).ok_or(Error::::OverflowError)?; + let proposals_by_campaign_count = >::get(&context_id); + let updated_proposals_by_campaign_count = proposals_by_campaign_count.checked_add(1).ok_or(Error::::OverflowError)?; + let proposals_by_owner_count = >::get(&sender); + let updated_proposals_by_owner_count = proposals_by_owner_count.checked_add(1).ok_or(Error::::OverflowError)?; // C O N F I G let proposal_type = ProposalType::Withdrawal; // treasury let voting_type = VotingType::Simple; // votes let nonce = Self::get_and_increment_nonce(); - // let phrase = b"just another withdrawal"; - let (proposal_id, _) = ::Randomness::random(&nonce.encode()); - ensure!(!>::contains_key(&context_id), Error::::HashCollision ); + let (proposal_id, _) = ::Randomness::random(&nonce); let proposal = Proposal { proposal_id: proposal_id.clone(), @@ -450,15 +472,6 @@ pub mod pallet { amount, }; - // C O U N T S - - let proposals_count = >::get(); - let updated_proposals_count = proposals_count.checked_add(1).ok_or(Error::::OverflowError)?; - let proposals_by_campaign_count = >::get(&context_id); - let updated_proposals_by_campaign_count = proposals_by_campaign_count.checked_add(1).ok_or(Error::::OverflowError)?; - let proposals_by_owner_count = >::get(&sender); - let updated_proposals_by_owner_count = proposals_by_owner_count.checked_add(1).ok_or(Error::::OverflowError)?; - // W R I T E Proposals::::insert(&proposal_id, proposal.clone()); @@ -478,14 +491,16 @@ pub mod pallet { >::insert((sender.clone(), proposal_id.clone()), proposals_by_owner_count); >::mutate( context_id.clone(), |proposals| proposals.push(proposal_id.clone()) ); - // ++ - - // >::mutate(|n| *n += 1); // todo: safe nonce increase - // E V E N T Self::deposit_event( - Event::::ProposalCreated(sender, context_id, proposal_id, amount, expiry) + Event::::ProposalCreated { + sender_id: sender, + context_id, + proposal_id, + amount, + expiry + } ); Ok(()) @@ -580,6 +595,7 @@ pub mod pallet { let contributors = T::Flow::campaign_contributors_count(&proposal.context_id); let threshold = contributors.checked_div(2).ok_or(Error::::DivisionError)?; if updated_approvers > threshold { + // todo: should this be called on finalize? Self::unlock_balance(proposal_id, updated_approvers)?; } // remove @@ -647,11 +663,11 @@ pub mod pallet { // dispatch vote event Self::deposit_event( - Event::::ProposalVoted( - sender, - proposal_id.clone(), + Event::::ProposalVoted { + sender_id: sender, + proposal_id:proposal_id.clone(), vote - ) + } ); Ok(()) @@ -701,12 +717,14 @@ pub mod pallet { Ok(t) => { if yes > t { proposal_state = ProposalState::Accepted; - Self::unlock_balance( proposal.proposal_id, yes ); + Self::unlock_balance(proposal.proposal_id, yes); } else { proposal_state = ProposalState::Rejected; } }, - Err(_err) => { } + Err(_err) => { + // todo: logic on error event + } } }, ProposalType::Member => { @@ -768,7 +786,7 @@ pub mod pallet { // let used_balance = Self::balance_used(proposal.context_id); let used_balance = >::get(proposal.context_id); let available_balance = total_balance - used_balance.clone(); - ensure!(available_balance >= proposal_balance, Error::::BalanceInsufficient ); + ensure!(available_balance >= proposal_balance, Error::::BalanceInsufficient); // Get the owner of the campaign let _owner = >::get(&proposal_id).ok_or("No owner for proposal")?; @@ -807,4 +825,4 @@ pub mod pallet { } } -// todo: Check storage fields and remove generices from those, who don't use Config trait \ No newline at end of file +// todo: Check storage fields and remove generices from those, who don't use Config trait diff --git a/signal/src/mock.rs b/signal/src/mock.rs index dd0dd4d42..1874ca653 100644 --- a/signal/src/mock.rs +++ b/signal/src/mock.rs @@ -2,17 +2,20 @@ use crate as pallet_signal; use frame_support::parameter_types; -use frame_system as system; +use frame_system; use frame_support_test::TestRandomness; use sp_std::cell::RefCell; +use sp_core::H256; use sp_runtime::{ testing::Header, traits::{BlakeTwo256, IdentityLookup}, // BuildStorage, }; // use orml_currencies::{Currency}; -use crate::traits::{Flow, FlowState}; -use support::{ControlPalletStorage, ControlState, ControlMemberState}; +use support::{ + ControlPalletStorage, ControlState, ControlMemberState, + FlowPalletStorage, FlowState +}; use primitives::{Balance, CurrencyId, Hash, TokenSymbol}; type AccountId = u64; @@ -25,17 +28,14 @@ pub struct ControlFixture { pub body_member_state: ControlMemberState, pub body_state: ControlState } -impl ControlFixture { - pub fn body_controller(&mut self, value: AccountId) { - self.body_controller = value; - } - pub fn body_state(&mut self, value: ControlState) { - self.body_state = value; - } - pub fn body_member_state(&mut self, value: ControlMemberState) { - self.body_member_state = value; - } + +pub struct FlowFixture { + pub campaign_balance: Balance, + pub campaign_state: FlowState, + pub campaign_contributors_count: u64, + pub campaign_org: Hash } + thread_local!( pub static control_fixture: RefCell = RefCell::new(ControlFixture { body_controller: ACC1, @@ -43,31 +43,29 @@ thread_local!( body_member_state: ControlMemberState::Active, body_state: ControlState::Active }); + pub static flow_fixture: RefCell = RefCell::new(FlowFixture { + campaign_balance: 15, + campaign_state: FlowState::Success, + campaign_contributors_count: 0, + campaign_org: H256::random() + }); ); pub struct ControlMock; impl ControlPalletStorage for ControlMock { - fn body_controller(_org: &Hash) -> AccountId { control_fixture.with(|v| v.borrow().body_controller.clone()) } fn body_treasury(_org: &Hash) -> AccountId { control_fixture.with(|v| v.borrow().body_treasury.clone()) } fn body_member_state(_hash: &Hash, _account_id: &AccountId) -> ControlMemberState { control_fixture.with(|v| v.borrow().body_member_state.clone()) } fn body_state(_hash: &Hash) -> ControlState { control_fixture.with(|v| v.borrow().body_state.clone()) } } -// impl ControlPalletStorage for ControlMock { - -// fn body_controller(_org: &Hash) -> AccountId { ACC1 } -// fn body_treasury(_org: &Hash) -> AccountId { ACC2 } -// fn body_member_state(_hash: &Hash, _account_id: &AccountId) -> ControlMemberState { ControlMemberState::Active } -// fn body_state(_hash: &Hash) -> ControlState { ControlState::Active } -// } pub struct FlowMock; -impl Flow for FlowMock { - fn campaign_balance(_hash: &Hash) -> Balance { Default::default() } - fn campaign_state(_hash: &Hash) -> FlowState { FlowState::Active } - fn campaign_contributors_count(_hash: &Hash) -> u64 { 0 } - fn campaign_org(_hash: &Hash) -> Hash { Default::default() } +impl FlowPalletStorage for FlowMock { + fn campaign_balance(_hash: &Hash) -> Balance { flow_fixture.with(|v| v.borrow().campaign_balance.clone()) } + fn campaign_state(_hash: &Hash) -> FlowState { flow_fixture.with(|v| v.borrow().campaign_state.clone()) } + fn campaign_contributors_count(_hash: &Hash) -> u64 { flow_fixture.with(|v| v.borrow().campaign_contributors_count.clone()) } + fn campaign_org(_hash: &Hash) -> Hash { flow_fixture.with(|v| v.borrow().campaign_org.clone()) } } type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; @@ -81,7 +79,7 @@ frame_support::construct_runtime!( UncheckedExtrinsic = UncheckedExtrinsic, { System: frame_system::{Pallet, Call, Config, Storage, Event}, - Signal: pallet_signal::{Pallet, Call, Storage, Event}, + Signal: pallet_signal, } ); @@ -99,7 +97,7 @@ parameter_types! { // impl pallet_randomness_collective_flip::Config for Test {} -impl system::Config for Test { +impl frame_system::Config for Test { type BaseCallFilter = frame_support::traits::Everything; type BlockWeights = (); type BlockLength = (); @@ -142,7 +140,7 @@ impl pallet_signal::Config for Test { pub struct ExtBuilder; impl ExtBuilder { pub fn build(self) -> sp_io::TestExternalities { - let t = system::GenesisConfig::default().build_storage::().unwrap(); + let t = frame_system::GenesisConfig::default().build_storage::().unwrap(); let mut ext = sp_io::TestExternalities::new(t); ext.execute_with(|| System::set_block_number(1)); ext diff --git a/signal/src/tests.rs b/signal/src/tests.rs index 16e370161..8e9aa309d 100644 --- a/signal/src/tests.rs +++ b/signal/src/tests.rs @@ -1,7 +1,6 @@ #[cfg(test)] use super::{ - Pallet as SignalPallet, Proposals, Metadata, Owners, @@ -18,31 +17,47 @@ use super::{ ProposalsByOwnerIndex, ProposalsByContext, ProposalTimeLimit, + ProposalSimpleVotes, + ProposalApprovers, + ProposalDeniers, + VotedBefore, + ProposalsByVoterCount, + ProposalVotesByVoters, + ProposalsByVoter, + ProposalVoters, + CampaignBalanceUsed, Nonce, Config, Error, - Event as SignalEvent, voting_structs::{Proposal, ProposalMetadata}, voting_enums::{VotingType, ProposalType, ProposalState}, - mock::{ACC1, ACC2, ExtBuilder, System, Origin, Test, Event, control_fixture} + mock::{ + Test, ExtBuilder, + ACC1, ACC2, + System, Origin, Event, Signal, + control_fixture, flow_fixture + } +}; +use support::{ + ControlState, ControlMemberState, + FlowState }; -use support::{ControlPalletStorage, ControlState, ControlMemberState}; use sp_runtime::traits::BadOrigin; use sp_core::H256; -use frame_support::{assert_ok, assert_noop, traits::{Randomness}}; +use frame_support::{assert_ok, assert_noop, traits::{Randomness, Hooks}}; use frame_system; #[test] -fn general_proposal_success() { +fn signal_general_proposal_success() { ExtBuilder::default().build().execute_with(|| { System::set_block_number(3); let nonce = vec![0]; let (proposal_id, _): (H256, _) = ::Randomness::random(&nonce); let ctx_id = H256::random(); assert_ok!( - SignalPallet::::general_proposal( + Signal::general_proposal( Origin::signed(ACC1), ctx_id, // context id vec![1,2,3], // title @@ -55,8 +70,8 @@ fn general_proposal_success() { .expect("No event generated").event; assert_eq!( event, - Event::from( - SignalEvent::Proposal { + Event::Signal( + crate::Event::Proposal { sender_id: ACC1, proposal_id: proposal_id } @@ -100,7 +115,7 @@ fn general_proposal_success() { let nonce = vec![1]; let (new_proposal_id, _): (H256, _) = ::Randomness::random(&nonce); assert_ok!( - SignalPallet::::general_proposal( + Signal::general_proposal( Origin::signed(ACC1), ctx_id, // context id vec![2,3,4], // title @@ -131,7 +146,7 @@ fn general_proposal_success() { } #[test] -fn general_proposal_error() { +fn signal_general_proposal_error() { ExtBuilder::default().build().execute_with(|| { System::set_block_number(3); let nonce = vec![0]; @@ -147,7 +162,7 @@ fn general_proposal_error() { expiry: 13 }); assert_noop!( - SignalPallet::::general_proposal( + Signal::general_proposal( Origin::signed(ACC1), ctx_id, // context id vec![1,2,3], // title @@ -162,7 +177,7 @@ fn general_proposal_error() { let proposal_ids = vec![H256::random(), H256::random()]; >::insert(15, proposal_ids); assert_noop!( - SignalPallet::::general_proposal( + Signal::general_proposal( Origin::signed(ACC1), ctx_id, // context id vec![1,2,3], // title @@ -174,7 +189,7 @@ fn general_proposal_error() { ); assert_noop!( - SignalPallet::::general_proposal( + Signal::general_proposal( Origin::signed(ACC1), ctx_id, // context id vec![1,2,3], // title @@ -185,7 +200,7 @@ fn general_proposal_error() { Error::::OutOfBounds ); assert_noop!( - SignalPallet::::general_proposal( + Signal::general_proposal( Origin::signed(ACC1), ctx_id, // context id vec![1,2,3], // title @@ -196,9 +211,9 @@ fn general_proposal_error() { Error::::OutOfBounds ); - control_fixture.with(|val|val.borrow_mut().body_member_state(ControlMemberState::Inactive)); + control_fixture.with(|val|val.borrow_mut().body_member_state = ControlMemberState::Inactive); assert_noop!( - SignalPallet::::general_proposal( + Signal::general_proposal( Origin::signed(ACC1), ctx_id, // context id vec![1,2,3], // title @@ -209,31 +224,440 @@ fn general_proposal_error() { Error::::AuthorizationError ); - control_fixture.with(|val|val.borrow_mut().body_state(ControlState::Inactive)); + control_fixture.with(|val|val.borrow_mut().body_state = ControlState::Inactive); assert_noop!( - SignalPallet::::general_proposal( + Signal::general_proposal( Origin::signed(ACC1), + ctx_id, + vec![1,2,3], + vec![1,2,3], + 3, + 15 + ), + Error::::DAOInactive + ); + + assert_noop!( + Signal::general_proposal( + Origin::none(), ctx_id, // context id vec![1,2,3], // title vec![1,2,3], // cid 3, // start 15 // expiry ), - Error::::DAOInactive + BadOrigin ); + + }); +} + + +#[test] +fn signal_withdraw_proposal_success() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(3); + + let nonce = vec![0]; + let (proposal_id, _): (H256, _) = ::Randomness::random(&nonce); + let ctx_id = H256::random(); + >::insert(ctx_id, 5); + + assert_ok!( + Signal::withdraw_proposal( + Origin::signed(ACC1), // origin + ctx_id, // context id + vec![1,2,3], // title + vec![1,2,3], // cid + 10, // amount + 3, // start + 15 // expiry + ) + ); + let event = >::events().pop() + .expect("No event generated").event; + assert_eq!( + event, + Event::Signal( + crate::Event::ProposalCreated { + sender_id: ACC1, + context_id: ctx_id, + proposal_id, + amount: 10, + expiry: 15 + } + ) + ); + assert_eq!( + >::get(&proposal_id), + Proposal { + proposal_id, + context_id: ctx_id, + proposal_type: ProposalType::Withdrawal, + voting_type: VotingType::Simple, + start: 3, + expiry: 15 + } + ); + assert_eq!( + >::get(&proposal_id), + ProposalMetadata { + title: vec![1,2,3], + cid: vec![1,2,3], + amount: 10 + } + ); + assert_eq!(>::get(&proposal_id), Some(ACC1)); + assert_eq!(>::get(&proposal_id), ProposalState::Active); + assert_eq!(>::get(15), vec![proposal_id.clone()]); + assert_eq!(>::get(0), proposal_id); + assert_eq!(>::get(), 1); + assert_eq!(>::get(&proposal_id), 0); + assert_eq!(>::get((ctx_id.clone(), 0)), proposal_id); + assert_eq!(>::get(ctx_id), 1); + assert_eq!(>::get((ctx_id, proposal_id)), 0); + assert_eq!(>::get((ACC1, 0)), proposal_id); + assert_eq!(>::get(ACC1), 1); + assert_eq!(>::get((ACC1, proposal_id)), 0); + assert_eq!(>::get(ctx_id), vec![proposal_id.clone()]); + assert_eq!(>::get(), 1); + + let nonce = vec![1]; + let (new_proposal_id, _): (H256, _) = ::Randomness::random(&nonce); + assert_ok!( + Signal::general_proposal( + Origin::signed(ACC1), + ctx_id, // context id + vec![2,3,4], // title + vec![2,3,4], // cid + 3, // start + 15 // expiry + ) + ); + assert_eq!( + >::get(15), + vec![proposal_id.clone(), new_proposal_id.clone()] + ); + assert_eq!(>::get(1), new_proposal_id); + assert_eq!(>::get(), 2); + assert_eq!(>::get(&new_proposal_id), 1); + assert_eq!(>::get((ctx_id.clone(), 1)), new_proposal_id); + assert_eq!(>::get(ctx_id), 2); + assert_eq!(>::get((ctx_id, new_proposal_id)), 1); + assert_eq!(>::get((ACC1, 1)), new_proposal_id); + assert_eq!(>::get(ACC1), 2); + assert_eq!(>::get((ACC1, new_proposal_id)), 1); + assert_eq!( + >::get(ctx_id), + vec![proposal_id.clone(), new_proposal_id.clone()] + ); + assert_eq!(>::get(), 2); + + }); +} + +#[test] +fn signal_withdraw_proposal_error() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(3); + + let nonce = vec![0]; + let (proposal_id, _): (H256, _) = ::Randomness::random(&nonce); + let ctx_id = H256::random(); + >::insert(ctx_id, 5); + + >::insert(ctx_id, Proposal { + proposal_id: proposal_id, + context_id: ctx_id, + proposal_type: ProposalType::Withdrawal, + voting_type: VotingType::Simple, + start: 2, + expiry: 13 + }); assert_noop!( - SignalPallet::::general_proposal( - Origin::none(), + Signal::withdraw_proposal( + Origin::signed(ACC1), // origin + ctx_id, // context id + vec![1,2,3], // title + vec![1,2,3], // cid + 10, // amount + 3, // start + 15 // expiry + ), + Error::::ProposalExists + ); + + let proposal_ids = vec![H256::random(), H256::random()]; + >::insert(15, proposal_ids); + assert_noop!( + Signal::withdraw_proposal( + Origin::signed(ACC1), // origin + ctx_id, // context id + vec![1,2,3], // title + vec![1,2,3], // cid + 10, // amount + 3, // start + 15 // expiry + ), + Error::::TooManyProposals + ); + + >::insert(ctx_id, 5); + flow_fixture.with(|v| v.borrow_mut().campaign_balance = 10 ); + assert_noop!( + Signal::withdraw_proposal( + Origin::signed(ACC1), // origin ctx_id, // context id vec![1,2,3], // title vec![1,2,3], // cid + 10, // amount 3, // start 15 // expiry ), + Error::::BalanceInsufficient + ); + + >::insert(ctx_id, 11); + assert_noop!( + Signal::withdraw_proposal( + Origin::signed(ACC1), // origin + ctx_id, // context id + vec![1,2,3], // title + vec![1,2,3], // cid + 10, // amount + 3, // start + 15 // expiry + ), + Error::::BalanceInsufficient + ); + + >::insert(ctx_id, 5); + flow_fixture.with(|v| v.borrow_mut().campaign_state = FlowState::Failed ); + >::insert(ctx_id, 11); + assert_noop!( + Signal::withdraw_proposal( + Origin::signed(ACC1), // origin + ctx_id, // context id + vec![1,2,3], // title + vec![1,2,3], // cid + 10, // amount + 3, // start + 15 // expiry + ), + Error::::CampaignFailed + ); + + assert_noop!( + Signal::withdraw_proposal( + Origin::none(), // origin + ctx_id, // context id + vec![1,2,3], // title + vec![1,2,3], // cid + 10, // amount + 3, // start + 15 // expiry + ), + BadOrigin + ); + }); +} + +#[test] +fn signal_simple_vote_success() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(3); + + let nonce = vec![0]; + let (proposal_id, _): (H256, _) = ::Randomness::random(&nonce); + let ctx_id = H256::random(); + >::insert(proposal_id, Proposal { + proposal_id: proposal_id, + context_id: ctx_id, + proposal_type: ProposalType::General, + voting_type: VotingType::Simple, + start: 2, + expiry: 13 + }); + >::insert(proposal_id, ProposalState::Active); + + assert_ok!( + Signal::simple_vote(Origin::signed(ACC2), proposal_id, true) + ); + let event = >::events().pop() + .expect("No event generated").event; + assert_eq!( + event, + Event::Signal( + crate::Event::ProposalVoted { + sender_id: ACC2, + proposal_id, + vote: true + } + ) + ); + assert_eq!(>::get(&proposal_id), (1, 0)); + assert_eq!(>::get(&proposal_id), 1); + assert_eq!(>::get((ACC2, proposal_id)), true); + assert_eq!(>::get(ACC2), 1); + assert_eq!(>::get(proposal_id), vec![(ACC2, true)]); + assert_eq!(>::get(ACC2), vec![(proposal_id, true)]); + assert_eq!(>::get(proposal_id), vec![ACC2]); + + assert_ok!( + Signal::simple_vote(Origin::signed(ACC1), proposal_id, false) + ); + let event = >::events().pop() + .expect("No event generated").event; + assert_eq!( + event, + Event::Signal( + crate::Event::ProposalVoted { + sender_id: ACC1, + proposal_id, + vote: false + } + ) + ); + assert_eq!(>::get(&proposal_id), (1, 1)); + assert_eq!(>::get(&proposal_id), 1); + assert_eq!(>::get(&proposal_id), 1); + assert_eq!(>::get((ACC1, proposal_id)), true); + assert_eq!(>::get(ACC1), 1); + assert_eq!(>::get(proposal_id), vec![(ACC2, true), (ACC1, false)]); + assert_eq!(>::get(ACC1), vec![(proposal_id, false)]); + assert_eq!(>::get(proposal_id), vec![ACC1, ACC2]); + + + let nonce = vec![1]; + let (proposal_id, _): (H256, _) = ::Randomness::random(&nonce); + let ctx_id = H256::random(); + >::insert(proposal_id, Proposal { + proposal_id: proposal_id, + context_id: ctx_id, + proposal_type: ProposalType::Withdrawal, + voting_type: VotingType::Simple, + start: 2, + expiry: 13 + }); + >::insert(proposal_id, ProposalState::Active); + flow_fixture.with(|v| v.borrow_mut().campaign_contributors_count = 1); + + assert_ok!( + Signal::simple_vote(Origin::signed(ACC1), proposal_id, false) + ); + let event = >::events().pop() + .expect("No event generated").event; + assert_eq!( + event, + Event::Signal( + crate::Event::ProposalVoted { + sender_id: ACC1, + proposal_id, + vote: false + } + ) + ); + assert_eq!(>::get(&proposal_id), (0, 1)); + assert_eq!(>::get(&proposal_id), 0); + assert_eq!(>::get(&proposal_id), 1); + + // assert_ok!( + // Signal::simple_vote(Origin::signed(ACC2), proposal_id, true) + // ); + // let event = >::events().pop() + // .expect("No event generated").event; + // assert_eq!( + // event, + // Event::Signal( + // crate::Event::ProposalVoted { + // sender_id: ACC2, + // proposal_id, + // vote: true + // } + // ) + // ); + // assert_eq!(>::get(&proposal_id), (1, 0)); + // assert_eq!(>::get(&proposal_id), 1); + + }); +} + + +#[test] +fn signal_simple_vote_error() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(3); + + let nonce = vec![0]; + let (proposal_id, _): (H256, _) = ::Randomness::random(&nonce); + let ctx_id = H256::random(); + >::insert(proposal_id, Proposal { + proposal_id: proposal_id, + context_id: ctx_id, + proposal_type: ProposalType::General, + voting_type: VotingType::Simple, + start: 2, + expiry: System::block_number() + }); + >::insert(proposal_id, ProposalState::Active); + assert_noop!( + Signal::simple_vote( + Origin::signed(ACC1), + proposal_id, + true + ), + Error::::ProposalExpired + ); + + >::insert((ACC1, proposal_id), true); + assert_noop!( + Signal::simple_vote( + Origin::signed(ACC1), + proposal_id, + true + ), + Error::::AlreadyVoted + ); + + >::insert(proposal_id, ProposalState::Expired); + assert_noop!( + Signal::simple_vote( + Origin::signed(ACC1), + proposal_id, + true + ), + Error::::ProposalEnded + ); + + assert_noop!( + Signal::simple_vote( + Origin::signed(ACC1), + H256::random(), + true + ), + Error::::ProposalUnknown + ); + + assert_noop!( + Signal::simple_vote( + Origin::none(), + proposal_id, + true + ), BadOrigin - ); + ); + }); +} +#[test] +fn signal_on_finalize_success() { + ExtBuilder::default().build().execute_with(|| { + let bn = 3; + System::set_block_number(bn); + // create 4 proposals + // two general and two withdrawal, one expired and one not + Signal::on_finalize(bn); }); } diff --git a/signal/src/traits.rs b/signal/src/traits.rs deleted file mode 100644 index 0328bad1c..000000000 --- a/signal/src/traits.rs +++ /dev/null @@ -1,26 +0,0 @@ -pub trait Flow { - fn campaign_balance(hash: &Hash) -> Balance; - fn campaign_state(hash: &Hash) -> FlowState; - fn campaign_contributors_count(hash: &Hash) -> u64; - fn campaign_org(hash: &Hash) -> Hash; -} - -#[derive(PartialEq)] -pub enum FlowState { - Init = 0, - Active = 1, - Paused = 2, - Success = 3, - Failed = 4, - Locked = 5, -} - - -pub struct ImplPlaceholder; - -impl Flow for ImplPlaceholder { - fn campaign_balance(_hash: &Hash) -> Balance { Default::default() } - fn campaign_state(_hash: &Hash) -> FlowState { FlowState::Failed } - fn campaign_contributors_count(_hash: &Hash) -> u64 { Default::default() } - fn campaign_org(_hash: &Hash) -> Hash { Default::default() } -} \ No newline at end of file diff --git a/support/Cargo.toml b/support/Cargo.toml index dbdd909e0..d7e05fb96 100644 --- a/support/Cargo.toml +++ b/support/Cargo.toml @@ -8,8 +8,19 @@ description = "" repository = "https://github.com/gamedaoco/gamedao-protocol" +[dependencies] +serde = { version = "1.0.124", optional = true } +codec = { package = "parity-scale-codec", version = "2.3.1", default-features = false } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } + [features] default = ["std"] std = [ - + "codec/std", + "serde/std", + "scale-info/std", + "frame-support/std", + "sp-std/std", ] diff --git a/support/src/lib.rs b/support/src/lib.rs index 1843f732c..079798ca0 100644 --- a/support/src/lib.rs +++ b/support/src/lib.rs @@ -1,16 +1,40 @@ #![cfg_attr(not(feature = "std"), no_std)] +use frame_support::codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_std::fmt::Debug; -#[derive(PartialEq)] +// TODO: discussion: where to store enums + +#[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 + } +} + +#[derive(Encode, Decode, PartialEq, Clone, Eq, PartialOrd, Ord, TypeInfo, Debug)] +#[repr(u8)] pub enum ControlMemberState { Inactive = 0, // eg inactive after threshold period - Active = 1, // active + Active = 1, Pending = 2, // application voting pending Kicked = 3, Banned = 4, Exited = 5, } -#[derive(PartialEq)] +#[derive(Encode, Decode, PartialEq, Clone, Eq, PartialOrd, Ord, TypeInfo, Debug)] +#[repr(u8)] pub enum ControlState { Inactive = 0, Active = 1, @@ -18,9 +42,15 @@ pub enum ControlState { } pub trait ControlPalletStorage { - fn body_controller(org: &Hash) -> AccountId; fn body_treasury(org: &Hash) -> AccountId; fn body_member_state(hash: &Hash, account_id: &AccountId) -> ControlMemberState; fn body_state(hash: &Hash) -> ControlState; } + +pub trait FlowPalletStorage { + fn campaign_balance(hash: &Hash) -> Balance; + fn campaign_state(hash: &Hash) -> FlowState; + fn campaign_contributors_count(hash: &Hash) -> u64; + fn campaign_org(hash: &Hash) -> Hash; +} From 3f74cd6608c3e7aa5eeaa280791d1109e1e7463e Mon Sep 17 00:00:00 2001 From: vasylenko-yevhen Date: Thu, 10 Mar 2022 17:26:27 +0200 Subject: [PATCH 06/10] Mocks and tests updated --- flow/src/lib.rs | 8 +-- signal/Cargo.toml | 10 ++- signal/src/lib.rs | 43 +++++++----- signal/src/mock.rs | 79 ++++++++++++++++++---- signal/src/tests.rs | 156 ++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 248 insertions(+), 48 deletions(-) diff --git a/flow/src/lib.rs b/flow/src/lib.rs index 0417d187c..c69b14ee5 100644 --- a/flow/src/lib.rs +++ b/flow/src/lib.rs @@ -484,7 +484,7 @@ pub mod pallet { 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); + let dao_treasury = T::Control::body_treasury(&dao); // check for cap reached if campaign_balance >= campaign.cap { @@ -656,13 +656,13 @@ pub mod pallet { ) -> DispatchResult { let creator = ensure_signed(origin)?; - let controller = T::Control::body_controller(org.clone()); + 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.clone()); + let treasury = T::Control::body_treasury(&org); let free_balance = T::Currency::free_balance(T::FundingCurrencyId::get(), &treasury); ensure!(free_balance > deposit, Error::::TreasuryBalanceTooLow); @@ -912,7 +912,7 @@ impl Pallet { // TODO: this should be a proper mechanism // to reserve some of the staked GAME - let treasury = T::Control::body_treasury(campaign.org.clone()); + let treasury = T::Control::body_treasury(&campaign.org); // let fundingCurrency = T::FundingCurrencyId::get(); T::Currency::reserve( diff --git a/signal/Cargo.toml b/signal/Cargo.toml index 3dd24a11a..832dc2bc6 100644 --- a/signal/Cargo.toml +++ b/signal/Cargo.toml @@ -37,8 +37,10 @@ support = { package = "gamedao-protocol-support", path = "../support", default-f 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" } sp-io = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.13" } -pallet-randomness-collective-flip = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.13" } frame-support-test = { git = "https://github.com/paritytech/substrate.git", 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] @@ -57,8 +59,10 @@ std = [ 'sp-std/std', 'sp-runtime/std', - "orml-traits/std", + "orml-traits/std", + "orml-tokens/std", + "orml-currencies/std", "support/std" ] -try-runtime = ['frame-support/try-runtime'] \ No newline at end of file +try-runtime = ['frame-support/try-runtime'] diff --git a/signal/src/lib.rs b/signal/src/lib.rs index 10d001f7b..5f923756e 100644 --- a/signal/src/lib.rs +++ b/signal/src/lib.rs @@ -14,6 +14,8 @@ #![cfg_attr(not(feature = "std"), no_std)] +pub use pallet::*; + pub mod voting_enums; pub mod voting_structs; @@ -21,8 +23,8 @@ pub mod voting_structs; pub mod mock; #[cfg(test)] mod tests; - -pub use pallet::*; +// #[cfg(feature = "runtime-benchmarks")] +mod benchmarking; #[frame_support::pallet] @@ -39,8 +41,8 @@ pub mod pallet { transactional }; use sp_std::vec::Vec; + use orml_traits::{MultiCurrency, MultiReservableCurrency}; - // use orml_traits::{MultiCurrency, MultiReservableCurrency}; use primitives::{Balance, CurrencyId}; use support::{ ControlPalletStorage, ControlState, ControlMemberState, @@ -56,8 +58,8 @@ pub mod pallet { #[pallet::config] pub trait Config: frame_system::Config { type Event: From> + IsType<::Event> + Into<::Event>; - // type Currency: MultiCurrency - // + MultiReservableCurrency; + type Currency: MultiCurrency + + MultiReservableCurrency; type Randomness: Randomness; type Control: ControlPalletStorage; type Flow: FlowPalletStorage; @@ -207,12 +209,20 @@ pub mod pallet { vote: bool }, ProposalFinalized(T::Hash, u8), - ProposalApproved(T::Hash), - ProposalRejected(T::Hash), + ProposalApproved { + proposal_id: T::Hash + }, + ProposalRejected { + proposal_id: T::Hash + }, ProposalExpired(T::Hash), ProposalAborted(T::Hash), ProposalError(T::Hash, Vec), - WithdrawalGranted(T::Hash, T::Hash, T::Hash), + WithdrawalGranted{ + proposal_id: T::Hash, + context_id: T::Hash, + body_id: T::Hash + }, } #[pallet::error] @@ -704,6 +714,7 @@ pub mod pallet { if yes > no { proposal_state = ProposalState::Accepted; } if yes < no { proposal_state = ProposalState::Rejected; } if yes == 0 && no == 0 { proposal_state = ProposalState::Expired; } + // todo: if same amount of yes/no votes? }, ProposalType::Withdrawal => { // treasury @@ -742,12 +753,12 @@ pub mod pallet { match proposal_state { ProposalState::Accepted => { Self::deposit_event( - Event::::ProposalApproved(proposal_id.clone()) + Event::::ProposalApproved {proposal_id: proposal_id.clone()} ); }, ProposalState::Rejected => { Self::deposit_event( - Event::::ProposalRejected(proposal_id.clone()) + Event::::ProposalRejected {proposal_id: proposal_id.clone()} ); }, ProposalState::Expired => { @@ -794,11 +805,11 @@ pub mod pallet { // get treasury account for related body and unlock balance let body = T::Flow::campaign_org(&proposal.context_id); let treasury_account = T::Control::body_treasury(&body); - // T::Currency::unreserve( - // T::FundingCurrencyId::get(), - // &treasury_account, - // proposal_balance - // ); + T::Currency::unreserve( + T::FundingCurrencyId::get(), + &treasury_account, + proposal_balance.clone() + ); // Change the used amount let new_used_balance = used_balance + proposal_balance; @@ -811,7 +822,7 @@ pub mod pallet { >::insert(proposal_id.clone(), proposal.clone()); Self::deposit_event( - Event::::WithdrawalGranted(proposal_id, proposal.context_id, body) + Event::::WithdrawalGranted {proposal_id, context_id: proposal.context_id, body_id: body} ); Ok(()) diff --git a/signal/src/mock.rs b/signal/src/mock.rs index 1874ca653..4db6b7dc4 100644 --- a/signal/src/mock.rs +++ b/signal/src/mock.rs @@ -2,6 +2,7 @@ use crate as pallet_signal; use frame_support::parameter_types; +use frame_support::traits::{GenesisBuild, Nothing}; use frame_system; use frame_support_test::TestRandomness; use sp_std::cell::RefCell; @@ -9,18 +10,20 @@ use sp_core::H256; use sp_runtime::{ testing::Header, traits::{BlakeTwo256, IdentityLookup}, - // BuildStorage, }; -// use orml_currencies::{Currency}; +use orml_traits::parameter_type_with_key; use support::{ ControlPalletStorage, ControlState, ControlMemberState, FlowPalletStorage, FlowState }; -use primitives::{Balance, CurrencyId, Hash, TokenSymbol}; +use primitives::{Amount, Balance, CurrencyId, Hash, TokenSymbol}; + +pub type AccountId = u64; +pub type BlockNumber = u32; -type AccountId = u64; pub const ACC1: AccountId = 1; pub const ACC2: AccountId = 2; +pub const TREASURY_ACC: AccountId = 3; pub struct ControlFixture { pub body_controller: AccountId, @@ -39,7 +42,7 @@ pub struct FlowFixture { thread_local!( pub static control_fixture: RefCell = RefCell::new(ControlFixture { body_controller: ACC1, - body_treasury: ACC2, + body_treasury: TREASURY_ACC, body_member_state: ControlMemberState::Active, body_state: ControlState::Active }); @@ -80,15 +83,59 @@ frame_support::construct_runtime!( { System: frame_system::{Pallet, Call, Config, Storage, Event}, Signal: pallet_signal, + Currencies: orml_currencies::{Pallet, Call, Event}, + Tokens: orml_tokens::{Pallet, Storage, Event, Config}, + PalletBalances: pallet_balances::{Pallet, Call, Storage, Event}, } ); +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; +} + +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 BlockHashCount: u64 = 250; pub const SS58Prefix: u8 = 42; pub BlockWeights: frame_system::limits::BlockWeights = frame_system::limits::BlockWeights::simple_max(1024); - pub static ExistentialDeposit: u64 = 0; + + pub const ExistentialDeposit: Balance = 1; pub const MaxProposalsPerBlock: u32 = 2; pub const MaxProposalDuration: u32 = 20; @@ -115,7 +162,7 @@ impl frame_system::Config for Test { type BlockHashCount = BlockHashCount; type Version = (); type PalletInfo = PalletInfo; - type AccountData = (); + type AccountData = pallet_balances::AccountData; type OnNewAccount = (); type OnKilledAccount = (); type SystemWeightInfo = (); @@ -133,16 +180,24 @@ impl pallet_signal::Config for Test { type MaxProposalDuration = MaxProposalDuration; type FundingCurrencyId = FundingCurrencyId; type Randomness = TestRandomness; - // type Currency = Currency; + type Currency = Currencies; } #[derive(Default)] pub struct ExtBuilder; impl ExtBuilder { pub fn build(self) -> sp_io::TestExternalities { - let t = frame_system::GenesisConfig::default().build_storage::().unwrap(); - let mut ext = sp_io::TestExternalities::new(t); - ext.execute_with(|| System::set_block_number(1)); - ext + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + let currency_id = TokenSymbol::GAME as u32; + orml_tokens::GenesisConfig:: { + balances: vec![ + (ACC1, currency_id, 100), + (ACC2, currency_id, 100), + (TREASURY_ACC, currency_id, 25) + ], + }.assimilate_storage(&mut t).unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext } } diff --git a/signal/src/tests.rs b/signal/src/tests.rs index 8e9aa309d..dd2d20288 100644 --- a/signal/src/tests.rs +++ b/signal/src/tests.rs @@ -33,7 +33,7 @@ use super::{ voting_enums::{VotingType, ProposalType, ProposalState}, mock::{ Test, ExtBuilder, - ACC1, ACC2, + AccountId, ACC1, ACC2, TREASURY_ACC, System, Origin, Event, Signal, control_fixture, flow_fixture } @@ -45,7 +45,8 @@ use support::{ use sp_runtime::traits::BadOrigin; use sp_core::H256; use frame_support::{assert_ok, assert_noop, traits::{Randomness, Hooks}}; -use frame_system; +use orml_tokens::Event as TokensEvent; +use orml_traits::{MultiReservableCurrency}; @@ -66,7 +67,7 @@ fn signal_general_proposal_success() { 15 // expiry ) ); - let event = >::events().pop() + let event = System::events().pop() .expect("No event generated").event; assert_eq!( event, @@ -275,7 +276,7 @@ fn signal_withdraw_proposal_success() { 15 // expiry ) ); - let event = >::events().pop() + let event = System::events().pop() .expect("No event generated").event; assert_eq!( event, @@ -484,7 +485,7 @@ fn signal_simple_vote_success() { assert_ok!( Signal::simple_vote(Origin::signed(ACC2), proposal_id, true) ); - let event = >::events().pop() + let event = System::events().pop() .expect("No event generated").event; assert_eq!( event, @@ -507,7 +508,7 @@ fn signal_simple_vote_success() { assert_ok!( Signal::simple_vote(Origin::signed(ACC1), proposal_id, false) ); - let event = >::events().pop() + let event = System::events().pop() .expect("No event generated").event; assert_eq!( event, @@ -546,7 +547,7 @@ fn signal_simple_vote_success() { assert_ok!( Signal::simple_vote(Origin::signed(ACC1), proposal_id, false) ); - let event = >::events().pop() + let event = System::events().pop() .expect("No event generated").event; assert_eq!( event, @@ -565,7 +566,7 @@ fn signal_simple_vote_success() { // assert_ok!( // Signal::simple_vote(Origin::signed(ACC2), proposal_id, true) // ); - // let event = >::events().pop() + // let event = System::events().pop() // .expect("No event generated").event; // assert_eq!( // event, @@ -653,11 +654,140 @@ fn signal_simple_vote_error() { #[test] fn signal_on_finalize_success() { ExtBuilder::default().build().execute_with(|| { - let bn = 3; - System::set_block_number(bn); + let (start, expiry) = (3, 15); + System::set_block_number(start); + let (proposal_id1, _): (H256, _) = ::Randomness::random(&vec![0]); + let (proposal_id2, _): (H256, _) = ::Randomness::random(&vec![1]); + let (proposal_id3, _): (H256, _) = ::Randomness::random(&vec![2]); + + assert_ok!( + Signal::general_proposal( + Origin::signed(ACC1), + H256::random(), // context id + vec![1,2,3], // title + vec![1,2,3], // cid + start, // start + expiry // expiry + ) + ); + assert_ok!( + Signal::withdraw_proposal( + Origin::signed(ACC1), // origin + H256::random(), // context id + vec![1,2,3], // title + vec![1,2,3], // cid + 10, // amount + start, // start + expiry // expiry + ) + ); + for i in 1..5 { + assert_ok!( + Signal::simple_vote( + Origin::signed(i), + proposal_id1, + i < 4 + ) + ); + } + for i in 1..5 { + assert_ok!( + Signal::simple_vote( + Origin::signed(i), + proposal_id2, + i == 5 + ) + ); + } + // todo: few votes for each item + + let mut events_before = System::events().len(); + assert_eq!(events_before, 10); + Signal::on_finalize(start); + assert_eq!(System::events().len(), events_before); + + System::set_block_number(expiry); + Signal::on_finalize(expiry); + let mut events = System::events(); + assert_eq!(events.len(), events_before + 2); + + let withdrawal_event = events.pop().unwrap().event; + let general_event = events.pop().unwrap().event; + assert_eq!( + withdrawal_event, + Event::Signal(crate::Event::ProposalRejected {proposal_id: proposal_id2}) + ); + assert_eq!( + >::get(proposal_id2), + ProposalState::Rejected + ); + + assert_eq!( + general_event, + Event::Signal(crate::Event::ProposalApproved {proposal_id: proposal_id1}) + ); + assert_eq!( + >::get(proposal_id1), + ProposalState::Accepted + ); + + events_before = 12; + let ctx_id = H256::random(); + assert_ok!( + Signal::withdraw_proposal( + Origin::signed(ACC1), // origin + ctx_id, // context id + vec![1,2,3], // title + vec![1,2,3], // cid + 10, // amount + 15, // start + 16 // expiry + ) + ); + assert_eq!(System::events().len(), events_before + 1); + + let res = <::Currency as MultiReservableCurrency>:: + reserve(::FundingCurrencyId::get(), &TREASURY_ACC, 25); + match res { + Ok(_) => {}, + Err(_) => panic!("Failed to reserve treasury balance") + } + assert_ok!( + Signal::simple_vote( + Origin::signed(ACC1), + proposal_id3, + true + ) + ); + assert_eq!(System::events().len(), events_before + 5); + System::set_block_number(16); + Signal::on_finalize(16); + let mut events = System::events(); + assert_eq!(events.len(), events_before + 5); + assert_eq!( + events.pop().unwrap().event, + Event::Signal(crate::Event::ProposalVoted {sender_id: ACC1, proposal_id: proposal_id3, vote: true}) + ); + assert_eq!( + events.pop().unwrap().event, + Event::Signal(crate::Event::WithdrawalGranted { + proposal_id: proposal_id3, + context_id: ctx_id, + body_id: flow_fixture.with(|v| v.borrow().campaign_org) + }) + ); + assert_eq!( + events.pop().unwrap().event, + Event::Tokens( + TokensEvent::Unreserved( + ::FundingCurrencyId::get(), + TREASURY_ACC, + 10 + ) + ) + ); + assert_eq!(>::get(ctx_id), 10); + assert_eq!(>::get(proposal_id3), ProposalState::Finalized); - // create 4 proposals - // two general and two withdrawal, one expired and one not - Signal::on_finalize(bn); }); } From b8881df74b506a51f47dd6b40a1e88147cc5d5ff Mon Sep 17 00:00:00 2001 From: vasylenko-yevhen Date: Fri, 11 Mar 2022 22:18:26 +0200 Subject: [PATCH 07/10] Use actual Sense pallet version --- sense/src/benchmarking.rs | 30 +-- sense/src/lib.rs | 485 +++++++++++++++++--------------------- sense/src/mock.rs | 23 +- sense/src/tests.rs | 92 ++++---- sense/src/weights.rs | 4 +- 5 files changed, 292 insertions(+), 342 deletions(-) mode change 100755 => 100644 sense/src/lib.rs diff --git a/sense/src/benchmarking.rs b/sense/src/benchmarking.rs index 2019cf7a9..bce558e95 100644 --- a/sense/src/benchmarking.rs +++ b/sense/src/benchmarking.rs @@ -2,30 +2,30 @@ use super::*; #[allow(unused)] -use crate::Pallet as ZeroSense; +use crate::Pallet as Sense; use frame_benchmarking::{account, benchmarks, impl_benchmark_test_suite}; use frame_system::RawOrigin; use sp_std::vec; 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])?; - }: _(RawOrigin::Root, account("1", 0, 0), 255) + 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])?; - }: _(RawOrigin::Root, account("1", 0, 0), 255) + 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])?; - }: _(RawOrigin::Root, account("1", 0, 0), 255) + 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 70bb3ab87..a78a0e47b --- a/sense/src/lib.rs +++ b/sense/src/lib.rs @@ -16,278 +16,235 @@ //! //! 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 pallet::*; pub use weights::WeightInfo; -#[cfg(test)] +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; mod mock; - -#[cfg(test)] mod tests; +pub mod weights; -#[cfg(feature = "runtime-benchmarks")] -mod benchmarking; +pub use pallet::*; -pub mod weights; +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, +} + +#[derive(Encode, Decode, Default, PartialEq, Eq, TypeInfo)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct EntityProperty { + value: u64, + mutated: BlockNumber, +} + +impl Entity { + pub fn new( + account: AccountId, + block_number: BlockNumber, + index: u128, + cid: Vec, + ) -> Entity + where + BlockNumber: Clone, + { + Entity { account, index, cid, created: block_number.clone(), mutated: block_number } + } +} + +impl EntityProperty { + pub fn new(value: u64, block_number: BlockNumber) -> EntityProperty { + EntityProperty { value, mutated: block_number } + } +} #[frame_support::pallet] pub mod pallet { - use super::*; - use frame_support::{dispatch::DispatchResult, pallet_prelude::*}; - use frame_system::pallet_prelude::*; - use scale_info::TypeInfo; - use sp_std::vec::Vec; - - pub const MAX_STRING_FIELD_LENGTH: usize = 256; - - #[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 Entity { - account: AccountId, - index: u128, - cid: Vec, - created: BlockNumber, - mutated: BlockNumber, - } - - #[derive(Encode, Decode, Default, PartialEq, Eq, TypeInfo)] - #[cfg_attr(feature = "std", derive(Debug))] - pub struct EntityProperty { - value: u64, - mutated: BlockNumber, - } - - 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, - } - } - } - - impl EntityProperty { - pub fn new(value: u64, block_number: BlockNumber) -> EntityProperty { - EntityProperty { - value: value, - mutated: block_number, - } - } - } - - #[pallet::pallet] - #[pallet::generate_store(pub(super) trait Store)] - pub struct Pallet(_); - - #[pallet::storage] - #[pallet::getter(fn entity)] - pub(super) type Sense = StorageMap< - _, - Blake2_128Concat, - T::AccountId, - Entity, - ValueQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn xp)] - 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>; - - #[pallet::storage] - #[pallet::getter(fn trust)] - pub(super) type SenseTrust = - StorageMap<_, Blake2_128Concat, T::AccountId, EntityProperty, ValueQuery>; - - #[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 { - EntityInit(T::AccountId, T::BlockNumber), - EntityMutateXP(T::AccountId, T::BlockNumber), - EntityMutateREP(T::AccountId, T::BlockNumber), - EntityMutateTrust(T::AccountId, T::BlockNumber), - } - - // Errors inform users that something went wrong. - #[pallet::error] - pub enum Error { - /// Entity Exists - EntityExists, - /// Entity Unknown - EntityUnknown, - /// Guru Meditation - GuruMeditation, - /// Param Limit Exceed - ParamLimitExceed, - /// Invalid Param - InvalidParam, - } - - #[pallet::call] - impl Pallet { - #[pallet::weight(::WeightInfo::create_entity())] - 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 - ); - - let current_block = >::block_number(); - let index = >::get(); - - let entity = Entity::new(account.clone(), current_block, index, cid.clone()); - let xp = EntityProperty { - value: 0, - mutated: current_block.clone(), - }; - 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); - // TODO: safe increment, checked_add - >::mutate(|n| *n += 1); - - Self::deposit_event(Event::EntityInit(account, current_block)); - Ok(()) - } - - // TODO: - // mutation of values should be restricted - // certain roles are allowed to mutate values - // xp: realm - // rep: social - // trust: id - // all: governance - // sudo ( until its removal ) - - #[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 - ); - - let now = >::block_number(); - let v = u64::from(value); - let current = Self::xp(&account); - - let updated = EntityProperty { - value: current - .value - .checked_add(v) - .ok_or(Error::::GuruMeditation)?, - mutated: now.clone(), - }; - - >::insert(account.clone(), updated); - - 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 - ); - - let now = >::block_number(); - let v = u64::from(value); - let current = Self::rep(&account); - - let updated = EntityProperty { - value: current - .value - .checked_add(v) - .ok_or(Error::::GuruMeditation)?, - mutated: now.clone(), - }; - - >::insert(account.clone(), updated); - - 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 - ); - - let now = >::block_number(); - let v = u64::from(value); - let current = Self::trust(&account); - - let updated = EntityProperty { - value: current - .value - .checked_add(v) - .ok_or(Error::::GuruMeditation)?, - mutated: now, - }; - - >::insert(account.clone(), updated); - - Self::deposit_event(Event::EntityMutateTrust(account, now)); - Ok(()) - } - - // TODO: - // generic mod for all properties - } + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + type Event: From> + + IsType<::Event> + + Into<::Event>; + type ForceOrigin: EnsureOrigin; + type WeightInfo: WeightInfo; + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::storage] + #[pallet::getter(fn entity)] + 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>; + + #[pallet::storage] + #[pallet::getter(fn rep)] + 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>; + + #[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 { + EntityInit(T::AccountId, T::BlockNumber), + EntityMutateXP(T::AccountId, T::BlockNumber), + EntityMutateREP(T::AccountId, T::BlockNumber), + EntityMutateTrust(T::AccountId, T::BlockNumber), + } + + // Errors inform users that something went wrong. + #[pallet::error] + pub enum Error { + /// Entity Exists + EntityExists, + /// Entity Unknown + EntityUnknown, + /// Guru Meditation + GuruMeditation, + /// Param Limit Exceed + ParamLimitExceed, + /// Invalid Param + InvalidParam, + } + + #[pallet::call] + impl Pallet { + #[pallet::weight(::WeightInfo::create_entity())] + 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); + + let current_block = >::block_number(); + let index = >::get(); + + let entity = Entity::new(account.clone(), current_block, index, cid.clone()); + let xp = EntityProperty { value: 0, mutated: current_block.clone() }; + 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); + // TODO: safe increment, checked_add + >::mutate(|n| *n += 1); + + Self::deposit_event(Event::EntityInit(account, current_block)); + Ok(()) + } + + // TODO: + // mutation of values should be restricted + // certain roles are allowed to mutate values + // xp: realm + // rep: social + // trust: id + // all: governance + // sudo ( until its removal ) + + #[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); + + let now = >::block_number(); + let v = u64::from(value); + let current = Self::xp(&account); + + let updated = EntityProperty { + value: current.value.checked_add(v).ok_or(Error::::GuruMeditation)?, + mutated: now.clone(), + }; + + >::insert(account.clone(), updated); + + 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); + + let now = >::block_number(); + let v = u64::from(value); + let current = Self::rep(&account); + + let updated = EntityProperty { + value: current.value.checked_add(v).ok_or(Error::::GuruMeditation)?, + mutated: now.clone(), + }; + + >::insert(account.clone(), updated); + + 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); + + let now = >::block_number(); + let v = u64::from(value); + let current = Self::trust(&account); + + let updated = EntityProperty { + value: current.value.checked_add(v).ok_or(Error::::GuruMeditation)?, + mutated: now, + }; + + >::insert(account.clone(), updated); + + 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 7090a3e86..2fa0360bb 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; @@ -12,16 +13,20 @@ type Block = frame_system::mocking::MockBlock; // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( - pub enum Test where - Block = Block, - NodeBlock = Block, - UncheckedExtrinsic = UncheckedExtrinsic, - { - System: frame_system::{Pallet, Call, Config, Storage, Event}, - ZeroSense: pallet_sense::{Pallet, Call, Storage, Event}, - } + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, 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 6df633439..bcf87b71d 100644 --- a/sense/src/tests.rs +++ b/sense/src/tests.rs @@ -1,5 +1,6 @@ +#![cfg(test)] use super::{ - mock::*, Entity, EntityProperty, Error, Event as ZeroEvent, Sense, SenseREP, SenseTrust, + mock::*, Entity, EntityProperty, Error, Event as SenseEvent, SenseEntity, SenseREP, SenseTrust, SenseXP, }; use frame_support::{assert_noop, assert_ok}; @@ -8,8 +9,8 @@ use sp_runtime::traits::BadOrigin; #[test] fn sense_create_entity() { - new_test_ext().execute_with(|| { - let cid = vec![1, 2, 3]; + new_test_ext().execute_with(|| { + let cid = vec![1, 2, 3]; let account = 1; let index = 0; @@ -17,60 +18,47 @@ fn sense_create_entity() { System::set_block_number(block_number); - assert_noop!( - ZeroSense::create_entity(RawOrigin::Root.into(), 1, vec![]), - Error::::InvalidParam - ); - assert_noop!( - ZeroSense::create_entity(RawOrigin::Root.into(), 1, vec![1u8; 257]), - Error::::ParamLimitExceed - ); + assert_noop!( + Sense::create_entity(RawOrigin::Root.into(), 1, vec![]), + Error::::InvalidParam + ); + 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) - ); + assert_eq!( + Entity::new(account, block_number, index, cid.clone()), + 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)), - topics: vec![], - }] - ); + assert_eq!( + System::events(), + vec![EventRecord { + phase: Phase::Initialization, + event: Event::Sense(SenseEvent::EntityInit(account, block_number)), + topics: vec![], + }] + ); - // TODO: Check Nonce value increased in storage as a result of successful extrinsic call. + // 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!(Sense::create_entity(Origin::signed(1), 1, vec![1u8]), BadOrigin); - assert_noop!( - ZeroSense::create_entity(RawOrigin::Root.into(), account, cid.clone()), - Error::::EntityExists - ); - }); + 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,)*) => { @@ -88,7 +76,7 @@ macro_rules! sense_mod_tests { Error::::EntityUnknown ); - Sense::::insert( + SenseEntity::::insert( account, Entity::new(account, block_number, 0, vec![1,2,3]) ); $storage::::insert( @@ -105,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 69ce70f7a..51c7084cd 100644 --- a/sense/src/weights.rs +++ b/sense/src/weights.rs @@ -39,8 +39,8 @@ #![allow(unused_imports)] use frame_support::{ - traits::Get, - weights::{constants::RocksDbWeight, Weight}, + traits::Get, + weights::{constants::RocksDbWeight, Weight}, }; use sp_std::marker::PhantomData; From ee2e88955e7eb6596136340da64549eb714538d0 Mon Sep 17 00:00:00 2001 From: vasylenko-yevhen Date: Fri, 11 Mar 2022 22:53:23 +0200 Subject: [PATCH 08/10] Refactoring and added some todos --- signal/Cargo.toml | 2 +- signal/src/lib.rs | 43 +++++++++++++++++++++++------------------- signal/src/mock.rs | 4 ++-- signal/src/tests.rs | 46 +++++---------------------------------------- 4 files changed, 32 insertions(+), 63 deletions(-) diff --git a/signal/Cargo.toml b/signal/Cargo.toml index 832dc2bc6..df52c460a 100644 --- a/signal/Cargo.toml +++ b/signal/Cargo.toml @@ -30,7 +30,7 @@ frame-support = { git = "https://github.com/paritytech/substrate", branch = "pol frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } 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 } +zero-primitives = { package = "zero-primitives", path = "../../primitives", default-features = false } support = { package = "gamedao-protocol-support", path = "../support", default-features = false } [dev-dependencies] diff --git a/signal/src/lib.rs b/signal/src/lib.rs index 5f923756e..53370841d 100644 --- a/signal/src/lib.rs +++ b/signal/src/lib.rs @@ -12,6 +12,15 @@ // Copyright (C) 2010-2020 ZERO Labs. // SPDX-License-Identifier: Apache-2.0 +// Proposals and voting space for organizations and campaigns. +// This pallet provides next features: +// * Allow members of organisations to generate proposals under campaign. +// Each proposal has a lifitime, expiration, details and number of votes. +// Specific type of proposal is withdrawal one. +// It allows (if approved) to release locked campaign balance for further usage. +// * Vote on those proposals. +// * Manage proposal lifetime, close and finalize those proposals once expired. + #![cfg_attr(not(feature = "std"), no_std)] pub use pallet::*; @@ -23,8 +32,8 @@ pub mod voting_structs; pub mod mock; #[cfg(test)] mod tests; -// #[cfg(feature = "runtime-benchmarks")] -mod benchmarking; +// #[cfg(feature = "runtime-benchmarks")] // todo +// mod benchmarking; #[frame_support::pallet] @@ -43,18 +52,17 @@ pub mod pallet { use sp_std::vec::Vec; use orml_traits::{MultiCurrency, MultiReservableCurrency}; - use primitives::{Balance, CurrencyId}; + use zero_primitives::{Balance, CurrencyId}; use support::{ ControlPalletStorage, ControlState, ControlMemberState, FlowPalletStorage, FlowState }; + use super::*; use voting_enums::{ProposalState, ProposalType, VotingType}; use voting_structs::{Proposal, ProposalMetadata}; - // type Balance = <::Currency as Currency<::AccountId>>::Balance; - #[pallet::config] pub trait Config: frame_system::Config { type Event: From> + IsType<::Event> + Into<::Event>; @@ -208,17 +216,19 @@ pub mod pallet { proposal_id: T::Hash, vote: bool }, - ProposalFinalized(T::Hash, u8), + // ProposalFinalized(T::Hash, u8), ProposalApproved { proposal_id: T::Hash }, ProposalRejected { proposal_id: T::Hash }, - ProposalExpired(T::Hash), - ProposalAborted(T::Hash), - ProposalError(T::Hash, Vec), - WithdrawalGranted{ + ProposalExpired { + proposal_id: T::Hash + }, + // ProposalAborted(T::Hash), + // ProposalError(T::Hash, Vec), + WithdrawalGranted { proposal_id: T::Hash, context_id: T::Hash, body_id: T::Hash @@ -368,15 +378,10 @@ pub mod pallet { // init votes >::insert(context_id, (0,0)); - // - // - // - - // nonce++ - // >::mutate(|n| *n += 1); // todo: use safe addition - // deposit event - Self::deposit_event(Event::::Proposal{sender_id: sender, proposal_id}); + Self::deposit_event( + Event::::Proposal{sender_id: sender, proposal_id} + ); Ok(()) } @@ -763,7 +768,7 @@ pub mod pallet { }, ProposalState::Expired => { Self::deposit_event( - Event::::ProposalExpired(proposal_id.clone()) + Event::::ProposalExpired {proposal_id: proposal_id.clone()} ); }, _ => {} diff --git a/signal/src/mock.rs b/signal/src/mock.rs index 4db6b7dc4..8a66a3be0 100644 --- a/signal/src/mock.rs +++ b/signal/src/mock.rs @@ -16,10 +16,9 @@ use support::{ ControlPalletStorage, ControlState, ControlMemberState, FlowPalletStorage, FlowState }; -use primitives::{Amount, Balance, CurrencyId, Hash, TokenSymbol}; +use zero_primitives::{Amount, Balance, BlockNumber, CurrencyId, Hash, TokenSymbol}; pub type AccountId = u64; -pub type BlockNumber = u32; pub const ACC1: AccountId = 1; pub const ACC2: AccountId = 2; @@ -39,6 +38,7 @@ pub struct FlowFixture { pub campaign_org: Hash } +// todo: use actual Control & Flow pallets once they are done thread_local!( pub static control_fixture: RefCell = RefCell::new(ControlFixture { body_controller: ACC1, diff --git a/signal/src/tests.rs b/signal/src/tests.rs index dd2d20288..a573d5cd6 100644 --- a/signal/src/tests.rs +++ b/signal/src/tests.rs @@ -1,34 +1,7 @@ #[cfg(test)] use super::{ - Proposals, - Metadata, - Owners, - ProposalsByBlock, - ProposalStates, - ProposalsCount, - ProposalsIndex, - ProposalsArray, - ProposalsByContextArray, - ProposalsByContextCount, - ProposalsByContextIndex, - ProposalsByOwnerArray, - ProposalsByOwnerCount, - ProposalsByOwnerIndex, - ProposalsByContext, - ProposalTimeLimit, - ProposalSimpleVotes, - ProposalApprovers, - ProposalDeniers, - VotedBefore, - ProposalsByVoterCount, - ProposalVotesByVoters, - ProposalsByVoter, - ProposalVoters, - CampaignBalanceUsed, - Nonce, - Config, - Error, + *, voting_structs::{Proposal, ProposalMetadata}, voting_enums::{VotingType, ProposalType, ProposalState}, mock::{ @@ -125,10 +98,7 @@ fn signal_general_proposal_success() { 15 // expiry ) ); - assert_eq!( - >::get(15), - vec![proposal_id.clone(), new_proposal_id.clone()] - ); + assert_eq!(>::get(15), vec![proposal_id.clone(), new_proposal_id.clone()]); assert_eq!(>::get(1), new_proposal_id); assert_eq!(>::get(), 2); assert_eq!(>::get(&new_proposal_id), 1); @@ -138,10 +108,7 @@ fn signal_general_proposal_success() { assert_eq!(>::get((ACC1, 1)), new_proposal_id); assert_eq!(>::get(ACC1), 2); assert_eq!(>::get((ACC1, new_proposal_id)), 1); - assert_eq!( - >::get(ctx_id), - vec![proposal_id.clone(), new_proposal_id.clone()] - ); + assert_eq!(>::get(ctx_id), vec![proposal_id.clone(), new_proposal_id.clone()]); assert_eq!(>::get(), 2); }); } @@ -349,10 +316,7 @@ fn signal_withdraw_proposal_success() { assert_eq!(>::get((ACC1, 1)), new_proposal_id); assert_eq!(>::get(ACC1), 2); assert_eq!(>::get((ACC1, new_proposal_id)), 1); - assert_eq!( - >::get(ctx_id), - vec![proposal_id.clone(), new_proposal_id.clone()] - ); + assert_eq!(>::get(ctx_id), vec![proposal_id.clone(), new_proposal_id.clone()]); assert_eq!(>::get(), 2); }); @@ -563,6 +527,7 @@ fn signal_simple_vote_success() { assert_eq!(>::get(&proposal_id), 0); assert_eq!(>::get(&proposal_id), 1); + // todo: delete this after `unlock_balance` will be moved out from extrinsic call // assert_ok!( // Signal::simple_vote(Origin::signed(ACC2), proposal_id, true) // ); @@ -699,7 +664,6 @@ fn signal_on_finalize_success() { ) ); } - // todo: few votes for each item let mut events_before = System::events().len(); assert_eq!(events_before, 10); From 90e417f36af5b5591e1041840f937056bedb5df7 Mon Sep 17 00:00:00 2001 From: vasylenko-yevhen Date: Fri, 11 Mar 2022 22:55:15 +0200 Subject: [PATCH 09/10] Use actual Sense pallet version --- sense/src/benchmarking.rs | 29 +++--- sense/src/lib.rs | 184 ++++++++++++++++++++------------------ sense/src/mock.rs | 80 ++++++++--------- sense/src/tests.rs | 77 +++++++++------- sense/src/weights.rs | 96 ++++++++++---------- 5 files changed, 238 insertions(+), 228 deletions(-) mode change 100644 => 100755 sense/src/lib.rs diff --git a/sense/src/benchmarking.rs b/sense/src/benchmarking.rs index bce558e95..d38ece214 100644 --- a/sense/src/benchmarking.rs +++ b/sense/src/benchmarking.rs @@ -2,30 +2,31 @@ use super::*; #[allow(unused)] -use crate::Pallet as Sense; -use frame_benchmarking::{account, benchmarks, impl_benchmark_test_suite}; +use crate::Pallet as ZeroSense; +use frame_benchmarking::{benchmarks, impl_benchmark_test_suite, account}; use frame_system::RawOrigin; use sp_std::vec; -benchmarks! { - create_entity {}: _(RawOrigin::Root, account("1", 0, 0), vec![1; 256]) +benchmarks!{ - mod_xp { - let caller_origin = ::Origin::from(RawOrigin::Root); - Sense::::create_entity(caller_origin, account("1", 0, 0), vec![1; 1])?; + 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])?; }: _(RawOrigin::Root, account("1", 0, 0), 255) - mod_rep { - let caller_origin = ::Origin::from(RawOrigin::Root); - Sense::::create_entity(caller_origin, account("1", 0, 0), vec![1; 1])?; + mod_rep { + let caller_origin = ::Origin::from(RawOrigin::Root); + ZeroSense::::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); - Sense::::create_entity(caller_origin, account("1", 0, 0), vec![1; 1])?; + mod_trust { + let caller_origin = ::Origin::from(RawOrigin::Root); + ZeroSense::::create_entity(caller_origin, account("1", 0, 0), vec![1; 1])?; }: _(RawOrigin::Root, account("1", 0, 0), 255) } -impl_benchmark_test_suite!(Sense, crate::mock::new_test_ext(), crate::mock::Test); +impl_benchmark_test_suite!(ZeroSense, crate::mock::new_test_ext(), crate::mock::Test); diff --git a/sense/src/lib.rs b/sense/src/lib.rs old mode 100644 new mode 100755 index a78a0e47b..19b178425 --- a/sense/src/lib.rs +++ b/sense/src/lib.rs @@ -16,104 +16,98 @@ //! //! 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; - -#[cfg(feature = "runtime-benchmarks")] -mod benchmarking; -mod mock; -mod tests; -pub mod weights; - pub use pallet::*; -pub const MAX_STRING_FIELD_LENGTH: usize = 256; +#[cfg(test)] +mod mock; -#[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, -} +#[cfg(test)] +mod tests; -#[derive(Encode, Decode, Default, PartialEq, Eq, TypeInfo)] -#[cfg_attr(feature = "std", derive(Debug))] -pub struct EntityProperty { - value: u64, - mutated: BlockNumber, -} +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; -impl Entity { - pub fn new( - account: AccountId, - block_number: BlockNumber, - index: u128, - cid: Vec, - ) -> Entity - where - BlockNumber: Clone, - { - Entity { account, index, cid, created: block_number.clone(), mutated: block_number } - } -} +pub mod weights; -impl EntityProperty { - pub fn new(value: u64, block_number: BlockNumber) -> EntityProperty { - EntityProperty { value, mutated: block_number } - } -} #[frame_support::pallet] pub mod pallet { - use super::*; - use frame_support::pallet_prelude::*; + 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; #[pallet::config] pub trait Config: frame_system::Config { - type Event: From> - + IsType<::Event> - + Into<::Event>; + 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 Entity { + account: AccountId, + index: u128, + cid: Vec, + created: BlockNumber, + mutated: BlockNumber, + } + + #[derive(Encode, Decode, Default, PartialEq, Eq, TypeInfo)] + #[cfg_attr(feature = "std", derive(Debug))] + pub struct EntityProperty { + value: u64, + mutated: BlockNumber, + } + + 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, + } + } + } + + impl EntityProperty { + pub fn new(value: u64, block_number: BlockNumber) + -> EntityProperty { + EntityProperty { + value: value, + mutated: block_number, + } + } + } + #[pallet::pallet] #[pallet::generate_store(pub(super) trait Store)] pub struct Pallet(_); #[pallet::storage] #[pallet::getter(fn entity)] - pub(super) type SenseEntity = StorageMap< - _, - Blake2_128Concat, - T::AccountId, - Entity, - ValueQuery, - >; + pub(super) type Sense = 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)] @@ -140,21 +134,19 @@ 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(); @@ -164,15 +156,18 @@ 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: @@ -186,8 +181,9 @@ 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); @@ -195,19 +191,23 @@ 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,19 +215,23 @@ 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); @@ -235,16 +239,20 @@ 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 2fa0360bb..67c1e4140 100644 --- a/sense/src/mock.rs +++ b/sense/src/mock.rs @@ -1,11 +1,10 @@ -#![cfg(test)] - +use crate as pallet_sense; use frame_support::parameter_types; use frame_system as system; use sp_core::H256; use sp_runtime::{ - testing::Header, - traits::{BlakeTwo256, IdentityLookup}, + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, }; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; @@ -19,58 +18,51 @@ frame_support::construct_runtime!( UncheckedExtrinsic = UncheckedExtrinsic, { System: frame_system::{Pallet, Call, Config, Storage, Event}, - Sense: pallet_sense::{Pallet, Call, Storage, Event}, + ZeroSense: 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; - pub BlockWeights: frame_system::limits::BlockWeights = - frame_system::limits::BlockWeights::simple_max(1024); - pub static ExistentialDeposit: u64 = 0; + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(1024); + pub static ExistentialDeposit: u64 = 0; } impl system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type Origin = Origin; - type Call = Call; - type Index = u64; - type BlockNumber = u64; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type Header = Header; - type Event = Event; - type BlockHashCount = BlockHashCount; - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = (); - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = SS58Prefix; - type OnSetCode = (); + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); } impl pallet_sense::Config for Test { - type Event = Event; - type ForceOrigin = frame_system::EnsureRoot; - type WeightInfo = (); + type Event = Event; + type ForceOrigin = frame_system::EnsureRoot; + type WeightInfo = (); } // Build genesis storage according to the mock runtime. pub fn new_test_ext() -> sp_io::TestExternalities { - system::GenesisConfig::default() - .build_storage::() - .unwrap() - .into() + system::GenesisConfig::default().build_storage::().unwrap().into() } diff --git a/sense/src/tests.rs b/sense/src/tests.rs index bcf87b71d..84f877ffe 100644 --- a/sense/src/tests.rs +++ b/sense/src/tests.rs @@ -1,64 +1,75 @@ -#![cfg(test)] -use super::{ - mock::*, Entity, EntityProperty, Error, Event as SenseEvent, SenseEntity, SenseREP, SenseTrust, - SenseXP, -}; +use super::{mock::*, Error, EntityProperty, Entity, Sense, SenseXP, SenseREP, SenseTrust, Event as ZeroEvent}; 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 account = 1; - let index = 0; - let block_number = 3; + let cid = vec![1,2,3]; + + let account = 1; + let index = 0; + let block_number = 3; - System::set_block_number(block_number); + System::set_block_number(block_number); - assert_noop!( - Sense::create_entity(RawOrigin::Root.into(), 1, vec![]), + assert_noop!(ZeroSense::create_entity( + RawOrigin::Root.into(), 1, vec![]), Error::::InvalidParam ); - assert_noop!( - Sense::create_entity(RawOrigin::Root.into(), 1, vec![1u8; 257]), + assert_noop!(ZeroSense::create_entity( + RawOrigin::Root.into(), 1, vec![1u8; 257]), Error::::ParamLimitExceed ); - assert_ok!(Sense::create_entity(RawOrigin::Root.into(), account, cid.clone())); - + assert_ok!(ZeroSense::create_entity( + RawOrigin::Root.into(), account, cid.clone()) + ); + assert_eq!( Entity::new(account, block_number, index, cid.clone()), - Sense::entity(account) + 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) ); - 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::Sense(SenseEvent::EntityInit(account, block_number)), + event: Event::ZeroSense(ZeroEvent::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!(Sense::create_entity(Origin::signed(1), 1, vec![1u8]), BadOrigin); - - assert_noop!( - Sense::create_entity(RawOrigin::Root.into(), account, cid.clone()), + assert_noop!(ZeroSense::create_entity( + RawOrigin::Root.into(), account, cid.clone()), Error::::EntityExists ); + }); + } // TODO: 1. Test: StorageMap value updated after calling extrinsic (SenseXP etc.) -// 2. Test: event is generated (EntityMutateXP etc.) +// 2. Tese: event is generated (EntityMutateXP etc.) // 3. Add comments macro_rules! sense_mod_tests { ($($name:ident: $storage:tt, $extrinsic:path,)*) => { @@ -69,20 +80,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 ); - - SenseEntity::::insert( + + Sense::::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) ); @@ -93,7 +104,7 @@ macro_rules! sense_mod_tests { } sense_mod_tests! { - sense_mod_xp: SenseXP, Sense::mod_xp, - sense_mod_rep: SenseREP, Sense::mod_rep, - sense_mod_trust: SenseTrust, Sense::mod_trust, + sense_mod_xp: SenseXP, ZeroSense::mod_xp, + sense_mod_rep: SenseREP, ZeroSense::mod_rep, + sense_mod_trust: SenseTrust, ZeroSense::mod_trust, } diff --git a/sense/src/weights.rs b/sense/src/weights.rs index 51c7084cd..967c51460 100644 --- a/sense/src/weights.rs +++ b/sense/src/weights.rs @@ -35,68 +35,66 @@ // --output=./pallets/sense/src/weights.rs // --template=./.maintain/frame-weight-template.hbs + #![allow(unused_parens)] #![allow(unused_imports)] -use frame_support::{ - traits::Get, - weights::{constants::RocksDbWeight, Weight}, -}; +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; use sp_std::marker::PhantomData; /// Weight functions needed for pallet_sense. pub trait WeightInfo { - fn create_entity() -> Weight; - fn mod_xp() -> Weight; - fn mod_rep() -> Weight; - fn mod_trust() -> Weight; + fn create_entity() -> Weight; + fn mod_xp() -> Weight; + fn mod_rep() -> Weight; + fn mod_trust() -> Weight; } /// Weights for pallet_sense using the Substrate node and recommended hardware. pub struct SubstrateWeight(PhantomData); impl WeightInfo for SubstrateWeight { - fn create_entity() -> Weight { - (23_000_000 as Weight) - .saturating_add(T::DbWeight::get().reads(2 as Weight)) - .saturating_add(T::DbWeight::get().writes(5 as Weight)) - } - fn mod_xp() -> Weight { - (18_000_000 as Weight) - .saturating_add(T::DbWeight::get().reads(2 as Weight)) - .saturating_add(T::DbWeight::get().writes(1 as Weight)) - } - fn mod_rep() -> Weight { - (17_000_000 as Weight) - .saturating_add(T::DbWeight::get().reads(2 as Weight)) - .saturating_add(T::DbWeight::get().writes(1 as Weight)) - } - fn mod_trust() -> Weight { - (17_000_000 as Weight) - .saturating_add(T::DbWeight::get().reads(2 as Weight)) - .saturating_add(T::DbWeight::get().writes(1 as Weight)) - } + fn create_entity() -> Weight { + (23_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(5 as Weight)) + } + fn mod_xp() -> Weight { + (18_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + fn mod_rep() -> Weight { + (17_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + fn mod_trust() -> Weight { + (17_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } } // For backwards compatibility and tests impl WeightInfo for () { - fn create_entity() -> Weight { - (23_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(2 as Weight)) - .saturating_add(RocksDbWeight::get().writes(5 as Weight)) - } - fn mod_xp() -> Weight { - (18_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(2 as Weight)) - .saturating_add(RocksDbWeight::get().writes(1 as Weight)) - } - fn mod_rep() -> Weight { - (17_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(2 as Weight)) - .saturating_add(RocksDbWeight::get().writes(1 as Weight)) - } - fn mod_trust() -> Weight { - (17_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(2 as Weight)) - .saturating_add(RocksDbWeight::get().writes(1 as Weight)) - } -} + fn create_entity() -> Weight { + (23_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(5 as Weight)) + } + fn mod_xp() -> Weight { + (18_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + fn mod_rep() -> Weight { + (17_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + fn mod_trust() -> Weight { + (17_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } +} \ No newline at end of file From 54c65e2d262e7daee6cd33089d166275a1ba8818 Mon Sep 17 00:00:00 2001 From: vasylenko-yevhen Date: Fri, 11 Mar 2022 23:16:38 +0200 Subject: [PATCH 10/10] Fix indentation --- signal/Cargo.toml | 26 +- signal/src/lib.rs | 1624 ++++++++++++++++++++++---------------------- signal/src/mock.rs | 114 ++-- 3 files changed, 882 insertions(+), 882 deletions(-) diff --git a/signal/Cargo.toml b/signal/Cargo.toml index df52c460a..2b259724a 100644 --- a/signal/Cargo.toml +++ b/signal/Cargo.toml @@ -15,9 +15,9 @@ description = 'Signal pallet' [package.metadata.substrate] categories = [ - 'zero', - 'core', - 'pallet' + 'zero', + 'core', + 'pallet' ] [dependencies] @@ -47,22 +47,22 @@ orml-currencies = { path = "../../orml/currencies", default-features = false } default = ['std'] runtime-benchmarks = ['frame-benchmarking'] std = [ - 'codec/std', - 'serde/std', - 'scale-info/std', + 'codec/std', + 'serde/std', + 'scale-info/std', - 'frame-support/std', - 'frame-system/std', - 'frame-benchmarking/std', + 'frame-support/std', + 'frame-system/std', + 'frame-benchmarking/std', - 'sp-core/std', - 'sp-std/std', - 'sp-runtime/std', + 'sp-core/std', + 'sp-std/std', + 'sp-runtime/std', "orml-traits/std", "orml-tokens/std", "orml-currencies/std", - "support/std" + "support/std" ] try-runtime = ['frame-support/try-runtime'] diff --git a/signal/src/lib.rs b/signal/src/lib.rs index 53370841d..177881c99 100644 --- a/signal/src/lib.rs +++ b/signal/src/lib.rs @@ -1,12 +1,12 @@ // -// _______________________________ ________ -// \____ /\_ _____/\______ \\_____ \ -// / / | __)_ | _/ / | \ -// / /_ | \ | | \/ | \ -// /_______ \/_______ / |____|_ /\_______ / -// \/ \/ \/ \/ -// Z E R O . I O N E T W O R K -// © C O P Y R I O T 2 0 7 5 @ Z E R O . I O +// _______________________________ ________ +// \____ /\_ _____/\______ \\_____ \ +// / / | __)_ | _/ / | \ +// / /_ | \ | | \/ | \ +// /_______ \/_______ / |____|_ /\_______ / +// \/ \/ \/ \/ +// Z E R O . I O N E T W O R K +// © C O P Y R I O T 2 0 7 5 @ Z E R O . I O // This file is part of ZERO Network. // Copyright (C) 2010-2020 ZERO Labs. @@ -15,9 +15,9 @@ // Proposals and voting space for organizations and campaigns. // This pallet provides next features: // * Allow members of organisations to generate proposals under campaign. -// Each proposal has a lifitime, expiration, details and number of votes. -// Specific type of proposal is withdrawal one. -// It allows (if approved) to release locked campaign balance for further usage. +// Each proposal has a lifitime, expiration, details and number of votes. +// Specific type of proposal is withdrawal one. +// It allows (if approved) to release locked campaign balance for further usage. // * Vote on those proposals. // * Manage proposal lifetime, close and finalize those proposals once expired. @@ -38,807 +38,807 @@ mod tests; #[frame_support::pallet] pub mod pallet { - use frame_system::{ - ensure_signed, - pallet_prelude::{OriginFor, BlockNumberFor}, - WeightInfo - }; - use frame_support::{ - dispatch::DispatchResult, - traits::{Randomness}, - pallet_prelude::*, - transactional - }; - use sp_std::vec::Vec; - use orml_traits::{MultiCurrency, MultiReservableCurrency}; - - use zero_primitives::{Balance, CurrencyId}; - use support::{ - ControlPalletStorage, ControlState, ControlMemberState, - FlowPalletStorage, FlowState - }; - - use super::*; - use voting_enums::{ProposalState, ProposalType, VotingType}; - use voting_structs::{Proposal, ProposalMetadata}; - - - #[pallet::config] - pub trait Config: frame_system::Config { - type Event: From> + IsType<::Event> + Into<::Event>; - type Currency: MultiCurrency - + MultiReservableCurrency; - type Randomness: Randomness; - type Control: ControlPalletStorage; - type Flow: FlowPalletStorage; - type ForceOrigin: EnsureOrigin; - type WeightInfo: WeightInfo; - - #[pallet::constant] - type MaxProposalsPerBlock: Get; // 3 - - #[pallet::constant] - type MaxProposalDuration: Get; // 864000, 60 * 60 * 24 * 30 / 3 - - #[pallet::constant] - type FundingCurrencyId: Get; - } - - - #[pallet::pallet] - #[pallet::generate_store(pub(super) trait Store)] - pub struct Pallet(_); - - - /// Global status - #[pallet::storage] - pub(super) type Proposals = StorageMap<_, Blake2_128Concat, T::Hash, Proposal, ValueQuery>; - - #[pallet::storage] - pub(super) type Metadata = StorageMap<_, Blake2_128Concat, T::Hash, ProposalMetadata, ValueQuery>; - - #[pallet::storage] - pub(super) type Owners = StorageMap<_, Blake2_128Concat, T::Hash, T::AccountId, OptionQuery>; - - #[pallet::storage] - pub(super) type ProposalStates = StorageMap<_, Blake2_128Concat, T::Hash, ProposalState, ValueQuery, GetDefault>; - - /// Maximum time limit for a proposal - #[pallet::type_value] - pub(super) fn ProposalTimeLimitDefault() -> T::BlockNumber { T::BlockNumber::from(T::MaxProposalDuration::get()) } - #[pallet::storage] - pub(super) type ProposalTimeLimit = StorageValue <_, T::BlockNumber, ValueQuery, ProposalTimeLimitDefault>; - - /// All proposals - #[pallet::storage] - pub(super) type ProposalsArray = StorageMap<_, Blake2_128Concat, u64, T::Hash, ValueQuery>; - - #[pallet::storage] - pub(super) type ProposalsCount = StorageValue<_, u64, ValueQuery>; - - #[pallet::storage] - pub(super) type ProposalsIndex = StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery>; - - /// Proposals by campaign / org - #[pallet::storage] - pub(super) type ProposalsByContextArray = StorageMap<_, Blake2_128Concat, (T::Hash, u64), T::Hash, ValueQuery>; - - #[pallet::storage] - pub(super) type ProposalsByContextCount = StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery>; - - #[pallet::storage] - pub(super) type ProposalsByContextIndex = StorageMap<_, Blake2_128Concat, (T::Hash, T::Hash), u64, ValueQuery>; - - /// all proposals for a given context - #[pallet::storage] - pub(super) type ProposalsByContext = StorageMap<_, Blake2_128Concat, T::Hash, Vec, ValueQuery>; - - /// Proposals by owner - #[pallet::storage] - pub(super) type ProposalsByOwnerArray = StorageMap<_, Blake2_128Concat, (T::AccountId, u64), T::Hash, ValueQuery>; - - #[pallet::storage] - pub(super) type ProposalsByOwnerCount = StorageMap<_, Blake2_128Concat, T::AccountId, u64, ValueQuery>; - - #[pallet::storage] - pub(super) type ProposalsByOwnerIndex = StorageMap<_, Blake2_128Concat, (T::AccountId, T::Hash), u64, ValueQuery>; - - /// Proposals where voter participated - #[pallet::storage] - pub(super) type ProposalsByVoter = StorageMap<_, Blake2_128Concat, T::AccountId, Vec<(T::Hash, bool)>, ValueQuery>; - - /// Proposal voters and votes by proposal - #[pallet::storage] - pub(super) type ProposalVotesByVoters = StorageMap<_, Blake2_128Concat, T::Hash, Vec<(T::AccountId, bool)>, ValueQuery>; - - /// Total proposals voted on by voter - #[pallet::storage] - pub(super) type ProposalsByVoterCount = StorageMap<_, Blake2_128Concat, T::AccountId, u64, ValueQuery>; - - /// Proposals ending in a block - #[pallet::storage] - pub(super) type ProposalsByBlock = StorageMap<_, Blake2_128Concat, T::BlockNumber, Vec, ValueQuery>; - - /// The amount of currency that a project has used - #[pallet::storage] - pub(super) type CampaignBalanceUsed = StorageMap<_, Blake2_128Concat, T::Hash, Balance, ValueQuery>; - - /// The number of people who approve a proposal - #[pallet::storage] - pub(super) type ProposalApprovers = StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery, GetDefault>; - - /// The number of people who deny a proposal - #[pallet::storage] - pub(super) type ProposalDeniers = StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery, GetDefault>; - - /// Voters per proposal - #[pallet::storage] - pub(super) type ProposalVoters = StorageMap<_, Blake2_128Concat, T::Hash, Vec, ValueQuery>; - - /// Voter count per proposal - #[pallet::storage] - pub(super) type ProposalVotes = StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery, GetDefault>; - - /// Ack vs Nack - #[pallet::storage] - pub(super) type ProposalSimpleVotes = StorageMap<_, Blake2_128Concat, T::Hash, (u64, u64), ValueQuery, GetDefault>; - - /// User has voted on a proposal - #[pallet::storage] - pub(super) type VotedBefore = StorageMap<_, Blake2_128Concat, (T::AccountId, T::Hash), bool, ValueQuery, GetDefault>; - - // TODO: ProposalTotalEligibleVoters - // TODO: ProposalApproversWeight - // TODO: ProposalDeniersWeight - // TODO: ProposalTotalEligibleWeight - - /// The total number of proposals - #[pallet::storage] - pub(super) type Nonce = StorageValue<_, u128, ValueQuery>; - - - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - Proposal { - sender_id: T::AccountId, - proposal_id: T::Hash - }, - ProposalCreated { - sender_id: T::AccountId, - context_id: T::Hash, - proposal_id: T::Hash, - amount: Balance, - expiry: T::BlockNumber, - }, - ProposalVoted { - sender_id: T::AccountId, - proposal_id: T::Hash, - vote: bool - }, - // ProposalFinalized(T::Hash, u8), - ProposalApproved { - proposal_id: T::Hash - }, - ProposalRejected { - proposal_id: T::Hash - }, - ProposalExpired { - proposal_id: T::Hash - }, - // ProposalAborted(T::Hash), - // ProposalError(T::Hash, Vec), - WithdrawalGranted { - proposal_id: T::Hash, - context_id: T::Hash, - body_id: T::Hash - }, - } - - #[pallet::error] - pub enum Error { - /// Proposal Ended - ProposalEnded, - /// Proposal Exists - ProposalExists, - /// Proposal Expired - ProposalExpired, - /// Already Voted - AlreadyVoted, - /// Proposal Unknown - ProposalUnknown, - /// DAO Inactive - DAOInactive, - /// Authorization Error - AuthorizationError, - /// Tangram Creation Failed - TangramCreationError, - /// Out Of Bounds Error - OutOfBounds, - /// Unknown Error - UnknownError, - ///MemberExists - MemberExists, - /// Unknown Campaign - CampaignUnknown, - /// Campaign Failed - CampaignFailed, - /// Balance Too Low - BalanceInsufficient, - /// Hash Collision - HashCollision, - /// Unknown Account - UnknownAccount, - /// Too Many Proposals for block - TooManyProposals, - /// Overflow Error - OverflowError, - /// Division Error - DivisionError - } - - #[pallet::call] - impl Pallet { - - - // TODO: general proposal for a DAO - #[pallet::weight(5_000_000)] - #[transactional] - pub fn general_proposal( - origin: OriginFor, - context_id: T::Hash, - title: Vec, - cid: Vec, - start: T::BlockNumber, - expiry: T::BlockNumber - ) -> DispatchResult { - - let sender = ensure_signed(origin)?; - - // active/existing dao? - ensure!(T::Control::body_state(&context_id) == ControlState::Active, Error::::DAOInactive); - - // member of body? - let member = T::Control::body_member_state(&context_id, &sender); - ensure!(member == ControlMemberState::Active, Error::::AuthorizationError); - - // ensure that start and expiry are in bounds - let current_block = >::block_number(); - // ensure!(start > current_block, Error::::OutOfBounds ); - ensure!(expiry > current_block, Error::::OutOfBounds ); - ensure!(expiry <= current_block + >::get(), Error::::OutOfBounds ); - - // ensure that number of proposals - // ending in target block - // do not exceed the maximum - let proposals = >::get(expiry); - // ensure!(proposals.len() as u32 < T::MaxProposalsPerBlock::get(), "Maximum number of proposals is reached for the target block, try another block"); - ensure!((proposals.len() as u32) < T::MaxProposalsPerBlock::get(), Error::::TooManyProposals); // todo: was error generated manually on purpose? - - let proposal_type = ProposalType::General; - let proposal_state = ProposalState::Active; - let voting_type = VotingType::Simple; - // ensure!(!>::contains_key(&context_id), "Proposal id already exists"); - ensure!(!>::contains_key(&context_id), Error::::ProposalExists); // todo: was error generated manually on purpose? - - - // check add - let proposals_count = >::get(); - let updated_proposals_count = proposals_count.checked_add(1).ok_or( Error::::OverflowError)?; - let proposals_by_campaign_count = >::get(&context_id); - let updated_proposals_by_campaign_count = proposals_by_campaign_count.checked_add(1).ok_or( Error::::OverflowError )?; - let proposals_by_owner_count = >::get(&sender); - let updated_proposals_by_owner_count = proposals_by_owner_count.checked_add(1).ok_or( Error::::OverflowError )?; - - // proposal - - let nonce = Self::get_and_increment_nonce(); - let (proposal_id, _) = ::random(&nonce); - let new_proposal = Proposal { - proposal_id: proposal_id.clone(), - context_id: context_id.clone(), - proposal_type, - voting_type, - start, - expiry, - }; - - // metadata - - let metadata = ProposalMetadata { - title: title, - cid: cid, - amount: 0 - }; - - // - // - // - - // insert proposals - >::insert(proposal_id.clone(), new_proposal.clone()); - >::insert(proposal_id.clone(), metadata.clone()); - >::insert(proposal_id.clone(), sender.clone()); - >::insert(proposal_id.clone(), proposal_state); - // update max per block - >::mutate(expiry, |proposals| proposals.push(proposal_id.clone())); - // update proposal map - >::insert(&proposals_count, proposal_id.clone()); - >::put(updated_proposals_count); - >::insert(proposal_id.clone(), proposals_count); - // update campaign map - >::insert((context_id.clone(), proposals_by_campaign_count.clone()), proposal_id.clone()); - >::insert(context_id.clone(), updated_proposals_by_campaign_count); - >::insert((context_id.clone(), proposal_id.clone()), proposals_by_campaign_count); - >::mutate( context_id.clone(), |proposals| proposals.push(proposal_id.clone()) ); - // update owner map - >::insert((sender.clone(), proposals_by_owner_count.clone()), proposal_id.clone()); - >::insert(sender.clone(), updated_proposals_by_owner_count); - >::insert((sender.clone(), proposal_id.clone()), proposals_by_owner_count); - // init votes - >::insert(context_id, (0,0)); - - // deposit event - Self::deposit_event( - Event::::Proposal{sender_id: sender, proposal_id} - ); - Ok(()) - } - - - // TODO: membership proposal for a DAO - - #[pallet::weight(5_000_000)] - pub fn membership_proposal( - origin: OriginFor, - context: T::Hash, - _member: T::Hash, - _action: u8, - _start: T::BlockNumber, - _expiry: T::BlockNumber - ) -> DispatchResult { - let sender = ensure_signed(origin)?; - // ensure active - // ensure member - // match action - // action - // deposit event - Self::deposit_event(Event::::Proposal{sender_id: sender, proposal_id: context}); - Ok(()) - } - - - // create a withdrawal proposal - // origin must be controller of the campaign == controller of the dao - // beneficiary must be the treasury of the dao - - #[pallet::weight(5_000_000)] - pub fn withdraw_proposal( - origin: OriginFor, - context_id: T::Hash, - title: Vec, - cid: Vec, - amount: Balance, - start: T::BlockNumber, - expiry: T::BlockNumber, - ) -> DispatchResult { - - let sender = ensure_signed(origin)?; - - // A C C E S S - - // ensure!( T::Flow::campaign_by_id(&context_id), Error::::CampaignUnknown ); - let state = T::Flow::campaign_state(&context_id); - ensure!( state == FlowState::Success, Error::::CampaignFailed ); - // todo: should this checks be performed? - // let owner = T::Flow::campaign_owner(&context_id); - // ensure!( sender == owner, Error::::AuthorizationError ); - - // B O U N D S - - // todo: should this checks be performed or not? - // let current_block = >::block_number(); - // ensure!(start > current_block, Error::::OutOfBounds ); - // ensure!(expiry > start, Error::::OutOfBounds ); - // ensure!(expiry <= current_block + Self::proposal_time_limit(), Error::::OutOfBounds ); - - // B A L A N C E - - let used_balance = >::get(&context_id); - let total_balance = T::Flow::campaign_balance(&context_id); - let remaining_balance = total_balance.checked_sub(used_balance).ok_or(Error::::BalanceInsufficient)? ; - ensure!(remaining_balance >= amount, Error::::BalanceInsufficient ); - - // T R A F F I C - - let proposals = >::get(expiry); - ensure!((proposals.len() as u32) < T::MaxProposalsPerBlock::get(), Error::::TooManyProposals); - ensure!(!>::contains_key(&context_id), Error::::ProposalExists); - - // C O U N T S - - let proposals_count = >::get(); - let updated_proposals_count = proposals_count.checked_add(1).ok_or(Error::::OverflowError)?; - let proposals_by_campaign_count = >::get(&context_id); - let updated_proposals_by_campaign_count = proposals_by_campaign_count.checked_add(1).ok_or(Error::::OverflowError)?; - let proposals_by_owner_count = >::get(&sender); - let updated_proposals_by_owner_count = proposals_by_owner_count.checked_add(1).ok_or(Error::::OverflowError)?; - - // C O N F I G - - let proposal_type = ProposalType::Withdrawal; // treasury - let voting_type = VotingType::Simple; // votes - let nonce = Self::get_and_increment_nonce(); - - let (proposal_id, _) = ::Randomness::random(&nonce); - - let proposal = Proposal { - proposal_id: proposal_id.clone(), - context_id: context_id.clone(), - proposal_type, - voting_type, - start, - expiry - }; - - let metadata = ProposalMetadata { - title, - cid, - amount, - }; - - // W R I T E - - Proposals::::insert(&proposal_id, proposal.clone()); - >::insert(&proposal_id, metadata.clone()); - >::insert(&proposal_id, sender.clone()); - >::insert(proposal_id.clone(), ProposalState::Active); - - >::mutate(expiry, |proposals| proposals.push(proposal_id.clone())); - >::insert(&proposals_count, proposal_id.clone()); - >::put(updated_proposals_count); - >::insert(proposal_id.clone(), proposals_count); - >::insert((context_id.clone(), proposals_by_campaign_count.clone()), proposal_id.clone()); - >::insert(context_id.clone(), updated_proposals_by_campaign_count); - >::insert((context_id.clone(), proposal_id.clone()), proposals_by_campaign_count); - >::insert((sender.clone(), proposals_by_owner_count.clone()), proposal_id.clone()); - >::insert(sender.clone(), updated_proposals_by_owner_count); - >::insert((sender.clone(), proposal_id.clone()), proposals_by_owner_count); - >::mutate( context_id.clone(), |proposals| proposals.push(proposal_id.clone()) ); - - // E V E N T - - Self::deposit_event( - Event::::ProposalCreated { - sender_id: sender, - context_id, - proposal_id, - amount, - expiry - } - ); - Ok(()) - - } - - // TODO: - // voting vs staking, e.g. - // 1. token weighted and democratic voting require yes/no - // 2. conviction voting requires ongoing staking - // 3. quadratic voting - - #[pallet::weight(5_000_000)] - pub fn simple_vote( - origin: OriginFor, - proposal_id: T::Hash, - vote: bool - ) -> DispatchResult { - - let sender = ensure_signed(origin)?; - - // Ensure the proposal exists - ensure!(>::contains_key(&proposal_id), Error::::ProposalUnknown); - - // Ensure the proposal has not ended - let proposal_state = >::get(&proposal_id); - ensure!(proposal_state == ProposalState::Active, Error::::ProposalEnded); - - // Ensure the contributor did not vote before - ensure!(!>::get((sender.clone(), proposal_id.clone())), Error::::AlreadyVoted); - - // Get the proposal - let proposal = >::get(&proposal_id); - // Ensure the proposal is not expired - ensure!(>::block_number() < proposal.expiry, Error::::ProposalExpired); - - // TODO: - // ensure origin is one of: - // a. member when the proposal is general - // b. contributor when the proposal is a withdrawal request - // let sender_balance = >::campaign_contribution(proposal.campaign_id, sender.clone()); - // ensure!( sender_balance > T::Balance::from(0), "You are not a contributor of this Campaign"); - - match &proposal.proposal_type { - // DAO Democratic Proposal - // simply one member one vote yes / no, - // TODO: ratio definable, now > 50% majority wins - ProposalType::General => { - - let (mut yes, mut no) = >::get(&proposal_id); - - match vote { - true => { - yes = yes.checked_add(1).ok_or(Error::::OverflowError)?; - let proposal_approvers = >::get(&proposal_id); - let updated_proposal_approvers = proposal_approvers.checked_add(1).ok_or(Error::::OverflowError)?; - >::insert( - proposal_id.clone(), - updated_proposal_approvers.clone() - ); - }, - false => { - no = no.checked_add(1).ok_or(Error::::OverflowError)?; - let proposal_deniers = >::get(&proposal_id); - let updated_proposal_deniers = proposal_deniers.checked_add(1).ok_or(Error::::OverflowError)?; - >::insert( - proposal_id.clone(), - updated_proposal_deniers.clone() - ); - } - } - - >::insert( - proposal_id.clone(), - (yes,no) - ); - - }, - // 50% majority over total number of campaign contributors - ProposalType::Withdrawal => { - - let (mut yes, mut no) = >::get(&proposal_id); - - match vote { - true => { - yes = yes.checked_add(1).ok_or(Error::::OverflowError)?; - - let current_approvers = >::get(&proposal_id); - let updated_approvers = current_approvers.checked_add(1).ok_or(Error::::OverflowError)?; - >::insert(proposal_id.clone(), updated_approvers.clone()); - - // TODO: make this variable - let contributors = T::Flow::campaign_contributors_count(&proposal.context_id); - let threshold = contributors.checked_div(2).ok_or(Error::::DivisionError)?; - if updated_approvers > threshold { - // todo: should this be called on finalize? - Self::unlock_balance(proposal_id, updated_approvers)?; - } - // remove - let proposal_approvers = >::get(&proposal_id); - let updated_proposal_approvers = proposal_approvers.checked_add(1).ok_or(Error::::OverflowError)?; - >::insert( - proposal_id.clone(), - updated_proposal_approvers.clone() - ); - - }, - false => { - no = no.checked_add(1).ok_or(Error::::OverflowError)?; - // remove - let proposal_deniers = >::get(&proposal_id); - let updated_proposal_deniers = proposal_deniers.checked_add(1).ok_or(Error::::OverflowError)?; - >::insert( - proposal_id.clone(), - updated_proposal_deniers.clone() - ); - } - } - - ProposalSimpleVotes::::insert( - proposal_id.clone(), - (yes,no) - ); - - - }, - - // Campaign Token Weighted Proposal - // total token balance yes vs no - // TODO: ratio definable, now > 50% majority wins - // ProposalType:: => { - // }, - - // Membership Voting - // simply one token one vote yes / no, - // TODO: ratio definable, now simple majority wins - ProposalType::Member => { - // approve - // deny - // kick - // ban - }, - // default - _ => { - }, - } - - VotedBefore::::insert( ( &sender, proposal_id.clone() ), true ); - ProposalsByVoterCount::::mutate( &sender, |v| *v +=1 ); - ProposalVotesByVoters::::mutate(&proposal_id, |votings| votings.push(( sender.clone(), vote.clone() )) ); - ProposalsByVoter::::mutate( &sender, |votings| votings.push((proposal_id.clone(), vote))); - - let mut voters = ProposalVoters::::get(&proposal_id); - match voters.binary_search(&sender) { - Ok(_) => {}, // should never happen - Err(index) => { - voters.insert(index, sender.clone()); - ProposalVoters::::insert( &proposal_id, voters ); - } - } - - // dispatch vote event - Self::deposit_event( - Event::::ProposalVoted { - sender_id: sender, - proposal_id:proposal_id.clone(), - vote - } - ); - Ok(()) - - } - - } - - #[pallet::hooks] - impl Hooks> for Pallet { - fn on_finalize(_n: T::BlockNumber) { - - // i'm still jenny from the block - let block_number = _n.clone(); - let proposal_hashes = >::get(block_number); - - for proposal_id in &proposal_hashes { - - let mut proposal_state = >::get(&proposal_id); - if proposal_state != ProposalState::Active { continue }; - - let proposal = >::get(&proposal_id); - - // TODO: - // a. result( accepted, rejected ) - // b. result( accepted, rejected, total_allowed ) - // c. result( required_majority, staked_accept, staked_reject, slash_amount ) - // d. threshold reached - // e. conviction - - match &proposal.proposal_type { - ProposalType::General => { - // simple vote - let (yes,no) = >::get(&proposal_id); - if yes > no { proposal_state = ProposalState::Accepted; } - if yes < no { proposal_state = ProposalState::Rejected; } - if yes == 0 && no == 0 { proposal_state = ProposalState::Expired; } - // todo: if same amount of yes/no votes? - }, - ProposalType::Withdrawal => { - // treasury - // 50% majority of eligible voters - let (yes,_no) = >::get(&proposal_id); - let context = proposal.context_id.clone(); - let contributors = T::Flow::campaign_contributors_count(&context); - // TODO: dynamic threshold - let threshold = contributors.checked_div(2).ok_or(Error::::DivisionError); - match threshold { - Ok(t) => { - if yes > t { - proposal_state = ProposalState::Accepted; - Self::unlock_balance(proposal.proposal_id, yes); - } else { - proposal_state = ProposalState::Rejected; - } - }, - Err(_err) => { - // todo: logic on error event - } - } - }, - ProposalType::Member => { - // membership - // - }, - _ => { - // no result - fail - proposal_state = ProposalState::Expired; - } - } - - >::insert(&proposal_id, proposal_state.clone()); - - match proposal_state { - ProposalState::Accepted => { - Self::deposit_event( - Event::::ProposalApproved {proposal_id: proposal_id.clone()} - ); - }, - ProposalState::Rejected => { - Self::deposit_event( - Event::::ProposalRejected {proposal_id: proposal_id.clone()} - ); - }, - ProposalState::Expired => { - Self::deposit_event( - Event::::ProposalExpired {proposal_id: proposal_id.clone()} - ); - }, - _ => {} - } - - } - - } - } - - impl Pallet { - - // TODO: DISCUSSION - // withdrawal proposals are accepted - // when the number of approvals is higher - // than the number of rejections - // accepted / denied >= 1 - fn unlock_balance( - proposal_id: T::Hash, - _supported_count: u64 - ) -> DispatchResult { - - // Get proposal and metadata - let proposal = >::get(proposal_id.clone()); - let metadata = >::get(proposal_id.clone()); - - // Ensure sufficient balance - let proposal_balance = metadata.amount; - let total_balance = T::Flow::campaign_balance(&proposal.context_id); - - // let used_balance = Self::balance_used(proposal.context_id); - let used_balance = >::get(proposal.context_id); - let available_balance = total_balance - used_balance.clone(); - ensure!(available_balance >= proposal_balance, Error::::BalanceInsufficient); - - // Get the owner of the campaign - let _owner = >::get(&proposal_id).ok_or("No owner for proposal")?; - - // get treasury account for related body and unlock balance - let body = T::Flow::campaign_org(&proposal.context_id); - let treasury_account = T::Control::body_treasury(&body); - T::Currency::unreserve( - T::FundingCurrencyId::get(), - &treasury_account, - proposal_balance.clone() - ); - - // Change the used amount - let new_used_balance = used_balance + proposal_balance; - >::insert(proposal.context_id, new_used_balance); - - // proposal completed - let proposal_state = ProposalState::Finalized; - >::insert(proposal_id.clone(), proposal_state); - - >::insert(proposal_id.clone(), proposal.clone()); - - Self::deposit_event( - Event::::WithdrawalGranted {proposal_id, context_id: proposal.context_id, body_id: body} - ); - Ok(()) - - } - - fn get_and_increment_nonce() -> Vec { - let nonce = Nonce::::get(); - Nonce::::put(nonce.wrapping_add(1)); - nonce.encode() - } - } + use frame_system::{ + ensure_signed, + pallet_prelude::{OriginFor, BlockNumberFor}, + WeightInfo + }; + use frame_support::{ + dispatch::DispatchResult, + traits::{Randomness}, + pallet_prelude::*, + transactional + }; + use sp_std::vec::Vec; + use orml_traits::{MultiCurrency, MultiReservableCurrency}; + + use zero_primitives::{Balance, CurrencyId}; + use support::{ + ControlPalletStorage, ControlState, ControlMemberState, + FlowPalletStorage, FlowState + }; + + use super::*; + use voting_enums::{ProposalState, ProposalType, VotingType}; + use voting_structs::{Proposal, ProposalMetadata}; + + + #[pallet::config] + pub trait Config: frame_system::Config { + type Event: From> + IsType<::Event> + Into<::Event>; + type Currency: MultiCurrency + + MultiReservableCurrency; + type Randomness: Randomness; + type Control: ControlPalletStorage; + type Flow: FlowPalletStorage; + type ForceOrigin: EnsureOrigin; + type WeightInfo: WeightInfo; + + #[pallet::constant] + type MaxProposalsPerBlock: Get; // 3 + + #[pallet::constant] + type MaxProposalDuration: Get; // 864000, 60 * 60 * 24 * 30 / 3 + + #[pallet::constant] + type FundingCurrencyId: Get; + } + + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + + /// Global status + #[pallet::storage] + pub(super) type Proposals = StorageMap<_, Blake2_128Concat, T::Hash, Proposal, ValueQuery>; + + #[pallet::storage] + pub(super) type Metadata = StorageMap<_, Blake2_128Concat, T::Hash, ProposalMetadata, ValueQuery>; + + #[pallet::storage] + pub(super) type Owners = StorageMap<_, Blake2_128Concat, T::Hash, T::AccountId, OptionQuery>; + + #[pallet::storage] + pub(super) type ProposalStates = StorageMap<_, Blake2_128Concat, T::Hash, ProposalState, ValueQuery, GetDefault>; + + /// Maximum time limit for a proposal + #[pallet::type_value] + pub(super) fn ProposalTimeLimitDefault() -> T::BlockNumber { T::BlockNumber::from(T::MaxProposalDuration::get()) } + #[pallet::storage] + pub(super) type ProposalTimeLimit = StorageValue <_, T::BlockNumber, ValueQuery, ProposalTimeLimitDefault>; + + /// All proposals + #[pallet::storage] + pub(super) type ProposalsArray = StorageMap<_, Blake2_128Concat, u64, T::Hash, ValueQuery>; + + #[pallet::storage] + pub(super) type ProposalsCount = StorageValue<_, u64, ValueQuery>; + + #[pallet::storage] + pub(super) type ProposalsIndex = StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery>; + + /// Proposals by campaign / org + #[pallet::storage] + pub(super) type ProposalsByContextArray = StorageMap<_, Blake2_128Concat, (T::Hash, u64), T::Hash, ValueQuery>; + + #[pallet::storage] + pub(super) type ProposalsByContextCount = StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery>; + + #[pallet::storage] + pub(super) type ProposalsByContextIndex = StorageMap<_, Blake2_128Concat, (T::Hash, T::Hash), u64, ValueQuery>; + + /// all proposals for a given context + #[pallet::storage] + pub(super) type ProposalsByContext = StorageMap<_, Blake2_128Concat, T::Hash, Vec, ValueQuery>; + + /// Proposals by owner + #[pallet::storage] + pub(super) type ProposalsByOwnerArray = StorageMap<_, Blake2_128Concat, (T::AccountId, u64), T::Hash, ValueQuery>; + + #[pallet::storage] + pub(super) type ProposalsByOwnerCount = StorageMap<_, Blake2_128Concat, T::AccountId, u64, ValueQuery>; + + #[pallet::storage] + pub(super) type ProposalsByOwnerIndex = StorageMap<_, Blake2_128Concat, (T::AccountId, T::Hash), u64, ValueQuery>; + + /// Proposals where voter participated + #[pallet::storage] + pub(super) type ProposalsByVoter = StorageMap<_, Blake2_128Concat, T::AccountId, Vec<(T::Hash, bool)>, ValueQuery>; + + /// Proposal voters and votes by proposal + #[pallet::storage] + pub(super) type ProposalVotesByVoters = StorageMap<_, Blake2_128Concat, T::Hash, Vec<(T::AccountId, bool)>, ValueQuery>; + + /// Total proposals voted on by voter + #[pallet::storage] + pub(super) type ProposalsByVoterCount = StorageMap<_, Blake2_128Concat, T::AccountId, u64, ValueQuery>; + + /// Proposals ending in a block + #[pallet::storage] + pub(super) type ProposalsByBlock = StorageMap<_, Blake2_128Concat, T::BlockNumber, Vec, ValueQuery>; + + /// The amount of currency that a project has used + #[pallet::storage] + pub(super) type CampaignBalanceUsed = StorageMap<_, Blake2_128Concat, T::Hash, Balance, ValueQuery>; + + /// The number of people who approve a proposal + #[pallet::storage] + pub(super) type ProposalApprovers = StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery, GetDefault>; + + /// The number of people who deny a proposal + #[pallet::storage] + pub(super) type ProposalDeniers = StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery, GetDefault>; + + /// Voters per proposal + #[pallet::storage] + pub(super) type ProposalVoters = StorageMap<_, Blake2_128Concat, T::Hash, Vec, ValueQuery>; + + /// Voter count per proposal + #[pallet::storage] + pub(super) type ProposalVotes = StorageMap<_, Blake2_128Concat, T::Hash, u64, ValueQuery, GetDefault>; + + /// Ack vs Nack + #[pallet::storage] + pub(super) type ProposalSimpleVotes = StorageMap<_, Blake2_128Concat, T::Hash, (u64, u64), ValueQuery, GetDefault>; + + /// User has voted on a proposal + #[pallet::storage] + pub(super) type VotedBefore = StorageMap<_, Blake2_128Concat, (T::AccountId, T::Hash), bool, ValueQuery, GetDefault>; + + // TODO: ProposalTotalEligibleVoters + // TODO: ProposalApproversWeight + // TODO: ProposalDeniersWeight + // TODO: ProposalTotalEligibleWeight + + /// The total number of proposals + #[pallet::storage] + pub(super) type Nonce = StorageValue<_, u128, ValueQuery>; + + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + Proposal { + sender_id: T::AccountId, + proposal_id: T::Hash + }, + ProposalCreated { + sender_id: T::AccountId, + context_id: T::Hash, + proposal_id: T::Hash, + amount: Balance, + expiry: T::BlockNumber, + }, + ProposalVoted { + sender_id: T::AccountId, + proposal_id: T::Hash, + vote: bool + }, + // ProposalFinalized(T::Hash, u8), + ProposalApproved { + proposal_id: T::Hash + }, + ProposalRejected { + proposal_id: T::Hash + }, + ProposalExpired { + proposal_id: T::Hash + }, + // ProposalAborted(T::Hash), + // ProposalError(T::Hash, Vec), + WithdrawalGranted { + proposal_id: T::Hash, + context_id: T::Hash, + body_id: T::Hash + }, + } + + #[pallet::error] + pub enum Error { + /// Proposal Ended + ProposalEnded, + /// Proposal Exists + ProposalExists, + /// Proposal Expired + ProposalExpired, + /// Already Voted + AlreadyVoted, + /// Proposal Unknown + ProposalUnknown, + /// DAO Inactive + DAOInactive, + /// Authorization Error + AuthorizationError, + /// Tangram Creation Failed + TangramCreationError, + /// Out Of Bounds Error + OutOfBounds, + /// Unknown Error + UnknownError, + ///MemberExists + MemberExists, + /// Unknown Campaign + CampaignUnknown, + /// Campaign Failed + CampaignFailed, + /// Balance Too Low + BalanceInsufficient, + /// Hash Collision + HashCollision, + /// Unknown Account + UnknownAccount, + /// Too Many Proposals for block + TooManyProposals, + /// Overflow Error + OverflowError, + /// Division Error + DivisionError + } + + #[pallet::call] + impl Pallet { + + + // TODO: general proposal for a DAO + #[pallet::weight(5_000_000)] + #[transactional] + pub fn general_proposal( + origin: OriginFor, + context_id: T::Hash, + title: Vec, + cid: Vec, + start: T::BlockNumber, + expiry: T::BlockNumber + ) -> DispatchResult { + + let sender = ensure_signed(origin)?; + + // active/existing dao? + ensure!(T::Control::body_state(&context_id) == ControlState::Active, Error::::DAOInactive); + + // member of body? + let member = T::Control::body_member_state(&context_id, &sender); + ensure!(member == ControlMemberState::Active, Error::::AuthorizationError); + + // ensure that start and expiry are in bounds + let current_block = >::block_number(); + // ensure!(start > current_block, Error::::OutOfBounds ); + ensure!(expiry > current_block, Error::::OutOfBounds ); + ensure!(expiry <= current_block + >::get(), Error::::OutOfBounds ); + + // ensure that number of proposals + // ending in target block + // do not exceed the maximum + let proposals = >::get(expiry); + // ensure!(proposals.len() as u32 < T::MaxProposalsPerBlock::get(), "Maximum number of proposals is reached for the target block, try another block"); + ensure!((proposals.len() as u32) < T::MaxProposalsPerBlock::get(), Error::::TooManyProposals); // todo: was error generated manually on purpose? + + let proposal_type = ProposalType::General; + let proposal_state = ProposalState::Active; + let voting_type = VotingType::Simple; + // ensure!(!>::contains_key(&context_id), "Proposal id already exists"); + ensure!(!>::contains_key(&context_id), Error::::ProposalExists); // todo: was error generated manually on purpose? + + + // check add + let proposals_count = >::get(); + let updated_proposals_count = proposals_count.checked_add(1).ok_or( Error::::OverflowError)?; + let proposals_by_campaign_count = >::get(&context_id); + let updated_proposals_by_campaign_count = proposals_by_campaign_count.checked_add(1).ok_or( Error::::OverflowError )?; + let proposals_by_owner_count = >::get(&sender); + let updated_proposals_by_owner_count = proposals_by_owner_count.checked_add(1).ok_or( Error::::OverflowError )?; + + // proposal + + let nonce = Self::get_and_increment_nonce(); + let (proposal_id, _) = ::random(&nonce); + let new_proposal = Proposal { + proposal_id: proposal_id.clone(), + context_id: context_id.clone(), + proposal_type, + voting_type, + start, + expiry, + }; + + // metadata + + let metadata = ProposalMetadata { + title: title, + cid: cid, + amount: 0 + }; + + // + // + // + + // insert proposals + >::insert(proposal_id.clone(), new_proposal.clone()); + >::insert(proposal_id.clone(), metadata.clone()); + >::insert(proposal_id.clone(), sender.clone()); + >::insert(proposal_id.clone(), proposal_state); + // update max per block + >::mutate(expiry, |proposals| proposals.push(proposal_id.clone())); + // update proposal map + >::insert(&proposals_count, proposal_id.clone()); + >::put(updated_proposals_count); + >::insert(proposal_id.clone(), proposals_count); + // update campaign map + >::insert((context_id.clone(), proposals_by_campaign_count.clone()), proposal_id.clone()); + >::insert(context_id.clone(), updated_proposals_by_campaign_count); + >::insert((context_id.clone(), proposal_id.clone()), proposals_by_campaign_count); + >::mutate( context_id.clone(), |proposals| proposals.push(proposal_id.clone()) ); + // update owner map + >::insert((sender.clone(), proposals_by_owner_count.clone()), proposal_id.clone()); + >::insert(sender.clone(), updated_proposals_by_owner_count); + >::insert((sender.clone(), proposal_id.clone()), proposals_by_owner_count); + // init votes + >::insert(context_id, (0,0)); + + // deposit event + Self::deposit_event( + Event::::Proposal{sender_id: sender, proposal_id} + ); + Ok(()) + } + + + // TODO: membership proposal for a DAO + + #[pallet::weight(5_000_000)] + pub fn membership_proposal( + origin: OriginFor, + context: T::Hash, + _member: T::Hash, + _action: u8, + _start: T::BlockNumber, + _expiry: T::BlockNumber + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + // ensure active + // ensure member + // match action + // action + // deposit event + Self::deposit_event(Event::::Proposal{sender_id: sender, proposal_id: context}); + Ok(()) + } + + + // create a withdrawal proposal + // origin must be controller of the campaign == controller of the dao + // beneficiary must be the treasury of the dao + + #[pallet::weight(5_000_000)] + pub fn withdraw_proposal( + origin: OriginFor, + context_id: T::Hash, + title: Vec, + cid: Vec, + amount: Balance, + start: T::BlockNumber, + expiry: T::BlockNumber, + ) -> DispatchResult { + + let sender = ensure_signed(origin)?; + + // A C C E S S + + // ensure!( T::Flow::campaign_by_id(&context_id), Error::::CampaignUnknown ); + let state = T::Flow::campaign_state(&context_id); + ensure!( state == FlowState::Success, Error::::CampaignFailed ); + // todo: should this checks be performed? + // let owner = T::Flow::campaign_owner(&context_id); + // ensure!( sender == owner, Error::::AuthorizationError ); + + // B O U N D S + + // todo: should this checks be performed or not? + // let current_block = >::block_number(); + // ensure!(start > current_block, Error::::OutOfBounds ); + // ensure!(expiry > start, Error::::OutOfBounds ); + // ensure!(expiry <= current_block + Self::proposal_time_limit(), Error::::OutOfBounds ); + + // B A L A N C E + + let used_balance = >::get(&context_id); + let total_balance = T::Flow::campaign_balance(&context_id); + let remaining_balance = total_balance.checked_sub(used_balance).ok_or(Error::::BalanceInsufficient)? ; + ensure!(remaining_balance >= amount, Error::::BalanceInsufficient ); + + // T R A F F I C + + let proposals = >::get(expiry); + ensure!((proposals.len() as u32) < T::MaxProposalsPerBlock::get(), Error::::TooManyProposals); + ensure!(!>::contains_key(&context_id), Error::::ProposalExists); + + // C O U N T S + + let proposals_count = >::get(); + let updated_proposals_count = proposals_count.checked_add(1).ok_or(Error::::OverflowError)?; + let proposals_by_campaign_count = >::get(&context_id); + let updated_proposals_by_campaign_count = proposals_by_campaign_count.checked_add(1).ok_or(Error::::OverflowError)?; + let proposals_by_owner_count = >::get(&sender); + let updated_proposals_by_owner_count = proposals_by_owner_count.checked_add(1).ok_or(Error::::OverflowError)?; + + // C O N F I G + + let proposal_type = ProposalType::Withdrawal; // treasury + let voting_type = VotingType::Simple; // votes + let nonce = Self::get_and_increment_nonce(); + + let (proposal_id, _) = ::Randomness::random(&nonce); + + let proposal = Proposal { + proposal_id: proposal_id.clone(), + context_id: context_id.clone(), + proposal_type, + voting_type, + start, + expiry + }; + + let metadata = ProposalMetadata { + title, + cid, + amount, + }; + + // W R I T E + + Proposals::::insert(&proposal_id, proposal.clone()); + >::insert(&proposal_id, metadata.clone()); + >::insert(&proposal_id, sender.clone()); + >::insert(proposal_id.clone(), ProposalState::Active); + + >::mutate(expiry, |proposals| proposals.push(proposal_id.clone())); + >::insert(&proposals_count, proposal_id.clone()); + >::put(updated_proposals_count); + >::insert(proposal_id.clone(), proposals_count); + >::insert((context_id.clone(), proposals_by_campaign_count.clone()), proposal_id.clone()); + >::insert(context_id.clone(), updated_proposals_by_campaign_count); + >::insert((context_id.clone(), proposal_id.clone()), proposals_by_campaign_count); + >::insert((sender.clone(), proposals_by_owner_count.clone()), proposal_id.clone()); + >::insert(sender.clone(), updated_proposals_by_owner_count); + >::insert((sender.clone(), proposal_id.clone()), proposals_by_owner_count); + >::mutate( context_id.clone(), |proposals| proposals.push(proposal_id.clone()) ); + + // E V E N T + + Self::deposit_event( + Event::::ProposalCreated { + sender_id: sender, + context_id, + proposal_id, + amount, + expiry + } + ); + Ok(()) + + } + + // TODO: + // voting vs staking, e.g. + // 1. token weighted and democratic voting require yes/no + // 2. conviction voting requires ongoing staking + // 3. quadratic voting + + #[pallet::weight(5_000_000)] + pub fn simple_vote( + origin: OriginFor, + proposal_id: T::Hash, + vote: bool + ) -> DispatchResult { + + let sender = ensure_signed(origin)?; + + // Ensure the proposal exists + ensure!(>::contains_key(&proposal_id), Error::::ProposalUnknown); + + // Ensure the proposal has not ended + let proposal_state = >::get(&proposal_id); + ensure!(proposal_state == ProposalState::Active, Error::::ProposalEnded); + + // Ensure the contributor did not vote before + ensure!(!>::get((sender.clone(), proposal_id.clone())), Error::::AlreadyVoted); + + // Get the proposal + let proposal = >::get(&proposal_id); + // Ensure the proposal is not expired + ensure!(>::block_number() < proposal.expiry, Error::::ProposalExpired); + + // TODO: + // ensure origin is one of: + // a. member when the proposal is general + // b. contributor when the proposal is a withdrawal request + // let sender_balance = >::campaign_contribution(proposal.campaign_id, sender.clone()); + // ensure!( sender_balance > T::Balance::from(0), "You are not a contributor of this Campaign"); + + match &proposal.proposal_type { + // DAO Democratic Proposal + // simply one member one vote yes / no, + // TODO: ratio definable, now > 50% majority wins + ProposalType::General => { + + let (mut yes, mut no) = >::get(&proposal_id); + + match vote { + true => { + yes = yes.checked_add(1).ok_or(Error::::OverflowError)?; + let proposal_approvers = >::get(&proposal_id); + let updated_proposal_approvers = proposal_approvers.checked_add(1).ok_or(Error::::OverflowError)?; + >::insert( + proposal_id.clone(), + updated_proposal_approvers.clone() + ); + }, + false => { + no = no.checked_add(1).ok_or(Error::::OverflowError)?; + let proposal_deniers = >::get(&proposal_id); + let updated_proposal_deniers = proposal_deniers.checked_add(1).ok_or(Error::::OverflowError)?; + >::insert( + proposal_id.clone(), + updated_proposal_deniers.clone() + ); + } + } + + >::insert( + proposal_id.clone(), + (yes,no) + ); + + }, + // 50% majority over total number of campaign contributors + ProposalType::Withdrawal => { + + let (mut yes, mut no) = >::get(&proposal_id); + + match vote { + true => { + yes = yes.checked_add(1).ok_or(Error::::OverflowError)?; + + let current_approvers = >::get(&proposal_id); + let updated_approvers = current_approvers.checked_add(1).ok_or(Error::::OverflowError)?; + >::insert(proposal_id.clone(), updated_approvers.clone()); + + // TODO: make this variable + let contributors = T::Flow::campaign_contributors_count(&proposal.context_id); + let threshold = contributors.checked_div(2).ok_or(Error::::DivisionError)?; + if updated_approvers > threshold { + // todo: should this be called on finalize? + Self::unlock_balance(proposal_id, updated_approvers)?; + } + // remove + let proposal_approvers = >::get(&proposal_id); + let updated_proposal_approvers = proposal_approvers.checked_add(1).ok_or(Error::::OverflowError)?; + >::insert( + proposal_id.clone(), + updated_proposal_approvers.clone() + ); + + }, + false => { + no = no.checked_add(1).ok_or(Error::::OverflowError)?; + // remove + let proposal_deniers = >::get(&proposal_id); + let updated_proposal_deniers = proposal_deniers.checked_add(1).ok_or(Error::::OverflowError)?; + >::insert( + proposal_id.clone(), + updated_proposal_deniers.clone() + ); + } + } + + ProposalSimpleVotes::::insert( + proposal_id.clone(), + (yes,no) + ); + + + }, + + // Campaign Token Weighted Proposal + // total token balance yes vs no + // TODO: ratio definable, now > 50% majority wins + // ProposalType:: => { + // }, + + // Membership Voting + // simply one token one vote yes / no, + // TODO: ratio definable, now simple majority wins + ProposalType::Member => { + // approve + // deny + // kick + // ban + }, + // default + _ => { + }, + } + + VotedBefore::::insert( ( &sender, proposal_id.clone() ), true ); + ProposalsByVoterCount::::mutate( &sender, |v| *v +=1 ); + ProposalVotesByVoters::::mutate(&proposal_id, |votings| votings.push(( sender.clone(), vote.clone() )) ); + ProposalsByVoter::::mutate( &sender, |votings| votings.push((proposal_id.clone(), vote))); + + let mut voters = ProposalVoters::::get(&proposal_id); + match voters.binary_search(&sender) { + Ok(_) => {}, // should never happen + Err(index) => { + voters.insert(index, sender.clone()); + ProposalVoters::::insert( &proposal_id, voters ); + } + } + + // dispatch vote event + Self::deposit_event( + Event::::ProposalVoted { + sender_id: sender, + proposal_id:proposal_id.clone(), + vote + } + ); + Ok(()) + + } + + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_finalize(_n: T::BlockNumber) { + + // i'm still jenny from the block + let block_number = _n.clone(); + let proposal_hashes = >::get(block_number); + + for proposal_id in &proposal_hashes { + + let mut proposal_state = >::get(&proposal_id); + if proposal_state != ProposalState::Active { continue }; + + let proposal = >::get(&proposal_id); + + // TODO: + // a. result( accepted, rejected ) + // b. result( accepted, rejected, total_allowed ) + // c. result( required_majority, staked_accept, staked_reject, slash_amount ) + // d. threshold reached + // e. conviction + + match &proposal.proposal_type { + ProposalType::General => { + // simple vote + let (yes,no) = >::get(&proposal_id); + if yes > no { proposal_state = ProposalState::Accepted; } + if yes < no { proposal_state = ProposalState::Rejected; } + if yes == 0 && no == 0 { proposal_state = ProposalState::Expired; } + // todo: if same amount of yes/no votes? + }, + ProposalType::Withdrawal => { + // treasury + // 50% majority of eligible voters + let (yes,_no) = >::get(&proposal_id); + let context = proposal.context_id.clone(); + let contributors = T::Flow::campaign_contributors_count(&context); + // TODO: dynamic threshold + let threshold = contributors.checked_div(2).ok_or(Error::::DivisionError); + match threshold { + Ok(t) => { + if yes > t { + proposal_state = ProposalState::Accepted; + Self::unlock_balance(proposal.proposal_id, yes); + } else { + proposal_state = ProposalState::Rejected; + } + }, + Err(_err) => { + // todo: logic on error event + } + } + }, + ProposalType::Member => { + // membership + // + }, + _ => { + // no result - fail + proposal_state = ProposalState::Expired; + } + } + + >::insert(&proposal_id, proposal_state.clone()); + + match proposal_state { + ProposalState::Accepted => { + Self::deposit_event( + Event::::ProposalApproved {proposal_id: proposal_id.clone()} + ); + }, + ProposalState::Rejected => { + Self::deposit_event( + Event::::ProposalRejected {proposal_id: proposal_id.clone()} + ); + }, + ProposalState::Expired => { + Self::deposit_event( + Event::::ProposalExpired {proposal_id: proposal_id.clone()} + ); + }, + _ => {} + } + + } + + } + } + + impl Pallet { + + // TODO: DISCUSSION + // withdrawal proposals are accepted + // when the number of approvals is higher + // than the number of rejections + // accepted / denied >= 1 + fn unlock_balance( + proposal_id: T::Hash, + _supported_count: u64 + ) -> DispatchResult { + + // Get proposal and metadata + let proposal = >::get(proposal_id.clone()); + let metadata = >::get(proposal_id.clone()); + + // Ensure sufficient balance + let proposal_balance = metadata.amount; + let total_balance = T::Flow::campaign_balance(&proposal.context_id); + + // let used_balance = Self::balance_used(proposal.context_id); + let used_balance = >::get(proposal.context_id); + let available_balance = total_balance - used_balance.clone(); + ensure!(available_balance >= proposal_balance, Error::::BalanceInsufficient); + + // Get the owner of the campaign + let _owner = >::get(&proposal_id).ok_or("No owner for proposal")?; + + // get treasury account for related body and unlock balance + let body = T::Flow::campaign_org(&proposal.context_id); + let treasury_account = T::Control::body_treasury(&body); + T::Currency::unreserve( + T::FundingCurrencyId::get(), + &treasury_account, + proposal_balance.clone() + ); + + // Change the used amount + let new_used_balance = used_balance + proposal_balance; + >::insert(proposal.context_id, new_used_balance); + + // proposal completed + let proposal_state = ProposalState::Finalized; + >::insert(proposal_id.clone(), proposal_state); + + >::insert(proposal_id.clone(), proposal.clone()); + + Self::deposit_event( + Event::::WithdrawalGranted {proposal_id, context_id: proposal.context_id, body_id: body} + ); + Ok(()) + + } + + fn get_and_increment_nonce() -> Vec { + let nonce = Nonce::::get(); + Nonce::::put(nonce.wrapping_add(1)); + nonce.encode() + } + } } // todo: Check storage fields and remove generices from those, who don't use Config trait diff --git a/signal/src/mock.rs b/signal/src/mock.rs index 8a66a3be0..a3d599486 100644 --- a/signal/src/mock.rs +++ b/signal/src/mock.rs @@ -8,8 +8,8 @@ use frame_support_test::TestRandomness; use sp_std::cell::RefCell; use sp_core::H256; use sp_runtime::{ - testing::Header, - traits::{BlakeTwo256, IdentityLookup}, + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, }; use orml_traits::parameter_type_with_key; use support::{ @@ -66,9 +66,9 @@ impl ControlPalletStorage for ControlMock { pub struct FlowMock; impl FlowPalletStorage for FlowMock { fn campaign_balance(_hash: &Hash) -> Balance { flow_fixture.with(|v| v.borrow().campaign_balance.clone()) } - fn campaign_state(_hash: &Hash) -> FlowState { flow_fixture.with(|v| v.borrow().campaign_state.clone()) } - fn campaign_contributors_count(_hash: &Hash) -> u64 { flow_fixture.with(|v| v.borrow().campaign_contributors_count.clone()) } - fn campaign_org(_hash: &Hash) -> Hash { flow_fixture.with(|v| v.borrow().campaign_org.clone()) } + fn campaign_state(_hash: &Hash) -> FlowState { flow_fixture.with(|v| v.borrow().campaign_state.clone()) } + fn campaign_contributors_count(_hash: &Hash) -> u64 { flow_fixture.with(|v| v.borrow().campaign_contributors_count.clone()) } + fn campaign_org(_hash: &Hash) -> Hash { flow_fixture.with(|v| v.borrow().campaign_org.clone()) } } type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; @@ -76,17 +76,17 @@ type Block = frame_system::mocking::MockBlock; // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( - pub enum Test where - Block = Block, - NodeBlock = Block, - UncheckedExtrinsic = UncheckedExtrinsic, - { - System: frame_system::{Pallet, Call, Config, Storage, Event}, - Signal: pallet_signal, - Currencies: orml_currencies::{Pallet, Call, Event}, - Tokens: orml_tokens::{Pallet, Storage, Event, Config}, + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Signal: pallet_signal, + Currencies: orml_currencies::{Pallet, Call, Event}, + Tokens: orml_tokens::{Pallet, Storage, Event, Config}, PalletBalances: pallet_balances::{Pallet, Call, Storage, Event}, - } + } ); parameter_type_with_key! { @@ -130,53 +130,53 @@ impl orml_currencies::Config for Test { parameter_types! { - pub const BlockHashCount: u64 = 250; - pub const SS58Prefix: u8 = 42; - pub BlockWeights: frame_system::limits::BlockWeights = - frame_system::limits::BlockWeights::simple_max(1024); + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(1024); - pub const ExistentialDeposit: Balance = 1; + pub const ExistentialDeposit: Balance = 1; - pub const MaxProposalsPerBlock: u32 = 2; - pub const MaxProposalDuration: u32 = 20; - pub const FundingCurrencyId: CurrencyId = TokenSymbol::GAME as u32; + pub const MaxProposalsPerBlock: u32 = 2; + pub const MaxProposalDuration: u32 = 20; + pub const FundingCurrencyId: CurrencyId = TokenSymbol::GAME as u32; } // impl pallet_randomness_collective_flip::Config for Test {} impl frame_system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type Origin = Origin; - type Call = Call; - type Index = u64; - type BlockNumber = u64; - type Hash = Hash; - type Hashing = BlakeTwo256; - type AccountId = AccountId; - type Lookup = IdentityLookup; - type Header = Header; - type Event = Event; - type BlockHashCount = BlockHashCount; - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = pallet_balances::AccountData; - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = SS58Prefix; - type OnSetCode = (); + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = Hash; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); } impl pallet_signal::Config for Test { - type Event = Event; - type ForceOrigin = frame_system::EnsureRoot; - type WeightInfo = (); - type Control = ControlMock; - type Flow = FlowMock; - type MaxProposalsPerBlock = MaxProposalsPerBlock; + type Event = Event; + type ForceOrigin = frame_system::EnsureRoot; + type WeightInfo = (); + type Control = ControlMock; + type Flow = FlowMock; + type MaxProposalsPerBlock = MaxProposalsPerBlock; type MaxProposalDuration = MaxProposalDuration; type FundingCurrencyId = FundingCurrencyId; type Randomness = TestRandomness; @@ -186,10 +186,10 @@ impl pallet_signal::Config for Test { #[derive(Default)] pub struct ExtBuilder; impl ExtBuilder { - pub fn build(self) -> sp_io::TestExternalities { - let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); - let currency_id = TokenSymbol::GAME as u32; - orml_tokens::GenesisConfig:: { + pub fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + let currency_id = TokenSymbol::GAME as u32; + orml_tokens::GenesisConfig:: { balances: vec![ (ACC1, currency_id, 100), (ACC2, currency_id, 100), @@ -199,5 +199,5 @@ impl ExtBuilder { let mut ext = sp_io::TestExternalities::new(t); ext.execute_with(|| System::set_block_number(1)); ext - } + } }