diff --git a/test/forge/BaseTest.sol b/test/forge/BaseTest.sol new file mode 100644 index 000000000..9c8ba1158 --- /dev/null +++ b/test/forge/BaseTest.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import {SigUtils} from "test/forge/helpers/SigUtils.sol"; +import "src/Morpho.sol"; +import {ERC20Mock as ERC20} from "src/mocks/ERC20Mock.sol"; +import {OracleMock as Oracle} from "src/mocks/OracleMock.sol"; +import {IrmMock as Irm} from "src/mocks/IrmMock.sol"; + +contract BaseTest is Test { + using MathLib for uint256; + using MarketLib for Market; + + uint256 internal constant HIGH_COLLATERAL_AMOUNT = 1e35; + uint256 internal constant MIN_TEST_AMOUNT = 100; + uint256 internal constant MAX_TEST_AMOUNT = 1e28; + uint256 internal constant MIN_TEST_SHARES = MIN_TEST_AMOUNT * SharesMathLib.VIRTUAL_SHARES; + uint256 internal constant MAX_TEST_SHARES = MAX_TEST_AMOUNT * SharesMathLib.VIRTUAL_SHARES; + uint256 internal constant MIN_COLLATERAL_PRICE = 1000; + uint256 internal constant MAX_COLLATERAL_PRICE = 1e40; + + address internal SUPPLIER = _addrFromHashedString("Morpho Supplier"); + address internal BORROWER = _addrFromHashedString("Morpho Borrower"); + address internal REPAYER = _addrFromHashedString("Morpho Repayer"); + address internal ONBEHALF = _addrFromHashedString("Morpho On Behalf"); + address internal RECEIVER = _addrFromHashedString("Morpho Receiver"); + address internal LIQUIDATOR = _addrFromHashedString("Morpho Liquidator"); + address internal OWNER = _addrFromHashedString("Morpho Owner"); + + uint256 internal constant LLTV = 0.8 ether; + + Morpho internal morpho; + ERC20 internal borrowableToken; + ERC20 internal collateralToken; + Oracle internal oracle; + Irm internal irm; + Market internal market; + Id internal id; + + function setUp() public { + vm.label(OWNER, "Owner"); + vm.label(SUPPLIER, "Supplier"); + vm.label(BORROWER, "Borrower"); + vm.label(REPAYER, "Repayer"); + vm.label(ONBEHALF, "OnBehalf"); + vm.label(RECEIVER, "Receiver"); + vm.label(LIQUIDATOR, "Liquidator"); + + // Create Morpho. + morpho = new Morpho(OWNER); + vm.label(address(morpho), "Morpho"); + + // List a market. + borrowableToken = new ERC20("borrowable", "B"); + vm.label(address(borrowableToken), "Borrowable asset"); + + collateralToken = new ERC20("collateral", "C"); + vm.label(address(collateralToken), "Collateral asset"); + + oracle = new Oracle(); + vm.label(address(oracle), "Oracle"); + + oracle.setPrice(1e36); + + irm = new Irm(morpho); + vm.label(address(irm), "IRM"); + + market = Market(address(borrowableToken), address(collateralToken), address(oracle), address(irm), LLTV); + id = market.id(); + + vm.startPrank(OWNER); + morpho.enableIrm(address(irm)); + morpho.enableLltv(LLTV); + morpho.createMarket(market); + vm.stopPrank(); + + borrowableToken.approve(address(morpho), type(uint256).max); + collateralToken.approve(address(morpho), type(uint256).max); + vm.startPrank(SUPPLIER); + borrowableToken.approve(address(morpho), type(uint256).max); + collateralToken.approve(address(morpho), type(uint256).max); + vm.stopPrank(); + vm.startPrank(BORROWER); + borrowableToken.approve(address(morpho), type(uint256).max); + collateralToken.approve(address(morpho), type(uint256).max); + vm.stopPrank(); + vm.startPrank(REPAYER); + borrowableToken.approve(address(morpho), type(uint256).max); + collateralToken.approve(address(morpho), type(uint256).max); + vm.stopPrank(); + vm.startPrank(LIQUIDATOR); + borrowableToken.approve(address(morpho), type(uint256).max); + collateralToken.approve(address(morpho), type(uint256).max); + vm.stopPrank(); + vm.startPrank(ONBEHALF); + borrowableToken.approve(address(morpho), type(uint256).max); + collateralToken.approve(address(morpho), type(uint256).max); + morpho.setAuthorization(BORROWER, true); + vm.stopPrank(); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 1 days); + } + + function _addrFromHashedString(string memory str) internal pure returns (address) { + return address(uint160(uint256(keccak256(bytes(str))))); + } + + function _supply(uint256 amount) internal { + borrowableToken.setBalance(address(this), amount); + morpho.supply(market, amount, 0, address(this), hex""); + } + + function _supplyCollateralForBorrower(address borrower) internal { + collateralToken.setBalance(borrower, HIGH_COLLATERAL_AMOUNT); + vm.startPrank(borrower); + collateralToken.approve(address(morpho), type(uint256).max); + morpho.supplyCollateral(market, HIGH_COLLATERAL_AMOUNT, borrower, hex""); + vm.stopPrank(); + } + + function _boundHealthyPosition(uint256 amountCollateral, uint256 amountBorrowed, uint256 priceCollateral) + internal + view + returns (uint256, uint256, uint256) + { + priceCollateral = bound(priceCollateral, MIN_COLLATERAL_PRICE, MAX_COLLATERAL_PRICE); + amountBorrowed = bound(amountBorrowed, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT); + + uint256 minCollateral = amountBorrowed.wDivUp(market.lltv).mulDivUp(ORACLE_PRICE_SCALE, priceCollateral); + // vm.assume(minCollateral <= MAX_TEST_AMOUNT); + + amountCollateral = bound(amountCollateral, minCollateral, max(minCollateral, MAX_TEST_AMOUNT)); + + return (amountCollateral, amountBorrowed, priceCollateral); + } + + function _boundUnhealthyPosition(uint256 amountCollateral, uint256 amountBorrowed, uint256 priceCollateral) + internal + view + returns (uint256, uint256, uint256) + { + priceCollateral = bound(priceCollateral, MIN_COLLATERAL_PRICE, MAX_COLLATERAL_PRICE); + amountBorrowed = bound(amountBorrowed, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT); + + uint256 maxCollateral = amountBorrowed.wDivDown(market.lltv).mulDivDown(ORACLE_PRICE_SCALE, priceCollateral); + vm.assume( + maxCollateral.mulDivDown(priceCollateral, ORACLE_PRICE_SCALE).wMulDown(market.lltv) < amountBorrowed + && maxCollateral > 0 + ); + + amountCollateral = bound(amountBorrowed, 1, maxCollateral); + + return (amountCollateral, amountBorrowed, priceCollateral); + } + + function _boundValidLltv(uint256 lltv) internal view returns (uint256) { + return bound(lltv, 0, WAD - 1); + } + + function _boundInvalidLltv(uint256 lltv) internal view returns (uint256) { + return bound(lltv, WAD, type(uint256).max); + } + + function _liquidationIncentive(uint256 lltv) internal pure returns (uint256) { + return + UtilsLib.min(MAX_LIQUIDATION_INCENTIVE_FACTOR, WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - lltv))); + } + + function neq(Market memory a, Market memory b) internal pure returns (bool) { + return (Id.unwrap(a.id()) != Id.unwrap(b.id())); + } + + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return a > b ? a : b; + } + + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } +} diff --git a/test/forge/Morpho.t.sol b/test/forge/Morpho.t.sol deleted file mode 100644 index ec5bd1512..000000000 --- a/test/forge/Morpho.t.sol +++ /dev/null @@ -1,1116 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; - -import "forge-std/Test.sol"; -import "forge-std/console.sol"; - -import {SigUtils} from "./helpers/SigUtils.sol"; - -import "src/Morpho.sol"; -import {SharesMathLib} from "src/libraries/SharesMathLib.sol"; -import { - IMorphoLiquidateCallback, - IMorphoRepayCallback, - IMorphoSupplyCallback, - IMorphoSupplyCollateralCallback -} from "src/interfaces/IMorphoCallbacks.sol"; -import {ERC20Mock as ERC20} from "src/mocks/ERC20Mock.sol"; -import {OracleMock as Oracle} from "src/mocks/OracleMock.sol"; -import {IrmMock as Irm} from "src/mocks/IrmMock.sol"; - -contract MorphoTest is - Test, - IMorphoSupplyCallback, - IMorphoSupplyCollateralCallback, - IMorphoRepayCallback, - IMorphoLiquidateCallback -{ - using MathLib for uint256; - using MarketLib for Market; - using SharesMathLib for uint256; - using stdStorage for StdStorage; - - address private constant BORROWER = address(0x1234); - address private constant LIQUIDATOR = address(0x5678); - uint256 private constant LLTV = 0.8 ether; - address private constant OWNER = address(0xdead); - - IMorpho private morpho; - ERC20 private borrowableToken; - ERC20 private collateralToken; - Oracle private oracle; - Irm private irm; - Market private market; - Id private id; - - function setUp() public { - // Create Morpho. - morpho = new Morpho(OWNER); - - // List a market. - borrowableToken = new ERC20("borrowable", "B"); - collateralToken = new ERC20("collateral", "C"); - oracle = new Oracle(); - - irm = new Irm(morpho); - - market = Market(address(borrowableToken), address(collateralToken), address(oracle), address(irm), LLTV); - id = market.id(); - - vm.startPrank(OWNER); - morpho.enableIrm(address(irm)); - morpho.enableLltv(LLTV); - morpho.createMarket(market); - vm.stopPrank(); - - oracle.setPrice(ORACLE_PRICE_SCALE); - - borrowableToken.approve(address(morpho), type(uint256).max); - collateralToken.approve(address(morpho), type(uint256).max); - - vm.startPrank(BORROWER); - borrowableToken.approve(address(morpho), type(uint256).max); - collateralToken.approve(address(morpho), type(uint256).max); - morpho.setAuthorization(address(this), true); - vm.stopPrank(); - - vm.startPrank(LIQUIDATOR); - borrowableToken.approve(address(morpho), type(uint256).max); - collateralToken.approve(address(morpho), type(uint256).max); - vm.stopPrank(); - } - - /// @dev Calculates the net worth of the given user quoted in borrowable asset. - // TODO: To move to a test utils file later. - function netWorth(address user) internal view returns (uint256) { - uint256 collateralPrice = IOracle(market.oracle).price(); - - uint256 collateralAssetValue = collateralToken.balanceOf(user).mulDivDown(collateralPrice, ORACLE_PRICE_SCALE); - uint256 borrowableAssetValue = borrowableToken.balanceOf(user); - - return collateralAssetValue + borrowableAssetValue; - } - - function supplyBalance(address user) internal view returns (uint256) { - uint256 supplyShares = morpho.supplyShares(id, user); - if (supplyShares == 0) return 0; - - uint256 totalShares = morpho.totalSupplyShares(id); - uint256 totalSupply = morpho.totalSupply(id); - return supplyShares.toAssetsDown(totalSupply, totalShares); - } - - function borrowBalance(address user) internal view returns (uint256) { - uint256 borrowerShares = morpho.borrowShares(id, user); - if (borrowerShares == 0) return 0; - - uint256 totalShares = morpho.totalBorrowShares(id); - uint256 totalBorrow = morpho.totalBorrow(id); - return borrowerShares.toAssetsUp(totalBorrow, totalShares); - } - - // Invariants - - function invariantLiquidity() public { - assertLe(morpho.totalBorrow(id), morpho.totalSupply(id), "liquidity"); - } - - function invariantLltvEnabled() public { - assertTrue(morpho.isLltvEnabled(LLTV)); - } - - function invariantIrmEnabled() public { - assertTrue(morpho.isIrmEnabled(address(irm))); - } - - function invariantMarketCreated() public { - assertGt(morpho.lastUpdate(id), 0); - } - - // Tests - - function testOwner(address newOwner) public { - Morpho morpho2 = new Morpho(newOwner); - - assertEq(morpho2.owner(), newOwner, "owner"); - } - - function testTransferOwnership(address oldOwner, address newOwner) public { - Morpho morpho2 = new Morpho(oldOwner); - - vm.prank(oldOwner); - morpho2.setOwner(newOwner); - assertEq(morpho2.owner(), newOwner, "owner"); - } - - function testTransferOwnershipWhenNotOwner(address attacker, address newOwner) public { - vm.assume(attacker != OWNER); - - Morpho morpho2 = new Morpho(OWNER); - - vm.prank(attacker); - vm.expectRevert(bytes(ErrorsLib.NOT_OWNER)); - morpho2.setOwner(newOwner); - } - - function testEnableIrmWhenNotOwner(address attacker, address newIrm) public { - vm.assume(attacker != morpho.owner()); - - vm.prank(attacker); - vm.expectRevert(bytes(ErrorsLib.NOT_OWNER)); - morpho.enableIrm(newIrm); - } - - function testEnableIrm(address newIrm) public { - vm.prank(OWNER); - morpho.enableIrm(newIrm); - - assertTrue(morpho.isIrmEnabled(newIrm)); - } - - function testCreateMarketWithEnabledIrm(Market memory marketFuzz) public { - marketFuzz.lltv = LLTV; - - vm.startPrank(OWNER); - morpho.enableIrm(marketFuzz.irm); - morpho.createMarket(marketFuzz); - vm.stopPrank(); - } - - function testCreateMarketWithNotEnabledIrm(Market memory marketFuzz) public { - vm.assume(marketFuzz.irm != address(irm)); - - vm.prank(OWNER); - vm.expectRevert(bytes(ErrorsLib.IRM_NOT_ENABLED)); - morpho.createMarket(marketFuzz); - } - - function testEnableLltvWhenNotOwner(address attacker, uint256 newLltv) public { - vm.assume(attacker != OWNER); - - vm.prank(attacker); - vm.expectRevert(bytes(ErrorsLib.NOT_OWNER)); - morpho.enableLltv(newLltv); - } - - function testEnableLltv(uint256 newLltv) public { - newLltv = bound(newLltv, 0, WAD - 1); - - vm.prank(OWNER); - morpho.enableLltv(newLltv); - - assertTrue(morpho.isLltvEnabled(newLltv)); - } - - function testEnableLltvShouldFailWhenLltvTooHigh(uint256 newLltv) public { - newLltv = bound(newLltv, WAD, type(uint256).max); - - vm.prank(OWNER); - vm.expectRevert(bytes(ErrorsLib.LLTV_TOO_HIGH)); - morpho.enableLltv(newLltv); - } - - function testSetFee(uint256 fee) public { - fee = bound(fee, 0, MAX_FEE); - - vm.prank(OWNER); - morpho.setFee(market, fee); - - assertEq(morpho.fee(id), fee); - } - - function testSetFeeShouldRevertIfTooHigh(uint256 fee) public { - fee = bound(fee, MAX_FEE + 1, type(uint256).max); - - vm.prank(OWNER); - vm.expectRevert(bytes(ErrorsLib.MAX_FEE_EXCEEDED)); - morpho.setFee(market, fee); - } - - function testSetFeeShouldRevertIfMarketNotCreated(Market memory marketFuzz, uint256 fee) public { - vm.assume(neq(marketFuzz, market)); - fee = bound(fee, 0, WAD); - - vm.prank(OWNER); - vm.expectRevert(bytes(ErrorsLib.MARKET_NOT_CREATED)); - morpho.setFee(marketFuzz, fee); - } - - function testSetFeeShouldRevertIfNotOwner(uint256 fee, address caller) public { - vm.assume(caller != OWNER); - fee = bound(fee, 0, WAD); - - vm.expectRevert(bytes(ErrorsLib.NOT_OWNER)); - morpho.setFee(market, fee); - } - - function testSetFeeRecipient(address recipient) public { - vm.prank(OWNER); - morpho.setFeeRecipient(recipient); - - assertEq(morpho.feeRecipient(), recipient); - } - - function testSetFeeRecipientShouldRevertIfNotOwner(address caller, address recipient) public { - vm.assume(caller != OWNER); - - vm.expectRevert(bytes(ErrorsLib.NOT_OWNER)); - vm.prank(caller); - morpho.setFeeRecipient(recipient); - } - - function testFeeAccrues(uint256 assetsLent, uint256 assetsBorrowed, uint256 fee, uint256 timeElapsed) public { - assetsLent = bound(assetsLent, 1, 2 ** 64); - assetsBorrowed = bound(assetsBorrowed, 1, assetsLent); - timeElapsed = bound(timeElapsed, 1, 365 days); - fee = bound(fee, 0, MAX_FEE); - address recipient = OWNER; - - vm.startPrank(OWNER); - morpho.setFee(market, fee); - morpho.setFeeRecipient(recipient); - vm.stopPrank(); - - borrowableToken.setBalance(address(this), assetsLent); - morpho.supply(market, assetsLent, 0, address(this), hex""); - - uint256 collateralAmount = assetsBorrowed.wDivUp(LLTV); - collateralToken.setBalance(address(this), collateralAmount); - morpho.supplyCollateral(market, collateralAmount, BORROWER, hex""); - - vm.prank(BORROWER); - morpho.borrow(market, assetsBorrowed, 0, BORROWER, BORROWER); - - uint256 totalSupplyBefore = morpho.totalSupply(id); - - // Trigger an accrue. - vm.warp(block.timestamp + timeElapsed); - - collateralToken.setBalance(address(this), 1); - morpho.supplyCollateral(market, 1, address(this), hex""); - morpho.withdrawCollateral(market, 1, address(this), address(this)); - - uint256 totalSupplyAfter = morpho.totalSupply(id); - vm.assume(totalSupplyAfter > totalSupplyBefore); - - uint256 accrued = totalSupplyAfter - totalSupplyBefore; - uint256 expectedFee = accrued.wMulDown(fee); - - assertApproxEqAbs(supplyBalance(recipient), expectedFee, 10); - } - - function testCreateMarketWithNotEnabledLltv(Market memory marketFuzz) public { - vm.assume(marketFuzz.lltv != LLTV); - marketFuzz.irm = address(irm); - - vm.prank(OWNER); - vm.expectRevert(bytes(ErrorsLib.LLTV_NOT_ENABLED)); - morpho.createMarket(marketFuzz); - } - - function testSupplyAmount(uint256 assets, address onBehalf) public { - vm.assume(onBehalf != address(0)); - vm.assume(onBehalf != address(morpho)); - assets = bound(assets, 1, 2 ** 64); - uint256 shares = assets.toSharesDown(morpho.totalSupply(id), morpho.totalSupplyShares(id)); - - borrowableToken.setBalance(address(this), assets); - morpho.supply(market, assets, 0, onBehalf, hex""); - - assertEq(morpho.supplyShares(id, onBehalf), shares, "supply share"); - assertEq(borrowableToken.balanceOf(onBehalf), 0, "lender balance"); - assertEq(borrowableToken.balanceOf(address(morpho)), assets, "morpho balance"); - } - - function testSupplyShares(uint256 shares, address onBehalf) public { - vm.assume(onBehalf != address(0)); - vm.assume(onBehalf != address(morpho)); - shares = bound(shares, 1, 2 ** 64); - uint256 assets = shares.toAssetsUp(morpho.totalSupply(id), morpho.totalSupplyShares(id)); - - borrowableToken.setBalance(address(this), assets); - morpho.supply(market, 0, shares, onBehalf, hex""); - - assertEq(morpho.supplyShares(id, onBehalf), shares, "supply share"); - assertEq(borrowableToken.balanceOf(onBehalf), 0, "lender balance"); - assertEq(borrowableToken.balanceOf(address(morpho)), assets, "morpho balance"); - } - - function testBorrowAmount(uint256 assetsLent, uint256 assetsBorrowed, address receiver) public { - vm.assume(receiver != address(0)); - vm.assume(receiver != address(morpho)); - assetsLent = bound(assetsLent, 1, 2 ** 64); - assetsBorrowed = bound(assetsBorrowed, 1, 2 ** 64); - uint256 shares = assetsBorrowed.toSharesUp(morpho.totalBorrow(id), morpho.totalBorrowShares(id)); - - borrowableToken.setBalance(address(this), assetsLent); - morpho.supply(market, assetsLent, 0, address(this), hex""); - - uint256 collateralAmount = shares.toAssetsUp(morpho.totalBorrow(id), morpho.totalBorrowShares(id)).wDivUp(LLTV); - collateralToken.setBalance(address(this), collateralAmount); - morpho.supplyCollateral(market, collateralAmount, BORROWER, hex""); - - if (assetsBorrowed > assetsLent) { - vm.prank(BORROWER); - vm.expectRevert(bytes(ErrorsLib.INSUFFICIENT_LIQUIDITY)); - morpho.borrow(market, assetsBorrowed, 0, BORROWER, receiver); - return; - } - - vm.prank(BORROWER); - morpho.borrow(market, assetsBorrowed, 0, BORROWER, receiver); - - assertEq(morpho.borrowShares(id, BORROWER), assetsBorrowed * SharesMathLib.VIRTUAL_SHARES, "borrow share"); - assertEq(borrowableToken.balanceOf(receiver), assetsBorrowed, "receiver balance"); - assertEq(borrowableToken.balanceOf(address(morpho)), assetsLent - assetsBorrowed, "morpho balance"); - } - - function testBorrowShares(uint256 shares) public { - shares = bound(shares, 1, 2 ** 64); - uint256 assets = shares.toAssetsDown(morpho.totalBorrow(id), morpho.totalBorrowShares(id)); - - borrowableToken.setBalance(address(this), assets); - if (assets > 0) morpho.supply(market, assets, 0, address(this), hex""); - - uint256 collateralAmount = shares.toAssetsUp(morpho.totalBorrow(id), morpho.totalBorrowShares(id)).wDivUp(LLTV); - collateralToken.setBalance(address(this), collateralAmount); - if (collateralAmount > 0) morpho.supplyCollateral(market, collateralAmount, BORROWER, hex""); - - vm.prank(BORROWER); - morpho.borrow(market, 0, shares, BORROWER, BORROWER); - - assertEq(morpho.borrowShares(id, BORROWER), shares, "borrow share"); - assertEq(borrowableToken.balanceOf(BORROWER), assets, "receiver balance"); - assertEq(borrowableToken.balanceOf(address(morpho)), 0, "morpho balance"); - } - - function _testWithdrawCommon(uint256 assetsLent) public { - assetsLent = bound(assetsLent, 1, 2 ** 64); - - borrowableToken.setBalance(address(this), assetsLent); - morpho.supply(market, assetsLent, 0, address(this), hex""); - - // Accrue interests. - stdstore.target(address(morpho)).sig("totalSupply(bytes32)").with_key(Id.unwrap(id)).checked_write( - morpho.totalSupply(id) * 4 / 3 - ); - borrowableToken.setBalance(address(morpho), morpho.totalSupply(id)); - } - - function testWithdrawShares(uint256 assetsLent, uint256 sharesWithdrawn, uint256 assetsBorrowed, address receiver) - public - { - vm.assume(receiver != BORROWER); - vm.assume(receiver != address(0)); - vm.assume(receiver != address(morpho)); - vm.assume(receiver != address(this)); - sharesWithdrawn = bound(sharesWithdrawn, 1, 2 ** 64); - - _testWithdrawCommon(assetsLent); - assetsBorrowed = bound(assetsBorrowed, 1, morpho.totalSupply(id)); - - uint256 collateralAmount = assetsBorrowed.wDivUp(LLTV); - collateralToken.setBalance(address(this), collateralAmount); - morpho.supplyCollateral(market, collateralAmount, BORROWER, hex""); - - morpho.borrow(market, assetsBorrowed, 0, BORROWER, BORROWER); - - uint256 totalSupplyBefore = morpho.totalSupply(id); - uint256 supplySharesBefore = morpho.supplyShares(id, address(this)); - uint256 assetsWithdrawn = sharesWithdrawn.toAssetsDown(morpho.totalSupply(id), morpho.totalSupplyShares(id)); - - if (sharesWithdrawn > morpho.supplyShares(id, address(this))) { - vm.expectRevert(stdError.arithmeticError); - morpho.withdraw(market, 0, sharesWithdrawn, address(this), receiver); - return; - } else if (assetsWithdrawn > totalSupplyBefore - assetsBorrowed) { - vm.expectRevert(bytes(ErrorsLib.INSUFFICIENT_LIQUIDITY)); - morpho.withdraw(market, 0, sharesWithdrawn, address(this), receiver); - return; - } - - morpho.withdraw(market, 0, sharesWithdrawn, address(this), receiver); - - assertEq(morpho.supplyShares(id, address(this)), supplySharesBefore - sharesWithdrawn, "supply share"); - assertEq(borrowableToken.balanceOf(receiver), assetsWithdrawn, "receiver balance"); - assertEq( - borrowableToken.balanceOf(address(morpho)), - totalSupplyBefore - assetsBorrowed - assetsWithdrawn, - "morpho balance" - ); - } - - function testWithdrawAmount(uint256 assetsLent, uint256 exactAmountWithdrawn) public { - _testWithdrawCommon(assetsLent); - - uint256 totalSupplyBefore = morpho.totalSupply(id); - uint256 supplySharesBefore = morpho.supplyShares(id, address(this)); - exactAmountWithdrawn = bound( - exactAmountWithdrawn, - 1, - supplySharesBefore.toAssetsDown(morpho.totalSupply(id), morpho.totalSupplyShares(id)) - ); - morpho.withdraw(market, exactAmountWithdrawn, 0, address(this), address(this)); - - // assertEq(morpho.supplyShares(id, address(this)), supplySharesBefore - sharesWithdrawn, "supply share"); - assertEq(borrowableToken.balanceOf(address(this)), exactAmountWithdrawn, "this balance"); - assertEq(borrowableToken.balanceOf(address(morpho)), totalSupplyBefore - exactAmountWithdrawn, "morpho balance"); - } - - function testWithdrawAll(uint256 assetsLent) public { - _testWithdrawCommon(assetsLent); - - uint256 totalSupplyBefore = morpho.totalSupply(id); - uint256 assetsWithdrawn = - morpho.supplyShares(id, address(this)).toAssetsDown(morpho.totalSupply(id), morpho.totalSupplyShares(id)); - morpho.withdraw(market, 0, morpho.supplyShares(id, address(this)), address(this), address(this)); - - assertEq(morpho.supplyShares(id, address(this)), 0, "supply share"); - assertEq(borrowableToken.balanceOf(address(this)), assetsWithdrawn, "this balance"); - assertEq(borrowableToken.balanceOf(address(morpho)), totalSupplyBefore - assetsWithdrawn, "morpho balance"); - } - - function _testRepayCommon(uint256 assetsBorrowed, address borrower) public { - assetsBorrowed = bound(assetsBorrowed, 1, 2 ** 64); - - borrowableToken.setBalance(address(this), 2 ** 66); - morpho.supply(market, assetsBorrowed, 0, address(this), hex""); - - uint256 collateralAmount = assetsBorrowed.wDivUp(LLTV); - collateralToken.setBalance(address(this), collateralAmount); - morpho.supplyCollateral(market, collateralAmount, borrower, hex""); - - vm.prank(borrower); - morpho.borrow(market, assetsBorrowed, 0, borrower, borrower); - - // Accrue interests. - stdstore.target(address(morpho)).sig("totalBorrow(bytes32)").with_key(Id.unwrap(id)).checked_write( - morpho.totalBorrow(id) * 4 / 3 - ); - } - - function testRepayShares(uint256 assetsBorrowed, uint256 sharesRepaid, address onBehalf) public { - vm.assume(onBehalf != address(0)); - vm.assume(onBehalf != address(morpho)); - _testRepayCommon(assetsBorrowed, onBehalf); - - uint256 thisBalanceBefore = borrowableToken.balanceOf(address(this)); - uint256 borrowSharesBefore = morpho.borrowShares(id, onBehalf); - sharesRepaid = bound(sharesRepaid, 1, borrowSharesBefore); - - uint256 assetsRepaid = sharesRepaid.toAssetsUp(morpho.totalBorrow(id), morpho.totalBorrowShares(id)); - morpho.repay(market, 0, sharesRepaid, onBehalf, hex""); - - assertEq(morpho.borrowShares(id, onBehalf), borrowSharesBefore - sharesRepaid, "borrow share"); - assertEq(borrowableToken.balanceOf(address(this)), thisBalanceBefore - assetsRepaid, "this balance"); - assertEq(borrowableToken.balanceOf(address(morpho)), assetsRepaid, "morpho balance"); - } - - function testRepayAmount(uint256 assetsBorrowed, uint256 exactAmountRepaid) public { - _testRepayCommon(assetsBorrowed, address(this)); - - uint256 thisBalanceBefore = borrowableToken.balanceOf(address(this)); - uint256 borrowSharesBefore = morpho.borrowShares(id, address(this)); - exactAmountRepaid = bound( - exactAmountRepaid, 1, borrowSharesBefore.toAssetsDown(morpho.totalBorrow(id), morpho.totalBorrowShares(id)) - ); - uint256 sharesRepaid = exactAmountRepaid.toSharesDown(morpho.totalBorrow(id), morpho.totalBorrowShares(id)); - morpho.repay(market, exactAmountRepaid, 0, address(this), hex""); - - assertEq(morpho.borrowShares(id, address(this)), borrowSharesBefore - sharesRepaid, "borrow share"); - assertEq(borrowableToken.balanceOf(address(this)), thisBalanceBefore - exactAmountRepaid, "this balance"); - assertEq(borrowableToken.balanceOf(address(morpho)), exactAmountRepaid, "morpho balance"); - } - - function testRepayAll(uint256 assetsBorrowed) public { - _testRepayCommon(assetsBorrowed, address(this)); - - uint256 assetsRepaid = - morpho.borrowShares(id, address(this)).toAssetsUp(morpho.totalBorrow(id), morpho.totalBorrowShares(id)); - borrowableToken.setBalance(address(this), assetsRepaid); - morpho.repay(market, 0, morpho.borrowShares(id, address(this)), address(this), hex""); - - assertEq(morpho.borrowShares(id, address(this)), 0, "borrow share"); - assertEq(borrowableToken.balanceOf(address(this)), 0, "this balance"); - assertEq(borrowableToken.balanceOf(address(morpho)), assetsRepaid, "morpho balance"); - } - - function testSupplyCollateralOnBehalf(uint256 assets, address onBehalf) public { - vm.assume(onBehalf != address(0)); - vm.assume(onBehalf != address(morpho)); - assets = bound(assets, 1, 2 ** 64); - - collateralToken.setBalance(address(this), assets); - morpho.supplyCollateral(market, assets, onBehalf, hex""); - - assertEq(morpho.collateral(id, onBehalf), assets, "collateral"); - assertEq(collateralToken.balanceOf(onBehalf), 0, "onBehalf balance"); - assertEq(collateralToken.balanceOf(address(morpho)), assets, "morpho balance"); - } - - function testWithdrawCollateral(uint256 assetsDeposited, uint256 assetsWithdrawn, address receiver) public { - vm.assume(receiver != address(0)); - vm.assume(receiver != address(morpho)); - assetsDeposited = bound(assetsDeposited, 1, 2 ** 64); - assetsWithdrawn = bound(assetsWithdrawn, 1, 2 ** 64); - - collateralToken.setBalance(address(this), assetsDeposited); - morpho.supplyCollateral(market, assetsDeposited, address(this), hex""); - - if (assetsWithdrawn > assetsDeposited) { - vm.expectRevert(stdError.arithmeticError); - morpho.withdrawCollateral(market, assetsWithdrawn, address(this), receiver); - return; - } - - morpho.withdrawCollateral(market, assetsWithdrawn, address(this), receiver); - - assertEq(morpho.collateral(id, address(this)), assetsDeposited - assetsWithdrawn, "this collateral"); - assertEq(collateralToken.balanceOf(receiver), assetsWithdrawn, "receiver balance"); - assertEq(collateralToken.balanceOf(address(morpho)), assetsDeposited - assetsWithdrawn, "morpho balance"); - } - - function testWithdrawCollateralAll(uint256 assetsDeposited, address receiver) public { - vm.assume(receiver != address(0)); - vm.assume(receiver != address(morpho)); - assetsDeposited = bound(assetsDeposited, 1, 2 ** 64); - - collateralToken.setBalance(address(this), assetsDeposited); - morpho.supplyCollateral(market, assetsDeposited, address(this), hex""); - morpho.withdrawCollateral(market, morpho.collateral(id, address(this)), address(this), receiver); - - assertEq(morpho.collateral(id, address(this)), 0, "this collateral"); - assertEq(collateralToken.balanceOf(receiver), assetsDeposited, "receiver balance"); - assertEq(collateralToken.balanceOf(address(morpho)), 0, "morpho balance"); - } - - function testCollateralRequirements(uint256 assetsCollateral, uint256 assetsBorrowed, uint256 collateralPrice) - public - { - assetsBorrowed = bound(assetsBorrowed, 1, 2 ** 64); - assetsCollateral = bound(assetsCollateral, 1, 2 ** 64); - collateralPrice = bound(collateralPrice, 0, 2 ** 64); - - oracle.setPrice(collateralPrice); - - borrowableToken.setBalance(address(this), assetsBorrowed); - collateralToken.setBalance(BORROWER, assetsCollateral); - - morpho.supply(market, assetsBorrowed, 0, address(this), hex""); - - vm.prank(BORROWER); - morpho.supplyCollateral(market, assetsCollateral, BORROWER, hex""); - - uint256 maxBorrow = assetsCollateral.mulDivDown(collateralPrice, ORACLE_PRICE_SCALE).wMulDown(LLTV); - - vm.prank(BORROWER); - if (maxBorrow < assetsBorrowed) vm.expectRevert(bytes(ErrorsLib.INSUFFICIENT_COLLATERAL)); - morpho.borrow(market, assetsBorrowed, 0, BORROWER, BORROWER); - } - - function testLiquidate(uint256 assetsLent) public { - oracle.setPrice(ORACLE_PRICE_SCALE); - assetsLent = bound(assetsLent, 1000, 2 ** 64); - - uint256 assetsCollateral = assetsLent; - uint256 borrowingPower = assetsCollateral.wMulDown(LLTV); - uint256 assetsBorrowed = borrowingPower.wMulDown(0.8e18); - uint256 toSeize = assetsCollateral.wMulDown(LLTV); - uint256 liquidationIncentiveFactor = - UtilsLib.min(MAX_LIQUIDATION_INCENTIVE_FACTOR, WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - LLTV))); - - borrowableToken.setBalance(address(this), assetsLent); - collateralToken.setBalance(BORROWER, assetsCollateral); - borrowableToken.setBalance(LIQUIDATOR, assetsBorrowed); - - // Supply - morpho.supply(market, assetsLent, 0, address(this), hex""); - - // Borrow - vm.startPrank(BORROWER); - morpho.supplyCollateral(market, assetsCollateral, BORROWER, hex""); - morpho.borrow(market, assetsBorrowed, 0, BORROWER, BORROWER); - vm.stopPrank(); - - // Price change - oracle.setPrice(ORACLE_PRICE_SCALE / 2); - - uint256 liquidatorNetWorthBefore = netWorth(LIQUIDATOR); - - // Liquidate - vm.prank(LIQUIDATOR); - (uint256 assetsRepaid,) = morpho.liquidate(market, BORROWER, toSeize, hex""); - - uint256 liquidatorNetWorthAfter = netWorth(LIQUIDATOR); - uint256 collateralPrice = IOracle(market.oracle).price(); - - uint256 expectedRepaid = - toSeize.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(liquidationIncentiveFactor); - uint256 expectedNetWorthAfter = - liquidatorNetWorthBefore + toSeize.mulDivDown(collateralPrice, ORACLE_PRICE_SCALE) - expectedRepaid; - assertEq(assetsRepaid, expectedRepaid, "wrong return repaid value"); - assertEq(liquidatorNetWorthAfter, expectedNetWorthAfter, "LIQUIDATOR net worth"); - assertApproxEqAbs(borrowBalance(BORROWER), assetsBorrowed - expectedRepaid, 100, "BORROWER balance"); - assertEq(morpho.collateral(id, BORROWER), assetsCollateral - toSeize, "BORROWER collateral"); - } - - function testRealizeBadDebt(uint256 assetsLent) public { - oracle.setPrice(ORACLE_PRICE_SCALE); - assetsLent = bound(assetsLent, 1000, 2 ** 64); - - uint256 assetsCollateral = assetsLent; - uint256 borrowingPower = assetsCollateral.wMulDown(LLTV); - uint256 assetsBorrowed = borrowingPower.wMulDown(0.8e18); - uint256 toSeize = assetsCollateral; - uint256 liquidationIncentiveFactor = - UtilsLib.min(MAX_LIQUIDATION_INCENTIVE_FACTOR, WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - LLTV))); - - borrowableToken.setBalance(address(this), assetsLent); - collateralToken.setBalance(BORROWER, assetsCollateral); - borrowableToken.setBalance(LIQUIDATOR, assetsBorrowed); - - // Supply - morpho.supply(market, assetsLent, 0, address(this), hex""); - - // Borrow - vm.startPrank(BORROWER); - morpho.supplyCollateral(market, assetsCollateral, BORROWER, hex""); - morpho.borrow(market, assetsBorrowed, 0, BORROWER, BORROWER); - vm.stopPrank(); - - // Price change - oracle.setPrice(ORACLE_PRICE_SCALE / 100); - - uint256 liquidatorNetWorthBefore = netWorth(LIQUIDATOR); - - // Liquidate - vm.prank(LIQUIDATOR); - (uint256 assetsRepaid,) = morpho.liquidate(market, BORROWER, toSeize, hex""); - - uint256 liquidatorNetWorthAfter = netWorth(LIQUIDATOR); - uint256 collateralPrice = IOracle(market.oracle).price(); - - uint256 expectedRepaid = - toSeize.mulDivUp(collateralPrice, ORACLE_PRICE_SCALE).wDivUp(liquidationIncentiveFactor); - uint256 expectedNetWorthAfter = - liquidatorNetWorthBefore + toSeize.mulDivDown(collateralPrice, ORACLE_PRICE_SCALE) - expectedRepaid; - assertEq(assetsRepaid, expectedRepaid, "wrong return repaid value"); - assertEq(liquidatorNetWorthAfter, expectedNetWorthAfter, "LIQUIDATOR net worth"); - assertEq(borrowBalance(BORROWER), 0, "BORROWER balance"); - assertEq(morpho.collateral(id, BORROWER), 0, "BORROWER collateral"); - uint256 expectedBadDebt = assetsBorrowed - expectedRepaid; - assertGt(expectedBadDebt, 0, "bad debt"); - assertApproxEqAbs(supplyBalance(address(this)), assetsLent - expectedBadDebt, 10, "lender supply balance"); - assertApproxEqAbs(morpho.totalBorrow(id), 0, 10, "total borrow"); - } - - function testTwoUsersSupply(uint256 firstAmount, uint256 secondAmount) public { - firstAmount = bound(firstAmount, 1, 2 ** 64); - secondAmount = bound(secondAmount, 1, 2 ** 64); - - borrowableToken.setBalance(address(this), firstAmount); - morpho.supply(market, firstAmount, 0, address(this), hex""); - - borrowableToken.setBalance(BORROWER, secondAmount); - vm.prank(BORROWER); - morpho.supply(market, secondAmount, 0, BORROWER, hex""); - - assertApproxEqAbs(supplyBalance(address(this)), firstAmount, 100, "same balance first user"); - assertEq( - morpho.supplyShares(id, address(this)), - firstAmount * SharesMathLib.VIRTUAL_SHARES, - "expected shares first user" - ); - assertApproxEqAbs(supplyBalance(BORROWER), secondAmount, 100, "same balance second user"); - assertApproxEqAbs( - morpho.supplyShares(id, BORROWER), - secondAmount * SharesMathLib.VIRTUAL_SHARES, - 100, - "expected shares second user" - ); - } - - function testUnknownMarket(Market memory marketFuzz) public { - vm.assume(neq(marketFuzz, market)); - - vm.expectRevert(bytes(ErrorsLib.MARKET_NOT_CREATED)); - morpho.supply(marketFuzz, 1, 0, address(this), hex""); - - vm.expectRevert(bytes(ErrorsLib.MARKET_NOT_CREATED)); - morpho.withdraw(marketFuzz, 1, 0, address(this), address(this)); - - vm.expectRevert(bytes(ErrorsLib.MARKET_NOT_CREATED)); - morpho.borrow(marketFuzz, 1, 0, address(this), address(this)); - - vm.expectRevert(bytes(ErrorsLib.MARKET_NOT_CREATED)); - morpho.repay(marketFuzz, 1, 0, address(this), hex""); - - vm.expectRevert(bytes(ErrorsLib.MARKET_NOT_CREATED)); - morpho.supplyCollateral(marketFuzz, 1, address(this), hex""); - - vm.expectRevert(bytes(ErrorsLib.MARKET_NOT_CREATED)); - morpho.withdrawCollateral(marketFuzz, 1, address(this), address(this)); - - vm.expectRevert(bytes(ErrorsLib.MARKET_NOT_CREATED)); - morpho.liquidate(marketFuzz, address(0), 1, hex""); - } - - function testInputZero() public { - vm.expectRevert(bytes(ErrorsLib.INCONSISTENT_INPUT)); - morpho.supply(market, 0, 0, address(this), hex""); - vm.expectRevert(bytes(ErrorsLib.INCONSISTENT_INPUT)); - morpho.supply(market, 1, 1, address(this), hex""); - - vm.expectRevert(bytes(ErrorsLib.INCONSISTENT_INPUT)); - morpho.withdraw(market, 0, 0, address(this), address(this)); - vm.expectRevert(bytes(ErrorsLib.INCONSISTENT_INPUT)); - morpho.withdraw(market, 1, 1, address(this), address(this)); - - vm.expectRevert(bytes(ErrorsLib.INCONSISTENT_INPUT)); - morpho.borrow(market, 0, 0, address(this), address(this)); - vm.expectRevert(bytes(ErrorsLib.INCONSISTENT_INPUT)); - morpho.borrow(market, 1, 1, address(this), address(this)); - - vm.expectRevert(bytes(ErrorsLib.INCONSISTENT_INPUT)); - morpho.repay(market, 0, 0, address(this), hex""); - vm.expectRevert(bytes(ErrorsLib.INCONSISTENT_INPUT)); - morpho.repay(market, 1, 1, address(this), hex""); - - vm.expectRevert(bytes(ErrorsLib.ZERO_ASSETS)); - morpho.supplyCollateral(market, 0, address(this), hex""); - - vm.expectRevert(bytes(ErrorsLib.ZERO_ASSETS)); - morpho.withdrawCollateral(market, 0, address(this), address(this)); - - vm.expectRevert(bytes(ErrorsLib.ZERO_ASSETS)); - morpho.liquidate(market, address(0), 0, hex""); - } - - function testZeroAddress() public { - vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); - morpho.supply(market, 0, 1, address(0), hex""); - - vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); - morpho.withdraw(market, 0, 1, address(this), address(0)); - - vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); - morpho.borrow(market, 0, 1, address(this), address(0)); - - vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); - morpho.repay(market, 0, 1, address(0), hex""); - - vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); - morpho.supplyCollateral(market, 1, address(0), hex""); - - vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); - morpho.withdrawCollateral(market, 1, address(this), address(0)); - } - - function testEmptyMarket(uint256 assets) public { - assets = bound(assets, 1, type(uint256).max / SharesMathLib.VIRTUAL_SHARES); - - vm.expectRevert(stdError.arithmeticError); - morpho.withdraw(market, assets, 0, address(this), address(this)); - - vm.expectRevert(stdError.arithmeticError); - morpho.repay(market, assets, 0, address(this), hex""); - - vm.expectRevert(stdError.arithmeticError); - morpho.withdrawCollateral(market, assets, address(this), address(this)); - } - - function testAccrueInterestsLowShares() public { - uint256 shares = 1e18; - uint256 assets = 1; - - vm.prank(OWNER); - morpho.setFee(market, MAX_FEE); - - // Have a low total supply shares. - borrowableToken.setBalance(address(this), assets); - morpho.supply(market, assets, 0, address(this), hex""); - morpho.withdraw(market, 0, shares - 1, address(this), address(this)); - - // Borrow to have non zero interests. - uint256 collateralAmount = assets.wDivUp(LLTV); - collateralToken.setBalance(address(this), collateralAmount); - morpho.supplyCollateral(market, collateralAmount, BORROWER, hex""); - vm.prank(BORROWER); - morpho.borrow(market, assets, 0, BORROWER, BORROWER); - - vm.warp(2 * 365 days); - morpho.accrueInterests(market); - - assertGt(morpho.supplyShares(id, morpho.feeRecipient()), 0, "recipient shares"); - } - - function testSetAuthorization(address authorized, bool isAuthorized) public { - morpho.setAuthorization(authorized, isAuthorized); - assertEq(morpho.isAuthorized(address(this), authorized), isAuthorized); - } - - function testNotAuthorized(address attacker) public { - vm.assume(attacker != address(this)); - - vm.startPrank(attacker); - - vm.expectRevert(bytes(ErrorsLib.UNAUTHORIZED)); - morpho.withdraw(market, 0, 1, address(this), address(this)); - vm.expectRevert(bytes(ErrorsLib.UNAUTHORIZED)); - morpho.withdrawCollateral(market, 1, address(this), address(this)); - vm.expectRevert(bytes(ErrorsLib.UNAUTHORIZED)); - morpho.borrow(market, 0, 1, address(this), address(this)); - - vm.stopPrank(); - } - - function testAuthorization(address authorized) public { - borrowableToken.setBalance(address(this), 100 ether); - collateralToken.setBalance(address(this), 100 ether); - - morpho.supply(market, 100 ether, 0, address(this), hex""); - morpho.supplyCollateral(market, 100 ether, address(this), hex""); - - morpho.setAuthorization(authorized, true); - - vm.startPrank(authorized); - - morpho.withdraw(market, 1 ether, 0, address(this), address(this)); - morpho.withdrawCollateral(market, 1 ether, address(this), address(this)); - morpho.borrow(market, 1 ether, 0, address(this), address(this)); - - vm.stopPrank(); - } - - function testAuthorizationWithSig(Authorization memory authorization, uint256 privateKey) public { - vm.assume(authorization.deadline > block.timestamp); - - // Private key must be less than the secp256k1 curve order. - privateKey = bound(privateKey, 1, type(uint32).max); - authorization.nonce = 0; - authorization.authorizer = vm.addr(privateKey); - - Signature memory sig; - bytes32 digest = SigUtils.getTypedDataHash(morpho.DOMAIN_SEPARATOR(), authorization); - (sig.v, sig.r, sig.s) = vm.sign(privateKey, digest); - - morpho.setAuthorizationWithSig(authorization, sig); - - assertEq(morpho.isAuthorized(authorization.authorizer, authorization.authorized), authorization.isAuthorized); - assertEq(morpho.nonce(authorization.authorizer), 1); - } - - function testAuthorizationWithSigWrongPK(Authorization memory authorization, uint256 privateKey) public { - vm.assume(authorization.deadline > block.timestamp); - - // Private key must be less than the secp256k1 curve order. - privateKey = bound(privateKey, 1, type(uint32).max); - authorization.nonce = 0; - - Signature memory sig; - bytes32 digest = SigUtils.getTypedDataHash(morpho.DOMAIN_SEPARATOR(), authorization); - (sig.v, sig.r, sig.s) = vm.sign(privateKey, digest); - - vm.expectRevert(bytes(ErrorsLib.INVALID_SIGNATURE)); - morpho.setAuthorizationWithSig(authorization, sig); - } - - function testAuthorizationWithSigWrongNonce(Authorization memory authorization, uint256 privateKey) public { - vm.assume(authorization.deadline > block.timestamp); - vm.assume(authorization.nonce != 0); - - // Private key must be less than the secp256k1 curve order. - privateKey = bound(privateKey, 1, type(uint32).max); - authorization.authorizer = vm.addr(privateKey); - - Signature memory sig; - bytes32 digest = SigUtils.getTypedDataHash(morpho.DOMAIN_SEPARATOR(), authorization); - (sig.v, sig.r, sig.s) = vm.sign(privateKey, digest); - - vm.expectRevert(bytes(ErrorsLib.INVALID_NONCE)); - morpho.setAuthorizationWithSig(authorization, sig); - } - - function testAuthorizationWithSigDeadline(Authorization memory authorization, uint256 privateKey) public { - vm.assume(authorization.deadline <= block.timestamp); - - // Private key must be less than the secp256k1 curve order. - privateKey = bound(privateKey, 1, type(uint32).max); - authorization.nonce = 0; - authorization.authorizer = vm.addr(privateKey); - - Signature memory sig; - bytes32 digest = SigUtils.getTypedDataHash(morpho.DOMAIN_SEPARATOR(), authorization); - (sig.v, sig.r, sig.s) = vm.sign(privateKey, digest); - - vm.expectRevert(bytes(ErrorsLib.SIGNATURE_EXPIRED)); - morpho.setAuthorizationWithSig(authorization, sig); - } - - function testFlashLoan(uint256 assets) public { - assets = bound(assets, 1, 2 ** 64); - - borrowableToken.setBalance(address(this), assets); - morpho.supply(market, assets, 0, address(this), hex""); - - morpho.flashLoan(address(borrowableToken), assets, bytes("")); - - assertEq(borrowableToken.balanceOf(address(morpho)), assets, "balanceOf"); - } - - function testExtsLoad(uint256 slot, bytes32 value0) public { - bytes32[] memory slots = new bytes32[](2); - slots[0] = bytes32(slot); - slots[1] = bytes32(slot / 2); - - bytes32 value1 = keccak256(abi.encode(value0)); - vm.store(address(morpho), slots[0], value0); - vm.store(address(morpho), slots[1], value1); - - bytes32[] memory values = morpho.extsload(slots); - - assertEq(values.length, 2, "values.length"); - assertEq(values[0], slot > 0 ? value0 : value1, "value0"); - assertEq(values[1], value1, "value1"); - } - - function testSupplyCallback(uint256 assets) public { - assets = bound(assets, 1, 2 ** 64); - borrowableToken.setBalance(address(this), assets); - borrowableToken.approve(address(morpho), 0); - - vm.expectRevert(); - morpho.supply(market, assets, 0, address(this), hex""); - morpho.supply(market, assets, 0, address(this), abi.encode(this.testSupplyCallback.selector, hex"")); - } - - function testSupplyCollateralCallback(uint256 assets) public { - assets = bound(assets, 1, 2 ** 64); - collateralToken.setBalance(address(this), assets); - collateralToken.approve(address(morpho), 0); - - vm.expectRevert(); - morpho.supplyCollateral(market, assets, address(this), hex""); - morpho.supplyCollateral( - market, assets, address(this), abi.encode(this.testSupplyCollateralCallback.selector, hex"") - ); - } - - function testRepayCallback(uint256 assets) public { - assets = bound(assets, 1, 2 ** 64); - - borrowableToken.setBalance(address(this), assets); - morpho.supply(market, assets, 0, address(this), hex""); - - uint256 collateralAmount = assets.wDivUp(LLTV); - collateralToken.setBalance(address(this), collateralAmount); - morpho.supplyCollateral(market, collateralAmount, address(this), hex""); - morpho.borrow(market, assets, 0, address(this), address(this)); - - borrowableToken.approve(address(morpho), 0); - - vm.expectRevert(bytes(ErrorsLib.TRANSFER_FROM_FAILED)); - morpho.repay(market, assets, 0, address(this), hex""); - morpho.repay(market, assets, 0, address(this), abi.encode(this.testRepayCallback.selector, hex"")); - } - - function testLiquidateCallback(uint256 assets) public { - assets = bound(assets, 10, 2 ** 64); - - borrowableToken.setBalance(address(this), assets); - morpho.supply(market, assets, 0, address(this), hex""); - - uint256 collateralAmount = assets.wDivUp(LLTV); - collateralToken.setBalance(address(this), collateralAmount); - morpho.supplyCollateral(market, collateralAmount, address(this), hex""); - morpho.borrow(market, assets.wMulDown(LLTV), 0, address(this), address(this)); - - oracle.setPrice(ORACLE_PRICE_SCALE / 2); - - borrowableToken.setBalance(address(this), assets); - borrowableToken.approve(address(morpho), 0); - vm.expectRevert(bytes(ErrorsLib.TRANSFER_FROM_FAILED)); - morpho.liquidate(market, address(this), collateralAmount, hex""); - morpho.liquidate( - market, address(this), collateralAmount, abi.encode(this.testLiquidateCallback.selector, hex"") - ); - } - - function testFlashActions(uint256 assets) public { - assets = bound(assets, 10, 2 ** 64); - oracle.setPrice(ORACLE_PRICE_SCALE); - uint256 toBorrow = assets.wMulDown(LLTV); - - borrowableToken.setBalance(address(this), 2 * toBorrow); - morpho.supply(market, toBorrow, 0, address(this), hex""); - - morpho.supplyCollateral( - market, assets, address(this), abi.encode(this.testFlashActions.selector, abi.encode(toBorrow)) - ); - assertGt(morpho.borrowShares(market.id(), address(this)), 0, "no borrow"); - - morpho.repay( - market, - 0, - morpho.borrowShares(id, address(this)), - address(this), - abi.encode(this.testFlashActions.selector, abi.encode(assets)) - ); - assertEq(morpho.collateral(market.id(), address(this)), 0, "no withdraw collateral"); - } - - // Callback functions. - - function onMorphoSupply(uint256 assets, bytes memory data) external { - require(msg.sender == address(morpho)); - bytes4 selector; - (selector, data) = abi.decode(data, (bytes4, bytes)); - if (selector == this.testSupplyCallback.selector) { - borrowableToken.approve(address(morpho), assets); - } - } - - function onMorphoSupplyCollateral(uint256 assets, bytes memory data) external { - require(msg.sender == address(morpho)); - bytes4 selector; - (selector, data) = abi.decode(data, (bytes4, bytes)); - if (selector == this.testSupplyCollateralCallback.selector) { - collateralToken.approve(address(morpho), assets); - } else if (selector == this.testFlashActions.selector) { - uint256 toBorrow = abi.decode(data, (uint256)); - collateralToken.setBalance(address(this), assets); - borrowableToken.setBalance(address(this), toBorrow); - morpho.borrow(market, toBorrow, 0, address(this), address(this)); - } - } - - function onMorphoRepay(uint256 assets, bytes memory data) external { - require(msg.sender == address(morpho)); - bytes4 selector; - (selector, data) = abi.decode(data, (bytes4, bytes)); - if (selector == this.testRepayCallback.selector) { - borrowableToken.approve(address(morpho), assets); - } else if (selector == this.testFlashActions.selector) { - uint256 toWithdraw = abi.decode(data, (uint256)); - morpho.withdrawCollateral(market, toWithdraw, address(this), address(this)); - } - } - - function onMorphoLiquidate(uint256 repaid, bytes memory data) external { - require(msg.sender == address(morpho)); - bytes4 selector; - (selector, data) = abi.decode(data, (bytes4, bytes)); - if (selector == this.testLiquidateCallback.selector) { - borrowableToken.approve(address(morpho), repaid); - } - } - - function onMorphoFlashLoan(uint256 assets, bytes calldata) external { - borrowableToken.approve(address(morpho), assets); - } -} - -function neq(Market memory a, Market memory b) pure returns (bool) { - return a.borrowableToken != b.borrowableToken || a.collateralToken != b.collateralToken || a.oracle != b.oracle - || a.lltv != b.lltv || a.irm != b.irm; -} diff --git a/test/forge/integration/TestIntegrationAccrueInterests.t.sol b/test/forge/integration/TestIntegrationAccrueInterests.t.sol new file mode 100644 index 000000000..c11be8ae0 --- /dev/null +++ b/test/forge/integration/TestIntegrationAccrueInterests.t.sol @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "../BaseTest.sol"; + +contract IntegrationAccrueInterestsTest is BaseTest { + using MathLib for uint256; + using SharesMathLib for uint256; + + function testAccrueInterestsNoTimeElapsed(uint256 amountSupplied, uint256 amountBorrowed) public { + amountSupplied = bound(amountSupplied, 2, MAX_TEST_AMOUNT); + amountBorrowed = bound(amountBorrowed, 1, amountSupplied); + + // Set fee parameters. + vm.prank(OWNER); + morpho.setFeeRecipient(OWNER); + + borrowableToken.setBalance(address(this), amountSupplied); + morpho.supply(market, amountSupplied, 0, address(this), hex""); + + uint256 collateralPrice = IOracle(market.oracle).price(); + collateralToken.setBalance(BORROWER, amountBorrowed.wDivUp(LLTV).mulDivUp(ORACLE_PRICE_SCALE, collateralPrice)); + + vm.startPrank(BORROWER); + morpho.supplyCollateral( + market, amountBorrowed.wDivUp(LLTV).mulDivUp(ORACLE_PRICE_SCALE, collateralPrice), BORROWER, hex"" + ); + morpho.borrow(market, amountBorrowed, 0, BORROWER, BORROWER); + vm.stopPrank(); + + uint256 totalBorrowBeforeAccrued = morpho.totalBorrow(id); + uint256 totalSupplyBeforeAccrued = morpho.totalSupply(id); + uint256 totalSupplySharesBeforeAccrued = morpho.totalSupplyShares(id); + + morpho.accrueInterests(market); + + assertEq(morpho.totalBorrow(id), totalBorrowBeforeAccrued, "total borrow"); + assertEq(morpho.totalSupply(id), totalSupplyBeforeAccrued, "total supply"); + assertEq(morpho.totalSupplyShares(id), totalSupplySharesBeforeAccrued, "total supply shares"); + assertEq(morpho.supplyShares(id, OWNER), 0, "feeRecipient's supply shares"); + } + + function testAccrueInterestsNoBorrow(uint256 amountSupplied, uint256 timeElapsed) public { + amountSupplied = bound(amountSupplied, 2, MAX_TEST_AMOUNT); + timeElapsed = uint32(bound(timeElapsed, 1, type(uint32).max)); + + // Set fee parameters. + vm.prank(OWNER); + morpho.setFeeRecipient(OWNER); + + borrowableToken.setBalance(address(this), amountSupplied); + morpho.supply(market, amountSupplied, 0, address(this), hex""); + + // New block. + vm.roll(block.number + 1); + vm.warp(block.timestamp + timeElapsed); + + uint256 totalBorrowBeforeAccrued = morpho.totalBorrow(id); + uint256 totalSupplyBeforeAccrued = morpho.totalSupply(id); + uint256 totalSupplySharesBeforeAccrued = morpho.totalSupplyShares(id); + + morpho.accrueInterests(market); + + assertEq(morpho.totalBorrow(id), totalBorrowBeforeAccrued, "total borrow"); + assertEq(morpho.totalSupply(id), totalSupplyBeforeAccrued, "total supply"); + assertEq(morpho.totalSupplyShares(id), totalSupplySharesBeforeAccrued, "total supply shares"); + assertEq(morpho.supplyShares(id, OWNER), 0, "feeRecipient's supply shares"); + assertEq(morpho.lastUpdate(id), block.timestamp, "last update"); + } + + function testAccrueInterestNoFee(uint256 amountSupplied, uint256 amountBorrowed, uint256 timeElapsed) public { + amountSupplied = bound(amountSupplied, 2, MAX_TEST_AMOUNT); + amountBorrowed = bound(amountBorrowed, 1, amountSupplied); + timeElapsed = uint32(bound(timeElapsed, 1, type(uint32).max)); + + // Set fee parameters. + vm.prank(OWNER); + morpho.setFeeRecipient(OWNER); + + borrowableToken.setBalance(address(this), amountSupplied); + borrowableToken.setBalance(address(this), amountSupplied); + morpho.supply(market, amountSupplied, 0, address(this), hex""); + + uint256 collateralPrice = IOracle(market.oracle).price(); + collateralToken.setBalance(BORROWER, amountBorrowed.wDivUp(LLTV).mulDivUp(ORACLE_PRICE_SCALE, collateralPrice)); + + vm.startPrank(BORROWER); + morpho.supplyCollateral( + market, amountBorrowed.wDivUp(LLTV).mulDivUp(ORACLE_PRICE_SCALE, collateralPrice), BORROWER, hex"" + ); + + morpho.borrow(market, amountBorrowed, 0, BORROWER, BORROWER); + vm.stopPrank(); + + // New block. + vm.roll(block.number + 1); + vm.warp(block.timestamp + timeElapsed); + + uint256 borrowRate = (morpho.totalBorrow(id).wDivDown(morpho.totalSupply(id))) / 365 days; + uint256 totalBorrowBeforeAccrued = morpho.totalBorrow(id); + uint256 totalSupplyBeforeAccrued = morpho.totalSupply(id); + uint256 totalSupplySharesBeforeAccrued = morpho.totalSupplyShares(id); + uint256 expectedAccruedInterests = totalBorrowBeforeAccrued.wMulDown(borrowRate.wTaylorCompounded(timeElapsed)); + + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.AccrueInterests(id, borrowRate, expectedAccruedInterests, 0); + morpho.accrueInterests(market); + + assertEq(morpho.totalBorrow(id), totalBorrowBeforeAccrued + expectedAccruedInterests, "total borrow"); + assertEq(morpho.totalSupply(id), totalSupplyBeforeAccrued + expectedAccruedInterests, "total supply"); + assertEq(morpho.totalSupplyShares(id), totalSupplySharesBeforeAccrued, "total supply shares"); + assertEq(morpho.supplyShares(id, OWNER), 0, "feeRecipient's supply shares"); + assertEq(morpho.lastUpdate(id), block.timestamp, "last update"); + } + + struct AccrueInterestWithFeesTestParams { + uint256 borrowRate; + uint256 totalBorrowBeforeAccrued; + uint256 totalSupplyBeforeAccrued; + uint256 totalSupplySharesBeforeAccrued; + uint256 expectedAccruedInterests; + uint256 feeAmount; + uint256 feeShares; + } + + function testAccrueInterestWithFees( + uint256 amountSupplied, + uint256 amountBorrowed, + uint256 timeElapsed, + uint256 fee + ) public { + AccrueInterestWithFeesTestParams memory params; + + amountSupplied = bound(amountSupplied, 2, MAX_TEST_AMOUNT); + amountBorrowed = bound(amountBorrowed, 1, amountSupplied); + timeElapsed = uint32(bound(timeElapsed, 1, 1e8)); + fee = bound(fee, 1, MAX_FEE); + + // Set fee parameters. + vm.startPrank(OWNER); + morpho.setFeeRecipient(OWNER); + morpho.setFee(market, fee); + vm.stopPrank(); + + borrowableToken.setBalance(address(this), amountSupplied); + morpho.supply(market, amountSupplied, 0, address(this), hex""); + + uint256 collateralPrice = IOracle(market.oracle).price(); + collateralToken.setBalance(BORROWER, amountBorrowed.wDivUp(LLTV).mulDivUp(ORACLE_PRICE_SCALE, collateralPrice)); + + vm.startPrank(BORROWER); + morpho.supplyCollateral( + market, amountBorrowed.wDivUp(LLTV).mulDivUp(ORACLE_PRICE_SCALE, collateralPrice), BORROWER, hex"" + ); + morpho.borrow(market, amountBorrowed, 0, BORROWER, BORROWER); + vm.stopPrank(); + + // New block. + vm.roll(block.number + 1); + vm.warp(block.timestamp + timeElapsed); + + params.borrowRate = (morpho.totalBorrow(id).wDivDown(morpho.totalSupply(id))) / 365 days; + params.totalBorrowBeforeAccrued = morpho.totalBorrow(id); + params.totalSupplyBeforeAccrued = morpho.totalSupply(id); + params.totalSupplySharesBeforeAccrued = morpho.totalSupplyShares(id); + params.expectedAccruedInterests = + params.totalBorrowBeforeAccrued.wMulDown(params.borrowRate.wTaylorCompounded(timeElapsed)); + params.feeAmount = params.expectedAccruedInterests.wMulDown(fee); + params.feeShares = params.feeAmount.toSharesDown( + params.totalSupplyBeforeAccrued + params.expectedAccruedInterests - params.feeAmount, + params.totalSupplySharesBeforeAccrued + ); + + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.AccrueInterests(id, params.borrowRate, params.expectedAccruedInterests, params.feeShares); + morpho.accrueInterests(market); + + assertEq( + morpho.totalBorrow(id), params.totalBorrowBeforeAccrued + params.expectedAccruedInterests, "total borrow" + ); + assertEq( + morpho.totalSupply(id), params.totalSupplyBeforeAccrued + params.expectedAccruedInterests, "total supply" + ); + assertEq( + morpho.totalSupplyShares(id), + params.totalSupplySharesBeforeAccrued + params.feeShares, + "total supply shares" + ); + assertEq(morpho.supplyShares(id, OWNER), params.feeShares, "feeRecipient's supply shares"); + assertEq(morpho.lastUpdate(id), block.timestamp, "last update"); + } +} diff --git a/test/forge/integration/TestIntegrationBorrow.t.sol b/test/forge/integration/TestIntegrationBorrow.t.sol new file mode 100644 index 000000000..608d02a38 --- /dev/null +++ b/test/forge/integration/TestIntegrationBorrow.t.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "../BaseTest.sol"; + +contract IntegrationBorrowTest is BaseTest { + using MathLib for uint256; + using SharesMathLib for uint256; + + function testBorrowMarketNotCreated(Market memory marketFuzz, address borrowerFuzz, uint256 amount) public { + vm.assume(neq(marketFuzz, market)); + + vm.prank(borrowerFuzz); + vm.expectRevert(bytes(ErrorsLib.MARKET_NOT_CREATED)); + morpho.borrow(marketFuzz, amount, 0, borrowerFuzz, RECEIVER); + } + + function testBorrowZeroAmount(address borrowerFuzz) public { + vm.prank(borrowerFuzz); + vm.expectRevert(bytes(ErrorsLib.INCONSISTENT_INPUT)); + morpho.borrow(market, 0, 0, borrowerFuzz, RECEIVER); + } + + function testBorrowInconsistentInput(address borrowerFuzz, uint256 amount, uint256 shares) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + shares = bound(shares, 1, MAX_TEST_SHARES); + + vm.prank(borrowerFuzz); + vm.expectRevert(bytes(ErrorsLib.INCONSISTENT_INPUT)); + morpho.borrow(market, amount, shares, borrowerFuzz, RECEIVER); + } + + function testBorrowToZeroAddress(address borrowerFuzz, uint256 amount) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + _supply(amount); + + vm.prank(borrowerFuzz); + vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); + morpho.borrow(market, amount, 0, borrowerFuzz, address(0)); + } + + function testBorrowUnauthorized(address supplier, address attacker, uint256 amount) public { + vm.assume(supplier != attacker && supplier != address(0)); + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + _supply(amount); + + collateralToken.setBalance(supplier, amount); + + vm.startPrank(supplier); + collateralToken.approve(address(morpho), amount); + morpho.supplyCollateral(market, amount, supplier, hex""); + vm.stopPrank(); + + vm.prank(attacker); + vm.expectRevert(bytes(ErrorsLib.UNAUTHORIZED)); + morpho.borrow(market, amount, 0, supplier, RECEIVER); + } + + function testBorrowUnhealthyPosition( + uint256 amountCollateral, + uint256 amountSupplied, + uint256 amountBorrowed, + uint256 priceCollateral + ) public { + (amountCollateral, amountBorrowed, priceCollateral) = + _boundUnhealthyPosition(amountCollateral, amountBorrowed, priceCollateral); + + amountSupplied = bound(amountSupplied, amountBorrowed, MAX_TEST_AMOUNT); + _supply(amountSupplied); + + oracle.setPrice(priceCollateral); + + collateralToken.setBalance(BORROWER, amountCollateral); + + vm.startPrank(BORROWER); + morpho.supplyCollateral(market, amountCollateral, BORROWER, hex""); + vm.expectRevert(bytes(ErrorsLib.INSUFFICIENT_COLLATERAL)); + morpho.borrow(market, amountBorrowed, 0, BORROWER, BORROWER); + vm.stopPrank(); + } + + function testBorrowUnsufficientLiquidity( + uint256 amountCollateral, + uint256 amountSupplied, + uint256 amountBorrowed, + uint256 priceCollateral + ) public { + (amountCollateral, amountBorrowed, priceCollateral) = + _boundHealthyPosition(amountCollateral, amountBorrowed, priceCollateral); + + amountSupplied = bound(amountSupplied, 1, amountBorrowed - 1); + _supply(amountSupplied); + + oracle.setPrice(priceCollateral); + + collateralToken.setBalance(BORROWER, amountCollateral); + + vm.startPrank(BORROWER); + morpho.supplyCollateral(market, amountCollateral, BORROWER, hex""); + vm.expectRevert(bytes(ErrorsLib.INSUFFICIENT_LIQUIDITY)); + morpho.borrow(market, amountBorrowed, 0, BORROWER, BORROWER); + vm.stopPrank(); + } + + function testBorrowAssets( + uint256 amountCollateral, + uint256 amountSupplied, + uint256 amountBorrowed, + uint256 priceCollateral + ) public { + (amountCollateral, amountBorrowed, priceCollateral) = + _boundHealthyPosition(amountCollateral, amountBorrowed, priceCollateral); + + amountSupplied = bound(amountSupplied, amountBorrowed, MAX_TEST_AMOUNT); + _supply(amountSupplied); + + oracle.setPrice(priceCollateral); + + collateralToken.setBalance(BORROWER, amountCollateral); + + vm.startPrank(BORROWER); + morpho.supplyCollateral(market, amountCollateral, BORROWER, hex""); + + uint256 expectedBorrowShares = amountBorrowed.toSharesUp(0, 0); + + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.Borrow(id, BORROWER, BORROWER, RECEIVER, amountBorrowed, expectedBorrowShares); + (uint256 returnAssets, uint256 returnShares) = morpho.borrow(market, amountBorrowed, 0, BORROWER, RECEIVER); + vm.stopPrank(); + + assertEq(returnAssets, amountBorrowed, "returned asset amount"); + assertEq(returnShares, expectedBorrowShares, "returned shares amount"); + assertEq(morpho.totalBorrow(id), amountBorrowed, "total borrow"); + assertEq(morpho.borrowShares(id, BORROWER), expectedBorrowShares, "borrow shares"); + assertEq(morpho.borrowShares(id, BORROWER), expectedBorrowShares, "total borrow shares"); + assertEq(borrowableToken.balanceOf(RECEIVER), amountBorrowed, "borrower balance"); + assertEq(borrowableToken.balanceOf(address(morpho)), amountSupplied - amountBorrowed, "morpho balance"); + } + + function testBorrowShares( + uint256 amountCollateral, + uint256 amountSupplied, + uint256 sharesBorrowed, + uint256 priceCollateral + ) public { + priceCollateral = bound(priceCollateral, MIN_COLLATERAL_PRICE, MAX_COLLATERAL_PRICE); + sharesBorrowed = bound(sharesBorrowed, MIN_TEST_SHARES, MAX_TEST_SHARES); + uint256 expectedAmountBorrowed = sharesBorrowed.toAssetsDown(0, 0); + + uint256 expectedBorrowedValue = sharesBorrowed.toAssetsUp(expectedAmountBorrowed, sharesBorrowed); + uint256 minCollateral = expectedBorrowedValue.wDivUp(market.lltv).mulDivUp(ORACLE_PRICE_SCALE, priceCollateral); + amountCollateral = bound(amountCollateral, minCollateral, max(minCollateral, MAX_TEST_AMOUNT)); + + amountSupplied = bound(amountSupplied, expectedAmountBorrowed, MAX_TEST_AMOUNT); + _supply(amountSupplied); + + oracle.setPrice(priceCollateral); + + collateralToken.setBalance(BORROWER, amountCollateral); + + vm.startPrank(BORROWER); + morpho.supplyCollateral(market, amountCollateral, BORROWER, hex""); + + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.Borrow(id, BORROWER, BORROWER, RECEIVER, expectedAmountBorrowed, sharesBorrowed); + (uint256 returnAssets, uint256 returnShares) = morpho.borrow(market, 0, sharesBorrowed, BORROWER, RECEIVER); + vm.stopPrank(); + + assertEq(returnAssets, expectedAmountBorrowed, "returned asset amount"); + assertEq(returnShares, sharesBorrowed, "returned shares amount"); + assertEq(morpho.totalBorrow(id), expectedAmountBorrowed, "total borrow"); + assertEq(morpho.borrowShares(id, BORROWER), sharesBorrowed, "borrow shares"); + assertEq(morpho.borrowShares(id, BORROWER), sharesBorrowed, "total borrow shares"); + assertEq(borrowableToken.balanceOf(RECEIVER), expectedAmountBorrowed, "borrower balance"); + assertEq(borrowableToken.balanceOf(address(morpho)), amountSupplied - expectedAmountBorrowed, "morpho balance"); + } + + function testBorrowAssetsOnBehalf( + uint256 amountCollateral, + uint256 amountSupplied, + uint256 amountBorrowed, + uint256 priceCollateral + ) public { + (amountCollateral, amountBorrowed, priceCollateral) = + _boundHealthyPosition(amountCollateral, amountBorrowed, priceCollateral); + + amountSupplied = bound(amountSupplied, amountBorrowed, MAX_TEST_AMOUNT); + _supply(amountSupplied); + + oracle.setPrice(priceCollateral); + + collateralToken.setBalance(ONBEHALF, amountCollateral); + + vm.startPrank(ONBEHALF); + collateralToken.approve(address(morpho), amountCollateral); + morpho.supplyCollateral(market, amountCollateral, ONBEHALF, hex""); + morpho.setAuthorization(BORROWER, true); + vm.stopPrank(); + + uint256 expectedBorrowShares = amountBorrowed.toSharesUp(0, 0); + + vm.prank(BORROWER); + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.Borrow(id, BORROWER, ONBEHALF, RECEIVER, amountBorrowed, expectedBorrowShares); + (uint256 returnAssets, uint256 returnShares) = morpho.borrow(market, amountBorrowed, 0, ONBEHALF, RECEIVER); + + assertEq(returnAssets, amountBorrowed, "returned asset amount"); + assertEq(returnShares, expectedBorrowShares, "returned shares amount"); + assertEq(morpho.borrowShares(id, ONBEHALF), expectedBorrowShares, "borrow shares"); + assertEq(morpho.totalBorrow(id), amountBorrowed, "total borrow"); + assertEq(morpho.totalBorrowShares(id), expectedBorrowShares, "total borrow shares"); + assertEq(borrowableToken.balanceOf(RECEIVER), amountBorrowed, "borrower balance"); + assertEq(borrowableToken.balanceOf(address(morpho)), amountSupplied - amountBorrowed, "morpho balance"); + } + + function testBorrowSharesOnBehalf( + uint256 amountCollateral, + uint256 amountSupplied, + uint256 sharesBorrowed, + uint256 priceCollateral + ) public { + priceCollateral = bound(priceCollateral, MIN_COLLATERAL_PRICE, MAX_COLLATERAL_PRICE); + sharesBorrowed = bound(sharesBorrowed, MIN_TEST_SHARES, MAX_TEST_SHARES); + uint256 expectedAmountBorrowed = sharesBorrowed.toAssetsDown(0, 0); + + uint256 expectedBorrowedValue = sharesBorrowed.toAssetsUp(expectedAmountBorrowed, sharesBorrowed); + uint256 minCollateral = expectedBorrowedValue.wDivUp(market.lltv).mulDivUp(ORACLE_PRICE_SCALE, priceCollateral); + amountCollateral = bound(amountCollateral, minCollateral, max(minCollateral, MAX_TEST_AMOUNT)); + + amountSupplied = bound(amountSupplied, expectedAmountBorrowed, MAX_TEST_AMOUNT); + _supply(amountSupplied); + + oracle.setPrice(priceCollateral); + + collateralToken.setBalance(ONBEHALF, amountCollateral); + + vm.startPrank(ONBEHALF); + collateralToken.approve(address(morpho), amountCollateral); + morpho.supplyCollateral(market, amountCollateral, ONBEHALF, hex""); + morpho.setAuthorization(BORROWER, true); + vm.stopPrank(); + + vm.prank(BORROWER); + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.Borrow(id, BORROWER, ONBEHALF, RECEIVER, expectedAmountBorrowed, sharesBorrowed); + (uint256 returnAssets, uint256 returnShares) = morpho.borrow(market, 0, sharesBorrowed, ONBEHALF, RECEIVER); + + assertEq(returnAssets, expectedAmountBorrowed, "returned asset amount"); + assertEq(returnShares, sharesBorrowed, "returned shares amount"); + assertEq(morpho.borrowShares(id, ONBEHALF), sharesBorrowed, "borrow shares"); + assertEq(morpho.totalBorrow(id), expectedAmountBorrowed, "total borrow"); + assertEq(morpho.totalBorrowShares(id), sharesBorrowed, "total borrow shares"); + assertEq(borrowableToken.balanceOf(RECEIVER), expectedAmountBorrowed, "borrower balance"); + assertEq(borrowableToken.balanceOf(address(morpho)), amountSupplied - expectedAmountBorrowed, "morpho balance"); + } +} diff --git a/test/forge/integration/TestIntegrationCallbacks.t.sol b/test/forge/integration/TestIntegrationCallbacks.t.sol new file mode 100644 index 000000000..88545e6e0 --- /dev/null +++ b/test/forge/integration/TestIntegrationCallbacks.t.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "../BaseTest.sol"; +import "src/interfaces/IMorphoCallbacks.sol"; + +contract IntegrationCallbacksTest is + BaseTest, + IMorphoLiquidateCallback, + IMorphoRepayCallback, + IMorphoSupplyCallback, + IMorphoSupplyCollateralCallback, + IMorphoFlashLoanCallback +{ + using MarketLib for Market; + using MathLib for uint256; + + // Callback functions. + + function onMorphoSupply(uint256 amount, bytes memory data) external { + require(msg.sender == address(morpho)); + bytes4 selector; + (selector, data) = abi.decode(data, (bytes4, bytes)); + if (selector == this.testSupplyCallback.selector) { + borrowableToken.approve(address(morpho), amount); + } + } + + function onMorphoSupplyCollateral(uint256 amount, bytes memory data) external { + require(msg.sender == address(morpho)); + bytes4 selector; + (selector, data) = abi.decode(data, (bytes4, bytes)); + if (selector == this.testSupplyCollateralCallback.selector) { + collateralToken.approve(address(morpho), amount); + } else if (selector == this.testFlashActions.selector) { + uint256 toBorrow = abi.decode(data, (uint256)); + collateralToken.setBalance(address(this), amount); + morpho.borrow(market, toBorrow, 0, address(this), address(this)); + } + } + + function onMorphoRepay(uint256 amount, bytes memory data) external { + require(msg.sender == address(morpho)); + bytes4 selector; + (selector, data) = abi.decode(data, (bytes4, bytes)); + if (selector == this.testRepayCallback.selector) { + borrowableToken.approve(address(morpho), amount); + } else if (selector == this.testFlashActions.selector) { + uint256 toWithdraw = abi.decode(data, (uint256)); + morpho.withdrawCollateral(market, toWithdraw, address(this), address(this)); + } + } + + function onMorphoLiquidate(uint256 repaid, bytes memory data) external { + require(msg.sender == address(morpho)); + bytes4 selector; + (selector, data) = abi.decode(data, (bytes4, bytes)); + if (selector == this.testLiquidateCallback.selector) { + borrowableToken.approve(address(morpho), repaid); + } + } + + function onMorphoFlashLoan(uint256 amount, bytes calldata) external { + borrowableToken.approve(address(morpho), amount); + } + + // Tests. + + function testFlashLoan(uint256 amount) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + borrowableToken.setBalance(address(this), amount); + morpho.supply(market, amount, 0, address(this), hex""); + + morpho.flashLoan(address(borrowableToken), amount, hex""); + + assertEq(borrowableToken.balanceOf(address(morpho)), amount, "balanceOf"); + } + + function testSupplyCallback(uint256 amount) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + borrowableToken.setBalance(address(this), amount); + borrowableToken.approve(address(morpho), 0); + + vm.expectRevert(); + morpho.supply(market, amount, 0, address(this), hex""); + morpho.supply(market, amount, 0, address(this), abi.encode(this.testSupplyCallback.selector, hex"")); + } + + function testSupplyCollateralCallback(uint256 amount) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + collateralToken.setBalance(address(this), amount); + collateralToken.approve(address(morpho), 0); + + vm.expectRevert(); + morpho.supplyCollateral(market, amount, address(this), hex""); + morpho.supplyCollateral( + market, amount, address(this), abi.encode(this.testSupplyCollateralCallback.selector, hex"") + ); + } + + function testRepayCallback(uint256 amount) public { + amount = bound(amount, MIN_COLLATERAL_PRICE, MAX_TEST_AMOUNT); + + oracle.setPrice(WAD); + + borrowableToken.setBalance(address(this), amount); + collateralToken.setBalance(address(this), amount.mulDivUp(ORACLE_PRICE_SCALE, WAD)); + + morpho.supply(market, amount, 0, address(this), hex""); + morpho.supplyCollateral(market, amount.mulDivUp(ORACLE_PRICE_SCALE, WAD), address(this), hex""); + morpho.borrow(market, amount.wMulDown(LLTV), 0, address(this), address(this)); + + borrowableToken.approve(address(morpho), 0); + + vm.expectRevert(); + morpho.repay(market, amount.wMulDown(LLTV), 0, address(this), hex""); + morpho.repay( + market, amount.wMulDown(LLTV), 0, address(this), abi.encode(this.testRepayCallback.selector, hex"") + ); + } + + function testLiquidateCallback(uint256 amount) public { + amount = bound(amount, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT); + + oracle.setPrice(WAD); + + borrowableToken.setBalance(address(this), amount); + collateralToken.setBalance(address(this), amount.mulDivUp(ORACLE_PRICE_SCALE, WAD).wDivUp(market.lltv)); + + morpho.supply(market, amount, 0, address(this), hex""); + morpho.supplyCollateral(market, amount.mulDivUp(ORACLE_PRICE_SCALE, WAD), address(this), hex""); + morpho.borrow(market, amount.wMulDown(LLTV), 0, address(this), address(this)); + + oracle.setPrice(0.99e18); + + uint256 toSeize = amount.wMulDown(LLTV); + + borrowableToken.setBalance(address(this), toSeize); + borrowableToken.approve(address(morpho), 0); + + vm.expectRevert(); + morpho.liquidate(market, address(this), toSeize, hex""); + morpho.liquidate(market, address(this), toSeize, abi.encode(this.testLiquidateCallback.selector, hex"")); + } + + function testFlashActions(uint256 amount) public { + amount = bound(amount, 10, MAX_TEST_AMOUNT); + + oracle.setPrice(WAD); + + uint256 toBorrow = amount.mulDivDown(WAD, ORACLE_PRICE_SCALE).wMulDown(LLTV); + vm.assume(toBorrow != 0); + + borrowableToken.setBalance(address(this), toBorrow); + morpho.supply(market, toBorrow, 0, address(this), hex""); + + morpho.supplyCollateral( + market, amount, address(this), abi.encode(this.testFlashActions.selector, abi.encode(toBorrow)) + ); + assertGt(morpho.borrowShares(market.id(), address(this)), 0, "no borrow"); + + morpho.repay(market, toBorrow, 0, address(this), abi.encode(this.testFlashActions.selector, abi.encode(amount))); + assertEq(morpho.collateral(market.id(), address(this)), 0, "no withdraw collateral"); + } +} diff --git a/test/forge/integration/TestIntegrationCreateMarket.t.sol b/test/forge/integration/TestIntegrationCreateMarket.t.sol new file mode 100644 index 000000000..77252db8a --- /dev/null +++ b/test/forge/integration/TestIntegrationCreateMarket.t.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "../BaseTest.sol"; + +contract IntegrationCreateMarketTest is BaseTest { + using MarketLib for Market; + using MathLib for uint256; + + function testCreateMarketWithNotEnabledIrmAndNotEnabledLltv(Market memory marketFuzz) public { + vm.assume(marketFuzz.irm != address(irm) && marketFuzz.lltv != LLTV); + + vm.prank(OWNER); + vm.expectRevert(bytes(ErrorsLib.IRM_NOT_ENABLED)); + morpho.createMarket(marketFuzz); + } + + function testCreateMarketWithNotEnabledIrmAndEnabledLltv(Market memory marketFuzz) public { + vm.assume(marketFuzz.irm != address(irm)); + marketFuzz.lltv = _boundValidLltv(marketFuzz.lltv); + + vm.startPrank(OWNER); + + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.EnableLltv(marketFuzz.lltv); + morpho.enableLltv(marketFuzz.lltv); + + vm.expectRevert(bytes(ErrorsLib.IRM_NOT_ENABLED)); + morpho.createMarket(marketFuzz); + vm.stopPrank(); + } + + function testCreateMarketWithEnabledIrmAndNotEnabledLltv(Market memory marketFuzz) public { + vm.assume(marketFuzz.lltv != LLTV); + + vm.startPrank(OWNER); + + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.EnableIrm(marketFuzz.irm); + morpho.enableIrm(marketFuzz.irm); + + vm.expectRevert(bytes(ErrorsLib.LLTV_NOT_ENABLED)); + morpho.createMarket(marketFuzz); + vm.stopPrank(); + } + + function testCreateMarketWithEnabledIrmAndLltv(Market memory marketFuzz) public { + marketFuzz.lltv = LLTV; + Id marketFuzzId = marketFuzz.id(); + + vm.prank(OWNER); + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.EnableIrm(marketFuzz.irm); + morpho.enableIrm(marketFuzz.irm); + + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.CreateMarket(marketFuzz.id(), marketFuzz); + morpho.createMarket(marketFuzz); + + assertEq(morpho.lastUpdate(marketFuzzId), block.timestamp, "lastUpdate != block.timestamp"); + assertEq(morpho.totalSupply(marketFuzzId), 0, "totalSupply != 0"); + assertEq(morpho.totalSupplyShares(marketFuzzId), 0, "totalSupplyShares != 0"); + assertEq(morpho.totalBorrow(marketFuzzId), 0, "totalBorrow != 0"); + assertEq(morpho.totalBorrowShares(marketFuzzId), 0, "totalBorrowShares != 0"); + assertEq(morpho.fee(marketFuzzId), 0, "fee != 0"); + } +} diff --git a/test/forge/integration/TestIntegrationGetter.t.sol b/test/forge/integration/TestIntegrationGetter.t.sol new file mode 100644 index 000000000..b3705b83f --- /dev/null +++ b/test/forge/integration/TestIntegrationGetter.t.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "../BaseTest.sol"; + +contract IntegrationGetterTest is BaseTest { + function testExtsLoad(uint256 slot, bytes32 value0) public { + bytes32[] memory slots = new bytes32[](2); + slots[0] = bytes32(slot); + slots[1] = bytes32(slot / 2); + + bytes32 value1 = keccak256(abi.encode(value0)); + vm.store(address(morpho), slots[0], value0); + vm.store(address(morpho), slots[1], value1); + + bytes32[] memory values = morpho.extsload(slots); + + assertEq(values.length, 2, "values.length"); + assertEq(values[0], slot > 0 ? value0 : value1, "value0"); + assertEq(values[1], value1, "value1"); + } +} diff --git a/test/forge/integration/TestIntegrationLiquidate.t.sol b/test/forge/integration/TestIntegrationLiquidate.t.sol new file mode 100644 index 000000000..f8ec5c78a --- /dev/null +++ b/test/forge/integration/TestIntegrationLiquidate.t.sol @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "../BaseTest.sol"; + +contract IntegrationLiquidateTest is BaseTest { + using MathLib for uint256; + using SharesMathLib for uint256; + + function testLiquidateNotCreatedMarket(Market memory marketFuzz) public { + vm.assume(neq(marketFuzz, market)); + + vm.expectRevert(bytes(ErrorsLib.MARKET_NOT_CREATED)); + morpho.liquidate(marketFuzz, address(this), 1, hex""); + } + + function testLiquidateZeroAmount() public { + vm.prank(BORROWER); + + vm.expectRevert(bytes(ErrorsLib.ZERO_ASSETS)); + morpho.liquidate(market, address(this), 0, hex""); + } + + function testLiquidateHealthyPosition( + uint256 amountCollateral, + uint256 amountSupplied, + uint256 amountBorrowed, + uint256 amountSeized, + uint256 priceCollateral + ) public { + (amountCollateral, amountBorrowed, priceCollateral) = + _boundHealthyPosition(amountCollateral, amountBorrowed, priceCollateral); + + amountSupplied = bound(amountSupplied, amountBorrowed, MAX_TEST_AMOUNT); + _supply(amountSupplied); + + amountSeized = bound(amountSeized, 1, amountCollateral); + + oracle.setPrice(priceCollateral); + + borrowableToken.setBalance(LIQUIDATOR, amountBorrowed); + collateralToken.setBalance(BORROWER, amountCollateral); + + vm.startPrank(BORROWER); + morpho.supplyCollateral(market, amountCollateral, BORROWER, hex""); + morpho.borrow(market, amountBorrowed, 0, BORROWER, BORROWER); + vm.stopPrank(); + + vm.prank(LIQUIDATOR); + vm.expectRevert(bytes(ErrorsLib.HEALTHY_POSITION)); + morpho.liquidate(market, BORROWER, amountSeized, hex""); + } + + function testLiquidateNoBadDebt( + uint256 amountCollateral, + uint256 amountSupplied, + uint256 amountBorrowed, + uint256 amountSeized, + uint256 priceCollateral + ) public { + (amountCollateral, amountBorrowed, priceCollateral) = + _boundUnhealthyPosition(amountCollateral, amountBorrowed, priceCollateral); + + vm.assume(amountCollateral > 1); + + amountSupplied = bound(amountSupplied, amountBorrowed, MAX_TEST_AMOUNT); + _supply(amountSupplied); + + uint256 incentive = _liquidationIncentive(market.lltv); + uint256 maxSeized = amountBorrowed.wMulDown(incentive).mulDivDown(ORACLE_PRICE_SCALE, priceCollateral); + amountSeized = bound(amountSeized, 1, min(maxSeized, amountCollateral - 1)); + uint256 expectedRepaid = amountSeized.mulDivUp(priceCollateral, ORACLE_PRICE_SCALE).wDivUp(incentive); + + borrowableToken.setBalance(LIQUIDATOR, amountBorrowed); + collateralToken.setBalance(BORROWER, amountCollateral); + + oracle.setPrice(type(uint256).max / amountCollateral); + + vm.startPrank(BORROWER); + morpho.supplyCollateral(market, amountCollateral, BORROWER, hex""); + morpho.borrow(market, amountBorrowed, 0, BORROWER, BORROWER); + vm.stopPrank(); + + oracle.setPrice(priceCollateral); + + uint256 expectedRepaidShares = expectedRepaid.toSharesDown(morpho.totalBorrow(id), morpho.totalBorrowShares(id)); + + vm.prank(LIQUIDATOR); + + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.Liquidate(id, LIQUIDATOR, BORROWER, expectedRepaid, expectedRepaidShares, amountSeized, 0); + (uint256 returnRepaid, uint256 returnRepaidShares) = morpho.liquidate(market, BORROWER, amountSeized, hex""); + + uint256 expectedBorrowShares = amountBorrowed.toSharesUp(0, 0) - expectedRepaidShares; + + assertEq(returnRepaid, expectedRepaid, "returned asset amount"); + assertEq(returnRepaidShares, expectedRepaidShares, "returned shares amount"); + assertEq(morpho.borrowShares(id, BORROWER), expectedBorrowShares, "borrow shares"); + assertEq(morpho.totalBorrow(id), amountBorrowed - expectedRepaid, "total borrow"); + assertEq(morpho.totalBorrowShares(id), expectedBorrowShares, "total borrow shares"); + assertEq(morpho.collateral(id, BORROWER), amountCollateral - amountSeized, "collateral"); + assertEq(borrowableToken.balanceOf(BORROWER), amountBorrowed, "borrower balance"); + assertEq(borrowableToken.balanceOf(LIQUIDATOR), amountBorrowed - expectedRepaid, "liquidator balance"); + assertEq( + borrowableToken.balanceOf(address(morpho)), + amountSupplied - amountBorrowed + expectedRepaid, + "morpho balance" + ); + assertEq( + collateralToken.balanceOf(address(morpho)), amountCollateral - amountSeized, "morpho collateral balance" + ); + assertEq(collateralToken.balanceOf(LIQUIDATOR), amountSeized, "liquidator collateral balance"); + } + + struct LiquidateBadDebtTestParams { + uint256 incentive; + uint256 expectedRepaid; + uint256 expectedRepaidShares; + uint256 borrowSharesBeforeLiquidation; + uint256 totalBorrowSharesBeforeLiquidation; + uint256 totalBorrowBeforeLiquidation; + uint256 totalSupplyBeforeLiquidation; + uint256 expectedBadDebt; + } + + function testLiquidateBadDebt( + uint256 amountCollateral, + uint256 amountSupplied, + uint256 amountBorrowed, + uint256 priceCollateral + ) public { + LiquidateBadDebtTestParams memory params; + + (amountCollateral, amountBorrowed, priceCollateral) = + _boundUnhealthyPosition(amountCollateral, amountBorrowed, priceCollateral); + + vm.assume(amountCollateral > 1); + + params.incentive = _liquidationIncentive(market.lltv); + params.expectedRepaid = amountCollateral.mulDivUp(priceCollateral, ORACLE_PRICE_SCALE).wDivUp(params.incentive); + + uint256 minBorrowed = max(params.expectedRepaid, amountBorrowed); + amountBorrowed = bound(amountBorrowed, minBorrowed, max(minBorrowed, MAX_TEST_AMOUNT)); + + amountSupplied = bound(amountSupplied, amountBorrowed, max(amountBorrowed, MAX_TEST_AMOUNT)); + _supply(amountSupplied); + + borrowableToken.setBalance(LIQUIDATOR, amountBorrowed); + collateralToken.setBalance(BORROWER, amountCollateral); + + oracle.setPrice(type(uint256).max / amountCollateral); + + vm.startPrank(BORROWER); + morpho.supplyCollateral(market, amountCollateral, BORROWER, hex""); + morpho.borrow(market, amountBorrowed, 0, BORROWER, BORROWER); + vm.stopPrank(); + + oracle.setPrice(priceCollateral); + + params.expectedRepaidShares = + params.expectedRepaid.toSharesDown(morpho.totalBorrow(id), morpho.totalBorrowShares(id)); + params.borrowSharesBeforeLiquidation = morpho.borrowShares(id, BORROWER); + params.totalBorrowSharesBeforeLiquidation = morpho.totalBorrowShares(id); + params.totalBorrowBeforeLiquidation = morpho.totalBorrow(id); + params.totalSupplyBeforeLiquidation = morpho.totalSupply(id); + params.expectedBadDebt = (params.borrowSharesBeforeLiquidation - params.expectedRepaidShares).toAssetsUp( + params.totalBorrowBeforeLiquidation - params.expectedRepaid, + params.totalBorrowSharesBeforeLiquidation - params.expectedRepaidShares + ); + + vm.prank(LIQUIDATOR); + + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.Liquidate( + id, + LIQUIDATOR, + BORROWER, + params.expectedRepaid, + params.expectedRepaidShares, + amountCollateral, + params.expectedBadDebt * SharesMathLib.VIRTUAL_SHARES + ); + (uint256 returnRepaid, uint256 returnRepaidShares) = morpho.liquidate(market, BORROWER, amountCollateral, hex""); + + assertEq(returnRepaid, params.expectedRepaid, "returned asset amount"); + assertEq(returnRepaidShares, params.expectedRepaidShares, "returned shares amount"); + assertEq(morpho.collateral(id, BORROWER), 0, "collateral"); + assertEq(borrowableToken.balanceOf(BORROWER), amountBorrowed, "borrower balance"); + assertEq(borrowableToken.balanceOf(LIQUIDATOR), amountBorrowed - params.expectedRepaid, "liquidator balance"); + assertEq( + borrowableToken.balanceOf(address(morpho)), + amountSupplied - amountBorrowed + params.expectedRepaid, + "morpho balance" + ); + assertEq(collateralToken.balanceOf(address(morpho)), 0, "morpho collateral balance"); + assertEq(collateralToken.balanceOf(LIQUIDATOR), amountCollateral, "liquidator collateral balance"); + + // Bad debt realization. + assertEq(morpho.borrowShares(id, BORROWER), 0, "borrow shares"); + assertEq(morpho.totalBorrowShares(id), 0, "total borrow shares"); + assertEq( + morpho.totalBorrow(id), + params.totalBorrowBeforeLiquidation - params.expectedRepaid - params.expectedBadDebt, + "total borrow" + ); + assertEq(morpho.totalSupply(id), params.totalSupplyBeforeLiquidation - params.expectedBadDebt, "total supply"); + } +} diff --git a/test/forge/integration/TestIntegrationOnlyOwner.t.sol b/test/forge/integration/TestIntegrationOnlyOwner.t.sol new file mode 100644 index 000000000..468834074 --- /dev/null +++ b/test/forge/integration/TestIntegrationOnlyOwner.t.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "../BaseTest.sol"; + +contract IntegrationOnlyOwnerTest is BaseTest { + using MarketLib for Market; + using MathLib for uint256; + + function testSetOwnerWhenNotOwner(address addressFuzz) public { + vm.assume(addressFuzz != OWNER); + + vm.prank(addressFuzz); + vm.expectRevert(bytes(ErrorsLib.NOT_OWNER)); + morpho.setOwner(addressFuzz); + } + + function testSetOwner(address newOwner) public { + vm.assume(newOwner != OWNER); + + vm.prank(OWNER); + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.SetOwner(newOwner); + morpho.setOwner(newOwner); + + assertEq(morpho.owner(), newOwner, "owner is not set"); + } + + function testEnableIrmWhenNotOwner(address addressFuzz, address irmFuzz) public { + vm.assume(addressFuzz != OWNER); + vm.assume(irmFuzz != address(irm)); + + vm.prank(addressFuzz); + vm.expectRevert(bytes(ErrorsLib.NOT_OWNER)); + morpho.enableIrm(irmFuzz); + } + + function testEnableIrm(address irmFuzz) public { + vm.assume(irmFuzz != address(irm)); + + vm.prank(OWNER); + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.EnableIrm(irmFuzz); + morpho.enableIrm(irmFuzz); + + assertTrue(morpho.isIrmEnabled(irmFuzz), "IRM is not enabled"); + } + + function testEnableLltvWhenNotOwner(address addressFuzz, uint256 lltvFuzz) public { + vm.assume(addressFuzz != OWNER); + vm.assume(lltvFuzz != LLTV); + + vm.prank(addressFuzz); + vm.expectRevert(bytes(ErrorsLib.NOT_OWNER)); + morpho.enableLltv(lltvFuzz); + } + + function testEnableTooHighLltv(uint256 lltvFuzz) public { + lltvFuzz = _boundInvalidLltv(lltvFuzz); + + vm.prank(OWNER); + vm.expectRevert(bytes(ErrorsLib.LLTV_TOO_HIGH)); + morpho.enableLltv(lltvFuzz); + } + + function testEnableLltv(uint256 lltvFuzz) public { + lltvFuzz = _boundValidLltv(lltvFuzz); + + vm.prank(OWNER); + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.EnableLltv(lltvFuzz); + morpho.enableLltv(lltvFuzz); + + assertTrue(morpho.isLltvEnabled(lltvFuzz), "LLTV is not enabled"); + } + + function testSetFeeWhenNotOwner(address addressFuzz, uint256 feeFuzz) public { + vm.assume(addressFuzz != OWNER); + + vm.prank(addressFuzz); + vm.expectRevert(bytes(ErrorsLib.NOT_OWNER)); + morpho.setFee(market, feeFuzz); + } + + function testSetFeeWhenMarketNotCreated(Market memory marketFuzz, uint256 feeFuzz) public { + vm.assume(neq(marketFuzz, market)); + + vm.prank(OWNER); + vm.expectRevert(bytes(ErrorsLib.MARKET_NOT_CREATED)); + morpho.setFee(marketFuzz, feeFuzz); + } + + function testSetTooHighFee(uint256 feeFuzz) public { + feeFuzz = bound(feeFuzz, MAX_FEE + 1, type(uint256).max); + + vm.prank(OWNER); + vm.expectRevert(bytes(ErrorsLib.MAX_FEE_EXCEEDED)); + morpho.setFee(market, feeFuzz); + } + + function testSetFee(uint256 feeFuzz) public { + feeFuzz = bound(feeFuzz, 0, MAX_FEE); + + vm.prank(OWNER); + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.SetFee(id, feeFuzz); + morpho.setFee(market, feeFuzz); + + assertEq(morpho.fee(id), feeFuzz); + } + + function testSetFeeRecipientWhenNotOwner(address addressFuzz) public { + vm.assume(addressFuzz != OWNER); + + vm.prank(addressFuzz); + vm.expectRevert(bytes(ErrorsLib.NOT_OWNER)); + morpho.setFeeRecipient(addressFuzz); + } + + function testSetFeeRecipient(address newFeeRecipient) public { + vm.assume(newFeeRecipient != OWNER); + + vm.prank(OWNER); + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.SetFeeRecipient(newFeeRecipient); + morpho.setFeeRecipient(newFeeRecipient); + + assertEq(morpho.feeRecipient(), newFeeRecipient); + } +} diff --git a/test/forge/integration/TestIntegrationRepay.t.sol b/test/forge/integration/TestIntegrationRepay.t.sol new file mode 100644 index 000000000..5c35c6deb --- /dev/null +++ b/test/forge/integration/TestIntegrationRepay.t.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "../BaseTest.sol"; + +contract IntegrationRepayTest is BaseTest { + using MathLib for uint256; + using SharesMathLib for uint256; + + function testRepayMarketNotCreated(Market memory marketFuzz) public { + vm.assume(neq(marketFuzz, market)); + + vm.expectRevert(bytes(ErrorsLib.MARKET_NOT_CREATED)); + morpho.repay(marketFuzz, 1, 0, address(this), hex""); + } + + function testRepayZeroAmount() public { + vm.expectRevert(bytes(ErrorsLib.INCONSISTENT_INPUT)); + morpho.repay(market, 0, 0, address(this), hex""); + } + + function testRepayInconsistentInput(uint256 amount, uint256 shares) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + shares = bound(shares, 1, MAX_TEST_SHARES); + + vm.expectRevert(bytes(ErrorsLib.INCONSISTENT_INPUT)); + morpho.repay(market, amount, shares, address(this), hex""); + } + + function testRepayOnBehalfZeroAddress(uint256 input, bool isAmount) public { + input = bound(input, 1, type(uint256).max); + vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); + morpho.repay(market, isAmount ? input : 0, isAmount ? 0 : input, address(0), hex""); + } + + function testRepayAssets( + uint256 amountSupplied, + uint256 amountCollateral, + uint256 amountBorrowed, + uint256 amountRepaid, + uint256 priceCollateral + ) public { + (amountCollateral, amountBorrowed, priceCollateral) = + _boundHealthyPosition(amountCollateral, amountBorrowed, priceCollateral); + + amountSupplied = bound(amountSupplied, amountBorrowed, MAX_TEST_AMOUNT); + _supply(amountSupplied); + + oracle.setPrice(priceCollateral); + + amountRepaid = bound(amountRepaid, 1, amountBorrowed); + uint256 expectedBorrowShares = amountBorrowed.toSharesUp(0, 0); + uint256 expectedRepaidShares = amountRepaid.toSharesDown(amountBorrowed, expectedBorrowShares); + + collateralToken.setBalance(ONBEHALF, amountCollateral); + borrowableToken.setBalance(REPAYER, amountRepaid); + + vm.startPrank(ONBEHALF); + morpho.supplyCollateral(market, amountCollateral, ONBEHALF, hex""); + morpho.borrow(market, amountBorrowed, 0, ONBEHALF, RECEIVER); + vm.stopPrank(); + + vm.prank(REPAYER); + + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.Repay(id, REPAYER, ONBEHALF, amountRepaid, expectedRepaidShares); + (uint256 returnAssets, uint256 returnShares) = morpho.repay(market, amountRepaid, 0, ONBEHALF, hex""); + + expectedBorrowShares -= expectedRepaidShares; + + assertEq(returnAssets, amountRepaid, "returned asset amount"); + assertEq(returnShares, expectedRepaidShares, "returned shares amount"); + assertEq(morpho.borrowShares(id, ONBEHALF), expectedBorrowShares, "borrow shares"); + assertEq(morpho.totalBorrow(id), amountBorrowed - amountRepaid, "total borrow"); + assertEq(morpho.totalBorrowShares(id), expectedBorrowShares, "total borrow shares"); + assertEq(borrowableToken.balanceOf(RECEIVER), amountBorrowed, "RECEIVER balance"); + assertEq( + borrowableToken.balanceOf(address(morpho)), amountSupplied - amountBorrowed + amountRepaid, "morpho balance" + ); + } + + function testRepayShares( + uint256 amountSupplied, + uint256 amountCollateral, + uint256 amountBorrowed, + uint256 sharesRepaid, + uint256 priceCollateral + ) public { + (amountCollateral, amountBorrowed, priceCollateral) = + _boundHealthyPosition(amountCollateral, amountBorrowed, priceCollateral); + + amountSupplied = bound(amountSupplied, amountBorrowed, MAX_TEST_AMOUNT); + _supply(amountSupplied); + + oracle.setPrice(priceCollateral); + + uint256 expectedBorrowShares = amountBorrowed.toSharesUp(0, 0); + sharesRepaid = bound(sharesRepaid, 1, expectedBorrowShares); + uint256 expectedAmountRepaid = sharesRepaid.toAssetsUp(amountBorrowed, expectedBorrowShares); + + collateralToken.setBalance(ONBEHALF, amountCollateral); + borrowableToken.setBalance(REPAYER, expectedAmountRepaid); + + vm.startPrank(ONBEHALF); + morpho.supplyCollateral(market, amountCollateral, ONBEHALF, hex""); + morpho.borrow(market, amountBorrowed, 0, ONBEHALF, RECEIVER); + vm.stopPrank(); + + vm.prank(REPAYER); + + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.Repay(id, REPAYER, ONBEHALF, expectedAmountRepaid, sharesRepaid); + (uint256 returnAssets, uint256 returnShares) = morpho.repay(market, 0, sharesRepaid, ONBEHALF, hex""); + + expectedBorrowShares -= sharesRepaid; + + assertEq(returnAssets, expectedAmountRepaid, "returned asset amount"); + assertEq(returnShares, sharesRepaid, "returned shares amount"); + assertEq(morpho.borrowShares(id, ONBEHALF), expectedBorrowShares, "borrow shares"); + assertEq(morpho.totalBorrow(id), amountBorrowed - expectedAmountRepaid, "total borrow"); + assertEq(morpho.totalBorrowShares(id), expectedBorrowShares, "total borrow shares"); + assertEq(borrowableToken.balanceOf(RECEIVER), amountBorrowed, "RECEIVER balance"); + assertEq( + borrowableToken.balanceOf(address(morpho)), + amountSupplied - amountBorrowed + expectedAmountRepaid, + "morpho balance" + ); + } +} diff --git a/test/forge/integration/TestIntegrationSetAuthorization.t.sol b/test/forge/integration/TestIntegrationSetAuthorization.t.sol new file mode 100644 index 000000000..eb3442579 --- /dev/null +++ b/test/forge/integration/TestIntegrationSetAuthorization.t.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {SigUtils} from "test/forge/helpers/SigUtils.sol"; + +import "../BaseTest.sol"; + +contract IntegrationAuthorization is BaseTest { + function testSetAuthorization(address addressFuzz) public { + vm.assume(addressFuzz != address(this)); + + morpho.setAuthorization(addressFuzz, true); + + assertTrue(morpho.isAuthorized(address(this), addressFuzz)); + + morpho.setAuthorization(addressFuzz, false); + + assertFalse(morpho.isAuthorized(address(this), addressFuzz)); + } + + function testSetAuthorizationWithSignatureDeadlineOutdated( + Authorization memory authorization, + uint256 privateKey, + uint256 elapsed + ) public { + elapsed = bound(elapsed, 1, type(uint32).max); + authorization.deadline = block.timestamp; + + // Private key must be less than the secp256k1 curve order. + privateKey = bound(privateKey, 1, type(uint32).max); + authorization.nonce = 0; + authorization.authorizer = vm.addr(privateKey); + + Signature memory sig; + bytes32 digest = SigUtils.getTypedDataHash(morpho.DOMAIN_SEPARATOR(), authorization); + (sig.v, sig.r, sig.s) = vm.sign(privateKey, digest); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + elapsed); + + vm.expectRevert(bytes(ErrorsLib.SIGNATURE_EXPIRED)); + morpho.setAuthorizationWithSig(authorization, sig); + } + + function testAuthorizationWithSigWrongPK(Authorization memory authorization, uint256 privateKey) public { + vm.assume(authorization.deadline > block.timestamp); + + // Private key must be less than the secp256k1 curve order. + privateKey = bound(privateKey, 1, type(uint32).max); + authorization.nonce = 0; + + Signature memory sig; + bytes32 digest = SigUtils.getTypedDataHash(morpho.DOMAIN_SEPARATOR(), authorization); + (sig.v, sig.r, sig.s) = vm.sign(privateKey, digest); + + vm.expectRevert(bytes(ErrorsLib.INVALID_SIGNATURE)); + morpho.setAuthorizationWithSig(authorization, sig); + } + + function testAuthorizationWithSigWrongNonce(Authorization memory authorization, uint256 privateKey) public { + vm.assume(authorization.deadline > block.timestamp); + vm.assume(authorization.nonce != 0); + + // Private key must be less than the secp256k1 curve order. + privateKey = bound(privateKey, 1, type(uint32).max); + authorization.authorizer = vm.addr(privateKey); + + Signature memory sig; + bytes32 digest = SigUtils.getTypedDataHash(morpho.DOMAIN_SEPARATOR(), authorization); + (sig.v, sig.r, sig.s) = vm.sign(privateKey, digest); + + vm.expectRevert(bytes(ErrorsLib.INVALID_NONCE)); + morpho.setAuthorizationWithSig(authorization, sig); + } + + function testAuthorizationWithSig(Authorization memory authorization, uint256 privateKey) public { + vm.assume(authorization.deadline > block.timestamp); + + // Private key must be less than the secp256k1 curve order. + privateKey = bound(privateKey, 1, type(uint32).max); + authorization.nonce = 0; + authorization.authorizer = vm.addr(privateKey); + + Signature memory sig; + bytes32 digest = SigUtils.getTypedDataHash(morpho.DOMAIN_SEPARATOR(), authorization); + (sig.v, sig.r, sig.s) = vm.sign(privateKey, digest); + + morpho.setAuthorizationWithSig(authorization, sig); + + assertEq(morpho.isAuthorized(authorization.authorizer, authorization.authorized), authorization.isAuthorized); + assertEq(morpho.nonce(authorization.authorizer), 1); + } +} diff --git a/test/forge/integration/TestIntegrationSupply.t.sol b/test/forge/integration/TestIntegrationSupply.t.sol new file mode 100644 index 000000000..232e43dd4 --- /dev/null +++ b/test/forge/integration/TestIntegrationSupply.t.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "../BaseTest.sol"; + +contract IntegrationSupplyTest is BaseTest { + using MathLib for uint256; + using SharesMathLib for uint256; + + function testSupplyMarketNotCreated(Market memory marketFuzz, uint256 amount) public { + vm.assume(neq(marketFuzz, market)); + + vm.prank(SUPPLIER); + vm.expectRevert(bytes(ErrorsLib.MARKET_NOT_CREATED)); + morpho.supply(marketFuzz, amount, 0, SUPPLIER, hex""); + } + + function testSupplyZeroAmount() public { + vm.assume(SUPPLIER != address(0)); + + vm.prank(SUPPLIER); + vm.expectRevert(bytes(ErrorsLib.INCONSISTENT_INPUT)); + morpho.supply(market, 0, 0, SUPPLIER, hex""); + } + + function testSupplyOnBehalfZeroAddress(uint256 amount) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + vm.prank(SUPPLIER); + vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); + morpho.supply(market, amount, 0, address(0), hex""); + } + + function testSupplyInconsistantInput(uint256 amount, uint256 shares) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + shares = bound(shares, 1, MAX_TEST_SHARES); + + vm.prank(SUPPLIER); + vm.expectRevert(bytes(ErrorsLib.INCONSISTENT_INPUT)); + morpho.supply(market, amount, shares, address(0), hex""); + } + + function testSupplyAssets(uint256 amount) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + borrowableToken.setBalance(SUPPLIER, amount); + + uint256 expectedSupplyShares = amount.toSharesDown(0, 0); + + vm.prank(SUPPLIER); + + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.Supply(id, SUPPLIER, ONBEHALF, amount, expectedSupplyShares); + (uint256 returnAssets, uint256 returnShares) = morpho.supply(market, amount, 0, ONBEHALF, hex""); + + assertEq(returnAssets, amount, "returned asset amount"); + assertEq(returnShares, expectedSupplyShares, "returned shares amount"); + assertEq(morpho.supplyShares(id, ONBEHALF), expectedSupplyShares, "supply shares"); + assertEq(morpho.totalSupply(id), amount, "total supply"); + assertEq(morpho.totalSupplyShares(id), expectedSupplyShares, "total supply shares"); + assertEq(borrowableToken.balanceOf(SUPPLIER), 0, "SUPPLIER balance"); + assertEq(borrowableToken.balanceOf(address(morpho)), amount, "morpho balance"); + } + + function testSupplyShares(uint256 shares) public { + shares = bound(shares, 1, MAX_TEST_SHARES); + + uint256 expectedSuppliedAmount = shares.toAssetsUp(0, 0); + + borrowableToken.setBalance(SUPPLIER, expectedSuppliedAmount); + + vm.prank(SUPPLIER); + + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.Supply(id, SUPPLIER, ONBEHALF, expectedSuppliedAmount, shares); + (uint256 returnAssets, uint256 returnShares) = morpho.supply(market, 0, shares, ONBEHALF, hex""); + + assertEq(returnAssets, expectedSuppliedAmount, "returned asset amount"); + assertEq(returnShares, shares, "returned shares amount"); + assertEq(morpho.supplyShares(id, ONBEHALF), shares, "supply shares"); + assertEq(morpho.totalSupply(id), expectedSuppliedAmount, "total supply"); + assertEq(morpho.totalSupplyShares(id), shares, "total supply shares"); + assertEq(borrowableToken.balanceOf(SUPPLIER), 0, "SUPPLIER balance"); + assertEq(borrowableToken.balanceOf(address(morpho)), expectedSuppliedAmount, "morpho balance"); + } +} diff --git a/test/forge/integration/TestIntegrationSupplyCollateral.t.sol b/test/forge/integration/TestIntegrationSupplyCollateral.t.sol new file mode 100644 index 000000000..b20163d37 --- /dev/null +++ b/test/forge/integration/TestIntegrationSupplyCollateral.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "../BaseTest.sol"; + +contract IntegrationSupplyCollateralTest is BaseTest { + function testSupplyCollateralMarketNotCreated(Market memory marketFuzz, uint256 amount) public { + vm.assume(neq(marketFuzz, market)); + + vm.prank(SUPPLIER); + vm.expectRevert(bytes(ErrorsLib.MARKET_NOT_CREATED)); + morpho.supply(marketFuzz, amount, 0, SUPPLIER, hex""); + } + + function testSupplyCollateralZeroAmount(address SUPPLIER) public { + vm.prank(SUPPLIER); + vm.expectRevert(bytes(ErrorsLib.ZERO_ASSETS)); + morpho.supplyCollateral(market, 0, SUPPLIER, hex""); + } + + function testSupplyCollateralOnBehalfZeroAddress(uint256 amount) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + vm.prank(SUPPLIER); + vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); + morpho.supplyCollateral(market, amount, address(0), hex""); + } + + function testSupplyCollateral(uint256 amount) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + collateralToken.setBalance(SUPPLIER, amount); + + vm.prank(SUPPLIER); + + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.SupplyCollateral(id, SUPPLIER, ONBEHALF, amount); + morpho.supplyCollateral(market, amount, ONBEHALF, hex""); + + assertEq(morpho.collateral(id, ONBEHALF), amount, "collateral"); + assertEq(collateralToken.balanceOf(SUPPLIER), 0, "SUPPLIER balance"); + assertEq(collateralToken.balanceOf(address(morpho)), amount, "morpho balance"); + } +} diff --git a/test/forge/integration/TestIntegrationWithdraw.t.sol b/test/forge/integration/TestIntegrationWithdraw.t.sol new file mode 100644 index 000000000..3f8f2623b --- /dev/null +++ b/test/forge/integration/TestIntegrationWithdraw.t.sol @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "../BaseTest.sol"; + +contract IntegrationWithdrawTest is BaseTest { + using MathLib for uint256; + using SharesMathLib for uint256; + + function testWithdrawMarketNotCreated(Market memory marketFuzz) public { + vm.assume(neq(marketFuzz, market)); + + vm.expectRevert(bytes(ErrorsLib.MARKET_NOT_CREATED)); + morpho.withdraw(marketFuzz, 1, 0, address(this), address(this)); + } + + function testWithdrawZeroAmount(uint256 amount) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + borrowableToken.setBalance(address(this), amount); + morpho.supply(market, amount, 0, address(this), hex""); + + vm.expectRevert(bytes(ErrorsLib.INCONSISTENT_INPUT)); + morpho.withdraw(market, 0, 0, address(this), address(this)); + } + + function testWithdrawInconsistentInput(uint256 amount, uint256 shares) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + shares = bound(shares, 1, MAX_TEST_SHARES); + + borrowableToken.setBalance(address(this), amount); + morpho.supply(market, amount, 0, address(this), hex""); + + vm.expectRevert(bytes(ErrorsLib.INCONSISTENT_INPUT)); + morpho.withdraw(market, amount, shares, address(this), address(this)); + } + + function testWithdrawToZeroAddress(uint256 amount) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + borrowableToken.setBalance(address(this), amount); + morpho.supply(market, amount, 0, address(this), hex""); + + vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); + morpho.withdraw(market, amount, 0, address(this), address(0)); + } + + function testWithdrawUnauthorized(address attacker, uint256 amount) public { + vm.assume(attacker != address(this)); + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + borrowableToken.setBalance(address(this), amount); + morpho.supply(market, amount, 0, address(this), hex""); + + vm.prank(attacker); + vm.expectRevert(bytes(ErrorsLib.UNAUTHORIZED)); + morpho.withdraw(market, amount, 0, address(this), address(this)); + } + + function testWithdrawInsufficientLiquidity(uint256 amountSupplied, uint256 amountBorrowed) public { + amountBorrowed = bound(amountBorrowed, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT); + amountSupplied = bound(amountSupplied, amountBorrowed + 1, MAX_TEST_AMOUNT + 1); + + borrowableToken.setBalance(SUPPLIER, amountSupplied); + + vm.prank(SUPPLIER); + morpho.supply(market, amountSupplied, 0, SUPPLIER, hex""); + + uint256 collateralPrice = IOracle(market.oracle).price(); + uint256 amountCollateral = amountBorrowed.wDivUp(LLTV).mulDivUp(ORACLE_PRICE_SCALE, collateralPrice); + + collateralToken.setBalance(BORROWER, amountCollateral); + + vm.startPrank(BORROWER); + morpho.supplyCollateral(market, amountCollateral, BORROWER, hex""); + morpho.borrow(market, amountBorrowed, 0, BORROWER, RECEIVER); + vm.stopPrank(); + + vm.prank(SUPPLIER); + vm.expectRevert(bytes(ErrorsLib.INSUFFICIENT_LIQUIDITY)); + morpho.withdraw(market, amountSupplied, 0, SUPPLIER, RECEIVER); + } + + function testWithdrawAssets(uint256 amountSupplied, uint256 amountBorrowed, uint256 amountWithdrawn) public { + amountSupplied = bound(amountSupplied, 2, MAX_TEST_AMOUNT); + amountBorrowed = bound(amountBorrowed, 1, amountSupplied - 1); + amountWithdrawn = bound(amountWithdrawn, 1, amountSupplied - amountBorrowed); + + uint256 collateralPrice = IOracle(market.oracle).price(); + uint256 amountCollateral = amountBorrowed.wDivUp(LLTV).mulDivUp(ORACLE_PRICE_SCALE, collateralPrice); + + borrowableToken.setBalance(address(this), amountSupplied); + collateralToken.setBalance(BORROWER, amountCollateral); + morpho.supply(market, amountSupplied, 0, address(this), hex""); + + vm.startPrank(BORROWER); + morpho.supplyCollateral(market, amountCollateral, BORROWER, hex""); + morpho.borrow(market, amountBorrowed, 0, BORROWER, BORROWER); + vm.stopPrank(); + + uint256 expectedSupplyShares = amountSupplied.toSharesDown(0, 0); + uint256 expectedWithdrawnShares = amountWithdrawn.toSharesUp(amountSupplied, expectedSupplyShares); + + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.Withdraw(id, address(this), address(this), RECEIVER, amountWithdrawn, expectedWithdrawnShares); + (uint256 returnAssets, uint256 returnShares) = + morpho.withdraw(market, amountWithdrawn, 0, address(this), RECEIVER); + + expectedSupplyShares -= expectedWithdrawnShares; + + assertEq(returnAssets, amountWithdrawn, "returned asset amount"); + assertEq(returnShares, expectedWithdrawnShares, "returned shares amount"); + assertEq(morpho.supplyShares(id, address(this)), expectedSupplyShares, "supply shares"); + assertEq(morpho.totalSupplyShares(id), expectedSupplyShares, "total supply shares"); + assertEq(morpho.totalSupply(id), amountSupplied - amountWithdrawn, "total supply"); + assertEq(borrowableToken.balanceOf(RECEIVER), amountWithdrawn, "RECEIVER balance"); + assertEq(borrowableToken.balanceOf(BORROWER), amountBorrowed, "borrower balance"); + assertEq( + borrowableToken.balanceOf(address(morpho)), + amountSupplied - amountBorrowed - amountWithdrawn, + "morpho balance" + ); + } + + function testWithdrawShares(uint256 amountSupplied, uint256 amountBorrowed, uint256 sharesWithdrawn) public { + amountSupplied = bound(amountSupplied, 2, MAX_TEST_AMOUNT); + amountBorrowed = bound(amountBorrowed, 1, amountSupplied - 1); + + uint256 collateralPrice = IOracle(market.oracle).price(); + uint256 amountCollateral = amountBorrowed.wDivUp(LLTV).mulDivUp(ORACLE_PRICE_SCALE, collateralPrice); + + uint256 expectedSupplyShares = amountSupplied.toSharesDown(0, 0); + uint256 availableLiquidity = amountSupplied - amountBorrowed; + uint256 withdrawableShares = availableLiquidity.toSharesDown(amountSupplied, expectedSupplyShares); + vm.assume(withdrawableShares != 0); + + sharesWithdrawn = bound(sharesWithdrawn, 1, withdrawableShares); + uint256 expectedAmountWithdrawn = sharesWithdrawn.toAssetsDown(amountSupplied, expectedSupplyShares); + + borrowableToken.setBalance(address(this), amountSupplied); + collateralToken.setBalance(BORROWER, amountCollateral); + morpho.supply(market, amountSupplied, 0, address(this), hex""); + + vm.startPrank(BORROWER); + morpho.supplyCollateral(market, amountCollateral, BORROWER, hex""); + morpho.borrow(market, amountBorrowed, 0, BORROWER, BORROWER); + vm.stopPrank(); + + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.Withdraw(id, address(this), address(this), RECEIVER, expectedAmountWithdrawn, sharesWithdrawn); + (uint256 returnAssets, uint256 returnShares) = + morpho.withdraw(market, 0, sharesWithdrawn, address(this), RECEIVER); + + expectedSupplyShares -= sharesWithdrawn; + + assertEq(returnAssets, expectedAmountWithdrawn, "returned asset amount"); + assertEq(returnShares, sharesWithdrawn, "returned shares amount"); + assertEq(morpho.supplyShares(id, address(this)), expectedSupplyShares, "supply shares"); + assertEq(morpho.totalSupply(id), amountSupplied - expectedAmountWithdrawn, "total supply"); + assertEq(morpho.totalSupplyShares(id), expectedSupplyShares, "total supply shares"); + assertEq(borrowableToken.balanceOf(RECEIVER), expectedAmountWithdrawn, "RECEIVER balance"); + assertEq( + borrowableToken.balanceOf(address(morpho)), + amountSupplied - amountBorrowed - expectedAmountWithdrawn, + "morpho balance" + ); + } + + function testWithdrawAssetsOnBehalf(uint256 amountSupplied, uint256 amountBorrowed, uint256 amountWithdrawn) + public + { + amountSupplied = bound(amountSupplied, 2, MAX_TEST_AMOUNT); + amountBorrowed = bound(amountBorrowed, 1, amountSupplied - 1); + amountWithdrawn = bound(amountWithdrawn, 1, amountSupplied - amountBorrowed); + + uint256 collateralPrice = IOracle(market.oracle).price(); + uint256 amountCollateral = amountBorrowed.wDivUp(LLTV).mulDivUp(ORACLE_PRICE_SCALE, collateralPrice); + + borrowableToken.setBalance(ONBEHALF, amountSupplied); + collateralToken.setBalance(ONBEHALF, amountCollateral); + + vm.startPrank(ONBEHALF); + morpho.supplyCollateral(market, amountCollateral, ONBEHALF, hex""); + morpho.supply(market, amountSupplied, 0, ONBEHALF, hex""); + morpho.borrow(market, amountBorrowed, 0, ONBEHALF, ONBEHALF); + vm.stopPrank(); + + uint256 expectedSupplyShares = amountSupplied.toSharesDown(0, 0); + uint256 expectedWithdrawnShares = amountWithdrawn.toSharesUp(amountSupplied, expectedSupplyShares); + + uint256 receiverBalanceBefore = borrowableToken.balanceOf(RECEIVER); + + vm.startPrank(BORROWER); + + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.Withdraw(id, BORROWER, ONBEHALF, RECEIVER, amountWithdrawn, expectedWithdrawnShares); + (uint256 returnAssets, uint256 returnShares) = morpho.withdraw(market, amountWithdrawn, 0, ONBEHALF, RECEIVER); + + expectedSupplyShares -= expectedWithdrawnShares; + + assertEq(returnAssets, amountWithdrawn, "returned asset amount"); + assertEq(returnShares, expectedWithdrawnShares, "returned shares amount"); + assertEq(morpho.supplyShares(id, ONBEHALF), expectedSupplyShares, "supply shares"); + assertEq(morpho.totalSupply(id), amountSupplied - amountWithdrawn, "total supply"); + assertEq(morpho.totalSupplyShares(id), expectedSupplyShares, "total supply shares"); + assertEq(borrowableToken.balanceOf(RECEIVER) - receiverBalanceBefore, amountWithdrawn, "RECEIVER balance"); + assertEq( + borrowableToken.balanceOf(address(morpho)), + amountSupplied - amountBorrowed - amountWithdrawn, + "morpho balance" + ); + } + + function testWithdrawSharesOnBehalf(uint256 amountSupplied, uint256 amountBorrowed, uint256 sharesWithdrawn) + public + { + amountSupplied = bound(amountSupplied, 2, MAX_TEST_AMOUNT); + amountBorrowed = bound(amountBorrowed, 1, amountSupplied - 1); + + uint256 collateralPrice = IOracle(market.oracle).price(); + uint256 amountCollateral = amountBorrowed.wDivUp(LLTV).mulDivUp(ORACLE_PRICE_SCALE, collateralPrice); + + uint256 expectedSupplyShares = amountSupplied.toSharesDown(0, 0); + uint256 availableLiquidity = amountSupplied - amountBorrowed; + uint256 withdrawableShares = availableLiquidity.toSharesDown(amountSupplied, expectedSupplyShares); + vm.assume(withdrawableShares != 0); + + sharesWithdrawn = bound(sharesWithdrawn, 1, withdrawableShares); + uint256 expectedAmountWithdrawn = sharesWithdrawn.toAssetsDown(amountSupplied, expectedSupplyShares); + + borrowableToken.setBalance(ONBEHALF, amountSupplied); + collateralToken.setBalance(ONBEHALF, amountCollateral); + + vm.startPrank(ONBEHALF); + morpho.supplyCollateral(market, amountCollateral, ONBEHALF, hex""); + morpho.supply(market, amountSupplied, 0, ONBEHALF, hex""); + morpho.borrow(market, amountBorrowed, 0, ONBEHALF, ONBEHALF); + vm.stopPrank(); + + uint256 receiverBalanceBefore = borrowableToken.balanceOf(RECEIVER); + + vm.startPrank(BORROWER); + + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.Withdraw(id, BORROWER, ONBEHALF, RECEIVER, expectedAmountWithdrawn, sharesWithdrawn); + (uint256 returnAssets, uint256 returnShares) = morpho.withdraw(market, 0, sharesWithdrawn, ONBEHALF, RECEIVER); + + expectedSupplyShares -= sharesWithdrawn; + + assertEq(returnAssets, expectedAmountWithdrawn, "returned asset amount"); + assertEq(returnShares, sharesWithdrawn, "returned shares amount"); + assertEq(morpho.supplyShares(id, ONBEHALF), expectedSupplyShares, "supply shares"); + assertEq(morpho.totalSupply(id), amountSupplied - expectedAmountWithdrawn, "total supply"); + assertEq(morpho.totalSupplyShares(id), expectedSupplyShares, "total supply shares"); + assertEq( + borrowableToken.balanceOf(RECEIVER) - receiverBalanceBefore, expectedAmountWithdrawn, "RECEIVER balance" + ); + assertEq( + borrowableToken.balanceOf(address(morpho)), + amountSupplied - amountBorrowed - expectedAmountWithdrawn, + "morpho balance" + ); + } +} diff --git a/test/forge/integration/TestIntegrationWithdrawCollateral.t.sol b/test/forge/integration/TestIntegrationWithdrawCollateral.t.sol new file mode 100644 index 000000000..3cf476e52 --- /dev/null +++ b/test/forge/integration/TestIntegrationWithdrawCollateral.t.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "../BaseTest.sol"; + +contract IntegrationWithdrawCollateralTest is BaseTest { + using MathLib for uint256; + + function testWithdrawCollateralMarketNotCreated(Market memory marketFuzz) public { + vm.assume(neq(marketFuzz, market)); + + vm.prank(SUPPLIER); + vm.expectRevert(bytes(ErrorsLib.MARKET_NOT_CREATED)); + morpho.withdrawCollateral(marketFuzz, 1, SUPPLIER, RECEIVER); + } + + function testWithdrawCollateralZeroAmount(uint256 amount) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + collateralToken.setBalance(SUPPLIER, amount); + + vm.startPrank(SUPPLIER); + collateralToken.approve(address(morpho), amount); + morpho.supplyCollateral(market, amount, SUPPLIER, hex""); + + vm.expectRevert(bytes(ErrorsLib.ZERO_ASSETS)); + morpho.withdrawCollateral(market, 0, SUPPLIER, RECEIVER); + vm.stopPrank(); + } + + function testWithdrawCollateralToZeroAddress(uint256 amount) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + collateralToken.setBalance(SUPPLIER, amount); + + vm.startPrank(SUPPLIER); + morpho.supplyCollateral(market, amount, SUPPLIER, hex""); + + vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); + morpho.withdrawCollateral(market, amount, SUPPLIER, address(0)); + vm.stopPrank(); + } + + function testWithdrawCollateralUnauthorized(address attacker, uint256 amount) public { + amount = bound(amount, 1, MAX_TEST_AMOUNT); + + collateralToken.setBalance(SUPPLIER, amount); + + vm.prank(SUPPLIER); + morpho.supplyCollateral(market, amount, SUPPLIER, hex""); + + vm.prank(attacker); + vm.expectRevert(bytes(ErrorsLib.UNAUTHORIZED)); + morpho.withdrawCollateral(market, amount, SUPPLIER, RECEIVER); + } + + function testWithdrawCollateralUnhealthyPosition( + uint256 amountCollateral, + uint256 amountSupplied, + uint256 amountBorrowed, + uint256 priceCollateral + ) public { + (amountCollateral, amountBorrowed, priceCollateral) = + _boundHealthyPosition(amountCollateral, amountBorrowed, priceCollateral); + + amountSupplied = bound(amountSupplied, amountBorrowed, MAX_TEST_AMOUNT); + _supply(amountSupplied); + + oracle.setPrice(priceCollateral); + + collateralToken.setBalance(BORROWER, amountCollateral); + + vm.startPrank(BORROWER); + morpho.supplyCollateral(market, amountCollateral, BORROWER, hex""); + morpho.borrow(market, amountBorrowed, 0, BORROWER, BORROWER); + vm.expectRevert(bytes(ErrorsLib.INSUFFICIENT_COLLATERAL)); + morpho.withdrawCollateral(market, amountCollateral, BORROWER, BORROWER); + vm.stopPrank(); + } + + function testWithdrawCollateral( + uint256 amountCollateral, + uint256 amountCollateralExcess, + uint256 amountSupplied, + uint256 amountBorrowed, + uint256 priceCollateral + ) public { + (amountCollateral, amountBorrowed, priceCollateral) = + _boundHealthyPosition(amountCollateral, amountBorrowed, priceCollateral); + + amountSupplied = bound(amountSupplied, amountBorrowed, MAX_TEST_AMOUNT); + _supply(amountSupplied); + + amountCollateralExcess = bound(amountCollateralExcess, 1, MAX_TEST_AMOUNT); + + oracle.setPrice(priceCollateral); + + collateralToken.setBalance(BORROWER, amountCollateral + amountCollateralExcess); + + vm.startPrank(BORROWER); + morpho.supplyCollateral(market, amountCollateral + amountCollateralExcess, BORROWER, hex""); + morpho.borrow(market, amountBorrowed, 0, BORROWER, BORROWER); + + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.WithdrawCollateral(id, BORROWER, BORROWER, RECEIVER, amountCollateralExcess); + morpho.withdrawCollateral(market, amountCollateralExcess, BORROWER, RECEIVER); + + vm.stopPrank(); + + assertEq(morpho.collateral(id, BORROWER), amountCollateral, "collateral balance"); + assertEq(collateralToken.balanceOf(RECEIVER), amountCollateralExcess, "lender balance"); + assertEq(collateralToken.balanceOf(address(morpho)), amountCollateral, "morpho balance"); + } + + function testWithdrawCollateralOnBehalf( + uint256 amountCollateral, + uint256 amountCollateralExcess, + uint256 amountSupplied, + uint256 amountBorrowed, + uint256 priceCollateral + ) public { + (amountCollateral, amountBorrowed, priceCollateral) = + _boundHealthyPosition(amountCollateral, amountBorrowed, priceCollateral); + + amountSupplied = bound(amountSupplied, amountBorrowed, MAX_TEST_AMOUNT); + _supply(amountSupplied); + + oracle.setPrice(priceCollateral); + + amountCollateralExcess = bound(amountCollateralExcess, 1, MAX_TEST_AMOUNT); + + collateralToken.setBalance(ONBEHALF, amountCollateral + amountCollateralExcess); + + vm.startPrank(ONBEHALF); + morpho.supplyCollateral(market, amountCollateral + amountCollateralExcess, ONBEHALF, hex""); + morpho.setAuthorization(BORROWER, true); + morpho.borrow(market, amountBorrowed, 0, ONBEHALF, ONBEHALF); + vm.stopPrank(); + + vm.prank(BORROWER); + + vm.expectEmit(true, true, true, true, address(morpho)); + emit EventsLib.WithdrawCollateral(id, BORROWER, ONBEHALF, RECEIVER, amountCollateralExcess); + morpho.withdrawCollateral(market, amountCollateralExcess, ONBEHALF, RECEIVER); + + assertEq(morpho.collateral(id, ONBEHALF), amountCollateral, "collateral balance"); + assertEq(collateralToken.balanceOf(RECEIVER), amountCollateralExcess, "lender balance"); + assertEq(collateralToken.balanceOf(address(morpho)), amountCollateral, "morpho balance"); + } +} diff --git a/test/morpho_tests.tree b/test/morpho_tests.tree index 87d731db5..da5dc2720 100644 --- a/test/morpho_tests.tree +++ b/test/morpho_tests.tree @@ -55,6 +55,7 @@ │ └── revert with MARKET_CREATED └── when market is not already created ├── it should set lastUpdate[market.id] to block.timestamp + ├── it should set idToMarket[id] to market └── it should emit CreateMarket(market.id, market) . └── supply(Market memory market, uint256 assets, uint256 shares, address onBehalf, bytes calldata data) external