Newer
Older
// 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.
//! # Child Bounties Pallet ( `pallet-child-bounties` )
//! > NOTE: This pallet is tightly coupled with `pallet-treasury` and `pallet-bounties`.
//!
//! With child bounties, a large bounty proposal can be divided into smaller chunks,
//! for parallel execution, and for efficient governance and tracking of spent funds.
//! A child bounty is a smaller piece of work, extracted from a parent bounty.
//! A curator is assigned after the child bounty is created by the parent bounty curator,
//! to be delegated with the responsibility of assigning a payout address once the specified
//! set of tasks is completed.
//!
//! ## Interface
//!
//! ### Dispatchable Functions
//!
//! Child Bounty protocol:
//! - `add_child_bounty` - Add a child bounty for a parent bounty to for dividing the work in
//! - `propose_curator` - Assign an account to a child bounty as candidate curator.
//! - `accept_curator` - Accept a child bounty assignment from the parent bounty curator, setting a
//! curator deposit.
//! - `award_child_bounty` - Close and pay out the specified amount for the completed work.
//! - `claim_child_bounty` - Claim a specific child bounty amount from the payout address.
//! - `unassign_curator` - Unassign an accepted curator from a specific child bounty.
//! - `close_child_bounty` - Cancel the child bounty for a specific treasury amount and close the
//! bounty.
// Most of the business logic in this pallet has been
// originally contributed by "https://github.com/shamb0",
// as part of the PR - https://github.com/paritytech/substrate/pull/7965.
// The code has been moved here and then refactored in order to
// extract child bounties as a separate pallet.
#![cfg_attr(not(feature = "std"), no_std)]
mod benchmarking;
pub mod migration;
/// The log target for this pallet.
const LOG_TARGET: &str = "runtime::child-bounties";
use frame_support::traits::{
Currency,
ExistenceRequirement::{AllowDeath, KeepAlive},
Get, OnUnbalanced, ReservableCurrency, WithdrawReasons,
};
use sp_runtime::{
traits::{
AccountIdConversion, BadOrigin, BlockNumberProvider, CheckedSub, Saturating, StaticLookup,
Zero,
},
DispatchResult, RuntimeDebug,
};
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::{
ensure_signed, BlockNumberFor as SystemBlockNumberFor, OriginFor,
};
use pallet_bounties::BountyStatus;
use scale_info::TypeInfo;
pub use weights::WeightInfo;
pub use pallet::*;
pub type BalanceOf<T> = pallet_treasury::BalanceOf<T>;
pub type BountiesError<T> = pallet_bounties::Error<T>;
pub type BountyIndex = pallet_bounties::BountyIndex;
pub type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
pub type BlockNumberFor<T> =
<<T as pallet_treasury::Config>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
/// A child bounty proposal.
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct ChildBounty<AccountId, Balance, BlockNumber> {
/// The parent of this child-bounty.
pub parent_bounty: BountyIndex,
/// The (total) amount that should be paid if this child-bounty is rewarded.
/// The deposit of child-bounty curator.
/// The status of this child-bounty.
pub status: ChildBountyStatus<AccountId, BlockNumber>,
}
/// The status of a child-bounty.
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub enum ChildBountyStatus<AccountId, BlockNumber> {
/// The child-bounty is added and waiting for curator assignment.
Added,
/// A curator has been proposed by the parent bounty curator. Waiting for
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
/// acceptance from the child-bounty curator.
CuratorProposed {
/// The assigned child-bounty curator of this bounty.
curator: AccountId,
},
/// The child-bounty is active and waiting to be awarded.
Active {
/// The curator of this child-bounty.
curator: AccountId,
},
/// The child-bounty is awarded and waiting to released after a delay.
PendingPayout {
/// The curator of this child-bounty.
curator: AccountId,
/// The beneficiary of the child-bounty.
beneficiary: AccountId,
/// When the child-bounty can be claimed.
unlock_at: BlockNumber,
},
}
#[frame_support::pallet]
pub mod pallet {
use super::*;
/// The in-code storage version.
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config:
frame_system::Config + pallet_treasury::Config + pallet_bounties::Config
{
/// Maximum number of child bounties that can be added to a parent bounty.
#[pallet::constant]
type MaxActiveChildBountyCount: Get<u32>;
/// Minimum value for a child-bounty.
#[pallet::constant]
type ChildBountyValueMinimum: Get<BalanceOf<Self>>;
/// The overarching event type.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
}
#[pallet::error]
pub enum Error<T> {
/// The parent bounty is not in active state.
ParentBountyNotActive,
/// The bounty balance is not enough to add new child-bounty.
InsufficientBountyBalance,
/// Number of child bounties exceeds limit `MaxActiveChildBountyCount`.
TooManyChildBounties,
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// A child-bounty is added.
Added { index: BountyIndex, child_index: BountyIndex },
/// A child-bounty is awarded to a beneficiary.
Awarded { index: BountyIndex, child_index: BountyIndex, beneficiary: T::AccountId },
/// A child-bounty is claimed by beneficiary.
Claimed {
index: BountyIndex,
child_index: BountyIndex,
payout: BalanceOf<T>,
beneficiary: T::AccountId,
},
/// A child-bounty is cancelled.
Canceled { index: BountyIndex, child_index: BountyIndex },
}
/// DEPRECATED: Replaced with `ParentTotalChildBounties` storage item keeping dedicated counts
/// for each parent bounty. Number of total child bounties. Will be removed in May 2025.
#[pallet::storage]
pub type ChildBountyCount<T: Config> = StorageValue<_, BountyIndex, ValueQuery>;
/// Number of active child bounties per parent bounty.
/// Map of parent bounty index to number of child bounties.
#[pallet::storage]
pub type ParentChildBounties<T: Config> =
StorageMap<_, Twox64Concat, BountyIndex, u32, ValueQuery>;
/// Number of total child bounties per parent bounty, including completed bounties.
#[pallet::storage]
pub type ParentTotalChildBounties<T: Config> =
StorageMap<_, Twox64Concat, BountyIndex, u32, ValueQuery>;
#[pallet::storage]
pub type ChildBounties<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
BountyIndex,
Twox64Concat,
BountyIndex,
ChildBounty<T::AccountId, BalanceOf<T>, BlockNumberFor<T>>,
/// The description of each child-bounty. Indexed by `(parent_id, child_id)`.
///
/// This item replaces the `ChildBountyDescriptions` storage item from the V0 storage version.
#[pallet::storage]
pub type ChildBountyDescriptionsV1<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
BountyIndex,
Twox64Concat,
BountyIndex,
BoundedVec<u8, T::MaximumReasonLength>,
>;
/// The mapping of the child bounty ids from storage version `V0` to the new `V1` version.
///
/// The `V0` ids based on total child bounty count [`ChildBountyCount`]`. The `V1` version ids
/// based on the child bounty count per parent bounty [`ParentTotalChildBounties`].
/// The item intended solely for client convenience and not used in the pallet's core logic.
pub type V0ToV1ChildBountyIds<T: Config> =
StorageMap<_, Twox64Concat, BountyIndex, (BountyIndex, BountyIndex)>;
/// The cumulative child-bounty curator fee for each parent bounty.
#[pallet::storage]
pub type ChildrenCuratorFees<T: Config> =
StorageMap<_, Twox64Concat, BountyIndex, BalanceOf<T>, ValueQuery>;
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Add a new child-bounty.
///
/// The dispatch origin for this call must be the curator of parent
/// bounty and the parent bounty must be in "active" state.
///
/// Child-bounty gets added successfully & fund gets transferred from
/// parent bounty to child-bounty account, if parent bounty has enough
/// funds, else the call fails.
///
/// Upper bound to maximum number of active child bounties that can be
/// added are managed via runtime trait config
/// [`Config::MaxActiveChildBountyCount`].
///
/// If the call is success, the status of child-bounty is updated to
/// "Added".
///
/// - `parent_bounty_id`: Index of parent bounty for which child-bounty is being added.
/// - `value`: Value for executing the proposal.
/// - `description`: Text description for the child-bounty.
#[pallet::weight(<T as Config>::WeightInfo::add_child_bounty(description.len() as u32))]
pub fn add_child_bounty(
origin: OriginFor<T>,
#[pallet::compact] parent_bounty_id: BountyIndex,
#[pallet::compact] value: BalanceOf<T>,
description: Vec<u8>,
) -> DispatchResult {
let signer = ensure_signed(origin)?;
// Verify the arguments.
let bounded_description =
description.try_into().map_err(|_| BountiesError::<T>::ReasonTooBig)?;
ensure!(value >= T::ChildBountyValueMinimum::get(), BountiesError::<T>::InvalidValue);
ensure!(
polka.dom
committed
ParentChildBounties::<T>::get(parent_bounty_id) <=
T::MaxActiveChildBountyCount::get() as u32,
Error::<T>::TooManyChildBounties,
);
let (curator, _) = Self::ensure_bounty_active(parent_bounty_id)?;
ensure!(signer == curator, BountiesError::<T>::RequireCurator);
// Read parent bounty account info.
let parent_bounty_account =
pallet_bounties::Pallet::<T>::bounty_account_id(parent_bounty_id);
// Ensure parent bounty has enough balance after adding child-bounty.
let bounty_balance = T::Currency::free_balance(&parent_bounty_account);
let new_bounty_balance = bounty_balance
.checked_sub(&value)
.ok_or(Error::<T>::InsufficientBountyBalance)?;
T::Currency::ensure_can_withdraw(
&parent_bounty_account,
value,
WithdrawReasons::TRANSFER,
new_bounty_balance,
)?;
// Get child-bounty ID.
let child_bounty_id = ParentTotalChildBounties::<T>::get(parent_bounty_id);
let child_bounty_account =
Self::child_bounty_account_id(parent_bounty_id, child_bounty_id);
// Transfer funds from parent bounty to child-bounty.
T::Currency::transfer(&parent_bounty_account, &child_bounty_account, value, KeepAlive)?;
// Increment the active child-bounty count.
polka.dom
committed
ParentChildBounties::<T>::mutate(parent_bounty_id, |count| count.saturating_inc());
ParentTotalChildBounties::<T>::insert(
parent_bounty_id,
child_bounty_id.saturating_add(1),
);
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
// Create child-bounty instance.
Self::create_child_bounty(
parent_bounty_id,
child_bounty_id,
value,
bounded_description,
);
Ok(())
}
/// Propose curator for funded child-bounty.
///
/// The dispatch origin for this call must be curator of parent bounty.
///
/// Parent bounty must be in active state, for this child-bounty call to
/// work.
///
/// Child-bounty must be in "Added" state, for processing the call. And
/// state of child-bounty is moved to "CuratorProposed" on successful
/// call completion.
///
/// - `parent_bounty_id`: Index of parent bounty.
/// - `child_bounty_id`: Index of child bounty.
/// - `curator`: Address of child-bounty curator.
/// - `fee`: payment fee to child-bounty curator for execution.
#[pallet::weight(<T as Config>::WeightInfo::propose_curator())]
pub fn propose_curator(
origin: OriginFor<T>,
#[pallet::compact] parent_bounty_id: BountyIndex,
#[pallet::compact] child_bounty_id: BountyIndex,
curator: AccountIdLookupOf<T>,
#[pallet::compact] fee: BalanceOf<T>,
) -> DispatchResult {
let signer = ensure_signed(origin)?;
let child_bounty_curator = T::Lookup::lookup(curator)?;
let (curator, _) = Self::ensure_bounty_active(parent_bounty_id)?;
ensure!(signer == curator, BountiesError::<T>::RequireCurator);
// Mutate the child-bounty instance.
ChildBounties::<T>::try_mutate_exists(
parent_bounty_id,
child_bounty_id,
|maybe_child_bounty| -> DispatchResult {
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
maybe_child_bounty.as_mut().ok_or(BountiesError::<T>::InvalidIndex)?;
// Ensure child-bounty is in expected state.
ensure!(
child_bounty.status == ChildBountyStatus::Added,
BountiesError::<T>::UnexpectedStatus,
);
// Ensure child-bounty curator fee is less than child-bounty value.
ensure!(fee < child_bounty.value, BountiesError::<T>::InvalidFee);
// Add child-bounty curator fee to the cumulative sum. To be
// subtracted from the parent bounty curator when claiming
// bounty.
ChildrenCuratorFees::<T>::mutate(parent_bounty_id, |value| {
*value = value.saturating_add(fee)
});
// Update the child-bounty curator fee.
child_bounty.fee = fee;
// Update the child-bounty state.
child_bounty.status =
ChildBountyStatus::CuratorProposed { curator: child_bounty_curator };
Ok(())
},
)
}
/// Accept the curator role for the child-bounty.
///
/// The dispatch origin for this call must be the curator of this
/// child-bounty.
///
/// A deposit will be reserved from the curator and refund upon
/// successful payout or cancellation.
///
/// Fee for curator is deducted from curator fee of parent bounty.
///
/// Parent bounty must be in active state, for this child-bounty call to
/// work.
///
/// Child-bounty must be in "CuratorProposed" state, for processing the
/// call. And state of child-bounty is moved to "Active" on successful
/// call completion.
///
/// - `parent_bounty_id`: Index of parent bounty.
/// - `child_bounty_id`: Index of child bounty.
#[pallet::weight(<T as Config>::WeightInfo::accept_curator())]
pub fn accept_curator(
origin: OriginFor<T>,
#[pallet::compact] parent_bounty_id: BountyIndex,
#[pallet::compact] child_bounty_id: BountyIndex,
) -> DispatchResult {
let signer = ensure_signed(origin)?;
let (parent_curator, _) = Self::ensure_bounty_active(parent_bounty_id)?;
// Mutate child-bounty.
ChildBounties::<T>::try_mutate_exists(
parent_bounty_id,
child_bounty_id,
|maybe_child_bounty| -> DispatchResult {
maybe_child_bounty.as_mut().ok_or(BountiesError::<T>::InvalidIndex)?;
// Ensure child-bounty is in expected state.
if let ChildBountyStatus::CuratorProposed { ref curator } = child_bounty.status
{
ensure!(signer == *curator, BountiesError::<T>::RequireCurator);
// Reserve child-bounty curator deposit.
let deposit = Self::calculate_curator_deposit(
&parent_curator,
curator,
&child_bounty.fee,
);
T::Currency::reserve(curator, deposit)?;
child_bounty.curator_deposit = deposit;
child_bounty.status =
ChildBountyStatus::Active { curator: curator.clone() };
Ok(())
} else {
Err(BountiesError::<T>::UnexpectedStatus.into())
}
},
)
}
/// Unassign curator from a child-bounty.
///
/// The dispatch origin for this call can be either `RejectOrigin`, or
/// the curator of the parent bounty, or any signed origin.
///
/// For the origin other than T::RejectOrigin and the child-bounty
/// curator, parent bounty must be in active state, for this call to
/// work. We allow child-bounty curator and T::RejectOrigin to execute
/// this call irrespective of the parent bounty state.
///
/// If this function is called by the `RejectOrigin` or the
/// parent bounty curator, we assume that the child-bounty curator is
/// malicious or inactive. As a result, child-bounty curator deposit is
/// slashed.
///
/// If the origin is the child-bounty curator, we take this as a sign
/// that they are unable to do their job, and are willingly giving up.
/// We could slash the deposit, but for now we allow them to unreserve
/// their deposit and exit without issue. (We may want to change this if
/// it is abused.)
///
/// Finally, the origin can be anyone iff the child-bounty curator is
/// "inactive". Expiry update due of parent bounty is used to estimate
/// inactive state of child-bounty curator.
///
/// This allows anyone in the community to call out that a child-bounty
/// curator is not doing their due diligence, and we should pick a new
/// one. In this case the child-bounty curator deposit is slashed.
///
/// State of child-bounty is moved to Added state on successful call
/// completion.
///
/// - `parent_bounty_id`: Index of parent bounty.
/// - `child_bounty_id`: Index of child bounty.
#[pallet::weight(<T as Config>::WeightInfo::unassign_curator())]
pub fn unassign_curator(
origin: OriginFor<T>,
#[pallet::compact] parent_bounty_id: BountyIndex,
#[pallet::compact] child_bounty_id: BountyIndex,
) -> DispatchResult {
let maybe_sender = ensure_signed(origin.clone())
.map(Some)
.or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?;
ChildBounties::<T>::try_mutate_exists(
parent_bounty_id,
child_bounty_id,
|maybe_child_bounty| -> DispatchResult {
maybe_child_bounty.as_mut().ok_or(BountiesError::<T>::InvalidIndex)?;
let slash_curator =
|curator: &T::AccountId, curator_deposit: &mut BalanceOf<T>| {
let imbalance =
T::Currency::slash_reserved(curator, *curator_deposit).0;
T::OnSlash::on_unbalanced(imbalance);
*curator_deposit = Zero::zero();
};
match child_bounty.status {
ChildBountyStatus::Added => {
// No curator to unassign at this point.
return Err(BountiesError::<T>::UnexpectedStatus.into())
},
ChildBountyStatus::CuratorProposed { ref curator } => {
// A child-bounty curator has been proposed, but not accepted yet.
// Either `RejectOrigin`, parent bounty curator or the proposed
// child-bounty curator can unassign the child-bounty curator.
ensure!(
maybe_sender.map_or(true, |sender| {
sender == *curator ||
Self::ensure_bounty_active(parent_bounty_id)
.map_or(false, |(parent_curator, _)| {
sender == parent_curator
})
}),
BadOrigin
);
// Continue to change bounty status below.
},
ChildBountyStatus::Active { ref curator } => {
// The child-bounty is active.
match maybe_sender {
// If the `RejectOrigin` is calling this function, slash the curator
// deposit.
None => {
slash_curator(curator, &mut child_bounty.curator_deposit);
// Continue to change child-bounty status below.
},
Some(sender) if sender == *curator => {
// This is the child-bounty curator, willingly giving up their
// role. Give back their deposit.
T::Currency::unreserve(curator, child_bounty.curator_deposit);
// Reset curator deposit.
child_bounty.curator_deposit = Zero::zero();
// Continue to change bounty status below.
},
Some(sender) => {
let (parent_curator, update_due) =
Self::ensure_bounty_active(parent_bounty_id)?;
if sender == parent_curator ||
update_due < Self::treasury_block_number()
{
// Slash the child-bounty curator if
// + the call is made by the parent bounty curator.
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
// + or the curator is inactive.
slash_curator(curator, &mut child_bounty.curator_deposit);
// Continue to change bounty status below.
} else {
// Curator has more time to give an update.
return Err(BountiesError::<T>::Premature.into())
}
},
}
},
ChildBountyStatus::PendingPayout { ref curator, .. } => {
let (parent_curator, _) = Self::ensure_bounty_active(parent_bounty_id)?;
ensure!(
maybe_sender.map_or(true, |sender| parent_curator == sender),
BadOrigin,
);
slash_curator(curator, &mut child_bounty.curator_deposit);
// Continue to change child-bounty status below.
},
};
// Move the child-bounty state to Added.
child_bounty.status = ChildBountyStatus::Added;
Ok(())
},
)
}
/// Award child-bounty to a beneficiary.
///
/// The beneficiary will be able to claim the funds after a delay.
///
/// The dispatch origin for this call must be the parent curator or
/// curator of this child-bounty.
///
/// Parent bounty must be in active state, for this child-bounty call to
/// work.
///
/// Child-bounty must be in active state, for processing the call. And
/// state of child-bounty is moved to "PendingPayout" on successful call
/// completion.
///
/// - `parent_bounty_id`: Index of parent bounty.
/// - `child_bounty_id`: Index of child bounty.
/// - `beneficiary`: Beneficiary account.
#[pallet::weight(<T as Config>::WeightInfo::award_child_bounty())]
pub fn award_child_bounty(
origin: OriginFor<T>,
#[pallet::compact] parent_bounty_id: BountyIndex,
#[pallet::compact] child_bounty_id: BountyIndex,
beneficiary: AccountIdLookupOf<T>,
) -> DispatchResult {
let signer = ensure_signed(origin)?;
let beneficiary = T::Lookup::lookup(beneficiary)?;
// Ensure parent bounty exists, and is active.
let (parent_curator, _) = Self::ensure_bounty_active(parent_bounty_id)?;
ChildBounties::<T>::try_mutate_exists(
parent_bounty_id,
child_bounty_id,
|maybe_child_bounty| -> DispatchResult {
maybe_child_bounty.as_mut().ok_or(BountiesError::<T>::InvalidIndex)?;
// Ensure child-bounty is in active state.
if let ChildBountyStatus::Active { ref curator } = child_bounty.status {
ensure!(
signer == *curator || signer == parent_curator,
BountiesError::<T>::RequireCurator,
);
// Move the child-bounty state to pending payout.
child_bounty.status = ChildBountyStatus::PendingPayout {
curator: signer,
beneficiary: beneficiary.clone(),
unlock_at: Self::treasury_block_number() +
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
T::BountyDepositPayoutDelay::get(),
};
Ok(())
} else {
Err(BountiesError::<T>::UnexpectedStatus.into())
}
},
)?;
// Trigger the event Awarded.
Self::deposit_event(Event::<T>::Awarded {
index: parent_bounty_id,
child_index: child_bounty_id,
beneficiary,
});
Ok(())
}
/// Claim the payout from an awarded child-bounty after payout delay.
///
/// The dispatch origin for this call may be any signed origin.
///
/// Call works independent of parent bounty state, No need for parent
/// bounty to be in active state.
///
/// The Beneficiary is paid out with agreed bounty value. Curator fee is
/// paid & curator deposit is unreserved.
///
/// Child-bounty must be in "PendingPayout" state, for processing the
/// call. And instance of child-bounty is removed from the state on
/// successful call completion.
///
/// - `parent_bounty_id`: Index of parent bounty.
/// - `child_bounty_id`: Index of child bounty.
#[pallet::weight(<T as Config>::WeightInfo::claim_child_bounty())]
pub fn claim_child_bounty(
origin: OriginFor<T>,
#[pallet::compact] parent_bounty_id: BountyIndex,
#[pallet::compact] child_bounty_id: BountyIndex,
) -> DispatchResult {
let _ = ensure_signed(origin)?;
// Ensure child-bounty is in expected state.
ChildBounties::<T>::try_mutate_exists(
parent_bounty_id,
child_bounty_id,
|maybe_child_bounty| -> DispatchResult {
let child_bounty =
maybe_child_bounty.as_mut().ok_or(BountiesError::<T>::InvalidIndex)?;
if let ChildBountyStatus::PendingPayout {
ref curator,
ref beneficiary,
ref unlock_at,
} = child_bounty.status
{
// Ensure block number is elapsed for processing the
// claim.
ensure!(
Self::treasury_block_number() >= *unlock_at,
BountiesError::<T>::Premature,
);
// Make curator fee payment.
let child_bounty_account =
Self::child_bounty_account_id(parent_bounty_id, child_bounty_id);
let balance = T::Currency::free_balance(&child_bounty_account);
let curator_fee = child_bounty.fee.min(balance);
let payout = balance.saturating_sub(curator_fee);
// Unreserve the curator deposit. Should not fail
// because the deposit is always reserved when curator is
// assigned.
let _ = T::Currency::unreserve(curator, child_bounty.curator_deposit);
// Make payout to child-bounty curator.
// Should not fail because curator fee is always less than bounty value.
let fee_transfer_result = T::Currency::transfer(
&child_bounty_account,
curator_fee,
AllowDeath,
);
debug_assert!(fee_transfer_result.is_ok());
// Make payout to beneficiary.
// Should not fail.
let payout_transfer_result = T::Currency::transfer(
&child_bounty_account,
beneficiary,
payout,
AllowDeath,
);
debug_assert!(payout_transfer_result.is_ok());
// Trigger the Claimed event.
Self::deposit_event(Event::<T>::Claimed {
index: parent_bounty_id,
child_index: child_bounty_id,
payout,
beneficiary: beneficiary.clone(),
});
// Update the active child-bounty tracking count.
polka.dom
committed
ParentChildBounties::<T>::mutate(parent_bounty_id, |count| {
count.saturating_dec()
});
// Remove the child-bounty description.
ChildBountyDescriptionsV1::<T>::remove(parent_bounty_id, child_bounty_id);
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
// Remove the child-bounty instance from the state.
*maybe_child_bounty = None;
Ok(())
} else {
Err(BountiesError::<T>::UnexpectedStatus.into())
}
},
)
}
/// Cancel a proposed or active child-bounty. Child-bounty account funds
/// are transferred to parent bounty account. The child-bounty curator
/// deposit may be unreserved if possible.
///
/// The dispatch origin for this call must be either parent curator or
/// `T::RejectOrigin`.
///
/// If the state of child-bounty is `Active`, curator deposit is
/// unreserved.
///
/// If the state of child-bounty is `PendingPayout`, call fails &
/// returns `PendingPayout` error.
///
/// For the origin other than T::RejectOrigin, parent bounty must be in
/// active state, for this child-bounty call to work. For origin
/// T::RejectOrigin execution is forced.
///
/// Instance of child-bounty is removed from the state on successful
/// call completion.
///
/// - `parent_bounty_id`: Index of parent bounty.
/// - `child_bounty_id`: Index of child bounty.
#[pallet::weight(<T as Config>::WeightInfo::close_child_bounty_added()
.max(<T as Config>::WeightInfo::close_child_bounty_active()))]
pub fn close_child_bounty(
origin: OriginFor<T>,
#[pallet::compact] parent_bounty_id: BountyIndex,
#[pallet::compact] child_bounty_id: BountyIndex,
) -> DispatchResult {
let maybe_sender = ensure_signed(origin.clone())
.map(Some)
.or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?;
// Ensure parent bounty exist, get parent curator.
let (parent_curator, _) = Self::ensure_bounty_active(parent_bounty_id)?;
ensure!(maybe_sender.map_or(true, |sender| parent_curator == sender), BadOrigin);
Self::impl_close_child_bounty(parent_bounty_id, child_bounty_id)?;
Ok(())
}
}
#[pallet::hooks]
impl<T: Config> Hooks<SystemBlockNumberFor<T>> for Pallet<T> {
fn integrity_test() {
let parent_bounty_id: BountyIndex = 1;
let child_bounty_id: BountyIndex = 2;
let _: T::AccountId = T::PalletId::get()
.try_into_sub_account(("cb", parent_bounty_id, child_bounty_id))
.expect(
"The `AccountId` type must be large enough to fit the child bounty account ID.",
);
}
}
}
impl<T: Config> Pallet<T> {
/// Get the block number used in the treasury pallet.
///
/// It may be configured to use the relay chain block number on a parachain.
pub fn treasury_block_number() -> BlockNumberFor<T> {
<T as pallet_treasury::Config>::BlockNumberProvider::current_block_number()
}
// This function will calculate the deposit of a curator.
fn calculate_curator_deposit(
parent_curator: &T::AccountId,
child_curator: &T::AccountId,
bounty_fee: &BalanceOf<T>,
) -> BalanceOf<T> {
if parent_curator == child_curator {
return Zero::zero()
}
// We just use the same logic from the parent bounties pallet.
pallet_bounties::Pallet::<T>::calculate_curator_deposit(bounty_fee)
}
/// The account ID of a child-bounty account.
pub fn child_bounty_account_id(
parent_bounty_id: BountyIndex,
child_bounty_id: BountyIndex,
) -> T::AccountId {
// This function is taken from the parent (bounties) pallet, but the
// prefix is changed to have different AccountId when the index of
// parent and child is same.
T::PalletId::get().into_sub_account_truncating(("cb", parent_bounty_id, child_bounty_id))
}
fn create_child_bounty(
parent_bounty_id: BountyIndex,
child_bounty_id: BountyIndex,
child_bounty_value: BalanceOf<T>,
description: BoundedVec<u8, T::MaximumReasonLength>,
) {
let child_bounty = ChildBounty {
parent_bounty: parent_bounty_id,
value: child_bounty_value,
fee: 0u32.into(),
curator_deposit: 0u32.into(),
status: ChildBountyStatus::Added,
};
ChildBounties::<T>::insert(parent_bounty_id, child_bounty_id, &child_bounty);
ChildBountyDescriptionsV1::<T>::insert(parent_bounty_id, child_bounty_id, description);
Self::deposit_event(Event::Added { index: parent_bounty_id, child_index: child_bounty_id });
}
fn ensure_bounty_active(
bounty_id: BountyIndex,
) -> Result<(T::AccountId, BlockNumberFor<T>), DispatchError> {
polka.dom
committed
let parent_bounty = pallet_bounties::Bounties::<T>::get(bounty_id)
.ok_or(BountiesError::<T>::InvalidIndex)?;
if let BountyStatus::Active { curator, update_due } = parent_bounty.get_status() {
Ok((curator, update_due))
} else {
Err(Error::<T>::ParentBountyNotActive.into())
}
}
fn impl_close_child_bounty(
parent_bounty_id: BountyIndex,
child_bounty_id: BountyIndex,
) -> DispatchResult {
ChildBounties::<T>::try_mutate_exists(
parent_bounty_id,
child_bounty_id,
|maybe_child_bounty| -> DispatchResult {
let child_bounty =
maybe_child_bounty.as_mut().ok_or(BountiesError::<T>::InvalidIndex)?;
match &child_bounty.status {
ChildBountyStatus::Added | ChildBountyStatus::CuratorProposed { .. } => {
// Nothing extra to do besides the removal of the child-bounty below.
},
ChildBountyStatus::Active { curator } => {
// Cancelled by parent curator or RejectOrigin,
// refund deposit of the working child-bounty curator.
let _ = T::Currency::unreserve(curator, child_bounty.curator_deposit);
// Then execute removal of the child-bounty below.
},
ChildBountyStatus::PendingPayout { .. } => {
// Child-bounty is already in pending payout. If parent
// curator or RejectOrigin wants to close this
// child-bounty, it should mean the child-bounty curator
// was acting maliciously. So first unassign the
// child-bounty curator, slashing their deposit.
return Err(BountiesError::<T>::PendingPayout.into())
},
}
// Revert the curator fee back to parent bounty curator &
// reduce the active child-bounty count.
ChildrenCuratorFees::<T>::mutate(parent_bounty_id, |value| {
*value = value.saturating_sub(child_bounty.fee)
});
polka.dom
committed
ParentChildBounties::<T>::mutate(parent_bounty_id, |count| {
*count = count.saturating_sub(1)
});
// Transfer fund from child-bounty to parent bounty.
let parent_bounty_account =
pallet_bounties::Pallet::<T>::bounty_account_id(parent_bounty_id);
let child_bounty_account =
Self::child_bounty_account_id(parent_bounty_id, child_bounty_id);
let balance = T::Currency::free_balance(&child_bounty_account);
let transfer_result = T::Currency::transfer(
&child_bounty_account,
&parent_bounty_account,
balance,
AllowDeath,
); // Should not fail; child bounty account gets this balance during creation.
debug_assert!(transfer_result.is_ok());
// Remove the child-bounty description.
ChildBountyDescriptionsV1::<T>::remove(parent_bounty_id, child_bounty_id);
*maybe_child_bounty = None;
Self::deposit_event(Event::<T>::Canceled {
index: parent_bounty_id,
child_index: child_bounty_id,
});
Ok(())
},
)
}
}
/// Implement ChildBountyManager to connect with the bounties pallet. This is
/// where we pass the active child bounties and child curator fees to the parent
/// bounty.
///
/// Function `children_curator_fees` not only returns the fee but also removes cumulative curator
/// fees during call.
impl<T: Config> pallet_bounties::ChildBountyManager<BalanceOf<T>> for Pallet<T> {
/// Returns number of active child bounties for `bounty_id`
fn child_bounties_count(
bounty_id: pallet_bounties::BountyIndex,
) -> pallet_bounties::BountyIndex {
polka.dom
committed
ParentChildBounties::<T>::get(bounty_id)
/// Returns cumulative child bounty curator fees for `bounty_id` also removing the associated
/// storage item. This function is assumed to be called when parent bounty is claimed.
fn children_curator_fees(bounty_id: pallet_bounties::BountyIndex) -> BalanceOf<T> {
// This is asked for when the parent bounty is being claimed. No use of
// keeping it in state after that. Hence removing.
polka.dom
committed
let children_fee_total = ChildrenCuratorFees::<T>::get(bounty_id);
ChildrenCuratorFees::<T>::remove(bounty_id);
/// Clean up the storage on a parent bounty removal.
fn bounty_removed(bounty_id: BountyIndex) {
debug_assert!(ParentChildBounties::<T>::get(bounty_id).is_zero());
debug_assert!(ChildrenCuratorFees::<T>::get(bounty_id).is_zero());
debug_assert!(ChildBounties::<T>::iter_key_prefix(bounty_id).count().is_zero());
debug_assert!(ChildBountyDescriptionsV1::<T>::iter_key_prefix(bounty_id).count().is_zero());
ParentChildBounties::<T>::remove(bounty_id);
ParentTotalChildBounties::<T>::remove(bounty_id);
}