proposals.rs 56 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572
  1. use codec::{Decode, Encode};
  2. use rstd::prelude::*;
  3. use runtime_primitives::{
  4. print,
  5. traits::{Hash, SaturatedConversion, Zero},
  6. };
  7. use srml_support::traits::{Currency, Get, ReservableCurrency};
  8. use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure};
  9. use system::{self, ensure_root, ensure_signed};
  10. #[cfg(feature = "std")]
  11. use serde::{Deserialize, Serialize};
  12. #[cfg(test)]
  13. use primitives::storage::well_known_keys;
  14. use super::council;
  15. pub use crate::currency::{BalanceOf, GovernanceCurrency};
  16. use crate::membership;
  17. const DEFAULT_APPROVAL_QUORUM: u32 = 60;
  18. const DEFAULT_MIN_STAKE: u32 = 100;
  19. const DEFAULT_CANCELLATION_FEE: u32 = 5;
  20. const DEFAULT_REJECTION_FEE: u32 = 10;
  21. const DEFAULT_VOTING_PERIOD_IN_DAYS: u32 = 10;
  22. const DEFAULT_VOTING_PERIOD_IN_SECS: u32 = DEFAULT_VOTING_PERIOD_IN_DAYS * 24 * 60 * 60;
  23. const DEFAULT_NAME_MAX_LEN: u32 = 100;
  24. const DEFAULT_DESCRIPTION_MAX_LEN: u32 = 10_000;
  25. const DEFAULT_WASM_CODE_MAX_LEN: u32 = 2_000_000;
  26. const MSG_STAKE_IS_TOO_LOW: &str = "Stake is too low";
  27. const MSG_STAKE_IS_GREATER_THAN_BALANCE: &str = "Balance is too low to be staked";
  28. const MSG_ONLY_MEMBERS_CAN_PROPOSE: &str = "Only members can make a proposal";
  29. const MSG_ONLY_COUNCILORS_CAN_VOTE: &str = "Only councilors can vote on proposals";
  30. const MSG_PROPOSAL_NOT_FOUND: &str = "This proposal does not exist";
  31. const MSG_PROPOSAL_EXPIRED: &str = "Voting period is expired for this proposal";
  32. const MSG_PROPOSAL_FINALIZED: &str = "Proposal is finalized already";
  33. const MSG_YOU_ALREADY_VOTED: &str = "You have already voted on this proposal";
  34. const MSG_YOU_DONT_OWN_THIS_PROPOSAL: &str = "You do not own this proposal";
  35. const MSG_PROPOSAL_STATUS_ALREADY_UPDATED: &str = "Proposal status has been updated already";
  36. const MSG_EMPTY_NAME_PROVIDED: &str = "Proposal cannot have an empty name";
  37. const MSG_EMPTY_DESCRIPTION_PROVIDED: &str = "Proposal cannot have an empty description";
  38. const MSG_EMPTY_WASM_CODE_PROVIDED: &str = "Proposal cannot have an empty WASM code";
  39. const MSG_TOO_LONG_NAME: &str = "Name is too long";
  40. const MSG_TOO_LONG_DESCRIPTION: &str = "Description is too long";
  41. const MSG_TOO_LONG_WASM_CODE: &str = "WASM code is too big";
  42. #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
  43. #[derive(Encode, Decode, Clone, PartialEq, Eq)]
  44. pub enum ProposalStatus {
  45. /// A new proposal that is available for voting.
  46. Active,
  47. /// If cancelled by a proposer.
  48. Cancelled,
  49. /// Not enough votes and voting period expired.
  50. Expired,
  51. /// To clear the quorum requirement, the percentage of council members with revealed votes
  52. /// must be no less than the quorum value for the given proposal type.
  53. Approved,
  54. Rejected,
  55. /// If all revealed votes are slashes, then the proposal is rejected,
  56. /// and the proposal stake is slashed.
  57. Slashed,
  58. }
  59. impl Default for ProposalStatus {
  60. fn default() -> Self {
  61. ProposalStatus::Active
  62. }
  63. }
  64. use self::ProposalStatus::*;
  65. #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
  66. #[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
  67. pub enum VoteKind {
  68. /// Signals presence, but unwillingness to cast judgment on substance of vote.
  69. Abstain,
  70. /// Pass, an alternative or a ranking, for binary, multiple choice
  71. /// and ranked choice propositions, respectively.
  72. Approve,
  73. /// Against proposal.
  74. Reject,
  75. /// Against the proposal, and slash proposal stake.
  76. Slash,
  77. }
  78. impl Default for VoteKind {
  79. fn default() -> Self {
  80. VoteKind::Abstain
  81. }
  82. }
  83. use self::VoteKind::*;
  84. #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
  85. #[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
  86. /// Proposal for node runtime update.
  87. pub struct RuntimeUpgradeProposal<AccountId, Balance, BlockNumber, Hash> {
  88. id: u32,
  89. proposer: AccountId,
  90. stake: Balance,
  91. name: Vec<u8>,
  92. description: Vec<u8>,
  93. wasm_hash: Hash,
  94. proposed_at: BlockNumber,
  95. status: ProposalStatus,
  96. }
  97. #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
  98. #[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
  99. pub struct TallyResult<BlockNumber> {
  100. proposal_id: u32,
  101. abstentions: u32,
  102. approvals: u32,
  103. rejections: u32,
  104. slashes: u32,
  105. status: ProposalStatus,
  106. finalized_at: BlockNumber,
  107. }
  108. pub trait Trait:
  109. timestamp::Trait + council::Trait + GovernanceCurrency + membership::members::Trait
  110. {
  111. /// The overarching event type.
  112. type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
  113. }
  114. decl_event!(
  115. pub enum Event<T>
  116. where
  117. <T as system::Trait>::Hash,
  118. <T as system::Trait>::BlockNumber,
  119. <T as system::Trait>::AccountId
  120. {
  121. // New events
  122. /// Params:
  123. /// * Account id of a member who proposed.
  124. /// * Id of a newly created proposal after it was saved in storage.
  125. ProposalCreated(AccountId, u32),
  126. ProposalCanceled(AccountId, u32),
  127. ProposalStatusUpdated(u32, ProposalStatus),
  128. /// Params:
  129. /// * Voter - an account id of a councilor.
  130. /// * Id of a proposal.
  131. /// * Kind of vote.
  132. Voted(AccountId, u32, VoteKind),
  133. TallyFinalized(TallyResult<BlockNumber>),
  134. /// * Hash - hash of wasm code of runtime update.
  135. RuntimeUpdated(u32, Hash),
  136. /// Root cancelled proposal
  137. ProposalVetoed(u32),
  138. }
  139. );
  140. decl_storage! {
  141. trait Store for Module<T: Trait> as Proposals {
  142. // Parameters (defaut values could be exported to config):
  143. // TODO rename 'approval_quorum' -> 'quorum_percent' ?!
  144. /// A percent (up to 100) of the council participants
  145. /// that must vote affirmatively in order to pass.
  146. ApprovalQuorum get(approval_quorum) config(): u32 = DEFAULT_APPROVAL_QUORUM;
  147. /// Minimum amount of a balance to be staked in order to make a proposal.
  148. MinStake get(min_stake) config(): BalanceOf<T> =
  149. BalanceOf::<T>::from(DEFAULT_MIN_STAKE);
  150. /// A fee to be slashed (burn) in case a proposer decides to cancel a proposal.
  151. CancellationFee get(cancellation_fee) config(): BalanceOf<T> =
  152. BalanceOf::<T>::from(DEFAULT_CANCELLATION_FEE);
  153. /// A fee to be slashed (burn) in case a proposal was rejected.
  154. RejectionFee get(rejection_fee) config(): BalanceOf<T> =
  155. BalanceOf::<T>::from(DEFAULT_REJECTION_FEE);
  156. /// Max duration of proposal in blocks until it will be expired if not enough votes.
  157. VotingPeriod get(voting_period) config(): T::BlockNumber =
  158. T::BlockNumber::from(DEFAULT_VOTING_PERIOD_IN_SECS /
  159. (<T as timestamp::Trait>::MinimumPeriod::get().saturated_into::<u32>() * 2));
  160. NameMaxLen get(name_max_len) config(): u32 = DEFAULT_NAME_MAX_LEN;
  161. DescriptionMaxLen get(description_max_len) config(): u32 = DEFAULT_DESCRIPTION_MAX_LEN;
  162. WasmCodeMaxLen get(wasm_code_max_len) config(): u32 = DEFAULT_WASM_CODE_MAX_LEN;
  163. // Persistent state (always relevant, changes constantly):
  164. /// Count of all proposals that have been created.
  165. ProposalCount get(proposal_count): u32;
  166. /// Get proposal details by its id.
  167. Proposals get(proposals): map u32 => RuntimeUpgradeProposal<T::AccountId, BalanceOf<T>, T::BlockNumber, T::Hash>;
  168. /// Ids of proposals that are open for voting (have not been finalized yet).
  169. ActiveProposalIds get(active_proposal_ids): Vec<u32> = vec![];
  170. /// Get WASM code of runtime upgrade by hash of its content.
  171. WasmCodeByHash get(wasm_code_by_hash): map T::Hash => Vec<u8>;
  172. VotesByProposal get(votes_by_proposal): map u32 => Vec<(T::AccountId, VoteKind)>;
  173. // TODO Rethink: this can be replaced with: votes_by_proposal.find(|vote| vote.0 == proposer)
  174. VoteByAccountAndProposal get(vote_by_account_and_proposal): map (T::AccountId, u32) => VoteKind;
  175. TallyResults get(tally_results): map u32 => TallyResult<T::BlockNumber>;
  176. }
  177. }
  178. decl_module! {
  179. pub struct Module<T: Trait> for enum Call where origin: T::Origin {
  180. fn deposit_event() = default;
  181. /// Use next code to create a proposal from Substrate UI's web console:
  182. /// ```js
  183. /// post({ sender: runtime.indices.ss58Decode('F7Gh'), call: calls.proposals.createProposal(2500, "0x123", "0x456", "0x789") }).tie(console.log)
  184. /// ```
  185. fn create_proposal(
  186. origin,
  187. stake: BalanceOf<T>,
  188. name: Vec<u8>,
  189. description: Vec<u8>,
  190. wasm_code: Vec<u8>
  191. ) {
  192. let proposer = ensure_signed(origin)?;
  193. ensure!(Self::can_participate(&proposer), MSG_ONLY_MEMBERS_CAN_PROPOSE);
  194. ensure!(stake >= Self::min_stake(), MSG_STAKE_IS_TOO_LOW);
  195. ensure!(!name.is_empty(), MSG_EMPTY_NAME_PROVIDED);
  196. ensure!(name.len() as u32 <= Self::name_max_len(), MSG_TOO_LONG_NAME);
  197. ensure!(!description.is_empty(), MSG_EMPTY_DESCRIPTION_PROVIDED);
  198. ensure!(description.len() as u32 <= Self::description_max_len(), MSG_TOO_LONG_DESCRIPTION);
  199. ensure!(!wasm_code.is_empty(), MSG_EMPTY_WASM_CODE_PROVIDED);
  200. ensure!(wasm_code.len() as u32 <= Self::wasm_code_max_len(), MSG_TOO_LONG_WASM_CODE);
  201. // Lock proposer's stake:
  202. T::Currency::reserve(&proposer, stake)
  203. .map_err(|_| MSG_STAKE_IS_GREATER_THAN_BALANCE)?;
  204. let proposal_id = Self::proposal_count() + 1;
  205. ProposalCount::put(proposal_id);
  206. // See in substrate repo @ srml/contract/src/wasm/code_cache.rs:73
  207. let wasm_hash = T::Hashing::hash(&wasm_code);
  208. let new_proposal = RuntimeUpgradeProposal {
  209. id: proposal_id,
  210. proposer: proposer.clone(),
  211. stake,
  212. name,
  213. description,
  214. wasm_hash,
  215. proposed_at: Self::current_block(),
  216. status: Active
  217. };
  218. if !<WasmCodeByHash<T>>::exists(wasm_hash) {
  219. <WasmCodeByHash<T>>::insert(wasm_hash, wasm_code);
  220. }
  221. <Proposals<T>>::insert(proposal_id, new_proposal);
  222. ActiveProposalIds::mutate(|ids| ids.push(proposal_id));
  223. Self::deposit_event(RawEvent::ProposalCreated(proposer.clone(), proposal_id));
  224. // Auto-vote with Approve if proposer is a councilor:
  225. if Self::is_councilor(&proposer) {
  226. Self::_process_vote(proposer, proposal_id, Approve)?;
  227. }
  228. }
  229. /// Use next code to create a proposal from Substrate UI's web console:
  230. /// ```js
  231. /// post({ sender: runtime.indices.ss58Decode('F7Gh'), call: calls.proposals.voteOnProposal(1, { option: "Approve", _type: "VoteKind" }) }).tie(console.log)
  232. /// ```
  233. fn vote_on_proposal(origin, proposal_id: u32, vote: VoteKind) {
  234. let voter = ensure_signed(origin)?;
  235. ensure!(Self::is_councilor(&voter), MSG_ONLY_COUNCILORS_CAN_VOTE);
  236. ensure!(<Proposals<T>>::exists(proposal_id), MSG_PROPOSAL_NOT_FOUND);
  237. let proposal = Self::proposals(proposal_id);
  238. ensure!(proposal.status == Active, MSG_PROPOSAL_FINALIZED);
  239. let not_expired = !Self::is_voting_period_expired(proposal.proposed_at);
  240. ensure!(not_expired, MSG_PROPOSAL_EXPIRED);
  241. let did_not_vote_before = !<VoteByAccountAndProposal<T>>::exists((voter.clone(), proposal_id));
  242. ensure!(did_not_vote_before, MSG_YOU_ALREADY_VOTED);
  243. Self::_process_vote(voter, proposal_id, vote)?;
  244. }
  245. // TODO add 'reason' why a proposer wants to cancel (UX + feedback)?
  246. /// Cancel a proposal by its original proposer. Some fee will be withdrawn from his balance.
  247. fn cancel_proposal(origin, proposal_id: u32) {
  248. let proposer = ensure_signed(origin)?;
  249. ensure!(<Proposals<T>>::exists(proposal_id), MSG_PROPOSAL_NOT_FOUND);
  250. let proposal = Self::proposals(proposal_id);
  251. ensure!(proposer == proposal.proposer, MSG_YOU_DONT_OWN_THIS_PROPOSAL);
  252. ensure!(proposal.status == Active, MSG_PROPOSAL_FINALIZED);
  253. // Spend some minimum fee on proposer's balance for canceling a proposal
  254. let fee = Self::cancellation_fee();
  255. let _ = T::Currency::slash_reserved(&proposer, fee);
  256. // Return unspent part of remaining staked deposit (after taking some fee)
  257. let left_stake = proposal.stake - fee;
  258. let _ = T::Currency::unreserve(&proposer, left_stake);
  259. Self::_update_proposal_status(proposal_id, Cancelled)?;
  260. Self::deposit_event(RawEvent::ProposalCanceled(proposer, proposal_id));
  261. }
  262. // Called on every block
  263. fn on_finalize(n: T::BlockNumber) {
  264. if let Err(e) = Self::end_block(n) {
  265. print(e);
  266. }
  267. }
  268. /// Cancel a proposal and return stake without slashing
  269. fn veto_proposal(origin, proposal_id: u32) {
  270. ensure_root(origin)?;
  271. ensure!(<Proposals<T>>::exists(proposal_id), MSG_PROPOSAL_NOT_FOUND);
  272. let proposal = Self::proposals(proposal_id);
  273. ensure!(proposal.status == Active, MSG_PROPOSAL_FINALIZED);
  274. let _ = T::Currency::unreserve(&proposal.proposer, proposal.stake);
  275. Self::_update_proposal_status(proposal_id, Cancelled)?;
  276. Self::deposit_event(RawEvent::ProposalVetoed(proposal_id));
  277. }
  278. fn set_approval_quorum(origin, new_value: u32) {
  279. ensure_root(origin)?;
  280. ensure!(new_value > 0, "approval quorom must be greater than zero");
  281. ApprovalQuorum::put(new_value);
  282. }
  283. }
  284. }
  285. impl<T: Trait> Module<T> {
  286. fn current_block() -> T::BlockNumber {
  287. <system::Module<T>>::block_number()
  288. }
  289. fn can_participate(sender: &T::AccountId) -> bool {
  290. !T::Currency::free_balance(sender).is_zero()
  291. && <membership::members::Module<T>>::is_member_account(sender)
  292. }
  293. fn is_councilor(sender: &T::AccountId) -> bool {
  294. <council::Module<T>>::is_councilor(sender)
  295. }
  296. fn councilors_count() -> u32 {
  297. <council::Module<T>>::active_council().len() as u32
  298. }
  299. fn approval_quorum_seats() -> u32 {
  300. (Self::approval_quorum() * Self::councilors_count()) / 100
  301. }
  302. fn is_voting_period_expired(proposed_at: T::BlockNumber) -> bool {
  303. Self::current_block() >= proposed_at + Self::voting_period()
  304. }
  305. fn _process_vote(voter: T::AccountId, proposal_id: u32, vote: VoteKind) -> dispatch::Result {
  306. let new_vote = (voter.clone(), vote.clone());
  307. if <VotesByProposal<T>>::exists(proposal_id) {
  308. // Append a new vote to other votes on this proposal:
  309. <VotesByProposal<T>>::mutate(proposal_id, |votes| votes.push(new_vote));
  310. } else {
  311. // This is the first vote on this proposal:
  312. <VotesByProposal<T>>::insert(proposal_id, vec![new_vote]);
  313. }
  314. <VoteByAccountAndProposal<T>>::insert((voter.clone(), proposal_id), &vote);
  315. Self::deposit_event(RawEvent::Voted(voter, proposal_id, vote));
  316. Ok(())
  317. }
  318. fn end_block(_now: T::BlockNumber) -> dispatch::Result {
  319. // TODO refactor this method
  320. // TODO iterate over not expired proposals and tally
  321. Self::tally()?;
  322. // TODO approve or reject a proposal
  323. Ok(())
  324. }
  325. /// Get the voters for the current proposal.
  326. pub fn tally() -> dispatch::Result {
  327. let councilors: u32 = Self::councilors_count();
  328. let quorum: u32 = Self::approval_quorum_seats();
  329. for &proposal_id in Self::active_proposal_ids().iter() {
  330. let votes = Self::votes_by_proposal(proposal_id);
  331. let mut abstentions: u32 = 0;
  332. let mut approvals: u32 = 0;
  333. let mut rejections: u32 = 0;
  334. let mut slashes: u32 = 0;
  335. for (_, vote) in votes.iter() {
  336. match vote {
  337. Abstain => abstentions += 1,
  338. Approve => approvals += 1,
  339. Reject => rejections += 1,
  340. Slash => slashes += 1,
  341. }
  342. }
  343. let proposal = Self::proposals(proposal_id);
  344. let is_expired = Self::is_voting_period_expired(proposal.proposed_at);
  345. // We need to check that the council is not empty because otherwise,
  346. // if there is no votes on a proposal it will be counted as if
  347. // all 100% (zero) councilors voted on the proposal and should be approved.
  348. let non_empty_council = councilors > 0;
  349. let all_councilors_voted = non_empty_council && votes.len() as u32 == councilors;
  350. let all_councilors_slashed = non_empty_council && slashes == councilors;
  351. let quorum_reached = quorum > 0 && approvals >= quorum;
  352. // Don't approve a proposal right after quorum reached
  353. // if not all councilors casted their votes.
  354. // Instead let other councilors cast their vote
  355. // up until the proposal's expired.
  356. let new_status: Option<ProposalStatus> = if all_councilors_slashed {
  357. Some(Slashed)
  358. } else if all_councilors_voted {
  359. if quorum_reached {
  360. Some(Approved)
  361. } else {
  362. Some(Rejected)
  363. }
  364. } else if is_expired {
  365. if quorum_reached {
  366. Some(Approved)
  367. } else {
  368. // Proposal has been expired and quorum not reached.
  369. Some(Expired)
  370. }
  371. } else {
  372. // Councilors still have time to vote on this proposal.
  373. None
  374. };
  375. // TODO move next block outside of tally to 'end_block'
  376. if let Some(status) = new_status {
  377. Self::_update_proposal_status(proposal_id, status.clone())?;
  378. let tally_result = TallyResult {
  379. proposal_id,
  380. abstentions,
  381. approvals,
  382. rejections,
  383. slashes,
  384. status,
  385. finalized_at: Self::current_block(),
  386. };
  387. <TallyResults<T>>::insert(proposal_id, &tally_result);
  388. Self::deposit_event(RawEvent::TallyFinalized(tally_result));
  389. }
  390. }
  391. Ok(())
  392. }
  393. /// Updates proposal status and removes proposal from active ids.
  394. fn _update_proposal_status(proposal_id: u32, new_status: ProposalStatus) -> dispatch::Result {
  395. let all_active_ids = Self::active_proposal_ids();
  396. let all_len = all_active_ids.len();
  397. let other_active_ids: Vec<u32> = all_active_ids
  398. .into_iter()
  399. .filter(|&id| id != proposal_id)
  400. .collect();
  401. let not_found_in_active = other_active_ids.len() == all_len;
  402. if not_found_in_active {
  403. // Seems like this proposal's status has been updated and removed from active.
  404. Err(MSG_PROPOSAL_STATUS_ALREADY_UPDATED)
  405. } else {
  406. let pid = proposal_id.clone();
  407. match new_status {
  408. Slashed => Self::_slash_proposal(pid)?,
  409. Rejected | Expired => Self::_reject_proposal(pid)?,
  410. Approved => Self::_approve_proposal(pid)?,
  411. Active | Cancelled => { /* nothing */ }
  412. }
  413. ActiveProposalIds::put(other_active_ids);
  414. <Proposals<T>>::mutate(proposal_id, |p| p.status = new_status.clone());
  415. Self::deposit_event(RawEvent::ProposalStatusUpdated(proposal_id, new_status));
  416. Ok(())
  417. }
  418. }
  419. /// Slash a proposal. The staked deposit will be slashed.
  420. fn _slash_proposal(proposal_id: u32) -> dispatch::Result {
  421. let proposal = Self::proposals(proposal_id);
  422. // Slash proposer's stake:
  423. let _ = T::Currency::slash_reserved(&proposal.proposer, proposal.stake);
  424. Ok(())
  425. }
  426. /// Reject a proposal. The staked deposit will be returned to a proposer.
  427. fn _reject_proposal(proposal_id: u32) -> dispatch::Result {
  428. let proposal = Self::proposals(proposal_id);
  429. let proposer = proposal.proposer;
  430. // Spend some minimum fee on proposer's balance to prevent spamming attacks:
  431. let fee = Self::rejection_fee();
  432. let _ = T::Currency::slash_reserved(&proposer, fee);
  433. // Return unspent part of remaining staked deposit (after taking some fee):
  434. let left_stake = proposal.stake - fee;
  435. let _ = T::Currency::unreserve(&proposer, left_stake);
  436. Ok(())
  437. }
  438. /// Approve a proposal. The staked deposit will be returned.
  439. fn _approve_proposal(proposal_id: u32) -> dispatch::Result {
  440. let proposal = Self::proposals(proposal_id);
  441. let wasm_code = Self::wasm_code_by_hash(proposal.wasm_hash);
  442. // Return staked deposit to proposer:
  443. let _ = T::Currency::unreserve(&proposal.proposer, proposal.stake);
  444. // Update wasm code of node's runtime:
  445. <system::Module<T>>::set_code(system::RawOrigin::Root.into(), wasm_code)?;
  446. Self::deposit_event(RawEvent::RuntimeUpdated(proposal_id, proposal.wasm_hash));
  447. Ok(())
  448. }
  449. }
  450. #[cfg(test)]
  451. mod tests {
  452. use super::*;
  453. use primitives::H256;
  454. // The testing primitives are very useful for avoiding having to work with signatures
  455. // or public keys. `u64` is used as the `AccountId` and no `Signature`s are requried.
  456. use runtime_primitives::{
  457. testing::Header,
  458. traits::{BlakeTwo256, IdentityLookup},
  459. Perbill,
  460. };
  461. use srml_support::*;
  462. impl_outer_origin! {
  463. pub enum Origin for Test {}
  464. }
  465. // Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
  466. #[derive(Clone, PartialEq, Eq, Debug)]
  467. pub struct Test;
  468. parameter_types! {
  469. pub const BlockHashCount: u64 = 250;
  470. pub const MaximumBlockWeight: u32 = 1024;
  471. pub const MaximumBlockLength: u32 = 2 * 1024;
  472. pub const AvailableBlockRatio: Perbill = Perbill::one();
  473. pub const MinimumPeriod: u64 = 5;
  474. }
  475. impl system::Trait for Test {
  476. type Origin = Origin;
  477. type Index = u64;
  478. type BlockNumber = u64;
  479. type Call = ();
  480. type Hash = H256;
  481. type Hashing = BlakeTwo256;
  482. type AccountId = u64;
  483. type Lookup = IdentityLookup<Self::AccountId>;
  484. type Header = Header;
  485. type Event = ();
  486. type BlockHashCount = BlockHashCount;
  487. type MaximumBlockWeight = MaximumBlockWeight;
  488. type MaximumBlockLength = MaximumBlockLength;
  489. type AvailableBlockRatio = AvailableBlockRatio;
  490. type Version = ();
  491. }
  492. impl timestamp::Trait for Test {
  493. type Moment = u64;
  494. type OnTimestampSet = ();
  495. type MinimumPeriod = MinimumPeriod;
  496. }
  497. parameter_types! {
  498. pub const ExistentialDeposit: u32 = 0;
  499. pub const TransferFee: u32 = 0;
  500. pub const CreationFee: u32 = 0;
  501. pub const TransactionBaseFee: u32 = 1;
  502. pub const TransactionByteFee: u32 = 0;
  503. pub const InitialMembersBalance: u32 = 0;
  504. }
  505. impl balances::Trait for Test {
  506. /// The type for recording an account's balance.
  507. type Balance = u64;
  508. /// What to do if an account's free balance gets zeroed.
  509. type OnFreeBalanceZero = ();
  510. /// What to do if a new account is created.
  511. type OnNewAccount = ();
  512. /// The ubiquitous event type.
  513. type Event = ();
  514. type DustRemoval = ();
  515. type TransferPayment = ();
  516. type ExistentialDeposit = ExistentialDeposit;
  517. type TransferFee = TransferFee;
  518. type CreationFee = CreationFee;
  519. }
  520. impl council::Trait for Test {
  521. type Event = ();
  522. type CouncilTermEnded = ();
  523. }
  524. impl GovernanceCurrency for Test {
  525. type Currency = balances::Module<Self>;
  526. }
  527. impl membership::members::Trait for Test {
  528. type Event = ();
  529. type MemberId = u32;
  530. type PaidTermId = u32;
  531. type SubscriptionId = u32;
  532. type ActorId = u32;
  533. type InitialMembersBalance = InitialMembersBalance;
  534. }
  535. impl Trait for Test {
  536. type Event = ();
  537. }
  538. type System = system::Module<Test>;
  539. type Balances = balances::Module<Test>;
  540. type Proposals = Module<Test>;
  541. const COUNCILOR1: u64 = 1;
  542. const COUNCILOR2: u64 = 2;
  543. const COUNCILOR3: u64 = 3;
  544. const COUNCILOR4: u64 = 4;
  545. const COUNCILOR5: u64 = 5;
  546. const PROPOSER1: u64 = 11;
  547. const PROPOSER2: u64 = 12;
  548. const NOT_COUNCILOR: u64 = 22;
  549. const ALL_COUNCILORS: [u64; 5] = [COUNCILOR1, COUNCILOR2, COUNCILOR3, COUNCILOR4, COUNCILOR5];
  550. // TODO Figure out how to test Events in test... (low priority)
  551. // mod proposals {
  552. // pub use ::Event;
  553. // }
  554. // impl_outer_event!{
  555. // pub enum TestEvent for Test {
  556. // balances<T>,system<T>,proposals<T>,
  557. // }
  558. // }
  559. // This function basically just builds a genesis storage key/value store according to
  560. // our desired mockup.
  561. fn new_test_ext() -> runtime_io::TestExternalities {
  562. let mut t = system::GenesisConfig::default()
  563. .build_storage::<Test>()
  564. .unwrap();
  565. // We use default for brevity, but you can configure as desired if needed.
  566. balances::GenesisConfig::<Test>::default()
  567. .assimilate_storage(&mut t)
  568. .unwrap();
  569. let council_mock: council::Seats<u64, u64> = ALL_COUNCILORS
  570. .iter()
  571. .map(|&c| council::Seat {
  572. member: c,
  573. stake: 0u64,
  574. backers: vec![],
  575. })
  576. .collect();
  577. council::GenesisConfig::<Test> {
  578. active_council: council_mock,
  579. term_ends_at: 0,
  580. }
  581. .assimilate_storage(&mut t)
  582. .unwrap();
  583. membership::members::GenesisConfig::<Test> {
  584. default_paid_membership_fee: 0,
  585. members: vec![
  586. (PROPOSER1, "alice".into(), "".into(), "".into()),
  587. (PROPOSER2, "bobby".into(), "".into(), "".into()),
  588. (COUNCILOR1, "councilor1".into(), "".into(), "".into()),
  589. (COUNCILOR2, "councilor2".into(), "".into(), "".into()),
  590. (COUNCILOR3, "councilor3".into(), "".into(), "".into()),
  591. (COUNCILOR4, "councilor4".into(), "".into(), "".into()),
  592. (COUNCILOR5, "councilor5".into(), "".into(), "".into()),
  593. ],
  594. }
  595. .assimilate_storage(&mut t)
  596. .unwrap();
  597. // t.extend(GenesisConfig::<Test>{
  598. // // Here we can override defaults.
  599. // }.build_storage().unwrap().0);
  600. t.into()
  601. }
  602. /// A shortcut to get minimum stake in tests.
  603. fn min_stake() -> u64 {
  604. Proposals::min_stake()
  605. }
  606. /// A shortcut to get cancellation fee in tests.
  607. fn cancellation_fee() -> u64 {
  608. Proposals::cancellation_fee()
  609. }
  610. /// A shortcut to get rejection fee in tests.
  611. fn rejection_fee() -> u64 {
  612. Proposals::rejection_fee()
  613. }
  614. /// Initial balance of Proposer 1.
  615. fn initial_balance() -> u64 {
  616. (min_stake() as f64 * 2.5) as u64
  617. }
  618. fn name() -> Vec<u8> {
  619. b"Proposal Name".to_vec()
  620. }
  621. fn description() -> Vec<u8> {
  622. b"Proposal Description".to_vec()
  623. }
  624. fn wasm_code() -> Vec<u8> {
  625. b"Proposal Wasm Code".to_vec()
  626. }
  627. fn _create_default_proposal() -> dispatch::Result {
  628. _create_proposal(None, None, None, None, None)
  629. }
  630. fn _create_proposal(
  631. origin: Option<u64>,
  632. stake: Option<u64>,
  633. name: Option<Vec<u8>>,
  634. description: Option<Vec<u8>>,
  635. wasm_code: Option<Vec<u8>>,
  636. ) -> dispatch::Result {
  637. Proposals::create_proposal(
  638. Origin::signed(origin.unwrap_or(PROPOSER1)),
  639. stake.unwrap_or(min_stake()),
  640. name.unwrap_or(self::name()),
  641. description.unwrap_or(self::description()),
  642. wasm_code.unwrap_or(self::wasm_code()),
  643. )
  644. }
  645. fn get_runtime_code() -> Option<Vec<u8>> {
  646. storage::unhashed::get_raw(well_known_keys::CODE)
  647. }
  648. macro_rules! assert_runtime_code_empty {
  649. () => {
  650. assert_eq!(get_runtime_code(), Some(vec![]))
  651. };
  652. }
  653. macro_rules! assert_runtime_code {
  654. ($code:expr) => {
  655. assert_eq!(get_runtime_code(), Some($code))
  656. };
  657. }
  658. #[test]
  659. fn check_default_values() {
  660. new_test_ext().execute_with(|| {
  661. assert_eq!(Proposals::approval_quorum(), DEFAULT_APPROVAL_QUORUM);
  662. assert_eq!(
  663. Proposals::min_stake(),
  664. BalanceOf::<Test>::from(DEFAULT_MIN_STAKE)
  665. );
  666. assert_eq!(
  667. Proposals::cancellation_fee(),
  668. BalanceOf::<Test>::from(DEFAULT_CANCELLATION_FEE)
  669. );
  670. assert_eq!(
  671. Proposals::rejection_fee(),
  672. BalanceOf::<Test>::from(DEFAULT_REJECTION_FEE)
  673. );
  674. assert_eq!(Proposals::name_max_len(), DEFAULT_NAME_MAX_LEN);
  675. assert_eq!(
  676. Proposals::description_max_len(),
  677. DEFAULT_DESCRIPTION_MAX_LEN
  678. );
  679. assert_eq!(Proposals::wasm_code_max_len(), DEFAULT_WASM_CODE_MAX_LEN);
  680. assert_eq!(Proposals::proposal_count(), 0);
  681. assert!(Proposals::active_proposal_ids().is_empty());
  682. });
  683. }
  684. #[test]
  685. fn member_create_proposal() {
  686. new_test_ext().execute_with(|| {
  687. let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
  688. assert_ok!(_create_default_proposal());
  689. assert_eq!(Proposals::active_proposal_ids().len(), 1);
  690. assert_eq!(Proposals::active_proposal_ids()[0], 1);
  691. let wasm_hash = BlakeTwo256::hash(&wasm_code());
  692. let expected_proposal = RuntimeUpgradeProposal {
  693. id: 1,
  694. proposer: PROPOSER1,
  695. stake: min_stake(),
  696. name: name(),
  697. description: description(),
  698. wasm_hash,
  699. proposed_at: 1,
  700. status: Active,
  701. };
  702. assert_eq!(Proposals::proposals(1), expected_proposal);
  703. // Check that stake amount has been locked on proposer's balance:
  704. assert_eq!(
  705. Balances::free_balance(PROPOSER1),
  706. initial_balance() - min_stake()
  707. );
  708. assert_eq!(Balances::reserved_balance(PROPOSER1), min_stake());
  709. // TODO expect event ProposalCreated(AccountId, u32)
  710. });
  711. }
  712. #[test]
  713. fn not_member_cannot_create_proposal() {
  714. new_test_ext().execute_with(|| {
  715. // In this test a proposer has an empty balance
  716. // thus he is not considered as a member.
  717. assert_eq!(
  718. _create_default_proposal(),
  719. Err(MSG_ONLY_MEMBERS_CAN_PROPOSE)
  720. );
  721. });
  722. }
  723. #[test]
  724. fn cannot_create_proposal_with_small_stake() {
  725. new_test_ext().execute_with(|| {
  726. let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
  727. assert_eq!(
  728. _create_proposal(None, Some(min_stake() - 1), None, None, None),
  729. Err(MSG_STAKE_IS_TOO_LOW)
  730. );
  731. // Check that balances remain unchanged afer a failed attempt to create a proposal:
  732. assert_eq!(Balances::free_balance(PROPOSER1), initial_balance());
  733. assert_eq!(Balances::reserved_balance(PROPOSER1), 0);
  734. });
  735. }
  736. #[test]
  737. fn cannot_create_proposal_when_stake_is_greater_than_balance() {
  738. new_test_ext().execute_with(|| {
  739. let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
  740. assert_eq!(
  741. _create_proposal(None, Some(initial_balance() + 1), None, None, None),
  742. Err(MSG_STAKE_IS_GREATER_THAN_BALANCE)
  743. );
  744. // Check that balances remain unchanged afer a failed attempt to create a proposal:
  745. assert_eq!(Balances::free_balance(PROPOSER1), initial_balance());
  746. assert_eq!(Balances::reserved_balance(PROPOSER1), 0);
  747. });
  748. }
  749. #[test]
  750. fn cannot_create_proposal_with_empty_values() {
  751. new_test_ext().execute_with(|| {
  752. let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
  753. // Empty name:
  754. assert_eq!(
  755. _create_proposal(None, None, Some(vec![]), None, None),
  756. Err(MSG_EMPTY_NAME_PROVIDED)
  757. );
  758. // Empty description:
  759. assert_eq!(
  760. _create_proposal(None, None, None, Some(vec![]), None),
  761. Err(MSG_EMPTY_DESCRIPTION_PROVIDED)
  762. );
  763. // Empty WASM code:
  764. assert_eq!(
  765. _create_proposal(None, None, None, None, Some(vec![])),
  766. Err(MSG_EMPTY_WASM_CODE_PROVIDED)
  767. );
  768. });
  769. }
  770. #[test]
  771. fn cannot_create_proposal_with_too_long_values() {
  772. new_test_ext().execute_with(|| {
  773. let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
  774. // Too long name:
  775. assert_eq!(
  776. _create_proposal(None, None, Some(too_long_name()), None, None),
  777. Err(MSG_TOO_LONG_NAME)
  778. );
  779. // Too long description:
  780. assert_eq!(
  781. _create_proposal(None, None, None, Some(too_long_description()), None),
  782. Err(MSG_TOO_LONG_DESCRIPTION)
  783. );
  784. // Too long WASM code:
  785. assert_eq!(
  786. _create_proposal(None, None, None, None, Some(too_long_wasm_code())),
  787. Err(MSG_TOO_LONG_WASM_CODE)
  788. );
  789. });
  790. }
  791. fn too_long_name() -> Vec<u8> {
  792. vec![65; Proposals::name_max_len() as usize + 1]
  793. }
  794. fn too_long_description() -> Vec<u8> {
  795. vec![65; Proposals::description_max_len() as usize + 1]
  796. }
  797. fn too_long_wasm_code() -> Vec<u8> {
  798. vec![65; Proposals::wasm_code_max_len() as usize + 1]
  799. }
  800. // -------------------------------------------------------------------
  801. // Cancellation
  802. #[test]
  803. fn owner_cancel_proposal() {
  804. new_test_ext().execute_with(|| {
  805. let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
  806. assert_ok!(_create_default_proposal());
  807. assert_ok!(Proposals::cancel_proposal(Origin::signed(PROPOSER1), 1));
  808. assert_eq!(Proposals::proposals(1).status, Cancelled);
  809. assert!(Proposals::active_proposal_ids().is_empty());
  810. // Check that proposer's balance reduced by cancellation fee and other part of his stake returned to his balance:
  811. assert_eq!(
  812. Balances::free_balance(PROPOSER1),
  813. initial_balance() - cancellation_fee()
  814. );
  815. assert_eq!(Balances::reserved_balance(PROPOSER1), 0);
  816. // TODO expect event ProposalCancelled(AccountId, u32)
  817. });
  818. }
  819. #[test]
  820. fn owner_cannot_cancel_proposal_if_its_finalized() {
  821. new_test_ext().execute_with(|| {
  822. let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
  823. assert_ok!(_create_default_proposal());
  824. assert_ok!(Proposals::cancel_proposal(Origin::signed(PROPOSER1), 1));
  825. assert_eq!(Proposals::proposals(1).status, Cancelled);
  826. // Get balances updated after cancelling a proposal:
  827. let updated_free_balance = Balances::free_balance(PROPOSER1);
  828. let updated_reserved_balance = Balances::reserved_balance(PROPOSER1);
  829. assert_eq!(
  830. Proposals::cancel_proposal(Origin::signed(PROPOSER1), 1),
  831. Err(MSG_PROPOSAL_FINALIZED)
  832. );
  833. // Check that proposer's balance and locked stake haven't been changed:
  834. assert_eq!(Balances::free_balance(PROPOSER1), updated_free_balance);
  835. assert_eq!(
  836. Balances::reserved_balance(PROPOSER1),
  837. updated_reserved_balance
  838. );
  839. });
  840. }
  841. #[test]
  842. fn not_owner_cannot_cancel_proposal() {
  843. new_test_ext().execute_with(|| {
  844. let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
  845. let _ = Balances::deposit_creating(&PROPOSER2, initial_balance());
  846. assert_ok!(_create_default_proposal());
  847. assert_eq!(
  848. Proposals::cancel_proposal(Origin::signed(PROPOSER2), 1),
  849. Err(MSG_YOU_DONT_OWN_THIS_PROPOSAL)
  850. );
  851. });
  852. }
  853. // -------------------------------------------------------------------
  854. // Voting
  855. #[test]
  856. fn councilor_vote_on_proposal() {
  857. new_test_ext().execute_with(|| {
  858. let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
  859. assert_ok!(_create_default_proposal());
  860. assert_ok!(Proposals::vote_on_proposal(
  861. Origin::signed(COUNCILOR1),
  862. 1,
  863. Approve
  864. ));
  865. // Check that a vote has been saved:
  866. assert_eq!(Proposals::votes_by_proposal(1), vec![(COUNCILOR1, Approve)]);
  867. assert_eq!(
  868. Proposals::vote_by_account_and_proposal((COUNCILOR1, 1)),
  869. Approve
  870. );
  871. // TODO expect event Voted(PROPOSER1, 1, Approve)
  872. });
  873. }
  874. #[test]
  875. fn councilor_cannot_vote_on_proposal_twice() {
  876. new_test_ext().execute_with(|| {
  877. let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
  878. assert_ok!(_create_default_proposal());
  879. assert_ok!(Proposals::vote_on_proposal(
  880. Origin::signed(COUNCILOR1),
  881. 1,
  882. Approve
  883. ));
  884. assert_eq!(
  885. Proposals::vote_on_proposal(Origin::signed(COUNCILOR1), 1, Approve),
  886. Err(MSG_YOU_ALREADY_VOTED)
  887. );
  888. });
  889. }
  890. #[test]
  891. fn autovote_with_approve_when_councilor_creates_proposal() {
  892. new_test_ext().execute_with(|| {
  893. let _ = Balances::deposit_creating(&COUNCILOR1, initial_balance());
  894. assert_ok!(_create_proposal(Some(COUNCILOR1), None, None, None, None));
  895. // Check that a vote has been sent automatically,
  896. // such as the proposer is a councilor:
  897. assert_eq!(Proposals::votes_by_proposal(1), vec![(COUNCILOR1, Approve)]);
  898. assert_eq!(
  899. Proposals::vote_by_account_and_proposal((COUNCILOR1, 1)),
  900. Approve
  901. );
  902. });
  903. }
  904. #[test]
  905. fn not_councilor_cannot_vote_on_proposal() {
  906. new_test_ext().execute_with(|| {
  907. let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
  908. assert_ok!(_create_default_proposal());
  909. assert_eq!(
  910. Proposals::vote_on_proposal(Origin::signed(NOT_COUNCILOR), 1, Approve),
  911. Err(MSG_ONLY_COUNCILORS_CAN_VOTE)
  912. );
  913. });
  914. }
  915. #[test]
  916. fn councilor_cannot_vote_on_proposal_if_it_has_been_cancelled() {
  917. new_test_ext().execute_with(|| {
  918. let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
  919. assert_ok!(_create_default_proposal());
  920. assert_ok!(Proposals::cancel_proposal(Origin::signed(PROPOSER1), 1));
  921. assert_eq!(
  922. Proposals::vote_on_proposal(Origin::signed(COUNCILOR1), 1, Approve),
  923. Err(MSG_PROPOSAL_FINALIZED)
  924. );
  925. });
  926. }
  927. #[test]
  928. fn councilor_cannot_vote_on_proposal_if_tally_has_been_finalized() {
  929. new_test_ext().execute_with(|| {
  930. let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
  931. assert_ok!(_create_default_proposal());
  932. // All councilors vote with 'Approve' on proposal:
  933. let mut expected_votes: Vec<(u64, VoteKind)> = vec![];
  934. for &councilor in ALL_COUNCILORS.iter() {
  935. expected_votes.push((councilor, Approve));
  936. assert_ok!(Proposals::vote_on_proposal(
  937. Origin::signed(councilor),
  938. 1,
  939. Approve
  940. ));
  941. assert_eq!(
  942. Proposals::vote_by_account_and_proposal((councilor, 1)),
  943. Approve
  944. );
  945. }
  946. assert_eq!(Proposals::votes_by_proposal(1), expected_votes);
  947. System::set_block_number(2);
  948. let _ = Proposals::end_block(2);
  949. assert!(Proposals::active_proposal_ids().is_empty());
  950. assert_eq!(Proposals::proposals(1).status, Approved);
  951. // Try to vote on finalized proposal:
  952. assert_eq!(
  953. Proposals::vote_on_proposal(Origin::signed(COUNCILOR1), 1, Reject),
  954. Err(MSG_PROPOSAL_FINALIZED)
  955. );
  956. });
  957. }
  958. // -------------------------------------------------------------------
  959. // Tally + Outcome:
  960. #[test]
  961. fn approve_proposal_when_all_councilors_approved_it() {
  962. new_test_ext().execute_with(|| {
  963. let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
  964. assert_ok!(_create_default_proposal());
  965. // All councilors approved:
  966. let mut expected_votes: Vec<(u64, VoteKind)> = vec![];
  967. for &councilor in ALL_COUNCILORS.iter() {
  968. expected_votes.push((councilor, Approve));
  969. assert_ok!(Proposals::vote_on_proposal(
  970. Origin::signed(councilor),
  971. 1,
  972. Approve
  973. ));
  974. assert_eq!(
  975. Proposals::vote_by_account_and_proposal((councilor, 1)),
  976. Approve
  977. );
  978. }
  979. assert_eq!(Proposals::votes_by_proposal(1), expected_votes);
  980. assert_runtime_code_empty!();
  981. System::set_block_number(2);
  982. let _ = Proposals::end_block(2);
  983. // Check that runtime code has been updated after proposal approved.
  984. assert_runtime_code!(wasm_code());
  985. assert!(Proposals::active_proposal_ids().is_empty());
  986. assert_eq!(Proposals::proposals(1).status, Approved);
  987. assert_eq!(
  988. Proposals::tally_results(1),
  989. TallyResult {
  990. proposal_id: 1,
  991. abstentions: 0,
  992. approvals: ALL_COUNCILORS.len() as u32,
  993. rejections: 0,
  994. slashes: 0,
  995. status: Approved,
  996. finalized_at: 2
  997. }
  998. );
  999. // Check that proposer's stake has been added back to his balance:
  1000. assert_eq!(Balances::free_balance(PROPOSER1), initial_balance());
  1001. assert_eq!(Balances::reserved_balance(PROPOSER1), 0);
  1002. // TODO expect event ProposalStatusUpdated(1, Approved)
  1003. });
  1004. }
  1005. #[test]
  1006. fn approve_proposal_when_all_councilors_voted_and_only_quorum_approved() {
  1007. new_test_ext().execute_with(|| {
  1008. let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
  1009. assert_ok!(_create_default_proposal());
  1010. // Only a quorum of councilors approved, others rejected:
  1011. let councilors = Proposals::councilors_count();
  1012. let approvals = Proposals::approval_quorum_seats();
  1013. let rejections = councilors - approvals;
  1014. for i in 0..councilors as usize {
  1015. let vote = if (i as u32) < approvals {
  1016. Approve
  1017. } else {
  1018. Reject
  1019. };
  1020. assert_ok!(Proposals::vote_on_proposal(
  1021. Origin::signed(ALL_COUNCILORS[i]),
  1022. 1,
  1023. vote
  1024. ));
  1025. }
  1026. assert_eq!(Proposals::votes_by_proposal(1).len() as u32, councilors);
  1027. assert_runtime_code_empty!();
  1028. System::set_block_number(2);
  1029. let _ = Proposals::end_block(2);
  1030. // Check that runtime code has been updated after proposal approved.
  1031. assert_runtime_code!(wasm_code());
  1032. assert!(Proposals::active_proposal_ids().is_empty());
  1033. assert_eq!(Proposals::proposals(1).status, Approved);
  1034. assert_eq!(
  1035. Proposals::tally_results(1),
  1036. TallyResult {
  1037. proposal_id: 1,
  1038. abstentions: 0,
  1039. approvals: approvals,
  1040. rejections: rejections,
  1041. slashes: 0,
  1042. status: Approved,
  1043. finalized_at: 2
  1044. }
  1045. );
  1046. // Check that proposer's stake has been added back to his balance:
  1047. assert_eq!(Balances::free_balance(PROPOSER1), initial_balance());
  1048. assert_eq!(Balances::reserved_balance(PROPOSER1), 0);
  1049. // TODO expect event ProposalStatusUpdated(1, Approved)
  1050. });
  1051. }
  1052. #[test]
  1053. fn approve_proposal_when_voting_period_expired_if_only_quorum_voted() {
  1054. new_test_ext().execute_with(|| {
  1055. let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
  1056. assert_ok!(_create_default_proposal());
  1057. // Only quorum of councilors approved, other councilors didn't vote:
  1058. let approvals = Proposals::approval_quorum_seats();
  1059. for i in 0..approvals as usize {
  1060. let vote = if (i as u32) < approvals {
  1061. Approve
  1062. } else {
  1063. Slash
  1064. };
  1065. assert_ok!(Proposals::vote_on_proposal(
  1066. Origin::signed(ALL_COUNCILORS[i]),
  1067. 1,
  1068. vote
  1069. ));
  1070. }
  1071. assert_eq!(Proposals::votes_by_proposal(1).len() as u32, approvals);
  1072. assert_runtime_code_empty!();
  1073. let expiration_block = System::block_number() + Proposals::voting_period();
  1074. System::set_block_number(2);
  1075. let _ = Proposals::end_block(2);
  1076. // Check that runtime code has NOT been updated yet,
  1077. // because not all councilors voted and voting period is not expired yet.
  1078. assert_runtime_code_empty!();
  1079. System::set_block_number(expiration_block);
  1080. let _ = Proposals::end_block(expiration_block);
  1081. // Check that runtime code has been updated after proposal approved.
  1082. assert_runtime_code!(wasm_code());
  1083. assert!(Proposals::active_proposal_ids().is_empty());
  1084. assert_eq!(Proposals::proposals(1).status, Approved);
  1085. assert_eq!(
  1086. Proposals::tally_results(1),
  1087. TallyResult {
  1088. proposal_id: 1,
  1089. abstentions: 0,
  1090. approvals: approvals,
  1091. rejections: 0,
  1092. slashes: 0,
  1093. status: Approved,
  1094. finalized_at: expiration_block
  1095. }
  1096. );
  1097. // Check that proposer's stake has been added back to his balance:
  1098. assert_eq!(Balances::free_balance(PROPOSER1), initial_balance());
  1099. assert_eq!(Balances::reserved_balance(PROPOSER1), 0);
  1100. // TODO expect event ProposalStatusUpdated(1, Approved)
  1101. });
  1102. }
  1103. #[test]
  1104. fn reject_proposal_when_all_councilors_voted_and_quorum_not_reached() {
  1105. new_test_ext().execute_with(|| {
  1106. let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
  1107. assert_ok!(_create_default_proposal());
  1108. // Less than a quorum of councilors approved, while others abstained:
  1109. let councilors = Proposals::councilors_count();
  1110. let approvals = Proposals::approval_quorum_seats() - 1;
  1111. let abstentions = councilors - approvals;
  1112. for i in 0..councilors as usize {
  1113. let vote = if (i as u32) < approvals {
  1114. Approve
  1115. } else {
  1116. Abstain
  1117. };
  1118. assert_ok!(Proposals::vote_on_proposal(
  1119. Origin::signed(ALL_COUNCILORS[i]),
  1120. 1,
  1121. vote
  1122. ));
  1123. }
  1124. assert_eq!(Proposals::votes_by_proposal(1).len() as u32, councilors);
  1125. assert_runtime_code_empty!();
  1126. System::set_block_number(2);
  1127. let _ = Proposals::end_block(2);
  1128. // Check that runtime code has NOT been updated after proposal slashed.
  1129. assert_runtime_code_empty!();
  1130. assert!(Proposals::active_proposal_ids().is_empty());
  1131. assert_eq!(Proposals::proposals(1).status, Rejected);
  1132. assert_eq!(
  1133. Proposals::tally_results(1),
  1134. TallyResult {
  1135. proposal_id: 1,
  1136. abstentions: abstentions,
  1137. approvals: approvals,
  1138. rejections: 0,
  1139. slashes: 0,
  1140. status: Rejected,
  1141. finalized_at: 2
  1142. }
  1143. );
  1144. // Check that proposer's balance reduced by burnt stake:
  1145. assert_eq!(
  1146. Balances::free_balance(PROPOSER1),
  1147. initial_balance() - rejection_fee()
  1148. );
  1149. assert_eq!(Balances::reserved_balance(PROPOSER1), 0);
  1150. // TODO expect event ProposalStatusUpdated(1, Rejected)
  1151. });
  1152. }
  1153. #[test]
  1154. fn reject_proposal_when_all_councilors_rejected_it() {
  1155. new_test_ext().execute_with(|| {
  1156. let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
  1157. assert_ok!(_create_default_proposal());
  1158. // All councilors rejected:
  1159. let mut expected_votes: Vec<(u64, VoteKind)> = vec![];
  1160. for &councilor in ALL_COUNCILORS.iter() {
  1161. expected_votes.push((councilor, Reject));
  1162. assert_ok!(Proposals::vote_on_proposal(
  1163. Origin::signed(councilor),
  1164. 1,
  1165. Reject
  1166. ));
  1167. assert_eq!(
  1168. Proposals::vote_by_account_and_proposal((councilor, 1)),
  1169. Reject
  1170. );
  1171. }
  1172. assert_eq!(Proposals::votes_by_proposal(1), expected_votes);
  1173. assert_runtime_code_empty!();
  1174. System::set_block_number(2);
  1175. let _ = Proposals::end_block(2);
  1176. // Check that runtime code has NOT been updated after proposal rejected.
  1177. assert_runtime_code_empty!();
  1178. assert!(Proposals::active_proposal_ids().is_empty());
  1179. assert_eq!(Proposals::proposals(1).status, Rejected);
  1180. assert_eq!(
  1181. Proposals::tally_results(1),
  1182. TallyResult {
  1183. proposal_id: 1,
  1184. abstentions: 0,
  1185. approvals: 0,
  1186. rejections: ALL_COUNCILORS.len() as u32,
  1187. slashes: 0,
  1188. status: Rejected,
  1189. finalized_at: 2
  1190. }
  1191. );
  1192. // Check that proposer's balance reduced by burnt stake:
  1193. assert_eq!(
  1194. Balances::free_balance(PROPOSER1),
  1195. initial_balance() - rejection_fee()
  1196. );
  1197. assert_eq!(Balances::reserved_balance(PROPOSER1), 0);
  1198. // TODO expect event ProposalStatusUpdated(1, Rejected)
  1199. });
  1200. }
  1201. #[test]
  1202. fn slash_proposal_when_all_councilors_slashed_it() {
  1203. new_test_ext().execute_with(|| {
  1204. let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
  1205. assert_ok!(_create_default_proposal());
  1206. // All councilors slashed:
  1207. let mut expected_votes: Vec<(u64, VoteKind)> = vec![];
  1208. for &councilor in ALL_COUNCILORS.iter() {
  1209. expected_votes.push((councilor, Slash));
  1210. assert_ok!(Proposals::vote_on_proposal(
  1211. Origin::signed(councilor),
  1212. 1,
  1213. Slash
  1214. ));
  1215. assert_eq!(
  1216. Proposals::vote_by_account_and_proposal((councilor, 1)),
  1217. Slash
  1218. );
  1219. }
  1220. assert_eq!(Proposals::votes_by_proposal(1), expected_votes);
  1221. assert_runtime_code_empty!();
  1222. System::set_block_number(2);
  1223. let _ = Proposals::end_block(2);
  1224. // Check that runtime code has NOT been updated after proposal slashed.
  1225. assert_runtime_code_empty!();
  1226. assert!(Proposals::active_proposal_ids().is_empty());
  1227. assert_eq!(Proposals::proposals(1).status, Slashed);
  1228. assert_eq!(
  1229. Proposals::tally_results(1),
  1230. TallyResult {
  1231. proposal_id: 1,
  1232. abstentions: 0,
  1233. approvals: 0,
  1234. rejections: 0,
  1235. slashes: ALL_COUNCILORS.len() as u32,
  1236. status: Slashed,
  1237. finalized_at: 2
  1238. }
  1239. );
  1240. // Check that proposer's balance reduced by burnt stake:
  1241. assert_eq!(
  1242. Balances::free_balance(PROPOSER1),
  1243. initial_balance() - min_stake()
  1244. );
  1245. assert_eq!(Balances::reserved_balance(PROPOSER1), 0);
  1246. // TODO expect event ProposalStatusUpdated(1, Slashed)
  1247. // TODO fix: event log assertion doesn't work and return empty event in every record
  1248. // assert_eq!(*System::events().last().unwrap(),
  1249. // EventRecord {
  1250. // phase: Phase::ApplyExtrinsic(0),
  1251. // event: RawEvent::ProposalStatusUpdated(1, Slashed),
  1252. // }
  1253. // );
  1254. });
  1255. }
  1256. // In this case a proposal will be marked as 'Expired'
  1257. // and it will be processed in the same way as if it has been rejected.
  1258. #[test]
  1259. fn expire_proposal_when_not_all_councilors_voted_and_quorum_not_reached() {
  1260. new_test_ext().execute_with(|| {
  1261. let _ = Balances::deposit_creating(&PROPOSER1, initial_balance());
  1262. assert_ok!(_create_default_proposal());
  1263. // Less than a quorum of councilors approved:
  1264. let approvals = Proposals::approval_quorum_seats() - 1;
  1265. for i in 0..approvals as usize {
  1266. let vote = if (i as u32) < approvals {
  1267. Approve
  1268. } else {
  1269. Slash
  1270. };
  1271. assert_ok!(Proposals::vote_on_proposal(
  1272. Origin::signed(ALL_COUNCILORS[i]),
  1273. 1,
  1274. vote
  1275. ));
  1276. }
  1277. assert_eq!(Proposals::votes_by_proposal(1).len() as u32, approvals);
  1278. assert_runtime_code_empty!();
  1279. let expiration_block = System::block_number() + Proposals::voting_period();
  1280. System::set_block_number(expiration_block);
  1281. let _ = Proposals::end_block(expiration_block);
  1282. // Check that runtime code has NOT been updated after proposal slashed.
  1283. assert_runtime_code_empty!();
  1284. assert!(Proposals::active_proposal_ids().is_empty());
  1285. assert_eq!(Proposals::proposals(1).status, Expired);
  1286. assert_eq!(
  1287. Proposals::tally_results(1),
  1288. TallyResult {
  1289. proposal_id: 1,
  1290. abstentions: 0,
  1291. approvals: approvals,
  1292. rejections: 0,
  1293. slashes: 0,
  1294. status: Expired,
  1295. finalized_at: expiration_block
  1296. }
  1297. );
  1298. // Check that proposer's balance reduced by burnt stake:
  1299. assert_eq!(
  1300. Balances::free_balance(PROPOSER1),
  1301. initial_balance() - rejection_fee()
  1302. );
  1303. assert_eq!(Balances::reserved_balance(PROPOSER1), 0);
  1304. // TODO expect event ProposalStatusUpdated(1, Rejected)
  1305. });
  1306. }
  1307. }