mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-18 15:21:05 +00:00
Named reserve (#7778)
* add NamedReservableCurrency * move currency related trait and types into a new file * implement NamedReservableCurrency * remove empty reserves * Update frame/support/src/traits.rs Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com> * fix build * bump year * add MaxReserves * repatriate_reserved_named should put reserved fund into named reserved * add tests * add some docs * fix warning * Update lib.rs * fix test * fix test * fix * fix * triggier CI * Move NamedReservableCurrency. * Use strongly bounded vec for reserves. * Fix test. * remove duplicated file * trigger CI * Make `ReserveIdentifier` assosicated type * add helpers * make ReserveIdentifier assosicated type * fix * update * trigger CI * Apply suggestions from code review Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com> * trigger CI * Apply suggestions from code review Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com> Co-authored-by: Gavin Wood <i@gavwood.com> Co-authored-by: Shaun Wang <spxwang@gmail.com>
This commit is contained in:
@@ -73,6 +73,7 @@
|
||||
//! - [`Currency`](frame_support::traits::Currency): Functions for dealing with a
|
||||
//! fungible assets system.
|
||||
//! - [`ReservableCurrency`](frame_support::traits::ReservableCurrency):
|
||||
//! - [`NamedReservableCurrency`](frame_support::traits::NamedReservableCurrency):
|
||||
//! Functions for dealing with assets that can be reserved from an account.
|
||||
//! - [`LockableCurrency`](frame_support::traits::LockableCurrency): Functions for
|
||||
//! dealing with accounts that allow liquidity restrictions.
|
||||
@@ -163,9 +164,9 @@ use frame_support::{
|
||||
traits::{
|
||||
Currency, OnUnbalanced, TryDrop, StoredMap, MaxEncodedLen,
|
||||
WithdrawReasons, LockIdentifier, LockableCurrency, ExistenceRequirement,
|
||||
Imbalance, SignedImbalance, ReservableCurrency, Get, ExistenceRequirement::KeepAlive,
|
||||
ExistenceRequirement::AllowDeath,
|
||||
tokens::{fungible, DepositConsequence, WithdrawConsequence, BalanceStatus as Status}
|
||||
Imbalance, SignedImbalance, ReservableCurrency, Get, ExistenceRequirement::{AllowDeath, KeepAlive},
|
||||
NamedReservableCurrency,
|
||||
tokens::{fungible, DepositConsequence, WithdrawConsequence, BalanceStatus as Status},
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "std")]
|
||||
@@ -214,6 +215,12 @@ pub mod pallet {
|
||||
/// The maximum number of locks that should exist on an account.
|
||||
/// Not strictly enforced, but used for weight estimation.
|
||||
type MaxLocks: Get<u32>;
|
||||
|
||||
/// The maximum number of named reserves that can exist on an account.
|
||||
type MaxReserves: Get<u32>;
|
||||
|
||||
/// The id type for named reserves.
|
||||
type ReserveIdentifier: Parameter + Member + MaxEncodedLen + Ord + Copy;
|
||||
}
|
||||
|
||||
#[pallet::pallet]
|
||||
@@ -409,6 +416,8 @@ pub mod pallet {
|
||||
ExistingVestingSchedule,
|
||||
/// Beneficiary account must pre-exist
|
||||
DeadAccount,
|
||||
/// Number of named reserves exceed MaxReserves
|
||||
TooManyReserves,
|
||||
}
|
||||
|
||||
/// The total units issued in the system.
|
||||
@@ -444,6 +453,17 @@ pub mod pallet {
|
||||
ConstU32<300_000>,
|
||||
>;
|
||||
|
||||
/// Named reserves on some account balances.
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn reserves)]
|
||||
pub type Reserves<T: Config<I>, I: 'static = ()> = StorageMap<
|
||||
_,
|
||||
Blake2_128Concat,
|
||||
T::AccountId,
|
||||
BoundedVec<ReserveData<T::ReserveIdentifier, T::Balance>, T::MaxReserves>,
|
||||
ValueQuery
|
||||
>;
|
||||
|
||||
/// Storage version of the pallet.
|
||||
///
|
||||
/// This is set to v2.0.0 for new networks.
|
||||
@@ -560,6 +580,15 @@ pub struct BalanceLock<Balance> {
|
||||
pub reasons: Reasons,
|
||||
}
|
||||
|
||||
/// Store named reserved balance.
|
||||
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen)]
|
||||
pub struct ReserveData<ReserveIdentifier, Balance> {
|
||||
/// The identifier for the named reserve.
|
||||
pub id: ReserveIdentifier,
|
||||
/// The amount of the named reserve.
|
||||
pub amount: Balance,
|
||||
}
|
||||
|
||||
/// All balance information for an account.
|
||||
#[derive(Encode, Decode, Clone, PartialEq, Eq, Default, RuntimeDebug, MaxEncodedLen)]
|
||||
pub struct AccountData<Balance> {
|
||||
@@ -575,6 +604,7 @@ pub struct AccountData<Balance> {
|
||||
///
|
||||
/// This balance is a 'reserve' balance that other subsystems use in order to set aside tokens
|
||||
/// that are still 'owned' by the account holder, but which are suspendable.
|
||||
/// This includes named reserve and unnamed reserve.
|
||||
pub reserved: Balance,
|
||||
/// The amount that `free` may not drop below when withdrawing for *anything except transaction
|
||||
/// fee payment*.
|
||||
@@ -1648,6 +1678,195 @@ impl<T: Config<I>, I: 'static> ReservableCurrency<T::AccountId> for Pallet<T, I>
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> NamedReservableCurrency<T::AccountId> for Pallet<T, I> where
|
||||
T::Balance: MaybeSerializeDeserialize + Debug
|
||||
{
|
||||
type ReserveIdentifier = T::ReserveIdentifier;
|
||||
|
||||
fn reserved_balance_named(id: &Self::ReserveIdentifier, who: &T::AccountId) -> Self::Balance {
|
||||
let reserves = Self::reserves(who);
|
||||
reserves
|
||||
.binary_search_by_key(id, |data| data.id)
|
||||
.map(|index| reserves[index].amount)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Move `value` from the free balance from `who` to a named reserve balance.
|
||||
///
|
||||
/// Is a no-op if value to be reserved is zero.
|
||||
fn reserve_named(id: &Self::ReserveIdentifier, who: &T::AccountId, value: Self::Balance) -> DispatchResult {
|
||||
if value.is_zero() { return Ok(()) }
|
||||
|
||||
Reserves::<T, I>::try_mutate(who, |reserves| -> DispatchResult {
|
||||
match reserves.binary_search_by_key(id, |data| data.id) {
|
||||
Ok(index) => {
|
||||
// this add can't overflow but just to be defensive.
|
||||
reserves[index].amount = reserves[index].amount.saturating_add(value);
|
||||
},
|
||||
Err(index) => {
|
||||
reserves.try_insert(index, ReserveData {
|
||||
id: id.clone(),
|
||||
amount: value
|
||||
}).map_err(|_| Error::<T, I>::TooManyReserves)?;
|
||||
},
|
||||
};
|
||||
<Self as ReservableCurrency<_>>::reserve(who, value)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Unreserve some funds, returning any amount that was unable to be unreserved.
|
||||
///
|
||||
/// Is a no-op if the value to be unreserved is zero.
|
||||
fn unreserve_named(id: &Self::ReserveIdentifier, who: &T::AccountId, value: Self::Balance) -> Self::Balance {
|
||||
if value.is_zero() { return Zero::zero() }
|
||||
|
||||
Reserves::<T, I>::mutate_exists(who, |maybe_reserves| -> Self::Balance {
|
||||
if let Some(reserves) = maybe_reserves.as_mut() {
|
||||
match reserves.binary_search_by_key(id, |data| data.id) {
|
||||
Ok(index) => {
|
||||
let to_change = cmp::min(reserves[index].amount, value);
|
||||
|
||||
let remain = <Self as ReservableCurrency<_>>::unreserve(who, to_change);
|
||||
|
||||
// remain should always be zero but just to be defensive here
|
||||
let actual = to_change.saturating_sub(remain);
|
||||
|
||||
// `actual <= to_change` and `to_change <= amount`; qed;
|
||||
reserves[index].amount -= actual;
|
||||
|
||||
if reserves[index].amount.is_zero() {
|
||||
if reserves.len() == 1 {
|
||||
// no more named reserves
|
||||
*maybe_reserves = None;
|
||||
} else {
|
||||
// remove this named reserve
|
||||
reserves.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
value - actual
|
||||
},
|
||||
Err(_) => {
|
||||
value
|
||||
},
|
||||
}
|
||||
} else {
|
||||
value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Slash from reserved balance, returning the negative imbalance created,
|
||||
/// and any amount that was unable to be slashed.
|
||||
///
|
||||
/// Is a no-op if the value to be slashed is zero.
|
||||
fn slash_reserved_named(
|
||||
id: &Self::ReserveIdentifier,
|
||||
who: &T::AccountId,
|
||||
value: Self::Balance
|
||||
) -> (Self::NegativeImbalance, Self::Balance) {
|
||||
if value.is_zero() { return (NegativeImbalance::zero(), Zero::zero()) }
|
||||
|
||||
Reserves::<T, I>::mutate(who, |reserves| -> (Self::NegativeImbalance, Self::Balance) {
|
||||
match reserves.binary_search_by_key(id, |data| data.id) {
|
||||
Ok(index) => {
|
||||
let to_change = cmp::min(reserves[index].amount, value);
|
||||
|
||||
let (imb, remain) = <Self as ReservableCurrency<_>>::slash_reserved(who, to_change);
|
||||
|
||||
// remain should always be zero but just to be defensive here
|
||||
let actual = to_change.saturating_sub(remain);
|
||||
|
||||
// `actual <= to_change` and `to_change <= amount`; qed;
|
||||
reserves[index].amount -= actual;
|
||||
|
||||
(imb, value - actual)
|
||||
},
|
||||
Err(_) => {
|
||||
(NegativeImbalance::zero(), value)
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Move the reserved balance of one account into the balance of another, according to `status`.
|
||||
/// If `status` is `Reserved`, the balance will be reserved with given `id`.
|
||||
///
|
||||
/// Is a no-op if:
|
||||
/// - the value to be moved is zero; or
|
||||
/// - the `slashed` id equal to `beneficiary` and the `status` is `Reserved`.
|
||||
fn repatriate_reserved_named(
|
||||
id: &Self::ReserveIdentifier,
|
||||
slashed: &T::AccountId,
|
||||
beneficiary: &T::AccountId,
|
||||
value: Self::Balance,
|
||||
status: Status,
|
||||
) -> Result<Self::Balance, DispatchError> {
|
||||
if value.is_zero() { return Ok(Zero::zero()) }
|
||||
|
||||
if slashed == beneficiary {
|
||||
return match status {
|
||||
Status::Free => Ok(Self::unreserve_named(id, slashed, value)),
|
||||
Status::Reserved => Ok(value.saturating_sub(Self::reserved_balance_named(id, slashed))),
|
||||
};
|
||||
}
|
||||
|
||||
Reserves::<T, I>::try_mutate(slashed, |reserves| -> Result<Self::Balance, DispatchError> {
|
||||
match reserves.binary_search_by_key(id, |data| data.id) {
|
||||
Ok(index) => {
|
||||
let to_change = cmp::min(reserves[index].amount, value);
|
||||
|
||||
let actual = if status == Status::Reserved {
|
||||
// make it the reserved under same identifier
|
||||
Reserves::<T, I>::try_mutate(beneficiary, |reserves| -> Result<T::Balance, DispatchError> {
|
||||
match reserves.binary_search_by_key(id, |data| data.id) {
|
||||
Ok(index) => {
|
||||
let remain = <Self as ReservableCurrency<_>>::repatriate_reserved(slashed, beneficiary, to_change, status)?;
|
||||
|
||||
// remain should always be zero but just to be defensive here
|
||||
let actual = to_change.saturating_sub(remain);
|
||||
|
||||
// this add can't overflow but just to be defensive.
|
||||
reserves[index].amount = reserves[index].amount.saturating_add(actual);
|
||||
|
||||
Ok(actual)
|
||||
},
|
||||
Err(index) => {
|
||||
let remain = <Self as ReservableCurrency<_>>::repatriate_reserved(slashed, beneficiary, to_change, status)?;
|
||||
|
||||
// remain should always be zero but just to be defensive here
|
||||
let actual = to_change.saturating_sub(remain);
|
||||
|
||||
reserves.try_insert(index, ReserveData {
|
||||
id: id.clone(),
|
||||
amount: actual
|
||||
}).map_err(|_| Error::<T, I>::TooManyReserves)?;
|
||||
|
||||
Ok(actual)
|
||||
},
|
||||
}
|
||||
})?
|
||||
} else {
|
||||
let remain = <Self as ReservableCurrency<_>>::repatriate_reserved(slashed, beneficiary, to_change, status)?;
|
||||
|
||||
// remain should always be zero but just to be defensive here
|
||||
to_change.saturating_sub(remain)
|
||||
};
|
||||
|
||||
// `actual <= to_change` and `to_change <= amount`; qed;
|
||||
reserves[index].amount -= actual;
|
||||
|
||||
Ok(value - actual)
|
||||
},
|
||||
Err(_) => {
|
||||
Ok(value)
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> LockableCurrency<T::AccountId> for Pallet<T, I>
|
||||
where
|
||||
T::Balance: MaybeSerializeDeserialize + Debug
|
||||
|
||||
@@ -964,5 +964,123 @@ macro_rules! decl_tests {
|
||||
assert_eq!(Balances::total_balance(&2), 100);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_reserve_should_work() {
|
||||
<$ext_builder>::default().build().execute_with(|| {
|
||||
let _ = Balances::deposit_creating(&1, 111);
|
||||
|
||||
let id_1 = [1u8; 8];
|
||||
let id_2 = [2u8; 8];
|
||||
let id_3 = [3u8; 8];
|
||||
|
||||
// reserve
|
||||
|
||||
assert_noop!(Balances::reserve_named(&id_1, &1, 112), Error::<Test, _>::InsufficientBalance);
|
||||
|
||||
assert_ok!(Balances::reserve_named(&id_1, &1, 12));
|
||||
|
||||
assert_eq!(Balances::reserved_balance(1), 12);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_1, &1), 12);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_2, &1), 0);
|
||||
|
||||
assert_ok!(Balances::reserve_named(&id_1, &1, 2));
|
||||
|
||||
assert_eq!(Balances::reserved_balance(1), 14);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_1, &1), 14);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_2, &1), 0);
|
||||
|
||||
assert_ok!(Balances::reserve_named(&id_2, &1, 23));
|
||||
|
||||
assert_eq!(Balances::reserved_balance(1), 37);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_1, &1), 14);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_2, &1), 23);
|
||||
|
||||
assert_ok!(Balances::reserve(&1, 34));
|
||||
|
||||
assert_eq!(Balances::reserved_balance(1), 71);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_1, &1), 14);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_2, &1), 23);
|
||||
|
||||
assert_eq!(Balances::total_balance(&1), 111);
|
||||
assert_eq!(Balances::free_balance(1), 40);
|
||||
|
||||
assert_noop!(Balances::reserve_named(&id_3, &1, 2), Error::<Test, _>::TooManyReserves);
|
||||
|
||||
// unreserve
|
||||
|
||||
assert_eq!(Balances::unreserve_named(&id_1, &1, 10), 0);
|
||||
|
||||
assert_eq!(Balances::reserved_balance(1), 61);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_1, &1), 4);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_2, &1), 23);
|
||||
|
||||
assert_eq!(Balances::unreserve_named(&id_1, &1, 5), 1);
|
||||
|
||||
assert_eq!(Balances::reserved_balance(1), 57);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_1, &1), 0);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_2, &1), 23);
|
||||
|
||||
assert_eq!(Balances::unreserve_named(&id_2, &1, 3), 0);
|
||||
|
||||
assert_eq!(Balances::reserved_balance(1), 54);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_1, &1), 0);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_2, &1), 20);
|
||||
|
||||
assert_eq!(Balances::total_balance(&1), 111);
|
||||
assert_eq!(Balances::free_balance(1), 57);
|
||||
|
||||
// slash_reserved_named
|
||||
|
||||
assert_ok!(Balances::reserve_named(&id_1, &1, 10));
|
||||
|
||||
assert_eq!(Balances::slash_reserved_named(&id_1, &1, 25).1, 15);
|
||||
|
||||
assert_eq!(Balances::reserved_balance(1), 54);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_1, &1), 0);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_2, &1), 20);
|
||||
assert_eq!(Balances::total_balance(&1), 101);
|
||||
|
||||
assert_eq!(Balances::slash_reserved_named(&id_2, &1, 5).1, 0);
|
||||
|
||||
assert_eq!(Balances::reserved_balance(1), 49);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_1, &1), 0);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_2, &1), 15);
|
||||
assert_eq!(Balances::total_balance(&1), 96);
|
||||
|
||||
// repatriate_reserved_named
|
||||
|
||||
let _ = Balances::deposit_creating(&2, 100);
|
||||
|
||||
assert_eq!(Balances::repatriate_reserved_named(&id_2, &1, &2, 10, Status::Reserved).unwrap(), 0);
|
||||
|
||||
assert_eq!(Balances::reserved_balance_named(&id_2, &1), 5);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_2, &2), 10);
|
||||
assert_eq!(Balances::reserved_balance(&2), 10);
|
||||
|
||||
assert_eq!(Balances::repatriate_reserved_named(&id_2, &2, &1, 11, Status::Reserved).unwrap(), 1);
|
||||
|
||||
assert_eq!(Balances::reserved_balance_named(&id_2, &1), 15);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_2, &2), 0);
|
||||
assert_eq!(Balances::reserved_balance(&2), 0);
|
||||
|
||||
assert_eq!(Balances::repatriate_reserved_named(&id_2, &1, &2, 10, Status::Free).unwrap(), 0);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_2, &1), 5);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_2, &2), 0);
|
||||
assert_eq!(Balances::free_balance(&2), 110);
|
||||
|
||||
// repatriate_reserved_named to self
|
||||
|
||||
assert_eq!(Balances::repatriate_reserved_named(&id_2, &1, &1, 10, Status::Reserved).unwrap(), 5);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_2, &1), 5);
|
||||
|
||||
assert_eq!(Balances::free_balance(&1), 47);
|
||||
|
||||
assert_eq!(Balances::repatriate_reserved_named(&id_2, &1, &1, 15, Status::Free).unwrap(), 10);
|
||||
assert_eq!(Balances::reserved_balance_named(&id_2, &1), 0);
|
||||
|
||||
assert_eq!(Balances::free_balance(&1), 52);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,10 @@ impl pallet_transaction_payment::Config for Test {
|
||||
type FeeMultiplierUpdate = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const MaxReserves: u32 = 2;
|
||||
}
|
||||
|
||||
impl Config for Test {
|
||||
type Balance = u64;
|
||||
type DustRemoval = ();
|
||||
@@ -94,6 +98,8 @@ impl Config for Test {
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type AccountStore = frame_system::Pallet<Test>;
|
||||
type MaxLocks = ();
|
||||
type MaxReserves = MaxReserves;
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ impl pallet_transaction_payment::Config for Test {
|
||||
}
|
||||
parameter_types! {
|
||||
pub const MaxLocks: u32 = 50;
|
||||
pub const MaxReserves: u32 = 2;
|
||||
}
|
||||
impl Config for Test {
|
||||
type Balance = u64;
|
||||
@@ -103,6 +104,8 @@ impl Config for Test {
|
||||
super::AccountData<u64>,
|
||||
>;
|
||||
type MaxLocks = MaxLocks;
|
||||
type MaxReserves = MaxReserves;
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ impl OnUnbalanced<NegativeImbalance<Test>> for OnDustRemoval {
|
||||
}
|
||||
parameter_types! {
|
||||
pub const MaxLocks: u32 = 50;
|
||||
pub const MaxReserves: u32 = 2;
|
||||
}
|
||||
impl Config for Test {
|
||||
type Balance = u64;
|
||||
@@ -119,6 +120,8 @@ impl Config for Test {
|
||||
super::AccountData<u64>,
|
||||
>;
|
||||
type MaxLocks = MaxLocks;
|
||||
type MaxReserves = MaxReserves;
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user