mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-13 23:21:06 +00:00
Insufficient asset quota and deposits (#10382)
* Allow asset accounts to exist by deposit * Place limit on consumers (and therefore freebie asset accounts) * Maximum number of assets * Fixes * Fixes * Formatting * Docs * Formatting * Destroyed assets are properly tidied * Update frame/assets/src/types.rs Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com> * Docs * Docs * Formatting * Docs * Docs * Fixes * Fixes Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>
This commit is contained in:
@@ -62,11 +62,11 @@ impl<T: Config<I>, I: 'static> ExtraMutator<T, I> {
|
||||
id: T::AssetId,
|
||||
who: impl sp_std::borrow::Borrow<T::AccountId>,
|
||||
) -> Option<ExtraMutator<T, I>> {
|
||||
if Account::<T, I>::contains_key(id, who.borrow()) {
|
||||
if let Some(a) = Account::<T, I>::get(id, who.borrow()) {
|
||||
Some(ExtraMutator::<T, I> {
|
||||
id,
|
||||
who: who.borrow().clone(),
|
||||
original: Account::<T, I>::get(id, who.borrow()).extra,
|
||||
original: a.extra,
|
||||
pending: None,
|
||||
})
|
||||
} else {
|
||||
@@ -77,13 +77,8 @@ impl<T: Config<I>, I: 'static> ExtraMutator<T, I> {
|
||||
/// Commit any changes to storage.
|
||||
pub fn commit(&mut self) -> Result<(), ()> {
|
||||
if let Some(extra) = self.pending.take() {
|
||||
Account::<T, I>::try_mutate_exists(self.id, self.who.borrow(), |maybe_account| {
|
||||
if let Some(ref mut account) = maybe_account {
|
||||
account.extra = extra;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
Account::<T, I>::try_mutate(self.id, self.who.borrow(), |maybe_account| {
|
||||
maybe_account.as_mut().ok_or(()).map(|account| account.extra = extra)
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
@@ -93,13 +88,11 @@ impl<T: Config<I>, I: 'static> ExtraMutator<T, I> {
|
||||
/// Revert any changes, even those already committed by `self` and drop self.
|
||||
pub fn revert(mut self) -> Result<(), ()> {
|
||||
self.pending = None;
|
||||
Account::<T, I>::try_mutate_exists(self.id, self.who.borrow(), |maybe_account| {
|
||||
if let Some(ref mut account) = maybe_account {
|
||||
account.extra = self.original.clone();
|
||||
Ok(())
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
Account::<T, I>::try_mutate(self.id, self.who.borrow(), |maybe_account| {
|
||||
maybe_account
|
||||
.as_mut()
|
||||
.ok_or(())
|
||||
.map(|account| account.extra = self.original.clone())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,14 @@
|
||||
use super::*;
|
||||
use frame_support::{traits::Get, BoundedVec};
|
||||
|
||||
#[must_use]
|
||||
pub(super) enum DeadConsequence {
|
||||
Remove,
|
||||
Keep,
|
||||
}
|
||||
|
||||
use DeadConsequence::*;
|
||||
|
||||
// The main implementation block for the module.
|
||||
impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
// Public immutables
|
||||
@@ -32,9 +40,17 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
ExtraMutator::maybe_new(id, who)
|
||||
}
|
||||
|
||||
/// Get the asset `id` balance of `who`.
|
||||
/// Get the asset `id` balance of `who`, or zero if the asset-account doesn't exist.
|
||||
pub fn balance(id: T::AssetId, who: impl sp_std::borrow::Borrow<T::AccountId>) -> T::Balance {
|
||||
Account::<T, I>::get(id, who.borrow()).balance
|
||||
Self::maybe_balance(id, who).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get the asset `id` balance of `who` if the asset-account exists.
|
||||
pub fn maybe_balance(
|
||||
id: T::AssetId,
|
||||
who: impl sp_std::borrow::Borrow<T::AccountId>,
|
||||
) -> Option<T::Balance> {
|
||||
Account::<T, I>::get(id, who.borrow()).map(|a| a.balance)
|
||||
}
|
||||
|
||||
/// Get the total supply of an asset `id`.
|
||||
@@ -45,34 +61,44 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
pub(super) fn new_account(
|
||||
who: &T::AccountId,
|
||||
d: &mut AssetDetails<T::Balance, T::AccountId, DepositBalanceOf<T, I>>,
|
||||
) -> Result<bool, DispatchError> {
|
||||
maybe_deposit: Option<DepositBalanceOf<T, I>>,
|
||||
) -> Result<ExistenceReason<DepositBalanceOf<T, I>>, DispatchError> {
|
||||
let accounts = d.accounts.checked_add(1).ok_or(ArithmeticError::Overflow)?;
|
||||
let is_sufficient = if d.is_sufficient {
|
||||
let reason = if let Some(deposit) = maybe_deposit {
|
||||
ExistenceReason::DepositHeld(deposit)
|
||||
} else if d.is_sufficient {
|
||||
frame_system::Pallet::<T>::inc_sufficients(who);
|
||||
d.sufficients += 1;
|
||||
true
|
||||
ExistenceReason::Sufficient
|
||||
} else {
|
||||
frame_system::Pallet::<T>::inc_consumers(who).map_err(|_| Error::<T, I>::NoProvider)?;
|
||||
false
|
||||
ExistenceReason::Consumer
|
||||
};
|
||||
d.accounts = accounts;
|
||||
Ok(is_sufficient)
|
||||
Ok(reason)
|
||||
}
|
||||
|
||||
pub(super) fn dead_account(
|
||||
what: T::AssetId,
|
||||
who: &T::AccountId,
|
||||
d: &mut AssetDetails<T::Balance, T::AccountId, DepositBalanceOf<T, I>>,
|
||||
sufficient: bool,
|
||||
) {
|
||||
if sufficient {
|
||||
d.sufficients = d.sufficients.saturating_sub(1);
|
||||
frame_system::Pallet::<T>::dec_sufficients(who);
|
||||
} else {
|
||||
frame_system::Pallet::<T>::dec_consumers(who);
|
||||
reason: &ExistenceReason<DepositBalanceOf<T, I>>,
|
||||
force: bool,
|
||||
) -> DeadConsequence {
|
||||
let mut result = Remove;
|
||||
match *reason {
|
||||
ExistenceReason::Consumer => frame_system::Pallet::<T>::dec_consumers(who),
|
||||
ExistenceReason::Sufficient => {
|
||||
d.sufficients = d.sufficients.saturating_sub(1);
|
||||
frame_system::Pallet::<T>::dec_sufficients(who);
|
||||
},
|
||||
ExistenceReason::DepositRefunded => {},
|
||||
ExistenceReason::DepositHeld(_) if !force => return Keep,
|
||||
ExistenceReason::DepositHeld(_) => result = Keep,
|
||||
}
|
||||
d.accounts = d.accounts.saturating_sub(1);
|
||||
T::Freezer::died(what, who)
|
||||
T::Freezer::died(what, who);
|
||||
result
|
||||
}
|
||||
|
||||
pub(super) fn can_increase(
|
||||
@@ -87,15 +113,15 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
if details.supply.checked_add(&amount).is_none() {
|
||||
return DepositConsequence::Overflow
|
||||
}
|
||||
let account = Account::<T, I>::get(id, who);
|
||||
if account.balance.checked_add(&amount).is_none() {
|
||||
return DepositConsequence::Overflow
|
||||
}
|
||||
if account.balance.is_zero() {
|
||||
if let Some(balance) = Self::maybe_balance(id, who) {
|
||||
if balance.checked_add(&amount).is_none() {
|
||||
return DepositConsequence::Overflow
|
||||
}
|
||||
} else {
|
||||
if amount < details.min_balance {
|
||||
return DepositConsequence::BelowMinimum
|
||||
}
|
||||
if !details.is_sufficient && frame_system::Pallet::<T>::providers(who) == 0 {
|
||||
if !details.is_sufficient && !frame_system::Pallet::<T>::can_inc_consumer(who) {
|
||||
return DepositConsequence::CannotCreate
|
||||
}
|
||||
if details.is_sufficient && details.sufficients.checked_add(1).is_none() {
|
||||
@@ -124,7 +150,13 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
if details.is_frozen {
|
||||
return Frozen
|
||||
}
|
||||
let account = Account::<T, I>::get(id, who);
|
||||
if amount.is_zero() {
|
||||
return Success
|
||||
}
|
||||
let account = match Account::<T, I>::get(id, who) {
|
||||
Some(a) => a,
|
||||
None => return NoFunds,
|
||||
};
|
||||
if account.is_frozen {
|
||||
return Frozen
|
||||
}
|
||||
@@ -165,7 +197,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
let details = Asset::<T, I>::get(id).ok_or_else(|| Error::<T, I>::Unknown)?;
|
||||
ensure!(!details.is_frozen, Error::<T, I>::Frozen);
|
||||
|
||||
let account = Account::<T, I>::get(id, who);
|
||||
let account = Account::<T, I>::get(id, who).ok_or(Error::<T, I>::NoAccount)?;
|
||||
ensure!(!account.is_frozen, Error::<T, I>::Frozen);
|
||||
|
||||
let amount = if let Some(frozen) = T::Freezer::frozen_balance(id, who) {
|
||||
@@ -253,6 +285,48 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
Ok((credit, maybe_burn))
|
||||
}
|
||||
|
||||
/// Creates a account for `who` to hold asset `id` with a zero balance and takes a deposit.
|
||||
pub(super) fn do_touch(id: T::AssetId, who: T::AccountId) -> DispatchResult {
|
||||
ensure!(!Account::<T, I>::contains_key(id, &who), Error::<T, I>::AlreadyExists);
|
||||
let deposit = T::AssetAccountDeposit::get();
|
||||
let mut details = Asset::<T, I>::get(&id).ok_or(Error::<T, I>::Unknown)?;
|
||||
let reason = Self::new_account(&who, &mut details, Some(deposit))?;
|
||||
T::Currency::reserve(&who, deposit)?;
|
||||
Asset::<T, I>::insert(&id, details);
|
||||
Account::<T, I>::insert(
|
||||
id,
|
||||
&who,
|
||||
AssetAccountOf::<T, I> {
|
||||
balance: Zero::zero(),
|
||||
is_frozen: false,
|
||||
reason,
|
||||
extra: T::Extra::default(),
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a deposit, destroying an asset-account.
|
||||
pub(super) fn do_refund(id: T::AssetId, who: T::AccountId, allow_burn: bool) -> DispatchResult {
|
||||
let mut account = Account::<T, I>::get(id, &who).ok_or(Error::<T, I>::NoDeposit)?;
|
||||
let deposit = account.reason.take_deposit().ok_or(Error::<T, I>::NoDeposit)?;
|
||||
let mut details = Asset::<T, I>::get(&id).ok_or(Error::<T, I>::Unknown)?;
|
||||
|
||||
ensure!(account.balance.is_zero() || allow_burn, Error::<T, I>::WouldBurn);
|
||||
ensure!(!details.is_frozen, Error::<T, I>::Frozen);
|
||||
ensure!(!account.is_frozen, Error::<T, I>::Frozen);
|
||||
|
||||
T::Currency::unreserve(&who, deposit);
|
||||
|
||||
if let Remove = Self::dead_account(id, &who, &mut details, &account.reason, false) {
|
||||
Account::<T, I>::remove(id, &who);
|
||||
} else {
|
||||
debug_assert!(false, "refund did not result in dead account?!");
|
||||
}
|
||||
Asset::<T, I>::insert(&id, details);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Increases the asset `id` balance of `beneficiary` by `amount`.
|
||||
///
|
||||
/// This alters the registered supply of the asset and emits an event.
|
||||
@@ -307,13 +381,22 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
|
||||
check(details)?;
|
||||
|
||||
Account::<T, I>::try_mutate(id, beneficiary, |t| -> DispatchResult {
|
||||
let new_balance = t.balance.saturating_add(amount);
|
||||
ensure!(new_balance >= details.min_balance, TokenError::BelowMinimum);
|
||||
if t.balance.is_zero() {
|
||||
t.sufficient = Self::new_account(beneficiary, details)?;
|
||||
Account::<T, I>::try_mutate(id, beneficiary, |maybe_account| -> DispatchResult {
|
||||
match maybe_account {
|
||||
Some(ref mut account) => {
|
||||
account.balance.saturating_accrue(amount);
|
||||
},
|
||||
maybe_account @ None => {
|
||||
// Note this should never fail as it's already checked by `can_increase`.
|
||||
ensure!(amount >= details.min_balance, TokenError::BelowMinimum);
|
||||
*maybe_account = Some(AssetAccountOf::<T, I> {
|
||||
balance: amount,
|
||||
reason: Self::new_account(beneficiary, details, None)?,
|
||||
is_frozen: false,
|
||||
extra: T::Extra::default(),
|
||||
});
|
||||
},
|
||||
}
|
||||
t.balance = new_balance;
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
@@ -375,23 +458,25 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
let actual = Self::prep_debit(id, target, amount, f)?;
|
||||
|
||||
Asset::<T, I>::try_mutate(id, |maybe_details| -> DispatchResult {
|
||||
let details = maybe_details.as_mut().ok_or(Error::<T, I>::Unknown)?;
|
||||
let mut details = maybe_details.as_mut().ok_or(Error::<T, I>::Unknown)?;
|
||||
|
||||
check(actual, details)?;
|
||||
|
||||
Account::<T, I>::try_mutate_exists(id, target, |maybe_account| -> DispatchResult {
|
||||
let mut account = maybe_account.take().unwrap_or_default();
|
||||
Account::<T, I>::try_mutate(id, target, |maybe_account| -> DispatchResult {
|
||||
let mut account = maybe_account.take().ok_or(Error::<T, I>::NoAccount)?;
|
||||
debug_assert!(account.balance >= actual, "checked in prep; qed");
|
||||
|
||||
// Make the debit.
|
||||
account.balance = account.balance.saturating_sub(actual);
|
||||
*maybe_account = if account.balance < details.min_balance {
|
||||
if account.balance < details.min_balance {
|
||||
debug_assert!(account.balance.is_zero(), "checked in prep; qed");
|
||||
Self::dead_account(id, target, details, account.sufficient);
|
||||
None
|
||||
} else {
|
||||
Some(account)
|
||||
if let Remove =
|
||||
Self::dead_account(id, target, &mut details, &account.reason, false)
|
||||
{
|
||||
return Ok(())
|
||||
}
|
||||
};
|
||||
*maybe_account = Some(account);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
@@ -432,7 +517,8 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
let debit = Self::prep_debit(id, &source, amount, f.into())?;
|
||||
let (credit, maybe_burn) = Self::prep_credit(id, &dest, amount, debit, f.burn_dust)?;
|
||||
|
||||
let mut source_account = Account::<T, I>::get(id, &source);
|
||||
let mut source_account =
|
||||
Account::<T, I>::get(id, &source).ok_or(Error::<T, I>::NoAccount)?;
|
||||
|
||||
Asset::<T, I>::try_mutate(id, |maybe_details| -> DispatchResult {
|
||||
let details = maybe_details.as_mut().ok_or(Error::<T, I>::Unknown)?;
|
||||
@@ -459,29 +545,40 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
debug_assert!(source_account.balance >= debit, "checked in prep; qed");
|
||||
source_account.balance = source_account.balance.saturating_sub(debit);
|
||||
|
||||
Account::<T, I>::try_mutate(id, &dest, |a| -> DispatchResult {
|
||||
// Calculate new balance; this will not saturate since it's already checked in prep.
|
||||
debug_assert!(a.balance.checked_add(&credit).is_some(), "checked in prep; qed");
|
||||
let new_balance = a.balance.saturating_add(credit);
|
||||
|
||||
// Create a new account if there wasn't one already.
|
||||
if a.balance.is_zero() {
|
||||
a.sufficient = Self::new_account(&dest, details)?;
|
||||
Account::<T, I>::try_mutate(id, &dest, |maybe_account| -> DispatchResult {
|
||||
match maybe_account {
|
||||
Some(ref mut account) => {
|
||||
// Calculate new balance; this will not saturate since it's already checked
|
||||
// in prep.
|
||||
debug_assert!(
|
||||
account.balance.checked_add(&credit).is_some(),
|
||||
"checked in prep; qed"
|
||||
);
|
||||
account.balance.saturating_accrue(credit);
|
||||
},
|
||||
maybe_account @ None => {
|
||||
*maybe_account = Some(AssetAccountOf::<T, I> {
|
||||
balance: credit,
|
||||
is_frozen: false,
|
||||
reason: Self::new_account(&dest, details, None)?,
|
||||
extra: T::Extra::default(),
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
a.balance = new_balance;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Remove source account if it's now dead.
|
||||
if source_account.balance < details.min_balance {
|
||||
debug_assert!(source_account.balance.is_zero(), "checked in prep; qed");
|
||||
Self::dead_account(id, &source, details, source_account.sufficient);
|
||||
Account::<T, I>::remove(id, &source);
|
||||
} else {
|
||||
Account::<T, I>::insert(id, &source, &source_account)
|
||||
if let Remove =
|
||||
Self::dead_account(id, &source, details, &source_account.reason, false)
|
||||
{
|
||||
Account::<T, I>::remove(id, &source);
|
||||
return Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Account::<T, I>::insert(id, &source, &source_account);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
@@ -554,7 +651,10 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
ensure!(details.approvals <= witness.approvals, Error::<T, I>::BadWitness);
|
||||
|
||||
for (who, v) in Account::<T, I>::drain_prefix(id) {
|
||||
Self::dead_account(id, &who, &mut details, v.sufficient);
|
||||
// We have to force this as it's destroying the entire asset class.
|
||||
// This could mean that some accounts now have irreversibly reserved
|
||||
// funds.
|
||||
let _ = Self::dead_account(id, &who, &mut details, &v.reason, true);
|
||||
}
|
||||
debug_assert_eq!(details.accounts, 0);
|
||||
debug_assert_eq!(details.sufficients, 0);
|
||||
|
||||
@@ -22,11 +22,7 @@ use super::*;
|
||||
impl<T: Config<I>, I: 'static> StoredMap<(T::AssetId, T::AccountId), T::Extra> for Pallet<T, I> {
|
||||
fn get(id_who: &(T::AssetId, T::AccountId)) -> T::Extra {
|
||||
let &(id, ref who) = id_who;
|
||||
if Account::<T, I>::contains_key(id, who) {
|
||||
Account::<T, I>::get(id, who).extra
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
Account::<T, I>::get(id, who).map(|a| a.extra).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn try_mutate_exists<R, E: From<DispatchError>>(
|
||||
@@ -34,13 +30,13 @@ impl<T: Config<I>, I: 'static> StoredMap<(T::AssetId, T::AccountId), T::Extra> f
|
||||
f: impl FnOnce(&mut Option<T::Extra>) -> Result<R, E>,
|
||||
) -> Result<R, E> {
|
||||
let &(id, ref who) = id_who;
|
||||
let mut maybe_extra = Some(Account::<T, I>::get(id, who).extra);
|
||||
let mut maybe_extra = Account::<T, I>::get(id, who).map(|a| a.extra);
|
||||
let r = f(&mut maybe_extra)?;
|
||||
// They want to write some value or delete it.
|
||||
// If the account existed and they want to write a value, then we write.
|
||||
// If the account didn't exist and they want to delete it, then we let it pass.
|
||||
// Otherwise, we fail.
|
||||
Account::<T, I>::try_mutate_exists(id, who, |maybe_account| {
|
||||
Account::<T, I>::try_mutate(id, who, |maybe_account| {
|
||||
if let Some(extra) = maybe_extra {
|
||||
// They want to write a value. Let this happen only if the account actually exists.
|
||||
if let Some(ref mut account) = maybe_account {
|
||||
|
||||
@@ -212,6 +212,11 @@ pub mod pallet {
|
||||
#[pallet::constant]
|
||||
type AssetDeposit: Get<DepositBalanceOf<Self, I>>;
|
||||
|
||||
/// The amount of funds that must be reserved for a non-provider asset account to be
|
||||
/// maintained.
|
||||
#[pallet::constant]
|
||||
type AssetAccountDeposit: Get<DepositBalanceOf<Self, I>>;
|
||||
|
||||
/// The basic amount of funds that must be reserved when adding metadata to your asset.
|
||||
#[pallet::constant]
|
||||
type MetadataDepositBase: Get<DepositBalanceOf<Self, I>>;
|
||||
@@ -250,15 +255,15 @@ pub mod pallet {
|
||||
>;
|
||||
|
||||
#[pallet::storage]
|
||||
/// The number of units of assets held by any given account.
|
||||
/// The holdings of a specific account for a specific asset.
|
||||
pub(super) type Account<T: Config<I>, I: 'static = ()> = StorageDoubleMap<
|
||||
_,
|
||||
Blake2_128Concat,
|
||||
T::AssetId,
|
||||
Blake2_128Concat,
|
||||
T::AccountId,
|
||||
AssetBalance<T::Balance, T::Extra>,
|
||||
ValueQuery,
|
||||
AssetAccountOf<T, I>,
|
||||
OptionQuery,
|
||||
GetDefault,
|
||||
ConstU32<300_000>,
|
||||
>;
|
||||
@@ -448,8 +453,8 @@ pub mod pallet {
|
||||
pub enum Error<T, I = ()> {
|
||||
/// Account balance must be greater than or equal to the transfer amount.
|
||||
BalanceLow,
|
||||
/// Balance should be non-zero.
|
||||
BalanceZero,
|
||||
/// The account to alter does not exist.
|
||||
NoAccount,
|
||||
/// The signing account has no permission to do the operation.
|
||||
NoPermission,
|
||||
/// The given asset ID is unknown.
|
||||
@@ -471,6 +476,12 @@ pub mod pallet {
|
||||
Unapproved,
|
||||
/// The source account would not survive the transfer and it needs to stay alive.
|
||||
WouldDie,
|
||||
/// The asset-account already exists.
|
||||
AlreadyExists,
|
||||
/// The asset-account doesn't have an associated deposit.
|
||||
NoDeposit,
|
||||
/// The operation would result in funds being burned.
|
||||
WouldBurn,
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
@@ -633,7 +644,7 @@ pub mod pallet {
|
||||
///
|
||||
/// Origin must be Signed and the sender should be the Manager of the asset `id`.
|
||||
///
|
||||
/// Bails with `BalanceZero` if the `who` is already dead.
|
||||
/// Bails with `NoAccount` if the `who` is already dead.
|
||||
///
|
||||
/// - `id`: The identifier of the asset to have some amount burned.
|
||||
/// - `who`: The account to be debited from.
|
||||
@@ -779,9 +790,11 @@ pub mod pallet {
|
||||
let d = Asset::<T, I>::get(id).ok_or(Error::<T, I>::Unknown)?;
|
||||
ensure!(&origin == &d.freezer, Error::<T, I>::NoPermission);
|
||||
let who = T::Lookup::lookup(who)?;
|
||||
ensure!(Account::<T, I>::contains_key(id, &who), Error::<T, I>::BalanceZero);
|
||||
|
||||
Account::<T, I>::mutate(id, &who, |a| a.is_frozen = true);
|
||||
Account::<T, I>::try_mutate(id, &who, |maybe_account| -> DispatchResult {
|
||||
maybe_account.as_mut().ok_or(Error::<T, I>::NoAccount)?.is_frozen = true;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Self::deposit_event(Event::<T, I>::Frozen { asset_id: id, who });
|
||||
Ok(())
|
||||
@@ -808,9 +821,11 @@ pub mod pallet {
|
||||
let details = Asset::<T, I>::get(id).ok_or(Error::<T, I>::Unknown)?;
|
||||
ensure!(&origin == &details.admin, Error::<T, I>::NoPermission);
|
||||
let who = T::Lookup::lookup(who)?;
|
||||
ensure!(Account::<T, I>::contains_key(id, &who), Error::<T, I>::BalanceZero);
|
||||
|
||||
Account::<T, I>::mutate(id, &who, |a| a.is_frozen = false);
|
||||
Account::<T, I>::try_mutate(id, &who, |maybe_account| -> DispatchResult {
|
||||
maybe_account.as_mut().ok_or(Error::<T, I>::NoAccount)?.is_frozen = false;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Self::deposit_event(Event::<T, I>::Thawed { asset_id: id, who });
|
||||
Ok(())
|
||||
@@ -1274,5 +1289,36 @@ pub mod pallet {
|
||||
let destination = T::Lookup::lookup(destination)?;
|
||||
Self::do_transfer_approved(id, &owner, &delegate, &destination, amount)
|
||||
}
|
||||
|
||||
/// Create an asset account for non-provider assets.
|
||||
///
|
||||
/// A deposit will be taken from the signer account.
|
||||
///
|
||||
/// - `origin`: Must be Signed; the signer account must have sufficient funds for a deposit
|
||||
/// to be taken.
|
||||
/// - `id`: The identifier of the asset for the account to be created.
|
||||
///
|
||||
/// Emits `Touched` event when successful.
|
||||
#[pallet::weight(T::WeightInfo::mint())]
|
||||
pub fn touch(origin: OriginFor<T>, #[pallet::compact] id: T::AssetId) -> DispatchResult {
|
||||
Self::do_touch(id, ensure_signed(origin)?)
|
||||
}
|
||||
|
||||
/// Return the deposit (if any) of an asset account.
|
||||
///
|
||||
/// The origin must be Signed.
|
||||
///
|
||||
/// - `id`: The identifier of the asset for the account to be created.
|
||||
/// - `allow_burn`: If `true` then assets may be destroyed in order to complete the refund.
|
||||
///
|
||||
/// Emits `Refunded` event when successful.
|
||||
#[pallet::weight(T::WeightInfo::mint())]
|
||||
pub fn refund(
|
||||
origin: OriginFor<T>,
|
||||
#[pallet::compact] id: T::AssetId,
|
||||
allow_burn: bool,
|
||||
) -> DispatchResult {
|
||||
Self::do_refund(id, ensure_signed(origin)?, allow_burn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,10 @@
|
||||
use super::*;
|
||||
use crate as pallet_assets;
|
||||
|
||||
use frame_support::{construct_runtime, parameter_types, traits::GenesisBuild};
|
||||
use frame_support::{
|
||||
construct_runtime,
|
||||
traits::{ConstU32, ConstU64, GenesisBuild},
|
||||
};
|
||||
use sp_core::H256;
|
||||
use sp_runtime::{
|
||||
testing::Header,
|
||||
@@ -42,9 +45,6 @@ construct_runtime!(
|
||||
}
|
||||
);
|
||||
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: u64 = 250;
|
||||
}
|
||||
impl frame_system::Config for Test {
|
||||
type BaseCallFilter = frame_support::traits::Everything;
|
||||
type BlockWeights = ();
|
||||
@@ -59,7 +59,7 @@ impl frame_system::Config for Test {
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Header = Header;
|
||||
type Event = Event;
|
||||
type BlockHashCount = BlockHashCount;
|
||||
type BlockHashCount = ConstU64<250>;
|
||||
type DbWeight = ();
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
@@ -69,17 +69,14 @@ impl frame_system::Config for Test {
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = ();
|
||||
type OnSetCode = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const ExistentialDeposit: u64 = 1;
|
||||
type MaxConsumers = ConstU32<2>;
|
||||
}
|
||||
|
||||
impl pallet_balances::Config for Test {
|
||||
type Balance = u64;
|
||||
type DustRemoval = ();
|
||||
type Event = Event;
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type ExistentialDeposit = ConstU64<1>;
|
||||
type AccountStore = System;
|
||||
type WeightInfo = ();
|
||||
type MaxLocks = ();
|
||||
@@ -87,25 +84,18 @@ impl pallet_balances::Config for Test {
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const AssetDeposit: u64 = 1;
|
||||
pub const ApprovalDeposit: u64 = 1;
|
||||
pub const StringLimit: u32 = 50;
|
||||
pub const MetadataDepositBase: u64 = 1;
|
||||
pub const MetadataDepositPerByte: u64 = 1;
|
||||
}
|
||||
|
||||
impl Config for Test {
|
||||
type Event = Event;
|
||||
type Balance = u64;
|
||||
type AssetId = u32;
|
||||
type Currency = Balances;
|
||||
type ForceOrigin = frame_system::EnsureRoot<u64>;
|
||||
type AssetDeposit = AssetDeposit;
|
||||
type MetadataDepositBase = MetadataDepositBase;
|
||||
type MetadataDepositPerByte = MetadataDepositPerByte;
|
||||
type ApprovalDeposit = ApprovalDeposit;
|
||||
type StringLimit = StringLimit;
|
||||
type AssetDeposit = ConstU64<1>;
|
||||
type AssetAccountDeposit = ConstU64<10>;
|
||||
type MetadataDepositBase = ConstU64<1>;
|
||||
type MetadataDepositPerByte = ConstU64<1>;
|
||||
type ApprovalDeposit = ConstU64<1>;
|
||||
type StringLimit = ConstU32<50>;
|
||||
type Freezer = TestFreezer;
|
||||
type WeightInfo = ();
|
||||
type Extra = ();
|
||||
|
||||
@@ -34,6 +34,97 @@ fn basic_minting_should_work() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minting_too_many_insufficient_assets_fails() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Assets::force_create(Origin::root(), 0, 1, false, 1));
|
||||
assert_ok!(Assets::force_create(Origin::root(), 1, 1, false, 1));
|
||||
assert_ok!(Assets::force_create(Origin::root(), 2, 1, false, 1));
|
||||
Balances::make_free_balance_be(&1, 100);
|
||||
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
|
||||
assert_ok!(Assets::mint(Origin::signed(1), 1, 1, 100));
|
||||
assert_noop!(Assets::mint(Origin::signed(1), 2, 1, 100), TokenError::CannotCreate);
|
||||
|
||||
Balances::make_free_balance_be(&2, 1);
|
||||
assert_ok!(Assets::transfer(Origin::signed(1), 0, 2, 100));
|
||||
assert_ok!(Assets::mint(Origin::signed(1), 2, 1, 100));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minting_insufficient_asset_with_deposit_should_work_when_consumers_exhausted() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Assets::force_create(Origin::root(), 0, 1, false, 1));
|
||||
assert_ok!(Assets::force_create(Origin::root(), 1, 1, false, 1));
|
||||
assert_ok!(Assets::force_create(Origin::root(), 2, 1, false, 1));
|
||||
Balances::make_free_balance_be(&1, 100);
|
||||
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
|
||||
assert_ok!(Assets::mint(Origin::signed(1), 1, 1, 100));
|
||||
assert_noop!(Assets::mint(Origin::signed(1), 2, 1, 100), TokenError::CannotCreate);
|
||||
|
||||
assert_ok!(Assets::touch(Origin::signed(1), 2));
|
||||
assert_eq!(Balances::reserved_balance(&1), 10);
|
||||
|
||||
assert_ok!(Assets::mint(Origin::signed(1), 2, 1, 100));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minting_insufficient_assets_with_deposit_without_consumer_should_work() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Assets::force_create(Origin::root(), 0, 1, false, 1));
|
||||
assert_noop!(Assets::mint(Origin::signed(1), 0, 1, 100), TokenError::CannotCreate);
|
||||
Balances::make_free_balance_be(&1, 100);
|
||||
assert_ok!(Assets::touch(Origin::signed(1), 0));
|
||||
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
|
||||
assert_eq!(Balances::reserved_balance(&1), 10);
|
||||
assert_eq!(System::consumers(&1), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refunding_asset_deposit_with_burn_should_work() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Assets::force_create(Origin::root(), 0, 1, false, 1));
|
||||
Balances::make_free_balance_be(&1, 100);
|
||||
assert_ok!(Assets::touch(Origin::signed(1), 0));
|
||||
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
|
||||
assert_ok!(Assets::refund(Origin::signed(1), 0, true));
|
||||
assert_eq!(Balances::reserved_balance(&1), 0);
|
||||
assert_eq!(Assets::balance(1, 0), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refunding_asset_deposit_with_burn_disallowed_should_fail() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Assets::force_create(Origin::root(), 0, 1, false, 1));
|
||||
Balances::make_free_balance_be(&1, 100);
|
||||
assert_ok!(Assets::touch(Origin::signed(1), 0));
|
||||
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
|
||||
assert_noop!(Assets::refund(Origin::signed(1), 0, false), Error::<Test>::WouldBurn);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refunding_asset_deposit_without_burn_should_work() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Assets::force_create(Origin::root(), 0, 1, false, 1));
|
||||
assert_noop!(Assets::mint(Origin::signed(1), 0, 1, 100), TokenError::CannotCreate);
|
||||
Balances::make_free_balance_be(&1, 100);
|
||||
assert_ok!(Assets::touch(Origin::signed(1), 0));
|
||||
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
|
||||
Balances::make_free_balance_be(&2, 100);
|
||||
assert_ok!(Assets::transfer(Origin::signed(1), 0, 2, 100));
|
||||
assert_eq!(Assets::balance(0, 2), 100);
|
||||
assert_eq!(Assets::balance(0, 1), 0);
|
||||
assert_eq!(Balances::reserved_balance(&1), 10);
|
||||
assert_ok!(Assets::refund(Origin::signed(1), 0, false));
|
||||
assert_eq!(Balances::reserved_balance(&1), 0);
|
||||
assert_eq!(Assets::balance(1, 0), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approval_lifecycle_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
@@ -299,17 +390,17 @@ fn min_balance_should_work() {
|
||||
|
||||
// When deducting from an account to below minimum, it should be reaped.
|
||||
assert_ok!(Assets::transfer(Origin::signed(1), 0, 2, 91));
|
||||
assert!(Assets::balance(0, 1).is_zero());
|
||||
assert!(Assets::maybe_balance(0, 1).is_none());
|
||||
assert_eq!(Assets::balance(0, 2), 100);
|
||||
assert_eq!(Asset::<Test>::get(0).unwrap().accounts, 1);
|
||||
|
||||
assert_ok!(Assets::force_transfer(Origin::signed(1), 0, 2, 1, 91));
|
||||
assert!(Assets::balance(0, 2).is_zero());
|
||||
assert!(Assets::maybe_balance(0, 2).is_none());
|
||||
assert_eq!(Assets::balance(0, 1), 100);
|
||||
assert_eq!(Asset::<Test>::get(0).unwrap().accounts, 1);
|
||||
|
||||
assert_ok!(Assets::burn(Origin::signed(1), 0, 1, 91));
|
||||
assert!(Assets::balance(0, 1).is_zero());
|
||||
assert!(Assets::maybe_balance(0, 1).is_none());
|
||||
assert_eq!(Asset::<Test>::get(0).unwrap().accounts, 0);
|
||||
});
|
||||
}
|
||||
@@ -488,7 +579,7 @@ fn transferring_amount_more_than_available_balance_should_not_work() {
|
||||
assert_eq!(Assets::balance(0, 2), 50);
|
||||
assert_ok!(Assets::burn(Origin::signed(1), 0, 1, u64::MAX));
|
||||
assert_eq!(Assets::balance(0, 1), 0);
|
||||
assert_noop!(Assets::transfer(Origin::signed(1), 0, 1, 50), Error::<Test>::BalanceLow);
|
||||
assert_noop!(Assets::transfer(Origin::signed(1), 0, 1, 50), Error::<Test>::NoAccount);
|
||||
assert_noop!(Assets::transfer(Origin::signed(2), 0, 1, 51), Error::<Test>::BalanceLow);
|
||||
});
|
||||
}
|
||||
@@ -536,7 +627,7 @@ fn burning_asset_balance_with_zero_balance_does_nothing() {
|
||||
assert_ok!(Assets::force_create(Origin::root(), 0, 1, true, 1));
|
||||
assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100));
|
||||
assert_eq!(Assets::balance(0, 2), 0);
|
||||
assert_ok!(Assets::burn(Origin::signed(1), 0, 2, u64::MAX));
|
||||
assert_noop!(Assets::burn(Origin::signed(1), 0, 2, u64::MAX), Error::<Test>::NoAccount);
|
||||
assert_eq!(Assets::balance(0, 2), 0);
|
||||
assert_eq!(Assets::total_supply(0), 100);
|
||||
});
|
||||
@@ -688,7 +779,7 @@ fn force_metadata_should_work() {
|
||||
);
|
||||
|
||||
// string length limit check
|
||||
let limit = StringLimit::get() as usize;
|
||||
let limit = 50usize;
|
||||
assert_noop!(
|
||||
Assets::force_set_metadata(
|
||||
Origin::root(),
|
||||
|
||||
@@ -26,6 +26,8 @@ use sp_runtime::{traits::Convert, FixedPointNumber, FixedPointOperand, FixedU128
|
||||
|
||||
pub(super) type DepositBalanceOf<T, I = ()> =
|
||||
<<T as Config<I>>::Currency as Currency<<T as SystemConfig>::AccountId>>::Balance;
|
||||
pub(super) type AssetAccountOf<T, I> =
|
||||
AssetAccount<<T as Config<I>>::Balance, DepositBalanceOf<T, I>, <T as Config<I>>::Extra>;
|
||||
|
||||
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
|
||||
pub struct AssetDetails<Balance, AccountId, DepositBalance> {
|
||||
@@ -76,14 +78,47 @@ pub struct Approval<Balance, DepositBalance> {
|
||||
pub(super) deposit: DepositBalance,
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default, MaxEncodedLen, TypeInfo)]
|
||||
pub struct AssetBalance<Balance, Extra> {
|
||||
#[test]
|
||||
fn ensure_bool_decodes_to_consumer_or_sufficient() {
|
||||
assert_eq!(false.encode(), ExistenceReason::<()>::Consumer.encode());
|
||||
assert_eq!(true.encode(), ExistenceReason::<()>::Sufficient.encode());
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
|
||||
pub enum ExistenceReason<Balance> {
|
||||
#[codec(index = 0)]
|
||||
Consumer,
|
||||
#[codec(index = 1)]
|
||||
Sufficient,
|
||||
#[codec(index = 2)]
|
||||
DepositHeld(Balance),
|
||||
#[codec(index = 3)]
|
||||
DepositRefunded,
|
||||
}
|
||||
|
||||
impl<Balance> ExistenceReason<Balance> {
|
||||
pub(crate) fn take_deposit(&mut self) -> Option<Balance> {
|
||||
if !matches!(self, ExistenceReason::DepositHeld(_)) {
|
||||
return None
|
||||
}
|
||||
if let ExistenceReason::DepositHeld(deposit) =
|
||||
sp_std::mem::replace(self, ExistenceReason::DepositRefunded)
|
||||
{
|
||||
return Some(deposit)
|
||||
} else {
|
||||
return None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
|
||||
pub struct AssetAccount<Balance, DepositBalance, Extra> {
|
||||
/// The balance.
|
||||
pub(super) balance: Balance,
|
||||
/// Whether the account is frozen.
|
||||
pub(super) is_frozen: bool,
|
||||
/// `true` if this balance gave the account a self-sufficient reference.
|
||||
pub(super) sufficient: bool,
|
||||
/// The reason for the existence of the account.
|
||||
pub(super) reason: ExistenceReason<DepositBalance>,
|
||||
/// Additional "sidecar" data, in case some other pallet wants to use this storage item.
|
||||
pub(super) extra: Extra,
|
||||
}
|
||||
@@ -124,12 +159,15 @@ pub struct DestroyWitness {
|
||||
pub trait FrozenBalance<AssetId, AccountId, Balance> {
|
||||
/// Return the frozen balance.
|
||||
///
|
||||
/// Under normal behaviour, the account balance should not go below the sum of this (if `Some`)
|
||||
/// and the asset's minimum balance.
|
||||
/// But the account balance can be below this sum (e.g. if less than the sum has been
|
||||
/// transfered to the account).
|
||||
/// Generally, the balance of every account must be at least the sum of this (if `Some`) and
|
||||
/// the asset's `minimum_balance` (the latter since there may be complications to destroying an
|
||||
/// asset's account completely).
|
||||
///
|
||||
/// In special case (privileged intervention) the account balance can go below the sum.
|
||||
/// Under normal behaviour, the account balance should not go below the sum of this (if `Some`)
|
||||
/// and the asset's minimum balance. However, the account balance may reasonably begin below
|
||||
/// this sum (e.g. if less than the sum had ever been transfered into the account).
|
||||
///
|
||||
/// In special cases (privileged intervention) the account balance may also go below the sum.
|
||||
///
|
||||
/// If `None` is returned, then nothing special is enforced.
|
||||
fn frozen_balance(asset: AssetId, who: &AccountId) -> Option<Balance>;
|
||||
|
||||
Reference in New Issue
Block a user