diff --git a/currencies/src/mock.rs b/currencies/src/mock.rs index b5c9258e9..1c1a0ff62 100644 --- a/currencies/src/mock.rs +++ b/currencies/src/mock.rs @@ -80,6 +80,8 @@ impl orml_tokens::Config for Runtime { type ExistentialDeposits = ExistentialDeposits; type OnDust = orml_tokens::TransferDust; type MaxLocks = ConstU32<100_000>; + type MaxReserves = ConstU32<100_000>; + type ReserveIdentifier = [u8; 8]; type DustRemovalWhitelist = Nothing; } diff --git a/tokens/src/lib.rs b/tokens/src/lib.rs index 3c47326ba..c590e526a 100644 --- a/tokens/src/lib.rs +++ b/tokens/src/lib.rs @@ -46,9 +46,10 @@ use frame_support::{ pallet_prelude::*, traits::{ tokens::{fungible, fungibles, DepositConsequence, WithdrawConsequence}, - BalanceStatus as Status, Contains, Currency as PalletCurrency, ExistenceRequirement, Get, Imbalance, - LockableCurrency as PalletLockableCurrency, ReservableCurrency as PalletReservableCurrency, SignedImbalance, - WithdrawReasons, + BalanceStatus as Status, Contains, Currency as PalletCurrency, DefensiveSaturating, ExistenceRequirement, Get, + Imbalance, LockableCurrency as PalletLockableCurrency, + NamedReservableCurrency as PalletNamedReservableCurrency, ReservableCurrency as PalletReservableCurrency, + SignedImbalance, WithdrawReasons, }, transactional, BoundedVec, }; @@ -61,13 +62,13 @@ use sp_runtime::{ }, ArithmeticError, DispatchError, DispatchResult, RuntimeDebug, }; -use sp_std::{convert::Infallible, marker, prelude::*, vec::Vec}; +use sp_std::{cmp, convert::Infallible, marker, prelude::*, vec::Vec}; use orml_traits::{ arithmetic::{self, Signed}, currency::TransferAll, BalanceStatus, GetByKey, LockIdentifier, MultiCurrency, MultiCurrencyExtended, MultiLockableCurrency, - MultiReservableCurrency, OnDust, + MultiReservableCurrency, NamedMultiReservableCurrency, OnDust, }; mod imbalances; @@ -119,6 +120,15 @@ pub struct BalanceLock { pub amount: Balance, } +/// Store named reserved balance. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct ReserveData { + /// The identifier for the named reserve. + pub id: ReserveIdentifier, + /// The amount of the named reserve. + pub amount: Balance, +} + /// balance information for an account. #[derive(Encode, Decode, Clone, PartialEq, Eq, Default, MaxEncodedLen, RuntimeDebug, TypeInfo)] pub struct AccountData { @@ -204,6 +214,13 @@ pub mod module { #[pallet::constant] type MaxLocks: Get; + /// The maximum number of named reserves that can exist on an account. + #[pallet::constant] + type MaxReserves: Get; + + /// The id type for named reserves. + type ReserveIdentifier: Parameter + Member + MaxEncodedLen + Ord + Copy; + // The whitelist of accounts that will not be reaped even if its total // is zero or below ED. type DustRemovalWhitelist: Contains; @@ -225,6 +242,8 @@ pub mod module { ExistentialDeposit, /// Beneficiary account must pre-exist DeadAccount, + // Number of named reserves exceed `T::MaxReserves` + TooManyReserves, } #[pallet::event] @@ -354,6 +373,19 @@ pub mod module { ValueQuery, >; + /// Named reserves on some account balances. + #[pallet::storage] + #[pallet::getter(fn reserves)] + pub type Reserves = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, + Twox64Concat, + T::CurrencyId, + BoundedVec, T::MaxReserves>, + ValueQuery, + >; + #[pallet::genesis_config] pub struct GenesisConfig { pub balances: Vec<(T::AccountId, T::CurrencyId, T::Balance)>, @@ -1351,6 +1383,253 @@ impl MultiReservableCurrency for Pallet { } } +impl NamedMultiReservableCurrency for Pallet { + type ReserveIdentifier = T::ReserveIdentifier; + + fn reserved_balance_named( + id: &Self::ReserveIdentifier, + currency_id: Self::CurrencyId, + who: &T::AccountId, + ) -> Self::Balance { + let reserves = Self::reserves(who, currency_id); + 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, + currency_id: Self::CurrencyId, + who: &T::AccountId, + value: Self::Balance, + ) -> DispatchResult { + if value.is_zero() { + return Ok(()); + } + + Reserves::::try_mutate(who, currency_id, |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.defensive_saturating_add(value); + } + Err(index) => { + reserves + .try_insert(index, ReserveData { id: *id, amount: value }) + .map_err(|_| Error::::TooManyReserves)?; + } + }; + >::reserve(currency_id, who, value) + }) + } + + /// 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, + currency_id: Self::CurrencyId, + who: &T::AccountId, + value: Self::Balance, + ) -> Self::Balance { + if value.is_zero() { + return Zero::zero(); + } + + Reserves::::mutate_exists(who, currency_id, |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 = >::unreserve(currency_id, who, to_change); + + // remain should always be zero but just to be defensive here. + let actual = to_change.defensive_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, + currency_id: Self::CurrencyId, + who: &T::AccountId, + value: Self::Balance, + ) -> Self::Balance { + if value.is_zero() { + return Zero::zero(); + } + + Reserves::::mutate(who, currency_id, |reserves| -> Self::Balance { + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + let to_change = cmp::min(reserves[index].amount, value); + + let remain = >::slash_reserved(currency_id, who, to_change); + + // remain should always be zero but just to be defensive here. + let actual = to_change.defensive_saturating_sub(remain); + + // `actual <= to_change` and `to_change <= amount`; qed; + reserves[index].amount -= actual; + + Self::deposit_event(Event::Slashed { + who: who.clone(), + currency_id, + free_amount: Zero::zero(), + reserved_amount: actual, + }); + value - actual + } + Err(_) => 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, + currency_id: Self::CurrencyId, + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: Self::Balance, + status: Status, + ) -> Result { + if value.is_zero() { + return Ok(Zero::zero()); + } + + if slashed == beneficiary { + return match status { + Status::Free => Ok(Self::unreserve_named(id, currency_id, slashed, value)), + Status::Reserved => Ok(value.saturating_sub(Self::reserved_balance_named(id, currency_id, slashed))), + }; + } + + Reserves::::try_mutate( + slashed, + currency_id, + |reserves| -> Result { + 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::::try_mutate( + beneficiary, + currency_id, + |reserves| -> Result { + match reserves.binary_search_by_key(id, |data| data.id) { + Ok(index) => { + let remain = >::repatriate_reserved( + currency_id, + slashed, + beneficiary, + to_change, + status, + )?; + + // remain should always be zero but just to be defensive + // here. + let actual = to_change.defensive_saturating_sub(remain); + + // this add can't overflow but just to be defensive. + reserves[index].amount = + reserves[index].amount.defensive_saturating_add(actual); + + Ok(actual) + } + Err(index) => { + let remain = >::repatriate_reserved( + currency_id, + slashed, + beneficiary, + to_change, + status, + )?; + + // remain should always be zero but just to be defensive + // here + let actual = to_change.defensive_saturating_sub(remain); + + reserves + .try_insert( + index, + ReserveData { + id: *id, + amount: actual, + }, + ) + .map_err(|_| Error::::TooManyReserves)?; + + Ok(actual) + } + } + }, + )? + } else { + let remain = >::repatriate_reserved( + currency_id, + slashed, + beneficiary, + to_change, + status, + )?; + + // remain should always be zero but just to be defensive here + to_change.defensive_saturating_sub(remain) + }; + + // `actual <= to_change` and `to_change <= amount`; qed; + reserves[index].amount -= actual; + + Ok(value - actual) + } + Err(_) => Ok(value), + } + }, + ) + } +} + impl fungibles::Inspect for Pallet { type AssetId = T::CurrencyId; type Balance = T::Balance; @@ -1769,6 +2048,53 @@ where } } +impl PalletNamedReservableCurrency for CurrencyAdapter +where + T: Config, + GetCurrencyId: Get, +{ + type ReserveIdentifier = T::ReserveIdentifier; + + fn reserved_balance_named(id: &Self::ReserveIdentifier, who: &T::AccountId) -> Self::Balance { + as NamedMultiReservableCurrency<_>>::reserved_balance_named(id, GetCurrencyId::get(), who) + } + + fn reserve_named(id: &Self::ReserveIdentifier, who: &T::AccountId, value: Self::Balance) -> DispatchResult { + as NamedMultiReservableCurrency<_>>::reserve_named(id, GetCurrencyId::get(), who, value) + } + + fn unreserve_named(id: &Self::ReserveIdentifier, who: &T::AccountId, value: Self::Balance) -> Self::Balance { + as NamedMultiReservableCurrency<_>>::unreserve_named(id, GetCurrencyId::get(), who, value) + } + + fn slash_reserved_named( + id: &Self::ReserveIdentifier, + who: &T::AccountId, + value: Self::Balance, + ) -> (Self::NegativeImbalance, Self::Balance) { + let actual = + as NamedMultiReservableCurrency<_>>::slash_reserved_named(id, GetCurrencyId::get(), who, value); + (Self::NegativeImbalance::zero(), actual) + } + + fn repatriate_reserved_named( + id: &Self::ReserveIdentifier, + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: Self::Balance, + status: Status, + ) -> sp_std::result::Result { + as NamedMultiReservableCurrency<_>>::repatriate_reserved_named( + id, + GetCurrencyId::get(), + slashed, + beneficiary, + value, + status, + ) + } +} + impl PalletLockableCurrency for CurrencyAdapter where T: Config, diff --git a/tokens/src/mock.rs b/tokens/src/mock.rs index c14617184..7be814521 100644 --- a/tokens/src/mock.rs +++ b/tokens/src/mock.rs @@ -23,6 +23,7 @@ use sp_std::cell::RefCell; pub type AccountId = AccountId32; pub type CurrencyId = u32; pub type Balance = u64; +pub type ReserveIdentifier = [u8; 8]; pub const DOT: CurrencyId = 1; pub const BTC: CurrencyId = 2; @@ -35,6 +36,8 @@ pub const TREASURY_ACCOUNT: AccountId = AccountId32::new([4u8; 32]); pub const ID_1: LockIdentifier = *b"1 "; pub const ID_2: LockIdentifier = *b"2 "; pub const ID_3: LockIdentifier = *b"3 "; +pub const RID_1: ReserveIdentifier = [1u8; 8]; +pub const RID_2: ReserveIdentifier = [2u8; 8]; use crate as tokens; @@ -230,6 +233,8 @@ impl Config for Runtime { type ExistentialDeposits = ExistentialDeposits; type OnDust = TransferDust; type MaxLocks = ConstU32<2>; + type MaxReserves = ConstU32<2>; + type ReserveIdentifier = ReserveIdentifier; type DustRemovalWhitelist = MockDustRemovalWhitelist; } pub type TreasuryCurrencyAdapter = ::Currency; diff --git a/tokens/src/tests.rs b/tokens/src/tests.rs index 698a997b6..864c9f59f 100644 --- a/tokens/src/tests.rs +++ b/tokens/src/tests.rs @@ -1540,6 +1540,239 @@ fn transfer_all_trait_should_work() { }); } +#[test] +fn named_multi_reservable_currency_slash_reserved_work() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100)]) + .build() + .execute_with(|| { + assert_ok!(Tokens::reserve_named(&RID_1, DOT, &ALICE, 50)); + assert_eq!(Tokens::free_balance(DOT, &ALICE), 50); + assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 50); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &ALICE), 50); + assert_eq!(Tokens::total_issuance(DOT), 100); + assert_eq!(Tokens::slash_reserved_named(&RID_1, DOT, &ALICE, 0), 0); + assert_eq!(Tokens::free_balance(DOT, &ALICE), 50); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &ALICE), 50); + assert_eq!(Tokens::total_issuance(DOT), 100); + assert_eq!(Tokens::slash_reserved_named(&RID_1, DOT, &ALICE, 100), 50); + assert_eq!(Tokens::free_balance(DOT, &ALICE), 50); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &ALICE), 0); + assert_eq!(Tokens::total_issuance(DOT), 50); + }); +} + +#[test] +fn named_multi_reservable_currency_reserve_work() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100)]) + .build() + .execute_with(|| { + assert_noop!( + Tokens::reserve_named(&RID_1, DOT, &ALICE, 101), + Error::::BalanceTooLow + ); + assert_ok!(Tokens::reserve_named(&RID_1, DOT, &ALICE, 0)); + assert_eq!(Tokens::free_balance(DOT, &ALICE), 100); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &ALICE), 0); + assert_eq!(Tokens::total_balance(DOT, &ALICE), 100); + assert_ok!(Tokens::reserve_named(&RID_1, DOT, &ALICE, 50)); + System::assert_last_event(Event::Tokens(crate::Event::Reserved { + currency_id: DOT, + who: ALICE, + amount: 50, + })); + assert_eq!(Tokens::free_balance(DOT, &ALICE), 50); + assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 50); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &ALICE), 50); + assert_eq!(Tokens::total_balance(DOT, &ALICE), 100); + + assert_ok!(Tokens::reserve_named(&RID_2, DOT, &ALICE, 50)); + System::assert_last_event(Event::Tokens(crate::Event::Reserved { + currency_id: DOT, + who: ALICE, + amount: 50, + })); + + assert_eq!(Tokens::free_balance(DOT, &ALICE), 0); + assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 100); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &ALICE), 50); + assert_eq!(Tokens::reserved_balance_named(&RID_2, DOT, &ALICE), 50); + assert_eq!(Tokens::total_balance(DOT, &ALICE), 100); + + // ensure will not trigger Endowed event + assert!(System::events().iter().all(|record| !matches!( + record.event, + Event::Tokens(crate::Event::Endowed { + currency_id: DOT, + who: ALICE, + amount: _ + }) + ))); + }); +} + +#[test] +fn named_multi_reservable_currency_unreserve_work() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100)]) + .build() + .execute_with(|| { + assert_eq!(Tokens::free_balance(DOT, &ALICE), 100); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &ALICE), 0); + assert_eq!(Tokens::unreserve_named(&RID_1, DOT, &ALICE, 0), 0); + + assert_ok!(Tokens::reserve_named(&RID_1, DOT, &ALICE, 30)); + System::assert_last_event(Event::Tokens(crate::Event::Reserved { + currency_id: DOT, + who: ALICE, + amount: 30, + })); + assert_eq!(Tokens::free_balance(DOT, &ALICE), 70); + assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 30); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &ALICE), 30); + + assert_ok!(Tokens::reserve_named(&RID_2, DOT, &ALICE, 30)); + System::assert_last_event(Event::Tokens(crate::Event::Reserved { + currency_id: DOT, + who: ALICE, + amount: 30, + })); + assert_eq!(Tokens::free_balance(DOT, &ALICE), 40); + assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 60); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &ALICE), 30); + assert_eq!(Tokens::reserved_balance_named(&RID_2, DOT, &ALICE), 30); + + assert_eq!(Tokens::unreserve_named(&RID_1, DOT, &ALICE, 30), 0); + System::assert_last_event(Event::Tokens(crate::Event::Unreserved { + currency_id: DOT, + who: ALICE, + amount: 30, + })); + assert_eq!(Tokens::free_balance(DOT, &ALICE), 70); + assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 30); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &ALICE), 0); + assert_eq!(Tokens::reserved_balance_named(&RID_2, DOT, &ALICE), 30); + + assert_eq!(Tokens::unreserve_named(&RID_2, DOT, &ALICE, 30), 0); + System::assert_last_event(Event::Tokens(crate::Event::Unreserved { + currency_id: DOT, + who: ALICE, + amount: 30, + })); + assert_eq!(Tokens::free_balance(DOT, &ALICE), 100); + assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 0); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &ALICE), 0); + assert_eq!(Tokens::reserved_balance_named(&RID_2, DOT, &ALICE), 0); + // ensure will not trigger Endowed event + assert!(System::events().iter().all(|record| !matches!( + record.event, + Event::Tokens(crate::Event::Endowed { + currency_id: DOT, + who: ALICE, + amount: _ + }) + ))); + }); +} + +#[test] +fn named_multi_reservable_currency_repatriate_reserved_work() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100), (BOB, DOT, 100)]) + .build() + .execute_with(|| { + assert_eq!(Tokens::free_balance(DOT, &ALICE), 100); + assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 0); + assert_eq!( + Tokens::repatriate_reserved_named(&RID_1, DOT, &ALICE, &ALICE, 0, BalanceStatus::Free), + Ok(0) + ); + assert_eq!( + Tokens::repatriate_reserved_named(&RID_1, DOT, &ALICE, &ALICE, 50, BalanceStatus::Free), + Ok(50) + ); + + assert_eq!(Tokens::free_balance(DOT, &ALICE), 100); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &ALICE), 0); + + assert_eq!(Tokens::free_balance(DOT, &BOB), 100); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &BOB), 0); + assert_ok!(Tokens::reserve_named(&RID_1, DOT, &BOB, 50)); + assert_eq!(Tokens::free_balance(DOT, &BOB), 50); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &BOB), 50); + assert_eq!( + Tokens::repatriate_reserved_named(&RID_1, DOT, &BOB, &BOB, 60, BalanceStatus::Reserved), + Ok(10) + ); + + assert_eq!(Tokens::free_balance(DOT, &BOB), 50); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &BOB), 50); + + assert_eq!( + Tokens::repatriate_reserved_named(&RID_1, DOT, &BOB, &ALICE, 30, BalanceStatus::Reserved), + Ok(0) + ); + System::assert_last_event(Event::Tokens(crate::Event::ReserveRepatriated { + currency_id: DOT, + from: BOB, + to: ALICE, + amount: 30, + status: BalanceStatus::Reserved, + })); + + assert_eq!(Tokens::free_balance(DOT, &ALICE), 100); + assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 30); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &ALICE), 30); + assert_eq!(Tokens::free_balance(DOT, &BOB), 50); + assert_eq!(Tokens::reserved_balance(DOT, &BOB), 20); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &BOB), 20); + + assert_eq!( + Tokens::repatriate_reserved_named(&RID_1, DOT, &BOB, &ALICE, 30, BalanceStatus::Free), + Ok(10) + ); + + // Actual amount repatriated is 20. + System::assert_last_event(Event::Tokens(crate::Event::ReserveRepatriated { + currency_id: DOT, + from: BOB, + to: ALICE, + amount: 20, + status: BalanceStatus::Free, + })); + + assert_eq!(Tokens::free_balance(DOT, &ALICE), 120); + assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 30); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &ALICE), 30); + assert_eq!(Tokens::free_balance(DOT, &BOB), 50); + assert_eq!(Tokens::reserved_balance(DOT, &BOB), 0); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &BOB), 0); + }); +} + +#[test] +fn slashed_reserved_named_works() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100)]) + .build() + .execute_with(|| { + assert_ok!(Tokens::reserve_named(&RID_1, DOT, &ALICE, 50)); + assert_eq!(Tokens::free_balance(DOT, &ALICE), 50); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &ALICE), 50); + assert_eq!(Tokens::total_issuance(DOT), 100); + + assert_eq!(Tokens::slash_reserved_named(&RID_1, DOT, &ALICE, 20), 0); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &ALICE), 30); + assert_eq!(Tokens::total_issuance(DOT), 80); + + assert_eq!(Tokens::slash_reserved_named(&RID_1, DOT, &ALICE, 40), 10); + assert_eq!(Tokens::reserved_balance_named(&RID_1, DOT, &ALICE), 0); + assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 0); + assert_eq!(Tokens::total_issuance(DOT), 50); + }); +} + // ************************************************* // tests for CurrencyAdapter // ************************************************* @@ -2119,6 +2352,80 @@ fn exceeding_max_locks_should_fail() { }); } +#[test] +fn currency_adapter_slashing_named_reserved_balance_should_work() { + ExtBuilder::default().build().execute_with(|| { + let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 111); + assert_eq!(TreasuryCurrencyAdapter::total_issuance(), 111); + assert_ok!(TreasuryCurrencyAdapter::reserve_named(&RID_1, &TREASURY_ACCOUNT, 111)); + assert_eq!( + TreasuryCurrencyAdapter::slash_reserved_named(&RID_1, &TREASURY_ACCOUNT, 42).1, + 0 + ); + assert_eq!( + TreasuryCurrencyAdapter::reserved_balance_named(&RID_1, &TREASURY_ACCOUNT), + 69 + ); + assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 0); + assert_eq!(TreasuryCurrencyAdapter::total_issuance(), 69); + }); +} + +#[test] +fn currency_adapter_named_slashing_incomplete_reserved_balance_should_work() { + ExtBuilder::default().build().execute_with(|| { + let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 111); + assert_eq!(TreasuryCurrencyAdapter::total_issuance(), 111); + assert_ok!(TreasuryCurrencyAdapter::reserve_named(&RID_1, &TREASURY_ACCOUNT, 42)); + assert_eq!( + TreasuryCurrencyAdapter::slash_reserved_named(&RID_1, &TREASURY_ACCOUNT, 69).1, + 27 + ); + assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 69); + assert_eq!( + TreasuryCurrencyAdapter::reserved_balance_named(&RID_1, &TREASURY_ACCOUNT), + 0 + ); + assert_eq!(TreasuryCurrencyAdapter::total_issuance(), 69); + }); +} + +#[test] +fn currency_adapter_repatriating_named_reserved_balance_should_work() { + ExtBuilder::default().build().execute_with(|| { + let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 110); + let _ = TreasuryCurrencyAdapter::deposit_creating(&ALICE, 2); + assert_ok!(TreasuryCurrencyAdapter::reserve_named(&RID_1, &TREASURY_ACCOUNT, 110)); + assert_ok!( + TreasuryCurrencyAdapter::repatriate_reserved_named(&RID_1, &TREASURY_ACCOUNT, &ALICE, 41, Status::Free), + 0 + ); + assert_eq!( + TreasuryCurrencyAdapter::reserved_balance_named(&RID_1, &TREASURY_ACCOUNT), + 69 + ); + assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 0); + assert_eq!(TreasuryCurrencyAdapter::reserved_balance_named(&RID_1, &ALICE), 0); + assert_eq!(TreasuryCurrencyAdapter::free_balance(&ALICE), 43); + }); +} + +#[test] +fn exceeding_max_reserves_should_fail() { + ExtBuilder::default() + .balances(vec![(ALICE, DOT, 100)]) + .build() + .execute_with(|| { + let id_3 = [3u8; 8]; + assert_ok!(Tokens::reserve_named(&RID_1, DOT, &ALICE, 10)); + assert_ok!(Tokens::reserve_named(&RID_2, DOT, &ALICE, 10)); + assert_noop!( + Tokens::reserve_named(&id_3, DOT, &ALICE, 10), + Error::::TooManyReserves + ); + }); +} + // ************************************************* // tests for fungibles traits // ************************************************* diff --git a/traits/src/currency.rs b/traits/src/currency.rs index ec590a34f..68c4fc72b 100644 --- a/traits/src/currency.rs +++ b/traits/src/currency.rs @@ -201,6 +201,95 @@ pub trait MultiReservableCurrency: MultiCurrency { ) -> result::Result; } +/// A fungible multi-currency system where funds can be reserved from the user +/// with an identifier. +pub trait NamedMultiReservableCurrency: MultiReservableCurrency { + /// An identifier for a reserve. Used for disambiguating different reserves + /// so that they can be individually replaced or removed. + type ReserveIdentifier; + + /// Deducts up to `value` from reserved balance of `who`. This function + /// cannot fail. + /// + /// As much funds up to `value` will be deducted as possible. If the reserve + /// balance of `who` is less than `value`, then a non-zero second item will + /// be returned. + fn slash_reserved_named( + id: &Self::ReserveIdentifier, + currency_id: Self::CurrencyId, + who: &AccountId, + value: Self::Balance, + ) -> Self::Balance; + + /// The amount of the balance of a given account that is externally + /// reserved; this can still get slashed, but gets slashed last of all. + /// + /// 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. + /// + /// When this balance falls below the value of `ExistentialDeposit`, then + /// this 'reserve account' is deleted: specifically, `ReservedBalance`. + /// + /// `system::AccountNonce` is also deleted if `FreeBalance` is also zero (it + /// also gets collapsed to zero if it ever becomes less than + /// `ExistentialDeposit`. + fn reserved_balance_named( + id: &Self::ReserveIdentifier, + currency_id: Self::CurrencyId, + who: &AccountId, + ) -> Self::Balance; + + /// Moves `value` from balance to reserved balance. + /// + /// If the free balance is lower than `value`, then no funds will be moved + /// and an `Err` will be returned to notify of this. This is different + /// behavior than `unreserve`. + fn reserve_named( + id: &Self::ReserveIdentifier, + currency_id: Self::CurrencyId, + who: &AccountId, + value: Self::Balance, + ) -> DispatchResult; + + /// Moves up to `value` from reserved balance to free balance. This function + /// cannot fail. + /// + /// As much funds up to `value` will be moved as possible. If the reserve + /// balance of `who` is less than `value`, then the remaining amount will be + /// returned. + /// + /// # NOTES + /// + /// - This is different from `reserve`. + /// - If the remaining reserved balance is less than `ExistentialDeposit`, + /// it will + /// invoke `on_reserved_too_low` and could reap the account. + fn unreserve_named( + id: &Self::ReserveIdentifier, + currency_id: Self::CurrencyId, + who: &AccountId, + value: Self::Balance, + ) -> Self::Balance; + + /// Moves up to `value` from reserved balance of account `slashed` to + /// balance of account `beneficiary`. `beneficiary` must exist for this to + /// succeed. If it does not, `Err` will be returned. Funds will be placed in + /// either the `free` balance or the `reserved` balance, depending on the + /// `status`. + /// + /// As much funds up to `value` will be deducted as possible. If this is + /// less than `value`, then `Ok(non_zero)` will be returned. + fn repatriate_reserved_named( + id: &Self::ReserveIdentifier, + currency_id: Self::CurrencyId, + slashed: &AccountId, + beneficiary: &AccountId, + value: Self::Balance, + status: BalanceStatus, + ) -> result::Result; +} + /// Abstraction over a fungible (single) currency system. pub trait BasicCurrency { /// The balance of an account. diff --git a/traits/src/lib.rs b/traits/src/lib.rs index 9274c5a8e..f2642558b 100644 --- a/traits/src/lib.rs +++ b/traits/src/lib.rs @@ -14,7 +14,8 @@ use serde::{Deserialize, Serialize}; pub use auction::{Auction, AuctionHandler, AuctionInfo, OnNewBidResult}; pub use currency::{ BalanceStatus, BasicCurrency, BasicCurrencyExtended, BasicLockableCurrency, BasicReservableCurrency, - LockIdentifier, MultiCurrency, MultiCurrencyExtended, MultiLockableCurrency, MultiReservableCurrency, OnDust, + LockIdentifier, MultiCurrency, MultiCurrencyExtended, MultiLockableCurrency, MultiReservableCurrency, + NamedMultiReservableCurrency, OnDust, }; pub use data_provider::{DataFeeder, DataProvider, DataProviderExtended}; pub use get_by_key::GetByKey; diff --git a/xtokens/src/mock/para.rs b/xtokens/src/mock/para.rs index a218d2033..a239964d7 100644 --- a/xtokens/src/mock/para.rs +++ b/xtokens/src/mock/para.rs @@ -84,6 +84,8 @@ impl orml_tokens::Config for Runtime { type ExistentialDeposits = ExistentialDeposits; type OnDust = (); type MaxLocks = ConstU32<50>; + type MaxReserves = ConstU32<50>; + type ReserveIdentifier = [u8; 8]; type DustRemovalWhitelist = Everything; } diff --git a/xtokens/src/mock/para_relative_view.rs b/xtokens/src/mock/para_relative_view.rs index 91a89503e..730c3fa8d 100644 --- a/xtokens/src/mock/para_relative_view.rs +++ b/xtokens/src/mock/para_relative_view.rs @@ -87,6 +87,8 @@ impl orml_tokens::Config for Runtime { type ExistentialDeposits = ExistentialDeposits; type OnDust = (); type MaxLocks = ConstU32<50>; + type MaxReserves = ConstU32<50>; + type ReserveIdentifier = [u8; 8]; type DustRemovalWhitelist = Everything; }