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