The Stability Pool's deposit and withdrawal mechanisms do not correctly account for the scaling effects of getNormalizedIncome()
, leading to a critical flaw where users lose RTokens permanently. When depositing, RTokens are scaled down but users still receive deTokens at a 1:1 ratio, meaning they are credited more than they should be. Upon withdrawal, deTokens are burned at the same 1:1 ratio, but due to the liquidity index increasing over time, fewer RTokens are effectively returned to the user than initially deposited. This discrepancy results in trapped RTokens in the Stability Pool that cannot be withdrawn, leading to a continuous accumulation of excess funds.
Issue 1: Deposited RTokens Are Scaled Down, But deTokens Are Minted 1:1
When a user deposits RTokens into the Stability Pool, they receive an equivalent amount of deTokens (1:1 exchange rate).
However, due to the overridden _update()
function in RToken.sol, the actual amount of RTokens received by the contract is scaled down based on getNormalizedIncome()
.
The user is effectively credited more deTokens
than the scaled-down RTokens
they actually contributed to StabilityPool.sol, leading to an imbalance between the two token supplies.
Example Scenario:
User deposits 1000 RTokens and expects to receive 1000 deTokens.
Assuming getNormalizedIncome() == 1.1
, only 1000/1.1 = 909 RTokens are actually received by the Stability Pool.
User still receives 1000 deTokens, creating an over-crediting issue.
Issue 2: Withdrawals Transfer More RTokens Than the User Initially Deposited
Over time, reserve.liquidityIndex
increases due to linearly accrued interest.
When a user later withdraws by burning their deTokens
, the contract transfers RTokens
at a 1:1 ratio, but since the liquidity index has increased, the RTokens
they receive are now worth more than when they initially deposited.
If the withdrawal logic does not properly account for this increase in value, the user gets fewer RTokens than they initially deposited. This leads to leftover RTokens accumulating in the Stability Pool that cannot be withdrawn, since the user has already burned all their deTokens.
Example Scenario:
User burns 1000 deTokens to withdraw RTokens.
The contract transfers 1000 RTokens, but due to interest accrual, they are worth more than the originally deposited RTokens. Assuming getNormalizedIncome() == 1.3
, only 1000/1.3 = 769 RTokens will be transferred back to the user even though they have deposited less, i.e. 909 instead of 1000 tokens.
The discrepancy results in "leftover", i.e. 909 - 769 = 140 RTokens
in the Stability Pool that no user can withdraw, leading to trapped liquidity.
deTokens
are over-credited upon deposit incurring static shift which has nonetheless kept the rewarding logic intact. Nevertheless, it belies the very fact that users cannot reclaim the full amount of their originally deposited RTokens, leading to permanent losses. In the end, the Stability Pool accumulates excess RTokens that can never be withdrawn, causing inefficiencies in the system.
The entire issues is circumvented only when deposit()
and withdraw()
are carried out atomically where the over-credited 1000 value is scaled down perfectly to 909 again while both transfers are referencing the same reserve.liquidityIndex
. (Note: This will greatly exacerbate the Vampire Attack on draining the currently unweighted and checkpoint free RAAC rewards distribution that I have reported separately.)
Manual
Consider implementing the following refactoring:
Note: The above fixes are based on the assumption that the issue on double scaling down associated with RToken transfers has been resolved that I have separately reported.
Note: This also underscores the need of having the state updated prior to making any transfer as I have also reported separately. Without the latest state update, users end up transferring more RTokens than intended to the contract. The excess amount of RTokens could have been used/leveraged elsewhere.
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.