diff --git a/tokens/src/lib.rs b/tokens/src/lib.rs index f80d14ad4..64f81cc36 100644 --- a/tokens/src/lib.rs +++ b/tokens/src/lib.rs @@ -205,6 +205,8 @@ pub mod module { MaxLocksExceeded, /// Transfer/payment would kill account KeepAlive, + /// Value too low to create account due to existential deposit + ExistentialDeposit, } #[pallet::event] @@ -570,6 +572,8 @@ impl Pallet { /// 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`. + /// Ensure from_account allow death or new balance above existential + /// deposit. Ensure to_account new balance above existential deposit. pub(crate) fn do_transfer( currency_id: T::CurrencyId, from: &T::AccountId, @@ -581,19 +585,28 @@ impl Pallet { return Ok(()); } - Pallet::::try_mutate_account(to, currency_id, |to_account, _is_new| -> DispatchResult { - Pallet::::try_mutate_account(from, currency_id, |from_account, _is_new| -> DispatchResult { + Pallet::::try_mutate_account(to, currency_id, |to_account, _existed| -> DispatchResult { + Pallet::::try_mutate_account(from, currency_id, |from_account, _existed| -> DispatchResult { from_account.free = from_account .free .checked_sub(&amount) .ok_or(Error::::BalanceTooLow)?; to_account.free = to_account.free.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; + let ed = T::ExistentialDeposits::get(¤cy_id); + // if to_account non_zero total is below existential deposit and the account is + // not a module account, would return an error. + ensure!( + to_account.total() >= ed || Self::is_module_account_id(to), + Error::::ExistentialDeposit + ); + 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::::is_provider_required(from); + // if from_account does not allow death and non_zero total is below existential + // deposit, would return an error. ensure!(allow_death || from_account.total() >= ed, Error::::KeepAlive); Ok(()) @@ -979,7 +992,7 @@ impl fungibles::Mutate for Pallet { if amount.is_zero() { return Ok(()); } - Pallet::::try_mutate_account(who, asset_id, |account, _is_new| -> DispatchResult { + Pallet::::try_mutate_account(who, asset_id, |account, _existed| -> DispatchResult { Pallet::::deposit_consequence(who, asset_id, amount, &account).into_result()?; // deposit_consequence already did overflow checking account.free += amount; @@ -998,14 +1011,17 @@ impl fungibles::Mutate for Pallet { if amount.is_zero() { return Ok(Self::Balance::zero()); } - let actual = - Pallet::::try_mutate_account(who, asset_id, |account, _is_new| -> Result { + let actual = Pallet::::try_mutate_account( + who, + asset_id, + |account, _existed| -> Result { let extra = Pallet::::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 >::mutate(asset_id, |t| *t -= actual); Ok(actual) @@ -1090,7 +1106,7 @@ impl fungibles::MutateHold for Pallet { return Ok(amount); } // Done on a best-effort basis. - Pallet::::try_mutate_account(who, asset_id, |a, _| { + Pallet::::try_mutate_account(who, asset_id, |a, _existed| { let new_free = a.free.saturating_add(amount.min(a.reserved)); let actual = new_free - a.free; // Guaranteed to be <= amount and <= a.reserved @@ -1248,7 +1264,7 @@ where } let currency_id = GetCurrencyId::get(); - Pallet::::try_mutate_account(who, currency_id, |account, _is_new| -> DispatchResult { + Pallet::::try_mutate_account(who, currency_id, |account, _existed| -> DispatchResult { account.free = account.free.checked_sub(&value).ok_or(Error::::BalanceTooLow)?; Pallet::::ensure_can_withdraw(currency_id, who, value)?; diff --git a/tokens/src/mock.rs b/tokens/src/mock.rs index b0e3d99af..92a1d9bc6 100644 --- a/tokens/src/mock.rs +++ b/tokens/src/mock.rs @@ -21,7 +21,8 @@ pub const BTC: CurrencyId = 2; pub const ETH: CurrencyId = 3; pub const ALICE: AccountId = AccountId32::new([0u8; 32]); pub const BOB: AccountId = AccountId32::new([1u8; 32]); -pub const TREASURY_ACCOUNT: AccountId = AccountId32::new([2u8; 32]); +pub const CHARLIE: AccountId = AccountId32::new([2u8; 32]); +pub const TREASURY_ACCOUNT: AccountId = AccountId32::new([3u8; 32]); pub const ID_1: LockIdentifier = *b"1 "; pub const ID_2: LockIdentifier = *b"2 "; pub const ID_3: LockIdentifier = *b"3 "; diff --git a/tokens/src/tests.rs b/tokens/src/tests.rs index a11562559..e29b225d5 100644 --- a/tokens/src/tests.rs +++ b/tokens/src/tests.rs @@ -324,6 +324,11 @@ fn transfer_should_work() { Tokens::transfer(Some(ALICE).into(), BOB, DOT, 60), Error::::BalanceTooLow, ); + assert_noop!( + Tokens::transfer(Some(ALICE).into(), CHARLIE, DOT, 1), + Error::::ExistentialDeposit, + ); + assert_ok!(Tokens::transfer(Some(ALICE).into(), CHARLIE, DOT, 2)); }); } @@ -708,7 +713,7 @@ fn currency_adapter_partial_locking_should_work() { assert_ok!(TreasuryCurrencyAdapter::transfer( &TREASURY_ACCOUNT, &ALICE, - 1, + 2, ExistenceRequirement::AllowDeath )); }); @@ -725,7 +730,7 @@ fn currency_adapter_lock_removal_should_work() { assert_ok!(TreasuryCurrencyAdapter::transfer( &TREASURY_ACCOUNT, &ALICE, - 1, + 2, ExistenceRequirement::AllowDeath )); }); @@ -742,7 +747,7 @@ fn currency_adapter_lock_replacement_should_work() { assert_ok!(TreasuryCurrencyAdapter::transfer( &TREASURY_ACCOUNT, &ALICE, - 1, + 2, ExistenceRequirement::AllowDeath )); }); @@ -759,7 +764,7 @@ fn currency_adapter_double_locking_should_work() { assert_ok!(TreasuryCurrencyAdapter::transfer( &TREASURY_ACCOUNT, &ALICE, - 1, + 2, ExistenceRequirement::AllowDeath )); }); @@ -775,7 +780,7 @@ fn currency_adapter_combination_locking_should_work() { TreasuryCurrencyAdapter::set_lock(ID_1, &TREASURY_ACCOUNT, u64::max_value(), WithdrawReasons::empty()); TreasuryCurrencyAdapter::set_lock(ID_2, &TREASURY_ACCOUNT, 0, WithdrawReasons::all()); assert_noop!( - TreasuryCurrencyAdapter::transfer(&TREASURY_ACCOUNT, &ALICE, 1, ExistenceRequirement::AllowDeath), + TreasuryCurrencyAdapter::transfer(&TREASURY_ACCOUNT, &ALICE, 2, ExistenceRequirement::AllowDeath), Error::::LiquidityRestrictions ); });