diff --git a/.travis.yml b/.travis.yml index 8667c5f4f2..6a87108095 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: rust rust: - - 1.41.1 + - 1.42.0 # Caching saves a lot of time but often causes stalled builds... # disabled for now # look into solution here: https://levans.fr/rust_travis_cache.html diff --git a/Cargo.toml b/Cargo.toml index 280ee5a743..a5bfcd4d5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,9 @@ [workspace] members = [ "runtime", + "runtime-modules/proposals/engine", + "runtime-modules/proposals/codex", + "runtime-modules/proposals/discussion", "runtime-modules/common", "runtime-modules/content-working-group", "runtime-modules/forum", diff --git a/modules/proposals/Cargo.toml b/modules/proposals/Cargo.toml deleted file mode 100644 index a9ca69eb1b..0000000000 --- a/modules/proposals/Cargo.toml +++ /dev/null @@ -1,6 +0,0 @@ -[workspace] -members = [ - "engine", - "codex", - "discussion", -] \ No newline at end of file diff --git a/modules/proposals/codex/src/proposal_types/mod.rs b/modules/proposals/codex/src/proposal_types/mod.rs deleted file mode 100644 index b089fcb9b7..0000000000 --- a/modules/proposals/codex/src/proposal_types/mod.rs +++ /dev/null @@ -1,53 +0,0 @@ -use codec::Decode; -use num_enum::{IntoPrimitive, TryFromPrimitive}; -use rstd::convert::TryFrom; -use rstd::prelude::*; - -use crate::{ProposalCodeDecoder, ProposalExecutable}; - -pub mod parameters; -mod runtime_upgrade; -mod text_proposal; - -pub use runtime_upgrade::RuntimeUpgradeProposalExecutable; -pub use text_proposal::TextProposalExecutable; - -/// Defines allowed proposals types. Integer value serves as proposal_type_id. -#[derive(Debug, Eq, PartialEq, TryFromPrimitive, IntoPrimitive)] -#[repr(u32)] -pub enum ProposalType { - /// Text(signal) proposal type - Text = 1, - - /// Runtime upgrade proposal type - RuntimeUpgrade = 2, -} - -impl ProposalType { - fn compose_executable( - &self, - proposal_data: Vec, - ) -> Result, &'static str> { - match self { - ProposalType::Text => TextProposalExecutable::decode(&mut &proposal_data[..]) - .map_err(|err| err.what()) - .map(|obj| Box::new(obj) as Box), - ProposalType::RuntimeUpgrade => { - >::decode(&mut &proposal_data[..]) - .map_err(|err| err.what()) - .map(|obj| Box::new(obj) as Box) - } - } - } -} - -impl ProposalCodeDecoder for ProposalType { - fn decode_proposal( - proposal_type: u32, - proposal_code: Vec, - ) -> Result, &'static str> { - Self::try_from(proposal_type) - .map_err(|_| "Unsupported proposal type")? - .compose_executable::(proposal_code) - } -} diff --git a/modules/proposals/codex/src/proposal_types/parameters.rs b/modules/proposals/codex/src/proposal_types/parameters.rs deleted file mode 100644 index 5f9db050a3..0000000000 --- a/modules/proposals/codex/src/proposal_types/parameters.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::{BalanceOf, ProposalParameters}; - -// Proposal parameters for the upgrade runtime proposal -pub(crate) fn upgrade_runtime() -> ProposalParameters> -{ - ProposalParameters { - voting_period: T::BlockNumber::from(50000u32), - grace_period: T::BlockNumber::from(10000u32), - approval_quorum_percentage: 80, - approval_threshold_percentage: 80, - slashing_quorum_percentage: 80, - slashing_threshold_percentage: 80, - required_stake: Some(>::from(50000u32)), - } -} -// Proposal parameters for the text proposal -pub(crate) fn text_proposal() -> ProposalParameters> { - ProposalParameters { - voting_period: T::BlockNumber::from(50000u32), - grace_period: T::BlockNumber::from(10000u32), - approval_quorum_percentage: 40, - approval_threshold_percentage: 51, - slashing_quorum_percentage: 80, - slashing_threshold_percentage: 80, - required_stake: Some(>::from(500u32)), - } -} diff --git a/modules/proposals/codex/src/proposal_types/runtime_upgrade.rs b/modules/proposals/codex/src/proposal_types/runtime_upgrade.rs deleted file mode 100644 index 107b558d56..0000000000 --- a/modules/proposals/codex/src/proposal_types/runtime_upgrade.rs +++ /dev/null @@ -1,39 +0,0 @@ -use codec::{Decode, Encode}; -use rstd::marker::PhantomData; -use rstd::prelude::*; - -use runtime_primitives::traits::ModuleDispatchError; -use srml_support::dispatch; - -use crate::{ProposalExecutable, ProposalType}; - -/// Text (signal) proposal executable code wrapper. Prints its content on execution. -#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug, Default)] -pub struct RuntimeUpgradeProposalExecutable { - /// Proposal title - pub title: Vec, - - /// Proposal description - pub description: Vec, - - /// Text proposal main text - pub wasm: Vec, - - /// Marker for the system::Trait. Required to execute runtime upgrade proposal on exact runtime. - pub marker: PhantomData, -} - -impl RuntimeUpgradeProposalExecutable { - /// Converts runtime proposal type to proposal_type_id - pub fn proposal_type(&self) -> u32 { - ProposalType::RuntimeUpgrade.into() - } -} - -impl ProposalExecutable for RuntimeUpgradeProposalExecutable { - fn execute(&self) -> dispatch::Result { - // Update wasm code of node's runtime: - >::set_code(system::RawOrigin::Root.into(), self.wasm.clone()) - .map_err(|err| err.as_str()) - } -} diff --git a/modules/proposals/codex/src/proposal_types/text_proposal.rs b/modules/proposals/codex/src/proposal_types/text_proposal.rs deleted file mode 100644 index c663ce2a59..0000000000 --- a/modules/proposals/codex/src/proposal_types/text_proposal.rs +++ /dev/null @@ -1,38 +0,0 @@ -use codec::{Decode, Encode}; -use rstd::prelude::*; - -use rstd::str::from_utf8; -use srml_support::{dispatch, print}; - -use crate::{ProposalExecutable, ProposalType}; - -/// Text (signal) proposal executable code wrapper. Prints its content on execution. -#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug, Default)] -pub struct TextProposalExecutable { - /// Text proposal title - pub title: Vec, - - /// Text proposal description - pub description: Vec, - - /// Text proposal main text - pub text: Vec, -} - -impl TextProposalExecutable { - /// Converts text proposal type to proposal_type_id - pub fn proposal_type(&self) -> u32 { - ProposalType::Text.into() - } -} - -impl ProposalExecutable for TextProposalExecutable { - fn execute(&self) -> dispatch::Result { - print("Proposal: "); - print(from_utf8(self.title.as_slice()).unwrap()); - print("Description:"); - print(from_utf8(self.description.as_slice()).unwrap()); - - Ok(()) - } -} diff --git a/modules/proposals/discussion/src/lib.rs b/modules/proposals/discussion/src/lib.rs deleted file mode 100644 index e043c77c2b..0000000000 --- a/modules/proposals/discussion/src/lib.rs +++ /dev/null @@ -1,195 +0,0 @@ -//! Proposals discussion module for the Joystream platform. Version 2. -//! Contains discussion subsystem for the proposals engine. -//! -//! Supported extrinsics: -//! - add_post - adds a post to existing discussion thread -//! -//! Public API: -//! - create_discussion - creates a discussion -//! - -// Ensure we're `no_std` when compiling for Wasm. -#![cfg_attr(not(feature = "std"), no_std)] - -// Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue. -//#![warn(missing_docs)] - -#[cfg(test)] -mod tests; -mod types; - -use rstd::clone::Clone; -use rstd::prelude::*; -use rstd::vec::Vec; -use runtime_primitives::traits::EnsureOrigin; -use srml_support::{decl_module, decl_storage, ensure, Parameter}; - -use srml_support::traits::Get; -use types::{Post, Thread}; - -// TODO: create events -// TODO: move errors to decl_error macro - -pub(crate) const MSG_NOT_AUTHOR: &str = "Author should match the post creator"; -pub(crate) const MSG_POST_EDITION_NUMBER_EXCEEDED: &str = "Post edition limit reached."; -pub(crate) const MSG_EMPTY_TITLE_PROVIDED: &str = "Discussion cannot have an empty title"; -pub(crate) const MSG_TOO_LONG_TITLE: &str = "Title is too long"; -pub(crate) const MSG_THREAD_DOESNT_EXIST: &str = "Thread doesn't exist"; -pub(crate) const MSG_POST_DOESNT_EXIST: &str = "Post doesn't exist"; -pub(crate) const MSG_EMPTY_POST_PROVIDED: &str = "Post cannot be empty"; -pub(crate) const MSG_TOO_LONG_POST: &str = "Post is too long"; - -/// 'Proposal discussion' substrate module Trait -pub trait Trait: system::Trait { - /// Origin from which thread author must come. - type ThreadAuthorOrigin: EnsureOrigin; - - /// Origin from which commenter must come. - type PostAuthorOrigin: EnsureOrigin; - - /// Discussion thread Id type - type ThreadId: From + Into + Parameter + Default + Copy; - - /// Post Id type - type PostId: From + Parameter + Default + Copy; - - /// Type for the thread author id. Should be authenticated by account id. - type ThreadAuthorId: From + Parameter + Default; - - /// Type for the post author id. Should be authenticated by account id. - type PostAuthorId: From + Parameter + Default; - - /// Defines post edition number limit. - type MaxPostEditionNumber: Get; - - /// Defines thread title length limit. - type ThreadTitleLengthLimit: Get; - - /// Defines post length limit. - type PostLengthLimit: Get; -} - -// Storage for the proposals discussion module -decl_storage! { - pub trait Store for Module as ProposalDiscussion { - /// Map thread identifier to corresponding thread. - pub ThreadById get(thread_by_id): map T::ThreadId => - Thread; - - /// Count of all threads that have been created. - pub ThreadCount get(fn thread_count): u32; - - /// Map thread id and post id to corresponding post. - pub PostThreadIdByPostId: double_map T::ThreadId, twox_128(T::PostId) => - Post; - - /// Count of all posts that have been created. - pub PostCount get(fn post_count): u32; - } -} - -decl_module! { - /// 'Proposal discussion' substrate module - pub struct Module for enum Call where origin: T::Origin { - - /// Adds a post with author origin check. - pub fn add_post(origin, thread_id : T::ThreadId, text : Vec) { - let account_id = T::ThreadAuthorOrigin::ensure_origin(origin)?; - let post_author_id = T::PostAuthorId::from(account_id); - - ensure!(>::exists(thread_id), MSG_THREAD_DOESNT_EXIST); - - ensure!(!text.is_empty(), MSG_EMPTY_POST_PROVIDED); - ensure!( - text.len() as u32 <= T::PostLengthLimit::get(), - MSG_TOO_LONG_POST - ); - - // mutation - - let next_post_count_value = Self::post_count() + 1; - let new_post_id = next_post_count_value; - - let new_post = Post { - text, - created_at: Self::current_block(), - updated_at: Self::current_block(), - author_id: post_author_id, - edition_number : 0, - thread_id, - }; - - let post_id = T::PostId::from(new_post_id); - >::insert(thread_id, post_id, new_post); - PostCount::put(next_post_count_value); - } - - /// Updates a post with author origin check. Update attempts number is limited. - pub fn update_post(origin, thread_id: T::ThreadId, post_id : T::PostId, text : Vec) { - let account_id = T::ThreadAuthorOrigin::ensure_origin(origin)?; - let post_author_id = T::PostAuthorId::from(account_id); - - ensure!(>::exists(thread_id), MSG_THREAD_DOESNT_EXIST); - ensure!(>::exists(thread_id, post_id), MSG_POST_DOESNT_EXIST); - - ensure!(!text.is_empty(), MSG_EMPTY_POST_PROVIDED); - ensure!( - text.len() as u32 <= T::PostLengthLimit::get(), - MSG_TOO_LONG_POST - ); - - let post = >::get(&thread_id, &post_id); - - ensure!(post.author_id == post_author_id, MSG_NOT_AUTHOR); - ensure!(post.edition_number < T::MaxPostEditionNumber::get(), - MSG_POST_EDITION_NUMBER_EXCEEDED); - - let new_post = Post { - text, - updated_at: Self::current_block(), - edition_number: post.edition_number + 1, - ..post - }; - - // mutation - - >::insert(thread_id, post_id, new_post); - } - } -} - -impl Module { - // Wrapper-function over system::block_number() - fn current_block() -> T::BlockNumber { - >::block_number() - } - - /// Create the discussion thread - pub fn create_thread(origin: T::Origin, title: Vec) -> Result { - let account_id = T::ThreadAuthorOrigin::ensure_origin(origin)?; - let thread_author_id = T::ThreadAuthorId::from(account_id); - - ensure!(!title.is_empty(), MSG_EMPTY_TITLE_PROVIDED); - ensure!( - title.len() as u32 <= T::ThreadTitleLengthLimit::get(), - MSG_TOO_LONG_TITLE - ); - - let next_thread_count_value = Self::thread_count() + 1; - let new_thread_id = next_thread_count_value; - - let new_thread = Thread { - title, - created_at: Self::current_block(), - author_id: thread_author_id, - }; - - // mutation - - let thread_id = T::ThreadId::from(new_thread_id); - >::insert(thread_id, new_thread); - ThreadCount::put(next_thread_count_value); - - Ok(thread_id) - } -} diff --git a/modules/proposals/engine/src/tests/mock/proposals.rs b/modules/proposals/engine/src/tests/mock/proposals.rs deleted file mode 100644 index 4a71933b18..0000000000 --- a/modules/proposals/engine/src/tests/mock/proposals.rs +++ /dev/null @@ -1,83 +0,0 @@ -use codec::{Decode, Encode}; -use num_enum::{IntoPrimitive, TryFromPrimitive}; -use rstd::convert::TryFrom; -use rstd::prelude::*; - -use srml_support::dispatch; - -use crate::{ProposalCodeDecoder, ProposalExecutable}; - -use super::*; - -/// Defines allowed proposals types. Integer value serves as proposal_type_id. -#[derive(Debug, Eq, PartialEq, TryFromPrimitive, IntoPrimitive)] -#[repr(u32)] -pub enum ProposalType { - /// Dummy(Text) proposal type - Dummy = 1, - - /// Testing proposal type for faults - Faulty = 10001, -} - -impl ProposalType { - fn compose_executable( - &self, - proposal_data: Vec, - ) -> Result, &'static str> { - match self { - ProposalType::Dummy => DummyExecutable::decode(&mut &proposal_data[..]) - .map_err(|err| err.what()) - .map(|obj| Box::new(obj) as Box), - ProposalType::Faulty => FaultyExecutable::decode(&mut &proposal_data[..]) - .map_err(|err| err.what()) - .map(|obj| Box::new(obj) as Box), - } - } -} - -impl ProposalCodeDecoder for ProposalType { - fn decode_proposal( - proposal_type: u32, - proposal_code: Vec, - ) -> Result, &'static str> { - Self::try_from(proposal_type) - .map_err(|_| "Unsupported proposal type")? - .compose_executable(proposal_code) - } -} - -/// Testing proposal type -#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug, Default)] -pub struct DummyExecutable { - pub title: Vec, - pub description: Vec, -} - -impl DummyExecutable { - pub fn proposal_type(&self) -> u32 { - ProposalType::Dummy.into() - } -} - -impl ProposalExecutable for DummyExecutable { - fn execute(&self) -> dispatch::Result { - Ok(()) - } -} - -/// Faulty proposal executable code wrapper. Used for failed proposal execution tests. -#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug, Default)] -pub struct FaultyExecutable; -impl ProposalExecutable for FaultyExecutable { - fn execute(&self) -> dispatch::Result { - Err("ExecutionFailed") - } -} - -impl FaultyExecutable { - /// Converts faulty proposal type to proposal_type_id - pub fn proposal_type(&self) -> u32 { - ProposalType::Faulty.into() - } -} diff --git a/runtime-modules/common/src/lib.rs b/runtime-modules/common/src/lib.rs index 23177ac457..e48bf36060 100644 --- a/runtime-modules/common/src/lib.rs +++ b/runtime-modules/common/src/lib.rs @@ -2,3 +2,4 @@ #![cfg_attr(not(feature = "std"), no_std)] pub mod currency; +pub mod origin_validator; diff --git a/runtime-modules/common/src/origin_validator.rs b/runtime-modules/common/src/origin_validator.rs new file mode 100644 index 0000000000..336331dda1 --- /dev/null +++ b/runtime-modules/common/src/origin_validator.rs @@ -0,0 +1,5 @@ +/// Abstract validator for the origin(account_id) and actor_id (eg.: thread author id). +pub trait ActorOriginValidator { + /// Check for valid combination of origin and actor_id + fn ensure_actor_origin(origin: Origin, actor_id: ActorId) -> Result; +} diff --git a/runtime-modules/content-working-group/src/lib.rs b/runtime-modules/content-working-group/src/lib.rs index b0c6aeea93..970181b9b9 100755 --- a/runtime-modules/content-working-group/src/lib.rs +++ b/runtime-modules/content-working-group/src/lib.rs @@ -1191,7 +1191,7 @@ decl_module! { // Increment NextChannelId NextChannelId::::mutate(|id| *id += as One>::one()); - /// CREDENTIAL STUFF /// + // CREDENTIAL STUFF // // Dial out to membership module and inform about new role as channe owner. let registered_role = >::register_role_on_member(owner, &member_in_role).is_ok(); diff --git a/runtime-modules/content-working-group/src/tests.rs b/runtime-modules/content-working-group/src/tests.rs index 582fd7ce30..03cc88e36d 100644 --- a/runtime-modules/content-working-group/src/tests.rs +++ b/runtime-modules/content-working-group/src/tests.rs @@ -511,17 +511,11 @@ fn begin_curator_applicant_review_success() { let opening = >::get(&normal_opening_constructed.curator_opening_id); match opening.stage { - hiring::OpeningStage::Active { - stage, - applications_added, - active_application_count, - unstaking_application_count, - deactivated_application_count, - } => { + hiring::OpeningStage::Active { stage, .. } => { match stage { hiring::ActiveOpeningStage::ReviewPeriod { - started_accepting_applicants_at_block, started_review_period_at_block, + .. } => { /* OK */ // assert_eq!(started_accepting_applicants_at_block, 0); @@ -921,12 +915,6 @@ impl UpdateCuratorRoleAccountFixture { assert_eq!(self.new_role_account, event_new_role_account); } - - pub fn call_and_assert_failed_result(&self, error_message: &'static str) { - let call_result = self.call(); - - assert_eq!(call_result, Err(error_message)); - } } #[test] @@ -958,6 +946,7 @@ struct UpdateCuratorRewardAccountFixture { } impl UpdateCuratorRewardAccountFixture { + #[allow(dead_code)] // delete if the method is unnecessary fn call(&self) -> Result<(), &'static str> { ContentWorkingGroup::update_curator_reward_account( self.origin.clone(), @@ -966,6 +955,7 @@ impl UpdateCuratorRewardAccountFixture { ) } + #[allow(dead_code)] // delete if the method is unnecessary pub fn call_and_assert_success(&self) { let _original_curator = CuratorById::::get(self.curator_id); @@ -996,12 +986,6 @@ impl UpdateCuratorRewardAccountFixture { assert_eq!(self.new_reward_account, event_reward_account); } - - pub fn call_and_assert_failed_result(&self, error_message: &'static str) { - let call_result = self.call(); - - assert_eq!(call_result, Err(error_message)); - } } #[test] @@ -1076,12 +1060,6 @@ impl LeaveCuratorRoleFixture { * recurringrewards, stake */ } - - pub fn call_and_assert_failed_result(&self, error_message: &'static str) { - let call_result = self.call(); - - assert_eq!(call_result, Err(error_message)); - } } #[test] @@ -1155,12 +1133,6 @@ impl TerminateCuratorRoleFixture { * recurringrewards, stake */ } - - pub fn call_and_assert_failed_result(&self, error_message: &'static str) { - let call_result = self.call(); - - assert_eq!(call_result, Err(error_message)); - } } #[test] @@ -1224,16 +1196,6 @@ impl SetLeadFixture { crate::RawEvent::LeadSet(new_lead_id) ); } - - pub fn call_and_assert_failed_result(&self, error_message: &'static str) { - let number_of_events_before_call = System::events().len(); - - let call_result = self.call(); - - assert_eq!(call_result, Err(error_message)); - - assert_eq!(System::events().len(), number_of_events_before_call); - } } #[test] @@ -1288,16 +1250,6 @@ impl UnsetLeadFixture { crate::RawEvent::LeadUnset(original_lead_id) ); } - - pub fn call_and_assert_failed_result(&self, error_message: &'static str) { - let number_of_events_before_call = System::events().len(); - - let call_result = self.call(); - - assert_eq!(call_result, Err(error_message)); - - assert_eq!(System::events().len(), number_of_events_before_call); - } } #[test] diff --git a/runtime-modules/governance/src/council.rs b/runtime-modules/governance/src/council.rs index 792977f073..b61cedd488 100644 --- a/runtime-modules/governance/src/council.rs +++ b/runtime-modules/governance/src/council.rs @@ -76,7 +76,7 @@ decl_module! { // Privileged methods /// Force set a zero staked council. Stakes in existing council will vanish into thin air! - fn set_council(origin, accounts: Vec) { + pub fn set_council(origin, accounts: Vec) { ensure_root(origin)?; let new_council: Seats> = accounts.into_iter().map(|account| { Seat { diff --git a/runtime-modules/governance/src/lib.rs b/runtime-modules/governance/src/lib.rs index 9e1d712f8b..d38dcb9689 100644 --- a/runtime-modules/governance/src/lib.rs +++ b/runtime-modules/governance/src/lib.rs @@ -3,7 +3,6 @@ pub mod council; pub mod election; -pub mod proposals; mod sealed_vote; mod stake; diff --git a/runtime-modules/governance/src/mock.rs b/runtime-modules/governance/src/mock.rs index 5e6dc33dbe..31a3f6b550 100644 --- a/runtime-modules/governance/src/mock.rs +++ b/runtime-modules/governance/src/mock.rs @@ -1,6 +1,6 @@ #![cfg(test)] -pub use super::{council, election, proposals}; +pub use super::{council, election}; pub use common::currency::GovernanceCurrency; pub use system; diff --git a/runtime-modules/governance/src/proposals.rs b/runtime-modules/governance/src/proposals.rs deleted file mode 100644 index e681e51d6c..0000000000 --- a/runtime-modules/governance/src/proposals.rs +++ /dev/null @@ -1,1572 +0,0 @@ -use codec::{Decode, Encode}; -use rstd::prelude::*; -use sr_primitives::{ - print, - traits::{Hash, SaturatedConversion, Zero}, -}; -use srml_support::traits::{Currency, Get, ReservableCurrency}; -use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure}; -use system::{self, ensure_root, ensure_signed}; - -#[cfg(feature = "std")] -use serde::{Deserialize, Serialize}; - -#[cfg(test)] -use primitives::storage::well_known_keys; - -use super::council; -pub use common::currency::{BalanceOf, GovernanceCurrency}; - -const DEFAULT_APPROVAL_QUORUM: u32 = 60; -const DEFAULT_MIN_STAKE: u32 = 100; -const DEFAULT_CANCELLATION_FEE: u32 = 5; -const DEFAULT_REJECTION_FEE: u32 = 10; - -const DEFAULT_VOTING_PERIOD_IN_DAYS: u32 = 10; -const DEFAULT_VOTING_PERIOD_IN_SECS: u32 = DEFAULT_VOTING_PERIOD_IN_DAYS * 24 * 60 * 60; - -const DEFAULT_NAME_MAX_LEN: u32 = 100; -const DEFAULT_DESCRIPTION_MAX_LEN: u32 = 10_000; -const DEFAULT_WASM_CODE_MAX_LEN: u32 = 2_000_000; - -const MSG_STAKE_IS_TOO_LOW: &str = "Stake is too low"; -const MSG_STAKE_IS_GREATER_THAN_BALANCE: &str = "Balance is too low to be staked"; -const MSG_ONLY_MEMBERS_CAN_PROPOSE: &str = "Only members can make a proposal"; -const MSG_ONLY_COUNCILORS_CAN_VOTE: &str = "Only councilors can vote on proposals"; -const MSG_PROPOSAL_NOT_FOUND: &str = "This proposal does not exist"; -const MSG_PROPOSAL_EXPIRED: &str = "Voting period is expired for this proposal"; -const MSG_PROPOSAL_FINALIZED: &str = "Proposal is finalized already"; -const MSG_YOU_ALREADY_VOTED: &str = "You have already voted on this proposal"; -const MSG_YOU_DONT_OWN_THIS_PROPOSAL: &str = "You do not own this proposal"; -const MSG_PROPOSAL_STATUS_ALREADY_UPDATED: &str = "Proposal status has been updated already"; -const MSG_EMPTY_NAME_PROVIDED: &str = "Proposal cannot have an empty name"; -const MSG_EMPTY_DESCRIPTION_PROVIDED: &str = "Proposal cannot have an empty description"; -const MSG_EMPTY_WASM_CODE_PROVIDED: &str = "Proposal cannot have an empty WASM code"; -const MSG_TOO_LONG_NAME: &str = "Name is too long"; -const MSG_TOO_LONG_DESCRIPTION: &str = "Description is too long"; -const MSG_TOO_LONG_WASM_CODE: &str = "WASM code is too big"; - -#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] -#[derive(Encode, Decode, Clone, PartialEq, Eq)] -pub enum ProposalStatus { - /// A new proposal that is available for voting. - Active, - /// If cancelled by a proposer. - Cancelled, - /// Not enough votes and voting period expired. - Expired, - /// To clear the quorum requirement, the percentage of council members with revealed votes - /// must be no less than the quorum value for the given proposal type. - Approved, - Rejected, - /// If all revealed votes are slashes, then the proposal is rejected, - /// and the proposal stake is slashed. - Slashed, -} - -impl Default for ProposalStatus { - fn default() -> Self { - ProposalStatus::Active - } -} - -use self::ProposalStatus::*; - -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)] -pub enum VoteKind { - /// Signals presence, but unwillingness to cast judgment on substance of vote. - Abstain, - /// Pass, an alternative or a ranking, for binary, multiple choice - /// and ranked choice propositions, respectively. - Approve, - /// Against proposal. - Reject, - /// Against the proposal, and slash proposal stake. - Slash, -} - -impl Default for VoteKind { - fn default() -> Self { - VoteKind::Abstain - } -} - -use self::VoteKind::*; - -#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] -#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)] -/// Proposal for node runtime update. -pub struct RuntimeUpgradeProposal { - id: u32, - proposer: AccountId, - stake: Balance, - name: Vec, - description: Vec, - wasm_hash: Hash, - proposed_at: BlockNumber, - status: ProposalStatus, -} - -#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] -#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)] -pub struct TallyResult { - proposal_id: u32, - abstentions: u32, - approvals: u32, - rejections: u32, - slashes: u32, - status: ProposalStatus, - finalized_at: BlockNumber, -} - -pub trait Trait: - timestamp::Trait + council::Trait + GovernanceCurrency + membership::members::Trait -{ - /// The overarching event type. - type Event: From> + Into<::Event>; -} - -decl_event!( - pub enum Event - where - ::Hash, - ::BlockNumber, - ::AccountId - { - // New events - - /// Params: - /// * Account id of a member who proposed. - /// * Id of a newly created proposal after it was saved in storage. - ProposalCreated(AccountId, u32), - ProposalCanceled(AccountId, u32), - ProposalStatusUpdated(u32, ProposalStatus), - - /// Params: - /// * Voter - an account id of a councilor. - /// * Id of a proposal. - /// * Kind of vote. - Voted(AccountId, u32, VoteKind), - - TallyFinalized(TallyResult), - - /// * Hash - hash of wasm code of runtime update. - RuntimeUpdated(u32, Hash), - - /// Root cancelled proposal - ProposalVetoed(u32), - } -); - -decl_storage! { - trait Store for Module as Proposals { - - // Parameters (defaut values could be exported to config): - - // TODO rename 'approval_quorum' -> 'quorum_percent' ?! - /// A percent (up to 100) of the council participants - /// that must vote affirmatively in order to pass. - ApprovalQuorum get(approval_quorum) config(): u32 = DEFAULT_APPROVAL_QUORUM; - - /// Minimum amount of a balance to be staked in order to make a proposal. - MinStake get(min_stake) config(): BalanceOf = - BalanceOf::::from(DEFAULT_MIN_STAKE); - - /// A fee to be slashed (burn) in case a proposer decides to cancel a proposal. - CancellationFee get(cancellation_fee) config(): BalanceOf = - BalanceOf::::from(DEFAULT_CANCELLATION_FEE); - - /// A fee to be slashed (burn) in case a proposal was rejected. - RejectionFee get(rejection_fee) config(): BalanceOf = - BalanceOf::::from(DEFAULT_REJECTION_FEE); - - /// Max duration of proposal in blocks until it will be expired if not enough votes. - VotingPeriod get(voting_period) config(): T::BlockNumber = - T::BlockNumber::from(DEFAULT_VOTING_PERIOD_IN_SECS / - (::MinimumPeriod::get().saturated_into::() * 2)); - - NameMaxLen get(name_max_len) config(): u32 = DEFAULT_NAME_MAX_LEN; - DescriptionMaxLen get(description_max_len) config(): u32 = DEFAULT_DESCRIPTION_MAX_LEN; - WasmCodeMaxLen get(wasm_code_max_len) config(): u32 = DEFAULT_WASM_CODE_MAX_LEN; - - // Persistent state (always relevant, changes constantly): - - /// Count of all proposals that have been created. - ProposalCount get(proposal_count): u32; - - /// Get proposal details by its id. - Proposals get(proposals): map u32 => RuntimeUpgradeProposal, T::BlockNumber, T::Hash>; - - /// Ids of proposals that are open for voting (have not been finalized yet). - ActiveProposalIds get(active_proposal_ids): Vec = vec![]; - - /// Get WASM code of runtime upgrade by hash of its content. - WasmCodeByHash get(wasm_code_by_hash): map T::Hash => Vec; - - VotesByProposal get(votes_by_proposal): map u32 => Vec<(T::AccountId, VoteKind)>; - - // TODO Rethink: this can be replaced with: votes_by_proposal.find(|vote| vote.0 == proposer) - VoteByAccountAndProposal get(vote_by_account_and_proposal): map (T::AccountId, u32) => VoteKind; - - TallyResults get(tally_results): map u32 => TallyResult; - } -} - -decl_module! { - pub struct Module for enum Call where origin: T::Origin { - - fn deposit_event() = default; - - /// Use next code to create a proposal from Substrate UI's web console: - /// ```js - /// post({ sender: runtime.indices.ss58Decode('F7Gh'), call: calls.proposals.createProposal(2500, "0x123", "0x456", "0x789") }).tie(console.log) - /// ``` - fn create_proposal( - origin, - stake: BalanceOf, - name: Vec, - description: Vec, - wasm_code: Vec - ) { - - let proposer = ensure_signed(origin)?; - ensure!(Self::can_participate(&proposer), MSG_ONLY_MEMBERS_CAN_PROPOSE); - ensure!(stake >= Self::min_stake(), MSG_STAKE_IS_TOO_LOW); - - ensure!(!name.is_empty(), MSG_EMPTY_NAME_PROVIDED); - ensure!(name.len() as u32 <= Self::name_max_len(), MSG_TOO_LONG_NAME); - - ensure!(!description.is_empty(), MSG_EMPTY_DESCRIPTION_PROVIDED); - ensure!(description.len() as u32 <= Self::description_max_len(), MSG_TOO_LONG_DESCRIPTION); - - ensure!(!wasm_code.is_empty(), MSG_EMPTY_WASM_CODE_PROVIDED); - ensure!(wasm_code.len() as u32 <= Self::wasm_code_max_len(), MSG_TOO_LONG_WASM_CODE); - - // Lock proposer's stake: - T::Currency::reserve(&proposer, stake) - .map_err(|_| MSG_STAKE_IS_GREATER_THAN_BALANCE)?; - - let proposal_id = Self::proposal_count() + 1; - ProposalCount::put(proposal_id); - - // See in substrate repo @ srml/contract/src/wasm/code_cache.rs:73 - let wasm_hash = T::Hashing::hash(&wasm_code); - - let new_proposal = RuntimeUpgradeProposal { - id: proposal_id, - proposer: proposer.clone(), - stake, - name, - description, - wasm_hash, - proposed_at: Self::current_block(), - status: Active - }; - - if !>::exists(wasm_hash) { - >::insert(wasm_hash, wasm_code); - } - >::insert(proposal_id, new_proposal); - ActiveProposalIds::mutate(|ids| ids.push(proposal_id)); - Self::deposit_event(RawEvent::ProposalCreated(proposer.clone(), proposal_id)); - - // Auto-vote with Approve if proposer is a councilor: - if Self::is_councilor(&proposer) { - Self::_process_vote(proposer, proposal_id, Approve)?; - } - } - - /// Use next code to create a proposal from Substrate UI's web console: - /// ```js - /// post({ sender: runtime.indices.ss58Decode('F7Gh'), call: calls.proposals.voteOnProposal(1, { option: "Approve", _type: "VoteKind" }) }).tie(console.log) - /// ``` - fn vote_on_proposal(origin, proposal_id: u32, vote: VoteKind) { - let voter = ensure_signed(origin)?; - ensure!(Self::is_councilor(&voter), MSG_ONLY_COUNCILORS_CAN_VOTE); - - ensure!(>::exists(proposal_id), MSG_PROPOSAL_NOT_FOUND); - let proposal = Self::proposals(proposal_id); - - ensure!(proposal.status == Active, MSG_PROPOSAL_FINALIZED); - - let not_expired = !Self::is_voting_period_expired(proposal.proposed_at); - ensure!(not_expired, MSG_PROPOSAL_EXPIRED); - - let did_not_vote_before = !>::exists((voter.clone(), proposal_id)); - ensure!(did_not_vote_before, MSG_YOU_ALREADY_VOTED); - - Self::_process_vote(voter, proposal_id, vote)?; - } - - // TODO add 'reason' why a proposer wants to cancel (UX + feedback)? - /// Cancel a proposal by its original proposer. Some fee will be withdrawn from his balance. - fn cancel_proposal(origin, proposal_id: u32) { - let proposer = ensure_signed(origin)?; - - ensure!(>::exists(proposal_id), MSG_PROPOSAL_NOT_FOUND); - let proposal = Self::proposals(proposal_id); - - ensure!(proposer == proposal.proposer, MSG_YOU_DONT_OWN_THIS_PROPOSAL); - ensure!(proposal.status == Active, MSG_PROPOSAL_FINALIZED); - - // Spend some minimum fee on proposer's balance for canceling a proposal - let fee = Self::cancellation_fee(); - let _ = T::Currency::slash_reserved(&proposer, fee); - - // Return unspent part of remaining staked deposit (after taking some fee) - let left_stake = proposal.stake - fee; - let _ = T::Currency::unreserve(&proposer, left_stake); - - Self::_update_proposal_status(proposal_id, Cancelled)?; - Self::deposit_event(RawEvent::ProposalCanceled(proposer, proposal_id)); - } - - // Called on every block - fn on_finalize(n: T::BlockNumber) { - if let Err(e) = Self::end_block(n) { - print(e); - } - } - - /// Cancel a proposal and return stake without slashing - fn veto_proposal(origin, proposal_id: u32) { - ensure_root(origin)?; - ensure!(>::exists(proposal_id), MSG_PROPOSAL_NOT_FOUND); - let proposal = Self::proposals(proposal_id); - ensure!(proposal.status == Active, MSG_PROPOSAL_FINALIZED); - - let _ = T::Currency::unreserve(&proposal.proposer, proposal.stake); - - Self::_update_proposal_status(proposal_id, Cancelled)?; - - Self::deposit_event(RawEvent::ProposalVetoed(proposal_id)); - } - - fn set_approval_quorum(origin, new_value: u32) { - ensure_root(origin)?; - ensure!(new_value > 0, "approval quorom must be greater than zero"); - ApprovalQuorum::put(new_value); - } - } -} - -impl Module { - fn current_block() -> T::BlockNumber { - >::block_number() - } - - fn can_participate(sender: &T::AccountId) -> bool { - !T::Currency::free_balance(sender).is_zero() - && >::is_member_account(sender) - } - - fn is_councilor(sender: &T::AccountId) -> bool { - >::is_councilor(sender) - } - - fn councilors_count() -> u32 { - >::active_council().len() as u32 - } - - fn approval_quorum_seats() -> u32 { - (Self::approval_quorum() * Self::councilors_count()) / 100 - } - - fn is_voting_period_expired(proposed_at: T::BlockNumber) -> bool { - Self::current_block() >= proposed_at + Self::voting_period() - } - - fn _process_vote(voter: T::AccountId, proposal_id: u32, vote: VoteKind) -> dispatch::Result { - let new_vote = (voter.clone(), vote.clone()); - if >::exists(proposal_id) { - // Append a new vote to other votes on this proposal: - >::mutate(proposal_id, |votes| votes.push(new_vote)); - } else { - // This is the first vote on this proposal: - >::insert(proposal_id, vec![new_vote]); - } - >::insert((voter.clone(), proposal_id), &vote); - Self::deposit_event(RawEvent::Voted(voter, proposal_id, vote)); - Ok(()) - } - - fn end_block(_now: T::BlockNumber) -> dispatch::Result { - // TODO refactor this method - - // TODO iterate over not expired proposals and tally - - Self::tally()?; - // TODO approve or reject a proposal - - Ok(()) - } - - /// Get the voters for the current proposal. - pub fn tally() -> dispatch::Result { - let councilors: u32 = Self::councilors_count(); - let quorum: u32 = Self::approval_quorum_seats(); - - for &proposal_id in Self::active_proposal_ids().iter() { - let votes = Self::votes_by_proposal(proposal_id); - let mut abstentions: u32 = 0; - let mut approvals: u32 = 0; - let mut rejections: u32 = 0; - let mut slashes: u32 = 0; - - for (_, vote) in votes.iter() { - match vote { - Abstain => abstentions += 1, - Approve => approvals += 1, - Reject => rejections += 1, - Slash => slashes += 1, - } - } - - let proposal = Self::proposals(proposal_id); - let is_expired = Self::is_voting_period_expired(proposal.proposed_at); - - // We need to check that the council is not empty because otherwise, - // if there is no votes on a proposal it will be counted as if - // all 100% (zero) councilors voted on the proposal and should be approved. - - let non_empty_council = councilors > 0; - let all_councilors_voted = non_empty_council && votes.len() as u32 == councilors; - let all_councilors_slashed = non_empty_council && slashes == councilors; - let quorum_reached = quorum > 0 && approvals >= quorum; - - // Don't approve a proposal right after quorum reached - // if not all councilors casted their votes. - // Instead let other councilors cast their vote - // up until the proposal's expired. - - let new_status: Option = if all_councilors_slashed { - Some(Slashed) - } else if all_councilors_voted { - if quorum_reached { - Some(Approved) - } else { - Some(Rejected) - } - } else if is_expired { - if quorum_reached { - Some(Approved) - } else { - // Proposal has been expired and quorum not reached. - Some(Expired) - } - } else { - // Councilors still have time to vote on this proposal. - None - }; - - // TODO move next block outside of tally to 'end_block' - if let Some(status) = new_status { - Self::_update_proposal_status(proposal_id, status.clone())?; - let tally_result = TallyResult { - proposal_id, - abstentions, - approvals, - rejections, - slashes, - status, - finalized_at: Self::current_block(), - }; - >::insert(proposal_id, &tally_result); - Self::deposit_event(RawEvent::TallyFinalized(tally_result)); - } - } - - Ok(()) - } - - /// Updates proposal status and removes proposal from active ids. - fn _update_proposal_status(proposal_id: u32, new_status: ProposalStatus) -> dispatch::Result { - let all_active_ids = Self::active_proposal_ids(); - let all_len = all_active_ids.len(); - let other_active_ids: Vec = all_active_ids - .into_iter() - .filter(|&id| id != proposal_id) - .collect(); - - let not_found_in_active = other_active_ids.len() == all_len; - if not_found_in_active { - // Seems like this proposal's status has been updated and removed from active. - Err(MSG_PROPOSAL_STATUS_ALREADY_UPDATED) - } else { - let pid = proposal_id.clone(); - match new_status { - Slashed => Self::_slash_proposal(pid)?, - Rejected | Expired => Self::_reject_proposal(pid)?, - Approved => Self::_approve_proposal(pid)?, - Active | Cancelled => { /* nothing */ } - } - ActiveProposalIds::put(other_active_ids); - >::mutate(proposal_id, |p| p.status = new_status.clone()); - Self::deposit_event(RawEvent::ProposalStatusUpdated(proposal_id, new_status)); - Ok(()) - } - } - - /// Slash a proposal. The staked deposit will be slashed. - fn _slash_proposal(proposal_id: u32) -> dispatch::Result { - let proposal = Self::proposals(proposal_id); - - // Slash proposer's stake: - let _ = T::Currency::slash_reserved(&proposal.proposer, proposal.stake); - - Ok(()) - } - - /// Reject a proposal. The staked deposit will be returned to a proposer. - fn _reject_proposal(proposal_id: u32) -> dispatch::Result { - let proposal = Self::proposals(proposal_id); - let proposer = proposal.proposer; - - // Spend some minimum fee on proposer's balance to prevent spamming attacks: - let fee = Self::rejection_fee(); - let _ = T::Currency::slash_reserved(&proposer, fee); - - // Return unspent part of remaining staked deposit (after taking some fee): - let left_stake = proposal.stake - fee; - let _ = T::Currency::unreserve(&proposer, left_stake); - - Ok(()) - } - - /// Approve a proposal. The staked deposit will be returned. - fn _approve_proposal(proposal_id: u32) -> dispatch::Result { - let proposal = Self::proposals(proposal_id); - let wasm_code = Self::wasm_code_by_hash(proposal.wasm_hash); - - // Return staked deposit to proposer: - let _ = T::Currency::unreserve(&proposal.proposer, proposal.stake); - - // Update wasm code of node's runtime: - >::set_code(system::RawOrigin::Root.into(), wasm_code)?; - - Self::deposit_event(RawEvent::RuntimeUpdated(proposal_id, proposal.wasm_hash)); - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - - use super::*; - use primitives::H256; - // The testing primitives are very useful for avoiding having to work with signatures - // or public keys. `u64` is used as the `AccountId` and no `Signature`s are requried. - use sr_primitives::{ - testing::Header, - traits::{BlakeTwo256, IdentityLookup}, - Perbill, - }; - use srml_support::*; - - impl_outer_origin! { - pub enum Origin for Test {} - } - - // Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted. - #[derive(Clone, PartialEq, Eq, Debug)] - pub struct Test; - - parameter_types! { - pub const BlockHashCount: u64 = 250; - pub const MaximumBlockWeight: u32 = 1024; - pub const MaximumBlockLength: u32 = 2 * 1024; - pub const AvailableBlockRatio: Perbill = Perbill::one(); - pub const MinimumPeriod: u64 = 5; - } - - impl system::Trait for Test { - type Origin = Origin; - type Index = u64; - type BlockNumber = u64; - type Call = (); - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type Header = Header; - type Event = (); - type BlockHashCount = BlockHashCount; - type MaximumBlockWeight = MaximumBlockWeight; - type MaximumBlockLength = MaximumBlockLength; - type AvailableBlockRatio = AvailableBlockRatio; - type Version = (); - } - - impl timestamp::Trait for Test { - type Moment = u64; - type OnTimestampSet = (); - type MinimumPeriod = MinimumPeriod; - } - - parameter_types! { - pub const ExistentialDeposit: u32 = 0; - pub const TransferFee: u32 = 0; - pub const CreationFee: u32 = 0; - pub const TransactionBaseFee: u32 = 1; - pub const TransactionByteFee: u32 = 0; - pub const InitialMembersBalance: u32 = 0; - } - - impl balances::Trait for Test { - /// The type for recording an account's balance. - type Balance = u64; - /// What to do if an account's free balance gets zeroed. - type OnFreeBalanceZero = (); - /// What to do if a new account is created. - type OnNewAccount = (); - /// The ubiquitous event type. - type Event = (); - - type DustRemoval = (); - type TransferPayment = (); - type ExistentialDeposit = ExistentialDeposit; - type TransferFee = TransferFee; - type CreationFee = CreationFee; - } - - impl council::Trait for Test { - type Event = (); - type CouncilTermEnded = (); - } - - impl GovernanceCurrency for Test { - type Currency = balances::Module; - } - - impl membership::members::Trait for Test { - type Event = (); - type MemberId = u32; - type PaidTermId = u32; - type SubscriptionId = u32; - type ActorId = u32; - type InitialMembersBalance = InitialMembersBalance; - } - - impl Trait for Test { - type Event = (); - } - - type System = system::Module; - type Balances = balances::Module; - type Proposals = Module; - - const COUNCILOR1: u64 = 1; - const COUNCILOR2: u64 = 2; - const COUNCILOR3: u64 = 3; - const COUNCILOR4: u64 = 4; - const COUNCILOR5: u64 = 5; - - const PROPOSER1: u64 = 11; - const PROPOSER2: u64 = 12; - - const NOT_COUNCILOR: u64 = 22; - - const ALL_COUNCILORS: [u64; 5] = [COUNCILOR1, COUNCILOR2, COUNCILOR3, COUNCILOR4, COUNCILOR5]; - - // TODO Figure out how to test Events in test... (low priority) - // mod proposals { - // pub use ::Event; - // } - // impl_outer_event!{ - // pub enum TestEvent for Test { - // balances,system,proposals, - // } - // } - - // This function basically just builds a genesis storage key/value store according to - // our desired mockup. - fn new_test_ext() -> runtime_io::TestExternalities { - let mut t = system::GenesisConfig::default() - .build_storage::() - .unwrap(); - - // balances doesn't contain GenesisConfig anymore - // // We use default for brevity, but you can configure as desired if needed. - // balances::GenesisConfig::::default() - // .assimilate_storage(&mut t) - // .unwrap(); - - let council_mock: council::Seats = ALL_COUNCILORS - .iter() - .map(|&c| council::Seat { - member: c, - stake: 0u64, - backers: vec![], - }) - .collect(); - - council::GenesisConfig:: { - active_council: council_mock, - term_ends_at: 0, - } - .assimilate_storage(&mut t) - .unwrap(); - - membership::members::GenesisConfig:: { - default_paid_membership_fee: 0, - members: vec![ - (PROPOSER1, "alice".into(), "".into(), "".into()), - (PROPOSER2, "bobby".into(), "".into(), "".into()), - (COUNCILOR1, "councilor1".into(), "".into(), "".into()), - (COUNCILOR2, "councilor2".into(), "".into(), "".into()), - (COUNCILOR3, "councilor3".into(), "".into(), "".into()), - (COUNCILOR4, "councilor4".into(), "".into(), "".into()), - (COUNCILOR5, "councilor5".into(), "".into(), "".into()), - ], - } - .assimilate_storage(&mut t) - .unwrap(); - // t.extend(GenesisConfig::{ - // // Here we can override defaults. - // }.build_storage().unwrap().0); - - t.into() - } - - /// A shortcut to get minimum stake in tests. - fn min_stake() -> u64 { - Proposals::min_stake() - } - - /// A shortcut to get cancellation fee in tests. - fn cancellation_fee() -> u64 { - Proposals::cancellation_fee() - } - - /// A shortcut to get rejection fee in tests. - fn rejection_fee() -> u64 { - Proposals::rejection_fee() - } - - /// Initial balance of Proposer 1. - fn initial_balance() -> u64 { - (min_stake() as f64 * 2.5) as u64 - } - - fn name() -> Vec { - b"Proposal Name".to_vec() - } - - fn description() -> Vec { - b"Proposal Description".to_vec() - } - - fn wasm_code() -> Vec { - b"Proposal Wasm Code".to_vec() - } - - fn _create_default_proposal() -> dispatch::Result { - _create_proposal(None, None, None, None, None) - } - - fn _create_proposal( - origin: Option, - stake: Option, - name: Option>, - description: Option>, - wasm_code: Option>, - ) -> dispatch::Result { - Proposals::create_proposal( - Origin::signed(origin.unwrap_or(PROPOSER1)), - stake.unwrap_or(min_stake()), - name.unwrap_or(self::name()), - description.unwrap_or(self::description()), - wasm_code.unwrap_or(self::wasm_code()), - ) - } - - fn get_runtime_code() -> Option> { - storage::unhashed::get_raw(well_known_keys::CODE) - } - - macro_rules! assert_runtime_code_empty { - () => { - assert_eq!(get_runtime_code(), Some(vec![])) - }; - } - - macro_rules! assert_runtime_code { - ($code:expr) => { - assert_eq!(get_runtime_code(), Some($code)) - }; - } - - #[test] - fn check_default_values() { - new_test_ext().execute_with(|| { - assert_eq!(Proposals::approval_quorum(), DEFAULT_APPROVAL_QUORUM); - assert_eq!( - Proposals::min_stake(), - BalanceOf::::from(DEFAULT_MIN_STAKE) - ); - assert_eq!( - Proposals::cancellation_fee(), - BalanceOf::::from(DEFAULT_CANCELLATION_FEE) - ); - assert_eq!( - Proposals::rejection_fee(), - BalanceOf::::from(DEFAULT_REJECTION_FEE) - ); - assert_eq!(Proposals::name_max_len(), DEFAULT_NAME_MAX_LEN); - assert_eq!( - Proposals::description_max_len(), - DEFAULT_DESCRIPTION_MAX_LEN - ); - assert_eq!(Proposals::wasm_code_max_len(), DEFAULT_WASM_CODE_MAX_LEN); - assert_eq!(Proposals::proposal_count(), 0); - assert!(Proposals::active_proposal_ids().is_empty()); - }); - } - - #[test] - fn member_create_proposal() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - assert_eq!(Proposals::active_proposal_ids().len(), 1); - assert_eq!(Proposals::active_proposal_ids()[0], 1); - - let wasm_hash = BlakeTwo256::hash(&wasm_code()); - let expected_proposal = RuntimeUpgradeProposal { - id: 1, - proposer: PROPOSER1, - stake: min_stake(), - name: name(), - description: description(), - wasm_hash, - proposed_at: 1, - status: Active, - }; - assert_eq!(Proposals::proposals(1), expected_proposal); - - // Check that stake amount has been locked on proposer's balance: - assert_eq!( - Balances::free_balance(PROPOSER1), - initial_balance() - min_stake() - ); - assert_eq!(Balances::reserved_balance(PROPOSER1), min_stake()); - - // TODO expect event ProposalCreated(AccountId, u32) - }); - } - - #[test] - fn not_member_cannot_create_proposal() { - new_test_ext().execute_with(|| { - // In this test a proposer has an empty balance - // thus he is not considered as a member. - assert_eq!( - _create_default_proposal(), - Err(MSG_ONLY_MEMBERS_CAN_PROPOSE) - ); - }); - } - - #[test] - fn cannot_create_proposal_with_small_stake() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_eq!( - _create_proposal(None, Some(min_stake() - 1), None, None, None), - Err(MSG_STAKE_IS_TOO_LOW) - ); - - // Check that balances remain unchanged afer a failed attempt to create a proposal: - assert_eq!(Balances::free_balance(PROPOSER1), initial_balance()); - assert_eq!(Balances::reserved_balance(PROPOSER1), 0); - }); - } - - #[test] - fn cannot_create_proposal_when_stake_is_greater_than_balance() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_eq!( - _create_proposal(None, Some(initial_balance() + 1), None, None, None), - Err(MSG_STAKE_IS_GREATER_THAN_BALANCE) - ); - - // Check that balances remain unchanged afer a failed attempt to create a proposal: - assert_eq!(Balances::free_balance(PROPOSER1), initial_balance()); - assert_eq!(Balances::reserved_balance(PROPOSER1), 0); - }); - } - - #[test] - fn cannot_create_proposal_with_empty_values() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - // Empty name: - assert_eq!( - _create_proposal(None, None, Some(vec![]), None, None), - Err(MSG_EMPTY_NAME_PROVIDED) - ); - - // Empty description: - assert_eq!( - _create_proposal(None, None, None, Some(vec![]), None), - Err(MSG_EMPTY_DESCRIPTION_PROVIDED) - ); - - // Empty WASM code: - assert_eq!( - _create_proposal(None, None, None, None, Some(vec![])), - Err(MSG_EMPTY_WASM_CODE_PROVIDED) - ); - }); - } - - #[test] - fn cannot_create_proposal_with_too_long_values() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - // Too long name: - assert_eq!( - _create_proposal(None, None, Some(too_long_name()), None, None), - Err(MSG_TOO_LONG_NAME) - ); - - // Too long description: - assert_eq!( - _create_proposal(None, None, None, Some(too_long_description()), None), - Err(MSG_TOO_LONG_DESCRIPTION) - ); - - // Too long WASM code: - assert_eq!( - _create_proposal(None, None, None, None, Some(too_long_wasm_code())), - Err(MSG_TOO_LONG_WASM_CODE) - ); - }); - } - - fn too_long_name() -> Vec { - vec![65; Proposals::name_max_len() as usize + 1] - } - - fn too_long_description() -> Vec { - vec![65; Proposals::description_max_len() as usize + 1] - } - - fn too_long_wasm_code() -> Vec { - vec![65; Proposals::wasm_code_max_len() as usize + 1] - } - - // ------------------------------------------------------------------- - // Cancellation - - #[test] - fn owner_cancel_proposal() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - assert_ok!(Proposals::cancel_proposal(Origin::signed(PROPOSER1), 1)); - assert_eq!(Proposals::proposals(1).status, Cancelled); - assert!(Proposals::active_proposal_ids().is_empty()); - - // Check that proposer's balance reduced by cancellation fee and other part of his stake returned to his balance: - assert_eq!( - Balances::free_balance(PROPOSER1), - initial_balance() - cancellation_fee() - ); - assert_eq!(Balances::reserved_balance(PROPOSER1), 0); - - // TODO expect event ProposalCancelled(AccountId, u32) - }); - } - - #[test] - fn owner_cannot_cancel_proposal_if_its_finalized() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - assert_ok!(Proposals::cancel_proposal(Origin::signed(PROPOSER1), 1)); - assert_eq!(Proposals::proposals(1).status, Cancelled); - - // Get balances updated after cancelling a proposal: - let updated_free_balance = Balances::free_balance(PROPOSER1); - let updated_reserved_balance = Balances::reserved_balance(PROPOSER1); - - assert_eq!( - Proposals::cancel_proposal(Origin::signed(PROPOSER1), 1), - Err(MSG_PROPOSAL_FINALIZED) - ); - - // Check that proposer's balance and locked stake haven't been changed: - assert_eq!(Balances::free_balance(PROPOSER1), updated_free_balance); - assert_eq!( - Balances::reserved_balance(PROPOSER1), - updated_reserved_balance - ); - }); - } - - #[test] - fn not_owner_cannot_cancel_proposal() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - let _ = Balances::deposit_creating(&PROPOSER2, initial_balance()); - assert_ok!(_create_default_proposal()); - assert_eq!( - Proposals::cancel_proposal(Origin::signed(PROPOSER2), 1), - Err(MSG_YOU_DONT_OWN_THIS_PROPOSAL) - ); - }); - } - - // ------------------------------------------------------------------- - // Voting - - #[test] - fn councilor_vote_on_proposal() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - - assert_ok!(Proposals::vote_on_proposal( - Origin::signed(COUNCILOR1), - 1, - Approve - )); - - // Check that a vote has been saved: - assert_eq!(Proposals::votes_by_proposal(1), vec![(COUNCILOR1, Approve)]); - assert_eq!( - Proposals::vote_by_account_and_proposal((COUNCILOR1, 1)), - Approve - ); - - // TODO expect event Voted(PROPOSER1, 1, Approve) - }); - } - - #[test] - fn councilor_cannot_vote_on_proposal_twice() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - - assert_ok!(Proposals::vote_on_proposal( - Origin::signed(COUNCILOR1), - 1, - Approve - )); - assert_eq!( - Proposals::vote_on_proposal(Origin::signed(COUNCILOR1), 1, Approve), - Err(MSG_YOU_ALREADY_VOTED) - ); - }); - } - - #[test] - fn autovote_with_approve_when_councilor_creates_proposal() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&COUNCILOR1, initial_balance()); - - assert_ok!(_create_proposal(Some(COUNCILOR1), None, None, None, None)); - - // Check that a vote has been sent automatically, - // such as the proposer is a councilor: - assert_eq!(Proposals::votes_by_proposal(1), vec![(COUNCILOR1, Approve)]); - assert_eq!( - Proposals::vote_by_account_and_proposal((COUNCILOR1, 1)), - Approve - ); - }); - } - - #[test] - fn not_councilor_cannot_vote_on_proposal() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - assert_eq!( - Proposals::vote_on_proposal(Origin::signed(NOT_COUNCILOR), 1, Approve), - Err(MSG_ONLY_COUNCILORS_CAN_VOTE) - ); - }); - } - - #[test] - fn councilor_cannot_vote_on_proposal_if_it_has_been_cancelled() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - assert_ok!(Proposals::cancel_proposal(Origin::signed(PROPOSER1), 1)); - assert_eq!( - Proposals::vote_on_proposal(Origin::signed(COUNCILOR1), 1, Approve), - Err(MSG_PROPOSAL_FINALIZED) - ); - }); - } - - #[test] - fn councilor_cannot_vote_on_proposal_if_tally_has_been_finalized() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - - // All councilors vote with 'Approve' on proposal: - let mut expected_votes: Vec<(u64, VoteKind)> = vec![]; - for &councilor in ALL_COUNCILORS.iter() { - expected_votes.push((councilor, Approve)); - assert_ok!(Proposals::vote_on_proposal( - Origin::signed(councilor), - 1, - Approve - )); - assert_eq!( - Proposals::vote_by_account_and_proposal((councilor, 1)), - Approve - ); - } - assert_eq!(Proposals::votes_by_proposal(1), expected_votes); - - System::set_block_number(2); - let _ = Proposals::end_block(2); - - assert!(Proposals::active_proposal_ids().is_empty()); - assert_eq!(Proposals::proposals(1).status, Approved); - - // Try to vote on finalized proposal: - assert_eq!( - Proposals::vote_on_proposal(Origin::signed(COUNCILOR1), 1, Reject), - Err(MSG_PROPOSAL_FINALIZED) - ); - }); - } - - // ------------------------------------------------------------------- - // Tally + Outcome: - - #[test] - fn approve_proposal_when_all_councilors_approved_it() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - - // All councilors approved: - let mut expected_votes: Vec<(u64, VoteKind)> = vec![]; - for &councilor in ALL_COUNCILORS.iter() { - expected_votes.push((councilor, Approve)); - assert_ok!(Proposals::vote_on_proposal( - Origin::signed(councilor), - 1, - Approve - )); - assert_eq!( - Proposals::vote_by_account_and_proposal((councilor, 1)), - Approve - ); - } - assert_eq!(Proposals::votes_by_proposal(1), expected_votes); - - assert_runtime_code_empty!(); - - System::set_block_number(2); - let _ = Proposals::end_block(2); - - // Check that runtime code has been updated after proposal approved. - assert_runtime_code!(wasm_code()); - - assert!(Proposals::active_proposal_ids().is_empty()); - assert_eq!(Proposals::proposals(1).status, Approved); - assert_eq!( - Proposals::tally_results(1), - TallyResult { - proposal_id: 1, - abstentions: 0, - approvals: ALL_COUNCILORS.len() as u32, - rejections: 0, - slashes: 0, - status: Approved, - finalized_at: 2 - } - ); - - // Check that proposer's stake has been added back to his balance: - assert_eq!(Balances::free_balance(PROPOSER1), initial_balance()); - assert_eq!(Balances::reserved_balance(PROPOSER1), 0); - - // TODO expect event ProposalStatusUpdated(1, Approved) - }); - } - - #[test] - fn approve_proposal_when_all_councilors_voted_and_only_quorum_approved() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - - // Only a quorum of councilors approved, others rejected: - let councilors = Proposals::councilors_count(); - let approvals = Proposals::approval_quorum_seats(); - let rejections = councilors - approvals; - for i in 0..councilors as usize { - let vote = if (i as u32) < approvals { - Approve - } else { - Reject - }; - assert_ok!(Proposals::vote_on_proposal( - Origin::signed(ALL_COUNCILORS[i]), - 1, - vote - )); - } - assert_eq!(Proposals::votes_by_proposal(1).len() as u32, councilors); - - assert_runtime_code_empty!(); - - System::set_block_number(2); - let _ = Proposals::end_block(2); - - // Check that runtime code has been updated after proposal approved. - assert_runtime_code!(wasm_code()); - - assert!(Proposals::active_proposal_ids().is_empty()); - assert_eq!(Proposals::proposals(1).status, Approved); - assert_eq!( - Proposals::tally_results(1), - TallyResult { - proposal_id: 1, - abstentions: 0, - approvals: approvals, - rejections: rejections, - slashes: 0, - status: Approved, - finalized_at: 2 - } - ); - - // Check that proposer's stake has been added back to his balance: - assert_eq!(Balances::free_balance(PROPOSER1), initial_balance()); - assert_eq!(Balances::reserved_balance(PROPOSER1), 0); - - // TODO expect event ProposalStatusUpdated(1, Approved) - }); - } - - #[test] - fn approve_proposal_when_voting_period_expired_if_only_quorum_voted() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - - // Only quorum of councilors approved, other councilors didn't vote: - let approvals = Proposals::approval_quorum_seats(); - for i in 0..approvals as usize { - let vote = if (i as u32) < approvals { - Approve - } else { - Slash - }; - assert_ok!(Proposals::vote_on_proposal( - Origin::signed(ALL_COUNCILORS[i]), - 1, - vote - )); - } - assert_eq!(Proposals::votes_by_proposal(1).len() as u32, approvals); - - assert_runtime_code_empty!(); - - let expiration_block = System::block_number() + Proposals::voting_period(); - System::set_block_number(2); - let _ = Proposals::end_block(2); - - // Check that runtime code has NOT been updated yet, - // because not all councilors voted and voting period is not expired yet. - assert_runtime_code_empty!(); - - System::set_block_number(expiration_block); - let _ = Proposals::end_block(expiration_block); - - // Check that runtime code has been updated after proposal approved. - assert_runtime_code!(wasm_code()); - - assert!(Proposals::active_proposal_ids().is_empty()); - assert_eq!(Proposals::proposals(1).status, Approved); - assert_eq!( - Proposals::tally_results(1), - TallyResult { - proposal_id: 1, - abstentions: 0, - approvals: approvals, - rejections: 0, - slashes: 0, - status: Approved, - finalized_at: expiration_block - } - ); - - // Check that proposer's stake has been added back to his balance: - assert_eq!(Balances::free_balance(PROPOSER1), initial_balance()); - assert_eq!(Balances::reserved_balance(PROPOSER1), 0); - - // TODO expect event ProposalStatusUpdated(1, Approved) - }); - } - - #[test] - fn reject_proposal_when_all_councilors_voted_and_quorum_not_reached() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - - // Less than a quorum of councilors approved, while others abstained: - let councilors = Proposals::councilors_count(); - let approvals = Proposals::approval_quorum_seats() - 1; - let abstentions = councilors - approvals; - for i in 0..councilors as usize { - let vote = if (i as u32) < approvals { - Approve - } else { - Abstain - }; - assert_ok!(Proposals::vote_on_proposal( - Origin::signed(ALL_COUNCILORS[i]), - 1, - vote - )); - } - assert_eq!(Proposals::votes_by_proposal(1).len() as u32, councilors); - - assert_runtime_code_empty!(); - - System::set_block_number(2); - let _ = Proposals::end_block(2); - - // Check that runtime code has NOT been updated after proposal slashed. - assert_runtime_code_empty!(); - - assert!(Proposals::active_proposal_ids().is_empty()); - assert_eq!(Proposals::proposals(1).status, Rejected); - assert_eq!( - Proposals::tally_results(1), - TallyResult { - proposal_id: 1, - abstentions: abstentions, - approvals: approvals, - rejections: 0, - slashes: 0, - status: Rejected, - finalized_at: 2 - } - ); - - // Check that proposer's balance reduced by burnt stake: - assert_eq!( - Balances::free_balance(PROPOSER1), - initial_balance() - rejection_fee() - ); - assert_eq!(Balances::reserved_balance(PROPOSER1), 0); - - // TODO expect event ProposalStatusUpdated(1, Rejected) - }); - } - - #[test] - fn reject_proposal_when_all_councilors_rejected_it() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - - // All councilors rejected: - let mut expected_votes: Vec<(u64, VoteKind)> = vec![]; - for &councilor in ALL_COUNCILORS.iter() { - expected_votes.push((councilor, Reject)); - assert_ok!(Proposals::vote_on_proposal( - Origin::signed(councilor), - 1, - Reject - )); - assert_eq!( - Proposals::vote_by_account_and_proposal((councilor, 1)), - Reject - ); - } - assert_eq!(Proposals::votes_by_proposal(1), expected_votes); - - assert_runtime_code_empty!(); - - System::set_block_number(2); - let _ = Proposals::end_block(2); - - // Check that runtime code has NOT been updated after proposal rejected. - assert_runtime_code_empty!(); - - assert!(Proposals::active_proposal_ids().is_empty()); - assert_eq!(Proposals::proposals(1).status, Rejected); - assert_eq!( - Proposals::tally_results(1), - TallyResult { - proposal_id: 1, - abstentions: 0, - approvals: 0, - rejections: ALL_COUNCILORS.len() as u32, - slashes: 0, - status: Rejected, - finalized_at: 2 - } - ); - - // Check that proposer's balance reduced by burnt stake: - assert_eq!( - Balances::free_balance(PROPOSER1), - initial_balance() - rejection_fee() - ); - assert_eq!(Balances::reserved_balance(PROPOSER1), 0); - - // TODO expect event ProposalStatusUpdated(1, Rejected) - }); - } - - #[test] - fn slash_proposal_when_all_councilors_slashed_it() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - - // All councilors slashed: - let mut expected_votes: Vec<(u64, VoteKind)> = vec![]; - for &councilor in ALL_COUNCILORS.iter() { - expected_votes.push((councilor, Slash)); - assert_ok!(Proposals::vote_on_proposal( - Origin::signed(councilor), - 1, - Slash - )); - assert_eq!( - Proposals::vote_by_account_and_proposal((councilor, 1)), - Slash - ); - } - assert_eq!(Proposals::votes_by_proposal(1), expected_votes); - - assert_runtime_code_empty!(); - - System::set_block_number(2); - let _ = Proposals::end_block(2); - - // Check that runtime code has NOT been updated after proposal slashed. - assert_runtime_code_empty!(); - - assert!(Proposals::active_proposal_ids().is_empty()); - assert_eq!(Proposals::proposals(1).status, Slashed); - assert_eq!( - Proposals::tally_results(1), - TallyResult { - proposal_id: 1, - abstentions: 0, - approvals: 0, - rejections: 0, - slashes: ALL_COUNCILORS.len() as u32, - status: Slashed, - finalized_at: 2 - } - ); - - // Check that proposer's balance reduced by burnt stake: - assert_eq!( - Balances::free_balance(PROPOSER1), - initial_balance() - min_stake() - ); - assert_eq!(Balances::reserved_balance(PROPOSER1), 0); - - // TODO expect event ProposalStatusUpdated(1, Slashed) - // TODO fix: event log assertion doesn't work and return empty event in every record - // assert_eq!(*System::events().last().unwrap(), - // EventRecord { - // phase: Phase::ApplyExtrinsic(0), - // event: RawEvent::ProposalStatusUpdated(1, Slashed), - // } - // ); - }); - } - - // In this case a proposal will be marked as 'Expired' - // and it will be processed in the same way as if it has been rejected. - #[test] - fn expire_proposal_when_not_all_councilors_voted_and_quorum_not_reached() { - new_test_ext().execute_with(|| { - let _ = Balances::deposit_creating(&PROPOSER1, initial_balance()); - - assert_ok!(_create_default_proposal()); - - // Less than a quorum of councilors approved: - let approvals = Proposals::approval_quorum_seats() - 1; - for i in 0..approvals as usize { - let vote = if (i as u32) < approvals { - Approve - } else { - Slash - }; - assert_ok!(Proposals::vote_on_proposal( - Origin::signed(ALL_COUNCILORS[i]), - 1, - vote - )); - } - assert_eq!(Proposals::votes_by_proposal(1).len() as u32, approvals); - - assert_runtime_code_empty!(); - - let expiration_block = System::block_number() + Proposals::voting_period(); - System::set_block_number(expiration_block); - let _ = Proposals::end_block(expiration_block); - - // Check that runtime code has NOT been updated after proposal slashed. - assert_runtime_code_empty!(); - - assert!(Proposals::active_proposal_ids().is_empty()); - assert_eq!(Proposals::proposals(1).status, Expired); - assert_eq!( - Proposals::tally_results(1), - TallyResult { - proposal_id: 1, - abstentions: 0, - approvals: approvals, - rejections: 0, - slashes: 0, - status: Expired, - finalized_at: expiration_block - } - ); - - // Check that proposer's balance reduced by burnt stake: - assert_eq!( - Balances::free_balance(PROPOSER1), - initial_balance() - rejection_fee() - ); - assert_eq!(Balances::reserved_balance(PROPOSER1), 0); - - // TODO expect event ProposalStatusUpdated(1, Rejected) - }); - } -} diff --git a/runtime-modules/membership/src/lib.rs b/runtime-modules/membership/src/lib.rs index a777802fd3..f3259534d5 100644 --- a/runtime-modules/membership/src/lib.rs +++ b/runtime-modules/membership/src/lib.rs @@ -5,5 +5,5 @@ pub mod genesis; pub mod members; pub mod role_types; -mod mock; +pub(crate) mod mock; mod tests; diff --git a/modules/proposals/codex/Cargo.toml b/runtime-modules/proposals/codex/Cargo.toml similarity index 72% rename from modules/proposals/codex/Cargo.toml rename to runtime-modules/proposals/codex/Cargo.toml index c70d81a060..ebb6ea31a2 100644 --- a/modules/proposals/codex/Cargo.toml +++ b/runtime-modules/proposals/codex/Cargo.toml @@ -20,6 +20,7 @@ std = [ 'proposal_discussion/std', 'stake/std', 'balances/std', + 'membership/std', ] @@ -42,50 +43,53 @@ version = '1.0.0' default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'substrate-primitives' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' [dependencies.rstd] default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'sr-std' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' [dependencies.runtime-primitives] default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'sr-primitives' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' [dependencies.srml-support] default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'srml-support' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' [dependencies.system] default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'srml-system' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' [dependencies.timestamp] default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'srml-timestamp' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' [dependencies.balances] package = 'srml-balances' default-features = false git = 'https://github.com/paritytech/substrate.git' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' [dependencies.stake] default_features = false -git = 'https://github.com/joystream/substrate-stake-module' package = 'substrate-stake-module' -rev = '0516efe9230da112bc095e28f34a3715c2e03ca8' +path = '../../stake' +[dependencies.membership] +default_features = false +package = 'substrate-membership-module' +path = '../../membership' [dependencies.proposal_engine] default_features = false @@ -97,8 +101,18 @@ default_features = false package = 'substrate-proposals-discussion-module' path = '../discussion' +[dependencies.common] +default_features = false +package = 'substrate-common-module' +path = '../../common' + [dev-dependencies.runtime-io] default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'sr-io' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' \ No newline at end of file +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dev-dependencies.governance] +default_features = false +package = 'substrate-governance-module' +path = '../../governance' \ No newline at end of file diff --git a/modules/proposals/codex/src/lib.rs b/runtime-modules/proposals/codex/src/lib.rs similarity index 54% rename from modules/proposals/codex/src/lib.rs rename to runtime-modules/proposals/codex/src/lib.rs index ddc5a3f59d..80ac1e8f18 100644 --- a/modules/proposals/codex/src/lib.rs +++ b/runtime-modules/proposals/codex/src/lib.rs @@ -11,28 +11,37 @@ // Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue. //#![warn(missing_docs)] -pub use proposal_types::{ProposalType, RuntimeUpgradeProposalExecutable, TextProposalExecutable}; - mod proposal_types; #[cfg(test)] mod tests; use codec::Encode; -use proposal_engine::*; use rstd::clone::Clone; -use rstd::marker::PhantomData; use rstd::prelude::*; +use rstd::str::from_utf8; use rstd::vec::Vec; -use srml_support::{decl_error, decl_module, decl_storage, ensure}; -use system::RawOrigin; +use srml_support::{decl_error, decl_module, decl_storage, ensure, print}; +use system::{ensure_root, RawOrigin}; + +use common::origin_validator::ActorOriginValidator; +use proposal_engine::ProposalParameters; /// 'Proposals codex' substrate module Trait -pub trait Trait: system::Trait + proposal_engine::Trait + proposal_discussion::Trait { +pub trait Trait: + system::Trait + proposal_engine::Trait + membership::members::Trait + proposal_discussion::Trait +{ /// Defines max allowed text proposal length. type TextProposalMaxLength: Get; /// Defines max wasm code length of the runtime upgrade proposal. type RuntimeUpgradeWasmProposalMaxLength: Get; + + /// Validates member id and origin combination + type MembershipOriginValidator: ActorOriginValidator< + Self::Origin, + MemberId, + Self::AccountId, + >; } use srml_support::traits::{Currency, Get}; @@ -44,6 +53,8 @@ pub type BalanceOf = pub type NegativeImbalance = <::Currency as Currency<::AccountId>>::NegativeImbalance; +type MemberId = ::MemberId; + decl_error! { pub enum Error { /// The size of the provided text for text proposal exceeded the limit @@ -57,6 +68,42 @@ decl_error! { /// Provided WASM code for the runtime upgrade proposal is empty RuntimeProposalIsEmpty, + + /// Require root origin in extrinsics + RequireRootOrigin, + + /// Errors from the proposal engine + ProposalsEngineError + } +} + +impl From for Error { + fn from(error: system::Error) -> Self { + match error { + system::Error::Other(msg) => Error::Other(msg), + system::Error::RequireRootOrigin => Error::RequireRootOrigin, + _ => Error::Other(error.into()), + } + } +} + +impl From for Error { + fn from(error: proposal_engine::Error) -> Self { + match error { + proposal_engine::Error::Other(msg) => Error::Other(msg), + proposal_engine::Error::RequireRootOrigin => Error::RequireRootOrigin, + _ => Error::Other(error.into()), + } + } +} + +impl From for Error { + fn from(error: proposal_discussion::Error) -> Self { + match error { + proposal_discussion::Error::Other(msg) => Error::Other(msg), + proposal_discussion::Error::RequireRootOrigin => Error::RequireRootOrigin, + _ => Error::Other(error.into()), + } } } @@ -78,39 +125,47 @@ decl_module! { /// Create text (signal) proposal type. On approval prints its content. pub fn create_text_proposal( origin, + member_id: MemberId, title: Vec, description: Vec, text: Vec, stake_balance: Option>, ) { + let account_id = T::MembershipOriginValidator::ensure_actor_origin(origin, member_id.clone())?; + let parameters = proposal_types::parameters::text_proposal::(); ensure!(!text.is_empty(), Error::TextProposalIsEmpty); ensure!(text.len() as u32 <= T::TextProposalMaxLength::get(), Error::TextProposalSizeExceeded); - let text_proposal = TextProposalExecutable{ - title: title.clone(), - description: description.clone(), - text, - }; - let proposal_code = text_proposal.encode(); + >::ensure_create_proposal_parameters_are_valid( + ¶meters, + &title, + &description, + stake_balance, + )?; - let (cloned_origin1, cloned_origin2) = Self::double_origin(origin); + >::ensure_can_create_thread( + &title, + member_id.clone(), + )?; + + let proposal_code = >::text_proposal(title.clone(), description.clone(), text); let discussion_thread_id = >::create_thread( - cloned_origin1, + member_id, title.clone(), )?; let proposal_id = >::create_proposal( - cloned_origin2, + account_id, + member_id, parameters, title, description, stake_balance, - text_proposal.proposal_type(), - proposal_code, + proposal_code.encode(), )?; >::insert(proposal_id, discussion_thread_id); @@ -119,44 +174,88 @@ decl_module! { /// Create runtime upgrade proposal type. On approval prints its content. pub fn create_runtime_upgrade_proposal( origin, + member_id: MemberId, title: Vec, description: Vec, wasm: Vec, stake_balance: Option>, ) { + let account_id = T::MembershipOriginValidator::ensure_actor_origin(origin, member_id.clone())?; + let parameters = proposal_types::parameters::upgrade_runtime::(); ensure!(!wasm.is_empty(), Error::RuntimeProposalIsEmpty); ensure!(wasm.len() as u32 <= T::RuntimeUpgradeWasmProposalMaxLength::get(), Error::RuntimeProposalSizeExceeded); - let proposal = RuntimeUpgradeProposalExecutable{ - title: title.clone(), - description: description.clone(), - wasm, - marker : PhantomData:: - }; - let proposal_code = proposal.encode(); + >::ensure_create_proposal_parameters_are_valid( + ¶meters, + &title, + &description, + stake_balance, + )?; - let (cloned_origin1, cloned_origin2) = Self::double_origin(origin); + >::ensure_can_create_thread( + &title, + member_id.clone(), + )?; + + let proposal_code = >::text_proposal(title.clone(), description.clone(), wasm); let discussion_thread_id = >::create_thread( - cloned_origin1, + member_id, title.clone(), )?; let proposal_id = >::create_proposal( - cloned_origin2, + account_id, + member_id, parameters, title, description, stake_balance, - proposal.proposal_type(), - proposal_code, + proposal_code.encode(), )?; >::insert(proposal_id, discussion_thread_id); } + +// *************** Extrinsic to execute + + /// Text proposal extrinsic. Should be used as callable object to pass to the engine module. + fn text_proposal( + origin, + title: Vec, + _description: Vec, + _text: Vec, + ) { + ensure_root(origin)?; + print("Text proposal: "); + let title_string_result = from_utf8(title.as_slice()); + if let Ok(title_string) = title_string_result{ + print(title_string); + } + } + + /// Runtime upgrade proposal extrinsic. + /// Should be used as callable object to pass to the engine module. + fn runtime_upgrade_proposal( + origin, + title: Vec, + _description: Vec, + wasm: Vec, + ) { + let (cloned_origin1, cloned_origin2) = Self::double_origin(origin); + ensure_root(cloned_origin1)?; + + print("Runtime upgrade proposal: "); + let title_string_result = from_utf8(title.as_slice()); + if let Ok(title_string) = title_string_result{ + print(title_string); + } + + >::set_code(cloned_origin2, wasm)?; + } } } diff --git a/runtime-modules/proposals/codex/src/proposal_types/mod.rs b/runtime-modules/proposals/codex/src/proposal_types/mod.rs new file mode 100644 index 0000000000..d58cb7b037 --- /dev/null +++ b/runtime-modules/proposals/codex/src/proposal_types/mod.rs @@ -0,0 +1,31 @@ +pub(crate) mod parameters { + use crate::{BalanceOf, ProposalParameters}; + + // Proposal parameters for the upgrade runtime proposal + pub(crate) fn upgrade_runtime( + ) -> ProposalParameters> { + ProposalParameters { + voting_period: T::BlockNumber::from(50000u32), + grace_period: T::BlockNumber::from(10000u32), + approval_quorum_percentage: 80, + approval_threshold_percentage: 80, + slashing_quorum_percentage: 80, + slashing_threshold_percentage: 80, + required_stake: Some(>::from(50000u32)), + } + } + + // Proposal parameters for the text proposal + pub(crate) fn text_proposal( + ) -> ProposalParameters> { + ProposalParameters { + voting_period: T::BlockNumber::from(50000u32), + grace_period: T::BlockNumber::from(10000u32), + approval_quorum_percentage: 40, + approval_threshold_percentage: 51, + slashing_quorum_percentage: 80, + slashing_threshold_percentage: 80, + required_stake: Some(>::from(500u32)), + } + } +} diff --git a/modules/proposals/codex/src/tests/mock.rs b/runtime-modules/proposals/codex/src/tests/mock.rs similarity index 80% rename from modules/proposals/codex/src/tests/mock.rs rename to runtime-modules/proposals/codex/src/tests/mock.rs index abe8d7dbcb..5cc6aa4bcc 100644 --- a/modules/proposals/codex/src/tests/mock.rs +++ b/runtime-modules/proposals/codex/src/tests/mock.rs @@ -7,7 +7,7 @@ pub use runtime_primitives::{ testing::{Digest, DigestItem, Header, UintAuthorityId}, traits::{BlakeTwo256, Convert, IdentityLookup, OnFinalize}, weights::Weight, - BuildStorage, Perbill, + BuildStorage, DispatchError, Perbill, }; use proposal_engine::VotersParameters; @@ -36,6 +36,19 @@ impl_outer_dispatch! { } } +impl common::currency::GovernanceCurrency for Test { + type Currency = balances::Module; +} + +impl membership::members::Trait for Test { + type Event = (); + type MemberId = u64; + type PaidTermId = u64; + type SubscriptionId = u64; + type ActorId = u64; + type InitialMembersBalance = (); +} + parameter_types! { pub const ExistentialDeposit: u32 = 0; pub const TransferFee: u32 = 0; @@ -77,50 +90,52 @@ parameter_types! { impl proposal_engine::Trait for Test { type Event = (); - - type ProposalOrigin = system::EnsureSigned; - - type VoteOrigin = system::EnsureSigned; - + type ProposerOriginValidator = (); + type VoterOriginValidator = (); type TotalVotersCounter = MockVotersParameters; - - type ProposalCodeDecoder = crate::ProposalType; - type ProposalId = u32; - - type ProposerId = u64; - - type VoterId = u64; - type StakeHandlerProvider = proposal_engine::DefaultStakeHandlerProvider; - type CancellationFee = CancellationFee; - type RejectionFee = RejectionFee; - type TitleMaxLength = TitleMaxLength; - type DescriptionMaxLength = DescriptionMaxLength; - type MaxActiveProposalLimit = MaxActiveProposalLimit; + type DispatchableCallCode = crate::Call; +} + +impl Default for crate::Call { + fn default() -> Self { + panic!("shouldn't call default for Call"); + } +} + +impl governance::council::Trait for Test { + type Event = (); + type CouncilTermEnded = (); +} + +impl common::origin_validator::ActorOriginValidator for () { + fn ensure_actor_origin(_: Origin, _: u64) -> Result { + Ok(1) + } } parameter_types! { pub const MaxPostEditionNumber: u32 = 5; + pub const MaxThreadInARowNumber: u32 = 3; pub const ThreadTitleLengthLimit: u32 = 200; pub const PostLengthLimit: u32 = 2000; } impl proposal_discussion::Trait for Test { - type ThreadAuthorOrigin = system::EnsureSigned; - type PostAuthorOrigin = system::EnsureSigned; + type Event = (); + type PostAuthorOriginValidator = (); type ThreadId = u32; type PostId = u32; - type ThreadAuthorId = u64; - type PostAuthorId = u64; type MaxPostEditionNumber = MaxPostEditionNumber; type ThreadTitleLengthLimit = ThreadTitleLengthLimit; type PostLengthLimit = PostLengthLimit; + type MaxThreadInARowNumber = MaxThreadInARowNumber; } pub struct MockVotersParameters; @@ -138,6 +153,7 @@ parameter_types! { impl crate::Trait for Test { type TextProposalMaxLength = TextProposalMaxLength; type RuntimeUpgradeWasmProposalMaxLength = RuntimeUpgradeWasmProposalMaxLength; + type MembershipOriginValidator = (); } impl system::Trait for Test { @@ -164,8 +180,6 @@ impl timestamp::Trait for Test { type MinimumPeriod = MinimumPeriod; } -// TODO add a Hook type to capture TriggerElection and CouncilElected hooks - pub fn initial_test_ext() -> runtime_io::TestExternalities { let t = system::GenesisConfig::default() .build_storage::() diff --git a/modules/proposals/codex/src/tests/mod.rs b/runtime-modules/proposals/codex/src/tests/mod.rs similarity index 91% rename from modules/proposals/codex/src/tests/mod.rs rename to runtime-modules/proposals/codex/src/tests/mod.rs index 6253fc6ad0..dc90276582 100644 --- a/modules/proposals/codex/src/tests/mod.rs +++ b/runtime-modules/proposals/codex/src/tests/mod.rs @@ -11,6 +11,7 @@ use mock::*; fn create_text_proposal_codex_call_succeeds() { initial_test_ext().execute_with(|| { let account_id = 1; + let proposer_id = 1; let origin = RawOrigin::Signed(account_id).into(); let required_stake = Some(>::from(500u32)); @@ -19,6 +20,7 @@ fn create_text_proposal_codex_call_succeeds() { assert_eq!( ProposalCodex::create_text_proposal( origin, + proposer_id, b"title".to_vec(), b"body".to_vec(), b"text".to_vec(), @@ -39,12 +41,13 @@ fn create_text_proposal_codex_call_fails_with_invalid_stake() { assert_eq!( ProposalCodex::create_text_proposal( RawOrigin::Signed(1).into(), + 1, b"title".to_vec(), b"body".to_vec(), b"text".to_vec(), None, ), - Err(Error::Other("Stake cannot be empty with this proposal")) + Err(Error::Other("EmptyStake")) ); let invalid_stake = Some(>::from(5000u32)); @@ -52,12 +55,13 @@ fn create_text_proposal_codex_call_fails_with_invalid_stake() { assert_eq!( ProposalCodex::create_text_proposal( RawOrigin::Signed(1).into(), + 1, b"title".to_vec(), b"body".to_vec(), b"text".to_vec(), invalid_stake, ), - Err(Error::Other("Stake differs from the proposal requirements")) + Err(Error::Other("StakeDiffersFromRequired")) ); }); } @@ -71,6 +75,7 @@ fn create_text_proposal_codex_call_fails_with_incorrect_text_size() { assert_eq!( ProposalCodex::create_text_proposal( origin, + 1, b"title".to_vec(), b"body".to_vec(), long_text, @@ -82,6 +87,7 @@ fn create_text_proposal_codex_call_fails_with_incorrect_text_size() { assert_eq!( ProposalCodex::create_text_proposal( RawOrigin::Signed(1).into(), + 1, b"title".to_vec(), b"body".to_vec(), Vec::new(), @@ -99,6 +105,7 @@ fn create_text_proposal_codex_call_fails_with_insufficient_rights() { assert!(ProposalCodex::create_text_proposal( origin, + 1, b"title".to_vec(), b"body".to_vec(), b"text".to_vec(), @@ -117,6 +124,7 @@ fn create_upgrade_runtime_proposal_codex_call_fails_with_incorrect_wasm_size() { assert_eq!( ProposalCodex::create_runtime_upgrade_proposal( origin, + 1, b"title".to_vec(), b"body".to_vec(), long_wasm, @@ -128,6 +136,7 @@ fn create_upgrade_runtime_proposal_codex_call_fails_with_incorrect_wasm_size() { assert_eq!( ProposalCodex::create_runtime_upgrade_proposal( RawOrigin::Signed(1).into(), + 1, b"title".to_vec(), b"body".to_vec(), Vec::new(), @@ -145,6 +154,7 @@ fn create_upgrade_runtime_proposal_codex_call_fails_with_insufficient_rights() { assert!(ProposalCodex::create_runtime_upgrade_proposal( origin, + 1, b"title".to_vec(), b"body".to_vec(), b"wasm".to_vec(), @@ -157,15 +167,17 @@ fn create_upgrade_runtime_proposal_codex_call_fails_with_insufficient_rights() { #[test] fn create_runtime_upgrade_proposal_codex_call_fails_with_invalid_stake() { initial_test_ext().execute_with(|| { + let proposer_id = 1; assert_eq!( ProposalCodex::create_runtime_upgrade_proposal( RawOrigin::Signed(1).into(), + proposer_id, b"title".to_vec(), b"body".to_vec(), b"wasm".to_vec(), None, ), - Err(Error::Other("Stake cannot be empty with this proposal")) + Err(Error::Other("EmptyStake")) ); let invalid_stake = Some(>::from(500u32)); @@ -173,12 +185,13 @@ fn create_runtime_upgrade_proposal_codex_call_fails_with_invalid_stake() { assert_eq!( ProposalCodex::create_runtime_upgrade_proposal( RawOrigin::Signed(1).into(), + proposer_id, b"title".to_vec(), b"body".to_vec(), b"wasm".to_vec(), invalid_stake, ), - Err(Error::Other("Stake differs from the proposal requirements")) + Err(Error::Other("StakeDiffersFromRequired")) ); }); } @@ -187,6 +200,7 @@ fn create_runtime_upgrade_proposal_codex_call_fails_with_invalid_stake() { fn create_runtime_upgrade_proposal_codex_call_succeeds() { initial_test_ext().execute_with(|| { let account_id = 1; + let proposer_id = 1; let origin = RawOrigin::Signed(account_id).into(); let required_stake = Some(>::from(50000u32)); @@ -195,6 +209,7 @@ fn create_runtime_upgrade_proposal_codex_call_succeeds() { assert_eq!( ProposalCodex::create_runtime_upgrade_proposal( origin, + proposer_id, b"title".to_vec(), b"body".to_vec(), b"wasm".to_vec(), diff --git a/modules/proposals/discussion/Cargo.toml b/runtime-modules/proposals/discussion/Cargo.toml similarity index 63% rename from modules/proposals/discussion/Cargo.toml rename to runtime-modules/proposals/discussion/Cargo.toml index ead2c561ba..c5bad3b951 100644 --- a/modules/proposals/discussion/Cargo.toml +++ b/runtime-modules/proposals/discussion/Cargo.toml @@ -12,13 +12,14 @@ std = [ 'rstd/std', 'srml-support/std', 'primitives/std', - 'runtime-primitives/std', + 'sr-primitives/std', 'system/std', 'timestamp/std', 'serde', + 'membership/std', + 'common/std', ] - [dependencies.num_enum] default_features = false version = "0.4.2" @@ -38,40 +39,56 @@ version = '1.0.0' default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'substrate-primitives' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' [dependencies.rstd] default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'sr-std' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' -[dependencies.runtime-primitives] +[dependencies.sr-primitives] default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'sr-primitives' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' [dependencies.srml-support] default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'srml-support' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' [dependencies.system] default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'srml-system' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' [dependencies.timestamp] default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'srml-timestamp' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.membership] +default_features = false +package = 'substrate-membership-module' +path = '../../membership' + +[dependencies.common] +default_features = false +package = 'substrate-common-module' +path = '../../common' [dev-dependencies.runtime-io] default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'sr-io' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' \ No newline at end of file +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dev-dependencies.balances] +package = 'srml-balances' +default-features = false +git = 'https://github.com/paritytech/substrate.git' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' \ No newline at end of file diff --git a/runtime-modules/proposals/discussion/src/lib.rs b/runtime-modules/proposals/discussion/src/lib.rs new file mode 100644 index 0000000000..8edd4f3b6c --- /dev/null +++ b/runtime-modules/proposals/discussion/src/lib.rs @@ -0,0 +1,321 @@ +//! Proposals discussion module for the Joystream platform. Version 2. +//! Contains discussion subsystem for the proposals engine. +//! +//! Supported extrinsics: +//! - add_post - adds a post to existing discussion thread +//! - update_post - updates existing post +//! +//! Public API: +//! - create_discussion - creates a discussion +//! + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +// Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue. +//#![warn(missing_docs)] + +#[cfg(test)] +mod tests; +mod types; + +use rstd::clone::Clone; +use rstd::prelude::*; +use rstd::vec::Vec; +use srml_support::{decl_error, decl_event, decl_module, decl_storage, ensure, Parameter}; + +use srml_support::traits::Get; +use types::{Post, Thread, ThreadCounter}; + +use common::origin_validator::ActorOriginValidator; +use srml_support::dispatch::DispatchResult; + +type MemberId = ::MemberId; + +decl_event!( + /// Proposals engine events + pub enum Event + where + ::ThreadId, + MemberId = MemberId, + ::PostId, + { + /// Emits on thread creation. + ThreadCreated(ThreadId, MemberId), + + /// Emits on post creation. + PostCreated(PostId, MemberId), + + /// Emits on post update. + PostUpdated(PostId, MemberId), + } +); + +/// 'Proposal discussion' substrate module Trait +pub trait Trait: system::Trait + membership::members::Trait { + /// Engine event type. + type Event: From> + Into<::Event>; + + /// Validates post author id and origin combination + type PostAuthorOriginValidator: ActorOriginValidator< + Self::Origin, + MemberId, + Self::AccountId, + >; + + /// Discussion thread Id type + type ThreadId: From + Into + Parameter + Default + Copy; + + /// Post Id type + type PostId: From + Parameter + Default + Copy; + + /// Defines post edition number limit. + type MaxPostEditionNumber: Get; + + /// Defines thread title length limit. + type ThreadTitleLengthLimit: Get; + + /// Defines post length limit. + type PostLengthLimit: Get; + + /// Defines max thread by same author in a row number limit. + type MaxThreadInARowNumber: Get; +} + +decl_error! { + pub enum Error { + /// The size of the provided text for text proposal exceeded the limit + TextProposalSizeExceeded, + + /// Author should match the post creator + NotAuthor, + + /// Post edition limit reached + PostEditionNumberExceeded, + + /// Discussion cannot have an empty title + EmptyTitleProvided, + + /// Title is too long + TitleIsTooLong, + + /// Thread doesn't exist + ThreadDoesntExist, + + /// Post doesn't exist + PostDoesntExist, + + /// Post cannot be empty + EmptyPostProvided, + + /// Post is too long + PostIsTooLong, + + /// Max number of threads by same author in a row limit exceeded + MaxThreadInARowLimitExceeded, + + /// Require root origin in extrinsics + RequireRootOrigin, + } +} + +impl From for Error { + fn from(error: system::Error) -> Self { + match error { + system::Error::Other(msg) => Error::Other(msg), + system::Error::RequireRootOrigin => Error::RequireRootOrigin, + _ => Error::Other(error.into()), + } + } +} + +// Storage for the proposals discussion module +decl_storage! { + pub trait Store for Module as ProposalDiscussion { + /// Map thread identifier to corresponding thread. + pub ThreadById get(thread_by_id): map T::ThreadId => + Thread, T::BlockNumber>; + + /// Count of all threads that have been created. + pub ThreadCount get(fn thread_count): u32; + + /// Map thread id and post id to corresponding post. + pub PostThreadIdByPostId: double_map T::ThreadId, twox_128(T::PostId) => + Post, T::BlockNumber, T::ThreadId>; + + /// Count of all posts that have been created. + pub PostCount get(fn post_count): u32; + + /// Last author thread counter (part of the antispam mechanism) + pub LastThreadAuthorCounter get(fn last_thread_author_counter): + Option>>; + } +} + +decl_module! { + /// 'Proposal discussion' substrate module + pub struct Module for enum Call where origin: T::Origin { + /// Predefined errors + type Error = Error; + + /// Emits an event. Default substrate implementation. + fn deposit_event() = default; + + /// Adds a post with author origin check. + pub fn add_post( + origin, + post_author_id: MemberId, + thread_id : T::ThreadId, + text : Vec + ) { + T::PostAuthorOriginValidator::ensure_actor_origin( + origin, + post_author_id.clone(), + )?; + ensure!(>::exists(thread_id), Error::ThreadDoesntExist); + + ensure!(!text.is_empty(),Error::EmptyPostProvided); + ensure!( + text.len() as u32 <= T::PostLengthLimit::get(), + Error::PostIsTooLong + ); + + // mutation + + let next_post_count_value = Self::post_count() + 1; + let new_post_id = next_post_count_value; + + let new_post = Post { + text, + created_at: Self::current_block(), + updated_at: Self::current_block(), + author_id: post_author_id.clone(), + edition_number : 0, + thread_id, + }; + + let post_id = T::PostId::from(new_post_id); + >::insert(thread_id, post_id, new_post); + PostCount::put(next_post_count_value); + Self::deposit_event(RawEvent::PostCreated(post_id, post_author_id)); + } + + /// Updates a post with author origin check. Update attempts number is limited. + pub fn update_post( + origin, + post_author_id: MemberId, + thread_id: T::ThreadId, + post_id : T::PostId, + text : Vec + ){ + T::PostAuthorOriginValidator::ensure_actor_origin( + origin, + post_author_id.clone(), + )?; + + ensure!(>::exists(thread_id), Error::ThreadDoesntExist); + ensure!(>::exists(thread_id, post_id), Error::PostDoesntExist); + + ensure!(!text.is_empty(), Error::EmptyPostProvided); + ensure!( + text.len() as u32 <= T::PostLengthLimit::get(), + Error::PostIsTooLong + ); + + let post = >::get(&thread_id, &post_id); + + ensure!(post.author_id == post_author_id, Error::NotAuthor); + ensure!(post.edition_number < T::MaxPostEditionNumber::get(), + Error::PostEditionNumberExceeded); + + let new_post = Post { + text, + updated_at: Self::current_block(), + edition_number: post.edition_number + 1, + ..post + }; + + // mutation + + >::insert(thread_id, post_id, new_post); + Self::deposit_event(RawEvent::PostUpdated(post_id, post_author_id)); + } + } +} + +impl Module { + // Wrapper-function over system::block_number() + fn current_block() -> T::BlockNumber { + >::block_number() + } + + /// Create the discussion thread. Cannot add more threads than 'predefined limit = MaxThreadInARowNumber' + /// times in a row by the same author. + pub fn create_thread( + thread_author_id: MemberId, + title: Vec, + ) -> Result { + Self::ensure_can_create_thread(&title, thread_author_id.clone())?; + + let next_thread_count_value = Self::thread_count() + 1; + let new_thread_id = next_thread_count_value; + + let new_thread = Thread { + title, + created_at: Self::current_block(), + author_id: thread_author_id.clone(), + }; + + // get new 'threads in a row' counter for the author + let current_thread_counter = Self::get_updated_thread_counter(thread_author_id.clone()); + + // mutation + + let thread_id = T::ThreadId::from(new_thread_id); + >::insert(thread_id, new_thread); + ThreadCount::put(next_thread_count_value); + >::put(current_thread_counter); + Self::deposit_event(RawEvent::ThreadCreated(thread_id, thread_author_id)); + + Ok(thread_id) + } + + // returns incremented thread counter if last thread author equals with provided parameter + fn get_updated_thread_counter(author_id: MemberId) -> ThreadCounter> { + // if thread counter exists + if let Some(last_thread_author_counter) = Self::last_thread_author_counter() { + // if last(previous) author is the same as current author + if last_thread_author_counter.author_id == author_id { + return last_thread_author_counter.increment(); + } + } + + // else return new counter (set with 1 thread number) + ThreadCounter::new(author_id) + } + + /// Ensures thread can be created. + /// Checks: + /// - title is valid + /// - max thread in a row by the same author + pub fn ensure_can_create_thread( + title: &[u8], + thread_author_id: MemberId, + ) -> DispatchResult { + ensure!(!title.is_empty(), Error::EmptyTitleProvided); + ensure!( + title.len() as u32 <= T::ThreadTitleLengthLimit::get(), + Error::TitleIsTooLong + ); + + // get new 'threads in a row' counter for the author + let current_thread_counter = Self::get_updated_thread_counter(thread_author_id.clone()); + + ensure!( + current_thread_counter.counter as u32 <= T::MaxThreadInARowNumber::get(), + Error::MaxThreadInARowLimitExceeded + ); + + Ok(()) + } +} diff --git a/modules/proposals/discussion/src/tests/mock.rs b/runtime-modules/proposals/discussion/src/tests/mock.rs similarity index 52% rename from modules/proposals/discussion/src/tests/mock.rs rename to runtime-modules/proposals/discussion/src/tests/mock.rs index 6b9980510f..347d43a892 100644 --- a/modules/proposals/discussion/src/tests/mock.rs +++ b/runtime-modules/proposals/discussion/src/tests/mock.rs @@ -3,14 +3,15 @@ pub use system; pub use primitives::{Blake2Hasher, H256}; -pub use runtime_primitives::{ +pub use sr_primitives::{ testing::{Digest, DigestItem, Header, UintAuthorityId}, traits::{BlakeTwo256, Convert, IdentityLookup, OnFinalize}, weights::Weight, BuildStorage, Perbill, }; -use srml_support::{impl_outer_origin, parameter_types}; +use crate::ActorOriginValidator; +use srml_support::{impl_outer_event, impl_outer_origin, parameter_types}; impl_outer_origin! { pub enum Origin for Test {} @@ -30,33 +31,95 @@ parameter_types! { parameter_types! { pub const MaxPostEditionNumber: u32 = 5; + pub const MaxThreadInARowNumber: u32 = 3; pub const ThreadTitleLengthLimit: u32 = 200; pub const PostLengthLimit: u32 = 2000; } +mod discussion { + pub use crate::Event; +} + +mod membership_mod { + pub use membership::members::Event; +} + +impl_outer_event! { + pub enum TestEvent for Test { + discussion, + balances, + membership_mod, + } +} + +parameter_types! { + pub const ExistentialDeposit: u32 = 0; + pub const TransferFee: u32 = 0; + pub const CreationFee: u32 = 0; +} + +impl balances::Trait for Test { + type Balance = u64; + /// What to do if an account's free balance gets zeroed. + type OnFreeBalanceZero = (); + type OnNewAccount = (); + type TransferPayment = (); + type DustRemoval = (); + type Event = TestEvent; + type ExistentialDeposit = ExistentialDeposit; + type TransferFee = TransferFee; + type CreationFee = CreationFee; +} + +impl common::currency::GovernanceCurrency for Test { + type Currency = balances::Module; +} + +impl membership::members::Trait for Test { + type Event = TestEvent; + type MemberId = u64; + type PaidTermId = u64; + type SubscriptionId = u64; + type ActorId = u64; + type InitialMembersBalance = (); +} + impl crate::Trait for Test { - type ThreadAuthorOrigin = system::EnsureSigned; - type PostAuthorOrigin = system::EnsureSigned; + type Event = TestEvent; + type PostAuthorOriginValidator = (); type ThreadId = u32; type PostId = u32; - type ThreadAuthorId = u64; - type PostAuthorId = u64; type MaxPostEditionNumber = MaxPostEditionNumber; type ThreadTitleLengthLimit = ThreadTitleLengthLimit; type PostLengthLimit = PostLengthLimit; + type MaxThreadInARowNumber = MaxThreadInARowNumber; +} + +impl ActorOriginValidator for () { + fn ensure_actor_origin(origin: Origin, actor_id: u64) -> Result { + if system::ensure_none(origin).is_ok() { + return Ok(1); + } + + if actor_id == 1 { + return Ok(1); + } + + Err("Invalid author") + } } impl system::Trait for Test { type Origin = Origin; + type Call = (); type Index = u64; type BlockNumber = u64; - type Call = (); type Hash = H256; type Hashing = BlakeTwo256; type AccountId = u64; type Lookup = IdentityLookup; type Header = Header; - type Event = (); + type Event = TestEvent; type BlockHashCount = BlockHashCount; type MaximumBlockWeight = MaximumBlockWeight; type MaximumBlockLength = MaximumBlockLength; @@ -79,3 +142,4 @@ pub fn initial_test_ext() -> runtime_io::TestExternalities { } pub type Discussions = crate::Module; +pub type System = system::Module; diff --git a/modules/proposals/discussion/src/tests/mod.rs b/runtime-modules/proposals/discussion/src/tests/mod.rs similarity index 75% rename from modules/proposals/discussion/src/tests/mod.rs rename to runtime-modules/proposals/discussion/src/tests/mod.rs index c9b72701fc..a2f51c458a 100644 --- a/modules/proposals/discussion/src/tests/mod.rs +++ b/runtime-modules/proposals/discussion/src/tests/mod.rs @@ -4,6 +4,23 @@ use mock::*; use crate::*; use system::RawOrigin; +use system::{EventRecord, Phase}; + +struct EventFixture; +impl EventFixture { + fn assert_events(expected_raw_events: Vec>) { + let expected_events = expected_raw_events + .iter() + .map(|ev| EventRecord { + phase: Phase::ApplyExtrinsic(0), + event: TestEvent::discussion(ev.clone()), + topics: vec![], + }) + .collect::>>(); + + assert_eq!(System::events(), expected_events); + } +} struct TestPostEntry { pub post_id: u32, @@ -46,6 +63,7 @@ fn assert_thread_content(thread_entry: TestThreadEntry, post_entries: Vec, pub origin: RawOrigin, + pub author_id: u64, } impl Default for DiscussionFixture { @@ -53,6 +71,7 @@ impl Default for DiscussionFixture { DiscussionFixture { title: b"title".to_vec(), origin: RawOrigin::Signed(1), + author_id: 1, } } } @@ -61,6 +80,15 @@ impl DiscussionFixture { fn with_title(self, title: Vec) -> Self { DiscussionFixture { title, ..self } } + + fn create_discussion_and_assert(&self, result: Result) -> Option { + let create_discussion_result = + Discussions::create_thread(self.author_id, self.title.clone()); + + assert_eq!(create_discussion_result, result); + + create_discussion_result.ok() + } } struct PostFixture { @@ -68,12 +96,14 @@ struct PostFixture { pub origin: RawOrigin, pub thread_id: u32, pub post_id: Option, + pub author_id: u64, } impl PostFixture { fn default_for_thread(thread_id: u32) -> Self { PostFixture { text: b"text".to_vec(), + author_id: 1, thread_id, origin: RawOrigin::Signed(1), post_id: None, @@ -88,6 +118,10 @@ impl PostFixture { PostFixture { origin, ..self } } + fn with_author(self, author_id: u64) -> Self { + PostFixture { author_id, ..self } + } + fn change_thread_id(self, thread_id: u32) -> Self { PostFixture { thread_id, ..self } } @@ -99,9 +133,10 @@ impl PostFixture { } } - fn add_post_and_assert(&mut self, result: Result<(), &'static str>) -> Option { + fn add_post_and_assert(&mut self, result: Result<(), Error>) -> Option { let add_post_result = Discussions::add_post( self.origin.clone().into(), + self.author_id, self.thread_id, self.text.clone(), ); @@ -115,13 +150,10 @@ impl PostFixture { self.post_id } - fn update_post_with_text_and_assert( - &mut self, - new_text: Vec, - result: Result<(), &'static str>, - ) { + fn update_post_with_text_and_assert(&mut self, new_text: Vec, result: Result<(), Error>) { let add_post_result = Discussions::update_post( self.origin.clone().into(), + self.author_id, self.thread_id, self.post_id.unwrap(), new_text, @@ -130,22 +162,11 @@ impl PostFixture { assert_eq!(add_post_result, result); } - fn update_post_and_assert(&mut self, result: Result<(), &'static str>) { + fn update_post_and_assert(&mut self, result: Result<(), Error>) { self.update_post_with_text_and_assert(self.text.clone(), result); } } -impl DiscussionFixture { - fn create_discussion_and_assert(&self, result: Result) -> Option { - let create_discussion_result = - Discussions::create_thread(self.origin.clone().into(), self.title.clone()); - - assert_eq!(create_discussion_result, result); - - create_discussion_result.ok() - } -} - #[test] fn create_discussion_call_succeeds() { initial_test_ext().execute_with(|| { @@ -183,6 +204,12 @@ fn update_post_call_succeeds() { post_fixture.add_post_and_assert(Ok(())); post_fixture.update_post_and_assert(Ok(())); + + EventFixture::assert_events(vec![ + RawEvent::ThreadCreated(1, 1), + RawEvent::PostCreated(1, 1), + RawEvent::PostUpdated(1, 1), + ]); }); } @@ -203,7 +230,7 @@ fn update_post_call_failes_because_of_post_edition_limit() { post_fixture.update_post_and_assert(Ok(())); } - post_fixture.update_post_and_assert(Err(MSG_POST_EDITION_NUMBER_EXCEEDED)); + post_fixture.update_post_and_assert(Err(Error::PostEditionNumberExceeded)); }); } @@ -220,9 +247,13 @@ fn update_post_call_failes_because_of_the_wrong_author() { post_fixture.add_post_and_assert(Ok(())); - post_fixture = post_fixture.with_origin(RawOrigin::Signed(2)); + post_fixture = post_fixture.with_author(2); + + post_fixture.update_post_and_assert(Err(Error::Other("Invalid author"))); + + post_fixture = post_fixture.with_origin(RawOrigin::None).with_author(2); - post_fixture.update_post_and_assert(Err(MSG_NOT_AUTHOR)); + post_fixture.update_post_and_assert(Err(Error::NotAuthor)); }); } @@ -267,10 +298,10 @@ fn thread_content_check_succeeded() { fn create_discussion_call_with_bad_title_failed() { initial_test_ext().execute_with(|| { let mut discussion_fixture = DiscussionFixture::default().with_title(Vec::new()); - discussion_fixture.create_discussion_and_assert(Err(crate::MSG_EMPTY_TITLE_PROVIDED)); + discussion_fixture.create_discussion_and_assert(Err(Error::EmptyTitleProvided)); discussion_fixture = DiscussionFixture::default().with_title([0; 201].to_vec()); - discussion_fixture.create_discussion_and_assert(Err(crate::MSG_TOO_LONG_TITLE)); + discussion_fixture.create_discussion_and_assert(Err(Error::TitleIsTooLong)); }); } @@ -283,7 +314,7 @@ fn add_post_call_with_invalid_thread_failed() { .unwrap(); let mut post_fixture = PostFixture::default_for_thread(2); - post_fixture.add_post_and_assert(Err(MSG_THREAD_DOESNT_EXIST)); + post_fixture.add_post_and_assert(Err(Error::ThreadDoesntExist)); }); } @@ -299,7 +330,7 @@ fn update_post_call_with_invalid_post_failed() { post_fixture1.add_post_and_assert(Ok(())).unwrap(); let mut post_fixture2 = post_fixture1.change_post_id(2); - post_fixture2.update_post_and_assert(Err(MSG_POST_DOESNT_EXIST)); + post_fixture2.update_post_and_assert(Err(Error::PostDoesntExist)); }); } @@ -315,7 +346,7 @@ fn update_post_call_with_invalid_thread_failed() { post_fixture1.add_post_and_assert(Ok(())).unwrap(); let mut post_fixture2 = post_fixture1.change_thread_id(2); - post_fixture2.update_post_and_assert(Err(MSG_THREAD_DOESNT_EXIST)); + post_fixture2.update_post_and_assert(Err(Error::ThreadDoesntExist)); }); } @@ -328,11 +359,11 @@ fn add_post_call_with_invalid_text_failed() { .unwrap(); let mut post_fixture1 = PostFixture::default_for_thread(thread_id).with_text(Vec::new()); - post_fixture1.add_post_and_assert(Err(MSG_EMPTY_POST_PROVIDED)); + post_fixture1.add_post_and_assert(Err(Error::EmptyPostProvided)); let mut post_fixture2 = PostFixture::default_for_thread(thread_id).with_text([0; 2001].to_vec()); - post_fixture2.add_post_and_assert(Err(MSG_TOO_LONG_POST)); + post_fixture2.add_post_and_assert(Err(Error::PostIsTooLong)); }); } @@ -348,9 +379,23 @@ fn update_post_call_with_invalid_text_failed() { post_fixture1.add_post_and_assert(Ok(())); let mut post_fixture2 = post_fixture1.with_text(Vec::new()); - post_fixture2.update_post_and_assert(Err(MSG_EMPTY_POST_PROVIDED)); + post_fixture2.update_post_and_assert(Err(Error::EmptyPostProvided)); let mut post_fixture3 = post_fixture2.with_text([0; 2001].to_vec()); - post_fixture3.update_post_and_assert(Err(MSG_TOO_LONG_POST)); + post_fixture3.update_post_and_assert(Err(Error::PostIsTooLong)); + }); +} + +#[test] +fn add_discussion_thread_fails_because_of_max_thread_by_same_author_in_a_row_limit_exceeded() { + initial_test_ext().execute_with(|| { + let discussion_fixture = DiscussionFixture::default(); + for idx in 1..=3 { + discussion_fixture + .create_discussion_and_assert(Ok(idx)) + .unwrap(); + } + + discussion_fixture.create_discussion_and_assert(Err(Error::MaxThreadInARowLimitExceeded)); }); } diff --git a/modules/proposals/discussion/src/types.rs b/runtime-modules/proposals/discussion/src/types.rs similarity index 58% rename from modules/proposals/discussion/src/types.rs rename to runtime-modules/proposals/discussion/src/types.rs index 50a2d4f049..27e20d9fcc 100644 --- a/modules/proposals/discussion/src/types.rs +++ b/runtime-modules/proposals/discussion/src/types.rs @@ -1,8 +1,8 @@ -use rstd::prelude::*; +use codec::{Decode, Encode}; #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; -use codec::{Decode, Encode}; +use rstd::prelude::*; /// Represents a discussion thread #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] @@ -40,3 +40,32 @@ pub struct Post { /// Defines how many times this post was edited. Zero on creation. pub edition_number: u32, } + +/// Post for the discussion thread +#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))] +#[derive(Encode, Decode, Default, Clone, Copy, PartialEq, Eq)] +pub struct ThreadCounter { + /// Author of the threads. + pub author_id: ThreadAuthorId, + + /// ThreadCount + pub counter: u32, +} + +impl ThreadCounter { + /// Increments existing counter + pub fn increment(&self) -> Self { + ThreadCounter { + counter: self.counter + 1, + author_id: self.author_id.clone(), + } + } + + /// Creates new counter by author_id. Counter instantiated with 1. + pub fn new(author_id: ThreadAuthorId) -> Self { + ThreadCounter { + author_id, + counter: 1, + } + } +} diff --git a/modules/proposals/engine/Cargo.toml b/runtime-modules/proposals/engine/Cargo.toml similarity index 70% rename from modules/proposals/engine/Cargo.toml rename to runtime-modules/proposals/engine/Cargo.toml index 130220b780..19f3027feb 100644 --- a/modules/proposals/engine/Cargo.toml +++ b/runtime-modules/proposals/engine/Cargo.toml @@ -12,12 +12,15 @@ std = [ 'rstd/std', 'srml-support/std', 'primitives/std', - 'runtime-primitives/std', 'system/std', 'timestamp/std', 'serde', 'stake/std', 'balances/std', + 'sr-primitives/std', + 'membership/std', + 'common/std', + ] @@ -40,49 +43,58 @@ version = '1.0.0' default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'substrate-primitives' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' [dependencies.rstd] default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'sr-std' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' - -[dependencies.runtime-primitives] -default_features = false -git = 'https://github.com/paritytech/substrate.git' -package = 'sr-primitives' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' [dependencies.srml-support] default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'srml-support' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' [dependencies.system] default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'srml-system' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' [dependencies.timestamp] default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'srml-timestamp' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' [dependencies.balances] package = 'srml-balances' default-features = false git = 'https://github.com/paritytech/substrate.git' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' + +[dependencies.sr-primitives] +default_features = false +git = 'https://github.com/paritytech/substrate.git' +package = 'sr-primitives' +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' [dependencies.stake] default_features = false -git = 'https://github.com/joystream/substrate-stake-module' package = 'substrate-stake-module' -rev = '0516efe9230da112bc095e28f34a3715c2e03ca8' +path = '../../stake' + +[dependencies.membership] +default_features = false +package = 'substrate-membership-module' +path = '../../membership' + +[dependencies.common] +default_features = false +package = 'substrate-common-module' +path = '../../common' [dev-dependencies] mockall = "0.6.0" @@ -91,6 +103,4 @@ mockall = "0.6.0" default_features = false git = 'https://github.com/paritytech/substrate.git' package = 'sr-io' -rev = '0e3001a1ad6fa3d1ba7da7342a8d0d3b3facb2f3' - - +rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8' diff --git a/modules/proposals/engine/src/errors.rs b/runtime-modules/proposals/engine/src/errors.rs similarity index 81% rename from modules/proposals/engine/src/errors.rs rename to runtime-modules/proposals/engine/src/errors.rs index 4fab05342d..35e45c8a66 100644 --- a/modules/proposals/engine/src/errors.rs +++ b/runtime-modules/proposals/engine/src/errors.rs @@ -12,7 +12,3 @@ pub const MSG_STAKE_SHOULD_BE_EMPTY: &str = "Stake should be empty for this prop pub const MSG_STAKE_DIFFERS_FROM_REQUIRED: &str = "Stake differs from the proposal requirements"; pub const MSG_INVALID_PARAMETER_APPROVAL_THRESHOLD: &str = "Approval threshold cannot be zero"; pub const MSG_INVALID_PARAMETER_SLASHING_THRESHOLD: &str = "Slashing threshold cannot be zero"; - -//pub const MSG_STAKE_IS_GREATER_THAN_BALANCE: &str = "Balance is too low to be staked"; -//pub const MSG_ONLY_MEMBERS_CAN_PROPOSE: &str = "Only members can make a proposal"; -//pub const MSG_ONLY_COUNCILORS_CAN_VOTE: &str = "Only councilors can vote on proposals"; diff --git a/modules/proposals/engine/src/lib.rs b/runtime-modules/proposals/engine/src/lib.rs similarity index 55% rename from modules/proposals/engine/src/lib.rs rename to runtime-modules/proposals/engine/src/lib.rs index 50bd37667d..d9bceef0e0 100644 --- a/modules/proposals/engine/src/lib.rs +++ b/runtime-modules/proposals/engine/src/lib.rs @@ -20,60 +20,63 @@ // TODO: Test module after the https://github.com/Joystream/substrate-runtime-joystream/issues/161 // issue will be fixed: "Fix stake module and allow slashing and unstaking in the same block." // TODO: Test cancellation, rejection fees +// TODO: Test StakingEventHandler +// TODO: Test refund_proposal_stake() -pub use types::BalanceOf; use types::FinalizedProposalData; use types::ProposalStakeManager; -pub use types::VotingResults; pub use types::{ - ApprovedProposalStatus, FinalizationData, Proposal, ProposalDecisionStatus, ProposalParameters, - ProposalStatus, + ActiveStake, ApprovedProposalStatus, FinalizationData, Proposal, ProposalDecisionStatus, + ProposalParameters, ProposalStatus, VotingResults, }; +pub use types::{BalanceOf, CurrencyOf, NegativeImbalance}; pub use types::{DefaultStakeHandlerProvider, StakeHandler, StakeHandlerProvider}; pub use types::{ProposalCodeDecoder, ProposalExecutable}; pub use types::{VoteKind, VotersParameters}; -mod errors; pub(crate) mod types; #[cfg(test)] mod tests; +use codec::Decode; use rstd::prelude::*; - -use runtime_primitives::traits::{EnsureOrigin, Zero}; -use srml_support::traits::Get; +use sr_primitives::traits::{DispatchResult, Zero}; +use srml_support::traits::{Currency, Get}; use srml_support::{ - decl_event, decl_module, decl_storage, dispatch, ensure, Parameter, StorageDoubleMap, + decl_error, decl_event, decl_module, decl_storage, ensure, print, Parameter, StorageDoubleMap, }; -use system::ensure_root; +use system::{ensure_root, RawOrigin}; + +use crate::types::ApprovedProposalData; +use common::origin_validator::ActorOriginValidator; +use srml_support::dispatch::Dispatchable; + +type MemberId = ::MemberId; /// Proposals engine trait. -pub trait Trait: system::Trait + timestamp::Trait + stake::Trait { +pub trait Trait: + system::Trait + timestamp::Trait + stake::Trait + membership::members::Trait +{ /// Engine event type. type Event: From> + Into<::Event>; - /// Origin from which proposals must come. - type ProposalOrigin: EnsureOrigin; + /// Validates proposer id and origin combination + type ProposerOriginValidator: ActorOriginValidator< + Self::Origin, + MemberId, + Self::AccountId, + >; - /// Origin from which votes must come. - type VoteOrigin: EnsureOrigin; + /// Validates voter id and origin combination + type VoterOriginValidator: ActorOriginValidator, Self::AccountId>; /// Provides data for voting. Defines maximum voters count for the proposal. type TotalVotersCounter: VotersParameters; - /// Converts proposal code binary to executable representation - type ProposalCodeDecoder: ProposalCodeDecoder; - /// Proposal Id type type ProposalId: From + Parameter + Default + Copy; - /// Type for the proposer id. Should be authenticated by account id. - type ProposerId: From + Parameter + Default; - - /// Type for the voter id. Should be authenticated by account id. - type VoterId: From + Parameter + Default + Clone; - /// Provides stake logic implementation. Can be used to mock stake logic. type StakeHandlerProvider: StakeHandlerProvider; @@ -91,6 +94,9 @@ pub trait Trait: system::Trait + timestamp::Trait + stake::Trait { /// Defines max simultaneous active proposals number. type MaxActiveProposalLimit: Get; + + /// Proposals executable code. Can be instantiated by external module Call enum members. + type DispatchableCallCode: Parameter + Dispatchable + Default; } decl_event!( @@ -98,42 +104,102 @@ decl_event!( pub enum Event where ::ProposalId, - ::ProposerId, - ::VoterId, + MemberId = MemberId, ::BlockNumber, + ::AccountId, + ::StakeId, { /// Emits on proposal creation. /// Params: - /// - Account id of a proposer. + /// - Member id of a proposer. /// - Id of a newly created proposal after it was saved in storage. - ProposalCreated(ProposerId, ProposalId), + ProposalCreated(MemberId, ProposalId), /// Emits on proposal status change. /// Params: /// - Id of a updated proposal. /// - New proposal status - ProposalStatusUpdated(ProposalId, ProposalStatus), + ProposalStatusUpdated(ProposalId, ProposalStatus), /// Emits on voting for the proposal /// Params: - /// - Voter - an account id of a voter. + /// - Voter - member id of a voter. /// - Id of a proposal. /// - Kind of vote. - Voted(VoterId, ProposalId, VoteKind), + Voted(MemberId, ProposalId, VoteKind), } ); +decl_error! { + pub enum Error { + /// Proposal cannot have an empty title" + EmptyTitleProvided, + + /// Proposal cannot have an empty body + EmptyDescriptionProvided, + + /// Title is too long + TitleIsTooLong, + + /// Description is too long + DescriptionIsTooLong, + + /// The proposal does not exist + ProposalNotFound, + + /// Proposal is finalized already + ProposalFinalized, + + /// The proposal have been already voted on + AlreadyVoted, + + /// Not an author + NotAuthor, + + /// Max active proposals number exceeded + MaxActiveProposalNumberExceeded, + + /// Stake cannot be empty with this proposal + EmptyStake, + + /// Stake should be empty for this proposal + StakeShouldBeEmpty, + + /// Stake differs from the proposal requirements + StakeDiffersFromRequired, + + /// Approval threshold cannot be zero + InvalidParameterApprovalThreshold, + + /// Slashing threshold cannot be zero + InvalidParameterSlashingThreshold, + + /// Require root origin in extrinsics + RequireRootOrigin, + } +} + +impl From for Error { + fn from(error: system::Error) -> Self { + match error { + system::Error::Other(msg) => Error::Other(msg), + system::Error::RequireRootOrigin => Error::RequireRootOrigin, + _ => Error::Other(error.into()), + } + } +} + // Storage for the proposals engine module decl_storage! { pub trait Store for Module as ProposalEngine{ /// Map proposal by its id. - pub Proposals get(fn proposals): map T::ProposalId => ProposalObject; + pub Proposals get(fn proposals): map T::ProposalId => ProposalOf; /// Count of all proposals that have been created. pub ProposalCount get(fn proposal_count): u32; /// Map proposal executable code by proposal id. - pub ProposalCode get(fn proposal_codes): map T::ProposalId => Vec; + pub DispatchableCallCode get(fn proposal_codes): map T::ProposalId => Vec; /// Count of active proposals. pub ActiveProposalCount get(fn active_proposal_count): u32; @@ -146,33 +212,40 @@ decl_storage! { /// Double map for preventing duplicate votes. Should be cleaned after usage. pub VoteExistsByProposalByVoter get(fn vote_by_proposal_by_voter): - double_map T::ProposalId, twox_256(T::VoterId) => VoteKind; + double_map T::ProposalId, twox_256(MemberId) => VoteKind; + + /// Map proposal id by stake id. Required by StakingEventsHandler callback call + pub StakesProposals get(fn stakes_proposals): map T::StakeId => T::ProposalId; } } decl_module! { /// 'Proposal engine' substrate module pub struct Module for enum Call where origin: T::Origin { + /// Predefined errors + type Error = Error; /// Emits an event. Default substrate implementation. fn deposit_event() = default; /// Vote extrinsic. Conditions: origin must allow votes. - pub fn vote(origin, proposal_id: T::ProposalId, vote: VoteKind) { - let account_id = T::VoteOrigin::ensure_origin(origin)?; - let voter_id = T::VoterId::from(account_id); + pub fn vote(origin, voter_id: MemberId, proposal_id: T::ProposalId, vote: VoteKind) { + T::VoterOriginValidator::ensure_actor_origin( + origin, + voter_id.clone(), + )?; - ensure!(>::exists(proposal_id), errors::MSG_PROPOSAL_NOT_FOUND); + ensure!(>::exists(proposal_id), Error::ProposalNotFound); let mut proposal = Self::proposals(proposal_id); - ensure!(proposal.status == ProposalStatus::Active, errors::MSG_PROPOSAL_FINALIZED); + ensure!(matches!(proposal.status, ProposalStatus::Active{..}), Error::ProposalFinalized); let did_not_vote_before = !>::exists( proposal_id, voter_id.clone(), ); - ensure!(did_not_vote_before, errors::MSG_YOU_ALREADY_VOTED); + ensure!(did_not_vote_before, Error::AlreadyVoted); proposal.voting_results.add_vote(vote.clone()); @@ -184,15 +257,17 @@ decl_module! { } /// Cancel a proposal by its original proposer. - pub fn cancel_proposal(origin, proposal_id: T::ProposalId) { - let account_id = T::ProposalOrigin::ensure_origin(origin)?; - let proposer_id = T::ProposerId::from(account_id); + pub fn cancel_proposal(origin, proposer_id: MemberId, proposal_id: T::ProposalId) { + T::ProposerOriginValidator::ensure_actor_origin( + origin, + proposer_id.clone(), + )?; - ensure!(>::exists(proposal_id), errors::MSG_PROPOSAL_NOT_FOUND); + ensure!(>::exists(proposal_id), Error::ProposalNotFound); let proposal = Self::proposals(proposal_id); - ensure!(proposer_id == proposal.proposer_id, errors::MSG_YOU_DONT_OWN_THIS_PROPOSAL); - ensure!(proposal.status == ProposalStatus::Active, errors::MSG_PROPOSAL_FINALIZED); + ensure!(proposer_id == proposal.proposer_id, Error::NotAuthor); + ensure!(matches!(proposal.status, ProposalStatus::Active{..}), Error::ProposalFinalized); // mutation @@ -203,10 +278,10 @@ decl_module! { pub fn veto_proposal(origin, proposal_id: T::ProposalId) { ensure_root(origin)?; - ensure!(>::exists(proposal_id), errors::MSG_PROPOSAL_NOT_FOUND); + ensure!(>::exists(proposal_id), Error::ProposalNotFound); let proposal = Self::proposals(proposal_id); - ensure!(proposal.status == ProposalStatus::Active, errors::MSG_PROPOSAL_FINALIZED); + ensure!(matches!(proposal.status, ProposalStatus::Active{..}), Error::ProposalFinalized); // mutation @@ -227,12 +302,12 @@ decl_module! { Self::finalize_proposal(proposal_data.proposal_id, proposal_data.status); } - let executable_proposal_ids = - Self::get_approved_proposal_with_expired_grace_period_ids(); + let executable_proposals = + Self::get_approved_proposal_with_expired_grace_period(); // Execute approved proposals with expired grace period - for proposal_id in executable_proposal_ids { - Self::execute_proposal(proposal_id); + for approved_proosal in executable_proposals { + Self::execute_proposal(approved_proosal); } } } @@ -241,17 +316,14 @@ decl_module! { impl Module { /// Create proposal. Requires 'proposal origin' membership. pub fn create_proposal( - origin: T::Origin, + account_id: T::AccountId, + proposer_id: MemberId, parameters: ProposalParameters>, title: Vec, description: Vec, stake_balance: Option>, - proposal_type: u32, - proposal_code: Vec, - ) -> Result { - let account_id = T::ProposalOrigin::ensure_origin(origin)?; - let proposer_id = T::ProposerId::from(account_id.clone()); - + encoded_dispatchable_call_code: Vec, + ) -> Result { Self::ensure_create_proposal_parameters_are_valid( ¶meters, &title, @@ -264,28 +336,38 @@ impl Module { let next_proposal_count_value = Self::proposal_count() + 1; let new_proposal_id = next_proposal_count_value; + let proposal_id = T::ProposalId::from(new_proposal_id); // Check stake_balance for value and create stake if value exists, else take None // If create_stake() returns error - return error from extrinsic - let stake_id = stake_balance - .map(|stake_amount| ProposalStakeManager::::create_stake(stake_amount, account_id)) + let stake_id_result = stake_balance + .map(|stake_amount| { + ProposalStakeManager::::create_stake(stake_amount, account_id.clone()) + }) .transpose()?; + let mut stake_data = None; + if let Some(stake_id) = stake_id_result { + stake_data = Some(ActiveStake { + stake_id, + source_account_id: account_id, + }); + + >::insert(stake_id, proposal_id); + } + let new_proposal = Proposal { created_at: Self::current_block(), parameters, title, description, proposer_id: proposer_id.clone(), - proposal_type, - status: ProposalStatus::Active, + status: ProposalStatus::Active(stake_data), voting_results: VotingResults::default(), - stake_id, }; - let proposal_id = T::ProposalId::from(new_proposal_id); >::insert(proposal_id, new_proposal); - >::insert(proposal_id, proposal_code); + >::insert(proposal_id, encoded_dispatchable_call_code); >::insert(proposal_id, ()); ProposalCount::put(next_proposal_count_value); Self::increase_active_proposal_counter(); @@ -294,6 +376,87 @@ impl Module { Ok(proposal_id) } + + /// Performs all checks for the proposal creation: + /// - title, body lengths + /// - mac active proposal + /// - provided parameters: approval_threshold_percentage and slashing_threshold_percentage > 0 + /// - provided stake balance and parameters.required_stake are valid + pub fn ensure_create_proposal_parameters_are_valid( + parameters: &ProposalParameters>, + title: &[u8], + description: &[u8], + stake_balance: Option>, + ) -> DispatchResult { + ensure!(!title.is_empty(), Error::EmptyTitleProvided); + ensure!( + title.len() as u32 <= T::TitleMaxLength::get(), + Error::TitleIsTooLong + ); + + ensure!(!description.is_empty(), Error::EmptyDescriptionProvided); + ensure!( + description.len() as u32 <= T::DescriptionMaxLength::get(), + Error::DescriptionIsTooLong + ); + + ensure!( + (Self::active_proposal_count()) < T::MaxActiveProposalLimit::get(), + Error::MaxActiveProposalNumberExceeded + ); + + ensure!( + parameters.approval_threshold_percentage > 0, + Error::InvalidParameterApprovalThreshold + ); + + ensure!( + parameters.slashing_threshold_percentage > 0, + Error::InvalidParameterSlashingThreshold + ); + + // check stake parameters + if let Some(required_stake) = parameters.required_stake { + if let Some(staked_balance) = stake_balance { + ensure!( + required_stake == staked_balance, + Error::StakeDiffersFromRequired + ); + } else { + return Err(Error::EmptyStake); + } + } + + if stake_balance.is_some() && parameters.required_stake.is_none() { + return Err(Error::StakeShouldBeEmpty); + } + + Ok(()) + } + + //TODO: candidate for invariant break or error saving to the state + /// Callback from StakingEventsHandler. Refunds unstaked imbalance back to the source account + pub fn refund_proposal_stake(stake_id: T::StakeId, imbalance: NegativeImbalance) { + if >::exists(stake_id) { + //TODO: handle non existence + + let proposal_id = Self::stakes_proposals(stake_id); + + if >::exists(proposal_id) { + let proposal = Self::proposals(proposal_id); + + if let ProposalStatus::Active(active_stake_result) = proposal.status { + if let Some(active_stake) = active_stake_result { + //TODO: handle the result + let _ = CurrencyOf::::resolve_into_existing( + &active_stake.source_account_id, + imbalance, + ); + } + } + } + } + } } impl Module { @@ -331,42 +494,38 @@ impl Module { } // Executes approved proposal code - fn execute_proposal(proposal_id: T::ProposalId) { - let mut proposal = Self::proposals(proposal_id); + fn execute_proposal(approved_proposal: ApprovedProposal) { + let proposal_code = Self::proposal_codes(approved_proposal.proposal_id); - // Execute only proposals with correct status - if let ProposalStatus::Finalized(finalized_status) = proposal.status.clone() { - let proposal_code = Self::proposal_codes(proposal_id); + let proposal_code_result = T::DispatchableCallCode::decode(&mut &proposal_code[..]); - let proposal_code_result = - T::ProposalCodeDecoder::decode_proposal(proposal.proposal_type, proposal_code); - - let approved_proposal_status = match proposal_code_result { - Ok(proposal_code) => { - if let Err(error) = proposal_code.execute() { - ApprovedProposalStatus::failed_execution(error) - } else { - ApprovedProposalStatus::Executed - } + let approved_proposal_status = match proposal_code_result { + Ok(proposal_code) => { + if let Err(error) = proposal_code.dispatch(T::Origin::from(RawOrigin::Root)) { + ApprovedProposalStatus::failed_execution( + error.into().message.unwrap_or("Dispatch error"), + ) + } else { + ApprovedProposalStatus::Executed } - Err(error) => ApprovedProposalStatus::failed_execution(error), - }; + } + Err(error) => ApprovedProposalStatus::failed_execution(error.what()), + }; - let proposal_execution_status = - finalized_status.create_approved_proposal_status(approved_proposal_status); + let proposal_execution_status = approved_proposal + .finalisation_status_data + .create_approved_proposal_status(approved_proposal_status); - proposal.status = proposal_execution_status.clone(); - >::insert(proposal_id, proposal); + let mut proposal = approved_proposal.proposal; + proposal.status = proposal_execution_status.clone(); + >::insert(approved_proposal.proposal_id, proposal); - Self::deposit_event(RawEvent::ProposalStatusUpdated( - proposal_id, - proposal_execution_status, - )); - } + Self::deposit_event(RawEvent::ProposalStatusUpdated( + approved_proposal.proposal_id, + proposal_execution_status, + )); - // Remove proposals from the 'pending execution' queue even in case of not finalized status - // to prevent eternal cycles. - >::remove(&proposal_id); + >::remove(&approved_proposal.proposal_id); } // Performs all actions on proposal finalization: @@ -381,43 +540,45 @@ impl Module { let mut proposal = Self::proposals(proposal_id); - if let ProposalDecisionStatus::Approved { .. } = decision_status { - >::insert(proposal_id, ()); - } - - // deal with stakes if necessary - let slash_balance = Self::calculate_slash_balance(&decision_status, &proposal.parameters); - let slash_and_unstake_result = Self::slash_and_unstake(proposal.stake_id, slash_balance); + if let ProposalStatus::Active(active_stake) = proposal.status.clone() { + if let ProposalDecisionStatus::Approved { .. } = decision_status { + >::insert(proposal_id, ()); + } - if slash_and_unstake_result.is_ok() { - proposal.stake_id = None; - } + // deal with stakes if necessary + let slash_balance = + Self::calculate_slash_balance(&decision_status, &proposal.parameters); + let slash_and_unstake_result = + Self::slash_and_unstake(active_stake.clone(), slash_balance); - // create finalized proposal status with error if any - let new_proposal_status = //TODO rename without an error - ProposalStatus::finalized_with_error(decision_status, slash_and_unstake_result.err(), Self::current_block()); + // create finalized proposal status with error if any + let new_proposal_status = //TODO rename without an error + ProposalStatus::finalized_with_error(decision_status, slash_and_unstake_result.err(), active_stake, Self::current_block()); - proposal.status = new_proposal_status.clone(); - >::insert(proposal_id, proposal); + proposal.status = new_proposal_status.clone(); + >::insert(proposal_id, proposal); - Self::deposit_event(RawEvent::ProposalStatusUpdated( - proposal_id, - new_proposal_status, - )); + Self::deposit_event(RawEvent::ProposalStatusUpdated( + proposal_id, + new_proposal_status, + )); + } else { + print("Broken invariant: proposal cannot be non-active during the finalisation"); + } } // Slashes the stake and perform unstake only in case of existing stake fn slash_and_unstake( - current_stake_id: Option, + current_stake_data: Option>, slash_balance: BalanceOf, ) -> Result<(), &'static str> { // only if stake exists - if let Some(stake_id) = current_stake_id { + if let Some(stake_data) = current_stake_data { if !slash_balance.is_zero() { - ProposalStakeManager::::slash(stake_id, slash_balance)?; + ProposalStakeManager::::slash(stake_data.stake_id, slash_balance)?; } - ProposalStakeManager::::remove_stake(stake_id)?; + ProposalStakeManager::::remove_stake(stake_data.stake_id)?; } Ok(()) @@ -444,13 +605,22 @@ impl Module { } // Enumerates approved proposals and checks their grace period expiration - fn get_approved_proposal_with_expired_grace_period_ids() -> Vec { + fn get_approved_proposal_with_expired_grace_period() -> Vec> { >::enumerate() .filter_map(|(proposal_id, _)| { let proposal = Self::proposals(proposal_id); if proposal.is_grace_period_expired(Self::current_block()) { - Some(proposal_id) + // this should be true, because it was tested inside is_grace_period_expired() + if let ProposalStatus::Finalized(finalisation_data) = proposal.status.clone() { + Some(ApprovedProposalData { + proposal_id, + proposal, + finalisation_status_data: finalisation_data, + }) + } else { + None + } } else { None } @@ -473,78 +643,33 @@ impl Module { ActiveProposalCount::put(next_active_proposal_count_value); }; } - - // Performs all checks for the proposal creation: - // - title, body lengths - // - mac active proposal - // - provided parameters: approval_threshold_percentage and slashing_threshold_percentage > 0 - // - provided stake balance and parameters.required_stake are valid - fn ensure_create_proposal_parameters_are_valid( - parameters: &ProposalParameters>, - title: &[u8], - body: &[u8], - stake_balance: Option>, - ) -> dispatch::Result { - ensure!(!title.is_empty(), errors::MSG_EMPTY_TITLE_PROVIDED); - ensure!( - title.len() as u32 <= T::TitleMaxLength::get(), - errors::MSG_TOO_LONG_TITLE - ); - - ensure!(!body.is_empty(), errors::MSG_EMPTY_BODY_PROVIDED); - ensure!( - body.len() as u32 <= T::DescriptionMaxLength::get(), - errors::MSG_TOO_LONG_BODY - ); - - ensure!( - (Self::active_proposal_count()) < T::MaxActiveProposalLimit::get(), - errors::MSG_MAX_ACTIVE_PROPOSAL_NUMBER_EXCEEDED - ); - - ensure!( - parameters.approval_threshold_percentage > 0, - errors::MSG_INVALID_PARAMETER_APPROVAL_THRESHOLD - ); - - ensure!( - parameters.slashing_threshold_percentage > 0, - errors::MSG_INVALID_PARAMETER_SLASHING_THRESHOLD - ); - - // check stake parameters - if let Some(required_stake) = parameters.required_stake { - if let Some(staked_balance) = stake_balance { - ensure!( - required_stake == staked_balance, - errors::MSG_STAKE_DIFFERS_FROM_REQUIRED - ); - } else { - return Err(errors::MSG_STAKE_IS_EMPTY); - } - } - - if stake_balance.is_some() && parameters.required_stake.is_none() { - return Err(errors::MSG_STAKE_SHOULD_BE_EMPTY); - } - - Ok(()) - } } // Simplification of the 'FinalizedProposalData' type type FinalizedProposal = FinalizedProposalData< ::ProposalId, ::BlockNumber, - ::ProposerId, + MemberId, + types::BalanceOf, + ::StakeId, + ::AccountId, +>; + +// Simplification of the 'ApprovedProposalData' type +type ApprovedProposal = ApprovedProposalData< + ::ProposalId, + ::BlockNumber, + MemberId, types::BalanceOf, ::StakeId, + ::AccountId, >; // Simplification of the 'Proposal' type -type ProposalObject = Proposal< +type ProposalOf = Proposal< ::BlockNumber, - ::ProposerId, + MemberId, types::BalanceOf, ::StakeId, + ::AccountId, >; diff --git a/modules/proposals/engine/src/tests/mock/balance_manager.rs b/runtime-modules/proposals/engine/src/tests/mock/balance_manager.rs similarity index 96% rename from modules/proposals/engine/src/tests/mock/balance_manager.rs rename to runtime-modules/proposals/engine/src/tests/mock/balance_manager.rs index b605a98e51..3116ed4ca1 100644 --- a/modules/proposals/engine/src/tests/mock/balance_manager.rs +++ b/runtime-modules/proposals/engine/src/tests/mock/balance_manager.rs @@ -1,6 +1,6 @@ #![cfg(test)] -pub use runtime_primitives::traits::Zero; +pub use sr_primitives::traits::Zero; use srml_support::traits::{Currency, Imbalance}; use super::*; diff --git a/modules/proposals/engine/src/tests/mock/mod.rs b/runtime-modules/proposals/engine/src/tests/mock/mod.rs similarity index 77% rename from modules/proposals/engine/src/tests/mock/mod.rs rename to runtime-modules/proposals/engine/src/tests/mock/mod.rs index 1f9ba495aa..5dd0ac9c60 100644 --- a/modules/proposals/engine/src/tests/mock/mod.rs +++ b/runtime-modules/proposals/engine/src/tests/mock/mod.rs @@ -8,17 +8,17 @@ #![cfg(test)] pub use primitives::{Blake2Hasher, H256}; -pub use runtime_primitives::{ +pub use sr_primitives::{ testing::{Digest, DigestItem, Header, UintAuthorityId}, traits::{BlakeTwo256, Convert, IdentityLookup, OnFinalize, Zero}, weights::Weight, - BuildStorage, Perbill, + BuildStorage, DispatchError, Perbill, }; -use srml_support::{impl_outer_dispatch, impl_outer_event, impl_outer_origin, parameter_types}; +use srml_support::{impl_outer_event, impl_outer_origin, parameter_types}; pub use system; mod balance_manager; -mod proposals; +pub(crate) mod proposals; mod stakes; use balance_manager::*; @@ -33,20 +33,19 @@ impl_outer_origin! { pub enum Origin for Test {} } -impl_outer_dispatch! { - pub enum Call for Test where origin: Origin { - proposals::ProposalsEngine, - } -} - mod engine { pub use crate::Event; } +mod membership_mod { + pub use membership::members::Event; +} + impl_outer_event! { pub enum TestEvent for Test { balances, engine, + membership_mod, } } @@ -64,15 +63,21 @@ impl balances::Trait for Test { /// What to do if a new account is created. type OnNewAccount = (); - type Event = TestEvent; + type TransferPayment = (); type DustRemoval = (); - type TransferPayment = (); + type Event = TestEvent; type ExistentialDeposit = ExistentialDeposit; type TransferFee = TransferFee; type CreationFee = CreationFee; } +impl common::currency::GovernanceCurrency for Test { + type Currency = balances::Module; +} + +impl proposals::Trait for Test {} + impl stake::Trait for Test { type Currency = Balances; type StakePoolId = StakePoolId; @@ -89,34 +94,42 @@ parameter_types! { pub const MaxActiveProposalLimit: u32 = 100; } -impl crate::Trait for Test { +impl membership::members::Trait for Test { type Event = TestEvent; + type MemberId = u64; + type PaidTermId = u64; + type SubscriptionId = u64; + type ActorId = u64; + type InitialMembersBalance = (); +} - type ProposalOrigin = system::EnsureSigned; - - type VoteOrigin = system::EnsureSigned; - +impl crate::Trait for Test { + type Event = TestEvent; + type ProposerOriginValidator = (); + type VoterOriginValidator = (); type TotalVotersCounter = (); - - type ProposalCodeDecoder = ProposalType; - type ProposalId = u32; - - type ProposerId = u64; - - type VoterId = u64; - type StakeHandlerProvider = stakes::TestStakeHandlerProvider; - type CancellationFee = CancellationFee; - type RejectionFee = RejectionFee; - type TitleMaxLength = TitleMaxLength; - type DescriptionMaxLength = DescriptionMaxLength; - type MaxActiveProposalLimit = MaxActiveProposalLimit; + type DispatchableCallCode = proposals::Call; +} + +impl Default for proposals::Call { + fn default() -> Self { + panic!("shouldn't call default for Call"); + } +} + +impl common::origin_validator::ActorOriginValidator for () { + fn ensure_actor_origin(origin: Origin, _account_id: u64) -> Result { + let signed_account_id = system::ensure_signed(origin)?; + + Ok(signed_account_id) + } } // If changing count is required, we can upgrade the implementation as shown here: @@ -138,9 +151,9 @@ parameter_types! { impl system::Trait for Test { type Origin = Origin; + type Call = (); type Index = u64; type BlockNumber = u64; - type Call = (); type Hash = H256; type Hashing = BlakeTwo256; type AccountId = u64; @@ -160,8 +173,6 @@ impl timestamp::Trait for Test { type MinimumPeriod = MinimumPeriod; } -// TODO add a Hook type to capture TriggerElection and CouncilElected hooks - pub fn initial_test_ext() -> runtime_io::TestExternalities { let t = system::GenesisConfig::default() .build_storage::() diff --git a/runtime-modules/proposals/engine/src/tests/mock/proposals.rs b/runtime-modules/proposals/engine/src/tests/mock/proposals.rs new file mode 100644 index 0000000000..b8b8cc6675 --- /dev/null +++ b/runtime-modules/proposals/engine/src/tests/mock/proposals.rs @@ -0,0 +1,18 @@ +//! Contains executable proposal extrinsic mocks + +use rstd::prelude::*; +use rstd::vec::Vec; +use srml_support::decl_module; +pub trait Trait: system::Trait {} + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + /// Working extrinsic test + pub fn dummy_proposal(_origin, _title: Vec, _description: Vec) {} + + /// Broken extrinsic test + pub fn faulty_proposal(_origin, _title: Vec, _description: Vec,) { + Err("ExecutionFailed")? + } + } +} diff --git a/modules/proposals/engine/src/tests/mock/stakes.rs b/runtime-modules/proposals/engine/src/tests/mock/stakes.rs similarity index 100% rename from modules/proposals/engine/src/tests/mock/stakes.rs rename to runtime-modules/proposals/engine/src/tests/mock/stakes.rs diff --git a/modules/proposals/engine/src/tests/mod.rs b/runtime-modules/proposals/engine/src/tests/mod.rs similarity index 89% rename from modules/proposals/engine/src/tests/mod.rs rename to runtime-modules/proposals/engine/src/tests/mod.rs index ac591d3110..bad900a3a0 100644 --- a/modules/proposals/engine/src/tests/mod.rs +++ b/runtime-modules/proposals/engine/src/tests/mod.rs @@ -1,12 +1,12 @@ -mod mock; +pub(crate) mod mock; use crate::*; use mock::*; use codec::Encode; use rstd::rc::Rc; -use runtime_primitives::traits::{OnFinalize, OnInitialize}; -use srml_support::{dispatch, StorageMap, StorageValue}; +use sr_primitives::traits::{DispatchResult, OnFinalize, OnInitialize}; +use srml_support::{StorageMap, StorageValue}; use system::RawOrigin; use system::{EventRecord, Phase}; @@ -58,8 +58,8 @@ impl Default for ProposalParametersFixture { #[derive(Clone)] struct DummyProposalFixture { parameters: ProposalParameters, - origin: RawOrigin, - proposal_type: u32, + account_id: u64, + proposer_id: u64, proposal_code: Vec, title: Vec, description: Vec, @@ -68,10 +68,10 @@ struct DummyProposalFixture { impl Default for DummyProposalFixture { fn default() -> Self { - let dummy_proposal = DummyExecutable { - title: b"title".to_vec(), - description: b"description".to_vec(), - }; + let title = b"title".to_vec(); + let description = b"description".to_vec(); + let dummy_proposal = + mock::proposals::Call::::dummy_proposal(title.clone(), description.clone()); DummyProposalFixture { parameters: ProposalParameters { @@ -83,11 +83,11 @@ impl Default for DummyProposalFixture { grace_period: 0, required_stake: None, }, - origin: RawOrigin::Signed(1), - proposal_type: dummy_proposal.proposal_type(), + account_id: 1, + proposer_id: 1, proposal_code: dummy_proposal.encode(), - title: dummy_proposal.title, - description: dummy_proposal.description, + title, + description, stake_balance: None, } } @@ -106,8 +106,8 @@ impl DummyProposalFixture { DummyProposalFixture { parameters, ..self } } - fn with_origin(self, origin: RawOrigin) -> Self { - DummyProposalFixture { origin, ..self } + fn with_account_id(self, account_id: u64) -> Self { + DummyProposalFixture { account_id, ..self } } fn with_stake(self, stake_balance: BalanceOf) -> Self { @@ -117,22 +117,21 @@ impl DummyProposalFixture { } } - fn with_proposal_type_and_code(self, proposal_type: u32, proposal_code: Vec) -> Self { + fn with_proposal_code(self, proposal_code: Vec) -> Self { DummyProposalFixture { - proposal_type, proposal_code, ..self } } - fn create_proposal_and_assert(self, result: Result) -> Option { + fn create_proposal_and_assert(self, result: Result) -> Option { let proposal_id_result = ProposalsEngine::create_proposal( - self.origin.into(), + self.account_id, + self.proposer_id, self.parameters, self.title, self.description, self.stake_balance, - self.proposal_type, self.proposal_code, ); assert_eq!(proposal_id_result, result); @@ -144,6 +143,7 @@ impl DummyProposalFixture { struct CancelProposalFixture { origin: RawOrigin, proposal_id: u32, + proposer_id: u64, } impl CancelProposalFixture { @@ -151,6 +151,7 @@ impl CancelProposalFixture { CancelProposalFixture { proposal_id, origin: RawOrigin::Signed(1), + proposer_id: 1, } } @@ -158,9 +159,20 @@ impl CancelProposalFixture { CancelProposalFixture { origin, ..self } } - fn cancel_and_assert(self, expected_result: dispatch::Result) { + fn with_proposer(self, proposer_id: u64) -> Self { + CancelProposalFixture { + proposer_id, + ..self + } + } + + fn cancel_and_assert(self, expected_result: DispatchResult) { assert_eq!( - ProposalsEngine::cancel_proposal(self.origin.into(), self.proposal_id,), + ProposalsEngine::cancel_proposal( + self.origin.into(), + self.proposer_id, + self.proposal_id + ), expected_result ); } @@ -182,7 +194,7 @@ impl VetoProposalFixture { VetoProposalFixture { origin, ..self } } - fn veto_and_assert(self, expected_result: dispatch::Result) { + fn veto_and_assert(self, expected_result: DispatchResult) { assert_eq!( ProposalsEngine::veto_proposal(self.origin.into(), self.proposal_id,), expected_result @@ -193,6 +205,7 @@ impl VetoProposalFixture { struct VoteGenerator { proposal_id: u32, current_account_id: u64, + current_voter_id: u64, pub auto_increment_voter_id: bool, } @@ -200,6 +213,7 @@ impl VoteGenerator { fn new(proposal_id: u32) -> Self { VoteGenerator { proposal_id, + current_voter_id: 0, current_account_id: 0, auto_increment_voter_id: true, } @@ -208,17 +222,19 @@ impl VoteGenerator { self.vote_and_assert(vote_kind, Ok(())); } - fn vote_and_assert(&mut self, vote_kind: VoteKind, expected_result: dispatch::Result) { + fn vote_and_assert(&mut self, vote_kind: VoteKind, expected_result: DispatchResult) { assert_eq!(self.vote(vote_kind.clone()), expected_result); } - fn vote(&mut self, vote_kind: VoteKind) -> dispatch::Result { + fn vote(&mut self, vote_kind: VoteKind) -> DispatchResult { if self.auto_increment_voter_id { self.current_account_id += 1; + self.current_voter_id += 1; } ProposalsEngine::vote( system::RawOrigin::Signed(self.current_account_id).into(), + self.current_voter_id, self.proposal_id, vote_kind, ) @@ -227,7 +243,7 @@ impl VoteGenerator { struct EventFixture; impl EventFixture { - fn assert_events(expected_raw_events: Vec>) { + fn assert_events(expected_raw_events: Vec>) { let expected_events = expected_raw_events .iter() .map(|ev| EventRecord { @@ -267,15 +283,6 @@ fn create_dummy_proposal_succeeds() { }); } -#[test] -fn create_dummy_proposal_fails_with_insufficient_rights() { - initial_test_ext().execute_with(|| { - let dummy_proposal = DummyProposalFixture::default().with_origin(RawOrigin::None); - - dummy_proposal.create_proposal_and_assert(Err("Invalid origin")); - }); -} - #[test] fn vote_succeeds() { initial_test_ext().execute_with(|| { @@ -291,8 +298,8 @@ fn vote_succeeds() { fn vote_fails_with_insufficient_rights() { initial_test_ext().execute_with(|| { assert_eq!( - ProposalsEngine::vote(system::RawOrigin::None.into(), 1, VoteKind::Approve), - Err("Invalid origin") + ProposalsEngine::vote(system::RawOrigin::None.into(), 1, 1, VoteKind::Approve), + Err(Error::Other("RequireSignedOrigin")) ); }); } @@ -321,7 +328,6 @@ fn proposal_execution_succeeds() { assert_eq!( proposal, Proposal { - proposal_type: 1, parameters: parameters_fixture.params(), proposer_id: 1, created_at: 1, @@ -334,7 +340,6 @@ fn proposal_execution_succeeds() { rejections: 0, slashes: 0, }, - stake_id: None, } ); @@ -347,11 +352,15 @@ fn proposal_execution_succeeds() { fn proposal_execution_failed() { initial_test_ext().execute_with(|| { let parameters_fixture = ProposalParametersFixture::default(); - let faulty_proposal = FaultyExecutable; + + let faulty_proposal = mock::proposals::Call::::faulty_proposal( + b"title".to_vec(), + b"description".to_vec(), + ); let dummy_proposal = DummyProposalFixture::default() .with_parameters(parameters_fixture.params()) - .with_proposal_type_and_code(faulty_proposal.proposal_type(), faulty_proposal.encode()); + .with_proposal_code(faulty_proposal.encode()); let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); @@ -368,7 +377,6 @@ fn proposal_execution_failed() { assert_eq!( proposal, Proposal { - proposal_type: faulty_proposal.proposal_type(), parameters: parameters_fixture.params(), proposer_id: 1, created_at: 1, @@ -384,7 +392,6 @@ fn proposal_execution_failed() { rejections: 0, slashes: 0, }, - stake_id: None, } ) }); @@ -468,21 +475,21 @@ fn create_proposal_fails_with_invalid_body_or_title() { initial_test_ext().execute_with(|| { let mut dummy_proposal = DummyProposalFixture::default().with_title_and_body(Vec::new(), b"body".to_vec()); - dummy_proposal.create_proposal_and_assert(Err("Proposal cannot have an empty title")); + dummy_proposal.create_proposal_and_assert(Err(Error::EmptyTitleProvided.into())); dummy_proposal = DummyProposalFixture::default().with_title_and_body(b"title".to_vec(), Vec::new()); - dummy_proposal.create_proposal_and_assert(Err("Proposal cannot have an empty body")); + dummy_proposal.create_proposal_and_assert(Err(Error::EmptyDescriptionProvided.into())); let too_long_title = vec![0; 200]; dummy_proposal = DummyProposalFixture::default().with_title_and_body(too_long_title, b"body".to_vec()); - dummy_proposal.create_proposal_and_assert(Err("Title is too long")); + dummy_proposal.create_proposal_and_assert(Err(Error::TitleIsTooLong.into())); let too_long_body = vec![0; 11000]; dummy_proposal = DummyProposalFixture::default().with_title_and_body(b"title".to_vec(), too_long_body); - dummy_proposal.create_proposal_and_assert(Err("Body is too long")); + dummy_proposal.create_proposal_and_assert(Err(Error::DescriptionIsTooLong.into())); }); } @@ -495,7 +502,7 @@ fn vote_fails_with_expired_voting_period() { run_to_block_and_finalize(6); let mut vote_generator = VoteGenerator::new(proposal_id); - vote_generator.vote_and_assert(VoteKind::Approve, Err("Proposal is finalized already")); + vote_generator.vote_and_assert(VoteKind::Approve, Err(Error::ProposalFinalized)); }); } @@ -514,8 +521,7 @@ fn vote_fails_with_not_active_proposal() { run_to_block_and_finalize(2); let mut vote_generator_to_fail = VoteGenerator::new(proposal_id); - vote_generator_to_fail - .vote_and_assert(VoteKind::Approve, Err("Proposal is finalized already")); + vote_generator_to_fail.vote_and_assert(VoteKind::Approve, Err(Error::ProposalFinalized)); }); } @@ -523,7 +529,7 @@ fn vote_fails_with_not_active_proposal() { fn vote_fails_with_absent_proposal() { initial_test_ext().execute_with(|| { let mut vote_generator = VoteGenerator::new(2); - vote_generator.vote_and_assert(VoteKind::Approve, Err("This proposal does not exist")); + vote_generator.vote_and_assert(VoteKind::Approve, Err(Error::ProposalNotFound)); }); } @@ -537,10 +543,7 @@ fn vote_fails_on_double_voting() { vote_generator.auto_increment_voter_id = false; vote_generator.vote_and_assert_ok(VoteKind::Approve); - vote_generator.vote_and_assert( - VoteKind::Approve, - Err("You have already voted on this proposal"), - ); + vote_generator.vote_and_assert(VoteKind::Approve, Err(Error::AlreadyVoted)); }); } @@ -560,7 +563,6 @@ fn cancel_proposal_succeeds() { assert_eq!( proposal, Proposal { - proposal_type: 1, parameters: parameters_fixture.params(), proposer_id: 1, created_at: 1, @@ -568,7 +570,6 @@ fn cancel_proposal_succeeds() { title: b"title".to_vec(), description: b"description".to_vec(), voting_results: VotingResults::default(), - stake_id: None, } ) }); @@ -583,7 +584,7 @@ fn cancel_proposal_fails_with_not_active_proposal() { run_to_block_and_finalize(6); let cancel_proposal = CancelProposalFixture::new(proposal_id); - cancel_proposal.cancel_and_assert(Err("Proposal is finalized already")); + cancel_proposal.cancel_and_assert(Err(Error::ProposalFinalized)); }); } @@ -591,7 +592,7 @@ fn cancel_proposal_fails_with_not_active_proposal() { fn cancel_proposal_fails_with_not_existing_proposal() { initial_test_ext().execute_with(|| { let cancel_proposal = CancelProposalFixture::new(2); - cancel_proposal.cancel_and_assert(Err("This proposal does not exist")); + cancel_proposal.cancel_and_assert(Err(Error::ProposalNotFound)); }); } @@ -601,9 +602,10 @@ fn cancel_proposal_fails_with_insufficient_rights() { let dummy_proposal = DummyProposalFixture::default(); let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); - let cancel_proposal = - CancelProposalFixture::new(proposal_id).with_origin(RawOrigin::Signed(2)); - cancel_proposal.cancel_and_assert(Err("You do not own this proposal")); + let cancel_proposal = CancelProposalFixture::new(proposal_id) + .with_origin(RawOrigin::Signed(2)) + .with_proposer(2); + cancel_proposal.cancel_and_assert(Err(Error::NotAuthor)); }); } @@ -629,7 +631,6 @@ fn veto_proposal_succeeds() { assert_eq!( proposal, Proposal { - proposal_type: 1, parameters: parameters_fixture.params(), proposer_id: 1, created_at: 1, @@ -637,7 +638,6 @@ fn veto_proposal_succeeds() { title: b"title".to_vec(), description: b"description".to_vec(), voting_results: VotingResults::default(), - stake_id: None, } ); @@ -655,7 +655,7 @@ fn veto_proposal_fails_with_not_active_proposal() { run_to_block_and_finalize(6); let veto_proposal = VetoProposalFixture::new(proposal_id); - veto_proposal.veto_and_assert(Err("Proposal is finalized already")); + veto_proposal.veto_and_assert(Err(Error::ProposalFinalized)); }); } @@ -663,7 +663,7 @@ fn veto_proposal_fails_with_not_active_proposal() { fn veto_proposal_fails_with_not_existing_proposal() { initial_test_ext().execute_with(|| { let veto_proposal = VetoProposalFixture::new(2); - veto_proposal.veto_and_assert(Err("This proposal does not exist")); + veto_proposal.veto_and_assert(Err(Error::ProposalNotFound)); }); } @@ -674,7 +674,7 @@ fn veto_proposal_fails_with_insufficient_rights() { let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); let veto_proposal = VetoProposalFixture::new(proposal_id).with_origin(RawOrigin::Signed(2)); - veto_proposal.veto_and_assert(Err("RequireRootOrigin")); + veto_proposal.veto_and_assert(Err(Error::RequireRootOrigin)); }); } @@ -722,7 +722,8 @@ fn cancel_proposal_event_emitted() { 1, ProposalStatus::Finalized(FinalizationData { proposal_status: ProposalDecisionStatus::Canceled, - finalization_error: None, + encoded_unstaking_error_due_to_broken_runtime: None, + stake_data_after_unstaking_error: None, finalized_at: 1, }), ), @@ -761,7 +762,6 @@ fn create_proposal_and_expire_it() { assert_eq!( proposal, Proposal { - proposal_type: 1, parameters: parameters_fixture.params(), proposer_id: 1, created_at: 1, @@ -769,7 +769,6 @@ fn create_proposal_and_expire_it() { title: b"title".to_vec(), description: b"description".to_vec(), voting_results: VotingResults::default(), - stake_id: None, } ) }); @@ -803,7 +802,6 @@ fn proposal_execution_postponed_because_of_grace_period() { assert_eq!( proposal, Proposal { - proposal_type: 1, parameters: parameters_fixture.params(), proposer_id: 1, created_at: 1, @@ -816,7 +814,6 @@ fn proposal_execution_postponed_because_of_grace_period() { rejections: 0, slashes: 0, }, - stake_id: None, } ); }); @@ -846,7 +843,6 @@ fn proposal_execution_succeeds_after_the_grace_period() { let mut proposal = >::get(proposal_id); let mut expected_proposal = Proposal { - proposal_type: 1, parameters: parameters_fixture.params(), proposer_id: 1, created_at: 1, @@ -859,7 +855,6 @@ fn proposal_execution_succeeds_after_the_grace_period() { rejections: 0, slashes: 0, }, - stake_id: None, }; assert_eq!(proposal, expected_proposal); @@ -890,7 +885,8 @@ fn create_proposal_fails_on_exceeding_max_active_proposals_count() { } let dummy_proposal = DummyProposalFixture::default(); - dummy_proposal.create_proposal_and_assert(Err("Max active proposals number exceeded")); + dummy_proposal + .create_proposal_and_assert(Err(Error::MaxActiveProposalNumberExceeded.into())); // internal active proposal counter check assert_eq!(::get(), 100); }); @@ -938,7 +934,7 @@ fn create_dummy_proposal_succeeds_with_stake() { let dummy_proposal = DummyProposalFixture::default() .with_parameters(parameters_fixture.params()) - .with_origin(RawOrigin::Signed(account_id)) + .with_account_id(account_id) .with_stake(200); let _imbalance = ::Currency::deposit_creating(&account_id, 500); @@ -950,15 +946,16 @@ fn create_dummy_proposal_succeeds_with_stake() { assert_eq!( proposal, Proposal { - proposal_type: 1, parameters: parameters_fixture.params(), proposer_id: 1, created_at: 1, - status: ProposalStatus::Active, + status: ProposalStatus::Active(Some(ActiveStake { + stake_id: 0, // valid stake_id + source_account_id: 1 + })), title: b"title".to_vec(), description: b"description".to_vec(), voting_results: VotingResults::default(), - stake_id: Some(0), // valid stake_id } ) }); @@ -974,10 +971,11 @@ fn create_dummy_proposal_fail_with_stake_on_empty_account() { ProposalParametersFixture::default().with_required_stake(required_stake); let dummy_proposal = DummyProposalFixture::default() .with_parameters(parameters_fixture.params()) - .with_origin(RawOrigin::Signed(account_id)) + .with_account_id(account_id) .with_stake(required_stake); - dummy_proposal.create_proposal_and_assert(Err("too few free funds in account")); + dummy_proposal + .create_proposal_and_assert(Err(Error::Other("too few free funds in account"))); }); } @@ -990,21 +988,20 @@ fn create_proposal_fais_with_invalid_stake_parameters() { .with_parameters(parameters_fixture.params()) .with_stake(200); - dummy_proposal.create_proposal_and_assert(Err("Stake should be empty for this proposal")); + dummy_proposal.create_proposal_and_assert(Err(Error::StakeShouldBeEmpty.into())); let parameters_fixture_stake_200 = parameters_fixture.with_required_stake(200); dummy_proposal = DummyProposalFixture::default().with_parameters(parameters_fixture_stake_200.params()); - dummy_proposal.create_proposal_and_assert(Err("Stake cannot be empty with this proposal")); + dummy_proposal.create_proposal_and_assert(Err(Error::EmptyStake.into())); let parameters_fixture_stake_300 = parameters_fixture.with_required_stake(300); dummy_proposal = DummyProposalFixture::default() .with_parameters(parameters_fixture_stake_300.params()) .with_stake(200); - dummy_proposal - .create_proposal_and_assert(Err("Stake differs from the proposal requirements")); + dummy_proposal.create_proposal_and_assert(Err(Error::StakeDiffersFromRequired.into())); }); } /* TODO: restore after the https://github.com/Joystream/substrate-runtime-joystream/issues/161 @@ -1114,7 +1111,7 @@ fn finalize_proposal_using_stake_mocks_succeeds() { ProposalParametersFixture::default().with_required_stake(stake_amount); let dummy_proposal = DummyProposalFixture::default() .with_parameters(parameters_fixture.params()) - .with_origin(RawOrigin::Signed(account_id)) + .with_account_id(account_id) .with_stake(stake_amount); let _proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); @@ -1156,8 +1153,9 @@ fn proposal_slashing_succeeds() { proposal.status, ProposalStatus::Finalized(FinalizationData { proposal_status: ProposalDecisionStatus::Slashed, - finalization_error: None, + encoded_unstaking_error_due_to_broken_runtime: None, finalized_at: 1, + stake_data_after_unstaking_error: None, }), ); assert!(!>::exists(proposal_id)); @@ -1197,7 +1195,7 @@ fn finalize_proposal_using_stake_mocks_failed() { ProposalParametersFixture::default().with_required_stake(stake_amount); let dummy_proposal = DummyProposalFixture::default() .with_parameters(parameters_fixture.params()) - .with_origin(RawOrigin::Signed(account_id)) + .with_account_id(account_id) .with_stake(stake_amount); let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap(); @@ -1208,19 +1206,21 @@ fn finalize_proposal_using_stake_mocks_failed() { assert_eq!( proposal, Proposal { - proposal_type: 1, parameters: parameters_fixture.params(), proposer_id: 1, created_at: 1, status: ProposalStatus::finalized_with_error( ProposalDecisionStatus::Expired, Some("Cannot remove stake"), + Some(ActiveStake { + stake_id: 1, + source_account_id: 1 + }), 4, ), title: b"title".to_vec(), description: b"description".to_vec(), voting_results: VotingResults::default(), - stake_id: Some(1), } ); }); diff --git a/modules/proposals/engine/src/types/mod.rs b/runtime-modules/proposals/engine/src/types/mod.rs similarity index 92% rename from modules/proposals/engine/src/types/mod.rs rename to runtime-modules/proposals/engine/src/types/mod.rs index 18724ceb8c..ad9f84fd40 100644 --- a/modules/proposals/engine/src/types/mod.rs +++ b/runtime-modules/proposals/engine/src/types/mod.rs @@ -111,13 +111,21 @@ impl VotingResults { } } +/// Contains created stake id and source account for the stake balance +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Encode, Decode, Default, Clone, Copy, PartialEq, Eq, Debug)] +pub struct ActiveStake { + /// Created stake id for the proposal + pub stake_id: StakeId, + + /// Source account of the stake balance. Refund if any will be provided using this account + pub source_account_id: AccountId, +} + /// 'Proposal' contains information necessary for the proposal system functioning. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] -pub struct Proposal { - /// Proposal type id - pub proposal_type: u32, - +pub struct Proposal { /// Proposals parameter, characterize different proposal types. pub parameters: ProposalParameters, @@ -134,18 +142,18 @@ pub struct Proposal { pub created_at: BlockNumber, /// Current proposal status - pub status: ProposalStatus, + pub status: ProposalStatus, /// Curring voting result for the proposal pub voting_results: VotingResults, - - /// Created stake id for the proposal - pub stake_id: Option, } -impl Proposal +impl + Proposal where BlockNumber: Add + PartialOrd + Copy, + StakeId: Clone, + AccountId: Clone, { /// Returns whether voting period expired by now pub fn is_voting_period_expired(&self, now: BlockNumber) -> bool { @@ -211,8 +219,8 @@ pub trait VotersParameters { } // Calculates quorum, votes threshold, expiration status -struct ProposalStatusResolution<'a, BlockNumber, ProposerId, Balance, StakeId> { - proposal: &'a Proposal, +struct ProposalStatusResolution<'a, BlockNumber, ProposerId, Balance, StakeId, AccountId> { + proposal: &'a Proposal, now: BlockNumber, votes_count: u32, total_voters_count: u32, @@ -220,10 +228,12 @@ struct ProposalStatusResolution<'a, BlockNumber, ProposerId, Balance, StakeId> { slashes: u32, } -impl<'a, BlockNumber, ProposerId, Balance, StakeId> - ProposalStatusResolution<'a, BlockNumber, ProposerId, Balance, StakeId> +impl<'a, BlockNumber, ProposerId, Balance, StakeId, AccountId> + ProposalStatusResolution<'a, BlockNumber, ProposerId, Balance, StakeId, AccountId> where BlockNumber: Add + PartialOrd + Copy, + StakeId: Clone, + AccountId: Clone, { // Proposal has been expired and quorum not reached. pub fn is_expired(&self) -> bool { @@ -307,12 +317,19 @@ pub type NegativeImbalance = pub type CurrencyOf = ::Currency; /// Data container for the finalized proposal results -pub(crate) struct FinalizedProposalData { +pub(crate) struct FinalizedProposalData< + ProposalId, + BlockNumber, + ProposerId, + Balance, + StakeId, + AccountId, +> { /// Proposal id pub proposal_id: ProposalId, /// Proposal to be finalized - pub proposal: Proposal, + pub proposal: Proposal, /// Proposal finalization status pub status: ProposalDecisionStatus, @@ -321,12 +338,31 @@ pub(crate) struct FinalizedProposalData { + /// Proposal id + pub proposal_id: ProposalId, + + /// Proposal to be finalized + pub proposal: Proposal, + + /// Proposal finalisation status data + pub finalisation_status_data: FinalizationData, +} + #[cfg(test)] mod tests { use crate::*; // Alias introduced for simplicity of changing Proposal exact types. - type ProposalObject = Proposal; + type ProposalObject = Proposal; #[test] fn proposal_voting_period_expired() { diff --git a/modules/proposals/engine/src/types/proposal_statuses.rs b/runtime-modules/proposals/engine/src/types/proposal_statuses.rs similarity index 65% rename from modules/proposals/engine/src/types/proposal_statuses.rs rename to runtime-modules/proposals/engine/src/types/proposal_statuses.rs index f9100a48cc..1ce4d634b4 100644 --- a/modules/proposals/engine/src/types/proposal_statuses.rs +++ b/runtime-modules/proposals/engine/src/types/proposal_statuses.rs @@ -1,45 +1,49 @@ use codec::{Decode, Encode}; use rstd::prelude::*; +use crate::ActiveStake; #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; /// Current status of the proposal #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)] -pub enum ProposalStatus { - /// A new proposal that is available for voting. - Active, +pub enum ProposalStatus { + /// A new proposal status that is available for voting (with optional stake data). + Active(Option>), /// The proposal decision was made. - Finalized(FinalizationData), + Finalized(FinalizationData), } -impl Default for ProposalStatus { +impl Default for ProposalStatus { fn default() -> Self { - ProposalStatus::Active + ProposalStatus::Active(None) } } -impl ProposalStatus { +impl ProposalStatus { /// Creates finalized proposal status with provided ProposalDecisionStatus pub fn finalized( decision_status: ProposalDecisionStatus, now: BlockNumber, - ) -> ProposalStatus { - Self::finalized_with_error(decision_status, None, now) + ) -> ProposalStatus { + Self::finalized_with_error(decision_status, None, None, now) } /// Creates finalized proposal status with provided ProposalDecisionStatus and error pub fn finalized_with_error( decision_status: ProposalDecisionStatus, - finalization_error: Option<&str>, + encoded_unstaking_error_due_to_broken_runtime: Option<&str>, + active_stake: Option>, now: BlockNumber, - ) -> ProposalStatus { + ) -> ProposalStatus { ProposalStatus::Finalized(FinalizationData { proposal_status: decision_status, - finalization_error: finalization_error.map(|err| err.as_bytes().to_vec()), + encoded_unstaking_error_due_to_broken_runtime: + encoded_unstaking_error_due_to_broken_runtime.map(|err| err.as_bytes().to_vec()), finalized_at: now, + stake_data_after_unstaking_error: active_stake, }) } @@ -47,11 +51,12 @@ impl ProposalStatus { pub fn approved( approved_status: ApprovedProposalStatus, now: BlockNumber, - ) -> ProposalStatus { + ) -> ProposalStatus { ProposalStatus::Finalized(FinalizationData { proposal_status: ProposalDecisionStatus::Approved(approved_status), - finalization_error: None, + encoded_unstaking_error_due_to_broken_runtime: None, finalized_at: now, + stake_data_after_unstaking_error: None, }) } } @@ -59,23 +64,26 @@ impl ProposalStatus { /// Final proposal status and potential error. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)] -pub struct FinalizationData { +pub struct FinalizationData { /// Final proposal status pub proposal_status: ProposalDecisionStatus, /// Proposal finalization block number pub finalized_at: BlockNumber, - /// Error occured during the proposal finalization - pub finalization_error: Option>, + /// Error occured during the proposal finalization - unstaking failed in the stake module + pub encoded_unstaking_error_due_to_broken_runtime: Option>, + + /// Stake data for the proposal, filled if the unstaking wasn't successful + pub stake_data_after_unstaking_error: Option>, } -impl FinalizationData { +impl FinalizationData { /// FinalizationData helper, creates ApprovedProposalStatus pub fn create_approved_proposal_status( self, approved_status: ApprovedProposalStatus, - ) -> ProposalStatus { + ) -> ProposalStatus { ProposalStatus::Finalized(FinalizationData { proposal_status: ProposalDecisionStatus::Approved(approved_status), ..self diff --git a/modules/proposals/engine/src/types/stakes.rs b/runtime-modules/proposals/engine/src/types/stakes.rs similarity index 99% rename from modules/proposals/engine/src/types/stakes.rs rename to runtime-modules/proposals/engine/src/types/stakes.rs index 5d4a5509df..94a4134829 100644 --- a/modules/proposals/engine/src/types/stakes.rs +++ b/runtime-modules/proposals/engine/src/types/stakes.rs @@ -3,7 +3,7 @@ use crate::Trait; use rstd::convert::From; use rstd::marker::PhantomData; use rstd::rc::Rc; -use runtime_primitives::traits::Zero; +use sr_primitives::traits::Zero; use srml_support::traits::{Currency, ExistenceRequirement, WithdrawReasons}; // Mocking dependencies for testing @@ -153,6 +153,7 @@ impl ProposalStakeManager { pub fn remove_stake(stake_id: T::StakeId) -> Result<(), &'static str> { T::StakeHandlerProvider::stakes().unstake(stake_id)?; + //TODO: can't remove stake before refunding T::StakeHandlerProvider::stakes().remove_stake(stake_id)?; Ok(()) diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 89bcbe3708..d601e787d2 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -353,3 +353,21 @@ default_features = false package = 'substrate-storage-module' path = '../runtime-modules/storage' version = '1.0.0' + +[dependencies.proposals_engine] +default_features = false +package = 'substrate-proposals-engine-module' +path = '../runtime-modules/proposals/engine' +version = '2.0.0' + +[dependencies.proposals_discussion] +default_features = false +package = 'substrate-proposals-discussion-module' +path = '../runtime-modules/proposals/discussion' +version = '2.0.0' + +[dependencies.proposals_codex] +default_features = false +package = 'substrate-proposals-codex-module' +path = '../runtime-modules/proposals/codex' +version = '2.0.0' \ No newline at end of file diff --git a/runtime/build.rs b/runtime/build.rs index f13170b2a4..02f0cdb0f5 100644 --- a/runtime/build.rs +++ b/runtime/build.rs @@ -15,7 +15,7 @@ // along with Substrate. If not, see . use std::{env, process::Command, string::String}; -use wasm_builder_runner::{build_current_project_with_rustflags, WasmBuilderSource}; +use wasm_builder_runner::{WasmBuilder, WasmBuilderSource}; fn main() { if !in_real_cargo_environment() { @@ -23,13 +23,18 @@ fn main() { println!("Building DUMMY Wasm binary"); } - build_current_project_with_rustflags( - "wasm_binary.rs", - WasmBuilderSource::Crates("1.0.8"), - // This instructs LLD to export __heap_base as a global variable, which is used by the - // external memory allocator. - "-Clink-arg=--export=__heap_base", - ); + let file_name = "wasm_binary.rs"; + let wasm_builder_source = WasmBuilderSource::Crates("1.0.8"); + // This instructs LLD to export __heap_base as a global variable, which is used by the + // external memory allocator. + let default_rust_flags = "-Clink-arg=--export=__heap_base"; + + WasmBuilder::new() + .with_current_project() + .with_wasm_builder_source(wasm_builder_source) + .append_to_rust_flags(default_rust_flags) + .set_file_name(file_name) + .build() } fn in_real_cargo_environment() -> bool { diff --git a/runtime/src/integration/mod.rs b/runtime/src/integration/mod.rs new file mode 100644 index 0000000000..8d18108be0 --- /dev/null +++ b/runtime/src/integration/mod.rs @@ -0,0 +1 @@ +pub mod proposals; diff --git a/runtime/src/integration/proposals/council_origin_validator.rs b/runtime/src/integration/proposals/council_origin_validator.rs new file mode 100644 index 0000000000..95428ca6b2 --- /dev/null +++ b/runtime/src/integration/proposals/council_origin_validator.rs @@ -0,0 +1,206 @@ +use rstd::marker::PhantomData; + +use common::origin_validator::ActorOriginValidator; +use proposals_engine::VotersParameters; + +use super::{MemberId, MembershipOriginValidator}; + +/// Handles work with the council. +/// Provides implementations for ActorOriginValidator and VotersParameters. +pub struct CouncilManager { + marker: PhantomData, +} + +impl + ActorOriginValidator<::Origin, MemberId, ::AccountId> + for CouncilManager +{ + /// Check for valid combination of origin and actor_id. Actor_id should be valid member_id of + /// the membership module + fn ensure_actor_origin( + origin: ::Origin, + actor_id: MemberId, + ) -> Result<::AccountId, &'static str> { + let account_id = >::ensure_actor_origin(origin, actor_id)?; + + if >::is_councilor(&account_id) { + return Ok(account_id); + } + + Err("Council validation failed: account id doesn't belong to a council member") + } +} + +impl VotersParameters for CouncilManager { + /// Implement total_voters_count() as council size + fn total_voters_count() -> u32 { + >::active_council().len() as u32 + } +} + +#[cfg(test)] +mod tests { + use super::CouncilManager; + use crate::Runtime; + use common::origin_validator::ActorOriginValidator; + use membership::members::UserInfo; + use proposals_engine::VotersParameters; + use sr_primitives::AccountId32; + use system::RawOrigin; + + type Council = governance::council::Module; + + fn initial_test_ext() -> runtime_io::TestExternalities { + let t = system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + t.into() + } + + type Membership = membership::members::Module; + + #[test] + fn council_origin_validator_fails_with_unregistered_member() { + initial_test_ext().execute_with(|| { + let origin = RawOrigin::Signed(AccountId32::default()); + let member_id = 1; + let error = "Membership validation failed: cannot find a profile for a member"; + + let validation_result = + CouncilManager::::ensure_actor_origin(origin.into(), member_id); + + assert_eq!(validation_result, Err(error)); + }); + } + + #[test] + fn council_origin_validator_succeeds() { + initial_test_ext().execute_with(|| { + let councilor1 = AccountId32::default(); + let councilor2: [u8; 32] = [2; 32]; + let councilor3: [u8; 32] = [3; 32]; + + assert!(Council::set_council( + system::RawOrigin::Root.into(), + vec![councilor1, councilor2.into(), councilor3.into()] + ) + .is_ok()); + + let account_id = AccountId32::default(); + let origin = RawOrigin::Signed(account_id.clone()); + let authority_account_id = AccountId32::default(); + Membership::set_screening_authority( + RawOrigin::Root.into(), + authority_account_id.clone(), + ) + .unwrap(); + + Membership::add_screened_member( + RawOrigin::Signed(authority_account_id).into(), + account_id.clone(), + UserInfo { + handle: Some(b"handle".to_vec()), + avatar_uri: None, + about: None, + }, + ) + .unwrap(); + let member_id = 0; // newly created member_id + + let validation_result = + CouncilManager::::ensure_actor_origin(origin.into(), member_id); + + assert_eq!(validation_result, Ok(account_id)); + }); + } + + #[test] + fn council_origin_validator_fails_with_incompatible_account_id_and_member_id() { + initial_test_ext().execute_with(|| { + let account_id = AccountId32::default(); + let error = + "Membership validation failed: given account doesn't match with profile accounts"; + let authority_account_id = AccountId32::default(); + Membership::set_screening_authority( + RawOrigin::Root.into(), + authority_account_id.clone(), + ) + .unwrap(); + + Membership::add_screened_member( + RawOrigin::Signed(authority_account_id).into(), + account_id.clone(), + UserInfo { + handle: Some(b"handle".to_vec()), + avatar_uri: None, + about: None, + }, + ) + .unwrap(); + let member_id = 0; // newly created member_id + + let invalid_account_id: [u8; 32] = [2; 32]; + let validation_result = CouncilManager::::ensure_actor_origin( + RawOrigin::Signed(invalid_account_id.into()).into(), + member_id, + ); + + assert_eq!(validation_result, Err(error)); + }); + } + + #[test] + fn council_origin_validator_fails_with_not_council_account_id() { + initial_test_ext().execute_with(|| { + let account_id = AccountId32::default(); + let origin = RawOrigin::Signed(account_id.clone()); + let error = "Council validation failed: account id doesn't belong to a council member"; + let authority_account_id = AccountId32::default(); + Membership::set_screening_authority( + RawOrigin::Root.into(), + authority_account_id.clone(), + ) + .unwrap(); + + Membership::add_screened_member( + RawOrigin::Signed(authority_account_id).into(), + account_id, + UserInfo { + handle: Some(b"handle".to_vec()), + avatar_uri: None, + about: None, + }, + ) + .unwrap(); + let member_id = 0; // newly created member_id + + let validation_result = + CouncilManager::::ensure_actor_origin(origin.into(), member_id); + + assert_eq!(validation_result, Err(error)); + }); + } + + #[test] + fn council_size_calculation_aka_total_voters_count_succeeds() { + initial_test_ext().execute_with(|| { + let councilor1 = AccountId32::default(); + let councilor2: [u8; 32] = [2; 32]; + let councilor3: [u8; 32] = [3; 32]; + let councilor4: [u8; 32] = [4; 32]; + assert!(Council::set_council( + system::RawOrigin::Root.into(), + vec![ + councilor1, + councilor2.into(), + councilor3.into(), + councilor4.into() + ] + ) + .is_ok()); + + assert_eq!(CouncilManager::::total_voters_count(), 4) + }); + } +} diff --git a/runtime/src/integration/proposals/membership_origin_validator.rs b/runtime/src/integration/proposals/membership_origin_validator.rs new file mode 100644 index 0000000000..82bb88cbbb --- /dev/null +++ b/runtime/src/integration/proposals/membership_origin_validator.rs @@ -0,0 +1,141 @@ +use rstd::marker::PhantomData; + +use common::origin_validator::ActorOriginValidator; +use system::ensure_signed; + +/// Member of the Joystream organization +pub type MemberId = ::MemberId; + +/// Default membership actor origin validator. +pub struct MembershipOriginValidator { + marker: PhantomData, +} + +impl + ActorOriginValidator<::Origin, MemberId, ::AccountId> + for MembershipOriginValidator +{ + /// Check for valid combination of origin and actor_id. Actor_id should be valid member_id of + /// the membership module + fn ensure_actor_origin( + origin: ::Origin, + actor_id: MemberId, + ) -> Result<::AccountId, &'static str> { + // check valid signed account_id + let account_id = ensure_signed(origin)?; + + // check whether actor_id belongs to the registered member + let profile_result = >::ensure_profile(actor_id); + + if let Ok(profile) = profile_result { + // whether the account_id belongs to the actor + if profile.controller_account == account_id { + return Ok(account_id); + } else { + return Err("Membership validation failed: given account doesn't match with profile accounts"); + } + } + + Err("Membership validation failed: cannot find a profile for a member") + } +} + +#[cfg(test)] +mod tests { + use super::MembershipOriginValidator; + use crate::Runtime; + use common::origin_validator::ActorOriginValidator; + use membership::members::UserInfo; + use sr_primitives::AccountId32; + use system::RawOrigin; + + type Membership = crate::members::Module; + + fn initial_test_ext() -> runtime_io::TestExternalities { + let t = system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + t.into() + } + + #[test] + fn membership_origin_validator_fails_with_unregistered_member() { + initial_test_ext().execute_with(|| { + let origin = RawOrigin::Signed(AccountId32::default()); + let member_id = 1; + let error = "Membership validation failed: cannot find a profile for a member"; + + let validation_result = + MembershipOriginValidator::::ensure_actor_origin(origin.into(), member_id); + + assert_eq!(validation_result, Err(error)); + }); + } + + #[test] + fn membership_origin_validator_succeeds() { + initial_test_ext().execute_with(|| { + let account_id = AccountId32::default(); + let origin = RawOrigin::Signed(account_id.clone()); + let authority_account_id = AccountId32::default(); + Membership::set_screening_authority( + RawOrigin::Root.into(), + authority_account_id.clone(), + ) + .unwrap(); + + Membership::add_screened_member( + RawOrigin::Signed(authority_account_id).into(), + account_id.clone(), + UserInfo { + handle: Some(b"handle".to_vec()), + avatar_uri: None, + about: None, + }, + ) + .unwrap(); + let member_id = 0; // newly created member_id + + let validation_result = + MembershipOriginValidator::::ensure_actor_origin(origin.into(), member_id); + + assert_eq!(validation_result, Ok(account_id)); + }); + } + + #[test] + fn membership_origin_validator_fails_with_incompatible_account_id_and_member_id() { + initial_test_ext().execute_with(|| { + let account_id = AccountId32::default(); + let error = + "Membership validation failed: given account doesn't match with profile accounts"; + let authority_account_id = AccountId32::default(); + Membership::set_screening_authority( + RawOrigin::Root.into(), + authority_account_id.clone(), + ) + .unwrap(); + + Membership::add_screened_member( + RawOrigin::Signed(authority_account_id).into(), + account_id, + UserInfo { + handle: Some(b"handle".to_vec()), + avatar_uri: None, + about: None, + }, + ) + .unwrap(); + let member_id = 0; // newly created member_id + + let invalid_account_id: [u8; 32] = [2; 32]; + let validation_result = MembershipOriginValidator::::ensure_actor_origin( + RawOrigin::Signed(invalid_account_id.into()).into(), + member_id, + ); + + assert_eq!(validation_result, Err(error)); + }); + } +} diff --git a/runtime/src/integration/proposals/mod.rs b/runtime/src/integration/proposals/mod.rs new file mode 100644 index 0000000000..172923a1ff --- /dev/null +++ b/runtime/src/integration/proposals/mod.rs @@ -0,0 +1,7 @@ +mod council_origin_validator; +mod membership_origin_validator; +mod staking_events_handler; + +pub use council_origin_validator::CouncilManager; +pub use membership_origin_validator::{MemberId, MembershipOriginValidator}; +pub use staking_events_handler::StakingEventsHandler; diff --git a/runtime/src/integration/proposals/staking_events_handler.rs b/runtime/src/integration/proposals/staking_events_handler.rs new file mode 100644 index 0000000000..ba8595a095 --- /dev/null +++ b/runtime/src/integration/proposals/staking_events_handler.rs @@ -0,0 +1,47 @@ +use rstd::marker::PhantomData; +use srml_support::traits::{Currency, Imbalance}; +use srml_support::StorageMap; + +// Balance alias +type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + +// Balance alias for staking +type NegativeImbalance = + <::Currency as Currency<::AccountId>>::NegativeImbalance; + +/// Proposal implementation of the staking event handler from the stake module. +/// 'marker' responsible for the 'Trait' binding. +pub struct StakingEventsHandler { + pub marker: PhantomData, +} + +impl stake::StakingEventsHandler + for StakingEventsHandler +{ + /// Unstake remaining sum back to the source_account_id + fn unstaked( + id: &::StakeId, + _unstaked_amount: BalanceOf, + remaining_imbalance: NegativeImbalance, + ) -> NegativeImbalance { + if >::exists(id) { + >::refund_proposal_stake(*id, remaining_imbalance); + + return >::zero(); // imbalance was consumed + } + + remaining_imbalance + } + + /// Empty handler for slashing + fn slashed( + _: &::StakeId, + _: &::SlashId, + _: BalanceOf, + _: BalanceOf, + remaining_imbalance: NegativeImbalance, + ) -> NegativeImbalance { + remaining_imbalance + } +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 748a61ca29..95138a278b 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -3,6 +3,9 @@ #![cfg_attr(not(feature = "std"), no_std)] // `construct_runtime!` does a lot of recursion and requires us to increase the limit to 256. #![recursion_limit = "256"] +// srml_staking_reward_curve::build! - substrate macro produces a warning. +// TODO: remove after post-Rome substrate upgrade +#![allow(array_into_iter)] // Make the WASM binary available. // This is required only by the node build. @@ -10,6 +13,8 @@ #[cfg(feature = "std")] include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); +mod integration; + use authority_discovery_primitives::{ AuthorityId as EncodedAuthorityId, Signature as EncodedSignature, }; @@ -51,6 +56,8 @@ pub use srml_support::{ pub use staking::StakerStatus; pub use timestamp::Call as TimestampCall; +use integration::proposals::{CouncilManager, MembershipOriginValidator}; + /// An index to a block. pub type BlockNumber = u32; @@ -396,7 +403,7 @@ impl finality_tracker::Trait for Runtime { } pub use forum; -use governance::{council, election, proposals}; +use governance::{council, election}; use membership::members; use storage::{data_directory, data_object_storage_registry, data_object_type_registry}; pub use versioned_store; @@ -577,7 +584,10 @@ parameter_types! { impl stake::Trait for Runtime { type Currency = ::Currency; type StakePoolId = StakePoolId; - type StakingEventsHandler = ContentWorkingGroupStakingEventHandler; + type StakingEventsHandler = ( + ContentWorkingGroupStakingEventHandler, + crate::integration::proposals::StakingEventsHandler, + ); type StakeId = u64; type SlashId = u64; } @@ -657,10 +667,6 @@ impl common::currency::GovernanceCurrency for Runtime { type Currency = balances::Module; } -impl governance::proposals::Trait for Runtime { - type Event = Event; -} - impl governance::election::Trait for Runtime { type Event = Event; type CouncilElected = (Council,); @@ -801,6 +807,63 @@ impl discovery::Trait for Runtime { type Roles = LookupRoles; } +parameter_types! { + pub const ProposalCancellationFee: u64 = 5; + pub const ProposalRejectionFee: u64 = 3; + pub const ProposalTitleMaxLength: u32 = 100; + pub const ProposalDescriptionMaxLength: u32 = 10000; + pub const ProposalMaxActiveProposalLimit: u32 = 100; +} + +impl proposals_engine::Trait for Runtime { + type Event = Event; + type ProposerOriginValidator = MembershipOriginValidator; + type VoterOriginValidator = CouncilManager; + type TotalVotersCounter = CouncilManager; + type ProposalId = u32; + type StakeHandlerProvider = proposals_engine::DefaultStakeHandlerProvider; + type CancellationFee = ProposalCancellationFee; + type RejectionFee = ProposalRejectionFee; + type TitleMaxLength = ProposalTitleMaxLength; + type DescriptionMaxLength = ProposalDescriptionMaxLength; + type MaxActiveProposalLimit = ProposalMaxActiveProposalLimit; + type DispatchableCallCode = Call; +} +impl Default for Call { + fn default() -> Self { + panic!("shouldn't call default for Call"); + } +} + +parameter_types! { + pub const ProposalMaxPostEditionNumber: u32 = 5; + pub const ProposalMaxThreadInARowNumber: u32 = 3; + pub const ProposalThreadTitleLengthLimit: u32 = 200; + pub const ProposalPostLengthLimit: u32 = 2000; +} + +impl proposals_discussion::Trait for Runtime { + type Event = Event; + type PostAuthorOriginValidator = MembershipOriginValidator; + type ThreadId = u32; + type PostId = u32; + type MaxPostEditionNumber = ProposalMaxPostEditionNumber; + type ThreadTitleLengthLimit = ProposalThreadTitleLengthLimit; + type PostLengthLimit = ProposalPostLengthLimit; + type MaxThreadInARowNumber = ProposalMaxThreadInARowNumber; +} + +parameter_types! { + pub const TextProposalMaxLength: u32 = 60_000; + pub const RuntimeUpgradeWasmProposalMaxLength: u32 = 2_000_000; +} + +impl proposals_codex::Trait for Runtime { + type MembershipOriginValidator = MembershipOriginValidator; + type TextProposalMaxLength = TextProposalMaxLength; + type RuntimeUpgradeWasmProposalMaxLength = RuntimeUpgradeWasmProposalMaxLength; +} + construct_runtime!( pub enum Runtime where Block = Block, @@ -825,7 +888,6 @@ construct_runtime!( RandomnessCollectiveFlip: randomness_collective_flip::{Module, Call, Storage}, Sudo: sudo, // Joystream - Proposals: proposals::{Module, Call, Storage, Event, Config}, CouncilElection: election::{Module, Call, Storage, Event, Config}, Council: council::{Module, Call, Storage, Event, Config}, Memo: memo::{Module, Call, Storage, Event}, @@ -844,6 +906,11 @@ construct_runtime!( RecurringRewards: recurringrewards::{Module, Call, Storage}, Hiring: hiring::{Module, Call, Storage}, ContentWorkingGroup: content_wg::{Module, Call, Storage, Event, Config}, + // --- Proposals + ProposalsEngine: proposals_engine::{Module, Call, Storage, Event}, + ProposalsDiscussion: proposals_discussion::{Module, Call, Storage, Event}, + ProposalsCodex: proposals_codex::{Module, Call, Storage, Error}, + // --- } );