Every new token a user holds in pallet_assets
increases the consumer count for that user +1. And most chains set the MaxConsumers
of frame_system
to 16.
Both mint_into
and transfer
call at some point on can_increase
, which calls can_accrue_consumers
. And if the asset is not set as sufficient, then it expects to be able to increase the consumers
substrate > frame > assets > src > functions.rs > impl Pallet > can_increase()
pub(super) fn can_increase(
id: T::AssetId,
who: &T::AccountId,
amount: T::Balance,
increase_supply: bool,
) -> DepositConsequence {
let details = match Asset::<T, I>::get(&id) {
Some(details) => details,
None => return DepositConsequence::UnknownAsset,
};
if increase_supply && details.supply.checked_add(&amount).is_none() {
return DepositConsequence::Overflow
}
if let Some(account) = Account::<T, I>::get(id, who) {
if account.status.is_blocked() {
return DepositConsequence::Blocked
}
if account.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>::can_accrue_consumers(who,{
return DepositConsequence::CannotCreate
}
if details.is_sufficient && details.sufficients.checked_add(1).is_none() {
return DepositConsequence::Overflow
}
}
DepositConsequence::Success
}
substrate > frame > system > src > lib.rs > impl Palet > can_accrue_consumers()
pub fn can_accrue_consumers(who: &T::AccountId, amount: u32) -> bool {
let a = Account::<T>::get(who);
match a.consumers.checked_add(amount) {
Some(c) => a.providers > 0 && c <= T::MaxConsumers::max_consumers(),
None => false,
}
}
After the checks, increase_balance
is called, which when that account-asset_id storage item does not exist, it increments the consumers by one:
substrate > frame > assets > src > functions.rs > impl Pallet > increase_balance()
pub(super) fn increase_balance(
id: T::AssetId,
beneficiary: &T::AccountId,
amount: T::Balance,
check: impl FnOnce(
&mut AssetDetails<T::Balance, T::AccountId, DepositBalanceOf<T, I>>,
) -> DispatchResult,
) -> DispatchResult {
if amount.is_zero() {
return Ok(())
}
Self::can_increase(id.clone(), beneficiary, amount, true).into_result()?;
Asset::<T, I>::try_mutate(&id, |maybe_details| -> DispatchResult {
let details = maybe_details.as_mut().ok_or(Error::<T, I>::Unknown)?;
ensure!(details.status == AssetStatus::Live, Error::<T, I>::AssetNotLive);
check(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)?,
status: AccountStatus::Liquid,
extra: T::Extra::default(),
});
},
}
Ok(())
})?;
Ok(())
})?;
Ok(())
}
substrate > frame > assets > src > functions.rs > impl Pallet > new_account()
pub(super) fn new_account(
who: &T::AccountId,
d: &mut AssetDetails<T::Balance, T::AccountId, DepositBalanceOf<T, I>>,
maybe_deposit: Option<(&T::AccountId, DepositBalanceOf<T, I>)>,
) -> Result<ExistenceReasonOf<T, I>, DispatchError> {
let accounts = d.accounts.checked_add(1).ok_or(ArithmeticError::Overflow)?;
let reason = if let Some((depositor, deposit)) = maybe_deposit {
if depositor == who {
ExistenceReason::DepositHeld(deposit)
} else {
ExistenceReason::DepositFrom(depositor.clone(), deposit)
}
} else if d.is_sufficient {
frame_system::Pallet::<T>::inc_sufficients(who);
d.sufficients += 1;
ExistenceReason::Sufficient
} else {
frame_system::Pallet::<T>::inc_consumers(who)
.map_err(|_| Error::<T, I>::UnavailableConsumer)?;
// We ensure that we can still increment consumers once more because we could otherwise
// allow accidental usage of all consumer references which could cause grief.
if !frame_system::Pallet::<T>::can_inc_consumer(who) {
frame_system::Pallet::<T>::dec_consumers(who);
return Err(Error::<T, I>::UnavailableConsumer.into())
}
ExistenceReason::Consumer
};
d.accounts = accounts;
Ok(reason)
}
As you can see, new_account
calls inc_consumers
when the asset is new for that account.
Since the AssetHub parachain also has MaxConsumers
set to 16, does that mean no one can hold more than 16 distinct assets in there?
For reference, here is the code I ran to test this:
mod sandbox {
use super::*;
use frame_support::traits::fungibles::Mutate as MutateFungibles;
use frame_support::traits::fungible::Mutate as MutateFungible;
use frame_support::traits::tokens::Preservation;
#[test]
fn transferring_more_than_max_consumers_tokens_to_one_user_fails() {
new_test_ext().execute_with(|| {
let receiver = 420u64;
mock::Balances::mint_into(&receiver, PLMC * 1000);
for admin in 0..20u64 {
mock::Balances::mint_into(&admin, PLMC * 1000);
mock::StatemintAssets::create(mock::RuntimeOrigin::signed(admin), (admin as u32).into(), admin, 1u128).unwrap();
mock::StatemintAssets::mint_into((admin as u32).into(), &admin, 100_0_000_000_000).unwrap();
<mock::StatemintAssets as MutateFungibles<AccountId>>::transfer((admin as u32).into(), &admin, &receiver, 50_0_000_000_000, Preservation::Preserve).unwrap();
}
});
}
#[test]
fn minting_more_than_max_consumers_tokens_to_one_user_fails() {
new_test_ext().execute_with(|| {
let receiver = 420u64;
mock::Balances::mint_into(&receiver, 10_000_000_000_u128 * 1000);
for admin in 0..20u64 {
mock::Balances::mint_into(&admin, 10_000_000_000_u128 * 1000);
mock::StatemintAssets::create(mock::RuntimeOrigin::signed(admin), (admin as u32).into(), admin, 1u128).unwrap();
mock::StatemintAssets::mint_into((admin as u32).into(), &receiver, 100_0_000_000_000).unwrap();
}
});
}
}