The LendingPool
contract implements a grace period for liquidations, during which borrowers cannot be liquidated even if their collateral value continues to decline. This creates a scenario where the protocol may incur larger losses than necessary, as positions that become deeply underwater during the grace period cannot be liquidated until the period expires.
The liquidation process in the LendingPool contract requires:
Someone calls initiateLiquidation()
when a position becomes unhealthy (health factor below threshold)
A 3-day grace period begins (configurable up to 7 days via liquidationGracePeriod
)
During this period:
The borrower can repay their debt and call closeLiquidation()
No one can liquidate the position, even if collateral value drops further
Only after the grace period expires can the Stability Pool call finalizeLiquidation()
The Collateral value declines further during the grace period
At the point where the collateral value equals the total borrowed amount + interest, the liquidation must happen to cover the debt.
The grace period has not ended yet, so there is no way to liquidate the position
The Collateral value declines further, below the borrowed amount => there is no reason now to for the borrower to pay back his debt because the borrowed amount is more worth than the collateral.
After the grace period someone needs to cover the bad debt (most likely the RAAC protocol itself) otherwise there is a high risk of insolvency.
Key problematic code sections:
The issue is that there is no mechanism to bypass or shorten the grace period, even when positions become severely underwater.
This test assumes that the issue in the StabilityPool::liquidateBorrower() function for the approval has been fixed (see issue: "StabilityPool can't liquidate positions because of wrong user debt amount being approved causing the transaction to fail")
For the purpose of this test I modified the function to approve type(uint256).max. This shouldn't be done in production and there is already a recommendation in the issue mentioned above.
Update Line 461 in the liquidateBorrower() function :
function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {- bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);+ bool approveSuccess = crvUSDToken.approve(address(lendingPool), type(uint256).max);}
In order to run the test you need to:
Run foundryup
to get the latest version of Foundry
Install hardhat-foundry: npm install --save-dev @nomicfoundation/hardhat-foundry
Import it in your Hardhat config: require("@nomicfoundation/hardhat-foundry");
Make sure you've set the BASE_RPC_URL
in the .env
file or comment out the forking
option in the hardhat config.
Run npx hardhat init-foundry
There is one file in the test folder that will throw an error during compilation so rename the file in test/unit/libraries/ReserveLibraryMock.sol
to => ReserveLibraryMock.sol_broken
so it doesn't get compiled anymore (we don't need it anyways).
Create a new folder test/foundry
Paste the below code into a new test file i.e.: FoundryTest.t.sol
Run the test: forge test --mc FoundryTest -vvvv
For the purpose of this test I minted all of the NFTs to a single user which deposits them into the Lendingpool but in a more realistic scenario these NFTs are owned by different users. The impact is the same => If many users provide their NFT as collateral to borrow capital and some of the House Prices start to decline, there is no way to immediately liquidate those bad positions.
Increased losses for the protocol as underwater positions cannot be liquidated promptly
Strategic defaulting by borrowers when their collateral value falls significantly below their debt during the grace period
Greater risk for the Stability Pool, which must absorb larger losses
Potential protocol insolvency in cases of sharp market downturns
Manual Review
Foundry
Dynamic Grace Period => When the liquidation gets initialized set the grace period based on the current health factor of the borrower. Lower health factor => lower grace period for that specific borrower (userAddress => gracePeriod mapping) .
Implement some kind of Emergency Liquidation Threshold. If the health factor drops below the EMERGENCY_THRESHOLD set a state to immediately liquidate the borrower to access the collateral:
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.