diff --git a/tokens/src/lib.rs b/tokens/src/lib.rs index 4b1f3bfc7ea4d54e0abb907053940a3513334d43..f80d14ad4ad695f63023a7daa7f70802e697a32a 100644 --- a/tokens/src/lib.rs +++ b/tokens/src/lib.rs @@ -43,6 +43,7 @@ use frame_support::{ ensure, log, pallet_prelude::*, traits::{ + tokens::{fungible, fungibles, DepositConsequence, WithdrawConsequence}, BalanceStatus as Status, Currency as PalletCurrency, ExistenceRequirement, Get, Imbalance, LockableCurrency as PalletLockableCurrency, MaxEncodedLen, ReservableCurrency as PalletReservableCurrency, SignedImbalance, WithdrawReasons, @@ -177,7 +178,8 @@ pub mod module { + MaybeSerializeDeserialize; /// The currency ID type - type CurrencyId: Parameter + Member + Copy + MaybeSerializeDeserialize + Ord; + // TODO: remove `Default` after https://github.com/paritytech/substrate/pull/9062 + type CurrencyId: Parameter + Member + Copy + MaybeSerializeDeserialize + Ord + Default; /// Weight information for extrinsics in this module. type WeightInfo: WeightInfo; @@ -201,6 +203,8 @@ pub mod module { LiquidityRestrictions, /// Failed because the maximum locks was exceeded MaxLocksExceeded, + /// Transfer/payment would kill account + KeepAlive, } #[pallet::event] @@ -359,6 +363,83 @@ impl<T: Config> Pallet<T> { PalletId::try_from_account(account_id).is_some() } + pub(crate) fn deposit_consequence( + _who: &T::AccountId, + currency_id: T::CurrencyId, + amount: T::Balance, + account: &AccountData<T::Balance>, + ) -> DepositConsequence { + if amount.is_zero() { + return DepositConsequence::Success; + } + + if TotalIssuance::<T>::get(currency_id).checked_add(&amount).is_none() { + return DepositConsequence::Overflow; + } + + let new_total_balance = match account.total().checked_add(&amount) { + Some(x) => x, + None => return DepositConsequence::Overflow, + }; + + if new_total_balance < T::ExistentialDeposits::get(¤cy_id) { + return DepositConsequence::BelowMinimum; + } + + // NOTE: We assume that we are a provider, so don't need to do any checks in the + // case of account creation. + + DepositConsequence::Success + } + + pub(crate) fn withdraw_consequence( + who: &T::AccountId, + currency_id: T::CurrencyId, + amount: T::Balance, + account: &AccountData<T::Balance>, + ) -> WithdrawConsequence<T::Balance> { + if amount.is_zero() { + return WithdrawConsequence::Success; + } + + if TotalIssuance::<T>::get(currency_id).checked_sub(&amount).is_none() { + return WithdrawConsequence::Underflow; + } + + let new_total_balance = match account.total().checked_sub(&amount) { + Some(x) => x, + None => return WithdrawConsequence::NoFunds, + }; + + // Provider restriction - total account balance cannot be reduced to zero if it + // cannot sustain the loss of a provider reference. + // NOTE: This assumes that the pallet is a provider (which is true). Is this + // ever changes, then this will need to adapt accordingly. + let ed = T::ExistentialDeposits::get(¤cy_id); + let success = if new_total_balance < ed { + if frame_system::Pallet::<T>::can_dec_provider(who) { + WithdrawConsequence::ReducedToZero(new_total_balance) + } else { + return WithdrawConsequence::WouldDie; + } + } else { + WithdrawConsequence::Success + }; + + // Enough free funds to have them be reduced. + let new_free_balance = match account.free.checked_sub(&amount) { + Some(b) => b, + None => return WithdrawConsequence::NoFunds, + }; + + // Eventual free funds must be no less than the frozen balance. + if new_free_balance < account.frozen() { + return WithdrawConsequence::Frozen; + } + + success + } + pub(crate) fn try_mutate_account<R, E>( who: &T::AccountId, currency_id: T::CurrencyId, @@ -485,6 +566,41 @@ impl<T: Config> Pallet<T> { Ok(()) } + + /// Transfer some free balance from `from` to `to`. + /// Is a no-op if value to be transferred is zero or the `from` is the + /// same as `to`. + pub(crate) fn do_transfer( + currency_id: T::CurrencyId, + from: &T::AccountId, + to: &T::AccountId, + amount: T::Balance, + existence_requirement: ExistenceRequirement, + ) -> DispatchResult { + if amount.is_zero() || from == to { + return Ok(()); + } + + Pallet::<T>::try_mutate_account(to, currency_id, |to_account, _is_new| -> DispatchResult { + Pallet::<T>::try_mutate_account(from, currency_id, |from_account, _is_new| -> DispatchResult { + from_account.free = from_account + .free + .checked_sub(&amount) + .ok_or(Error::<T>::BalanceTooLow)?; + to_account.free = to_account.free.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; + + Self::ensure_can_withdraw(currency_id, from, amount)?; + + let ed = T::ExistentialDeposits::get(¤cy_id); + let allow_death = existence_requirement == ExistenceRequirement::AllowDeath; + let allow_death = allow_death && !frame_system::Pallet::<T>::is_provider_required(from); + ensure!(allow_death || from_account.total() >= ed, Error::<T>::KeepAlive); + + Ok(()) + })?; + Ok(()) + }) + } } impl<T: Config> MultiCurrency<T::AccountId> for Pallet<T> { @@ -534,20 +650,7 @@ impl<T: Config> MultiCurrency<T::AccountId> for Pallet<T> { to: &T::AccountId, amount: Self::Balance, ) -> DispatchResult { - if amount.is_zero() || from == to { - return Ok(()); - } - Self::ensure_can_withdraw(currency_id, from, amount)?; - - let from_balance = Self::free_balance(currency_id, from); - let to_balance = Self::free_balance(currency_id, to) - .checked_add(&amount) - .ok_or(ArithmeticError::Overflow)?; - // Cannot underflow because ensure_can_withdraw check - Self::set_free_balance(currency_id, from, from_balance - amount); - Self::set_free_balance(currency_id, to, to_balance); - - Ok(()) + Self::do_transfer(currency_id, from, to, amount, ExistenceRequirement::AllowDeath) } /// Deposit some `amount` into the free balance of account `who`. @@ -833,6 +936,183 @@ impl<T: Config> MultiReservableCurrency<T::AccountId> for Pallet<T> { } } +impl<T: Config> fungibles::Inspect<T::AccountId> for Pallet<T> { + type AssetId = T::CurrencyId; + type Balance = T::Balance; + + fn total_issuance(asset_id: Self::AssetId) -> Self::Balance { + Pallet::<T>::total_issuance(asset_id) + } + fn minimum_balance(asset_id: Self::AssetId) -> Self::Balance { + <Self as MultiCurrency<_>>::minimum_balance(asset_id) + } + fn balance(asset_id: Self::AssetId, who: &T::AccountId) -> Self::Balance { + Pallet::<T>::total_balance(asset_id, who) + } + fn reducible_balance(asset_id: Self::AssetId, who: &T::AccountId, keep_alive: bool) -> Self::Balance { + let a = Pallet::<T>::accounts(who, asset_id); + // Liquid balance is what is neither reserved nor locked/frozen. + let liquid = a.free.saturating_sub(a.frozen); + if frame_system::Pallet::<T>::can_dec_provider(who) && !keep_alive { + liquid + } else { + // `must_remain_to_exist` is the part of liquid balance which must remain to + // keep total over ED. + let must_remain_to_exist = T::ExistentialDeposits::get(&asset_id).saturating_sub(a.total() - liquid); + liquid.saturating_sub(must_remain_to_exist) + } + } + fn can_deposit(asset_id: Self::AssetId, who: &T::AccountId, amount: Self::Balance) -> DepositConsequence { + Pallet::<T>::deposit_consequence(who, asset_id, amount, &Pallet::<T>::accounts(who, asset_id)) + } + fn can_withdraw( + asset_id: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + ) -> WithdrawConsequence<Self::Balance> { + Pallet::<T>::withdraw_consequence(who, asset_id, amount, &Pallet::<T>::accounts(who, asset_id)) + } +} + +impl<T: Config> fungibles::Mutate<T::AccountId> for Pallet<T> { + fn mint_into(asset_id: Self::AssetId, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + if amount.is_zero() { + return Ok(()); + } + Pallet::<T>::try_mutate_account(who, asset_id, |account, _is_new| -> DispatchResult { + Pallet::<T>::deposit_consequence(who, asset_id, amount, &account).into_result()?; + // deposit_consequence already did overflow checking + account.free += amount; + Ok(()) + })?; + // deposit_consequence already did overflow checking + <TotalIssuance<T>>::mutate(asset_id, |t| *t += amount); + Ok(()) + } + + fn burn_from( + asset_id: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + ) -> Result<Self::Balance, DispatchError> { + if amount.is_zero() { + return Ok(Self::Balance::zero()); + } + let actual = + Pallet::<T>::try_mutate_account(who, asset_id, |account, _is_new| -> Result<T::Balance, DispatchError> { + let extra = Pallet::<T>::withdraw_consequence(who, asset_id, amount, &account).into_result()?; + // withdraw_consequence already did underflow checking + let actual = amount + extra; + account.free -= actual; + Ok(actual) + })?; + // withdraw_consequence already did underflow checking + <TotalIssuance<T>>::mutate(asset_id, |t| *t -= actual); + Ok(actual) + } +} + +impl<T: Config> fungibles::Transfer<T::AccountId> for Pallet<T> { + fn transfer( + asset_id: Self::AssetId, + source: &T::AccountId, + dest: &T::AccountId, + amount: T::Balance, + keep_alive: bool, + ) -> Result<T::Balance, DispatchError> { + let er = if keep_alive { + ExistenceRequirement::KeepAlive + } else { + ExistenceRequirement::AllowDeath + }; + Self::do_transfer(asset_id, source, dest, amount, er).map(|_| amount) + } +} + +impl<T: Config> fungibles::Unbalanced<T::AccountId> for Pallet<T> { + fn set_balance(asset_id: Self::AssetId, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + // Balance is the same type and will not overflow + Pallet::<T>::mutate_account(who, asset_id, |account, _| account.free = amount); + Ok(()) + } + + fn set_total_issuance(asset_id: Self::AssetId, amount: Self::Balance) { + // Balance is the same type and will not overflow + <TotalIssuance<T>>::mutate(asset_id, |t| *t = amount); + } +} + +impl<T: Config> fungibles::InspectHold<T::AccountId> for Pallet<T> { + fn balance_on_hold(asset_id: Self::AssetId, who: &T::AccountId) -> T::Balance { + Pallet::<T>::accounts(who, asset_id).reserved + } + fn can_hold(asset_id: Self::AssetId, who: &T::AccountId, amount: T::Balance) -> bool { + let a = Pallet::<T>::accounts(who, asset_id); + let min_balance = T::ExistentialDeposits::get(&asset_id).max(a.frozen); + if a.reserved.checked_add(&amount).is_none() { + return false; + } + // We require it to be min_balance + amount to ensure that the full reserved + // funds may be slashed without compromising locked funds or destroying the + // account. + let required_free = match min_balance.checked_add(&amount) { + Some(x) => x, + None => return false, + }; + a.free >= required_free + } +} + +impl<T: Config> fungibles::MutateHold<T::AccountId> for Pallet<T> { + fn hold(asset_id: Self::AssetId, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + if amount.is_zero() { + return Ok(()); + } + ensure!( + Pallet::<T>::can_reserve(asset_id, who, amount), + Error::<T>::BalanceTooLow + ); + Pallet::<T>::mutate_account(who, asset_id, |a, _| { + // `can_reserve` has did underflow checking + a.free -= amount; + // Cannot overflow as `amount` is from `a.free` + a.reserved += amount; + }); + Ok(()) + } + fn release( + asset_id: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + best_effort: bool, + ) -> Result<T::Balance, DispatchError> { + if amount.is_zero() { + return Ok(amount); + } + // Done on a best-effort basis. + Pallet::<T>::try_mutate_account(who, asset_id, |a, _| { + let new_free = a.free.saturating_add(amount.min(a.reserved)); + let actual = new_free - a.free; + // Guaranteed to be <= amount and <= a.reserved + ensure!(best_effort || actual == amount, Error::<T>::BalanceTooLow); + a.free = new_free; + a.reserved = a.reserved.saturating_sub(actual); + Ok(actual) + }) + } + fn transfer_held( + asset_id: Self::AssetId, + source: &T::AccountId, + dest: &T::AccountId, + amount: Self::Balance, + _best_effort: bool, + on_hold: bool, + ) -> Result<Self::Balance, DispatchError> { + let status = if on_hold { Status::Reserved } else { Status::Free }; + Pallet::<T>::repatriate_reserved(asset_id, source, dest, amount, status) + } +} + pub struct CurrencyAdapter<T, GetCurrencyId>(marker::PhantomData<(T, GetCurrencyId)>); impl<T, GetCurrencyId> PalletCurrency<T::AccountId> for CurrencyAdapter<T, GetCurrencyId> @@ -903,9 +1183,9 @@ where source: &T::AccountId, dest: &T::AccountId, value: Self::Balance, - _existence_requirement: ExistenceRequirement, + existence_requirement: ExistenceRequirement, ) -> DispatchResult { - <Pallet<T> as MultiCurrency<T::AccountId>>::transfer(GetCurrencyId::get(), &source, &dest, value) + Pallet::<T>::do_transfer(GetCurrencyId::get(), &source, &dest, value, existence_requirement) } fn slash(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { @@ -961,14 +1241,25 @@ where who: &T::AccountId, value: Self::Balance, _reasons: WithdrawReasons, - _liveness: ExistenceRequirement, + liveness: ExistenceRequirement, ) -> sp_std::result::Result<Self::NegativeImbalance, DispatchError> { if value.is_zero() { return Ok(Self::NegativeImbalance::zero()); } + let currency_id = GetCurrencyId::get(); - Pallet::<T>::ensure_can_withdraw(currency_id, who, value)?; - Pallet::<T>::set_free_balance(currency_id, who, Pallet::<T>::free_balance(currency_id, who) - value); + Pallet::<T>::try_mutate_account(who, currency_id, |account, _is_new| -> DispatchResult { + account.free = account.free.checked_sub(&value).ok_or(Error::<T>::BalanceTooLow)?; + + Pallet::<T>::ensure_can_withdraw(currency_id, who, value)?; + + let ed = T::ExistentialDeposits::get(¤cy_id); + let allow_death = liveness == ExistenceRequirement::AllowDeath; + let allow_death = allow_death && !frame_system::Pallet::<T>::is_provider_required(who); + ensure!(allow_death || account.total() >= ed, Error::<T>::KeepAlive); + + Ok(()) + })?; Ok(Self::NegativeImbalance::new(value)) } @@ -1070,3 +1361,113 @@ impl<T: Config> TransferAll<T::AccountId> for Pallet<T> { }) } } + +impl<T, GetCurrencyId> fungible::Inspect<T::AccountId> for CurrencyAdapter<T, GetCurrencyId> +where + T: Config, + GetCurrencyId: Get<T::CurrencyId>, +{ + type Balance = T::Balance; + + fn total_issuance() -> Self::Balance { + <Pallet<T> as fungibles::Inspect<_>>::total_issuance(GetCurrencyId::get()) + } + fn minimum_balance() -> Self::Balance { + <Pallet<T> as fungibles::Inspect<_>>::minimum_balance(GetCurrencyId::get()) + } + fn balance(who: &T::AccountId) -> Self::Balance { + <Pallet<T> as fungibles::Inspect<_>>::balance(GetCurrencyId::get(), who) + } + fn reducible_balance(who: &T::AccountId, keep_alive: bool) -> Self::Balance { + <Pallet<T> as fungibles::Inspect<_>>::reducible_balance(GetCurrencyId::get(), who, keep_alive) + } + fn can_deposit(who: &T::AccountId, amount: Self::Balance) -> DepositConsequence { + <Pallet<T> as fungibles::Inspect<_>>::can_deposit(GetCurrencyId::get(), who, amount) + } + fn can_withdraw(who: &T::AccountId, amount: Self::Balance) -> WithdrawConsequence<Self::Balance> { + <Pallet<T> as fungibles::Inspect<_>>::can_withdraw(GetCurrencyId::get(), who, amount) + } +} + +impl<T, GetCurrencyId> fungible::Mutate<T::AccountId> for CurrencyAdapter<T, GetCurrencyId> +where + T: Config, + GetCurrencyId: Get<T::CurrencyId>, +{ + fn mint_into(who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + <Pallet<T> as fungibles::Mutate<_>>::mint_into(GetCurrencyId::get(), who, amount) + } + fn burn_from(who: &T::AccountId, amount: Self::Balance) -> Result<Self::Balance, DispatchError> { + <Pallet<T> as fungibles::Mutate<_>>::burn_from(GetCurrencyId::get(), who, amount) + } +} + +impl<T, GetCurrencyId> fungible::Transfer<T::AccountId> for CurrencyAdapter<T, GetCurrencyId> +where + T: Config, + GetCurrencyId: Get<T::CurrencyId>, +{ + fn transfer( + source: &T::AccountId, + dest: &T::AccountId, + amount: T::Balance, + keep_alive: bool, + ) -> Result<T::Balance, DispatchError> { + <Pallet<T> as fungibles::Transfer<_>>::transfer(GetCurrencyId::get(), source, dest, amount, keep_alive) + } +} + +impl<T, GetCurrencyId> fungible::Unbalanced<T::AccountId> for CurrencyAdapter<T, GetCurrencyId> +where + T: Config, + GetCurrencyId: Get<T::CurrencyId>, +{ + fn set_balance(who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + <Pallet<T> as fungibles::Unbalanced<_>>::set_balance(GetCurrencyId::get(), who, amount) + } + fn set_total_issuance(amount: Self::Balance) { + <Pallet<T> as fungibles::Unbalanced<_>>::set_total_issuance(GetCurrencyId::get(), amount) + } +} + +impl<T, GetCurrencyId> fungible::InspectHold<T::AccountId> for CurrencyAdapter<T, GetCurrencyId> +where + T: Config, + GetCurrencyId: Get<T::CurrencyId>, +{ + fn balance_on_hold(who: &T::AccountId) -> T::Balance { + <Pallet<T> as fungibles::InspectHold<_>>::balance_on_hold(GetCurrencyId::get(), who) + } + fn can_hold(who: &T::AccountId, amount: T::Balance) -> bool { + <Pallet<T> as fungibles::InspectHold<_>>::can_hold(GetCurrencyId::get(), who, amount) + } +} + +impl<T, GetCurrencyId> fungible::MutateHold<T::AccountId> for CurrencyAdapter<T, GetCurrencyId> +where + T: Config, + GetCurrencyId: Get<T::CurrencyId>, +{ + fn hold(who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + <Pallet<T> as fungibles::MutateHold<_>>::hold(GetCurrencyId::get(), who, amount) + } + fn release(who: &T::AccountId, amount: Self::Balance, best_effort: bool) -> Result<T::Balance, DispatchError> { + <Pallet<T> as fungibles::MutateHold<_>>::release(GetCurrencyId::get(), who, amount, best_effort) + } + fn transfer_held( + source: &T::AccountId, + dest: &T::AccountId, + amount: Self::Balance, + best_effort: bool, + on_hold: bool, + ) -> Result<Self::Balance, DispatchError> { + <Pallet<T> as fungibles::MutateHold<_>>::transfer_held( + GetCurrencyId::get(), + source, + dest, + amount, + best_effort, + on_hold, + ) + } +} diff --git a/tokens/src/tests.rs b/tokens/src/tests.rs index 297da4d173b3398520ad52ff7279ab21ead28424..a11562559092a14a7fc33d21f701218d3f1b3ac2 100644 --- a/tokens/src/tests.rs +++ b/tokens/src/tests.rs @@ -137,7 +137,7 @@ fn frozen_can_limit_liquidity() { Error::<Runtime>::LiquidityRestrictions, ); assert_ok!(Tokens::set_lock(ID_1, DOT, &ALICE, 10)); - assert_ok!(<Tokens as MultiCurrency<_>>::transfer(DOT, &ALICE, &BOB, 11),); + assert_ok!(<Tokens as MultiCurrency<_>>::transfer(DOT, &ALICE, &BOB, 11)); }); } @@ -159,7 +159,7 @@ fn reserve_should_work() { .one_hundred_for_alice_n_bob() .build() .execute_with(|| { - assert_noop!(Tokens::reserve(DOT, &ALICE, 101), Error::<Runtime>::BalanceTooLow,); + assert_noop!(Tokens::reserve(DOT, &ALICE, 101), Error::<Runtime>::BalanceTooLow); assert_ok!(Tokens::reserve(DOT, &ALICE, 0)); assert_eq!(Tokens::free_balance(DOT, &ALICE), 100); assert_eq!(Tokens::reserved_balance(DOT, &ALICE), 0); @@ -999,3 +999,98 @@ fn exceeding_max_locks_should_fail() { assert_eq!(Tokens::locks(ALICE, DOT).len(), 2); }); } + +#[test] +fn fungibles_inspect_trait_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + assert_eq!(<Tokens as fungibles::Inspect<_>>::total_issuance(DOT), 200); + assert_eq!(<Tokens as fungibles::Inspect<_>>::minimum_balance(DOT), 2); + assert_eq!(<Tokens as fungibles::Inspect<_>>::balance(DOT, &ALICE), 100); + assert_eq!( + <Tokens as fungibles::Inspect<_>>::reducible_balance(DOT, &ALICE, true), + 98 + ); + assert_ok!(<Tokens as fungibles::Inspect<_>>::can_deposit(DOT, &ALICE, 1).into_result()); + assert_ok!(<Tokens as fungibles::Inspect<_>>::can_withdraw(DOT, &ALICE, 1).into_result()); + }); +} + +#[test] +fn fungibles_mutate_trait_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + assert_ok!(<Tokens as fungibles::Mutate<_>>::mint_into(DOT, &ALICE, 10)); + assert_eq!(<Tokens as fungibles::Mutate<_>>::burn_from(DOT, &ALICE, 8), Ok(8)); + }); +} + +#[test] +fn fungibles_transfer_trait_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + assert_eq!(<Tokens as fungibles::Inspect<_>>::balance(DOT, &ALICE), 100); + assert_eq!(<Tokens as fungibles::Inspect<_>>::balance(DOT, &BOB), 100); + assert_ok!(<Tokens as fungibles::Transfer<_>>::transfer( + DOT, &ALICE, &BOB, 10, true + )); + assert_eq!(<Tokens as fungibles::Inspect<_>>::balance(DOT, &ALICE), 90); + assert_eq!(<Tokens as fungibles::Inspect<_>>::balance(DOT, &BOB), 110); + }); +} + +#[test] +fn fungibles_unbalanced_trait_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + assert_eq!(<Tokens as fungibles::Inspect<_>>::balance(DOT, &ALICE), 100); + assert_ok!(<Tokens as fungibles::Unbalanced<_>>::set_balance(DOT, &ALICE, 10)); + assert_eq!(<Tokens as fungibles::Inspect<_>>::balance(DOT, &ALICE), 10); + + assert_eq!(<Tokens as fungibles::Inspect<_>>::total_issuance(DOT), 200); + <Tokens as fungibles::Unbalanced<_>>::set_total_issuance(DOT, 10); + assert_eq!(<Tokens as fungibles::Inspect<_>>::total_issuance(DOT), 10); + }); +} + +#[test] +fn fungibles_inspect_hold_trait_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + assert_eq!(<Tokens as fungibles::InspectHold<_>>::balance_on_hold(DOT, &ALICE), 0); + assert_eq!(<Tokens as fungibles::InspectHold<_>>::can_hold(DOT, &ALICE, 50), true); + assert_eq!(<Tokens as fungibles::InspectHold<_>>::can_hold(DOT, &ALICE, 100), false); + }); +} + +#[test] +fn fungibles_mutate_hold_trait_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + assert_noop!( + <Tokens as fungibles::MutateHold<_>>::hold(DOT, &ALICE, 200), + Error::<Runtime>::BalanceTooLow + ); + assert_ok!(<Tokens as fungibles::MutateHold<_>>::hold(DOT, &ALICE, 100)); + assert_eq!( + <Tokens as fungibles::MutateHold<_>>::release(DOT, &ALICE, 50, true), + Ok(50) + ); + assert_eq!( + <Tokens as fungibles::MutateHold<_>>::transfer_held(DOT, &ALICE, &BOB, 100, true, true), + Ok(50) + ); + }); +}