Skip to content

Cannot reserve when (free balance) is lower than (frozen balance) #8099

@RomarQ

Description

@RomarQ

Description

I am creating this issue to confirm if the behaviour reported below is a bug or not. If it is not a bug, the documentation should be updated with an example explaining it.

Recently, for moonbeam, we received a bug report where the user could not add a proxy, even though he had enough usable balance to pay for the deposit.

Based on the documentation, the usable balance is computed with the following formula:

// If Fortitude == Polite 
let usable_balance = free - max(frozen - reserved, existential balance)

The problem:

If an account's free balance is lower than frozen balance, no reserves will be allowed even though the usable balance is enough to cover the reserve, resulting in the following error LiquidityRestrictions.

Based on the documentation, this should not happen.

Visual example of how usable balance works:

|__total__________________________________|
|__on_hold__|_____________free____________|
|__________frozen___________|
|__on_hold__|__ed__|
            |__untouchable__|__spendable__|

Based on the account example below, the user should be able to reserve an amount of 5 tokens.

free: 5
frozen: 10
reserved: 10

In my understanding, the function below is incorrect, and instead of checking that the new free balance is higher or equal to the frozen balance, it should make sure that amount is less or equal to the usable balance.

fn ensure_can_withdraw(
who: &T::AccountId,
amount: T::Balance,
_reasons: WithdrawReasons,
new_balance: T::Balance,
) -> DispatchResult {
if amount.is_zero() {
return Ok(())
}
ensure!(new_balance >= Self::account(who).frozen, Error::<T, I>::LiquidityRestrictions);
Ok(())
}

If it is confirmed to be a bug, I can work on the fix and open a pull request.

Suggested fix for function ensure_can_withdraw:

	fn ensure_can_withdraw(
		who: &T::AccountId,
		amount: T::Balance,
		reasons: WithdrawReasons,
		new_balance: T::Balance,
	) -> DispatchResult {
		if amount.is_zero() {
			return Ok(())
		}
		let account = Self::account(who);

		// Calculate the new reserved amount only if withdrawing for reservation.
		let updated_reserved = if matches!(reasons, WithdrawReasons::RESERVE) {
			account.reserved.saturating_add(amount)
		} else {
			account.reserved
		};

		// Frozen balance applies to total. Anything on hold therefore gets discounted from the
		// limit given by the freezes minus reserves.
		let untouchable = account
			.frozen
			.saturating_sub(updated_reserved)
			.max(T::ExistentialDeposit::get());
		ensure!(new_balance >= untouchable, Error::<T, I>::LiquidityRestrictions);
		Ok(())
	}

Steps to reproduce

  1. Have an account with 20 tokens
  2. Set a lock with 10 tokens
  3. Reserve 10 tokens
  4. Try to reserve 1 token // This will fail with LiquidityRestrictions
    4.1. (But transferring the spendable balance works)

Metadata

Metadata

Assignees

No one assigned

    Labels

    I10-unconfirmedIssue might be valid, but it's not yet known.I2-bugThe node fails to follow expected behavior.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions