// This file is part of Bizinikiwi. // Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! > Made with *Bizinikiwi*, for *Pezkuwi*. //! //! [![github]](https://github.com/pezkuwichain/pezkuwi-sdk/tree/master/bizinikiwi/pezframe/multi-asset-bounties) - //! [![pezkuwi]](https://pezkuwichain.io) //! //! [pezkuwi]: https://img.shields.io/badge/polkadot-E6007A?style=for-the-badge&logo=polkadot&logoColor=white //! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github //! //! //! # Multi Asset Bounties Pezpallet ( `pezpallet-multi-asset-bounties` ) //! //! ## Bounty //! //! A bounty is a reward for completing a specified body of work or achieving a defined set of //! objectives. The work must be completed for a predefined amount to be paid out. A curator is //! assigned when the bounty is funded, and is responsible for awarding the bounty once the //! objectives are met. To support parallel execution and better governance, a bounty can be split //! into multiple child bounties. Each child bounty represents a smaller task derived from the //! parent bounty. The parent bounty curator may assign a separate curator to each child bounty at //! creation time. The curator may be unassigned, resulting in a new curator election. A bounty may //! be cancelled at any time—unless a payment has already been attempted and is awaiting status //! confirmation. //! //! > NOTE: A parent bounty cannot be closed if it has any active child bounties associated with it. //! //! ### Terminology //! //! - **Bounty:** A reward for a predefined body of work upon completion. A bounty defines the total //! reward and can be subdivided into multiple child bounties. When referenced in the context of //! child bounties, it is referred to as *parent bounty*. //! - **Curator:** An account managing the bounty and assigning a payout address. //! - **Child Bounty:** A subtask or milestone funded by a parent bounty. It may carry its own //! curator, and reward similar to the parent bounty. //! - **Curator deposit:** The payment in native asset from a candidate willing to curate a funded //! bounty. The deposit is returned when/if the bounty is completed. //! - **Bounty value:** The total amount in a given asset kind that should be paid to the //! Beneficiary if the bounty is rewarded. //! - **Beneficiary:** The account/location to which the total or part of the bounty is assigned to. //! //! ### Example //! //! 1. Fund a bounty approved by spend origin of some asset kind with a proposed curator. #![doc = docify::embed!("src/tests.rs", fund_bounty_works)] //! //! 2. Award a bounty to a beneficiary. #![doc = docify::embed!("src/tests.rs", award_bounty_works)] //! //! ## Pezpallet API //! //! See the [`pezpallet`] module for more information about the interfaces this pezpallet exposes, //! including its configuration trait, dispatchables, storage items, events and errors. #![cfg_attr(not(feature = "std"), no_std)] mod benchmarking; mod mock; mod tests; pub mod weights; #[cfg(feature = "runtime-benchmarks")] pub use benchmarking::ArgumentsFactory; pub use pezpallet::*; pub use weights::WeightInfo; extern crate alloc; use alloc::{boxed::Box, collections::btree_map::BTreeMap}; use pezframe_support::{ dispatch::{DispatchResult, DispatchResultWithPostInfo}, dispatch_context::with_context, pezpallet_prelude::*, traits::{ tokens::{ Balance, ConversionFromAssetBalance, ConversionToAssetBalance, PayWithSource, PaymentStatus, }, Consideration, EnsureOrigin, Get, QueryPreimage, StorePreimage, }, PalletId, }; use pezframe_system::pezpallet_prelude::{ ensure_signed, BlockNumberFor as SystemBlockNumberFor, OriginFor, }; use pezsp_runtime::{ traits::{AccountIdConversion, BadOrigin, Convert, Saturating, StaticLookup, TryConvert, Zero}, Permill, RuntimeDebug, }; use scale_info::TypeInfo; pub type BalanceOf = <>::Paymaster as PayWithSource>::Balance; pub type BeneficiaryLookupOf = <>::BeneficiaryLookup as StaticLookup>::Source; /// An index of a bounty. Just a `u32`. pub type BountyIndex = u32; pub type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; pub type PaymentIdOf = <>::Paymaster as PayWithSource>::Id; /// Convenience alias for `Bounty`. pub type BountyOf = Bounty< ::AccountId, BalanceOf, >::AssetKind, ::Hash, PaymentIdOf, >::Beneficiary, >; /// Convenience alias for `ChildBounty`. pub type ChildBountyOf = ChildBounty< ::AccountId, BalanceOf, ::Hash, PaymentIdOf, >::Beneficiary, >; /// A funded bounty. #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub struct Bounty { /// The kind of asset this bounty is rewarded in. pub asset_kind: AssetKind, /// The amount that should be paid if the bounty is rewarded, including /// beneficiary payout and possible child bounties. /// /// The asset class determined by `asset_kind`. pub value: Balance, /// The metadata concerning the bounty. /// /// The `Hash` refers to the preimage of the `Preimages` provider which can be a JSON /// dump or IPFS hash of a JSON file. pub metadata: Hash, /// The status of this bounty. pub status: BountyStatus, } /// A funded child-bounty. #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub struct ChildBounty { /// The parent bounty index of this child-bounty. pub parent_bounty: BountyIndex, /// The amount that should be paid if the child-bounty is rewarded. /// /// The asset class determined by the parent bounty `asset_kind`. pub value: Balance, /// The metadata concerning the child-bounty. /// /// The `Hash` refers to the preimage of the `Preimages` provider which can be a JSON /// dump or IPFS hash of a JSON file. pub metadata: Hash, /// The status of this child-bounty. pub status: BountyStatus, } /// The status of a child-/bounty proposal. #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub enum BountyStatus { /// The child-/bounty funding has been attempted and is waiting to confirm the funds /// allocation. /// /// Call `check_status` to confirm whether the funding payment succeeded. If successful, the /// child-/bounty transitions to [`BountyStatus::Funded`]. Otherwise, use `retry_payment` to /// reinitiate the funding payment. FundingAttempted { /// The proposed curator of this child-/bounty. curator: AccountId, /// The funding payment status from the source (e.g. Treasury, parent bounty) to /// the child-/bounty account/location. payment_status: PaymentState, }, /// The child-/bounty is funded and waiting for curator to accept role. Funded { /// The proposed curator of this child-/bounty. curator: AccountId, }, /// The child-/bounty previously assigned curator has been unassigned. /// /// It remains funded and is waiting for a curator proposal. CuratorUnassigned, /// The child-/bounty is active and waiting to be awarded. /// /// During the `Active` state, the curator can call `fund_child_bounty` to create multiple /// child bounties. Active { /// The curator of this child-/bounty. curator: AccountId, }, /// The child-/bounty is closed, and the funds are being refunded to the original source (e.g., /// Treasury). Once `check_status` confirms the payment succeeded, the child-/bounty is /// finalized and removed from storage. Otherwise, use `retry_payment` to reinitiate the refund /// payment. RefundAttempted { /// The curator of this child-/bounty. /// /// If `None`, it means the child-/bounty curator was unassigned. curator: Option, /// The refund payment status from the child-/bounty account/location to the source (e.g. /// Treasury, parent bounty). payment_status: PaymentState, }, /// The child-/bounty payout to a beneficiary has been attempted. /// /// Call `check_status` to confirm whether the payout payment succeeded. If successful, the /// child-/bounty is finalized and removed from storage. Otherwise, use `retry_payment` to /// reinitiate the payout payment. PayoutAttempted { /// The curator of this child-/bounty. curator: AccountId, /// The beneficiary stash account/location. beneficiary: Beneficiary, /// The payout payment status from the child-/bounty account/location to the beneficiary. payment_status: PaymentState, }, } /// The state of a single payment. /// /// When a payment is initiated via `Paymaster::pay`, it begins in the `Pending` state. The /// `check_status` call updates the payment state and advances the child-/bounty status. The /// `retry_payment` call can be used to reattempt payments in either `Pending` or `Failed` states. #[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)] pub enum PaymentState { /// Pending claim. Pending, /// Payment attempted with a payment identifier. Attempted { id: Id }, /// Payment failed. Failed, /// Payment succeeded. Succeeded, } impl PaymentState { /// Used to check if payment can be retried. pub fn is_pending_or_failed(&self) -> bool { matches!(self, PaymentState::Pending | PaymentState::Failed) } /// If a payment has been initiated, returns its identifier, which is used to check its /// status. pub fn get_attempt_id(&self) -> Option { match self { PaymentState::Attempted { id } => Some(id.clone()), _ => None, } } } #[pezframe_support::pezpallet] pub mod pezpallet { use super::*; const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); #[pezpallet::pezpallet] #[pezpallet::storage_version(STORAGE_VERSION)] pub struct Pezpallet(_); #[pezpallet::config] pub trait Config: pezframe_system::Config { /// The type in which the assets are measured. type Balance: Balance; /// Origin from which rejections must come. type RejectOrigin: EnsureOrigin; /// The origin required for funding the bounty. The `Success` value is the maximum amount in /// a native asset that this origin is allowed to spend at a time. type SpendOrigin: EnsureOrigin>; /// Type parameter representing the asset kinds used to fund, refund and spend from /// bounties. type AssetKind: Parameter + MaxEncodedLen; /// Type parameter used to identify the beneficiaries eligible to receive payments. type Beneficiary: Parameter + MaxEncodedLen; /// Converting trait to take a source type and convert to [`Self::Beneficiary`]. type BeneficiaryLookup: StaticLookup; /// Minimum value for a bounty. #[pezpallet::constant] type BountyValueMinimum: Get>; /// Minimum value for a child-bounty. #[pezpallet::constant] type ChildBountyValueMinimum: Get>; /// Maximum number of child bounties that can be added to a parent bounty. #[pezpallet::constant] type MaxActiveChildBountyCount: Get; /// Weight information for extrinsics in this pezpallet. type WeightInfo: WeightInfo; /// Converts an `AssetKind` into the funding source account/location. /// /// Used when initiating funding and refund payments to and from a bounty. type FundingSource: TryConvert< Self::AssetKind, <>::Paymaster as PayWithSource>::Source, >; /// Converts a bounty index and `AssetKind` into its account/location. /// /// Used when initiating the funding, refund, and payout payments to and from a bounty. type BountySource: TryConvert< (BountyIndex, Self::AssetKind), <>::Paymaster as PayWithSource>::Source, >; /// Converts a parent bounty index, child bounty index, and `AssetKind` into the /// child-bounty account/location. /// /// Used when initiating the funding, refund, and payout payments to and from a /// child-bounty. type ChildBountySource: TryConvert< (BountyIndex, BountyIndex, Self::AssetKind), <>::Paymaster as PayWithSource>::Source, >; /// Type for processing payments of [`Self::AssetKind`] from a `Source` in favor of /// [`Self::Beneficiary`]. type Paymaster: PayWithSource< Balance = Self::Balance, Source = Self::Beneficiary, Beneficiary = Self::Beneficiary, AssetKind = Self::AssetKind, >; /// Type for converting the balance of an [`Self::AssetKind`] to the balance of the native /// asset, solely for the purpose of asserting the result against the maximum allowed spend /// amount of the [`Self::SpendOrigin`]. /// /// The conversion from the native asset balance to the balance of an [`Self::AssetKind`] is /// used in benchmarks to convert [`Self::BountyValueMinimum`] to the asset kind amount. type BalanceConverter: ConversionFromAssetBalance> + ConversionToAssetBalance, Self::AssetKind, Self::Balance>; /// The preimage provider used for child-/bounty metadata. type Preimages: QueryPreimage + StorePreimage; /// Means of associating a cost with committing to the curator role, which is incurred by /// the child-/bounty curator. /// /// The footprint accounts for the child-/bounty value in the native asset (returned in the /// `Success` type of [`Self::SpendOrigin`]). The cost taken from the curator `AccountId` /// may vary based on this balance. type Consideration: Consideration; /// Helper type for benchmarks. #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper: benchmarking::ArgumentsFactory< Self::AssetKind, Self::Beneficiary, BalanceOf, >; } #[pezpallet::error] pub enum Error { /// No child-/bounty at that index. InvalidIndex, /// The reason given is just too big. ReasonTooBig, /// Invalid child-/bounty value. InvalidValue, /// The balance of the asset kind is not convertible to the balance of the native asset for /// asserting the origin permissions. FailedToConvertBalance, /// The child-/bounty status is unexpected. UnexpectedStatus, /// Require child-/bounty curator. RequireCurator, /// The spend origin is valid but the amount it is allowed to spend is lower than the /// requested amount. InsufficientPermission, /// There was issue with funding the child-/bounty. FundingError, /// There was issue with refunding the child-/bounty. RefundError, // There was issue paying out the child-/bounty. PayoutError, /// Child-/bounty funding has not concluded yet. FundingInconclusive, /// Child-/bounty refund has not concluded yet. RefundInconclusive, /// Child-/bounty payout has not concluded yet. PayoutInconclusive, /// The child-/bounty or funding source account could not be derived from the indexes and /// asset kind. FailedToConvertSource, /// The parent bounty cannot be closed because it has active child bounties. HasActiveChildBounty, /// Number of child bounties exceeds limit `MaxActiveChildBountyCount`. TooManyChildBounties, /// The parent bounty value is not enough to add new child-bounty. InsufficientBountyValue, /// The preimage does not exist. PreimageNotExist, } #[pezpallet::event] #[pezpallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event, I: 'static = ()> { /// A new bounty was created and funding has been initiated. BountyCreated { index: BountyIndex }, /// A new child-bounty was created and funding has been initiated. ChildBountyCreated { index: BountyIndex, child_index: BountyIndex }, /// The curator accepted role and child-/bounty became active. BountyBecameActive { index: BountyIndex, child_index: Option, curator: T::AccountId, }, /// A child-/bounty was awarded to a beneficiary. BountyAwarded { index: BountyIndex, child_index: Option, beneficiary: T::Beneficiary, }, /// Payout payment to the beneficiary has concluded successfully. BountyPayoutProcessed { index: BountyIndex, child_index: Option, asset_kind: T::AssetKind, value: BalanceOf, beneficiary: T::Beneficiary, }, /// Funding payment has concluded successfully. BountyFundingProcessed { index: BountyIndex, child_index: Option }, /// Refund payment has concluded successfully. BountyRefundProcessed { index: BountyIndex, child_index: Option }, /// A child-/bounty was cancelled. BountyCanceled { index: BountyIndex, child_index: Option }, /// A child-/bounty curator was unassigned. CuratorUnassigned { index: BountyIndex, child_index: Option }, /// A child-/bounty curator was proposed. CuratorProposed { index: BountyIndex, child_index: Option, curator: T::AccountId, }, /// A payment failed and can be retried. PaymentFailed { index: BountyIndex, child_index: Option, payment_id: PaymentIdOf, }, /// A payment happened and can be checked. Paid { index: BountyIndex, child_index: Option, payment_id: PaymentIdOf }, } /// A reason for this pezpallet placing a hold on funds. #[pezpallet::composite_enum] pub enum HoldReason { /// The funds are held as deposit for the curator commitment to a bounty. #[codec(index = 0)] CuratorDeposit, } /// Number of bounty proposals that have been made. #[pezpallet::storage] pub type BountyCount, I: 'static = ()> = StorageValue<_, BountyIndex, ValueQuery>; /// Bounties that have been made. #[pezpallet::storage] pub type Bounties, I: 'static = ()> = StorageMap<_, Twox64Concat, BountyIndex, BountyOf>; /// Child bounties that have been added. /// /// Indexed by `(parent_bounty_id, child_bounty_id)`. #[pezpallet::storage] pub type ChildBounties, I: 'static = ()> = StorageDoubleMap< _, Twox64Concat, BountyIndex, Twox64Concat, BountyIndex, ChildBountyOf, >; /// Number of active child bounties per parent bounty. /// /// Indexed by `parent_bounty_id`. #[pezpallet::storage] pub type ChildBountiesPerParent, I: 'static = ()> = StorageMap<_, Twox64Concat, BountyIndex, u32, ValueQuery>; /// Number of total child bounties per parent bounty, including completed bounties. /// /// Indexed by `parent_bounty_id`. #[pezpallet::storage] pub type TotalChildBountiesPerParent, I: 'static = ()> = StorageMap<_, Twox64Concat, BountyIndex, u32, ValueQuery>; /// The cumulative child-bounty value for each parent bounty. To be subtracted from the parent /// bounty payout when awarding bounty. /// /// Indexed by `parent_bounty_id`. #[pezpallet::storage] pub type ChildBountiesValuePerParent, I: 'static = ()> = StorageMap<_, Twox64Concat, BountyIndex, BalanceOf, ValueQuery>; /// The consideration cost incurred by the child-/bounty curator for committing to the role. /// /// Determined by [`pezpallet::Config::Consideration`]. It is created when the curator accepts /// the role, and is either burned if the curator misbehaves or consumed upon successful /// completion of the child-/bounty. /// /// Note: If the parent curator is also assigned to the child-bounty, /// the consideration cost is charged only once — when the curator /// accepts the role for the parent bounty. /// /// Indexed by `(parent_bounty_id, child_bounty_id)`. #[pezpallet::storage] pub type CuratorDeposit, I: 'static = ()> = StorageDoubleMap< _, Twox64Concat, BountyIndex, Twox64Concat, Option, T::Consideration, >; /// Temporarily tracks spending limits within the current context to prevent overspending. #[derive(Default)] pub struct SpendContext { pub spend_in_context: BTreeMap, } #[pezpallet::call] impl, I: 'static> Pezpallet { /// Fund a new bounty with a proposed curator, initiating the payment from the /// funding source to the bounty account/location. /// /// ## Dispatch Origin /// /// Must be [`Config::SpendOrigin`] with the `Success` value being at least /// the converted native amount of the bounty. The bounty value is validated /// against the maximum spendable amount of the [`Config::SpendOrigin`]. /// /// ## Details /// /// - The `SpendOrigin` must have sufficient permissions to fund the bounty. /// - In case of a funding failure, the bounty status must be updated with the /// `check_status` call before retrying with `retry_payment` call. /// /// ### Parameters /// - `asset_kind`: An indicator of the specific asset class to be funded. /// - `value`: The total payment amount of this bounty. /// - `curator`: Address of bounty curator. /// - `metadata`: The hash of an on-chain stored preimage with bounty metadata. /// /// ## Events /// /// Emits [`Event::BountyCreated`] and [`Event::Paid`] if successful. #[pezpallet::call_index(0)] #[pezpallet::weight(>::WeightInfo::fund_bounty())] pub fn fund_bounty( origin: OriginFor, asset_kind: Box, #[pezpallet::compact] value: BalanceOf, curator: AccountIdLookupOf, metadata: T::Hash, ) -> DispatchResult { let max_amount = T::SpendOrigin::ensure_origin(origin)?; let curator = T::Lookup::lookup(curator)?; ensure!(T::Preimages::len(&metadata).is_some(), Error::::PreimageNotExist); let native_amount = T::BalanceConverter::from_asset_balance(value, *asset_kind.clone()) .map_err(|_| Error::::FailedToConvertBalance)?; ensure!(native_amount >= T::BountyValueMinimum::get(), Error::::InvalidValue); ensure!(native_amount <= max_amount, Error::::InsufficientPermission); with_context::>, _>(|v| { let context = v.or_default(); let funding = context.spend_in_context.entry(max_amount).or_default(); if funding.checked_add(&native_amount).map(|s| s > max_amount).unwrap_or(true) { Err(Error::::InsufficientPermission) } else { *funding = funding.saturating_add(native_amount); Ok(()) } }) .unwrap_or(Ok(()))?; let index = BountyCount::::get(); let payment_status = Self::do_process_funding_payment(index, None, *asset_kind.clone(), value, None)?; let bounty = BountyOf:: { asset_kind: *asset_kind, value, metadata, status: BountyStatus::FundingAttempted { curator, payment_status }, }; Bounties::::insert(index, &bounty); T::Preimages::request(&metadata); BountyCount::::put(index + 1); Self::deposit_event(Event::::BountyCreated { index }); Ok(()) } /// Fund a new child-bounty with a proposed curator, initiating the payment from the parent /// bounty to the child-bounty account/location. /// /// ## Dispatch Origin /// /// Must be signed by the parent curator. /// /// ## Details /// /// - If `curator` is not provided, the child-bounty will default to using the parent /// curator, allowing the parent curator to immediately call `check_status` and /// `award_bounty` to payout the child-bounty. /// - In case of a funding failure, the child-/bounty status must be updated with the /// `check_status` call before retrying with `retry_payment` call. /// /// ### Parameters /// - `parent_bounty_id`: Index of parent bounty for which child-bounty is being added. /// - `value`: The payment amount of this child-bounty. /// - `curator`: Address of child-bounty curator. /// - `metadata`: The hash of an on-chain stored preimage with child-bounty metadata. /// /// ## Events /// /// Emits [`Event::ChildBountyCreated`] and [`Event::Paid`] if successful. #[pezpallet::call_index(1)] #[pezpallet::weight(>::WeightInfo::fund_child_bounty())] pub fn fund_child_bounty( origin: OriginFor, #[pezpallet::compact] parent_bounty_id: BountyIndex, #[pezpallet::compact] value: BalanceOf, curator: Option>, metadata: T::Hash, ) -> DispatchResult { let signer = ensure_signed(origin)?; ensure!(T::Preimages::len(&metadata).is_some(), Error::::PreimageNotExist); let (asset_kind, parent_value, _, _, parent_curator) = Self::get_bounty_details(parent_bounty_id, None) .map_err(|_| Error::::InvalidIndex)?; let native_amount = T::BalanceConverter::from_asset_balance(value, asset_kind.clone()) .map_err(|_| Error::::FailedToConvertBalance)?; ensure!( native_amount >= T::ChildBountyValueMinimum::get(), Error::::InvalidValue ); ensure!( ChildBountiesPerParent::::get(parent_bounty_id) < T::MaxActiveChildBountyCount::get() as u32, Error::::TooManyChildBounties, ); // Parent bounty must be `Active` with a curator assigned. let parent_curator = parent_curator.ok_or(Error::::UnexpectedStatus)?; let final_curator = match curator { Some(curator) => T::Lookup::lookup(curator)?, None => parent_curator.clone(), }; ensure!(signer == parent_curator, Error::::RequireCurator); // Check value let child_bounties_value = ChildBountiesValuePerParent::::get(parent_bounty_id); let remaining_parent_value = parent_value.saturating_sub(child_bounties_value); ensure!(remaining_parent_value >= value, Error::::InsufficientBountyValue); // Get child-bounty ID. let child_bounty_id = TotalChildBountiesPerParent::::get(parent_bounty_id); // Initiate funding payment let payment_status = Self::do_process_funding_payment( parent_bounty_id, Some(child_bounty_id), asset_kind, value, None, )?; let child_bounty = ChildBounty { parent_bounty: parent_bounty_id, value, metadata, status: BountyStatus::FundingAttempted { curator: final_curator, payment_status: payment_status.clone(), }, }; ChildBounties::::insert(parent_bounty_id, child_bounty_id, child_bounty); T::Preimages::request(&metadata); // Add child-bounty value to the cumulative value sum. To be // subtracted from the parent bounty payout when awarding // bounty. ChildBountiesValuePerParent::::mutate(parent_bounty_id, |children_value| { *children_value = children_value.saturating_add(value) }); // Increment the active child-bounty count. ChildBountiesPerParent::::mutate(parent_bounty_id, |count| { count.saturating_inc() }); TotalChildBountiesPerParent::::insert( parent_bounty_id, child_bounty_id.saturating_add(1), ); Self::deposit_event(Event::::ChildBountyCreated { index: parent_bounty_id, child_index: child_bounty_id, }); Ok(()) } /// Propose a new curator for a child-/bounty after the previous was unassigned. /// /// ## Dispatch Origin /// /// Must be signed by `T::SpendOrigin` for a bounty, or by the parent bounty curator /// for a child-bounty. /// /// ## Details /// /// - The child-/bounty must be in the `CuratorUnassigned` state. /// - For a bounty, the `SpendOrigin` must have sufficient permissions to propose the /// curator. /// /// ### Parameters /// - `parent_bounty_id`: Index of bounty. /// - `child_bounty_id`: Index of child-bounty. /// - `curator`: Account to be proposed as the curator. /// /// ## Events /// /// Emits [`Event::CuratorProposed`] if successful. #[pezpallet::call_index(2)] #[pezpallet::weight(match child_bounty_id { None => >::WeightInfo::propose_curator_parent_bounty(), Some(_) => >::WeightInfo::propose_curator_child_bounty(), })] pub fn propose_curator( origin: OriginFor, #[pezpallet::compact] parent_bounty_id: BountyIndex, child_bounty_id: Option, curator: AccountIdLookupOf, ) -> DispatchResult { let maybe_sender = ensure_signed(origin.clone()) .map(Some) .or_else(|_| T::SpendOrigin::ensure_origin(origin.clone()).map(|_| None))?; let curator = T::Lookup::lookup(curator)?; let (asset_kind, value, _, status, parent_curator) = Self::get_bounty_details(parent_bounty_id, child_bounty_id)?; ensure!(status == BountyStatus::CuratorUnassigned, Error::::UnexpectedStatus); match child_bounty_id { // Only `SpendOrigin` can propose curator for bounty None => { ensure!(maybe_sender.is_none(), BadOrigin); let max_amount = T::SpendOrigin::ensure_origin(origin)?; let native_amount = T::BalanceConverter::from_asset_balance(value, asset_kind) .map_err(|_| Error::::FailedToConvertBalance)?; ensure!(native_amount <= max_amount, Error::::InsufficientPermission); }, // Only parent curator can propose curator for child-bounty Some(_) => { let parent_curator = parent_curator.ok_or(Error::::UnexpectedStatus)?; let sender = maybe_sender.ok_or(BadOrigin)?; ensure!(sender == parent_curator, BadOrigin); }, }; let new_status = BountyStatus::Funded { curator: curator.clone() }; Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?; Self::deposit_event(Event::::CuratorProposed { index: parent_bounty_id, child_index: child_bounty_id, curator, }); Ok(()) } /// Accept the curator role for a child-/bounty. /// /// ## Dispatch Origin /// /// Must be signed by the proposed curator. /// /// ## Details /// /// - The child-/bounty must be in the `Funded` state. /// - The curator must accept the role by calling this function. /// - A deposit will be reserved from the curator and refunded upon successful payout. /// /// ### Parameters /// - `parent_bounty_id`: Index of parent bounty. /// - `child_bounty_id`: Index of child-bounty. /// /// ## Events /// /// Emits [`Event::BountyBecameActive`] if successful. #[pezpallet::call_index(3)] #[pezpallet::weight(>::WeightInfo::accept_curator())] pub fn accept_curator( origin: OriginFor, #[pezpallet::compact] parent_bounty_id: BountyIndex, child_bounty_id: Option, ) -> DispatchResult { let signer = ensure_signed(origin)?; let (asset_kind, value, _, status, _) = Self::get_bounty_details(parent_bounty_id, child_bounty_id)?; let BountyStatus::Funded { ref curator } = status else { return Err(Error::::UnexpectedStatus.into()); }; ensure!(signer == *curator, Error::::RequireCurator); let native_amount = T::BalanceConverter::from_asset_balance(value, asset_kind) .map_err(|_| Error::::FailedToConvertBalance)?; let curator_deposit = T::Consideration::new(&curator, native_amount)?; CuratorDeposit::::insert(parent_bounty_id, child_bounty_id, curator_deposit); let new_status = BountyStatus::Active { curator: curator.clone() }; Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?; Self::deposit_event(Event::::BountyBecameActive { index: parent_bounty_id, child_index: child_bounty_id, curator: signer, }); Ok(()) } /// Unassign curator from a child-/bounty. /// /// ## Dispatch Origin /// /// This function can only be called by the `RejectOrigin` or the child-/bounty curator. /// /// ## Details /// /// - If this function is called by the `RejectOrigin`, or by the parent curator in the case /// of a child bounty, we assume that the curator is malicious or inactive. As a result, /// we will slash the curator when possible. /// - If the origin is the child-/bounty curator, we take this as a sign they are unable to /// do their job and they willingly give up. We could slash them, but for now we allow /// them to recover their deposit and exit without issue. (We may want to change this if /// it is abused). /// - If successful, the child-/bounty status is updated to `CuratorUnassigned`. To /// reactivate the bounty, a new curator must be proposed and must accept the role. /// /// ### Parameters /// - `parent_bounty_id`: Index of parent bounty. /// - `child_bounty_id`: Index of child-bounty. /// /// ## Events /// /// Emits [`Event::CuratorUnassigned`] if successful. #[pezpallet::call_index(4)] #[pezpallet::weight(>::WeightInfo::unassign_curator())] pub fn unassign_curator( origin: OriginFor, #[pezpallet::compact] parent_bounty_id: BountyIndex, child_bounty_id: Option, ) -> DispatchResult { let maybe_sender = ensure_signed(origin.clone()) .map(Some) .or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?; let (_, _, _, status, parent_curator) = Self::get_bounty_details(parent_bounty_id, child_bounty_id)?; match status { BountyStatus::Funded { ref curator } => { // A bounty curator has been proposed, but not accepted yet. // Either `RejectOrigin`, parent bounty curator or the proposed // curator can unassign the child-/bounty curator. ensure!( maybe_sender.map_or(true, |sender| { sender == *curator || parent_curator .map_or(false, |parent_curator| sender == parent_curator) }), BadOrigin ); }, BountyStatus::Active { ref curator, .. } => { let maybe_curator_deposit = CuratorDeposit::::take(parent_bounty_id, child_bounty_id); // The child-/bounty is active. match maybe_sender { // If the `RejectOrigin` is calling this function, burn the curator deposit. None => { if let Some(curator_deposit) = maybe_curator_deposit { T::Consideration::burn(curator_deposit, curator); } // Continue to change bounty status below... }, Some(sender) if sender == *curator => { if let Some(curator_deposit) = maybe_curator_deposit { // This is the curator, willingly giving up their role. Free their // deposit. T::Consideration::drop(curator_deposit, curator)?; } // Continue to change bounty status below... }, Some(sender) => { if let Some(parent_curator) = parent_curator { // If the parent curator is unassigning a child curator, that is not // itself, burn the child curator deposit. if sender == parent_curator && *curator != parent_curator { if let Some(curator_deposit) = maybe_curator_deposit { T::Consideration::burn(curator_deposit, curator); } } else { return Err(BadOrigin.into()); } } }, } }, _ => return Err(Error::::UnexpectedStatus.into()), }; let new_status = BountyStatus::CuratorUnassigned; Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?; Self::deposit_event(Event::::CuratorUnassigned { index: parent_bounty_id, child_index: child_bounty_id, }); Ok(()) } /// Awards the child-/bounty to a beneficiary account/location, /// initiating the payout payments to both the beneficiary and the curator. /// /// ## Dispatch Origin /// /// This function can only be called by the `RejectOrigin` or the child-/bounty curator. /// /// ## Details /// /// - The child-/bounty must be in the `Active` state. /// - if awarding a parent bounty it must not have active or funded child bounties. /// - Initiates payout payment from the child-/bounty to the beneficiary account/location. /// - If successful the child-/bounty status is updated to `PayoutAttempted`. /// - In case of a payout failure, the child-/bounty status must be updated with /// `check_status` call before retrying with `retry_payment` call. /// /// ### Parameters /// - `parent_bounty_id`: Index of parent bounty. /// - `child_bounty_id`: Index of child-bounty. /// - `beneficiary`: Account/location to be awarded the child-/bounty. /// /// ## Events /// /// Emits [`Event::BountyAwarded`] and [`Event::Paid`] if successful. #[pezpallet::call_index(5)] #[pezpallet::weight(>::WeightInfo::award_bounty())] pub fn award_bounty( origin: OriginFor, #[pezpallet::compact] parent_bounty_id: BountyIndex, child_bounty_id: Option, beneficiary: BeneficiaryLookupOf, ) -> DispatchResult { let signer = ensure_signed(origin)?; let beneficiary = T::BeneficiaryLookup::lookup(beneficiary)?; let (asset_kind, value, _, status, _) = Self::get_bounty_details(parent_bounty_id, child_bounty_id)?; if child_bounty_id.is_none() { ensure!( ChildBountiesPerParent::::get(parent_bounty_id) == 0, Error::::HasActiveChildBounty ); } let BountyStatus::Active { ref curator } = status else { return Err(Error::::UnexpectedStatus.into()); }; ensure!(signer == *curator, Error::::RequireCurator); let beneficiary_payment_status = Self::do_process_payout_payment( parent_bounty_id, child_bounty_id, asset_kind, value, beneficiary.clone(), None, )?; let new_status = BountyStatus::PayoutAttempted { curator: curator.clone(), beneficiary: beneficiary.clone(), payment_status: beneficiary_payment_status.clone(), }; Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?; Self::deposit_event(Event::::BountyAwarded { index: parent_bounty_id, child_index: child_bounty_id, beneficiary, }); Ok(()) } /// Cancel an active child-/bounty. A payment to send all the funds to the funding source is /// initialized. /// /// ## Dispatch Origin /// /// This function can only be called by the `RejectOrigin` or the parent bounty curator. /// /// ## Details /// /// - If the child-/bounty is in the `Funded` state, a refund payment is initiated. /// - If the child-/bounty is in the `Active` state, a refund payment is initiated and the /// child-/bounty status is updated with the curator account/location. /// - If the child-/bounty is in the funding or payout phase, it cannot be canceled. /// - In case of a refund failure, the child-/bounty status must be updated with the /// `check_status` call before retrying with `retry_payment` call. /// /// ### Parameters /// - `parent_bounty_id`: Index of parent bounty. /// - `child_bounty_id`: Index of child-bounty. /// /// ## Events /// /// Emits [`Event::BountyCanceled`] and [`Event::Paid`] if successful. #[pezpallet::call_index(6)] #[pezpallet::weight(match child_bounty_id { None => >::WeightInfo::close_parent_bounty(), Some(_) => >::WeightInfo::close_child_bounty(), })] pub fn close_bounty( origin: OriginFor, #[pezpallet::compact] parent_bounty_id: BountyIndex, child_bounty_id: Option, ) -> DispatchResult { let maybe_sender = ensure_signed(origin.clone()) .map(Some) .or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?; let (asset_kind, value, _, status, parent_curator) = Self::get_bounty_details(parent_bounty_id, child_bounty_id)?; let maybe_curator = match status { BountyStatus::Funded { curator } | BountyStatus::Active { curator, .. } => { Some(curator) }, BountyStatus::CuratorUnassigned => None, _ => return Err(Error::::UnexpectedStatus.into()), }; match child_bounty_id { None => { // Parent bounty can only be closed if it has no active child bounties. ensure!( ChildBountiesPerParent::::get(parent_bounty_id) == 0, Error::::HasActiveChildBounty ); // Bounty can be closed by `RejectOrigin` or the curator. if let Some(sender) = maybe_sender.as_ref() { let is_curator = maybe_curator.as_ref().map_or(false, |curator| curator == sender); ensure!(is_curator, BadOrigin); } }, Some(_) => { // Child-bounty can be closed by `RejectOrigin`, the curator or parent curator. if let Some(sender) = maybe_sender.as_ref() { let is_curator = maybe_curator.as_ref().map_or(false, |curator| curator == sender); let is_parent_curator = parent_curator .as_ref() .map_or(false, |parent_curator| parent_curator == sender); ensure!(is_curator || is_parent_curator, BadOrigin); } }, }; let payment_status = Self::do_process_refund_payment( parent_bounty_id, child_bounty_id, asset_kind, value, None, )?; let new_status = BountyStatus::RefundAttempted { payment_status: payment_status.clone(), curator: maybe_curator.clone(), }; Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?; Self::deposit_event(Event::::BountyCanceled { index: parent_bounty_id, child_index: child_bounty_id, }); Ok(()) } /// Check and update the payment status of a child-/bounty. /// /// ## Dispatch Origin /// /// Must be signed. /// /// ## Details /// /// - If the child-/bounty status is `FundingAttempted`, it checks if the funding payment /// has succeeded. If successful, the bounty status becomes `Funded`. /// - If the child-/bounty status is `RefundAttempted`, it checks if the refund payment has /// succeeded. If successful, the child-/bounty is removed from storage. /// - If the child-/bounty status is `PayoutAttempted`, it checks if the payout payment has /// succeeded. If successful, the child-/bounty is removed from storage. /// /// ### Parameters /// - `parent_bounty_id`: Index of parent bounty. /// - `child_bounty_id`: Index of child-bounty. /// /// ## Events /// /// Emits [`Event::BountyBecameActive`] if the child/bounty status transitions to `Active`. /// Emits [`Event::BountyRefundProcessed`] if the refund payment has succeed. /// Emits [`Event::BountyPayoutProcessed`] if the payout payment has succeed. /// Emits [`Event::PaymentFailed`] if the funding, refund our payment payment has failed. #[pezpallet::call_index(7)] #[pezpallet::weight(>::WeightInfo::check_status_funding().max( >::WeightInfo::check_status_refund(), ).max(>::WeightInfo::check_status_payout()))] pub fn check_status( origin: OriginFor, #[pezpallet::compact] parent_bounty_id: BountyIndex, child_bounty_id: Option, ) -> DispatchResultWithPostInfo { use BountyStatus::*; ensure_signed(origin)?; let (asset_kind, value, metadata, status, parent_curator) = Self::get_bounty_details(parent_bounty_id, child_bounty_id)?; let (new_status, weight) = match status { FundingAttempted { ref payment_status, curator } => { let new_payment_status = Self::do_check_funding_payment_status( parent_bounty_id, child_bounty_id, payment_status.clone(), )?; let new_status = match new_payment_status { PaymentState::Succeeded => match (child_bounty_id, parent_curator) { (Some(_), Some(parent_curator)) if curator == parent_curator => { BountyStatus::Active { curator } }, _ => BountyStatus::Funded { curator }, }, PaymentState::Pending | PaymentState::Failed | PaymentState::Attempted { .. } => BountyStatus::FundingAttempted { payment_status: new_payment_status, curator, }, }; let weight = >::WeightInfo::check_status_funding(); (new_status, weight) }, RefundAttempted { ref payment_status, ref curator } => { let new_payment_status = Self::do_check_refund_payment_status( parent_bounty_id, child_bounty_id, payment_status.clone(), )?; let new_status = match new_payment_status { PaymentState::Succeeded => { if let Some(curator) = curator { // Drop the curator deposit when payment succeeds // If the parent curator is also the child curator, there // is no deposit if let Some(curator_deposit) = CuratorDeposit::::take(parent_bounty_id, child_bounty_id) { T::Consideration::drop(curator_deposit, curator)?; } } if let Some(_) = child_bounty_id { // Revert the value back to parent bounty ChildBountiesValuePerParent::::mutate( parent_bounty_id, |total_value| *total_value = total_value.saturating_sub(value), ); } // refund succeeded, cleanup the bounty Self::remove_bounty(parent_bounty_id, child_bounty_id, metadata); return Ok(Pays::No.into()); }, PaymentState::Pending | PaymentState::Failed | PaymentState::Attempted { .. } => BountyStatus::RefundAttempted { payment_status: new_payment_status, curator: curator.clone(), }, }; let weight = >::WeightInfo::check_status_refund(); (new_status, weight) }, PayoutAttempted { ref curator, ref beneficiary, ref payment_status } => { let new_payment_status = Self::do_check_payout_payment_status( parent_bounty_id, child_bounty_id, asset_kind, value, beneficiary.clone(), payment_status.clone(), )?; let new_status = match new_payment_status { PaymentState::Succeeded => { if let Some(curator_deposit) = CuratorDeposit::::take(parent_bounty_id, child_bounty_id) { // Drop the curator deposit when both payments succeed // If the child curator is the parent curator, the // deposit is 0 T::Consideration::drop(curator_deposit, curator)?; } // payout succeeded, cleanup the bounty Self::remove_bounty(parent_bounty_id, child_bounty_id, metadata); return Ok(Pays::No.into()); }, PaymentState::Pending | PaymentState::Failed | PaymentState::Attempted { .. } => BountyStatus::PayoutAttempted { curator: curator.clone(), beneficiary: beneficiary.clone(), payment_status: new_payment_status.clone(), }, }; let weight = >::WeightInfo::check_status_payout(); (new_status, weight) }, _ => return Err(Error::::UnexpectedStatus.into()), }; Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?; Ok(Some(weight).into()) } /// Retry the funding, refund or payout payments. /// /// ## Dispatch Origin /// /// Must be signed. /// /// ## Details /// /// - If the child-/bounty status is `FundingAttempted`, it retries the funding payment from /// funding source the child-/bounty account/location. /// - If the child-/bounty status is `RefundAttempted`, it retries the refund payment from /// the child-/bounty account/location to the funding source. /// - If the child-/bounty status is `PayoutAttempted`, it retries the payout payment from /// the child-/bounty account/location to the beneficiary account/location. /// /// ### Parameters /// - `parent_bounty_id`: Index of parent bounty. /// - `child_bounty_id`: Index of child-bounty. /// /// ## Events /// /// Emits [`Event::Paid`] if the funding, refund or payout payment has initiated. #[pezpallet::call_index(8)] #[pezpallet::weight(>::WeightInfo::retry_payment_funding().max( >::WeightInfo::retry_payment_refund(), ).max(>::WeightInfo::retry_payment_payout()))] pub fn retry_payment( origin: OriginFor, #[pezpallet::compact] parent_bounty_id: BountyIndex, child_bounty_id: Option, ) -> DispatchResultWithPostInfo { use BountyStatus::*; ensure_signed(origin)?; let (asset_kind, value, _, status, _) = Self::get_bounty_details(parent_bounty_id, child_bounty_id)?; let (new_status, weight) = match status { FundingAttempted { ref payment_status, ref curator } => { let new_payment_status = Self::do_process_funding_payment( parent_bounty_id, child_bounty_id, asset_kind, value, Some(payment_status.clone()), )?; ( FundingAttempted { payment_status: new_payment_status, curator: curator.clone(), }, >::WeightInfo::retry_payment_funding(), ) }, RefundAttempted { ref curator, ref payment_status } => { let new_payment_status = Self::do_process_refund_payment( parent_bounty_id, child_bounty_id, asset_kind, value, Some(payment_status.clone()), )?; ( RefundAttempted { curator: curator.clone(), payment_status: new_payment_status, }, >::WeightInfo::retry_payment_refund(), ) }, PayoutAttempted { ref curator, ref beneficiary, ref payment_status } => { let new_payment_status = Self::do_process_payout_payment( parent_bounty_id, child_bounty_id, asset_kind, value, beneficiary.clone(), Some(payment_status.clone()), )?; ( PayoutAttempted { curator: curator.clone(), beneficiary: beneficiary.clone(), payment_status: new_payment_status, }, >::WeightInfo::retry_payment_payout(), ) }, _ => return Err(Error::::UnexpectedStatus.into()), }; Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?; Ok(Some(weight).into()) } } #[pezpallet::hooks] impl, I: 'static> Hooks> for Pezpallet { #[cfg(feature = "try-runtime")] fn try_state(_n: SystemBlockNumberFor) -> Result<(), pezsp_runtime::TryRuntimeError> { Self::do_try_state() } } } #[cfg(any(feature = "try-runtime", test))] impl, I: 'static> Pezpallet { /// Ensure the correctness of the state of this pezpallet. /// /// This should be valid before or after each state transition of this pezpallet. pub fn do_try_state() -> Result<(), pezsp_runtime::TryRuntimeError> { Self::try_state_bounties_count()?; for parent_bounty_id in Bounties::::iter_keys() { Self::try_state_child_bounties_count(parent_bounty_id)?; } Ok(()) } /// # Bounty Invariants /// /// * `BountyCount` should be greater or equals to the length of the number of items in /// `Bounties`. fn try_state_bounties_count() -> Result<(), pezsp_runtime::TryRuntimeError> { let bounties_length = Bounties::::iter().count() as u32; ensure!( >::get() >= bounties_length, "`BountyCount` must be grater or equals the number of `Bounties` in storage" ); Ok(()) } /// # Child-Bounty Invariants for a given parent bounty /// /// * `ChildBountyCount` should be greater or equals to the length of the number of items in /// `ChildBounties`. fn try_state_child_bounties_count( parent_bounty_id: BountyIndex, ) -> Result<(), pezsp_runtime::TryRuntimeError> { let child_bounties_length = ChildBounties::::iter_prefix(parent_bounty_id).count() as u32; ensure!( >::get(parent_bounty_id) >= child_bounties_length, "`ChildBountiesPerParent` must be grater or equals the number of `ChildBounties` in storage" ); Ok(()) } } impl, I: 'static> Pezpallet { /// The account/location of the funding source. pub fn funding_source_account( asset_kind: T::AssetKind, ) -> Result { T::FundingSource::try_convert(asset_kind) .map_err(|_| Error::::FailedToConvertSource.into()) } /// The account/location of a bounty. pub fn bounty_account( bounty_id: BountyIndex, asset_kind: T::AssetKind, ) -> Result { T::BountySource::try_convert((bounty_id, asset_kind)) .map_err(|_| Error::::FailedToConvertSource.into()) } /// The account/location of a child-bounty. pub fn child_bounty_account( parent_bounty_id: BountyIndex, child_bounty_id: BountyIndex, asset_kind: T::AssetKind, ) -> Result { T::ChildBountySource::try_convert((parent_bounty_id, child_bounty_id, asset_kind)) .map_err(|_| Error::::FailedToConvertSource.into()) } /// Returns the asset kind, value, status and parent curator (if parent bounty /// active) of a child-/bounty. /// /// The asset kind derives from the parent bounty. pub fn get_bounty_details( parent_bounty_id: BountyIndex, child_bounty_id: Option, ) -> Result< ( T::AssetKind, BalanceOf, T::Hash, BountyStatus, T::Beneficiary>, Option, ), DispatchError, > { let parent_bounty = Bounties::::get(parent_bounty_id).ok_or(Error::::InvalidIndex)?; // Ensures child-bounty uses parent curator only when parent bounty is active. let parent_curator = if let BountyStatus::Active { curator } = &parent_bounty.status { Some(curator.clone()) } else { None }; match child_bounty_id { None => Ok(( parent_bounty.asset_kind, parent_bounty.value, parent_bounty.metadata, parent_bounty.status, parent_curator, )), Some(child_bounty_id) => { let child_bounty = ChildBounties::::get(parent_bounty_id, child_bounty_id) .ok_or(Error::::InvalidIndex)?; Ok(( parent_bounty.asset_kind, child_bounty.value, child_bounty.metadata, child_bounty.status, parent_curator, )) }, } } /// Updates the status of a child-/bounty. pub fn update_bounty_status( parent_bounty_id: BountyIndex, child_bounty_id: Option, new_status: BountyStatus, T::Beneficiary>, ) -> Result<(), DispatchError> { match child_bounty_id { None => { let mut bounty = Bounties::::get(parent_bounty_id).ok_or(Error::::InvalidIndex)?; bounty.status = new_status; Bounties::::insert(parent_bounty_id, bounty); }, Some(child_bounty_id) => { let mut bounty = ChildBounties::::get(parent_bounty_id, child_bounty_id) .ok_or(Error::::InvalidIndex)?; bounty.status = new_status; ChildBounties::::insert(parent_bounty_id, child_bounty_id, bounty); }, } Ok(()) } /// Calculates amount the beneficiary receives during child-/bounty payout. fn calculate_payout( parent_bounty_id: BountyIndex, child_bounty_id: Option, value: BalanceOf, ) -> BalanceOf { match child_bounty_id { None => { // Get total child bounties value, and subtract it from the parent // value. let children_value = ChildBountiesValuePerParent::::take(parent_bounty_id); debug_assert!(children_value <= value); let payout = value.saturating_sub(children_value); payout }, Some(_) => value, } } /// Cleanup a child-/bounty from the storage. fn remove_bounty( parent_bounty_id: BountyIndex, child_bounty_id: Option, metadata: T::Hash, ) { match child_bounty_id { None => { Bounties::::remove(parent_bounty_id); ChildBountiesPerParent::::remove(parent_bounty_id); TotalChildBountiesPerParent::::remove(parent_bounty_id); debug_assert!(ChildBountiesValuePerParent::::get(parent_bounty_id).is_zero()); }, Some(child_bounty_id) => { ChildBounties::::remove(parent_bounty_id, child_bounty_id); ChildBountiesPerParent::::mutate(parent_bounty_id, |count| { count.saturating_dec() }); }, } T::Preimages::unrequest(&metadata); } /// Initiates payment from the funding source to the child-/bounty account/location. fn do_process_funding_payment( parent_bounty_id: BountyIndex, child_bounty_id: Option, asset_kind: T::AssetKind, value: BalanceOf, maybe_payment_status: Option>>, ) -> Result>, DispatchError> { if let Some(payment_status) = maybe_payment_status { ensure!(payment_status.is_pending_or_failed(), Error::::UnexpectedStatus); } let (source, beneficiary) = match child_bounty_id { None => ( Self::funding_source_account(asset_kind.clone())?, Self::bounty_account(parent_bounty_id, asset_kind.clone())?, ), Some(child_bounty_id) => ( Self::bounty_account(parent_bounty_id, asset_kind.clone())?, Self::child_bounty_account(parent_bounty_id, child_bounty_id, asset_kind.clone())?, ), }; let id = >::Paymaster::pay(&source, &beneficiary, asset_kind, value) .map_err(|_| Error::::FundingError)?; Self::deposit_event(Event::::Paid { index: parent_bounty_id, child_index: child_bounty_id, payment_id: id, }); Ok(PaymentState::Attempted { id }) } /// Queries the status of the payment from the funding source to the child-/bounty /// account/location and returns a new payment status. fn do_check_funding_payment_status( parent_bounty_id: BountyIndex, child_bounty_id: Option, payment_status: PaymentState>, ) -> Result>, DispatchError> { let payment_id = payment_status.get_attempt_id().ok_or(Error::::UnexpectedStatus)?; match >::Paymaster::check_payment(payment_id) { PaymentStatus::Success => { Self::deposit_event(Event::::BountyFundingProcessed { index: parent_bounty_id, child_index: child_bounty_id, }); Ok(PaymentState::Succeeded) }, PaymentStatus::InProgress | PaymentStatus::Unknown => { return Err(Error::::FundingInconclusive.into()) }, PaymentStatus::Failure => { Self::deposit_event(Event::::PaymentFailed { index: parent_bounty_id, child_index: child_bounty_id, payment_id, }); return Ok(PaymentState::Failed); }, } } /// Initializes payment from the child-/bounty account/location to the funding source (i.e. /// treasury pot, parent bounty). fn do_process_refund_payment( parent_bounty_id: BountyIndex, child_bounty_id: Option, asset_kind: T::AssetKind, value: BalanceOf, payment_status: Option>>, ) -> Result>, DispatchError> { if let Some(payment_status) = payment_status { ensure!(payment_status.is_pending_or_failed(), Error::::UnexpectedStatus); } let (source, beneficiary) = match child_bounty_id { None => ( Self::bounty_account(parent_bounty_id, asset_kind.clone())?, Self::funding_source_account(asset_kind.clone())?, ), Some(child_bounty_id) => ( Self::child_bounty_account(parent_bounty_id, child_bounty_id, asset_kind.clone())?, Self::bounty_account(parent_bounty_id, asset_kind.clone())?, ), }; let id = >::Paymaster::pay(&source, &beneficiary, asset_kind, value) .map_err(|_| Error::::RefundError)?; Self::deposit_event(Event::::Paid { index: parent_bounty_id, child_index: child_bounty_id, payment_id: id, }); Ok(PaymentState::Attempted { id }) } /// Queries the status of the refund payment from the child-/bounty account/location to the /// funding source and returns a new payment status. fn do_check_refund_payment_status( parent_bounty_id: BountyIndex, child_bounty_id: Option, payment_status: PaymentState>, ) -> Result>, DispatchError> { let payment_id = payment_status.get_attempt_id().ok_or(Error::::UnexpectedStatus)?; match >::Paymaster::check_payment(payment_id) { PaymentStatus::Success => { Self::deposit_event(Event::::BountyRefundProcessed { index: parent_bounty_id, child_index: child_bounty_id, }); Ok(PaymentState::Succeeded) }, PaymentStatus::InProgress | PaymentStatus::Unknown => // nothing new to report { Err(Error::::RefundInconclusive.into()) }, PaymentStatus::Failure => { // assume payment has failed, allow user to retry Self::deposit_event(Event::::PaymentFailed { index: parent_bounty_id, child_index: child_bounty_id, payment_id, }); Ok(PaymentState::Failed) }, } } /// Initializes payment from the child-/bounty to the beneficiary account/location. fn do_process_payout_payment( parent_bounty_id: BountyIndex, child_bounty_id: Option, asset_kind: T::AssetKind, value: BalanceOf, beneficiary: T::Beneficiary, payment_status: Option>>, ) -> Result>, DispatchError> { if let Some(payment_status) = payment_status { ensure!(payment_status.is_pending_or_failed(), Error::::UnexpectedStatus); } let payout = Self::calculate_payout(parent_bounty_id, child_bounty_id, value); let source = match child_bounty_id { None => Self::bounty_account(parent_bounty_id, asset_kind.clone())?, Some(child_bounty_id) => { Self::child_bounty_account(parent_bounty_id, child_bounty_id, asset_kind.clone())? }, }; let id = >::Paymaster::pay(&source, &beneficiary, asset_kind, payout) .map_err(|_| Error::::RefundError)?; Self::deposit_event(Event::::Paid { index: parent_bounty_id, child_index: child_bounty_id, payment_id: id, }); Ok(PaymentState::Attempted { id }) } /// Queries the status of the payment from the child-/bounty to the beneficiary account/location /// and returns a new payment status. fn do_check_payout_payment_status( parent_bounty_id: BountyIndex, child_bounty_id: Option, asset_kind: T::AssetKind, value: BalanceOf, beneficiary: T::Beneficiary, payment_status: PaymentState>, ) -> Result>, DispatchError> { let payment_id = payment_status.get_attempt_id().ok_or(Error::::UnexpectedStatus)?; match >::Paymaster::check_payment(payment_id) { PaymentStatus::Success => { let payout = Self::calculate_payout(parent_bounty_id, child_bounty_id, value); Self::deposit_event(Event::::BountyPayoutProcessed { index: parent_bounty_id, child_index: child_bounty_id, asset_kind: asset_kind.clone(), value: payout, beneficiary, }); Ok(PaymentState::Succeeded) }, PaymentStatus::InProgress | PaymentStatus::Unknown => // nothing new to report { Err(Error::::PayoutInconclusive.into()) }, PaymentStatus::Failure => { // assume payment has failed, allow user to retry Self::deposit_event(Event::::PaymentFailed { index: parent_bounty_id, child_index: child_bounty_id, payment_id, }); Ok(PaymentState::Failed) }, } } } /// Type implementing curator deposit as a percentage of the child-/bounty value. /// /// It implements `Convert` trait and can be used with types like `HoldConsideration` implementing /// `Consideration` trait. pub struct CuratorDepositAmount(PhantomData<(Mult, Min, Max, Balance)>); impl Convert for CuratorDepositAmount where Balance: pezframe_support::traits::tokens::Balance, Min: Get>, Max: Get>, Mult: Get, { fn convert(value: Balance) -> Balance { let mut deposit = Mult::get().mul_floor(value); if let Some(min) = Min::get() { if deposit < min { deposit = min; } } if let Some(max) = Max::get() { if deposit > max { deposit = max; } } deposit } } /// Derives the funding account used as the source of funds for bounties. /// /// Used when the [`PalletId`] itself owns the funds (i.e. pezpallet-treasury id). pub struct PalletIdAsFundingSource(PhantomData<(Id, T, I)>); impl TryConvert for PalletIdAsFundingSource where Id: Get, T: crate::Config, T::Beneficiary: From, { fn try_convert(_asset_kind: T::AssetKind) -> Result { let account = Id::get().into_account_truncating(); Ok(account) } } /// Derives the bounty account from its index. /// /// Used when the [`PalletId`] itself owns the funds (i.e. pezpallet-treasury id). pub struct BountySourceAccount(PhantomData<(Id, T, I)>); impl TryConvert<(BountyIndex, T::AssetKind), T::Beneficiary> for BountySourceAccount where Id: Get, T: crate::Config, T::Beneficiary: From, { fn try_convert( (parent_bounty_id, _asset_kind): (BountyIndex, T::AssetKind), ) -> Result { let account = Id::get().into_sub_account_truncating(("bt", parent_bounty_id)); Ok(account) } } /// Derives the child-bounty account from its index and the parent bounty index. /// /// Used when the [`PalletId`] itself owns the funds (i.e. pezpallet-treasury id). pub struct ChildBountySourceAccount(PhantomData<(Id, T, I)>); impl TryConvert<(BountyIndex, BountyIndex, T::AssetKind), T::Beneficiary> for ChildBountySourceAccount where Id: Get, T: crate::Config, T::Beneficiary: From, { fn try_convert( (parent_bounty_id, child_bounty_id, _asset_kind): (BountyIndex, BountyIndex, T::AssetKind), ) -> Result { // The prefix is changed to have different AccountId when the index of // parent and child is same. let account = Id::get().into_sub_account_truncating(("cb", parent_bounty_id, child_bounty_id)); Ok(account) } }