When a borrower is liquidated through the finalizeLiquidation
function in the LendingPool
contract, their NFT collateral is transferred to the StabilityPool
. However, there is no functionality in the StabilityPool
contract to transfer or withdraw these NFTs, resulting in them being permanently locked in the contract.
The issue occurs in the following sequence:
User deposits his NFT as collateral to borrow liquidity from the LendingPool
Collateral price drops
Someone kicks of the liquidation process
Borrower fails to repay his debt
StabilityPool liquidates the Borrowers position
LiquidityPool sends all Borrower NFTs to the StabilityPool
NFTs are "stuck" now in the StabilityPool
When we look at the finalizeLiquidation()
function below we can see that all of the NFTs gets transferred to the StabilityPool:
The Proof of concept below demonstrates that the NFT get transferred to the StabilityPool, however there is no transfer / withdraw function in the StabilityPool. We can easily verify that there is no way to transfer the NFT by searching for the RAACNFT or IRAACNFT contract which should be provided to the StabilityPool contract in order to transfer/withdraw the NFT.
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
It's also worth to mention here that during the minting process of the NFT the provided capital is locked in the NFT contract but this should be used to repay the users debt when he gets liquidated (see issue: "Permanent Fund Lock in RAACNFT Contract Due to Missing Fund Distribution Logic").
This combination of locked NFTs and inaccessible minting capital leads to:
NFTs used as collateral become permanently locked in the StabilityPool contract after liquidation
The value of these NFTs is effectively lost to the protocol and its users
The protocol's ability to handle liquidations properly is compromised as the collateral cannot be sold to cover the debt
Foundry
Manual Review
There are different solutions to this:
There could be some kind of auction mechanism after NFT gets send to the StabilityPool
The StabilityPool burn / sells the NFT and access the underlying collateral to cover the defaulted debt
use safeTransfer to validate that the receiving contract (StabilityPool) has a onERC721Received function
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.