From d94f888a8dd423d4e397b96f2f97a08e7fb1d6f3 Mon Sep 17 00:00:00 2001 From: Gav Date: Sat, 2 Sep 2023 15:55:04 +0200 Subject: [PATCH] Consideration trait --- substrate/frame/support/src/traits.rs | 9 +- .../frame/support/src/traits/messages.rs | 8 +- .../frame/support/src/traits/preimages.rs | 5 +- substrate/frame/support/src/traits/storage.rs | 70 +++++++++- .../src/traits/tokens/fungible/freeze.rs | 57 +++++++- .../src/traits/tokens/fungible/hold.rs | 37 ++++- .../support/src/traits/tokens/fungible/mod.rs | 132 ++++++++++++++++++ .../src/traits/tokens/fungibles/freeze.rs | 74 +++++++++- .../frame/support/src/traits/tokens/misc.rs | 16 ++- 9 files changed, 387 insertions(+), 21 deletions(-) diff --git a/substrate/frame/support/src/traits.rs b/substrate/frame/support/src/traits.rs index f669046f858f..40e348b5e373 100644 --- a/substrate/frame/support/src/traits.rs +++ b/substrate/frame/support/src/traits.rs @@ -90,8 +90,8 @@ pub use hooks::{ pub mod schedule; mod storage; pub use storage::{ - Incrementable, Instance, PartialStorageInfoTrait, StorageInfo, StorageInfoTrait, - StorageInstance, TrackedStorageKey, WhitelistedStorageKeys, + Consideration, Footprint, Incrementable, Instance, PartialStorageInfoTrait, StorageInfo, + StorageInfoTrait, StorageInstance, TrackedStorageKey, WhitelistedStorageKeys, }; mod dispatch; @@ -111,9 +111,8 @@ pub use preimages::{Bounded, BoundedInline, FetchResult, Hash, QueryPreimage, St mod messages; pub use messages::{ - EnqueueMessage, EnqueueWithOrigin, ExecuteOverweightError, Footprint, HandleMessage, - NoopServiceQueues, ProcessMessage, ProcessMessageError, QueuePausedQuery, ServiceQueues, - TransformOrigin, + EnqueueMessage, EnqueueWithOrigin, ExecuteOverweightError, HandleMessage, NoopServiceQueues, + ProcessMessage, ProcessMessageError, QueuePausedQuery, ServiceQueues, TransformOrigin, }; mod safe_mode; diff --git a/substrate/frame/support/src/traits/messages.rs b/substrate/frame/support/src/traits/messages.rs index 36fa7957dff7..0db163e072b1 100644 --- a/substrate/frame/support/src/traits/messages.rs +++ b/substrate/frame/support/src/traits/messages.rs @@ -17,6 +17,7 @@ //! Traits for managing message queuing and handling. +use super::storage::Footprint; use codec::{Decode, Encode, FullCodec, MaxEncodedLen}; use scale_info::TypeInfo; use sp_core::{ConstU32, Get, TypedGet}; @@ -115,13 +116,6 @@ impl ServiceQueues for NoopServiceQueues { } } -/// The resource footprint of a queue. -#[derive(Default, Copy, Clone, Eq, PartialEq, RuntimeDebug)] -pub struct Footprint { - pub count: u64, - pub size: u64, -} - /// Can enqueue messages for multiple origins. pub trait EnqueueMessage { /// The maximal length any enqueued message may have. diff --git a/substrate/frame/support/src/traits/preimages.rs b/substrate/frame/support/src/traits/preimages.rs index 3e78116202b4..bf08a286dd7c 100644 --- a/substrate/frame/support/src/traits/preimages.rs +++ b/substrate/frame/support/src/traits/preimages.rs @@ -18,6 +18,7 @@ //! Stuff for dealing with 32-byte hashed preimages. use codec::{Decode, Encode, EncodeLike, MaxEncodedLen}; +use scale_info::TypeInfo; use sp_core::{RuntimeDebug, H256}; use sp_io::hashing::blake2_256; use sp_runtime::{traits::ConstU32, DispatchError}; @@ -29,9 +30,7 @@ pub type BoundedInline = crate::BoundedVec>; /// The maximum we expect a single legacy hash lookup to be. const MAX_LEGACY_LEN: u32 = 1_000_000; -#[derive( - Encode, Decode, MaxEncodedLen, Clone, Eq, PartialEq, scale_info::TypeInfo, RuntimeDebug, -)] +#[derive(Encode, Decode, MaxEncodedLen, Clone, Eq, PartialEq, TypeInfo, RuntimeDebug)] #[codec(mel_bound())] pub enum Bounded { /// A Blake2 256 hash with no preimage length. We diff --git a/substrate/frame/support/src/traits/storage.rs b/substrate/frame/support/src/traits/storage.rs index 64eddf51b7fa..4a3bd8d3411a 100644 --- a/substrate/frame/support/src/traits/storage.rs +++ b/substrate/frame/support/src/traits/storage.rs @@ -17,9 +17,14 @@ //! Traits for encoding data related to pallet's storage items. +use codec::{Encode, FullCodec, MaxEncodedLen}; use impl_trait_for_tuples::impl_for_tuples; +use scale_info::TypeInfo; pub use sp_core::storage::TrackedStorageKey; -use sp_runtime::{traits::Saturating, RuntimeDebug}; +use sp_runtime::{ + traits::{Member, Saturating}, + DispatchError, RuntimeDebug, +}; use sp_std::{collections::btree_set::BTreeSet, prelude::*}; /// An instance of a pallet in the storage. @@ -127,6 +132,69 @@ impl WhitelistedStorageKeys for Tuple { } } +/// The resource footprint of a bunch of blobs. We assume only the number of blobs and their total +/// size in bytes matter. +#[derive(Default, Copy, Clone, Eq, PartialEq, RuntimeDebug)] +pub struct Footprint { + /// The number of blobs. + pub count: u64, + /// The total size of the blobs in bytes. + pub size: u64, +} + +impl Footprint { + pub fn from_parts(items: usize, len: usize) -> Self { + Self { count: items as u64, size: len as u64 } + } + + pub fn from_encodable(e: impl Encode) -> Self { + Self::from_parts(1, e.encoded_size()) + } +} + +/// Some sort of cost taken from account temporarily in order to offset the cost to the chain of +/// holding some data `Footprint` in state. +/// +/// The cost may be increased, reduced or dropped entirely as the footprint changes. +pub trait Consideration { + /// A single ticket corresponding to some particular datum held in storage. This is an opaque + /// type, but must itself be stored and generally it should be placed alongside whatever data + /// the ticket was created for. + /// + /// While not technically a linear type owing to the need for `FullCodec`, *this should be + /// treated as one*. Don't type to duplicate it, and remember to drop it when you're done with + /// it. + type Ticket: Member + FullCodec + TypeInfo + MaxEncodedLen + Default; + + /// Optionally consume an old ticket and alter the footprint, enforcing the new cost to `who` + /// and returning the new ticket (or an error if there was an issue). + /// + /// For creating tickets and dropping them, you can use the simpler `new` and `drop` instead. + fn update( + who: &AccountId, + old: Option, + new: Option, + ) -> Result; + + /// Create a ticket for the `new` footprint attributable to `who`. This ticket *must* be + /// consumed (through either `drop` or `update`) if the footprint changes or is removed. + fn new(who: &AccountId, new: Footprint) -> Result { + Self::update(who, None, Some(new)) + } + + /// Consume a ticket for some `old` footprint attributable to `who` which has now been freed. + fn drop(who: &AccountId, old: Self::Ticket) -> Result<(), DispatchError> { + Self::update(who, Some(old), None).map(|_| ()) + } +} + +impl Consideration for () { + type Ticket = (); + fn update(_: &A, _: Option<()>, _: Option) -> Result<(), DispatchError> { + Ok(()) + } +} + macro_rules! impl_incrementable { ($($type:ty),+) => { $( diff --git a/substrate/frame/support/src/traits/tokens/fungible/freeze.rs b/substrate/frame/support/src/traits/tokens/fungible/freeze.rs index bc8f22b959cb..c8ce8b2770fd 100644 --- a/substrate/frame/support/src/traits/tokens/fungible/freeze.rs +++ b/substrate/frame/support/src/traits/tokens/fungible/freeze.rs @@ -18,7 +18,13 @@ //! The traits for putting freezes within a single fungible token class. use scale_info::TypeInfo; -use sp_runtime::DispatchResult; +use sp_arithmetic::{ + traits::{CheckedAdd, CheckedSub}, + ArithmeticError, +}; +use sp_runtime::{DispatchResult, TokenError}; + +use crate::{ensure, traits::tokens::Fortitude}; /// Trait for inspecting a fungible asset which can be frozen. Freezing is essentially setting a /// minimum balance bellow which the total balance (inclusive of any funds placed on hold) may not @@ -65,4 +71,53 @@ pub trait Mutate: Inspect { /// Remove an existing lock. fn thaw(id: &Self::Id, who: &AccountId) -> DispatchResult; + + /// Attempt to alter the amount frozen under the given `id` to `amount`. + /// + /// Fail if the account of `who` has fewer freezable funds than `amount`, unless `fortitude` is + /// `Fortitude::Force`. + fn set_frozen( + id: &Self::Id, + who: &AccountId, + amount: Self::Balance, + fortitude: Fortitude, + ) -> DispatchResult { + let force = fortitude == Fortitude::Force; + ensure!(force || Self::balance_freezable(who) >= amount, TokenError::FundsUnavailable); + Self::set_freeze(id, who, amount) + } + + /// Attempt to set the amount frozen under the given `id` to `amount`, iff this would increase + /// the amount frozen under `id`. Do nothing otherwise. + /// + /// Fail if the account of `who` has fewer freezable funds than `amount`, unless `fortitude` is + /// `Fortitude::Force`. + fn ensure_frozen( + id: &Self::Id, + who: &AccountId, + amount: Self::Balance, + fortitude: Fortitude, + ) -> DispatchResult { + let force = fortitude == Fortitude::Force; + ensure!(force || Self::balance_freezable(who) >= amount, TokenError::FundsUnavailable); + Self::extend_freeze(id, who, amount) + } + + /// Decrease the amount which is being frozen for a particular lock, failing in the case of + /// underflow. + fn decrease_frozen(id: &Self::Id, who: &AccountId, amount: Self::Balance) -> DispatchResult { + let a = Self::balance_frozen(id, who) + .checked_sub(&amount) + .ok_or(ArithmeticError::Underflow)?; + Self::set_freeze(id, who, a) + } + + /// Increase the amount which is being frozen for a particular lock, failing in the case that + /// too little balance is available for being frozen. + fn increase_frozen(id: &Self::Id, who: &AccountId, amount: Self::Balance) -> DispatchResult { + let a = Self::balance_frozen(id, who) + .checked_add(&amount) + .ok_or(ArithmeticError::Overflow)?; + Self::set_frozen(id, who, a, Fortitude::Polite) + } } diff --git a/substrate/frame/support/src/traits/tokens/fungible/hold.rs b/substrate/frame/support/src/traits/tokens/fungible/hold.rs index 15c046fbe9a2..9c7e86584517 100644 --- a/substrate/frame/support/src/traits/tokens/fungible/hold.rs +++ b/substrate/frame/support/src/traits/tokens/fungible/hold.rs @@ -92,11 +92,11 @@ pub trait Inspect: super::Inspect { who: &AccountId, amount: Self::Balance, ) -> DispatchResult { - ensure!(Self::hold_available(reason, who), TokenError::CannotCreateHold); ensure!( amount <= Self::reducible_balance(who, Protect, Force), TokenError::FundsUnavailable ); + ensure!(Self::hold_available(reason, who), TokenError::CannotCreateHold); Ok(()) } @@ -242,6 +242,41 @@ pub trait Mutate: Ok(actual) } + /// Hold or release funds in the account of `who` to bring the balance on hold for `reason` to + /// exactly `amount`. + fn set_on_hold( + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + ) -> DispatchResult { + let current_amount = Self::balance_on_hold(reason, who); + if current_amount < amount { + Self::hold(reason, who, amount - current_amount) + } else if current_amount > amount { + Self::release(reason, who, current_amount - amount, Precision::Exact).map(|_| ()) + } else { + Ok(()) + } + } + + /// Release all funds in the account of `who` on hold for `reason`. + /// + /// The actual amount released is returned with `Ok`. + /// + /// If `precision` is `BestEffort`, then the amount actually unreserved and returned as the + /// inner value of `Ok` may be smaller than the `amount` passed. + /// + /// NOTE! The inner of the `Ok` result variant returns the *actual* amount released. This is the + /// opposite of the `ReservableCurrency::unreserve()` result, which gives the amount not able + /// to be released! + fn release_all( + reason: &Self::Reason, + who: &AccountId, + precision: Precision, + ) -> Result { + Self::release(reason, who, Self::balance_on_hold(reason, who), precision) + } + /// Attempt to decrease the balance of `who` which is held for the given `reason` by `amount`. /// /// If `precision` is `BestEffort`, then as much as possible is reduced, up to `amount`, and the diff --git a/substrate/frame/support/src/traits/tokens/fungible/mod.rs b/substrate/frame/support/src/traits/tokens/fungible/mod.rs index 2205fd5dc589..54aef0897f13 100644 --- a/substrate/frame/support/src/traits/tokens/fungible/mod.rs +++ b/substrate/frame/support/src/traits/tokens/fungible/mod.rs @@ -45,6 +45,7 @@ mod imbalance; mod item_of; mod regular; +use super::Precision::BestEffort; pub use freeze::{Inspect as InspectFreeze, Mutate as MutateFreeze}; pub use hold::{ Balanced as BalancedHold, Inspect as InspectHold, Mutate as MutateHold, @@ -55,3 +56,134 @@ pub use item_of::ItemOf; pub use regular::{ Balanced, DecreaseIssuance, Dust, IncreaseIssuance, Inspect, Mutate, Unbalanced, }; +use sp_arithmetic::traits::Zero; +use sp_core::Get; +use sp_runtime::{traits::Convert, DispatchError}; + +use crate::{ + ensure, + traits::{Consideration, Footprint}, +}; + +/// Consideration method using a `fungible` balance frozen as the cost exacted for the footprint. +/// +/// The aggregate amount frozen under `R::get()` for any account which has multiple tickets, +/// is the *cumulative* amounts of each ticket's footprint (each individually determined by `D`). +pub struct FreezeConsideration(sp_std::marker::PhantomData<(A, F, R, D)>); +impl, R: Get, D: Convert> Consideration + for FreezeConsideration +{ + type Ticket = F::Balance; + fn update( + who: &A, + old: Option, + new: Option, + ) -> Result { + match (old, new) { + (None, Some(footprint)) => { + let new = D::convert(footprint); + F::increase_frozen(&R::get(), who, new)?; + Ok(new) + }, + (Some(old), Some(footprint)) => { + let new = D::convert(footprint); + if old > new { + F::decrease_frozen(&R::get(), who, old - new)?; + } else if new > old { + F::increase_frozen(&R::get(), who, new - old)?; + } + Ok(new) + }, + (Some(old), None) => { + F::decrease_frozen(&R::get(), who, old)?; + Ok(Default::default()) + }, + (None, None) => Ok(Default::default()), + } + } +} + +/// Consideration method using a `fungible` balance frozen as the cost exacted for the footprint. +pub struct HoldConsideration(sp_std::marker::PhantomData<(A, F, R, D)>); +impl, R: Get, D: Convert> Consideration + for HoldConsideration +{ + type Ticket = F::Balance; + fn update( + who: &A, + old: Option, + new: Option, + ) -> Result { + match (old, new) { + (None, Some(footprint)) => { + let new = D::convert(footprint); + F::hold(&R::get(), who, new)?; + Ok(new) + }, + (Some(old), Some(footprint)) => { + let new = D::convert(footprint); + if old > new { + F::release(&R::get(), who, old - new, BestEffort)?; + } else if new > old { + F::hold(&R::get(), who, new - old)?; + } + Ok(new) + }, + (Some(old), None) => { + F::release(&R::get(), who, old, BestEffort)?; + Ok(Default::default()) + }, + (None, None) => Ok(Default::default()), + } + } +} + +/// Basic consideration method using a `fungible` balance frozen as the cost exacted for the +/// footprint. +/// +/// NOTE: This is an optimized implementation, which can only be used for systems where each +/// account has only a single active ticket associated with it since individual tickets do not +/// track the specific balance which is frozen. If you are uncertain then use `FreezeConsideration` +/// instead, since this works in all circumstances. +pub struct SingletonFreezeConsideration(sp_std::marker::PhantomData<(A, F, R, D)>); +impl, R: Get, D: Convert> Consideration + for SingletonFreezeConsideration +{ + type Ticket = (); + fn update( + who: &A, + _old: Option, + new: Option, + ) -> Result { + ensure!(F::balance_frozen(&R::get(), who).is_zero(), DispatchError::Unavailable); + match new { + Some(footprint) => F::set_freeze(&R::get(), who, D::convert(footprint)), + None => F::thaw(&R::get(), who), + } + } +} + +/// Basic consideration method using a `fungible` balance placed on hold as the cost exacted for the +/// footprint. +/// +/// NOTE: This is an optimized implementation, which can only be used for systems where each +/// account has only a single active ticket associated with it since individual tickets do not +/// track the specific balance which is frozen. If you are uncertain then use `FreezeConsideration` +/// instead, since this works in all circumstances. +pub struct SingletonHoldConsideration(sp_std::marker::PhantomData<(A, F, R, D)>); +impl, R: Get, D: Convert> Consideration + for SingletonHoldConsideration +{ + type Ticket = (); + fn update( + who: &A, + _old: Option, + new: Option, + ) -> Result { + ensure!(F::balance_on_hold(&R::get(), who).is_zero(), DispatchError::Unavailable); + match new { + Some(footprint) => F::set_on_hold(&R::get(), who, D::convert(footprint)), + None => F::release_all(&R::get(), who, super::Precision::BestEffort).map(|_| ()), + } + } +} diff --git a/substrate/frame/support/src/traits/tokens/fungibles/freeze.rs b/substrate/frame/support/src/traits/tokens/fungibles/freeze.rs index 88f083b0a210..b07d20d6c41c 100644 --- a/substrate/frame/support/src/traits/tokens/fungibles/freeze.rs +++ b/substrate/frame/support/src/traits/tokens/fungibles/freeze.rs @@ -17,8 +17,13 @@ //! The traits for putting freezes within a single fungible token class. +use crate::{ensure, traits::tokens::Fortitude}; use scale_info::TypeInfo; -use sp_runtime::DispatchResult; +use sp_arithmetic::{ + traits::{CheckedAdd, CheckedSub}, + ArithmeticError, +}; +use sp_runtime::{DispatchResult, TokenError}; /// Trait for inspecting a fungible asset which can be frozen. Freezing is essentially setting a /// minimum balance below which the total balance (inclusive of any funds placed on hold) may not @@ -75,4 +80,71 @@ pub trait Mutate: Inspect { /// Remove an existing lock. fn thaw(asset: Self::AssetId, id: &Self::Id, who: &AccountId) -> DispatchResult; + + /// Attempt to alter the amount frozen under the given `id` to `amount`. + /// + /// Fail if the account of `who` has fewer freezable funds than `amount`, unless `fortitude` is + /// `Fortitude::Force`. + fn set_frozen( + asset: Self::AssetId, + id: &Self::Id, + who: &AccountId, + amount: Self::Balance, + fortitude: Fortitude, + ) -> DispatchResult { + let force = fortitude == Fortitude::Force; + ensure!( + force || Self::balance_freezable(asset.clone(), who) >= amount, + TokenError::FundsUnavailable + ); + Self::set_freeze(asset, id, who, amount) + } + + /// Attempt to set the amount frozen under the given `id` to `amount`, iff this would increase + /// the amount frozen under `id`. Do nothing otherwise. + /// + /// Fail if the account of `who` has fewer freezable funds than `amount`, unless `fortitude` is + /// `Fortitude::Force`. + fn ensure_frozen( + asset: Self::AssetId, + id: &Self::Id, + who: &AccountId, + amount: Self::Balance, + fortitude: Fortitude, + ) -> DispatchResult { + let force = fortitude == Fortitude::Force; + ensure!( + force || Self::balance_freezable(asset.clone(), who) >= amount, + TokenError::FundsUnavailable + ); + Self::extend_freeze(asset, id, who, amount) + } + + /// Decrease the amount which is being frozen for a particular lock, failing in the case of + /// underflow. + fn decrease_frozen( + asset: Self::AssetId, + id: &Self::Id, + who: &AccountId, + amount: Self::Balance, + ) -> DispatchResult { + let a = Self::balance_frozen(asset.clone(), id, who) + .checked_sub(&amount) + .ok_or(ArithmeticError::Underflow)?; + Self::set_frozen(asset, id, who, a, Fortitude::Polite) + } + + /// Increase the amount which is being frozen for a particular lock, failing in the case that + /// too little balance is available for being frozen. + fn increase_frozen( + asset: Self::AssetId, + id: &Self::Id, + who: &AccountId, + amount: Self::Balance, + ) -> DispatchResult { + let a = Self::balance_frozen(asset.clone(), id, who) + .checked_add(&amount) + .ok_or(ArithmeticError::Overflow)?; + Self::set_frozen(asset, id, who, a, Fortitude::Polite) + } } diff --git a/substrate/frame/support/src/traits/tokens/misc.rs b/substrate/frame/support/src/traits/tokens/misc.rs index baf3fd5f3546..84bbe3e8d9c8 100644 --- a/substrate/frame/support/src/traits/tokens/misc.rs +++ b/substrate/frame/support/src/traits/tokens/misc.rs @@ -231,7 +231,16 @@ impl Balance for T { }