Browse Source

Merge pull request #2316 from Joystream/bounty

Merge bounty branch into the olympia branch.
Gabriel Alejandro Steinberg 4 years ago
parent
commit
19d5e16ac0
59 changed files with 8572 additions and 870 deletions
  1. 36 627
      Cargo.lock
  2. 3 2
      Cargo.toml
  3. 1 1
      node/Cargo.toml
  4. 4 2
      runtime-modules/blog/src/lib.rs
  5. 4 4
      runtime-modules/blog/src/mock.rs
  6. 53 0
      runtime-modules/bounty/Cargo.toml
  7. 171 0
      runtime-modules/bounty/src/actors.rs
  8. 1087 0
      runtime-modules/bounty/src/benchmarking.rs
  9. 1731 0
      runtime-modules/bounty/src/lib.rs
  10. 182 0
      runtime-modules/bounty/src/stages.rs
  11. 816 0
      runtime-modules/bounty/src/tests/fixtures.rs
  12. 559 0
      runtime-modules/bounty/src/tests/mocks.rs
  13. 3419 0
      runtime-modules/bounty/src/tests/mod.rs
  14. 16 0
      runtime-modules/common/src/council.rs
  15. 4 41
      runtime-modules/common/src/lib.rs
  16. 60 0
      runtime-modules/common/src/membership.rs
  17. 0 19
      runtime-modules/common/src/origin.rs
  18. 1 1
      runtime-modules/common/src/working_group.rs
  19. 1 1
      runtime-modules/constitution/src/lib.rs
  20. 9 5
      runtime-modules/content-directory/src/lib.rs
  21. 6 6
      runtime-modules/content-directory/src/mock.rs
  22. 26 12
      runtime-modules/council/src/lib.rs
  23. 6 6
      runtime-modules/council/src/mock.rs
  24. 26 1
      runtime-modules/council/src/tests.rs
  25. 2 2
      runtime-modules/forum/src/benchmarking.rs
  26. 2 2
      runtime-modules/forum/src/lib.rs
  27. 5 5
      runtime-modules/forum/src/mock.rs
  28. 1 1
      runtime-modules/membership/src/benchmarking.rs
  29. 16 6
      runtime-modules/membership/src/lib.rs
  30. 9 9
      runtime-modules/membership/src/tests/mock.rs
  31. 27 1
      runtime-modules/membership/src/tests/mod.rs
  32. 2 2
      runtime-modules/proposals/codex/src/lib.rs
  33. 6 6
      runtime-modules/proposals/codex/src/tests/mock.rs
  34. 1 1
      runtime-modules/proposals/discussion/src/benchmarking.rs
  35. 4 3
      runtime-modules/proposals/discussion/src/lib.rs
  36. 4 4
      runtime-modules/proposals/discussion/src/tests/mock.rs
  37. 4 3
      runtime-modules/proposals/engine/src/lib.rs
  38. 6 6
      runtime-modules/proposals/engine/src/tests/mock/mod.rs
  39. 19 8
      runtime-modules/referendum/src/benchmarking.rs
  40. 15 11
      runtime-modules/referendum/src/lib.rs
  41. 14 11
      runtime-modules/referendum/src/mock.rs
  42. 5 5
      runtime-modules/service-discovery/src/mock.rs
  43. 3 3
      runtime-modules/staking-handler/src/lib.rs
  44. 1 1
      runtime-modules/staking-handler/src/mock.rs
  45. 1 1
      runtime-modules/storage/src/data_directory.rs
  46. 1 1
      runtime-modules/storage/src/data_object_type_registry.rs
  47. 5 5
      runtime-modules/storage/src/tests/mock.rs
  48. 6 6
      runtime-modules/utility/src/tests/mocks.rs
  49. 2 2
      runtime-modules/working-group/src/lib.rs
  50. 2 2
      runtime-modules/working-group/src/tests/mock.rs
  51. 3 3
      runtime-modules/working-group/src/types.rs
  52. 7 4
      runtime/Cargo.toml
  53. 6 0
      runtime/src/constants.rs
  54. 27 3
      runtime/src/lib.rs
  55. 8 6
      runtime/src/runtime_api.rs
  56. 22 19
      runtime/src/tests/proposals_integration/working_group_proposals.rs
  57. 113 0
      runtime/src/weights/bounty.rs
  58. 1 0
      runtime/src/weights/mod.rs
  59. 1 0
      scripts/generate-weights.sh

File diff suppressed because it is too large
+ 36 - 627
Cargo.lock


+ 3 - 2
Cargo.toml

@@ -16,11 +16,12 @@ members = [
 	"runtime-modules/content-directory",
 	"runtime-modules/constitution",
 	"runtime-modules/staking-handler",
+	"runtime-modules/bounty",
     "runtime-modules/blog",
     "runtime-modules/utility",
 	"node",
-	"utils/chain-spec-builder/",
-    "analyses/bench"
+	"utils/chain-spec-builder/"
+#    "analyses/bench" # doesn't work with the "all features" release build
 ]
 
 [profile.release]

+ 1 - 1
node/Cargo.toml

@@ -3,7 +3,7 @@ authors = ['Joystream contributors']
 build = 'build.rs'
 edition = '2018'
 name = 'joystream-node'
-version = '4.0.2'
+version = '4.1.0'
 default-run = "joystream-node"
 
 [[bin]]

+ 4 - 2
runtime-modules/blog/src/lib.rs

@@ -35,7 +35,7 @@
 #![cfg_attr(not(feature = "std"), no_std)]
 
 use codec::{Codec, Decode, Encode};
-use common::origin::MemberOriginValidator;
+use common::membership::MemberOriginValidator;
 use errors::Error;
 pub use frame_support::dispatch::{DispatchError, DispatchResult};
 use frame_support::weights::Weight;
@@ -74,7 +74,9 @@ pub trait WeightInfo {
 }
 
 // The pallet's configuration trait.
-pub trait Trait<I: Instance = DefaultInstance>: frame_system::Trait + common::Trait {
+pub trait Trait<I: Instance = DefaultInstance>:
+    frame_system::Trait + common::membership::Trait
+{
     /// Origin from which participant must come.
     type ParticipantEnsureOrigin: MemberOriginValidator<
         Self::Origin,

+ 4 - 4
runtime-modules/blog/src/mock.rs

@@ -136,7 +136,7 @@ impl common::working_group::WorkingGroupBudgetHandler<Runtime> for () {
 impl common::working_group::WorkingGroupAuthenticator<Runtime> for () {
     fn ensure_worker_origin(
         _origin: <Runtime as frame_system::Trait>::Origin,
-        _worker_id: &<Runtime as common::Trait>::ActorId,
+        _worker_id: &<Runtime as common::membership::Trait>::ActorId,
     ) -> DispatchResult {
         unimplemented!()
     }
@@ -145,7 +145,7 @@ impl common::working_group::WorkingGroupAuthenticator<Runtime> for () {
         unimplemented!()
     }
 
-    fn get_leader_member_id() -> Option<<Runtime as common::Trait>::MemberId> {
+    fn get_leader_member_id() -> Option<<Runtime as common::membership::Trait>::MemberId> {
         unimplemented!()
     }
 
@@ -155,7 +155,7 @@ impl common::working_group::WorkingGroupAuthenticator<Runtime> for () {
 
     fn is_worker_account_id(
         _: &<Runtime as frame_system::Trait>::AccountId,
-        _worker_id: &<Runtime as common::Trait>::ActorId,
+        _worker_id: &<Runtime as common::membership::Trait>::ActorId,
     ) -> bool {
         unimplemented!();
     }
@@ -282,7 +282,7 @@ impl
     }
 }
 
-impl common::Trait for Runtime {
+impl common::membership::Trait for Runtime {
     type MemberId = u64;
     type ActorId = u64;
 }

+ 53 - 0
runtime-modules/bounty/Cargo.toml

@@ -0,0 +1,53 @@
+[package]
+name = 'pallet-bounty'
+version = '1.0.1'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[dependencies]
+serde = { version = "1.0.101", optional = true, features = ["derive"] }
+codec = { package = 'parity-scale-codec', version = '1.3.4', default-features = false, features = ['derive'] }
+sp-arithmetic = { package = 'sp-arithmetic', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
+sp-std = { package = 'sp-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
+frame-support = { package = 'frame-support', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
+frame-system = { package = 'frame-system', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
+balances = { package = 'pallet-balances', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
+sp-runtime = { package = 'sp-runtime', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
+common = { package = 'pallet-common', default-features = false, path = '../common'}
+staking-handler = { package = 'pallet-staking-handler', default-features = false, path = '../staking-handler'}
+
+# Benchmarking
+frame-benchmarking = { package = 'frame-benchmarking', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca', optional = true}
+council = { package = 'pallet-council', default-features = false, path = '../council'}
+referendum = { package = 'pallet-referendum', default-features = false, path = '../referendum'}
+membership = { package = 'pallet-membership', default-features = false, path = '../membership'}
+
+[dev-dependencies]
+pallet-timestamp = { package = 'pallet-timestamp', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
+sp-io = { package = 'sp-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
+sp-core = { package = 'sp-core', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
+sp-runtime = { package = 'sp-runtime', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
+membership = { package = 'pallet-membership', default-features = false, path = '../membership'}
+council = { package = 'pallet-council', default-features = false, path = '../council'}
+staking-handler = { package = 'pallet-staking-handler', default-features = false, path = '../staking-handler'}
+
+[features]
+default = ['std']
+runtime-benchmarks = [
+	"frame-benchmarking",
+	"sp-runtime/runtime-benchmarks",
+	"council/runtime-benchmarks",
+	"membership/runtime-benchmarks"
+]
+std = [
+	'serde',
+	'codec/std',
+	'sp-arithmetic/std',
+	'sp-std/std',
+	'frame-support/std',
+	'frame-system/std',
+	'balances/std',
+	'sp-runtime/std',
+	'common/std',
+	'staking-handler/std',
+]

+ 171 - 0
runtime-modules/bounty/src/actors.rs

@@ -0,0 +1,171 @@
+//! This module contains the BountyActorManager - a bounty actor management helper.
+//! It simplifies the interface of dealing with two different actor types: members and council.
+//! BountyActorManager contains methods to validate actor origin, transfer funds to/from the bounty
+//! account, etc.
+
+use crate::{BalanceOf, BountyActor, Error, Module, Trait};
+
+use frame_support::dispatch::{DispatchError, DispatchResult};
+use frame_support::ensure;
+use frame_support::traits::Currency;
+use frame_system::ensure_root;
+use sp_arithmetic::traits::Saturating;
+
+use common::council::CouncilBudgetManager;
+use common::membership::{MemberId, MemberOriginValidator, MembershipInfoProvider};
+
+// Helper enum for the bounty management.
+pub(crate) enum BountyActorManager<T: Trait> {
+    // Bounty was created or funded by a council.
+    Council,
+
+    // Bounty was created or funded by a member.
+    Member(T::AccountId, MemberId<T>),
+}
+
+impl<T: Trait> BountyActorManager<T> {
+    // Construct BountyActor by extrinsic origin and optional member_id.
+    pub(crate) fn ensure_bounty_actor_manager(
+        origin: T::Origin,
+        actor: BountyActor<MemberId<T>>,
+    ) -> Result<BountyActorManager<T>, DispatchError> {
+        match actor {
+            BountyActor::Member(member_id) => {
+                let account_id =
+                    T::Membership::ensure_member_controller_account_origin(origin, member_id)?;
+
+                Ok(BountyActorManager::Member(account_id, member_id))
+            }
+            BountyActor::Council => {
+                ensure_root(origin)?;
+
+                Ok(BountyActorManager::Council)
+            }
+        }
+    }
+
+    // Construct BountyActor.
+    pub(crate) fn get_bounty_actor_manager(
+        actor: BountyActor<MemberId<T>>,
+    ) -> Result<BountyActorManager<T>, DispatchError> {
+        match actor {
+            BountyActor::Member(member_id) => {
+                let account_id = T::Membership::controller_account_id(member_id)?;
+
+                Ok(BountyActorManager::Member(account_id, member_id))
+            }
+            BountyActor::Council => Ok(BountyActorManager::Council),
+        }
+    }
+
+    // Validate balance is sufficient for the bounty
+    pub(crate) fn validate_balance_sufficiency(
+        &self,
+        required_balance: BalanceOf<T>,
+    ) -> DispatchResult {
+        let balance_is_sufficient = match self {
+            BountyActorManager::Council => {
+                BountyActorManager::<T>::check_council_budget(required_balance)
+            }
+            BountyActorManager::Member(account_id, _) => {
+                Module::<T>::check_balance_for_account(required_balance, account_id)
+            }
+        };
+
+        ensure!(
+            balance_is_sufficient,
+            Error::<T>::InsufficientBalanceForBounty
+        );
+
+        Ok(())
+    }
+
+    // Verifies that council budget is sufficient for a bounty.
+    fn check_council_budget(amount: BalanceOf<T>) -> bool {
+        T::CouncilBudgetManager::get_budget() >= amount
+    }
+
+    // Validate that provided actor relates to the initial BountyActor.
+    pub(crate) fn validate_actor(&self, actor: &BountyActor<MemberId<T>>) -> DispatchResult {
+        let initial_actor = match self {
+            BountyActorManager::Council => BountyActor::Council,
+            BountyActorManager::Member(_, member_id) => BountyActor::Member(*member_id),
+        };
+
+        ensure!(initial_actor == actor.clone(), Error::<T>::NotBountyActor);
+
+        Ok(())
+    }
+
+    // Transfer funds for the bounty creation.
+    pub(crate) fn transfer_funds_to_bounty_account(
+        &self,
+        bounty_id: T::BountyId,
+        required_balance: BalanceOf<T>,
+    ) -> DispatchResult {
+        match self {
+            BountyActorManager::Council => {
+                BountyActorManager::<T>::transfer_balance_from_council_budget(
+                    bounty_id,
+                    required_balance,
+                );
+            }
+            BountyActorManager::Member(account_id, _) => {
+                Module::<T>::transfer_funds_to_bounty_account(
+                    account_id,
+                    bounty_id,
+                    required_balance,
+                )?;
+            }
+        }
+
+        Ok(())
+    }
+
+    // Restore a balance for the bounty creator.
+    pub(crate) fn transfer_funds_from_bounty_account(
+        &self,
+        bounty_id: T::BountyId,
+        required_balance: BalanceOf<T>,
+    ) -> DispatchResult {
+        match self {
+            BountyActorManager::Council => {
+                BountyActorManager::<T>::transfer_balance_to_council_budget(
+                    bounty_id,
+                    required_balance,
+                );
+            }
+            BountyActorManager::Member(account_id, _) => {
+                Module::<T>::transfer_funds_from_bounty_account(
+                    account_id,
+                    bounty_id,
+                    required_balance,
+                )?;
+            }
+        }
+
+        Ok(())
+    }
+
+    // Remove some balance from the council budget and transfer it to the bounty account.
+    fn transfer_balance_from_council_budget(bounty_id: T::BountyId, amount: BalanceOf<T>) {
+        let budget = T::CouncilBudgetManager::get_budget();
+        let new_budget = budget.saturating_sub(amount);
+
+        T::CouncilBudgetManager::set_budget(new_budget);
+
+        let bounty_account_id = Module::<T>::bounty_account_id(bounty_id);
+        let _ = balances::Module::<T>::deposit_creating(&bounty_account_id, amount);
+    }
+
+    // Add some balance from the council budget and slash from the bounty account.
+    fn transfer_balance_to_council_budget(bounty_id: T::BountyId, amount: BalanceOf<T>) {
+        let bounty_account_id = Module::<T>::bounty_account_id(bounty_id);
+        let _ = balances::Module::<T>::slash(&bounty_account_id, amount);
+
+        let budget = T::CouncilBudgetManager::get_budget();
+        let new_budget = budget.saturating_add(amount);
+
+        T::CouncilBudgetManager::set_budget(new_budget);
+    }
+}

+ 1087 - 0
runtime-modules/bounty/src/benchmarking.rs

@@ -0,0 +1,1087 @@
+#![cfg(feature = "runtime-benchmarks")]
+
+use frame_benchmarking::{account, benchmarks};
+use frame_support::storage::StorageMap;
+use frame_support::traits::{Currency, Get, OnFinalize, OnInitialize};
+use frame_system::{EventRecord, RawOrigin};
+use sp_arithmetic::traits::{One, Zero};
+use sp_runtime::SaturatedConversion;
+use sp_std::boxed::Box;
+use sp_std::collections::btree_map::BTreeMap;
+use sp_std::collections::btree_set::BTreeSet;
+use sp_std::vec;
+use sp_std::vec::Vec;
+
+use crate::Module as Bounty;
+use balances::Module as Balances;
+use common::council::CouncilBudgetManager;
+use frame_system::Module as System;
+use membership::Module as Membership;
+
+use crate::{
+    AssuranceContractType, BalanceOf, Bounties, BountyActor, BountyCreationParameters,
+    BountyMilestone, Call, Entries, Event, FundingType, Module, OracleWorkEntryJudgment, Trait,
+};
+
+pub fn run_to_block<T: Trait>(target_block: T::BlockNumber) {
+    let mut current_block = System::<T>::block_number();
+    while current_block < target_block {
+        System::<T>::on_finalize(current_block);
+        Bounty::<T>::on_finalize(current_block);
+
+        current_block += One::one();
+        System::<T>::set_block_number(current_block);
+
+        System::<T>::on_initialize(current_block);
+        Bounty::<T>::on_initialize(current_block);
+    }
+}
+
+fn assert_last_event<T: Trait>(generic_event: <T as Trait>::Event) {
+    let events = System::<T>::events();
+    let system_event: <T as frame_system::Trait>::Event = generic_event.into();
+    // compare to the last event record
+    let EventRecord { event, .. } = &events[events.len() - 1];
+    assert_eq!(event, &system_event);
+}
+
+fn assert_was_fired<T: Trait>(generic_event: <T as Trait>::Event) {
+    let events = System::<T>::events();
+    let system_event: <T as frame_system::Trait>::Event = generic_event.into();
+
+    assert!(events.iter().any(|ev| ev.event == system_event));
+}
+
+fn get_byte(num: u32, byte_number: u8) -> u8 {
+    ((num & (0xff << (8 * byte_number))) >> 8 * byte_number) as u8
+}
+
+// Method to generate a distintic valid handle
+// for a membership. For each index.
+fn handle_from_id<T: Trait + membership::Trait>(id: u32) -> Vec<u8> {
+    let mut handle = vec![];
+
+    for i in 0..4 {
+        handle.push(get_byte(id, i));
+    }
+
+    handle
+}
+
+//defines initial balance
+fn initial_balance<T: Trait>() -> T::Balance {
+    1000000.into()
+}
+
+fn member_funded_account<T: Trait + membership::Trait>(
+    name: &'static str,
+    id: u32,
+) -> (T::AccountId, T::MemberId) {
+    let account_id = account::<T::AccountId>(name, id, SEED);
+    let handle = handle_from_id::<T>(id);
+
+    // Give balance for buying membership
+    let _ = Balances::<T>::make_free_balance_be(&account_id, initial_balance::<T>());
+
+    let params = membership::BuyMembershipParameters {
+        root_account: account_id.clone(),
+        controller_account: account_id.clone(),
+        handle: Some(handle),
+        metadata: Vec::new(),
+        referrer_id: None,
+    };
+
+    let new_member_id = Membership::<T>::members_created();
+
+    Membership::<T>::buy_membership(RawOrigin::Signed(account_id.clone()).into(), params).unwrap();
+
+    let _ = Balances::<T>::make_free_balance_be(&account_id, initial_balance::<T>());
+
+    Membership::<T>::add_staking_account_candidate(
+        RawOrigin::Signed(account_id.clone()).into(),
+        new_member_id,
+    )
+    .unwrap();
+
+    Membership::<T>::confirm_staking_account(
+        RawOrigin::Signed(account_id.clone()).into(),
+        new_member_id,
+        account_id.clone(),
+    )
+    .unwrap();
+
+    (account_id, new_member_id)
+}
+
+fn announce_entry_and_submit_work<T: Trait + membership::Trait>(
+    bounty_id: &T::BountyId,
+    index: u32,
+) -> T::EntryId {
+    let membership_index = 1000 + index;
+    let (account_id, member_id) = member_funded_account::<T>("work entrants", membership_index);
+
+    Bounty::<T>::announce_work_entry(
+        RawOrigin::Signed(account_id.clone()).into(),
+        member_id,
+        *bounty_id,
+        account_id.clone(),
+    )
+    .unwrap();
+
+    let entry_id: T::EntryId = Bounty::<T>::entry_count().into();
+
+    let work_data = b"work_data".to_vec();
+
+    Bounty::<T>::submit_work(
+        RawOrigin::Signed(account_id.clone()).into(),
+        member_id,
+        *bounty_id,
+        entry_id,
+        work_data,
+    )
+    .unwrap();
+
+    entry_id
+}
+
+fn create_max_funded_bounty<T: Trait>(params: BountyCreationParameters<T>) -> T::BountyId {
+    let funding_amount = match params.funding_type {
+        FundingType::Perpetual { target } => target,
+        FundingType::Limited {
+            max_funding_amount, ..
+        } => max_funding_amount,
+    };
+
+    create_funded_bounty::<T>(params.clone(), funding_amount)
+}
+
+fn create_min_funded_bounty<T: Trait>(params: BountyCreationParameters<T>) -> T::BountyId {
+    let funding_amount = match params.funding_type {
+        FundingType::Perpetual { target } => target,
+        FundingType::Limited {
+            min_funding_amount, ..
+        } => min_funding_amount,
+    };
+
+    create_funded_bounty::<T>(params.clone(), funding_amount)
+}
+
+fn create_funded_bounty<T: Trait>(
+    params: BountyCreationParameters<T>,
+    funding_amount: BalanceOf<T>,
+) -> T::BountyId {
+    T::CouncilBudgetManager::set_budget(params.cherry + funding_amount);
+
+    Bounty::<T>::create_bounty(RawOrigin::Root.into(), params, Vec::new()).unwrap();
+
+    let bounty_id: T::BountyId = Bounty::<T>::bounty_count().into();
+
+    assert!(Bounties::<T>::contains_key(bounty_id));
+
+    Bounty::<T>::fund_bounty(
+        RawOrigin::Root.into(),
+        BountyActor::Council,
+        bounty_id,
+        funding_amount,
+    )
+    .unwrap();
+
+    bounty_id
+}
+
+const MAX_BYTES: u32 = 50000;
+const SEED: u32 = 0;
+const MAX_WORK_ENTRIES: u32 = 100;
+
+benchmarks! {
+    where_clause {
+        where T: council::Trait,
+              T: balances::Trait,
+              T: membership::Trait,
+              T: Trait,
+    }
+    _{ }
+
+    create_bounty_by_council {
+        let i in 1 .. MAX_BYTES;
+        let j in 1 .. T::ClosedContractSizeLimit::get();
+
+        let metadata = vec![0u8].repeat(i as usize);
+        let cherry: BalanceOf<T> = 100.into();
+        let entrant_stake: BalanceOf<T> = T::MinWorkEntrantStake::get();
+        let max_amount: BalanceOf<T> = 1000.into();
+
+        T::CouncilBudgetManager::set_budget(cherry);
+
+        let members = (1..=j)
+            .map(|id| id.saturated_into())
+            .collect::<BTreeSet<_>>();
+
+        let params = BountyCreationParameters::<T>{
+            work_period: One::one(),
+            judging_period: One::one(),
+            cherry,
+            entrant_stake,
+            funding_type: FundingType::Perpetual{ target: max_amount },
+            contract_type: AssuranceContractType::Closed(members),
+            ..Default::default()
+        };
+
+    }: create_bounty (RawOrigin::Root, params.clone(), metadata.clone())
+    verify {
+        let bounty_id: T::BountyId = 1u32.into();
+
+        assert!(Bounties::<T>::contains_key(bounty_id));
+        assert_eq!(Bounty::<T>::bounty_count(), 1); // Bounty counter was updated.
+        assert_last_event::<T>(Event::<T>::BountyCreated(bounty_id, params, metadata).into());
+    }
+
+    create_bounty_by_member {
+        let i in 1 .. MAX_BYTES;
+        let j in 1 .. T::ClosedContractSizeLimit::get();
+
+        let metadata = vec![0u8].repeat(i as usize);
+        let cherry: BalanceOf<T> = 100.into();
+        let entrant_stake: BalanceOf<T> = T::MinWorkEntrantStake::get();
+        let max_amount: BalanceOf<T> = 1000.into();
+
+        let (account_id, member_id) = member_funded_account::<T>("member1", 0);
+
+        T::CouncilBudgetManager::set_budget(cherry);
+
+        let members = (1..=j)
+            .map(|id| id.saturated_into())
+            .collect::<BTreeSet<_>>();
+
+        let params = BountyCreationParameters::<T>{
+            work_period: One::one(),
+            judging_period: One::one(),
+            cherry,
+            entrant_stake,
+            creator: BountyActor::Member(member_id),
+            funding_type: FundingType::Perpetual{ target: max_amount },
+            contract_type: AssuranceContractType::Closed(members),
+            ..Default::default()
+        };
+
+    }: create_bounty (RawOrigin::Signed(account_id), params.clone(), metadata.clone())
+    verify {
+        let bounty_id: T::BountyId = 1u32.into();
+
+        assert!(Bounties::<T>::contains_key(bounty_id));
+        assert_eq!(Bounty::<T>::bounty_count(), 1); // Bounty counter was updated.
+        assert_last_event::<T>(Event::<T>::BountyCreated(bounty_id, params, metadata).into());
+    }
+
+    cancel_bounty_by_council {
+        let cherry: BalanceOf<T> = 100.into();
+        let max_amount: BalanceOf<T> = 1000.into();
+        let entrant_stake: BalanceOf<T> = T::MinWorkEntrantStake::get();
+
+        T::CouncilBudgetManager::set_budget(cherry);
+
+        let creator = BountyActor::Council;
+        let params = BountyCreationParameters::<T>{
+            work_period: One::one(),
+            judging_period: One::one(),
+            cherry,
+            creator: creator.clone(),
+            // same complexity with limited funding and FundingExpired stage.
+            funding_type: FundingType::Perpetual{ target: max_amount },
+            entrant_stake,
+            ..Default::default()
+        };
+
+        Bounty::<T>::create_bounty(RawOrigin::Root.into(), params, Vec::new()).unwrap();
+
+        let bounty_id: T::BountyId = Bounty::<T>::bounty_count().into();
+
+        assert!(Bounties::<T>::contains_key(bounty_id));
+    }: cancel_bounty(RawOrigin::Root, creator.clone(), bounty_id)
+    verify {
+        assert!(!Bounties::<T>::contains_key(bounty_id));
+        assert_last_event::<T>(Event::<T>::BountyCanceled(bounty_id, creator).into());
+    }
+
+    cancel_bounty_by_member {
+        let cherry: BalanceOf<T> = 100.into();
+        let max_amount: BalanceOf<T> = 1000.into();
+        let entrant_stake: BalanceOf<T> = T::MinWorkEntrantStake::get();
+
+        let (account_id, member_id) = member_funded_account::<T>("member1", 0);
+
+        T::CouncilBudgetManager::set_budget(cherry);
+
+        let creator = BountyActor::Member(member_id);
+
+        let params = BountyCreationParameters::<T>{
+            work_period: One::one(),
+            judging_period: One::one(),
+            cherry,
+            creator: creator.clone(),
+            // same complexity with limited funding and FundingExpired stage.
+            funding_type: FundingType::Perpetual{ target: max_amount },
+            entrant_stake,
+            ..Default::default()
+        };
+
+        Bounty::<T>::create_bounty(
+            RawOrigin::Signed(account_id.clone()).into(),
+            params,
+            Vec::new()
+        ).unwrap();
+
+        let bounty_id: T::BountyId = Bounty::<T>::bounty_count().into();
+
+        assert!(Bounties::<T>::contains_key(bounty_id));
+    }: cancel_bounty(RawOrigin::Signed(account_id), creator.clone(), bounty_id)
+    verify {
+        assert!(!Bounties::<T>::contains_key(bounty_id));
+        assert_last_event::<T>(Event::<T>::BountyCanceled(bounty_id, creator).into());
+    }
+
+    veto_bounty {
+        let max_amount: BalanceOf<T> = 1000.into();
+        let cherry: BalanceOf<T> = 100.into();
+        let entrant_stake: BalanceOf<T> = T::MinWorkEntrantStake::get();
+
+        T::CouncilBudgetManager::set_budget(cherry);
+
+        let params = BountyCreationParameters::<T>{
+            work_period: One::one(),
+            judging_period: One::one(),
+            funding_type: FundingType::Perpetual{ target: max_amount },
+            cherry,
+            entrant_stake,
+            ..Default::default()
+        };
+
+        Bounty::<T>::create_bounty(RawOrigin::Root.into(), params, Vec::new()).unwrap();
+
+        let bounty_id: T::BountyId = Bounty::<T>::bounty_count().into();
+
+        assert!(Bounties::<T>::contains_key(bounty_id));
+    }: _ (RawOrigin::Root, bounty_id)
+    verify {
+        assert!(!Bounties::<T>::contains_key(bounty_id));
+        assert_last_event::<T>(Event::<T>::BountyVetoed(bounty_id).into());
+    }
+
+    fund_bounty_by_member {
+        let max_amount: BalanceOf<T> = 100.into();
+        let cherry: BalanceOf<T> = 100.into();
+        let entrant_stake: BalanceOf<T> = T::MinWorkEntrantStake::get();
+
+        T::CouncilBudgetManager::set_budget(cherry);
+
+        let params = BountyCreationParameters::<T>{
+            work_period: One::one(),
+            judging_period: One::one(),
+            funding_type: FundingType::Perpetual{ target: max_amount },
+            cherry,
+            entrant_stake,
+            ..Default::default()
+        };
+        // should reach default max bounty funding amount
+        let amount: BalanceOf<T> = 100.into();
+
+        let (account_id, member_id) = member_funded_account::<T>("member1", 0);
+
+        Bounty::<T>::create_bounty(RawOrigin::Root.into(), params, Vec::new()).unwrap();
+
+        let bounty_id: T::BountyId = Bounty::<T>::bounty_count().into();
+
+        assert!(Bounties::<T>::contains_key(bounty_id));
+    }: fund_bounty (RawOrigin::Signed(account_id.clone()), BountyActor::Member(member_id), bounty_id, amount)
+    verify {
+        assert_eq!(
+            Balances::<T>::usable_balance(&account_id),
+            // included staking account deposit
+            initial_balance::<T>() - amount - T::CandidateStake::get()
+        );
+        assert_last_event::<T>(Event::<T>::BountyMaxFundingReached(bounty_id).into());
+    }
+
+    fund_bounty_by_council {
+        let max_amount: BalanceOf<T> = 100.into();
+        let cherry: BalanceOf<T> = 100.into();
+        let entrant_stake: BalanceOf<T> = T::MinWorkEntrantStake::get();
+
+        T::CouncilBudgetManager::set_budget(cherry + max_amount);
+
+        let params = BountyCreationParameters::<T>{
+            work_period: One::one(),
+            judging_period: One::one(),
+            funding_type: FundingType::Perpetual{ target: max_amount },
+            cherry,
+            entrant_stake,
+            ..Default::default()
+        };
+        // should reach default max bounty funding amount
+        let amount: BalanceOf<T> = 100.into();
+
+        Bounty::<T>::create_bounty(RawOrigin::Root.into(), params, Vec::new()).unwrap();
+
+        let bounty_id: T::BountyId = Bounty::<T>::bounty_count().into();
+
+        assert!(Bounties::<T>::contains_key(bounty_id));
+    }: fund_bounty (RawOrigin::Root, BountyActor::Council, bounty_id, amount)
+    verify {
+        assert_eq!(T::CouncilBudgetManager::get_budget(), Zero::zero());
+        assert_last_event::<T>(Event::<T>::BountyMaxFundingReached(bounty_id).into());
+    }
+
+    withdraw_funding_by_member {
+        let funding_period = 1;
+        let bounty_amount: BalanceOf<T> = 200.into();
+        let cherry: BalanceOf<T> = 100.into();
+        let entrant_stake: BalanceOf<T> = T::MinWorkEntrantStake::get();
+
+        T::CouncilBudgetManager::set_budget(cherry);
+
+        let params = BountyCreationParameters::<T>{
+            work_period: One::one(),
+            judging_period: One::one(),
+            funding_type: FundingType::Limited{
+                min_funding_amount: bounty_amount,
+                max_funding_amount: bounty_amount,
+                funding_period: funding_period.into(),
+            },
+            cherry,
+            entrant_stake,
+            ..Default::default()
+        };
+        // should reach default max bounty funding amount
+        let amount: BalanceOf<T> = 100.into();
+
+        let (account_id, member_id) = member_funded_account::<T>("member1", 0);
+
+        Bounty::<T>::create_bounty(RawOrigin::Root.into(), params, Vec::new()).unwrap();
+
+        let bounty_id: T::BountyId = Bounty::<T>::bounty_count().into();
+
+        assert!(Bounties::<T>::contains_key(bounty_id));
+
+        let funder = BountyActor::Member(member_id);
+
+        Bounty::<T>::fund_bounty(
+            RawOrigin::Signed(account_id.clone()).into(),
+            funder.clone(),
+            bounty_id,
+            amount
+        ).unwrap();
+
+        run_to_block::<T>((funding_period + 1).into());
+
+    }: withdraw_funding (RawOrigin::Signed(account_id.clone()), funder, bounty_id)
+    verify {
+        assert_eq!(
+            Balances::<T>::usable_balance(&account_id),
+            // included staking account deposit
+            initial_balance::<T>() - T::CandidateStake::get() + cherry
+        );
+
+        assert_last_event::<T>(Event::<T>::BountyRemoved(bounty_id).into());
+    }
+
+    withdraw_funding_by_council {
+        let funding_period = 1;
+        let bounty_amount: BalanceOf<T> = 200.into();
+        let cherry: BalanceOf<T> = 100.into();
+        let funding_amount: BalanceOf<T> = 100.into();
+        let entrant_stake: BalanceOf<T> = T::MinWorkEntrantStake::get();
+
+        T::CouncilBudgetManager::set_budget(cherry + funding_amount);
+
+        let params = BountyCreationParameters::<T>{
+            work_period: One::one(),
+            judging_period: One::one(),
+            funding_type: FundingType::Limited{
+                min_funding_amount: bounty_amount,
+                max_funding_amount: bounty_amount,
+                funding_period: funding_period.into(),
+            },
+            cherry,
+            entrant_stake,
+            ..Default::default()
+        };
+
+        Bounty::<T>::create_bounty(RawOrigin::Root.into(), params, Vec::new()).unwrap();
+
+        let bounty_id: T::BountyId = Bounty::<T>::bounty_count().into();
+
+        assert!(Bounties::<T>::contains_key(bounty_id));
+
+        let funder = BountyActor::Council;
+
+        Bounty::<T>::fund_bounty(
+            RawOrigin::Root.into(),
+            funder.clone(),
+            bounty_id,
+            funding_amount
+        ).unwrap();
+
+        run_to_block::<T>((funding_period + 1).into());
+
+    }: withdraw_funding(RawOrigin::Root, funder, bounty_id)
+    verify {
+        assert_eq!(T::CouncilBudgetManager::get_budget(), cherry + funding_amount);
+        assert_last_event::<T>(Event::<T>::BountyRemoved(bounty_id).into());
+    }
+
+    announce_work_entry {
+        let i in 1 .. T::ClosedContractSizeLimit::get();
+
+        let cherry: BalanceOf<T> = 100.into();
+        let funding_amount: BalanceOf<T> = 100.into();
+        let stake: BalanceOf<T> = 100.into();
+
+        let member_ids = (0..i)
+            .into_iter()
+            .map(|id| id.saturated_into())
+            .collect::<BTreeSet<T::MemberId>>();
+
+        let contract_type = AssuranceContractType::Closed(member_ids);
+
+        let params = BountyCreationParameters::<T>{
+            work_period: One::one(),
+            judging_period: One::one(),
+            funding_type: FundingType::Perpetual{ target: funding_amount },
+            cherry,
+            contract_type,
+            entrant_stake: stake,
+            ..Default::default()
+        };
+
+        let bounty_id = create_max_funded_bounty::<T>(params);
+
+        let (account_id, member_id) = member_funded_account::<T>("member1", 1);
+
+    }: _(
+        RawOrigin::Signed(account_id.clone()),
+        member_id,
+        bounty_id,
+        account_id.clone()
+    )
+    verify {
+        let entry_id: T::EntryId = Bounty::<T>::entry_count().into();
+
+        assert!(Entries::<T>::contains_key(entry_id));
+        assert_last_event::<T>(
+            Event::<T>::WorkEntryAnnounced(
+                bounty_id,
+                entry_id,
+                member_id,
+                account_id
+            ).into()
+        );
+    }
+
+    withdraw_work_entry {
+        let cherry: BalanceOf<T> = 100.into();
+        let funding_amount: BalanceOf<T> = 100.into();
+        let stake: BalanceOf<T> = 100.into();
+
+        let params = BountyCreationParameters::<T>{
+            work_period: One::one(),
+            judging_period: One::one(),
+            cherry,
+            funding_type: FundingType::Perpetual{ target: funding_amount },
+            entrant_stake: stake,
+            ..Default::default()
+        };
+
+        let bounty_id = create_max_funded_bounty::<T>(params);
+
+        let (account_id, member_id) = member_funded_account::<T>("member1", 1);
+
+        Bounty::<T>::announce_work_entry(
+            RawOrigin::Signed(account_id.clone()).into(),
+            member_id,
+            bounty_id,
+            account_id.clone()
+        ).unwrap();
+
+        let entry_id: T::EntryId = Bounty::<T>::entry_count().into();
+
+    }: _(RawOrigin::Signed(account_id.clone()), member_id, bounty_id, entry_id)
+    verify {
+        assert!(!Entries::<T>::contains_key(entry_id));
+        assert_last_event::<T>(
+            Event::<T>::WorkEntryWithdrawn(bounty_id, entry_id, member_id).into()
+        );
+    }
+
+    submit_work {
+        let i in 0 .. MAX_BYTES;
+        let work_data = vec![0u8].repeat(i as usize);
+
+        let cherry: BalanceOf<T> = 100.into();
+        let funding_amount: BalanceOf<T> = 100.into();
+        let max_amount: BalanceOf<T> = 10000.into();
+        let entrant_stake: BalanceOf<T> = T::MinWorkEntrantStake::get();
+        let work_period: T::BlockNumber = One::one();
+        let judging_period: T::BlockNumber = One::one();
+        let funding_period: T::BlockNumber = One::one();
+
+        let params = BountyCreationParameters::<T>{
+            work_period,
+            judging_period,
+            cherry,
+            funding_type: FundingType::Limited{
+                min_funding_amount: funding_amount,
+                max_funding_amount: max_amount,
+                funding_period
+            },
+            entrant_stake,
+            ..Default::default()
+        };
+
+        let bounty_id = create_min_funded_bounty::<T>(params);
+
+        run_to_block::<T>((funding_period + One::one()).into());
+
+        let bounty = Bounty::<T>::bounties(bounty_id);
+        assert!(matches!(bounty.milestone, BountyMilestone::Created { has_contributions: true, ..}));
+
+        let (account_id, member_id) = member_funded_account::<T>("member1", 1);
+
+        Bounty::<T>::announce_work_entry(
+            RawOrigin::Signed(account_id.clone()).into(),
+            member_id,
+            bounty_id,
+            account_id.clone()
+        ).unwrap();
+
+        let entry_id: T::EntryId = Bounty::<T>::entry_count().into();
+
+    }: _(RawOrigin::Signed(account_id.clone()), member_id, bounty_id, entry_id, work_data.clone())
+    verify {
+        let entry = Bounty::<T>::entries(entry_id);
+
+        assert!(entry.work_submitted);
+        assert_last_event::<T>(
+            Event::<T>::WorkSubmitted(bounty_id, entry_id, member_id, work_data).into()
+        );
+    }
+
+    submit_oracle_judgment_by_council_all_winners {
+        let i in 1 .. MAX_WORK_ENTRIES;
+
+        let work_period: T::BlockNumber = One::one();
+        let cherry: BalanceOf<T> = 100.into();
+        let funding_amount: BalanceOf<T> = 10000000.into();
+        let oracle = BountyActor::Council;
+        let entrant_stake: BalanceOf<T> = T::MinWorkEntrantStake::get();
+
+        let params = BountyCreationParameters::<T> {
+            work_period,
+            judging_period: One::one(),
+            creator: BountyActor::Council,
+            cherry,
+            entrant_stake,
+            funding_type: FundingType::Perpetual{ target: funding_amount },
+            oracle: oracle.clone(),
+            ..Default::default()
+        };
+
+        let bounty_id = create_max_funded_bounty::<T>(params);
+
+        let entry_ids = (0..i)
+            .into_iter()
+            .map(|i| { announce_entry_and_submit_work::<T>(&bounty_id, i)})
+            .collect::<Vec<_>>();
+
+        let winner_reward: BalanceOf<T> = (funding_amount / i.into()).into();
+        let correction = funding_amount - winner_reward * i.into(); // for total sum = 100%
+        let judgment = entry_ids
+            .iter()
+            .map(|entry_id| {
+                let corrected_winner_reward = if *entry_id == One::one() {
+                    winner_reward + correction
+                } else {
+                    winner_reward
+                };
+
+                (*entry_id, OracleWorkEntryJudgment::Winner {reward : corrected_winner_reward})
+            })
+            .collect::<BTreeMap<_, _>>();
+
+        run_to_block::<T>((work_period + One::one()).into());
+
+    }: submit_oracle_judgment(RawOrigin::Root, oracle.clone(), bounty_id, judgment.clone())
+    verify {
+        for entry_id in entry_ids {
+            let entry = Bounty::<T>::entries(entry_id);
+            let corrected_winner_reward = if entry_id == One::one() {
+                    winner_reward + correction
+                } else {
+                    winner_reward
+                };
+            assert_eq!(
+                entry.oracle_judgment_result,
+                Some(OracleWorkEntryJudgment::Winner {reward : corrected_winner_reward})
+            );
+        }
+        assert_last_event::<T>(
+            Event::<T>::OracleJudgmentSubmitted(bounty_id, oracle, judgment).into()
+        );
+    }
+
+    submit_oracle_judgment_by_council_all_rejected {
+        let i in 1 .. MAX_WORK_ENTRIES;
+
+        let work_period: T::BlockNumber = One::one();
+        let cherry: BalanceOf<T> = 100.into();
+        let funding_amount: BalanceOf<T> = 100.into();
+        let oracle = BountyActor::Council;
+        let entrant_stake: BalanceOf<T> = T::MinWorkEntrantStake::get();
+
+        let params = BountyCreationParameters::<T> {
+            work_period,
+            judging_period: One::one(),
+            creator: BountyActor::Council,
+            cherry,
+            entrant_stake,
+            funding_type: FundingType::Perpetual{ target: funding_amount },
+            oracle: oracle.clone(),
+            ..Default::default()
+        };
+
+        let bounty_id = create_max_funded_bounty::<T>(params);
+
+        let entry_ids = (0..i)
+            .into_iter()
+            .map(|i| { announce_entry_and_submit_work::<T>(&bounty_id, i)})
+            .collect::<Vec<_>>();
+
+        let judgment = entry_ids.iter()
+            .map(|entry_id| (*entry_id, OracleWorkEntryJudgment::Rejected))
+            .collect::<BTreeMap<_, _>>();
+
+        run_to_block::<T>((work_period + One::one()).into());
+
+    }: submit_oracle_judgment(RawOrigin::Root, oracle.clone(), bounty_id, judgment.clone())
+    verify {
+        for entry_id in entry_ids {
+            assert!(!<Entries<T>>::contains_key(entry_id));
+        }
+        assert_last_event::<T>(
+            Event::<T>::OracleJudgmentSubmitted(bounty_id, oracle, judgment).into()
+        );
+    }
+
+    submit_oracle_judgment_by_member_all_winners {
+        let i in 1 .. MAX_WORK_ENTRIES;
+
+        let work_period: T::BlockNumber = One::one();
+        let cherry: BalanceOf<T> = 100.into();
+        let funding_amount: BalanceOf<T> = 10000000.into();
+        let work_period: T::BlockNumber = One::one();
+        let (oracle_account_id, oracle_member_id) = member_funded_account::<T>("oracle", 1);
+        let oracle = BountyActor::Member(oracle_member_id);
+        let entrant_stake: BalanceOf<T> = T::MinWorkEntrantStake::get();
+
+        let params = BountyCreationParameters::<T> {
+            work_period,
+            judging_period: One::one(),
+            creator: BountyActor::Council,
+            cherry,
+            entrant_stake,
+            funding_type: FundingType::Perpetual{ target: funding_amount },
+            oracle: oracle.clone(),
+            ..Default::default()
+        };
+
+        let bounty_id = create_max_funded_bounty::<T>(params);
+
+        let entry_ids = (0..i)
+            .into_iter()
+            .map(|i| { announce_entry_and_submit_work::<T>(&bounty_id, i)})
+            .collect::<Vec<_>>();
+
+        let winner_reward: BalanceOf<T> = (funding_amount / i.into()).into();
+        let correction = funding_amount - winner_reward * i.into(); // for total sum = 100%
+        let judgment = entry_ids
+            .iter()
+            .map(|entry_id| {
+                let corrected_winner_reward = if *entry_id == One::one() {
+                    winner_reward + correction
+                } else {
+                    winner_reward
+                };
+
+                (*entry_id, OracleWorkEntryJudgment::Winner {reward : corrected_winner_reward})
+            })
+            .collect::<BTreeMap<_, _>>();
+
+        run_to_block::<T>((work_period + One::one()).into());
+
+    }: submit_oracle_judgment(
+        RawOrigin::Signed(oracle_account_id),
+        oracle.clone(),
+        bounty_id,
+        judgment.clone()
+    )
+    verify {
+        for entry_id in entry_ids {
+            let entry = Bounty::<T>::entries(entry_id);
+            let corrected_winner_reward = if entry_id == One::one() {
+                    winner_reward + correction
+                } else {
+                    winner_reward
+                };
+            assert_eq!(
+                entry.oracle_judgment_result,
+                Some(OracleWorkEntryJudgment::Winner {reward : corrected_winner_reward})
+            );
+        }
+        assert_last_event::<T>(
+            Event::<T>::OracleJudgmentSubmitted(bounty_id, oracle, judgment).into()
+        );
+    }
+
+    submit_oracle_judgment_by_member_all_rejected {
+        let i in 1 .. MAX_WORK_ENTRIES;
+
+        let work_period: T::BlockNumber = One::one();
+        let cherry: BalanceOf<T> = 100.into();
+        let funding_amount: BalanceOf<T> = 100.into();
+        let work_period: T::BlockNumber = One::one();
+        let (oracle_account_id, oracle_member_id) = member_funded_account::<T>("oracle", 1);
+        let oracle = BountyActor::Member(oracle_member_id);
+        let entrant_stake: BalanceOf<T> = T::MinWorkEntrantStake::get();
+
+        let params = BountyCreationParameters::<T> {
+            work_period,
+            judging_period: One::one(),
+            creator: BountyActor::Council,
+            cherry,
+            entrant_stake,
+            funding_type: FundingType::Perpetual{ target: funding_amount },
+            oracle: oracle.clone(),
+            ..Default::default()
+        };
+
+        let bounty_id = create_max_funded_bounty::<T>(params);
+
+        let entry_ids = (0..i)
+            .into_iter()
+            .map(|i| { announce_entry_and_submit_work::<T>(&bounty_id, i)})
+            .collect::<Vec<_>>();
+
+        let judgment = entry_ids.iter()
+            .map(|entry_id| (*entry_id, OracleWorkEntryJudgment::Rejected))
+            .collect::<BTreeMap<_, _>>();
+
+        run_to_block::<T>((work_period + One::one()).into());
+
+    }: submit_oracle_judgment(
+        RawOrigin::Signed(oracle_account_id),
+        oracle.clone(),
+        bounty_id,
+        judgment.clone()
+    )
+    verify {
+        for entry_id in entry_ids {
+            assert!(!<Entries<T>>::contains_key(entry_id));
+        }
+        assert_last_event::<T>(
+            Event::<T>::OracleJudgmentSubmitted(bounty_id, oracle, judgment).into()
+        );
+    }
+
+    withdraw_work_entrant_funds {
+        let work_period: T::BlockNumber = One::one();
+        let cherry: BalanceOf<T> = 100.into();
+        let funding_amount: BalanceOf<T> = 100.into();
+        let work_period: T::BlockNumber = One::one();
+        let (oracle_account_id, oracle_member_id) = member_funded_account::<T>("oracle", 1);
+        let oracle = BountyActor::Member(oracle_member_id);
+        let stake: BalanceOf<T> = 100.into();
+        let creator = BountyActor::Council;
+
+        let params = BountyCreationParameters::<T> {
+            work_period,
+            judging_period: One::one(),
+            creator: creator.clone(),
+            cherry,
+            funding_type: FundingType::Perpetual{ target: funding_amount },
+            oracle: oracle.clone(),
+            entrant_stake: stake,
+            ..Default::default()
+        };
+
+        let bounty_id = create_max_funded_bounty::<T>(params);
+
+        let (work_account_id, work_member_id) = member_funded_account::<T>("work entrants", 0);
+
+        Bounty::<T>::announce_work_entry(
+            RawOrigin::Signed(work_account_id.clone()).into(),
+            work_member_id,
+            bounty_id,
+            work_account_id.clone(),
+        )
+        .unwrap();
+
+        let entry_id: T::EntryId = Bounty::<T>::entry_count().into();
+
+        let work_data = b"work_data".to_vec();
+        Bounty::<T>::submit_work(
+            RawOrigin::Signed(work_account_id.clone()).into(),
+            work_member_id,
+            bounty_id,
+            entry_id,
+            work_data,
+        )
+        .unwrap();
+
+        let winner_reward: BalanceOf<T> = funding_amount;
+        let judgment = vec![entry_id].iter()
+            .map(|entry_id| (*entry_id, OracleWorkEntryJudgment::Winner {reward : winner_reward}))
+            .collect::<BTreeMap<_, _>>();
+
+        run_to_block::<T>((work_period + One::one()).into());
+
+        Bounty::<T>::submit_oracle_judgment(
+            RawOrigin::Signed(oracle_account_id).into(),
+            oracle.clone(),
+            bounty_id,
+            judgment.clone()
+        ).unwrap();
+
+    }: _ (RawOrigin::Signed(work_account_id), work_member_id, bounty_id, entry_id)
+    verify {
+        assert!(!<Entries<T>>::contains_key(entry_id));
+        assert_was_fired::<T>(
+            Event::<T>::WorkEntrantFundsWithdrawn(bounty_id, entry_id, work_member_id).into()
+        );
+        assert_last_event::<T>(Event::<T>::BountyRemoved(bounty_id).into());
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::tests::mocks::{build_test_externalities, Test};
+    use frame_support::assert_ok;
+
+    #[test]
+    fn create_bounty_by_council() {
+        build_test_externalities().execute_with(|| {
+            assert_ok!(test_benchmark_create_bounty_by_council::<Test>());
+        });
+    }
+
+    #[test]
+    fn create_bounty_by_member() {
+        build_test_externalities().execute_with(|| {
+            assert_ok!(test_benchmark_create_bounty_by_member::<Test>());
+        });
+    }
+
+    #[test]
+    fn cancel_bounty_by_council() {
+        build_test_externalities().execute_with(|| {
+            assert_ok!(test_benchmark_cancel_bounty_by_council::<Test>());
+        });
+    }
+
+    #[test]
+    fn cancel_bounty_by_member() {
+        build_test_externalities().execute_with(|| {
+            assert_ok!(test_benchmark_cancel_bounty_by_member::<Test>());
+        });
+    }
+
+    #[test]
+    fn veto_bounty() {
+        build_test_externalities().execute_with(|| {
+            assert_ok!(test_benchmark_veto_bounty::<Test>());
+        });
+    }
+
+    #[test]
+    fn fund_bounty_by_member() {
+        build_test_externalities().execute_with(|| {
+            assert_ok!(test_benchmark_fund_bounty_by_member::<Test>());
+        });
+    }
+
+    #[test]
+    fn fund_bounty_by_council() {
+        build_test_externalities().execute_with(|| {
+            assert_ok!(test_benchmark_fund_bounty_by_council::<Test>());
+        });
+    }
+
+    #[test]
+    fn withdraw_funding_by_member() {
+        build_test_externalities().execute_with(|| {
+            assert_ok!(test_benchmark_withdraw_funding_by_member::<Test>());
+        });
+    }
+
+    #[test]
+    fn withdraw_funding_by_council() {
+        build_test_externalities().execute_with(|| {
+            assert_ok!(test_benchmark_withdraw_funding_by_council::<Test>());
+        });
+    }
+
+    #[test]
+    fn announce_work_entry() {
+        build_test_externalities().execute_with(|| {
+            assert_ok!(test_benchmark_announce_work_entry::<Test>());
+        });
+    }
+
+    #[test]
+    fn withdraw_work_entry() {
+        build_test_externalities().execute_with(|| {
+            assert_ok!(test_benchmark_withdraw_work_entry::<Test>());
+        });
+    }
+
+    #[test]
+    fn submit_work() {
+        build_test_externalities().execute_with(|| {
+            assert_ok!(test_benchmark_submit_work::<Test>());
+        });
+    }
+
+    #[test]
+    fn submit_oracle_judgment_by_council_all_winners() {
+        build_test_externalities().execute_with(|| {
+            assert_ok!(test_benchmark_submit_oracle_judgment_by_council_all_winners::<Test>());
+        });
+    }
+
+    #[test]
+    fn submit_oracle_judgment_by_council_all_rejected() {
+        build_test_externalities().execute_with(|| {
+            assert_ok!(test_benchmark_submit_oracle_judgment_by_council_all_rejected::<Test>());
+        });
+    }
+
+    #[test]
+    fn submit_oracle_judgment_by_member_all_winners() {
+        build_test_externalities().execute_with(|| {
+            assert_ok!(test_benchmark_submit_oracle_judgment_by_member_all_winners::<Test>());
+        });
+    }
+
+    #[test]
+    fn submit_oracle_judgment_by_member_all_rejected() {
+        build_test_externalities().execute_with(|| {
+            assert_ok!(test_benchmark_submit_oracle_judgment_by_member_all_rejected::<Test>());
+        });
+    }
+
+    #[test]
+    fn withdraw_work_entrant_funds() {
+        build_test_externalities().execute_with(|| {
+            assert_ok!(test_benchmark_withdraw_work_entrant_funds::<Test>());
+        });
+    }
+}

+ 1731 - 0
runtime-modules/bounty/src/lib.rs

@@ -0,0 +1,1731 @@
+//! This pallet works with crowd funded bounties that allows a member, or the council, to crowd
+//! fund work on projects with a public benefit.
+//!
+//! ### Bounty stages
+//! - Funding - a bounty is being funded.
+//! - FundingExpired - a bounty is expired. It can be only canceled.
+//! - WorkSubmission - interested participants can submit their work.
+//! - Judgment - working periods ended and the oracle should provide their judgment.
+//! - SuccessfulBountyWithdrawal - work entrants' stakes and rewards can be withdrawn.
+//! - FailedBountyWithdrawal - contributors' funds can be withdrawn along with a split cherry.
+//!
+//! A detailed description could be found [here](https://github.com/Joystream/joystream/issues/1998).
+//!
+//! ### Supported extrinsics
+//! - [create_bounty](./struct.Module.html#method.create_bounty) - creates a bounty
+//!
+//! #### Funding stage
+//! - [cancel_bounty](./struct.Module.html#method.cancel_bounty) - cancels a bounty
+//! - [veto_bounty](./struct.Module.html#method.veto_bounty) - vetoes a bounty
+//! - [fund_bounty](./struct.Module.html#method.fund_bounty) - provide funding for a bounty
+//!
+//! #### FundingExpired stage
+//! - [cancel_bounty](./struct.Module.html#method.cancel_bounty) - cancels a bounty
+//!
+//! #### Work submission stage
+//! - [announce_work_entry](./struct.Module.html#method.announce_work_entry) - announce
+//! work entry for a successful bounty.
+//! - [withdraw_work_entry](./struct.Module.html#method.withdraw_work_entry) - withdraw
+//! work entry for a bounty.
+//! - [submit_work](./struct.Module.html#method.submit_work) - submit work for a bounty.
+//!
+//! #### Judgment stage
+//! - [submit_oracle_judgment](./struct.Module.html#method.submit_oracle_judgment) - submits an
+//! oracle judgment for a bounty.
+//!
+//! #### SuccessfulBountyWithdrawal stage
+//! - [withdraw_work_entrant_funds](./struct.Module.html#method.withdraw_work_entrant_funds) -
+//! withdraw work entrant funds.
+//!
+//! #### FailedBountyWithdrawal stage
+//! - [withdraw_funding](./struct.Module.html#method.withdraw_funding) - withdraw
+//! funding for a failed bounty.
+
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+#[cfg(test)]
+pub(crate) mod tests;
+
+mod actors;
+mod stages;
+
+#[cfg(feature = "runtime-benchmarks")]
+mod benchmarking;
+
+/// pallet_bounty WeightInfo.
+/// Note: This was auto generated through the benchmark CLI using the `--weight-trait` flag
+pub trait WeightInfo {
+    fn create_bounty_by_council(i: u32, j: u32) -> Weight;
+    fn create_bounty_by_member(i: u32, j: u32) -> Weight;
+    fn cancel_bounty_by_member() -> Weight;
+    fn cancel_bounty_by_council() -> Weight;
+    fn veto_bounty() -> Weight;
+    fn fund_bounty_by_member() -> Weight;
+    fn fund_bounty_by_council() -> Weight;
+    fn withdraw_funding_by_member() -> Weight;
+    fn withdraw_funding_by_council() -> Weight;
+    fn announce_work_entry(i: u32) -> Weight;
+    fn withdraw_work_entry() -> Weight;
+    fn submit_work(i: u32) -> Weight;
+    fn submit_oracle_judgment_by_council_all_winners(i: u32) -> Weight;
+    fn submit_oracle_judgment_by_council_all_rejected(i: u32) -> Weight;
+    fn submit_oracle_judgment_by_member_all_winners(i: u32) -> Weight;
+    fn submit_oracle_judgment_by_member_all_rejected(i: u32) -> Weight;
+    fn withdraw_work_entrant_funds() -> Weight;
+}
+
+type WeightInfoBounty<T> = <T as Trait>::WeightInfo;
+
+pub(crate) use actors::BountyActorManager;
+pub(crate) use stages::BountyStageCalculator;
+
+use codec::{Decode, Encode};
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+
+use frame_support::dispatch::{DispatchError, DispatchResult};
+use frame_support::traits::{Currency, ExistenceRequirement, Get};
+use frame_support::weights::Weight;
+use frame_support::{decl_error, decl_event, decl_module, decl_storage, ensure, Parameter};
+use frame_system::ensure_root;
+use sp_arithmetic::traits::{One, Saturating, Zero};
+use sp_runtime::{traits::AccountIdConversion, ModuleId};
+use sp_runtime::{Perbill, SaturatedConversion};
+use sp_std::collections::btree_map::BTreeMap;
+use sp_std::collections::btree_set::BTreeSet;
+use sp_std::vec::Vec;
+
+use common::council::CouncilBudgetManager;
+use common::membership::{
+    MemberId, MemberOriginValidator, MembershipInfoProvider, StakingAccountValidator,
+};
+use staking_handler::StakingHandler;
+
+/// Main pallet-bounty trait.
+pub trait Trait: frame_system::Trait + balances::Trait + common::membership::Trait {
+    /// Events
+    type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
+
+    /// The bounty's module id, used for deriving its sovereign account ID.
+    type ModuleId: Get<ModuleId>;
+
+    /// Bounty Id type
+    type BountyId: From<u32> + Parameter + Default + Copy;
+
+    /// Validates staking account ownership for a member, member ID and origin combination and
+    /// providers controller id for a member.
+    type Membership: StakingAccountValidator<Self>
+        + MembershipInfoProvider<Self>
+        + MemberOriginValidator<Self::Origin, MemberId<Self>, Self::AccountId>;
+
+    /// Weight information for extrinsics in this pallet.
+    type WeightInfo: WeightInfo;
+
+    /// Provides an access for the council budget.
+    type CouncilBudgetManager: CouncilBudgetManager<BalanceOf<Self>>;
+
+    /// Provides stake logic implementation.
+    type StakingHandler: StakingHandler<Self::AccountId, BalanceOf<Self>, MemberId<Self>>;
+
+    /// Work entry Id type
+    type EntryId: From<u32> + Parameter + Default + Copy + Ord + One;
+
+    /// Defines max work entry number for a closed assurance type contract bounty.
+    type ClosedContractSizeLimit: Get<u32>;
+
+    /// Defines min cherry for a bounty.
+    type MinCherryLimit: Get<BalanceOf<Self>>;
+
+    /// Defines min funding amount for a bounty.
+    type MinFundingLimit: Get<BalanceOf<Self>>;
+
+    /// Defines min work entrant stake for a bounty.
+    type MinWorkEntrantStake: Get<BalanceOf<Self>>;
+}
+
+/// Alias type for the BountyParameters.
+pub type BountyCreationParameters<T> = BountyParameters<
+    BalanceOf<T>,
+    <T as frame_system::Trait>::BlockNumber,
+    <T as common::membership::Trait>::MemberId,
+>;
+
+/// Defines who can submit the work.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
+pub enum AssuranceContractType<MemberId: Ord> {
+    /// Anyone can submit the work.
+    Open,
+
+    /// Only specific members can submit the work.
+    Closed(BTreeSet<MemberId>),
+}
+
+impl<MemberId: Ord> Default for AssuranceContractType<MemberId> {
+    fn default() -> Self {
+        AssuranceContractType::Open
+    }
+}
+
+/// Defines funding conditions.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
+pub enum FundingType<BlockNumber, Balance> {
+    /// Funding has no time limits.
+    Perpetual {
+        /// Desired funding.
+        target: Balance,
+    },
+
+    /// Funding has a time limitation.
+    Limited {
+        /// Minimum amount of funds for a successful bounty.
+        min_funding_amount: Balance,
+
+        /// Upper boundary for a bounty funding.
+        max_funding_amount: Balance,
+
+        /// Maximum allowed funding period.
+        funding_period: BlockNumber,
+    },
+}
+
+impl<BlockNumber, Balance: Default> Default for FundingType<BlockNumber, Balance> {
+    fn default() -> Self {
+        Self::Perpetual {
+            target: Default::default(),
+        }
+    }
+}
+
+/// Defines parameters for the bounty creation.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct BountyParameters<Balance, BlockNumber, MemberId: Ord> {
+    /// Origin that will select winner(s), is either a given member or a council.
+    pub oracle: BountyActor<MemberId>,
+
+    /// Contract type defines who can submit the work.
+    pub contract_type: AssuranceContractType<MemberId>,
+
+    /// Bounty creator: could be a member or a council.
+    pub creator: BountyActor<MemberId>,
+
+    /// An amount of funding provided by the creator which will be split among all other
+    /// contributors should the bounty not be successful. If successful, cherry is returned to
+    /// the creator. When council is creating bounty, this comes out of their budget, when a member
+    /// does it, it comes from an account.
+    pub cherry: Balance,
+
+    /// Amount of stake required to enter bounty as entrant.
+    pub entrant_stake: Balance,
+
+    /// Defines parameters for different funding types.
+    pub funding_type: FundingType<BlockNumber, Balance>,
+
+    /// Number of blocks from end of funding period until people can no longer submit
+    /// bounty submissions.
+    pub work_period: BlockNumber,
+
+    /// Number of block from end of work period until oracle can no longer decide winners.
+    pub judging_period: BlockNumber,
+}
+
+/// Bounty actor to perform operations for a bounty.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
+pub enum BountyActor<MemberId> {
+    /// Council performs operations for a bounty.
+    Council,
+
+    /// Member performs operations for a bounty.
+    Member(MemberId),
+}
+
+impl<MemberId> Default for BountyActor<MemberId> {
+    fn default() -> Self {
+        BountyActor::Council
+    }
+}
+
+/// Defines current bounty stage.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug, Copy)]
+pub enum BountyStage {
+    /// Bounty funding stage.
+    Funding {
+        /// Bounty has already some contributions.
+        has_contributions: bool,
+    },
+
+    /// Bounty funding period expired with no contributions.
+    FundingExpired,
+
+    /// A bounty has gathered necessary funds and ready to accept work submissions.
+    WorkSubmission,
+
+    /// Working periods ended and the oracle should provide their judgment.
+    Judgment,
+
+    /// Indicates a withdrawal on bounty success. Workers get rewards and their stake.
+    SuccessfulBountyWithdrawal,
+
+    /// Indicates a withdrawal on bounty failure. Workers get their stake back. Funders
+    /// get their contribution back as well as part of the cherry.
+    FailedBountyWithdrawal,
+}
+
+/// Defines current bounty state.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
+pub enum BountyMilestone<BlockNumber> {
+    /// Bounty was created at given block number.
+    /// Boolean value defines whether the bounty has some funding contributions.
+    Created {
+        /// Bounty creation block.
+        created_at: BlockNumber,
+        /// Bounty has already some contributions.
+        has_contributions: bool,
+    },
+
+    /// A bounty funding was successful and it exceeded max funding amount.
+    BountyMaxFundingReached {
+        ///  A bounty funding was successful on the provided block.
+        max_funding_reached_at: BlockNumber,
+    },
+
+    /// Some work was submitted for a bounty.
+    WorkSubmitted {
+        ///  Starting block for the work period.
+        work_period_started_at: BlockNumber,
+    },
+
+    /// A judgment was submitted for a bounty.
+    JudgmentSubmitted {
+        /// The bounty judgment contains at least a single winner.
+        successful_bounty: bool,
+    },
+}
+
+impl<BlockNumber: Default> Default for BountyMilestone<BlockNumber> {
+    fn default() -> Self {
+        BountyMilestone::Created {
+            created_at: Default::default(),
+            has_contributions: false,
+        }
+    }
+}
+
+/// Alias type for the Bounty.
+pub type Bounty<T> = BountyRecord<
+    BalanceOf<T>,
+    <T as frame_system::Trait>::BlockNumber,
+    <T as common::membership::Trait>::MemberId,
+>;
+
+/// Crowdfunded bounty record.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct BountyRecord<Balance, BlockNumber, MemberId: Ord> {
+    /// Bounty creation parameters.
+    pub creation_params: BountyParameters<Balance, BlockNumber, MemberId>,
+
+    /// Total funding balance reached so far.
+    /// Includes initial funding by a creator and other members funding.
+    pub total_funding: Balance,
+
+    /// Bounty current milestone(state). It represents fact known about the bounty, eg.:
+    /// it was canceled or max funding amount was reached.
+    pub milestone: BountyMilestone<BlockNumber>,
+
+    /// Current active work entry counter.
+    pub active_work_entry_count: u32,
+}
+
+impl<Balance: PartialOrd + Clone, BlockNumber: Clone, MemberId: Ord>
+    BountyRecord<Balance, BlockNumber, MemberId>
+{
+    // Increments bounty active work entry counter.
+    fn increment_active_work_entry_counter(&mut self) {
+        self.active_work_entry_count += 1;
+    }
+
+    // Decrements bounty active work entry counter. Nothing happens on zero counter.
+    fn decrement_active_work_entry_counter(&mut self) {
+        if self.active_work_entry_count > 0 {
+            self.active_work_entry_count -= 1;
+        }
+    }
+
+    // Defines whether the maximum funding amount will be reached for the current funding type.
+    fn is_maximum_funding_reached(&self, total_funding: Balance) -> bool {
+        match self.creation_params.funding_type {
+            FundingType::Perpetual { ref target } => total_funding >= *target,
+            FundingType::Limited {
+                ref max_funding_amount,
+                ..
+            } => total_funding >= *max_funding_amount,
+        }
+    }
+
+    // Returns the maximum funding amount for the current funding type.
+    pub(crate) fn maximum_funding(&self) -> Balance {
+        match self.creation_params.funding_type.clone() {
+            FundingType::Perpetual { target } => target,
+            FundingType::Limited {
+                max_funding_amount, ..
+            } => max_funding_amount,
+        }
+    }
+}
+
+/// Alias type for the Entry.
+pub type Entry<T> = EntryRecord<
+    <T as frame_system::Trait>::AccountId,
+    <T as common::membership::Trait>::MemberId,
+    <T as frame_system::Trait>::BlockNumber,
+    BalanceOf<T>,
+>;
+
+/// Work entry.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct EntryRecord<AccountId, MemberId, BlockNumber, Balance> {
+    /// Work entrant member ID.
+    pub member_id: MemberId,
+
+    /// Account ID for staking lock.
+    pub staking_account_id: AccountId,
+
+    /// Work entry submission block.
+    pub submitted_at: BlockNumber,
+
+    /// Signifies that an entry has at least one submitted work.
+    pub work_submitted: bool,
+
+    /// Optional oracle judgment for the work entry.
+    /// Absent value means neither winner nor rejected entry - "legitimate user" that gets their
+    /// stake back without slashing but doesn't get a reward.
+    pub oracle_judgment_result: Option<OracleWorkEntryJudgment<Balance>>,
+}
+
+/// Defines the oracle judgment for the work entry.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug, Copy)]
+pub enum OracleWorkEntryJudgment<Balance> {
+    /// The work entry is selected as a winner.
+    Winner { reward: Balance },
+
+    /// The work entry is considered harmful. The stake will be slashed.
+    Rejected,
+}
+
+impl<Balance> Default for OracleWorkEntryJudgment<Balance> {
+    fn default() -> Self {
+        Self::Rejected
+    }
+}
+
+impl<Balance> OracleWorkEntryJudgment<Balance> {
+    // Work entry judgment helper. Returns true for winners.
+    pub(crate) fn is_winner(&self) -> bool {
+        matches!(*self, Self::Winner { .. })
+    }
+}
+
+/// Balance alias for `balances` module.
+pub type BalanceOf<T> = <T as balances::Trait>::Balance;
+
+// Entrant stake helper struct.
+struct RequiredStakeInfo<T: Trait> {
+    // stake amount
+    amount: BalanceOf<T>,
+    // staking_account_id
+    account_id: T::AccountId,
+}
+
+/// An alias for the OracleJudgment.
+pub type OracleJudgmentOf<T> = OracleJudgment<<T as Trait>::EntryId, BalanceOf<T>>;
+
+/// The collection of the oracle judgments for the work entries.
+pub type OracleJudgment<EntryId, Balance> = BTreeMap<EntryId, OracleWorkEntryJudgment<Balance>>;
+
+decl_storage! {
+    trait Store for Module<T: Trait> as Bounty {
+        /// Bounty storage.
+        pub Bounties get(fn bounties) : map hasher(blake2_128_concat) T::BountyId => Bounty<T>;
+
+        /// Double map for bounty funding. It stores a member or council funding for bounties.
+        pub BountyContributions get(fn contribution_by_bounty_by_actor): double_map
+            hasher(blake2_128_concat) T::BountyId,
+            hasher(blake2_128_concat) BountyActor<MemberId<T>> => BalanceOf<T>;
+
+        /// Count of all bounties that have been created.
+        pub BountyCount get(fn bounty_count): u32;
+
+        /// Work entry storage map.
+        pub Entries get(fn entries): map hasher(blake2_128_concat) T::EntryId => Entry<T>;
+
+        /// Count of all work entries that have been created.
+        pub EntryCount get(fn entry_count): u32;
+    }
+}
+
+decl_event! {
+    pub enum Event<T>
+    where
+        <T as Trait>::BountyId,
+        <T as Trait>::EntryId,
+        Balance = BalanceOf<T>,
+        MemberId = MemberId<T>,
+        <T as frame_system::Trait>::AccountId,
+        BountyCreationParameters = BountyCreationParameters<T>,
+        OracleJudgment = OracleJudgmentOf<T>,
+    {
+        /// A bounty was created.
+        /// Params:
+        /// - bounty ID
+        /// - creation parameters
+        /// - bounty metadata
+        BountyCreated(BountyId, BountyCreationParameters, Vec<u8>),
+
+        /// A bounty was canceled.
+        /// Params:
+        /// - bounty ID
+        /// - bounty creator
+        BountyCanceled(BountyId, BountyActor<MemberId>),
+
+        /// A bounty was vetoed.
+        /// Params:
+        /// - bounty ID
+        BountyVetoed(BountyId),
+
+        /// A bounty was funded by a member or a council.
+        /// Params:
+        /// - bounty ID
+        /// - bounty funder
+        /// - funding amount
+        BountyFunded(BountyId, BountyActor<MemberId>, Balance),
+
+        /// A bounty has reached its maximum funding amount.
+        /// Params:
+        /// - bounty ID
+        BountyMaxFundingReached(BountyId),
+
+        /// A member or a council has withdrawn the funding.
+        /// Params:
+        /// - bounty ID
+        /// - bounty funder
+        BountyFundingWithdrawal(BountyId, BountyActor<MemberId>),
+
+        /// A bounty creator has withdrawn the cherry (member or council).
+        /// Params:
+        /// - bounty ID
+        /// - bounty creator
+        BountyCreatorCherryWithdrawal(BountyId, BountyActor<MemberId>),
+
+        /// A bounty was removed.
+        /// Params:
+        /// - bounty ID
+        BountyRemoved(BountyId),
+
+        /// Work entry was announced.
+        /// Params:
+        /// - bounty ID
+        /// - created entry ID
+        /// - entrant member ID
+        /// - staking account ID
+        WorkEntryAnnounced(BountyId, EntryId, MemberId, AccountId),
+
+        /// Work entry was withdrawn.
+        /// Params:
+        /// - bounty ID
+        /// - entry ID
+        /// - entrant member ID
+        WorkEntryWithdrawn(BountyId, EntryId, MemberId),
+
+        /// Work entry was slashed.
+        /// Params:
+        /// - bounty ID
+        /// - entry ID
+        WorkEntrySlashed(BountyId, EntryId),
+
+        /// Submit work.
+        /// Params:
+        /// - bounty ID
+        /// - created entry ID
+        /// - entrant member ID
+        /// - work data (description, URL, BLOB, etc.)
+        WorkSubmitted(BountyId, EntryId, MemberId, Vec<u8>),
+
+        /// Submit oracle judgment.
+        /// Params:
+        /// - bounty ID
+        /// - oracle
+        /// - judgment data
+        OracleJudgmentSubmitted(BountyId, BountyActor<MemberId>, OracleJudgment),
+
+        /// Work entry was slashed.
+        /// Params:
+        /// - bounty ID
+        /// - entry ID
+        /// - entrant member ID
+        WorkEntrantFundsWithdrawn(BountyId, EntryId, MemberId),
+    }
+}
+
+decl_error! {
+    /// Bounty pallet predefined errors
+    pub enum Error for Module<T: Trait> {
+        /// Min funding amount cannot be greater than max amount.
+        MinFundingAmountCannotBeGreaterThanMaxAmount,
+
+        /// Bounty doesnt exist.
+        BountyDoesntExist,
+
+        /// Operation can be performed only by a bounty creator.
+        NotBountyActor,
+
+        /// Work period cannot be zero.
+        WorkPeriodCannotBeZero,
+
+        /// Judging period cannot be zero.
+        JudgingPeriodCannotBeZero,
+
+        /// Unexpected bounty stage for an operation: Funding.
+        InvalidStageUnexpectedFunding,
+
+        /// Unexpected bounty stage for an operation: FundingExpired.
+        InvalidStageUnexpectedFundingExpired,
+
+        /// Unexpected bounty stage for an operation: WorkSubmission.
+        InvalidStageUnexpectedWorkSubmission,
+
+        /// Unexpected bounty stage for an operation: Judgment.
+        InvalidStageUnexpectedJudgment,
+
+        /// Unexpected bounty stage for an operation: SuccessfulBountyWithdrawal.
+        InvalidStageUnexpectedSuccessfulBountyWithdrawal,
+
+        /// Unexpected bounty stage for an operation: FailedBountyWithdrawal.
+        InvalidStageUnexpectedFailedBountyWithdrawal,
+
+        /// Insufficient balance for a bounty cherry.
+        InsufficientBalanceForBounty,
+
+        /// Funding period is not expired for the bounty.
+        FundingPeriodNotExpired,
+
+        /// Cannot found bounty contribution.
+        NoBountyContributionFound,
+
+        /// There is nothing to withdraw.
+        NothingToWithdraw,
+
+        /// Incorrect funding amount.
+        ZeroFundingAmount,
+
+        /// There is not enough balance for a stake.
+        InsufficientBalanceForStake,
+
+        /// The conflicting stake discovered. Cannot stake.
+        ConflictingStakes,
+
+        /// Work entry doesnt exist.
+        WorkEntryDoesntExist,
+
+        /// Cannot add work entry because of the limit.
+        MaxWorkEntryLimitReached,
+
+        /// Cherry less then minimum allowed.
+        CherryLessThenMinimumAllowed,
+
+        /// Funding amount less then minimum allowed.
+        FundingLessThenMinimumAllowed,
+
+        /// Incompatible assurance contract type for a member: cannot submit work to the 'closed
+        /// assurance' bounty contract.
+        CannotSubmitWorkToClosedContractBounty,
+
+        /// Cannot create a 'closed assurance contract' bounty with empty member list.
+        ClosedContractMemberListIsEmpty,
+
+        /// Cannot create a 'closed assurance contract' bounty with member list larger
+        /// than allowed max work entry limit.
+        ClosedContractMemberListIsTooLarge,
+
+        /// Staking account doesn't belong to a member.
+        InvalidStakingAccountForMember,
+
+        /// Cannot set zero reward for winners.
+        ZeroWinnerReward,
+
+        /// The total reward for winners should be equal to total bounty funding.
+        TotalRewardShouldBeEqualToTotalFunding,
+
+        /// Cannot create a bounty with an entrant stake is less than required minimum.
+        EntrantStakeIsLessThanMininum,
+
+        /// Cannot create a bounty with zero funding amount parameter.
+        FundingAmountCannotBeZero,
+
+        /// Cannot create a bounty with zero funding period parameter.
+        FundingPeriodCannotBeZero,
+
+        /// Cannot submit a judgment without active work entries. A probable case for an error:
+        /// an entry with a single submission for a bounty was withdrawn.
+        NoActiveWorkEntries,
+
+        /// Invalid judgment - all winners should have work submissions.
+        WinnerShouldHasWorkSubmission,
+    }
+}
+
+decl_module! {
+    /// Bounty pallet Substrate Module
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+        /// Predefined errors
+        type Error = Error<T>;
+
+        /// Emits an event. Default substrate implementation.
+        fn deposit_event() = default;
+
+        /// Exports const - max work entry number for a closed assurance type contract bounty.
+        const ClosedContractSizeLimit: u32 = T::ClosedContractSizeLimit::get();
+
+        /// Exports const - min cherry value limit for a bounty.
+        const MinCherryLimit: BalanceOf<T> = T::MinCherryLimit::get();
+
+        /// Exports const - min funding amount limit for a bounty.
+        const MinFundingLimit: BalanceOf<T> = T::MinFundingLimit::get();
+
+        /// Exports const - min work entrant stake for a bounty.
+        const MinWorkEntrantStake: BalanceOf<T> = T::MinWorkEntrantStake::get();
+
+        /// Creates a bounty. Metadata stored in the transaction log but discarded after that.
+        /// <weight>
+        ///
+        /// ## Weight
+        /// `O (W)` where:
+        /// - `W` is the _metadata length.
+        /// - `M` is closed contract member list length.
+        /// - DB:
+        ///    - O(M) (O(1) on open contract)
+        /// # </weight>
+        #[weight = Module::<T>::create_bounty_weight(&params, &metadata)]
+        pub fn create_bounty(origin, params: BountyCreationParameters<T>, metadata: Vec<u8>) {
+            let bounty_creator_manager = BountyActorManager::<T>::ensure_bounty_actor_manager(
+                origin,
+                params.creator.clone()
+            )?;
+
+            Self::ensure_create_bounty_parameters_valid(&params)?;
+
+            bounty_creator_manager.validate_balance_sufficiency(params.cherry)?;
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            let next_bounty_count_value = Self::bounty_count() + 1;
+            let bounty_id = T::BountyId::from(next_bounty_count_value);
+
+            bounty_creator_manager.transfer_funds_to_bounty_account(bounty_id, params.cherry)?;
+
+            let created_bounty_milestone = BountyMilestone::Created {
+                created_at: Self::current_block(),
+                has_contributions: false, // just created - no contributions
+            };
+
+            let bounty = Bounty::<T> {
+                total_funding: Zero::zero(),
+                creation_params: params.clone(),
+                milestone: created_bounty_milestone,
+                active_work_entry_count: 0,
+            };
+
+            <Bounties<T>>::insert(bounty_id, bounty);
+            BountyCount::mutate(|count| {
+                *count = next_bounty_count_value
+            });
+            Self::deposit_event(RawEvent::BountyCreated(bounty_id, params, metadata));
+        }
+
+        /// Cancels a bounty.
+        /// It returns a cherry to creator and removes bounty.
+        /// # <weight>
+        ///
+        /// ## weight
+        /// `O (1)`
+        /// - db:
+        ///    - `O(1)` doesn't depend on the state or parameters
+        /// # </weight>
+        #[weight = WeightInfoBounty::<T>::cancel_bounty_by_member()
+              .max(WeightInfoBounty::<T>::cancel_bounty_by_council())]
+        pub fn cancel_bounty(origin, creator: BountyActor<MemberId<T>>, bounty_id: T::BountyId) {
+            let bounty_creator_manager = BountyActorManager::<T>::ensure_bounty_actor_manager(
+                origin,
+                creator.clone(),
+            )?;
+
+            let bounty = Self::ensure_bounty_exists(&bounty_id)?;
+
+            bounty_creator_manager.validate_actor(&bounty.creation_params.creator)?;
+
+            let current_bounty_stage = Self::get_bounty_stage(&bounty);
+
+            Self::ensure_bounty_stage_for_canceling(current_bounty_stage)?;
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            Self::return_bounty_cherry_to_creator(bounty_id, &bounty)?;
+
+            Self::remove_bounty(&bounty_id);
+
+            Self::deposit_event(RawEvent::BountyCanceled(bounty_id, creator));
+        }
+
+        /// Vetoes a bounty.
+        /// It returns a cherry to creator and removes bounty.
+        /// # <weight>
+        ///
+        /// ## weight
+        /// `O (1)`
+        /// - db:
+        ///    - `O(1)` doesn't depend on the state or parameters
+        /// # </weight>
+        #[weight = WeightInfoBounty::<T>::veto_bounty()]
+        pub fn veto_bounty(origin, bounty_id: T::BountyId) {
+            ensure_root(origin)?;
+
+            let bounty = Self::ensure_bounty_exists(&bounty_id)?;
+
+            let current_bounty_stage = Self::get_bounty_stage(&bounty);
+
+            Self::ensure_bounty_stage(
+                current_bounty_stage,
+                BountyStage::Funding { has_contributions: false }
+            )?;
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            Self::return_bounty_cherry_to_creator(bounty_id, &bounty)?;
+
+            Self::remove_bounty(&bounty_id);
+
+            Self::deposit_event(RawEvent::BountyVetoed(bounty_id));
+        }
+
+        /// Provides bounty funding.
+        /// # <weight>
+        ///
+        /// ## weight
+        /// `O (1)`
+        /// - db:
+        ///    - `O(1)` doesn't depend on the state or parameters
+        /// # </weight>
+        #[weight = WeightInfoBounty::<T>::fund_bounty_by_member()
+              .max(WeightInfoBounty::<T>::fund_bounty_by_council())]
+        pub fn fund_bounty(
+            origin,
+            funder: BountyActor<MemberId<T>>,
+            bounty_id: T::BountyId,
+            amount: BalanceOf<T>
+        ) {
+            let bounty_funder_manager = BountyActorManager::<T>::ensure_bounty_actor_manager(
+                origin,
+                funder.clone(),
+            )?;
+
+            let bounty = Self::ensure_bounty_exists(&bounty_id)?;
+
+            ensure!(amount > Zero::zero(), Error::<T>::ZeroFundingAmount);
+
+            ensure!(amount >= T::MinFundingLimit::get(), Error::<T>::FundingLessThenMinimumAllowed);
+
+            bounty_funder_manager.validate_balance_sufficiency(amount)?;
+
+            let current_bounty_stage = Self::get_bounty_stage(&bounty);
+            ensure!(
+                matches!(current_bounty_stage, BountyStage::Funding{..}),
+                Self::unexpected_bounty_stage_error(current_bounty_stage),
+            );
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            let maximum_funding_reached = bounty.is_maximum_funding_reached(
+                bounty.total_funding.saturating_add(amount)
+            );
+
+            //
+            let actual_funding = if maximum_funding_reached {
+                bounty.maximum_funding().saturating_sub(bounty.total_funding)
+            } else {
+                amount
+            };
+
+            bounty_funder_manager.transfer_funds_to_bounty_account(bounty_id, actual_funding)?;
+
+
+            let new_milestone = Self::get_bounty_milestone_on_funding(
+                    maximum_funding_reached,
+                    bounty.milestone
+            );
+
+            // Update bounty record.
+            <Bounties<T>>::mutate(bounty_id, |bounty| {
+                bounty.total_funding = bounty.total_funding.saturating_add(actual_funding);
+                bounty.milestone = new_milestone;
+            });
+
+            // Update member funding record checking previous funding.
+            let funds_so_far = Self::contribution_by_bounty_by_actor(bounty_id, &funder);
+            let total_funding = funds_so_far.saturating_add(actual_funding);
+            <BountyContributions<T>>::insert(bounty_id, funder.clone(), total_funding);
+
+            // Fire events.
+            Self::deposit_event(RawEvent::BountyFunded(bounty_id, funder, actual_funding));
+            if  maximum_funding_reached{
+                Self::deposit_event(RawEvent::BountyMaxFundingReached(bounty_id));
+            }
+        }
+
+        /// Withdraw bounty funding by a member or a council.
+        /// # <weight>
+        ///
+        /// ## weight
+        /// `O (1)`
+        /// - db:
+        ///    - `O(1)` doesn't depend on the state or parameters
+        /// # </weight>
+        #[weight = WeightInfoBounty::<T>::withdraw_funding_by_member()
+              .max(WeightInfoBounty::<T>::withdraw_funding_by_council())]
+        pub fn withdraw_funding(
+            origin,
+            funder: BountyActor<MemberId<T>>,
+            bounty_id: T::BountyId,
+        ) {
+            let bounty_funder_manager = BountyActorManager::<T>::ensure_bounty_actor_manager(
+                origin,
+                funder.clone(),
+            )?;
+
+            let bounty = Self::ensure_bounty_exists(&bounty_id)?;
+
+            let current_bounty_stage = Self::get_bounty_stage(&bounty);
+            Self::ensure_bounty_stage(current_bounty_stage, BountyStage::FailedBountyWithdrawal)?;
+
+            ensure!(
+                <BountyContributions<T>>::contains_key(&bounty_id, &funder),
+                Error::<T>::NoBountyContributionFound,
+            );
+
+            let funding_amount = <BountyContributions<T>>::get(&bounty_id, &funder);
+            let cherry_fraction = Self::get_cherry_fraction_for_member(&bounty, funding_amount);
+            let withdrawal_amount = funding_amount + cherry_fraction;
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            bounty_funder_manager.transfer_funds_from_bounty_account(bounty_id, withdrawal_amount)?;
+
+            <BountyContributions<T>>::remove(&bounty_id, &funder);
+
+            Self::deposit_event(RawEvent::BountyFundingWithdrawal(bounty_id, funder));
+
+            if Self::withdrawal_completed(&current_bounty_stage, &bounty_id) {
+                Self::remove_bounty(&bounty_id);
+            }
+        }
+
+
+        /// Announce work entry for a successful bounty.
+        /// # <weight>
+        ///
+        /// ## weight
+        /// `O (1)`
+        /// - db:
+        ///    - `O(1)` doesn't depend on the state or parameters
+        /// # </weight>
+        #[weight = WeightInfoBounty::<T>::announce_work_entry(T::ClosedContractSizeLimit::get()
+            .saturated_into())]
+        pub fn announce_work_entry(
+            origin,
+            member_id: MemberId<T>,
+            bounty_id: T::BountyId,
+            staking_account_id: T::AccountId,
+        ) {
+            T::Membership::ensure_member_controller_account_origin(origin, member_id)?;
+
+            let bounty = Self::ensure_bounty_exists(&bounty_id)?;
+
+            let current_bounty_stage = Self::get_bounty_stage(&bounty);
+
+            Self::ensure_bounty_stage(current_bounty_stage, BountyStage::WorkSubmission)?;
+
+            let stake = Self::validate_entrant_stake(
+                member_id,
+                &bounty,
+                staking_account_id.clone()
+            )?;
+
+            Self::ensure_valid_contract_type(&bounty, &member_id)?;
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            let next_entry_count_value = Self::entry_count() + 1;
+            let entry_id = T::EntryId::from(next_entry_count_value);
+
+            // Lock stake balance for bounty if the stake is required.
+            if let Some(stake) = stake {
+                T::StakingHandler::lock(&stake.account_id, stake.amount);
+            }
+
+            let entry = Entry::<T> {
+                member_id,
+                staking_account_id: staking_account_id.clone(),
+                submitted_at: Self::current_block(),
+                work_submitted: false,
+                oracle_judgment_result: None,
+            };
+
+            <Entries<T>>::insert(entry_id, entry);
+            EntryCount::mutate(|count| {
+                *count = next_entry_count_value
+            });
+
+            // Increment work entry counter and update bounty record.
+            <Bounties<T>>::mutate(bounty_id, |bounty| {
+                bounty.increment_active_work_entry_counter();
+            });
+
+            Self::deposit_event(RawEvent::WorkEntryAnnounced(
+                bounty_id,
+                entry_id,
+                member_id,
+                staking_account_id,
+            ));
+        }
+
+        /// Withdraw work entry for a bounty. Existing stake could be partially slashed.
+        /// # <weight>
+        ///
+        /// ## weight
+        /// `O (1)`
+        /// - db:
+        ///    - `O(1)` doesn't depend on the state or parameters
+        /// # </weight>
+        #[weight = WeightInfoBounty::<T>::withdraw_work_entry()]
+        pub fn withdraw_work_entry(
+            origin,
+            member_id: MemberId<T>,
+            bounty_id: T::BountyId,
+            entry_id: T::EntryId,
+        ) {
+            T::Membership::ensure_member_controller_account_origin(origin, member_id)?;
+
+            let bounty = Self::ensure_bounty_exists(&bounty_id)?;
+
+            let current_bounty_stage = Self::get_bounty_stage(&bounty);
+
+            Self::ensure_bounty_stage(current_bounty_stage, BountyStage::WorkSubmission)?;
+
+            let entry = Self::ensure_work_entry_exists(&entry_id)?;
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            Self::unlock_work_entry_stake_with_possible_penalty(&bounty, &entry);
+
+            Self::remove_work_entry(&bounty_id, &entry_id);
+
+            Self::deposit_event(RawEvent::WorkEntryWithdrawn(bounty_id, entry_id, member_id));
+        }
+
+        /// Submit work for a bounty.
+        /// # <weight>
+        ///
+        /// ## weight
+        /// `O (N)`
+        /// - `N` is the work_data length,
+        /// - db:
+        ///    - `O(1)` doesn't depend on the state or parameters
+        /// # </weight>
+        #[weight =  WeightInfoBounty::<T>::submit_work(work_data.len().saturated_into())]
+        pub fn submit_work(
+            origin,
+            member_id: MemberId<T>,
+            bounty_id: T::BountyId,
+            entry_id: T::EntryId,
+            work_data: Vec<u8>
+        ) {
+            T::Membership::ensure_member_controller_account_origin(origin, member_id)?;
+
+            let bounty = Self::ensure_bounty_exists(&bounty_id)?;
+
+            let current_bounty_stage = Self::get_bounty_stage(&bounty);
+
+            Self::ensure_bounty_stage(current_bounty_stage, BountyStage::WorkSubmission)?;
+
+            Self::ensure_work_entry_exists(&entry_id)?;
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            // Update entry
+            <Entries<T>>::mutate(entry_id, |entry| {
+                entry.work_submitted = true;
+            });
+
+            let new_milestone = Self::get_bounty_milestone_on_work_submitting(&bounty);
+
+            // Update bounty record.
+            <Bounties<T>>::mutate(bounty_id, |bounty| {
+                bounty.milestone = new_milestone;
+            });
+
+            Self::deposit_event(RawEvent::WorkSubmitted(bounty_id, entry_id, member_id, work_data));
+        }
+
+        /// Submits an oracle judgment for a bounty.
+        /// # <weight>
+        ///
+        /// ## weight
+        /// `O (N)`
+        /// - `N` is the work_data length,
+        /// - db:
+        ///    - `O(N)`
+        /// # </weight>
+        #[weight = Module::<T>::submit_oracle_judgement_weight(&judgment)]
+        pub fn submit_oracle_judgment(
+            origin,
+            oracle: BountyActor<MemberId<T>>,
+            bounty_id: T::BountyId,
+            judgment: OracleJudgment<T::EntryId, BalanceOf<T>>
+        ) {
+            let bounty_oracle_manager = BountyActorManager::<T>::ensure_bounty_actor_manager(
+                origin,
+                oracle.clone(),
+            )?;
+
+            let bounty = Self::ensure_bounty_exists(&bounty_id)?;
+
+            bounty_oracle_manager.validate_actor(&bounty.creation_params.oracle)?;
+
+            let current_bounty_stage = Self::get_bounty_stage(&bounty);
+
+            Self::ensure_bounty_stage(current_bounty_stage, BountyStage::Judgment)?;
+
+            ensure!(bounty.active_work_entry_count != 0, Error::<T>::NoActiveWorkEntries);
+
+            Self::validate_judgment(&bounty, &judgment)?;
+
+            // Lookup for any winners in the judgment.
+            let successful_bounty = Self::judgment_has_winners(&judgment);
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            // Return a cherry to a creator.
+            if successful_bounty {
+                Self::return_bounty_cherry_to_creator(bounty_id, &bounty)?;
+            }
+
+            // Update bounty record.
+            <Bounties<T>>::mutate(bounty_id, |bounty| {
+                bounty.milestone = BountyMilestone::JudgmentSubmitted {
+                    successful_bounty
+                };
+            });
+
+            // Judgments triage.
+            for (entry_id, work_entry_judgment) in judgment.iter() {
+                // Update work entries for winners.
+                if matches!(*work_entry_judgment, OracleWorkEntryJudgment::Winner{ .. }) {
+                    <Entries<T>>::mutate(entry_id, |entry| {
+                        entry.oracle_judgment_result = Some(*work_entry_judgment);
+                    });
+                } else {
+                    let entry = Self::entries(entry_id);
+
+                    Self::slash_work_entry_stake(&entry);
+
+                    Self::remove_work_entry(&bounty_id, &entry_id);
+
+                    Self::deposit_event(RawEvent::WorkEntrySlashed(bounty_id, *entry_id));
+                }
+            }
+
+            // Fire a judgment event.
+            Self::deposit_event(RawEvent::OracleJudgmentSubmitted(bounty_id, oracle, judgment));
+        }
+
+        /// Withdraw work entrant funds.
+        /// Both legitimate participants and winners get their stake unlocked. Winners also get a
+        /// bounty reward.
+        /// # <weight>
+        ///
+        /// ## weight
+        /// `O (1)`
+        /// - db:
+        ///    - `O(1)` doesn't depend on the state or parameters
+        /// # </weight>
+        #[weight = WeightInfoBounty::<T>::withdraw_work_entrant_funds()]
+        pub fn withdraw_work_entrant_funds(
+            origin,
+            member_id: MemberId<T>,
+            bounty_id: T::BountyId,
+            entry_id: T::EntryId,
+        ) {
+            let controller_account_id =
+                T::Membership::ensure_member_controller_account_origin(origin, member_id)?;
+
+            let bounty = Self::ensure_bounty_exists(&bounty_id)?;
+
+            let current_bounty_stage = Self::get_bounty_stage(&bounty);
+
+            // Ensure withdrawal for successful or failed bounty.
+            ensure!(
+                current_bounty_stage == BountyStage::FailedBountyWithdrawal ||
+                current_bounty_stage == BountyStage::SuccessfulBountyWithdrawal,
+                Self::unexpected_bounty_stage_error(current_bounty_stage)
+            );
+
+            let entry = Self::ensure_work_entry_exists(&entry_id)?;
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            // Claim the winner reward.
+            if let Some(OracleWorkEntryJudgment::Winner { reward }) = entry.oracle_judgment_result {
+                Self::transfer_funds_from_bounty_account(
+                    &controller_account_id,
+                    bounty_id,
+                    reward
+                )?;
+            }
+
+            // Unstake the full work entry state.
+            T::StakingHandler::unlock(&entry.staking_account_id);
+
+            // Delete the work entry record from the storage.
+            Self::remove_work_entry(&bounty_id, &entry_id);
+
+            // Fire an event.
+            Self::deposit_event(RawEvent::WorkEntrantFundsWithdrawn(bounty_id, entry_id, member_id));
+
+            // Remove the bounty in case of the last withdrawal operation.
+            if Self::withdrawal_completed(&current_bounty_stage, &bounty_id) {
+                Self::remove_bounty(&bounty_id);
+            }
+        }
+    }
+}
+
+impl<T: Trait> Module<T> {
+    // Wrapper-function over System::block_number()
+    pub(crate) fn current_block() -> T::BlockNumber {
+        <frame_system::Module<T>>::block_number()
+    }
+
+    // Validates parameters for a bounty creation.
+    fn ensure_create_bounty_parameters_valid(
+        params: &BountyCreationParameters<T>,
+    ) -> DispatchResult {
+        ensure!(
+            params.work_period != Zero::zero(),
+            Error::<T>::WorkPeriodCannotBeZero
+        );
+
+        ensure!(
+            params.judging_period != Zero::zero(),
+            Error::<T>::JudgingPeriodCannotBeZero
+        );
+
+        match params.funding_type {
+            FundingType::Perpetual { target } => {
+                ensure!(
+                    target != Zero::zero(),
+                    Error::<T>::FundingAmountCannotBeZero
+                );
+            }
+            FundingType::Limited {
+                min_funding_amount,
+                max_funding_amount,
+                funding_period,
+            } => {
+                ensure!(
+                    min_funding_amount != Zero::zero(),
+                    Error::<T>::FundingAmountCannotBeZero
+                );
+
+                ensure!(
+                    max_funding_amount != Zero::zero(),
+                    Error::<T>::FundingAmountCannotBeZero
+                );
+
+                ensure!(
+                    funding_period != Zero::zero(),
+                    Error::<T>::FundingPeriodCannotBeZero
+                );
+
+                ensure!(
+                    min_funding_amount <= max_funding_amount,
+                    Error::<T>::MinFundingAmountCannotBeGreaterThanMaxAmount
+                );
+            }
+        }
+
+        ensure!(
+            params.cherry >= T::MinCherryLimit::get(),
+            Error::<T>::CherryLessThenMinimumAllowed
+        );
+
+        ensure!(
+            params.entrant_stake >= T::MinWorkEntrantStake::get(),
+            Error::<T>::EntrantStakeIsLessThanMininum
+        );
+
+        if let AssuranceContractType::Closed(ref member_ids) = params.contract_type {
+            ensure!(
+                !member_ids.is_empty(),
+                Error::<T>::ClosedContractMemberListIsEmpty
+            );
+
+            ensure!(
+                member_ids.len() <= T::ClosedContractSizeLimit::get().saturated_into(),
+                Error::<T>::ClosedContractMemberListIsTooLarge
+            );
+        }
+
+        Ok(())
+    }
+
+    // Verifies that member balance is sufficient for a bounty.
+    fn check_balance_for_account(amount: BalanceOf<T>, account_id: &T::AccountId) -> bool {
+        balances::Module::<T>::usable_balance(account_id) >= amount
+    }
+
+    // Transfer funds from the member account to the bounty account.
+    fn transfer_funds_to_bounty_account(
+        account_id: &T::AccountId,
+        bounty_id: T::BountyId,
+        amount: BalanceOf<T>,
+    ) -> DispatchResult {
+        let bounty_account_id = Self::bounty_account_id(bounty_id);
+
+        <balances::Module<T> as Currency<T::AccountId>>::transfer(
+            account_id,
+            &bounty_account_id,
+            amount,
+            ExistenceRequirement::AllowDeath,
+        )
+    }
+
+    // Transfer funds from the bounty account to the member account.
+    fn transfer_funds_from_bounty_account(
+        account_id: &T::AccountId,
+        bounty_id: T::BountyId,
+        amount: BalanceOf<T>,
+    ) -> DispatchResult {
+        let bounty_account_id = Self::bounty_account_id(bounty_id);
+
+        <balances::Module<T> as Currency<T::AccountId>>::transfer(
+            &bounty_account_id,
+            account_id,
+            amount,
+            ExistenceRequirement::AllowDeath,
+        )
+    }
+
+    // Verifies bounty existence and retrieves a bounty from the storage.
+    fn ensure_bounty_exists(bounty_id: &T::BountyId) -> Result<Bounty<T>, DispatchError> {
+        ensure!(
+            <Bounties<T>>::contains_key(bounty_id),
+            Error::<T>::BountyDoesntExist
+        );
+
+        let bounty = <Bounties<T>>::get(bounty_id);
+
+        Ok(bounty)
+    }
+
+    // Calculate cherry fraction to reward member for an unsuccessful bounty.
+    // Cherry fraction = cherry * (member funding / total funding).
+    fn get_cherry_fraction_for_member(
+        bounty: &Bounty<T>,
+        funding_amount: BalanceOf<T>,
+    ) -> BalanceOf<T> {
+        let funding_share =
+            Perbill::from_rational_approximation(funding_amount, bounty.total_funding);
+
+        // cherry share
+        funding_share * bounty.creation_params.cherry
+    }
+
+    // Remove bounty and all related info from the storage.
+    fn remove_bounty(bounty_id: &T::BountyId) {
+        <Bounties<T>>::remove(bounty_id);
+        <BountyContributions<T>>::remove_prefix(bounty_id);
+
+        // Slash remaining funds.
+        let bounty_account_id = Self::bounty_account_id(*bounty_id);
+        let all = balances::Module::<T>::usable_balance(&bounty_account_id);
+        if all != Zero::zero() {
+            let _ = balances::Module::<T>::slash(&bounty_account_id, all);
+        }
+
+        Self::deposit_event(RawEvent::BountyRemoved(*bounty_id));
+    }
+
+    // Verifies that the bounty has no pending fund withdrawals left.
+    fn withdrawal_completed(stage: &BountyStage, bounty_id: &T::BountyId) -> bool {
+        let has_no_contributions = !Self::contributions_exist(bounty_id);
+        let has_no_work_entries = !Self::work_entries_exist(bounty_id);
+
+        match stage {
+            BountyStage::SuccessfulBountyWithdrawal => {
+                // All work entrants withdrew their stakes and rewards.
+                has_no_work_entries
+            }
+            BountyStage::FailedBountyWithdrawal => {
+                // All work entrants withdrew their stakes and all funders withdrew cherry and
+                // provided funds.
+                has_no_contributions && has_no_work_entries
+            }
+            // Not withdrawal stage
+            _ => false,
+        }
+    }
+
+    // Verifies that bounty has some contribution to withdraw.
+    // Should be O(1) because of the single inner call of the next() function of the iterator.
+    pub(crate) fn contributions_exist(bounty_id: &T::BountyId) -> bool {
+        <BountyContributions<T>>::iter_prefix_values(bounty_id)
+            .peekable()
+            .peek()
+            .is_some()
+    }
+
+    // Verifies that bounty has some work entries to withdraw.
+    pub(crate) fn work_entries_exist(bounty_id: &T::BountyId) -> bool {
+        Self::bounties(bounty_id).active_work_entry_count > 0
+    }
+
+    // The account ID of a bounty account. Tests require AccountID type to be at least u128.
+    pub(crate) fn bounty_account_id(bounty_id: T::BountyId) -> T::AccountId {
+        T::ModuleId::get().into_sub_account(bounty_id)
+    }
+
+    // Calculates bounty milestone on member funding.
+    fn get_bounty_milestone_on_funding(
+        maximum_funding_reached: bool,
+        previous_milestone: BountyMilestone<T::BlockNumber>,
+    ) -> BountyMilestone<T::BlockNumber> {
+        let now = Self::current_block();
+
+        if maximum_funding_reached {
+            // Bounty maximum funding reached.
+            BountyMilestone::BountyMaxFundingReached {
+                max_funding_reached_at: now,
+            }
+        // No previous contributions.
+        } else if let BountyMilestone::Created {
+            created_at,
+            has_contributions: false,
+        } = previous_milestone
+        {
+            // The bounty has some contributions now.
+            BountyMilestone::Created {
+                created_at,
+                has_contributions: true,
+            }
+        } else {
+            // No changes.
+            previous_milestone
+        }
+    }
+
+    // Calculates bounty milestone on work submitting.
+    fn get_bounty_milestone_on_work_submitting(
+        bounty: &Bounty<T>,
+    ) -> BountyMilestone<T::BlockNumber> {
+        let previous_milestone = bounty.milestone.clone();
+
+        match bounty.milestone.clone() {
+            BountyMilestone::Created { created_at, .. } => {
+                match bounty.creation_params.funding_type {
+                    FundingType::Perpetual { .. } => previous_milestone,
+                    FundingType::Limited { funding_period, .. } => BountyMilestone::WorkSubmitted {
+                        work_period_started_at: created_at + funding_period,
+                    },
+                }
+            }
+            BountyMilestone::BountyMaxFundingReached {
+                max_funding_reached_at,
+            } => BountyMilestone::WorkSubmitted {
+                work_period_started_at: max_funding_reached_at,
+            },
+            _ => previous_milestone,
+        }
+    }
+
+    // Validates stake on announcing the work entry.
+    fn validate_entrant_stake(
+        member_id: MemberId<T>,
+        bounty: &Bounty<T>,
+        staking_account_id: T::AccountId,
+    ) -> Result<Option<RequiredStakeInfo<T>>, DispatchError> {
+        let staking_balance = bounty.creation_params.entrant_stake;
+
+        ensure!(
+            T::Membership::is_member_staking_account(&member_id, &staking_account_id),
+            Error::<T>::InvalidStakingAccountForMember
+        );
+
+        ensure!(
+            T::StakingHandler::is_account_free_of_conflicting_stakes(&staking_account_id),
+            Error::<T>::ConflictingStakes
+        );
+
+        ensure!(
+            T::StakingHandler::is_enough_balance_for_stake(&staking_account_id, staking_balance),
+            Error::<T>::InsufficientBalanceForStake
+        );
+
+        Ok(Some(RequiredStakeInfo {
+            amount: staking_balance,
+            account_id: staking_account_id,
+        }))
+    }
+
+    // Verifies work entry existence and retrieves an entry from the storage.
+    fn ensure_work_entry_exists(entry_id: &T::EntryId) -> Result<Entry<T>, DispatchError> {
+        ensure!(
+            <Entries<T>>::contains_key(entry_id),
+            Error::<T>::WorkEntryDoesntExist
+        );
+
+        let entry = Self::entries(entry_id);
+
+        Ok(entry)
+    }
+
+    // Unlocks the work entry stake.
+    // It also calculates and slashes the stake on work entry withdrawal.
+    // The slashing amount depends on the entry active period.
+    fn unlock_work_entry_stake_with_possible_penalty(bounty: &Bounty<T>, entry: &Entry<T>) {
+        let staking_account_id = &entry.staking_account_id;
+
+        let now = Self::current_block();
+        let staking_balance = bounty.creation_params.entrant_stake;
+
+        let entry_was_active_period = now.saturating_sub(entry.submitted_at);
+
+        let slashing_share = Perbill::from_rational_approximation(
+            entry_was_active_period,
+            bounty.creation_params.work_period,
+        );
+
+        // No more than staking_balance.
+        let slashing_amount = (slashing_share * staking_balance).min(staking_balance);
+
+        if slashing_amount > Zero::zero() {
+            T::StakingHandler::slash(staking_account_id, Some(slashing_amount));
+        }
+
+        T::StakingHandler::unlock(staking_account_id);
+    }
+
+    // Slashed the work entry stake.
+    fn slash_work_entry_stake(entry: &Entry<T>) {
+        T::StakingHandler::slash(&entry.staking_account_id, None);
+    }
+
+    // Validates the contract type for a bounty
+    fn ensure_valid_contract_type(bounty: &Bounty<T>, member_id: &MemberId<T>) -> DispatchResult {
+        if let AssuranceContractType::Closed(ref valid_members) =
+            bounty.creation_params.contract_type
+        {
+            ensure!(
+                valid_members.contains(member_id),
+                Error::<T>::CannotSubmitWorkToClosedContractBounty
+            );
+        }
+
+        Ok(())
+    }
+
+    // Computes the stage of a bounty based on its creation parameters and the current state.
+    pub(crate) fn get_bounty_stage(bounty: &Bounty<T>) -> BountyStage {
+        let sc = BountyStageCalculator::<T> {
+            now: Self::current_block(),
+            bounty,
+        };
+
+        sc.get_bounty_stage()
+    }
+
+    // Validates oracle judgment.
+    fn validate_judgment(bounty: &Bounty<T>, judgment: &OracleJudgmentOf<T>) -> DispatchResult {
+        // Total judgment reward accumulator.
+        let mut reward_sum_from_judgment: BalanceOf<T> = Zero::zero();
+
+        // Validate all work entry judgements.
+        for (entry_id, work_entry_judgment) in judgment.iter() {
+            if let OracleWorkEntryJudgment::Winner { reward } = work_entry_judgment {
+                // Check for zero reward.
+                ensure!(*reward != Zero::zero(), Error::<T>::ZeroWinnerReward);
+
+                reward_sum_from_judgment += *reward;
+            }
+
+            // Check winner work submission.
+            let entry = Self::ensure_work_entry_exists(entry_id)?;
+            ensure!(
+                entry.work_submitted,
+                Error::<T>::WinnerShouldHasWorkSubmission
+            );
+        }
+
+        // Check for invalid total sum for successful bounty.
+        if reward_sum_from_judgment != Zero::zero() {
+            ensure!(
+                reward_sum_from_judgment == bounty.total_funding, // 100% bounty distribution
+                Error::<T>::TotalRewardShouldBeEqualToTotalFunding
+            );
+        }
+
+        Ok(())
+    }
+
+    // Removes the work entry and decrements active entry count in a bounty.
+    fn remove_work_entry(bounty_id: &T::BountyId, entry_id: &T::EntryId) {
+        <Entries<T>>::remove(entry_id);
+
+        // Decrement work entry counter and update bounty record.
+        <Bounties<T>>::mutate(bounty_id, |bounty| {
+            bounty.decrement_active_work_entry_counter();
+        });
+    }
+
+    // Calculates weight for submit_oracle_judgement extrinsic.
+    fn submit_oracle_judgement_weight(judgement: &OracleJudgmentOf<T>) -> Weight {
+        let collection_length: u32 = judgement.len().saturated_into();
+
+        WeightInfoBounty::<T>::submit_oracle_judgment_by_council_all_winners(collection_length)
+            .max(
+                WeightInfoBounty::<T>::submit_oracle_judgment_by_council_all_rejected(
+                    collection_length,
+                ),
+            )
+            .max(
+                WeightInfoBounty::<T>::submit_oracle_judgment_by_member_all_winners(
+                    collection_length,
+                ),
+            )
+            .max(
+                WeightInfoBounty::<T>::submit_oracle_judgment_by_member_all_rejected(
+                    collection_length,
+                ),
+            )
+    }
+
+    // Bounty stage validator.
+    fn ensure_bounty_stage(
+        actual_stage: BountyStage,
+        expected_stage: BountyStage,
+    ) -> DispatchResult {
+        ensure!(
+            actual_stage == expected_stage,
+            Self::unexpected_bounty_stage_error(actual_stage)
+        );
+
+        Ok(())
+    }
+
+    // Bounty stage validator for cancel_bounty() extrinsic.
+    fn ensure_bounty_stage_for_canceling(actual_stage: BountyStage) -> DispatchResult {
+        let funding_stage_with_no_contributions = BountyStage::Funding {
+            has_contributions: false,
+        };
+
+        ensure!(
+            actual_stage == funding_stage_with_no_contributions
+                || actual_stage == BountyStage::FundingExpired,
+            Self::unexpected_bounty_stage_error(actual_stage)
+        );
+
+        Ok(())
+    }
+
+    // Provides fined-grained errors for a bounty stages
+    fn unexpected_bounty_stage_error(unexpected_stage: BountyStage) -> DispatchError {
+        match unexpected_stage {
+            BountyStage::Funding { .. } => Error::<T>::InvalidStageUnexpectedFunding.into(),
+            BountyStage::FundingExpired => Error::<T>::InvalidStageUnexpectedFundingExpired.into(),
+            BountyStage::WorkSubmission => Error::<T>::InvalidStageUnexpectedWorkSubmission.into(),
+            BountyStage::Judgment => Error::<T>::InvalidStageUnexpectedJudgment.into(),
+            BountyStage::SuccessfulBountyWithdrawal => {
+                Error::<T>::InvalidStageUnexpectedSuccessfulBountyWithdrawal.into()
+            }
+            BountyStage::FailedBountyWithdrawal => {
+                Error::<T>::InvalidStageUnexpectedFailedBountyWithdrawal.into()
+            }
+        }
+    }
+
+    // Oracle judgment helper. Returns true if a judgement contains at least one winner.
+    pub(crate) fn judgment_has_winners(judgment: &OracleJudgmentOf<T>) -> bool {
+        judgment.iter().any(|(_, j)| j.is_winner())
+    }
+
+    // Transfers cherry back to the bounty creator and fires an event.
+    fn return_bounty_cherry_to_creator(
+        bounty_id: T::BountyId,
+        bounty: &Bounty<T>,
+    ) -> DispatchResult {
+        let bounty_creator_manager = BountyActorManager::<T>::get_bounty_actor_manager(
+            bounty.creation_params.creator.clone(),
+        )?;
+
+        bounty_creator_manager
+            .transfer_funds_from_bounty_account(bounty_id, bounty.creation_params.cherry)?;
+
+        Self::deposit_event(RawEvent::BountyCreatorCherryWithdrawal(
+            bounty_id,
+            bounty.creation_params.creator.clone(),
+        ));
+
+        Ok(())
+    }
+
+    // Calculates weight for create_bounty extrinsic.
+    fn create_bounty_weight(params: &BountyCreationParameters<T>, metadata: &[u8]) -> Weight {
+        let metadata_length = metadata.len().saturated_into();
+        let member_list_length =
+            if let AssuranceContractType::Closed(ref members) = params.contract_type {
+                members.len().saturated_into()
+            } else {
+                1 // consider open contract member list as one.
+            };
+
+        WeightInfoBounty::<T>::create_bounty_by_member(metadata_length, member_list_length).max(
+            WeightInfoBounty::<T>::create_bounty_by_council(metadata_length, member_list_length),
+        )
+    }
+}

+ 182 - 0
runtime-modules/bounty/src/stages.rs

@@ -0,0 +1,182 @@
+//! This module contains the BountyStageCalculator - a bounty stage calculation helper.
+//! It allows to get a bounty stage based on the current bounty state and the current system block.
+
+use crate::{Bounty, BountyMilestone, BountyStage, FundingType, Trait};
+
+// Bounty stage helper.
+pub(crate) struct BountyStageCalculator<'a, T: Trait> {
+    // current block number
+    pub now: T::BlockNumber,
+    // bounty object
+    pub bounty: &'a Bounty<T>,
+}
+
+impl<'a, T: Trait> BountyStageCalculator<'a, T> {
+    // Calculates the current bounty stage.
+    pub(crate) fn get_bounty_stage(&self) -> BountyStage {
+        self.is_funding_stage()
+            .or_else(|| self.is_funding_expired_stage())
+            .or_else(|| self.is_work_submission_stage())
+            .or_else(|| self.is_judgment_stage())
+            .or_else(|| self.is_successful_bounty_withdrawal_stage())
+            .unwrap_or(BountyStage::FailedBountyWithdrawal)
+    }
+
+    // Calculates funding stage of the bounty.
+    // Returns None if conditions are not met.
+    fn is_funding_stage(&self) -> Option<BountyStage> {
+        // Bounty was created. There can be some contributions. Funding period is not over.
+        if let BountyMilestone::Created {
+            has_contributions,
+            created_at,
+        } = self.bounty.milestone.clone()
+        {
+            let funding_period_is_not_expired = !self.funding_period_expired(created_at);
+
+            if funding_period_is_not_expired {
+                return Some(BountyStage::Funding { has_contributions });
+            }
+        }
+
+        None
+    }
+
+    // Calculates 'funding expired' stage of the bounty.
+    // Returns None if conditions are not met.
+    fn is_funding_expired_stage(&self) -> Option<BountyStage> {
+        // Bounty was created. There can be some contributions. Funding period is not over.
+        if let BountyMilestone::Created {
+            has_contributions,
+            created_at,
+        } = self.bounty.milestone.clone()
+        {
+            let funding_period_is_expired = self.funding_period_expired(created_at);
+
+            if funding_period_is_expired && !has_contributions {
+                return Some(BountyStage::FundingExpired);
+            }
+        }
+
+        None
+    }
+
+    // Calculates work submission stage of the bounty.
+    // Returns None if conditions are not met.
+    fn is_work_submission_stage(&self) -> Option<BountyStage> {
+        match self.bounty.milestone.clone() {
+            // Funding period is over. Minimum funding reached. Work period is not expired.
+            BountyMilestone::Created { created_at, .. } => {
+                match self.bounty.creation_params.funding_type {
+                    // Perpetual funding is not reached its target yet.
+                    FundingType::Perpetual { .. } => return None,
+                    FundingType::Limited { funding_period, .. } => {
+                        let minimum_funding_reached = self.minimum_funding_reached();
+                        let funding_period_expired = self.funding_period_expired(created_at);
+                        let working_period_is_not_expired =
+                            !self.work_period_expired(created_at + funding_period);
+
+                        if minimum_funding_reached
+                            && funding_period_expired
+                            && working_period_is_not_expired
+                        {
+                            return Some(BountyStage::WorkSubmission);
+                        }
+                    }
+                }
+            }
+            // Maximum funding reached. Work period is not expired.
+            BountyMilestone::BountyMaxFundingReached {
+                max_funding_reached_at,
+            } => {
+                let work_period_is_not_expired = !self.work_period_expired(max_funding_reached_at);
+
+                if work_period_is_not_expired {
+                    return Some(BountyStage::WorkSubmission);
+                }
+            }
+            // Work in progress. Work period is not expired.
+            BountyMilestone::WorkSubmitted {
+                work_period_started_at,
+            } => {
+                let work_period_is_not_expired = !self.work_period_expired(work_period_started_at);
+
+                if work_period_is_not_expired {
+                    return Some(BountyStage::WorkSubmission);
+                }
+            }
+            _ => return None,
+        }
+
+        None
+    }
+
+    // Calculates judgment stage of the bounty.
+    // Returns None if conditions are not met.
+    fn is_judgment_stage(&self) -> Option<BountyStage> {
+        // Can be judged only if there are work submissions.
+        if let BountyMilestone::WorkSubmitted {
+            work_period_started_at,
+        } = self.bounty.milestone
+        {
+            let work_period_expired = self.work_period_expired(work_period_started_at);
+
+            let judgment_period_is_not_expired =
+                !self.judgment_period_expired(work_period_started_at);
+
+            if work_period_expired && judgment_period_is_not_expired {
+                return Some(BountyStage::Judgment);
+            }
+        }
+
+        None
+    }
+
+    // Calculates withdrawal stage for the successful bounty.
+    // Returns None if conditions are not met.
+    fn is_successful_bounty_withdrawal_stage(&self) -> Option<BountyStage> {
+        // The bounty judgment was submitted and the bounty is successful (there are some winners).
+        if let BountyMilestone::JudgmentSubmitted { successful_bounty } =
+            self.bounty.milestone.clone()
+        {
+            if successful_bounty {
+                return Some(BountyStage::SuccessfulBountyWithdrawal);
+            }
+        }
+
+        None
+    }
+
+    // Checks whether the minimum funding reached for the bounty.
+    fn minimum_funding_reached(&self) -> bool {
+        match self.bounty.creation_params.funding_type {
+            // There is no minimum for the perpetual funding type - only maximum (target).
+            FundingType::Perpetual { .. } => false,
+            FundingType::Limited {
+                min_funding_amount, ..
+            } => self.bounty.total_funding >= min_funding_amount,
+        }
+    }
+
+    // Checks whether the work period expired by now starting from the provided block number.
+    fn work_period_expired(&self, work_period_started_at: T::BlockNumber) -> bool {
+        work_period_started_at + self.bounty.creation_params.work_period < self.now
+    }
+
+    // Checks whether the funding period expired by now starting from the provided block number.
+    fn funding_period_expired(&self, created_at: T::BlockNumber) -> bool {
+        match self.bounty.creation_params.funding_type {
+            // Never expires
+            FundingType::Perpetual { .. } => false,
+            FundingType::Limited { funding_period, .. } => created_at + funding_period < self.now,
+        }
+    }
+
+    // Checks whether the judgment period expired by now when work period start from the provided
+    // block number.
+    fn judgment_period_expired(&self, work_period_started_at: T::BlockNumber) -> bool {
+        work_period_started_at
+            + self.bounty.creation_params.work_period
+            + self.bounty.creation_params.judging_period
+            < self.now
+    }
+}

+ 816 - 0
runtime-modules/bounty/src/tests/fixtures.rs

@@ -0,0 +1,816 @@
+use frame_support::dispatch::DispatchResult;
+use frame_support::storage::StorageMap;
+use frame_support::traits::{Currency, OnFinalize, OnInitialize};
+use frame_system::{EventRecord, Phase, RawOrigin};
+use sp_runtime::offchain::storage_lock::BlockNumberProvider;
+use sp_std::collections::btree_set::BTreeSet;
+use sp_std::iter::FromIterator;
+
+use super::mocks::{Balances, Bounty, System, Test, TestEvent};
+use crate::{
+    AssuranceContractType, BountyActor, BountyCreationParameters, BountyMilestone, BountyRecord,
+    Entry, FundingType, OracleJudgmentOf, RawEvent,
+};
+use common::council::CouncilBudgetManager;
+
+// Recommendation from Parity on testing on_finalize
+// https://substrate.dev/docs/en/next/development/module/tests
+pub fn run_to_block(n: u64) {
+    while System::block_number() < n {
+        <System as OnFinalize<u64>>::on_finalize(System::block_number());
+        <Bounty 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());
+        <Bounty as OnInitialize<u64>>::on_initialize(System::block_number());
+    }
+}
+
+pub fn set_council_budget(new_budget: u64) {
+    <super::mocks::CouncilBudgetManager as CouncilBudgetManager<u64>>::set_budget(new_budget);
+}
+
+pub fn get_council_budget() -> u64 {
+    <super::mocks::CouncilBudgetManager as CouncilBudgetManager<u64>>::get_budget()
+}
+
+pub fn increase_total_balance_issuance_using_account_id(account_id: u128, balance: u64) {
+    let initial_balance = Balances::total_issuance();
+    {
+        let _ = Balances::deposit_creating(&account_id, balance);
+    }
+    assert_eq!(Balances::total_issuance(), initial_balance + balance);
+}
+
+pub fn increase_account_balance(account_id: &u128, balance: u64) {
+    let _ = Balances::deposit_creating(&account_id, balance);
+}
+
+pub struct EventFixture;
+impl EventFixture {
+    pub fn assert_last_crate_event(
+        expected_raw_event: RawEvent<
+            u64,
+            u64,
+            u64,
+            u64,
+            u128,
+            BountyCreationParameters<Test>,
+            OracleJudgmentOf<Test>,
+        >,
+    ) {
+        let converted_event = TestEvent::bounty(expected_raw_event);
+
+        Self::assert_last_global_event(converted_event)
+    }
+
+    pub fn contains_crate_event(
+        expected_raw_event: RawEvent<
+            u64,
+            u64,
+            u64,
+            u64,
+            u128,
+            BountyCreationParameters<Test>,
+            OracleJudgmentOf<Test>,
+        >,
+    ) {
+        let converted_event = TestEvent::bounty(expected_raw_event);
+
+        Self::contains_global_event(converted_event)
+    }
+
+    pub fn assert_last_global_event(expected_event: TestEvent) {
+        let expected_event = EventRecord {
+            phase: Phase::Initialization,
+            event: expected_event,
+            topics: vec![],
+        };
+
+        assert_eq!(System::events().pop().unwrap(), expected_event);
+    }
+
+    fn contains_global_event(expected_event: TestEvent) {
+        let expected_event = EventRecord {
+            phase: Phase::Initialization,
+            event: expected_event,
+            topics: vec![],
+        };
+
+        assert!(System::events().iter().any(|ev| *ev == expected_event));
+    }
+}
+
+pub const DEFAULT_BOUNTY_CHERRY: u64 = 10;
+pub const DEFAULT_BOUNTY_ENTRANT_STAKE: u64 = 10;
+pub const DEFAULT_BOUNTY_MAX_AMOUNT: u64 = 1000;
+pub const DEFAULT_BOUNTY_MIN_AMOUNT: u64 = 1;
+pub const DEFAULT_BOUNTY_FUNDING_PERIOD: u64 = 1;
+
+pub struct CreateBountyFixture {
+    origin: RawOrigin<u128>,
+    metadata: Vec<u8>,
+    creator: BountyActor<u64>,
+    funding_type: FundingType<u64, u64>,
+    work_period: u64,
+    judging_period: u64,
+    cherry: u64,
+    expected_milestone: Option<BountyMilestone<u64>>,
+    entrant_stake: u64,
+    contract_type: AssuranceContractType<u64>,
+    oracle: BountyActor<u64>,
+}
+
+impl CreateBountyFixture {
+    pub fn default() -> Self {
+        Self {
+            origin: RawOrigin::Root,
+            metadata: Vec::new(),
+            creator: BountyActor::Council,
+            funding_type: FundingType::Perpetual {
+                target: DEFAULT_BOUNTY_MAX_AMOUNT,
+            },
+            work_period: 1,
+            judging_period: 1,
+            cherry: DEFAULT_BOUNTY_CHERRY,
+            expected_milestone: None,
+            entrant_stake: DEFAULT_BOUNTY_ENTRANT_STAKE,
+            contract_type: AssuranceContractType::Open,
+            oracle: BountyActor::Council,
+        }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u128>) -> Self {
+        Self { origin, ..self }
+    }
+
+    pub fn with_creator_member_id(self, member_id: u64) -> Self {
+        Self {
+            creator: BountyActor::Member(member_id),
+            ..self
+        }
+    }
+
+    pub fn with_oracle_member_id(self, member_id: u64) -> Self {
+        Self {
+            oracle: BountyActor::Member(member_id),
+            ..self
+        }
+    }
+
+    pub fn with_metadata(self, metadata: Vec<u8>) -> Self {
+        Self { metadata, ..self }
+    }
+
+    pub fn with_work_period(self, work_period: u64) -> Self {
+        Self {
+            work_period,
+            ..self
+        }
+    }
+
+    pub fn with_limited_funding(
+        self,
+        min_funding_amount: u64,
+        max_funding_amount: u64,
+        funding_period: u64,
+    ) -> Self {
+        Self {
+            funding_type: FundingType::Limited {
+                funding_period,
+                min_funding_amount,
+                max_funding_amount,
+            },
+            ..self
+        }
+    }
+
+    pub fn with_perpetual_funding(self, target: u64) -> Self {
+        Self {
+            funding_type: FundingType::Perpetual { target },
+            ..self
+        }
+    }
+
+    pub fn with_funding_period(self, funding_period: u64) -> Self {
+        Self {
+            funding_type: FundingType::Limited {
+                funding_period,
+                min_funding_amount: DEFAULT_BOUNTY_MIN_AMOUNT,
+                max_funding_amount: DEFAULT_BOUNTY_MAX_AMOUNT,
+            },
+            ..self
+        }
+    }
+
+    pub fn with_max_funding_amount(self, max_funding_amount: u64) -> Self {
+        Self {
+            funding_type: FundingType::Limited {
+                funding_period: DEFAULT_BOUNTY_FUNDING_PERIOD,
+                min_funding_amount: DEFAULT_BOUNTY_MIN_AMOUNT,
+                max_funding_amount,
+            },
+            ..self
+        }
+    }
+
+    pub fn with_judging_period(self, judging_period: u64) -> Self {
+        Self {
+            judging_period,
+            ..self
+        }
+    }
+
+    pub fn with_cherry(self, cherry: u64) -> Self {
+        Self { cherry, ..self }
+    }
+
+    pub fn with_entrant_stake(self, entrant_stake: u64) -> Self {
+        Self {
+            entrant_stake,
+            ..self
+        }
+    }
+
+    pub fn with_closed_contract(self, member_ids: Vec<u64>) -> Self {
+        let member_id_set = BTreeSet::from_iter(member_ids.into_iter());
+
+        Self {
+            contract_type: AssuranceContractType::Closed(member_id_set),
+            ..self
+        }
+    }
+
+    pub fn get_bounty_creation_parameters(&self) -> BountyCreationParameters<Test> {
+        BountyCreationParameters::<Test> {
+            creator: self.creator.clone(),
+            funding_type: self.funding_type.clone(),
+            work_period: self.work_period.clone(),
+            judging_period: self.judging_period.clone(),
+            cherry: self.cherry,
+            entrant_stake: self.entrant_stake,
+            contract_type: self.contract_type.clone(),
+            oracle: self.oracle.clone(),
+            ..Default::default()
+        }
+    }
+
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let params = self.get_bounty_creation_parameters();
+
+        let next_bounty_count_value = Bounty::bounty_count() + 1;
+        let bounty_id: u64 = next_bounty_count_value.into();
+
+        let actual_result = Bounty::create_bounty(
+            self.origin.clone().into(),
+            params.clone(),
+            self.metadata.clone(),
+        );
+
+        assert_eq!(actual_result, expected_result);
+
+        if actual_result.is_ok() {
+            assert_eq!(next_bounty_count_value, Bounty::bounty_count());
+            assert!(<crate::Bounties<Test>>::contains_key(&bounty_id));
+
+            let expected_milestone = if let Some(milestone) = self.expected_milestone.clone() {
+                milestone
+            } else {
+                BountyMilestone::Created {
+                    created_at: System::block_number(),
+                    has_contributions: false,
+                }
+            };
+
+            let expected_bounty = BountyRecord::<u64, u64, u64> {
+                creation_params: params.clone(),
+                total_funding: 0,
+                milestone: expected_milestone,
+                active_work_entry_count: 0,
+            };
+
+            assert_eq!(expected_bounty, Bounty::bounties(bounty_id));
+        } else {
+            assert_eq!(next_bounty_count_value - 1, Bounty::bounty_count());
+            assert!(!<crate::Bounties<Test>>::contains_key(&bounty_id));
+        }
+    }
+}
+
+pub struct CancelBountyFixture {
+    origin: RawOrigin<u128>,
+    creator: BountyActor<u64>,
+    bounty_id: u64,
+}
+
+impl CancelBountyFixture {
+    pub fn default() -> Self {
+        Self {
+            origin: RawOrigin::Root,
+            creator: BountyActor::Council,
+            bounty_id: 1,
+        }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u128>) -> Self {
+        Self { origin, ..self }
+    }
+
+    pub fn with_creator_member_id(self, member_id: u64) -> Self {
+        Self {
+            creator: BountyActor::Member(member_id),
+            ..self
+        }
+    }
+
+    pub fn with_bounty_id(self, bounty_id: u64) -> Self {
+        Self { bounty_id, ..self }
+    }
+
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let actual_result = Bounty::cancel_bounty(
+            self.origin.clone().into(),
+            self.creator.clone(),
+            self.bounty_id.clone(),
+        );
+
+        assert_eq!(actual_result, expected_result);
+
+        if actual_result.is_ok() {
+            assert!(!<crate::Bounties<Test>>::contains_key(&self.bounty_id));
+        }
+    }
+}
+
+pub struct VetoBountyFixture {
+    origin: RawOrigin<u128>,
+    bounty_id: u64,
+}
+
+impl VetoBountyFixture {
+    pub fn default() -> Self {
+        Self {
+            origin: RawOrigin::Root,
+            bounty_id: 1,
+        }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u128>) -> Self {
+        Self { origin, ..self }
+    }
+
+    pub fn with_bounty_id(self, bounty_id: u64) -> Self {
+        Self { bounty_id, ..self }
+    }
+
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let actual_result = Bounty::veto_bounty(self.origin.clone().into(), self.bounty_id.clone());
+
+        assert_eq!(actual_result, expected_result);
+
+        if actual_result.is_ok() {
+            assert!(!<crate::Bounties<Test>>::contains_key(&self.bounty_id));
+        }
+    }
+}
+
+pub struct FundBountyFixture {
+    origin: RawOrigin<u128>,
+    funder: BountyActor<u64>,
+    bounty_id: u64,
+    amount: u64,
+}
+
+impl FundBountyFixture {
+    pub fn default() -> Self {
+        Self {
+            origin: RawOrigin::Signed(1),
+            funder: BountyActor::Member(1),
+            bounty_id: 1,
+            amount: 100,
+        }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u128>) -> Self {
+        Self { origin, ..self }
+    }
+
+    pub fn with_member_id(self, member_id: u64) -> Self {
+        Self {
+            funder: BountyActor::Member(member_id),
+            ..self
+        }
+    }
+
+    pub fn with_council(self) -> Self {
+        Self {
+            funder: BountyActor::Council,
+            ..self
+        }
+    }
+
+    pub fn with_bounty_id(self, bounty_id: u64) -> Self {
+        Self { bounty_id, ..self }
+    }
+
+    pub fn with_amount(self, amount: u64) -> Self {
+        Self { amount, ..self }
+    }
+
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let old_bounty_funding =
+            Bounty::contribution_by_bounty_by_actor(self.bounty_id, &self.funder);
+
+        let old_bounty = Bounty::bounties(self.bounty_id);
+
+        let actual_result = Bounty::fund_bounty(
+            self.origin.clone().into(),
+            self.funder.clone(),
+            self.bounty_id.clone(),
+            self.amount.clone(),
+        );
+
+        assert_eq!(actual_result, expected_result);
+
+        let new_bounty_funding =
+            Bounty::contribution_by_bounty_by_actor(self.bounty_id, &self.funder);
+        if actual_result.is_ok() {
+            let new_bounty = Bounty::bounties(self.bounty_id);
+            if new_bounty.total_funding == new_bounty.maximum_funding() {
+                assert_eq!(
+                    new_bounty_funding,
+                    old_bounty_funding + old_bounty.maximum_funding() - old_bounty.total_funding
+                );
+            } else {
+                assert_eq!(new_bounty_funding, old_bounty_funding + self.amount);
+            }
+        } else {
+            assert_eq!(new_bounty_funding, old_bounty_funding);
+        }
+    }
+}
+
+pub struct WithdrawFundingFixture {
+    origin: RawOrigin<u128>,
+    funder: BountyActor<u64>,
+    bounty_id: u64,
+}
+
+impl WithdrawFundingFixture {
+    pub fn default() -> Self {
+        Self {
+            origin: RawOrigin::Signed(1),
+            funder: BountyActor::Member(1),
+            bounty_id: 1,
+        }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u128>) -> Self {
+        Self { origin, ..self }
+    }
+
+    pub fn with_member_id(self, member_id: u64) -> Self {
+        Self {
+            funder: BountyActor::Member(member_id),
+            ..self
+        }
+    }
+
+    pub fn with_council(self) -> Self {
+        Self {
+            funder: BountyActor::Council,
+            ..self
+        }
+    }
+
+    pub fn with_bounty_id(self, bounty_id: u64) -> Self {
+        Self { bounty_id, ..self }
+    }
+
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let actual_result = Bounty::withdraw_funding(
+            self.origin.clone().into(),
+            self.funder.clone(),
+            self.bounty_id.clone(),
+        );
+
+        assert_eq!(actual_result, expected_result);
+    }
+}
+
+pub struct AnnounceWorkEntryFixture {
+    origin: RawOrigin<u128>,
+    bounty_id: u64,
+    member_id: u64,
+    staking_account_id: u128,
+}
+
+impl AnnounceWorkEntryFixture {
+    pub fn default() -> Self {
+        Self {
+            origin: RawOrigin::Signed(1),
+            bounty_id: 1,
+            member_id: 1,
+            staking_account_id: 1,
+        }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u128>) -> Self {
+        Self { origin, ..self }
+    }
+
+    pub fn with_member_id(self, member_id: u64) -> Self {
+        Self { member_id, ..self }
+    }
+
+    pub fn with_bounty_id(self, bounty_id: u64) -> Self {
+        Self { bounty_id, ..self }
+    }
+
+    pub fn with_staking_account_id(self, staking_account_id: u128) -> Self {
+        Self {
+            staking_account_id,
+            ..self
+        }
+    }
+
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let old_bounty = Bounty::bounties(self.bounty_id);
+        let next_entry_count_value = Bounty::entry_count() + 1;
+        let entry_id: u64 = next_entry_count_value.into();
+
+        let actual_result = Bounty::announce_work_entry(
+            self.origin.clone().into(),
+            self.member_id,
+            self.bounty_id,
+            self.staking_account_id,
+        );
+
+        assert_eq!(actual_result, expected_result);
+
+        let new_bounty = Bounty::bounties(self.bounty_id);
+        if actual_result.is_ok() {
+            assert_eq!(next_entry_count_value, Bounty::entry_count());
+            assert!(<crate::Entries<Test>>::contains_key(&entry_id));
+
+            let expected_entry = Entry::<Test> {
+                member_id: self.member_id,
+                staking_account_id: self.staking_account_id,
+                submitted_at: System::current_block_number(),
+                work_submitted: false,
+                oracle_judgment_result: None,
+            };
+
+            assert_eq!(expected_entry, Bounty::entries(entry_id));
+
+            assert_eq!(
+                new_bounty.active_work_entry_count,
+                old_bounty.active_work_entry_count + 1
+            );
+        } else {
+            assert_eq!(next_entry_count_value - 1, Bounty::entry_count());
+            assert!(!<crate::Entries<Test>>::contains_key(&entry_id));
+
+            assert_eq!(
+                new_bounty.active_work_entry_count,
+                old_bounty.active_work_entry_count
+            );
+        }
+    }
+}
+
+pub struct WithdrawWorkEntryFixture {
+    origin: RawOrigin<u128>,
+    entry_id: u64,
+    bounty_id: u64,
+    member_id: u64,
+}
+
+impl WithdrawWorkEntryFixture {
+    pub fn default() -> Self {
+        Self {
+            origin: RawOrigin::Signed(1),
+            entry_id: 1,
+            bounty_id: 1,
+            member_id: 1,
+        }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u128>) -> Self {
+        Self { origin, ..self }
+    }
+
+    pub fn with_member_id(self, member_id: u64) -> Self {
+        Self { member_id, ..self }
+    }
+
+    pub fn with_bounty_id(self, bounty_id: u64) -> Self {
+        Self { bounty_id, ..self }
+    }
+
+    pub fn with_entry_id(self, entry_id: u64) -> Self {
+        Self { entry_id, ..self }
+    }
+
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let old_bounty = Bounty::bounties(self.bounty_id);
+        let actual_result = Bounty::withdraw_work_entry(
+            self.origin.clone().into(),
+            self.member_id,
+            self.bounty_id,
+            self.entry_id,
+        );
+
+        assert_eq!(actual_result, expected_result);
+
+        if actual_result.is_ok() {
+            assert!(!<crate::Entries<Test>>::contains_key(&self.entry_id));
+
+            let new_bounty = Bounty::bounties(self.bounty_id);
+            assert_eq!(
+                new_bounty.active_work_entry_count,
+                old_bounty.active_work_entry_count - 1
+            );
+        }
+    }
+}
+
+pub struct SubmitWorkFixture {
+    origin: RawOrigin<u128>,
+    entry_id: u64,
+    bounty_id: u64,
+    member_id: u64,
+    work_data: Vec<u8>,
+}
+
+impl SubmitWorkFixture {
+    pub fn default() -> Self {
+        Self {
+            origin: RawOrigin::Signed(1),
+            entry_id: 1,
+            bounty_id: 1,
+            member_id: 1,
+            work_data: Vec::new(),
+        }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u128>) -> Self {
+        Self { origin, ..self }
+    }
+
+    pub fn with_member_id(self, member_id: u64) -> Self {
+        Self { member_id, ..self }
+    }
+
+    pub fn with_bounty_id(self, bounty_id: u64) -> Self {
+        Self { bounty_id, ..self }
+    }
+
+    pub fn with_entry_id(self, entry_id: u64) -> Self {
+        Self { entry_id, ..self }
+    }
+
+    pub fn with_work_data(self, work_data: Vec<u8>) -> Self {
+        Self { work_data, ..self }
+    }
+
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let old_entry = Bounty::entries(self.entry_id);
+        let actual_result = Bounty::submit_work(
+            self.origin.clone().into(),
+            self.member_id,
+            self.bounty_id,
+            self.entry_id,
+            self.work_data.clone(),
+        );
+
+        assert_eq!(actual_result, expected_result);
+
+        let new_entry = Bounty::entries(self.entry_id);
+
+        if actual_result.is_ok() {
+            assert!(new_entry.work_submitted);
+        } else {
+            assert_eq!(new_entry, old_entry);
+        }
+    }
+}
+
+pub struct SubmitJudgmentFixture {
+    origin: RawOrigin<u128>,
+    bounty_id: u64,
+    oracle: BountyActor<u64>,
+    judgment: OracleJudgmentOf<Test>,
+}
+
+impl SubmitJudgmentFixture {
+    pub fn default() -> Self {
+        Self {
+            origin: RawOrigin::Root,
+            bounty_id: 1,
+            oracle: BountyActor::Council,
+            judgment: Default::default(),
+        }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u128>) -> Self {
+        Self { origin, ..self }
+    }
+
+    pub fn with_bounty_id(self, bounty_id: u64) -> Self {
+        Self { bounty_id, ..self }
+    }
+
+    pub fn with_oracle_member_id(self, member_id: u64) -> Self {
+        Self {
+            oracle: BountyActor::Member(member_id),
+            ..self
+        }
+    }
+
+    pub fn with_judgment(self, judgment: OracleJudgmentOf<Test>) -> Self {
+        Self { judgment, ..self }
+    }
+
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let old_bounty = Bounty::bounties(self.bounty_id);
+        let actual_result = Bounty::submit_oracle_judgment(
+            self.origin.clone().into(),
+            self.oracle.clone(),
+            self.bounty_id,
+            self.judgment.clone(),
+        );
+
+        assert_eq!(actual_result, expected_result);
+
+        let new_bounty = Bounty::bounties(self.bounty_id);
+
+        if actual_result.is_ok() {
+            assert_eq!(
+                new_bounty.milestone,
+                BountyMilestone::JudgmentSubmitted {
+                    successful_bounty: Bounty::judgment_has_winners(&self.judgment)
+                }
+            );
+        } else {
+            assert_eq!(new_bounty, old_bounty);
+        }
+    }
+}
+
+pub struct WithdrawWorkEntrantFundsFixture {
+    origin: RawOrigin<u128>,
+    entry_id: u64,
+    bounty_id: u64,
+    member_id: u64,
+}
+
+impl WithdrawWorkEntrantFundsFixture {
+    pub fn default() -> Self {
+        Self {
+            origin: RawOrigin::Signed(1),
+            entry_id: 1,
+            bounty_id: 1,
+            member_id: 1,
+        }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u128>) -> Self {
+        Self { origin, ..self }
+    }
+
+    pub fn with_member_id(self, member_id: u64) -> Self {
+        Self { member_id, ..self }
+    }
+
+    pub fn with_bounty_id(self, bounty_id: u64) -> Self {
+        Self { bounty_id, ..self }
+    }
+
+    pub fn with_entry_id(self, entry_id: u64) -> Self {
+        Self { entry_id, ..self }
+    }
+
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let old_bounty = Bounty::bounties(self.bounty_id);
+        let actual_result = Bounty::withdraw_work_entrant_funds(
+            self.origin.clone().into(),
+            self.member_id,
+            self.bounty_id,
+            self.entry_id,
+        );
+
+        assert_eq!(actual_result, expected_result);
+
+        if actual_result.is_ok() {
+            assert!(!<crate::Entries<Test>>::contains_key(&self.entry_id));
+
+            if <crate::Bounties<Test>>::contains_key(self.bounty_id) {
+                let new_bounty = Bounty::bounties(self.bounty_id);
+                assert_eq!(
+                    new_bounty.active_work_entry_count,
+                    old_bounty.active_work_entry_count - 1
+                );
+            }
+        }
+    }
+}

+ 559 - 0
runtime-modules/bounty/src/tests/mocks.rs

@@ -0,0 +1,559 @@
+#![cfg(test)]
+
+use frame_support::dispatch::{DispatchError, DispatchResult};
+use frame_support::traits::{Currency, LockIdentifier};
+use frame_support::weights::Weight;
+use frame_support::{impl_outer_event, impl_outer_origin, parameter_types};
+use frame_system::{EnsureOneOf, EnsureRoot, EnsureSigned};
+use sp_core::H256;
+use sp_runtime::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    ModuleId, Perbill,
+};
+
+use crate::{Module, Trait};
+use staking_handler::{LockComparator, StakingManager};
+
+impl_outer_origin! {
+    pub enum Origin for Test {}
+}
+
+mod bounty {
+    pub use crate::Event;
+}
+
+mod membership_mod {
+    pub use membership::Event;
+}
+
+mod council_mod {
+    pub use council::Event;
+}
+
+mod referendum_mod {
+    pub use referendum::Event;
+    pub use referendum::Instance1;
+}
+
+impl_outer_event! {
+    pub enum TestEvent for Test {
+        bounty<T>,
+        frame_system<T>,
+        balances<T>,
+        membership_mod<T>,
+        council_mod<T>,
+        referendum_mod Instance1 <T>,
+    }
+}
+
+// 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 BountyModuleId: ModuleId = ModuleId(*b"m:bounty"); // module : bounty
+    pub const BountyLockId: [u8; 8] = [12; 8];
+    pub const ClosedContractSizeLimit: u32 = 3;
+    pub const MinCherryLimit: u64 = 10;
+    pub const MinFundingLimit: u64 = 50;
+    pub const MinWorkEntrantStake: u64 = 10;
+}
+
+impl frame_system::Trait for Test {
+    type BaseCallFilter = ();
+    type Origin = Origin;
+    type Call = ();
+    type Index = u64;
+    type BlockNumber = u64;
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = u128;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    type Event = TestEvent;
+    type BlockHashCount = BlockHashCount;
+    type MaximumBlockWeight = MaximumBlockWeight;
+    type DbWeight = ();
+    type BlockExecutionWeight = ();
+    type ExtrinsicBaseWeight = ();
+    type MaximumExtrinsicWeight = ();
+    type MaximumBlockLength = MaximumBlockLength;
+    type AvailableBlockRatio = AvailableBlockRatio;
+    type Version = ();
+    type AccountData = balances::AccountData<u64>;
+    type OnNewAccount = ();
+    type OnKilledAccount = ();
+    type PalletInfo = ();
+    type SystemWeightInfo = ();
+}
+
+impl Trait for Test {
+    type Event = TestEvent;
+    type ModuleId = BountyModuleId;
+    type BountyId = u64;
+    type Membership = ();
+    type WeightInfo = ();
+    type CouncilBudgetManager = CouncilBudgetManager;
+    type StakingHandler = StakingManager<Test, BountyLockId>;
+    type EntryId = u64;
+    type ClosedContractSizeLimit = ClosedContractSizeLimit;
+    type MinCherryLimit = MinCherryLimit;
+    type MinFundingLimit = MinFundingLimit;
+    type MinWorkEntrantStake = MinWorkEntrantStake;
+}
+
+pub const STAKING_ACCOUNT_ID_NOT_BOUND_TO_MEMBER: u128 = 10000;
+impl common::StakingAccountValidator<Test> for () {
+    fn is_member_staking_account(_: &u64, account_id: &u128) -> bool {
+        *account_id != STAKING_ACCOUNT_ID_NOT_BOUND_TO_MEMBER
+    }
+}
+
+impl common::membership::MembershipInfoProvider<Test> for () {
+    fn controller_account_id(member_id: u64) -> Result<u128, DispatchError> {
+        if member_id < 10 {
+            return Ok(member_id as u128); // similar account_id
+        }
+
+        Err(membership::Error::<Test>::MemberProfileNotFound.into())
+    }
+}
+
+pub const COUNCIL_BUDGET_ACCOUNT_ID: u128 = 90000000;
+pub struct CouncilBudgetManager;
+impl common::council::CouncilBudgetManager<u64> for CouncilBudgetManager {
+    fn get_budget() -> u64 {
+        balances::Module::<Test>::usable_balance(&COUNCIL_BUDGET_ACCOUNT_ID)
+    }
+
+    fn set_budget(budget: u64) {
+        let old_budget = Self::get_budget();
+
+        if budget > old_budget {
+            let _ = balances::Module::<Test>::deposit_creating(
+                &COUNCIL_BUDGET_ACCOUNT_ID,
+                budget - old_budget,
+            );
+        }
+
+        if budget < old_budget {
+            let _ =
+                balances::Module::<Test>::slash(&COUNCIL_BUDGET_ACCOUNT_ID, old_budget - budget);
+        }
+    }
+}
+
+impl crate::WeightInfo for () {
+    fn create_bounty_by_council(_i: u32, _j: u32) -> u64 {
+        0
+    }
+    fn create_bounty_by_member(_i: u32, _j: u32) -> u64 {
+        0
+    }
+    fn cancel_bounty_by_member() -> u64 {
+        0
+    }
+    fn cancel_bounty_by_council() -> u64 {
+        0
+    }
+    fn veto_bounty() -> u64 {
+        0
+    }
+    fn fund_bounty_by_member() -> u64 {
+        0
+    }
+    fn fund_bounty_by_council() -> u64 {
+        0
+    }
+    fn withdraw_funding_by_member() -> u64 {
+        0
+    }
+    fn withdraw_funding_by_council() -> u64 {
+        0
+    }
+    fn announce_work_entry(_i: u32) -> u64 {
+        0
+    }
+    fn withdraw_work_entry() -> u64 {
+        0
+    }
+    fn submit_work(_i: u32) -> u64 {
+        0
+    }
+    fn submit_oracle_judgment_by_council_all_winners(_i: u32) -> u64 {
+        0
+    }
+    fn submit_oracle_judgment_by_council_all_rejected(_i: u32) -> u64 {
+        0
+    }
+    fn submit_oracle_judgment_by_member_all_winners(_i: u32) -> u64 {
+        0
+    }
+    fn submit_oracle_judgment_by_member_all_rejected(_i: u32) -> u64 {
+        0
+    }
+    fn withdraw_work_entrant_funds() -> u64 {
+        0
+    }
+}
+
+impl common::membership::Trait for Test {
+    type MemberId = u64;
+    type ActorId = u64;
+}
+
+impl common::membership::MemberOriginValidator<Origin, u64, u128> for () {
+    fn ensure_member_controller_account_origin(
+        origin: Origin,
+        _member_id: u64,
+    ) -> Result<u128, DispatchError> {
+        let signed_account_id = frame_system::ensure_signed(origin)?;
+
+        Ok(signed_account_id)
+    }
+
+    fn is_member_controller_account(_member_id: &u64, _account_id: &u128) -> bool {
+        true
+    }
+}
+
+parameter_types! {
+    pub const DefaultMembershipPrice: u64 = 100;
+    pub const InvitedMemberLockId: [u8; 8] = [2; 8];
+    pub const ReferralCutMaximumPercent: u8 = 50;
+    pub const MinimumStakeForOpening: u32 = 50;
+    pub const MinimumApplicationStake: u32 = 50;
+    pub const LeaderOpeningStake: u32 = 20;
+    pub const StakingCandidateLockId: [u8; 8] = [3; 8];
+    pub const DefaultInitialInvitationBalance: u64 = 100;
+    pub const CandidateStake: u64 = 130;
+}
+
+// Weights info stub
+pub struct Weights;
+impl membership::WeightInfo for Weights {
+    fn buy_membership_without_referrer(_: u32, _: u32) -> Weight {
+        unimplemented!()
+    }
+    fn buy_membership_with_referrer(_: u32, _: u32) -> Weight {
+        unimplemented!()
+    }
+    fn update_profile(_: u32) -> Weight {
+        unimplemented!()
+    }
+    fn update_accounts_none() -> Weight {
+        unimplemented!()
+    }
+    fn update_accounts_root() -> Weight {
+        unimplemented!()
+    }
+    fn update_accounts_controller() -> Weight {
+        unimplemented!()
+    }
+    fn update_accounts_both() -> Weight {
+        unimplemented!()
+    }
+    fn set_referral_cut() -> Weight {
+        unimplemented!()
+    }
+    fn transfer_invites() -> Weight {
+        unimplemented!()
+    }
+    fn invite_member(_: u32, _: u32) -> Weight {
+        unimplemented!()
+    }
+    fn set_membership_price() -> Weight {
+        unimplemented!()
+    }
+    fn update_profile_verification() -> Weight {
+        unimplemented!()
+    }
+    fn set_leader_invitation_quota() -> Weight {
+        unimplemented!()
+    }
+    fn set_initial_invitation_balance() -> Weight {
+        unimplemented!()
+    }
+    fn set_initial_invitation_count() -> Weight {
+        unimplemented!()
+    }
+    fn add_staking_account_candidate() -> Weight {
+        unimplemented!()
+    }
+    fn confirm_staking_account() -> Weight {
+        unimplemented!()
+    }
+    fn remove_staking_account() -> Weight {
+        unimplemented!()
+    }
+}
+
+impl pallet_timestamp::Trait for Test {
+    type Moment = u64;
+    type OnTimestampSet = ();
+    type MinimumPeriod = MinimumPeriod;
+    type WeightInfo = ();
+}
+
+impl membership::Trait for Test {
+    type Event = TestEvent;
+    type DefaultMembershipPrice = DefaultMembershipPrice;
+    type ReferralCutMaximumPercent = ReferralCutMaximumPercent;
+    type WorkingGroup = ();
+    type DefaultInitialInvitationBalance = DefaultInitialInvitationBalance;
+    type InvitedMemberStakingHandler = staking_handler::StakingManager<Self, InvitedMemberLockId>;
+    type StakingCandidateStakingHandler =
+        staking_handler::StakingManager<Self, StakingCandidateLockId>;
+    type WeightInfo = Weights;
+    type CandidateStake = CandidateStake;
+}
+
+impl LockComparator<<Test as balances::Trait>::Balance> for Test {
+    fn are_locks_conflicting(new_lock: &LockIdentifier, existing_locks: &[LockIdentifier]) -> bool {
+        if *new_lock != BountyLockId::get() {
+            return false;
+        }
+
+        existing_locks.contains(new_lock)
+    }
+}
+
+impl common::working_group::WorkingGroupBudgetHandler<Test> for () {
+    fn get_budget() -> u64 {
+        unimplemented!()
+    }
+
+    fn set_budget(_new_value: u64) {
+        unimplemented!()
+    }
+}
+
+impl common::working_group::WorkingGroupAuthenticator<Test> for () {
+    fn ensure_worker_origin(
+        _origin: <Test as frame_system::Trait>::Origin,
+        _worker_id: &<Test as common::membership::Trait>::ActorId,
+    ) -> DispatchResult {
+        unimplemented!();
+    }
+
+    fn ensure_leader_origin(_origin: <Test as frame_system::Trait>::Origin) -> DispatchResult {
+        unimplemented!()
+    }
+
+    fn get_leader_member_id() -> Option<<Test as common::membership::Trait>::MemberId> {
+        unimplemented!();
+    }
+
+    fn is_leader_account_id(_account_id: &<Test as frame_system::Trait>::AccountId) -> bool {
+        unimplemented!()
+    }
+
+    fn is_worker_account_id(
+        _account_id: &<Test as frame_system::Trait>::AccountId,
+        _worker_id: &<Test as common::membership::Trait>::ActorId,
+    ) -> bool {
+        unimplemented!()
+    }
+}
+
+parameter_types! {
+    pub const ExistentialDeposit: u32 = 0;
+    pub const MinimumPeriod: u64 = 5;
+}
+
+impl balances::Trait for Test {
+    type Balance = u64;
+    type DustRemoval = ();
+    type Event = TestEvent;
+    type ExistentialDeposit = ExistentialDeposit;
+    type AccountStore = System;
+    type WeightInfo = ();
+    type MaxLocks = ();
+}
+
+parameter_types! {
+    pub const MinNumberOfExtraCandidates: u64 = 1;
+    pub const AnnouncingPeriodDuration: u64 = 15;
+    pub const IdlePeriodDuration: u64 = 27;
+    pub const CouncilSize: u64 = 3;
+    pub const MinCandidateStake: u64 = 11000;
+    pub const CandidacyLockId: LockIdentifier = *b"council1";
+    pub const CouncilorLockId: LockIdentifier = *b"council2";
+    pub const ElectedMemberRewardPeriod: u64 = 10;
+    pub const BudgetRefillAmount: u64 = 1000;
+    // intentionally high number that prevents side-effecting tests other than  budget refill tests
+    pub const BudgetRefillPeriod: u64 = 1000;
+}
+
+pub type ReferendumInstance = referendum::Instance1;
+
+impl council::Trait for Test {
+    type Event = TestEvent;
+    type Referendum = referendum::Module<Test, ReferendumInstance>;
+    type MinNumberOfExtraCandidates = MinNumberOfExtraCandidates;
+    type CouncilSize = CouncilSize;
+    type AnnouncingPeriodDuration = AnnouncingPeriodDuration;
+    type IdlePeriodDuration = IdlePeriodDuration;
+    type MinCandidateStake = MinCandidateStake;
+    type CandidacyLock = StakingManager<Self, CandidacyLockId>;
+    type CouncilorLock = StakingManager<Self, CouncilorLockId>;
+    type ElectedMemberRewardPeriod = ElectedMemberRewardPeriod;
+    type BudgetRefillPeriod = BudgetRefillPeriod;
+    type StakingAccountValidator = ();
+    type WeightInfo = CouncilWeightInfo;
+    type MemberOriginValidator = ();
+
+    fn new_council_elected(_: &[council::CouncilMemberOf<Self>]) {}
+}
+
+pub struct CouncilWeightInfo;
+impl council::WeightInfo for CouncilWeightInfo {
+    fn try_process_budget() -> Weight {
+        0
+    }
+    fn try_progress_stage_idle() -> Weight {
+        0
+    }
+    fn try_progress_stage_announcing_start_election(_: u32) -> Weight {
+        0
+    }
+    fn try_progress_stage_announcing_restart() -> Weight {
+        0
+    }
+    fn announce_candidacy() -> Weight {
+        0
+    }
+    fn release_candidacy_stake() -> Weight {
+        0
+    }
+    fn set_candidacy_note(_: u32) -> Weight {
+        0
+    }
+    fn withdraw_candidacy() -> Weight {
+        0
+    }
+    fn set_budget() -> Weight {
+        0
+    }
+    fn plan_budget_refill() -> Weight {
+        0
+    }
+    fn set_budget_increment() -> Weight {
+        0
+    }
+    fn set_councilor_reward() -> Weight {
+        0
+    }
+    fn funding_request(_: u32) -> Weight {
+        0
+    }
+}
+
+parameter_types! {
+    pub const VoteStageDuration: u64 = 19;
+    pub const RevealStageDuration: u64 = 23;
+    pub const MinimumVotingStake: u64 = 10000;
+    pub const MaxSaltLength: u64 = 32; // use some multiple of 8 for ez testing
+    pub const VotingLockId: LockIdentifier = *b"referend";
+    pub const MaxWinnerTargetCount: u64 = 10;
+}
+
+impl referendum::Trait<ReferendumInstance> for Test {
+    type Event = TestEvent;
+    type MaxSaltLength = MaxSaltLength;
+    type StakingHandler = staking_handler::StakingManager<Self, VotingLockId>;
+    type ManagerOrigin =
+        EnsureOneOf<Self::AccountId, EnsureSigned<Self::AccountId>, EnsureRoot<Self::AccountId>>;
+    type VotePower = u64;
+    type VoteStageDuration = VoteStageDuration;
+    type RevealStageDuration = RevealStageDuration;
+    type MinimumStake = MinimumVotingStake;
+    type WeightInfo = ReferendumWeightInfo;
+    type MaxWinnerTargetCount = MaxWinnerTargetCount;
+
+    fn calculate_vote_power(
+        _: &<Self as frame_system::Trait>::AccountId,
+        _: &Self::Balance,
+    ) -> Self::VotePower {
+        1
+    }
+
+    fn can_unlock_vote_stake(
+        _: &referendum::CastVote<Self::Hash, Self::Balance, Self::MemberId>,
+    ) -> bool {
+        true
+    }
+
+    fn process_results(winners: &[referendum::OptionResult<Self::MemberId, Self::VotePower>]) {
+        let tmp_winners: Vec<referendum::OptionResult<Self::MemberId, Self::VotePower>> = winners
+            .iter()
+            .map(|item| referendum::OptionResult {
+                option_id: item.option_id,
+                vote_power: item.vote_power.into(),
+            })
+            .collect();
+        <council::Module<Test> as council::ReferendumConnection<Test>>::recieve_referendum_results(
+            tmp_winners.as_slice(),
+        );
+    }
+
+    fn is_valid_option_id(option_index: &u64) -> bool {
+        <council::Module<Test> as council::ReferendumConnection<Test>>::is_valid_candidate_id(
+            option_index,
+        )
+    }
+
+    fn get_option_power(option_id: &u64) -> Self::VotePower {
+        <council::Module<Test> as council::ReferendumConnection<Test>>::get_option_power(option_id)
+    }
+
+    fn increase_option_power(option_id: &u64, amount: &Self::VotePower) {
+        <council::Module<Test> as council::ReferendumConnection<Test>>::increase_option_power(
+            option_id, amount,
+        );
+    }
+}
+
+pub struct ReferendumWeightInfo;
+impl referendum::WeightInfo for ReferendumWeightInfo {
+    fn on_initialize_revealing(_: u32) -> Weight {
+        0
+    }
+    fn on_initialize_voting() -> Weight {
+        0
+    }
+    fn vote() -> Weight {
+        0
+    }
+    fn reveal_vote_space_for_new_winner(_: u32) -> Weight {
+        0
+    }
+    fn reveal_vote_space_not_in_winners(_: u32) -> Weight {
+        0
+    }
+    fn reveal_vote_space_replace_last_winner(_: u32) -> Weight {
+        0
+    }
+    fn reveal_vote_already_existing(_: u32) -> Weight {
+        0
+    }
+    fn release_vote_stake() -> Weight {
+        0
+    }
+}
+
+pub fn build_test_externalities() -> sp_io::TestExternalities {
+    let t = frame_system::GenesisConfig::default()
+        .build_storage::<Test>()
+        .unwrap();
+
+    t.into()
+}
+
+pub type System = frame_system::Module<Test>;
+pub type Bounty = Module<Test>;
+pub type Balances = balances::Module<Test>;

+ 3419 - 0
runtime-modules/bounty/src/tests/mod.rs

@@ -0,0 +1,3419 @@
+#![cfg(test)]
+
+pub(crate) mod fixtures;
+pub(crate) mod mocks;
+
+use frame_support::storage::StorageMap;
+use frame_support::traits::Currency;
+use frame_system::RawOrigin;
+use sp_runtime::DispatchError;
+use sp_std::collections::btree_map::BTreeMap;
+
+use crate::{
+    Bounties, BountyActor, BountyCreationParameters, BountyMilestone, BountyRecord, BountyStage,
+    Entries, Error, FundingType, OracleWorkEntryJudgment, RawEvent,
+};
+use fixtures::{
+    get_council_budget, increase_account_balance, increase_total_balance_issuance_using_account_id,
+    run_to_block, set_council_budget, AnnounceWorkEntryFixture, CancelBountyFixture,
+    CreateBountyFixture, EventFixture, FundBountyFixture, SubmitJudgmentFixture, SubmitWorkFixture,
+    VetoBountyFixture, WithdrawFundingFixture, WithdrawWorkEntrantFundsFixture,
+    WithdrawWorkEntryFixture, DEFAULT_BOUNTY_CHERRY,
+};
+use mocks::{
+    build_test_externalities, Balances, Bounty, ClosedContractSizeLimit, MinFundingLimit, System,
+    Test, COUNCIL_BUDGET_ACCOUNT_ID, STAKING_ACCOUNT_ID_NOT_BOUND_TO_MEMBER,
+};
+
+const DEFAULT_WINNER_REWARD: u64 = 10;
+
+#[test]
+fn validate_funding_bounty_stage() {
+    build_test_externalities().execute_with(|| {
+        let created_at = 10;
+
+        // Perpetual funding period
+        // No contributions.
+        let bounty = BountyRecord {
+            creation_params: BountyCreationParameters::<Test> {
+                ..Default::default()
+            },
+            milestone: BountyMilestone::Created {
+                created_at,
+                has_contributions: false,
+            },
+            ..Default::default()
+        };
+
+        assert_eq!(
+            Bounty::get_bounty_stage(&bounty),
+            BountyStage::Funding {
+                has_contributions: false
+            }
+        );
+
+        // Has contributions
+        let bounty = BountyRecord {
+            creation_params: BountyCreationParameters::<Test> {
+                ..Default::default()
+            },
+            milestone: BountyMilestone::Created {
+                created_at,
+                has_contributions: true,
+            },
+            ..Default::default()
+        };
+
+        assert_eq!(
+            Bounty::get_bounty_stage(&bounty),
+            BountyStage::Funding {
+                has_contributions: true
+            }
+        );
+
+        // Limited funding period
+        let funding_period = 10;
+
+        let bounty = BountyRecord {
+            creation_params: BountyCreationParameters::<Test> {
+                funding_type: FundingType::Limited {
+                    funding_period,
+                    min_funding_amount: 10,
+                    max_funding_amount: 10,
+                },
+                ..Default::default()
+            },
+            milestone: BountyMilestone::Created {
+                created_at,
+                has_contributions: false,
+            },
+            ..Default::default()
+        };
+
+        System::set_block_number(created_at + 1);
+
+        assert_eq!(
+            Bounty::get_bounty_stage(&bounty),
+            BountyStage::Funding {
+                has_contributions: false
+            }
+        );
+
+        let bounty = BountyRecord {
+            creation_params: BountyCreationParameters::<Test> {
+                funding_type: FundingType::Limited {
+                    funding_period,
+                    min_funding_amount: 10,
+                    max_funding_amount: 10,
+                },
+                ..Default::default()
+            },
+            milestone: BountyMilestone::Created {
+                created_at,
+                has_contributions: true,
+            },
+            ..Default::default()
+        };
+
+        System::set_block_number(created_at + 2);
+
+        assert_eq!(
+            Bounty::get_bounty_stage(&bounty),
+            BountyStage::Funding {
+                has_contributions: true
+            }
+        );
+    });
+}
+
+#[test]
+fn validate_funding_expired_bounty_stage() {
+    build_test_externalities().execute_with(|| {
+        let created_at = 10;
+        let funding_period = 10;
+
+        // Limited funding period
+        // No contributions.
+        let bounty = BountyRecord {
+            creation_params: BountyCreationParameters::<Test> {
+                funding_type: FundingType::Limited {
+                    funding_period,
+                    min_funding_amount: 10,
+                    max_funding_amount: 10,
+                },
+                ..Default::default()
+            },
+            milestone: BountyMilestone::Created {
+                created_at,
+                has_contributions: false,
+            },
+            ..Default::default()
+        };
+
+        System::set_block_number(created_at + funding_period + 1);
+
+        assert_eq!(
+            Bounty::get_bounty_stage(&bounty),
+            BountyStage::FundingExpired
+        );
+    });
+}
+
+#[test]
+fn validate_work_submission_bounty_stage() {
+    build_test_externalities().execute_with(|| {
+        let created_at = 10;
+        let funding_period = 10;
+        let work_period = 10;
+        let judging_period = 10;
+        let min_funding_amount = 100;
+        let work_period_started_at = created_at + funding_period;
+
+        // Limited funding period
+        let params = BountyCreationParameters::<Test> {
+            funding_type: FundingType::Limited {
+                funding_period,
+                min_funding_amount,
+                max_funding_amount: 10,
+            },
+            work_period,
+            ..Default::default()
+        };
+
+        let bounty = BountyRecord {
+            creation_params: params.clone(),
+            milestone: BountyMilestone::Created {
+                created_at,
+                has_contributions: true,
+            },
+            total_funding: min_funding_amount,
+            ..Default::default()
+        };
+
+        System::set_block_number(created_at + funding_period + 1);
+
+        assert_eq!(
+            Bounty::get_bounty_stage(&bounty),
+            BountyStage::WorkSubmission
+        );
+
+        // Max funding reached.
+        let max_funding_reached_at = 30;
+
+        let bounty = BountyRecord {
+            creation_params: params,
+            milestone: BountyMilestone::BountyMaxFundingReached {
+                max_funding_reached_at,
+            },
+            ..Default::default()
+        };
+
+        System::set_block_number(max_funding_reached_at + 1);
+
+        assert_eq!(
+            Bounty::get_bounty_stage(&bounty),
+            BountyStage::WorkSubmission
+        );
+
+        // Work period is not expired.
+        let bounty = BountyRecord {
+            creation_params: BountyCreationParameters::<Test> {
+                funding_type: FundingType::Limited {
+                    funding_period,
+                    min_funding_amount,
+                    max_funding_amount: 10,
+                },
+                work_period,
+                judging_period,
+                ..Default::default()
+            },
+            milestone: BountyMilestone::WorkSubmitted {
+                work_period_started_at,
+            },
+            ..Default::default()
+        };
+
+        System::set_block_number(work_period_started_at + 1);
+
+        assert_eq!(
+            Bounty::get_bounty_stage(&bounty),
+            BountyStage::WorkSubmission
+        );
+    });
+}
+
+#[test]
+fn validate_judgment_bounty_stage() {
+    build_test_externalities().execute_with(|| {
+        let created_at = 10;
+        let funding_period = 10;
+        let work_period = 10;
+        let judging_period = 10;
+        let min_funding_amount = 100;
+        let work_period_started_at = created_at + funding_period;
+
+        // Work period is not expired.
+        let bounty = BountyRecord {
+            creation_params: BountyCreationParameters::<Test> {
+                funding_type: FundingType::Limited {
+                    funding_period,
+                    min_funding_amount,
+                    max_funding_amount: 10,
+                },
+                work_period,
+                judging_period,
+                ..Default::default()
+            },
+            milestone: BountyMilestone::WorkSubmitted {
+                work_period_started_at,
+            },
+            ..Default::default()
+        };
+
+        System::set_block_number(work_period_started_at + work_period + 1);
+
+        assert_eq!(Bounty::get_bounty_stage(&bounty), BountyStage::Judgment);
+    });
+}
+
+#[test]
+fn validate_successful_withdrawal_bounty_stage() {
+    build_test_externalities().execute_with(|| {
+        // Judging was submitted.
+        let successful_bounty = true;
+        let bounty = BountyRecord {
+            milestone: BountyMilestone::JudgmentSubmitted { successful_bounty },
+            ..Default::default()
+        };
+
+        assert_eq!(
+            Bounty::get_bounty_stage(&bounty),
+            BountyStage::SuccessfulBountyWithdrawal
+        );
+    });
+}
+
+#[test]
+fn create_bounty_succeeds() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let text = b"Bounty text".to_vec();
+
+        let create_bounty_fixture = CreateBountyFixture::default().with_metadata(text.clone());
+        create_bounty_fixture.call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        EventFixture::assert_last_crate_event(RawEvent::BountyCreated(
+            bounty_id,
+            create_bounty_fixture.get_bounty_creation_parameters(),
+            text,
+        ));
+    });
+}
+
+#[test]
+fn create_bounty_fails_with_invalid_closed_contract() {
+    build_test_externalities().execute_with(|| {
+        CreateBountyFixture::default()
+            .with_closed_contract(Vec::new())
+            .call_and_assert(Err(Error::<Test>::ClosedContractMemberListIsEmpty.into()));
+
+        let large_member_id_list: Vec<u64> = (1..(ClosedContractSizeLimit::get() + 10))
+            .map(|x| x.into())
+            .collect();
+
+        CreateBountyFixture::default()
+            .with_closed_contract(large_member_id_list)
+            .call_and_assert(Err(Error::<Test>::ClosedContractMemberListIsTooLarge.into()));
+    });
+}
+
+#[test]
+fn create_bounty_fails_with_insufficient_cherry_value() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        CreateBountyFixture::default()
+            .with_cherry(0)
+            .call_and_assert(Err(Error::<Test>::CherryLessThenMinimumAllowed.into()));
+    });
+}
+
+#[test]
+fn create_bounty_transfers_member_balance_correctly() {
+    build_test_externalities().execute_with(|| {
+        let member_id = 1;
+        let account_id = 1;
+        let cherry = 100;
+        let initial_balance = 500;
+
+        increase_total_balance_issuance_using_account_id(account_id, initial_balance);
+
+        // Insufficient member controller account balance.
+        CreateBountyFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_creator_member_id(member_id)
+            .with_cherry(cherry)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            balances::Module::<Test>::usable_balance(&account_id),
+            initial_balance - cherry
+        );
+
+        let bounty_id = 1;
+
+        assert_eq!(
+            balances::Module::<Test>::usable_balance(&Bounty::bounty_account_id(bounty_id)),
+            cherry
+        );
+    });
+}
+
+#[test]
+fn create_bounty_transfers_the_council_balance_correctly() {
+    build_test_externalities().execute_with(|| {
+        let cherry = 100;
+        let initial_balance = 500;
+
+        set_council_budget(initial_balance);
+
+        // Insufficient member controller account balance.
+        CreateBountyFixture::default()
+            .with_cherry(cherry)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(get_council_budget(), initial_balance - cherry);
+
+        let bounty_id = 1;
+
+        assert_eq!(
+            balances::Module::<Test>::usable_balance(&Bounty::bounty_account_id(bounty_id)),
+            cherry
+        );
+    });
+}
+
+#[test]
+fn create_bounty_fails_with_invalid_origin() {
+    build_test_externalities().execute_with(|| {
+        // For a council bounty.
+        CreateBountyFixture::default()
+            .with_origin(RawOrigin::Signed(1))
+            .call_and_assert(Err(DispatchError::BadOrigin));
+
+        // For a member bounty.
+        CreateBountyFixture::default()
+            .with_origin(RawOrigin::Root)
+            .with_creator_member_id(1)
+            .call_and_assert(Err(DispatchError::BadOrigin));
+    });
+}
+
+#[test]
+fn create_bounty_fails_with_invalid_funding_parameters() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        CreateBountyFixture::default()
+            .with_limited_funding(0, 1, 1)
+            .call_and_assert(Err(Error::<Test>::FundingAmountCannotBeZero.into()));
+
+        CreateBountyFixture::default()
+            .with_limited_funding(1, 0, 1)
+            .call_and_assert(Err(Error::<Test>::FundingAmountCannotBeZero.into()));
+
+        CreateBountyFixture::default()
+            .with_limited_funding(1, 1, 0)
+            .call_and_assert(Err(Error::<Test>::FundingPeriodCannotBeZero.into()));
+
+        CreateBountyFixture::default()
+            .with_perpetual_funding(0)
+            .call_and_assert(Err(Error::<Test>::FundingAmountCannotBeZero.into()));
+
+        CreateBountyFixture::default()
+            .with_limited_funding(100, 1, 100)
+            .call_and_assert(Err(
+                Error::<Test>::MinFundingAmountCannotBeGreaterThanMaxAmount.into(),
+            ));
+    });
+}
+
+#[test]
+fn create_bounty_fails_with_invalid_entrant_stake() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        let invalid_stake = 1;
+        CreateBountyFixture::default()
+            .with_entrant_stake(invalid_stake)
+            .call_and_assert(Err(Error::<Test>::EntrantStakeIsLessThanMininum.into()));
+    });
+}
+
+#[test]
+fn create_bounty_fails_with_invalid_periods() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        CreateBountyFixture::default()
+            .with_work_period(0)
+            .call_and_assert(Err(Error::<Test>::WorkPeriodCannotBeZero.into()));
+
+        CreateBountyFixture::default()
+            .with_judging_period(0)
+            .call_and_assert(Err(Error::<Test>::JudgingPeriodCannotBeZero.into()));
+    });
+}
+
+#[test]
+fn create_bounty_fails_with_insufficient_balances() {
+    build_test_externalities().execute_with(|| {
+        let member_id = 1;
+        let account_id = 1;
+        let cherry = 100;
+
+        // Insufficient council budget.
+        CreateBountyFixture::default()
+            .with_cherry(cherry)
+            .call_and_assert(Err(Error::<Test>::InsufficientBalanceForBounty.into()));
+
+        // Insufficient member controller account balance.
+        CreateBountyFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_creator_member_id(member_id)
+            .with_cherry(cherry)
+            .call_and_assert(Err(Error::<Test>::InsufficientBalanceForBounty.into()));
+    });
+}
+
+#[test]
+fn cancel_bounty_succeeds_full_test() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let cherry = 100;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_cherry(cherry)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        assert_eq!(get_council_budget(), initial_balance - cherry);
+
+        CancelBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(get_council_budget(), initial_balance);
+
+        EventFixture::contains_crate_event(RawEvent::BountyCreatorCherryWithdrawal(
+            bounty_id,
+            BountyActor::Council,
+        ));
+
+        EventFixture::contains_crate_event(RawEvent::BountyRemoved(bounty_id));
+
+        EventFixture::assert_last_crate_event(RawEvent::BountyCanceled(
+            bounty_id,
+            BountyActor::Council,
+        ));
+    });
+}
+
+#[test]
+fn cancel_bounty_succeeds_at_funding_expired_stage() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        let funding_period = 10;
+        CreateBountyFixture::default()
+            .with_funding_period(funding_period)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        run_to_block(funding_period + 1);
+
+        // Funding period expired with no contribution.
+        CancelBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+    });
+}
+
+#[test]
+fn cancel_bounty_by_member_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let member_id = 1;
+        let account_id = 1;
+        let initial_balance = 500;
+
+        increase_total_balance_issuance_using_account_id(account_id, initial_balance);
+
+        CreateBountyFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_creator_member_id(member_id)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        CancelBountyFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_creator_member_id(member_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+    });
+}
+
+#[test]
+fn cancel_bounty_fails_with_invalid_bounty_id() {
+    build_test_externalities().execute_with(|| {
+        let invalid_bounty_id = 11u64;
+
+        CancelBountyFixture::default()
+            .with_bounty_id(invalid_bounty_id)
+            .call_and_assert(Err(Error::<Test>::BountyDoesntExist.into()));
+    });
+}
+
+#[test]
+fn cancel_bounty_fails_with_invalid_origin() {
+    build_test_externalities().execute_with(|| {
+        let member_id = 1;
+        let account_id = 1;
+        let initial_balance = 500;
+
+        increase_account_balance(&account_id, initial_balance);
+        set_council_budget(initial_balance);
+
+        // Created by council - try to cancel with bad origin
+        CreateBountyFixture::default()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+        CancelBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_origin(RawOrigin::Signed(account_id))
+            .call_and_assert(Err(DispatchError::BadOrigin));
+
+        // Created by a member - try to cancel with invalid member_id
+        CreateBountyFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_creator_member_id(member_id)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 2u64;
+        let invalid_member_id = 2;
+
+        CancelBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_creator_member_id(invalid_member_id)
+            .call_and_assert(Err(Error::<Test>::NotBountyActor.into()));
+
+        // Created by a member - try to cancel with bad origin
+        CreateBountyFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_creator_member_id(member_id)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 3u64;
+
+        CancelBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_origin(RawOrigin::None)
+            .call_and_assert(Err(DispatchError::BadOrigin));
+
+        // Created by a member  - try to cancel by council
+        CreateBountyFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_creator_member_id(member_id)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 4u64;
+
+        CancelBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Err(Error::<Test>::NotBountyActor.into()));
+    });
+}
+
+#[test]
+fn cancel_bounty_fails_with_invalid_stage() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        // Test bounty with funding.
+        CreateBountyFixture::default().call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(MinFundingLimit::get())
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        CancelBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Err(Error::<Test>::InvalidStageUnexpectedFunding.into()));
+    });
+}
+
+#[test]
+fn veto_bounty_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let cherry = 100;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_cherry(cherry)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        assert_eq!(get_council_budget(), initial_balance - cherry);
+
+        VetoBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(get_council_budget(), initial_balance);
+
+        EventFixture::contains_crate_event(RawEvent::BountyCreatorCherryWithdrawal(
+            bounty_id,
+            BountyActor::Council,
+        ));
+
+        EventFixture::contains_crate_event(RawEvent::BountyRemoved(bounty_id));
+
+        EventFixture::assert_last_crate_event(RawEvent::BountyVetoed(bounty_id));
+    });
+}
+
+#[test]
+fn veto_bounty_fails_with_invalid_bounty_id() {
+    build_test_externalities().execute_with(|| {
+        let invalid_bounty_id = 11u64;
+
+        VetoBountyFixture::default()
+            .with_bounty_id(invalid_bounty_id)
+            .call_and_assert(Err(Error::<Test>::BountyDoesntExist.into()));
+    });
+}
+
+#[test]
+fn veto_bounty_fails_with_invalid_origin() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        let account_id = 1;
+
+        CreateBountyFixture::default()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        VetoBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_origin(RawOrigin::Signed(account_id))
+            .call_and_assert(Err(DispatchError::BadOrigin));
+    });
+}
+
+#[test]
+fn veto_bounty_fails_with_invalid_stage() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        // Test bounty with funding.
+        CreateBountyFixture::default().call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(MinFundingLimit::get())
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        VetoBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Err(Error::<Test>::InvalidStageUnexpectedFunding.into()));
+    });
+}
+
+#[test]
+fn fund_bounty_succeeds_by_member() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let max_amount = 500;
+        let amount = 100;
+        let account_id = 1;
+        let member_id = 1;
+        let initial_balance = 500;
+        let cherry = DEFAULT_BOUNTY_CHERRY;
+
+        increase_total_balance_issuance_using_account_id(account_id, initial_balance);
+        increase_total_balance_issuance_using_account_id(
+            COUNCIL_BUDGET_ACCOUNT_ID,
+            initial_balance,
+        );
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_cherry(cherry)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(amount)
+            .with_member_id(member_id)
+            .with_origin(RawOrigin::Signed(account_id))
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            balances::Module::<Test>::usable_balance(&account_id),
+            initial_balance - amount
+        );
+
+        assert_eq!(
+            crate::Module::<Test>::contribution_by_bounty_by_actor(
+                bounty_id,
+                BountyActor::Member(member_id)
+            ),
+            amount
+        );
+
+        assert_eq!(
+            balances::Module::<Test>::usable_balance(&Bounty::bounty_account_id(bounty_id)),
+            amount + cherry
+        );
+
+        EventFixture::assert_last_crate_event(RawEvent::BountyFunded(
+            bounty_id,
+            BountyActor::Member(member_id),
+            amount,
+        ));
+    });
+}
+
+#[test]
+fn fund_bounty_succeeds_by_council() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let max_amount = 500;
+        let amount = 100;
+        let initial_balance = 500;
+        let cherry = DEFAULT_BOUNTY_CHERRY;
+
+        increase_account_balance(&COUNCIL_BUDGET_ACCOUNT_ID, initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_cherry(cherry)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            balances::Module::<Test>::usable_balance(&COUNCIL_BUDGET_ACCOUNT_ID),
+            initial_balance - amount - cherry
+        );
+
+        assert_eq!(
+            crate::Module::<Test>::contribution_by_bounty_by_actor(bounty_id, BountyActor::Council),
+            amount
+        );
+
+        assert_eq!(
+            balances::Module::<Test>::usable_balance(&Bounty::bounty_account_id(bounty_id)),
+            amount + cherry
+        );
+
+        EventFixture::assert_last_crate_event(RawEvent::BountyFunded(
+            bounty_id,
+            BountyActor::Council,
+            amount,
+        ));
+    });
+}
+
+#[test]
+fn fund_bounty_succeeds_with_reaching_max_funding_amount() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let max_amount = 50;
+        let amount = 100;
+        let account_id = 1;
+        let member_id = 1;
+        let initial_balance = 500;
+
+        increase_total_balance_issuance_using_account_id(account_id, initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(amount)
+            .with_member_id(member_id)
+            .with_origin(RawOrigin::Signed(account_id))
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            balances::Module::<Test>::usable_balance(&account_id),
+            initial_balance - max_amount
+        );
+
+        let bounty = Bounty::bounties(&bounty_id);
+        assert_eq!(
+            bounty.milestone,
+            BountyMilestone::BountyMaxFundingReached {
+                max_funding_reached_at: starting_block,
+            }
+        );
+
+        EventFixture::assert_last_crate_event(RawEvent::BountyMaxFundingReached(bounty_id));
+    });
+}
+
+#[test]
+fn multiple_fund_bounty_succeed() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        let max_amount = 5000;
+        let amount = 100;
+        let account_id = 1;
+        let member_id = 1;
+        let initial_balance = 500;
+        let cherry = DEFAULT_BOUNTY_CHERRY;
+
+        increase_total_balance_issuance_using_account_id(account_id, initial_balance);
+        increase_total_balance_issuance_using_account_id(
+            COUNCIL_BUDGET_ACCOUNT_ID,
+            initial_balance,
+        );
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_cherry(cherry)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(amount)
+            .with_member_id(member_id)
+            .with_origin(RawOrigin::Signed(account_id))
+            .call_and_assert(Ok(()));
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(amount)
+            .with_member_id(member_id)
+            .with_origin(RawOrigin::Signed(account_id))
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            balances::Module::<Test>::usable_balance(&account_id),
+            initial_balance - 2 * amount
+        );
+
+        assert_eq!(
+            balances::Module::<Test>::usable_balance(&Bounty::bounty_account_id(bounty_id)),
+            2 * amount + cherry
+        );
+    });
+}
+
+#[test]
+fn fund_bounty_fails_with_invalid_bounty_id() {
+    build_test_externalities().execute_with(|| {
+        let invalid_bounty_id = 11u64;
+
+        FundBountyFixture::default()
+            .with_bounty_id(invalid_bounty_id)
+            .call_and_assert(Err(Error::<Test>::BountyDoesntExist.into()));
+    });
+}
+
+#[test]
+fn fund_bounty_fails_with_invalid_origin() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        CreateBountyFixture::default()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Err(DispatchError::BadOrigin));
+    });
+}
+
+#[test]
+fn fund_bounty_fails_with_insufficient_balance() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        let member_id = 1;
+        let account_id = 1;
+        let amount = 100;
+
+        CreateBountyFixture::default()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        FundBountyFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_amount(amount)
+            .call_and_assert(Err(Error::<Test>::InsufficientBalanceForBounty.into()));
+    });
+}
+
+#[test]
+fn fund_bounty_fails_with_zero_amount() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        let member_id = 1;
+        let account_id = 1;
+        let amount = 0;
+
+        CreateBountyFixture::default()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        FundBountyFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_amount(amount)
+            .call_and_assert(Err(Error::<Test>::ZeroFundingAmount.into()));
+    });
+}
+
+#[test]
+fn fund_bounty_fails_with_less_than_minimum_amount() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        let member_id = 1;
+        let account_id = 1;
+        let amount = 10;
+
+        CreateBountyFixture::default()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        FundBountyFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_amount(amount)
+            .call_and_assert(Err(Error::<Test>::FundingLessThenMinimumAllowed.into()));
+    });
+}
+
+#[test]
+fn fund_bounty_fails_with_invalid_stage() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        let max_amount = 100;
+        let amount = 100;
+        let account_id = 1;
+        let member_id = 1;
+        let initial_balance = 500;
+
+        increase_total_balance_issuance_using_account_id(account_id, initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        // Fund to maximum.
+        FundBountyFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .call_and_assert(Ok(()));
+
+        FundBountyFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_bounty_id(bounty_id)
+            .with_amount(amount)
+            .call_and_assert(Err(
+                Error::<Test>::InvalidStageUnexpectedWorkSubmission.into()
+            ));
+    });
+}
+
+#[test]
+fn fund_bounty_fails_with_expired_funding_period() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        let amount = 100;
+        let account_id = 1;
+        let member_id = 1;
+        let initial_balance = 500;
+        let funding_period = 10;
+
+        increase_total_balance_issuance_using_account_id(account_id, initial_balance);
+
+        CreateBountyFixture::default()
+            .with_origin(RawOrigin::Root)
+            .with_funding_period(funding_period)
+            .call_and_assert(Ok(()));
+
+        run_to_block(funding_period + 1);
+
+        FundBountyFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_amount(amount)
+            .call_and_assert(Err(
+                Error::<Test>::InvalidStageUnexpectedFundingExpired.into()
+            ));
+    });
+}
+
+#[test]
+fn withdraw_member_funding_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let max_amount = 500;
+        let amount = 100;
+        let account_id = 1;
+        let member_id = 1;
+        let initial_balance = 500;
+        let cherry = 200;
+        let funding_period = 10;
+
+        increase_account_balance(&COUNCIL_BUDGET_ACCOUNT_ID, initial_balance);
+        increase_account_balance(&account_id, initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_limited_funding(max_amount, max_amount, funding_period)
+            .with_cherry(cherry)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        FundBountyFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_amount(amount)
+            .call_and_assert(Ok(()));
+
+        run_to_block(funding_period + starting_block + 1);
+
+        WithdrawFundingFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_member_id(member_id)
+            .with_origin(RawOrigin::Signed(account_id))
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            balances::Module::<Test>::usable_balance(&account_id),
+            initial_balance + cherry
+        );
+
+        assert_eq!(
+            balances::Module::<Test>::usable_balance(&COUNCIL_BUDGET_ACCOUNT_ID),
+            initial_balance - cherry
+        );
+
+        assert_eq!(
+            balances::Module::<Test>::usable_balance(&Bounty::bounty_account_id(bounty_id)),
+            0
+        );
+
+        EventFixture::assert_last_crate_event(RawEvent::BountyRemoved(bounty_id));
+    });
+}
+
+#[test]
+fn withdraw_council_funding_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let max_amount = 500;
+        let amount = 100;
+        let initial_balance = 500;
+        let cherry = 200;
+        let funding_period = 10;
+
+        increase_account_balance(&COUNCIL_BUDGET_ACCOUNT_ID, initial_balance);
+
+        CreateBountyFixture::default()
+            .with_limited_funding(max_amount, max_amount, funding_period)
+            .with_cherry(cherry)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        FundBountyFixture::default()
+            .with_origin(RawOrigin::Root)
+            .with_council()
+            .with_amount(amount)
+            .call_and_assert(Ok(()));
+
+        run_to_block(funding_period + starting_block + 1);
+
+        WithdrawFundingFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            balances::Module::<Test>::usable_balance(&COUNCIL_BUDGET_ACCOUNT_ID),
+            initial_balance
+        );
+
+        assert_eq!(
+            balances::Module::<Test>::usable_balance(&Bounty::bounty_account_id(bounty_id)),
+            0
+        );
+
+        EventFixture::assert_last_crate_event(RawEvent::BountyRemoved(bounty_id));
+    });
+}
+
+#[test]
+fn withdraw_member_funding_with_half_cherry() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let max_amount = 500;
+        let amount = 100;
+        let account_id1 = 1;
+        let member_id1 = 1;
+        let account_id2 = 2;
+        let member_id2 = 2;
+        let initial_balance = 500;
+        let cherry = 200;
+        let funding_period = 10;
+
+        increase_account_balance(&COUNCIL_BUDGET_ACCOUNT_ID, initial_balance);
+        increase_account_balance(&account_id1, initial_balance);
+        increase_account_balance(&account_id2, initial_balance);
+
+        CreateBountyFixture::default()
+            .with_limited_funding(max_amount, max_amount, funding_period)
+            .with_cherry(cherry)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        FundBountyFixture::default()
+            .with_origin(RawOrigin::Signed(account_id1))
+            .with_member_id(member_id1)
+            .with_amount(amount)
+            .call_and_assert(Ok(()));
+
+        FundBountyFixture::default()
+            .with_origin(RawOrigin::Signed(account_id2))
+            .with_member_id(member_id2)
+            .with_amount(amount)
+            .call_and_assert(Ok(()));
+
+        run_to_block(funding_period + starting_block + 1);
+
+        WithdrawFundingFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_member_id(member_id1)
+            .with_origin(RawOrigin::Signed(account_id1))
+            .call_and_assert(Ok(()));
+
+        // A half of the cherry
+        assert_eq!(
+            balances::Module::<Test>::usable_balance(&account_id1),
+            initial_balance + cherry / 2
+        );
+
+        // On funding amount + creation funding + half of the cherry left.
+        assert_eq!(
+            balances::Module::<Test>::usable_balance(&Bounty::bounty_account_id(bounty_id)),
+            amount + cherry / 2
+        );
+
+        EventFixture::assert_last_crate_event(RawEvent::BountyFundingWithdrawal(
+            bounty_id,
+            BountyActor::Member(member_id1),
+        ));
+    });
+}
+
+#[test]
+fn withdraw_member_funding_removes_bounty() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let max_amount = 500;
+        let amount = 100;
+        let account_id = 1;
+        let member_id = 1;
+        let initial_balance = 500;
+        let cherry = 200;
+        let funding_period = 10;
+
+        increase_account_balance(&COUNCIL_BUDGET_ACCOUNT_ID, initial_balance);
+        increase_account_balance(&account_id, initial_balance);
+
+        CreateBountyFixture::default()
+            .with_limited_funding(max_amount, max_amount, funding_period)
+            .with_cherry(cherry)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        FundBountyFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_amount(amount)
+            .call_and_assert(Ok(()));
+
+        run_to_block(funding_period + starting_block + 1);
+
+        WithdrawFundingFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_member_id(member_id)
+            .with_origin(RawOrigin::Signed(account_id))
+            .call_and_assert(Ok(()));
+
+        EventFixture::assert_last_crate_event(RawEvent::BountyRemoved(bounty_id));
+    });
+}
+
+#[test]
+fn withdraw_member_funding_fails_with_invalid_bounty_id() {
+    build_test_externalities().execute_with(|| {
+        let invalid_bounty_id = 11u64;
+
+        WithdrawFundingFixture::default()
+            .with_bounty_id(invalid_bounty_id)
+            .call_and_assert(Err(Error::<Test>::BountyDoesntExist.into()));
+    });
+}
+
+#[test]
+fn withdraw_member_funding_fails_with_invalid_origin() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        CreateBountyFixture::default()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        WithdrawFundingFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Err(DispatchError::BadOrigin));
+    });
+}
+
+#[test]
+fn withdraw_member_funding_fails_with_invalid_stage() {
+    build_test_externalities().execute_with(|| {
+        let max_amount = 500;
+        let amount = 100;
+        let account_id = 1;
+        let member_id = 1;
+        let initial_balance = 500;
+        let cherry = 200;
+        let funding_period = 10;
+
+        increase_account_balance(&COUNCIL_BUDGET_ACCOUNT_ID, initial_balance);
+        increase_account_balance(&account_id, initial_balance);
+
+        CreateBountyFixture::default()
+            .with_limited_funding(max_amount, max_amount, funding_period)
+            .with_cherry(cherry)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        FundBountyFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_amount(amount)
+            .call_and_assert(Ok(()));
+
+        WithdrawFundingFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_member_id(member_id)
+            .with_origin(RawOrigin::Signed(account_id))
+            .call_and_assert(Err(Error::<Test>::InvalidStageUnexpectedFunding.into()));
+    });
+}
+
+#[test]
+fn withdraw_member_funding_fails_with_invalid_bounty_funder() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let max_amount = 500;
+        let amount = 100;
+        let account_id = 1;
+        let member_id = 1;
+        let initial_balance = 500;
+        let cherry = 200;
+        let funding_period = 10;
+
+        increase_account_balance(&COUNCIL_BUDGET_ACCOUNT_ID, initial_balance);
+        increase_account_balance(&account_id, initial_balance);
+
+        CreateBountyFixture::default()
+            .with_limited_funding(max_amount, max_amount, funding_period)
+            .with_cherry(cherry)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        FundBountyFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_amount(amount)
+            .call_and_assert(Ok(()));
+
+        // Bounty failed because of the funding period
+        run_to_block(starting_block + funding_period + 1);
+
+        let invalid_account_id = 2;
+        let invalid_member_id = 2;
+
+        WithdrawFundingFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_member_id(invalid_member_id)
+            .with_origin(RawOrigin::Signed(invalid_account_id))
+            .call_and_assert(Err(Error::<Test>::NoBountyContributionFound.into()));
+    });
+}
+
+#[test]
+fn withdraw_member_funding_fails_with_successful_bounty() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+        let winner_reward = max_amount;
+        let entrant_stake = 37;
+        let work_period = 1;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_work_period(work_period)
+            .with_entrant_stake(entrant_stake)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        let member_id1 = 1;
+        let account_id1 = 1;
+        increase_account_balance(&account_id1, initial_balance);
+
+        // Winner
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id1))
+            .with_member_id(member_id1)
+            .with_staking_account_id(account_id1)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            Balances::usable_balance(&account_id1),
+            initial_balance - entrant_stake
+        );
+
+        let entry_id1 = 1;
+
+        SubmitWorkFixture::default()
+            .with_origin(RawOrigin::Signed(account_id1))
+            .with_member_id(member_id1)
+            .with_entry_id(entry_id1)
+            .call_and_assert(Ok(()));
+
+        // Judgment
+        let mut judgment = BTreeMap::new();
+        judgment.insert(
+            entry_id1,
+            OracleWorkEntryJudgment::Winner {
+                reward: winner_reward,
+            },
+        );
+
+        run_to_block(starting_block + work_period + 1);
+
+        SubmitJudgmentFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_judgment(judgment)
+            .call_and_assert(Ok(()));
+
+        WithdrawFundingFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Err(
+                Error::<Test>::InvalidStageUnexpectedSuccessfulBountyWithdrawal.into(),
+            ));
+    });
+}
+
+#[test]
+fn bounty_removal_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let max_amount = 500;
+        let amount = 100;
+        let initial_balance = 500;
+        let cherry = 100;
+        let account_id = 1;
+        let member_id = 1;
+        let funding_period = 10;
+
+        increase_account_balance(&account_id, initial_balance);
+
+        // Increment block in order to get Substrate events (no events on block 0).
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        // Create bounty
+        CreateBountyFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_creator_member_id(member_id)
+            .with_cherry(cherry)
+            .with_limited_funding(max_amount, max_amount, funding_period)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        // Member funding
+        let funding_account_id1 = 2;
+        let funding_member_id1 = 2;
+        increase_account_balance(&funding_account_id1, initial_balance);
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(amount)
+            .with_member_id(funding_member_id1)
+            .with_origin(RawOrigin::Signed(funding_account_id1))
+            .call_and_assert(Ok(()));
+
+        let funding_account_id2 = 3;
+        let funding_member_id2 = 3;
+        increase_account_balance(&funding_account_id2, initial_balance);
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(amount)
+            .with_member_id(funding_member_id2)
+            .with_origin(RawOrigin::Signed(funding_account_id2))
+            .call_and_assert(Ok(()));
+
+        let funding_account_id3 = 4;
+        let funding_member_id3 = 4;
+        increase_account_balance(&funding_account_id3, initial_balance);
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(amount)
+            .with_member_id(funding_member_id3)
+            .with_origin(RawOrigin::Signed(funding_account_id3))
+            .call_and_assert(Ok(()));
+
+        // Bounty failed because of the funding period
+        run_to_block(starting_block + funding_period + 1);
+
+        // Withdraw member funding
+        WithdrawFundingFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_member_id(funding_member_id1)
+            .with_origin(RawOrigin::Signed(funding_account_id1))
+            .call_and_assert(Ok(()));
+
+        WithdrawFundingFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_member_id(funding_member_id2)
+            .with_origin(RawOrigin::Signed(funding_account_id2))
+            .call_and_assert(Ok(()));
+
+        let cherry_remaining_fraction = cherry - (cherry * 2 / 3) + amount;
+        assert_eq!(
+            balances::Module::<Test>::usable_balance(&Bounty::bounty_account_id(bounty_id)),
+            cherry_remaining_fraction
+        );
+
+        WithdrawFundingFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_member_id(funding_member_id3)
+            .with_origin(RawOrigin::Signed(funding_account_id3))
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            balances::Module::<Test>::usable_balance(&account_id),
+            initial_balance - cherry
+        );
+
+        // Bounty removal effects.
+        assert_eq!(
+            balances::Module::<Test>::usable_balance(&Bounty::bounty_account_id(bounty_id)),
+            0
+        );
+
+        assert!(!crate::Bounties::<Test>::contains_key(bounty_id));
+        assert!(!Bounty::contributions_exist(&bounty_id));
+
+        EventFixture::assert_last_crate_event(RawEvent::BountyRemoved(bounty_id));
+    });
+}
+
+#[test]
+fn announce_work_entry_succeeded() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+        let entrant_stake = 37;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_entrant_stake(entrant_stake)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        let member_id = 1;
+        let account_id = 1;
+
+        increase_account_balance(&account_id, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_staking_account_id(account_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            Balances::usable_balance(&account_id),
+            initial_balance - entrant_stake
+        );
+
+        let entry_id = 1;
+
+        EventFixture::assert_last_crate_event(RawEvent::WorkEntryAnnounced(
+            entry_id, bounty_id, member_id, account_id,
+        ));
+    });
+}
+
+#[test]
+fn announce_work_entry_failed_with_closed_contract() {
+    build_test_externalities().execute_with(|| {
+        let initial_balance = 500;
+        let max_amount = 100;
+        let entrant_stake = 37;
+
+        let closed_contract_member_ids = vec![2, 3];
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_entrant_stake(entrant_stake)
+            .with_closed_contract(closed_contract_member_ids)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        let member_id = 1;
+        let account_id = 1;
+
+        increase_account_balance(&account_id, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_staking_account_id(account_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Err(
+                Error::<Test>::CannotSubmitWorkToClosedContractBounty.into()
+            ));
+    });
+}
+
+#[test]
+fn announce_work_entry_fails_with_invalid_bounty_id() {
+    build_test_externalities().execute_with(|| {
+        let invalid_bounty_id = 11u64;
+
+        AnnounceWorkEntryFixture::default()
+            .with_bounty_id(invalid_bounty_id)
+            .call_and_assert(Err(Error::<Test>::BountyDoesntExist.into()));
+    });
+}
+
+#[test]
+fn announce_work_entry_fails_with_invalid_origin() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        CreateBountyFixture::default()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        AnnounceWorkEntryFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Err(DispatchError::BadOrigin));
+    });
+}
+
+#[test]
+fn announce_work_entry_fails_with_invalid_stage() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        CreateBountyFixture::default().call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        AnnounceWorkEntryFixture::default()
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Err(Error::<Test>::InvalidStageUnexpectedFunding.into()));
+    });
+}
+
+#[test]
+fn announce_work_entry_fails_with_invalid_staking_data() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+        let entrant_stake = 37;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_entrant_stake(entrant_stake)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        let member_id = 1;
+        let account_id = 1;
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_staking_account_id(account_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Err(Error::<Test>::InsufficientBalanceForStake.into()));
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_staking_account_id(STAKING_ACCOUNT_ID_NOT_BOUND_TO_MEMBER)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Err(Error::<Test>::InvalidStakingAccountForMember.into()));
+
+        increase_account_balance(&account_id, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_staking_account_id(account_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_staking_account_id(account_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Err(Error::<Test>::ConflictingStakes.into()));
+    });
+}
+
+#[test]
+fn withdraw_work_entry_succeeded() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+        let entrant_stake = 37;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_entrant_stake(entrant_stake)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+        let member_id = 1;
+        let account_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        increase_account_balance(&account_id, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_staking_account_id(account_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            Balances::usable_balance(&account_id),
+            initial_balance - entrant_stake
+        );
+
+        let entry_id = 1;
+
+        WithdrawWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_entry_id(entry_id)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(Balances::usable_balance(&account_id), initial_balance);
+
+        EventFixture::assert_last_crate_event(RawEvent::WorkEntryWithdrawn(
+            bounty_id, entry_id, member_id,
+        ));
+    });
+}
+
+#[test]
+fn withdraw_work_slashes_successfully1() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+        let entrant_stake = 100;
+        let work_period = 1000;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_entrant_stake(entrant_stake)
+            .with_work_period(work_period)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        // Announcing entry with no slashes
+        let member_id1 = 1;
+        let account_id1 = 1;
+
+        increase_account_balance(&account_id1, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id1))
+            .with_member_id(member_id1)
+            .with_staking_account_id(account_id1)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            Balances::usable_balance(&account_id1),
+            initial_balance - entrant_stake
+        );
+
+        let entry_id1 = 1;
+
+        // Announcing entry with half slashing.
+
+        let member_id2 = 2;
+        let account_id2 = 2;
+
+        increase_account_balance(&account_id2, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id2))
+            .with_member_id(member_id2)
+            .with_staking_account_id(account_id2)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            Balances::usable_balance(&account_id2),
+            initial_balance - entrant_stake
+        );
+
+        let entry_id2 = 2;
+
+        // No slashes
+        WithdrawWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id1))
+            .with_member_id(member_id1)
+            .with_entry_id(entry_id1)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(Balances::usable_balance(&account_id1), initial_balance);
+
+        // Slashes half.
+        let half_period = work_period / 2;
+        run_to_block(starting_block + half_period);
+
+        WithdrawWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id2))
+            .with_member_id(member_id2)
+            .with_entry_id(entry_id2)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            Balances::usable_balance(&account_id2),
+            initial_balance - entrant_stake / 2
+        );
+    });
+}
+
+#[test]
+fn withdraw_work_slashes_successfully2() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+        let entrant_stake = 100;
+        let work_period = 1000;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_entrant_stake(entrant_stake)
+            .with_work_period(work_period)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        // Announcing entry with 33%
+        let member_id1 = 1;
+        let account_id1 = 1;
+
+        increase_account_balance(&account_id1, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id1))
+            .with_member_id(member_id1)
+            .with_staking_account_id(account_id1)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            Balances::usable_balance(&account_id1),
+            initial_balance - entrant_stake
+        );
+
+        let entry_id1 = 1;
+
+        // Announcing entry with full slashing.
+
+        let member_id2 = 2;
+        let account_id2 = 2;
+
+        increase_account_balance(&account_id2, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id2))
+            .with_member_id(member_id2)
+            .with_staking_account_id(account_id2)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            Balances::usable_balance(&account_id2),
+            initial_balance - entrant_stake
+        );
+
+        let entry_id2 = 2;
+
+        // Slashes half.
+        let one_third_period = work_period / 3;
+        run_to_block(starting_block + one_third_period);
+
+        WithdrawWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id1))
+            .with_member_id(member_id1)
+            .with_entry_id(entry_id1)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            Balances::usable_balance(&account_id1),
+            initial_balance - entrant_stake / 3
+        );
+
+        // Slashes all.
+        run_to_block(starting_block + work_period);
+
+        WithdrawWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id2))
+            .with_member_id(member_id2)
+            .with_entry_id(entry_id2)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            Balances::usable_balance(&account_id2),
+            initial_balance - entrant_stake
+        );
+    });
+}
+
+#[test]
+fn withdraw_work_entry_fails_with_invalid_bounty_id() {
+    build_test_externalities().execute_with(|| {
+        let invalid_bounty_id = 11u64;
+
+        WithdrawWorkEntryFixture::default()
+            .with_bounty_id(invalid_bounty_id)
+            .call_and_assert(Err(Error::<Test>::BountyDoesntExist.into()));
+    });
+}
+
+#[test]
+fn withdraw_work_entry_fails_with_invalid_entry_id() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+        let entrant_stake = 37;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_entrant_stake(entrant_stake)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        let invalid_entry_id = 11u64;
+
+        WithdrawWorkEntryFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_entry_id(invalid_entry_id)
+            .call_and_assert(Err(Error::<Test>::WorkEntryDoesntExist.into()));
+    });
+}
+
+#[test]
+fn withdraw_work_entry_fails_with_invalid_origin() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+        let member_id = 1;
+        let account_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        increase_account_balance(&account_id, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        let entry_id = 1;
+
+        WithdrawWorkEntryFixture::default()
+            .with_entry_id(entry_id)
+            .with_bounty_id(bounty_id)
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Err(DispatchError::BadOrigin));
+    });
+}
+
+#[test]
+fn withdraw_work_entry_fails_with_invalid_stage() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+        let entrant_stake = 37;
+        let work_period = 10;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_entrant_stake(entrant_stake)
+            .with_work_period(work_period)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+        let member_id = 1;
+        let account_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        increase_account_balance(&account_id, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_staking_account_id(account_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            Balances::usable_balance(&account_id),
+            initial_balance - entrant_stake
+        );
+
+        let entry_id = 1;
+
+        run_to_block(starting_block + work_period + 1);
+
+        WithdrawWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_entry_id(entry_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Err(
+                Error::<Test>::InvalidStageUnexpectedFailedBountyWithdrawal.into(),
+            ));
+    });
+}
+
+#[test]
+fn submit_work_succeeded() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+        let entrant_stake = 37;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_entrant_stake(entrant_stake)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+        let member_id = 1;
+        let account_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        increase_account_balance(&account_id, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_staking_account_id(account_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        let entry_id = 1;
+
+        let work_data = b"Work submitted".to_vec();
+        SubmitWorkFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_entry_id(entry_id)
+            .with_work_data(work_data.clone())
+            .call_and_assert(Ok(()));
+
+        EventFixture::assert_last_crate_event(RawEvent::WorkSubmitted(
+            bounty_id, entry_id, member_id, work_data,
+        ));
+    });
+}
+
+#[test]
+fn submit_work_fails_with_invalid_bounty_id() {
+    build_test_externalities().execute_with(|| {
+        let invalid_bounty_id = 11u64;
+
+        SubmitWorkFixture::default()
+            .with_bounty_id(invalid_bounty_id)
+            .call_and_assert(Err(Error::<Test>::BountyDoesntExist.into()));
+    });
+}
+
+#[test]
+fn submit_work_fails_with_invalid_entry_id() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+        let entrant_stake = 37;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_entrant_stake(entrant_stake)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        let invalid_entry_id = 11u64;
+
+        SubmitWorkFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_entry_id(invalid_entry_id)
+            .call_and_assert(Err(Error::<Test>::WorkEntryDoesntExist.into()));
+    });
+}
+
+#[test]
+fn submit_work_fails_with_invalid_origin() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+        let member_id = 1;
+        let account_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        increase_account_balance(&account_id, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        let entry_id = 1;
+
+        SubmitWorkFixture::default()
+            .with_entry_id(entry_id)
+            .with_bounty_id(bounty_id)
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Err(DispatchError::BadOrigin));
+    });
+}
+
+#[test]
+fn submit_work_fails_with_invalid_stage() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+        let entrant_stake = 37;
+        let work_period = 10;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_entrant_stake(entrant_stake)
+            .with_work_period(work_period)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+        let member_id = 1;
+        let account_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        increase_account_balance(&account_id, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_staking_account_id(account_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            Balances::usable_balance(&account_id),
+            initial_balance - entrant_stake
+        );
+
+        let entry_id = 1;
+
+        run_to_block(starting_block + work_period + 1);
+
+        SubmitWorkFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_entry_id(entry_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Err(
+                Error::<Test>::InvalidStageUnexpectedFailedBountyWithdrawal.into(),
+            ));
+    });
+}
+
+#[test]
+fn submit_judgment_by_council_succeeded_with_complex_judgment() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+        let entrant_stake = 37;
+        let working_period = 10;
+        let judging_period = 10;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_entrant_stake(entrant_stake)
+            .with_work_period(working_period)
+            .with_judging_period(judging_period)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+        let member_id = 1;
+        let account_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        // First work entry
+        increase_account_balance(&account_id, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_staking_account_id(account_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        let entry_id1 = 1u64;
+
+        let work_data = b"Work submitted".to_vec();
+        SubmitWorkFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_entry_id(entry_id1)
+            .with_work_data(work_data.clone())
+            .call_and_assert(Ok(()));
+
+        // Second work entry
+        let member_id = 2;
+        let account_id = 2;
+
+        increase_account_balance(&account_id, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_staking_account_id(account_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        let entry_id2 = 2u64;
+
+        let work_data = b"Work submitted".to_vec();
+        SubmitWorkFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_entry_id(entry_id2)
+            .with_work_data(work_data.clone())
+            .call_and_assert(Ok(()));
+
+        // Third work entry
+        let member_id = 3;
+        let account_id = 3;
+
+        increase_account_balance(&account_id, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_staking_account_id(account_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        let entry_id3 = 3u64;
+
+        let work_data = b"Work submitted".to_vec();
+        SubmitWorkFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_entry_id(entry_id3)
+            .with_work_data(work_data.clone())
+            .call_and_assert(Ok(()));
+
+        run_to_block(starting_block + working_period + 1);
+
+        // Judgment
+        let judgment = vec![
+            (
+                entry_id1,
+                OracleWorkEntryJudgment::Winner { reward: max_amount },
+            ),
+            (entry_id3, OracleWorkEntryJudgment::Rejected),
+        ]
+        .iter()
+        .cloned()
+        .collect::<BTreeMap<_, _>>();
+
+        assert!(<Entries<Test>>::contains_key(entry_id3));
+        assert_eq!(Balances::total_balance(&account_id), initial_balance);
+
+        SubmitJudgmentFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_judgment(judgment.clone())
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            Bounty::entries(entry_id1).oracle_judgment_result,
+            Some(OracleWorkEntryJudgment::Winner { reward: max_amount })
+        );
+        assert_eq!(Bounty::entries(entry_id2).oracle_judgment_result, None);
+        assert!(!<Entries<Test>>::contains_key(entry_id3));
+        assert_eq!(
+            Balances::total_balance(&account_id),
+            initial_balance - entrant_stake
+        );
+
+        EventFixture::contains_crate_event(RawEvent::WorkEntrySlashed(bounty_id, entry_id3));
+        EventFixture::assert_last_crate_event(RawEvent::OracleJudgmentSubmitted(
+            bounty_id,
+            BountyActor::Council,
+            judgment,
+        ));
+    });
+}
+
+#[test]
+fn submit_judgment_returns_cherry_on_successful_bounty() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+        let cherry = DEFAULT_BOUNTY_CHERRY;
+        let entrant_stake = 37;
+        let working_period = 10;
+        let judging_period = 10;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_entrant_stake(entrant_stake)
+            .with_work_period(working_period)
+            .with_judging_period(judging_period)
+            .with_cherry(cherry)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+        let member_id = 1;
+        let account_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        increase_account_balance(&account_id, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_staking_account_id(account_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        let entry_id = 1u64;
+
+        let work_data = b"Work submitted".to_vec();
+        SubmitWorkFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_entry_id(entry_id)
+            .with_work_data(work_data.clone())
+            .call_and_assert(Ok(()));
+
+        run_to_block(starting_block + working_period + 1);
+
+        // Judgment
+        let judgment = vec![(
+            entry_id,
+            OracleWorkEntryJudgment::Winner { reward: max_amount },
+        )]
+        .iter()
+        .cloned()
+        .collect::<BTreeMap<_, _>>();
+
+        SubmitJudgmentFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_judgment(judgment.clone())
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            Bounty::entries(entry_id).oracle_judgment_result,
+            Some(OracleWorkEntryJudgment::Winner { reward: max_amount })
+        );
+
+        // Cherry returned.
+        assert_eq!(
+            get_council_budget(),
+            initial_balance - max_amount // initial - funding_amount
+        );
+
+        EventFixture::contains_crate_event(RawEvent::BountyCreatorCherryWithdrawal(
+            bounty_id,
+            BountyActor::Council,
+        ));
+    });
+}
+
+#[test]
+fn submit_judgment_dont_return_cherry_on_unsuccessful_bounty() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+        let cherry = DEFAULT_BOUNTY_CHERRY;
+        let entrant_stake = 37;
+        let working_period = 10;
+        let judging_period = 10;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_entrant_stake(entrant_stake)
+            .with_work_period(working_period)
+            .with_judging_period(judging_period)
+            .with_cherry(cherry)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+        let member_id = 1;
+        let account_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        increase_account_balance(&account_id, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_staking_account_id(account_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        let entry_id = 1u64;
+
+        let work_data = b"Work submitted".to_vec();
+        SubmitWorkFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_entry_id(entry_id)
+            .with_work_data(work_data.clone())
+            .call_and_assert(Ok(()));
+
+        run_to_block(starting_block + working_period + 1);
+
+        // Judgment
+        let judgment = vec![(entry_id, OracleWorkEntryJudgment::Rejected)]
+            .iter()
+            .cloned()
+            .collect::<BTreeMap<_, _>>();
+
+        assert!(<Entries<Test>>::contains_key(entry_id));
+
+        SubmitJudgmentFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_judgment(judgment.clone())
+            .call_and_assert(Ok(()));
+
+        assert!(!<Entries<Test>>::contains_key(entry_id));
+
+        // Cherry not returned.
+        assert_eq!(
+            get_council_budget(),
+            initial_balance - max_amount - cherry // initial - funding_amount - cherry
+        );
+    });
+}
+
+#[test]
+fn submit_judgment_by_member_succeeded() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+        let entrant_stake = 37;
+        let working_period = 10;
+        let judging_period = 10;
+        let oracle_member_id = 1;
+        let oracle_account_id = 1;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_entrant_stake(entrant_stake)
+            .with_work_period(working_period)
+            .with_judging_period(judging_period)
+            .with_oracle_member_id(oracle_member_id)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+        let member_id = 1;
+        let account_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        increase_account_balance(&account_id, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_staking_account_id(account_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        let entry_id = 1;
+
+        let work_data = b"Work submitted".to_vec();
+        SubmitWorkFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_entry_id(entry_id)
+            .with_work_data(work_data.clone())
+            .call_and_assert(Ok(()));
+
+        run_to_block(starting_block + working_period + 1);
+
+        let judgment = vec![entry_id]
+            .iter()
+            .map(|entry_id| {
+                (
+                    *entry_id,
+                    OracleWorkEntryJudgment::Winner { reward: max_amount },
+                )
+            })
+            .collect::<BTreeMap<_, _>>();
+
+        SubmitJudgmentFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_origin(RawOrigin::Signed(oracle_account_id))
+            .with_oracle_member_id(oracle_member_id)
+            .with_judgment(judgment.clone())
+            .call_and_assert(Ok(()));
+
+        EventFixture::assert_last_crate_event(RawEvent::OracleJudgmentSubmitted(
+            bounty_id,
+            BountyActor::Member(oracle_member_id),
+            judgment,
+        ));
+    });
+}
+
+#[test]
+fn submit_judgment_fails_with_invalid_bounty_id() {
+    build_test_externalities().execute_with(|| {
+        let invalid_bounty_id = 11u64;
+
+        SubmitJudgmentFixture::default()
+            .with_bounty_id(invalid_bounty_id)
+            .call_and_assert(Err(Error::<Test>::BountyDoesntExist.into()));
+    });
+}
+
+#[test]
+fn submit_judgment_fails_with_invalid_origin() {
+    build_test_externalities().execute_with(|| {
+        let member_id = 1;
+        let account_id = 1;
+        let initial_balance = 500;
+
+        increase_account_balance(&account_id, initial_balance);
+        set_council_budget(initial_balance);
+
+        // Oracle is set to a council - try to submit judgment with bad origin
+        CreateBountyFixture::default()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+        SubmitJudgmentFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_origin(RawOrigin::Signed(account_id))
+            .call_and_assert(Err(DispatchError::BadOrigin));
+
+        // Oracle is set to a member - try to submit judgment with invalid member_id
+        CreateBountyFixture::default()
+            .with_oracle_member_id(member_id)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 2u64;
+        let invalid_member_id = 2;
+
+        SubmitJudgmentFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_oracle_member_id(invalid_member_id)
+            .call_and_assert(Err(Error::<Test>::NotBountyActor.into()));
+
+        // Oracle is set to a member - try to submit judgment with bad origin
+        CreateBountyFixture::default()
+            .with_oracle_member_id(member_id)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 3u64;
+
+        SubmitJudgmentFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_origin(RawOrigin::None)
+            .call_and_assert(Err(DispatchError::BadOrigin));
+
+        // Oracle is set to a member - try to submit judgment as a council
+        CreateBountyFixture::default()
+            .with_oracle_member_id(member_id)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 4u64;
+
+        SubmitJudgmentFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Err(Error::<Test>::NotBountyActor.into()));
+    });
+}
+
+#[test]
+fn submit_judgment_fails_with_invalid_stage() {
+    build_test_externalities().execute_with(|| {
+        set_council_budget(500);
+
+        // Test already cancelled bounty.
+        CreateBountyFixture::default().call_and_assert(Ok(()));
+
+        let bounty_id = 1u64;
+
+        SubmitJudgmentFixture::default()
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Err(Error::<Test>::InvalidStageUnexpectedFunding.into()));
+    });
+}
+
+#[test]
+fn submit_judgment_fails_with_zero_work_entries() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+        let entrant_stake = 37;
+        let working_period = 10;
+        let judging_period = 10;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_entrant_stake(entrant_stake)
+            .with_work_period(working_period)
+            .with_judging_period(judging_period)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+        let member_id = 1;
+        let account_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        increase_account_balance(&account_id, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_staking_account_id(account_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        let entry_id = 1;
+
+        let work_data = b"Work submitted".to_vec();
+        SubmitWorkFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_entry_id(entry_id)
+            .with_work_data(work_data.clone())
+            .call_and_assert(Ok(()));
+
+        WithdrawWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_entry_id(entry_id)
+            .call_and_assert(Ok(()));
+
+        run_to_block(starting_block + working_period + 1);
+
+        // Withdrawn entry.
+        let judgment = vec![entry_id]
+            .iter()
+            .map(|entry_id| {
+                (
+                    *entry_id,
+                    OracleWorkEntryJudgment::Winner {
+                        reward: DEFAULT_WINNER_REWARD,
+                    },
+                )
+            })
+            .collect::<BTreeMap<_, _>>();
+
+        SubmitJudgmentFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_judgment(judgment)
+            .call_and_assert(Err(Error::<Test>::NoActiveWorkEntries.into()));
+    });
+}
+
+#[test]
+fn submit_judgment_fails_with_invalid_judgment() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+        let entrant_stake = 37;
+        let working_period = 10;
+        let judging_period = 10;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_entrant_stake(entrant_stake)
+            .with_work_period(working_period)
+            .with_judging_period(judging_period)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+        let member_id = 1;
+        let account_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        increase_account_balance(&account_id, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_staking_account_id(account_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        let entry_id = 1;
+
+        let account_id2 = 2;
+        increase_account_balance(&account_id2, initial_balance);
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_staking_account_id(account_id2)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        let entry_id2 = 2;
+
+        let work_data = b"Work submitted".to_vec();
+        SubmitWorkFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_entry_id(entry_id)
+            .with_work_data(work_data.clone())
+            .call_and_assert(Ok(()));
+
+        run_to_block(starting_block + working_period + 1);
+
+        // Invalid entry_id
+        let invalid_entry_id = 1111u64;
+        let judgment = vec![invalid_entry_id]
+            .iter()
+            .map(|entry_id| {
+                (
+                    *entry_id,
+                    OracleWorkEntryJudgment::Winner {
+                        reward: DEFAULT_WINNER_REWARD,
+                    },
+                )
+            })
+            .collect::<BTreeMap<_, _>>();
+
+        SubmitJudgmentFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_judgment(judgment)
+            .call_and_assert(Err(Error::<Test>::WorkEntryDoesntExist.into()));
+
+        // Zero reward for winners.
+        let invalid_reward = 0;
+        let judgment = vec![entry_id]
+            .iter()
+            .map(|entry_id| {
+                (
+                    *entry_id,
+                    OracleWorkEntryJudgment::Winner {
+                        reward: invalid_reward,
+                    },
+                )
+            })
+            .collect::<BTreeMap<_, _>>();
+
+        SubmitJudgmentFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_judgment(judgment)
+            .call_and_assert(Err(Error::<Test>::ZeroWinnerReward.into()));
+
+        // Winner reward is not equal to the total bounty funding.
+        let invalid_reward = max_amount * 2;
+        let judgment = vec![entry_id]
+            .iter()
+            .map(|entry_id| {
+                (
+                    *entry_id,
+                    OracleWorkEntryJudgment::Winner {
+                        reward: invalid_reward,
+                    },
+                )
+            })
+            .collect::<BTreeMap<_, _>>();
+
+        SubmitJudgmentFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_judgment(judgment)
+            .call_and_assert(Err(
+                Error::<Test>::TotalRewardShouldBeEqualToTotalFunding.into()
+            ));
+
+        // No work submission for a winner.
+        let winner_reward = max_amount;
+        let judgment = vec![entry_id2]
+            .iter()
+            .map(|entry_id| {
+                (
+                    *entry_id,
+                    OracleWorkEntryJudgment::Winner {
+                        reward: winner_reward,
+                    },
+                )
+            })
+            .collect::<BTreeMap<_, _>>();
+
+        SubmitJudgmentFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_judgment(judgment)
+            .call_and_assert(Err(Error::<Test>::WinnerShouldHasWorkSubmission.into()));
+    });
+}
+
+#[test]
+fn withdraw_work_entrant_funds_succeeded() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+        let winner_reward = max_amount;
+        let entrant_stake = 37;
+        let work_period = 1;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_work_period(work_period)
+            .with_entrant_stake(entrant_stake)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        let member_id1 = 1;
+        let account_id1 = 1;
+        increase_account_balance(&account_id1, initial_balance);
+
+        // Winner
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id1))
+            .with_member_id(member_id1)
+            .with_staking_account_id(account_id1)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            Balances::usable_balance(&account_id1),
+            initial_balance - entrant_stake
+        );
+
+        let entry_id1 = 1;
+
+        SubmitWorkFixture::default()
+            .with_origin(RawOrigin::Signed(account_id1))
+            .with_member_id(member_id1)
+            .with_entry_id(entry_id1)
+            .call_and_assert(Ok(()));
+
+        // Legitimate participant
+        let member_id2 = 2;
+        let account_id2 = 2;
+        increase_account_balance(&account_id2, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id2))
+            .with_member_id(member_id2)
+            .with_staking_account_id(account_id2)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            Balances::usable_balance(&account_id2),
+            initial_balance - entrant_stake
+        );
+
+        let entry_id2 = 2;
+
+        SubmitWorkFixture::default()
+            .with_origin(RawOrigin::Signed(account_id2))
+            .with_member_id(member_id2)
+            .with_entry_id(entry_id2)
+            .call_and_assert(Ok(()));
+
+        // Judgment
+        let mut judgment = BTreeMap::new();
+        judgment.insert(
+            entry_id1,
+            OracleWorkEntryJudgment::Winner {
+                reward: winner_reward,
+            },
+        );
+
+        run_to_block(starting_block + work_period + 1);
+
+        SubmitJudgmentFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_judgment(judgment)
+            .call_and_assert(Ok(()));
+
+        // Withdraw work entrant.
+        WithdrawWorkEntrantFundsFixture::default()
+            .with_origin(RawOrigin::Signed(account_id1))
+            .with_member_id(member_id1)
+            .with_entry_id(entry_id1)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(
+            Balances::usable_balance(&account_id1),
+            initial_balance + winner_reward
+        );
+
+        EventFixture::assert_last_crate_event(RawEvent::WorkEntrantFundsWithdrawn(
+            bounty_id, entry_id1, member_id1,
+        ));
+
+        // Bounty exists before the last withdrawal call.
+        assert!(<Bounties<Test>>::contains_key(bounty_id));
+
+        WithdrawWorkEntrantFundsFixture::default()
+            .with_origin(RawOrigin::Signed(account_id2))
+            .with_member_id(member_id2)
+            .with_entry_id(entry_id2)
+            .call_and_assert(Ok(()));
+
+        assert_eq!(Balances::usable_balance(&account_id2), initial_balance);
+
+        EventFixture::contains_crate_event(RawEvent::WorkEntrantFundsWithdrawn(
+            bounty_id, entry_id2, member_id2,
+        ));
+
+        // Bounty was removed with the last withdrawal call.
+        assert!(!<Bounties<Test>>::contains_key(bounty_id));
+
+        EventFixture::assert_last_crate_event(RawEvent::BountyRemoved(bounty_id));
+    });
+}
+
+#[test]
+fn withdraw_work_entrant_funds_fails_with_invalid_bounty_id() {
+    build_test_externalities().execute_with(|| {
+        let invalid_bounty_id = 11u64;
+
+        WithdrawWorkEntrantFundsFixture::default()
+            .with_bounty_id(invalid_bounty_id)
+            .call_and_assert(Err(Error::<Test>::BountyDoesntExist.into()));
+    });
+}
+
+#[test]
+fn withdraw_work_entrant_funds_fails_with_invalid_entry_id() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+        let entrant_stake = 37;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_entrant_stake(entrant_stake)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        run_to_block(100);
+
+        let invalid_entry_id = 11u64;
+
+        WithdrawWorkEntrantFundsFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_entry_id(invalid_entry_id)
+            .call_and_assert(Err(Error::<Test>::WorkEntryDoesntExist.into()));
+    });
+}
+
+#[test]
+fn withdraw_work_entrant_funds_fails_with_invalid_origin() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+        let member_id = 1;
+        let account_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        increase_account_balance(&account_id, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        let entry_id = 1;
+
+        WithdrawWorkEntrantFundsFixture::default()
+            .with_entry_id(entry_id)
+            .with_bounty_id(bounty_id)
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Err(DispatchError::BadOrigin));
+    });
+}
+
+#[test]
+fn withdraw_work_entrant_funds_fails_with_invalid_stage() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 500;
+        let max_amount = 100;
+        let entrant_stake = 37;
+        let work_period = 10;
+
+        set_council_budget(initial_balance);
+
+        CreateBountyFixture::default()
+            .with_max_funding_amount(max_amount)
+            .with_entrant_stake(entrant_stake)
+            .with_work_period(work_period)
+            .call_and_assert(Ok(()));
+
+        let bounty_id = 1;
+        let member_id = 1;
+        let account_id = 1;
+
+        FundBountyFixture::default()
+            .with_bounty_id(bounty_id)
+            .with_amount(max_amount)
+            .with_council()
+            .with_origin(RawOrigin::Root)
+            .call_and_assert(Ok(()));
+
+        increase_account_balance(&account_id, initial_balance);
+
+        AnnounceWorkEntryFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_staking_account_id(account_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Ok(()));
+
+        let entry_id = 1;
+
+        run_to_block(starting_block + 1);
+
+        WithdrawWorkEntrantFundsFixture::default()
+            .with_origin(RawOrigin::Signed(account_id))
+            .with_member_id(member_id)
+            .with_entry_id(entry_id)
+            .with_bounty_id(bounty_id)
+            .call_and_assert(Err(
+                Error::<Test>::InvalidStageUnexpectedWorkSubmission.into()
+            ));
+    });
+}

+ 16 - 0
runtime-modules/common/src/council.rs

@@ -0,0 +1,16 @@
+use frame_support::dispatch::DispatchResult;
+
+/// Provides an interface for the council budget.
+pub trait CouncilBudgetManager<Balance> {
+    /// Returns the current council balance.
+    fn get_budget() -> Balance;
+
+    /// Set the current budget value.
+    fn set_budget(budget: Balance);
+}
+
+/// Council validator for the origin(account_id) and member_id.
+pub trait CouncilOriginValidator<Origin, MemberId, AccountId> {
+    /// Check for valid combination of origin and member_id for a councilor.
+    fn ensure_member_consulate(origin: Origin, member_id: MemberId) -> DispatchResult;
+}

+ 4 - 41
runtime-modules/common/src/lib.rs

@@ -2,52 +2,15 @@
 #![cfg_attr(not(feature = "std"), no_std)]
 
 pub mod constraints;
-pub mod origin;
+pub mod council;
+pub mod membership;
 pub mod working_group;
 
-use codec::{Codec, Decode, Encode};
-use frame_support::Parameter;
+use codec::{Decode, Encode};
 #[cfg(feature = "std")]
 use serde::{Deserialize, Serialize};
-use sp_arithmetic::traits::BaseArithmetic;
-use sp_runtime::traits::{MaybeSerialize, Member};
 
-/// Member id type alias
-pub type MemberId<T> = <T as Trait>::MemberId;
-
-/// Actor id type alias
-pub type ActorId<T> = <T as Trait>::ActorId;
-
-/// Generic trait for membership dependent pallets.
-pub trait Trait: frame_system::Trait {
-    /// Describes the common type for the members.
-    type MemberId: Parameter
-        + Member
-        + BaseArithmetic
-        + Codec
-        + Default
-        + Copy
-        + MaybeSerialize
-        + Ord
-        + Eq;
-
-    /// Describes the common type for the working group members (workers).
-    type ActorId: Parameter
-        + Member
-        + BaseArithmetic
-        + Codec
-        + Default
-        + Copy
-        + MaybeSerialize
-        + Ord
-        + PartialEq;
-}
-
-/// Validates staking account ownership for a member.
-pub trait StakingAccountValidator<T: Trait> {
-    /// Verifies that staking account bound to the member.
-    fn is_member_staking_account(member_id: &MemberId<T>, account_id: &T::AccountId) -> bool;
-}
+pub use membership::{ActorId, MemberId, StakingAccountValidator};
 
 /// Defines time in both block number and substrate time abstraction.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]

+ 60 - 0
runtime-modules/common/src/membership.rs

@@ -0,0 +1,60 @@
+use codec::Codec;
+use frame_support::dispatch::DispatchError;
+use frame_support::Parameter;
+use sp_arithmetic::traits::BaseArithmetic;
+use sp_runtime::traits::{MaybeSerialize, Member};
+
+/// Member id type alias
+pub type MemberId<T> = <T as Trait>::MemberId;
+
+/// Actor id type alias
+pub type ActorId<T> = <T as Trait>::ActorId;
+
+/// Generic trait for membership dependent pallets.
+pub trait Trait: frame_system::Trait {
+    /// Describes the common type for the members.
+    type MemberId: Parameter
+        + Member
+        + BaseArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + Ord
+        + PartialEq;
+
+    /// Describes the common type for the working group members (workers).
+    type ActorId: Parameter
+        + Member
+        + BaseArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + Ord
+        + PartialEq;
+}
+
+/// Validates staking account ownership for a member.
+pub trait StakingAccountValidator<T: Trait> {
+    /// Verifies that staking account bound to the member.
+    fn is_member_staking_account(member_id: &MemberId<T>, account_id: &T::AccountId) -> bool;
+}
+
+/// Membership validator for the origin(account_id) and member_id (eg.: thread author id).
+pub trait MemberOriginValidator<Origin, MemberId, AccountId> {
+    /// Check for valid combination of origin and member_id. Returns member controller account ID.
+    fn ensure_member_controller_account_origin(
+        origin: Origin,
+        member_id: MemberId,
+    ) -> Result<AccountId, DispatchError>;
+
+    /// Verifies that provided account is the controller account of the member
+    fn is_member_controller_account(member_id: &MemberId, account_id: &AccountId) -> bool;
+}
+
+/// Gives access to some membership information.
+pub trait MembershipInfoProvider<T: Trait> {
+    /// Returns current controller account for a member.
+    fn controller_account_id(member_id: MemberId<T>) -> Result<T::AccountId, DispatchError>;
+}

+ 0 - 19
runtime-modules/common/src/origin.rs

@@ -1,19 +0,0 @@
-use frame_support::dispatch::{DispatchError, DispatchResult};
-
-/// Council validator for the origin(account_id) and member_id.
-pub trait CouncilOriginValidator<Origin, MemberId, AccountId> {
-    /// Check for valid combination of origin and member_id for a councilor.
-    fn ensure_member_consulate(origin: Origin, member_id: MemberId) -> DispatchResult;
-}
-
-/// Membership validator for the origin(account_id) and member_id (eg.: thread author id).
-pub trait MemberOriginValidator<Origin, MemberId, AccountId> {
-    /// Check for valid combination of origin and member_id.
-    fn ensure_member_controller_account_origin(
-        origin: Origin,
-        member_id: MemberId,
-    ) -> Result<AccountId, DispatchError>;
-
-    /// Verifies that provided account is the controller account of the member
-    fn is_member_controller_account(member_id: &MemberId, account_id: &AccountId) -> bool;
-}

+ 1 - 1
runtime-modules/common/src/working_group.rs

@@ -20,7 +20,7 @@ pub enum WorkingGroup {
 }
 
 /// Working group interface to work with its members - workers and leaders.
-pub trait WorkingGroupAuthenticator<T: crate::Trait> {
+pub trait WorkingGroupAuthenticator<T: crate::membership::Trait> {
     /// Validate origin for the worker.
     fn ensure_worker_origin(origin: T::Origin, worker_id: &T::ActorId) -> DispatchResult;
 

+ 1 - 1
runtime-modules/constitution/src/lib.rs

@@ -72,7 +72,7 @@ decl_module! {
         /// - Db writes: 1 (constant value)
         /// # </weight>
         #[weight = WeightInfoConstitution::<T>::amend_constitution(constitution_text.len().saturated_into())]
-        fn amend_constitution(origin, constitution_text: Vec<u8>) {
+        pub fn amend_constitution(origin, constitution_text: Vec<u8>) {
             ensure_root(origin)?;
 
             //

+ 9 - 5
runtime-modules/content-directory/src/lib.rs

@@ -158,7 +158,7 @@ use sp_std::collections::{btree_map::BTreeMap, btree_set::BTreeSet};
 use sp_std::vec;
 use sp_std::vec::Vec;
 
-use common::origin::MemberOriginValidator;
+use common::membership::MemberOriginValidator;
 
 pub use errors::Error;
 
@@ -170,7 +170,7 @@ pub type MaxNumber = u32;
 /// Type simplification
 pub type EntityOf<T> = Entity<
     <T as Trait>::ClassId,
-    <T as common::Trait>::MemberId,
+    <T as common::membership::Trait>::MemberId,
     <T as frame_system::Trait>::Hash,
     <T as Trait>::EntityId,
     <T as Trait>::Nonce,
@@ -191,7 +191,7 @@ pub type StoredPropertyValueOf<T> = StoredPropertyValue<
 pub type CuratorId<T> = common::ActorId<T>;
 
 /// Module configuration trait for this Substrate module.
-pub trait Trait: frame_system::Trait + common::Trait {
+pub trait Trait: frame_system::Trait + common::membership::Trait {
     /// The overarching event type.
     type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
 
@@ -2958,10 +2958,14 @@ decl_event!(
         CuratorId = CuratorId<T>,
         ClassId = <T as Trait>::ClassId,
         EntityId = <T as Trait>::EntityId,
-        EntityController = EntityController<<T as common::Trait>::MemberId>,
+        EntityController = EntityController<<T as common::membership::Trait>::MemberId>,
         EntityCreationVoucher = EntityCreationVoucher<T>,
         Status = bool,
-        Actor = Actor<<T as Trait>::CuratorGroupId, CuratorId<T>, <T as common::Trait>::MemberId>,
+        Actor = Actor<
+            <T as Trait>::CuratorGroupId,
+            CuratorId<T>,
+            <T as common::membership::Trait>::MemberId,
+        >,
         Nonce = <T as Trait>::Nonce,
         SideEffects = Option<ReferenceCounterSideEffects<T>>,
         SideEffect = Option<(<T as Trait>::EntityId, EntityReferenceCounterSideEffect)>,

+ 6 - 6
runtime-modules/content-directory/src/mock.rs

@@ -24,7 +24,7 @@ pub type Hashed = <Runtime as frame_system::Trait>::Hash;
 
 pub type TestCuratorId = CuratorId<Runtime>;
 pub type CuratorGroupId = <Runtime as Trait>::CuratorGroupId;
-pub type MemberId = <Runtime as common::Trait>::MemberId;
+pub type MemberId = <Runtime as common::membership::Trait>::MemberId;
 
 /// Origins
 
@@ -275,7 +275,7 @@ impl Trait for Runtime {
 impl common::working_group::WorkingGroupAuthenticator<Runtime> for () {
     fn ensure_worker_origin(
         _origin: <Runtime as frame_system::Trait>::Origin,
-        _worker_id: &<Runtime as common::Trait>::ActorId,
+        _worker_id: &<Runtime as common::membership::Trait>::ActorId,
     ) -> DispatchResult {
         unimplemented!()
     }
@@ -284,7 +284,7 @@ impl common::working_group::WorkingGroupAuthenticator<Runtime> for () {
         unimplemented!()
     }
 
-    fn get_leader_member_id() -> Option<<Runtime as common::Trait>::MemberId> {
+    fn get_leader_member_id() -> Option<<Runtime as common::membership::Trait>::MemberId> {
         unimplemented!()
     }
 
@@ -296,7 +296,7 @@ impl common::working_group::WorkingGroupAuthenticator<Runtime> for () {
 
     fn is_worker_account_id(
         account_id: &<Runtime as frame_system::Trait>::AccountId,
-        worker_id: &<Runtime as common::Trait>::ActorId,
+        worker_id: &<Runtime as common::membership::Trait>::ActorId,
     ) -> bool {
         let first_curator_account_id = ensure_signed(Origin::signed(FIRST_CURATOR_ORIGIN)).unwrap();
         let second_curator_account_id =
@@ -306,12 +306,12 @@ impl common::working_group::WorkingGroupAuthenticator<Runtime> for () {
     }
 }
 
-impl common::Trait for Runtime {
+impl common::membership::Trait for Runtime {
     type MemberId = u64;
     type ActorId = u64;
 }
 
-impl common::origin::MemberOriginValidator<Origin, u64, u64> for () {
+impl common::membership::MemberOriginValidator<Origin, u64, u64> for () {
     fn ensure_member_controller_account_origin(
         _origin: Origin,
         _member_id: u64,

+ 26 - 12
runtime-modules/council/src/lib.rs

@@ -55,7 +55,8 @@ use serde::{Deserialize, Serialize};
 use sp_runtime::traits::{Hash, SaturatedConversion, Saturating, Zero};
 use sp_std::vec::Vec;
 
-use common::origin::{CouncilOriginValidator, MemberOriginValidator};
+use common::council::CouncilOriginValidator;
+use common::membership::MemberOriginValidator;
 use common::{FundingRequestParameters, StakingAccountValidator};
 use referendum::{CastVote, OptionResult, ReferendumManager};
 use staking_handler::StakingHandler;
@@ -175,15 +176,18 @@ pub type Balance<T> = <T as balances::Trait>::Balance;
 pub type VotePowerOf<T> = <<T as Trait>::Referendum as ReferendumManager<
     <T as frame_system::Trait>::Origin,
     <T as frame_system::Trait>::AccountId,
-    <T as common::Trait>::MemberId,
+    <T as common::membership::Trait>::MemberId,
     <T as frame_system::Trait>::Hash,
 >>::VotePower;
-pub type CastVoteOf<T> =
-    CastVote<<T as frame_system::Trait>::Hash, Balance<T>, <T as common::Trait>::MemberId>;
+pub type CastVoteOf<T> = CastVote<
+    <T as frame_system::Trait>::Hash,
+    Balance<T>,
+    <T as common::membership::Trait>::MemberId,
+>;
 
 pub type CouncilMemberOf<T> = CouncilMember<
     <T as frame_system::Trait>::AccountId,
-    <T as common::Trait>::MemberId,
+    <T as common::membership::Trait>::MemberId,
     Balance<T>,
     <T as frame_system::Trait>::BlockNumber,
 >;
@@ -218,7 +222,7 @@ pub trait WeightInfo {
 type CouncilWeightInfo<T> = <T as Trait>::WeightInfo;
 
 /// The main council trait.
-pub trait Trait: frame_system::Trait + common::Trait + balances::Trait {
+pub trait Trait: frame_system::Trait + common::membership::Trait + balances::Trait {
     /// The overarching event type.
     type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
 
@@ -272,7 +276,7 @@ pub trait ReferendumConnection<T: Trait> {
     /// Process referendum results. This function MUST be called in runtime's implementation of
     /// referendum's `process_results()`.
     fn recieve_referendum_results(
-        winners: &[OptionResult<<T as common::Trait>::MemberId, VotePowerOf<T>>],
+        winners: &[OptionResult<<T as common::membership::Trait>::MemberId, VotePowerOf<T>>],
     );
 
     /// Process referendum results. This function MUST be called in runtime's implementation of
@@ -328,7 +332,7 @@ decl_event! {
     where
         Balance = Balance<T>,
         <T as frame_system::Trait>::BlockNumber,
-        <T as common::Trait>::MemberId,
+        <T as common::membership::Trait>::MemberId,
         <T as frame_system::Trait>::AccountId,
     {
         /// New council was elected
@@ -888,7 +892,7 @@ impl<T: Trait> Module<T> {
 
     // Conclude election period and elect new council if possible.
     fn end_election_period(
-        winners: &[OptionResult<<T as common::Trait>::MemberId, VotePowerOf<T>>],
+        winners: &[OptionResult<<T as common::membership::Trait>::MemberId, VotePowerOf<T>>],
     ) {
         let council_size = T::CouncilSize::get();
         if winners.len() as u64 != council_size {
@@ -1066,7 +1070,7 @@ impl<T: Trait> Module<T> {
 impl<T: Trait> ReferendumConnection<T> for Module<T> {
     // Process candidates' results recieved from the referendum.
     fn recieve_referendum_results(
-        winners: &[OptionResult<<T as common::Trait>::MemberId, VotePowerOf<T>>],
+        winners: &[OptionResult<<T as common::membership::Trait>::MemberId, VotePowerOf<T>>],
     ) {
         //
         // == MUTATION SAFE ==
@@ -1566,8 +1570,8 @@ impl<T: Trait> EnsureChecks<T> {
     }
 }
 
-impl<T: Trait + common::Trait> CouncilOriginValidator<T::Origin, T::MemberId, T::AccountId>
-    for Module<T>
+impl<T: Trait + common::membership::Trait>
+    CouncilOriginValidator<T::Origin, T::MemberId, T::AccountId> for Module<T>
 {
     fn ensure_member_consulate(origin: T::Origin, member_id: T::MemberId) -> DispatchResult {
         EnsureChecks::<T>::ensure_user_membership(origin, &member_id)?;
@@ -1581,3 +1585,13 @@ impl<T: Trait + common::Trait> CouncilOriginValidator<T::Origin, T::MemberId, T:
         Ok(())
     }
 }
+
+impl<T: Trait + balances::Trait> common::council::CouncilBudgetManager<Balance<T>> for Module<T> {
+    fn get_budget() -> Balance<T> {
+        Self::budget()
+    }
+
+    fn set_budget(budget: Balance<T>) {
+        Mutations::<T>::set_budget(budget);
+    }
+}

+ 6 - 6
runtime-modules/council/src/mock.rs

@@ -69,7 +69,7 @@ parameter_types! {
     pub const BudgetRefillPeriod: u64 = 1000;
 }
 
-impl common::Trait for Runtime {
+impl common::membership::Trait for Runtime {
     type MemberId = u64;
     type ActorId = u64;
 }
@@ -107,7 +107,7 @@ impl Trait for Runtime {
     type MemberOriginValidator = ();
 }
 
-impl common::origin::MemberOriginValidator<Origin, u64, u64> for () {
+impl common::membership::MemberOriginValidator<Origin, u64, u64> for () {
     fn ensure_member_controller_account_origin(
         origin: Origin,
         member_id: u64,
@@ -465,12 +465,12 @@ impl common::working_group::WorkingGroupBudgetHandler<Runtime> for () {
 impl common::working_group::WorkingGroupAuthenticator<Runtime> for () {
     fn ensure_worker_origin(
         _origin: <Runtime as frame_system::Trait>::Origin,
-        _worker_id: &<Runtime as common::Trait>::ActorId,
+        _worker_id: &<Runtime as common::membership::Trait>::ActorId,
     ) -> DispatchResult {
         unimplemented!();
     }
 
-    fn get_leader_member_id() -> Option<<Runtime as common::Trait>::MemberId> {
+    fn get_leader_member_id() -> Option<<Runtime as common::membership::Trait>::MemberId> {
         unimplemented!();
     }
 
@@ -484,7 +484,7 @@ impl common::working_group::WorkingGroupAuthenticator<Runtime> for () {
 
     fn is_worker_account_id(
         _account_id: &<Runtime as frame_system::Trait>::AccountId,
-        _worker_id: &<Runtime as common::Trait>::ActorId,
+        _worker_id: &<Runtime as common::membership::Trait>::ActorId,
     ) -> bool {
         unimplemented!()
     }
@@ -777,7 +777,7 @@ where
 
     pub fn vote_commitment(
         account_id: &<T as frame_system::Trait>::AccountId,
-        vote_option_index: &<T as common::Trait>::MemberId,
+        vote_option_index: &<T as common::membership::Trait>::MemberId,
         cycle_id: &u64,
     ) -> (T::Hash, Vec<u8>) {
         let salt = Self::generate_salt();

+ 26 - 1
runtime-modules/council/src/tests.rs

@@ -5,7 +5,8 @@ use super::{
     CouncilStageAnnouncing, Error, Module, Trait,
 };
 use crate::mock::*;
-use common::origin::CouncilOriginValidator;
+use common::council::CouncilBudgetManager;
+use common::council::CouncilOriginValidator;
 use frame_support::traits::Currency;
 use frame_support::StorageValue;
 use frame_system::RawOrigin;
@@ -1729,3 +1730,27 @@ fn test_funding_request_succeeds() {
         );
     });
 }
+
+#[test]
+fn test_council_budget_manager_works_correctlyl() {
+    let config = default_genesis_config();
+
+    build_test_externalities(config).execute_with(|| {
+        let origin = OriginType::Root;
+        let initial_budget = 100;
+
+        Mocks::set_budget(origin.clone(), initial_budget, Ok(()));
+
+        assert_eq!(
+            <Module<Runtime> as CouncilBudgetManager<u64>>::get_budget(),
+            initial_budget
+        );
+
+        let new_budget = 200;
+        <Module<Runtime> as CouncilBudgetManager<u64>>::set_budget(new_budget);
+        assert_eq!(
+            <Module<Runtime> as CouncilBudgetManager<u64>>::get_budget(),
+            new_budget
+        );
+    });
+}

+ 2 - 2
runtime-modules/forum/src/benchmarking.rs

@@ -147,7 +147,7 @@ where
     )
     .unwrap();
 
-    let actor_id = <T as common::Trait>::ActorId::from(id.try_into().unwrap());
+    let actor_id = <T as common::membership::Trait>::ActorId::from(id.try_into().unwrap());
     assert!(WorkerById::<T, ForumWorkingGroupInstance>::contains_key(
         actor_id
     ));
@@ -184,7 +184,7 @@ where
     )
     .unwrap();
 
-    let actor_id = <T as common::Trait>::ActorId::from(id.try_into().unwrap());
+    let actor_id = <T as common::membership::Trait>::ActorId::from(id.try_into().unwrap());
     assert!(WorkerById::<T, ForumWorkingGroupInstance>::contains_key(
         actor_id
     ));

+ 2 - 2
runtime-modules/forum/src/lib.rs

@@ -21,7 +21,7 @@ use sp_std::collections::btree_set::BTreeSet;
 use sp_std::fmt::Debug;
 use sp_std::prelude::*;
 
-use common::origin::MemberOriginValidator;
+use common::membership::MemberOriginValidator;
 use common::working_group::WorkingGroupAuthenticator;
 
 mod mock;
@@ -84,7 +84,7 @@ pub trait WeightInfo {
 }
 
 pub trait Trait:
-    frame_system::Trait + pallet_timestamp::Trait + common::Trait + balances::Trait
+    frame_system::Trait + pallet_timestamp::Trait + common::membership::Trait + balances::Trait
 {
     type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
 

+ 5 - 5
runtime-modules/forum/src/mock.rs

@@ -94,7 +94,7 @@ impl balances::Trait for Runtime {
     type MaxLocks = ();
 }
 
-impl common::Trait for Runtime {
+impl common::membership::Trait for Runtime {
     type MemberId = u128;
     type ActorId = u128;
 }
@@ -360,7 +360,7 @@ impl Trait for Runtime {
     type WeightInfo = ();
 }
 
-impl common::origin::MemberOriginValidator<Origin, u128, u128> for () {
+impl common::membership::MemberOriginValidator<Origin, u128, u128> for () {
     fn ensure_member_controller_account_origin(
         origin: Origin,
         member_id: u128,
@@ -394,7 +394,7 @@ impl common::origin::MemberOriginValidator<Origin, u128, u128> for () {
 impl common::working_group::WorkingGroupAuthenticator<Runtime> for () {
     fn ensure_worker_origin(
         _origin: <Runtime as frame_system::Trait>::Origin,
-        _worker_id: &<Runtime as common::Trait>::ActorId,
+        _worker_id: &<Runtime as common::membership::Trait>::ActorId,
     ) -> DispatchResult {
         unimplemented!()
     }
@@ -403,7 +403,7 @@ impl common::working_group::WorkingGroupAuthenticator<Runtime> for () {
         unimplemented!()
     }
 
-    fn get_leader_member_id() -> Option<<Runtime as common::Trait>::MemberId> {
+    fn get_leader_member_id() -> Option<<Runtime as common::membership::Trait>::MemberId> {
         unimplemented!()
     }
 
@@ -413,7 +413,7 @@ impl common::working_group::WorkingGroupAuthenticator<Runtime> for () {
 
     fn is_worker_account_id(
         account_id: &<Runtime as frame_system::Trait>::AccountId,
-        _worker_id: &<Runtime as common::Trait>::ActorId,
+        _worker_id: &<Runtime as common::membership::Trait>::ActorId,
     ) -> bool {
         *account_id != NOT_FORUM_MODERATOR_ORIGIN_ID
     }

+ 1 - 1
runtime-modules/membership/src/benchmarking.rs

@@ -85,7 +85,7 @@ fn handle_from_id<T: Trait>(id: u32) -> Vec<u8> {
 
 benchmarks! {
     where_clause { where T: balances::Trait, T: Trait, T: MembershipWorkingGroupHelper<<T as
-        frame_system::Trait>::AccountId, <T as common::Trait>::MemberId, <T as common::Trait>::ActorId> }
+        frame_system::Trait>::AccountId, <T as common::membership::Trait>::MemberId, <T as common::membership::Trait>::ActorId> }
     _{  }
 
     buy_membership_without_referrer{

+ 16 - 6
runtime-modules/membership/src/lib.rs

@@ -61,7 +61,7 @@ use sp_runtime::traits::{Hash, Saturating};
 use sp_runtime::SaturatedConversion;
 use sp_std::vec::Vec;
 
-use common::origin::MemberOriginValidator;
+use common::membership::{MemberOriginValidator, MembershipInfoProvider};
 use common::working_group::{WorkingGroupAuthenticator, WorkingGroupBudgetHandler};
 use staking_handler::StakingHandler;
 
@@ -97,7 +97,7 @@ pub trait WeightInfo {
 }
 
 pub trait Trait:
-    frame_system::Trait + balances::Trait + pallet_timestamp::Trait + common::Trait
+    frame_system::Trait + balances::Trait + pallet_timestamp::Trait + common::membership::Trait
 {
     /// Membership module event type.
     type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
@@ -335,17 +335,17 @@ decl_storage! {
 
 decl_event! {
     pub enum Event<T> where
-      <T as common::Trait>::MemberId,
+      <T as common::membership::Trait>::MemberId,
       Balance = BalanceOf<T>,
       <T as frame_system::Trait>::AccountId,
       BuyMembershipParameters = BuyMembershipParameters<
           <T as frame_system::Trait>::AccountId,
-          <T as common::Trait>::MemberId,
+          <T as common::membership::Trait>::MemberId,
         >,
-      <T as common::Trait>::ActorId,
+      <T as common::membership::Trait>::ActorId,
       InviteMembershipParameters = InviteMembershipParameters<
           <T as frame_system::Trait>::AccountId,
-          <T as common::Trait>::MemberId,
+          <T as common::membership::Trait>::MemberId,
         >,
     {
         MemberInvited(MemberId, InviteMembershipParameters),
@@ -1188,3 +1188,13 @@ impl<T: Trait> MemberOriginValidator<T::Origin, T::MemberId, T::AccountId> for M
         Self::ensure_is_controller_account_for_member(member_id, account_id).is_ok()
     }
 }
+
+impl<T: Trait> MembershipInfoProvider<T> for Module<T> {
+    fn controller_account_id(
+        member_id: common::MemberId<T>,
+    ) -> Result<T::AccountId, DispatchError> {
+        let membership = Self::ensure_membership(member_id)?;
+
+        Ok(membership.controller_account)
+    }
+}

+ 9 - 9
runtime-modules/membership/src/tests/mock.rs

@@ -100,7 +100,7 @@ impl balances::Trait for Test {
     type MaxLocks = ();
 }
 
-impl common::Trait for Test {
+impl common::membership::Trait for Test {
     type MemberId = u64;
     type ActorId = u64;
 }
@@ -287,7 +287,7 @@ impl WeightInfo for () {
     }
 }
 
-impl common::origin::MemberOriginValidator<Origin, u64, u64> for () {
+impl common::membership::MemberOriginValidator<Origin, u64, u64> for () {
     fn ensure_member_controller_account_origin(
         origin: Origin,
         _: u64,
@@ -337,7 +337,7 @@ impl common::working_group::WorkingGroupBudgetHandler<Test> for () {
 impl common::working_group::WorkingGroupAuthenticator<Test> for () {
     fn ensure_worker_origin(
         origin: <Test as frame_system::Trait>::Origin,
-        worker_id: &<Test as common::Trait>::ActorId,
+        worker_id: &<Test as common::membership::Trait>::ActorId,
     ) -> DispatchResult {
         let raw_origin: Result<RawOrigin<u64>, <Test as frame_system::Trait>::Origin> =
             origin.into();
@@ -357,7 +357,7 @@ impl common::working_group::WorkingGroupAuthenticator<Test> for () {
         unimplemented!()
     }
 
-    fn get_leader_member_id() -> Option<<Test as common::Trait>::MemberId> {
+    fn get_leader_member_id() -> Option<<Test as common::membership::Trait>::MemberId> {
         LEAD_SET.with(|lead_set| {
             if *lead_set.borrow() {
                 Some(ALICE_MEMBER_ID)
@@ -373,7 +373,7 @@ impl common::working_group::WorkingGroupAuthenticator<Test> for () {
 
     fn is_worker_account_id(
         _account_id: &<Test as frame_system::Trait>::AccountId,
-        _worker_id: &<Test as common::Trait>::ActorId,
+        _worker_id: &<Test as common::membership::Trait>::ActorId,
     ) -> bool {
         unimplemented!()
     }
@@ -383,15 +383,15 @@ impl common::working_group::WorkingGroupAuthenticator<Test> for () {
 impl
     crate::MembershipWorkingGroupHelper<
         <Test as frame_system::Trait>::AccountId,
-        <Test as common::Trait>::MemberId,
-        <Test as common::Trait>::ActorId,
+        <Test as common::membership::Trait>::MemberId,
+        <Test as common::membership::Trait>::ActorId,
     > for Test
 {
     fn insert_a_lead(
         _opening_id: u32,
         _caller_id: &<Test as frame_system::Trait>::AccountId,
-        _member_id: <Test as common::Trait>::MemberId,
-    ) -> <Test as common::Trait>::ActorId {
+        _member_id: <Test as common::membership::Trait>::MemberId,
+    ) -> <Test as common::membership::Trait>::ActorId {
         ALICE_MEMBER_ID
     }
 }

+ 27 - 1
runtime-modules/membership/src/tests/mod.rs

@@ -7,7 +7,7 @@ use crate::{Error, Event};
 pub use fixtures::*;
 pub use mock::*;
 
-use common::origin::MemberOriginValidator;
+use common::membership::{MemberOriginValidator, MembershipInfoProvider};
 use common::working_group::WorkingGroupBudgetHandler;
 use common::StakingAccountValidator;
 use frame_support::traits::{LockIdentifier, LockableCurrency, WithdrawReasons};
@@ -1013,3 +1013,29 @@ fn membership_origin_validator_fails_with_incompatible_account_id_and_member_id(
         assert_eq!(validation_result, Err(error.into()));
     });
 }
+
+#[test]
+fn membership_info_provider_controller_account_id_fails_with_invalid_member_id() {
+    let initial_members = [(ALICE_MEMBER_ID, ALICE_ACCOUNT_ID)];
+
+    build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
+        let invalid_member_id = BOB_MEMBER_ID;
+        let validation_result = Membership::controller_account_id(invalid_member_id);
+
+        assert_eq!(
+            validation_result,
+            Err(Error::<Test>::MemberProfileNotFound.into())
+        );
+    });
+}
+
+#[test]
+fn membership_info_provider_controller_account_id_succeeds() {
+    let initial_members = [(ALICE_MEMBER_ID, ALICE_ACCOUNT_ID)];
+
+    build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
+        let validation_result = Membership::controller_account_id(ALICE_MEMBER_ID);
+
+        assert_eq!(validation_result, Ok(ALICE_ACCOUNT_ID));
+    });
+}

+ 2 - 2
runtime-modules/proposals/codex/src/lib.rs

@@ -55,7 +55,7 @@ use sp_runtime::SaturatedConversion;
 use sp_std::clone::Clone;
 use sp_std::collections::btree_set::BTreeSet;
 
-use common::origin::MemberOriginValidator;
+use common::membership::MemberOriginValidator;
 use common::MemberId;
 use proposals_discussion::ThreadMode;
 use proposals_engine::{
@@ -110,7 +110,7 @@ pub trait Trait:
     frame_system::Trait
     + proposals_engine::Trait
     + proposals_discussion::Trait
-    + common::Trait
+    + common::membership::Trait
     + staking::Trait
     + proposals_engine::Trait
 {

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

@@ -72,7 +72,7 @@ impl_outer_dispatch! {
     }
 }
 
-impl common::Trait for Test {
+impl common::membership::Trait for Test {
     type MemberId = u64;
     type ActorId = u64;
 }
@@ -162,7 +162,7 @@ impl common::working_group::WorkingGroupBudgetHandler<Test> for () {
 impl common::working_group::WorkingGroupAuthenticator<Test> for () {
     fn ensure_worker_origin(
         _origin: <Test as frame_system::Trait>::Origin,
-        _worker_id: &<Test as common::Trait>::ActorId,
+        _worker_id: &<Test as common::membership::Trait>::ActorId,
     ) -> DispatchResult {
         unimplemented!();
     }
@@ -171,7 +171,7 @@ impl common::working_group::WorkingGroupAuthenticator<Test> for () {
         unimplemented!()
     }
 
-    fn get_leader_member_id() -> Option<<Test as common::Trait>::MemberId> {
+    fn get_leader_member_id() -> Option<<Test as common::membership::Trait>::MemberId> {
         unimplemented!();
     }
 
@@ -181,7 +181,7 @@ impl common::working_group::WorkingGroupAuthenticator<Test> for () {
 
     fn is_worker_account_id(
         _account_id: &<Test as frame_system::Trait>::AccountId,
-        _worker_id: &<Test as common::Trait>::ActorId,
+        _worker_id: &<Test as common::membership::Trait>::ActorId,
     ) -> bool {
         unimplemented!()
     }
@@ -284,7 +284,7 @@ impl Default for crate::Call<Test> {
     }
 }
 
-impl common::origin::MemberOriginValidator<Origin, u64, u64> for () {
+impl common::membership::MemberOriginValidator<Origin, u64, u64> for () {
     fn ensure_member_controller_account_origin(
         origin: Origin,
         _: u64,
@@ -299,7 +299,7 @@ impl common::origin::MemberOriginValidator<Origin, u64, u64> for () {
     }
 }
 
-impl common::origin::CouncilOriginValidator<Origin, u64, u64> for () {
+impl common::council::CouncilOriginValidator<Origin, u64, u64> for () {
     fn ensure_member_consulate(origin: Origin, _: u64) -> DispatchResult {
         frame_system::ensure_signed(origin)?;
 

+ 1 - 1
runtime-modules/proposals/discussion/src/benchmarking.rs

@@ -70,7 +70,7 @@ fn assert_last_event<T: Trait>(generic_event: <T as Trait>::Event) {
     assert_eq!(event, &system_event);
 }
 
-fn member_account<T: common::Trait + balances::Trait + membership::Trait>(
+fn member_account<T: common::membership::Trait + balances::Trait + membership::Trait>(
     name: &'static str,
     id: u32,
 ) -> (T::AccountId, T::MemberId) {

+ 4 - 3
runtime-modules/proposals/discussion/src/lib.rs

@@ -26,7 +26,7 @@
 //! use frame_system::ensure_root;
 //! use pallet_proposals_discussion::{self as discussions, ThreadMode};
 //!
-//! pub trait Trait: discussions::Trait + common::Trait {}
+//! pub trait Trait: discussions::Trait + common::membership::Trait {}
 //!
 //! decl_module! {
 //!     pub struct Module<T: Trait> for enum Call where origin: T::Origin {
@@ -62,7 +62,8 @@ use frame_support::{
 use sp_std::clone::Clone;
 use sp_std::vec::Vec;
 
-use common::origin::{CouncilOriginValidator, MemberOriginValidator};
+use common::council::CouncilOriginValidator;
+use common::membership::MemberOriginValidator;
 use common::MemberId;
 use types::{DiscussionPost, DiscussionThread};
 
@@ -107,7 +108,7 @@ pub trait CouncilMembership<AccountId, MemberId> {
 }
 
 /// 'Proposal discussion' substrate module Trait
-pub trait Trait: frame_system::Trait + common::Trait {
+pub trait Trait: frame_system::Trait + common::membership::Trait {
     /// Discussion event type.
     type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
 

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

@@ -149,7 +149,7 @@ impl balances::Trait for Test {
     type MaxLocks = ();
 }
 
-impl common::Trait for Test {
+impl common::membership::Trait for Test {
     type MemberId = u64;
     type ActorId = u64;
 }
@@ -189,7 +189,7 @@ impl common::working_group::WorkingGroupBudgetHandler<Test> for () {
 impl common::working_group::WorkingGroupAuthenticator<Test> for () {
     fn ensure_worker_origin(
         _origin: <Test as frame_system::Trait>::Origin,
-        _worker_id: &<Test as common::Trait>::ActorId,
+        _worker_id: &<Test as common::membership::Trait>::ActorId,
     ) -> DispatchResult {
         unimplemented!();
     }
@@ -198,7 +198,7 @@ impl common::working_group::WorkingGroupAuthenticator<Test> for () {
         unimplemented!()
     }
 
-    fn get_leader_member_id() -> Option<<Test as common::Trait>::MemberId> {
+    fn get_leader_member_id() -> Option<<Test as common::membership::Trait>::MemberId> {
         unimplemented!();
     }
 
@@ -208,7 +208,7 @@ impl common::working_group::WorkingGroupAuthenticator<Test> for () {
 
     fn is_worker_account_id(
         _account_id: &<Test as frame_system::Trait>::AccountId,
-        _worker_id: &<Test as common::Trait>::ActorId,
+        _worker_id: &<Test as common::membership::Trait>::ActorId,
     ) -> bool {
         unimplemented!()
     }

+ 4 - 3
runtime-modules/proposals/engine/src/lib.rs

@@ -74,7 +74,7 @@
 //! use codec::Encode;
 //! use pallet_proposals_engine::{self as engine, ProposalParameters, ProposalCreationParameters};
 //!
-//! pub trait Trait: engine::Trait + common::Trait {}
+//! pub trait Trait: engine::Trait + common::membership::Trait {}
 //!
 //! decl_module! {
 //!     pub struct Module<T: Trait> for enum Call where origin: T::Origin {
@@ -152,7 +152,8 @@ use frame_system::{ensure_root, RawOrigin};
 use sp_arithmetic::traits::{SaturatedConversion, Saturating, Zero};
 use sp_std::vec::Vec;
 
-use common::origin::{CouncilOriginValidator, MemberOriginValidator};
+use common::council::CouncilOriginValidator;
+use common::membership::MemberOriginValidator;
 use common::{MemberId, StakingAccountValidator};
 use staking_handler::StakingHandler;
 
@@ -174,7 +175,7 @@ type WeightInfoEngine<T> = <T as Trait>::WeightInfo;
 
 /// Proposals engine trait.
 pub trait Trait:
-    frame_system::Trait + pallet_timestamp::Trait + common::Trait + balances::Trait
+    frame_system::Trait + pallet_timestamp::Trait + common::membership::Trait + balances::Trait
 {
     /// Engine event type.
     type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;

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

@@ -186,7 +186,7 @@ parameter_types! {
     pub const CandidateStake: u64 = 100;
 }
 
-impl common::Trait for Test {
+impl common::membership::Trait for Test {
     type MemberId = u64;
     type ActorId = u64;
 }
@@ -276,7 +276,7 @@ impl common::working_group::WorkingGroupBudgetHandler<Test> for () {
 impl common::working_group::WorkingGroupAuthenticator<Test> for () {
     fn ensure_worker_origin(
         _origin: <Test as frame_system::Trait>::Origin,
-        _worker_id: &<Test as common::Trait>::ActorId,
+        _worker_id: &<Test as common::membership::Trait>::ActorId,
     ) -> DispatchResult {
         unimplemented!();
     }
@@ -285,7 +285,7 @@ impl common::working_group::WorkingGroupAuthenticator<Test> for () {
         unimplemented!()
     }
 
-    fn get_leader_member_id() -> Option<<Test as common::Trait>::MemberId> {
+    fn get_leader_member_id() -> Option<<Test as common::membership::Trait>::MemberId> {
         unimplemented!();
     }
 
@@ -295,7 +295,7 @@ impl common::working_group::WorkingGroupAuthenticator<Test> for () {
 
     fn is_worker_account_id(
         _account_id: &<Test as frame_system::Trait>::AccountId,
-        _worker_id: &<Test as common::Trait>::ActorId,
+        _worker_id: &<Test as common::membership::Trait>::ActorId,
     ) -> bool {
         unimplemented!()
     }
@@ -375,7 +375,7 @@ impl Default for proposals::Call<Test> {
     }
 }
 
-impl common::origin::MemberOriginValidator<Origin, u64, u64> for () {
+impl common::membership::MemberOriginValidator<Origin, u64, u64> for () {
     fn ensure_member_controller_account_origin(
         origin: Origin,
         _account_id: u64,
@@ -390,7 +390,7 @@ impl common::origin::MemberOriginValidator<Origin, u64, u64> for () {
     }
 }
 
-impl common::origin::CouncilOriginValidator<Origin, u64, u64> for () {
+impl common::council::CouncilOriginValidator<Origin, u64, u64> for () {
     fn ensure_member_consulate(origin: Origin, _: u64) -> DispatchResult {
         frame_system::ensure_signed(origin)?;
 

+ 19 - 8
runtime-modules/referendum/src/benchmarking.rs

@@ -52,7 +52,10 @@ fn funded_account<T: Trait<I>, I: Instance>(name: &'static str, id: u32) -> T::A
 fn make_multiple_votes_for_multiple_options<
     T: Trait<I>
         + membership::Trait
-        + OptionCreator<<T as frame_system::Trait>::AccountId, <T as common::Trait>::MemberId>,
+        + OptionCreator<
+            <T as frame_system::Trait>::AccountId,
+            <T as common::membership::Trait>::MemberId,
+        >,
     I: Instance,
 >(
     number_of_options: u32,
@@ -88,7 +91,10 @@ fn make_multiple_votes_for_multiple_options<
 fn vote_for<
     T: Trait<I>
         + membership::Trait
-        + OptionCreator<<T as frame_system::Trait>::AccountId, <T as common::Trait>::MemberId>,
+        + OptionCreator<
+            <T as frame_system::Trait>::AccountId,
+            <T as common::membership::Trait>::MemberId,
+        >,
     I: Instance,
 >(
     name: &'static str,
@@ -136,7 +142,10 @@ fn vote_for<
 fn create_account_and_vote<
     T: Trait<I>
         + membership::Trait
-        + OptionCreator<<T as frame_system::Trait>::AccountId, <T as common::Trait>::MemberId>,
+        + OptionCreator<
+            <T as frame_system::Trait>::AccountId,
+            <T as common::membership::Trait>::MemberId,
+        >,
     I: Instance,
 >(
     name: &'static str,
@@ -247,8 +256,10 @@ fn member_funded_account<T: Trait<I> + membership::Trait, I: Instance>(
 
 fn add_and_reveal_multiple_votes_and_add_extra_unrevealed_vote<
     T: Trait<I>
-        + OptionCreator<<T as frame_system::Trait>::AccountId, <T as common::Trait>::MemberId>
-        + membership::Trait,
+        + OptionCreator<
+            <T as frame_system::Trait>::AccountId,
+            <T as common::membership::Trait>::MemberId,
+        > + membership::Trait,
     I: Instance,
 >(
     target_winners: u32,
@@ -324,7 +335,7 @@ fn add_and_reveal_multiple_votes_and_add_extra_unrevealed_vote<
 benchmarks_instance! {
     where_clause {
         where T: OptionCreator<<T as frame_system::Trait>::AccountId,
-        <T as common::Trait>::MemberId>,
+        <T as common::membership::Trait>::MemberId>,
         T: membership::Trait
     }
     _ { }
@@ -686,12 +697,12 @@ mod tests {
     impl
         OptionCreator<
             <Runtime as frame_system::Trait>::AccountId,
-            <Runtime as common::Trait>::MemberId,
+            <Runtime as common::membership::Trait>::MemberId,
         > for Runtime
     {
         fn create_option(
             _: <Runtime as frame_system::Trait>::AccountId,
-            _: <Runtime as common::Trait>::MemberId,
+            _: <Runtime as common::membership::Trait>::MemberId,
         ) {
         }
     }

+ 15 - 11
runtime-modules/referendum/src/lib.rs

@@ -134,17 +134,20 @@ pub struct CastVote<Hash, Currency, MemberId> {
 
 // types simplifying access to common structs and enums
 pub type BalanceOf<T> = <T as balances::Trait>::Balance;
-pub type CastVoteOf<T> =
-    CastVote<<T as frame_system::Trait>::Hash, BalanceOf<T>, <T as common::Trait>::MemberId>;
+pub type CastVoteOf<T> = CastVote<
+    <T as frame_system::Trait>::Hash,
+    BalanceOf<T>,
+    <T as common::membership::Trait>::MemberId,
+>;
 pub type ReferendumStageVotingOf<T> =
     ReferendumStageVoting<<T as frame_system::Trait>::BlockNumber>;
 pub type ReferendumStageRevealingOf<T, I> = ReferendumStageRevealing<
     <T as frame_system::Trait>::BlockNumber,
-    <T as common::Trait>::MemberId,
+    <T as common::membership::Trait>::MemberId,
     <T as Trait<I>>::VotePower,
 >;
 pub type OptionResultOf<T, I> =
-    OptionResult<<T as common::Trait>::MemberId, <T as Trait<I>>::VotePower>;
+    OptionResult<<T as common::membership::Trait>::MemberId, <T as Trait<I>>::VotePower>;
 
 // types aliases for check functions return values
 pub type CanRevealResult<T, I> = (
@@ -207,7 +210,7 @@ pub trait ReferendumManager<Origin, AccountId, MemberId, Hash> {
 
 /// The main Referendum module's trait.
 pub trait Trait<I: Instance = DefaultInstance>:
-    frame_system::Trait + common::Trait + balances::Trait
+    frame_system::Trait + common::membership::Trait + balances::Trait
 {
     /// The overarching event type.
     type Event: From<Event<Self, I>> + Into<<Self as frame_system::Trait>::Event>;
@@ -295,7 +298,7 @@ decl_event! {
         <T as frame_system::Trait>::Hash,
         <T as frame_system::Trait>::AccountId,
         <T as Trait<I>>::VotePower,
-        <T as common::Trait>::MemberId,
+        <T as common::membership::Trait>::MemberId,
     {
         /// Referendum started
         ReferendumStarted(u64),
@@ -455,7 +458,7 @@ decl_module! {
         pub fn reveal_vote(
             origin,
             salt: Vec<u8>,
-            vote_option_id: <T as common::Trait>::MemberId
+            vote_option_id: <T as common::membership::Trait>::MemberId
         ) -> Result<(), Error<T, I>> {
             let (stage_data, account_id, cast_vote) =
                 EnsureChecks::<T, I>::can_reveal_vote::<Self>(origin, &salt, &vote_option_id)?;
@@ -623,7 +626,7 @@ impl<T: Trait<I>, I: Instance> ReferendumManager<T::Origin, T::AccountId, T::Mem
         account_id: &<T as frame_system::Trait>::AccountId,
         salt: &[u8],
         cycle_id: &u64,
-        vote_option_id: &<T as common::Trait>::MemberId,
+        vote_option_id: &<T as common::membership::Trait>::MemberId,
     ) -> T::Hash {
         let mut payload = account_id.encode();
         let mut mut_option_id = vote_option_id.encode();
@@ -674,7 +677,8 @@ impl<T: Trait<I>, I: Instance> Mutations<T, I> {
     // Conclude referendum, count votes, and select the winners.
     fn conclude_referendum(
         revealing_stage: ReferendumStageRevealingOf<T, I>,
-    ) -> Vec<OptionResult<<T as common::Trait>::MemberId, <T as Trait<I>>::VotePower>> {
+    ) -> Vec<OptionResult<<T as common::membership::Trait>::MemberId, <T as Trait<I>>::VotePower>>
+    {
         // reset referendum state
         Stage::<T, I>::put(ReferendumStage::Inactive);
 
@@ -710,7 +714,7 @@ impl<T: Trait<I>, I: Instance> Mutations<T, I> {
     fn reveal_vote(
         stage_data: ReferendumStageRevealingOf<T, I>,
         account_id: &<T as frame_system::Trait>::AccountId,
-        option_id: &<T as common::Trait>::MemberId,
+        option_id: &<T as common::membership::Trait>::MemberId,
         cast_vote: CastVoteOf<T>,
     ) -> Result<(), Error<T, I>> {
         // prepare new values
@@ -924,7 +928,7 @@ impl<T: Trait<I>, I: Instance> EnsureChecks<T, I> {
     fn can_reveal_vote<R: ReferendumManager<T::Origin, T::AccountId, T::MemberId, T::Hash>>(
         origin: T::Origin,
         salt: &[u8],
-        vote_option_id: &<T as common::Trait>::MemberId,
+        vote_option_id: &<T as common::membership::Trait>::MemberId,
     ) -> Result<CanRevealResult<T, I>, Error<T, I>> {
         // ensure superuser requested action
         let account_id = Self::ensure_regular_user(origin)?;

+ 14 - 11
runtime-modules/referendum/src/mock.rs

@@ -272,7 +272,7 @@ impl common::working_group::WorkingGroupBudgetHandler<Runtime> for () {
 impl common::working_group::WorkingGroupAuthenticator<Runtime> for () {
     fn ensure_worker_origin(
         _origin: <Runtime as frame_system::Trait>::Origin,
-        _worker_id: &<Runtime as common::Trait>::ActorId,
+        _worker_id: &<Runtime as common::membership::Trait>::ActorId,
     ) -> DispatchResult {
         unimplemented!()
     }
@@ -281,7 +281,7 @@ impl common::working_group::WorkingGroupAuthenticator<Runtime> for () {
         unimplemented!()
     }
 
-    fn get_leader_member_id() -> Option<<Runtime as common::Trait>::MemberId> {
+    fn get_leader_member_id() -> Option<<Runtime as common::membership::Trait>::MemberId> {
         unimplemented!()
     }
 
@@ -291,13 +291,13 @@ impl common::working_group::WorkingGroupAuthenticator<Runtime> for () {
 
     fn is_worker_account_id(
         _account_id: &<Runtime as frame_system::Trait>::AccountId,
-        _worker_id: &<Runtime as common::Trait>::ActorId,
+        _worker_id: &<Runtime as common::membership::Trait>::ActorId,
     ) -> bool {
         true
     }
 }
 
-impl common::Trait for Runtime {
+impl common::membership::Trait for Runtime {
     type MemberId = u64;
     type ActorId = u64;
 }
@@ -501,7 +501,7 @@ where
 
     pub fn calculate_commitment(
         account_id: &<T as frame_system::Trait>::AccountId,
-        vote_option_index: &<T as common::Trait>::MemberId,
+        vote_option_index: &<T as common::membership::Trait>::MemberId,
         cycle_id: &u64,
     ) -> (T::Hash, Vec<u8>) {
         Self::calculate_commitment_for_cycle(account_id, &cycle_id, vote_option_index, None)
@@ -509,7 +509,7 @@ where
 
     pub fn calculate_commitment_custom_salt(
         account_id: &<T as frame_system::Trait>::AccountId,
-        vote_option_index: &<T as common::Trait>::MemberId,
+        vote_option_index: &<T as common::membership::Trait>::MemberId,
         custom_salt: &[u8],
         cycle_id: &u64,
     ) -> (T::Hash, Vec<u8>) {
@@ -530,7 +530,7 @@ where
     pub fn calculate_commitment_for_cycle(
         account_id: &<T as frame_system::Trait>::AccountId,
         cycle_id: &u64,
-        vote_option_index: &<T as common::Trait>::MemberId,
+        vote_option_index: &<T as common::membership::Trait>::MemberId,
         custom_salt: Option<&[u8]>,
     ) -> (T::Hash, Vec<u8>) {
         let salt = match custom_salt {
@@ -542,7 +542,7 @@ where
             <Module<T, I> as ReferendumManager<
                 <T as frame_system::Trait>::Origin,
                 <T as frame_system::Trait>::AccountId,
-                <T as common::Trait>::MemberId,
+                <T as common::membership::Trait>::MemberId,
                 <T as frame_system::Trait>::Hash,
             >>::calculate_commitment(account_id, &salt, cycle_id, vote_option_index),
             salt.to_vec(),
@@ -611,7 +611,7 @@ impl InstanceMocks<Runtime, DefaultInstance> {
             <Module::<Runtime> as ReferendumManager<
                 <Runtime as frame_system::Trait>::Origin,
                 <Runtime as frame_system::Trait>::AccountId,
-                <Runtime as common::Trait>::MemberId,
+                <Runtime as common::membership::Trait>::MemberId,
                 <Runtime as frame_system::Trait>::Hash,
             >>::start_referendum(
                 InstanceMockUtils::<Runtime, DefaultInstance>::mock_origin(OriginType::Root),
@@ -631,7 +631,7 @@ impl InstanceMocks<Runtime, DefaultInstance> {
         <Module<Runtime> as ReferendumManager<
             <Runtime as frame_system::Trait>::Origin,
             <Runtime as frame_system::Trait>::AccountId,
-            <Runtime as common::Trait>::MemberId,
+            <Runtime as common::membership::Trait>::MemberId,
             <Runtime as frame_system::Trait>::Hash,
         >>::force_start(extra_winning_target_count, cycle_id);
     }
@@ -693,7 +693,10 @@ impl InstanceMocks<Runtime, DefaultInstance> {
 
     pub fn check_revealing_finished(
         expected_winners: Vec<
-            OptionResult<<Runtime as common::Trait>::MemberId, <Runtime as Trait>::VotePower>,
+            OptionResult<
+                <Runtime as common::membership::Trait>::MemberId,
+                <Runtime as Trait>::VotePower,
+            >,
         >,
         expected_referendum_result: BTreeMap<u64, <Runtime as Trait>::VotePower>,
     ) {

+ 5 - 5
runtime-modules/service-discovery/src/mock.rs

@@ -89,7 +89,7 @@ impl Trait for Test {
     type Event = MetaEvent;
 }
 
-impl common::Trait for Test {
+impl common::membership::Trait for Test {
     type MemberId = u64;
     type ActorId = u64;
 }
@@ -179,7 +179,7 @@ impl common::working_group::WorkingGroupBudgetHandler<Test> for () {
 impl common::working_group::WorkingGroupAuthenticator<Test> for () {
     fn ensure_worker_origin(
         _origin: <Test as frame_system::Trait>::Origin,
-        _worker_id: &<Test as common::Trait>::ActorId,
+        _worker_id: &<Test as common::membership::Trait>::ActorId,
     ) -> DispatchResult {
         unimplemented!();
     }
@@ -188,7 +188,7 @@ impl common::working_group::WorkingGroupAuthenticator<Test> for () {
         unimplemented!()
     }
 
-    fn get_leader_member_id() -> Option<<Test as common::Trait>::MemberId> {
+    fn get_leader_member_id() -> Option<<Test as common::membership::Trait>::MemberId> {
         unimplemented!();
     }
 
@@ -198,7 +198,7 @@ impl common::working_group::WorkingGroupAuthenticator<Test> for () {
 
     fn is_worker_account_id(
         _account_id: &<Test as frame_system::Trait>::AccountId,
-        _worker_id: &<Test as common::Trait>::ActorId,
+        _worker_id: &<Test as common::membership::Trait>::ActorId,
     ) -> bool {
         unimplemented!()
     }
@@ -307,7 +307,7 @@ impl working_group::WeightInfo for WorkingGroupWeightInfo {
     }
 }
 
-impl common::origin::MemberOriginValidator<Origin, u64, u64> for () {
+impl common::membership::MemberOriginValidator<Origin, u64, u64> for () {
     fn ensure_member_controller_account_origin(
         origin: Origin,
         _: u64,

+ 3 - 3
runtime-modules/staking-handler/src/lib.rs

@@ -62,7 +62,7 @@ pub trait StakingHandler<AccountId, Balance, MemberId> {
 pub struct StakingManager<
     T: frame_system::Trait
         + pallet_balances::Trait
-        + common::Trait
+        + common::membership::Trait
         + LockComparator<<T as pallet_balances::Trait>::Balance>,
     LockId: Get<LockIdentifier>,
 > {
@@ -73,14 +73,14 @@ pub struct StakingManager<
 impl<
         T: frame_system::Trait
             + pallet_balances::Trait
-            + common::Trait
+            + common::membership::Trait
             + LockComparator<<T as pallet_balances::Trait>::Balance>,
         LockId: Get<LockIdentifier>,
     >
     StakingHandler<
         <T as frame_system::Trait>::AccountId,
         <T as pallet_balances::Trait>::Balance,
-        <T as common::Trait>::MemberId,
+        <T as common::membership::Trait>::MemberId,
     > for StakingManager<T, LockId>
 {
     fn lock(

+ 1 - 1
runtime-modules/staking-handler/src/mock.rs

@@ -66,7 +66,7 @@ impl pallet_balances::Trait for Test {
     type MaxLocks = ();
 }
 
-impl common::Trait for Test {
+impl common::membership::Trait for Test {
     type MemberId = u64;
     type ActorId = u64;
 }

+ 1 - 1
runtime-modules/storage/src/data_directory.rs

@@ -32,7 +32,7 @@ use sp_std::vec::Vec;
 #[cfg(feature = "std")]
 use serde::{Deserialize, Serialize};
 
-use common::origin::MemberOriginValidator;
+use common::membership::MemberOriginValidator;
 use common::working_group::WorkingGroupAuthenticator;
 pub(crate) use common::BlockAndTime;
 

+ 1 - 1
runtime-modules/storage/src/data_object_type_registry.rs

@@ -33,7 +33,7 @@ const DEFAULT_TYPE_DESCRIPTION: &str = "Default data object type for audio and v
 const DEFAULT_FIRST_DATA_OBJECT_TYPE_ID: u8 = 1;
 
 /// The _Data object type registry_ main _Trait_.
-pub trait Trait: frame_system::Trait + common::Trait {
+pub trait Trait: frame_system::Trait + common::membership::Trait {
     /// _Data object type registry_ event type.
     type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
 

+ 5 - 5
runtime-modules/storage/src/tests/mock.rs

@@ -306,7 +306,7 @@ impl membership::WeightInfo for Weights {
     }
 }
 
-impl common::origin::MemberOriginValidator<Origin, u64, u64> for () {
+impl common::membership::MemberOriginValidator<Origin, u64, u64> for () {
     fn ensure_member_controller_account_origin(
         origin: Origin,
         _: u64,
@@ -348,7 +348,7 @@ impl data_object_storage_registry::Trait for Test {
     type ContentIdExists = MockContent;
 }
 
-impl common::Trait for Test {
+impl common::membership::Trait for Test {
     type MemberId = u64;
     type ActorId = u32;
 }
@@ -379,7 +379,7 @@ impl common::working_group::WorkingGroupBudgetHandler<Test> for () {
 impl common::working_group::WorkingGroupAuthenticator<Test> for () {
     fn ensure_worker_origin(
         _origin: <Test as frame_system::Trait>::Origin,
-        _worker_id: &<Test as common::Trait>::ActorId,
+        _worker_id: &<Test as common::membership::Trait>::ActorId,
     ) -> DispatchResult {
         unimplemented!();
     }
@@ -388,7 +388,7 @@ impl common::working_group::WorkingGroupAuthenticator<Test> for () {
         unimplemented!()
     }
 
-    fn get_leader_member_id() -> Option<<Test as common::Trait>::MemberId> {
+    fn get_leader_member_id() -> Option<<Test as common::membership::Trait>::MemberId> {
         unimplemented!();
     }
 
@@ -398,7 +398,7 @@ impl common::working_group::WorkingGroupAuthenticator<Test> for () {
 
     fn is_worker_account_id(
         _account_id: &<Test as frame_system::Trait>::AccountId,
-        _worker_id: &<Test as common::Trait>::ActorId,
+        _worker_id: &<Test as common::membership::Trait>::ActorId,
     ) -> bool {
         unimplemented!()
     }

+ 6 - 6
runtime-modules/utility/src/tests/mocks.rs

@@ -254,7 +254,7 @@ impl common::working_group::WorkingGroupBudgetHandler<Test> for () {
 impl common::working_group::WorkingGroupAuthenticator<Test> for () {
     fn ensure_worker_origin(
         _origin: <Test as frame_system::Trait>::Origin,
-        _worker_id: &<Test as common::Trait>::ActorId,
+        _worker_id: &<Test as common::membership::Trait>::ActorId,
     ) -> DispatchResult {
         unimplemented!();
     }
@@ -263,7 +263,7 @@ impl common::working_group::WorkingGroupAuthenticator<Test> for () {
         unimplemented!()
     }
 
-    fn get_leader_member_id() -> Option<<Test as common::Trait>::MemberId> {
+    fn get_leader_member_id() -> Option<<Test as common::membership::Trait>::MemberId> {
         unimplemented!();
     }
 
@@ -273,7 +273,7 @@ impl common::working_group::WorkingGroupAuthenticator<Test> for () {
 
     fn is_worker_account_id(
         _account_id: &<Test as frame_system::Trait>::AccountId,
-        _worker_id: &<Test as common::Trait>::ActorId,
+        _worker_id: &<Test as common::membership::Trait>::ActorId,
     ) -> bool {
         unimplemented!()
     }
@@ -699,7 +699,7 @@ impl common::StakingAccountValidator<Test> for () {
     }
 }
 
-impl common::origin::MemberOriginValidator<Origin, u64, u64> for () {
+impl common::membership::MemberOriginValidator<Origin, u64, u64> for () {
     fn ensure_member_controller_account_origin(
         origin: Origin,
         _: u64,
@@ -714,7 +714,7 @@ impl common::origin::MemberOriginValidator<Origin, u64, u64> for () {
     }
 }
 
-impl common::origin::CouncilOriginValidator<Origin, u64, u64> for () {
+impl common::council::CouncilOriginValidator<Origin, u64, u64> for () {
     fn ensure_member_consulate(origin: Origin, _: u64) -> DispatchResult {
         frame_system::ensure_signed(origin)?;
 
@@ -722,7 +722,7 @@ impl common::origin::CouncilOriginValidator<Origin, u64, u64> for () {
     }
 }
 
-impl common::Trait for Test {
+impl common::membership::Trait for Test {
     type MemberId = u64;
     type ActorId = u64;
 }

+ 2 - 2
runtime-modules/working-group/src/lib.rs

@@ -56,7 +56,7 @@ use types::{ApplicationInfo, WorkerInfo};
 
 pub use checks::{ensure_worker_exists, ensure_worker_signed};
 
-use common::origin::MemberOriginValidator;
+use common::membership::MemberOriginValidator;
 use common::{MemberId, StakingAccountValidator};
 use frame_support::dispatch::DispatchResult;
 use staking_handler::StakingHandler;
@@ -92,7 +92,7 @@ pub trait WeightInfo {
 
 /// The _Group_ main _Trait_
 pub trait Trait<I: Instance = DefaultInstance>:
-    frame_system::Trait + balances::Trait + common::Trait
+    frame_system::Trait + balances::Trait + common::membership::Trait
 {
     /// _Administration_ event type.
     type Event: From<Event<Self, I>> + Into<<Self as frame_system::Trait>::Event>;

+ 2 - 2
runtime-modules/working-group/src/tests/mock.rs

@@ -95,7 +95,7 @@ impl balances::Trait for Test {
     type MaxLocks = ();
 }
 
-impl common::Trait for Test {
+impl common::membership::Trait for Test {
     type MemberId = u64;
     type ActorId = u64;
 }
@@ -280,7 +280,7 @@ impl crate::WeightInfo for () {
 
 pub const ACTOR_ORIGIN_ERROR: &'static str = "Invalid membership";
 
-impl common::origin::MemberOriginValidator<Origin, u64, u64> for () {
+impl common::membership::MemberOriginValidator<Origin, u64, u64> for () {
     fn ensure_member_controller_account_origin(
         origin: Origin,
         member_id: u64,

+ 3 - 3
runtime-modules/working-group/src/types.rs

@@ -29,13 +29,13 @@ pub(crate) struct ApplicationInfo<T: crate::Trait<I>, I: crate::Instance> {
 }
 
 // WorkerId - Worker - helper struct.
-pub(crate) struct WorkerInfo<T: common::Trait + frame_system::Trait + balances::Trait> {
+pub(crate) struct WorkerInfo<T: common::membership::Trait + frame_system::Trait + balances::Trait> {
     pub worker_id: WorkerId<T>,
     pub worker: Worker<T>,
 }
 
-impl<T: common::Trait + frame_system::Trait + balances::Trait> From<(WorkerId<T>, Worker<T>)>
-    for WorkerInfo<T>
+impl<T: common::membership::Trait + frame_system::Trait + balances::Trait>
+    From<(WorkerId<T>, Worker<T>)> for WorkerInfo<T>
 {
     fn from((worker_id, worker): (WorkerId<T>, Worker<T>)) -> Self {
         WorkerInfo { worker_id, worker }

+ 7 - 4
runtime/Cargo.toml

@@ -4,7 +4,7 @@ edition = '2018'
 name = 'joystream-node-runtime'
 # Follow convention: https://github.com/Joystream/substrate-runtime-joystream/issues/1
 # {Authoring}.{Spec}.{Impl} of the RuntimeVersion
-version = '8.0.1'
+version = '8.1.0'
 
 [dependencies]
 # Third-party dependencies
@@ -76,8 +76,9 @@ proposals-engine = { package = 'pallet-proposals-engine', default-features = fal
 proposals-discussion = { package = 'pallet-proposals-discussion', default-features = false, path = '../runtime-modules/proposals/discussion'}
 proposals-codex = { package = 'pallet-proposals-codex', default-features = false, path = '../runtime-modules/proposals/codex'}
 content-directory = { package = 'pallet-content-directory', default-features = false, path = '../runtime-modules/content-directory' }
-pallet_constitution = { package = 'pallet-constitution', default-features = false, path = '../runtime-modules/constitution' }
+pallet-constitution = { package = 'pallet-constitution', default-features = false, path = '../runtime-modules/constitution' }
 staking-handler = { package = 'pallet-staking-handler', default-features = false, path = '../runtime-modules/staking-handler'}
+bounty = { package = 'pallet-bounty', default-features = false, path = '../runtime-modules/bounty'}
 blog = { package = 'pallet-blog', default-features = false, path = '../runtime-modules/blog'}
 joystream-utility = { package = 'pallet-utility', default-features = false, path = '../runtime-modules/utility'}
 
@@ -151,8 +152,9 @@ std = [
     'proposals-discussion/std',
     'proposals-codex/std',
     'content-directory/std',
-    'pallet_constitution/std',
+    'pallet-constitution/std',
     'staking-handler/std',
+    'bounty/std',
     'blog/std',
     'joystream-utility/std'
 ]
@@ -172,12 +174,13 @@ runtime-benchmarks = [
     "proposals-engine/runtime-benchmarks",
     "proposals-codex/runtime-benchmarks",
     "joystream-utility/runtime-benchmarks",
-    "pallet_constitution/runtime-benchmarks",
+    "pallet-constitution/runtime-benchmarks",
     "working-group/runtime-benchmarks",
     "forum/runtime-benchmarks",
     "membership/runtime-benchmarks",
     "council/runtime-benchmarks",
     "referendum/runtime-benchmarks",
+    "bounty/runtime-benchmarks",
     "blog/runtime-benchmarks",
     "hex-literal",
 ]

+ 6 - 0
runtime/src/constants.rs

@@ -112,6 +112,7 @@ parameter_types! {
     pub const MembershipWorkingGroupLockId: LockIdentifier = [9; 8];
     pub const InvitedMemberLockId: LockIdentifier = [10; 8];
     pub const StakingCandidateLockId: LockIdentifier = [11; 8];
+    pub const BountyLockId: LockIdentifier = [12; 8];
 }
 
 // Staking lock ID used by nomination and validation in the staking pallet.
@@ -197,6 +198,11 @@ lazy_static! {
             VotingLockId::get(),
             StakingCandidateLockId::get(),
         ].to_vec()),
+        // Bounty
+        (BountyLockId::get(), [
+            VotingLockId::get(),
+            StakingCandidateLockId::get(),
+        ].to_vec()),
     ]
     .iter()
     .fold(BTreeSet::new(), |mut acc, item| {

+ 27 - 3
runtime/src/lib.rs

@@ -96,8 +96,8 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
     spec_name: create_runtime_str!("joystream-node"),
     impl_name: create_runtime_str!("joystream-node"),
     authoring_version: 8,
-    spec_version: 0,
-    impl_version: 1,
+    spec_version: 1,
+    impl_version: 0,
     apis: crate::runtime_api::EXPORTED_RUNTIME_API_VERSIONS,
     transaction_version: 1,
 };
@@ -623,7 +623,7 @@ impl storage::data_object_storage_registry::Trait for Runtime {
     type ContentIdExists = DataDirectory;
 }
 
-impl common::Trait for Runtime {
+impl common::membership::Trait for Runtime {
     type MemberId = MemberId;
     type ActorId = ActorId;
 }
@@ -919,6 +919,29 @@ impl pallet_constitution::Trait for Runtime {
     type WeightInfo = weights::pallet_constitution::WeightInfo;
 }
 
+parameter_types! {
+    pub const BountyModuleId: ModuleId = ModuleId(*b"m:bounty"); // module : bounty
+    pub const ClosedContractSizeLimit: u32 = 50;
+    pub const MinCherryLimit: Balance = 10;
+    pub const MinFundingLimit: Balance = 10;
+    pub const MinWorkEntrantStake: Balance = 100;
+}
+
+impl bounty::Trait for Runtime {
+    type Event = Event;
+    type ModuleId = BountyModuleId;
+    type BountyId = u64;
+    type Membership = Members;
+    type WeightInfo = weights::bounty::WeightInfo;
+    type CouncilBudgetManager = Council;
+    type StakingHandler = staking_handler::StakingManager<Self, BountyLockId>;
+    type EntryId = u64;
+    type ClosedContractSizeLimit = ClosedContractSizeLimit;
+    type MinCherryLimit = MinCherryLimit;
+    type MinFundingLimit = MinFundingLimit;
+    type MinWorkEntrantStake = MinWorkEntrantStake;
+}
+
 parameter_types! {
     pub const PostsMaxNumber: u64 = 20;
     pub const RepliesMaxNumber: u64 = 100;
@@ -988,6 +1011,7 @@ construct_runtime!(
         Forum: forum::{Module, Call, Storage, Event<T>, Config<T>},
         ContentDirectory: content_directory::{Module, Call, Storage, Event<T>, Config<T>},
         Constitution: pallet_constitution::{Module, Call, Storage, Event},
+        Bounty: bounty::{Module, Call, Storage, Event<T>},
         Blog: blog::<Instance1>::{Module, Call, Storage, Event<T>},
         JoystreamUtility: joystream_utility::{Module, Call, Event<T>},
         // --- Storage

+ 8 - 6
runtime/src/runtime_api.rs

@@ -294,6 +294,7 @@ impl_runtime_apis! {
             use crate::ImOnline;
             use crate::Council;
             use crate::Referendum;
+            use crate::Bounty;
             use crate::Blog;
             use crate::JoystreamUtility;
             use crate::Staking;
@@ -304,8 +305,8 @@ impl_runtime_apis! {
             // we need these two lines below.
             impl pallet_session_benchmarking::Trait for Runtime {}
             impl frame_system_benchmarking::Trait for Runtime {}
-            impl referendum::OptionCreator<<Runtime as frame_system::Trait>::AccountId, <Runtime as common::Trait>::MemberId> for Runtime {
-                fn create_option(account_id: <Runtime as frame_system::Trait>::AccountId, member_id: <Runtime as common::Trait>::MemberId) {
+            impl referendum::OptionCreator<<Runtime as frame_system::Trait>::AccountId, <Runtime as common::membership::Trait>::MemberId> for Runtime {
+                fn create_option(account_id: <Runtime as frame_system::Trait>::AccountId, member_id: <Runtime as common::membership::Trait>::MemberId) {
                     crate::council::Module::<Runtime>::announce_candidacy(
                         RawOrigin::Signed(account_id.clone()).into(),
                         member_id,
@@ -322,15 +323,15 @@ impl_runtime_apis! {
 
             impl membership::MembershipWorkingGroupHelper<
                 <Runtime as frame_system::Trait>::AccountId,
-                <Runtime as common::Trait>::MemberId,
-                <Runtime as common::Trait>::ActorId,
+                <Runtime as common::membership::Trait>::MemberId,
+                <Runtime as common::membership::Trait>::ActorId,
                     > for Runtime
             {
                 fn insert_a_lead(
                     opening_id: u32,
                     caller_id: &<Runtime as frame_system::Trait>::AccountId,
-                    member_id: <Runtime as common::Trait>::MemberId,
-                ) -> <Runtime as common::Trait>::ActorId {
+                    member_id: <Runtime as common::membership::Trait>::MemberId,
+                ) -> <Runtime as common::membership::Trait>::ActorId {
                     working_group::benchmarking::complete_opening::<Runtime, crate::MembershipWorkingGroupInstance>(
                         working_group::OpeningType::Leader,
                         opening_id,
@@ -387,6 +388,7 @@ impl_runtime_apis! {
             add_benchmark!(params, batches, working_group, ContentDirectoryWorkingGroup);
             add_benchmark!(params, batches, referendum, Referendum);
             add_benchmark!(params, batches, council, Council);
+            add_benchmark!(params, batches, bounty, Bounty);
             add_benchmark!(params, batches, blog, Blog);
             add_benchmark!(params, batches, joystream_utility, JoystreamUtility);
 

+ 22 - 19
runtime/src/tests/proposals_integration/working_group_proposals.rs

@@ -390,7 +390,7 @@ fn run_create_add_working_group_leader_opening_proposal_execution_succeeds<
 >(
     working_group: WorkingGroup,
 ) where
-    <T as common::Trait>::MemberId: From<u64>,
+    <T as common::membership::Trait>::MemberId: From<u64>,
 {
     initial_test_ext().execute_with(|| {
         let member_id: MemberId = 1;
@@ -465,7 +465,7 @@ fn run_create_fill_working_group_leader_opening_proposal_execution_succeeds<
     working_group: WorkingGroup,
 ) where
     <T as frame_system::Trait>::AccountId: From<[u8; 32]>,
-    <T as common::Trait>::MemberId: From<u64>,
+    <T as common::membership::Trait>::MemberId: From<u64>,
     common::MemberId<T>: From<u64>,
 {
     initial_test_ext().execute_with(|| {
@@ -574,19 +574,22 @@ fn create_decrease_group_leader_stake_proposal_execution_succeeds() {
 }
 
 fn run_create_decrease_group_leader_stake_proposal_execution_succeeds<
-    T: working_group::Trait<I> + frame_system::Trait + common::Trait + pallet_balances::Trait,
+    T: working_group::Trait<I>
+        + frame_system::Trait
+        + common::membership::Trait
+        + pallet_balances::Trait,
     I: frame_support::traits::Instance,
     SM: staking_handler::StakingHandler<
         <T as frame_system::Trait>::AccountId,
         <T as pallet_balances::Trait>::Balance,
-        <T as common::Trait>::MemberId,
+        <T as common::membership::Trait>::MemberId,
     >,
 >(
     working_group: WorkingGroup,
 ) where
     <T as frame_system::Trait>::AccountId: From<[u8; 32]>,
-    <T as common::Trait>::MemberId: From<u64>,
-    <T as common::Trait>::ActorId: Into<u64>,
+    <T as common::membership::Trait>::MemberId: From<u64>,
+    <T as common::membership::Trait>::ActorId: Into<u64>,
     <T as pallet_balances::Trait>::Balance: From<u128>,
 {
     initial_test_ext().execute_with(|| {
@@ -725,14 +728,14 @@ fn run_create_slash_group_leader_stake_proposal_execution_succeeds<
     SM: staking_handler::StakingHandler<
         <T as frame_system::Trait>::AccountId,
         <T as pallet_balances::Trait>::Balance,
-        <T as common::Trait>::MemberId,
+        <T as common::membership::Trait>::MemberId,
     >,
 >(
     working_group: WorkingGroup,
 ) where
     <T as frame_system::Trait>::AccountId: From<[u8; 32]>,
-    <T as common::Trait>::MemberId: From<u64>,
-    <T as common::Trait>::ActorId: Into<u64>,
+    <T as common::membership::Trait>::MemberId: From<u64>,
+    <T as common::membership::Trait>::ActorId: Into<u64>,
     <T as pallet_balances::Trait>::Balance: From<u128>,
 {
     initial_test_ext().execute_with(|| {
@@ -872,7 +875,7 @@ fn run_create_set_working_group_mint_capacity_proposal_execution_succeeds<
     working_group: WorkingGroup,
 ) where
     <T as frame_system::Trait>::AccountId: From<[u8; 32]>,
-    <T as common::Trait>::MemberId: From<u64>,
+    <T as common::membership::Trait>::MemberId: From<u64>,
     working_group::BalanceOf<T>: From<u128>,
 {
     initial_test_ext().execute_with(|| {
@@ -912,7 +915,7 @@ fn run_create_syphon_working_group_mint_capacity_proposal_execution_succeeds<
     working_group: WorkingGroup,
 ) where
     <T as frame_system::Trait>::AccountId: From<[u8; 32]>,
-    <T as common::Trait>::MemberId: From<u64>,
+    <T as common::membership::Trait>::MemberId: From<u64>,
     working_group::BalanceOf<T>: From<u128>,
 {
     initial_test_ext().execute_with(|| {
@@ -1059,8 +1062,8 @@ fn run_create_set_group_leader_reward_proposal_execution_succeeds<
     working_group: WorkingGroup,
 ) where
     <T as frame_system::Trait>::AccountId: From<[u8; 32]>,
-    <T as common::Trait>::MemberId: From<u64>,
-    <T as common::Trait>::ActorId: Into<u64>,
+    <T as common::membership::Trait>::MemberId: From<u64>,
+    <T as common::membership::Trait>::ActorId: Into<u64>,
     working_group::BalanceOf<T>: From<u128>,
 {
     initial_test_ext().execute_with(|| {
@@ -1192,15 +1195,15 @@ fn run_create_terminate_group_leader_role_proposal_execution_succeeds<
     SM: staking_handler::StakingHandler<
         <T as frame_system::Trait>::AccountId,
         <T as pallet_balances::Trait>::Balance,
-        <T as common::Trait>::MemberId,
+        <T as common::membership::Trait>::MemberId,
     >,
 >(
     working_group: WorkingGroup,
 ) where
     <T as frame_system::Trait>::AccountId: From<[u8; 32]>,
-    <T as common::Trait>::MemberId: From<u64>,
+    <T as common::membership::Trait>::MemberId: From<u64>,
     common::MemberId<T>: From<u64>,
-    <T as common::Trait>::ActorId: Into<u64>,
+    <T as common::membership::Trait>::ActorId: Into<u64>,
     <T as pallet_balances::Trait>::Balance: From<u128>,
 {
     initial_test_ext().execute_with(|| {
@@ -1334,14 +1337,14 @@ fn run_create_terminate_group_leader_role_proposal_with_slashing_execution_succe
     SM: staking_handler::StakingHandler<
         <T as frame_system::Trait>::AccountId,
         <T as pallet_balances::Trait>::Balance,
-        <T as common::Trait>::MemberId,
+        <T as common::membership::Trait>::MemberId,
     >,
 >(
     working_group: WorkingGroup,
 ) where
     <T as frame_system::Trait>::AccountId: From<[u8; 32]>,
-    <T as common::Trait>::MemberId: From<u64>,
-    <T as common::Trait>::ActorId: Into<u64>,
+    <T as common::membership::Trait>::MemberId: From<u64>,
+    <T as common::membership::Trait>::ActorId: Into<u64>,
     <T as pallet_balances::Trait>::Balance: From<u128>,
 {
     initial_test_ext().execute_with(|| {

+ 113 - 0
runtime/src/weights/bounty.rs

@@ -0,0 +1,113 @@
+//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 2.0.0
+
+#![allow(unused_parens)]
+#![allow(unused_imports)]
+
+use frame_support::weights::{constants::RocksDbWeight as DbWeight, Weight};
+
+pub struct WeightInfo;
+impl bounty::WeightInfo for WeightInfo {
+    fn create_bounty_by_council(i: u32, j: u32) -> Weight {
+        (0 as Weight)
+            .saturating_add((194_000 as Weight).saturating_mul(i as Weight))
+            .saturating_add((28_742_000 as Weight).saturating_mul(j as Weight))
+            .saturating_add(DbWeight::get().reads(3 as Weight))
+            .saturating_add(DbWeight::get().writes(4 as Weight))
+    }
+    fn create_bounty_by_member(i: u32, j: u32) -> Weight {
+        (843_057_000 as Weight)
+            .saturating_add((174_000 as Weight).saturating_mul(i as Weight))
+            .saturating_add((6_931_000 as Weight).saturating_mul(j as Weight))
+            .saturating_add(DbWeight::get().reads(4 as Weight))
+            .saturating_add(DbWeight::get().writes(4 as Weight))
+    }
+    fn cancel_bounty_by_council() -> Weight {
+        (527_000_000 as Weight)
+            .saturating_add(DbWeight::get().reads(3 as Weight))
+            .saturating_add(DbWeight::get().writes(3 as Weight))
+    }
+    fn cancel_bounty_by_member() -> Weight {
+        (872_000_000 as Weight)
+            .saturating_add(DbWeight::get().reads(4 as Weight))
+            .saturating_add(DbWeight::get().writes(3 as Weight))
+    }
+    fn veto_bounty() -> Weight {
+        (576_000_000 as Weight)
+            .saturating_add(DbWeight::get().reads(3 as Weight))
+            .saturating_add(DbWeight::get().writes(3 as Weight))
+    }
+    fn fund_bounty_by_member() -> Weight {
+        (866_000_000 as Weight)
+            .saturating_add(DbWeight::get().reads(5 as Weight))
+            .saturating_add(DbWeight::get().writes(4 as Weight))
+    }
+    fn fund_bounty_by_council() -> Weight {
+        (559_000_000 as Weight)
+            .saturating_add(DbWeight::get().reads(4 as Weight))
+            .saturating_add(DbWeight::get().writes(4 as Weight))
+    }
+    fn withdraw_funding_by_member() -> Weight {
+        (939_000_000 as Weight)
+            .saturating_add(DbWeight::get().reads(6 as Weight))
+            .saturating_add(DbWeight::get().writes(4 as Weight))
+    }
+    fn withdraw_funding_by_council() -> Weight {
+        (688_000_000 as Weight)
+            .saturating_add(DbWeight::get().reads(5 as Weight))
+            .saturating_add(DbWeight::get().writes(4 as Weight))
+    }
+    fn announce_work_entry(i: u32) -> Weight {
+        (774_826_000 as Weight)
+            .saturating_add((10_400_000 as Weight).saturating_mul(i as Weight))
+            .saturating_add(DbWeight::get().reads(6 as Weight))
+            .saturating_add(DbWeight::get().writes(5 as Weight))
+    }
+    fn withdraw_work_entry() -> Weight {
+        (911_000_000 as Weight)
+            .saturating_add(DbWeight::get().reads(5 as Weight))
+            .saturating_add(DbWeight::get().writes(4 as Weight))
+    }
+    fn submit_work(i: u32) -> Weight {
+        (546_484_000 as Weight)
+            .saturating_add((171_000 as Weight).saturating_mul(i as Weight))
+            .saturating_add(DbWeight::get().reads(3 as Weight))
+            .saturating_add(DbWeight::get().writes(2 as Weight))
+    }
+    fn submit_oracle_judgment_by_council_all_winners(i: u32) -> Weight {
+        (0 as Weight)
+            .saturating_add((150_234_000 as Weight).saturating_mul(i as Weight))
+            .saturating_add(DbWeight::get().reads(3 as Weight))
+            .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(i as Weight)))
+            .saturating_add(DbWeight::get().writes(3 as Weight))
+            .saturating_add(DbWeight::get().writes((1 as Weight).saturating_mul(i as Weight)))
+    }
+    fn submit_oracle_judgment_by_council_all_rejected(i: u32) -> Weight {
+        (3_192_844_000 as Weight)
+            .saturating_add((552_887_000 as Weight).saturating_mul(i as Weight))
+            .saturating_add(DbWeight::get().reads(1 as Weight))
+            .saturating_add(DbWeight::get().reads((3 as Weight).saturating_mul(i as Weight)))
+            .saturating_add(DbWeight::get().writes(1 as Weight))
+            .saturating_add(DbWeight::get().writes((3 as Weight).saturating_mul(i as Weight)))
+    }
+    fn submit_oracle_judgment_by_member_all_winners(i: u32) -> Weight {
+        (317_671_000 as Weight)
+            .saturating_add((130_010_000 as Weight).saturating_mul(i as Weight))
+            .saturating_add(DbWeight::get().reads(4 as Weight))
+            .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(i as Weight)))
+            .saturating_add(DbWeight::get().writes(3 as Weight))
+            .saturating_add(DbWeight::get().writes((1 as Weight).saturating_mul(i as Weight)))
+    }
+    fn submit_oracle_judgment_by_member_all_rejected(i: u32) -> Weight {
+        (261_974_000 as Weight)
+            .saturating_add((593_591_000 as Weight).saturating_mul(i as Weight))
+            .saturating_add(DbWeight::get().reads(2 as Weight))
+            .saturating_add(DbWeight::get().reads((3 as Weight).saturating_mul(i as Weight)))
+            .saturating_add(DbWeight::get().writes(1 as Weight))
+            .saturating_add(DbWeight::get().writes((3 as Weight).saturating_mul(i as Weight)))
+    }
+    fn withdraw_work_entrant_funds() -> Weight {
+        (1_248_000_000 as Weight)
+            .saturating_add(DbWeight::get().reads(8 as Weight))
+            .saturating_add(DbWeight::get().writes(6 as Weight))
+    }
+}

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

@@ -25,6 +25,7 @@ pub mod substrate_utility;
 
 // Joystream pallets
 pub mod blog;
+pub mod bounty;
 pub mod council;
 pub mod forum;
 pub mod joystream_utility;

+ 1 - 0
scripts/generate-weights.sh

@@ -62,5 +62,6 @@ benchmark council
 benchmark referendum
 benchmark forum
 benchmark membership
+benchmark bounty
 benchmark blog
 benchmark joystream_utility

Some files were not shown because too many files changed in this diff