Assessment reports>Prisma Finance>Critical findings>High-fraction liquidations can cause the product P to become 0
Category: Business Logic

High-fraction liquidations can cause the product P to become 0

Critical Severity
Critical Impact
High Likelihood

Description

During liquidations, each depositor is rewarded with the collateral and some amount of DebtTokens are burned from their deposits. As it would be impossible to update each user's balance during liquidation, the protocol uses a global running product P and sum S to derive the compounded DebtToken deposit and corresponding collateral gain as a function of the initial deposit. Refer to the Appendix ref for a list of terms and the derivation of P.

Continuous high-fraction liquidations can cause the value of the global-running product P to become 0, leading to potential disruptions in withdrawals and reward claims from the stability pool.

The function _updateRewardSumAndProduct is responsible for updating the value of P when it falls below 1e9 by multiplying it by 1e9. However, certain liquidation scenarios can update P in a way that multiplying it by 1e9 is insufficient to bring its value above 1e9.

Refer to the Appendix ref for the exploit of the vulnerability. Following is the output of the exploit:

Running 1 test for test/Exploit.t.sol:Exploit
[PASS] testPto0Exploit() (gas: 2053310)
Logs:
  Value of P after first liquidation 1000000000
  Value of P after second liquidation 2
  Value of P after third liquidation 0
  Alice's deposits in stability pool before the withdrawal 9999999999999999980000
  Alice's balance of debttoken before the withdrawal 70000000000000000040000
  Alice's deposits in stability pool after the withdrawal 0
  Alice's balance of debttoken after the withdrawal 70000000000000000040000
  Alice's deposits are now erased from the pool without being returned

First thing to note is that if newProductFactor is 1, then multiplying by 1e9 is not enough to bring P back to the correct scale.

The value of newProductFactor can be set to 1 by making _debtLossPerUnitStaked equal to 1e18 - 1. This requires calculating the _debtToOffset value to pass to the offset function such that _debtLossPerUnitStaked is 1e18 - 1. The calculations for this are as follows:

_debtLossPerUnitStaked = ( _debtToOffset * 1e18 / _totalDebtTokenDeposits ) + 1
1e18 - 1 = ( _debtToOffset * 1e18 / _totalDebtTokenDeposits ) + 1  
// (We need _debtLossPerUnitStaked to be 1e18 - 1)
1e18 - 2 = ( _debtToOffset * 1e18 / _totalDebtTokenDeposits )
Fixing _totalDebtTokenDeposits to 10000 * 1e18
_debtToOffset = 10000e18 * (1e18 - 2) / 1e18
_debtToOffset = 9999999999999999980000

Performing a liquidation with _debtToOffset as 9999999999999999980000 can bring newP from 1e18 to 1e9 in one liquidation, assuming currentP is 1e18, due to the calculation in _updateRewardSumAndProduct:

newP = (currentP * newProductFactor * 1e9) / 1e18; (we already know newProductFactor is 1 )

Now, by creating three troves with the required debt amount, each having _debtToOffset as 9999999999999999980000, and subsequently liquidating them while maintaining the deposits in the stability pool at exactly 10,000 * 1e18, P becomes 0. Consequently, users may face difficulties withdrawing from the stability pool.

As _debtToOffset is the compositeDebt of the trove (the requested debt amount + debt borrowing fee + debt gas comp), we need to solve the following equation to calculate the _debtAmount needed to open such trove:

x + (x * 5000000000000000 / (1e18) ) + (200 * (1e18)) = 9999999999999999980000

Here x comes out to be x = 9751243781094527343284.

Using this _debtAmount, an attacker may open three troves, and when the ICR < MCR, they can liquidate the troves while maintaining the deposits in the SP to be exactly 10,000 * 1e18. After three liquidations, the value of P becomes 0.

Due to this, the function getCompoundedDebtDeposit will return 0 for all the depositors, and thus users would not be able to make any withdrawals from the stability pool.

Impact

Withdrawals and claimable rewards for any new deposits will fail as P_Snapshot stored for these deposits would be 0.

Recommendations

Add an assertion as shown below in _updateRewardSumAndProduct so that such high-fraction liquidations would be reverted.

+       assert(newP > 0);
        P = newP;

        emit P_Updated(newP);

Remediation

This issue has been acknowledged by Prisma Finance, and a fix was implemented in commit ecc58eb7.

Zellic © 2025Back to top ↑