#![doc = docify::embed!("src/tests.rs", spend_local_origin_works)]
#![doc = docify::embed!("src/tests.rs", spend_payout_works)]
#![cfg_attr(not(feature = "std"), no_std)]
mod benchmarking;
pub mod migration;
#[cfg(test)]
mod tests;
pub mod weights;
use core::marker::PhantomData;
#[cfg(feature = "runtime-benchmarks")]
pub use benchmarking::ArgumentsFactory;
extern crate alloc;
use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use alloc::{boxed::Box, collections::btree_map::BTreeMap};
use sp_runtime::{
traits::{
AccountIdConversion, BlockNumberProvider, CheckedAdd, One, Saturating, StaticLookup,
UniqueSaturatedInto, Zero,
},
Debug, PerThing, Permill,
};
use frame_support::{
dispatch::{DispatchResult, DispatchResultWithPostInfo},
ensure, print,
traits::{
tokens::Pay, Currency, ExistenceRequirement::KeepAlive, Get, Imbalance, OnUnbalanced,
ReservableCurrency, WithdrawReasons,
},
weights::Weight,
BoundedVec, PalletId,
};
use frame_system::pallet_prelude::BlockNumberFor as SystemBlockNumberFor;
pub use pallet::*;
pub use weights::WeightInfo;
pub type BalanceOf<T, I = ()> =
<<T as Config<I>>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
pub type AssetBalanceOf<T, I> = <<T as Config<I>>::Paymaster as Pay>::Balance;
pub type PositiveImbalanceOf<T, I = ()> = <<T as Config<I>>::Currency as Currency<
<T as frame_system::Config>::AccountId,
>>::PositiveImbalance;
pub type NegativeImbalanceOf<T, I = ()> = <<T as Config<I>>::Currency as Currency<
<T as frame_system::Config>::AccountId,
>>::NegativeImbalance;
type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
type BeneficiaryLookupOf<T, I> = <<T as Config<I>>::BeneficiaryLookup as StaticLookup>::Source;
pub type BlockNumberFor<T, I = ()> =
<<T as Config<I>>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
#[impl_trait_for_tuples::impl_for_tuples(30)]
pub trait SpendFunds<T: Config<I>, I: 'static = ()> {
fn spend_funds(
budget_remaining: &mut BalanceOf<T, I>,
imbalance: &mut PositiveImbalanceOf<T, I>,
total_weight: &mut Weight,
missed_any: &mut bool,
);
}
pub type ProposalIndex = u32;
#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
#[derive(
Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, MaxEncodedLen, Debug, TypeInfo,
)]
pub struct Proposal<AccountId, Balance> {
pub proposer: AccountId,
pub value: Balance,
pub beneficiary: AccountId,
pub bond: Balance,
}
#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
#[derive(
Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, MaxEncodedLen, Debug, TypeInfo,
)]
pub enum PaymentState<Id> {
Pending,
Attempted { id: Id },
Failed,
}
#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
#[derive(
Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, MaxEncodedLen, Debug, TypeInfo,
)]
pub struct SpendStatus<AssetKind, AssetBalance, Beneficiary, BlockNumber, PaymentId> {
pub asset_kind: AssetKind,
pub amount: AssetBalance,
pub beneficiary: Beneficiary,
pub valid_from: BlockNumber,
pub expire_at: BlockNumber,
pub status: PaymentState<PaymentId>,
}
pub type SpendIndex = u32;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::{
dispatch_context::with_context,
pallet_prelude::*,
traits::tokens::{ConversionFromAssetBalance, PaymentStatus},
};
use frame_system::pallet_prelude::{ensure_signed, OriginFor};
#[pallet::pallet]
pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
#[pallet::config]
pub trait Config<I: 'static = ()>: frame_system::Config {
type Currency: Currency<Self::AccountId> + ReservableCurrency<Self::AccountId>;
type RejectOrigin: EnsureOrigin<Self::RuntimeOrigin>;
#[allow(deprecated)]
type RuntimeEvent: From<Event<Self, I>>
+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
#[pallet::constant]
type SpendPeriod: Get<BlockNumberFor<Self, I>>;
#[pallet::constant]
type Burn: Get<Permill>;
#[pallet::constant]
type PalletId: Get<PalletId>;
type BurnDestination: OnUnbalanced<NegativeImbalanceOf<Self, I>>;
type WeightInfo: WeightInfo;
type SpendFunds: SpendFunds<Self, I>;
#[pallet::constant]
type MaxApprovals: Get<u32>;
type SpendOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = BalanceOf<Self, I>>;
type AssetKind: Parameter + MaxEncodedLen;
type Beneficiary: Parameter + MaxEncodedLen;
type BeneficiaryLookup: StaticLookup<Target = Self::Beneficiary>;
type Paymaster: Pay<Beneficiary = Self::Beneficiary, AssetKind = Self::AssetKind>;
type BalanceConverter: ConversionFromAssetBalance<
<Self::Paymaster as Pay>::Balance,
Self::AssetKind,
BalanceOf<Self, I>,
>;
#[pallet::constant]
type PayoutPeriod: Get<BlockNumberFor<Self, I>>;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper: ArgumentsFactory<Self::AssetKind, Self::Beneficiary>;
type BlockNumberProvider: BlockNumberProvider;
}
#[pallet::extra_constants]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
fn pot_account() -> T::AccountId {
Self::account_id()
}
}
#[pallet::storage]
pub type ProposalCount<T, I = ()> = StorageValue<_, ProposalIndex, ValueQuery>;
#[pallet::storage]
pub type Proposals<T: Config<I>, I: 'static = ()> = StorageMap<
_,
Twox64Concat,
ProposalIndex,
Proposal<T::AccountId, BalanceOf<T, I>>,
OptionQuery,
>;
#[pallet::storage]
pub type Deactivated<T: Config<I>, I: 'static = ()> =
StorageValue<_, BalanceOf<T, I>, ValueQuery>;
#[pallet::storage]
pub type Approvals<T: Config<I>, I: 'static = ()> =
StorageValue<_, BoundedVec<ProposalIndex, T::MaxApprovals>, ValueQuery>;
#[pallet::storage]
pub type SpendCount<T, I = ()> = StorageValue<_, SpendIndex, ValueQuery>;
#[pallet::storage]
pub type Spends<T: Config<I>, I: 'static = ()> = StorageMap<
_,
Twox64Concat,
SpendIndex,
SpendStatus<
T::AssetKind,
AssetBalanceOf<T, I>,
T::Beneficiary,
BlockNumberFor<T, I>,
<T::Paymaster as Pay>::Id,
>,
OptionQuery,
>;
#[pallet::storage]
pub type LastSpendPeriod<T, I = ()> = StorageValue<_, BlockNumberFor<T, I>, OptionQuery>;
#[pallet::genesis_config]
#[derive(frame_support::DefaultNoBound)]
pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
#[serde(skip)]
_config: core::marker::PhantomData<(T, I)>,
}
#[pallet::genesis_build]
impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
fn build(&self) {
let account_id = Pallet::<T, I>::account_id();
let min = T::Currency::minimum_balance();
if T::Currency::free_balance(&account_id) < min {
let _ = T::Currency::make_free_balance_be(&account_id, min);
}
}
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config<I>, I: 'static = ()> {
Spending { budget_remaining: BalanceOf<T, I> },
Awarded { proposal_index: ProposalIndex, award: BalanceOf<T, I>, account: T::AccountId },
Burnt { burnt_funds: BalanceOf<T, I> },
Rollover { rollover_balance: BalanceOf<T, I> },
Deposit { value: BalanceOf<T, I> },
SpendApproved {
proposal_index: ProposalIndex,
amount: BalanceOf<T, I>,
beneficiary: T::AccountId,
},
UpdatedInactive { reactivated: BalanceOf<T, I>, deactivated: BalanceOf<T, I> },
AssetSpendApproved {
index: SpendIndex,
asset_kind: T::AssetKind,
amount: AssetBalanceOf<T, I>,
beneficiary: T::Beneficiary,
valid_from: BlockNumberFor<T, I>,
expire_at: BlockNumberFor<T, I>,
},
AssetSpendVoided { index: SpendIndex },
Paid { index: SpendIndex, payment_id: <T::Paymaster as Pay>::Id },
PaymentFailed { index: SpendIndex, payment_id: <T::Paymaster as Pay>::Id },
SpendProcessed { index: SpendIndex },
}
#[pallet::error]
pub enum Error<T, I = ()> {
InvalidIndex,
TooManyApprovals,
InsufficientPermission,
ProposalNotApproved,
FailedToConvertBalance,
SpendExpired,
EarlyPayout,
AlreadyAttempted,
PayoutError,
NotAttempted,
Inconclusive,
}
#[pallet::hooks]
impl<T: Config<I>, I: 'static> Hooks<SystemBlockNumberFor<T>> for Pallet<T, I> {
fn on_initialize(_do_not_use_local_block_number: SystemBlockNumberFor<T>) -> Weight {
let block_number = T::BlockNumberProvider::current_block_number();
let pot = Self::pot();
let deactivated = Deactivated::<T, I>::get();
if pot != deactivated {
T::Currency::reactivate(deactivated);
T::Currency::deactivate(pot);
Deactivated::<T, I>::put(&pot);
Self::deposit_event(Event::<T, I>::UpdatedInactive {
reactivated: deactivated,
deactivated: pot,
});
}
let last_spend_period = LastSpendPeriod::<T, I>::get()
.unwrap_or_else(|| Self::update_last_spend_period());
let blocks_since_last_spend_period = block_number.saturating_sub(last_spend_period);
let safe_spend_period = T::SpendPeriod::get().max(BlockNumberFor::<T, I>::one());
let (spend_periods_passed, extra_blocks) = (
blocks_since_last_spend_period / safe_spend_period,
blocks_since_last_spend_period % safe_spend_period,
);
let new_last_spend_period = block_number.saturating_sub(extra_blocks);
if spend_periods_passed > BlockNumberFor::<T, I>::zero() {
Self::spend_funds(spend_periods_passed, new_last_spend_period)
} else {
Weight::zero()
}
}
#[cfg(feature = "try-runtime")]
fn try_state(_: SystemBlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
Self::do_try_state()?;
Ok(())
}
}
#[derive(Default)]
struct SpendContext<Balance> {
spend_in_context: BTreeMap<Balance, Balance>,
}
#[pallet::call]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
#[pallet::call_index(3)]
#[pallet::weight(T::WeightInfo::spend_local())]
#[deprecated(
note = "The `spend_local` call will be removed by May 2025. Migrate to the new flow and use the `spend` call."
)]
#[allow(deprecated)]
pub fn spend_local(
origin: OriginFor<T>,
#[pallet::compact] amount: BalanceOf<T, I>,
beneficiary: AccountIdLookupOf<T>,
) -> DispatchResult {
let max_amount = T::SpendOrigin::ensure_origin(origin)?;
ensure!(amount <= max_amount, Error::<T, I>::InsufficientPermission);
with_context::<SpendContext<BalanceOf<T, I>>, _>(|v| {
let context = v.or_default();
let spend = context.spend_in_context.entry(max_amount).or_default();
if spend.checked_add(&amount).map(|s| s > max_amount).unwrap_or(true) {
Err(Error::<T, I>::InsufficientPermission)
} else {
*spend = spend.saturating_add(amount);
Ok(())
}
})
.unwrap_or(Ok(()))?;
let beneficiary = T::Lookup::lookup(beneficiary)?;
#[allow(deprecated)]
let proposal_index = ProposalCount::<T, I>::get();
#[allow(deprecated)]
Approvals::<T, I>::try_append(proposal_index)
.map_err(|_| Error::<T, I>::TooManyApprovals)?;
let proposal = Proposal {
proposer: beneficiary.clone(),
value: amount,
beneficiary: beneficiary.clone(),
bond: Default::default(),
};
#[allow(deprecated)]
Proposals::<T, I>::insert(proposal_index, proposal);
#[allow(deprecated)]
ProposalCount::<T, I>::put(proposal_index + 1);
Self::deposit_event(Event::SpendApproved { proposal_index, amount, beneficiary });
Ok(())
}
#[pallet::call_index(4)]
#[pallet::weight((T::WeightInfo::remove_approval(), DispatchClass::Operational))]
#[deprecated(
note = "The `remove_approval` call will be removed by May 2025. It associated with the deprecated `spend_local` call."
)]
#[allow(deprecated)]
pub fn remove_approval(
origin: OriginFor<T>,
#[pallet::compact] proposal_id: ProposalIndex,
) -> DispatchResult {
T::RejectOrigin::ensure_origin(origin)?;
#[allow(deprecated)]
Approvals::<T, I>::try_mutate(|v| -> DispatchResult {
if let Some(index) = v.iter().position(|x| x == &proposal_id) {
v.remove(index);
Ok(())
} else {
Err(Error::<T, I>::ProposalNotApproved.into())
}
})?;
Ok(())
}
#[pallet::call_index(5)]
#[pallet::weight(T::WeightInfo::spend())]
pub fn spend(
origin: OriginFor<T>,
asset_kind: Box<T::AssetKind>,
#[pallet::compact] amount: AssetBalanceOf<T, I>,
beneficiary: Box<BeneficiaryLookupOf<T, I>>,
valid_from: Option<BlockNumberFor<T, I>>,
) -> DispatchResult {
let max_amount = T::SpendOrigin::ensure_origin(origin)?;
let beneficiary = T::BeneficiaryLookup::lookup(*beneficiary)?;
let now = T::BlockNumberProvider::current_block_number();
let valid_from = valid_from.unwrap_or(now);
let expire_at = valid_from.saturating_add(T::PayoutPeriod::get());
ensure!(expire_at > now, Error::<T, I>::SpendExpired);
let native_amount =
T::BalanceConverter::from_asset_balance(amount, *asset_kind.clone())
.map_err(|_| Error::<T, I>::FailedToConvertBalance)?;
ensure!(native_amount <= max_amount, Error::<T, I>::InsufficientPermission);
with_context::<SpendContext<BalanceOf<T, I>>, _>(|v| {
let context = v.or_default();
let spend = context.spend_in_context.entry(max_amount).or_default();
if spend.checked_add(&native_amount).map(|s| s > max_amount).unwrap_or(true) {
Err(Error::<T, I>::InsufficientPermission)
} else {
*spend = spend.saturating_add(native_amount);
Ok(())
}
})
.unwrap_or(Ok(()))?;
let index = SpendCount::<T, I>::get();
Spends::<T, I>::insert(
index,
SpendStatus {
asset_kind: *asset_kind.clone(),
amount,
beneficiary: beneficiary.clone(),
valid_from,
expire_at,
status: PaymentState::Pending,
},
);
SpendCount::<T, I>::put(index + 1);
Self::deposit_event(Event::AssetSpendApproved {
index,
asset_kind: *asset_kind,
amount,
beneficiary,
valid_from,
expire_at,
});
Ok(())
}
#[pallet::call_index(6)]
#[pallet::weight(T::WeightInfo::payout())]
pub fn payout(origin: OriginFor<T>, index: SpendIndex) -> DispatchResult {
ensure_signed(origin)?;
let mut spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
let now = T::BlockNumberProvider::current_block_number();
ensure!(now >= spend.valid_from, Error::<T, I>::EarlyPayout);
ensure!(spend.expire_at > now, Error::<T, I>::SpendExpired);
ensure!(
matches!(spend.status, PaymentState::Pending | PaymentState::Failed),
Error::<T, I>::AlreadyAttempted
);
let id = T::Paymaster::pay(&spend.beneficiary, spend.asset_kind.clone(), spend.amount)
.map_err(|_| Error::<T, I>::PayoutError)?;
spend.status = PaymentState::Attempted { id };
spend.expire_at = now.saturating_add(T::PayoutPeriod::get());
Spends::<T, I>::insert(index, spend);
Self::deposit_event(Event::<T, I>::Paid { index, payment_id: id });
Ok(())
}
#[pallet::call_index(7)]
#[pallet::weight(T::WeightInfo::check_status())]
pub fn check_status(origin: OriginFor<T>, index: SpendIndex) -> DispatchResultWithPostInfo {
use PaymentState as State;
use PaymentStatus as Status;
ensure_signed(origin)?;
let mut spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
let now = T::BlockNumberProvider::current_block_number();
if now > spend.expire_at && !matches!(spend.status, State::Attempted { .. }) {
Spends::<T, I>::remove(index);
Self::deposit_event(Event::<T, I>::SpendProcessed { index });
return Ok(Pays::No.into());
}
let payment_id = match spend.status {
State::Attempted { id } => id,
_ => return Err(Error::<T, I>::NotAttempted.into()),
};
match T::Paymaster::check_payment(payment_id) {
Status::Failure => {
spend.status = PaymentState::Failed;
Spends::<T, I>::insert(index, spend);
Self::deposit_event(Event::<T, I>::PaymentFailed { index, payment_id });
},
Status::Success | Status::Unknown => {
Spends::<T, I>::remove(index);
Self::deposit_event(Event::<T, I>::SpendProcessed { index });
return Ok(Pays::No.into());
},
Status::InProgress => return Err(Error::<T, I>::Inconclusive.into()),
}
return Ok(Pays::Yes.into());
}
#[pallet::call_index(8)]
#[pallet::weight(T::WeightInfo::void_spend())]
pub fn void_spend(origin: OriginFor<T>, index: SpendIndex) -> DispatchResult {
T::RejectOrigin::ensure_origin(origin)?;
let spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
ensure!(
matches!(spend.status, PaymentState::Pending | PaymentState::Failed),
Error::<T, I>::AlreadyAttempted
);
Spends::<T, I>::remove(index);
Self::deposit_event(Event::<T, I>::AssetSpendVoided { index });
Ok(())
}
}
}
impl<T: Config<I>, I: 'static> Pallet<T, I> {
pub fn account_id() -> T::AccountId {
T::PalletId::get().into_account_truncating()
}
fn update_last_spend_period() -> BlockNumberFor<T, I> {
let block_number = T::BlockNumberProvider::current_block_number();
let spend_period = T::SpendPeriod::get().max(BlockNumberFor::<T, I>::one());
let time_since_last_spend = block_number % spend_period;
let last_spend_period = if time_since_last_spend.is_zero() {
block_number.saturating_sub(spend_period)
} else {
block_number.saturating_sub(time_since_last_spend)
};
LastSpendPeriod::<T, I>::put(last_spend_period);
last_spend_period
}
#[deprecated(
note = "This function will be removed by May 2025. Configure pallet to use PayFromAccount for Paymaster type instead"
)]
pub fn proposal_count() -> ProposalIndex {
#[allow(deprecated)]
ProposalCount::<T, I>::get()
}
#[deprecated(
note = "This function will be removed by May 2025. Configure pallet to use PayFromAccount for Paymaster type instead"
)]
pub fn proposals(index: ProposalIndex) -> Option<Proposal<T::AccountId, BalanceOf<T, I>>> {
#[allow(deprecated)]
Proposals::<T, I>::get(index)
}
#[deprecated(
note = "This function will be removed by May 2025. Configure pallet to use PayFromAccount for Paymaster type instead"
)]
#[allow(deprecated)]
pub fn approvals() -> BoundedVec<ProposalIndex, T::MaxApprovals> {
Approvals::<T, I>::get()
}
pub fn spend_funds(
spend_periods_passed: BlockNumberFor<T, I>,
new_last_spend_period: BlockNumberFor<T, I>,
) -> Weight {
LastSpendPeriod::<T, I>::put(new_last_spend_period);
let mut total_weight = Weight::zero();
let mut budget_remaining = Self::pot();
Self::deposit_event(Event::Spending { budget_remaining });
let account_id = Self::account_id();
let mut missed_any = false;
let mut imbalance = PositiveImbalanceOf::<T, I>::zero();
#[allow(deprecated)]
let proposals_len = Approvals::<T, I>::mutate(|v| {
let proposals_approvals_len = v.len() as u32;
v.retain(|&index| {
if let Some(p) = Proposals::<T, I>::get(index) {
if p.value <= budget_remaining {
budget_remaining -= p.value;
Proposals::<T, I>::remove(index);
let err_amount = T::Currency::unreserve(&p.proposer, p.bond);
debug_assert!(err_amount.is_zero());
imbalance.subsume(T::Currency::deposit_creating(&p.beneficiary, p.value));
Self::deposit_event(Event::Awarded {
proposal_index: index,
award: p.value,
account: p.beneficiary,
});
false
} else {
missed_any = true;
true
}
} else {
false
}
});
proposals_approvals_len
});
total_weight += T::WeightInfo::on_initialize_proposals(proposals_len);
T::SpendFunds::spend_funds(
&mut budget_remaining,
&mut imbalance,
&mut total_weight,
&mut missed_any,
);
if !missed_any && !T::Burn::get().is_zero() {
let one_minus_burn = T::Burn::get().left_from_one();
let percent_left =
one_minus_burn.saturating_pow(spend_periods_passed.unique_saturated_into());
let new_budget_remaining = percent_left * budget_remaining;
let burn = budget_remaining.saturating_sub(new_budget_remaining);
budget_remaining = new_budget_remaining;
let (debit, credit) = T::Currency::pair(burn);
imbalance.subsume(debit);
T::BurnDestination::on_unbalanced(credit);
Self::deposit_event(Event::Burnt { burnt_funds: burn })
}
if let Err(problem) =
T::Currency::settle(&account_id, imbalance, WithdrawReasons::TRANSFER, KeepAlive)
{
print("Inconsistent state - couldn't settle imbalance for funds spent by treasury");
drop(problem);
}
Self::deposit_event(Event::Rollover { rollover_balance: budget_remaining });
total_weight
}
pub fn pot() -> BalanceOf<T, I> {
T::Currency::free_balance(&Self::account_id())
.saturating_sub(T::Currency::minimum_balance())
}
#[cfg(any(feature = "try-runtime", test))]
fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
Self::try_state_proposals()?;
Self::try_state_spends()?;
Ok(())
}
#[cfg(any(feature = "try-runtime", test))]
fn try_state_proposals() -> Result<(), sp_runtime::TryRuntimeError> {
let current_proposal_count = ProposalCount::<T, I>::get();
ensure!(
current_proposal_count as usize >= Proposals::<T, I>::iter().count(),
"Actual number of proposals exceeds `ProposalCount`."
);
Proposals::<T, I>::iter_keys().try_for_each(|proposal_index| -> DispatchResult {
ensure!(
current_proposal_count as u32 > proposal_index,
"`ProposalCount` should by strictly greater than any ProposalIndex used as a key for `Proposals`."
);
Ok(())
})?;
Approvals::<T, I>::get()
.iter()
.try_for_each(|proposal_index| -> DispatchResult {
ensure!(
Proposals::<T, I>::contains_key(proposal_index),
"Proposal indices in `Approvals` must also be contained in `Proposals`."
);
Ok(())
})?;
Ok(())
}
#[cfg(any(feature = "try-runtime", test))]
fn try_state_spends() -> Result<(), sp_runtime::TryRuntimeError> {
let current_spend_count = SpendCount::<T, I>::get();
ensure!(
current_spend_count as usize >= Spends::<T, I>::iter().count(),
"Actual number of spends exceeds `SpendCount`."
);
Spends::<T, I>::iter_keys().try_for_each(|spend_index| -> DispatchResult {
ensure!(
current_spend_count > spend_index,
"`SpendCount` should by strictly greater than any SpendIndex used as a key for `Spends`."
);
Ok(())
})?;
Spends::<T, I>::iter().try_for_each(|(_index, spend)| -> DispatchResult {
ensure!(
spend.valid_from < spend.expire_at,
"Spend cannot expire before it becomes valid."
);
Ok(())
})?;
Ok(())
}
}
impl<T: Config<I>, I: 'static> OnUnbalanced<NegativeImbalanceOf<T, I>> for Pallet<T, I> {
fn on_nonzero_unbalanced(amount: NegativeImbalanceOf<T, I>) {
let numeric_amount = amount.peek();
let _ = T::Currency::resolve_creating(&Self::account_id(), amount);
Self::deposit_event(Event::Deposit { value: numeric_amount });
}
}
pub struct TreasuryAccountId<R>(PhantomData<R>);
impl<R> sp_runtime::traits::TypedGet for TreasuryAccountId<R>
where
R: crate::Config,
{
type Type = <R as frame_system::Config>::AccountId;
fn get() -> Self::Type {
crate::Pallet::<R>::account_id()
}
}