mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-07-13 17:25:49 +00:00
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:
@@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user