Summary
The function StabilityPool::liquidateBorrower()
allows the owner or manager to finalize a liquidation by using the funds in StabilityPool
contract. However, the function calculates the debt amount inaccurately which can cause liquidation reverts due to insufficient balance
Vulnerability Details
The function liquidateBorrower()
calls LendingPool contract to fetch user debt by lendingPool.getUserDebt(userAddress)
. The function LendingPool::getUserDebt()
returns the debt including interest.
The problem arises when that returned value is scaled once more by lending pool's normalized debt uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt())
. This causes the result of calculating debt higher than the actual position debt. Hence, this can make the balance check fail when the manager/owner only prepares enough funds (not excessive) for the liquidation.
function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
@> uint256 userDebt = lendingPool.getUserDebt(userAddress);
@> uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
if (userDebt == 0) revert InvalidAmount();
@> uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
@> if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
if (!approveSuccess) revert ApprovalFailed();
lendingPool.updateState();
lendingPool.finalizeLiquidation(userAddress);
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}
PoC
Add below test to test/unit/core/pools/StabilityPool/StabilityPool.test.js
describe("Deposits", function () {
it.only('debt is incorrectly computed in liquidate flow', async function(){
await userBorrow(user1, ethers.parseEther("100"), ethers.parseEther("100") , 1)
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine");
await lendingPool.connect(user2).initiateLiquidation(user1.address);
await ethers.provider.send("evm_increaseTime", [3 * 86400 + 1]);
await ethers.provider.send("evm_mine");
await lendingPool.updateState();
await stabilityPool.connect(owner).addManager(user2.address, 1n)
let debt = await lendingPool.getUserDebt(user1.address);
await crvusd.mint(user2.address, debt)
await crvusd.connect(user2).transfer(stabilityPool.target, debt)
await stabilityPool.connect(user2).liquidateBorrower(user1.address);
})
async function userBorrow(user, collateralValue, borrowAmount, nftId){
const tokenId = nftId;
const price = collateralValue;
await raacHousePrices.setOracle(owner.address);
await raacHousePrices.setHousePrice(tokenId, price);
await crvusd.mint(user.address, price)
await crvusd.connect(user).approve(raacNFT.target, price)
await raacNFT.connect(user).mint(tokenId, price);
await raacNFT.connect(user).approve(lendingPool.target, tokenId);
await lendingPool.connect(user).depositNFT(tokenId);
await lendingPool.connect(user).borrow(borrowAmount);
}
Run the test and console shows
StabilityPool
Core Functionality
Deposits
1) debt is incorrectly computed in liquidate flow
0 passing (2s)
1 failing
1) StabilityPool
Core Functionality
Deposits
debt is incorrectly computed in liquidate flow:
Error: VM Exception while processing transaction: reverted with custom error 'InsufficientBalance()'
at StabilityPool.liquidateBorrower (contracts/core/pools/StabilityPool/StabilityPool.sol:494)
Impact
Tools Used
Manual
Recommendations
function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
// Get the user's debt from the LendingPool.
uint256 userDebt = lendingPool.getUserDebt(userAddress);
- uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
if (userDebt == 0) revert InvalidAmount();
uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
- if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
+ if (crvUSDBalance < userDebt) revert InsufficientBalance();
// Approve the LendingPool to transfer the debt amount
bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
if (!approveSuccess) revert ApprovalFailed();
// Update lending pool state before liquidation
lendingPool.updateState();
// Call finalizeLiquidation on LendingPool
lendingPool.finalizeLiquidation(userAddress);
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}
`