2

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();

            }
        });
    }

}

2
  • I love the detailed question. I looks like you have already answered it yourself, I guess the question here is 'how you can make the accounts on your chain be able to hold more than 16 assets?' Would creating these assets in a local pallet-assets solve the issue? Oct 3, 2023 at 5:03
  • Thanks! it seems that this is the case yes. What do you mean by a "local" pallet-assets? If you mean a new instance then I don't think so since the consumer count is stored in the frame-system pallet. If I understood correctly, pallet-assets max out the consumer limit for that account would have implications on the functioning of other pallets :( Oct 4, 2023 at 15:51

1 Answer 1

1

An account can own more than 16 assets. If someone places a deposit (in DOT), inc_consumers won't be called.

        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)
                // ...        
        }

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.