diff --git a/prdoc/pr_4596.prdoc b/prdoc/pr_4596.prdoc new file mode 100644 index 0000000000000..d47aa3aedfb85 --- /dev/null +++ b/prdoc/pr_4596.prdoc @@ -0,0 +1,18 @@ +title: "Frame: `Consideration` trait generic over `Footprint` and handles zero cost" + +doc: + - audience: Runtime Dev + description: | + `Consideration` trait generic over `Footprint` and can handle zero cost for a give footprint. + + `Consideration` trait is generic over `Footprint` (currently defined over the type with the same name). This makes it possible to setup a custom footprint (e.g. current number of proposals in the storage). + + `Consideration::new` and `Consideration::update` return an `Option` instead `Self`, this make it possible to define no cost for a specific footprint (e.g. current number of proposals in the storage < max_proposal_count / 2). + +crates: + - name: frame-support + bump: major + - name: pallet-preimage + bump: major + - name: pallet-balances + bump: patch diff --git a/substrate/frame/balances/src/tests/fungible_tests.rs b/substrate/frame/balances/src/tests/fungible_tests.rs index 52fbe10bedec0..1a09303a6590d 100644 --- a/substrate/frame/balances/src/tests/fungible_tests.rs +++ b/substrate/frame/balances/src/tests/fungible_tests.rs @@ -18,13 +18,20 @@ //! Tests regarding the functionality of the `fungible` trait set implementations. use super::*; -use frame_support::traits::tokens::{ - Fortitude::{Force, Polite}, - Precision::{BestEffort, Exact}, - Preservation::{Expendable, Preserve, Protect}, - Restriction::Free, +use frame_support::traits::{ + tokens::{ + Fortitude::{Force, Polite}, + Precision::{BestEffort, Exact}, + Preservation::{Expendable, Preserve, Protect}, + Restriction::Free, + }, + Consideration, Footprint, LinearStoragePrice, }; -use fungible::{Inspect, InspectFreeze, InspectHold, Mutate, MutateFreeze, MutateHold, Unbalanced}; +use fungible::{ + FreezeConsideration, HoldConsideration, Inspect, InspectFreeze, InspectHold, + LoneFreezeConsideration, LoneHoldConsideration, Mutate, MutateFreeze, MutateHold, Unbalanced, +}; +use sp_core::ConstU64; #[test] fn inspect_trait_reducible_balance_basic_works() { @@ -493,3 +500,161 @@ fn withdraw_precision_exact_works() { ); }); } + +#[test] +fn freeze_consideration_works() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + type Consideration = FreezeConsideration< + u64, + Balances, + FooReason, + LinearStoragePrice, ConstU64<1>, u64>, + Footprint, + >; + + let who = 4; + // freeze amount taken somewhere outside of our (Consideration) scope. + let extend_freeze = 15; + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 0); + + let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap().unwrap(); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 10); + + let ticket = ticket.update(&who, Footprint::from_parts(4, 1)).unwrap().unwrap(); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 4); + + assert_ok!(Balances::increase_frozen(&TestId::Foo, &who, extend_freeze)); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 4 + extend_freeze); + + let ticket = ticket.update(&who, Footprint::from_parts(8, 1)).unwrap().unwrap(); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 8 + extend_freeze); + + assert_eq!(ticket.update(&who, Footprint::from_parts(0, 0)).unwrap(), None); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 0 + extend_freeze); + + let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap().unwrap(); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 10 + extend_freeze); + + let _ = ticket.drop(&who).unwrap(); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 0 + extend_freeze); + }); +} + +#[test] +fn hold_consideration_works() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + type Consideration = HoldConsideration< + u64, + Balances, + FooReason, + LinearStoragePrice, ConstU64<1>, u64>, + Footprint, + >; + + let who = 4; + // hold amount taken somewhere outside of our (Consideration) scope. + let extend_hold = 15; + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 0); + + let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap().unwrap(); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 10); + + let ticket = ticket.update(&who, Footprint::from_parts(4, 1)).unwrap().unwrap(); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 4); + + assert_ok!(Balances::hold(&TestId::Foo, &who, extend_hold)); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 4 + extend_hold); + + let ticket = ticket.update(&who, Footprint::from_parts(8, 1)).unwrap().unwrap(); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 8 + extend_hold); + + assert_eq!(ticket.update(&who, Footprint::from_parts(0, 0)).unwrap(), None); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 0 + extend_hold); + + let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap().unwrap(); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 10 + extend_hold); + + let _ = ticket.drop(&who).unwrap(); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 0 + extend_hold); + }); +} + +#[test] +fn lone_freeze_consideration_works() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + type Consideration = LoneFreezeConsideration< + u64, + Balances, + FooReason, + LinearStoragePrice, ConstU64<1>, u64>, + Footprint, + >; + + let who = 4; + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 0); + + let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap().unwrap(); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 10); + + assert_ok!(Balances::increase_frozen(&TestId::Foo, &who, 5)); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 15); + + let ticket = ticket.update(&who, Footprint::from_parts(4, 1)).unwrap().unwrap(); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 4); + + assert_eq!(ticket.update(&who, Footprint::from_parts(0, 0)).unwrap(), None); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 0); + + let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap().unwrap(); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 10); + + let _ = ticket.drop(&who).unwrap(); + assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 0); + }); +} + +#[test] +fn lone_hold_consideration_works() { + ExtBuilder::default() + .existential_deposit(1) + .monied(true) + .build_and_execute_with(|| { + type Consideration = LoneHoldConsideration< + u64, + Balances, + FooReason, + LinearStoragePrice, ConstU64<1>, u64>, + Footprint, + >; + + let who = 4; + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 0); + + let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap().unwrap(); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 10); + + assert_ok!(Balances::hold(&TestId::Foo, &who, 5)); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 15); + + let ticket = ticket.update(&who, Footprint::from_parts(4, 1)).unwrap().unwrap(); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 4); + + assert_eq!(ticket.update(&who, Footprint::from_parts(0, 0)).unwrap(), None); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 0); + + let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap().unwrap(); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 10); + + let _ = ticket.drop(&who).unwrap(); + assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 0); + }); +} diff --git a/substrate/frame/balances/src/tests/mod.rs b/substrate/frame/balances/src/tests/mod.rs index 5ed37170407f2..ba0cdabdabbbd 100644 --- a/substrate/frame/balances/src/tests/mod.rs +++ b/substrate/frame/balances/src/tests/mod.rs @@ -107,6 +107,10 @@ impl pallet_transaction_payment::Config for Test { type FeeMultiplierUpdate = (); } +parameter_types! { + pub FooReason: TestId = TestId::Foo; +} + #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] impl Config for Test { type DustRemoval = DustTrap; diff --git a/substrate/frame/preimage/src/benchmarking.rs b/substrate/frame/preimage/src/benchmarking.rs index d0c3404f40a91..f2b76a7999d6c 100644 --- a/substrate/frame/preimage/src/benchmarking.rs +++ b/substrate/frame/preimage/src/benchmarking.rs @@ -116,7 +116,7 @@ benchmarks! { T::ManagerOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?, hash ) verify { - let ticket = TicketOf::::new(¬er, Footprint { count: 1, size: MAX_SIZE as u64 }).unwrap(); + let ticket = TicketOf::::new(¬er, Footprint { count: 1, size: MAX_SIZE as u64 }).unwrap().unwrap(); let s = RequestStatus::Requested { maybe_ticket: Some((noter, ticket)), count: 1, maybe_len: Some(MAX_SIZE) }; assert_eq!(RequestStatusFor::::get(&hash), Some(s)); } diff --git a/substrate/frame/preimage/src/lib.rs b/substrate/frame/preimage/src/lib.rs index 4e47468516663..dd323a12b8f84 100644 --- a/substrate/frame/preimage/src/lib.rs +++ b/substrate/frame/preimage/src/lib.rs @@ -122,7 +122,9 @@ pub mod pallet { type ManagerOrigin: EnsureOrigin; /// A means of providing some cost while data is stored on-chain. - type Consideration: Consideration; + /// + /// Should never return a `None`, implying no cost for a non-empty preimage. + type Consideration: Consideration; } #[pallet::pallet] @@ -158,6 +160,8 @@ pub mod pallet { TooMany, /// Too few hashes were requested to be upgraded (i.e. zero). TooFew, + /// No ticket with a cost was returned by [`Config::Consideration`] to store the preimage. + NoCost, } /// A reason for this pallet placing a hold on funds. @@ -268,10 +272,10 @@ impl Pallet { // unreserve deposit T::Currency::unreserve(&who, amount); // take consideration - let Ok(ticket) = + let Ok(Some(ticket)) = T::Consideration::new(&who, Footprint::from_parts(1, len as usize)) - .defensive_proof("Unexpected inability to take deposit after unreserved") else { + defensive!("None ticket or inability to take deposit after unreserved"); return true }; RequestStatus::Unrequested { ticket: (who, ticket), len } @@ -282,12 +286,10 @@ impl Pallet { T::Currency::unreserve(&who, deposit); // take consideration if let Some(len) = maybe_len { - let Ok(ticket) = + let Ok(Some(ticket)) = T::Consideration::new(&who, Footprint::from_parts(1, len as usize)) - .defensive_proof( - "Unexpected inability to take deposit after unreserved", - ) else { + defensive!("None ticket or inability to take deposit after unreserved"); return true }; Some((who, ticket)) @@ -347,7 +349,8 @@ impl Pallet { RequestStatus::Requested { maybe_ticket: None, count: 1, maybe_len: Some(len) }, (None, Some(depositor)) => { let ticket = - T::Consideration::new(depositor, Footprint::from_parts(1, len as usize))?; + T::Consideration::new(depositor, Footprint::from_parts(1, len as usize))? + .ok_or(Error::::NoCost)?; RequestStatus::Unrequested { ticket: (depositor.clone(), ticket), len } }, }; diff --git a/substrate/frame/support/src/traits/storage.rs b/substrate/frame/support/src/traits/storage.rs index 9e467aea4220d..875ff56bea19e 100644 --- a/substrate/frame/support/src/traits/storage.rs +++ b/substrate/frame/support/src/traits/storage.rs @@ -194,7 +194,7 @@ where } /// Some sort of cost taken from account temporarily in order to offset the cost to the chain of -/// holding some data [`Footprint`] in state. +/// holding some data `Footprint` (e.g. [`Footprint`]) in state. /// /// The cost may be increased, reduced or dropped entirely as the footprint changes. /// @@ -206,16 +206,20 @@ where /// treated as one*. Don't type to duplicate it, and remember to drop it when you're done with /// it. #[must_use] -pub trait Consideration: Member + FullCodec + TypeInfo + MaxEncodedLen { +pub trait Consideration: + Member + FullCodec + TypeInfo + MaxEncodedLen +{ /// Create a ticket for the `new` footprint attributable to `who`. This ticket *must* ultimately - /// be consumed through `update` or `drop` once the footprint changes or is removed. - fn new(who: &AccountId, new: Footprint) -> Result; + /// be consumed through `update` or `drop` once the footprint changes or is removed. `None` + /// implies no cost for a given footprint. + fn new(who: &AccountId, new: Footprint) -> Result, DispatchError>; /// 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). + /// and returning the new ticket (or an error if there was an issue). `None` implies no cost for + /// a given footprint. /// /// For creating tickets and dropping them, you can use the simpler `new` and `drop` instead. - fn update(self, who: &AccountId, new: Footprint) -> Result; + fn update(self, who: &AccountId, new: Footprint) -> Result, DispatchError>; /// Consume a ticket for some `old` footprint attributable to `who` which should now been freed. fn drop(self, who: &AccountId) -> Result<(), DispatchError>; @@ -230,12 +234,12 @@ pub trait Consideration: Member + FullCodec + TypeInfo + MaxEncodedLe } } -impl Consideration for () { - fn new(_: &A, _: Footprint) -> Result { - Ok(()) +impl Consideration for () { + fn new(_: &A, _: F) -> Result, DispatchError> { + Ok(Some(())) } - fn update(self, _: &A, _: Footprint) -> Result<(), DispatchError> { - Ok(()) + fn update(self, _: &A, _: F) -> Result, DispatchError> { + Ok(Some(())) } fn drop(self, _: &A) -> Result<(), DispatchError> { Ok(()) diff --git a/substrate/frame/support/src/traits/tokens/fungible/mod.rs b/substrate/frame/support/src/traits/tokens/fungible/mod.rs index 01c3b9dfe46a5..b8e985648983e 100644 --- a/substrate/frame/support/src/traits/tokens/fungible/mod.rs +++ b/substrate/frame/support/src/traits/tokens/fungible/mod.rs @@ -198,31 +198,40 @@ use crate::{ MaxEncodedLen, RuntimeDebugNoBound, )] -#[scale_info(skip_type_params(A, F, R, D))] +#[scale_info(skip_type_params(A, F, R, D, Fp))] #[codec(mel_bound())] -pub struct FreezeConsideration(F::Balance, PhantomData (A, R, D)>) +pub struct FreezeConsideration(F::Balance, PhantomData (A, R, D, Fp)>) where F: MutateFreeze; impl< A: 'static, F: 'static + MutateFreeze, R: 'static + Get, - D: 'static + Convert, - > Consideration for FreezeConsideration + D: 'static + Convert, + Fp: 'static, + > Consideration for FreezeConsideration { - fn new(who: &A, footprint: Footprint) -> Result { + fn new(who: &A, footprint: Fp) -> Result, DispatchError> { let new = D::convert(footprint); - F::increase_frozen(&R::get(), who, new)?; - Ok(Self(new, PhantomData)) + if new.is_zero() { + Ok(None) + } else { + F::increase_frozen(&R::get(), who, new)?; + Ok(Some(Self(new, PhantomData))) + } } - fn update(self, who: &A, footprint: Footprint) -> Result { + fn update(self, who: &A, footprint: Fp) -> Result, DispatchError> { let new = D::convert(footprint); if self.0 > new { F::decrease_frozen(&R::get(), who, self.0 - new)?; } else if new > self.0 { F::increase_frozen(&R::get(), who, new - self.0)?; } - Ok(Self(new, PhantomData)) + if new.is_zero() { + Ok(None) + } else { + Ok(Some(Self(new, PhantomData))) + } } fn drop(self, who: &A) -> Result<(), DispatchError> { F::decrease_frozen(&R::get(), who, self.0).map(|_| ()) @@ -240,31 +249,43 @@ impl< MaxEncodedLen, RuntimeDebugNoBound, )] -#[scale_info(skip_type_params(A, F, R, D))] +#[scale_info(skip_type_params(A, F, R, D, Fp))] #[codec(mel_bound())] -pub struct HoldConsideration(F::Balance, PhantomData (A, R, D)>) +pub struct HoldConsideration( + F::Balance, + PhantomData (A, R, D, Fp)>, +) where F: MutateHold; impl< A: 'static, F: 'static + MutateHold, R: 'static + Get, - D: 'static + Convert, - > Consideration for HoldConsideration + D: 'static + Convert, + Fp: 'static, + > Consideration for HoldConsideration { - fn new(who: &A, footprint: Footprint) -> Result { + fn new(who: &A, footprint: Fp) -> Result, DispatchError> { let new = D::convert(footprint); - F::hold(&R::get(), who, new)?; - Ok(Self(new, PhantomData)) + if new.is_zero() { + Ok(None) + } else { + F::hold(&R::get(), who, new)?; + Ok(Some(Self(new, PhantomData))) + } } - fn update(self, who: &A, footprint: Footprint) -> Result { + fn update(self, who: &A, footprint: Fp) -> Result, DispatchError> { let new = D::convert(footprint); if self.0 > new { F::release(&R::get(), who, self.0 - new, BestEffort)?; } else if new > self.0 { F::hold(&R::get(), who, new - self.0)?; } - Ok(Self(new, PhantomData)) + if new.is_zero() { + Ok(None) + } else { + Ok(Some(Self(new, PhantomData))) + } } fn drop(self, who: &A) -> Result<(), DispatchError> { F::release(&R::get(), who, self.0, BestEffort).map(|_| ()) @@ -291,22 +312,34 @@ impl< MaxEncodedLen, RuntimeDebugNoBound, )] -#[scale_info(skip_type_params(A, Fx, Rx, D))] +#[scale_info(skip_type_params(A, Fx, Rx, D, Fp))] #[codec(mel_bound())] -pub struct LoneFreezeConsideration(PhantomData (A, Fx, Rx, D)>); +pub struct LoneFreezeConsideration(PhantomData (A, Fx, Rx, D, Fp)>); impl< A: 'static, Fx: 'static + MutateFreeze, Rx: 'static + Get, - D: 'static + Convert, - > Consideration for LoneFreezeConsideration + D: 'static + Convert, + Fp: 'static, + > Consideration for LoneFreezeConsideration { - fn new(who: &A, footprint: Footprint) -> Result { + fn new(who: &A, footprint: Fp) -> Result, DispatchError> { ensure!(Fx::balance_frozen(&Rx::get(), who).is_zero(), DispatchError::Unavailable); - Fx::set_frozen(&Rx::get(), who, D::convert(footprint), Polite).map(|_| Self(PhantomData)) + let new = D::convert(footprint); + if new.is_zero() { + Ok(None) + } else { + Fx::set_frozen(&Rx::get(), who, new, Polite).map(|_| Some(Self(PhantomData))) + } } - fn update(self, who: &A, footprint: Footprint) -> Result { - Fx::set_frozen(&Rx::get(), who, D::convert(footprint), Polite).map(|_| Self(PhantomData)) + fn update(self, who: &A, footprint: Fp) -> Result, DispatchError> { + let new = D::convert(footprint); + let _ = Fx::set_frozen(&Rx::get(), who, new, Polite)?; + if new.is_zero() { + Ok(None) + } else { + Ok(Some(Self(PhantomData))) + } } fn drop(self, who: &A) -> Result<(), DispatchError> { Fx::thaw(&Rx::get(), who).map(|_| ()) @@ -330,22 +363,34 @@ impl< MaxEncodedLen, RuntimeDebugNoBound, )] -#[scale_info(skip_type_params(A, Fx, Rx, D))] +#[scale_info(skip_type_params(A, Fx, Rx, D, Fp))] #[codec(mel_bound())] -pub struct LoneHoldConsideration(PhantomData (A, Fx, Rx, D)>); +pub struct LoneHoldConsideration(PhantomData (A, Fx, Rx, D, Fp)>); impl< A: 'static, F: 'static + MutateHold, R: 'static + Get, - D: 'static + Convert, - > Consideration for LoneHoldConsideration + D: 'static + Convert, + Fp: 'static, + > Consideration for LoneHoldConsideration { - fn new(who: &A, footprint: Footprint) -> Result { + fn new(who: &A, footprint: Fp) -> Result, DispatchError> { ensure!(F::balance_on_hold(&R::get(), who).is_zero(), DispatchError::Unavailable); - F::set_on_hold(&R::get(), who, D::convert(footprint)).map(|_| Self(PhantomData)) + let new = D::convert(footprint); + if new.is_zero() { + Ok(None) + } else { + F::set_on_hold(&R::get(), who, new).map(|_| Some(Self(PhantomData))) + } } - fn update(self, who: &A, footprint: Footprint) -> Result { - F::set_on_hold(&R::get(), who, D::convert(footprint)).map(|_| Self(PhantomData)) + fn update(self, who: &A, footprint: Fp) -> Result, DispatchError> { + let new = D::convert(footprint); + let _ = F::set_on_hold(&R::get(), who, new)?; + if new.is_zero() { + Ok(None) + } else { + Ok(Some(Self(PhantomData))) + } } fn drop(self, who: &A) -> Result<(), DispatchError> { F::release_all(&R::get(), who, BestEffort).map(|_| ())