Skip to content

Commit b4cbd09

Browse files
author
Maxime Brugel
committed
test: liquidation tests
1 parent c8cb5ed commit b4cbd09

1 file changed

Lines changed: 206 additions & 0 deletions

File tree

‎test/borrow/MorphoBorrowPosition.t.sol‎

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1340,6 +1340,212 @@ contract MorphoBorrowPositionTest is Test {
13401340
assertTrue(borrowPosition.isHealthy(), "Position should be healthy after rapid ops");
13411341
}
13421342

1343+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
1344+
/* LIQUIDATION TESTS */
1345+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
1346+
1347+
function test_liquidation_PositionBecomesUnhealthyAfterPriceDropAndGetsLiquidated() public {
1348+
uint256 collateralAmount = COLLATERAL_AMOUNT;
1349+
1350+
// Supply liquidity for borrowing and liquidation
1351+
_supplyLiquidity(LOAN_AMOUNT * 10);
1352+
1353+
// Supply collateral
1354+
collateralToken.setBalance(positionManager, collateralAmount);
1355+
vm.prank(positionManager);
1356+
borrowPosition.supplyCollateral(collateralAmount);
1357+
1358+
// Borrow near max (80% of max)
1359+
uint256 maxBorrow = borrowPosition.maxBorrow();
1360+
uint256 borrowAmountActual = (maxBorrow * 80) / 100;
1361+
1362+
vm.prank(positionManager);
1363+
borrowPosition.borrow(borrowAmountActual);
1364+
1365+
// Verify position is healthy initially
1366+
assertTrue(borrowPosition.isHealthy(), "Position should be healthy initially");
1367+
uint256 collateralBefore = borrowPosition.totalCollateral();
1368+
uint256 borrowedBefore = borrowPosition.totalBorrowed();
1369+
1370+
// Crash the collateral price to make position unhealthy
1371+
// Drop price by 50% to trigger liquidation
1372+
oracle.setPrice(DEFAULT_ORACLE_PRICE / 2);
1373+
1374+
// Position should now be unhealthy
1375+
assertFalse(borrowPosition.isHealthy(), "Position should be unhealthy after price drop");
1376+
1377+
// Setup liquidator
1378+
address liquidator = makeAddr("liquidator");
1379+
1380+
// Give liquidator loan tokens to repay debt
1381+
loanToken.setBalance(liquidator, borrowedBefore);
1382+
vm.startPrank(liquidator);
1383+
loanToken.approve(address(morpho), type(uint256).max);
1384+
1385+
// Liquidate the position - Morpho allows liquidating unhealthy positions
1386+
// Liquidator seizes collateral by repaying debt
1387+
(uint256 seizedAssets, uint256 repaidShares) = morpho.liquidate(
1388+
marketParams,
1389+
address(borrowPosition), // borrower address
1390+
borrowedBefore, // assets to seize (up to full debt)
1391+
0, // min shares received
1392+
"" // data
1393+
);
1394+
1395+
vm.stopPrank();
1396+
1397+
// Verify liquidation occurred
1398+
assertGt(seizedAssets, 0, "Should have seized collateral");
1399+
assertGt(repaidShares, 0, "Should have repaid debt");
1400+
1401+
// Verify position state after liquidation
1402+
uint256 collateralAfter = borrowPosition.totalCollateral();
1403+
uint256 borrowedAfter = borrowPosition.totalBorrowed();
1404+
1405+
assertLt(collateralAfter, collateralBefore, "Collateral should decrease after liquidation");
1406+
assertLt(borrowedAfter, borrowedBefore, "Borrowed amount should decrease after liquidation");
1407+
1408+
// Note: Due to liquidation incentive, the position might still be unhealthy
1409+
// or even have zero collateral left after full liquidation.
1410+
// The key is that debt and collateral both decreased.
1411+
}
1412+
1413+
function test_liquidation_PartialLiquidationReducesDebt() public {
1414+
uint256 collateralAmount = COLLATERAL_AMOUNT;
1415+
1416+
// Supply liquidity
1417+
_supplyLiquidity(LOAN_AMOUNT * 10);
1418+
1419+
// Supply collateral
1420+
collateralToken.setBalance(positionManager, collateralAmount);
1421+
vm.prank(positionManager);
1422+
borrowPosition.supplyCollateral(collateralAmount);
1423+
1424+
// Borrow near max (90% of max)
1425+
uint256 maxBorrow = borrowPosition.maxBorrow();
1426+
uint256 borrowAmountActual = (maxBorrow * 90) / 100;
1427+
1428+
vm.prank(positionManager);
1429+
borrowPosition.borrow(borrowAmountActual);
1430+
1431+
assertTrue(borrowPosition.isHealthy(), "Position should be healthy initially");
1432+
1433+
// Price drops 30% - makes position unhealthy
1434+
oracle.setPrice((DEFAULT_ORACLE_PRICE * 70) / 100);
1435+
1436+
assertFalse(borrowPosition.isHealthy(), "Position should be unhealthy after price drop");
1437+
1438+
// Liquidator performs partial liquidation
1439+
address liquidator = makeAddr("liquidator");
1440+
uint256 borrowedBefore = borrowPosition.totalBorrowed();
1441+
uint256 partialRepayAmount = borrowedBefore / 2; // Repay 50% of debt
1442+
1443+
loanToken.setBalance(liquidator, partialRepayAmount);
1444+
vm.startPrank(liquidator);
1445+
loanToken.approve(address(morpho), type(uint256).max);
1446+
1447+
morpho.liquidate(marketParams, address(borrowPosition), partialRepayAmount, 0, "");
1448+
1449+
vm.stopPrank();
1450+
1451+
// Debt should be reduced
1452+
uint256 borrowedAfter = borrowPosition.totalBorrowed();
1453+
assertLt(borrowedAfter, borrowedBefore, "Debt should be reduced after liquidation");
1454+
1455+
// Note: Due to liquidation bonus, position might still be unhealthy after partial liquidation
1456+
// The test verifies that liquidation reduces debt, not necessarily restores full health
1457+
}
1458+
1459+
function test_liquidation_OwnerCanRepayInsteadOfBeingLiquidated() public {
1460+
uint256 collateralAmount = COLLATERAL_AMOUNT;
1461+
1462+
// Supply liquidity
1463+
_supplyLiquidity(LOAN_AMOUNT * 10);
1464+
1465+
// Supply collateral and borrow
1466+
collateralToken.setBalance(positionManager, collateralAmount);
1467+
vm.prank(positionManager);
1468+
borrowPosition.supplyCollateral(collateralAmount);
1469+
1470+
uint256 maxBorrow = borrowPosition.maxBorrow();
1471+
uint256 borrowAmountActual = (maxBorrow * 85) / 100;
1472+
1473+
vm.prank(positionManager);
1474+
borrowPosition.borrow(borrowAmountActual);
1475+
1476+
assertTrue(borrowPosition.isHealthy(), "Position should be healthy initially");
1477+
1478+
// Price drops 40%
1479+
oracle.setPrice((DEFAULT_ORACLE_PRICE * 60) / 100);
1480+
1481+
assertFalse(borrowPosition.isHealthy(), "Position should be unhealthy after price drop");
1482+
1483+
// Owner repays enough to restore health
1484+
uint256 borrowedBefore = borrowPosition.totalBorrowed();
1485+
uint256 repayAmount = borrowedBefore / 2;
1486+
1487+
loanToken.setBalance(positionManager, repayAmount);
1488+
vm.prank(positionManager);
1489+
borrowPosition.repay(repayAmount);
1490+
1491+
// Position should be healthy again
1492+
assertTrue(borrowPosition.isHealthy(), "Position should be healthy after repayment");
1493+
1494+
// Verify debt decreased
1495+
assertLt(borrowPosition.totalBorrowed(), borrowedBefore, "Debt should be reduced");
1496+
}
1497+
1498+
function test_liquidation_VerifyCollateralAndDebtStateAfterFullLiquidation() public {
1499+
uint256 collateralAmount = COLLATERAL_AMOUNT;
1500+
1501+
// Supply liquidity
1502+
_supplyLiquidity(LOAN_AMOUNT * 10);
1503+
1504+
// Supply collateral and borrow
1505+
collateralToken.setBalance(positionManager, collateralAmount);
1506+
vm.prank(positionManager);
1507+
borrowPosition.supplyCollateral(collateralAmount);
1508+
1509+
uint256 maxBorrow = borrowPosition.maxBorrow();
1510+
uint256 borrowAmountActual = (maxBorrow * 95) / 100;
1511+
1512+
vm.prank(positionManager);
1513+
borrowPosition.borrow(borrowAmountActual);
1514+
1515+
// Severe price drop (60% crash)
1516+
oracle.setPrice((DEFAULT_ORACLE_PRICE * 40) / 100);
1517+
1518+
assertFalse(borrowPosition.isHealthy(), "Position should be unhealthy");
1519+
1520+
// Liquidator liquidates entire position
1521+
address liquidator = makeAddr("liquidator");
1522+
uint256 borrowedAmount = borrowPosition.totalBorrowed();
1523+
uint256 collateralBefore = borrowPosition.totalCollateral();
1524+
1525+
loanToken.setBalance(liquidator, borrowedAmount * 2); // Excess to ensure full liquidation
1526+
vm.startPrank(liquidator);
1527+
loanToken.approve(address(morpho), type(uint256).max);
1528+
1529+
// Try to seize all collateral to fully liquidate
1530+
(uint256 seizedAssets, uint256 repaidShares) =
1531+
morpho.liquidate(marketParams, address(borrowPosition), collateralBefore, 0, "");
1532+
1533+
vm.stopPrank();
1534+
1535+
// Verify liquidation results
1536+
assertGt(seizedAssets, 0, "Should have seized collateral");
1537+
1538+
// After liquidation, collateral and debt should be reduced significantly
1539+
uint256 collateralAfter = borrowPosition.totalCollateral();
1540+
uint256 borrowedAfter = borrowPosition.totalBorrowed();
1541+
1542+
assertLt(collateralAfter, collateralBefore, "Collateral should be seized");
1543+
1544+
// Verify that liquidation occurred and debt was reduced
1545+
// Note: In Morpho, liquidation includes a bonus, so the position may be fully liquidated
1546+
// with both collateral and debt approaching zero
1547+
}
1548+
13431549
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
13441550
/* AUTHORIZATION TESTS */
13451551
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

0 commit comments

Comments
 (0)