High - Considering usage index is designed to only go up, liquidating positions is made significantly harder as time passes and eventually, impossible as the funds needed would far surpass the total protocol liquidity.
High - This will occur for every liquidation attempt through the Stability Pool, making it a systematic issue rather than an edge case.
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "contracts/core/tokens/DebtToken.sol";
import "contracts/core/tokens/RToken.sol";
import "contracts/core/tokens/RAACNFT.sol";
import "contracts/core/pools/LendingPool/LendingPool.sol";
import "contracts/core/primitives/RAACHousePrices.sol";
import "contracts/core/pools/StabilityPool/StabilityPool.sol";
import "contracts/core/tokens/RAACToken.sol";
import "contracts/core/tokens/DEToken.sol";
import "contracts/core/minters/RAACMinter/RAACMinter.sol";
import "contracts/libraries/math/PercentageMath.sol";
import "contracts/libraries/math/WadRayMath.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract MockERC20 is ERC20 {
constructor() ERC20("Mock", "MCK") {}
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
contract MasterTest is Test {
using PercentageMath for uint256;
using WadRayMath for uint256;
DebtToken public debtToken;
RToken public rToken;
RAACNFT public nft;
RAACToken public raacToken;
DEToken public deToken;
RAACHousePrices public priceOracle;
LendingPool public lendingPool;
StabilityPool public stabilityPool;
RAACMinter public raacMinter;
MockERC20 public mockCrvUSD;
address borrower = address(0x1);
address lender = address(0x2);
address lender2 = address(0x3);
address treasury = address(0x4);
address repairFund = address(0x5);
address protocolOwner = address(0x999);
function setUp() public {
vm.warp(1000 days);
vm.startPrank(protocolOwner);
mockCrvUSD = new MockERC20();
priceOracle = new RAACHousePrices(protocolOwner);
raacToken = new RAACToken(
protocolOwner,
100,
50
);
rToken = new RToken(
"RToken",
"RTKN",
protocolOwner,
address(mockCrvUSD)
);
debtToken = new DebtToken(
"DebtToken",
"DEBT",
protocolOwner
);
deToken = new DEToken(
"DEToken",
"DETKN",
protocolOwner,
address(rToken)
);
nft = new RAACNFT(
address(mockCrvUSD),
address(priceOracle),
protocolOwner
);
lendingPool = new LendingPool(
address(mockCrvUSD),
address(rToken),
address(debtToken),
address(nft),
address(priceOracle),
1e27
);
stabilityPool = new StabilityPool(protocolOwner);
raacMinter = new RAACMinter(
address(raacToken),
address(stabilityPool),
address(lendingPool),
protocolOwner
);
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(mockCrvUSD),
address(lendingPool)
);
raacMinter = new RAACMinter(
address(raacToken),
address(stabilityPool),
address(lendingPool),
protocolOwner
);
debtToken.setReservePool(address(lendingPool));
rToken.setReservePool(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
raacToken.setMinter(address(raacMinter));
lendingPool.setStabilityPool(address(stabilityPool));
mockCrvUSD.mint(borrower, 100_000e18);
mockCrvUSD.mint(lender, 500_000e18);
vm.stopPrank();
vm.startPrank(borrower);
mockCrvUSD.approve(address(lendingPool), type(uint256).max);
mockCrvUSD.approve(address(nft), type(uint256).max);
nft.setApprovalForAll(address(lendingPool), true);
vm.stopPrank();
vm.startPrank(protocolOwner);
priceOracle.setOracle(protocolOwner);
priceOracle.setHousePrice(1, 100_000e18);
vm.stopPrank();
vm.startPrank(lender);
mockCrvUSD.approve(address(lendingPool), type(uint256).max);
lendingPool.deposit(500_000e18);
vm.stopPrank();
}
function test_doubleScaledLiquidation() public {
vm.startPrank(borrower);
nft.mint(1, 100_000e18);
lendingPool.depositNFT(1);
uint256 borrowAmount = 50_000e18;
lendingPool.borrow(borrowAmount);
vm.stopPrank();
uint256 initialUsageIndex = lendingPool.getNormalizedDebt();
console.log("Initial Usage Index:", initialUsageIndex);
console.log("Initial Borrow Amount:", borrowAmount);
mockCrvUSD.mint(address(stabilityPool), 80_000e18);
vm.warp(block.timestamp + 365 days);
lendingPool.updateState();
uint256 currentUsageIndex = lendingPool.getNormalizedDebt();
uint256 normalizedDebt = lendingPool.getUserDebt(borrower);
uint256 doubleScaledDebt = WadRayMath.rayMul(normalizedDebt, currentUsageIndex);
console.log("Current Usage Index:", currentUsageIndex);
console.log("Normalized Debt (Already Scaled):", normalizedDebt);
console.log("Double-Scaled Debt:", doubleScaledDebt);
console.log("Stability Pool Balance:", mockCrvUSD.balanceOf(address(stabilityPool)));
assertTrue(normalizedDebt < mockCrvUSD.balanceOf(address(stabilityPool)),
"Stability Pool should have enough for actual debt");
assertTrue(doubleScaledDebt > mockCrvUSD.balanceOf(address(stabilityPool)),
"Stability Pool should not have enough for double-scaled debt");
vm.startPrank(protocolOwner);
priceOracle.setHousePrice(1, 40_000e18);
vm.stopPrank();
lendingPool.initiateLiquidation(borrower);
vm.warp(block.timestamp + 4 days);
vm.startPrank(protocolOwner);
vm.expectRevert();
stabilityPool.liquidateBorrower(borrower);
vm.stopPrank();
}