pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {LendingPool} from "contracts/core/pools/LendingPool/LendingPool.sol";
import {StabilityPool} from "contracts/core/pools/StabilityPool/StabilityPool.sol";
import {crvUSDToken} from "contracts/mocks/core/tokens/crvUSDToken.sol";
import {RToken} from "contracts/core/tokens/RToken.sol";
import {DebtToken} from "contracts/core/tokens/DebtToken.sol";
import {RAACNFT} from "contracts/core/tokens/RAACNFT.sol";
import {RAACHousePricesMock} from "contracts/mocks/core/primitives/RAACHousePricesMock.sol";
import {RAACHousePriceOracle} from "contracts/core/oracles/RAACHousePriceOracle.sol";
import {MockFunctionsRouter} from "contracts/mocks/core/oracles/MockFunctionsRouter.sol";
import {FeeCollector} from "contracts/core/collectors/FeeCollector.sol";
import {MockVeToken} from "contracts/mocks/core/tokens/MockVeToken.sol";
import {RAACMinter} from "contracts/core/minters/RAACMinter/RAACMinter.sol";
import {DEToken} from "contracts/core/tokens/DEToken.sol";
import {RAACToken} from "contracts/core/tokens/RAACToken.sol";
import {MockVaultV3} from "contracts/mocks/CurveVaultMock.sol";
contract SetupContract is Test {
address public user1;
address public user2;
address public user3;
uint256 public currentBlockTimestamp = 1000 days;
address public treasury;
address public repairFund;
LendingPool public lendingPool;
crvUSDToken public _crvUSDToken;
RToken public rToken;
MockVeToken public veRToken;
DebtToken public debtToken;
RAACHousePricesMock public raacHousePrices;
RAACNFT public raacNFT;
RAACHousePriceOracle public raacHousePriceOracle;
FeeCollector public feeCollector;
StabilityPool public stabilityPool;
RAACMinter public raacMinter;
DEToken public deToken;
RAACToken public raacToken;
MockVaultV3 public curveVault;
uint256 public constant INITIAL_PRIME_RATE = 1e26;
function setUp() external {
vm.warp(currentBlockTimestamp);
user1 = makeAddr("user1");
user2 = makeAddr("user2");
user3 = makeAddr("user3");
treasury = makeAddr("treasury");
repairFund = makeAddr("repairFund");
stabilityPool = new StabilityPool(address(this));
veRToken = new MockVeToken();
_crvUSDToken = new crvUSDToken(address(this));
rToken = new RToken("rtoken", "rtk", address(this), address(_crvUSDToken));
raacToken = new RAACToken(address(this), 0, 0);
deToken = new DEToken("deToken", "detk", address(this), address(rToken));
debtToken = new DebtToken("debtToken", "dtk", address(this));
raacHousePrices = new RAACHousePricesMock();
raacNFT = new RAACNFT(address(_crvUSDToken), address(raacHousePrices), address(this));
raacHousePriceOracle = new RAACHousePriceOracle(
address(new MockFunctionsRouter()), bytes32(bytes("fun-ethereum-mainnet-1")), address(this)
);
curveVault = new MockVaultV3(address(_crvUSDToken), "vault", "vv");
feeCollector = new FeeCollector(address(rToken), address(veRToken), treasury, repairFund, address(this));
lendingPool = new LendingPool(
address(_crvUSDToken),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
INITIAL_PRIME_RATE
);
raacMinter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), address(this));
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(_crvUSDToken),
address(lendingPool)
);
raacToken.setMinter(address(raacMinter));
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
lendingPool.setStabilityPool(address(stabilityPool));
_crvUSDToken.mint(user1, 10000e18);
_crvUSDToken.mint(user2, 100e18);
_crvUSDToken.mint(user3, 1000e18);
_crvUSDToken.mint(address(stabilityPool), 100000e18);
}
function testLiquidations() public {
vm.startPrank(user1);
_crvUSDToken.approve(address(lendingPool), 1000e18);
lendingPool.deposit(1000e18);
vm.stopPrank();
raacHousePrices.setTokenPrice(1, 100e18);
raacHousePrices.setTokenPrice(2, 100e18);
vm.startPrank(user3);
_crvUSDToken.approve(address(raacNFT), 200e18);
raacNFT.mint(1, 100e18);
raacNFT.mint(2, 100e18);
raacNFT.approve(address(lendingPool), 1);
raacNFT.approve(address(lendingPool), 2);
lendingPool.depositNFT(1);
lendingPool.borrow(120e18);
vm.stopPrank();
lendingPool.initiateLiquidation(user3);
vm.warp(block.timestamp + 4 days);
stabilityPool.liquidateBorrower(user3);
}
│ ├─ [78018] LendingPool::finalizeLiquidation(user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec])
│ │ ├─ [50408] RAACNFT::transferFrom(LendingPool: [0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7], StabilityPool: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], 1)
│ │ │ ├─ emit Transfer(from: LendingPool: [0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7], to: StabilityPool: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], tokenId: 1)
│ │ │ └─ ← [Stop]
│ │ ├─ [13331] DebtToken::burn(user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], 120047680703074231840 [1.2e20], 1000397339192285265334448368 [1e27])
│ │ │ ├─ [417] LendingPool::getNormalizedDebt() [staticcall]
│ │ │ │ └─ ← [Return] 1000397339192285265334448368 [1e27]
│ │ │ ├─ [417] LendingPool::getNormalizedDebt() [staticcall]
│ │ │ │ └─ ← [Return] 1000397339192285265334448368 [1e27]
│ │ │ ├─ [417] LendingPool::getNormalizedDebt() [staticcall]
│ │ │ │ └─ ← [Return] 1000397339192285265334448368 [1e27]
│ │ │ ├─ emit Transfer(from: user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], to: 0x0000000000000000000000000000000000000000, value: 120000000000000000000 [1.2e20])
│ │ │ ├─ emit Transfer(from: user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], to: 0x0000000000000000000000000000000000000000, value: 120047680703074231840 [1.2e20])
│ │ │ ├─ emit Burn(from: user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], amount: 120000000000000000000 [1.2e20], index: 1000397339192285265334448368 [1e27])
│ │ │ ├─ [417] LendingPool::getNormalizedDebt() [staticcall]
│ │ │ │ └─ ← [Return] 1000397339192285265334448368 [1e27]
│ │ │ └─ ← [Return] 120047680703074231840 [1.2e20], 0, 120000000000000000000 [1.2e20], 47699648486278949 [4.769e16]
│ │ ├─ [969] crvUSDToken::transferFrom(StabilityPool: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], RToken: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], 120047680703074231840 [1.2e20])
│ │ │ └─ ← [Revert] ERC20InsufficientAllowance(0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7, 120000000000000000000 [1.2e20], 120047680703074231840 [1.2e20])
│ │ └─ ← [Revert] ERC20InsufficientAllowance(0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7, 120000000000000000000 [1.2e20], 120047680703074231840 [1.2e20])
│ └─ ← [Revert] ERC20InsufficientAllowance(0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7, 120000000000000000000 [1.2e20], 120047680703074231840 [1.2e20])
└─ ← [Revert] ERC20InsufficientAllowance(0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7, 120000000000000000000 [1.2e20], 120047680703074231840 [1.2e20])
The liquidation mechanism becomes non-functional because the Stability Pool's token approval doesn't account for accumulated interest on the debt. This prevents liquidations from being executed successfully, leaving bad debt in the system and blocking the proper liquidation of NFT collateral.
The approval in liquidateBorrower should account for potential interest accumulation. Alternatively, approve the maximum possible amount (type(uint256).max) if the Stability Pool contract is trusted to handle only legitimate liquidations.