A critical scaling error in the liquidation process causes the StabilityPool to use doubly-compounded debt values, leading to failed liquidations and protocol insolvency risk. The StabilityPool.liquidateBorrower
function incorrectly applies interest scaling twice when:
Incorrectly scaling debt twice by multiplying getUserDebt()
(already interest-adjusted) with getNormalizedDebt()
(the same interest index)
Checking CRVUSD balances against this over-inflated debt
Approving transfers using the incorrect scaled amount
This forces the StabilityPool to hold excess funds for liquidations beyond actual requirements, allowing undercollateralized positions to persist during market stress. The error fundamentally undermines the protocol's risk management system by preventing valid liquidations when most needed.
The vulnerability arises from an incorrect debt calculation in the liquidation process that uses doubly-scaled debt values. The key issue occurs in StabilityPool.liquidateBorrower
(StabilityPool.sol#L452-L462) where:
Double Scaling of Debt:
userDebt
is retrieved from LendingPool.getUserDebt
which already returns scaled debt (scaledDebtBalance * usageIndex)
This value is then incorrectly scaled again by multiplying with LendingPool.getNormalizedDebt
(which returns the same usageIndex)
Insufficient Balance Check:
StabilityPool.liquidateBorrower
checks CRVUSD balance against this doubly-scaled debt value rather than the actual debt amount
Improper Approval:
Approves the doubly-scaled value for transfer instead of the actual debt amount
The root cause stems from misunderstanding the return values of LendingPool
functions:
getUserDebt()
returns already scaled debt (rayMul(scaledDebtBalance, usageIndex))
getNormalizedDebt()
returns the same usage index used in the first scaling
This results in debt being scaled twice (userDebt * usageIndex²) when it should only be scaled once. The proper debt amount to use is the initial userDebt
value returned from getUserDebt()
, as the subsequent operations in LendingPool.finalizeLiquidation()
and DebtToken.burn()
expect the single-scaled value:
Under normal protocol operation where usageIndex > 1
(indicating accrued interest), this creates a dangerous mismatch:
Example Scenario: With usageIndex = 1.1e27
(10% interest) and actual debt of 100 CRVUSD:
Correct debt: 100 * 1.1 = 110 CRVUSD
Current calculation: 100 * 1.1 * 1.1 = 121 CRVUSD
This forces the StabilityPool to hold 21% more CRVUSD than actually needed for liquidation. Valid liquidations will fail unnecessarily when the pool has sufficient (but not excess) funds, while protocol-level debt continues accumulating.
The error directly undermines the protocol's liquidation safety mechanism, creating systemic risk by allowing undercollateralized positions to persist unaddressed. This could lead to cascading insolvency if multiple positions enter liquidation simultaneously during market stress.
This vulnerability creates a critical failure in the protocol's liquidation mechanism with severe consequences:
Failed Liquidations During Market Stress
When the utilization index > 1 (normal operation with accrued interest), the StabilityPool will reject valid liquidations unless it holds excess CRVUSD beyond actual debt obligations. This allows undercollateralized positions to persist, accumulating more risk over time.
Protocol Insolvency Risk
Unliquidated bad debt directly threatens protocol solvency. As interest compounds on unresolved positions, the system's liability grows exponentially while collateral values remain static.
Cascading Liquidation Failures
A single failed liquidation creates a domino effect - the unresolved debt reduces available liquidity, increasing the likelihood of subsequent liquidation failures during market downturns.
The vulnerability fundamentally breaks a critical safety mechanism designed to maintain protocol health, creating existential risk during periods of market volatility.
Manual Review
Fix Redundant Scaling in StabilityPool.liquidateBorrower
:
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.