Skip to content
Snippets Groups Projects
Unverified Commit ffa1a264 authored by zjb0807's avatar zjb0807 Committed by GitHub
Browse files

Implement fungible and fungibles for tokens (#517)

* implement fungible and fungibles for tokens

* fix clippy

* add TODO

* add helper do_transfer

* Apply review suggestions
parent 55f4ff75
No related branches found
No related tags found
No related merge requests found
......@@ -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(&currency_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(&currency_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(&currency_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(&currency_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,
)
}
}
......@@ -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)
);
});
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment