Skip to content

Commit

Permalink
Merge pull request #596 from morpho-org/ci/invariant-no-interest
Browse files Browse the repository at this point in the history
test(invariant): re-implement lost invariants
  • Loading branch information
MathisGD authored Jan 17, 2024
2 parents 75e5b5a + c3a3f9b commit a5b900f
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 114 deletions.
26 changes: 17 additions & 9 deletions test/forge/BaseTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,12 @@ contract BaseTest is Test {
{
Id _id = _marketParams.id();

uint256 borrowShares = morpho.borrowShares(_id, onBehalf);

(,, uint256 totalBorrowAssets, uint256 totalBorrowShares) = morpho.expectedMarketBalances(_marketParams);
uint256 maxRepayAssets = morpho.borrowShares(_id, onBehalf).toAssetsDown(totalBorrowAssets, totalBorrowShares);
// Rounding assets up can yield a value larger than `totalBorrowAssets` in case `totalBorrowAssets` is zero.
uint256 maxRepayAssets =
UtilsLib.min(borrowShares.toAssetsUp(totalBorrowAssets, totalBorrowShares), totalBorrowAssets);

return bound(assets, 0, maxRepayAssets);
}
Expand All @@ -335,14 +339,19 @@ contract BaseTest is Test {
{
Id _id = _marketParams.id();

uint256 collateral = morpho.collateral(_id, borrower);
(,, uint256 totalBorrowAssets, uint256 totalBorrowShares) = morpho.expectedMarketBalances(_marketParams);

// Rounding assets up can yield a value larger than `totalBorrowAssets` in case `totalBorrowAssets` is zero.
uint256 maxRepaidAssets = UtilsLib.min(
morpho.borrowShares(_id, borrower).toAssetsUp(totalBorrowAssets, totalBorrowShares), totalBorrowAssets
);

uint256 collateralPrice = IOracle(_marketParams.oracle).price();
uint256 maxRepaidAssets = morpho.expectedBorrowAssets(_marketParams, borrower);
uint256 maxSeizedAssets = maxRepaidAssets.wMulDown(_liquidationIncentiveFactor(_marketParams.lltv)).mulDivDown(
ORACLE_PRICE_SCALE, collateralPrice
);

return bound(seizedAssets, 0, Math.min(collateral, maxSeizedAssets));
return bound(seizedAssets, 0, Math.min(morpho.collateral(_id, borrower), maxSeizedAssets));
}

function _boundLiquidateRepaidShares(MarketParams memory _marketParams, address borrower, uint256 repaidShares)
Expand All @@ -352,15 +361,14 @@ contract BaseTest is Test {
{
Id _id = _marketParams.id();

uint256 borrowShares = morpho.borrowShares(_id, borrower);
uint256 collateralPrice = IOracle(_marketParams.oracle).price();
uint256 maxRepaidAssets = morpho.collateral(_id, borrower).mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(
_liquidationIncentiveFactor(_marketParams.lltv)
);
uint256 maxRepaidAssets = morpho.collateral(_id, borrower).mulDivDown(collateralPrice, ORACLE_PRICE_SCALE)
.wDivDown(_liquidationIncentiveFactor(_marketParams.lltv));

(,, uint256 totalBorrowAssets, uint256 totalBorrowShares) = morpho.expectedMarketBalances(marketParams);
uint256 maxRepaidShares = maxRepaidAssets.toSharesDown(totalBorrowAssets, totalBorrowShares);

return bound(repaidShares, 0, Math.min(borrowShares, maxRepaidShares));
return bound(repaidShares, 0, Math.min(morpho.borrowShares(_id, borrower), maxRepaidShares));
}

function _maxBorrow(MarketParams memory _marketParams, address user) internal view returns (uint256) {
Expand Down
4 changes: 1 addition & 3 deletions test/forge/InvariantTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ contract InvariantTest is BaseTest {

_targetSenders();

_weightSelector(this.mine.selector, 100);

targetContract(address(this));
targetSelector(FuzzSelector({addr: address(this), selectors: selectors}));
}
Expand Down Expand Up @@ -58,7 +56,7 @@ contract InvariantTest is BaseTest {
/* HANDLERS */

function mine(uint256 blocks) external {
blocks = bound(blocks, 1, 50_400);
blocks = bound(blocks, 1, 1 days / BLOCK_TIME);

_forward(blocks);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@ pragma solidity ^0.8.0;

import "../InvariantTest.sol";

contract MorphoInvariantTest is InvariantTest {
contract BaseMorphoInvariantTest is InvariantTest {
using MathLib for uint256;
using SharesMathLib for uint256;
using MorphoLib for IMorpho;
using MorphoBalancesLib for IMorpho;
using MarketParamsLib for MarketParams;

uint256 internal immutable MIN_PRICE = ORACLE_PRICE_SCALE / 10;
uint256 internal immutable MAX_PRICE = ORACLE_PRICE_SCALE * 10;

address internal immutable USER;

MarketParams[] internal allMarketParams;
Expand All @@ -22,19 +19,6 @@ contract MorphoInvariantTest is InvariantTest {
}

function setUp() public virtual override {
_weightSelector(this.setPrice.selector, 10);
_weightSelector(this.setFeeNoRevert.selector, 5);
_weightSelector(this.supplyAssetsOnBehalfNoRevert.selector, 100);
_weightSelector(this.supplySharesOnBehalfNoRevert.selector, 100);
_weightSelector(this.withdrawAssetsOnBehalfNoRevert.selector, 50);
_weightSelector(this.borrowAssetsOnBehalfNoRevert.selector, 75);
_weightSelector(this.repayAssetsOnBehalfNoRevert.selector, 35);
_weightSelector(this.repaySharesOnBehalfNoRevert.selector, 35);
_weightSelector(this.supplyCollateralOnBehalfNoRevert.selector, 100);
_weightSelector(this.withdrawCollateralOnBehalfNoRevert.selector, 50);
_weightSelector(this.liquidateSeizedAssetsNoRevert.selector, 5);
_weightSelector(this.liquidateRepaidSharesNoRevert.selector, 5);

super.setUp();

allMarketParams.push(marketParams);
Expand Down Expand Up @@ -191,12 +175,6 @@ contract MorphoInvariantTest is InvariantTest {

/* HANDLERS */

function setPrice(uint256 price) external {
price = bound(price, MIN_PRICE, MAX_PRICE);

oracle.setPrice(price);
}

function setFeeNoRevert(uint256 marketSeed, uint256 newFee) external {
MarketParams memory _marketParams = _randomMarket(marketSeed);
Id _id = _marketParams.id();
Expand Down Expand Up @@ -316,10 +294,10 @@ contract MorphoInvariantTest is InvariantTest {
_withdrawCollateral(_marketParams, assets, onBehalf, receiver);
}

function liquidateSeizedAssetsNoRevert(uint256 marketSeed, uint256 seizedAssets, uint256 onBehalfSeed) external {
function liquidateSeizedAssetsNoRevert(uint256 marketSeed, uint256 seizedAssets, uint256 borrowerSeed) external {
MarketParams memory _marketParams = _randomMarket(marketSeed);

address borrower = _randomUnhealthyBorrower(targetSenders(), _marketParams, onBehalfSeed);
address borrower = _randomUnhealthyBorrower(targetSenders(), _marketParams, borrowerSeed);
if (borrower == address(0)) return;

seizedAssets = _boundLiquidateSeizedAssets(_marketParams, borrower, seizedAssets);
Expand All @@ -328,90 +306,15 @@ contract MorphoInvariantTest is InvariantTest {
_liquidateSeizedAssets(_marketParams, borrower, seizedAssets);
}

function liquidateRepaidSharesNoRevert(uint256 marketSeed, uint256 repaidShares, uint256 onBehalfSeed) external {
function liquidateRepaidSharesNoRevert(uint256 marketSeed, uint256 repaidShares, uint256 borrowerSeed) external {
MarketParams memory _marketParams = _randomMarket(marketSeed);

address borrower = _randomUnhealthyBorrower(targetSenders(), _marketParams, onBehalfSeed);
address borrower = _randomUnhealthyBorrower(targetSenders(), _marketParams, borrowerSeed);
if (borrower == address(0)) return;

repaidShares = _boundLiquidateRepaidShares(_marketParams, borrower, repaidShares);
if (repaidShares == 0) return;

_liquidateRepaidShares(_marketParams, borrower, repaidShares);
}

/* INVARIANTS */

function invariantSupplyShares() public {
address[] memory users = targetSenders();

for (uint256 i; i < allMarketParams.length; ++i) {
MarketParams memory _marketParams = allMarketParams[i];
Id _id = _marketParams.id();

uint256 sumSupplyShares = morpho.supplyShares(_id, FEE_RECIPIENT);
for (uint256 j; j < users.length; ++j) {
sumSupplyShares += morpho.supplyShares(_id, users[j]);
}

assertEq(sumSupplyShares, morpho.totalSupplyShares(_id), vm.toString(_marketParams.lltv));
}
}

function invariantBorrowShares() public {
address[] memory users = targetSenders();

for (uint256 i; i < allMarketParams.length; ++i) {
MarketParams memory _marketParams = allMarketParams[i];
Id _id = _marketParams.id();

uint256 sumBorrowShares;
for (uint256 j; j < users.length; ++j) {
sumBorrowShares += morpho.borrowShares(_id, users[j]);
}

assertEq(sumBorrowShares, morpho.totalBorrowShares(_id), vm.toString(_marketParams.lltv));
}
}

function invariantTotalSupplyGeTotalBorrow() public {
for (uint256 i; i < allMarketParams.length; ++i) {
MarketParams memory _marketParams = allMarketParams[i];
Id _id = _marketParams.id();

assertGe(morpho.totalSupplyAssets(_id), morpho.totalBorrowAssets(_id));
}
}

function invariantMorphoBalance() public {
for (uint256 i; i < allMarketParams.length; ++i) {
MarketParams memory _marketParams = allMarketParams[i];
Id _id = _marketParams.id();

assertGe(
loanToken.balanceOf(address(morpho)) + morpho.totalBorrowAssets(_id), morpho.totalSupplyAssets(_id)
);
}
}

function invariantBadDebt() public {
address[] memory users = targetSenders();

for (uint256 i; i < allMarketParams.length; ++i) {
MarketParams memory _marketParams = allMarketParams[i];
Id _id = _marketParams.id();

for (uint256 j; j < users.length; ++j) {
address user = users[j];

if (morpho.collateral(_id, user) == 0) {
assertEq(
morpho.borrowShares(_id, user),
0,
string.concat(vm.toString(_marketParams.lltv), ":", vm.toString(user))
);
}
}
}
}
}
116 changes: 116 additions & 0 deletions test/forge/invariant/MorphoDynamicInvariantTest.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import "./BaseMorphoInvariantTest.sol";

contract MorphoDynamicInvariantTest is BaseMorphoInvariantTest {
using MathLib for uint256;
using SharesMathLib for uint256;
using MorphoLib for IMorpho;
using MorphoBalancesLib for IMorpho;
using MarketParamsLib for MarketParams;

uint256 internal immutable MIN_PRICE = ORACLE_PRICE_SCALE / 10;
uint256 internal immutable MAX_PRICE = ORACLE_PRICE_SCALE * 10;

function setUp() public virtual override {
_weightSelector(this.supplyAssetsOnBehalfNoRevert.selector, 15);
_weightSelector(this.supplySharesOnBehalfNoRevert.selector, 5);
_weightSelector(this.withdrawAssetsOnBehalfNoRevert.selector, 10);
_weightSelector(this.borrowAssetsOnBehalfNoRevert.selector, 15);
_weightSelector(this.repayAssetsOnBehalfNoRevert.selector, 10);
_weightSelector(this.repaySharesOnBehalfNoRevert.selector, 10);
_weightSelector(this.supplyCollateralOnBehalfNoRevert.selector, 15);
_weightSelector(this.withdrawCollateralOnBehalfNoRevert.selector, 10);
_weightSelector(this.liquidateSeizedAssetsNoRevert.selector, 2);
_weightSelector(this.liquidateRepaidSharesNoRevert.selector, 2);
_weightSelector(this.setFeeNoRevert.selector, 1);
_weightSelector(this.setPrice.selector, 5);
_weightSelector(this.mine.selector, 100);

super.setUp();
}

/* HANDLERS */

function setPrice(uint256 price) external {
price = bound(price, MIN_PRICE, MAX_PRICE);

oracle.setPrice(price);
}

/* INVARIANTS */

function invariantSupplyShares() public {
address[] memory users = targetSenders();

for (uint256 i; i < allMarketParams.length; ++i) {
MarketParams memory _marketParams = allMarketParams[i];
Id _id = _marketParams.id();

uint256 sumSupplyShares = morpho.supplyShares(_id, FEE_RECIPIENT);
for (uint256 j; j < users.length; ++j) {
sumSupplyShares += morpho.supplyShares(_id, users[j]);
}

assertEq(sumSupplyShares, morpho.totalSupplyShares(_id), vm.toString(_marketParams.lltv));
}
}

function invariantBorrowShares() public {
address[] memory users = targetSenders();

for (uint256 i; i < allMarketParams.length; ++i) {
MarketParams memory _marketParams = allMarketParams[i];
Id _id = _marketParams.id();

uint256 sumBorrowShares;
for (uint256 j; j < users.length; ++j) {
sumBorrowShares += morpho.borrowShares(_id, users[j]);
}

assertEq(sumBorrowShares, morpho.totalBorrowShares(_id), vm.toString(_marketParams.lltv));
}
}

function invariantTotalSupplyGeTotalBorrow() public {
for (uint256 i; i < allMarketParams.length; ++i) {
MarketParams memory _marketParams = allMarketParams[i];
Id _id = _marketParams.id();

assertGe(morpho.totalSupplyAssets(_id), morpho.totalBorrowAssets(_id));
}
}

function invariantMorphoBalance() public {
for (uint256 i; i < allMarketParams.length; ++i) {
MarketParams memory _marketParams = allMarketParams[i];
Id _id = _marketParams.id();

assertGe(
loanToken.balanceOf(address(morpho)) + morpho.totalBorrowAssets(_id), morpho.totalSupplyAssets(_id)
);
}
}

function invariantBadDebt() public {
address[] memory users = targetSenders();

for (uint256 i; i < allMarketParams.length; ++i) {
MarketParams memory _marketParams = allMarketParams[i];
Id _id = _marketParams.id();

for (uint256 j; j < users.length; ++j) {
address user = users[j];

if (morpho.collateral(_id, user) == 0) {
assertEq(
morpho.borrowShares(_id, user),
0,
string.concat(vm.toString(_marketParams.lltv), ":", vm.toString(user))
);
}
}
}
}
}
40 changes: 40 additions & 0 deletions test/forge/invariant/MorphoStaticInvariantTest.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import "./BaseMorphoInvariantTest.sol";

contract MorphoStaticInvariantTest is BaseMorphoInvariantTest {
using MathLib for uint256;
using SharesMathLib for uint256;
using MorphoLib for IMorpho;
using MorphoBalancesLib for IMorpho;
using MarketParamsLib for MarketParams;

function setUp() public virtual override {
_weightSelector(this.supplyAssetsOnBehalfNoRevert.selector, 12);
_weightSelector(this.supplySharesOnBehalfNoRevert.selector, 5);
_weightSelector(this.withdrawAssetsOnBehalfNoRevert.selector, 12);
_weightSelector(this.borrowAssetsOnBehalfNoRevert.selector, 17);
_weightSelector(this.repayAssetsOnBehalfNoRevert.selector, 12);
_weightSelector(this.repaySharesOnBehalfNoRevert.selector, 10);
_weightSelector(this.supplyCollateralOnBehalfNoRevert.selector, 15);
_weightSelector(this.withdrawCollateralOnBehalfNoRevert.selector, 10);
_weightSelector(this.setFeeNoRevert.selector, 2);

super.setUp();
}

/* INVARIANTS */

function invariantHealthy() public {
address[] memory users = targetSenders();

for (uint256 i; i < allMarketParams.length; ++i) {
MarketParams memory _marketParams = allMarketParams[i];

for (uint256 j; j < users.length; ++j) {
assertTrue(_isHealthy(_marketParams, users[j]));
}
}
}
}

0 comments on commit a5b900f

Please sign in to comment.