The StabilityPool
implements a fixed 1:1 exchange rate between rToken
(interest-bearing) and deToken
, causing yield to be permanently stuck in the contract. Additionally, liquidations could lead to contract insolvency as rTokens
are withdrawn without accounting for user deposits.
The StabilityPool has two critical issues in its exchange rate implementation:
In withdraw()
, the contract burns deTokens and transfers rTokens at a 1:1 ratio, ignoring that rToken is interest-bearing:
During liquidations, rTokens are withdrawn from the lending pool to repay borrower debt without accounting for user deposits, potentially leading to insolvency. Please note that this second issue is a guess from my side, as the code is not clear about it. The liquidateBorrower()
function is not clear about how it gets the crvUSD
to repay the debt. There is no way to deposit or withdraw crvUSD
from the StabilityPool
contract. The only way to get crvUSD
is to withdraw the rToken
from the LendingPool
contract. Which also would give purpose to the deposited rToken
in the StabilityPool
contract by users. The other option would be that some external actor just transfers crvUSD
to the StabilityPool
contract like it is done in the tests. However, this does not make sense as this actor would not get any benefit from it.
The getExchangeRate()
function has a commented out implementation that would track the proper exchange rate, but currently just returns 1e18:
Permanent loss of yield: When users withdraw, they only receive their initial deposit amount in rTokens, while the accrued interest remains stuck in the contract forever.
Potential insolvency: During liquidations, rTokens could be withdrawn to repay debt, reducing the contract's rToken balance below user deposits. This can lead to later withdrawals failing due to insufficient funds.
Example scenario for yield loss:
User A deposits 100 rTokens (worth 100 crvUSD) and receives 100 deTokens
After time T, due to yield, the 100 rTokens are now worth 110 crvUSD
User A withdraws with 100 deTokens
They only receive 100 rTokens (worth 100 crvUSD)
The 10 crvUSD worth of yield is permanently stuck in the contract
Manual review
First a bug need to be fixed in the RToken
contract to make sure the correct amount of rTokens is transferred, otherwise there will be a dust left in the contract which is not caused by the yield. The transfer()
function is scaling the amount down twice, once directly and the other in the _update()
function.
Add the following test case to the test/e2e/protocols-tests.js
file in the StabilityPool
section:
Implement proper exchange rate tracking:
Enable the commented out code in getExchangeRate()
Update exchange rate calculations during deposits/withdrawals
Consider making deToken interest-bearing to match rToken growth
For liquidations:
Track rToken withdrawals during liquidations
Implement a mechanism to fairly distribute losses among depositors
Consider requiring overcollateralization to protect against insolvency
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.