Bug description
When liquidateBorrower
is called, it gets the user's debt from the lending pool via a call to getUserDebt
.
LendingPool.sol#L579-L582
function getUserDebt(address userAddress) public view returns (uint256) {
UserData storage user = userData[userAddress];
return user.scaledDebtBalance.rayMul(reserve.usageIndex);
}
As can be seen, because user's debt is multiplied by usageIndex, it includes interest accrued to it.
However, liquidateBorrower()
scales the obtained value by usageIndex again.
StabilityPool.sol#L452-L458
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();
And since the function requires that the balance must have at least scaledUserDebt
, which has interest accrued twice to it, it will require more funds than necessary to liquidate a position. And because finalizeLiquidation()
will only transfer out the actual debt, it will leave the excess in the stability pool without a way to retrieve it.
LendingPool.sol#L508-L525
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex); <----@ debt with interest accrued to it only once
isUnderLiquidation[userAddress] = false;
liquidationStartTime[userAddress] = 0;
for (uint256 i = 0; i < user.nftTokenIds.length; i++) {
uint256 tokenId = user.nftTokenIds[i];
user.depositedNFTs[tokenId] = false;
raacNFT.transferFrom(address(this), stabilityPool, tokenId);
}
delete user.nftTokenIds;
(
uint256 amountScaled, <----@ debt with interest accrued only once
uint256 newTotalSupply,
uint256 amountBurned,
uint256 balanceIncrease
) = IDebtToken(reserve.reserveDebtTokenAddress).burn(
userAddress,
userDebt,
reserve.usageIndex
);
IERC20(reserve.reserveAssetAddress).safeTransferFrom(
msg.sender,
reserve.reserveRTokenAddress,
amountScaled <----@ actual debt with interest accrued only once is transferred out
);
Impact
liquidateBorrower
requires more funds the necessary to liquidate a position, leaving excess on the balance of the stability pool without a way to retrieve it.
Proof of Concept
Please add import "hardhat/console.sol";
to the top of the StabilityPool.sol
and LendingPool.sol
.
Modify lending pool's finalizeLiquidation()
function.
LendingPool.sol#L522-L523
(
uint256 amountScaled,
uint256 newTotalSupply,
uint256 amountBurned,
uint256 balanceIncrease
) = IDebtToken(reserve.reserveDebtTokenAddress).burn(
userAddress,
userDebt,
reserve.usageIndex
);
+ console.log("Lending pool debt: ", amountScaled);
Please modify stability pool's liquidateBorrower()
function.
StabilityPool.sol#L452-L454
+ lendingPool.updateState(); // to fix another bug
uint256 userDebt = lendingPool.getUserDebt(userAddress);
uint256 scaledUserDebt = WadRayMath.rayMul(
userDebt,
lendingPool.getNormalizedDebt()
);
+ console.log("stabilityPool debt: ", scaledUserDebt);
Please add this test to StabilityPool.test.js
and run it with npx hardhat test --grep "liquidateBorrower scales debt twice"
.
describe("sl1", function () {
it("liquidateBorrower scales debt twice", async () => {
await raacHousePrices.setHousePrice(1, ethers.parseEther("1000"));
await ethers.provider.send("evm_mine", []);
const tokenId = 1;
const amountToPay = ethers.parseEther("1000");
await crvusd.mint(user1.address, amountToPay);
await crvusd.connect(user1).approve(raacNFT.target, amountToPay);
await raacNFT.connect(user1).mint(tokenId, amountToPay);
await raacNFT.connect(user1).approve(lendingPool.target, tokenId);
await lendingPool.connect(user1).depositNFT(tokenId);
await lendingPool.connect(user1).borrow(ethers.parseEther("1000"));
await lendingPool.initiateLiquidation(user1.address);
await ethers.provider.send("evm_increaseTime", [72 * 60 * 60 + 1]);
await ethers.provider.send("evm_mine");
await crvusd.mint(
await stabilityPool.getAddress(),
ethers.parseEther("1001")
);
await stabilityPool.liquidateBorrower(user1.address);
});
};
The console output of the test:
stabilityPool debt: 1000925088732468965074
Lending pool debt: 1000462437442040582019
As can be seen, debt calculated in stability pool is greater than the actual debt in lending pool.
Recommended Mitigation
Do not scale the debt second time in StabilityPool::liquidateBorrower()
StabilityPool.sol#L452-L453
uint256 userDebt = lendingPool.getUserDebt(userAddress);
- uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());