Explorar o código

Merge pull request #156 from Joystream/proposals_v2

2.0 Proposals system
Bedeho Mender %!s(int64=4) %!d(string=hai) anos
pai
achega
480391c247
Modificáronse 47 ficheiros con 8942 adicións e 1719 borrados
  1. 227 113
      Cargo.lock
  2. 3 0
      Cargo.toml
  3. 2 12
      node/src/chain_spec.rs
  4. 1 0
      runtime-modules/common/src/lib.rs
  5. 5 0
      runtime-modules/common/src/origin_validator.rs
  6. 1 1
      runtime-modules/content-working-group/src/lib.rs
  7. 1 1
      runtime-modules/governance/src/council.rs
  8. 13 0
      runtime-modules/governance/src/election.rs
  9. 0 1
      runtime-modules/governance/src/lib.rs
  10. 1 1
      runtime-modules/governance/src/mock.rs
  11. 0 1578
      runtime-modules/governance/src/proposals.rs
  12. 1 1
      runtime-modules/membership/Cargo.toml
  13. 1 1
      runtime-modules/membership/src/lib.rs
  14. 4 0
      runtime-modules/membership/src/role_types.rs
  15. 182 0
      runtime-modules/proposals/codex/Cargo.toml
  16. 904 0
      runtime-modules/proposals/codex/src/lib.rs
  17. 49 0
      runtime-modules/proposals/codex/src/proposal_types/mod.rs
  18. 156 0
      runtime-modules/proposals/codex/src/proposal_types/parameters.rs
  19. 288 0
      runtime-modules/proposals/codex/src/tests/mock.rs
  20. 1054 0
      runtime-modules/proposals/codex/src/tests/mod.rs
  21. 94 0
      runtime-modules/proposals/discussion/Cargo.toml
  22. 352 0
      runtime-modules/proposals/discussion/src/lib.rs
  23. 145 0
      runtime-modules/proposals/discussion/src/tests/mock.rs
  24. 417 0
      runtime-modules/proposals/discussion/src/tests/mod.rs
  25. 102 0
      runtime-modules/proposals/discussion/src/types.rs
  26. 106 0
      runtime-modules/proposals/engine/Cargo.toml
  27. 812 0
      runtime-modules/proposals/engine/src/lib.rs
  28. 33 0
      runtime-modules/proposals/engine/src/tests/mock/balance_manager.rs
  29. 186 0
      runtime-modules/proposals/engine/src/tests/mock/mod.rs
  30. 18 0
      runtime-modules/proposals/engine/src/tests/mock/proposals.rs
  31. 66 0
      runtime-modules/proposals/engine/src/tests/mock/stakes.rs
  32. 1581 0
      runtime-modules/proposals/engine/src/tests/mod.rs
  33. 793 0
      runtime-modules/proposals/engine/src/types/mod.rs
  34. 197 0
      runtime-modules/proposals/engine/src/types/proposal_statuses.rs
  35. 247 0
      runtime-modules/proposals/engine/src/types/stakes.rs
  36. 1 1
      runtime-modules/roles/Cargo.toml
  37. 4 0
      runtime-modules/roles/src/actors.rs
  38. 18 0
      runtime/Cargo.toml
  39. 1 0
      runtime/src/integration/mod.rs
  40. 14 0
      runtime/src/integration/proposals/council_elected_handler.rs
  41. 208 0
      runtime/src/integration/proposals/council_origin_validator.rs
  42. 143 0
      runtime/src/integration/proposals/membership_origin_validator.rs
  43. 11 0
      runtime/src/integration/proposals/mod.rs
  44. 49 0
      runtime/src/integration/proposals/staking_events_handler.rs
  45. 79 9
      runtime/src/lib.rs
  46. 5 0
      runtime/src/test/mod.rs
  47. 367 0
      runtime/src/test/proposals_integration.rs

+ 227 - 113
Cargo.lock

@@ -131,7 +131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0d0864d84b8e07b145449be9a8537db86bf9de5ce03b913214694643b4743502"
 dependencies = [
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -159,9 +159,9 @@ checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
 
 [[package]]
 name = "backtrace"
-version = "0.3.45"
+version = "0.3.46"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad235dabf00f36301792cfe82499880ba54c6486be094d1047b02bacb67c14e8"
+checksum = "b1e692897359247cc6bb902933361652380af0f1b7651ae5c5013407f30e109e"
 dependencies = [
  "backtrace-sys",
  "cfg-if",
@@ -171,9 +171,9 @@ dependencies = [
 
 [[package]]
 name = "backtrace-sys"
-version = "0.1.34"
+version = "0.1.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca797db0057bae1a7aa2eef3283a874695455cecf08a43bfb8507ee0ebc1ed69"
+checksum = "7de8aba10a69c8e8d7622c5710229485ec32e9d55fdad160ea559c086fdcd118"
 dependencies = [
  "cc",
  "libc",
@@ -230,9 +230,13 @@ checksum = "5da9b3d9f6f585199287a473f4f8dfab6566cf827d15c00c219f53c645687ead"
 
 [[package]]
 name = "bitvec"
-version = "0.15.2"
+version = "0.17.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a993f74b4c99c1908d156b8d2e0fb6277736b0ecbd833982fd1241d39b2766a6"
+checksum = "41262f11d771fd4a61aa3ce019fca363b4b6c282fca9da2a31186d3965a47a5c"
+dependencies = [
+ "either",
+ "radium",
+]
 
 [[package]]
 name = "blake2"
@@ -309,9 +313,9 @@ dependencies = [
 
 [[package]]
 name = "bumpalo"
-version = "3.2.0"
+version = "3.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f359dc14ff8911330a51ef78022d376f25ed00248912803b58f00cb1c27f742"
+checksum = "12ae9db68ad7fac5fe51304d20f016c911539251075a214f8e663babefa35187"
 
 [[package]]
 name = "byte-slice-cast"
@@ -459,7 +463,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2f1af9ac737b2dd2d577701e59fd09ba34822f6f2ebdb30a7647405d9e55e16a"
 dependencies = [
  "const-random-macro",
- "proc-macro-hack 0.5.12",
+ "proc-macro-hack 0.5.15",
 ]
 
 [[package]]
@@ -469,7 +473,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "25e4c606eb459dd29f7c57b2e0879f2b6f14ee130918c2b78ccb58a9624e6c7a"
 dependencies = [
  "getrandom",
- "proc-macro-hack 0.5.12",
+ "proc-macro-hack 0.5.15",
 ]
 
 [[package]]
@@ -640,6 +644,17 @@ version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "11c0346158a19b3627234e15596f5e465c360fcdb97d817bcb255e0510f5a788"
 
+[[package]]
+name = "derivative"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1eae4d76b7cefedd1b4f8cc24378b2fbd1ac1b66e3bbebe8e2192d3be81cb355"
+dependencies = [
+ "proc-macro2 1.0.10",
+ "quote 1.0.3",
+ "syn 1.0.17",
+]
+
 [[package]]
 name = "derive_more"
 version = "0.14.1"
@@ -776,9 +791,9 @@ checksum = "516aa8d7a71cb00a1c4146f0798549b93d083d4f189b3ced8f3de6b8f11ee6c4"
 
 [[package]]
 name = "erased-serde"
-version = "0.3.10"
+version = "0.3.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd7d80305c9bd8cd78e3c753eb9fb110f83621e5211f1a3afffcc812b104daf9"
+checksum = "d88b6d1705e16a4d62e05ea61cc0496c2bd190f4fa8e5c1f11ce747be6bcf3d1"
 dependencies = [
  "serde",
 ]
@@ -809,9 +824,9 @@ version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "030a733c8287d6213886dd487564ff5c8f6aae10278b3588ed177f9d18f8d231"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
  "synstructure",
 ]
 
@@ -1045,10 +1060,10 @@ version = "0.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9a5081aa3de1f7542a794a397cde100ed903b0630152d0973479018fd85423a7"
 dependencies = [
- "proc-macro-hack 0.5.12",
- "proc-macro2 1.0.9",
+ "proc-macro-hack 0.5.15",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -1108,7 +1123,7 @@ dependencies = [
  "futures-task",
  "memchr",
  "pin-utils",
- "proc-macro-hack 0.5.12",
+ "proc-macro-hack 0.5.15",
  "proc-macro-nested",
  "slab",
 ]
@@ -1269,9 +1284,9 @@ dependencies = [
 
 [[package]]
 name = "hermit-abi"
-version = "0.1.8"
+version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8"
+checksum = "725cf19794cf90aa94e65050cb4191ff5d8fa87a498383774c47b332e3af952e"
 dependencies = [
  "libc",
 ]
@@ -1305,7 +1320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "961de220ec9a91af2e1e5bd80d02109155695e516771762381ef8581317066e0"
 dependencies = [
  "hex-literal-impl 0.2.1",
- "proc-macro-hack 0.5.12",
+ "proc-macro-hack 0.5.15",
 ]
 
 [[package]]
@@ -1323,7 +1338,7 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9d4c5c844e2fee0bf673d54c2c177f1713b3d2af2ff6e666b49cb7572e6cf42d"
 dependencies = [
- "proc-macro-hack 0.5.12",
+ "proc-macro-hack 0.5.15",
 ]
 
 [[package]]
@@ -1487,9 +1502,9 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7ef5550a42e3740a0e71f909d4c861056a284060af885ae7aa6242820f920d9d"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -1641,6 +1656,9 @@ dependencies = [
  "substrate-memo-module",
  "substrate-offchain-primitives",
  "substrate-primitives",
+ "substrate-proposals-codex-module",
+ "substrate-proposals-discussion-module",
+ "substrate-proposals-engine-module",
  "substrate-recurring-reward-module",
  "substrate-roles-module",
  "substrate-service-discovery-module",
@@ -1655,9 +1673,9 @@ dependencies = [
 
 [[package]]
 name = "js-sys"
-version = "0.3.36"
+version = "0.3.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cb931d43e71f560c81badb0191596562bafad2be06a3f9025b845c847c60df5"
+checksum = "6a27d435371a2fa5b6d2b028a74bbdb1234f308da363226a2854ca3ff8ba7055"
 dependencies = [
  "wasm-bindgen",
 ]
@@ -1720,9 +1738,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8609af8f63b626e8e211f52441fcdb6ec54f1a446606b10d5c89ae9bf8a20058"
 dependencies = [
  "proc-macro-crate",
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -2357,8 +2375,8 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e37c5d4cd9473c5f4c9c111f033f15d4df9bd378fdf615944e360a4f55a05f0b"
 dependencies = [
- "proc-macro2 1.0.9",
- "syn 1.0.16",
+ "proc-macro2 1.0.10",
+ "syn 1.0.17",
  "synstructure",
 ]
 
@@ -2504,9 +2522,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d5a615a1ad92048ad5d9633251edb7492b8abc057d7a679a9898476aef173935"
 dependencies = [
  "cfg-if",
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -2642,6 +2660,28 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "num_enum"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca565a7df06f3d4b485494f25ba05da1435950f4dc263440eda7a6fa9b8e36e4"
+dependencies = [
+ "derivative",
+ "num_enum_derive",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffa5a33ddddfee04c0283a7653987d634e880347e96b5b2ed64de07efb59db9d"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2 1.0.10",
+ "quote 1.0.3",
+ "syn 1.0.17",
+]
+
 [[package]]
 name = "ole32-sys"
 version = "0.2.0"
@@ -2755,9 +2795,9 @@ dependencies = [
 
 [[package]]
 name = "parity-scale-codec"
-version = "1.2.0"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f509c5e67ca0605ee17dcd3f91ef41cadd685c75a298fb6261b781a5acb3f910"
+checksum = "329c8f7f4244ddb5c37c103641027a76c530e65e8e4b8240b29f81ea40508b17"
 dependencies = [
  "arrayvec 0.5.1",
  "bitvec",
@@ -2773,9 +2813,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5a0ec292e92e8ec7c58e576adacc1e3f399c597c8f263c42f18420abe58e7245"
 dependencies = [
  "proc-macro-crate",
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -2926,24 +2966,24 @@ dependencies = [
 
 [[package]]
 name = "paste"
-version = "0.1.7"
+version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63e1afe738d71b1ebab5f1207c055054015427dbfc7bbe9ee1266894156ec046"
+checksum = "ab4fb1930692d1b6a9cfabdde3d06ea0a7d186518e2f4d67660d8970e2fa647a"
 dependencies = [
  "paste-impl",
- "proc-macro-hack 0.5.12",
+ "proc-macro-hack 0.5.15",
 ]
 
 [[package]]
 name = "paste-impl"
-version = "0.1.7"
+version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d4dc4a7f6f743211c5aab239640a65091535d97d43d92a52bca435a640892bb"
+checksum = "a62486e111e571b1e93b710b61e8f493c0013be39629b714cb166bdb06aa5a8a"
 dependencies = [
- "proc-macro-hack 0.5.12",
- "proc-macro2 1.0.9",
+ "proc-macro-hack 0.5.15",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -3063,9 +3103,9 @@ version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "aeccfe4d5d8ea175d5f0e4a2ad0637e0f4121d63bd99d356fb1f39ab2e7c6097"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -3079,14 +3119,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro-hack"
-version = "0.5.12"
+version = "0.5.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f918f2b601f93baa836c1c2945faef682ba5b6d4828ecb45eeb7cc3c71b811b4"
-dependencies = [
- "proc-macro2 1.0.9",
- "quote 1.0.3",
- "syn 1.0.16",
-]
+checksum = "0d659fe7c6d27f25e9d80a1a094c223f5246f6a6596453e09d7229bf42750b63"
 
 [[package]]
 name = "proc-macro-hack-impl"
@@ -3111,9 +3146,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.9"
+version = "1.0.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435"
+checksum = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3"
 dependencies = [
  "unicode-xid 0.2.0",
 ]
@@ -3197,9 +3232,15 @@ version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
 ]
 
+[[package]]
+name = "radium"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "def50a86306165861203e7f84ecffbbdfdea79f0e51039b33de1e952358c47ac"
+
 [[package]]
 name = "rand"
 version = "0.3.23"
@@ -3424,9 +3465,9 @@ checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
 
 [[package]]
 name = "regex"
-version = "1.3.5"
+version = "1.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8900ebc1363efa7ea1c399ccc32daed870b4002651e0bed86e72d501ebbe0048"
+checksum = "7f6946991529684867e47d86474e3a6d0c0ab9b82d5821e314b1ede31fa3a4b3"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -3451,9 +3492,9 @@ dependencies = [
 
 [[package]]
 name = "ring"
-version = "0.16.11"
+version = "0.16.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "741ba1704ae21999c00942f9f5944f801e977f54302af346b596287599ad1862"
+checksum = "1ba5a8ec64ee89a76c98c549af81ff14813df09c3e6dc4766c3856da48597a0c"
 dependencies = [
  "cc",
  "lazy_static",
@@ -3606,29 +3647,29 @@ checksum = "a0eddf2e8f50ced781f288c19f18621fa72a3779e3cb58dbf23b07469b0abeb4"
 
 [[package]]
 name = "serde"
-version = "1.0.104"
+version = "1.0.106"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449"
+checksum = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.104"
+version = "1.0.106"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64"
+checksum = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.48"
+version = "1.0.51"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9371ade75d4c2d6cb154141b9752cf3781ec9c05e0e5cf35060e1e70ee7b9c25"
+checksum = "da07b57ee2623368351e9a0488bb0b261322a15a6e0ae53e243cbdc0f4208da9"
 dependencies = [
  "itoa",
  "ryu",
@@ -3706,7 +3747,7 @@ dependencies = [
 [[package]]
 name = "slog-async"
 version = "2.3.0"
-source = "git+https://github.com/paritytech/slog-async#107848e7ded5e80dc43f6296c2b96039eb92c0a5"
+source = "git+https://github.com/paritytech/slog-async#0329dc74feb3afe93d0cd2533a472b7ceab44aaf"
 dependencies = [
  "crossbeam-channel",
  "slog",
@@ -3810,9 +3851,9 @@ source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a1232
 dependencies = [
  "blake2-rfc",
  "proc-macro-crate",
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -4129,9 +4170,9 @@ version = "2.0.0"
 source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
 dependencies = [
  "proc-macro-crate",
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -4174,11 +4215,11 @@ name = "srml-support-procedural"
 version = "2.0.0"
 source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
  "sr-api-macros",
  "srml-support-procedural-tools",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -4187,10 +4228,10 @@ version = "2.0.0"
 source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
 dependencies = [
  "proc-macro-crate",
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
  "srml-support-procedural-tools-derive",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -4198,9 +4239,9 @@ name = "srml-support-procedural-tools-derive"
 version = "2.0.0"
 source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -4323,9 +4364,9 @@ checksum = "ea692d40005b3ceba90a9fe7a78fa8d4b82b0ce627eebbffc329aab850f3410e"
 dependencies = [
  "heck",
  "proc-macro-error",
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -4441,9 +4482,9 @@ version = "2.0.0"
 source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
 dependencies = [
  "proc-macro-crate",
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -4678,9 +4719,9 @@ name = "substrate-debug-derive"
 version = "2.0.0"
 source = "git+https://github.com/paritytech/substrate.git?rev=c37bb08535c49a12320af7facfd555ce05cce2e8#c37bb08535c49a12320af7facfd555ce05cce2e8"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
 ]
 
 [[package]]
@@ -4871,7 +4912,7 @@ dependencies = [
 
 [[package]]
 name = "substrate-membership-module"
-version = "1.0.0"
+version = "1.0.1"
 dependencies = [
  "parity-scale-codec",
  "serde",
@@ -5058,6 +5099,79 @@ dependencies = [
  "substrate-debug-derive",
 ]
 
+[[package]]
+name = "substrate-proposals-codex-module"
+version = "2.0.0"
+dependencies = [
+ "num_enum",
+ "parity-scale-codec",
+ "serde",
+ "sr-io",
+ "sr-primitives",
+ "sr-staking-primitives",
+ "sr-std",
+ "srml-balances",
+ "srml-staking",
+ "srml-staking-reward-curve",
+ "srml-support",
+ "srml-system",
+ "srml-timestamp",
+ "substrate-common-module",
+ "substrate-content-working-group-module",
+ "substrate-governance-module",
+ "substrate-hiring-module",
+ "substrate-membership-module",
+ "substrate-primitives",
+ "substrate-proposals-discussion-module",
+ "substrate-proposals-engine-module",
+ "substrate-recurring-reward-module",
+ "substrate-roles-module",
+ "substrate-stake-module",
+ "substrate-token-mint-module",
+ "substrate-versioned-store",
+ "substrate-versioned-store-permissions-module",
+]
+
+[[package]]
+name = "substrate-proposals-discussion-module"
+version = "2.0.0"
+dependencies = [
+ "num_enum",
+ "parity-scale-codec",
+ "serde",
+ "sr-io",
+ "sr-primitives",
+ "sr-std",
+ "srml-balances",
+ "srml-support",
+ "srml-system",
+ "srml-timestamp",
+ "substrate-common-module",
+ "substrate-membership-module",
+ "substrate-primitives",
+]
+
+[[package]]
+name = "substrate-proposals-engine-module"
+version = "2.0.0"
+dependencies = [
+ "mockall",
+ "num_enum",
+ "parity-scale-codec",
+ "serde",
+ "sr-io",
+ "sr-primitives",
+ "sr-std",
+ "srml-balances",
+ "srml-support",
+ "srml-system",
+ "srml-timestamp",
+ "substrate-common-module",
+ "substrate-membership-module",
+ "substrate-primitives",
+ "substrate-stake-module",
+]
+
 [[package]]
 name = "substrate-recurring-reward-module"
 version = "1.0.1"
@@ -5081,7 +5195,7 @@ dependencies = [
 
 [[package]]
 name = "substrate-roles-module"
-version = "1.0.0"
+version = "1.0.1"
 dependencies = [
  "parity-scale-codec",
  "serde",
@@ -5478,11 +5592,11 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "1.0.16"
+version = "1.0.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "123bd9499cfb380418d509322d7a6d52e5315f064fe4b3ad18a53d6b92c07859"
+checksum = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
  "unicode-xid 0.2.0",
 ]
@@ -5493,9 +5607,9 @@ version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
  "unicode-xid 0.2.0",
 ]
 
@@ -6072,9 +6186,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.59"
+version = "0.2.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3557c397ab5a8e347d434782bcd31fc1483d927a6826804cec05cc792ee2519d"
+checksum = "2cc57ce05287f8376e998cbddfb4c8cb43b84a7ec55cf4551d7c00eef317a47f"
 dependencies = [
  "cfg-if",
  "wasm-bindgen-macro",
@@ -6082,16 +6196,16 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.59"
+version = "0.2.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0da9c9a19850d3af6df1cb9574970b566d617ecfaf36eb0b706b6f3ef9bd2f8"
+checksum = "d967d37bf6c16cca2973ca3af071d0a2523392e4a594548155d89a678f4237cd"
 dependencies = [
  "bumpalo",
  "lazy_static",
  "log",
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
  "wasm-bindgen-shared",
 ]
 
@@ -6110,9 +6224,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.59"
+version = "0.2.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f6fde1d36e75a714b5fe0cffbb78978f222ea6baebb726af13c78869fdb4205"
+checksum = "8bd151b63e1ea881bb742cd20e1d6127cef28399558f3b5d415289bc41eee3a4"
 dependencies = [
  "quote 1.0.3",
  "wasm-bindgen-macro-support",
@@ -6120,22 +6234,22 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.59"
+version = "0.2.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25bda4168030a6412ea8a047e27238cadf56f0e53516e1e83fec0a8b7c786f6d"
+checksum = "d68a5b36eef1be7868f668632863292e37739656a80fc4b9acec7b0bd35a4931"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.59"
+version = "0.2.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc9f36ad51f25b0219a3d4d13b90eb44cd075dff8b6280cca015775d7acaddd8"
+checksum = "daf76fe7d25ac79748a37538b7daeed1c7a6867c92d3245c12c6222e4a20d639"
 
 [[package]]
 name = "wasm-timer"
@@ -6176,9 +6290,9 @@ dependencies = [
 
 [[package]]
 name = "web-sys"
-version = "0.3.36"
+version = "0.3.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "721c6263e2c66fd44501cc5efbfa2b7dfa775d13e4ea38c46299646ed1f9c70a"
+checksum = "2d6f51648d8c56c366144378a33290049eafdd784071077f6fe37dae64c1c4cb"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -6252,9 +6366,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
 
 [[package]]
 name = "winapi-util"
-version = "0.1.3"
+version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ccfbf554c6ad11084fb7517daca16cfdcaccbdadba4fc336f032a8b12c2ad80"
+checksum = "fa515c5163a99cc82bab70fd3bfdd36d827be85de63737b40fcef2ce084a436e"
 dependencies = [
  "winapi 0.3.8",
 ]
@@ -6354,8 +6468,8 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2"
 dependencies = [
- "proc-macro2 1.0.9",
+ "proc-macro2 1.0.10",
  "quote 1.0.3",
- "syn 1.0.16",
+ "syn 1.0.17",
  "synstructure",
 ]

+ 3 - 0
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",

+ 2 - 12
node/src/chain_spec.rs

@@ -19,8 +19,8 @@ use node_runtime::{
     AuthorityDiscoveryConfig, BabeConfig, Balance, BalancesConfig, ContentWorkingGroupConfig,
     CouncilConfig, CouncilElectionConfig, DataObjectStorageRegistryConfig,
     DataObjectTypeRegistryConfig, ElectionParameters, GrandpaConfig, ImOnlineConfig, IndicesConfig,
-    MembersConfig, Perbill, ProposalsConfig, SessionConfig, SessionKeys, Signature, StakerStatus,
-    StakingConfig, SudoConfig, SystemConfig, VersionedStoreConfig, DAYS, WASM_BINARY,
+    MembersConfig, Perbill, SessionConfig, SessionKeys, Signature, StakerStatus, StakingConfig,
+    SudoConfig, SystemConfig, VersionedStoreConfig, DAYS, WASM_BINARY,
 };
 pub use node_runtime::{AccountId, GenesisConfig};
 use primitives::{sr25519, Pair, Public};
@@ -246,16 +246,6 @@ pub fn testnet_genesis(
                 min_voting_stake: 1 * DOLLARS,
             },
         }),
-        proposals: Some(ProposalsConfig {
-            approval_quorum: 66,
-            min_stake: 2 * DOLLARS,
-            cancellation_fee: 10 * CENTS,
-            rejection_fee: 1 * DOLLARS,
-            voting_period: 2 * DAYS,
-            name_max_len: 512,
-            description_max_len: 10_000,
-            wasm_code_max_len: 2_000_000,
-        }),
         members: Some(MembersConfig {
             default_paid_membership_fee: 100u128,
             members: crate::members_config::initial_members(),

+ 1 - 0
runtime-modules/common/src/lib.rs

@@ -2,3 +2,4 @@
 #![cfg_attr(not(feature = "std"), no_std)]
 
 pub mod currency;
+pub mod origin_validator;

+ 5 - 0
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<Origin, ActorId, AccountId> {
+    /// Check for valid combination of origin and actor_id
+    fn ensure_actor_origin(origin: Origin, actor_id: ActorId) -> Result<AccountId, &'static str>;
+}

+ 1 - 1
runtime-modules/content-working-group/src/lib.rs

@@ -1195,7 +1195,7 @@ decl_module! {
             // Increment NextChannelId
             NextChannelId::<T>::mutate(|id| *id += <ChannelId<T> as One>::one());
 
-            /// CREDENTIAL STUFF ///
+            // CREDENTIAL STUFF //
 
             // Dial out to membership module and inform about new role as channe owner.
             let registered_role = <members::Module<T>>::register_role_on_member(owner, &member_in_role).is_ok();

+ 1 - 1
runtime-modules/governance/src/council.rs

@@ -91,7 +91,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<T::AccountId>) {
+        pub fn set_council(origin, accounts: Vec<T::AccountId>) {
             ensure_root(origin)?;
             let new_council: Seats<T::AccountId, BalanceOf<T>> = accounts.into_iter().map(|account| {
                 Seat {

+ 13 - 0
runtime-modules/governance/src/election.rs

@@ -99,6 +99,19 @@ impl<Elected, Term, X: CouncilElected<Elected, Term>> CouncilElected<Elected, Te
         X::council_elected(new_council, term);
     }
 }
+// Chain of handlers.
+impl<
+        Elected: Clone,
+        Term: Clone,
+        X: CouncilElected<Elected, Term>,
+        Y: CouncilElected<Elected, Term>,
+    > CouncilElected<Elected, Term> for (X, Y)
+{
+    fn council_elected(new_council: Elected, term: Term) {
+        X::council_elected(new_council.clone(), term.clone());
+        Y::council_elected(new_council, term);
+    }
+}
 
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
 #[derive(Clone, Copy, Encode, Decode, Default)]

+ 0 - 1
runtime-modules/governance/src/lib.rs

@@ -4,7 +4,6 @@
 pub mod council;
 pub mod election;
 pub mod election_params;
-pub mod proposals;
 
 mod sealed_vote;
 mod stake;

+ 1 - 1
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;
 

+ 0 - 1578
runtime-modules/governance/src/proposals.rs

@@ -1,1578 +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<AccountId, Balance, BlockNumber, Hash> {
-    id: u32,
-    proposer: AccountId,
-    stake: Balance,
-    name: Vec<u8>,
-    description: Vec<u8>,
-    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<BlockNumber> {
-    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<Event<Self>> + Into<<Self as system::Trait>::Event>;
-}
-
-decl_event!(
-    pub enum Event<T>
-    where
-        <T as system::Trait>::Hash,
-        <T as system::Trait>::BlockNumber,
-        <T as system::Trait>::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<BlockNumber>),
-
-        /// * Hash - hash of wasm code of runtime update.
-        RuntimeUpdated(u32, Hash),
-
-        /// Root cancelled proposal
-        ProposalVetoed(u32),
-    }
-);
-
-decl_storage! {
-    trait Store for Module<T: Trait> 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<T> =
-            BalanceOf::<T>::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<T> =
-            BalanceOf::<T>::from(DEFAULT_CANCELLATION_FEE);
-
-        /// A fee to be slashed (burn) in case a proposal was rejected.
-        RejectionFee get(rejection_fee) config(): BalanceOf<T> =
-            BalanceOf::<T>::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 /
-            (<T as timestamp::Trait>::MinimumPeriod::get().saturated_into::<u32>() * 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::AccountId, BalanceOf<T>, T::BlockNumber, T::Hash>;
-
-        /// Ids of proposals that are open for voting (have not been finalized yet).
-        ActiveProposalIds get(active_proposal_ids): Vec<u32> = vec![];
-
-        /// Get WASM code of runtime upgrade by hash of its content.
-        WasmCodeByHash get(wasm_code_by_hash): map T::Hash => Vec<u8>;
-
-        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<T::BlockNumber>;
-    }
-}
-
-decl_module! {
-    pub struct Module<T: Trait> 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<T>,
-            name: Vec<u8>,
-            description: Vec<u8>,
-            wasm_code: Vec<u8>
-        ) {
-
-            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 as GovernanceCurrency>::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 !<WasmCodeByHash<T>>::exists(wasm_hash) {
-              <WasmCodeByHash<T>>::insert(wasm_hash, wasm_code);
-            }
-            <Proposals<T>>::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!(<Proposals<T>>::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 = !<VoteByAccountAndProposal<T>>::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!(<Proposals<T>>::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 as GovernanceCurrency>::Currency::slash_reserved(&proposer, fee);
-
-            // Return unspent part of remaining staked deposit (after taking some fee)
-            let left_stake = proposal.stake - fee;
-            let _ = <T as GovernanceCurrency>::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!(<Proposals<T>>::exists(proposal_id), MSG_PROPOSAL_NOT_FOUND);
-            let proposal = Self::proposals(proposal_id);
-            ensure!(proposal.status == Active, MSG_PROPOSAL_FINALIZED);
-
-            let _ = <T as GovernanceCurrency>::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<T: Trait> Module<T> {
-    fn current_block() -> T::BlockNumber {
-        <system::Module<T>>::block_number()
-    }
-
-    fn can_participate(sender: &T::AccountId) -> bool {
-        !<T as GovernanceCurrency>::Currency::free_balance(sender).is_zero()
-            && <membership::members::Module<T>>::is_member_account(sender)
-    }
-
-    fn is_councilor(sender: &T::AccountId) -> bool {
-        <council::Module<T>>::is_councilor(sender)
-    }
-
-    fn councilors_count() -> u32 {
-        <council::Module<T>>::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 <VotesByProposal<T>>::exists(proposal_id) {
-            // Append a new vote to other votes on this proposal:
-            <VotesByProposal<T>>::mutate(proposal_id, |votes| votes.push(new_vote));
-        } else {
-            // This is the first vote on this proposal:
-            <VotesByProposal<T>>::insert(proposal_id, vec![new_vote]);
-        }
-        <VoteByAccountAndProposal<T>>::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<ProposalStatus> = 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(),
-                };
-                <TallyResults<T>>::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<u32> = 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);
-            <Proposals<T>>::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 as GovernanceCurrency>::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 as GovernanceCurrency>::Currency::slash_reserved(&proposer, fee);
-
-        // Return unspent part of remaining staked deposit (after taking some fee):
-        let left_stake = proposal.stake - fee;
-        let _ = <T as GovernanceCurrency>::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 as GovernanceCurrency>::Currency::unreserve(&proposal.proposer, proposal.stake);
-
-        // Update wasm code of node's runtime:
-        <system::Module<T>>::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<Self::AccountId>;
-        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<Self>;
-    }
-
-    impl membership::members::Trait for Test {
-        type Event = ();
-        type MemberId = u32;
-        type PaidTermId = u32;
-        type SubscriptionId = u32;
-        type ActorId = u32;
-        type InitialMembersBalance = InitialMembersBalance;
-    }
-
-    impl minting::Trait for Test {
-        type Currency = balances::Module<Self>;
-        type MintId = u64;
-    }
-
-    impl Trait for Test {
-        type Event = ();
-    }
-
-    type System = system::Module<Test>;
-    type Balances = balances::Module<Test>;
-    type Proposals = Module<Test>;
-
-    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<T>,system<T>,proposals<T>,
-    //     }
-    // }
-
-    // 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::<Test>()
-            .unwrap();
-
-        // balances doesn't contain GenesisConfig anymore
-        // // We use default for brevity, but you can configure as desired if needed.
-        // balances::GenesisConfig::<Test>::default()
-        //     .assimilate_storage(&mut t)
-        //     .unwrap();
-
-        let council_mock: council::Seats<u64, u64> = ALL_COUNCILORS
-            .iter()
-            .map(|&c| council::Seat {
-                member: c,
-                stake: 0u64,
-                backers: vec![],
-            })
-            .collect();
-
-        council::GenesisConfig::<Test> {
-            active_council: council_mock,
-            term_ends_at: 0,
-        }
-        .assimilate_storage(&mut t)
-        .unwrap();
-
-        membership::members::GenesisConfig::<Test> {
-            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::<Test>{
-        //     // 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<u8> {
-        b"Proposal Name".to_vec()
-    }
-
-    fn description() -> Vec<u8> {
-        b"Proposal Description".to_vec()
-    }
-
-    fn wasm_code() -> Vec<u8> {
-        b"Proposal Wasm Code".to_vec()
-    }
-
-    fn _create_default_proposal() -> dispatch::Result {
-        _create_proposal(None, None, None, None, None)
-    }
-
-    fn _create_proposal(
-        origin: Option<u64>,
-        stake: Option<u64>,
-        name: Option<Vec<u8>>,
-        description: Option<Vec<u8>>,
-        wasm_code: Option<Vec<u8>>,
-    ) -> 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<Vec<u8>> {
-        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::<Test>::from(DEFAULT_MIN_STAKE)
-            );
-            assert_eq!(
-                Proposals::cancellation_fee(),
-                BalanceOf::<Test>::from(DEFAULT_CANCELLATION_FEE)
-            );
-            assert_eq!(
-                Proposals::rejection_fee(),
-                BalanceOf::<Test>::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<u8> {
-        vec![65; Proposals::name_max_len() as usize + 1]
-    }
-
-    fn too_long_description() -> Vec<u8> {
-        vec![65; Proposals::description_max_len() as usize + 1]
-    }
-
-    fn too_long_wasm_code() -> Vec<u8> {
-        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)
-        });
-    }
-}

+ 1 - 1
runtime-modules/membership/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = 'substrate-membership-module'
-version = '1.0.0'
+version = '1.0.1'
 authors = ['Joystream contributors']
 edition = '2018'
 

+ 1 - 1
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;

+ 4 - 0
runtime-modules/membership/src/role_types.rs

@@ -2,6 +2,10 @@ use codec::{Decode, Encode};
 use rstd::collections::btree_set::BTreeSet;
 use rstd::prelude::*;
 
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)]
 pub enum Role {
     StorageProvider,

+ 182 - 0
runtime-modules/proposals/codex/Cargo.toml

@@ -0,0 +1,182 @@
+[package]
+name = 'substrate-proposals-codex-module'
+version = '2.0.0'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[features]
+default = ['std']
+no_std = []
+std = [
+    'codec/std',
+    'rstd/std',
+    'srml-support/std',
+    'primitives/std',
+    'sr-primitives/std',
+    'system/std',
+    'timestamp/std',
+    'staking/std',
+    'serde',
+    'proposal_engine/std',
+    'proposal_discussion/std',
+    'stake/std',
+    'balances/std',
+    'membership/std',
+    'governance/std',
+    'mint/std',
+    'roles/std',
+]
+
+
+[dependencies.num_enum]
+default_features = false
+version = "0.4.2"
+
+[dependencies.serde]
+features = ['derive']
+optional = true
+version = '1.0.101'
+
+[dependencies.codec]
+default-features = false
+features = ['derive']
+package = 'parity-scale-codec'
+version = '1.0.0'
+
+[dependencies.primitives]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'substrate-primitives'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.rstd]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-std'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.sr-primitives]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-primitives'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.srml-support]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-support'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.system]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-system'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.balances]
+package = 'srml-balances'
+default-features = false
+git = 'https://github.com/paritytech/substrate.git'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.staking]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-staking'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.stake]
+default_features = false
+package = 'substrate-stake-module'
+path = '../../stake'
+
+[dependencies.membership]
+default_features = false
+package = 'substrate-membership-module'
+path = '../../membership'
+
+[dependencies.governance]
+default_features = false
+package = 'substrate-governance-module'
+path = '../../governance'
+
+[dependencies.mint]
+default_features = false
+package = 'substrate-token-mint-module'
+path = '../../token-minting'
+
+[dependencies.proposal_engine]
+default_features = false
+package = 'substrate-proposals-engine-module'
+path = '../engine'
+
+[dependencies.proposal_discussion]
+default_features = false
+package = 'substrate-proposals-discussion-module'
+path = '../discussion'
+
+[dependencies.common]
+default_features = false
+package = 'substrate-common-module'
+path = '../../common'
+
+[dependencies.content_working_group]
+default_features = false
+package = 'substrate-content-working-group-module'
+path = '../../content-working-group'
+
+[dependencies.roles]
+default_features = false
+package = 'substrate-roles-module'
+path = '../../roles'
+
+[dev-dependencies.hiring]
+default_features = false
+package = 'substrate-hiring-module'
+path = '../../hiring'
+
+[dev-dependencies.versioned_store]
+default_features = false
+package ='substrate-versioned-store'
+path = '../../versioned-store'
+
+[dependencies.versioned_store]
+default_features = false
+package ='substrate-versioned-store'
+path = '../../versioned-store'
+
+[dev-dependencies.versioned_store_permissions]
+default_features = false
+package = 'substrate-versioned-store-permissions-module'
+path = '../../versioned-store-permissions'
+
+[dev-dependencies.recurring_rewards]
+default_features = false
+package = 'substrate-recurring-reward-module'
+path = '../../recurring-reward'
+
+[dev-dependencies.sr-staking-primitives]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-staking-primitives'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+# don't rename the dependency it is causing some strange compiler error:
+# https://github.com/rust-lang/rust/issues/64450
+[dev-dependencies.srml-staking-reward-curve]
+package = 'srml-staking-reward-curve'
+git = 'https://github.com/paritytech/substrate.git'
+default_features = false
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'

+ 904 - 0
runtime-modules/proposals/codex/src/lib.rs

@@ -0,0 +1,904 @@
+//! # Proposals codex module
+//! Proposals `codex` module for the Joystream platform. Version 2.
+//! Component of the proposals system. It contains preset proposal types.
+//!
+//! ## Overview
+//!
+//! The proposals codex module serves as a facade and entry point of the proposals system. It uses
+//! proposals `engine` module to maintain a lifecycle of the proposal and to execute proposals.
+//! During the proposal creation, `codex` also create a discussion thread using the `discussion`
+//! proposals module. `Codex` uses predefined parameters (eg.:`voting_period`) for each proposal and
+//! encodes extrinsic calls from dependency modules in order to create proposals inside the `engine`
+//! module. For each proposal, [its crucial details](./enum.ProposalDetails.html) are saved to the
+//! `ProposalDetailsByProposalId` map.
+//!
+//! ### Supported extrinsics (proposal types)
+//! - [create_text_proposal](./struct.Module.html#method.create_text_proposal)
+//! - [create_runtime_upgrade_proposal](./struct.Module.html#method.create_runtime_upgrade_proposal)
+//! - [create_set_election_parameters_proposal](./struct.Module.html#method.create_set_election_parameters_proposal)
+//! - [create_set_content_working_group_mint_capacity_proposal](./struct.Module.html#method.create_set_content_working_group_mint_capacity_proposal)
+//! - [create_spending_proposal](./struct.Module.html#method.create_spending_proposal)
+//! - [create_set_lead_proposal](./struct.Module.html#method.create_set_lead_proposal)
+//! - [create_evict_storage_provider_proposal](./struct.Module.html#method.create_evict_storage_provider_proposal)
+//! - [create_set_validator_count_proposal](./struct.Module.html#method.create_set_validator_count_proposal)
+//! - [create_set_storage_role_parameters_proposal](./struct.Module.html#method.create_set_storage_role_parameters_proposal)
+//!
+//! ### Proposal implementations of this module
+//! - execute_text_proposal - prints the proposal to the log
+//! - execute_runtime_upgrade_proposal - sets the runtime code
+//!
+//! ### Dependencies:
+//! - [proposals engine](../substrate_proposals_engine_module/index.html)
+//! - [proposals discussion](../substrate_proposals_discussion_module/index.html)
+//! - [membership](../substrate_membership_module/index.html)
+//! - [governance](../substrate_governance_module/index.html)
+//! - [content_working_group](../substrate_content_working_group_module/index.html)
+//!
+
+// 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)]
+
+mod proposal_types;
+#[cfg(test)]
+mod tests;
+
+use codec::Encode;
+use common::origin_validator::ActorOriginValidator;
+use governance::election_params::ElectionParameters;
+use proposal_engine::ProposalParameters;
+use roles::actors::{Role, RoleParameters};
+use rstd::clone::Clone;
+use rstd::convert::TryInto;
+use rstd::prelude::*;
+use rstd::str::from_utf8;
+use rstd::vec::Vec;
+use runtime_io::blake2_256;
+use sr_primitives::traits::SaturatedConversion;
+use sr_primitives::traits::{One, Zero};
+use sr_primitives::Perbill;
+use srml_support::dispatch::DispatchResult;
+use srml_support::traits::{Currency, Get};
+use srml_support::{decl_error, decl_module, decl_storage, ensure, print};
+use system::{ensure_root, RawOrigin};
+
+pub use proposal_types::ProposalDetails;
+
+// Percentage of the total token issue as max mint balance value. Shared with spending
+// proposal max balance percentage.
+const COUNCIL_MINT_MAX_BALANCE_PERCENT: u32 = 2;
+
+/// 'Proposals codex' substrate module Trait
+pub trait Trait:
+    system::Trait
+    + proposal_engine::Trait
+    + proposal_discussion::Trait
+    + membership::members::Trait
+    + governance::election::Trait
+    + content_working_group::Trait
+    + roles::actors::Trait
+    + staking::Trait
+{
+    /// Defines max allowed text proposal length.
+    type TextProposalMaxLength: Get<u32>;
+
+    /// Defines max wasm code length of the runtime upgrade proposal.
+    type RuntimeUpgradeWasmProposalMaxLength: Get<u32>;
+
+    /// Validates member id and origin combination
+    type MembershipOriginValidator: ActorOriginValidator<
+        Self::Origin,
+        MemberId<Self>,
+        Self::AccountId,
+    >;
+}
+
+/// Balance alias for `stake` module
+pub type BalanceOf<T> =
+    <<T as stake::Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::Balance;
+
+/// Currency alias for `stake` module
+pub type CurrencyOf<T> = <T as stake::Trait>::Currency;
+
+/// Balance alias for GovernanceCurrency from `common` module. TODO: replace with BalanceOf
+pub type BalanceOfGovernanceCurrency<T> =
+    <<T as common::currency::GovernanceCurrency>::Currency as Currency<
+        <T as system::Trait>::AccountId,
+    >>::Balance;
+
+/// Balance alias for token mint balance from `token mint` module. TODO: replace with BalanceOf
+pub type BalanceOfMint<T> =
+    <<T as mint::Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::Balance;
+
+/// Negative imbalance alias for staking
+pub type NegativeImbalance<T> =
+    <<T as stake::Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::NegativeImbalance;
+
+type MemberId<T> = <T as membership::members::Trait>::MemberId;
+
+decl_error! {
+    /// Codex module predefined errors
+    pub enum Error {
+        /// The size of the provided text for text proposal exceeded the limit
+        TextProposalSizeExceeded,
+
+        /// Provided text for text proposal is empty
+        TextProposalIsEmpty,
+
+        /// The size of the provided WASM code for the runtime upgrade proposal exceeded the limit
+        RuntimeProposalSizeExceeded,
+
+        /// Provided WASM code for the runtime upgrade proposal is empty
+        RuntimeProposalIsEmpty,
+
+        /// Invalid balance value for the spending proposal
+        InvalidSpendingProposalBalance,
+
+        /// Invalid validator count for the 'set validator count' proposal
+        InvalidValidatorCount,
+
+        /// Require root origin in extrinsics
+        RequireRootOrigin,
+
+        /// Invalid storage role parameter - min_actors
+        InvalidStorageRoleParameterMinActors,
+
+        /// Invalid storage role parameter - max_actors
+        InvalidStorageRoleParameterMaxActors,
+
+        /// Invalid storage role parameter - reward_period
+        InvalidStorageRoleParameterRewardPeriod,
+
+        /// Invalid storage role parameter - bonding_period
+        InvalidStorageRoleParameterBondingPeriod,
+
+        /// Invalid storage role parameter - unbonding_period
+        InvalidStorageRoleParameterUnbondingPeriod,
+
+        /// Invalid storage role parameter - min_service_period
+        InvalidStorageRoleParameterMinServicePeriod,
+
+        /// Invalid storage role parameter - startup_grace_period
+        InvalidStorageRoleParameterStartupGracePeriod,
+
+        /// Invalid council election parameter - council_size
+        InvalidCouncilElectionParameterCouncilSize,
+
+        /// Invalid council election parameter - candidacy-limit
+        InvalidCouncilElectionParameterCandidacyLimit,
+
+        /// Invalid council election parameter - min-voting_stake
+        InvalidCouncilElectionParameterMinVotingStake,
+
+        /// Invalid council election parameter - new_term_duration
+        InvalidCouncilElectionParameterNewTermDuration,
+
+        /// Invalid council election parameter - min_council_stake
+        InvalidCouncilElectionParameterMinCouncilStake,
+
+        /// Invalid council election parameter - revealing_period
+        InvalidCouncilElectionParameterRevealingPeriod,
+
+        /// Invalid council election parameter - voting_period
+        InvalidCouncilElectionParameterVotingPeriod,
+
+        /// Invalid council election parameter - announcing_period
+        InvalidCouncilElectionParameterAnnouncingPeriod,
+
+        /// Invalid council election parameter - min_stake
+        InvalidStorageRoleParameterMinStake,
+
+        /// Invalid council election parameter - reward
+        InvalidStorageRoleParameterReward,
+
+        /// Invalid council election parameter - entry_request_fee
+        InvalidStorageRoleParameterEntryRequestFee,
+
+        /// Invalid working group mint capacity parameter
+        InvalidStorageWorkingGroupMintCapacity,
+
+        /// Invalid 'set lead proposal' parameter - proposed lead cannot be a councilor
+        InvalidSetLeadParameterCannotBeCouncilor
+    }
+}
+
+impl From<system::Error> 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<proposal_engine::Error> 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<proposal_discussion::Error> 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()),
+        }
+    }
+}
+
+// Storage for the proposals codex module
+decl_storage! {
+    pub trait Store for Module<T: Trait> as ProposalCodex{
+        /// Map proposal id to its discussion thread id
+        pub ThreadIdByProposalId get(fn thread_id_by_proposal_id):
+            map T::ProposalId => T::ThreadId;
+
+        /// Map proposal id to proposal details
+        pub ProposalDetailsByProposalId get(fn proposal_details_by_proposal_id):
+            map T::ProposalId => ProposalDetails<
+                BalanceOfMint<T>,
+                BalanceOfGovernanceCurrency<T>,
+                T::BlockNumber,
+                T::AccountId,
+                T::MemberId
+            >;
+    }
+}
+
+decl_module! {
+    /// Proposal codex substrate module Call
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+        /// Predefined errors
+        type Error = Error;
+
+        /// Create 'Text (signal)' proposal type.
+        pub fn create_text_proposal(
+            origin,
+            member_id: MemberId<T>,
+            title: Vec<u8>,
+            description: Vec<u8>,
+            stake_balance: Option<BalanceOf<T>>,
+            text: Vec<u8>,
+        ) {
+            ensure!(!text.is_empty(), Error::TextProposalIsEmpty);
+            ensure!(text.len() as u32 <=  T::TextProposalMaxLength::get(),
+                Error::TextProposalSizeExceeded);
+
+            let proposal_parameters = proposal_types::parameters::text_proposal::<T>();
+            let proposal_code =
+                <Call<T>>::execute_text_proposal(title.clone(), description.clone(), text.clone());
+
+            Self::create_proposal(
+                origin,
+                member_id,
+                title,
+                description,
+                stake_balance,
+                proposal_code.encode(),
+                proposal_parameters,
+                ProposalDetails::Text(text),
+            )?;
+        }
+
+        /// Create 'Runtime upgrade' proposal type. Runtime upgrade can be initiated only by
+        /// members from the hardcoded list `RuntimeUpgradeProposalAllowedProposers`
+        pub fn create_runtime_upgrade_proposal(
+            origin,
+            member_id: MemberId<T>,
+            title: Vec<u8>,
+            description: Vec<u8>,
+            stake_balance: Option<BalanceOf<T>>,
+            wasm: Vec<u8>,
+        ) {
+            ensure!(!wasm.is_empty(), Error::RuntimeProposalIsEmpty);
+            ensure!(wasm.len() as u32 <= T::RuntimeUpgradeWasmProposalMaxLength::get(),
+                Error::RuntimeProposalSizeExceeded);
+
+            let wasm_hash = blake2_256(&wasm);
+
+            let proposal_code =
+                <Call<T>>::execute_runtime_upgrade_proposal(title.clone(), description.clone(), wasm);
+
+            let proposal_parameters = proposal_types::parameters::runtime_upgrade_proposal::<T>();
+
+            Self::create_proposal(
+                origin,
+                member_id,
+                title,
+                description,
+                stake_balance,
+                proposal_code.encode(),
+                proposal_parameters,
+                ProposalDetails::RuntimeUpgrade(wasm_hash.to_vec()),
+            )?;
+        }
+
+        /// Create 'Set election parameters' proposal type. This proposal uses `set_election_parameters()`
+        /// extrinsic from the `governance::election module`.
+        pub fn create_set_election_parameters_proposal(
+            origin,
+            member_id: MemberId<T>,
+            title: Vec<u8>,
+            description: Vec<u8>,
+            stake_balance: Option<BalanceOf<T>>,
+            election_parameters: ElectionParameters<BalanceOfGovernanceCurrency<T>, T::BlockNumber>,
+        ) {
+            election_parameters.ensure_valid()?;
+
+            Self::ensure_council_election_parameters_valid(&election_parameters)?;
+
+            let proposal_code =
+                <governance::election::Call<T>>::set_election_parameters(election_parameters.clone());
+
+            let proposal_parameters =
+                proposal_types::parameters::set_election_parameters_proposal::<T>();
+
+            Self::create_proposal(
+                origin,
+                member_id,
+                title,
+                description,
+                stake_balance,
+                proposal_code.encode(),
+                proposal_parameters,
+                ProposalDetails::SetElectionParameters(election_parameters),
+            )?;
+        }
+
+        /// Create 'Set content working group mint capacity' proposal type.
+        /// This proposal uses `set_mint_capacity()` extrinsic from the `content-working-group`  module.
+        pub fn create_set_content_working_group_mint_capacity_proposal(
+            origin,
+            member_id: MemberId<T>,
+            title: Vec<u8>,
+            description: Vec<u8>,
+            stake_balance: Option<BalanceOf<T>>,
+            mint_balance: BalanceOfMint<T>,
+        ) {
+
+            let max_mint_capacity: u32 = get_required_stake_by_fraction::<T>(1, 100)
+                .try_into()
+                .unwrap_or_default() as u32;
+            ensure!(
+                mint_balance < <BalanceOfMint<T>>::from(max_mint_capacity),
+                Error::InvalidStorageWorkingGroupMintCapacity
+            );
+
+            let proposal_code =
+                <content_working_group::Call<T>>::set_mint_capacity(mint_balance.clone());
+
+            let proposal_parameters =
+                proposal_types::parameters::set_content_working_group_mint_capacity_proposal::<T>();
+
+            Self::create_proposal(
+                origin,
+                member_id,
+                title,
+                description,
+                stake_balance,
+                proposal_code.encode(),
+                proposal_parameters,
+                ProposalDetails::SetContentWorkingGroupMintCapacity(mint_balance),
+            )?;
+        }
+
+        /// Create 'Spending' proposal type.
+        /// This proposal uses `spend_from_council_mint()` extrinsic from the `governance::council`  module.
+        pub fn create_spending_proposal(
+            origin,
+            member_id: MemberId<T>,
+            title: Vec<u8>,
+            description: Vec<u8>,
+            stake_balance: Option<BalanceOf<T>>,
+            balance: BalanceOfMint<T>,
+            destination: T::AccountId,
+        ) {
+            ensure!(balance != BalanceOfMint::<T>::zero(), Error::InvalidSpendingProposalBalance);
+
+            let max_balance: u32 = get_required_stake_by_fraction::<T>(
+                COUNCIL_MINT_MAX_BALANCE_PERCENT,
+                100
+            )
+            .try_into()
+            .unwrap_or_default() as u32;
+
+            ensure!(
+                balance < <BalanceOfMint<T>>::from(max_balance),
+                Error::InvalidSpendingProposalBalance
+            );
+
+            let proposal_code = <governance::council::Call<T>>::spend_from_council_mint(
+                balance.clone(),
+                destination.clone()
+            );
+
+            let proposal_parameters =
+                proposal_types::parameters::spending_proposal::<T>();
+
+            Self::create_proposal(
+                origin,
+                member_id,
+                title,
+                description,
+                stake_balance,
+                proposal_code.encode(),
+                proposal_parameters,
+                ProposalDetails::Spending(balance, destination),
+            )?;
+        }
+
+
+        /// Create 'Set lead' proposal type.
+        /// This proposal uses `replace_lead()` extrinsic from the `content_working_group`  module.
+        pub fn create_set_lead_proposal(
+            origin,
+            member_id: MemberId<T>,
+            title: Vec<u8>,
+            description: Vec<u8>,
+            stake_balance: Option<BalanceOf<T>>,
+            new_lead: Option<(T::MemberId, T::AccountId)>
+        ) {
+            if let Some(lead) = new_lead.clone() {
+                let account_id = lead.1;
+                ensure!(
+                    !<governance::council::Module<T>>::is_councilor(&account_id),
+                    Error::InvalidSetLeadParameterCannotBeCouncilor
+                );
+            }
+
+            let proposal_code =
+                <content_working_group::Call<T>>::replace_lead(new_lead.clone());
+
+            let proposal_parameters =
+                proposal_types::parameters::set_lead_proposal::<T>();
+
+            Self::create_proposal(
+                origin,
+                member_id,
+                title,
+                description,
+                stake_balance,
+                proposal_code.encode(),
+                proposal_parameters,
+                ProposalDetails::SetLead(new_lead),
+            )?;
+        }
+
+        /// Create 'Evict storage provider' proposal type.
+        /// This proposal uses `remove_actor()` extrinsic from the `roles::actors`  module.
+        pub fn create_evict_storage_provider_proposal(
+            origin,
+            member_id: MemberId<T>,
+            title: Vec<u8>,
+            description: Vec<u8>,
+            stake_balance: Option<BalanceOf<T>>,
+            actor_account: T::AccountId,
+        ) {
+            let proposal_code =
+                <roles::actors::Call<T>>::remove_actor(actor_account.clone());
+
+            let proposal_parameters =
+                proposal_types::parameters::evict_storage_provider_proposal::<T>();
+
+            Self::create_proposal(
+                origin,
+                member_id,
+                title,
+                description,
+                stake_balance,
+                proposal_code.encode(),
+                proposal_parameters,
+                ProposalDetails::EvictStorageProvider(actor_account),
+            )?;
+        }
+
+        /// Create 'Evict storage provider' proposal type.
+        /// This proposal uses `set_validator_count()` extrinsic from the Substrate `staking`  module.
+        pub fn create_set_validator_count_proposal(
+            origin,
+            member_id: MemberId<T>,
+            title: Vec<u8>,
+            description: Vec<u8>,
+            stake_balance: Option<BalanceOf<T>>,
+            new_validator_count: u32,
+        ) {
+            ensure!(
+                new_validator_count >= <staking::Module<T>>::minimum_validator_count(),
+                Error::InvalidValidatorCount
+            );
+
+            ensure!(
+                new_validator_count <= 1000, // max validator count
+                Error::InvalidValidatorCount
+            );
+
+            let proposal_code =
+                <staking::Call<T>>::set_validator_count(new_validator_count);
+
+            let proposal_parameters =
+                proposal_types::parameters::set_validator_count_proposal::<T>();
+
+            Self::create_proposal(
+                origin,
+                member_id,
+                title,
+                description,
+                stake_balance,
+                proposal_code.encode(),
+                proposal_parameters,
+                ProposalDetails::SetValidatorCount(new_validator_count),
+            )?;
+        }
+
+        /// Create 'Set storage roles parameters' proposal type.
+        /// This proposal uses `set_role_parameters()` extrinsic from the Substrate `roles::actors`  module.
+        pub fn create_set_storage_role_parameters_proposal(
+            origin,
+            member_id: MemberId<T>,
+            title: Vec<u8>,
+            description: Vec<u8>,
+            stake_balance: Option<BalanceOf<T>>,
+            role_parameters: RoleParameters<BalanceOfGovernanceCurrency<T>, T::BlockNumber>
+        ) {
+            Self::ensure_storage_role_parameters_valid(&role_parameters)?;
+
+            let proposal_code = <roles::actors::Call<T>>::set_role_parameters(
+                Role::StorageProvider,
+                role_parameters.clone()
+            );
+
+            let proposal_parameters =
+                proposal_types::parameters::set_storage_role_parameters_proposal::<T>();
+
+            Self::create_proposal(
+                origin,
+                member_id,
+                title,
+                description,
+                stake_balance,
+                proposal_code.encode(),
+                proposal_parameters,
+                ProposalDetails::SetStorageRoleParameters(role_parameters),
+            )?;
+        }
+
+// *************** Extrinsic to execute
+
+        /// Text proposal extrinsic. Should be used as callable object to pass to the `engine` module.
+        fn execute_text_proposal(
+            origin,
+            title: Vec<u8>,
+            _description: Vec<u8>,
+            _text: Vec<u8>,
+        ) {
+            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 execute_runtime_upgrade_proposal(
+            origin,
+            title: Vec<u8>,
+            _description: Vec<u8>,
+            wasm: Vec<u8>,
+        ) {
+            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);
+            }
+
+            <system::Module<T>>::set_code(cloned_origin2, wasm)?;
+        }
+    }
+}
+
+impl<T: Trait> Module<T> {
+    // Multiplies the T::Origin.
+    // In our current substrate version system::Origin doesn't support clone(),
+    // but it will be supported in latest up-to-date substrate version.
+    // TODO: delete when T::Origin will support the clone()
+    fn double_origin(origin: T::Origin) -> (T::Origin, T::Origin) {
+        let coerced_origin = origin.into().ok().unwrap_or(RawOrigin::None);
+
+        let (cloned_origin1, cloned_origin2) = match coerced_origin {
+            RawOrigin::None => (RawOrigin::None, RawOrigin::None),
+            RawOrigin::Root => (RawOrigin::Root, RawOrigin::Root),
+            RawOrigin::Signed(account_id) => (
+                RawOrigin::Signed(account_id.clone()),
+                RawOrigin::Signed(account_id),
+            ),
+        };
+
+        (cloned_origin1.into(), cloned_origin2.into())
+    }
+
+    // Generic template proposal builder
+    fn create_proposal(
+        origin: T::Origin,
+        member_id: MemberId<T>,
+        title: Vec<u8>,
+        description: Vec<u8>,
+        stake_balance: Option<BalanceOf<T>>,
+        proposal_code: Vec<u8>,
+        proposal_parameters: ProposalParameters<T::BlockNumber, BalanceOf<T>>,
+        proposal_details: ProposalDetails<
+            BalanceOfMint<T>,
+            BalanceOfGovernanceCurrency<T>,
+            T::BlockNumber,
+            T::AccountId,
+            T::MemberId,
+        >,
+    ) -> DispatchResult<Error> {
+        let account_id =
+            T::MembershipOriginValidator::ensure_actor_origin(origin, member_id.clone())?;
+
+        <proposal_engine::Module<T>>::ensure_create_proposal_parameters_are_valid(
+            &proposal_parameters,
+            &title,
+            &description,
+            stake_balance,
+        )?;
+
+        <proposal_discussion::Module<T>>::ensure_can_create_thread(member_id.clone(), &title)?;
+
+        let discussion_thread_id =
+            <proposal_discussion::Module<T>>::create_thread(member_id, title.clone())?;
+
+        let proposal_id = <proposal_engine::Module<T>>::create_proposal(
+            account_id,
+            member_id,
+            proposal_parameters,
+            title,
+            description,
+            stake_balance,
+            proposal_code,
+        )?;
+
+        <ThreadIdByProposalId<T>>::insert(proposal_id, discussion_thread_id);
+        <ProposalDetailsByProposalId<T>>::insert(proposal_id, proposal_details);
+
+        Ok(())
+    }
+
+    // validates storage role parameters for the 'Set storage role parameters' proposal
+    fn ensure_storage_role_parameters_valid(
+        role_parameters: &RoleParameters<BalanceOfGovernanceCurrency<T>, T::BlockNumber>,
+    ) -> Result<(), Error> {
+        ensure!(
+            role_parameters.min_actors <= 5,
+            Error::InvalidStorageRoleParameterMinActors
+        );
+
+        ensure!(
+            role_parameters.max_actors >= 5,
+            Error::InvalidStorageRoleParameterMaxActors
+        );
+
+        ensure!(
+            role_parameters.max_actors < 100,
+            Error::InvalidStorageRoleParameterMaxActors
+        );
+
+        ensure!(
+            role_parameters.reward_period >= T::BlockNumber::from(600),
+            Error::InvalidStorageRoleParameterRewardPeriod
+        );
+
+        ensure!(
+            role_parameters.reward_period <= T::BlockNumber::from(3600),
+            Error::InvalidStorageRoleParameterRewardPeriod
+        );
+
+        ensure!(
+            role_parameters.bonding_period >= T::BlockNumber::from(600),
+            Error::InvalidStorageRoleParameterBondingPeriod
+        );
+
+        ensure!(
+            role_parameters.bonding_period <= T::BlockNumber::from(28800),
+            Error::InvalidStorageRoleParameterBondingPeriod
+        );
+
+        ensure!(
+            role_parameters.unbonding_period >= T::BlockNumber::from(600),
+            Error::InvalidStorageRoleParameterUnbondingPeriod
+        );
+
+        ensure!(
+            role_parameters.unbonding_period <= T::BlockNumber::from(28800),
+            Error::InvalidStorageRoleParameterUnbondingPeriod
+        );
+
+        ensure!(
+            role_parameters.min_service_period >= T::BlockNumber::from(600),
+            Error::InvalidStorageRoleParameterMinServicePeriod
+        );
+
+        ensure!(
+            role_parameters.min_service_period <= T::BlockNumber::from(28800),
+            Error::InvalidStorageRoleParameterMinServicePeriod
+        );
+
+        ensure!(
+            role_parameters.startup_grace_period >= T::BlockNumber::from(600),
+            Error::InvalidStorageRoleParameterStartupGracePeriod
+        );
+
+        ensure!(
+            role_parameters.startup_grace_period <= T::BlockNumber::from(28800),
+            Error::InvalidStorageRoleParameterStartupGracePeriod
+        );
+
+        ensure!(
+            role_parameters.min_stake > <BalanceOfGovernanceCurrency<T>>::from(0u32),
+            Error::InvalidStorageRoleParameterMinStake
+        );
+
+        let max_min_stake: u32 = get_required_stake_by_fraction::<T>(1, 100)
+            .try_into()
+            .unwrap_or_default() as u32;
+
+        ensure!(
+            role_parameters.min_stake < <BalanceOfGovernanceCurrency<T>>::from(max_min_stake),
+            Error::InvalidStorageRoleParameterMinStake
+        );
+
+        ensure!(
+            role_parameters.entry_request_fee > <BalanceOfGovernanceCurrency<T>>::from(0u32),
+            Error::InvalidStorageRoleParameterEntryRequestFee
+        );
+
+        let max_entry_request_fee: u32 = get_required_stake_by_fraction::<T>(1, 100)
+            .try_into()
+            .unwrap_or_default() as u32;
+
+        ensure!(
+            role_parameters.entry_request_fee
+                < <BalanceOfGovernanceCurrency<T>>::from(max_entry_request_fee),
+            Error::InvalidStorageRoleParameterEntryRequestFee
+        );
+
+        ensure!(
+            role_parameters.reward > <BalanceOfGovernanceCurrency<T>>::from(0u32),
+            Error::InvalidStorageRoleParameterReward
+        );
+
+        let max_reward: u32 = get_required_stake_by_fraction::<T>(1, 1000)
+            .try_into()
+            .unwrap_or_default() as u32;
+
+        ensure!(
+            role_parameters.reward < <BalanceOfGovernanceCurrency<T>>::from(max_reward),
+            Error::InvalidStorageRoleParameterReward
+        );
+
+        Ok(())
+    }
+
+    /*
+    entry_request_fee [tJOY]	>0	<1%	NA
+    * Not enforced by runtime. Should not be displayed in the UI, or at least grayed out.
+    ** Should not be displayed in the UI, or at least grayed out.
+        */
+
+    // validates council election parameters for the 'Set election parameters' proposal
+    pub(crate) fn ensure_council_election_parameters_valid(
+        election_parameters: &ElectionParameters<BalanceOfGovernanceCurrency<T>, T::BlockNumber>,
+    ) -> Result<(), Error> {
+        ensure!(
+            election_parameters.council_size >= 4,
+            Error::InvalidCouncilElectionParameterCouncilSize
+        );
+
+        ensure!(
+            election_parameters.council_size <= 20,
+            Error::InvalidCouncilElectionParameterCouncilSize
+        );
+
+        ensure!(
+            election_parameters.candidacy_limit >= 25,
+            Error::InvalidCouncilElectionParameterCandidacyLimit
+        );
+
+        ensure!(
+            election_parameters.candidacy_limit <= 100,
+            Error::InvalidCouncilElectionParameterCandidacyLimit
+        );
+
+        ensure!(
+            election_parameters.min_voting_stake >= <BalanceOfGovernanceCurrency<T>>::one(),
+            Error::InvalidCouncilElectionParameterMinVotingStake
+        );
+
+        ensure!(
+            election_parameters.min_voting_stake
+                <= <BalanceOfGovernanceCurrency<T>>::from(100000u32),
+            Error::InvalidCouncilElectionParameterMinVotingStake
+        );
+
+        ensure!(
+            election_parameters.new_term_duration >= T::BlockNumber::from(14400),
+            Error::InvalidCouncilElectionParameterNewTermDuration
+        );
+
+        ensure!(
+            election_parameters.new_term_duration <= T::BlockNumber::from(432000),
+            Error::InvalidCouncilElectionParameterNewTermDuration
+        );
+
+        ensure!(
+            election_parameters.revealing_period >= T::BlockNumber::from(14400),
+            Error::InvalidCouncilElectionParameterRevealingPeriod
+        );
+
+        ensure!(
+            election_parameters.revealing_period <= T::BlockNumber::from(43200),
+            Error::InvalidCouncilElectionParameterRevealingPeriod
+        );
+
+        ensure!(
+            election_parameters.voting_period >= T::BlockNumber::from(14400),
+            Error::InvalidCouncilElectionParameterVotingPeriod
+        );
+
+        ensure!(
+            election_parameters.voting_period <= T::BlockNumber::from(43200),
+            Error::InvalidCouncilElectionParameterVotingPeriod
+        );
+
+        ensure!(
+            election_parameters.announcing_period >= T::BlockNumber::from(14400),
+            Error::InvalidCouncilElectionParameterAnnouncingPeriod
+        );
+
+        ensure!(
+            election_parameters.announcing_period <= T::BlockNumber::from(43200),
+            Error::InvalidCouncilElectionParameterAnnouncingPeriod
+        );
+
+        ensure!(
+            election_parameters.min_council_stake >= <BalanceOfGovernanceCurrency<T>>::one(),
+            Error::InvalidCouncilElectionParameterMinCouncilStake
+        );
+
+        ensure!(
+            election_parameters.min_council_stake
+                <= <BalanceOfGovernanceCurrency<T>>::from(100000u32),
+            Error::InvalidCouncilElectionParameterMinCouncilStake
+        );
+
+        Ok(())
+    }
+}
+
+// calculates required stake value using total issuance value and stake percentage. Truncates to
+// lowest integer value. Value fraction is defined by numerator and denominator.
+pub(crate) fn get_required_stake_by_fraction<T: crate::Trait>(
+    numerator: u32,
+    denominator: u32,
+) -> BalanceOf<T> {
+    let total_issuance: u128 = <CurrencyOf<T>>::total_issuance().try_into().unwrap_or(0) as u128;
+    let required_stake =
+        Perbill::from_rational_approximation(numerator, denominator) * total_issuance;
+
+    let balance: BalanceOf<T> = required_stake.saturated_into();
+
+    balance
+}

+ 49 - 0
runtime-modules/proposals/codex/src/proposal_types/mod.rs

@@ -0,0 +1,49 @@
+pub(crate) mod parameters;
+
+use codec::{Decode, Encode};
+use rstd::vec::Vec;
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+
+use crate::ElectionParameters;
+use roles::actors::RoleParameters;
+
+/// Proposal details provide voters the information required for the perceived voting.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Debug)]
+pub enum ProposalDetails<MintedBalance, CurrencyBalance, BlockNumber, AccountId, MemberId> {
+    /// The text of the `text` proposal
+    Text(Vec<u8>),
+
+    /// The hash of wasm code for the `runtime upgrade` proposal
+    RuntimeUpgrade(Vec<u8>),
+
+    /// Election parameters for the `set election parameters` proposal
+    SetElectionParameters(ElectionParameters<CurrencyBalance, BlockNumber>),
+
+    /// Balance and destination account for the `spending` proposal
+    Spending(MintedBalance, AccountId),
+
+    /// New leader memberId and account_id for the `set lead` proposal
+    SetLead(Option<(MemberId, AccountId)>),
+
+    /// Balance for the `set content working group mint capacity` proposal
+    SetContentWorkingGroupMintCapacity(MintedBalance),
+
+    /// AccountId for the `evict storage provider` proposal
+    EvictStorageProvider(AccountId),
+
+    /// Validator count for the `set validator count` proposal
+    SetValidatorCount(u32),
+
+    /// Role parameters for the `set storage role parameters` proposal
+    SetStorageRoleParameters(RoleParameters<CurrencyBalance, BlockNumber>),
+}
+
+impl<MintedBalance, CurrencyBalance, BlockNumber, AccountId, MemberId> Default
+    for ProposalDetails<MintedBalance, CurrencyBalance, BlockNumber, AccountId, MemberId>
+{
+    fn default() -> Self {
+        ProposalDetails::Text(b"invalid proposal details".to_vec())
+    }
+}

+ 156 - 0
runtime-modules/proposals/codex/src/proposal_types/parameters.rs

@@ -0,0 +1,156 @@
+use crate::{get_required_stake_by_fraction, BalanceOf, ProposalParameters};
+
+// Proposal parameters for the 'Set validator count' proposal
+pub(crate) fn set_validator_count_proposal<T: crate::Trait>(
+) -> ProposalParameters<T::BlockNumber, BalanceOf<T>> {
+    ProposalParameters {
+        voting_period: T::BlockNumber::from(43200u32),
+        grace_period: T::BlockNumber::from(0u32),
+        approval_quorum_percentage: 66,
+        approval_threshold_percentage: 80,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(get_required_stake_by_fraction::<T>(25, 10000)),
+    }
+}
+
+// Proposal parameters for the upgrade runtime proposal
+pub(crate) fn runtime_upgrade_proposal<T: crate::Trait>(
+) -> ProposalParameters<T::BlockNumber, BalanceOf<T>> {
+    ProposalParameters {
+        voting_period: T::BlockNumber::from(72000u32),
+        grace_period: T::BlockNumber::from(72000u32),
+        approval_quorum_percentage: 80,
+        approval_threshold_percentage: 100,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(get_required_stake_by_fraction::<T>(1, 100)),
+    }
+}
+
+// Proposal parameters for the text proposal
+pub(crate) fn text_proposal<T: crate::Trait>() -> ProposalParameters<T::BlockNumber, BalanceOf<T>> {
+    ProposalParameters {
+        voting_period: T::BlockNumber::from(72000u32),
+        grace_period: T::BlockNumber::from(0u32),
+        approval_quorum_percentage: 66,
+        approval_threshold_percentage: 80,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(get_required_stake_by_fraction::<T>(25, 10000)),
+    }
+}
+
+// Proposal parameters for the 'Set Election Parameters' proposal
+pub(crate) fn set_election_parameters_proposal<T: crate::Trait>(
+) -> ProposalParameters<T::BlockNumber, BalanceOf<T>> {
+    ProposalParameters {
+        voting_period: T::BlockNumber::from(72000u32),
+        grace_period: T::BlockNumber::from(201601u32),
+        approval_quorum_percentage: 66,
+        approval_threshold_percentage: 80,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(get_required_stake_by_fraction::<T>(75, 10000)),
+    }
+}
+
+// Proposal parameters for the 'Set content working group mint capacity' proposal
+pub(crate) fn set_content_working_group_mint_capacity_proposal<T: crate::Trait>(
+) -> ProposalParameters<T::BlockNumber, BalanceOf<T>> {
+    ProposalParameters {
+        voting_period: T::BlockNumber::from(43200u32),
+        grace_period: T::BlockNumber::from(0u32),
+        approval_quorum_percentage: 50,
+        approval_threshold_percentage: 75,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(get_required_stake_by_fraction::<T>(25, 10000)),
+    }
+}
+
+// Proposal parameters for the 'Spending' proposal
+pub(crate) fn spending_proposal<T: crate::Trait>(
+) -> ProposalParameters<T::BlockNumber, BalanceOf<T>> {
+    ProposalParameters {
+        voting_period: T::BlockNumber::from(72000u32),
+        grace_period: T::BlockNumber::from(14400u32),
+        approval_quorum_percentage: 66,
+        approval_threshold_percentage: 80,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(get_required_stake_by_fraction::<T>(25, 10000)),
+    }
+}
+
+// Proposal parameters for the 'Set lead' proposal
+pub(crate) fn set_lead_proposal<T: crate::Trait>(
+) -> ProposalParameters<T::BlockNumber, BalanceOf<T>> {
+    ProposalParameters {
+        voting_period: T::BlockNumber::from(43200u32),
+        grace_period: T::BlockNumber::from(0u32),
+        approval_quorum_percentage: 66,
+        approval_threshold_percentage: 80,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(get_required_stake_by_fraction::<T>(25, 10000)),
+    }
+}
+
+// Proposal parameters for the 'Evict storage provider' proposal
+pub(crate) fn evict_storage_provider_proposal<T: crate::Trait>(
+) -> ProposalParameters<T::BlockNumber, BalanceOf<T>> {
+    ProposalParameters {
+        voting_period: T::BlockNumber::from(43200u32),
+        grace_period: T::BlockNumber::from(0u32),
+        approval_quorum_percentage: 50,
+        approval_threshold_percentage: 75,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(get_required_stake_by_fraction::<T>(1, 1000)),
+    }
+}
+
+// Proposal parameters for the 'Set storage role parameters' proposal
+pub(crate) fn set_storage_role_parameters_proposal<T: crate::Trait>(
+) -> ProposalParameters<T::BlockNumber, BalanceOf<T>> {
+    ProposalParameters {
+        voting_period: T::BlockNumber::from(43200u32),
+        grace_period: T::BlockNumber::from(14400u32),
+        approval_quorum_percentage: 75,
+        approval_threshold_percentage: 80,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(get_required_stake_by_fraction::<T>(25, 10000)),
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use crate::proposal_types::parameters::get_required_stake_by_fraction;
+    use crate::tests::{increase_total_balance_issuance, initial_test_ext, Test};
+
+    pub use sr_primitives::Perbill;
+
+    #[test]
+    fn calculate_get_required_stake_by_fraction_with_zero_issuance() {
+        initial_test_ext()
+            .execute_with(|| assert_eq!(get_required_stake_by_fraction::<Test>(5, 7), 0));
+    }
+
+    #[test]
+    fn calculate_stake_by_percentage_for_defined_issuance_succeeds() {
+        initial_test_ext().execute_with(|| {
+            increase_total_balance_issuance(50000);
+            assert_eq!(get_required_stake_by_fraction::<Test>(1, 1000), 50)
+        });
+    }
+
+    #[test]
+    fn calculate_stake_by_percentage_for_defined_issuance_with_fraction_loss() {
+        initial_test_ext().execute_with(|| {
+            increase_total_balance_issuance(1111);
+            assert_eq!(get_required_stake_by_fraction::<Test>(3, 1000), 3);
+        });
+    }
+}

+ 288 - 0
runtime-modules/proposals/codex/src/tests/mock.rs

@@ -0,0 +1,288 @@
+#![cfg(test)]
+// srml_staking_reward_curve::build! - substrate macro produces a warning.
+// TODO: remove after post-Rome substrate upgrade
+#![allow(array_into_iter)]
+
+pub use primitives::{Blake2Hasher, H256};
+use proposal_engine::VotersParameters;
+use sr_primitives::curve::PiecewiseLinear;
+pub use sr_primitives::{
+    testing::{Digest, DigestItem, Header, UintAuthorityId},
+    traits::{BlakeTwo256, Convert, IdentityLookup, OnFinalize},
+    weights::Weight,
+    BuildStorage, DispatchError, Perbill,
+};
+use sr_staking_primitives::SessionIndex;
+use srml_support::{impl_outer_dispatch, impl_outer_origin, parameter_types};
+pub use system;
+
+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;
+    pub const StakePoolId: [u8; 8] = *b"joystake";
+}
+
+impl_outer_dispatch! {
+    pub enum Call for Test where origin: Origin {
+        codex::ProposalCodex,
+        proposals::ProposalsEngine,
+    }
+}
+
+impl common::currency::GovernanceCurrency for Test {
+    type Currency = balances::Module<Self>;
+}
+
+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;
+    pub const CreationFee: 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 = ();
+
+    type Event = ();
+
+    type DustRemoval = ();
+    type TransferPayment = ();
+    type ExistentialDeposit = ExistentialDeposit;
+    type TransferFee = TransferFee;
+    type CreationFee = CreationFee;
+}
+
+impl stake::Trait for Test {
+    type Currency = Balances;
+    type StakePoolId = StakePoolId;
+    type StakingEventsHandler = ();
+    type StakeId = u64;
+    type SlashId = u64;
+}
+
+parameter_types! {
+    pub const CancellationFee: u64 = 5;
+    pub const RejectionFee: u64 = 3;
+    pub const TitleMaxLength: u32 = 100;
+    pub const DescriptionMaxLength: u32 = 10000;
+    pub const MaxActiveProposalLimit: u32 = 100;
+}
+
+impl proposal_engine::Trait for Test {
+    type Event = ();
+    type ProposerOriginValidator = ();
+    type VoterOriginValidator = ();
+    type TotalVotersCounter = MockVotersParameters;
+    type ProposalId = u32;
+    type StakeHandlerProvider = proposal_engine::DefaultStakeHandlerProvider;
+    type CancellationFee = CancellationFee;
+    type RejectionFee = RejectionFee;
+    type TitleMaxLength = TitleMaxLength;
+    type DescriptionMaxLength = DescriptionMaxLength;
+    type MaxActiveProposalLimit = MaxActiveProposalLimit;
+    type DispatchableCallCode = crate::Call<Test>;
+}
+
+impl Default for crate::Call<Test> {
+    fn default() -> Self {
+        panic!("shouldn't call default for Call");
+    }
+}
+
+impl mint::Trait for Test {
+    type Currency = Balances;
+    type MintId = u64;
+}
+
+impl governance::council::Trait for Test {
+    type Event = ();
+    type CouncilTermEnded = ();
+}
+
+impl common::origin_validator::ActorOriginValidator<Origin, u64, u64> for () {
+    fn ensure_actor_origin(origin: Origin, _: u64) -> Result<u64, &'static str> {
+        let account_id = system::ensure_signed(origin)?;
+
+        Ok(account_id)
+    }
+}
+
+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 Event = ();
+    type PostAuthorOriginValidator = ();
+    type ThreadId = u32;
+    type PostId = u32;
+    type MaxPostEditionNumber = MaxPostEditionNumber;
+    type ThreadTitleLengthLimit = ThreadTitleLengthLimit;
+    type PostLengthLimit = PostLengthLimit;
+    type MaxThreadInARowNumber = MaxThreadInARowNumber;
+}
+
+pub struct MockVotersParameters;
+impl VotersParameters for MockVotersParameters {
+    fn total_voters_count() -> u32 {
+        4
+    }
+}
+
+parameter_types! {
+    pub const TextProposalMaxLength: u32 = 20_000;
+    pub const RuntimeUpgradeWasmProposalMaxLength: u32 = 20_000;
+}
+
+impl governance::election::Trait for Test {
+    type Event = ();
+    type CouncilElected = ();
+}
+
+impl content_working_group::Trait for Test {
+    type Event = ();
+}
+
+impl recurring_rewards::Trait for Test {
+    type PayoutStatusHandler = ();
+    type RecipientId = u64;
+    type RewardRelationshipId = u64;
+}
+
+impl versioned_store_permissions::Trait for Test {
+    type Credential = u64;
+    type CredentialChecker = ();
+    type CreateClassPermissionsChecker = ();
+}
+
+impl versioned_store::Trait for Test {
+    type Event = ();
+}
+
+impl hiring::Trait for Test {
+    type OpeningId = u64;
+    type ApplicationId = u64;
+    type ApplicationDeactivatedHandler = ();
+    type StakeHandlerProvider = hiring::Module<Self>;
+}
+
+impl roles::actors::Trait for Test {
+    type Event = ();
+    type OnActorRemoved = ();
+}
+
+impl roles::actors::ActorRemoved<Test> for () {
+    fn actor_removed(_: &u64) {}
+}
+
+srml_staking_reward_curve::build! {
+    const I_NPOS: PiecewiseLinear<'static> = curve!(
+        min_inflation: 0_025_000,
+        max_inflation: 0_100_000,
+        ideal_stake: 0_500_000,
+        falloff: 0_050_000,
+        max_piece_count: 40,
+        test_precision: 0_005_000,
+    );
+}
+
+parameter_types! {
+    pub const SessionsPerEra: SessionIndex = 3;
+    pub const BondingDuration: staking::EraIndex = 3;
+    pub const RewardCurve: &'static PiecewiseLinear<'static> = &I_NPOS;
+}
+impl staking::Trait for Test {
+    type Currency = balances::Module<Self>;
+    type Time = timestamp::Module<Self>;
+    type CurrencyToVote = ();
+    type RewardRemainder = ();
+    type Event = ();
+    type Slash = ();
+    type Reward = ();
+    type SessionsPerEra = SessionsPerEra;
+    type BondingDuration = BondingDuration;
+    type SessionInterface = Self;
+    type RewardCurve = RewardCurve;
+}
+
+impl staking::SessionInterface<u64> for Test {
+    fn disable_validator(_: &u64) -> Result<bool, ()> {
+        unimplemented!()
+    }
+
+    fn validators() -> Vec<u64> {
+        unimplemented!()
+    }
+
+    fn prune_historical_up_to(_: u32) {
+        unimplemented!()
+    }
+}
+
+impl crate::Trait for Test {
+    type TextProposalMaxLength = TextProposalMaxLength;
+    type RuntimeUpgradeWasmProposalMaxLength = RuntimeUpgradeWasmProposalMaxLength;
+    type MembershipOriginValidator = ();
+}
+
+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<Self::AccountId>;
+    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;
+}
+
+pub fn initial_test_ext() -> runtime_io::TestExternalities {
+    let t = system::GenesisConfig::default()
+        .build_storage::<Test>()
+        .unwrap();
+
+    t.into()
+}
+
+pub type ProposalCodex = crate::Module<Test>;
+pub type ProposalsEngine = proposal_engine::Module<Test>;
+pub type Balances = balances::Module<Test>;

+ 1054 - 0
runtime-modules/proposals/codex/src/tests/mod.rs

@@ -0,0 +1,1054 @@
+mod mock;
+
+use governance::election_params::ElectionParameters;
+use srml_support::traits::Currency;
+use srml_support::StorageMap;
+use system::RawOrigin;
+
+use crate::{BalanceOf, Error, ProposalDetails};
+use proposal_engine::ProposalParameters;
+use roles::actors::RoleParameters;
+use runtime_io::blake2_256;
+use srml_support::dispatch::DispatchResult;
+
+pub use mock::*;
+
+pub(crate) fn increase_total_balance_issuance(balance: u64) {
+    increase_total_balance_issuance_using_account_id(999, balance);
+}
+
+pub(crate) fn increase_total_balance_issuance_using_account_id(account_id: u64, balance: u64) {
+    let initial_balance = Balances::total_issuance();
+    {
+        let _ = <Test as stake::Trait>::Currency::deposit_creating(&account_id, balance);
+    }
+    assert_eq!(Balances::total_issuance(), initial_balance + balance);
+}
+
+struct ProposalTestFixture<InsufficientRightsCall, EmptyStakeCall, InvalidStakeCall, SuccessfulCall>
+where
+    InsufficientRightsCall: Fn() -> DispatchResult<crate::Error>,
+    EmptyStakeCall: Fn() -> DispatchResult<crate::Error>,
+    InvalidStakeCall: Fn() -> DispatchResult<crate::Error>,
+    SuccessfulCall: Fn() -> DispatchResult<crate::Error>,
+{
+    insufficient_rights_call: InsufficientRightsCall,
+    empty_stake_call: EmptyStakeCall,
+    invalid_stake_call: InvalidStakeCall,
+    successful_call: SuccessfulCall,
+    proposal_parameters: ProposalParameters<u64, u64>,
+    proposal_details: ProposalDetails<u64, u64, u64, u64, u64>,
+}
+
+impl<InsufficientRightsCall, EmptyStakeCall, InvalidStakeCall, SuccessfulCall>
+    ProposalTestFixture<InsufficientRightsCall, EmptyStakeCall, InvalidStakeCall, SuccessfulCall>
+where
+    InsufficientRightsCall: Fn() -> DispatchResult<crate::Error>,
+    EmptyStakeCall: Fn() -> DispatchResult<crate::Error>,
+    InvalidStakeCall: Fn() -> DispatchResult<crate::Error>,
+    SuccessfulCall: Fn() -> DispatchResult<crate::Error>,
+{
+    fn check_for_invalid_stakes(&self) {
+        assert_eq!((self.empty_stake_call)(), Err(Error::Other("EmptyStake")));
+
+        assert_eq!(
+            (self.invalid_stake_call)(),
+            Err(Error::Other("StakeDiffersFromRequired"))
+        );
+    }
+
+    fn check_call_for_insufficient_rights(&self) {
+        assert_eq!(
+            (self.insufficient_rights_call)(),
+            Err(Error::Other("RequireSignedOrigin"))
+        );
+    }
+
+    fn check_for_successful_call(&self) {
+        let account_id = 1;
+        let _imbalance = <Test as stake::Trait>::Currency::deposit_creating(&account_id, 50000);
+
+        assert_eq!((self.successful_call)(), Ok(()));
+
+        // a discussion was created
+        let thread_id = <crate::ThreadIdByProposalId<Test>>::get(1);
+        assert_eq!(thread_id, 1);
+
+        let proposal_id = 1;
+        let proposal = ProposalsEngine::proposals(proposal_id);
+        // check for correct proposal parameters
+        assert_eq!(proposal.parameters, self.proposal_parameters);
+
+        // proposal details was set
+        let details = <crate::ProposalDetailsByProposalId<Test>>::get(proposal_id);
+        assert_eq!(details, self.proposal_details);
+    }
+
+    pub fn check_all(&self) {
+        self.check_call_for_insufficient_rights();
+        self.check_for_invalid_stakes();
+        self.check_for_successful_call();
+    }
+}
+
+#[test]
+fn create_text_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance(500000);
+
+        let proposal_fixture = ProposalTestFixture {
+            insufficient_rights_call: || {
+                ProposalCodex::create_text_proposal(
+                    RawOrigin::None.into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    b"text".to_vec(),
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_text_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    b"text".to_vec(),
+                )
+            },
+            invalid_stake_call: || {
+                ProposalCodex::create_text_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(5000u32)),
+                    b"text".to_vec(),
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_text_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(1250u32)),
+                    b"text".to_vec(),
+                )
+            },
+            proposal_parameters: crate::proposal_types::parameters::text_proposal::<Test>(),
+            proposal_details: ProposalDetails::Text(b"text".to_vec()),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+#[test]
+fn create_text_proposal_codex_call_fails_with_incorrect_text_size() {
+    initial_test_ext().execute_with(|| {
+        let origin = RawOrigin::Signed(1).into();
+
+        let long_text = [0u8; 30000].to_vec();
+        assert_eq!(
+            ProposalCodex::create_text_proposal(
+                origin,
+                1,
+                b"title".to_vec(),
+                b"body".to_vec(),
+                None,
+                long_text,
+            ),
+            Err(Error::TextProposalSizeExceeded)
+        );
+
+        assert_eq!(
+            ProposalCodex::create_text_proposal(
+                RawOrigin::Signed(1).into(),
+                1,
+                b"title".to_vec(),
+                b"body".to_vec(),
+                None,
+                Vec::new(),
+            ),
+            Err(Error::TextProposalIsEmpty)
+        );
+    });
+}
+
+#[test]
+fn create_runtime_upgrade_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance(500000);
+
+        let proposal_fixture = ProposalTestFixture {
+            insufficient_rights_call: || {
+                ProposalCodex::create_runtime_upgrade_proposal(
+                    RawOrigin::None.into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    b"wasm".to_vec(),
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_runtime_upgrade_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    b"wasm".to_vec(),
+                )
+            },
+            invalid_stake_call: || {
+                ProposalCodex::create_runtime_upgrade_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(500u32)),
+                    b"wasm".to_vec(),
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_runtime_upgrade_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(5000u32)),
+                    b"wasm".to_vec(),
+                )
+            },
+            proposal_parameters: crate::proposal_types::parameters::runtime_upgrade_proposal::<Test>(),
+            proposal_details: ProposalDetails::RuntimeUpgrade(blake2_256(b"wasm").to_vec()),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+#[test]
+fn create_upgrade_runtime_proposal_codex_call_fails_with_incorrect_wasm_size() {
+    initial_test_ext().execute_with(|| {
+        let origin = RawOrigin::Signed(1).into();
+
+        let long_wasm = [0u8; 30000].to_vec();
+        assert_eq!(
+            ProposalCodex::create_runtime_upgrade_proposal(
+                origin,
+                1,
+                b"title".to_vec(),
+                b"body".to_vec(),
+                None,
+                long_wasm,
+            ),
+            Err(Error::RuntimeProposalSizeExceeded)
+        );
+
+        assert_eq!(
+            ProposalCodex::create_runtime_upgrade_proposal(
+                RawOrigin::Signed(1).into(),
+                1,
+                b"title".to_vec(),
+                b"body".to_vec(),
+                None,
+                Vec::new(),
+            ),
+            Err(Error::RuntimeProposalIsEmpty)
+        );
+    });
+}
+
+#[test]
+fn create_set_election_parameters_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance(500000);
+
+        let proposal_fixture = ProposalTestFixture {
+            insufficient_rights_call: || {
+                ProposalCodex::create_set_election_parameters_proposal(
+                    RawOrigin::None.into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    get_valid_election_parameters(),
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_set_election_parameters_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    get_valid_election_parameters(),
+                )
+            },
+            invalid_stake_call: || {
+                ProposalCodex::create_set_election_parameters_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(50000u32)),
+                    get_valid_election_parameters(),
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_set_election_parameters_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(3750u32)),
+                    get_valid_election_parameters(),
+                )
+            },
+            proposal_parameters:
+                crate::proposal_types::parameters::set_election_parameters_proposal::<Test>(),
+            proposal_details: ProposalDetails::SetElectionParameters(
+                get_valid_election_parameters(),
+            ),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+fn assert_failed_election_parameters_call(
+    election_parameters: ElectionParameters<u64, u64>,
+    error: Error,
+) {
+    assert_eq!(
+        ProposalCodex::create_set_election_parameters_proposal(
+            RawOrigin::Signed(1).into(),
+            1,
+            b"title".to_vec(),
+            b"body".to_vec(),
+            Some(<BalanceOf<Test>>::from(3750u32)),
+            election_parameters,
+        ),
+        Err(error)
+    );
+}
+
+fn get_valid_election_parameters() -> ElectionParameters<u64, u64> {
+    ElectionParameters {
+        announcing_period: 14400,
+        voting_period: 14400,
+        revealing_period: 14400,
+        council_size: 4,
+        candidacy_limit: 25,
+        new_term_duration: 14400,
+        min_council_stake: 1,
+        min_voting_stake: 1,
+    }
+}
+
+#[test]
+fn create_set_election_parameters_call_fails_with_incorrect_parameters() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance_using_account_id(1, 500000);
+
+        let mut election_parameters = get_valid_election_parameters();
+        election_parameters.council_size = 2;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterCouncilSize,
+        );
+
+        election_parameters.council_size = 21;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterCouncilSize,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.candidacy_limit = 22;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterCandidacyLimit,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.candidacy_limit = 122;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterCandidacyLimit,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.min_voting_stake = 0;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterMinVotingStake,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.min_voting_stake = 200000;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterMinVotingStake,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.new_term_duration = 10000;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterNewTermDuration,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.new_term_duration = 500000;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterNewTermDuration,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.min_council_stake = 0;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterMinCouncilStake,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.min_council_stake = 200000;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterMinCouncilStake,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.voting_period = 10000;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterVotingPeriod,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.voting_period = 50000;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterVotingPeriod,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.revealing_period = 10000;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterRevealingPeriod,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.revealing_period = 50000;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterRevealingPeriod,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.announcing_period = 10000;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterAnnouncingPeriod,
+        );
+
+        election_parameters = get_valid_election_parameters();
+        election_parameters.announcing_period = 50000;
+        assert_failed_election_parameters_call(
+            election_parameters,
+            Error::InvalidCouncilElectionParameterAnnouncingPeriod,
+        );
+    });
+}
+
+#[test]
+fn create_working_group_mint_capacity_proposal_fails_with_invalid_parameters() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance(500000);
+
+        assert_eq!(
+            ProposalCodex::create_set_content_working_group_mint_capacity_proposal(
+                RawOrigin::Signed(1).into(),
+                1,
+                b"title".to_vec(),
+                b"body".to_vec(),
+                Some(<BalanceOf<Test>>::from(1250u32)),
+                5001,
+            ),
+            Err(Error::InvalidStorageWorkingGroupMintCapacity)
+        );
+    });
+}
+
+#[test]
+fn create_set_content_working_group_mint_capacity_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance(500000);
+
+        let proposal_fixture = ProposalTestFixture {
+            insufficient_rights_call: || {
+                ProposalCodex::create_set_content_working_group_mint_capacity_proposal(
+                    RawOrigin::None.into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    0,
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_set_content_working_group_mint_capacity_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    0,
+                )
+            },
+            invalid_stake_call: || {
+                ProposalCodex::create_set_content_working_group_mint_capacity_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(5000u32)),
+                    0,
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_set_content_working_group_mint_capacity_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(1250u32)),
+                    10,
+                )
+            },
+            proposal_parameters: crate::proposal_types::parameters::set_content_working_group_mint_capacity_proposal::<Test>(),
+            proposal_details: ProposalDetails::SetContentWorkingGroupMintCapacity(10),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+#[test]
+fn create_spending_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance(500000);
+
+        let proposal_fixture = ProposalTestFixture {
+            insufficient_rights_call: || {
+                ProposalCodex::create_spending_proposal(
+                    RawOrigin::None.into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    20,
+                    10,
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_spending_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    20,
+                    10,
+                )
+            },
+            invalid_stake_call: || {
+                ProposalCodex::create_spending_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(5000u32)),
+                    20,
+                    10,
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_spending_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(1250u32)),
+                    100,
+                    2,
+                )
+            },
+            proposal_parameters: crate::proposal_types::parameters::spending_proposal::<Test>(),
+            proposal_details: ProposalDetails::Spending(100, 2),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+#[test]
+fn create_spending_proposal_call_fails_with_incorrect_balance() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance_using_account_id(500000, 1);
+
+        assert_eq!(
+            ProposalCodex::create_spending_proposal(
+                RawOrigin::Signed(1).into(),
+                1,
+                b"title".to_vec(),
+                b"body".to_vec(),
+                Some(<BalanceOf<Test>>::from(1250u32)),
+                0,
+                2,
+            ),
+            Err(Error::InvalidSpendingProposalBalance)
+        );
+
+        assert_eq!(
+            ProposalCodex::create_spending_proposal(
+                RawOrigin::Signed(1).into(),
+                1,
+                b"title".to_vec(),
+                b"body".to_vec(),
+                Some(<BalanceOf<Test>>::from(1250u32)),
+                1001,
+                2,
+            ),
+            Err(Error::InvalidSpendingProposalBalance)
+        );
+    });
+}
+
+#[test]
+fn create_set_lead_proposal_fails_with_proposed_councilor() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance_using_account_id(1, 500000);
+
+        let lead_account_id = 20;
+        <governance::council::Module<Test>>::set_council(
+            RawOrigin::Root.into(),
+            vec![lead_account_id],
+        )
+        .unwrap();
+
+        assert_eq!(
+            ProposalCodex::create_set_lead_proposal(
+                RawOrigin::Signed(1).into(),
+                1,
+                b"title".to_vec(),
+                b"body".to_vec(),
+                Some(<BalanceOf<Test>>::from(1250u32)),
+                Some((20, lead_account_id)),
+            ),
+            Err(Error::InvalidSetLeadParameterCannotBeCouncilor)
+        );
+    });
+}
+
+#[test]
+fn create_set_lead_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance(500000);
+
+        let proposal_fixture = ProposalTestFixture {
+            insufficient_rights_call: || {
+                ProposalCodex::create_set_lead_proposal(
+                    RawOrigin::None.into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    Some((20, 10)),
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_set_lead_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    Some((20, 10)),
+                )
+            },
+            invalid_stake_call: || {
+                ProposalCodex::create_set_lead_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(5000u32)),
+                    Some((20, 10)),
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_set_lead_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(1250u32)),
+                    Some((20, 10)),
+                )
+            },
+            proposal_parameters: crate::proposal_types::parameters::set_lead_proposal::<Test>(),
+            proposal_details: ProposalDetails::SetLead(Some((20, 10))),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+#[test]
+fn create_evict_storage_provider_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance(500000);
+
+        let proposal_fixture = ProposalTestFixture {
+            insufficient_rights_call: || {
+                ProposalCodex::create_evict_storage_provider_proposal(
+                    RawOrigin::None.into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    1,
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_evict_storage_provider_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    1,
+                )
+            },
+            invalid_stake_call: || {
+                ProposalCodex::create_evict_storage_provider_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(5000u32)),
+                    1,
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_evict_storage_provider_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(500u32)),
+                    1,
+                )
+            },
+            proposal_parameters: crate::proposal_types::parameters::evict_storage_provider_proposal::<Test>(),
+            proposal_details: ProposalDetails::EvictStorageProvider(1),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+#[test]
+fn create_set_validator_count_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance(500000);
+
+        let proposal_fixture = ProposalTestFixture {
+            insufficient_rights_call: || {
+                ProposalCodex::create_set_validator_count_proposal(
+                    RawOrigin::None.into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    4,
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_set_validator_count_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    4,
+                )
+            },
+            invalid_stake_call: || {
+                ProposalCodex::create_set_validator_count_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(5000u32)),
+                    4,
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_set_validator_count_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(1250u32)),
+                    4,
+                )
+            },
+            proposal_parameters: crate::proposal_types::parameters::set_validator_count_proposal::<
+                Test,
+            >(),
+            proposal_details: ProposalDetails::SetValidatorCount(4),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+#[test]
+fn create_set_validator_count_proposal_failed_with_invalid_validator_count() {
+    initial_test_ext().execute_with(|| {
+        assert_eq!(
+            ProposalCodex::create_set_validator_count_proposal(
+                RawOrigin::Signed(1).into(),
+                1,
+                b"title".to_vec(),
+                b"body".to_vec(),
+                Some(<BalanceOf<Test>>::from(500u32)),
+                3,
+            ),
+            Err(Error::InvalidValidatorCount)
+        );
+
+        assert_eq!(
+            ProposalCodex::create_set_validator_count_proposal(
+                RawOrigin::Signed(1).into(),
+                1,
+                b"title".to_vec(),
+                b"body".to_vec(),
+                Some(<BalanceOf<Test>>::from(1001u32)),
+                3,
+            ),
+            Err(Error::InvalidValidatorCount)
+        );
+    });
+}
+
+#[test]
+fn create_set_storage_role_parameters_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance(500000);
+
+        let proposal_fixture = ProposalTestFixture {
+            insufficient_rights_call: || {
+                ProposalCodex::create_set_storage_role_parameters_proposal(
+                    RawOrigin::None.into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    RoleParameters::default(),
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_set_storage_role_parameters_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    None,
+                    RoleParameters::default(),
+                )
+            },
+            invalid_stake_call: || {
+                ProposalCodex::create_set_storage_role_parameters_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(5000u32)),
+                    RoleParameters::default(),
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_set_storage_role_parameters_proposal(
+                    RawOrigin::Signed(1).into(),
+                    1,
+                    b"title".to_vec(),
+                    b"body".to_vec(),
+                    Some(<BalanceOf<Test>>::from(1250u32)),
+                    RoleParameters::default(),
+                )
+            },
+            proposal_parameters:
+                crate::proposal_types::parameters::set_storage_role_parameters_proposal::<Test>(),
+            proposal_details: ProposalDetails::SetStorageRoleParameters(RoleParameters::default()),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+fn assert_failed_set_storage_parameters_call(
+    role_parameters: RoleParameters<u64, u64>,
+    error: Error,
+) {
+    assert_eq!(
+        ProposalCodex::create_set_storage_role_parameters_proposal(
+            RawOrigin::Signed(1).into(),
+            1,
+            b"title".to_vec(),
+            b"body".to_vec(),
+            Some(<BalanceOf<Test>>::from(500u32)),
+            role_parameters,
+        ),
+        Err(error)
+    );
+}
+
+#[test]
+fn create_set_storage_role_parameters_proposal_fails_with_invalid_parameters() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance(500000);
+
+        let mut role_parameters = RoleParameters::default();
+        role_parameters.min_actors = 6;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterMinActors,
+        );
+
+        role_parameters = RoleParameters::default();
+        role_parameters.max_actors = 4;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterMaxActors,
+        );
+
+        role_parameters = RoleParameters::default();
+        role_parameters.max_actors = 100;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterMaxActors,
+        );
+
+        role_parameters = RoleParameters::default();
+        role_parameters.reward_period = 599;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterRewardPeriod,
+        );
+
+        role_parameters.reward_period = 28801;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterRewardPeriod,
+        );
+
+        role_parameters = RoleParameters::default();
+        role_parameters.bonding_period = 599;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterBondingPeriod,
+        );
+
+        role_parameters = RoleParameters::default();
+        role_parameters.bonding_period = 28801;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterBondingPeriod,
+        );
+
+        role_parameters = RoleParameters::default();
+        role_parameters.unbonding_period = 599;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterUnbondingPeriod,
+        );
+
+        role_parameters = RoleParameters::default();
+        role_parameters.unbonding_period = 28801;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterUnbondingPeriod,
+        );
+
+        role_parameters = RoleParameters::default();
+        role_parameters.min_service_period = 599;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterMinServicePeriod,
+        );
+
+        role_parameters = RoleParameters::default();
+        role_parameters.min_service_period = 28801;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterMinServicePeriod,
+        );
+
+        role_parameters = RoleParameters::default();
+        role_parameters.startup_grace_period = 599;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterStartupGracePeriod,
+        );
+
+        role_parameters = RoleParameters::default();
+        role_parameters.startup_grace_period = 28801;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterStartupGracePeriod,
+        );
+
+        role_parameters = RoleParameters::default();
+        role_parameters.min_stake = 0;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterMinStake,
+        );
+
+        role_parameters = RoleParameters::default();
+        role_parameters.min_stake = 5001;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterMinStake,
+        );
+
+        role_parameters = RoleParameters::default();
+        role_parameters.entry_request_fee = 0;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterEntryRequestFee,
+        );
+
+        role_parameters = RoleParameters::default();
+        role_parameters.entry_request_fee = 5001;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterEntryRequestFee,
+        );
+
+        role_parameters = RoleParameters::default();
+        role_parameters.reward = 0;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterReward,
+        );
+
+        role_parameters = RoleParameters::default();
+        role_parameters.reward = 501;
+        assert_failed_set_storage_parameters_call(
+            role_parameters,
+            Error::InvalidStorageRoleParameterReward,
+        );
+    });
+}

+ 94 - 0
runtime-modules/proposals/discussion/Cargo.toml

@@ -0,0 +1,94 @@
+[package]
+name = 'substrate-proposals-discussion-module'
+version = '2.0.0'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[features]
+default = ['std']
+no_std = []
+std = [
+    'codec/std',
+    'rstd/std',
+    'srml-support/std',
+    'primitives/std',
+    'sr-primitives/std',
+    'system/std',
+    'timestamp/std',
+    'serde',
+    'membership/std',
+    'common/std',
+]
+
+[dependencies.num_enum]
+default_features = false
+version = "0.4.2"
+
+[dependencies.serde]
+features = ['derive']
+optional = true
+version = '1.0.101'
+
+[dependencies.codec]
+default-features = false
+features = ['derive']
+package = 'parity-scale-codec'
+version = '1.0.0'
+
+[dependencies.primitives]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'substrate-primitives'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.rstd]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-std'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.sr-primitives]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-primitives'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.srml-support]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-support'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.system]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-system'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+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 = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies.balances]
+package = 'srml-balances'
+default-features = false
+git = 'https://github.com/paritytech/substrate.git'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'

+ 352 - 0
runtime-modules/proposals/discussion/src/lib.rs

@@ -0,0 +1,352 @@
+//! # Proposals discussion module
+//! Proposals `discussion` module for the Joystream platform. Version 2.
+//! It contains discussion subsystem of the proposals.
+//!
+//! ## Overview
+//!
+//! The proposals discussion module is used by the codex module to provide a platform for discussions
+//! about different proposals. It allows to create discussion threads and then add and update related
+//! posts.
+//!
+//! ## Supported extrinsics
+//! - [add_post](./struct.Module.html#method.add_post) - adds a post to an existing discussion thread
+//! - [update_post](./struct.Module.html#method.update_post) - updates existing post
+//!
+//! ## Public API methods
+//! - [create_thread](./struct.Module.html#method.create_thread) - creates a discussion thread
+//! - [ensure_can_create_thread](./struct.Module.html#method.ensure_can_create_thread) - ensures safe thread creation
+//!
+//! ## Usage
+//!
+//! ```
+//! use srml_support::{decl_module, dispatch::Result};
+//! use system::ensure_root;
+//! use substrate_proposals_discussion_module::{self as discussions};
+//!
+//! pub trait Trait: discussions::Trait + membership::members::Trait {}
+//!
+//! decl_module! {
+//!     pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+//!         pub fn create_discussion(origin, title: Vec<u8>, author_id : T::MemberId) -> Result {
+//!             ensure_root(origin)?;
+//!             <discussions::Module<T>>::ensure_can_create_thread(author_id, &title)?;
+//!             <discussions::Module<T>>::create_thread(author_id, title)?;
+//!             Ok(())
+//!         }
+//!     }
+//! }
+//! # fn main() {}
+//! ```
+
+//!
+
+// 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<T> = <T as membership::members::Trait>::MemberId;
+
+decl_event!(
+    /// Proposals engine events
+    pub enum Event<T>
+    where
+        <T as Trait>::ThreadId,
+        MemberId = MemberId<T>,
+        <T as Trait>::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<Event<Self>> + Into<<Self as system::Trait>::Event>;
+
+    /// Validates post author id and origin combination
+    type PostAuthorOriginValidator: ActorOriginValidator<
+        Self::Origin,
+        MemberId<Self>,
+        Self::AccountId,
+    >;
+
+    /// Discussion thread Id type
+    type ThreadId: From<u32> + Into<u32> + Parameter + Default + Copy;
+
+    /// Post Id type
+    type PostId: From<u32> + Parameter + Default + Copy;
+
+    /// Defines post edition number limit.
+    type MaxPostEditionNumber: Get<u32>;
+
+    /// Defines thread title length limit.
+    type ThreadTitleLengthLimit: Get<u32>;
+
+    /// Defines post length limit.
+    type PostLengthLimit: Get<u32>;
+
+    /// Defines max thread by same author in a row number limit.
+    type MaxThreadInARowNumber: Get<u32>;
+}
+
+decl_error! {
+    /// Discussion module predefined errors
+    pub enum Error {
+        /// 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<system::Error> 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<T: Trait> as ProposalDiscussion {
+        /// Map thread identifier to corresponding thread.
+        pub ThreadById get(thread_by_id): map T::ThreadId =>
+            Thread<MemberId<T>, 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<MemberId<T>, 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<ThreadCounter<MemberId<T>>>;
+    }
+}
+
+decl_module! {
+    /// 'Proposal discussion' substrate module
+    pub struct Module<T: Trait> 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<T>,
+            thread_id : T::ThreadId,
+            text : Vec<u8>
+        ) {
+            T::PostAuthorOriginValidator::ensure_actor_origin(
+                origin,
+                post_author_id,
+            )?;
+            ensure!(<ThreadById<T>>::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,
+                edition_number : 0,
+                thread_id,
+            };
+
+            let post_id = T::PostId::from(new_post_id);
+            <PostThreadIdByPostId<T>>::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<T>,
+            thread_id: T::ThreadId,
+            post_id : T::PostId,
+            text : Vec<u8>
+        ){
+            T::PostAuthorOriginValidator::ensure_actor_origin(
+                origin,
+                post_author_id,
+            )?;
+
+            ensure!(<ThreadById<T>>::exists(thread_id), Error::ThreadDoesntExist);
+            ensure!(<PostThreadIdByPostId<T>>::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 = <PostThreadIdByPostId<T>>::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
+
+            <PostThreadIdByPostId<T>>::insert(thread_id, post_id, new_post);
+            Self::deposit_event(RawEvent::PostUpdated(post_id, post_author_id));
+       }
+    }
+}
+
+impl<T: Trait> Module<T> {
+    /// 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<T>,
+        title: Vec<u8>,
+    ) -> Result<T::ThreadId, Error> {
+        Self::ensure_can_create_thread(thread_author_id, &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,
+        };
+
+        // get new 'threads in a row' counter for the author
+        let current_thread_counter = Self::get_updated_thread_counter(thread_author_id);
+
+        // mutation
+
+        let thread_id = T::ThreadId::from(new_thread_id);
+        <ThreadById<T>>::insert(thread_id, new_thread);
+        ThreadCount::put(next_thread_count_value);
+        <LastThreadAuthorCounter<T>>::put(current_thread_counter);
+        Self::deposit_event(RawEvent::ThreadCreated(thread_id, thread_author_id));
+
+        Ok(thread_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(
+        thread_author_id: MemberId<T>,
+        title: &[u8],
+    ) -> DispatchResult<Error> {
+        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);
+
+        ensure!(
+            current_thread_counter.counter as u32 <= T::MaxThreadInARowNumber::get(),
+            Error::MaxThreadInARowLimitExceeded
+        );
+
+        Ok(())
+    }
+}
+
+impl<T: Trait> Module<T> {
+    // Wrapper-function over system::block_number()
+    fn current_block() -> T::BlockNumber {
+        <system::Module<T>>::block_number()
+    }
+
+    // returns incremented thread counter if last thread author equals with provided parameter
+    fn get_updated_thread_counter(author_id: MemberId<T>) -> ThreadCounter<MemberId<T>> {
+        // 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)
+    }
+}

+ 145 - 0
runtime-modules/proposals/discussion/src/tests/mock.rs

@@ -0,0 +1,145 @@
+#![cfg(test)]
+
+pub use system;
+
+pub use primitives::{Blake2Hasher, H256};
+pub use sr_primitives::{
+    testing::{Digest, DigestItem, Header, UintAuthorityId},
+    traits::{BlakeTwo256, Convert, IdentityLookup, OnFinalize},
+    weights::Weight,
+    BuildStorage, Perbill,
+};
+
+use crate::ActorOriginValidator;
+use srml_support::{impl_outer_event, impl_outer_origin, parameter_types};
+
+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;
+    pub const StakePoolId: [u8; 8] = *b"joystake";
+}
+
+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<T>,
+        balances<T>,
+        membership_mod<T>,
+    }
+}
+
+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<Self>;
+}
+
+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 Event = TestEvent;
+    type PostAuthorOriginValidator = ();
+    type ThreadId = u32;
+    type PostId = u32;
+    type MaxPostEditionNumber = MaxPostEditionNumber;
+    type ThreadTitleLengthLimit = ThreadTitleLengthLimit;
+    type PostLengthLimit = PostLengthLimit;
+    type MaxThreadInARowNumber = MaxThreadInARowNumber;
+}
+
+impl ActorOriginValidator<Origin, u64, u64> for () {
+    fn ensure_actor_origin(origin: Origin, actor_id: u64) -> Result<u64, &'static str> {
+        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 Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = u64;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    type Event = TestEvent;
+    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;
+}
+
+pub fn initial_test_ext() -> runtime_io::TestExternalities {
+    let t = system::GenesisConfig::default()
+        .build_storage::<Test>()
+        .unwrap();
+
+    t.into()
+}
+
+pub type Discussions = crate::Module<Test>;
+pub type System = system::Module<Test>;

+ 417 - 0
runtime-modules/proposals/discussion/src/tests/mod.rs

@@ -0,0 +1,417 @@
+mod mock;
+
+use mock::*;
+
+use crate::*;
+use system::RawOrigin;
+use system::{EventRecord, Phase};
+
+struct EventFixture;
+impl EventFixture {
+    fn assert_events(expected_raw_events: Vec<RawEvent<u32, u64, u32>>) {
+        let expected_events = expected_raw_events
+            .iter()
+            .map(|ev| EventRecord {
+                phase: Phase::ApplyExtrinsic(0),
+                event: TestEvent::discussion(ev.clone()),
+                topics: vec![],
+            })
+            .collect::<Vec<EventRecord<_, _>>>();
+
+        assert_eq!(System::events(), expected_events);
+    }
+}
+
+struct TestPostEntry {
+    pub post_id: u32,
+    pub text: Vec<u8>,
+    pub edition_number: u32,
+}
+
+struct TestThreadEntry {
+    pub thread_id: u32,
+    pub title: Vec<u8>,
+}
+
+fn assert_thread_content(thread_entry: TestThreadEntry, post_entries: Vec<TestPostEntry>) {
+    assert!(<ThreadById<Test>>::exists(thread_entry.thread_id));
+
+    let actual_thread = <ThreadById<Test>>::get(thread_entry.thread_id);
+    let expected_thread = Thread {
+        title: thread_entry.title,
+        created_at: 1,
+        author_id: 1,
+    };
+    assert_eq!(actual_thread, expected_thread);
+
+    for post_entry in post_entries {
+        let actual_post =
+            <PostThreadIdByPostId<Test>>::get(thread_entry.thread_id, post_entry.post_id);
+        let expected_post = Post {
+            text: post_entry.text,
+            created_at: 1,
+            updated_at: 1,
+            author_id: 1,
+            thread_id: thread_entry.thread_id,
+            edition_number: post_entry.edition_number,
+        };
+
+        assert_eq!(actual_post, expected_post);
+    }
+}
+
+struct DiscussionFixture {
+    pub title: Vec<u8>,
+    pub origin: RawOrigin<u64>,
+    pub author_id: u64,
+}
+
+impl Default for DiscussionFixture {
+    fn default() -> Self {
+        DiscussionFixture {
+            title: b"title".to_vec(),
+            origin: RawOrigin::Signed(1),
+            author_id: 1,
+        }
+    }
+}
+
+impl DiscussionFixture {
+    fn with_title(self, title: Vec<u8>) -> Self {
+        DiscussionFixture { title, ..self }
+    }
+
+    fn create_discussion_and_assert(&self, result: Result<u32, Error>) -> Option<u32> {
+        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 {
+    pub text: Vec<u8>,
+    pub origin: RawOrigin<u64>,
+    pub thread_id: u32,
+    pub post_id: Option<u32>,
+    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,
+        }
+    }
+
+    fn with_text(self, text: Vec<u8>) -> Self {
+        PostFixture { text, ..self }
+    }
+
+    fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        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 }
+    }
+
+    fn change_post_id(self, post_id: u32) -> Self {
+        PostFixture {
+            post_id: Some(post_id),
+            ..self
+        }
+    }
+
+    fn add_post_and_assert(&mut self, result: Result<(), Error>) -> Option<u32> {
+        let add_post_result = Discussions::add_post(
+            self.origin.clone().into(),
+            self.author_id,
+            self.thread_id,
+            self.text.clone(),
+        );
+
+        assert_eq!(add_post_result, result);
+
+        if result.is_ok() {
+            self.post_id = Some(<PostCount>::get());
+        }
+
+        self.post_id
+    }
+
+    fn update_post_with_text_and_assert(&mut self, new_text: Vec<u8>, 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,
+        );
+
+        assert_eq!(add_post_result, result);
+    }
+
+    fn update_post_and_assert(&mut self, result: Result<(), Error>) {
+        self.update_post_with_text_and_assert(self.text.clone(), result);
+    }
+}
+
+#[test]
+fn create_discussion_call_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+
+        discussion_fixture.create_discussion_and_assert(Ok(1));
+    });
+}
+
+#[test]
+fn create_post_call_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture = PostFixture::default_for_thread(thread_id);
+
+        post_fixture.add_post_and_assert(Ok(()));
+    });
+}
+
+#[test]
+fn update_post_call_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture = PostFixture::default_for_thread(thread_id);
+
+        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),
+        ]);
+    });
+}
+
+#[test]
+fn update_post_call_fails_because_of_post_edition_limit() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture = PostFixture::default_for_thread(thread_id);
+
+        post_fixture.add_post_and_assert(Ok(()));
+
+        for _ in 1..6 {
+            post_fixture.update_post_and_assert(Ok(()));
+        }
+
+        post_fixture.update_post_and_assert(Err(Error::PostEditionNumberExceeded));
+    });
+}
+
+#[test]
+fn update_post_call_fails_because_of_the_wrong_author() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture = PostFixture::default_for_thread(thread_id);
+
+        post_fixture.add_post_and_assert(Ok(()));
+
+        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(Error::NotAuthor));
+    });
+}
+
+#[test]
+fn thread_content_check_succeeded() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture1 = PostFixture::default_for_thread(thread_id);
+        let post_id1 = post_fixture1.add_post_and_assert(Ok(())).unwrap();
+
+        let mut post_fixture2 = PostFixture::default_for_thread(thread_id);
+        let post_id2 = post_fixture2.add_post_and_assert(Ok(())).unwrap();
+        post_fixture1.update_post_with_text_and_assert(b"new_text".to_vec(), Ok(()));
+
+        assert_thread_content(
+            TestThreadEntry {
+                thread_id,
+                title: b"title".to_vec(),
+            },
+            vec![
+                TestPostEntry {
+                    post_id: post_id1,
+                    text: b"new_text".to_vec(),
+                    edition_number: 1,
+                },
+                TestPostEntry {
+                    post_id: post_id2,
+                    text: b"text".to_vec(),
+                    edition_number: 0,
+                },
+            ],
+        );
+    });
+}
+
+#[test]
+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(Error::EmptyTitleProvided));
+
+        discussion_fixture = DiscussionFixture::default().with_title([0; 201].to_vec());
+        discussion_fixture.create_discussion_and_assert(Err(Error::TitleIsTooLong));
+    });
+}
+
+#[test]
+fn add_post_call_with_invalid_thread_failed() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+        discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture = PostFixture::default_for_thread(2);
+        post_fixture.add_post_and_assert(Err(Error::ThreadDoesntExist));
+    });
+}
+
+#[test]
+fn update_post_call_with_invalid_post_failed() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture1 = PostFixture::default_for_thread(thread_id);
+        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(Error::PostDoesntExist));
+    });
+}
+
+#[test]
+fn update_post_call_with_invalid_thread_failed() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture1 = PostFixture::default_for_thread(thread_id);
+        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(Error::ThreadDoesntExist));
+    });
+}
+
+#[test]
+fn add_post_call_with_invalid_text_failed() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture1 = PostFixture::default_for_thread(thread_id).with_text(Vec::new());
+        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(Error::PostIsTooLong));
+    });
+}
+
+#[test]
+fn update_post_call_with_invalid_text_failed() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture1 = PostFixture::default_for_thread(thread_id);
+        post_fixture1.add_post_and_assert(Ok(()));
+
+        let mut post_fixture2 = post_fixture1.with_text(Vec::new());
+        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(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));
+    });
+}
+
+#[test]
+fn discussion_thread_and_post_counters_are_valid() {
+    initial_test_ext().execute_with(|| {
+        let discussion_fixture = DiscussionFixture::default();
+        let thread_id = discussion_fixture
+            .create_discussion_and_assert(Ok(1))
+            .unwrap();
+
+        let mut post_fixture1 = PostFixture::default_for_thread(thread_id);
+        let _ = post_fixture1.add_post_and_assert(Ok(())).unwrap();
+
+        assert_eq!(Discussions::thread_count(), 1);
+        assert_eq!(Discussions::post_count(), 1);
+    });
+}

+ 102 - 0
runtime-modules/proposals/discussion/src/types.rs

@@ -0,0 +1,102 @@
+#![warn(missing_docs)]
+
+use codec::{Decode, Encode};
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+
+use rstd::prelude::*;
+
+/// Represents a discussion thread
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct Thread<ThreadAuthorId, BlockNumber> {
+    /// Title
+    pub title: Vec<u8>,
+
+    /// When thread was established.
+    pub created_at: BlockNumber,
+
+    /// Author of the thread.
+    pub author_id: ThreadAuthorId,
+}
+
+/// Post for the discussion thread
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct Post<PostAuthorId, BlockNumber, ThreadId> {
+    /// Text
+    pub text: Vec<u8>,
+
+    /// When post was added.
+    pub created_at: BlockNumber,
+
+    /// When post was updated last time.
+    pub updated_at: BlockNumber,
+
+    /// Author of the post.
+    pub author_id: PostAuthorId,
+
+    /// Parent thread id for this post
+    pub thread_id: ThreadId,
+
+    /// 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<ThreadAuthorId> {
+    /// Author of the threads.
+    pub author_id: ThreadAuthorId,
+
+    /// ThreadCount
+    pub counter: u32,
+}
+
+impl<ThreadAuthorId: Clone> ThreadCounter<ThreadAuthorId> {
+    /// 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,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::types::ThreadCounter;
+
+    #[test]
+    fn thread_counter_increment_works() {
+        let test = ThreadCounter {
+            author_id: 56,
+            counter: 56,
+        };
+        let expected = ThreadCounter {
+            author_id: 56,
+            counter: 57,
+        };
+
+        assert_eq!(expected, test.increment());
+    }
+
+    #[test]
+    fn thread_counter_new_works() {
+        let expected = ThreadCounter {
+            author_id: 56,
+            counter: 1,
+        };
+
+        assert_eq!(expected, ThreadCounter::new(56));
+    }
+}

+ 106 - 0
runtime-modules/proposals/engine/Cargo.toml

@@ -0,0 +1,106 @@
+[package]
+name = 'substrate-proposals-engine-module'
+version = '2.0.0'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[features]
+default = ['std']
+no_std = []
+std = [
+    'codec/std',
+    'rstd/std',
+    'srml-support/std',
+    'primitives/std',
+    'system/std',
+    'timestamp/std',
+    'serde',
+    'stake/std',
+    'balances/std',
+    'sr-primitives/std',
+    'membership/std',
+    'common/std',
+
+]
+
+
+[dependencies.num_enum]
+default_features = false
+version = "0.4.2"
+
+[dependencies.serde]
+features = ['derive']
+optional = true
+version = '1.0.101'
+
+[dependencies.codec]
+default-features = false
+features = ['derive']
+package = 'parity-scale-codec'
+version = '1.0.0'
+
+[dependencies.primitives]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'substrate-primitives'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.rstd]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-std'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.srml-support]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-support'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.system]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-system'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.balances]
+package = 'srml-balances'
+default-features = false
+git = 'https://github.com/paritytech/substrate.git'
+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
+package = 'substrate-stake-module'
+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"
+
+[dev-dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'

+ 812 - 0
runtime-modules/proposals/engine/src/lib.rs

@@ -0,0 +1,812 @@
+//! # Proposals engine module
+//! Proposals `engine` module for the Joystream platform. Version 2.
+//! The main component of the proposals system. Provides methods and extrinsics to create and
+//! vote for proposals, inspired by Parity **Democracy module**.
+//!
+//! ## Overview
+//! Proposals `engine` module provides an abstract mechanism to work with proposals: creation, voting,
+//! execution, canceling, etc. Proposal execution demands serialized _Dispatchable_ proposal code.
+//! It could be any _Dispatchable_ + _Parameter_ type, but most likely, it would be serialized (via
+//! Parity _codec_ crate) extrisic call. A proposal stage can be described by its [status](./enum.ProposalStatus.html).
+//!
+//! ## Proposal lifecycle
+//! When a proposal passes [checks](./struct.Module.html#method.ensure_create_proposal_parameters_are_valid)
+//! for its [parameters](./struct.ProposalParameters.html) - it can be [created](./struct.Module.html#method.create_proposal).
+//! The newly created proposal has _Active_ status. The proposal can be voted on or canceled during its
+//! _voting period_. Votes can be [different](./enum.VoteKind.html). When the proposal gets enough votes
+//! to be slashed or approved or _voting period_ ends - the proposal becomes _Finalized_. If the proposal
+//! got approved and _grace period_ passed - the  `engine` module tries to execute the proposal.
+//! The final [approved status](./enum.ApprovedProposalStatus.html) of the proposal defines
+//! an overall proposal outcome.
+//!
+//! ### Notes
+//!
+//! - The proposal can be [vetoed](./struct.Module.html#method.veto_proposal)
+//! anytime before the proposal execution by the _sudo_.
+//! - When the proposal is created with some stake - refunding on proposal finalization with
+//! different statuses should be accomplished from the external handler from the _stake module_
+//! (_StakingEventsHandler_). Such a handler should call
+//! [refund_proposal_stake](./struct.Module.html#method.refund_proposal_stake) callback function.
+//! - If the _council_ got reelected during the proposal _voting period_ the external handler calls
+//! [reset_active_proposals](./trait.Module.html#method.reset_active_proposals) function and
+//! all voting results get cleared.
+//!
+//! ### Important abstract types to be implemented
+//! Proposals `engine` module has several abstractions to be implemented in order to work correctly.
+//! - _VoterOriginValidator_ - ensure valid voter identity. Voters should have permissions to vote:
+//! they should be council members.
+//! - [VotersParameters](./trait.VotersParameters.html) - defines total voter number, which is
+//! the council size
+//! - _ProposerOriginValidator_ - ensure valid proposer identity. Proposers should have permissions
+//! to create a proposal: they should be members of the Joystream.
+//! - [StakeHandlerProvider](./trait.StakeHandlerProvider.html) - defines an interface for the staking.
+//!
+//! A full list of the abstractions can be found [here](./trait.Trait.html).
+//!
+//! ### Supported extrinsics
+//! - [vote](./struct.Module.html#method.vote) - registers a vote for the proposal
+//! - [cancel_proposal](./struct.Module.html#method.cancel_proposal) - cancels the proposal (can be canceled only by owner)
+//! - [veto_proposal](./struct.Module.html#method.veto_proposal) - vetoes the proposal
+//!
+//! ### Public API
+//! - [create_proposal](./struct.Module.html#method.create_proposal) - creates proposal using provided parameters
+//! - [ensure_create_proposal_parameters_are_valid](./struct.Module.html#method.ensure_create_proposal_parameters_are_valid) - ensures that we can create the proposal
+//! - [refund_proposal_stake](./struct.Module.html#method.refund_proposal_stake) - a callback for _StakingHandlerEvents_
+//! - [reset_active_proposals](./trait.Module.html#method.reset_active_proposals) - resets voting results for active proposals
+//!
+//! ## Usage
+//!
+//! ```
+//! use srml_support::{decl_module, dispatch::Result, print};
+//! use system::ensure_signed;
+//! use codec::Encode;
+//! use substrate_proposals_engine_module::{self as engine, ProposalParameters};
+//!
+//! pub trait Trait: engine::Trait + membership::members::Trait {}
+//!
+//! decl_module! {
+//!     pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+//!         fn executable_proposal(origin) {
+//!             print("executed!");
+//!         }
+//!
+//!         pub fn create_spending_proposal(
+//!             origin,
+//!             proposer_id: T::MemberId,
+//!         ) -> Result {
+//!             let account_id = ensure_signed(origin)?;
+//!             let parameters = ProposalParameters::default();
+//!             let title = b"Spending proposal".to_vec();
+//!             let description = b"We need to spend some tokens to support the working group lead."
+//!                 .to_vec();
+//!             let encoded_proposal_code = <Call<T>>::executable_proposal().encode();
+//!
+//!             <engine::Module<T>>::ensure_create_proposal_parameters_are_valid(
+//!                 &parameters,
+//!                 &title,
+//!                 &description,
+//!                 None
+//!             )?;
+//!             <engine::Module<T>>::create_proposal(
+//!                 account_id,
+//!                 proposer_id,
+//!                 parameters,
+//!                 title,
+//!                 description,
+//!                 None,
+//!                 encoded_proposal_code
+//!             )?;
+//!             Ok(())
+//!         }
+//!     }
+//! }
+//! # fn main() {}
+//! ```
+
+// 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)]
+
+use types::FinalizedProposalData;
+use types::ProposalStakeManager;
+pub use types::{
+    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};
+
+pub(crate) mod types;
+
+#[cfg(test)]
+mod tests;
+
+use codec::Decode;
+use rstd::prelude::*;
+use sr_primitives::traits::{DispatchResult, Zero};
+use srml_support::traits::{Currency, Get};
+use srml_support::{
+    decl_error, decl_event, decl_module, decl_storage, ensure, print, Parameter, StorageDoubleMap,
+};
+use system::{ensure_root, RawOrigin};
+
+use crate::types::ApprovedProposalData;
+use common::origin_validator::ActorOriginValidator;
+use srml_support::dispatch::Dispatchable;
+
+type MemberId<T> = <T as membership::members::Trait>::MemberId;
+
+/// Proposals engine trait.
+pub trait Trait:
+    system::Trait + timestamp::Trait + stake::Trait + membership::members::Trait
+{
+    /// Engine event type.
+    type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
+
+    /// Validates proposer id and origin combination
+    type ProposerOriginValidator: ActorOriginValidator<
+        Self::Origin,
+        MemberId<Self>,
+        Self::AccountId,
+    >;
+
+    /// Validates voter id and origin combination
+    type VoterOriginValidator: ActorOriginValidator<Self::Origin, MemberId<Self>, Self::AccountId>;
+
+    /// Provides data for voting. Defines maximum voters count for the proposal.
+    type TotalVotersCounter: VotersParameters;
+
+    /// Proposal Id type
+    type ProposalId: From<u32> + Parameter + Default + Copy;
+
+    /// Provides stake logic implementation. Can be used to mock stake logic.
+    type StakeHandlerProvider: StakeHandlerProvider<Self>;
+
+    /// The fee is applied when cancel the proposal. A fee would be slashed (burned).
+    type CancellationFee: Get<BalanceOf<Self>>;
+
+    /// The fee is applied when the proposal gets rejected. A fee would be slashed (burned).
+    type RejectionFee: Get<BalanceOf<Self>>;
+
+    /// Defines max allowed proposal title length.
+    type TitleMaxLength: Get<u32>;
+
+    /// Defines max allowed proposal description length.
+    type DescriptionMaxLength: Get<u32>;
+
+    /// Defines max simultaneous active proposals number.
+    type MaxActiveProposalLimit: Get<u32>;
+
+    /// Proposals executable code. Can be instantiated by external module Call enum members.
+    type DispatchableCallCode: Parameter + Dispatchable<Origin = Self::Origin> + Default;
+}
+
+decl_event!(
+    /// Proposals engine events
+    pub enum Event<T>
+    where
+        <T as Trait>::ProposalId,
+        MemberId = MemberId<T>,
+        <T as system::Trait>::BlockNumber,
+        <T as system::Trait>::AccountId,
+        <T as stake::Trait>::StakeId,
+    {
+    	/// Emits on proposal creation.
+        /// Params:
+        /// - Member id of a proposer.
+        /// - Id of a newly created proposal after it was saved in storage.
+        ProposalCreated(MemberId, ProposalId),
+
+        /// Emits on proposal status change.
+        /// Params:
+        /// - Id of a updated proposal.
+        /// - New proposal status
+        ProposalStatusUpdated(ProposalId, ProposalStatus<BlockNumber, StakeId, AccountId>),
+
+        /// Emits on voting for the proposal
+        /// Params:
+        /// - Voter - member id of a voter.
+        /// - Id of a proposal.
+        /// - Kind of vote.
+        Voted(MemberId, ProposalId, VoteKind),
+    }
+);
+
+decl_error! {
+    /// Engine module predefined errors
+    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<system::Error> 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<T: Trait> as ProposalEngine{
+        /// Map proposal by its id.
+        pub Proposals get(fn proposals): map T::ProposalId => ProposalOf<T>;
+
+        /// Count of all proposals that have been created.
+        pub ProposalCount get(fn proposal_count): u32;
+
+        /// Map proposal executable code by proposal id.
+        pub DispatchableCallCode get(fn proposal_codes): map T::ProposalId =>  Vec<u8>;
+
+        /// Count of active proposals.
+        pub ActiveProposalCount get(fn active_proposal_count): u32;
+
+        /// Ids of proposals that are open for voting (have not been finalized yet).
+        pub ActiveProposalIds get(fn active_proposal_ids): linked_map T::ProposalId=> ();
+
+        /// Ids of proposals that were approved and theirs grace period was not expired.
+        pub PendingExecutionProposalIds get(fn pending_proposal_ids): linked_map T::ProposalId=> ();
+
+        /// 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(MemberId<T>) => 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<T: Trait> 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, voter_id: MemberId<T>, proposal_id: T::ProposalId, vote: VoteKind)  {
+            T::VoterOriginValidator::ensure_actor_origin(
+                origin,
+                voter_id,
+            )?;
+
+            ensure!(<Proposals<T>>::exists(proposal_id), Error::ProposalNotFound);
+            let mut proposal = Self::proposals(proposal_id);
+
+            ensure!(matches!(proposal.status, ProposalStatus::Active{..}), Error::ProposalFinalized);
+
+            let did_not_vote_before = !<VoteExistsByProposalByVoter<T>>::exists(
+                proposal_id,
+                voter_id,
+            );
+
+            ensure!(did_not_vote_before, Error::AlreadyVoted);
+
+            proposal.voting_results.add_vote(vote.clone());
+
+            // mutation
+
+            <Proposals<T>>::insert(proposal_id, proposal);
+            <VoteExistsByProposalByVoter<T>>::insert( proposal_id, voter_id, vote.clone());
+            Self::deposit_event(RawEvent::Voted(voter_id, proposal_id, vote));
+        }
+
+        /// Cancel a proposal by its original proposer.
+        pub fn cancel_proposal(origin, proposer_id: MemberId<T>, proposal_id: T::ProposalId) {
+            T::ProposerOriginValidator::ensure_actor_origin(
+                origin,
+                proposer_id,
+            )?;
+
+            ensure!(<Proposals<T>>::exists(proposal_id), Error::ProposalNotFound);
+            let proposal = Self::proposals(proposal_id);
+
+            ensure!(proposer_id == proposal.proposer_id, Error::NotAuthor);
+            ensure!(matches!(proposal.status, ProposalStatus::Active{..}), Error::ProposalFinalized);
+
+            // mutation
+
+            Self::finalize_proposal(proposal_id, ProposalDecisionStatus::Canceled);
+        }
+
+        /// Veto a proposal. Must be root.
+        pub fn veto_proposal(origin, proposal_id: T::ProposalId) {
+            ensure_root(origin)?;
+
+            ensure!(<Proposals<T>>::exists(proposal_id), Error::ProposalNotFound);
+            let proposal = Self::proposals(proposal_id);
+
+            // mutation
+
+            if <PendingExecutionProposalIds<T>>::exists(proposal_id) {
+                Self::veto_pending_execution_proposal(proposal_id, proposal);
+            } else {
+                ensure!(matches!(proposal.status, ProposalStatus::Active{..}), Error::ProposalFinalized);
+                Self::finalize_proposal(proposal_id, ProposalDecisionStatus::Vetoed);
+            }
+        }
+
+        /// Block finalization. Perform voting period check, vote result tally, approved proposals
+        /// grace period checks, and proposal execution.
+        fn on_finalize(_n: T::BlockNumber) {
+            let finalized_proposals = Self::get_finalized_proposals();
+
+            // mutation
+
+            // Check vote results. Approved proposals with zero grace period will be
+            // transitioned to the PendingExecution status.
+            for  proposal_data in finalized_proposals {
+                <Proposals<T>>::insert(proposal_data.proposal_id, proposal_data.proposal);
+                Self::finalize_proposal(proposal_data.proposal_id, proposal_data.status);
+            }
+
+            let executable_proposals =
+                Self::get_approved_proposal_with_expired_grace_period();
+
+            // Execute approved proposals with expired grace period
+            for approved_proosal in executable_proposals {
+                Self::execute_proposal(approved_proosal);
+            }
+        }
+    }
+}
+
+impl<T: Trait> Module<T> {
+    /// Create proposal. Requires 'proposal origin' membership.
+    pub fn create_proposal(
+        account_id: T::AccountId,
+        proposer_id: MemberId<T>,
+        parameters: ProposalParameters<T::BlockNumber, types::BalanceOf<T>>,
+        title: Vec<u8>,
+        description: Vec<u8>,
+        stake_balance: Option<types::BalanceOf<T>>,
+        encoded_dispatchable_call_code: Vec<u8>,
+    ) -> Result<T::ProposalId, Error> {
+        Self::ensure_create_proposal_parameters_are_valid(
+            &parameters,
+            &title,
+            &description,
+            stake_balance,
+        )?;
+
+        // checks passed
+        // mutation
+
+        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_result = stake_balance
+            .map(|stake_amount| {
+                ProposalStakeManager::<T>::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,
+            });
+
+            <StakesProposals<T>>::insert(stake_id, proposal_id);
+        }
+
+        let new_proposal = Proposal {
+            created_at: Self::current_block(),
+            parameters,
+            title,
+            description,
+            proposer_id,
+            status: ProposalStatus::Active(stake_data),
+            voting_results: VotingResults::default(),
+        };
+
+        <Proposals<T>>::insert(proposal_id, new_proposal);
+        <DispatchableCallCode<T>>::insert(proposal_id, encoded_dispatchable_call_code);
+        <ActiveProposalIds<T>>::insert(proposal_id, ());
+        ProposalCount::put(next_proposal_count_value);
+        Self::increase_active_proposal_counter();
+
+        Self::deposit_event(RawEvent::ProposalCreated(proposer_id, proposal_id));
+
+        Ok(proposal_id)
+    }
+
+    /// Performs all checks for the proposal creation:
+    /// - title, body lengths
+    /// - max 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<T::BlockNumber, types::BalanceOf<T>>,
+        title: &[u8],
+        description: &[u8],
+        stake_balance: Option<types::BalanceOf<T>>,
+    ) -> DispatchResult<Error> {
+        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(())
+    }
+
+    /// Callback from StakingEventsHandler. Refunds unstaked imbalance back to the source account.
+    /// There can be a lot of invariant breaks in the scope of this proposal.
+    /// Such situations are handled by adding error messages to the log.
+    pub fn refund_proposal_stake(stake_id: T::StakeId, imbalance: NegativeImbalance<T>) {
+        if <StakesProposals<T>>::exists(stake_id) {
+            let proposal_id = Self::stakes_proposals(stake_id);
+
+            if <Proposals<T>>::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 {
+                        let refunding_result = CurrencyOf::<T>::resolve_into_existing(
+                            &active_stake.source_account_id,
+                            imbalance,
+                        );
+
+                        if refunding_result.is_err() {
+                            print("Broken invariant: cannot refund");
+                        }
+                    }
+                } else {
+                    print("Broken invariant: proposal status is not Active");
+                }
+            } else {
+                print("Broken invariant: proposal doesn't exist");
+            }
+        } else {
+            print("Broken invariant: stake doesn't exist");
+        }
+    }
+
+    /// Resets voting results for active proposals.
+    /// Possible application includes new council elections.
+    pub fn reset_active_proposals() {
+        <ActiveProposalIds<T>>::enumerate().for_each(|(proposal_id, _)| {
+            <Proposals<T>>::mutate(proposal_id, |proposal| {
+                proposal.reset_proposal();
+                <VoteExistsByProposalByVoter<T>>::remove_prefix(&proposal_id);
+            });
+        });
+    }
+}
+
+impl<T: Trait> Module<T> {
+    // Wrapper-function over system::block_number()
+    fn current_block() -> T::BlockNumber {
+        <system::Module<T>>::block_number()
+    }
+
+    // Enumerates through active proposals. Tally Voting results.
+    // Returns proposals with finalized status and id
+    fn get_finalized_proposals() -> Vec<FinalizedProposal<T>> {
+        // Enumerate active proposals id and gather finalization data.
+        // Skip proposals with unfinished voting.
+        <ActiveProposalIds<T>>::enumerate()
+            .filter_map(|(proposal_id, _)| {
+                // load current proposal
+                let proposal = Self::proposals(proposal_id);
+
+                // Calculates votes, takes in account voting period expiration.
+                // If voting process is in progress, then decision status is None.
+                let decision_status = proposal.define_proposal_decision_status(
+                    T::TotalVotersCounter::total_voters_count(),
+                    Self::current_block(),
+                );
+
+                // map to FinalizedProposalData if decision for the proposal is made or return None
+                decision_status.map(|status| FinalizedProposalData {
+                    proposal_id,
+                    proposal,
+                    status,
+                    finalized_at: Self::current_block(),
+                })
+            })
+            .collect() // compose output vector
+    }
+
+    // Veto approved proposal during its grace period. Saves a new proposal status and removes
+    // proposal id from the 'PendingExecutionProposalIds'
+    fn veto_pending_execution_proposal(proposal_id: T::ProposalId, proposal: ProposalOf<T>) {
+        <PendingExecutionProposalIds<T>>::remove(proposal_id);
+
+        let vetoed_proposal_status = ProposalStatus::finalized(
+            ProposalDecisionStatus::Vetoed,
+            None,
+            None,
+            Self::current_block(),
+        );
+
+        <Proposals<T>>::insert(
+            proposal_id,
+            Proposal {
+                status: vetoed_proposal_status,
+                ..proposal
+            },
+        );
+    }
+
+    // Executes approved proposal code
+    fn execute_proposal(approved_proposal: ApprovedProposal<T>) {
+        let proposal_code = Self::proposal_codes(approved_proposal.proposal_id);
+
+        let proposal_code_result = T::DispatchableCallCode::decode(&mut &proposal_code[..]);
+
+        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.what()),
+        };
+
+        let proposal_execution_status = approved_proposal
+            .finalisation_status_data
+            .create_approved_proposal_status(approved_proposal_status);
+
+        let mut proposal = approved_proposal.proposal;
+        proposal.status = proposal_execution_status.clone();
+        <Proposals<T>>::insert(approved_proposal.proposal_id, proposal);
+
+        Self::deposit_event(RawEvent::ProposalStatusUpdated(
+            approved_proposal.proposal_id,
+            proposal_execution_status,
+        ));
+
+        <PendingExecutionProposalIds<T>>::remove(&approved_proposal.proposal_id);
+    }
+
+    // Performs all actions on proposal finalization:
+    // - clean active proposal cache
+    // - update proposal status fields (status, finalized_at)
+    // - add to pending execution proposal cache if approved
+    // - slash and unstake proposal stake if stake exists
+    // - decrease active proposal counter
+    // - fire an event
+    // It prints an error message in case of an attempt to finalize the non-active proposal.
+    fn finalize_proposal(proposal_id: T::ProposalId, decision_status: ProposalDecisionStatus) {
+        Self::decrease_active_proposal_counter();
+        <ActiveProposalIds<T>>::remove(&proposal_id.clone());
+
+        let mut proposal = Self::proposals(proposal_id);
+
+        if let ProposalStatus::Active(active_stake) = proposal.status.clone() {
+            if let ProposalDecisionStatus::Approved { .. } = decision_status {
+                <PendingExecutionProposalIds<T>>::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(active_stake.clone(), slash_balance);
+
+            // create finalized proposal status with error if any
+            let new_proposal_status = ProposalStatus::finalized(
+                decision_status,
+                slash_and_unstake_result.err(),
+                active_stake,
+                Self::current_block(),
+            );
+
+            proposal.status = new_proposal_status.clone();
+            <Proposals<T>>::insert(proposal_id, proposal);
+
+            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_data: Option<ActiveStake<T::StakeId, T::AccountId>>,
+        slash_balance: BalanceOf<T>,
+    ) -> Result<(), &'static str> {
+        // only if stake exists
+        if let Some(stake_data) = current_stake_data {
+            if !slash_balance.is_zero() {
+                ProposalStakeManager::<T>::slash(stake_data.stake_id, slash_balance)?;
+            }
+
+            ProposalStakeManager::<T>::remove_stake(stake_data.stake_id)?;
+        }
+
+        Ok(())
+    }
+
+    // Calculates required slash based on finalization ProposalDecisionStatus and proposal parameters.
+    // Method visibility allows testing.
+    pub(crate) fn calculate_slash_balance(
+        decision_status: &ProposalDecisionStatus,
+        proposal_parameters: &ProposalParameters<T::BlockNumber, types::BalanceOf<T>>,
+    ) -> types::BalanceOf<T> {
+        match decision_status {
+            ProposalDecisionStatus::Rejected | ProposalDecisionStatus::Expired => {
+                T::RejectionFee::get()
+            }
+            ProposalDecisionStatus::Approved { .. } | ProposalDecisionStatus::Vetoed => {
+                BalanceOf::<T>::zero()
+            }
+            ProposalDecisionStatus::Canceled => T::CancellationFee::get(),
+            ProposalDecisionStatus::Slashed => proposal_parameters
+                .required_stake
+                .clone()
+                .unwrap_or_else(BalanceOf::<T>::zero), // stake if set or zero
+        }
+    }
+
+    // Enumerates approved proposals and checks their grace period expiration
+    fn get_approved_proposal_with_expired_grace_period() -> Vec<ApprovedProposal<T>> {
+        <PendingExecutionProposalIds<T>>::enumerate()
+            .filter_map(|(proposal_id, _)| {
+                let proposal = Self::proposals(proposal_id);
+
+                if proposal.is_grace_period_expired(Self::current_block()) {
+                    // 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
+                }
+            })
+            .collect()
+    }
+
+    // Increases active proposal counter.
+    fn increase_active_proposal_counter() {
+        let next_active_proposal_count_value = Self::active_proposal_count() + 1;
+        ActiveProposalCount::put(next_active_proposal_count_value);
+    }
+
+    // Decreases active proposal counter down to zero. Decreasing below zero has no effect.
+    fn decrease_active_proposal_counter() {
+        let current_active_proposal_counter = Self::active_proposal_count();
+
+        if current_active_proposal_counter > 0 {
+            let next_active_proposal_count_value = current_active_proposal_counter - 1;
+            ActiveProposalCount::put(next_active_proposal_count_value);
+        };
+    }
+}
+
+// Simplification of the 'FinalizedProposalData' type
+type FinalizedProposal<T> = FinalizedProposalData<
+    <T as Trait>::ProposalId,
+    <T as system::Trait>::BlockNumber,
+    MemberId<T>,
+    types::BalanceOf<T>,
+    <T as stake::Trait>::StakeId,
+    <T as system::Trait>::AccountId,
+>;
+
+// Simplification of the 'ApprovedProposalData' type
+type ApprovedProposal<T> = ApprovedProposalData<
+    <T as Trait>::ProposalId,
+    <T as system::Trait>::BlockNumber,
+    MemberId<T>,
+    types::BalanceOf<T>,
+    <T as stake::Trait>::StakeId,
+    <T as system::Trait>::AccountId,
+>;
+
+// Simplification of the 'Proposal' type
+type ProposalOf<T> = Proposal<
+    <T as system::Trait>::BlockNumber,
+    MemberId<T>,
+    types::BalanceOf<T>,
+    <T as stake::Trait>::StakeId,
+    <T as system::Trait>::AccountId,
+>;

+ 33 - 0
runtime-modules/proposals/engine/src/tests/mock/balance_manager.rs

@@ -0,0 +1,33 @@
+#![cfg(test)]
+
+pub use sr_primitives::traits::Zero;
+use srml_support::traits::{Currency, Imbalance};
+
+use super::*;
+
+/// StakingEventsHandler implementation for the stake::Trait. Restores balances after the unstaking
+/// and slashes balances if necessary.
+pub struct BalanceManagerStakingEventsHandler;
+impl stake::StakingEventsHandler<Test> for BalanceManagerStakingEventsHandler {
+    fn unstaked(
+        _id: &u64,
+        _unstaked_amount: stake::BalanceOf<Test>,
+        imbalance: stake::NegativeImbalance<Test>,
+    ) -> stake::NegativeImbalance<Test> {
+        let default_account_id = 1;
+
+        <Test as stake::Trait>::Currency::resolve_creating(&default_account_id, imbalance);
+
+        stake::NegativeImbalance::<Test>::zero()
+    }
+
+    fn slashed(
+        _id: &u64,
+        _slash_id: Option<<Test as stake::Trait>::SlashId>,
+        _slashed_amount: stake::BalanceOf<Test>,
+        _remaining_stake: stake::BalanceOf<Test>,
+        imbalance: stake::NegativeImbalance<Test>,
+    ) -> stake::NegativeImbalance<Test> {
+        imbalance
+    }
+}

+ 186 - 0
runtime-modules/proposals/engine/src/tests/mock/mod.rs

@@ -0,0 +1,186 @@
+//! Mock runtime for the module testing.
+//!
+//! Submodules:
+//! - stakes: contains support for mocking external 'stake' module
+//! - balance_restorator: restores balances after unstaking
+//! - proposals: provides types for proposal execution tests
+//!
+
+#![cfg(test)]
+pub use primitives::{Blake2Hasher, H256};
+pub use sr_primitives::{
+    testing::{Digest, DigestItem, Header, UintAuthorityId},
+    traits::{BlakeTwo256, Convert, IdentityLookup, OnFinalize, Zero},
+    weights::Weight,
+    BuildStorage, DispatchError, Perbill,
+};
+use srml_support::{impl_outer_event, impl_outer_origin, parameter_types};
+pub use system;
+
+mod balance_manager;
+pub(crate) mod proposals;
+mod stakes;
+
+use balance_manager::*;
+pub use proposals::*;
+pub use stakes::*;
+
+// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct Test;
+
+impl_outer_origin! {
+    pub enum Origin for Test {}
+}
+
+mod engine {
+    pub use crate::Event;
+}
+
+mod membership_mod {
+    pub use membership::members::Event;
+}
+
+impl_outer_event! {
+    pub enum TestEvent for Test {
+        balances<T>,
+        engine<T>,
+        membership_mod<T>,
+    }
+}
+
+parameter_types! {
+    pub const ExistentialDeposit: u32 = 0;
+    pub const TransferFee: u32 = 0;
+    pub const CreationFee: 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 = ();
+
+    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<Self>;
+}
+
+impl proposals::Trait for Test {}
+
+impl stake::Trait for Test {
+    type Currency = Balances;
+    type StakePoolId = StakePoolId;
+    type StakingEventsHandler = BalanceManagerStakingEventsHandler;
+    type StakeId = u64;
+    type SlashId = u64;
+}
+
+parameter_types! {
+    pub const CancellationFee: u64 = 5;
+    pub const RejectionFee: u64 = 3;
+    pub const TitleMaxLength: u32 = 100;
+    pub const DescriptionMaxLength: u32 = 10000;
+    pub const MaxActiveProposalLimit: u32 = 100;
+}
+
+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 Event = TestEvent;
+    type ProposerOriginValidator = ();
+    type VoterOriginValidator = ();
+    type TotalVotersCounter = ();
+    type ProposalId = u32;
+    type StakeHandlerProvider = stakes::TestStakeHandlerProvider;
+    type CancellationFee = CancellationFee;
+    type RejectionFee = RejectionFee;
+    type TitleMaxLength = TitleMaxLength;
+    type DescriptionMaxLength = DescriptionMaxLength;
+    type MaxActiveProposalLimit = MaxActiveProposalLimit;
+    type DispatchableCallCode = proposals::Call<Test>;
+}
+
+impl Default for proposals::Call<Test> {
+    fn default() -> Self {
+        panic!("shouldn't call default for Call");
+    }
+}
+
+impl common::origin_validator::ActorOriginValidator<Origin, u64, u64> for () {
+    fn ensure_actor_origin(origin: Origin, _account_id: u64) -> Result<u64, &'static str> {
+        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:
+// https://substrate.dev/recipes/3-entrees/testing/externalities.html
+impl crate::VotersParameters for () {
+    fn total_voters_count() -> u32 {
+        4
+    }
+}
+
+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;
+    pub const StakePoolId: [u8; 8] = *b"joystake";
+}
+
+impl system::Trait for Test {
+    type Origin = Origin;
+    type Call = ();
+    type Index = u64;
+    type BlockNumber = u64;
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = u64;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    type Event = TestEvent;
+    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;
+}
+
+pub fn initial_test_ext() -> runtime_io::TestExternalities {
+    let t = system::GenesisConfig::default()
+        .build_storage::<Test>()
+        .unwrap();
+
+    t.into()
+}
+
+pub type ProposalsEngine = crate::Module<Test>;
+pub type System = system::Module<Test>;
+pub type Balances = balances::Module<Test>;

+ 18 - 0
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<T: Trait> for enum Call where origin: T::Origin {
+        /// Working extrinsic test
+        pub fn dummy_proposal(_origin, _title: Vec<u8>, _description: Vec<u8>) {}
+
+        /// Broken extrinsic test
+        pub fn faulty_proposal(_origin, _title: Vec<u8>, _description: Vec<u8>,) {
+             Err("ExecutionFailed")?
+        }
+    }
+}

+ 66 - 0
runtime-modules/proposals/engine/src/tests/mock/stakes.rs

@@ -0,0 +1,66 @@
+#![cfg(test)]
+
+use rstd::marker::PhantomData;
+use std::cell::RefCell;
+use std::panic;
+use std::rc::Rc;
+
+use super::Test;
+
+// Intercepts panic method
+// Returns: whether panic occurred
+pub(crate) fn panics<F: std::panic::RefUnwindSafe + Fn()>(could_panic_func: F) -> bool {
+    {
+        let default_hook = panic::take_hook();
+        panic::set_hook(Box::new(|info| {
+            println!("{}", info);
+        }));
+
+        // intercept panic
+        let result = panic::catch_unwind(|| could_panic_func());
+
+        //restore default behaviour
+        panic::set_hook(default_hook);
+
+        result.is_err()
+    }
+}
+
+// Test StakeHandlerProvider implementation based on local thread static variables
+pub struct TestStakeHandlerProvider;
+impl crate::StakeHandlerProvider<Test> for TestStakeHandlerProvider {
+    /// Returns StakeHandler. Mock entry point for stake module.
+    fn stakes() -> Rc<dyn crate::StakeHandler<Test>> {
+        THREAD_LOCAL_STAKE_HANDLER.with(|f| f.borrow().clone())
+    }
+}
+
+// 1. RefCell - thread_local! mutation pattern
+// 2. Rc - ability to have multiple references
+thread_local! {
+    pub static THREAD_LOCAL_STAKE_HANDLER:
+      RefCell<Rc<dyn crate::StakeHandler<Test>>> = RefCell::new(Rc::new(crate::types::DefaultStakeHandler{marker: PhantomData::<Test>}));
+}
+
+// Sets stake handler implementation. Mockall framework integration.
+pub(crate) fn set_stake_handler_impl(mock: Rc<dyn crate::StakeHandler<Test>>) {
+    THREAD_LOCAL_STAKE_HANDLER.with(|f| {
+        *f.borrow_mut() = mock.clone();
+    });
+}
+
+// Tests mock expectation and restores default behaviour
+pub(crate) fn test_expectation_and_clear_mock() {
+    set_stake_handler_impl(Rc::new(crate::types::DefaultStakeHandler {
+        marker: PhantomData::<Test>,
+    }));
+}
+
+// Intercepts panic in provided function, test mock expectation and restores default behaviour
+pub(crate) fn handle_mock<F: std::panic::RefUnwindSafe + Fn()>(func: F) {
+    let panicked = panics(func);
+
+    test_expectation_and_clear_mock();
+
+    assert!(!panicked);
+}

+ 1581 - 0
runtime-modules/proposals/engine/src/tests/mod.rs

@@ -0,0 +1,1581 @@
+pub(crate) mod mock;
+
+use crate::*;
+use mock::*;
+
+use codec::Encode;
+use rstd::rc::Rc;
+use sr_primitives::traits::{DispatchResult, OnFinalize, OnInitialize};
+use srml_support::{StorageDoubleMap, StorageMap, StorageValue};
+use system::RawOrigin;
+use system::{EventRecord, Phase};
+
+use srml_support::traits::Currency;
+
+pub(crate) fn increase_total_balance_issuance_using_account_id(account_id: u64, balance: u64) {
+    let initial_balance = Balances::total_issuance();
+    {
+        let _ = <Test as stake::Trait>::Currency::deposit_creating(&account_id, balance);
+    }
+    assert_eq!(Balances::total_issuance(), initial_balance + balance);
+}
+
+struct ProposalParametersFixture {
+    parameters: ProposalParameters<u64, u64>,
+}
+
+impl ProposalParametersFixture {
+    fn with_required_stake(&self, required_stake: BalanceOf<Test>) -> Self {
+        ProposalParametersFixture {
+            parameters: ProposalParameters {
+                required_stake: Some(required_stake),
+                ..self.parameters
+            },
+        }
+    }
+    fn with_grace_period(&self, grace_period: u64) -> Self {
+        ProposalParametersFixture {
+            parameters: ProposalParameters {
+                grace_period,
+                ..self.parameters
+            },
+        }
+    }
+
+    fn params(&self) -> ProposalParameters<u64, u64> {
+        self.parameters.clone()
+    }
+}
+
+impl Default for ProposalParametersFixture {
+    fn default() -> Self {
+        ProposalParametersFixture {
+            parameters: ProposalParameters {
+                voting_period: 3,
+                approval_quorum_percentage: 60,
+                approval_threshold_percentage: 60,
+                slashing_quorum_percentage: 60,
+                slashing_threshold_percentage: 60,
+                grace_period: 0,
+                required_stake: None,
+            },
+        }
+    }
+}
+
+#[derive(Clone)]
+struct DummyProposalFixture {
+    parameters: ProposalParameters<u64, u64>,
+    account_id: u64,
+    proposer_id: u64,
+    proposal_code: Vec<u8>,
+    title: Vec<u8>,
+    description: Vec<u8>,
+    stake_balance: Option<BalanceOf<Test>>,
+}
+
+impl Default for DummyProposalFixture {
+    fn default() -> Self {
+        let title = b"title".to_vec();
+        let description = b"description".to_vec();
+        let dummy_proposal =
+            mock::proposals::Call::<Test>::dummy_proposal(title.clone(), description.clone());
+
+        DummyProposalFixture {
+            parameters: ProposalParameters {
+                voting_period: 3,
+                approval_quorum_percentage: 60,
+                approval_threshold_percentage: 60,
+                slashing_quorum_percentage: 60,
+                slashing_threshold_percentage: 60,
+                grace_period: 0,
+                required_stake: None,
+            },
+            account_id: 1,
+            proposer_id: 1,
+            proposal_code: dummy_proposal.encode(),
+            title,
+            description,
+            stake_balance: None,
+        }
+    }
+}
+
+impl DummyProposalFixture {
+    fn with_title_and_body(self, title: Vec<u8>, description: Vec<u8>) -> Self {
+        DummyProposalFixture {
+            title,
+            description,
+            ..self
+        }
+    }
+
+    fn with_parameters(self, parameters: ProposalParameters<u64, u64>) -> Self {
+        DummyProposalFixture { parameters, ..self }
+    }
+
+    fn with_account_id(self, account_id: u64) -> Self {
+        DummyProposalFixture { account_id, ..self }
+    }
+
+    fn with_stake(self, stake_balance: BalanceOf<Test>) -> Self {
+        DummyProposalFixture {
+            stake_balance: Some(stake_balance),
+            ..self
+        }
+    }
+
+    fn with_proposal_code(self, proposal_code: Vec<u8>) -> Self {
+        DummyProposalFixture {
+            proposal_code,
+            ..self
+        }
+    }
+
+    fn create_proposal_and_assert(self, result: Result<u32, Error>) -> Option<u32> {
+        let proposal_id_result = ProposalsEngine::create_proposal(
+            self.account_id,
+            self.proposer_id,
+            self.parameters,
+            self.title,
+            self.description,
+            self.stake_balance,
+            self.proposal_code,
+        );
+        assert_eq!(proposal_id_result, result);
+
+        proposal_id_result.ok()
+    }
+}
+
+struct CancelProposalFixture {
+    origin: RawOrigin<u64>,
+    proposal_id: u32,
+    proposer_id: u64,
+}
+
+impl CancelProposalFixture {
+    fn new(proposal_id: u32) -> Self {
+        CancelProposalFixture {
+            proposal_id,
+            origin: RawOrigin::Signed(1),
+            proposer_id: 1,
+        }
+    }
+
+    fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        CancelProposalFixture { origin, ..self }
+    }
+
+    fn with_proposer(self, proposer_id: u64) -> Self {
+        CancelProposalFixture {
+            proposer_id,
+            ..self
+        }
+    }
+
+    fn cancel_and_assert(self, expected_result: DispatchResult<Error>) {
+        assert_eq!(
+            ProposalsEngine::cancel_proposal(
+                self.origin.into(),
+                self.proposer_id,
+                self.proposal_id
+            ),
+            expected_result
+        );
+    }
+}
+struct VetoProposalFixture {
+    origin: RawOrigin<u64>,
+    proposal_id: u32,
+}
+
+impl VetoProposalFixture {
+    fn new(proposal_id: u32) -> Self {
+        VetoProposalFixture {
+            proposal_id,
+            origin: RawOrigin::Root,
+        }
+    }
+
+    fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        VetoProposalFixture { origin, ..self }
+    }
+
+    fn veto_and_assert(self, expected_result: DispatchResult<Error>) {
+        assert_eq!(
+            ProposalsEngine::veto_proposal(self.origin.into(), self.proposal_id,),
+            expected_result
+        );
+    }
+}
+
+struct VoteGenerator {
+    proposal_id: u32,
+    current_account_id: u64,
+    current_voter_id: u64,
+    pub auto_increment_voter_id: bool,
+}
+
+impl VoteGenerator {
+    fn new(proposal_id: u32) -> Self {
+        VoteGenerator {
+            proposal_id,
+            current_voter_id: 0,
+            current_account_id: 0,
+            auto_increment_voter_id: true,
+        }
+    }
+    fn vote_and_assert_ok(&mut self, vote_kind: VoteKind) {
+        self.vote_and_assert(vote_kind, Ok(()));
+    }
+
+    fn vote_and_assert(&mut self, vote_kind: VoteKind, expected_result: DispatchResult<Error>) {
+        assert_eq!(self.vote(vote_kind.clone()), expected_result);
+    }
+
+    fn vote(&mut self, vote_kind: VoteKind) -> DispatchResult<Error> {
+        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,
+        )
+    }
+}
+
+struct EventFixture;
+impl EventFixture {
+    fn assert_events(expected_raw_events: Vec<RawEvent<u32, u64, u64, u64, u64>>) {
+        let expected_events = expected_raw_events
+            .iter()
+            .map(|ev| EventRecord {
+                phase: Phase::ApplyExtrinsic(0),
+                event: TestEvent::engine(ev.clone()),
+                topics: vec![],
+            })
+            .collect::<Vec<EventRecord<_, _>>>();
+
+        assert_eq!(System::events(), expected_events);
+    }
+}
+
+// Recommendation from Parity on testing on_finalize
+// https://substrate.dev/docs/en/next/development/module/tests
+fn run_to_block(n: u64) {
+    while System::block_number() < n {
+        <System as OnFinalize<u64>>::on_finalize(System::block_number());
+        <ProposalsEngine as OnFinalize<u64>>::on_finalize(System::block_number());
+        System::set_block_number(System::block_number() + 1);
+        <System as OnInitialize<u64>>::on_initialize(System::block_number());
+        <ProposalsEngine as OnInitialize<u64>>::on_initialize(System::block_number());
+    }
+}
+
+fn run_to_block_and_finalize(n: u64) {
+    run_to_block(n);
+    <ProposalsEngine as OnFinalize<u64>>::on_finalize(n);
+}
+
+#[test]
+fn create_dummy_proposal_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+
+        dummy_proposal.create_proposal_and_assert(Ok(1));
+    });
+}
+
+#[test]
+fn vote_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+    });
+}
+
+#[test]
+fn vote_fails_with_insufficient_rights() {
+    initial_test_ext().execute_with(|| {
+        assert_eq!(
+            ProposalsEngine::vote(system::RawOrigin::None.into(), 1, 1, VoteKind::Approve),
+            Err(Error::Other("RequireSignedOrigin"))
+        );
+    });
+}
+
+#[test]
+fn proposal_execution_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let parameters_fixture = ProposalParametersFixture::default();
+        let dummy_proposal =
+            DummyProposalFixture::default().with_parameters(parameters_fixture.params());
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 1);
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+
+        run_to_block_and_finalize(1);
+
+        let proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal,
+            Proposal {
+                parameters: parameters_fixture.params(),
+                proposer_id: 1,
+                created_at: 1,
+                status: ProposalStatus::approved(ApprovedProposalStatus::Executed, 1),
+                title: b"title".to_vec(),
+                description: b"description".to_vec(),
+                voting_results: VotingResults {
+                    abstentions: 0,
+                    approvals: 4,
+                    rejections: 0,
+                    slashes: 0,
+                },
+            }
+        );
+
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 0);
+    });
+}
+
+#[test]
+fn proposal_execution_failed() {
+    initial_test_ext().execute_with(|| {
+        let parameters_fixture = ProposalParametersFixture::default();
+
+        let faulty_proposal = mock::proposals::Call::<Test>::faulty_proposal(
+            b"title".to_vec(),
+            b"description".to_vec(),
+        );
+
+        let dummy_proposal = DummyProposalFixture::default()
+            .with_parameters(parameters_fixture.params())
+            .with_proposal_code(faulty_proposal.encode());
+
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+
+        run_to_block_and_finalize(2);
+
+        let proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal,
+            Proposal {
+                parameters: parameters_fixture.params(),
+                proposer_id: 1,
+                created_at: 1,
+                status: ProposalStatus::approved(
+                    ApprovedProposalStatus::failed_execution("ExecutionFailed"),
+                    1
+                ),
+                title: b"title".to_vec(),
+                description: b"description".to_vec(),
+                voting_results: VotingResults {
+                    abstentions: 0,
+                    approvals: 4,
+                    rejections: 0,
+                    slashes: 0,
+                },
+            }
+        )
+    });
+}
+
+#[test]
+fn voting_results_calculation_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let parameters = ProposalParameters {
+            voting_period: 3,
+            approval_quorum_percentage: 50,
+            approval_threshold_percentage: 50,
+            slashing_quorum_percentage: 60,
+            slashing_threshold_percentage: 60,
+            grace_period: 0,
+            required_stake: None,
+        };
+        let dummy_proposal = DummyProposalFixture::default().with_parameters(parameters);
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Reject);
+        vote_generator.vote_and_assert_ok(VoteKind::Abstain);
+
+        run_to_block_and_finalize(2);
+
+        let proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 1,
+                approvals: 2,
+                rejections: 1,
+                slashes: 0,
+            }
+        )
+    });
+}
+
+#[test]
+fn rejected_voting_results_and_remove_proposal_id_from_active_succeeds() {
+    initial_test_ext().execute_with(|| {
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 0);
+
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Reject);
+        vote_generator.vote_and_assert_ok(VoteKind::Reject);
+        vote_generator.vote_and_assert_ok(VoteKind::Abstain);
+        vote_generator.vote_and_assert_ok(VoteKind::Abstain);
+
+        assert!(<ActiveProposalIds<Test>>::exists(proposal_id));
+
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 1);
+
+        run_to_block_and_finalize(2);
+
+        let proposal = <Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 2,
+                approvals: 0,
+                rejections: 2,
+                slashes: 0,
+            }
+        );
+
+        assert_eq!(
+            proposal.status,
+            ProposalStatus::finalized_successfully(ProposalDecisionStatus::Rejected, 1),
+        );
+        assert!(!<ActiveProposalIds<Test>>::exists(proposal_id));
+
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 0);
+    });
+}
+
+#[test]
+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(Error::EmptyTitleProvided.into()));
+
+        dummy_proposal =
+            DummyProposalFixture::default().with_title_and_body(b"title".to_vec(), Vec::new());
+        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(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(Error::DescriptionIsTooLong.into()));
+    });
+}
+
+#[test]
+fn vote_fails_with_expired_voting_period() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        run_to_block_and_finalize(6);
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert(VoteKind::Approve, Err(Error::ProposalFinalized));
+    });
+}
+
+#[test]
+fn vote_fails_with_not_active_proposal() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Reject);
+        vote_generator.vote_and_assert_ok(VoteKind::Reject);
+        vote_generator.vote_and_assert_ok(VoteKind::Abstain);
+        vote_generator.vote_and_assert_ok(VoteKind::Abstain);
+
+        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(Error::ProposalFinalized));
+    });
+}
+
+#[test]
+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(Error::ProposalNotFound));
+    });
+}
+
+#[test]
+fn vote_fails_on_double_voting() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.auto_increment_voter_id = false;
+
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert(VoteKind::Approve, Err(Error::AlreadyVoted));
+    });
+}
+
+#[test]
+fn cancel_proposal_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let parameters_fixture = ProposalParametersFixture::default();
+        let dummy_proposal =
+            DummyProposalFixture::default().with_parameters(parameters_fixture.params());
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 1);
+
+        let cancel_proposal = CancelProposalFixture::new(proposal_id);
+        cancel_proposal.cancel_and_assert(Ok(()));
+
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 0);
+
+        let proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal,
+            Proposal {
+                parameters: parameters_fixture.params(),
+                proposer_id: 1,
+                created_at: 1,
+                status: ProposalStatus::finalized_successfully(ProposalDecisionStatus::Canceled, 1),
+                title: b"title".to_vec(),
+                description: b"description".to_vec(),
+                voting_results: VotingResults::default(),
+            }
+        )
+    });
+}
+
+#[test]
+fn cancel_proposal_fails_with_not_active_proposal() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        run_to_block_and_finalize(6);
+
+        let cancel_proposal = CancelProposalFixture::new(proposal_id);
+        cancel_proposal.cancel_and_assert(Err(Error::ProposalFinalized));
+    });
+}
+
+#[test]
+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(Error::ProposalNotFound));
+    });
+}
+
+#[test]
+fn cancel_proposal_fails_with_insufficient_rights() {
+    initial_test_ext().execute_with(|| {
+        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))
+            .with_proposer(2);
+        cancel_proposal.cancel_and_assert(Err(Error::NotAuthor));
+    });
+}
+
+#[test]
+fn veto_proposal_succeeds() {
+    initial_test_ext().execute_with(|| {
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 0);
+
+        let parameters_fixture = ProposalParametersFixture::default();
+        let dummy_proposal =
+            DummyProposalFixture::default().with_parameters(parameters_fixture.params());
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 1);
+
+        let veto_proposal = VetoProposalFixture::new(proposal_id);
+        veto_proposal.veto_and_assert(Ok(()));
+
+        let proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal,
+            Proposal {
+                parameters: parameters_fixture.params(),
+                proposer_id: 1,
+                created_at: 1,
+                status: ProposalStatus::finalized_successfully(ProposalDecisionStatus::Vetoed, 1),
+                title: b"title".to_vec(),
+                description: b"description".to_vec(),
+                voting_results: VotingResults::default(),
+            }
+        );
+
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 0);
+    });
+}
+
+#[test]
+fn veto_proposal_fails_with_not_active_proposal() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        run_to_block_and_finalize(6);
+
+        let veto_proposal = VetoProposalFixture::new(proposal_id);
+        veto_proposal.veto_and_assert(Err(Error::ProposalFinalized));
+    });
+}
+
+#[test]
+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(Error::ProposalNotFound));
+    });
+}
+
+#[test]
+fn veto_proposal_fails_with_insufficient_rights() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        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(Error::RequireRootOrigin));
+    });
+}
+
+#[test]
+fn create_proposal_event_emitted() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        dummy_proposal.create_proposal_and_assert(Ok(1));
+
+        EventFixture::assert_events(vec![RawEvent::ProposalCreated(1, 1)]);
+    });
+}
+
+#[test]
+fn veto_proposal_event_emitted() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let veto_proposal = VetoProposalFixture::new(proposal_id);
+        veto_proposal.veto_and_assert(Ok(()));
+
+        EventFixture::assert_events(vec![
+            RawEvent::ProposalCreated(1, 1),
+            RawEvent::ProposalStatusUpdated(
+                1,
+                ProposalStatus::finalized_successfully(ProposalDecisionStatus::Vetoed, 1),
+            ),
+        ]);
+    });
+}
+
+#[test]
+fn cancel_proposal_event_emitted() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let cancel_proposal = CancelProposalFixture::new(proposal_id);
+        cancel_proposal.cancel_and_assert(Ok(()));
+
+        EventFixture::assert_events(vec![
+            RawEvent::ProposalCreated(1, 1),
+            RawEvent::ProposalStatusUpdated(
+                1,
+                ProposalStatus::Finalized(FinalizationData {
+                    proposal_status: ProposalDecisionStatus::Canceled,
+                    encoded_unstaking_error_due_to_broken_runtime: None,
+                    stake_data_after_unstaking_error: None,
+                    finalized_at: 1,
+                }),
+            ),
+        ]);
+    });
+}
+
+#[test]
+fn vote_proposal_event_emitted() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+
+        EventFixture::assert_events(vec![
+            RawEvent::ProposalCreated(1, 1),
+            RawEvent::Voted(1, 1, VoteKind::Approve),
+        ]);
+    });
+}
+
+#[test]
+fn create_proposal_and_expire_it() {
+    initial_test_ext().execute_with(|| {
+        let parameters_fixture = ProposalParametersFixture::default();
+        let dummy_proposal =
+            DummyProposalFixture::default().with_parameters(parameters_fixture.params());
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        run_to_block_and_finalize(8);
+
+        let proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal,
+            Proposal {
+                parameters: parameters_fixture.params(),
+                proposer_id: 1,
+                created_at: 1,
+                status: ProposalStatus::finalized_successfully(ProposalDecisionStatus::Expired, 4),
+                title: b"title".to_vec(),
+                description: b"description".to_vec(),
+                voting_results: VotingResults::default(),
+            }
+        )
+    });
+}
+
+#[test]
+fn proposal_execution_postponed_because_of_grace_period() {
+    initial_test_ext().execute_with(|| {
+        let parameters_fixture = ProposalParametersFixture::default().with_grace_period(2);
+        let dummy_proposal =
+            DummyProposalFixture::default().with_parameters(parameters_fixture.params());
+
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+
+        run_to_block_and_finalize(1);
+        run_to_block_and_finalize(2);
+
+        // check internal cache for proposal_id presense
+        assert!(<PendingExecutionProposalIds<Test>>::enumerate()
+            .find(|(x, _)| *x == proposal_id)
+            .is_some());
+
+        let proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal,
+            Proposal {
+                parameters: parameters_fixture.params(),
+                proposer_id: 1,
+                created_at: 1,
+                status: ProposalStatus::approved(ApprovedProposalStatus::PendingExecution, 1),
+                title: b"title".to_vec(),
+                description: b"description".to_vec(),
+                voting_results: VotingResults {
+                    abstentions: 0,
+                    approvals: 4,
+                    rejections: 0,
+                    slashes: 0,
+                },
+            }
+        );
+    });
+}
+
+#[test]
+fn proposal_execution_vetoed_successfully_during_the_grace_period() {
+    initial_test_ext().execute_with(|| {
+        let parameters_fixture = ProposalParametersFixture::default().with_grace_period(2);
+        let dummy_proposal =
+            DummyProposalFixture::default().with_parameters(parameters_fixture.params());
+
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+
+        run_to_block_and_finalize(1);
+        run_to_block_and_finalize(2);
+
+        // check internal cache for proposal_id presense
+        assert!(<PendingExecutionProposalIds<Test>>::enumerate()
+            .find(|(x, _)| *x == proposal_id)
+            .is_some());
+
+        let proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal,
+            Proposal {
+                parameters: parameters_fixture.params(),
+                proposer_id: 1,
+                created_at: 1,
+                status: ProposalStatus::approved(ApprovedProposalStatus::PendingExecution, 1),
+                title: b"title".to_vec(),
+                description: b"description".to_vec(),
+                voting_results: VotingResults {
+                    abstentions: 0,
+                    approvals: 4,
+                    rejections: 0,
+                    slashes: 0,
+                },
+            }
+        );
+
+        let veto_proposal = VetoProposalFixture::new(proposal_id);
+        veto_proposal.veto_and_assert(Ok(()));
+
+        let proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal,
+            Proposal {
+                parameters: parameters_fixture.params(),
+                proposer_id: 1,
+                created_at: 1,
+                status: ProposalStatus::finalized_successfully(ProposalDecisionStatus::Vetoed, 2),
+                title: b"title".to_vec(),
+                description: b"description".to_vec(),
+                voting_results: VotingResults {
+                    abstentions: 0,
+                    approvals: 4,
+                    rejections: 0,
+                    slashes: 0,
+                },
+            }
+        );
+
+        // check internal cache for proposal_id presense
+        assert!(<PendingExecutionProposalIds<Test>>::enumerate()
+            .find(|(x, _)| *x == proposal_id)
+            .is_none());
+    });
+}
+
+#[test]
+fn proposal_execution_succeeds_after_the_grace_period() {
+    initial_test_ext().execute_with(|| {
+        let parameters_fixture = ProposalParametersFixture::default().with_grace_period(1);
+        let dummy_proposal =
+            DummyProposalFixture::default().with_parameters(parameters_fixture.params());
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+        vote_generator.vote_and_assert_ok(VoteKind::Approve);
+
+        run_to_block_and_finalize(1);
+
+        // check internal cache for proposal_id presence
+        assert!(<PendingExecutionProposalIds<Test>>::enumerate()
+            .find(|(x, _)| *x == proposal_id)
+            .is_some());
+
+        let mut proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        let mut expected_proposal = Proposal {
+            parameters: parameters_fixture.params(),
+            proposer_id: 1,
+            created_at: 1,
+            status: ProposalStatus::approved(ApprovedProposalStatus::PendingExecution, 1),
+            title: b"title".to_vec(),
+            description: b"description".to_vec(),
+            voting_results: VotingResults {
+                abstentions: 0,
+                approvals: 4,
+                rejections: 0,
+                slashes: 0,
+            },
+        };
+
+        assert_eq!(proposal, expected_proposal);
+
+        run_to_block_and_finalize(2);
+
+        proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        expected_proposal.status = ProposalStatus::approved(ApprovedProposalStatus::Executed, 1);
+
+        assert_eq!(proposal, expected_proposal);
+
+        // check internal cache for proposal_id absense
+        assert!(<PendingExecutionProposalIds<Test>>::enumerate()
+            .find(|(x, _)| *x == proposal_id)
+            .is_none());
+    });
+}
+
+#[test]
+fn create_proposal_fails_on_exceeding_max_active_proposals_count() {
+    initial_test_ext().execute_with(|| {
+        for idx in 1..101 {
+            let dummy_proposal = DummyProposalFixture::default();
+            dummy_proposal.create_proposal_and_assert(Ok(idx));
+            // internal active proposal counter check
+            assert_eq!(<ActiveProposalCount>::get(), idx);
+        }
+
+        let dummy_proposal = DummyProposalFixture::default();
+        dummy_proposal
+            .create_proposal_and_assert(Err(Error::MaxActiveProposalNumberExceeded.into()));
+        // internal active proposal counter check
+        assert_eq!(<ActiveProposalCount>::get(), 100);
+    });
+}
+
+#[test]
+fn voting_internal_cache_exists_after_proposal_finalization() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        dummy_proposal.create_proposal_and_assert(Ok(1));
+
+        // last created proposal id equals current proposal count
+        let proposal_id = <ProposalCount>::get();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Reject);
+        vote_generator.vote_and_assert_ok(VoteKind::Reject);
+        vote_generator.vote_and_assert_ok(VoteKind::Abstain);
+        vote_generator.vote_and_assert_ok(VoteKind::Abstain);
+
+        // cache exists
+        assert!(<crate::VoteExistsByProposalByVoter<Test>>::exists(
+            proposal_id,
+            1
+        ));
+
+        run_to_block_and_finalize(2);
+
+        // cache still exists and is not cleared
+        assert!(<crate::VoteExistsByProposalByVoter<Test>>::exists(
+            proposal_id,
+            1
+        ));
+    });
+}
+
+#[test]
+fn create_dummy_proposal_succeeds_with_stake() {
+    initial_test_ext().execute_with(|| {
+        let account_id = 1;
+
+        let required_stake = 200;
+        let parameters_fixture =
+            ProposalParametersFixture::default().with_required_stake(required_stake);
+
+        let dummy_proposal = DummyProposalFixture::default()
+            .with_parameters(parameters_fixture.params())
+            .with_account_id(account_id)
+            .with_stake(200);
+
+        let _imbalance = <Test as stake::Trait>::Currency::deposit_creating(&account_id, 500);
+
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal,
+            Proposal {
+                parameters: parameters_fixture.params(),
+                proposer_id: 1,
+                created_at: 1,
+                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(),
+            }
+        )
+    });
+}
+
+#[test]
+fn create_dummy_proposal_fail_with_stake_on_empty_account() {
+    initial_test_ext().execute_with(|| {
+        let account_id = 1;
+
+        let required_stake = 200;
+        let parameters_fixture =
+            ProposalParametersFixture::default().with_required_stake(required_stake);
+        let dummy_proposal = DummyProposalFixture::default()
+            .with_parameters(parameters_fixture.params())
+            .with_account_id(account_id)
+            .with_stake(required_stake);
+
+        dummy_proposal
+            .create_proposal_and_assert(Err(Error::Other("too few free funds in account")));
+    });
+}
+
+#[test]
+fn create_proposal_fais_with_invalid_stake_parameters() {
+    initial_test_ext().execute_with(|| {
+        let parameters_fixture = ProposalParametersFixture::default();
+
+        let mut dummy_proposal = DummyProposalFixture::default()
+            .with_parameters(parameters_fixture.params())
+            .with_stake(200);
+
+        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(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(Error::StakeDiffersFromRequired.into()));
+    });
+}
+
+#[test]
+fn finalize_expired_proposal_and_check_stake_removing_with_balance_checks_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let account_id = 1;
+
+        let stake_amount = 200;
+        let parameters = ProposalParameters {
+            voting_period: 3,
+            approval_quorum_percentage: 50,
+            approval_threshold_percentage: 60,
+            slashing_quorum_percentage: 60,
+            slashing_threshold_percentage: 60,
+            grace_period: 5,
+            required_stake: Some(stake_amount),
+        };
+        let dummy_proposal = DummyProposalFixture::default()
+            .with_parameters(parameters)
+            .with_account_id(account_id)
+            .with_stake(stake_amount);
+
+        let account_balance = 500;
+        let _imbalance =
+            <Test as stake::Trait>::Currency::deposit_creating(&account_id, account_balance);
+
+        assert_eq!(
+            <Test as stake::Trait>::Currency::total_balance(&account_id),
+            account_balance
+        );
+
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+        assert_eq!(
+            <Test as stake::Trait>::Currency::total_balance(&account_id),
+            account_balance - stake_amount
+        );
+
+        let mut proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        let mut expected_proposal = Proposal {
+            parameters,
+            proposer_id: 1,
+            created_at: 1,
+            status: ProposalStatus::Active(Some(ActiveStake {
+                stake_id: 0,
+                source_account_id: 1,
+            })),
+            title: b"title".to_vec(),
+            description: b"description".to_vec(),
+            voting_results: VotingResults::default(),
+        };
+
+        assert_eq!(proposal, expected_proposal);
+
+        run_to_block_and_finalize(5);
+
+        proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        expected_proposal.status = ProposalStatus::Finalized(FinalizationData {
+            proposal_status: ProposalDecisionStatus::Expired,
+            finalized_at: 4,
+            encoded_unstaking_error_due_to_broken_runtime: None,
+            stake_data_after_unstaking_error: None,
+        });
+
+        assert_eq!(proposal, expected_proposal);
+
+        let rejection_fee = RejectionFee::get();
+        assert_eq!(
+            <Test as stake::Trait>::Currency::total_balance(&account_id),
+            account_balance - rejection_fee
+        );
+    });
+}
+
+#[test]
+fn proposal_cancellation_with_slashes_with_balance_checks_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let account_id = 1;
+
+        let stake_amount = 200;
+        let parameters = ProposalParameters {
+            voting_period: 3,
+            approval_quorum_percentage: 50,
+            approval_threshold_percentage: 60,
+            slashing_quorum_percentage: 60,
+            slashing_threshold_percentage: 60,
+            grace_period: 5,
+            required_stake: Some(stake_amount),
+        };
+        let dummy_proposal = DummyProposalFixture::default()
+            .with_parameters(parameters)
+            .with_account_id(account_id.clone())
+            .with_stake(stake_amount);
+
+        let account_balance = 500;
+        let _imbalance =
+            <Test as stake::Trait>::Currency::deposit_creating(&account_id, account_balance);
+
+        assert_eq!(
+            <Test as stake::Trait>::Currency::total_balance(&account_id),
+            account_balance
+        );
+
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+        assert_eq!(
+            <Test as stake::Trait>::Currency::total_balance(&account_id),
+            account_balance - stake_amount
+        );
+
+        let mut proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        let mut expected_proposal = Proposal {
+            parameters,
+            proposer_id: 1,
+            created_at: 1,
+            status: ProposalStatus::Active(Some(ActiveStake {
+                stake_id: 0,
+                source_account_id: 1,
+            })),
+            title: b"title".to_vec(),
+            description: b"description".to_vec(),
+            voting_results: VotingResults::default(),
+        };
+
+        assert_eq!(proposal, expected_proposal);
+
+        let cancel_proposal_fixture = CancelProposalFixture::new(proposal_id);
+
+        cancel_proposal_fixture.cancel_and_assert(Ok(()));
+
+        proposal = <crate::Proposals<Test>>::get(proposal_id);
+
+        expected_proposal.status = ProposalStatus::Finalized(FinalizationData {
+            proposal_status: ProposalDecisionStatus::Canceled,
+            finalized_at: 1,
+            encoded_unstaking_error_due_to_broken_runtime: None,
+            stake_data_after_unstaking_error: None,
+        });
+
+        assert_eq!(proposal, expected_proposal);
+
+        let cancellation_fee = CancellationFee::get();
+        assert_eq!(
+            <Test as stake::Trait>::Currency::total_balance(&account_id),
+            account_balance - cancellation_fee
+        );
+    });
+}
+
+#[test]
+fn finalize_proposal_using_stake_mocks_succeeds() {
+    handle_mock(|| {
+        initial_test_ext().execute_with(|| {
+            let mock = {
+                let mut mock = crate::types::MockStakeHandler::<Test>::new();
+                mock.expect_create_stake().times(1).returning(|| Ok(1));
+
+                mock.expect_make_stake_imbalance()
+                    .times(1)
+                    .returning(|_, _| Ok(crate::types::NegativeImbalance::<Test>::new(200)));
+
+                mock.expect_stake().times(1).returning(|_, _| Ok(()));
+
+                mock.expect_remove_stake().times(1).returning(|_| Ok(()));
+
+                mock.expect_unstake().times(1).returning(|_| Ok(()));
+
+                mock.expect_slash().times(1).returning(|_, _| Ok(()));
+
+                Rc::new(mock)
+            };
+            set_stake_handler_impl(mock.clone());
+
+            let account_id = 1;
+
+            let stake_amount = 200;
+            let parameters_fixture =
+                ProposalParametersFixture::default().with_required_stake(stake_amount);
+            let dummy_proposal = DummyProposalFixture::default()
+                .with_parameters(parameters_fixture.params())
+                .with_account_id(account_id)
+                .with_stake(stake_amount);
+
+            let _proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+            run_to_block_and_finalize(5);
+        });
+    });
+}
+
+#[test]
+fn proposal_slashing_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Reject);
+        vote_generator.vote_and_assert_ok(VoteKind::Slash);
+        vote_generator.vote_and_assert_ok(VoteKind::Slash);
+        vote_generator.vote_and_assert_ok(VoteKind::Slash);
+
+        assert!(<ActiveProposalIds<Test>>::exists(proposal_id));
+
+        run_to_block_and_finalize(2);
+
+        let proposal = <Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 0,
+                approvals: 0,
+                rejections: 1,
+                slashes: 3,
+            }
+        );
+
+        assert_eq!(
+            proposal.status,
+            ProposalStatus::Finalized(FinalizationData {
+                proposal_status: ProposalDecisionStatus::Slashed,
+                encoded_unstaking_error_due_to_broken_runtime: None,
+                finalized_at: 1,
+                stake_data_after_unstaking_error: None,
+            }),
+        );
+        assert!(!<ActiveProposalIds<Test>>::exists(proposal_id));
+    });
+}
+
+#[test]
+fn finalize_proposal_using_stake_mocks_failed() {
+    handle_mock(|| {
+        initial_test_ext().execute_with(|| {
+            let mock = {
+                let mut mock = crate::types::MockStakeHandler::<Test>::new();
+                mock.expect_create_stake().times(1).returning(|| Ok(1));
+
+                mock.expect_remove_stake()
+                    .times(1)
+                    .returning(|_| Err("Cannot remove stake"));
+
+                mock.expect_make_stake_imbalance()
+                    .times(1)
+                    .returning(|_, _| Ok(crate::types::NegativeImbalance::<Test>::new(200)));
+
+                mock.expect_stake().times(1).returning(|_, _| Ok(()));
+
+                mock.expect_unstake().times(1).returning(|_| Ok(()));
+
+                mock.expect_slash().times(1).returning(|_, _| Ok(()));
+
+                Rc::new(mock)
+            };
+            set_stake_handler_impl(mock.clone());
+
+            let account_id = 1;
+
+            let stake_amount = 200;
+            let parameters_fixture =
+                ProposalParametersFixture::default().with_required_stake(stake_amount);
+            let dummy_proposal = DummyProposalFixture::default()
+                .with_parameters(parameters_fixture.params())
+                .with_account_id(account_id)
+                .with_stake(stake_amount);
+
+            let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+            run_to_block_and_finalize(5);
+
+            let proposal = <Proposals<Test>>::get(proposal_id);
+            assert_eq!(
+                proposal,
+                Proposal {
+                    parameters: parameters_fixture.params(),
+                    proposer_id: 1,
+                    created_at: 1,
+                    status: ProposalStatus::finalized(
+                        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(),
+                }
+            );
+        });
+    });
+}
+
+#[test]
+fn create_proposal_fails_with_invalid_threshold_parameters() {
+    initial_test_ext().execute_with(|| {
+        let mut parameters = ProposalParameters {
+            voting_period: 3,
+            approval_quorum_percentage: 50,
+            approval_threshold_percentage: 0,
+            slashing_quorum_percentage: 60,
+            slashing_threshold_percentage: 60,
+            grace_period: 5,
+            required_stake: None,
+        };
+
+        let mut dummy_proposal = DummyProposalFixture::default().with_parameters(parameters);
+
+        dummy_proposal
+            .create_proposal_and_assert(Err(Error::InvalidParameterApprovalThreshold.into()));
+
+        parameters.approval_threshold_percentage = 60;
+        parameters.slashing_threshold_percentage = 0;
+        dummy_proposal = DummyProposalFixture::default().with_parameters(parameters);
+
+        dummy_proposal
+            .create_proposal_and_assert(Err(Error::InvalidParameterSlashingThreshold.into()));
+    });
+}
+
+#[test]
+fn proposal_reset_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Reject);
+        vote_generator.vote_and_assert_ok(VoteKind::Abstain);
+        vote_generator.vote_and_assert_ok(VoteKind::Slash);
+
+        assert!(<ActiveProposalIds<Test>>::exists(proposal_id));
+        assert_eq!(
+            <VoteExistsByProposalByVoter<Test>>::get(&proposal_id, &2),
+            VoteKind::Abstain
+        );
+
+        run_to_block_and_finalize(2);
+
+        let proposal = <Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 1,
+                approvals: 0,
+                rejections: 1,
+                slashes: 1,
+            }
+        );
+
+        ProposalsEngine::reset_active_proposals();
+
+        let updated_proposal = <Proposals<Test>>::get(proposal_id);
+
+        assert_eq!(
+            updated_proposal.voting_results,
+            VotingResults {
+                abstentions: 0,
+                approvals: 0,
+                rejections: 0,
+                slashes: 0,
+            }
+        );
+
+        // whole double map prefix was removed (should return default value)
+        assert_eq!(
+            <VoteExistsByProposalByVoter<Test>>::get(&proposal_id, &2),
+            VoteKind::default()
+        );
+    });
+}
+
+#[test]
+fn proposal_counters_are_valid() {
+    initial_test_ext().execute_with(|| {
+        let mut dummy_proposal = DummyProposalFixture::default();
+        let _ = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        dummy_proposal = DummyProposalFixture::default();
+        let _ = dummy_proposal.create_proposal_and_assert(Ok(2)).unwrap();
+
+        dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(3)).unwrap();
+
+        assert_eq!(ActiveProposalCount::get(), 3);
+        assert_eq!(ProposalCount::get(), 3);
+
+        let cancel_proposal_fixture = CancelProposalFixture::new(proposal_id);
+        cancel_proposal_fixture.cancel_and_assert(Ok(()));
+
+        assert_eq!(ActiveProposalCount::get(), 2);
+        assert_eq!(ProposalCount::get(), 3);
+    });
+}
+
+#[test]
+fn proposal_stake_cache_is_valid() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance_using_account_id(1, 50000);
+
+        let stake = 250u32;
+        let parameters = ProposalParametersFixture::default().with_required_stake(stake.into());
+        let dummy_proposal = DummyProposalFixture::default()
+            .with_parameters(parameters.params())
+            .with_stake(stake as u64);
+
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+        let expected_stake_id = 0;
+        assert_eq!(
+            <StakesProposals<Test>>::get(&expected_stake_id),
+            proposal_id
+        );
+    });
+}
+
+#[test]
+fn slash_balance_is_calculated_correctly() {
+    initial_test_ext().execute_with(|| {
+        let vetoed_slash_balance = ProposalsEngine::calculate_slash_balance(
+            &ProposalDecisionStatus::Vetoed,
+            &ProposalParametersFixture::default().params(),
+        );
+
+        assert_eq!(vetoed_slash_balance, 0);
+
+        let approved_slash_balance = ProposalsEngine::calculate_slash_balance(
+            &ProposalDecisionStatus::Approved(ApprovedProposalStatus::Executed),
+            &ProposalParametersFixture::default().params(),
+        );
+
+        assert_eq!(approved_slash_balance, 0);
+
+        let rejection_fee = <Test as crate::Trait>::RejectionFee::get();
+
+        let rejected_slash_balance = ProposalsEngine::calculate_slash_balance(
+            &ProposalDecisionStatus::Rejected,
+            &ProposalParametersFixture::default().params(),
+        );
+
+        assert_eq!(rejected_slash_balance, rejection_fee);
+
+        let expired_slash_balance = ProposalsEngine::calculate_slash_balance(
+            &ProposalDecisionStatus::Expired,
+            &ProposalParametersFixture::default().params(),
+        );
+
+        assert_eq!(expired_slash_balance, rejection_fee);
+
+        let cancellation_fee = <Test as crate::Trait>::CancellationFee::get();
+
+        let cancellation_slash_balance = ProposalsEngine::calculate_slash_balance(
+            &ProposalDecisionStatus::Canceled,
+            &ProposalParametersFixture::default().params(),
+        );
+
+        assert_eq!(cancellation_slash_balance, cancellation_fee);
+
+        let slash_balance_with_no_stake = ProposalsEngine::calculate_slash_balance(
+            &ProposalDecisionStatus::Slashed,
+            &ProposalParametersFixture::default().params(),
+        );
+
+        assert_eq!(slash_balance_with_no_stake, 0);
+
+        let stake = 256;
+        let slash_balance_with_stake = ProposalsEngine::calculate_slash_balance(
+            &ProposalDecisionStatus::Slashed,
+            &ProposalParametersFixture::default()
+                .with_required_stake(stake)
+                .params(),
+        );
+
+        assert_eq!(slash_balance_with_stake, stake);
+    });
+}

+ 793 - 0
runtime-modules/proposals/engine/src/types/mod.rs

@@ -0,0 +1,793 @@
+//! Proposals types module for the Joystream platform. Version 2.
+//! Provides types for the proposal engine.
+
+#![warn(missing_docs)]
+
+use codec::{Decode, Encode};
+use rstd::cmp::PartialOrd;
+use rstd::ops::Add;
+use rstd::prelude::*;
+
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+use sr_primitives::Perbill;
+use srml_support::dispatch;
+use srml_support::traits::Currency;
+
+mod proposal_statuses;
+mod stakes;
+
+pub use proposal_statuses::{
+    ApprovedProposalStatus, FinalizationData, ProposalDecisionStatus, ProposalStatus,
+};
+pub(crate) use stakes::ProposalStakeManager;
+pub use stakes::{DefaultStakeHandlerProvider, StakeHandler, StakeHandlerProvider};
+
+#[cfg(test)]
+pub(crate) use stakes::DefaultStakeHandler;
+
+#[cfg(test)]
+pub(crate) use stakes::MockStakeHandler;
+
+/// Vote kind for the proposal. Sum of all votes defines proposal status.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
+pub enum VoteKind {
+    /// Pass, an alternative or a ranking, for binary, multiple choice
+    /// and ranked choice propositions, respectively.
+    Approve,
+
+    /// Against proposal.
+    Reject,
+
+    /// Reject proposal and slash it stake.
+    Slash,
+
+    /// Signals presence, but unwillingness to cast judgment on substance of vote.
+    Abstain,
+}
+
+impl Default for VoteKind {
+    fn default() -> Self {
+        VoteKind::Reject
+    }
+}
+
+/// Proposal parameters required to manage proposal risk.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, Copy, PartialEq, Eq, Debug)]
+pub struct ProposalParameters<BlockNumber, Balance> {
+    /// During this period, votes can be accepted
+    pub voting_period: BlockNumber,
+
+    /// A pause before execution of the approved proposal. Zero means approved proposal would be
+    /// executed immediately.
+    pub grace_period: BlockNumber,
+
+    /// Quorum percentage of approving voters required to pass the proposal.
+    pub approval_quorum_percentage: u32,
+
+    /// Approval votes percentage threshold to pass the proposal.
+    pub approval_threshold_percentage: u32,
+
+    /// Quorum percentage of voters required to slash the proposal.
+    pub slashing_quorum_percentage: u32,
+
+    /// Slashing votes percentage threshold to slash the proposal.
+    pub slashing_threshold_percentage: u32,
+
+    /// Proposal stake
+    pub required_stake: Option<Balance>,
+}
+
+/// Contains current voting results
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct VotingResults {
+    /// 'Abstain' votes counter
+    pub abstentions: u32,
+
+    /// 'Approve' votes counter
+    pub approvals: u32,
+
+    /// 'Reject' votes counter
+    pub rejections: u32,
+
+    /// 'Slash' votes counter
+    pub slashes: u32,
+}
+
+impl VotingResults {
+    /// Add vote to the related counter
+    pub fn add_vote(&mut self, vote: VoteKind) {
+        match vote {
+            VoteKind::Abstain => self.abstentions += 1,
+            VoteKind::Approve => self.approvals += 1,
+            VoteKind::Reject => self.rejections += 1,
+            VoteKind::Slash => self.slashes += 1,
+        }
+    }
+
+    /// Calculates number of votes so far
+    pub fn votes_number(&self) -> u32 {
+        self.abstentions + self.approvals + self.rejections + self.slashes
+    }
+}
+
+/// 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<StakeId, AccountId> {
+    /// 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<BlockNumber, ProposerId, Balance, StakeId, AccountId> {
+    /// Proposals parameter, characterize different proposal types.
+    pub parameters: ProposalParameters<BlockNumber, Balance>,
+
+    /// Identifier of member proposing.
+    pub proposer_id: ProposerId,
+
+    /// Proposal description
+    pub title: Vec<u8>,
+
+    /// Proposal body
+    pub description: Vec<u8>,
+
+    /// When it was created.
+    pub created_at: BlockNumber,
+
+    /// Current proposal status
+    pub status: ProposalStatus<BlockNumber, StakeId, AccountId>,
+
+    /// Curring voting result for the proposal
+    pub voting_results: VotingResults,
+}
+
+impl<BlockNumber, ProposerId, Balance, StakeId, AccountId>
+    Proposal<BlockNumber, ProposerId, Balance, StakeId, AccountId>
+where
+    BlockNumber: Add<Output = BlockNumber> + PartialOrd + Copy,
+    StakeId: Clone,
+    AccountId: Clone,
+{
+    /// Returns whether voting period expired by now
+    pub fn is_voting_period_expired(&self, now: BlockNumber) -> bool {
+        now >= self.created_at + self.parameters.voting_period
+    }
+
+    /// Returns whether grace period expired by now.
+    /// Grace period can be expired only if proposal is finalized with Approved status.
+    /// Returns false otherwise.
+    pub fn is_grace_period_expired(&self, now: BlockNumber) -> bool {
+        if let ProposalStatus::Finalized(finalized_status) = self.status.clone() {
+            if let ProposalDecisionStatus::Approved(_) = finalized_status.proposal_status {
+                return now >= finalized_status.finalized_at + self.parameters.grace_period;
+            }
+        }
+
+        false
+    }
+
+    /// Determines the finalized proposal status using voting results tally for current proposal.
+    /// Calculates votes, takes in account voting period expiration.
+    /// If voting process is in progress, then decision status is None.
+    /// Parameters: current time, total voters number involved (council size).
+    /// Returns the proposal finalized status if any.
+    pub fn define_proposal_decision_status(
+        &self,
+        total_voters_count: u32,
+        now: BlockNumber,
+    ) -> Option<ProposalDecisionStatus> {
+        let proposal_status_resolution = ProposalStatusResolution {
+            proposal: self,
+            approvals: self.voting_results.approvals,
+            slashes: self.voting_results.slashes,
+            now,
+            votes_count: self.voting_results.votes_number(),
+            total_voters_count,
+        };
+
+        if proposal_status_resolution.is_approval_quorum_reached()
+            && proposal_status_resolution.is_approval_threshold_reached()
+        {
+            Some(ProposalDecisionStatus::Approved(
+                ApprovedProposalStatus::PendingExecution,
+            ))
+        } else if proposal_status_resolution.is_slashing_quorum_reached()
+            && proposal_status_resolution.is_slashing_threshold_reached()
+        {
+            Some(ProposalDecisionStatus::Slashed)
+        } else if proposal_status_resolution.is_expired() {
+            Some(ProposalDecisionStatus::Expired)
+        } else if proposal_status_resolution.is_voting_completed() {
+            Some(ProposalDecisionStatus::Rejected)
+        } else {
+            None
+        }
+    }
+
+    /// Reset the proposal in Active status. Proposal with other status won't be changed.
+    /// Reset proposal operation clears voting results.
+    pub fn reset_proposal(&mut self) {
+        if let ProposalStatus::Active(_) = self.status.clone() {
+            self.voting_results = VotingResults::default();
+        }
+    }
+}
+
+/// Provides data for the voting.
+pub trait VotersParameters {
+    /// Defines maximum voters count for the proposal
+    fn total_voters_count() -> u32;
+}
+
+// Calculates quorum, votes threshold, expiration status
+struct ProposalStatusResolution<'a, BlockNumber, ProposerId, Balance, StakeId, AccountId> {
+    proposal: &'a Proposal<BlockNumber, ProposerId, Balance, StakeId, AccountId>,
+    now: BlockNumber,
+    votes_count: u32,
+    total_voters_count: u32,
+    approvals: u32,
+    slashes: u32,
+}
+
+impl<'a, BlockNumber, ProposerId, Balance, StakeId, AccountId>
+    ProposalStatusResolution<'a, BlockNumber, ProposerId, Balance, StakeId, AccountId>
+where
+    BlockNumber: Add<Output = BlockNumber> + PartialOrd + Copy,
+    StakeId: Clone,
+    AccountId: Clone,
+{
+    // Proposal has been expired and quorum not reached.
+    pub fn is_expired(&self) -> bool {
+        self.proposal.is_voting_period_expired(self.now)
+    }
+
+    // Approval quorum reached for the proposal. Compares predefined parameter with actual
+    // votes sum divided by total possible votes count.
+    pub fn is_approval_quorum_reached(&self) -> bool {
+        let actual_votes_fraction =
+            Perbill::from_rational_approximation(self.votes_count, self.total_voters_count);
+        let approval_quorum_fraction =
+            Perbill::from_percent(self.proposal.parameters.approval_quorum_percentage);
+
+        actual_votes_fraction.deconstruct() >= approval_quorum_fraction.deconstruct()
+    }
+
+    // Slashing quorum reached for the proposal. Compares predefined parameter with actual
+    // votes sum divided by total possible votes count.
+    pub fn is_slashing_quorum_reached(&self) -> bool {
+        let actual_votes_fraction =
+            Perbill::from_rational_approximation(self.votes_count, self.total_voters_count);
+        let slashing_quorum_fraction =
+            Perbill::from_percent(self.proposal.parameters.slashing_quorum_percentage);
+
+        actual_votes_fraction.deconstruct() >= slashing_quorum_fraction.deconstruct()
+    }
+
+    // Approval threshold reached for the proposal. Compares predefined parameter with 'approve'
+    // votes sum divided by actual votes count.
+    pub fn is_approval_threshold_reached(&self) -> bool {
+        let approval_votes_fraction =
+            Perbill::from_rational_approximation(self.approvals, self.votes_count);
+        let required_threshold_fraction =
+            Perbill::from_percent(self.proposal.parameters.approval_threshold_percentage);
+
+        approval_votes_fraction.deconstruct() >= required_threshold_fraction.deconstruct()
+    }
+
+    // Slashing threshold reached for the proposal. Compares predefined parameter with 'approve'
+    // votes sum divided by actual votes count.
+    pub fn is_slashing_threshold_reached(&self) -> bool {
+        let slashing_votes_fraction =
+            Perbill::from_rational_approximation(self.slashes, self.votes_count);
+        let required_threshold_fraction =
+            Perbill::from_percent(self.proposal.parameters.slashing_threshold_percentage);
+
+        slashing_votes_fraction.deconstruct() >= required_threshold_fraction.deconstruct()
+    }
+
+    // All voters had voted
+    pub fn is_voting_completed(&self) -> bool {
+        self.votes_count == self.total_voters_count
+    }
+}
+
+/// Proposal executable code wrapper
+pub trait ProposalExecutable {
+    /// Executes proposal code
+    fn execute(&self) -> dispatch::Result;
+}
+
+/// Proposal code binary converter
+pub trait ProposalCodeDecoder<T: system::Trait> {
+    /// Converts proposal code binary to executable representation
+    fn decode_proposal(
+        proposal_type: u32,
+        proposal_code: Vec<u8>,
+    ) -> Result<Box<dyn ProposalExecutable>, &'static str>;
+}
+
+/// Balance alias
+pub type BalanceOf<T> =
+    <<T as stake::Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::Balance;
+
+/// Balance alias for staking
+pub type NegativeImbalance<T> =
+    <<T as stake::Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::NegativeImbalance;
+
+/// Balance type of runtime
+pub type CurrencyOf<T> = <T as stake::Trait>::Currency;
+
+/// Data container for the finalized proposal results
+pub(crate) struct FinalizedProposalData<
+    ProposalId,
+    BlockNumber,
+    ProposerId,
+    Balance,
+    StakeId,
+    AccountId,
+> {
+    /// Proposal id
+    pub proposal_id: ProposalId,
+
+    /// Proposal to be finalized
+    pub proposal: Proposal<BlockNumber, ProposerId, Balance, StakeId, AccountId>,
+
+    /// Proposal finalization status
+    pub status: ProposalDecisionStatus,
+
+    /// Proposal finalization block number
+    pub finalized_at: BlockNumber,
+}
+
+/// Data container for the approved proposal results
+pub(crate) struct ApprovedProposalData<
+    ProposalId,
+    BlockNumber,
+    ProposerId,
+    Balance,
+    StakeId,
+    AccountId,
+> {
+    /// Proposal id
+    pub proposal_id: ProposalId,
+
+    /// Proposal to be finalized
+    pub proposal: Proposal<BlockNumber, ProposerId, Balance, StakeId, AccountId>,
+
+    /// Proposal finalisation status data
+    pub finalisation_status_data: FinalizationData<BlockNumber, StakeId, AccountId>,
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::types::ProposalStatusResolution;
+    use crate::*;
+
+    // Alias introduced for simplicity of changing Proposal exact types.
+    type ProposalObject = Proposal<u64, u64, u64, u64, u64>;
+
+    #[test]
+    fn proposal_voting_period_expired() {
+        let mut proposal = ProposalObject::default();
+
+        proposal.created_at = 1;
+        proposal.parameters.voting_period = 3;
+
+        assert!(proposal.is_voting_period_expired(4));
+    }
+
+    #[test]
+    fn proposal_voting_period_not_expired() {
+        let mut proposal = ProposalObject::default();
+
+        proposal.created_at = 1;
+        proposal.parameters.voting_period = 3;
+
+        assert!(!proposal.is_voting_period_expired(3));
+    }
+
+    #[test]
+    fn proposal_grace_period_expired() {
+        let mut proposal = ProposalObject::default();
+
+        proposal.parameters.grace_period = 3;
+        proposal.status = ProposalStatus::finalized_successfully(
+            ProposalDecisionStatus::Approved(ApprovedProposalStatus::PendingExecution),
+            0,
+        );
+
+        assert!(proposal.is_grace_period_expired(4));
+    }
+
+    #[test]
+    fn proposal_grace_period_auto_expired() {
+        let mut proposal = ProposalObject::default();
+
+        proposal.parameters.grace_period = 0;
+        proposal.status = ProposalStatus::finalized_successfully(
+            ProposalDecisionStatus::Approved(ApprovedProposalStatus::PendingExecution),
+            0,
+        );
+
+        assert!(proposal.is_grace_period_expired(1));
+    }
+
+    #[test]
+    fn proposal_grace_period_not_expired() {
+        let mut proposal = ProposalObject::default();
+
+        proposal.parameters.grace_period = 3;
+
+        assert!(!proposal.is_grace_period_expired(3));
+    }
+
+    #[test]
+    fn proposal_grace_period_not_expired_because_of_not_approved_proposal() {
+        let mut proposal = ProposalObject::default();
+
+        proposal.parameters.grace_period = 3;
+
+        assert!(!proposal.is_grace_period_expired(3));
+    }
+
+    #[test]
+    fn define_proposal_decision_status_returns_expired() {
+        let mut proposal = ProposalObject::default();
+        let now = 5;
+        proposal.created_at = 1;
+        proposal.parameters.voting_period = 3;
+        proposal.parameters.approval_quorum_percentage = 80;
+        proposal.parameters.approval_threshold_percentage = 40;
+        proposal.parameters.slashing_quorum_percentage = 50;
+        proposal.parameters.slashing_threshold_percentage = 50;
+
+        proposal.voting_results.add_vote(VoteKind::Reject);
+        proposal.voting_results.add_vote(VoteKind::Approve);
+        proposal.voting_results.add_vote(VoteKind::Approve);
+
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 0,
+                approvals: 2,
+                rejections: 1,
+                slashes: 0,
+            }
+        );
+
+        let expected_proposal_status = proposal.define_proposal_decision_status(5, now);
+        assert_eq!(
+            expected_proposal_status,
+            Some(ProposalDecisionStatus::Expired)
+        );
+    }
+
+    #[test]
+    fn define_proposal_decision_status_returns_approved() {
+        let now = 2;
+        let mut proposal = ProposalObject::default();
+        proposal.created_at = 1;
+        proposal.parameters.voting_period = 3;
+        proposal.parameters.approval_quorum_percentage = 60;
+        proposal.parameters.slashing_quorum_percentage = 50;
+        proposal.parameters.slashing_threshold_percentage = 50;
+
+        proposal.voting_results.add_vote(VoteKind::Reject);
+        proposal.voting_results.add_vote(VoteKind::Approve);
+        proposal.voting_results.add_vote(VoteKind::Approve);
+        proposal.voting_results.add_vote(VoteKind::Approve);
+
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 0,
+                approvals: 3,
+                rejections: 1,
+                slashes: 0,
+            }
+        );
+
+        let expected_proposal_status = proposal.define_proposal_decision_status(5, now);
+        assert_eq!(
+            expected_proposal_status,
+            Some(ProposalDecisionStatus::Approved(
+                ApprovedProposalStatus::PendingExecution
+            ))
+        );
+    }
+
+    #[test]
+    fn define_proposal_decision_status_returns_rejected() {
+        let mut proposal = ProposalObject::default();
+        let now = 2;
+
+        proposal.created_at = 1;
+        proposal.parameters.voting_period = 3;
+        proposal.parameters.approval_quorum_percentage = 50;
+        proposal.parameters.approval_threshold_percentage = 51;
+        proposal.parameters.slashing_quorum_percentage = 50;
+        proposal.parameters.slashing_threshold_percentage = 50;
+
+        proposal.voting_results.add_vote(VoteKind::Reject);
+        proposal.voting_results.add_vote(VoteKind::Reject);
+        proposal.voting_results.add_vote(VoteKind::Abstain);
+        proposal.voting_results.add_vote(VoteKind::Approve);
+
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 1,
+                approvals: 1,
+                rejections: 2,
+                slashes: 0,
+            }
+        );
+
+        let expected_proposal_status = proposal.define_proposal_decision_status(4, now);
+        assert_eq!(
+            expected_proposal_status,
+            Some(ProposalDecisionStatus::Rejected)
+        );
+    }
+
+    #[test]
+    fn define_proposal_decision_status_returns_slashed() {
+        let mut proposal = ProposalObject::default();
+        let now = 2;
+
+        proposal.created_at = 1;
+        proposal.parameters.voting_period = 3;
+        proposal.parameters.approval_quorum_percentage = 50;
+        proposal.parameters.approval_threshold_percentage = 50;
+        proposal.parameters.slashing_quorum_percentage = 50;
+        proposal.parameters.slashing_threshold_percentage = 50;
+
+        proposal.voting_results.add_vote(VoteKind::Slash);
+        proposal.voting_results.add_vote(VoteKind::Reject);
+        proposal.voting_results.add_vote(VoteKind::Abstain);
+        proposal.voting_results.add_vote(VoteKind::Slash);
+
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 1,
+                approvals: 0,
+                rejections: 1,
+                slashes: 2,
+            }
+        );
+
+        let expected_proposal_status = proposal.define_proposal_decision_status(4, now);
+        assert_eq!(
+            expected_proposal_status,
+            Some(ProposalDecisionStatus::Slashed)
+        );
+    }
+
+    #[test]
+    fn define_proposal_decision_status_returns_none() {
+        let mut proposal = ProposalObject::default();
+        let now = 2;
+
+        proposal.created_at = 1;
+        proposal.parameters.voting_period = 3;
+        proposal.parameters.approval_quorum_percentage = 60;
+        proposal.parameters.slashing_quorum_percentage = 50;
+
+        proposal.voting_results.add_vote(VoteKind::Abstain);
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 1,
+                approvals: 0,
+                rejections: 0,
+                slashes: 0,
+            }
+        );
+
+        let expected_proposal_status = proposal.define_proposal_decision_status(5, now);
+        assert_eq!(expected_proposal_status, None);
+    }
+
+    #[test]
+    fn define_proposal_decision_status_returns_approved_before_slashing_before_rejection() {
+        let mut proposal = ProposalObject::default();
+        let now = 2;
+
+        proposal.created_at = 1;
+        proposal.parameters.voting_period = 3;
+        proposal.parameters.approval_quorum_percentage = 50;
+        proposal.parameters.approval_threshold_percentage = 30;
+        proposal.parameters.slashing_quorum_percentage = 50;
+        proposal.parameters.slashing_threshold_percentage = 30;
+
+        proposal.voting_results.add_vote(VoteKind::Approve);
+        proposal.voting_results.add_vote(VoteKind::Approve);
+        proposal.voting_results.add_vote(VoteKind::Reject);
+        proposal.voting_results.add_vote(VoteKind::Reject);
+        proposal.voting_results.add_vote(VoteKind::Slash);
+        proposal.voting_results.add_vote(VoteKind::Slash);
+
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 0,
+                approvals: 2,
+                rejections: 2,
+                slashes: 2,
+            }
+        );
+
+        let expected_proposal_status = proposal.define_proposal_decision_status(6, now);
+
+        assert_eq!(
+            expected_proposal_status,
+            Some(ProposalDecisionStatus::Approved(
+                ApprovedProposalStatus::PendingExecution
+            ))
+        );
+    }
+
+    #[test]
+    fn define_proposal_decision_status_returns_slashed_before_rejection() {
+        let mut proposal = ProposalObject::default();
+        let now = 2;
+
+        proposal.created_at = 1;
+        proposal.parameters.voting_period = 3;
+        proposal.parameters.approval_quorum_percentage = 50;
+        proposal.parameters.approval_threshold_percentage = 30;
+        proposal.parameters.slashing_quorum_percentage = 50;
+        proposal.parameters.slashing_threshold_percentage = 30;
+
+        proposal.voting_results.add_vote(VoteKind::Abstain);
+        proposal.voting_results.add_vote(VoteKind::Approve);
+        proposal.voting_results.add_vote(VoteKind::Reject);
+        proposal.voting_results.add_vote(VoteKind::Reject);
+        proposal.voting_results.add_vote(VoteKind::Slash);
+        proposal.voting_results.add_vote(VoteKind::Slash);
+
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 1,
+                approvals: 1,
+                rejections: 2,
+                slashes: 2,
+            }
+        );
+
+        let expected_proposal_status = proposal.define_proposal_decision_status(6, now);
+
+        assert_eq!(
+            expected_proposal_status,
+            Some(ProposalDecisionStatus::Slashed)
+        );
+    }
+
+    #[test]
+    fn proposal_status_resolution_approval_quorum_works_correctly() {
+        let no_approval_quorum_proposal: Proposal<u64, u64, u64, u64, u64> = Proposal {
+            parameters: ProposalParameters {
+                approval_quorum_percentage: 63,
+                slashing_threshold_percentage: 63,
+                ..ProposalParameters::default()
+            },
+            ..Proposal::default()
+        };
+        let no_approval_proposal_status_resolution = ProposalStatusResolution {
+            proposal: &no_approval_quorum_proposal,
+            now: 20,
+            votes_count: 314,
+            total_voters_count: 500,
+            approvals: 3,
+            slashes: 3,
+        };
+
+        assert!(!no_approval_proposal_status_resolution.is_approval_quorum_reached());
+
+        let approval_quorum_proposal_status_resolution = ProposalStatusResolution {
+            votes_count: 315,
+            ..no_approval_proposal_status_resolution
+        };
+
+        assert!(approval_quorum_proposal_status_resolution.is_approval_quorum_reached());
+    }
+
+    #[test]
+    fn proposal_status_resolution_slashing_quorum_works_correctly() {
+        let no_slashing_quorum_proposal: Proposal<u64, u64, u64, u64, u64> = Proposal {
+            parameters: ProposalParameters {
+                approval_quorum_percentage: 63,
+                slashing_quorum_percentage: 63,
+                ..ProposalParameters::default()
+            },
+            ..Proposal::default()
+        };
+        let no_slashing_proposal_status_resolution = ProposalStatusResolution {
+            proposal: &no_slashing_quorum_proposal,
+            now: 20,
+            votes_count: 314,
+            total_voters_count: 500,
+            approvals: 3,
+            slashes: 3,
+        };
+
+        assert!(!no_slashing_proposal_status_resolution.is_slashing_quorum_reached());
+
+        let slashing_quorum_proposal_status_resolution = ProposalStatusResolution {
+            votes_count: 315,
+            ..no_slashing_proposal_status_resolution
+        };
+
+        assert!(slashing_quorum_proposal_status_resolution.is_slashing_quorum_reached());
+    }
+
+    #[test]
+    fn proposal_status_resolution_approval_threshold_works_correctly() {
+        let no_approval_threshold_proposal: Proposal<u64, u64, u64, u64, u64> = Proposal {
+            parameters: ProposalParameters {
+                slashing_threshold_percentage: 63,
+                approval_threshold_percentage: 63,
+                ..ProposalParameters::default()
+            },
+            ..Proposal::default()
+        };
+        let no_approval_proposal_status_resolution = ProposalStatusResolution {
+            proposal: &no_approval_threshold_proposal,
+            now: 20,
+            votes_count: 500,
+            total_voters_count: 600,
+            approvals: 314,
+            slashes: 3,
+        };
+
+        assert!(!no_approval_proposal_status_resolution.is_approval_threshold_reached());
+
+        let approval_threshold_proposal_status_resolution = ProposalStatusResolution {
+            approvals: 315,
+            ..no_approval_proposal_status_resolution
+        };
+
+        assert!(approval_threshold_proposal_status_resolution.is_approval_threshold_reached());
+    }
+
+    #[test]
+    fn proposal_status_resolution_slashing_threshold_works_correctly() {
+        let no_slashing_threshold_proposal: Proposal<u64, u64, u64, u64, u64> = Proposal {
+            parameters: ProposalParameters {
+                slashing_threshold_percentage: 63,
+                approval_threshold_percentage: 63,
+                ..ProposalParameters::default()
+            },
+            ..Proposal::default()
+        };
+        let no_slashing_proposal_status_resolution = ProposalStatusResolution {
+            proposal: &no_slashing_threshold_proposal,
+            now: 20,
+            votes_count: 500,
+            total_voters_count: 600,
+            approvals: 3,
+            slashes: 314,
+        };
+
+        assert!(!no_slashing_proposal_status_resolution.is_slashing_threshold_reached());
+
+        let slashing_threshold_proposal_status_resolution = ProposalStatusResolution {
+            slashes: 315,
+            ..no_slashing_proposal_status_resolution
+        };
+
+        assert!(slashing_threshold_proposal_status_resolution.is_slashing_threshold_reached());
+    }
+}

+ 197 - 0
runtime-modules/proposals/engine/src/types/proposal_statuses.rs

@@ -0,0 +1,197 @@
+#![warn(missing_docs)]
+
+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<BlockNumber, StakeId, AccountId> {
+    /// A new proposal status that is available for voting (with optional stake data).
+    Active(Option<ActiveStake<StakeId, AccountId>>),
+
+    /// The proposal decision was made.
+    Finalized(FinalizationData<BlockNumber, StakeId, AccountId>),
+}
+
+impl<BlockNumber, StakeId, AccountId> Default for ProposalStatus<BlockNumber, StakeId, AccountId> {
+    fn default() -> Self {
+        ProposalStatus::Active(None)
+    }
+}
+
+impl<BlockNumber, StakeId, AccountId> ProposalStatus<BlockNumber, StakeId, AccountId> {
+    /// Creates finalized proposal status with provided ProposalDecisionStatus
+    pub fn finalized_successfully(
+        decision_status: ProposalDecisionStatus,
+        now: BlockNumber,
+    ) -> ProposalStatus<BlockNumber, StakeId, AccountId> {
+        Self::finalized(decision_status, None, None, now)
+    }
+
+    /// Creates finalized proposal status with provided ProposalDecisionStatus and error
+    pub fn finalized(
+        decision_status: ProposalDecisionStatus,
+        encoded_unstaking_error_due_to_broken_runtime: Option<&str>,
+        active_stake: Option<ActiveStake<StakeId, AccountId>>,
+        now: BlockNumber,
+    ) -> ProposalStatus<BlockNumber, StakeId, AccountId> {
+        // drop the stake information if there were no errors on unstaking
+        let actual_stake = if encoded_unstaking_error_due_to_broken_runtime.is_some() {
+            active_stake
+        } else {
+            None
+        };
+        ProposalStatus::Finalized(FinalizationData {
+            proposal_status: decision_status,
+            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: actual_stake,
+        })
+    }
+
+    /// Creates finalized and approved proposal status with provided ApprovedProposalStatus
+    pub fn approved(
+        approved_status: ApprovedProposalStatus,
+        now: BlockNumber,
+    ) -> ProposalStatus<BlockNumber, StakeId, AccountId> {
+        ProposalStatus::Finalized(FinalizationData {
+            proposal_status: ProposalDecisionStatus::Approved(approved_status),
+            encoded_unstaking_error_due_to_broken_runtime: None,
+            finalized_at: now,
+            stake_data_after_unstaking_error: None,
+        })
+    }
+}
+
+/// Final proposal status and potential error.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
+pub struct FinalizationData<BlockNumber, StakeId, AccountId> {
+    /// Final proposal status
+    pub proposal_status: ProposalDecisionStatus,
+
+    /// Proposal finalization block number
+    pub finalized_at: BlockNumber,
+
+    /// Error occured during the proposal finalization - unstaking failed in the stake module
+    pub encoded_unstaking_error_due_to_broken_runtime: Option<Vec<u8>>,
+
+    /// Stake data for the proposal, filled if the unstaking wasn't successful
+    pub stake_data_after_unstaking_error: Option<ActiveStake<StakeId, AccountId>>,
+}
+
+impl<BlockNumber, StakeId, AccountId> FinalizationData<BlockNumber, StakeId, AccountId> {
+    /// FinalizationData helper, creates ApprovedProposalStatus
+    pub fn create_approved_proposal_status(
+        self,
+        approved_status: ApprovedProposalStatus,
+    ) -> ProposalStatus<BlockNumber, StakeId, AccountId> {
+        ProposalStatus::Finalized(FinalizationData {
+            proposal_status: ProposalDecisionStatus::Approved(approved_status),
+            ..self
+        })
+    }
+}
+
+/// Status of the approved proposal. Defines execution stages.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
+pub enum ApprovedProposalStatus {
+    /// A proposal was approved and grace period is in effect
+    PendingExecution,
+
+    /// Proposal was successfully executed
+    Executed,
+
+    /// Proposal was executed and failed with an error
+    ExecutionFailed {
+        /// Error message
+        error: Vec<u8>,
+    },
+}
+
+impl ApprovedProposalStatus {
+    /// ApprovedProposalStatus helper, creates ExecutionFailed approved proposal status
+    pub fn failed_execution(err: &str) -> ApprovedProposalStatus {
+        ApprovedProposalStatus::ExecutionFailed {
+            error: err.as_bytes().to_vec(),
+        }
+    }
+}
+
+/// Status for the proposal with finalized decision
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
+pub enum ProposalDecisionStatus {
+    /// Proposal was withdrawn by its proposer.
+    Canceled,
+
+    /// Proposal was vetoed by root.
+    Vetoed,
+
+    /// A proposal was rejected
+    Rejected,
+
+    /// A proposal was rejected ans its stake should be slashed
+    Slashed,
+
+    /// 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(ApprovedProposalStatus),
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::{
+        ActiveStake, ApprovedProposalStatus, FinalizationData, ProposalDecisionStatus,
+        ProposalStatus,
+    };
+
+    #[test]
+    fn approved_proposal_status_helper_succeeds() {
+        let msg = "error";
+
+        assert_eq!(
+            ApprovedProposalStatus::failed_execution(&msg),
+            ApprovedProposalStatus::ExecutionFailed {
+                error: msg.as_bytes().to_vec()
+            }
+        );
+    }
+
+    #[test]
+    fn finalized_proposal_status_helper_succeeds() {
+        let msg = "error";
+        let block_number = 20;
+        let stake = ActiveStake {
+            stake_id: 50,
+            source_account_id: 2,
+        };
+
+        let proposal_status = ProposalStatus::finalized(
+            ProposalDecisionStatus::Slashed,
+            Some(msg),
+            Some(stake),
+            block_number,
+        );
+
+        assert_eq!(
+            ProposalStatus::Finalized(FinalizationData {
+                proposal_status: ProposalDecisionStatus::Slashed,
+                finalized_at: block_number,
+                encoded_unstaking_error_due_to_broken_runtime: Some(msg.as_bytes().to_vec()),
+                stake_data_after_unstaking_error: Some(stake)
+            }),
+            proposal_status
+        );
+    }
+}

+ 247 - 0
runtime-modules/proposals/engine/src/types/stakes.rs

@@ -0,0 +1,247 @@
+#![warn(missing_docs)]
+
+use super::{BalanceOf, CurrencyOf, NegativeImbalance};
+use crate::Trait;
+use rstd::convert::From;
+use rstd::marker::PhantomData;
+use rstd::rc::Rc;
+use srml_support::traits::{Currency, ExistenceRequirement, WithdrawReasons};
+
+// Mocking dependencies for testing
+#[cfg(test)]
+use mockall::predicate::*;
+#[cfg(test)]
+use mockall::*;
+
+/// Returns registered stake handler. This is scaffolds for the mocking of the stake module.
+pub trait StakeHandlerProvider<T: Trait> {
+    /// Returns stake logic handler
+    fn stakes() -> Rc<dyn StakeHandler<T>>;
+}
+
+/// Default implementation of the stake module logic provider. Returns actual implementation
+/// dependent on the stake module.
+pub struct DefaultStakeHandlerProvider;
+impl<T: Trait> StakeHandlerProvider<T> for DefaultStakeHandlerProvider {
+    /// Returns stake logic handler
+    fn stakes() -> Rc<dyn StakeHandler<T>> {
+        Rc::new(DefaultStakeHandler {
+            marker: PhantomData::<T>::default(),
+        })
+    }
+}
+
+/// Stake logic handler.
+#[cfg_attr(test, automock)] // attributes creates mocks in testing environment
+pub trait StakeHandler<T: Trait> {
+    /// Creates a stake. Returns created stake id or an error.
+    fn create_stake(&self) -> Result<T::StakeId, &'static str>;
+
+    /// Stake the imbalance
+    fn stake(
+        &self,
+        stake_id: &T::StakeId,
+        stake_imbalance: NegativeImbalance<T>,
+    ) -> Result<(), &'static str>;
+
+    /// Removes stake
+    fn remove_stake(&self, stake_id: T::StakeId) -> Result<(), &'static str>;
+
+    /// Execute unstaking
+    fn unstake(&self, stake_id: T::StakeId) -> Result<(), &'static str>;
+
+    /// Slash balance from the existing stake
+    fn slash(&self, stake_id: T::StakeId, slash_balance: BalanceOf<T>) -> Result<(), &'static str>;
+
+    /// Withdraw some balance from the source account and create stake imbalance
+    fn make_stake_imbalance(
+        &self,
+        balance: BalanceOf<T>,
+        source_account_id: &T::AccountId,
+    ) -> Result<NegativeImbalance<T>, &'static str>;
+}
+
+/// Default implementation of the stake logic. Uses actual stake module.
+/// 'marker' responsible for the 'Trait' binding.
+pub(crate) struct DefaultStakeHandler<T> {
+    pub marker: PhantomData<T>,
+}
+
+impl<T: Trait> StakeHandler<T> for DefaultStakeHandler<T> {
+    /// Creates a stake. Returns created stake id or an error.
+    fn create_stake(&self) -> Result<<T as stake::Trait>::StakeId, &'static str> {
+        Ok(stake::Module::<T>::create_stake())
+    }
+
+    /// Stake the imbalance
+    fn stake(
+        &self,
+        stake_id: &<T as stake::Trait>::StakeId,
+        stake_imbalance: NegativeImbalance<T>,
+    ) -> Result<(), &'static str> {
+        stake::Module::<T>::stake(&stake_id, stake_imbalance).map_err(WrappedError)?;
+
+        Ok(())
+    }
+
+    /// Removes stake
+    fn remove_stake(&self, stake_id: <T as stake::Trait>::StakeId) -> Result<(), &'static str> {
+        stake::Module::<T>::remove_stake(&stake_id).map_err(WrappedError)?;
+
+        Ok(())
+    }
+
+    /// Execute unstaking
+    fn unstake(&self, stake_id: <T as stake::Trait>::StakeId) -> Result<(), &'static str> {
+        stake::Module::<T>::initiate_unstaking(&stake_id, None).map_err(WrappedError)?;
+
+        Ok(())
+    }
+
+    /// Slash balance from the existing stake
+    fn slash(
+        &self,
+        stake_id: <T as stake::Trait>::StakeId,
+        slash_balance: BalanceOf<T>,
+    ) -> Result<(), &'static str> {
+        let _ignored_successful_result =
+            stake::Module::<T>::slash_immediate(&stake_id, slash_balance, false)
+                .map_err(WrappedError)?;
+
+        Ok(())
+    }
+
+    /// Withdraw some balance from the source account and create stake imbalance
+    fn make_stake_imbalance(
+        &self,
+        balance: BalanceOf<T>,
+        source_account_id: &T::AccountId,
+    ) -> Result<NegativeImbalance<T>, &'static str> {
+        CurrencyOf::<T>::withdraw(
+            source_account_id,
+            balance,
+            WithdrawReasons::all(),
+            ExistenceRequirement::AllowDeath,
+        )
+    }
+}
+
+/// Proposal implementation of the stake logic.
+/// 'marker' responsible for the 'Trait' binding.
+pub(crate) struct ProposalStakeManager<T> {
+    pub marker: PhantomData<T>,
+}
+
+impl<T: Trait> ProposalStakeManager<T> {
+    /// Creates a stake using stake balance and source account.
+    /// Returns created stake id or an error.
+    pub fn create_stake(
+        stake_balance: BalanceOf<T>,
+        source_account_id: T::AccountId,
+    ) -> Result<T::StakeId, &'static str> {
+        let stake_id = T::StakeHandlerProvider::stakes().create_stake()?;
+
+        let stake_imbalance = T::StakeHandlerProvider::stakes()
+            .make_stake_imbalance(stake_balance, &source_account_id)?;
+
+        T::StakeHandlerProvider::stakes().stake(&stake_id, stake_imbalance)?;
+
+        Ok(stake_id)
+    }
+
+    /// Execute unstaking and removes the stake
+    pub fn remove_stake(stake_id: T::StakeId) -> Result<(), &'static str> {
+        T::StakeHandlerProvider::stakes().unstake(stake_id)?;
+
+        T::StakeHandlerProvider::stakes().remove_stake(stake_id)?;
+
+        Ok(())
+    }
+
+    /// Slash balance from the existing stake
+    pub fn slash(stake_id: T::StakeId, slash_balance: BalanceOf<T>) -> Result<(), &'static str> {
+        T::StakeHandlerProvider::stakes().slash(stake_id, slash_balance)
+    }
+}
+
+// 'New type' pattern for the error
+// https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#using-the-newtype-pattern-to-implement-external-traits-on-external-types
+struct WrappedError<E>(E);
+
+// error conversion for the Wrapped StakeActionError with the inner InitiateUnstakingError
+impl From<WrappedError<stake::StakeActionError<stake::InitiateUnstakingError>>> for &str {
+    fn from(wrapper: WrappedError<stake::StakeActionError<stake::InitiateUnstakingError>>) -> Self {
+        {
+            match wrapper.0 {
+                stake::StakeActionError::StakeNotFound => "StakeNotFound",
+                stake::StakeActionError::Error(err) => match err {
+                    stake::InitiateUnstakingError::UnstakingPeriodShouldBeGreaterThanZero => {
+                        "UnstakingPeriodShouldBeGreaterThanZero"
+                    }
+                    stake::InitiateUnstakingError::UnstakingError(e) => match e {
+                        stake::UnstakingError::NotStaked => "NotStaked",
+                        stake::UnstakingError::AlreadyUnstaking => "AlreadyUnstaking",
+                        stake::UnstakingError::CannotUnstakeWhileSlashesOngoing => {
+                            "CannotUnstakeWhileSlashesOngoing"
+                        }
+                    },
+                },
+            }
+        }
+    }
+}
+
+// error conversion for the Wrapped StakeActionError with the inner StakingError
+impl From<WrappedError<stake::StakeActionError<stake::StakingError>>> for &str {
+    fn from(wrapper: WrappedError<stake::StakeActionError<stake::StakingError>>) -> Self {
+        {
+            match wrapper.0 {
+                stake::StakeActionError::StakeNotFound => "StakeNotFound",
+                stake::StakeActionError::Error(err) => match err {
+                    stake::StakingError::CannotStakeZero => "CannotStakeZero",
+                    stake::StakingError::CannotStakeLessThanMinimumBalance => {
+                        "CannotStakeLessThanMinimumBalance"
+                    }
+                    stake::StakingError::AlreadyStaked => "AlreadyStaked",
+                },
+            }
+        }
+    }
+}
+
+// error conversion for the Wrapped StakeActionError with the inner InitiateSlashingError
+impl From<WrappedError<stake::StakeActionError<stake::InitiateSlashingError>>> for &str {
+    fn from(wrapper: WrappedError<stake::StakeActionError<stake::InitiateSlashingError>>) -> Self {
+        {
+            match wrapper.0 {
+                stake::StakeActionError::StakeNotFound => "StakeNotFound",
+                stake::StakeActionError::Error(err) => match err {
+                    stake::InitiateSlashingError::NotStaked => "NotStaked",
+                    stake::InitiateSlashingError::SlashPeriodShouldBeGreaterThanZero => {
+                        "SlashPeriodShouldBeGreaterThanZero"
+                    }
+                    stake::InitiateSlashingError::SlashAmountShouldBeGreaterThanZero => {
+                        "SlashAmountShouldBeGreaterThanZero"
+                    }
+                },
+            }
+        }
+    }
+}
+
+// error conversion for the Wrapped StakeActionError with the inner ImmediateSlashingError
+impl From<WrappedError<stake::StakeActionError<stake::ImmediateSlashingError>>> for &str {
+    fn from(wrapper: WrappedError<stake::StakeActionError<stake::ImmediateSlashingError>>) -> Self {
+        {
+            match wrapper.0 {
+                stake::StakeActionError::StakeNotFound => "StakeNotFound",
+                stake::StakeActionError::Error(err) => match err {
+                    stake::ImmediateSlashingError::NotStaked => "NotStaked",
+                    stake::ImmediateSlashingError::SlashAmountShouldBeGreaterThanZero => {
+                        "SlashAmountShouldBeGreaterThanZero"
+                    }
+                },
+            }
+        }
+    }
+}

+ 1 - 1
runtime-modules/roles/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = 'substrate-roles-module'
-version = '1.0.0'
+version = '1.0.1'
 authors = ['Joystream contributors']
 edition = '2018'
 

+ 4 - 0
runtime-modules/roles/src/actors.rs

@@ -8,10 +8,14 @@ use srml_support::traits::{
 use srml_support::{decl_event, decl_module, decl_storage, ensure};
 use system::{self, ensure_root, ensure_signed};
 
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+
 pub use membership::members::Role;
 
 const STAKING_ID: LockIdentifier = *b"role_stk";
 
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Copy, Clone, Eq, PartialEq, Debug)]
 pub struct RoleParameters<Balance, BlockNumber> {
     // minium balance required to stake to enter a role

+ 18 - 0
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'

+ 1 - 0
runtime/src/integration/mod.rs

@@ -0,0 +1 @@
+pub mod proposals;

+ 14 - 0
runtime/src/integration/proposals/council_elected_handler.rs

@@ -0,0 +1,14 @@
+#![warn(missing_docs)]
+
+use crate::Runtime;
+use governance::election::CouncilElected;
+
+/// 'Council elected' event handler. Should be applied to the 'election' substrate module.
+/// CouncilEvent is handled by resetting active proposals.
+pub struct CouncilElectedHandler;
+
+impl<Elected, Term> CouncilElected<Elected, Term> for CouncilElectedHandler {
+    fn council_elected(_new_council: Elected, _term: Term) {
+        <proposals_engine::Module<Runtime>>::reset_active_proposals();
+    }
+}

+ 208 - 0
runtime/src/integration/proposals/council_origin_validator.rs

@@ -0,0 +1,208 @@
+#![warn(missing_docs)]
+
+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<T> {
+    marker: PhantomData<T>,
+}
+
+impl<T: governance::council::Trait + membership::members::Trait>
+    ActorOriginValidator<<T as system::Trait>::Origin, MemberId<T>, <T as system::Trait>::AccountId>
+    for CouncilManager<T>
+{
+    /// 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: <T as system::Trait>::Origin,
+        actor_id: MemberId<T>,
+    ) -> Result<<T as system::Trait>::AccountId, &'static str> {
+        let account_id = <MembershipOriginValidator<T>>::ensure_actor_origin(origin, actor_id)?;
+
+        if <governance::council::Module<T>>::is_councilor(&account_id) {
+            return Ok(account_id);
+        }
+
+        Err("Council validation failed: account id doesn't belong to a council member")
+    }
+}
+
+impl<T: governance::council::Trait> VotersParameters for CouncilManager<T> {
+    /// Implement total_voters_count() as council size
+    fn total_voters_count() -> u32 {
+        <governance::council::Module<T>>::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<Runtime>;
+
+    fn initial_test_ext() -> runtime_io::TestExternalities {
+        let t = system::GenesisConfig::default()
+            .build_storage::<Runtime>()
+            .unwrap();
+
+        t.into()
+    }
+
+    type Membership = membership::members::Module<Runtime>;
+
+    #[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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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::<Runtime>::total_voters_count(), 4)
+        });
+    }
+}

+ 143 - 0
runtime/src/integration/proposals/membership_origin_validator.rs

@@ -0,0 +1,143 @@
+#![warn(missing_docs)]
+
+use rstd::marker::PhantomData;
+
+use common::origin_validator::ActorOriginValidator;
+use system::ensure_signed;
+
+/// Member of the Joystream organization
+pub type MemberId<T> = <T as crate::members::Trait>::MemberId;
+
+/// Default membership actor origin validator.
+pub struct MembershipOriginValidator<T> {
+    marker: PhantomData<T>,
+}
+
+impl<T: crate::members::Trait>
+    ActorOriginValidator<<T as system::Trait>::Origin, MemberId<T>, <T as system::Trait>::AccountId>
+    for MembershipOriginValidator<T>
+{
+    /// 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: <T as system::Trait>::Origin,
+        actor_id: MemberId<T>,
+    ) -> Result<<T as system::Trait>::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 = <crate::members::Module<T>>::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<Runtime>;
+
+    fn initial_test_ext() -> runtime_io::TestExternalities {
+        let t = system::GenesisConfig::default()
+            .build_storage::<Runtime>()
+            .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::<Runtime>::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::<Runtime>::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::<Runtime>::ensure_actor_origin(
+                RawOrigin::Signed(invalid_account_id.into()).into(),
+                member_id,
+            );
+
+            assert_eq!(validation_result, Err(error));
+        });
+    }
+}

+ 11 - 0
runtime/src/integration/proposals/mod.rs

@@ -0,0 +1,11 @@
+#![warn(missing_docs)]
+
+mod council_elected_handler;
+mod council_origin_validator;
+mod membership_origin_validator;
+mod staking_events_handler;
+
+pub use council_elected_handler::CouncilElectedHandler;
+pub use council_origin_validator::CouncilManager;
+pub use membership_origin_validator::{MemberId, MembershipOriginValidator};
+pub use staking_events_handler::StakingEventsHandler;

+ 49 - 0
runtime/src/integration/proposals/staking_events_handler.rs

@@ -0,0 +1,49 @@
+#![warn(missing_docs)]
+
+use rstd::marker::PhantomData;
+use srml_support::traits::{Currency, Imbalance};
+use srml_support::StorageMap;
+
+// Balance alias
+type BalanceOf<T> =
+    <<T as stake::Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::Balance;
+
+// Balance alias for staking
+type NegativeImbalance<T> =
+    <<T as stake::Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::NegativeImbalance;
+
+/// Proposal implementation of the staking event handler from the stake module.
+/// 'marker' responsible for the 'Trait' binding.
+pub struct StakingEventsHandler<T> {
+    pub marker: PhantomData<T>,
+}
+
+impl<T: stake::Trait + proposals_engine::Trait> stake::StakingEventsHandler<T>
+    for StakingEventsHandler<T>
+{
+    /// Unstake remaining sum back to the source_account_id
+    fn unstaked(
+        id: &<T as stake::Trait>::StakeId,
+        _unstaked_amount: BalanceOf<T>,
+        remaining_imbalance: NegativeImbalance<T>,
+    ) -> NegativeImbalance<T> {
+        if <proposals_engine::StakesProposals<T>>::exists(id) {
+            <proposals_engine::Module<T>>::refund_proposal_stake(*id, remaining_imbalance);
+
+            return <NegativeImbalance<T>>::zero(); // imbalance was consumed
+        }
+
+        remaining_imbalance
+    }
+
+    /// Empty handler for slashing
+    fn slashed(
+        _: &<T as stake::Trait>::StakeId,
+        _: Option<<T as stake::Trait>::SlashId>,
+        _: BalanceOf<T>,
+        _: BalanceOf<T>,
+        remaining_imbalance: NegativeImbalance<T>,
+    ) -> NegativeImbalance<T> {
+        remaining_imbalance
+    }
+}

+ 79 - 9
runtime/src/lib.rs

@@ -1,8 +1,14 @@
-//! The Substrate Node Template runtime. This can be compiled with `#[no_std]`, ready for Wasm.
+//! The Joystream Substrate Node runtime.
 
 #![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)]
+
+// Runtime integration tests
+mod test;
 
 // Make the WASM binary available.
 // This is required only by the node build.
@@ -10,6 +16,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 +59,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;
 
@@ -397,7 +407,7 @@ impl finality_tracker::Trait for Runtime {
 
 pub use forum;
 pub use governance::election_params::ElectionParameters;
-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;
@@ -578,7 +588,10 @@ parameter_types! {
 impl stake::Trait for Runtime {
     type Currency = <Self as common::currency::GovernanceCurrency>::Currency;
     type StakePoolId = StakePoolId;
-    type StakingEventsHandler = ContentWorkingGroupStakingEventHandler;
+    type StakingEventsHandler = (
+        ContentWorkingGroupStakingEventHandler,
+        crate::integration::proposals::StakingEventsHandler<Self>,
+    );
     type StakeId = u64;
     type SlashId = u64;
 }
@@ -658,13 +671,9 @@ impl common::currency::GovernanceCurrency for Runtime {
     type Currency = balances::Module<Self>;
 }
 
-impl governance::proposals::Trait for Runtime {
-    type Event = Event;
-}
-
 impl governance::election::Trait for Runtime {
     type Event = Event;
-    type CouncilElected = (Council,);
+    type CouncilElected = (Council, integration::proposals::CouncilElectedHandler);
 }
 
 impl governance::council::Trait for Runtime {
@@ -802,6 +811,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 = 40;
+    pub const ProposalDescriptionMaxLength: u32 = 3000;
+    pub const ProposalMaxActiveProposalLimit: u32 = 5;
+}
+
+impl proposals_engine::Trait for Runtime {
+    type Event = Event;
+    type ProposerOriginValidator = MembershipOriginValidator<Self>;
+    type VoterOriginValidator = CouncilManager<Self>;
+    type TotalVotersCounter = CouncilManager<Self>;
+    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 = 0; // post update is disabled
+    pub const ProposalMaxThreadInARowNumber: u32 = 100000; // will not be used
+    pub const ProposalThreadTitleLengthLimit: u32 = 40;
+    pub const ProposalPostLengthLimit: u32 = 1000;
+}
+
+impl proposals_discussion::Trait for Runtime {
+    type Event = Event;
+    type PostAuthorOriginValidator = MembershipOriginValidator<Self>;
+    type ThreadId = u32;
+    type PostId = u32;
+    type MaxPostEditionNumber = ProposalMaxPostEditionNumber;
+    type ThreadTitleLengthLimit = ProposalThreadTitleLengthLimit;
+    type PostLengthLimit = ProposalPostLengthLimit;
+    type MaxThreadInARowNumber = ProposalMaxThreadInARowNumber;
+}
+
+parameter_types! {
+    pub const TextProposalMaxLength: u32 = 5_000;
+    pub const RuntimeUpgradeWasmProposalMaxLength: u32 = 2_000_000;
+}
+
+impl proposals_codex::Trait for Runtime {
+    type MembershipOriginValidator = MembershipOriginValidator<Self>;
+    type TextProposalMaxLength = TextProposalMaxLength;
+    type RuntimeUpgradeWasmProposalMaxLength = RuntimeUpgradeWasmProposalMaxLength;
+}
+
 construct_runtime!(
 	pub enum Runtime where
 		Block = Block,
@@ -826,7 +892,6 @@ construct_runtime!(
         RandomnessCollectiveFlip: randomness_collective_flip::{Module, Call, Storage},
         Sudo: sudo,
         // Joystream
-        Proposals: proposals::{Module, Call, Storage, Event<T>, Config<T>},
         CouncilElection: election::{Module, Call, Storage, Event<T>, Config<T>},
         Council: council::{Module, Call, Storage, Event<T>, Config<T>},
         Memo: memo::{Module, Call, Storage, Event<T>},
@@ -845,6 +910,11 @@ construct_runtime!(
         RecurringRewards: recurringrewards::{Module, Call, Storage},
         Hiring: hiring::{Module, Call, Storage},
         ContentWorkingGroup: content_wg::{Module, Call, Storage, Event<T>, Config<T>},
+        // --- Proposals
+        ProposalsEngine: proposals_engine::{Module, Call, Storage, Event<T>},
+        ProposalsDiscussion: proposals_discussion::{Module, Call, Storage, Event<T>},
+        ProposalsCodex: proposals_codex::{Module, Call, Storage, Error},
+        // ---
 	}
 );
 

+ 5 - 0
runtime/src/test/mod.rs

@@ -0,0 +1,5 @@
+//! The Joystream Substrate Node runtime integration tests.
+
+#![cfg(test)]
+
+mod proposals_integration;

+ 367 - 0
runtime/src/test/proposals_integration.rs

@@ -0,0 +1,367 @@
+//! Proposals integration tests - with stake, membership, governance modules.
+
+#![cfg(test)]
+
+use crate::{ProposalCancellationFee, Runtime};
+use codec::Encode;
+use governance::election::CouncilElected;
+use membership::members;
+use proposals_engine::{
+    ActiveStake, BalanceOf, Error, FinalizationData, Proposal, ProposalDecisionStatus,
+    ProposalParameters, ProposalStatus, VoteKind, VotersParameters, VotingResults,
+};
+use sr_primitives::traits::DispatchResult;
+use sr_primitives::AccountId32;
+use srml_support::traits::Currency;
+use srml_support::StorageLinkedMap;
+use system::RawOrigin;
+
+use crate::CouncilManager;
+
+fn initial_test_ext() -> runtime_io::TestExternalities {
+    let t = system::GenesisConfig::default()
+        .build_storage::<Runtime>()
+        .unwrap();
+
+    t.into()
+}
+
+type Membership = membership::members::Module<Runtime>;
+type ProposalsEngine = proposals_engine::Module<Runtime>;
+type Council = governance::council::Module<Runtime>;
+
+fn setup_members(count: u8) {
+    let authority_account_id = <Runtime as system::Trait>::AccountId::default();
+    Membership::set_screening_authority(RawOrigin::Root.into(), authority_account_id.clone())
+        .unwrap();
+
+    for i in 0..count {
+        let account_id: [u8; 32] = [i; 32];
+        Membership::add_screened_member(
+            RawOrigin::Signed(authority_account_id.clone().into()).into(),
+            account_id.clone().into(),
+            members::UserInfo {
+                handle: Some(account_id.to_vec()),
+                avatar_uri: None,
+                about: None,
+            },
+        )
+        .unwrap();
+    }
+}
+
+fn setup_council() {
+    let councilor0 = AccountId32::default();
+    let councilor1: [u8; 32] = [1; 32];
+    let councilor2: [u8; 32] = [2; 32];
+    let councilor3: [u8; 32] = [3; 32];
+    let councilor4: [u8; 32] = [4; 32];
+    let councilor5: [u8; 32] = [5; 32];
+    assert!(Council::set_council(
+        system::RawOrigin::Root.into(),
+        vec![
+            councilor0,
+            councilor1.into(),
+            councilor2.into(),
+            councilor3.into(),
+            councilor4.into(),
+            councilor5.into()
+        ]
+    )
+    .is_ok());
+}
+
+struct VoteGenerator {
+    proposal_id: u32,
+    current_account_id: AccountId32,
+    current_account_id_seed: u8,
+    current_voter_id: u64,
+    pub auto_increment_voter_id: bool,
+}
+
+impl VoteGenerator {
+    fn new(proposal_id: u32) -> Self {
+        VoteGenerator {
+            proposal_id,
+            current_voter_id: 0,
+            current_account_id_seed: 0,
+            current_account_id: AccountId32::default(),
+            auto_increment_voter_id: true,
+        }
+    }
+    fn vote_and_assert_ok(&mut self, vote_kind: VoteKind) {
+        self.vote_and_assert(vote_kind, Ok(()));
+    }
+
+    fn vote_and_assert(&mut self, vote_kind: VoteKind, expected_result: DispatchResult<Error>) {
+        assert_eq!(self.vote(vote_kind.clone()), expected_result);
+    }
+
+    fn vote(&mut self, vote_kind: VoteKind) -> DispatchResult<Error> {
+        if self.auto_increment_voter_id {
+            self.current_account_id_seed += 1;
+            self.current_voter_id += 1;
+            let account_id: [u8; 32] = [self.current_account_id_seed; 32];
+            self.current_account_id = account_id.into();
+        }
+
+        ProposalsEngine::vote(
+            system::RawOrigin::Signed(self.current_account_id.clone()).into(),
+            self.current_voter_id,
+            self.proposal_id,
+            vote_kind,
+        )
+    }
+}
+
+#[derive(Clone)]
+struct DummyProposalFixture {
+    parameters: ProposalParameters<u32, u128>,
+    account_id: AccountId32,
+    proposer_id: u64,
+    proposal_code: Vec<u8>,
+    title: Vec<u8>,
+    description: Vec<u8>,
+    stake_balance: Option<BalanceOf<Runtime>>,
+}
+
+impl Default for DummyProposalFixture {
+    fn default() -> Self {
+        let title = b"title".to_vec();
+        let description = b"description".to_vec();
+        let dummy_proposal = proposals_codex::Call::<Runtime>::execute_text_proposal(
+            title.clone(),
+            description.clone(),
+            b"text".to_vec(),
+        );
+
+        DummyProposalFixture {
+            parameters: ProposalParameters {
+                voting_period: 3,
+                approval_quorum_percentage: 60,
+                approval_threshold_percentage: 60,
+                slashing_quorum_percentage: 60,
+                slashing_threshold_percentage: 60,
+                grace_period: 0,
+                required_stake: None,
+            },
+            account_id: <Runtime as system::Trait>::AccountId::default(),
+            proposer_id: 0,
+            proposal_code: dummy_proposal.encode(),
+            title,
+            description,
+            stake_balance: None,
+        }
+    }
+}
+
+impl DummyProposalFixture {
+    fn with_parameters(self, parameters: ProposalParameters<u32, u128>) -> Self {
+        DummyProposalFixture { parameters, ..self }
+    }
+
+    fn with_account_id(self, account_id: AccountId32) -> Self {
+        DummyProposalFixture { account_id, ..self }
+    }
+
+    fn with_stake(self, stake_balance: BalanceOf<Runtime>) -> Self {
+        DummyProposalFixture {
+            stake_balance: Some(stake_balance),
+            ..self
+        }
+    }
+
+    fn with_proposer(self, proposer_id: u64) -> Self {
+        DummyProposalFixture {
+            proposer_id,
+            ..self
+        }
+    }
+
+    fn create_proposal_and_assert(self, result: Result<u32, Error>) -> Option<u32> {
+        let proposal_id_result = ProposalsEngine::create_proposal(
+            self.account_id,
+            self.proposer_id,
+            self.parameters,
+            self.title,
+            self.description,
+            self.stake_balance,
+            self.proposal_code,
+        );
+        assert_eq!(proposal_id_result, result);
+
+        proposal_id_result.ok()
+    }
+}
+
+struct CancelProposalFixture {
+    origin: RawOrigin<AccountId32>,
+    proposal_id: u32,
+    proposer_id: u64,
+}
+
+impl CancelProposalFixture {
+    fn new(proposal_id: u32) -> Self {
+        let account_id = <Runtime as system::Trait>::AccountId::default();
+        CancelProposalFixture {
+            proposal_id,
+            origin: RawOrigin::Signed(account_id),
+            proposer_id: 0,
+        }
+    }
+
+    fn with_proposer(self, proposer_id: u64) -> Self {
+        CancelProposalFixture {
+            proposer_id,
+            ..self
+        }
+    }
+
+    fn cancel_and_assert(self, expected_result: DispatchResult<Error>) {
+        assert_eq!(
+            ProposalsEngine::cancel_proposal(
+                self.origin.into(),
+                self.proposer_id,
+                self.proposal_id
+            ),
+            expected_result
+        );
+    }
+}
+
+/// Main purpose of this integration test: check balance of the member on proposal finalization (cancellation)
+/// It tests StakingEventsHandler integration. Also, membership module is tested during the proposal creation (ActorOriginValidator).
+#[test]
+fn proposal_cancellation_with_slashes_with_balance_checks_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let account_id = <Runtime as system::Trait>::AccountId::default();
+
+        setup_members(2);
+        let member_id = 0; // newly created member_id
+
+        let stake_amount = 200u128;
+        let parameters = ProposalParameters {
+            voting_period: 3,
+            approval_quorum_percentage: 50,
+            approval_threshold_percentage: 60,
+            slashing_quorum_percentage: 60,
+            slashing_threshold_percentage: 60,
+            grace_period: 5,
+            required_stake: Some(stake_amount),
+        };
+        let dummy_proposal = DummyProposalFixture::default()
+            .with_parameters(parameters)
+            .with_account_id(account_id.clone())
+            .with_stake(stake_amount)
+            .with_proposer(member_id);
+
+        let account_balance = 500;
+        let _imbalance =
+            <Runtime as stake::Trait>::Currency::deposit_creating(&account_id, account_balance);
+
+        assert_eq!(
+            <Runtime as stake::Trait>::Currency::total_balance(&account_id),
+            account_balance
+        );
+
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+        assert_eq!(
+            <Runtime as stake::Trait>::Currency::total_balance(&account_id),
+            account_balance - stake_amount
+        );
+
+        let mut proposal = ProposalsEngine::proposals(proposal_id);
+
+        let mut expected_proposal = Proposal {
+            parameters,
+            proposer_id: member_id,
+            created_at: 1,
+            status: ProposalStatus::Active(Some(ActiveStake {
+                stake_id: 0,
+                source_account_id: account_id.clone(),
+            })),
+            title: b"title".to_vec(),
+            description: b"description".to_vec(),
+            voting_results: VotingResults::default(),
+        };
+
+        assert_eq!(proposal, expected_proposal);
+
+        let cancel_proposal_fixture =
+            CancelProposalFixture::new(proposal_id).with_proposer(member_id);
+
+        cancel_proposal_fixture.cancel_and_assert(Ok(()));
+
+        proposal = ProposalsEngine::proposals(proposal_id);
+
+        expected_proposal.status = ProposalStatus::Finalized(FinalizationData {
+            proposal_status: ProposalDecisionStatus::Canceled,
+            finalized_at: 1,
+            encoded_unstaking_error_due_to_broken_runtime: None,
+            stake_data_after_unstaking_error: None,
+        });
+
+        assert_eq!(proposal, expected_proposal);
+
+        let cancellation_fee = ProposalCancellationFee::get() as u128;
+        assert_eq!(
+            <Runtime as stake::Trait>::Currency::total_balance(&account_id),
+            account_balance - cancellation_fee
+        );
+    });
+}
+
+#[test]
+fn proposal_reset_succeeds() {
+    initial_test_ext().execute_with(|| {
+        setup_members(4);
+        setup_council();
+        // create proposal
+        let dummy_proposal = DummyProposalFixture::default();
+        let proposal_id = dummy_proposal.create_proposal_and_assert(Ok(1)).unwrap();
+
+        // create some votes
+        let mut vote_generator = VoteGenerator::new(proposal_id);
+        vote_generator.vote_and_assert_ok(VoteKind::Reject);
+        vote_generator.vote_and_assert_ok(VoteKind::Abstain);
+        vote_generator.vote_and_assert_ok(VoteKind::Slash);
+
+        assert!(<proposals_engine::ActiveProposalIds<Runtime>>::exists(
+            proposal_id
+        ));
+
+        // check
+        let proposal = ProposalsEngine::proposals(proposal_id);
+        assert_eq!(
+            proposal.voting_results,
+            VotingResults {
+                abstentions: 1,
+                approvals: 0,
+                rejections: 1,
+                slashes: 1,
+            }
+        );
+
+        // Ensure council was elected
+        assert_eq!(CouncilManager::<Runtime>::total_voters_count(), 6);
+
+        // Check proposals CouncilElected hook
+        // just trigger the election hook, we don't care about the parameters
+        <Runtime as governance::election::Trait>::CouncilElected::council_elected(Vec::new(), 10);
+
+        let updated_proposal = ProposalsEngine::proposals(proposal_id);
+
+        assert_eq!(
+            updated_proposal.voting_results,
+            VotingResults {
+                abstentions: 0,
+                approvals: 0,
+                rejections: 0,
+                slashes: 0,
+            }
+        );
+
+        // Check council CouncilElected hook. It should set current council. And we passed empty council.
+        assert_eq!(CouncilManager::<Runtime>::total_voters_count(), 0);
+    });
+}