diff --git a/.gitmodules b/.gitmodules index 6330fc0..7c20a1e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "lib/metamorpho"] path = lib/metamorpho url = https://github.com/morpho-org/metamorpho +[submodule "lib/solmate"] + path = lib/solmate + url = https://github.com/transmissions11/solmate diff --git a/foundry.toml b/foundry.toml index 74f6d23..5827ad8 100644 --- a/foundry.toml +++ b/foundry.toml @@ -25,5 +25,6 @@ script = "/dev/null" [profile.test] via-ir = false +fuzz.max_test_rejects = 100000 # See more config options https://github.com/foundry-rs/foundry/tree/master/config diff --git a/lib/metamorpho b/lib/metamorpho index 9c66465..74d51b9 160000 --- a/lib/metamorpho +++ b/lib/metamorpho @@ -1 +1 @@ -Subproject commit 9c66465f00a76ad078c2ddf2387f8545cad35e78 +Subproject commit 74d51b9f5bb1fb484f0db0e8f7aa67d0f62bcc78 diff --git a/lib/solmate b/lib/solmate new file mode 160000 index 0000000..e0e9ff0 --- /dev/null +++ b/lib/solmate @@ -0,0 +1 @@ +Subproject commit e0e9ff05d8aa5c7c48465511f85a6efdf5d5c30d diff --git a/remappings.txt b/remappings.txt index d776195..dbaebf9 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,15 +1,14 @@ @forge-std/=lib/morpho-blue/lib/forge-std/src/ @morpho-blue/=lib/morpho-blue/src/ -@morpho-blue-test/=lib/morpho-blue/test/ +@morpho-blue-test/=lib/morpho-blue/test/forge/ @openzeppelin4/=lib/openzeppelin-contracts/contracts/ -@openzeppelin5/=lib/metamorpho/lib/openzeppelin-contracts/contracts/ +@openzeppelin/=lib/metamorpho/lib/openzeppelin-contracts/contracts/ @snippets/=src/ -@solmate/=lib/morpho-blue/lib/solmate/src/ -solmate/=lib/morpho-blue/lib/permit2/lib/solmate/ +@solmate/=lib/solmate/src/ @metamorpho/=lib/metamorpho/src/ @metamorpho-test/=lib/metamorpho/test/forge/ \ No newline at end of file diff --git a/src/blue/BlueSnippets.sol b/src/blue/BlueSnippets.sol index b02cb64..59868d7 100644 --- a/src/blue/BlueSnippets.sol +++ b/src/blue/BlueSnippets.sol @@ -5,6 +5,7 @@ import {Id, IMorpho, MarketParams, Market} from "@morpho-blue/interfaces/IMorpho import {IERC20} from "@morpho-blue/interfaces/IERC20.sol"; import {IIrm} from "@morpho-blue/interfaces/IIrm.sol"; import {IOracle} from "@morpho-blue/interfaces/IOracle.sol"; + import {ERC20} from "@openzeppelin4/token/ERC20/ERC20.sol"; import {SafeERC20} from "@openzeppelin4/token/ERC20/utils/SafeERC20.sol"; import {MorphoBalancesLib} from "@morpho-blue/libraries/periphery/MorphoBalancesLib.sol"; diff --git a/src/blue/CallbacksSnippets.sol b/src/blue/CallbacksSnippets.sol new file mode 100644 index 0000000..d7fd20b --- /dev/null +++ b/src/blue/CallbacksSnippets.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {SwapMock} from "@snippets/blue/mocks/SwapMock.sol"; +import { + IMorphoSupplyCollateralCallback, + IMorphoLiquidateCallback, + IMorphoRepayCallback +} from "@morpho-blue/interfaces/IMorphoCallbacks.sol"; + +import {Id, IMorpho, MarketParams, Market} from "@morpho-blue/interfaces/IMorpho.sol"; +import {SafeTransferLib, ERC20} from "@solmate/utils/SafeTransferLib.sol"; +import {MathLib} from "@morpho-blue/libraries/MathLib.sol"; +import {MorphoLib} from "@morpho-blue/libraries/periphery/MorphoLib.sol"; +import {MarketParamsLib} from "@morpho-blue/libraries/MarketParamsLib.sol"; + +/* +The SwapMock contract only has educational purpose. It simulates a contract allowing to swap a token against another, +with the exact price returned by an arbitrary oracle. + +The introduction of the SwapMock contract is to showcase the functioning of leverage on Morpho Blue (using callbacks) +without highlighting any known DEX. + +Therefore, SwapMock must be replaced (by the swap of your choice) in your implementation. The functions +`swapCollatToLoan` and `swapLoanToCollat` must as well be adapted to match the ones of the chosen swap contract. + +One should be aware that has to be taken into account on potential swap: + 1. slippage + 2. fees + +add a definition of what snippets are + */ +contract CallbacksSnippets is IMorphoSupplyCollateralCallback, IMorphoRepayCallback, IMorphoLiquidateCallback { + using MathLib for uint256; + using MorphoLib for IMorpho; + using MarketParamsLib for MarketParams; + using SafeTransferLib for ERC20; + + IMorpho public immutable morpho; + SwapMock swapMock; + + constructor(address morphoAddress) { + morpho = IMorpho(morphoAddress); + } + + /* + + Callbacks + + Reminder: for a given market, one can leverage his position up to a leverageFactor = 1/1-LLTV, + + Example : with a LLTV of 80% -> 5 is the max leverage factor + + */ + + function onMorphoSupplyCollateral(uint256 amount, bytes calldata data) external onlyMorpho { + (uint256 toBorrow, MarketParams memory marketParams, address user) = + abi.decode(data, (uint256, MarketParams, address)); + (uint256 amountBis,) = morpho.borrow(marketParams, toBorrow, 0, user, address(this)); + + ERC20(marketParams.loanToken).approve(address(swapMock), amount); + + // Logic to Implement. Following example is a swap, could be a 'unwrap + stake + wrap staked' for + // wETH(wstETH) Market + // _approveMaxTo(marketParams.); + swapMock.swapLoanToCollat(amountBis); + } + + function onMorphoLiquidate(uint256 repaidAssets, bytes calldata data) external onlyMorpho { + (uint256 toSwap, MarketParams memory marketParams) = abi.decode(data, (uint256, MarketParams)); + uint256 returnedAmount = swapMock.swapCollatToLoan(toSwap); + require(returnedAmount > repaidAssets); // Add logic for gas cost threshold for instance + ERC20(marketParams.loanToken).approve(address(swapMock), returnedAmount); + } + + function onMorphoRepay(uint256 amount, bytes calldata data) external onlyMorpho { + (MarketParams memory marketParams, address user) = abi.decode(data, (MarketParams, address)); + uint256 toWithdraw = morpho.collateral(marketParams.id(), user); + + morpho.withdrawCollateral(marketParams, toWithdraw, user, address(this)); + + ERC20(marketParams.collateralToken).approve(address(swapMock), amount); + swapMock.swapCollatToLoan(amount); + } + + function leverageMe( + uint256 leverageFactor, + uint256 initAmountCollateral, + SwapMock _swapMock, + MarketParams calldata marketParams + ) public { + _setSwapMock(_swapMock); + + ERC20(marketParams.collateralToken).safeTransferFrom(msg.sender, address(this), initAmountCollateral); + + uint256 finalAmountcollateral = initAmountCollateral * leverageFactor; + + // The amount of LoanToken to be borrowed (and then swapped against collateralToken) to perform the callback is + // the following : + + // (leverageFactor - 1) * InitAmountCollateral.mulDivDown.(ORACLE_PRICE_SCALE, IOracle(oracle).price()) + + // However here we have price = `ORACLE_PRICE_SCALE`, so loanAmount = (leverageFactor - 1) * + // InitAmountCollateral + + // Warning : When using real swaps, price doesn't equal `ORACLE_PRICE_SCALE` anymore, so + // mulDivDown.(ORACLE_PRICE_SCALE, IOracle(oracle).price()) can't be removed from the calculus, and therefore an + // oracle should be used to compute the correct amount. + // Warning : When using real swaps, fees and slippage should also be taken into account to compute `loanAmount`. + + uint256 loanAmount = (leverageFactor - 1) * initAmountCollateral; + + _approveMaxTo(marketParams.collateralToken, address(morpho)); + + morpho.supplyCollateral( + marketParams, finalAmountcollateral, msg.sender, abi.encode(loanAmount, marketParams, msg.sender) + ); + } + + function liquidateWithoutCollat( + address borrower, + uint256 loanAmountToRepay, + uint256 assetsToSeize, + SwapMock _swapMock, + MarketParams calldata marketParams + ) public returns (uint256 seizedAssets, uint256 repaidAssets) { + _setSwapMock(_swapMock); + + _approveMaxTo(address(marketParams.collateralToken), address(this)); + + uint256 repaidShares; + + (seizedAssets, repaidAssets) = + morpho.liquidate(marketParams, borrower, assetsToSeize, repaidShares, abi.encode(loanAmountToRepay)); + } + + function deLeverageMe(SwapMock _swapMock, MarketParams calldata marketParams) + public + returns (uint256 amountRepayed) + { + _setSwapMock(_swapMock); + + uint256 totalShares = morpho.borrowShares(marketParams.id(), msg.sender); + + _approveMaxTo(marketParams.loanToken, address(morpho)); + + (amountRepayed,) = morpho.repay(marketParams, 0, totalShares, msg.sender, abi.encode(marketParams, msg.sender)); + + ERC20(marketParams.collateralToken).safeTransfer( + msg.sender, ERC20(marketParams.collateralToken).balanceOf(msg.sender) + ); + } + + modifier onlyMorpho() { + require(msg.sender == address(morpho), "msg.sender should be Morpho Blue"); + _; + } + + function _approveMaxTo(address asset, address spender) internal { + if (ERC20(asset).allowance(address(this), spender) == 0) { + ERC20(asset).approve(spender, type(uint256).max); + } + } + + function _setSwapMock(SwapMock _swapMock) public { + swapMock = _swapMock; + } +} diff --git a/src/blue/mocks/SwapMock.sol b/src/blue/mocks/SwapMock.sol new file mode 100644 index 0000000..60fe7e8 --- /dev/null +++ b/src/blue/mocks/SwapMock.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import {ORACLE_PRICE_SCALE} from "@morpho-blue/libraries/ConstantsLib.sol"; + +import "@morpho-blue/mocks/ERC20Mock.sol"; +import {IOracle} from "@morpho-blue/interfaces/IOracle.sol"; + +import "@morpho-blue/libraries/MathLib.sol"; + +contract SwapMock { + using MathLib for uint256; + + ERC20Mock public immutable collateralToken; + ERC20Mock public immutable loanToken; + + address public immutable oracle; + + constructor(address collateralAddress, address loanAddress, address oracleAddress) { + collateralToken = ERC20Mock(collateralAddress); + loanToken = ERC20Mock(loanAddress); + + oracle = oracleAddress; + } + + function swapCollatToLoan(uint256 amount) external returns (uint256 returnedAmount) { + returnedAmount = amount.mulDivDown(IOracle(oracle).price(), ORACLE_PRICE_SCALE); + + collateralToken.transferFrom(msg.sender, address(this), amount); + + loanToken.setBalance(address(this), returnedAmount); + loanToken.transfer(msg.sender, returnedAmount); + } + + function swapLoanToCollat(uint256 amount) external returns (uint256 returnedAmount) { + returnedAmount = amount.mulDivDown(ORACLE_PRICE_SCALE, IOracle(oracle).price()); + + loanToken.transferFrom(msg.sender, address(this), amount); + + collateralToken.setBalance(address(this), returnedAmount); + collateralToken.transfer(msg.sender, returnedAmount); + } +} diff --git a/src/metamorpho/MetamorphoSnippets.sol b/src/metamorpho/MetamorphoSnippets.sol index 66dd792..c28049b 100644 --- a/src/metamorpho/MetamorphoSnippets.sol +++ b/src/metamorpho/MetamorphoSnippets.sol @@ -10,14 +10,12 @@ import {IrmMock} from "@metamorpho/mocks/IrmMock.sol"; import {MorphoBalancesLib} from "@morpho-blue/libraries/periphery/MorphoBalancesLib.sol"; import {MathLib, WAD} from "@morpho-blue/libraries/MathLib.sol"; -import {Math} from "@openzeppelin5/utils/math/Math.sol"; -import {ERC20} from "@openzeppelin5/token/ERC20/ERC20.sol"; +import {Math} from "@openzeppelin/utils/math/Math.sol"; +import {ERC20} from "@openzeppelin/token/ERC20/ERC20.sol"; contract MetamorphoSnippets { - uint256 constant FEE = 0.2 ether; // 20% - - IMetaMorpho public immutable vault; IMorpho public immutable morpho; + IMetaMorpho public immutable vault; using MathLib for uint256; using Math for uint256; @@ -36,8 +34,7 @@ contract MetamorphoSnippets { totalAssets = vault.lastTotalAssets(); } - /// @dev note that one can adapt the address in the call to the morpho contract - function vaultAmountInMarket(MarketParams memory marketParams) public view returns (uint256 vaultAmount) { + function vaultAssetsInMarket(MarketParams memory marketParams) public view returns (uint256 vaultAmount) { vaultAmount = morpho.expectedSupplyAssets(marketParams, address(vault)); } @@ -45,6 +42,7 @@ contract MetamorphoSnippets { totalSharesUser = vault.balanceOf(user); } + // The following function will return the current supply queue of the vault function supplyQueueVault() public view returns (Id[] memory supplyQueueList) { uint256 queueLength = vault.supplyQueueLength(); supplyQueueList = new Id[](queueLength); @@ -54,6 +52,7 @@ contract MetamorphoSnippets { return supplyQueueList; } + // The following function will return the current withdraw queue of the vault function withdrawQueueVault() public view returns (Id[] memory withdrawQueueList) { uint256 queueLength = vault.supplyQueueLength(); withdrawQueueList = new Id[](queueLength); @@ -68,7 +67,6 @@ contract MetamorphoSnippets { cap = vault.config(id).cap; } - // TO TEST function supplyAPRMarket(MarketParams memory marketParams, Market memory market) public view @@ -85,11 +83,6 @@ contract MetamorphoSnippets { supplyRate = borrowRate.wMulDown(1 ether - market.fee).wMulDown(utilization); } - // TODO: edit comment + Test function - // same function as Morpho Blue Snippets - // a amount at 6%, B amount at 3 %: - // (a*6%) + (B*3%) / (a+b+ IDLE) - function supplyAPRVault() public view returns (uint256 avgSupplyRate) { uint256 ratio; uint256 queueLength = vault.withdrawQueueLength(); @@ -100,7 +93,6 @@ contract MetamorphoSnippets { for (uint256 i; i < queueLength; ++i) { Id idMarket = vault.withdrawQueue(i); - // To change once the cantina-review branch is merged (address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) = (morpho.idToMarketParams(idMarket)); @@ -108,7 +100,7 @@ contract MetamorphoSnippets { Market memory market = morpho.market(idMarket); uint256 currentSupplyAPR = supplyAPRMarket(marketParams, market); - uint256 vaultAsset = vaultAmountInMarket(marketParams); + uint256 vaultAsset = vaultAssetsInMarket(marketParams); ratio += currentSupplyAPR.wMulDown(vaultAsset); } @@ -122,14 +114,24 @@ contract MetamorphoSnippets { shares = vault.deposit(assets, onBehalf); } - function withdrawFromVault(uint256 assets, address onBehalf) public returns (uint256 redeemed) { + // withdraw from the vault a nb of asset + function withdrawFromVaultAmount(uint256 assets, address onBehalf) public returns (uint256 redeemed) { address receiver = onBehalf; redeemed = vault.withdraw(assets, receiver, onBehalf); } - function redeemAllFromVault(address receiver) public returns (uint256 redeemed) { + // maxWithdraw from the vault + function withdrawFromVaultAll(address onBehalf) public returns (uint256 redeemed) { + address receiver = onBehalf; + uint256 assets = vault.maxWithdraw(address(this)); + redeemed = vault.withdraw(assets, receiver, onBehalf); + } + + // maxRedeem from the vault + function redeemAllFromVault(address onBehalf) public returns (uint256 redeemed) { + address receiver = onBehalf; uint256 maxToRedeem = vault.maxRedeem(address(this)); - redeemed = vault.redeem(maxToRedeem, receiver, address(this)); + redeemed = vault.redeem(maxToRedeem, receiver, onBehalf); } // TODO: diff --git a/test/forge/blue/TestBlueSnippets.sol b/test/forge/blue/TestBlueSnippets.sol index bc70501..4533aab 100644 --- a/test/forge/blue/TestBlueSnippets.sol +++ b/test/forge/blue/TestBlueSnippets.sol @@ -10,7 +10,7 @@ import {MathLib} from "@morpho-blue/libraries/MathLib.sol"; import {SharesMathLib} from "@morpho-blue/libraries/SharesMathLib.sol"; // we need to import everything in there -import "@morpho-blue-test/forge/BaseTest.sol"; +import "@morpho-blue-test/BaseTest.sol"; contract TestIntegrationSnippets is BaseTest { using MathLib for uint256; @@ -130,7 +130,7 @@ contract TestIntegrationSnippets is BaseTest { vm.assume(market.totalSupplyAssets >= market.totalBorrowAssets); (uint256 totalSupplyAssets,, uint256 totalBorrowAssets,) = morpho.expectedMarketBalances(marketParams); - uint256 borrowTrue = irm.borrowRate(marketParams, market); + uint256 borrowTrue = irm.borrowRateView(marketParams, market); uint256 utilization = totalBorrowAssets == 0 ? 0 : totalBorrowAssets.wDivUp(totalSupplyAssets); uint256 supplyTrue = borrowTrue.wMulDown(1 ether - market.fee).wMulDown(utilization); diff --git a/test/forge/blue/TestCallbacksSnippets.sol b/test/forge/blue/TestCallbacksSnippets.sol new file mode 100644 index 0000000..9f87698 --- /dev/null +++ b/test/forge/blue/TestCallbacksSnippets.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import "@morpho-blue-test/BaseTest.sol"; +import {SwapMock} from "@snippets/blue/mocks/SwapMock.sol"; +import {CallbacksSnippets} from "@snippets/blue/CallbacksSnippets.sol"; + +contract CallbacksIntegrationTest is BaseTest { + using MathLib for uint256; + using MorphoLib for IMorpho; + using MorphoBalancesLib for IMorpho; + using MarketParamsLib for MarketParams; + using SharesMathLib for uint256; + + address internal USER; + + SwapMock internal swapMock; + + CallbacksSnippets public snippets; + + function setUp() public virtual override { + super.setUp(); + + USER = makeAddr("User"); + + swapMock = new SwapMock(address(collateralToken), address(loanToken), address(oracle)); + snippets = new CallbacksSnippets(address(morpho)); // todos add the addres of WETH, lido, wsteth + + vm.startPrank(USER); + collateralToken.approve(address(snippets), type(uint256).max); + morpho.setAuthorization(address(snippets), true); + vm.stopPrank(); + } + + function testLeverageMe(uint256 initAmountCollateral) public { + // INITIALISATION + + uint256 leverageFactor = 4; // nb to set + + initAmountCollateral = bound(initAmountCollateral, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT / leverageFactor); + uint256 finalAmountCollateral = initAmountCollateral * leverageFactor; + + oracle.setPrice(ORACLE_PRICE_SCALE); + + // supplying enough liquidity in the market + vm.startPrank(SUPPLIER); + loanToken.setBalance(address(SUPPLIER), finalAmountCollateral); + morpho.supply(marketParams, finalAmountCollateral, 0, address(SUPPLIER), hex""); + vm.stopPrank(); + + collateralToken.setBalance(USER, initAmountCollateral); + vm.prank(USER); + snippets.leverageMe(leverageFactor, initAmountCollateral, swapMock, marketParams); + + uint256 loanAmount = initAmountCollateral * (leverageFactor - 1); + + assertGt(morpho.borrowShares(marketParams.id(), USER), 0, "no borrow"); + assertEq(morpho.collateral(marketParams.id(), USER), finalAmountCollateral, "no collateral"); + assertEq(morpho.expectedBorrowAssets(marketParams, USER), loanAmount, "no collateral"); + } + + function testDeLeverageMe(uint256 initAmountCollateral) public { + /// same as testLeverageMe + + uint256 leverageFactor = 4; // nb to set + + initAmountCollateral = bound(initAmountCollateral, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT / leverageFactor); + uint256 finalAmountCollateral = initAmountCollateral * leverageFactor; + + oracle.setPrice(ORACLE_PRICE_SCALE); + + vm.startPrank(SUPPLIER); + loanToken.setBalance(address(SUPPLIER), finalAmountCollateral); + morpho.supply(marketParams, finalAmountCollateral, 0, address(SUPPLIER), hex""); + vm.stopPrank(); + + uint256 loanAmount = initAmountCollateral * (leverageFactor - 1); + + collateralToken.setBalance(USER, initAmountCollateral); + vm.prank(USER); + snippets.leverageMe(leverageFactor, initAmountCollateral, swapMock, marketParams); + + assertGt(morpho.borrowShares(marketParams.id(), USER), 0, "no borrow"); + assertEq(morpho.collateral(marketParams.id(), USER), finalAmountCollateral, "no collateral"); + assertEq(morpho.expectedBorrowAssets(marketParams, USER), loanAmount, "no collateral"); + + /// end of testLeverageMe + vm.prank(USER); + uint256 amountRepayed = snippets.deLeverageMe(swapMock, marketParams); + + assertEq(morpho.borrowShares(marketParams.id(), USER), 0, "no borrow"); + assertEq(amountRepayed, loanAmount, "no repaid"); + } + + struct LiquidateTestParams { + uint256 amountCollateral; + uint256 amountSupplied; + uint256 amountBorrowed; + uint256 priceCollateral; + uint256 lltv; + } + + // TODOS: implement the following function + // function testLiquidateWithoutCollateral(LiquidateTestParams memory params, uint256 amountSeized) public { + // _setLltv(_boundTestLltv(params.lltv)); + // (params.amountCollateral, params.amountBorrowed, params.priceCollateral) = + // _boundUnhealthyPosition(params.amountCollateral, params.amountBorrowed, params.priceCollateral); + + // vm.assume(params.amountCollateral > 1); + + // params.amountSupplied = + // bound(params.amountSupplied, params.amountBorrowed, params.amountBorrowed + MAX_TEST_AMOUNT); + // _supply(params.amountSupplied); + + // collateralToken.setBalance(BORROWER, params.amountCollateral); + + // oracle.setPrice(type(uint256).max / params.amountCollateral); + + // vm.startPrank(BORROWER); + // morpho.supplyCollateral(marketParams, params.amountCollateral, BORROWER, hex""); + // morpho.borrow(marketParams, params.amountBorrowed, 0, BORROWER, BORROWER); + // vm.stopPrank(); + + // oracle.setPrice(params.priceCollateral); + + // // uint256 borrowShares = morpho.borrowShares(id, BORROWER); + // uint256 liquidationIncentiveFactor = _liquidationIncentiveFactor(marketParams.lltv); + // uint256 maxSeized = params.amountBorrowed.wMulDown(liquidationIncentiveFactor).mulDivDown( + // ORACLE_PRICE_SCALE, params.priceCollateral + // ); + // vm.assume(maxSeized != 0); + + // amountSeized = bound(amountSeized, 1, Math.min(maxSeized, params.amountCollateral - 1)); + + // uint256 expectedRepaid = + // amountSeized.mulDivUp(params.priceCollateral, ORACLE_PRICE_SCALE).wDivUp(liquidationIncentiveFactor); + // // uint256 expectedRepaidShares = + // // expectedRepaid.toSharesDown(morpho.totalBorrowAssets(id), morpho.totalBorrowShares(id)); + + // vm.startPrank(address(snippets)); + // loanToken.approve(address(morpho), type(uint256).max); + // loanToken.approve(address(swapMock), type(uint256).max); + // collateralToken.approve(address(morpho), type(uint256).max); + // collateralToken.approve(address(swapMock), type(uint256).max); + // loanToken.approve(address(snippets), type(uint256).max); + // collateralToken.approve(address(snippets), type(uint256).max); + // loanToken.setBalance(address(snippets), params.amountBorrowed); + + // // vm.prank(LIQUIDATOR); + + // (uint256 returnSeized, uint256 returnRepaid) = + // snippets.liquidateWithoutCollat(BORROWER, params.amountBorrowed, amountSeized, swapMock, marketParams); + // // morpho.liquidate(marketParams, BORROWER, amountSeized, 0, hex""); + // // uint256 expectedCollateral = params.amountCollateral - amountSeized; + // // uint256 expectedBorrowed = params.amountBorrowed - expectedRepaid; + // // uint256 expectedBorrowShares = borrowShares - expectedRepaidShares; + + // assertEq(returnSeized, amountSeized, "returned seized amount"); + // assertEq(returnRepaid, expectedRepaid, "returned asset amount"); + // // assertEq(morpho.borrowShares(id, BORROWER), expectedBorrowShares, "borrow shares"); + // // assertEq(morpho.totalBorrowAssets(id), expectedBorrowed, "total borrow"); + // // assertEq(morpho.totalBorrowShares(id), expectedBorrowShares, "total borrow shares"); + // // assertEq(morpho.collateral(id, BORROWER), expectedCollateral, "collateral"); + // // assertEq(loanToken.balanceOf(BORROWER), params.amountBorrowed, "borrower balance"); + // // assertEq(loanToken.balanceOf(LIQUIDATOR), expectedBorrowed, "liquidator balance"); + // // assertEq(loanToken.balanceOf(address(morpho)), params.amountSupplied - expectedBorrowed, "morpho + // balance"); + // // assertEq(collateralToken.balanceOf(address(morpho)), expectedCollateral, "morpho collateral balance"); + // // assertEq(collateralToken.balanceOf(LIQUIDATOR), amountSeized, "liquidator collateral balance"); + // } +} diff --git a/test/forge/blue/mocks/TestSwapMock.sol b/test/forge/blue/mocks/TestSwapMock.sol new file mode 100644 index 0000000..334f252 --- /dev/null +++ b/test/forge/blue/mocks/TestSwapMock.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import "@morpho-blue-test/BaseTest.sol"; +import {SwapMock} from "@snippets/blue/mocks/SwapMock.sol"; + +contract TestIntegrationSnippets is BaseTest { + SwapMock internal swapMock; + + function setUp() public virtual override { + super.setUp(); + swapMock = new SwapMock(address(collateralToken), address(loanToken), address(oracle)); + } + + function testSwapCollatToLoan(uint256 amount) public { + amount = bound(amount, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT); + collateralToken.setBalance(address(this), amount); + collateralToken.approve(address(swapMock), type(uint256).max); + + uint256 swappedAssets = swapMock.swapCollatToLoan(amount); + assertEq(swappedAssets, amount, " error in swap"); + } + + function testSwapLoanToCollat(uint256 amount) public { + amount = bound(amount, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT); + loanToken.setBalance(address(this), amount); + loanToken.approve(address(swapMock), type(uint256).max); + + uint256 swappedAssets = swapMock.swapLoanToCollat(amount); + assertEq(swappedAssets, amount, " error in swap"); + } +} diff --git a/test/forge/metamorpho/TestMetamorphoSnippets.sol b/test/forge/metamorpho/TestMetamorphoSnippets.sol index 8f961b7..60b775d 100644 --- a/test/forge/metamorpho/TestMetamorphoSnippets.sol +++ b/test/forge/metamorpho/TestMetamorphoSnippets.sol @@ -4,7 +4,8 @@ pragma solidity ^0.8.0; import {MetamorphoSnippets} from "@snippets/metamorpho/MetamorphoSnippets.sol"; import "@metamorpho-test/helpers/IntegrationTest.sol"; import "@morpho-blue/libraries/SharesMathLib.sol"; -import {SafeCast} from "@openzeppelin5/utils/math/SafeCast.sol"; +import {MorphoBalancesLib} from "@morpho-blue/libraries/periphery/MorphoBalancesLib.sol"; +import {SafeCast} from "@openzeppelin/utils/math/SafeCast.sol"; contract TestIntegrationSnippets is IntegrationTest { MetamorphoSnippets internal snippets; @@ -19,6 +20,8 @@ contract TestIntegrationSnippets is IntegrationTest { function setUp() public virtual override { super.setUp(); snippets = new MetamorphoSnippets(address(vault), address(morpho)); + _setCap(allMarkets[0], CAP); + _sortSupplyQueueIdleLast(); } function testTotalDepositVault(uint256 deposited) public { @@ -43,24 +46,7 @@ contract TestIntegrationSnippets is IntegrationTest { assertEq(firstDeposit + secondDeposit, snippetTotalAsset2, "lastTotalAssets2"); } - function testDeposit(uint256 assets) public { - _setCap(allMarkets[0], CAP); - assets = bound(assets, MIN_TEST_ASSETS, MAX_TEST_ASSETS); - - loanToken.setBalance(SUPPLIER, assets); - - vm.expectEmit(); - emit EventsLib.UpdateLastTotalAssets(vault.totalAssets() + assets); - vm.prank(SUPPLIER); - uint256 shares = vault.deposit(assets, ONBEHALF); - - assertGt(shares, 0, "shares"); - assertEq(vault.balanceOf(ONBEHALF), shares, "balanceOf(ONBEHALF)"); - assertEq(morpho.expectedSupplyAssets(allMarkets[0], address(vault)), assets, "expectedSupplyAssets(vault)"); - } - - function testVaultAmountInMarket(uint256 assets) public { - _setCap(allMarkets[0], CAP); + function testVaultAssetsInMarket(uint256 assets) public { assets = bound(assets, MIN_TEST_ASSETS, MAX_TEST_ASSETS); loanToken.setBalance(SUPPLIER, assets); @@ -72,7 +58,7 @@ contract TestIntegrationSnippets is IntegrationTest { assertEq(vault.balanceOf(ONBEHALF), shares, "balanceOf(ONBEHALF)"); assertEq(morpho.expectedSupplyAssets(allMarkets[0], address(vault)), assets, "expectedSupplyAssets(vault)"); - uint256 vaultAmount = snippets.vaultAmountInMarket(allMarkets[0]); + uint256 vaultAmount = snippets.vaultAssetsInMarket(allMarkets[0]); assertEq(assets, vaultAmount, "expectedSupplyAssets(vault)"); } @@ -91,11 +77,6 @@ contract TestIntegrationSnippets is IntegrationTest { function testSupplyQueueVault() public { _setCaps(); - - assertEq(Id.unwrap(vault.supplyQueue(0)), Id.unwrap(allMarkets[0].id())); - assertEq(Id.unwrap(vault.supplyQueue(1)), Id.unwrap(allMarkets[1].id())); - assertEq(Id.unwrap(vault.supplyQueue(2)), Id.unwrap(allMarkets[2].id())); - Id[] memory supplyQueue = new Id[](2); supplyQueue[0] = allMarkets[1].id(); supplyQueue[1] = allMarkets[2].id(); @@ -103,9 +84,10 @@ contract TestIntegrationSnippets is IntegrationTest { vm.prank(ALLOCATOR); vault.setSupplyQueue(supplyQueue); - Id[] memory supplyQueueList = snippets.supplyQueueVault(); assertEq(Id.unwrap(vault.supplyQueue(0)), Id.unwrap(allMarkets[1].id())); assertEq(Id.unwrap(vault.supplyQueue(1)), Id.unwrap(allMarkets[2].id())); + + Id[] memory supplyQueueList = snippets.supplyQueueVault(); assertEq(Id.unwrap(supplyQueueList[0]), Id.unwrap(allMarkets[1].id())); assertEq(Id.unwrap(supplyQueueList[1]), Id.unwrap(allMarkets[2].id())); } @@ -113,33 +95,34 @@ contract TestIntegrationSnippets is IntegrationTest { function testWithdrawQueueVault() public { _setCaps(); - assertEq(Id.unwrap(vault.withdrawQueue(0)), Id.unwrap(allMarkets[0].id())); - assertEq(Id.unwrap(vault.withdrawQueue(1)), Id.unwrap(allMarkets[1].id())); - assertEq(Id.unwrap(vault.withdrawQueue(2)), Id.unwrap(allMarkets[2].id())); - - uint256[] memory indexes = new uint256[](3); + uint256[] memory indexes = new uint256[](4); indexes[0] = 1; indexes[1] = 2; - indexes[2] = 0; + indexes[2] = 3; + indexes[3] = 0; - Id[] memory expectedWithdrawQueue = new Id[](3); - expectedWithdrawQueue[0] = allMarkets[1].id(); - expectedWithdrawQueue[1] = allMarkets[2].id(); - expectedWithdrawQueue[2] = allMarkets[0].id(); + Id[] memory expectedWithdrawQueue = new Id[](4); + expectedWithdrawQueue[0] = allMarkets[0].id(); + expectedWithdrawQueue[1] = allMarkets[1].id(); + expectedWithdrawQueue[2] = allMarkets[2].id(); + expectedWithdrawQueue[3] = idleParams.id(); + vm.expectEmit(address(vault)); + emit EventsLib.SetWithdrawQueue(ALLOCATOR, expectedWithdrawQueue); vm.prank(ALLOCATOR); vault.updateWithdrawQueue(indexes); - Id[] memory withdrawQueueList = snippets.withdrawQueueVault(); assertEq(Id.unwrap(vault.withdrawQueue(0)), Id.unwrap(expectedWithdrawQueue[0])); assertEq(Id.unwrap(vault.withdrawQueue(1)), Id.unwrap(expectedWithdrawQueue[1])); assertEq(Id.unwrap(vault.withdrawQueue(2)), Id.unwrap(expectedWithdrawQueue[2])); + assertEq(Id.unwrap(vault.withdrawQueue(3)), Id.unwrap(expectedWithdrawQueue[3])); + + Id[] memory withdrawQueueList = snippets.withdrawQueueVault(); assertEq(Id.unwrap(withdrawQueueList[0]), Id.unwrap(expectedWithdrawQueue[0])); assertEq(Id.unwrap(withdrawQueueList[1]), Id.unwrap(expectedWithdrawQueue[1])); } - // OK function testCapMarket(MarketParams memory marketParams) public { Id idMarket = marketParams.id(); uint192 cap = vault.config(idMarket).cap; @@ -147,44 +130,120 @@ contract TestIntegrationSnippets is IntegrationTest { assertEq(cap, snippetCap, "cap per market"); } - // TODO Implement the TEST SUPPLY APR EQUAL 0 Function - // function testSupplyAPREqual0(MarketParams memory marketParams, Market memory market) public { - // vm.assume(market.totalBorrowAssets == 0); - // vm.assume(market.totalBorrowShares == 0); - // vm.assume(market.totalSupplyAssets > 100000); - // vm.assume(market.lastUpdate > 0); - // vm.assume(market.fee < 1 ether); - // vm.assume(market.totalSupplyAssets >= market.totalBorrowAssets); - - // (uint256 totalSupplyAssets,, uint256 totalBorrowAssets,) = morpho.expectedMarketAssetss(marketParams); - // uint256 borrowTrue = irm.borrowRate(marketParams, market); - // uint256 utilization = totalBorrowAssets == 0 ? 0 : totalBorrowAssets.wDivUp(totalSupplyAssets); - - // uint256 supplyTrue = borrowTrue.wMulDown(1 ether - market.fee).wMulDown(utilization); - // uint256 supplyToTest = snippets.supplyAPRMarket(marketParams, market); - // assertEq(supplyTrue, 0, "Diff in snippets vs integration supplyAPR test"); - // assertEq(supplyToTest, 0, "Diff in snippets vs integration supplyAPR test"); - // } - - // TODO Implement the TEST SUPPLY APR Function - // function testSupplyAPRMarket(MarketParams memory marketParams, Market memory market) public { - // vm.assume(market.totalBorrowAssets > 0); - // vm.assume(market.fee < 1 ether); - // vm.assume(market.totalSupplyAssets >= market.totalBorrowAssets); - - // (uint256 totalSupplyAssets,, uint256 totalBorrowAssets,) = morpho.expectedMarketAssetss(marketParams); - // uint256 borrowTrue = irm.borrowRate(marketParams, market); - // assertGt(borrowTrue, 0, "intermediary test"); - // uint256 utilization = totalBorrowAssets == 0 ? 0 : totalBorrowAssets.wDivUp(totalSupplyAssets); - // assertGt(utilization, 0, "intermediary test"); - // uint256 supplyTrue = borrowTrue.wMulDown(1 ether - market.fee).wMulDown(utilization); - // // assertGt(supplyTrue, 0, "intermediary test"); - // // uint256 supplyToTest = snippets.supplyAPRMarket(marketParams, market); - - // // assertEq(supplyTrue, supplyToTest, "Diff in snippets vs integration supplyAPR test"); - // } - - // TODO Implement the TEST SUPPLY APR Vault Function + function testSupplyAPR0(Market memory market) public { + vm.assume(market.totalBorrowAssets == 0); + vm.assume(market.lastUpdate > 0); + vm.assume(market.fee < 1 ether); + vm.assume(market.totalSupplyAssets >= market.totalBorrowAssets); + MarketParams memory marketParams = allMarkets[0]; + (uint256 totalSupplyAssets,, uint256 totalBorrowAssets,) = morpho.expectedMarketBalances(marketParams); + uint256 borrowTrue = irm.borrowRateView(marketParams, market); + uint256 utilization = totalBorrowAssets == 0 ? 0 : totalBorrowAssets.wDivUp(totalSupplyAssets); + + uint256 supplyTrue = borrowTrue.wMulDown(1 ether - market.fee).wMulDown(utilization); + uint256 supplyToTest = snippets.supplyAPRMarket(marketParams, market); + assertEq(utilization, 0, "Diff in snippets vs integration supplyAPR test"); + assertEq(supplyTrue, 0, "Diff in snippets vs integration supplyAPR test"); + assertEq(supplyToTest, 0, "Diff in snippets vs integration supplyAPR test"); + } + + function testSupplyAPRMarket(Market memory market) public { + vm.assume(market.totalBorrowAssets > 0); + vm.assume(market.totalBorrowShares > 0); + vm.assume(market.totalSupplyAssets > 0); + vm.assume(market.totalSupplyShares > 0); + vm.assume(market.fee < 1 ether); + vm.assume(market.totalSupplyAssets >= market.totalBorrowAssets); + + MarketParams memory marketParams = allMarkets[0]; + (uint256 totalSupplyAssets,, uint256 totalBorrowAssets,) = morpho.expectedMarketBalances(marketParams); + + uint256 borrowTrue = irm.borrowRateView(marketParams, market); + uint256 utilization = totalBorrowAssets == 0 ? 0 : totalBorrowAssets.wDivUp(totalSupplyAssets); + uint256 supplyTrue = borrowTrue.wMulDown(1 ether - market.fee).wMulDown(utilization); + + uint256 supplyToTest = snippets.supplyAPRMarket(marketParams, market); + + // handling in if-else the situation where utilization = 0 otherwise too many rejects + if (utilization == 0) { + assertEq(supplyTrue, 0, "supply rate ==0"); + assertEq(supplyTrue, supplyToTest, "Diff in snippets vs integration supplyAPR test"); + } else { + assertGt(supplyTrue, 0, "supply rate ==0"); + assertEq(supplyTrue, supplyToTest, "Diff in snippets vs integration supplyAPR test"); + } + } + + // TODO: enhance the test + function testSupplyAPRVault(uint256 deposited) public { + // set 2 suppliers and 1 borrower + uint256 firstDeposit = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS / 2); + uint256 secondDeposit = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS / 2); + _setCap(allMarkets[0], firstDeposit); + _setCap(allMarkets[1], secondDeposit); + + Id[] memory supplyQueue = new Id[](2); + supplyQueue[0] = allMarkets[0].id(); + supplyQueue[1] = allMarkets[1].id(); + + vm.prank(ALLOCATOR); + vault.setSupplyQueue(supplyQueue); + loanToken.setBalance(SUPPLIER, firstDeposit); + + vm.prank(SUPPLIER); + vault.deposit(firstDeposit, ONBEHALF); + + uint256 snippetTotalAsset = snippets.totalDepositVault(); + + assertEq(firstDeposit, snippetTotalAsset, "lastTotalAssets"); + + loanToken.setBalance(SUPPLIER, secondDeposit); + vm.prank(SUPPLIER); + vault.deposit(secondDeposit, ONBEHALF); + + uint256 snippetTotalAsset2 = snippets.totalDepositVault(); + assertEq(firstDeposit + secondDeposit, snippetTotalAsset2, "lastTotalAssets2"); + + collateralToken.setBalance(BORROWER, type(uint256).max); + vm.startPrank(BORROWER); + morpho.supplyCollateral(allMarkets[0], MAX_TEST_ASSETS, BORROWER, hex""); + morpho.borrow(allMarkets[0], firstDeposit, 0, BORROWER, BORROWER); + vm.stopPrank(); + + // // in the current state: borrower borrowed some liquidity in market 0 + + collateralToken.setBalance(BORROWER, type(uint256).max - MAX_TEST_ASSETS); + vm.startPrank(BORROWER); + morpho.supplyCollateral(allMarkets[1], MAX_TEST_ASSETS, BORROWER, hex""); + morpho.borrow(allMarkets[1], secondDeposit / 4, 0, BORROWER, BORROWER); + vm.stopPrank(); + + // in the current state: borrower borrowed some liquidity in market 1 as well, up to 1/4 of the liquidity + + uint256 avgSupplyRateSnippets = snippets.supplyAPRVault(); + assertGt(avgSupplyRateSnippets, 0, "avgSupplyRateSnippets ==0"); + + // market 0: utilization 100% -> firstDeposit*50% + secondDeposit * (50%/4) (only a quarter is borrowed) + _setFee(0); + + Id id0 = Id(allMarkets[0].id()); + + Id id1 = Id(allMarkets[1].id()); + Market memory market0 = morpho.market(id0); + Market memory market1 = morpho.market(id1); + + uint256 rateMarket0 = snippets.supplyAPRMarket(allMarkets[0], market0); + uint256 rateMarket1 = snippets.supplyAPRMarket(allMarkets[1], market1); + assertGt(rateMarket0, 0, "supply rate ==0"); + assertGt(rateMarket1, 0, "supply rate ==0"); + + uint256 avgRateNum = rateMarket0.wMulDown(firstDeposit) + rateMarket1.wMulDown(secondDeposit); + // uint256 totalDeposited = + // uint256 avgRate = avgRateNum.wDivDown(firstDeposit+secondDeposit) + uint256 avgRate = (firstDeposit + secondDeposit) == 0 ? 0 : avgRateNum.wDivUp(firstDeposit + secondDeposit); + assertGt(avgRate, 0, "supply rate ==0"); + assertEq(avgSupplyRateSnippets, avgRate, "avgSupplyRateSnippets ==0"); + } // MANAGING FUNCTION @@ -220,7 +279,7 @@ contract TestIntegrationSnippets is IntegrationTest { uint256 shares = vault.deposit(deposited, address(snippets)); - uint256 redeemed = snippets.withdrawFromVault(withdrawn, address(snippets)); + uint256 redeemed = snippets.withdrawFromVaultAmount(withdrawn, address(snippets)); vm.stopPrank(); assertEq(vault.balanceOf(address(snippets)), shares - redeemed, "balanceOf(address(snippets))"); @@ -241,7 +300,7 @@ contract TestIntegrationSnippets is IntegrationTest { assertEq(vault.maxWithdraw(address(snippets)), assets, "maxWithdraw(ONBEHALF)"); - uint256 redeemed = snippets.withdrawFromVault(assets, address(snippets)); + uint256 redeemed = snippets.withdrawFromVaultAll(address(snippets)); vm.stopPrank(); assertEq(redeemed, minted, "shares"); @@ -250,7 +309,7 @@ contract TestIntegrationSnippets is IntegrationTest { assertEq(morpho.expectedSupplyAssets(allMarkets[0], address(vault)), 0, "expectedSupplyAssets(vault)"); } - function testRedeemAll(uint256 deposited) public { + function testRedeemAllFromVault(uint256 deposited) public { deposited = bound(deposited, MIN_TEST_ASSETS, MAX_TEST_ASSETS); loanToken.setBalance(address(snippets), deposited);