diff --git a/foundry.toml b/foundry.toml index 3a78666be..482371c7d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,4 +5,4 @@ optimizer_runs = 4294967295 [fmt] wrap_comments = true -# See more config options https://github.com/foundry-rs/foundry/tree/master/crates/config +# See more config options https://github.com/foundry-rs/foundry/tree/master/crates/config \ No newline at end of file diff --git a/test/forge/BaseTest.sol b/test/forge/BaseTest.sol index b406fc84c..4f5a6f066 100644 --- a/test/forge/BaseTest.sol +++ b/test/forge/BaseTest.sol @@ -46,7 +46,7 @@ contract BaseTest is Test { MarketParams internal marketParams; Id internal id; - function setUp() public { + function setUp() public virtual { vm.label(OWNER, "Owner"); vm.label(SUPPLIER, "Supplier"); vm.label(BORROWER, "Borrower"); @@ -183,10 +183,10 @@ contract BaseTest is Test { UtilsLib.min(MAX_LIQUIDATION_INCENTIVE_FACTOR, WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - lltv))); } - function _accrueInterest() internal { + function _accrueInterest(MarketParams memory market) internal { collateralToken.setBalance(address(this), 1); - morpho.supplyCollateral(marketParams, 1, address(this), hex""); - morpho.withdrawCollateral(marketParams, 1, address(this), address(10)); + morpho.supplyCollateral(market, 1, address(this), hex""); + morpho.withdrawCollateral(market, 1, address(this), address(10)); } function neq(MarketParams memory a, MarketParams memory b) internal pure returns (bool) { diff --git a/test/forge/InvariantBase.sol b/test/forge/InvariantBase.sol new file mode 100644 index 000000000..67a2b7d50 --- /dev/null +++ b/test/forge/InvariantBase.sol @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "test/forge/BaseTest.sol"; + +contract InvariantBaseTest is BaseTest { + using MathLib for uint256; + using MorphoLib for Morpho; + using SharesMathLib for uint256; + + uint256 blockNumber; + uint256 timestamp; + + bytes4[] internal selectors; + + address[] internal addressArray; + + function setUp() public virtual override { + super.setUp(); + + targetContract(address(this)); + } + + function _targetDefaultSenders() internal { + targetSender(_addrFromHashedString("Morpho address1")); + targetSender(_addrFromHashedString("Morpho address2")); + targetSender(_addrFromHashedString("Morpho address3")); + targetSender(_addrFromHashedString("Morpho address4")); + targetSender(_addrFromHashedString("Morpho address5")); + targetSender(_addrFromHashedString("Morpho address6")); + targetSender(_addrFromHashedString("Morpho address7")); + targetSender(_addrFromHashedString("Morpho address8")); + } + + function _weightSelector(bytes4 selector, uint256 weight) internal { + for (uint256 i; i < weight; ++i) { + selectors.push(selector); + } + } + + function _approveSendersTransfers(address[] memory senders) internal { + for (uint256 i; i < senders.length; ++i) { + vm.startPrank(senders[i]); + borrowableToken.approve(address(morpho), type(uint256).max); + collateralToken.approve(address(morpho), type(uint256).max); + vm.stopPrank(); + } + } + + function _supplyHighAmountOfCollateralForAllSenders(address[] memory senders, MarketParams memory marketParams) + internal + { + for (uint256 i; i < senders.length; ++i) { + collateralToken.setBalance(senders[i], 1e30); + vm.prank(senders[i]); + morpho.supplyCollateral(marketParams, 1e30, senders[i], hex""); + } + } + + /// @dev Apparently permanently setting block number and timestamp with cheatcodes in this function doesn't work, + /// they get reset to the ones defined in the set up function after each function call. + /// The solution we choose is to store these in storage, and set them with roll and warp cheatcodes with the + /// setCorrectBlock function at the the begenning of each function. + /// The purpose of this function is to increment these variables to simulate a new block. + function newBlock(uint256 elapsed) public { + elapsed = bound(elapsed, 10, 1 days); + + blockNumber += 1; + timestamp += elapsed; + } + + modifier setCorrectBlock() { + vm.roll(blockNumber); + vm.warp(timestamp); + _; + } + + function _randomSenderToWithdrawOnBehalf(address[] memory addresses, address seed, address sender) + internal + returns (address randomSenderToWithdrawOnBehalf) + { + for (uint256 i; i < addresses.length; ++i) { + if (morpho.supplyShares(id, addresses[i]) != 0) { + addressArray.push(addresses[i]); + } + } + if (addressArray.length == 0) return address(0); + + randomSenderToWithdrawOnBehalf = addressArray[uint256(uint160(seed)) % addressArray.length]; + + vm.prank(randomSenderToWithdrawOnBehalf); + morpho.setAuthorization(sender, true); + + delete addressArray; + } + + function _randomSenderToBorrowOnBehalf(address[] memory addresses, address seed, address sender) + internal + returns (address randomSenderToBorrowOnBehalf) + { + for (uint256 i; i < addresses.length; ++i) { + if (morpho.collateral(id, addresses[i]) != 0 && isHealthy(id, addresses[i])) { + addressArray.push(addresses[i]); + } + } + if (addressArray.length == 0) return address(0); + + randomSenderToBorrowOnBehalf = addressArray[uint256(uint160(seed)) % addressArray.length]; + + vm.prank(randomSenderToBorrowOnBehalf); + morpho.setAuthorization(sender, true); + + delete addressArray; + } + + function _randomSenderToRepayOnBehalf(address[] memory addresses, address seed) + internal + returns (address randomSenderToRepayOnBehalf) + { + for (uint256 i; i < addresses.length; ++i) { + if (morpho.borrowShares(id, addresses[i]) != 0) { + addressArray.push(addresses[i]); + } + } + if (addressArray.length == 0) return address(0); + + randomSenderToRepayOnBehalf = addressArray[uint256(uint160(seed)) % addressArray.length]; + + delete addressArray; + } + + function _randomSenderToWithdrawCollateralOnBehalf(address[] memory addresses, address seed, address sender) + internal + returns (address randomSenderToWithdrawCollateralOnBehalf) + { + for (uint256 i; i < addresses.length; ++i) { + if (morpho.collateral(id, addresses[i]) != 0 && isHealthy(id, addresses[i])) { + addressArray.push(addresses[i]); + } + } + if (addressArray.length == 0) return address(0); + + randomSenderToWithdrawCollateralOnBehalf = addressArray[uint256(uint160(seed)) % addressArray.length]; + + vm.prank(randomSenderToWithdrawCollateralOnBehalf); + morpho.setAuthorization(sender, true); + + delete addressArray; + } + + function _randomSenderToLiquidate(address[] memory addresses, address seed) + internal + returns (address randomSenderToLiquidate) + { + for (uint256 i; i < addresses.length; ++i) { + if (morpho.borrowShares(id, addresses[i]) != 0 && !isHealthy(id, addresses[i])) { + addressArray.push(addresses[i]); + } + } + if (addressArray.length == 0) return address(0); + + randomSenderToLiquidate = addressArray[uint256(uint160(seed)) % addressArray.length]; + + delete addressArray; + } + + function sumUsersSupplyShares(address[] memory addresses) internal view returns (uint256 sum) { + for (uint256 i; i < addresses.length; ++i) { + sum += morpho.supplyShares(id, addresses[i]); + } + sum += morpho.supplyShares(id, morpho.feeRecipient()); + } + + function sumUsersBorrowShares(address[] memory addresses) internal view returns (uint256 sum) { + for (uint256 i; i < addresses.length; ++i) { + sum += morpho.borrowShares(id, addresses[i]); + } + } + + function sumUsersSuppliedAmounts(address[] memory addresses) internal view returns (uint256 sum) { + for (uint256 i; i < addresses.length; ++i) { + sum += morpho.supplyShares(id, addresses[i]).toAssetsDown( + morpho.totalSupplyAssets(id), morpho.totalSupplyShares(id) + ); + } + sum += morpho.supplyShares(id, morpho.feeRecipient()).toAssetsDown( + morpho.totalSupplyAssets(id), morpho.totalSupplyShares(id) + ); + } + + function sumUsersBorrowedAmounts(address[] memory addresses) internal view returns (uint256 sum) { + for (uint256 i; i < addresses.length; ++i) { + sum += morpho.borrowShares(id, addresses[i]).toAssetsUp( + morpho.totalBorrowAssets(id), morpho.totalBorrowShares(id) + ); + } + } + + function isHealthy(Id id, address user) public view returns (bool) { + uint256 collateralPrice = IOracle(marketParams.oracle).price(); + + uint256 borrowed = + morpho.borrowShares(id, user).toAssetsUp(morpho.totalBorrowAssets(id), morpho.totalBorrowShares(id)); + uint256 maxBorrow = + morpho.collateral(id, user).mulDivDown(collateralPrice, ORACLE_PRICE_SCALE).wMulDown(marketParams.lltv); + + return maxBorrow >= borrowed; + } + + function _liquidationIncentiveFactor(uint256 lltv) internal pure returns (uint256) { + return + UtilsLib.min(MAX_LIQUIDATION_INCENTIVE_FACTOR, WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - lltv))); + } +} diff --git a/test/forge/integration/TestIntegrationAccrueInterest.t.sol b/test/forge/integration/TestIntegrationAccrueInterest.t.sol index a774eed09..945fb254e 100644 --- a/test/forge/integration/TestIntegrationAccrueInterest.t.sol +++ b/test/forge/integration/TestIntegrationAccrueInterest.t.sol @@ -32,7 +32,7 @@ contract IntegrationAccrueInterestTest is BaseTest { uint256 totalSupplyBeforeAccrued = morpho.totalSupplyAssets(id); uint256 totalSupplySharesBeforeAccrued = morpho.totalSupplyShares(id); - _accrueInterest(); + _accrueInterest(marketParams); assertEq(morpho.totalBorrowAssets(id), totalBorrowBeforeAccrued, "total borrow"); assertEq(morpho.totalSupplyAssets(id), totalSupplyBeforeAccrued, "total supply"); @@ -59,7 +59,7 @@ contract IntegrationAccrueInterestTest is BaseTest { uint256 totalSupplyBeforeAccrued = morpho.totalSupplyAssets(id); uint256 totalSupplySharesBeforeAccrued = morpho.totalSupplyShares(id); - _accrueInterest(); + _accrueInterest(marketParams); assertEq(morpho.totalBorrowAssets(id), totalBorrowBeforeAccrued, "total borrow"); assertEq(morpho.totalSupplyAssets(id), totalSupplyBeforeAccrued, "total supply"); diff --git a/test/forge/invariant/TestInvariantSingleMarket.t.sol b/test/forge/invariant/TestInvariantSingleMarket.t.sol new file mode 100644 index 000000000..efe9ce432 --- /dev/null +++ b/test/forge/invariant/TestInvariantSingleMarket.t.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "test/forge/InvariantBase.sol"; + +contract SingleMarketInvariantTest is InvariantBaseTest { + using MathLib for uint256; + using MorphoLib for Morpho; + using SharesMathLib for uint256; + + function setUp() public virtual override { + super.setUp(); + + _targetDefaultSenders(); + + _approveSendersTransfers(targetSenders()); + _supplyHighAmountOfCollateralForAllSenders(targetSenders(), marketParams); + + // High price because of the 1e36 price scale + oracle.setPrice(1e40); + + _weightSelector(this.setMarketFee.selector, 5); + _weightSelector(this.supplyOnMorpho.selector, 20); + _weightSelector(this.borrowOnMorpho.selector, 15); + _weightSelector(this.repayOnMorpho.selector, 10); + _weightSelector(this.withdrawOnMorpho.selector, 15); + _weightSelector(this.supplyCollateralOnMorpho.selector, 20); + _weightSelector(this.withdrawCollateralOnMorpho.selector, 15); + _weightSelector(this.newBlock.selector, 20); + + blockNumber = block.number; + timestamp = block.timestamp; + + targetSelector(FuzzSelector({addr: address(this), selectors: selectors})); + } + + function setMarketFee(uint256 newFee) public setCorrectBlock { + newFee = bound(newFee, 0.1e18, MAX_FEE); + + vm.prank(OWNER); + morpho.setFee(marketParams, newFee); + } + + function supplyOnMorpho(uint256 amount) public setCorrectBlock { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + borrowableToken.setBalance(msg.sender, amount); + vm.prank(msg.sender); + morpho.supply(marketParams, amount, 0, msg.sender, hex""); + } + + function withdrawOnMorpho(uint256 amount) public setCorrectBlock { + _accrueInterest(marketParams); + + uint256 availableLiquidity = morpho.totalSupplyAssets(id) - morpho.totalBorrowAssets(id); + if (morpho.supplyShares(id, msg.sender) == 0) return; + if (availableLiquidity == 0) return; + + uint256 supplierBalance = + morpho.supplyShares(id, msg.sender).toAssetsDown(morpho.totalSupplyAssets(id), morpho.totalSupplyShares(id)); + if (supplierBalance == 0) return; + amount = bound(amount, 1, min(supplierBalance, availableLiquidity)); + + vm.prank(msg.sender); + morpho.withdraw(marketParams, amount, 0, msg.sender, msg.sender); + } + + function borrowOnMorpho(uint256 amount) public setCorrectBlock { + _accrueInterest(marketParams); + + uint256 availableLiquidity = morpho.totalSupplyAssets(id) - morpho.totalBorrowAssets(id); + if (availableLiquidity == 0) return; + + _accrueInterest(marketParams); + amount = bound(amount, 1, availableLiquidity); + + vm.prank(msg.sender); + morpho.borrow(marketParams, amount, 0, msg.sender, msg.sender); + } + + function repayOnMorpho(uint256 amount) public setCorrectBlock { + _accrueInterest(marketParams); + + if (morpho.borrowShares(id, msg.sender) == 0) return; + + _accrueInterest(marketParams); + uint256 borrowerBalance = + morpho.borrowShares(id, msg.sender).toAssetsDown(morpho.totalBorrowAssets(id), morpho.totalBorrowShares(id)); + if (borrowerBalance == 0) return; + amount = bound(amount, 1, borrowerBalance); + + borrowableToken.setBalance(msg.sender, amount); + vm.prank(msg.sender); + morpho.repay(marketParams, amount, 0, msg.sender, hex""); + } + + function supplyCollateralOnMorpho(uint256 amount) public setCorrectBlock { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + collateralToken.setBalance(msg.sender, amount); + vm.prank(msg.sender); + morpho.supplyCollateral(marketParams, amount, msg.sender, hex""); + } + + function withdrawCollateralOnMorpho(uint256 amount) public setCorrectBlock { + _accrueInterest(marketParams); + + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + vm.prank(msg.sender); + morpho.withdrawCollateral(marketParams, amount, msg.sender, msg.sender); + } + + function invariantSupplyShares() public { + assertEq(sumUsersSupplyShares(targetSenders()), morpho.totalSupplyShares(id)); + } + + function invariantBorrowShares() public { + assertEq(sumUsersBorrowShares(targetSenders()), morpho.totalBorrowShares(id)); + } + + function invariantTotalSupply() public { + assertLe(sumUsersSuppliedAmounts(targetSenders()), morpho.totalSupplyAssets(id)); + } + + function invariantTotalBorrow() public { + assertGe(sumUsersBorrowedAmounts(targetSenders()), morpho.totalBorrowAssets(id)); + } + + function invariantTotalSupplyGreaterThanTotalBorrow() public { + assertGe(morpho.totalSupplyAssets(id), morpho.totalBorrowAssets(id)); + } + + function invariantMorphoBalance() public { + assertEq( + morpho.totalSupplyAssets(id) - morpho.totalBorrowAssets(id), borrowableToken.balanceOf(address(morpho)) + ); + } +} diff --git a/test/forge/invariant/TestInvariantSingleMarketChangingPrice.t.sol b/test/forge/invariant/TestInvariantSingleMarketChangingPrice.t.sol new file mode 100644 index 000000000..7404d2b1a --- /dev/null +++ b/test/forge/invariant/TestInvariantSingleMarketChangingPrice.t.sol @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "test/forge/InvariantBase.sol"; + +contract SingleMarketChangingPriceInvariantTest is InvariantBaseTest { + using MathLib for uint256; + using MorphoLib for Morpho; + using SharesMathLib for uint256; + + address user; + + function setUp() public virtual override { + super.setUp(); + + _targetDefaultSenders(); + + _approveSendersTransfers(targetSenders()); + + _weightSelector(this.supplyOnMorpho.selector, 20); + _weightSelector(this.withdrawOnMorpho.selector, 5); + _weightSelector(this.withdrawOnMorphoOnBehalf.selector, 5); + _weightSelector(this.borrowOnMorpho.selector, 5); + _weightSelector(this.borrowOnMorphoOnBehalf.selector, 5); + _weightSelector(this.repayOnMorpho.selector, 2); + _weightSelector(this.repayOnMorphoOnBehalf.selector, 2); + _weightSelector(this.supplyCollateralOnMorpho.selector, 20); + _weightSelector(this.withdrawCollateralOnMorpho.selector, 5); + _weightSelector(this.withdrawCollateralOnMorphoOnBehalf.selector, 5); + _weightSelector(this.newBlock.selector, 20); + _weightSelector(this.changePrice.selector, 5); + _weightSelector(this.setMarketFee.selector, 2); + + blockNumber = block.number; + timestamp = block.timestamp; + + targetSelector(FuzzSelector({addr: address(this), selectors: selectors})); + + oracle.setPrice(1e36); + } + + function changePrice(uint256 variation) public { + // price variation bounded between -20% and +20% + variation = bound(variation, 0.8e18, 1.2e18); + uint256 currentPrice = IOracle(marketParams.oracle).price(); + oracle.setPrice(currentPrice.wMulDown(variation)); + } + + function setMarketFee(uint256 newFee) public setCorrectBlock { + newFee = bound(newFee, 0, MAX_FEE); + + vm.prank(OWNER); + morpho.setFee(marketParams, newFee); + } + + function supplyOnMorpho(uint256 amount) public setCorrectBlock { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + borrowableToken.setBalance(msg.sender, amount); + + vm.prank(msg.sender); + morpho.supply(marketParams, amount, 0, msg.sender, hex""); + } + + function withdrawOnMorpho(uint256 amount) public setCorrectBlock { + _accrueInterest(marketParams); + + uint256 availableLiquidity = morpho.totalSupplyAssets(id) - morpho.totalBorrowAssets(id); + if (morpho.supplyShares(id, msg.sender) == 0) return; + if (availableLiquidity == 0) return; + + uint256 supplierBalance = + morpho.supplyShares(id, msg.sender).toAssetsDown(morpho.totalSupplyAssets(id), morpho.totalSupplyShares(id)); + + if (supplierBalance == 0) return; + amount = bound(amount, 1, min(supplierBalance, availableLiquidity)); + + vm.prank(msg.sender); + morpho.withdraw(marketParams, amount, 0, msg.sender, msg.sender); + } + + function withdrawOnMorphoOnBehalf(uint256 amount, address seed) public setCorrectBlock { + _accrueInterest(marketParams); + + uint256 availableLiquidity = morpho.totalSupplyAssets(id) - morpho.totalBorrowAssets(id); + if (availableLiquidity == 0) return; + + address onBehalf = _randomSenderToWithdrawOnBehalf(targetSenders(), seed, msg.sender); + if (onBehalf == address(0)) return; + if (morpho.supplyShares(id, onBehalf) != 0) return; + uint256 supplierBalance = + morpho.supplyShares(id, onBehalf).toAssetsDown(morpho.totalSupplyAssets(id), morpho.totalSupplyShares(id)); + + if (supplierBalance == 0) return; + amount = bound(amount, 1, min(supplierBalance, availableLiquidity)); + + vm.prank(msg.sender); + morpho.withdraw(marketParams, amount, 0, onBehalf, msg.sender); + } + + function borrowOnMorpho(uint256 amount) public setCorrectBlock { + _accrueInterest(marketParams); + + uint256 availableLiquidity = morpho.totalSupplyAssets(id) - morpho.totalBorrowAssets(id); + if (availableLiquidity == 0 || morpho.collateral(id, msg.sender) == 0 || !isHealthy(id, msg.sender)) { + return; + } + uint256 collateralPrice = IOracle(marketParams.oracle).price(); + + uint256 totalBorrowPower = morpho.collateral(id, msg.sender).mulDivDown(collateralPrice, ORACLE_PRICE_SCALE) + .wMulDown(marketParams.lltv); + uint256 borrowed = + morpho.borrowShares(id, msg.sender).toAssetsUp(morpho.totalBorrowAssets(id), morpho.totalBorrowShares(id)); + uint256 currentBorrowPower = totalBorrowPower - borrowed; + + if (currentBorrowPower == 0) return; + amount = bound(amount, 1, min(currentBorrowPower, availableLiquidity)); + + vm.prank(msg.sender); + morpho.borrow(marketParams, amount, 0, msg.sender, msg.sender); + } + + function borrowOnMorphoOnBehalf(uint256 amount, address seed) public setCorrectBlock { + _accrueInterest(marketParams); + + uint256 availableLiquidity = morpho.totalSupplyAssets(id) - morpho.totalBorrowAssets(id); + if (availableLiquidity == 0) return; + + address onBehalf = _randomSenderToBorrowOnBehalf(targetSenders(), seed, msg.sender); + if (onBehalf == address(0)) return; + + uint256 collateralPrice = IOracle(marketParams.oracle).price(); + + uint256 totalBorrowPower = + morpho.collateral(id, onBehalf).mulDivDown(collateralPrice, ORACLE_PRICE_SCALE).wMulDown(marketParams.lltv); + uint256 borrowed = + morpho.borrowShares(id, onBehalf).toAssetsUp(morpho.totalBorrowAssets(id), morpho.totalBorrowShares(id)); + uint256 currentBorrowPower = totalBorrowPower - borrowed; + + if (currentBorrowPower == 0) return; + amount = bound(amount, 1, min(currentBorrowPower, availableLiquidity)); + + vm.prank(msg.sender); + morpho.borrow(marketParams, amount, 0, onBehalf, msg.sender); + } + + function repayOnMorpho(uint256 shares) public setCorrectBlock { + _accrueInterest(marketParams); + + uint256 borrowShares = morpho.borrowShares(id, msg.sender); + if (borrowShares == 0) return; + + shares = bound(shares, 1, borrowShares); + uint256 repaidAmount = shares.toAssetsUp(morpho.totalBorrowAssets(id), morpho.totalBorrowShares(id)); + if (repaidAmount == 0) return; + + borrowableToken.setBalance(msg.sender, repaidAmount); + + vm.prank(msg.sender); + morpho.repay(marketParams, 0, shares, msg.sender, hex""); + } + + function repayOnMorphoOnBehalf(uint256 shares, address seed) public setCorrectBlock { + _accrueInterest(marketParams); + + address onBehalf = _randomSenderToRepayOnBehalf(targetSenders(), seed); + if (onBehalf == address(0)) return; + + uint256 borrowShares = morpho.borrowShares(id, onBehalf); + shares = bound(shares, 1, borrowShares); + uint256 repaidAmount = shares.toAssetsUp(morpho.totalBorrowAssets(id), morpho.totalBorrowShares(id)); + if (repaidAmount == 0) return; + + borrowableToken.setBalance(msg.sender, repaidAmount); + vm.prank(msg.sender); + morpho.repay(marketParams, 0, shares, onBehalf, hex""); + } + + function supplyCollateralOnMorpho(uint256 amount) public setCorrectBlock { + _accrueInterest(marketParams); + + amount = bound(amount, 1, MAX_TEST_AMOUNT); + collateralToken.setBalance(msg.sender, amount); + + vm.prank(msg.sender); + morpho.supplyCollateral(marketParams, amount, msg.sender, hex""); + } + + function withdrawCollateralOnMorpho(uint256 amount) public setCorrectBlock { + _accrueInterest(marketParams); + + if (morpho.collateral(id, msg.sender) == 0 || !isHealthy(id, msg.sender)) return; + + uint256 collateralPrice = IOracle(marketParams.oracle).price(); + + uint256 borrowPower = morpho.collateral(id, msg.sender).mulDivDown(collateralPrice, ORACLE_PRICE_SCALE).wMulDown( + marketParams.lltv + ); + uint256 borrowed = + morpho.borrowShares(id, msg.sender).toAssetsUp(morpho.totalBorrowAssets(id), morpho.totalBorrowShares(id)); + uint256 withdrawableCollateral = + (borrowPower - borrowed).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice).wDivDown(marketParams.lltv); + + if (withdrawableCollateral == 0) return; + amount = bound(amount, 1, withdrawableCollateral); + + vm.prank(msg.sender); + morpho.withdrawCollateral(marketParams, amount, msg.sender, msg.sender); + } + + function withdrawCollateralOnMorphoOnBehalf(uint256 amount, address seed) public setCorrectBlock { + _accrueInterest(marketParams); + + address onBehalf = _randomSenderToWithdrawCollateralOnBehalf(targetSenders(), seed, msg.sender); + if (onBehalf == address(0)) return; + + uint256 collateralPrice = IOracle(marketParams.oracle).price(); + + uint256 borrowPower = + morpho.collateral(id, onBehalf).mulDivDown(collateralPrice, ORACLE_PRICE_SCALE).wMulDown(marketParams.lltv); + uint256 borrowed = + morpho.borrowShares(id, onBehalf).toAssetsUp(morpho.totalBorrowAssets(id), morpho.totalBorrowShares(id)); + uint256 withdrawableCollateral = + (borrowPower - borrowed).mulDivDown(ORACLE_PRICE_SCALE, collateralPrice).wDivDown(marketParams.lltv); + + if (withdrawableCollateral == 0) return; + amount = bound(amount, 1, withdrawableCollateral); + + vm.prank(msg.sender); + morpho.withdrawCollateral(marketParams, amount, onBehalf, msg.sender); + } + + function liquidateOnMorpho(uint256 seized, address seed) public setCorrectBlock { + _accrueInterest(marketParams); + + user = _randomSenderToLiquidate(targetSenders(), seed); + if (user == address(0)) return; + + uint256 collateralPrice = IOracle(marketParams.oracle).price(); + + uint256 repaid = + seized.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(_liquidationIncentiveFactor(marketParams.lltv)); + uint256 repaidShares = repaid.toSharesDown(morpho.totalBorrowAssets(id), morpho.totalBorrowShares(id)); + + if (repaidShares > morpho.borrowShares(id, user)) { + seized = seized / 2; + } + borrowableToken.setBalance(msg.sender, repaid); + + vm.prank(msg.sender); + morpho.liquidate(marketParams, user, seized, hex""); + } + + function invariantSupplyShares() public { + assertEq(sumUsersSupplyShares(targetSenders()), morpho.totalSupplyShares(id)); + } + + function invariantBorrowShares() public { + assertEq(sumUsersBorrowShares(targetSenders()), morpho.totalBorrowShares(id)); + } + + function invariantTotalSupply() public { + assertLe(sumUsersSuppliedAmounts(targetSenders()), morpho.totalSupplyAssets(id)); + } + + function invariantTotalBorrow() public { + assertGe(sumUsersBorrowedAmounts(targetSenders()), morpho.totalBorrowAssets(id)); + } + + function invariantTotalSupplyGreaterThanTotalBorrow() public { + assertGe(morpho.totalSupplyAssets(id), morpho.totalBorrowAssets(id)); + } + + function invariantMorphoBalance() public { + assertEq( + morpho.totalSupplyAssets(id) - morpho.totalBorrowAssets(id), borrowableToken.balanceOf(address(morpho)) + ); + } +} diff --git a/test/forge/invariant/TestInvariantSinglePosition.t.sol b/test/forge/invariant/TestInvariantSinglePosition.t.sol new file mode 100644 index 000000000..74f6fc65f --- /dev/null +++ b/test/forge/invariant/TestInvariantSinglePosition.t.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "test/forge/InvariantBase.sol"; + +contract SinglePositionInvariantTest is InvariantBaseTest { + using MathLib for uint256; + using MorphoLib for Morpho; + using SharesMathLib for uint256; + + address user; + + function setUp() public virtual override { + super.setUp(); + + user = _addrFromHashedString("Morpho user"); + targetSender(user); + + collateralToken.setBalance(user, 1e30); + vm.startPrank(user); + borrowableToken.approve(address(morpho), type(uint256).max); + collateralToken.approve(address(morpho), type(uint256).max); + morpho.supplyCollateral(marketParams, 1e30, user, hex""); + vm.stopPrank(); + + // High price because of the 1e36 price scale + oracle.setPrice(1e40); + + _weightSelector(this.supplyOnMorpho.selector, 20); + _weightSelector(this.borrowOnMorpho.selector, 15); + _weightSelector(this.repayOnMorpho.selector, 10); + _weightSelector(this.withdrawOnMorpho.selector, 15); + _weightSelector(this.supplyCollateralOnMorpho.selector, 20); + _weightSelector(this.withdrawCollateralOnMorpho.selector, 15); + + targetSelector(FuzzSelector({addr: address(this), selectors: selectors})); + } + + function supplyOnMorpho(uint256 amount) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + borrowableToken.setBalance(msg.sender, amount); + vm.prank(msg.sender); + morpho.supply(marketParams, amount, 0, msg.sender, hex""); + } + + function withdrawOnMorpho(uint256 amount) public { + uint256 availableLiquidity = morpho.totalSupplyAssets(id) - morpho.totalBorrowAssets(id); + if (morpho.supplyShares(id, msg.sender) == 0) return; + if (availableLiquidity == 0) return; + + uint256 supplierBalance = + morpho.supplyShares(id, msg.sender).toAssetsDown(morpho.totalSupplyAssets(id), morpho.totalSupplyShares(id)); + if (supplierBalance == 0) return; + amount = bound(amount, 1, min(supplierBalance, availableLiquidity)); + + vm.prank(msg.sender); + morpho.withdraw(marketParams, amount, 0, msg.sender, msg.sender); + } + + function borrowOnMorpho(uint256 amount) public { + uint256 availableLiquidity = morpho.totalSupplyAssets(id) - morpho.totalBorrowAssets(id); + if (availableLiquidity == 0) return; + + amount = bound(amount, 1, availableLiquidity); + + vm.prank(msg.sender); + morpho.borrow(marketParams, amount, 0, msg.sender, msg.sender); + } + + function repayOnMorpho(uint256 amount) public { + if (morpho.borrowShares(id, msg.sender) == 0) return; + + uint256 borrowerBalance = + morpho.borrowShares(id, msg.sender).toAssetsDown(morpho.totalBorrowAssets(id), morpho.totalBorrowShares(id)); + if (borrowerBalance == 0) return; + amount = bound(amount, 1, borrowerBalance); + + borrowableToken.setBalance(msg.sender, amount); + vm.prank(msg.sender); + morpho.repay(marketParams, amount, 0, msg.sender, hex""); + } + + function supplyCollateralOnMorpho(uint256 amount) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + collateralToken.setBalance(msg.sender, amount); + vm.prank(msg.sender); + morpho.supplyCollateral(marketParams, amount, msg.sender, hex""); + } + + function withdrawCollateralOnMorpho(uint256 amount) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + vm.prank(msg.sender); + morpho.withdrawCollateral(marketParams, amount, msg.sender, msg.sender); + } + + function invariantSupplyShares() public { + assertEq(morpho.supplyShares(id, user), morpho.totalSupplyShares(id)); + } + + function invariantBorrowShares() public { + assertEq(morpho.borrowShares(id, user), morpho.totalBorrowShares(id)); + } + + function invariantTotalSupply() public { + uint256 suppliedAmount = + morpho.supplyShares(id, user).toAssetsDown(morpho.totalSupplyAssets(id), morpho.totalSupplyShares(id)); + assertLe(suppliedAmount, morpho.totalSupplyAssets(id)); + } + + function invariantTotalBorrow() public { + uint256 borrowedAmount = + morpho.borrowShares(id, user).toAssetsUp(morpho.totalBorrowAssets(id), morpho.totalBorrowShares(id)); + assertGe(borrowedAmount, morpho.totalBorrowAssets(id)); + } + + function invariantTotalSupplyGreaterThanTotalBorrow() public { + assertGe(morpho.totalSupplyAssets(id), morpho.totalBorrowAssets(id)); + } + + function invariantMorphoBalance() public { + assertEq( + morpho.totalSupplyAssets(id) - morpho.totalBorrowAssets(id), borrowableToken.balanceOf(address(morpho)) + ); + } + + // No price changes, and no new blocks so position has to remain healthy. + function invariantHealthyPosition() public { + assertTrue(isHealthy(id, user)); + } +} diff --git a/test/forge/invariant/TestInvariantTwoMarkets.t.sol b/test/forge/invariant/TestInvariantTwoMarkets.t.sol new file mode 100644 index 000000000..430631ce8 --- /dev/null +++ b/test/forge/invariant/TestInvariantTwoMarkets.t.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "test/forge/InvariantBase.sol"; + +contract TwoMarketsInvariantTest is InvariantBaseTest { + using MathLib for uint256; + using MorphoLib for Morpho; + using SharesMathLib for uint256; + using MarketParamsLib for MarketParams; + + Irm internal irm2; + MarketParams public marketParams2; + Id public id2; + + function setUp() public virtual override { + super.setUp(); + + irm2 = new Irm(morpho); + vm.label(address(irm2), "IRM2"); + + marketParams2 = + MarketParams(address(borrowableToken), address(collateralToken), address(oracle), address(irm2), LLTV + 1); + id2 = marketParams2.id(); + + vm.startPrank(OWNER); + morpho.enableIrm(address(irm2)); + morpho.enableLltv(LLTV + 1); + morpho.createMarket(marketParams2); + vm.stopPrank(); + + _targetDefaultSenders(); + + _approveSendersTransfers(targetSenders()); + _supplyHighAmountOfCollateralForAllSenders(targetSenders(), marketParams); + _supplyHighAmountOfCollateralForAllSenders(targetSenders(), marketParams2); + + // High price because of the 1e36 price scale + oracle.setPrice(1e40); + + _weightSelector(this.setMarketFee.selector, 5); + _weightSelector(this.supplyOnMorpho.selector, 20); + _weightSelector(this.borrowOnMorpho.selector, 15); + _weightSelector(this.repayOnMorpho.selector, 10); + _weightSelector(this.withdrawOnMorpho.selector, 15); + _weightSelector(this.supplyCollateralOnMorpho.selector, 20); + _weightSelector(this.withdrawCollateralOnMorpho.selector, 15); + _weightSelector(this.newBlock.selector, 20); + + blockNumber = block.number; + timestamp = block.timestamp; + + targetSelector(FuzzSelector({addr: address(this), selectors: selectors})); + } + + function chooseMarket(bool changeMarket) internal view returns (MarketParams memory chosenMarket, Id chosenId) { + if (!changeMarket) { + chosenMarket = marketParams; + chosenId = id; + } else { + chosenMarket = marketParams2; + chosenId = id2; + } + } + + function setMarketFee(uint256 newFee, bool changeMarket) public setCorrectBlock { + (MarketParams memory chosenMarket,) = chooseMarket(changeMarket); + + newFee = bound(newFee, 0.1e18, MAX_FEE); + + vm.prank(OWNER); + morpho.setFee(chosenMarket, newFee); + } + + function supplyOnMorpho(uint256 amount, bool changeMarket) public setCorrectBlock { + (MarketParams memory chosenMarket,) = chooseMarket(changeMarket); + + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + borrowableToken.setBalance(msg.sender, amount); + vm.prank(msg.sender); + morpho.supply(chosenMarket, amount, 0, msg.sender, hex""); + } + + function withdrawOnMorpho(uint256 amount, bool changeMarket) public setCorrectBlock { + _accrueInterest(marketParams); + + (MarketParams memory chosenMarket, Id chosenId) = chooseMarket(changeMarket); + + uint256 availableLiquidity = morpho.totalSupplyAssets(chosenId) - morpho.totalBorrowAssets(chosenId); + if (morpho.supplyShares(chosenId, msg.sender) == 0) return; + if (availableLiquidity == 0) return; + + _accrueInterest(marketParams); + uint256 supplierBalance = morpho.supplyShares(chosenId, msg.sender).toAssetsDown( + morpho.totalSupplyAssets(chosenId), morpho.totalSupplyShares(chosenId) + ); + amount = bound(amount, 1, min(supplierBalance, availableLiquidity)); + + vm.prank(msg.sender); + morpho.withdraw(chosenMarket, amount, 0, msg.sender, msg.sender); + } + + function borrowOnMorpho(uint256 amount, bool changeMarket) public setCorrectBlock { + _accrueInterest(marketParams); + + (MarketParams memory chosenMarket, Id chosenId) = chooseMarket(changeMarket); + + uint256 availableLiquidity = morpho.totalSupplyAssets(chosenId) - morpho.totalBorrowAssets(chosenId); + if (availableLiquidity == 0) return; + + _accrueInterest(marketParams); + amount = bound(amount, 1, availableLiquidity); + + vm.prank(msg.sender); + morpho.borrow(chosenMarket, amount, 0, msg.sender, msg.sender); + } + + function repayOnMorpho(uint256 amount, bool changeMarket) public setCorrectBlock { + _accrueInterest(marketParams); + + (MarketParams memory chosenMarket, Id chosenId) = chooseMarket(changeMarket); + + if (morpho.borrowShares(chosenId, msg.sender) == 0) return; + + _accrueInterest(marketParams); + amount = bound( + amount, + 1, + morpho.borrowShares(chosenId, msg.sender).toAssetsDown( + morpho.totalBorrowAssets(chosenId), morpho.totalBorrowShares(chosenId) + ) + ); + + borrowableToken.setBalance(msg.sender, amount); + vm.prank(msg.sender); + morpho.repay(chosenMarket, amount, 0, msg.sender, hex""); + } + + function supplyCollateralOnMorpho(uint256 amount, bool changeMarket) public setCorrectBlock { + (MarketParams memory chosenMarket,) = chooseMarket(changeMarket); + + amount = bound(amount, 1, MAX_TEST_AMOUNT); + collateralToken.setBalance(msg.sender, amount); + + vm.prank(msg.sender); + morpho.supplyCollateral(chosenMarket, amount, msg.sender, hex""); + } + + function withdrawCollateralOnMorpho(uint256 amount, bool changeMarket) public setCorrectBlock { + _accrueInterest(marketParams); + + (MarketParams memory chosenMarket,) = chooseMarket(changeMarket); + + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + vm.prank(msg.sender); + morpho.withdrawCollateral(chosenMarket, amount, msg.sender, msg.sender); + } + + function invariantMorphoBalance() public { + uint256 marketAvailableAmount = morpho.totalSupplyAssets(id) - morpho.totalBorrowAssets(id); + uint256 market2AvailableAmount = morpho.totalSupplyAssets(id2) - morpho.totalBorrowAssets(id2); + assertEq(marketAvailableAmount + market2AvailableAmount, borrowableToken.balanceOf(address(morpho))); + } +} diff --git a/test/forge/periphery/MorphoBalancesLib.t.sol b/test/forge/periphery/MorphoBalancesLib.t.sol index ae320c878..25cc2501a 100644 --- a/test/forge/periphery/MorphoBalancesLib.t.sol +++ b/test/forge/periphery/MorphoBalancesLib.t.sol @@ -21,7 +21,7 @@ contract MorphoBalancesLibTest is BaseTest { uint256 virtualTotalBorrowShares ) = morpho.expectedMarketBalances(marketParams); - _accrueInterest(); + _accrueInterest(marketParams); assertEq(virtualTotalSupply, morpho.totalSupplyAssets(id), "total supply"); assertEq(virtualTotalBorrow, morpho.totalBorrowAssets(id), "total borrow"); @@ -36,7 +36,7 @@ contract MorphoBalancesLibTest is BaseTest { uint256 expectedTotalSupply = morpho.expectedTotalSupply(marketParams); - _accrueInterest(); + _accrueInterest(marketParams); assertEq(expectedTotalSupply, morpho.totalSupplyAssets(id)); } @@ -48,7 +48,7 @@ contract MorphoBalancesLibTest is BaseTest { uint256 expectedTotalBorrow = morpho.expectedTotalBorrow(marketParams); - _accrueInterest(); + _accrueInterest(marketParams); assertEq(expectedTotalBorrow, morpho.totalBorrowAssets(id)); } @@ -63,7 +63,7 @@ contract MorphoBalancesLibTest is BaseTest { uint256 expectedTotalSupplyShares = morpho.expectedTotalSupplyShares(marketParams); - _accrueInterest(); + _accrueInterest(marketParams); assertEq(expectedTotalSupplyShares, morpho.totalSupplyShares(id)); } @@ -75,7 +75,7 @@ contract MorphoBalancesLibTest is BaseTest { uint256 expectedSupplyBalance = morpho.expectedSupplyBalance(marketParams, address(this)); - _accrueInterest(); + _accrueInterest(marketParams); uint256 actualSupplyBalance = morpho.supplyShares(id, address(this)).toAssetsDown( morpho.totalSupplyAssets(id), morpho.totalSupplyShares(id) @@ -91,7 +91,7 @@ contract MorphoBalancesLibTest is BaseTest { uint256 expectedBorrowBalance = morpho.expectedBorrowBalance(marketParams, address(this)); - _accrueInterest(); + _accrueInterest(marketParams); uint256 actualBorrowBalance = morpho.borrowShares(id, address(this)).toAssetsUp( morpho.totalBorrowAssets(id), morpho.totalBorrowShares(id)