When StabilityPool liquidates a borrower, it needs some crvUSD to cover borrower's total debt. However, crvUSD amount calculation is done incorrectly. StabiltyPool requires more crvUSD than user's actual debt.
This will lead to potential liquidation failure. Moreover, excess crvUSD will be stuck at StabilityPool.
StabilityPool needs to hold some crvUSD to cover user's total debt who is being liquidated.
If current crvUSD balance is below than user's total debt, it is expected to revert.
To see why the calculation is incorrect. Let's check how userDebt
and scaledUserDebt
variable is calculated.
In order to finalize the liquidation, managers are forced to deposit more crvUSD.
But LendingPool will only spend exact amount of user's debt, so excess crvUSD will be stuck at StabilityPool.
pragma solidity ^0.8.19;
import "../lib/forge-std/src/Test.sol";
import {RToken} from "../contracts/core/tokens/RToken.sol";
import {DebtToken} from "../contracts/core/tokens/DebtToken.sol";
import {DEToken} from "../contracts/core/tokens/DEToken.sol";
import {RAACToken} from "../contracts/core/tokens/RAACToken.sol";
import {RAACNFT} from "../contracts/core/tokens/RAACNFT.sol";
import {LendingPool} from "../contracts/core/pools/LendingPool/LendingPool.sol";
import {StabilityPool} from "../contracts/core/pools/StabilityPool/StabilityPool.sol";
import {IStabilityPool} from "../contracts/interfaces/core/pools/StabilityPool/IStabilityPool.sol";
import {RAACMinter} from "../contracts/core/minters/RAACMinter/RAACMinter.sol";
import {crvUSDToken} from "../contracts/mocks/core/tokens/crvUSDToken.sol";
import "../contracts/libraries/math/WadRayMath.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract RAACHousePricesMock {
mapping(uint256 => uint256) public prices;
function getLatestPrice(uint256 tokenId) external view returns (uint256, uint256) {
return (prices[tokenId], block.timestamp);
}
function setTokenPrice(uint256 tokenId, uint256 price) external {
prices[tokenId] = price;
}
function tokenToHousePrice(uint256 tokenId) external view returns (uint256) {
return prices[tokenId];
}
}
contract LendingPoolTest is Test {
using WadRayMath for uint256;
RToken rtoken;
DebtToken debtToken;
RAACToken raacToken;
DEToken deToken;
RAACNFT raacNft;
RAACMinter raacMinter;
crvUSDToken asset;
LendingPool lendingPool;
StabilityPool stabilityPool;
RAACHousePricesMock housePrice;
address depositor = makeAddr("depositor");
address borrower = makeAddr("borrower");
address user = makeAddr("user");
uint256 userAssetAmount = 10_000e18;
uint256 nftPrice = 50_000e18;
uint256 initialBurnTaxRate = 50;
uint256 initialSwapTaxRate = 100;
uint256 initialPrimeRate = 0.1e27;
function setUp() external {
vm.warp(1e9);
asset = new crvUSDToken(address(this));
housePrice = new RAACHousePricesMock();
debtToken = new DebtToken("DebtToken", "DTK", address(this));
rtoken = new RToken("RToken", "RTK", address(this), address(asset));
raacNft = new RAACNFT(address(asset), address(housePrice), address(this));
lendingPool = new LendingPool(
address(asset), address(rtoken), address(debtToken), address(raacNft), address(housePrice), 0.1e27
);
rtoken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
deToken = new DEToken("DEToken", "DET", address(this), address(rtoken));
raacToken = new RAACToken(address(this), initialSwapTaxRate, initialBurnTaxRate);
raacToken.setMinter(address(this));
stabilityPool = new StabilityPool(address(this));
stabilityPool.initialize(
address(rtoken), address(deToken), address(raacToken), address(this), address(asset), address(lendingPool)
);
lendingPool.setStabilityPool(address(stabilityPool));
raacMinter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), address(this));
stabilityPool.setRAACMinter(address(raacMinter));
deToken.setStabilityPool(address(stabilityPool));
uint256 depositorAmount = userAssetAmount * 100;
deal(address(asset), depositor, depositorAmount);
vm.startPrank(depositor);
asset.approve(address(lendingPool), depositorAmount);
lendingPool.deposit(depositorAmount);
vm.stopPrank();
}
function testLiquidation() external {
_mintNFT(user, 1, 100_000e18);
_depositNFT(user, 1);
_borrow(user, 80_000e18);
skip(30 days);
lendingPool.initiateLiquidation(user);
skip(3 days + 1);
lendingPool.updateState();
deal(address(asset), address(stabilityPool), lendingPool.getUserDebt(user));
vm.expectRevert(IStabilityPool.InsufficientBalance.selector);
stabilityPool.liquidateBorrower(user);
deal(
address(asset),
address(stabilityPool),
lendingPool.getUserDebt(user).rayMul(lendingPool.getNormalizedDebt())
);
stabilityPool.liquidateBorrower(user);
assertEq(debtToken.balanceOf(user), 0);
uint256 stabilityBalance = asset.balanceOf(address(stabilityPool));
emit log_named_decimal_uint("stability balance", stabilityBalance, 18);
assertGt(stabilityBalance, 0);
}
function _mintNFT(address account, uint256 tokenId, uint256 price) internal {
housePrice.setTokenPrice(tokenId, price);
deal(address(asset), account, price);
vm.startPrank(account);
asset.approve(address(raacNft), price);
raacNft.mint(tokenId, price);
vm.stopPrank();
assertEq(raacNft.ownerOf(tokenId), account);
}
function _depositNFT(address account, uint256 tokenId) internal {
vm.startPrank(account);
raacNft.approve(address(lendingPool), tokenId);
lendingPool.depositNFT(tokenId);
vm.stopPrank();
assertEq(raacNft.ownerOf(tokenId), address(lendingPool));
}
function _borrow(address account, uint256 amount) internal {
uint256 beforeBalance = asset.balanceOf(account);
vm.startPrank(account);
lendingPool.borrow(amount);
vm.stopPrank();
uint256 afterBalance = asset.balanceOf(account);
assertEq(afterBalance - beforeBalance, amount);
}
}
Liquidation needs more crvUSD than expected amount, and excess crvUSD cannot be redeemed.
This will lead to protocol's fund loss.
@@ -449,20 +449,19 @@ contract StabilityPool is IStabilityPool, Initializable, ReentrancyGuard, Ownabl
function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
// 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);
+ bool approveSuccess = crvUSDToken.approve(address(lendingPool), userDebt);
if (!approveSuccess) revert ApprovalFailed();
// Call finalizeLiquidation on LendingPool
lendingPool.finalizeLiquidation(userAddress);
- emit BorrowerLiquidated(userAddress, scaledUserDebt);
+ emit BorrowerLiquidated(userAddress, userDebt);
}
}