Treasury spends various asset kinds (#1333)

### Summary 

This PR introduces new dispatchables to the treasury pallet, allowing
spends of various asset types. The enhanced features of the treasury
pallet, in conjunction with the asset-rate pallet, are set up and
enabled for Westend and Rococo.

### Westend and Rococo runtimes.

Polkadot/Kusams/Rococo Treasury can accept proposals for `spends` of
various asset kinds by specifying the asset's location and ID.

#### Treasury Instance New Dispatchables:
- `spend(AssetKind, AssetBalance, Beneficiary, Option<ValidFrom>)` -
propose and approve a spend;
- `payout(SpendIndex)` - payout an approved spend or retry a failed
payout
- `check_payment(SpendIndex)` - check the status of a payout;
- `void_spend(SpendIndex)` - void previously approved spend;
> existing spend dispatchable renamed to spend_local

in this context, the `AssetKind` parameter contains the asset's location
and it's corresponding `asset_id`, for example:
`USDT` on `AssetHub`,
``` rust
location = MultiLocation(0, X1(Parachain(1000)))
asset_id = MultiLocation(0, X2(PalletInstance(50), GeneralIndex(1984)))
```

the `Beneficiary` parameter is a `MultiLocation` in the context of the
asset's location, for example
``` rust
// the Fellowship salary pallet's location / account
FellowshipSalaryPallet = MultiLocation(1, X2(Parachain(1001), PalletInstance(64)))
// or custom `AccountId`
Alice = MultiLocation(0, AccountId32(network: None, id: [1,...]))
```

the `AssetBalance` represents the amount of the `AssetKind` to be
transferred to the `Beneficiary`. For permission checks, the asset
amount is converted to the native amount and compared against the
maximum spendable amount determined by the commanding spend origin.

the `spend` dispatchable allows for batching spends with different
`ValidFrom` arguments, enabling milestone-based spending. If the
expectations tied to an approved spend are not met, it is possible to
void the spend later using the `void_spend` dispatchable.

Asset Rate Pallet provides the conversion rate from the `AssetKind` to
the native balance.

#### Asset Rate Instance Dispatchables:
- `create(AssetKind, Rate)` - initialize a conversion rate to the native
balance for the given asset
- `update(AssetKind, Rate)` - update the conversion rate to the native
balance for the given asset
- `remove(AssetKind)` - remove an existing conversion rate to the native
balance for the given asset

the pallet's dispatchables can be executed by the Root or Treasurer
origins.

### Treasury Pallet

Treasury Pallet can accept proposals for `spends` of various asset kinds
and pay them out through the implementation of the `Pay` trait.

New Dispatchables:
- `spend(Config::AssetKind, AssetBalance, Config::Beneficiary,
Option<ValidFrom>)` - propose and approve a spend;
- `payout(SpendIndex)` - payout an approved spend or retry a failed
payout;
- `check_payment(SpendIndex)` - check the status of a payout;
- `void_spend(SpendIndex)` - void previously approved spend;
> existing spend dispatchable renamed to spend_local

The parameters' types of the `spend` dispatchable exposed via the
pallet's `Config` and allows to propose and accept a spend of a certain
amount.

An approved spend can be claimed via the `payout` within the
`Config::SpendPeriod`. Clients provide an implementation of the `Pay`
trait which can pay an asset of the `AssetKind` to the `Beneficiary` in
`AssetBalance` units.

The implementation of the Pay trait might not have an immediate final
payment status, for example if implemented over `XCM` and the actual
transfer happens on a remote chain.

The `check_status` dispatchable can be executed to update the spend's
payment state and retry the `payout` if the payment has failed.

---------

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>
Co-authored-by: command-bot <>
This commit is contained in:
Muharem
2023-10-07 19:32:35 +02:00
committed by GitHub
parent 5a6912606a
commit cb944dc548
32 changed files with 2213 additions and 286 deletions
+451 -45
View File
@@ -15,46 +15,60 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! > Made with *Substrate*, for *Polkadot*.
//!
//! [![github]](https://github.com/paritytech/substrate/frame/fast-unstake) -
//! [![polkadot]](https://polkadot.network)
//!
//! [polkadot]: 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
//!
//! # Treasury Pallet
//!
//! The Treasury pallet provides a "pot" of funds that can be managed by stakeholders in the system
//! and a structure for making spending proposals from this pot.
//!
//! - [`Config`]
//! - [`Call`]
//!
//! ## Overview
//!
//! The Treasury Pallet itself provides the pot to store funds, and a means for stakeholders to
//! propose, approve, and deny expenditures. The chain will need to provide a method (e.g.
//! inflation, fees) for collecting funds.
//! propose and claim expenditures (aka spends). The chain will need to provide a method to approve
//! spends (e.g. public referendum) and a method for collecting funds (e.g. inflation, fees).
//!
//! By way of example, the Council could vote to fund the Treasury with a portion of the block
//! By way of example, stakeholders could vote to fund the Treasury with a portion of the block
//! reward and use the funds to pay developers.
//!
//!
//! ### Terminology
//!
//! - **Proposal:** A suggestion to allocate funds from the pot to a beneficiary.
//! - **Beneficiary:** An account who will receive the funds from a proposal iff the proposal is
//! approved.
//! - **Deposit:** Funds that a proposer must lock when making a proposal. The deposit will be
//! returned or slashed if the proposal is approved or rejected respectively.
//! - **Pot:** Unspent funds accumulated by the treasury pallet.
//! - **Spend** An approved proposal for transferring a specific amount of funds to a designated
//! beneficiary.
//!
//! ## Interface
//! ### Example
//!
//! ### Dispatchable Functions
//! 1. Multiple local spends approved by spend origins and received by a beneficiary.
#![doc = docify::embed!("src/tests.rs", spend_local_origin_works)]
//!
//! General spending/proposal protocol:
//! - `propose_spend` - Make a spending proposal and stake the required deposit.
//! - `reject_proposal` - Reject a proposal, slashing the deposit.
//! - `approve_proposal` - Accept the proposal, returning the deposit.
//! - `remove_approval` - Remove an approval, the deposit will no longer be returned.
//! 2. Approve a spend of some asset kind and claim it.
#![doc = docify::embed!("src/tests.rs", spend_payout_works)]
//!
//! ## GenesisConfig
//! ## Pallet API
//!
//! The Treasury pallet depends on the [`GenesisConfig`].
//! See the [`pallet`] module for more information about the interfaces this pallet exposes,
//! including its configuration trait, dispatchables, storage items, events and errors.
//!
//! ## Low Level / Implementation Details
//!
//! Spends can be initiated using either the `spend_local` or `spend` dispatchable. The
//! `spend_local` dispatchable enables the creation of spends using the native currency of the
//! chain, utilizing the funds stored in the pot. These spends are automatically paid out every
//! [`pallet::Config::SpendPeriod`]. On the other hand, the `spend` dispatchable allows spending of
//! any asset kind managed by the treasury, with payment facilitated by a designated
//! [`pallet::Config::Paymaster`]. To claim these spends, the `payout` dispatchable should be called
//! within some temporal bounds, starting from the moment they become valid and within one
//! [`pallet::Config::PayoutPeriod`].
#![cfg_attr(not(feature = "std"), no_std)]
@@ -62,6 +76,8 @@ mod benchmarking;
#[cfg(test)]
mod tests;
pub mod weights;
#[cfg(feature = "runtime-benchmarks")]
pub use benchmarking::ArgumentsFactory;
use codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
@@ -75,7 +91,7 @@ use sp_std::{collections::btree_map::BTreeMap, prelude::*};
use frame_support::{
print,
traits::{
Currency, ExistenceRequirement::KeepAlive, Get, Imbalance, OnUnbalanced,
tokens::Pay, Currency, ExistenceRequirement::KeepAlive, Get, Imbalance, OnUnbalanced,
ReservableCurrency, WithdrawReasons,
},
weights::Weight,
@@ -87,6 +103,7 @@ 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;
@@ -94,6 +111,7 @@ pub type NegativeImbalanceOf<T, I = ()> = <<T as Config<I>>::Currency as Currenc
<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;
/// A trait to allow the Treasury Pallet to spend it's funds for other purposes.
/// There is an expectation that the implementer of this trait will correctly manage
@@ -133,10 +151,47 @@ pub struct Proposal<AccountId, Balance> {
bond: Balance,
}
/// The state of the payment claim.
#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)]
pub enum PaymentState<Id> {
/// Pending claim.
Pending,
/// Payment attempted with a payment identifier.
Attempted { id: Id },
/// Payment failed.
Failed,
}
/// Info regarding an approved treasury spend.
#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)]
pub struct SpendStatus<AssetKind, AssetBalance, Beneficiary, BlockNumber, PaymentId> {
// The kind of asset to be spent.
asset_kind: AssetKind,
/// The asset amount of the spend.
amount: AssetBalance,
/// The beneficiary of the spend.
beneficiary: Beneficiary,
/// The block number from which the spend can be claimed.
valid_from: BlockNumber,
/// The block number by which the spend has to be claimed.
expire_at: BlockNumber,
/// The status of the payout/claim.
status: PaymentState<PaymentId>,
}
/// Index of an approved treasury spend.
pub type SpendIndex = u32;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::{dispatch_context::with_context, pallet_prelude::*};
use frame_support::{
dispatch_context::with_context,
pallet_prelude::*,
traits::tokens::{ConversionFromAssetBalance, PaymentStatus},
};
use frame_system::pallet_prelude::*;
#[pallet::pallet]
@@ -201,9 +256,38 @@ pub mod pallet {
type MaxApprovals: Get<u32>;
/// The origin required for approving spends from the treasury outside of the proposal
/// process. The `Success` value is the maximum amount that this origin is allowed to
/// spend at a time.
/// process. The `Success` value is the maximum amount in a native asset that this origin
/// is allowed to spend at a time.
type SpendOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = BalanceOf<Self, I>>;
/// Type parameter representing the asset kinds to be spent from the treasury.
type AssetKind: Parameter + MaxEncodedLen;
/// Type parameter used to identify the beneficiaries eligible to receive treasury spends.
type Beneficiary: Parameter + MaxEncodedLen;
/// Converting trait to take a source type and convert to [`Self::Beneficiary`].
type BeneficiaryLookup: StaticLookup<Target = Self::Beneficiary>;
/// Type for processing spends of [Self::AssetKind] in favor of [`Self::Beneficiary`].
type Paymaster: Pay<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`].
type BalanceConverter: ConversionFromAssetBalance<
<Self::Paymaster as Pay>::Balance,
Self::AssetKind,
BalanceOf<Self, I>,
>;
/// The period during which an approved treasury spend has to be claimed.
#[pallet::constant]
type PayoutPeriod: Get<BlockNumberFor<Self>>;
/// Helper type for benchmarks.
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper: ArgumentsFactory<Self::AssetKind, Self::Beneficiary>;
}
/// Number of proposals that have been made.
@@ -233,6 +317,27 @@ pub mod pallet {
pub type Approvals<T: Config<I>, I: 'static = ()> =
StorageValue<_, BoundedVec<ProposalIndex, T::MaxApprovals>, ValueQuery>;
/// The count of spends that have been made.
#[pallet::storage]
pub(crate) type SpendCount<T, I = ()> = StorageValue<_, SpendIndex, ValueQuery>;
/// Spends that have been approved and being processed.
// Hasher: Twox safe since `SpendIndex` is an internal count based index.
#[pallet::storage]
pub type Spends<T: Config<I>, I: 'static = ()> = StorageMap<
_,
Twox64Concat,
SpendIndex,
SpendStatus<
T::AssetKind,
AssetBalanceOf<T, I>,
T::Beneficiary,
BlockNumberFor<T>,
<T::Paymaster as Pay>::Id,
>,
OptionQuery,
>;
#[pallet::genesis_config]
#[derive(frame_support::DefaultNoBound)]
pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
@@ -277,6 +382,24 @@ pub mod pallet {
},
/// The inactive funds of the pallet have been updated.
UpdatedInactive { reactivated: BalanceOf<T, I>, deactivated: BalanceOf<T, I> },
/// A new asset spend proposal has been approved.
AssetSpendApproved {
index: SpendIndex,
asset_kind: T::AssetKind,
amount: AssetBalanceOf<T, I>,
beneficiary: T::Beneficiary,
valid_from: BlockNumberFor<T>,
expire_at: BlockNumberFor<T>,
},
/// An approved spend was voided.
AssetSpendVoided { index: SpendIndex },
/// A payment happened.
Paid { index: SpendIndex, payment_id: <T::Paymaster as Pay>::Id },
/// A payment failed and can be retried.
PaymentFailed { index: SpendIndex, payment_id: <T::Paymaster as Pay>::Id },
/// A spend was processed and removed from the storage. It might have been successfully
/// paid or it may have expired.
SpendProcessed { index: SpendIndex },
}
/// Error for the treasury pallet.
@@ -284,7 +407,7 @@ pub mod pallet {
pub enum Error<T, I = ()> {
/// Proposer's balance is too low.
InsufficientProposersBalance,
/// No proposal or bounty at that index.
/// No proposal, bounty or spend at that index.
InvalidIndex,
/// Too many approvals in the queue.
TooManyApprovals,
@@ -293,6 +416,20 @@ pub mod pallet {
InsufficientPermission,
/// Proposal has not been approved.
ProposalNotApproved,
/// The balance of the asset kind is not convertible to the balance of the native asset.
FailedToConvertBalance,
/// The spend has expired and cannot be claimed.
SpendExpired,
/// The spend is not yet eligible for payout.
EarlyPayout,
/// The payment has already been attempted.
AlreadyAttempted,
/// There was some issue with the mechanism of payment.
PayoutError,
/// The payout was not yet attempted/claimed.
NotAttempted,
/// The payment has neither failed nor succeeded yet.
Inconclusive,
}
#[pallet::hooks]
@@ -328,12 +465,22 @@ pub mod pallet {
#[pallet::call]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// Put forward a suggestion for spending. A deposit proportional to the value
/// is reserved and slashed if the proposal is rejected. It is returned once the
/// proposal is awarded.
/// Put forward a suggestion for spending.
///
/// ## Complexity
/// ## Dispatch Origin
///
/// Must be signed.
///
/// ## Details
/// A deposit proportional to the value is reserved and slashed if the proposal is rejected.
/// It is returned once the proposal is awarded.
///
/// ### Complexity
/// - O(1)
///
/// ## Events
///
/// Emits [`Event::Proposed`] if successful.
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::propose_spend())]
#[allow(deprecated)]
@@ -360,12 +507,21 @@ pub mod pallet {
Ok(())
}
/// Reject a proposed spend. The original deposit will be slashed.
/// Reject a proposed spend.
///
/// May only be called from `T::RejectOrigin`.
/// ## Dispatch Origin
///
/// ## Complexity
/// Must be [`Config::RejectOrigin`].
///
/// ## Details
/// The original deposit will be slashed.
///
/// ### Complexity
/// - O(1)
///
/// ## Events
///
/// Emits [`Event::Rejected`] if successful.
#[pallet::call_index(1)]
#[pallet::weight((T::WeightInfo::reject_proposal(), DispatchClass::Operational))]
#[allow(deprecated)]
@@ -391,13 +547,23 @@ pub mod pallet {
Ok(())
}
/// Approve a proposal. At a later time, the proposal will be allocated to the beneficiary
/// and the original deposit will be returned.
/// Approve a proposal.
///
/// May only be called from `T::ApproveOrigin`.
/// ## Dispatch Origin
///
/// ## Complexity
/// Must be [`Config::ApproveOrigin`].
///
/// ## Details
///
/// At a later time, the proposal will be allocated to the beneficiary and the original
/// deposit will be returned.
///
/// ### Complexity
/// - O(1).
///
/// ## Events
///
/// No events are emitted from this dispatch.
#[pallet::call_index(2)]
#[pallet::weight((T::WeightInfo::approve_proposal(T::MaxApprovals::get()), DispatchClass::Operational))]
#[allow(deprecated)]
@@ -418,15 +584,24 @@ pub mod pallet {
/// Propose and approve a spend of treasury funds.
///
/// - `origin`: Must be `SpendOrigin` with the `Success` value being at least `amount`.
/// ## Dispatch Origin
///
/// Must be [`Config::SpendOrigin`] with the `Success` value being at least `amount`.
///
/// ### Details
/// NOTE: For record-keeping purposes, the proposer is deemed to be equivalent to the
/// beneficiary.
///
/// ### Parameters
/// - `amount`: The amount to be transferred from the treasury to the `beneficiary`.
/// - `beneficiary`: The destination account for the transfer.
///
/// NOTE: For record-keeping purposes, the proposer is deemed to be equivalent to the
/// beneficiary.
/// ## Events
///
/// Emits [`Event::SpendApproved`] if successful.
#[pallet::call_index(3)]
#[pallet::weight(T::WeightInfo::spend())]
pub fn spend(
#[pallet::weight(T::WeightInfo::spend_local())]
pub fn spend_local(
origin: OriginFor<T>,
#[pallet::compact] amount: BalanceOf<T, I>,
beneficiary: AccountIdLookupOf<T>,
@@ -472,18 +647,26 @@ pub mod pallet {
}
/// Force a previously approved proposal to be removed from the approval queue.
///
/// ## Dispatch Origin
///
/// Must be [`Config::RejectOrigin`].
///
/// ## Details
///
/// The original deposit will no longer be returned.
///
/// May only be called from `T::RejectOrigin`.
/// ### Parameters
/// - `proposal_id`: The index of a proposal
///
/// ## Complexity
/// ### Complexity
/// - O(A) where `A` is the number of approvals
///
/// Errors:
/// - `ProposalNotApproved`: The `proposal_id` supplied was not found in the approval queue,
/// i.e., the proposal has not been approved. This could also mean the proposal does not
/// exist altogether, thus there is no way it would have been approved in the first place.
/// ### Errors
/// - [`Error::ProposalNotApproved`]: The `proposal_id` supplied was not found in the
/// approval queue, i.e., the proposal has not been approved. This could also mean the
/// proposal does not exist altogether, thus there is no way it would have been approved
/// in the first place.
#[pallet::call_index(4)]
#[pallet::weight((T::WeightInfo::remove_approval(), DispatchClass::Operational))]
pub fn remove_approval(
@@ -503,6 +686,229 @@ pub mod pallet {
Ok(())
}
/// Propose and approve a spend of treasury funds.
///
/// ## Dispatch Origin
///
/// Must be [`Config::SpendOrigin`] with the `Success` value being at least
/// `amount` of `asset_kind` in the native asset. The amount of `asset_kind` is converted
/// for assertion using the [`Config::BalanceConverter`].
///
/// ## Details
///
/// Create an approved spend for transferring a specific `amount` of `asset_kind` to a
/// designated beneficiary. The spend must be claimed using the `payout` dispatchable within
/// the [`Config::PayoutPeriod`].
///
/// ### Parameters
/// - `asset_kind`: An indicator of the specific asset class to be spent.
/// - `amount`: The amount to be transferred from the treasury to the `beneficiary`.
/// - `beneficiary`: The beneficiary of the spend.
/// - `valid_from`: The block number from which the spend can be claimed. It can refer to
/// the past if the resulting spend has not yet expired according to the
/// [`Config::PayoutPeriod`]. If `None`, the spend can be claimed immediately after
/// approval.
///
/// ## Events
///
/// Emits [`Event::AssetSpendApproved`] if successful.
#[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>>,
) -> DispatchResult {
let max_amount = T::SpendOrigin::ensure_origin(origin)?;
let beneficiary = T::BeneficiaryLookup::lookup(*beneficiary)?;
let now = frame_system::Pallet::<T>::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();
// We group based on `max_amount`, to distinguish between different kind of
// origins. (assumes that all origins have different `max_amount`)
//
// Worst case is that we reject some "valid" request.
let spend = context.spend_in_context.entry(max_amount).or_default();
// Ensure that we don't overflow nor use more than `max_amount`
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(())
}
/// Claim a spend.
///
/// ## Dispatch Origin
///
/// Must be signed.
///
/// ## Details
///
/// Spends must be claimed within some temporal bounds. A spend may be claimed within one
/// [`Config::PayoutPeriod`] from the `valid_from` block.
/// In case of a payout failure, the spend status must be updated with the `check_status`
/// dispatchable before retrying with the current function.
///
/// ### Parameters
/// - `index`: The spend index.
///
/// ## Events
///
/// Emits [`Event::Paid`] if successful.
#[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 = frame_system::Pallet::<T>::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 };
Spends::<T, I>::insert(index, spend);
Self::deposit_event(Event::<T, I>::Paid { index, payment_id: id });
Ok(())
}
/// Check the status of the spend and remove it from the storage if processed.
///
/// ## Dispatch Origin
///
/// Must be signed.
///
/// ## Details
///
/// The status check is a prerequisite for retrying a failed payout.
/// If a spend has either succeeded or expired, it is removed from the storage by this
/// function. In such instances, transaction fees are refunded.
///
/// ### Parameters
/// - `index`: The spend index.
///
/// ## Events
///
/// Emits [`Event::PaymentFailed`] if the spend payout has failed.
/// Emits [`Event::SpendProcessed`] if the spend payout has succeed.
#[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 = frame_system::Pallet::<T>::block_number();
if now > spend.expire_at && !matches!(spend.status, State::Attempted { .. }) {
// spend has expired and no further status update is expected.
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())
}
/// Void previously approved spend.
///
/// ## Dispatch Origin
///
/// Must be [`Config::RejectOrigin`].
///
/// ## Details
///
/// A spend void is only possible if the payout has not been attempted yet.
///
/// ### Parameters
/// - `index`: The spend index.
///
/// ## Events
///
/// Emits [`Event::AssetSpendVoided`] if successful.
#[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(())
}
}
}