diff --git a/.changeset/brave-lions-swim.md b/.changeset/brave-lions-swim.md new file mode 100644 index 000000000..527423f26 --- /dev/null +++ b/.changeset/brave-lions-swim.md @@ -0,0 +1,5 @@ +--- +"@exactly/protocol": patch +--- + +✨ adapter: use morpho flash loans diff --git a/.gas-snapshot b/.gas-snapshot index 3237eca19..37571540d 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -49,7 +49,7 @@ DebtManagerTest:testFloatingToFixedRollHigherThanAvailableLiquidityWithSlippage( DebtManagerTest:testFloatingToFixedRollHigherThanAvailableLiquidityWithSlippageWithThreePools() (gas: 2818426) DebtManagerTest:testFloatingToFixedRollWithAccurateSlippage() (gas: 1732739) DebtManagerTest:testFloatingToFixedRollWithAccurateSlippageWithPreviousPosition() (gas: 1954939) -DebtManagerTest:testFuzzRolls(uint8[4],uint8[4],uint256[4],uint40[4],uint8[4]) (runs: 256, μ: 8031714, ~: 8129987) +DebtManagerTest:testFuzzRolls(uint8[4],uint8[4],uint256[4],uint40[4],uint8[4]) (runs: 256, μ: 8000226, ~: 8068563) DebtManagerTest:testLateFixedRoll() (gas: 1454770) DebtManagerTest:testLateFixedRollWithThreeLoops() (gas: 2043936) DebtManagerTest:testLateFixedToFloatingRoll() (gas: 1449634) @@ -165,13 +165,17 @@ FirewallTest:test_allow_reverts_whenNotOriginalAllower() (gas: 171661) FirewallTest:test_disallow_disallows_whenAllowedAndOriginalAllower() (gas: 116906) FirewallTest:test_disallow_disallows_whenAllowedAndOriginalAllowerRoleIsRevoked() (gas: 217133) FirewallTest:test_disallow_reverts_whenAllowedAndNotOriginalAllower() (gas: 171638) -FlashLoanAdapterTest:test_consumeAdapter() (gas: 2334825) -FlashLoanAdapterTest:test_consumeAdapter_withWToken() (gas: 8860215) -FlashLoanAdapterTest:test_flashLoan_reverts_whenAmountsLengthNotOne() (gas: 32678) -FlashLoanAdapterTest:test_flashLoan_reverts_whenTokensLengthNotOne() (gas: 32591) -FlashLoanAdapterTest:test_setWToken_emitsWTokenSet() (gas: 80696) -FlashLoanAdapterTest:test_setWToken_reverts_whenNotAdmin() (gas: 66649) -FlashLoanAdapterTest:test_setWToken_sets_whenAdmin() (gas: 82048) +FlashLoanAdapterTest:test_flashLoan_forwardsArguments() (gas: 135720) +FlashLoanAdapterTest:test_flashLoan_lends_fullBalance_usdc() (gas: 228513) +FlashLoanAdapterTest:test_flashLoan_lends_fullBalance_weth() (gas: 180042) +FlashLoanAdapterTest:test_flashLoan_lends_usdc(uint256) (runs: 256, μ: 246067, ~: 246305) +FlashLoanAdapterTest:test_flashLoan_lends_whenCallerIsNotRecipient() (gas: 228715) +FlashLoanAdapterTest:test_flashLoan_reverts_whenAmountsLengthNotOne() (gas: 35030) +FlashLoanAdapterTest:test_flashLoan_reverts_whenOverAvailable() (gas: 84243) +FlashLoanAdapterTest:test_flashLoan_reverts_whenTokensLengthNotOne() (gas: 35731) +FlashLoanAdapterTest:test_flashLoan_reverts_whenZeroAmount() (gas: 44160) +FlashLoanAdapterTest:test_onMorphoFlashLoan_reverts_whenNotMorpho() (gas: 327385) +FlashLoanAdapterTest:test_rollFixed_rolls_withDebtRoller() (gas: 4580555) InstallmentsRouterTest:testAmountsLength() (gas: 36806) InstallmentsRouterTest:testBorrowETHToAnotherReceiver() (gas: 801220) InstallmentsRouterTest:testBorrowETHWithPermit() (gas: 1239104) @@ -194,18 +198,18 @@ IntegrationPreviewerTest:test_fixedRepayPosition_beforeMaturity() (gas: 579991) IntegrationPreviewerTest:test_fixedRepayPosition_maxUint() (gas: 79468) IntegrationPreviewerTest:test_fixedRepayPosition_saturatedFallback() (gas: 63069) IntegrationPreviewerTest:test_healthFactor() (gas: 463879) -IntegrationPreviewerTest:test_previewDeposit_(uint256) (runs: 256, μ: 1590393, ~: 1594456) +IntegrationPreviewerTest:test_previewDeposit_(uint256) (runs: 256, μ: 1590434, ~: 1594456) IntegrationPreviewerTest:test_previewHealthFactor() (gas: 5805294) -IntegrationPreviewerTest:test_previewWithdraw_(uint256) (runs: 256, μ: 1649847, ~: 1654116) +IntegrationPreviewerTest:test_previewWithdraw_(uint256) (runs: 256, μ: 1649893, ~: 1654116) InterestRateModelTest:testFixedBorrowRate() (gas: 2088868) InterestRateModelTest:testFixedRateRevertAlreadyMatured() (gas: 2083135) InterestRateModelTest:testFixedRateRevertUtilizationExceeded() (gas: 2097203) InterestRateModelTest:testFloatingBorrowRate() (gas: 2082456) -InterestRateModelTest:testFuzzFixedRateGrowth(uint256,uint256,uint256,uint256) (runs: 256, μ: 2109079, ~: 2105195) -InterestRateModelTest:testFuzzFixedRateTimeSensitivity(uint256,uint256,uint256) (runs: 256, μ: 2117002, ~: 2117161) -InterestRateModelTest:testFuzzReferenceLegacyRateFixed(uint32,uint256,uint256[2],uint256[2],uint256,uint256,uint256) (runs: 256, μ: 12909428, ~: 13130610) -InterestRateModelTest:testFuzzReferenceRateFixed(uint256,uint256,uint256,uint256,uint256,uint256,(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,int256,uint256,uint256)) (runs: 256, μ: 2384608, ~: 2386846) -InterestRateModelTest:testFuzzReferenceRateFloating(uint256,uint256,(uint256,uint256,uint256,uint256,uint256,uint256,uint256)) (runs: 256, μ: 2324354, ~: 2324970) +InterestRateModelTest:testFuzzFixedRateGrowth(uint256,uint256,uint256,uint256) (runs: 256, μ: 2108931, ~: 2105195) +InterestRateModelTest:testFuzzFixedRateTimeSensitivity(uint256,uint256,uint256) (runs: 256, μ: 2116999, ~: 2117161) +InterestRateModelTest:testFuzzReferenceLegacyRateFixed(uint32,uint256,uint256[2],uint256[2],uint256,uint256,uint256) (runs: 256, μ: 12921605, ~: 13130650) +InterestRateModelTest:testFuzzReferenceRateFixed(uint256,uint256,uint256,uint256,uint256,uint256,(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,int256,uint256,uint256)) (runs: 256, μ: 2384629, ~: 2386937) +InterestRateModelTest:testFuzzReferenceRateFloating(uint256,uint256,(uint256,uint256,uint256,uint256,uint256,uint256,uint256)) (runs: 256, μ: 2324337, ~: 2325007) InterestRateModelTest:testMinTimeToMaturity() (gas: 2107232) InterestRateModelTest:testRevertMaxUtilizationLowerThanWad() (gas: 268402) MarketTest:testAccountLiquidityAdjustedDebt() (gas: 654260) @@ -540,8 +544,8 @@ StakedEXATest:invariantNoDuplicatedReward() (runs: 10, calls: 5000, reverts: 0) StakedEXATest:invariantRewardsUpOnly() (runs: 10, calls: 5000, reverts: 0) StakedEXATest:invariantShareValueIsOne() (runs: 10, calls: 5000, reverts: 0) StakedEXATest:testAlreadyListedError() (gas: 43677) -StakedEXATest:testAvgIndex(uint256[3],uint256[2]) (runs: 256, μ: 1321279, ~: 1370843) -StakedEXATest:testAvgStartTime(uint256[3],uint256[2]) (runs: 256, μ: 1249345, ~: 1298909) +StakedEXATest:testAvgIndex(uint256[3],uint256[2]) (runs: 256, μ: 1321490, ~: 1370842) +StakedEXATest:testAvgStartTime(uint256[3],uint256[2]) (runs: 256, μ: 1249556, ~: 1298908) StakedEXATest:testBalanceOfDeposit(uint80) (runs: 256, μ: 331003, ~: 339807) StakedEXATest:testBalanceOfWithdraw(uint256) (runs: 256, μ: 82393, ~: 82407) StakedEXATest:testCanChangeRewardsDurationWhenDisabled() (gas: 176331) @@ -550,12 +554,12 @@ StakedEXATest:testClaimAndUnstake() (gas: 1622618) StakedEXATest:testClaimAndWithdrawAfterRefTime() (gas: 1029325) StakedEXATest:testClaimBeforeFirstHarvest() (gas: 527334) StakedEXATest:testDepositClaimsRewardsToReceiver() (gas: 1121108) -StakedEXATest:testDepositEvent(uint256) (runs: 256, μ: 335234, ~: 334978) -StakedEXATest:testDepositShouldClaim(uint256[2],uint32) (runs: 256, μ: 821450, ~: 754892) +StakedEXATest:testDepositEvent(uint256) (runs: 256, μ: 335234, ~: 334990) +StakedEXATest:testDepositShouldClaim(uint256[2],uint32) (runs: 256, μ: 822448, ~: 754904) StakedEXATest:testDepositToAnotherWithAllowance() (gas: 375284) StakedEXATest:testDepositToAnotherWithoutAllowanceShouldFail() (gas: 121959) -StakedEXATest:testDepositWithdrawAvgStartTimeAndIndex(uint256[3],uint256,uint256[5]) (runs: 256, μ: 1886121, ~: 1905249) -StakedEXATest:testEarnedWithTime(uint256) (runs: 256, μ: 86132, ~: 86326) +StakedEXATest:testDepositWithdrawAvgStartTimeAndIndex(uint256[3],uint256,uint256[5]) (runs: 256, μ: 1886018, ~: 1905249) +StakedEXATest:testEarnedWithTime(uint256) (runs: 256, μ: 86137, ~: 86326) StakedEXATest:testEmergencyAdminCanPauseNotUnpause() (gas: 175029) StakedEXATest:testFinishDistributionEmitEvent() (gas: 240752) StakedEXATest:testFinishDistributionLetsClaimUnclaimed() (gas: 1784877) @@ -566,9 +570,9 @@ StakedEXATest:testGrantRevokeEmergencyAdmin() (gas: 117824) StakedEXATest:testGrantRevokePauser() (gas: 117836) StakedEXATest:testHandlerClaim(uint8) (runs: 256, μ: 485340, ~: 485340) StakedEXATest:testHandlerDeposit(uint80) (runs: 256, μ: 1240331, ~: 1276242) -StakedEXATest:testHandlerHarvest(uint64) (runs: 256, μ: 402354, ~: 411143) -StakedEXATest:testHandlerNotifyRewardAmount(uint64) (runs: 256, μ: 165017, ~: 156797) -StakedEXATest:testHandlerSetDuration(uint32) (runs: 256, μ: 195324, ~: 218711) +StakedEXATest:testHandlerHarvest(uint64) (runs: 256, μ: 402355, ~: 411143) +StakedEXATest:testHandlerNotifyRewardAmount(uint64) (runs: 256, μ: 165160, ~: 156797) +StakedEXATest:testHandlerSetDuration(uint32) (runs: 256, μ: 197124, ~: 218711) StakedEXATest:testHandlerSetMarket() (gas: 144553) StakedEXATest:testHandlerWithdraw(uint256) (runs: 256, μ: 91497, ~: 91511) StakedEXATest:testHarvestAmountWithReducedAllowance() (gas: 184605) @@ -579,13 +583,13 @@ StakedEXATest:testHarvestWhenFinished() (gas: 326352) StakedEXATest:testHarvestX() (gas: 180952) StakedEXATest:testHarvestZero() (gas: 255436) StakedEXATest:testInitialValues() (gas: 196484) -StakedEXATest:testInsufficientBalanceError(uint256) (runs: 256, μ: 63922, ~: 64151) +StakedEXATest:testInsufficientBalanceError(uint256) (runs: 256, μ: 63927, ~: 64151) StakedEXATest:testMaxRewardsGasConsumption() (gas: 144674963) StakedEXATest:testMultipleClaimsVsOne() (gas: 21936440) StakedEXATest:testMultipleHarvests() (gas: 284083) -StakedEXATest:testNoRewardsAfterPeriod(uint256) (runs: 256, μ: 1809475, ~: 1804427) +StakedEXATest:testNoRewardsAfterPeriod(uint256) (runs: 256, μ: 1809429, ~: 1804427) StakedEXATest:testNotPausingRoleError() (gas: 39124) -StakedEXATest:testNotifyRewardAmount(uint256,uint256) (runs: 256, μ: 155635, ~: 155564) +StakedEXATest:testNotifyRewardAmount(uint256,uint256) (runs: 256, μ: 155614, ~: 155564) StakedEXATest:testNotifyRewardWithUnderlyingAsset() (gas: 460608) StakedEXATest:testOnlyAdminEnableReward() (gas: 1297302) StakedEXATest:testOnlyAdminFinishDistribution() (gas: 213640) @@ -604,12 +608,12 @@ StakedEXATest:testPermitAndDeposit() (gas: 452146) StakedEXATest:testPermitFailKeepsFlow() (gas: 484480) StakedEXATest:testRemoveDepositAllowance() (gas: 466913) StakedEXATest:testResetDepositAfterRefTime(uint256) (runs: 256, μ: 1033248, ~: 1032994) -StakedEXATest:testRewardAmountNotifiedEvent(uint256) (runs: 256, μ: 105543, ~: 105293) +StakedEXATest:testRewardAmountNotifiedEvent(uint256) (runs: 256, μ: 105551, ~: 105293) StakedEXATest:testRewardNotListedError() (gas: 1177184) -StakedEXATest:testRewardPaidEvent(uint256,uint256) (runs: 256, μ: 811252, ~: 854321) -StakedEXATest:testRewardsAmounts(uint256) (runs: 256, μ: 1707520, ~: 1707287) +StakedEXATest:testRewardPaidEvent(uint256,uint256) (runs: 256, μ: 810170, ~: 854321) +StakedEXATest:testRewardsAmounts(uint256) (runs: 256, μ: 1707519, ~: 1707287) StakedEXATest:testRewardsDurationSetEvent(uint40) (runs: 256, μ: 52173, ~: 52149) -StakedEXATest:testSetDuration(uint256,uint40) (runs: 256, μ: 66028, ~: 64453) +StakedEXATest:testSetDuration(uint256,uint40) (runs: 256, μ: 66012, ~: 64453) StakedEXATest:testSetMarketAddressZero() (gas: 36979) StakedEXATest:testSetMarketOnlyAdmin() (gas: 1345554) StakedEXATest:testSetMaxRewardsTokensExceeded() (gas: 111958823) @@ -621,12 +625,12 @@ StakedEXATest:testSetSavingsZeroAddressError() (gas: 37006) StakedEXATest:testTotalSupplyDeposit(uint80) (runs: 256, μ: 330389, ~: 339193) StakedEXATest:testTotalSupplyWithdraw(uint256) (runs: 256, μ: 81753, ~: 81767) StakedEXATest:testUntransferable(uint80) (runs: 256, μ: 338580, ~: 348672) -StakedEXATest:testWithdrawEvent(uint256) (runs: 256, μ: 483401, ~: 483158) +StakedEXATest:testWithdrawEvent(uint256) (runs: 256, μ: 483401, ~: 483176) StakedEXATest:testWithdrawRewardUnderlyingAsset() (gas: 456617) StakedEXATest:testWithdrawRewardsOnlyAdmin() (gas: 242200) StakedEXATest:testWithdrawRewardsOnlyReward() (gas: 1177174) -StakedEXATest:testWithdrawSameAmountRewardsShouldEqual(uint256,uint256) (runs: 256, μ: 1065854, ~: 1107874) -StakedEXATest:testWithdrawWithRewards(uint256) (runs: 256, μ: 895625, ~: 895382) +StakedEXATest:testWithdrawSameAmountRewardsShouldEqual(uint256,uint256) (runs: 256, μ: 1065998, ~: 1107874) +StakedEXATest:testWithdrawWithRewards(uint256) (runs: 256, μ: 895625, ~: 895400) StakedEXATest:testZeroRateError() (gas: 58274) StakingPreviewerTest:testAllClaimable() (gas: 769205) StakingPreviewerTest:testAllClaimed() (gas: 818952) diff --git a/contracts/periphery/FlashLoanAdapter.sol b/contracts/periphery/FlashLoanAdapter.sol index 1aff7d959..3cd6227d3 100644 --- a/contracts/periphery/FlashLoanAdapter.sol +++ b/contracts/periphery/FlashLoanAdapter.sol @@ -1,27 +1,25 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.20; -import { AccessControl } from "@openzeppelin/contracts-v4/access/AccessControl.sol"; import { IERC20 } from "@openzeppelin/contracts-v4/interfaces/IERC20.sol"; -import { IERC4626 } from "@openzeppelin/contracts-v4/interfaces/IERC4626.sol"; +import { SafeTransferLib } from "solady/src/utils/SafeTransferLib.sol"; + +import { IFlashLoaner, IFlashLoanRecipient } from "./DebtRoller.sol"; /// @title FlashLoanAdapter -/// @notice Adapter to use Balancer V3 vault with Balancer V2 interface. -/// Uses wrapped tokens when there's not enough liquidity in the vault. -contract FlashLoanAdapter is AccessControl { - IBalancerVaultV3 public immutable vault; +/// @notice Adapter to use Morpho flash loans with the `IFlashLoaner` interface. +/// Morpho flash loans are free, so reported fees are always zero. +contract FlashLoanAdapter is IFlashLoaner { + using SafeTransferLib for address; - /// @notice Mapping to check if a token is a wrapped token. - mapping(IERC20 wToken => bool isWToken) public isWToken; - /// @notice Mapping to get the wrapped token for an asset. - mapping(IERC20 asset => IERC4626 wToken) public wTokens; + /// @notice Morpho contract that provides the flash loans. + IMorpho public immutable morpho; - constructor(IBalancerVaultV3 vault_, address owner) { - _grantRole(DEFAULT_ADMIN_ROLE, owner); - vault = vault_; + constructor(IMorpho morpho_) { + morpho = morpho_; } - /// @notice Performs a flash loan using the Balancer V3 vault. + /// @notice Performs a flash loan using Morpho. /// @param recipient The address to receive the flash loan. /// @param tokens The tokens to borrow. /// @param amounts The amounts to borrow. @@ -29,80 +27,35 @@ contract FlashLoanAdapter is AccessControl { // solhint-disable-next-line gas-calldata-parameters function flashLoan(address recipient, IERC20[] memory tokens, uint256[] memory amounts, bytes memory data) external { if (tokens.length != 1 || amounts.length != 1) revert InvalidLength(); - uint256[] memory fees = new uint256[](1); - if (tokens[0].balanceOf(address(vault)) < amounts[0]) { - IERC4626 wToken = wTokens[tokens[0]]; - if (wToken == IERC4626(address(0)) || wToken.convertToAssets(wToken.balanceOf(address(vault))) < amounts[0]) { - revert InsufficientLiquidity(); - } - - tokens[0] = IERC20(address(wToken)); - uint256 shares = wToken.previewWithdraw(amounts[0]); - fees[0] = wToken.previewMint(shares) - amounts[0]; - amounts[0] = shares; - } - vault.unlock( - abi.encodeWithSelector(this.receiveFlashLoan.selector, abi.encode(recipient, tokens, amounts, fees, data)) - ); + morpho.flashLoan(address(tokens[0]), amounts[0], abi.encode(recipient, tokens, amounts, data)); } - /// @notice Receives a flash loan from the Balancer V3 vault. - /// @param payload The payload containing the recipient, tokens, amounts, fees, and data. - function receiveFlashLoan(bytes calldata payload) external { - if (msg.sender != address(vault)) revert UnauthorizedVault(); - ( - address recipient, - IERC20[] memory tokens, - uint256[] memory amounts, - uint256[] memory fees, - bytes memory data - ) = abi.decode(payload, (address, IERC20[], uint256[], uint256[], bytes)); + /// @notice Receives a flash loan from Morpho, forwards it to the recipient and approves the repayment. + /// @param payload The payload containing the recipient, tokens, amounts, and data. + function onMorphoFlashLoan(uint256, bytes calldata payload) external { + if (msg.sender != address(morpho)) revert UnauthorizedMorpho(); + (address recipient, IERC20[] memory tokens, uint256[] memory amounts, bytes memory data) = abi.decode( + payload, + (address, IERC20[], uint256[], bytes) + ); + address token = address(tokens[0]); - if (isWToken[tokens[0]]) { - IERC4626 wToken = IERC4626(address(tokens[0])); - vault.sendTo(tokens[0], address(this), amounts[0]); - tokens[0] = IERC20(wToken.asset()); - amounts[0] = wToken.redeem(amounts[0], recipient, address(this)); - IFlashLoanRecipient(recipient).receiveFlashLoan(tokens, amounts, fees, data); + token.safeTransfer(recipient, amounts[0]); + IFlashLoanRecipient(recipient).receiveFlashLoan(tokens, amounts, new uint256[](1), data); - tokens[0].approve(address(wToken), amounts[0] + fees[0]); - wToken.deposit(amounts[0] + fees[0], address(vault)); - vault.settle(wToken, amounts[0]); - } else { - vault.sendTo(tokens[0], recipient, amounts[0]); - IFlashLoanRecipient(recipient).receiveFlashLoan(tokens, amounts, fees, data); - tokens[0].transfer(address(vault), amounts[0]); - vault.settle(tokens[0], amounts[0]); - } + token.safeApprove(address(morpho), amounts[0]); } - /// @notice Sets a wrapped token for an asset. - /// @param asset The asset to set the wrapped token for. - /// @param token The wrapped token to set. - function setWToken(IERC20 asset, IERC4626 token) external onlyRole(DEFAULT_ADMIN_ROLE) { - wTokens[asset] = token; - isWToken[token] = true; - emit WTokenSet(asset, token, msg.sender); + /// @notice Returns the amount of `token` available to be flash loaned. + /// @param token The token to check. + function available(IERC20 token) external view returns (uint256) { + return token.balanceOf(address(morpho)); } } -interface IBalancerVaultV3 { - function sendTo(IERC20 token, address to, uint256 amount) external; - function settle(IERC20 token, uint256 amountHint) external returns (uint256 credit); - function unlock(bytes calldata data) external returns (bytes memory); +interface IMorpho { + function flashLoan(address token, uint256 assets, bytes calldata data) external; } -interface IFlashLoanRecipient { - function receiveFlashLoan( - IERC20[] memory tokens, - uint256[] memory amounts, - uint256[] memory fees, - bytes calldata data - ) external; -} - -error InsufficientLiquidity(); error InvalidLength(); -error UnauthorizedVault(); - -event WTokenSet(IERC20 indexed asset, IERC4626 indexed wToken, address indexed account); +error UnauthorizedMorpho(); diff --git a/deploy/FlashLoan.ts b/deploy/FlashLoan.ts index 3310505b0..190cd062e 100644 --- a/deploy/FlashLoan.ts +++ b/deploy/FlashLoan.ts @@ -3,16 +3,12 @@ import type { DeployFunction } from "hardhat-deploy/types"; import tenderlify from "./.utils/tenderlify"; const func: DeployFunction = async ({ deployments: { deploy, get }, getNamedAccounts }) => { - const [{ address: flashLoaner }, { address: timelock }, { deployer }] = await Promise.all([ - get("Balancer3Vault"), - get("TimelockController"), - getNamedAccounts(), - ]); + const [{ address: morpho }, { deployer }] = await Promise.all([get("Morpho"), getNamedAccounts()]); await tenderlify( "FlashLoanAdapter", await deploy("FlashLoanAdapter", { - args: [flashLoaner, timelock], + args: [morpho], skipIfAlreadyDeployed: !JSON.parse(env[`DEPLOY_FLASH_LOAN`] ?? "false"), from: deployer, log: true, @@ -21,8 +17,6 @@ const func: DeployFunction = async ({ deployments: { deploy, get }, getNamedAcco }; func.tags = ["FlashLoan"]; -func.dependencies = ["Governance"]; -func.skip = async ({ network, deployments }) => - !!network.config.sunset || !(await deployments.getOrNull("Balancer3Vault")); +func.skip = async ({ network, deployments }) => !!network.config.sunset || !(await deployments.getOrNull("Morpho")); export default func; diff --git a/deployments/base/Balancer3Vault.json b/deployments/base/Balancer3Vault.json deleted file mode 100644 index 561eeed0b..000000000 --- a/deployments/base/Balancer3Vault.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "address": "0xbA1333333333a1BA1108E8412f11850A5C319bA9" -} diff --git a/deployments/base/Morpho.json b/deployments/base/Morpho.json new file mode 100644 index 000000000..457f9ec18 --- /dev/null +++ b/deployments/base/Morpho.json @@ -0,0 +1,3 @@ +{ + "address": "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb" +} diff --git a/deployments/optimism/Morpho.json b/deployments/optimism/Morpho.json new file mode 100644 index 000000000..f754ebe23 --- /dev/null +++ b/deployments/optimism/Morpho.json @@ -0,0 +1,3 @@ +{ + "address": "0xce95AfbB8EA029495c66020883F87aaE8864AF92" +} diff --git a/test/FlashLoanAdapter.t.sol b/test/FlashLoanAdapter.t.sol index 06039d68c..392b52967 100644 --- a/test/FlashLoanAdapter.t.sol +++ b/test/FlashLoanAdapter.t.sol @@ -1,117 +1,201 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.0; // solhint-disable-line one-contract-per-file -import { IERC20 } from "@openzeppelin/contracts-v4/interfaces/IERC20.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import { ForkTest } from "./Fork.t.sol"; +import { FixedLib } from "../contracts/utils/FixedLib.sol"; +import { Auditor, DebtRoller, IFlashLoanRecipient, Market } from "../contracts/periphery/DebtRoller.sol"; import { - FlashLoanAdapter, - IBalancerVaultV3, - IERC4626, - IFlashLoanRecipient, + IERC20, + IMorpho, InvalidLength, - WTokenSet + FlashLoanAdapter, + UnauthorizedMorpho } from "../contracts/periphery/FlashLoanAdapter.sol"; contract FlashLoanAdapterTest is ForkTest { FlashLoanAdapter internal adapter; - IBalancerVaultV3 internal vaultV3; + FlashLoanConsumer internal consumer; + IMorpho internal morpho; + IERC20 internal usdc; + IERC20 internal weth; function setUp() external { - adapter = new FlashLoanAdapter(IBalancerVaultV3(address(0)), address(this)); + vm.createSelectFork(vm.envString("OPTIMISM_NODE"), 157_331_000); + + morpho = IMorpho(deployment("Morpho")); + usdc = IERC20(deployment("USDC")); + weth = IERC20(deployment("WETH")); + adapter = new FlashLoanAdapter(morpho); + consumer = new FlashLoanConsumer(adapter); } - function test_setWToken_sets_whenAdmin() external { - IERC20 asset = IERC20(address(0x1)); - IERC4626 wToken = IERC4626(address(0x2)); + function test_flashLoan_lends_usdc(uint256 amount) external { + amount = bound(amount, 1, adapter.available(usdc)); + uint256 morphoBalance = usdc.balanceOf(address(morpho)); - adapter.setWToken(asset, wToken); - assertEq(address(adapter.wTokens(asset)), address(wToken), "wToken not set"); + consumer.callFlashLoan(usdc, amount, ""); + + assertLent(usdc, amount, morphoBalance); } - function test_setWToken_emitsWTokenSet() external { - IERC20 asset = IERC20(address(0x1)); - IERC4626 wToken = IERC4626(address(0x2)); - vm.expectEmit(true, true, true, true, address(adapter)); - emit WTokenSet(asset, wToken, address(this)); - adapter.setWToken(asset, wToken); + function test_flashLoan_lends_fullBalance_usdc() external { + uint256 amount = adapter.available(usdc); + assertGt(amount, 0, "no usdc available"); + + consumer.callFlashLoan(usdc, amount, ""); + + assertLent(usdc, amount, amount); } - function test_setWToken_reverts_whenNotAdmin() external { - address nonAdmin = address(0x1); - vm.startPrank(nonAdmin); - vm.expectRevert(bytes("")); - adapter.setWToken(IERC20(address(0x1)), IERC4626(address(0x2))); - assertEq(address(adapter.wTokens(IERC20(address(0x1)))), address(0), "wToken set"); + function test_flashLoan_lends_fullBalance_weth() external { + uint256 amount = adapter.available(weth); + assertGt(amount, 0, "no weth available"); + + consumer.callFlashLoan(weth, amount, ""); + + assertLent(weth, amount, amount); } - function test_consumeAdapter() external { - _setUpFork(); - IERC20 rETH = IERC20(0x9Bcef72be871e61ED4fBbc7630889beE758eb81D); - FlashLoanConsumer consumer = new FlashLoanConsumer(adapter, rETH); - consumer.callFlashLoan(); + function test_flashLoan_forwardsArguments() external { + uint256 amount = 1_000e6; + bytes memory data = abi.encode("data"); + IERC20[] memory tokens = new IERC20[](1); + tokens[0] = usdc; + uint256[] memory amounts = new uint256[](1); + amounts[0] = amount; + + vm.expectCall( + address(morpho), + abi.encodeCall(IMorpho.flashLoan, (address(usdc), amount, abi.encode(address(consumer), tokens, amounts, data))) + ); + vm.expectCall( + address(consumer), + abi.encodeCall(IFlashLoanRecipient.receiveFlashLoan, (tokens, amounts, new uint256[](1), data)) + ); + consumer.callFlashLoan(usdc, amount, data); } - function test_consumeAdapter_withWToken() external { - _setUpFork(); - IERC20 usdc = IERC20(deployment("USDC")); - IERC4626 waOptUSDCn = IERC4626(address(0x41B334E9F2C0ED1f30fD7c351874a6071C53a78E)); - adapter.setWToken(usdc, waOptUSDCn); - FlashLoanConsumer consumer = new FlashLoanConsumer(adapter, usdc); + function test_flashLoan_lends_whenCallerIsNotRecipient() external { + uint256 amount = 1_000e6; + uint256 morphoBalance = usdc.balanceOf(address(morpho)); + IERC20[] memory tokens = new IERC20[](1); + tokens[0] = usdc; + uint256[] memory amounts = new uint256[](1); + amounts[0] = amount; + + vm.prank(makeAddr("alice")); + adapter.flashLoan(address(consumer), tokens, amounts, ""); + + assertLent(usdc, amount, morphoBalance); + } - deal(address(usdc), address(consumer), 1); - consumer.callFlashLoan(); + function test_flashLoan_reverts_whenOverAvailable() external { + uint256 amount = adapter.available(usdc) + 1; + vm.expectRevert(bytes("transfer reverted")); + consumer.callFlashLoan(usdc, amount, ""); + } - assertEq(usdc.balanceOf(address(consumer)), 0); - assertEq(usdc.balanceOf(address(adapter)), 0); + function test_flashLoan_reverts_whenZeroAmount() external { + vm.expectRevert(bytes("zero assets")); + consumer.callFlashLoan(usdc, 0, ""); } function test_flashLoan_reverts_whenTokensLengthNotOne() external { vm.expectRevert(InvalidLength.selector); - adapter.flashLoan(address(0), new IERC20[](0), new uint256[](1), ""); + adapter.flashLoan(address(consumer), new IERC20[](2), new uint256[](1), ""); } function test_flashLoan_reverts_whenAmountsLengthNotOne() external { vm.expectRevert(InvalidLength.selector); - adapter.flashLoan(address(0), new IERC20[](1), new uint256[](0), ""); + adapter.flashLoan(address(consumer), new IERC20[](1), new uint256[](0), ""); + } + + function test_onMorphoFlashLoan_reverts_whenNotMorpho() external { + uint256 amount = 1_000e6; + address attacker = makeAddr("attacker"); + deal(address(usdc), address(adapter), amount); + IERC20[] memory tokens = new IERC20[](1); + tokens[0] = usdc; + uint256[] memory amounts = new uint256[](1); + amounts[0] = amount; + + vm.prank(attacker); + vm.expectRevert(UnauthorizedMorpho.selector); + adapter.onMorphoFlashLoan(amount, abi.encode(attacker, tokens, amounts, "")); + + assertEq(usdc.balanceOf(address(adapter)), amount, "adapter lost tokens"); + assertEq(usdc.allowance(address(adapter), address(morpho)), 0, "allowance set"); } - function _setUpFork() internal { - vm.createSelectFork(vm.envString("OPTIMISM_NODE"), 141_227_400); + function test_rollFixed_rolls_withDebtRoller() external { + Auditor auditor = Auditor(deployment("Auditor")); + Market exaUSDC = Market(deployment("MarketUSDC")); + DebtRoller debtRoller = DebtRoller(address(new ERC1967Proxy(address(new DebtRoller(auditor, adapter)), ""))); + debtRoller.initialize(); + + address bob = makeAddr("bob"); + uint256 maturity = block.timestamp - (block.timestamp % FixedLib.INTERVAL) + FixedLib.INTERVAL; + uint256 borrowAmount = 1_000e6; + deal(address(usdc), bob, 10_000e6); + + vm.startPrank(bob); + usdc.approve(address(exaUSDC), type(uint256).max); + exaUSDC.deposit(10_000e6, bob); + auditor.enterMarket(exaUSDC); + exaUSDC.borrowAtMaturity(maturity, borrowAmount, type(uint256).max, bob, bob); + + uint256 marketBalance = usdc.balanceOf(address(exaUSDC)); + uint256 morphoBalance = usdc.balanceOf(address(morpho)); + exaUSDC.approve(address(debtRoller), type(uint256).max); + debtRoller.rollFixed(exaUSDC, maturity, maturity + FixedLib.INTERVAL, borrowAmount * 2, type(uint256).max, 1e18); + vm.stopPrank(); + + (uint256 principal, uint256 fee) = exaUSDC.fixedBorrowPositions(maturity, bob); + assertEq(principal + fee, 0, "repay maturity not rolled"); + (principal, ) = exaUSDC.fixedBorrowPositions(maturity + FixedLib.INTERVAL, bob); + assertGe(principal, borrowAmount, "borrow maturity not rolled"); + assertEq(usdc.balanceOf(address(exaUSDC)), marketBalance, "flash loan fee charged"); + assertEq(usdc.balanceOf(address(morpho)), morphoBalance, "morpho not repaid"); + assertEq(usdc.balanceOf(address(debtRoller)), 0, "debt roller kept tokens"); + assertEq(usdc.balanceOf(address(adapter)), 0, "adapter kept tokens"); + } - vaultV3 = IBalancerVaultV3(0xbA1333333333a1BA1108E8412f11850A5C319bA9); - adapter = new FlashLoanAdapter(vaultV3, address(this)); + function assertLent(IERC20 token, uint256 amount, uint256 morphoBalance) internal view { + assertEq(consumer.received(), amount, "recipient not funded"); + assertEq(token.balanceOf(address(consumer)), 0, "recipient kept tokens"); + assertEq(token.balanceOf(address(morpho)), morphoBalance, "morpho not repaid"); + assertEq(token.balanceOf(address(adapter)), 0, "adapter kept tokens"); + assertEq(token.allowance(address(adapter), address(morpho)), 0, "allowance not consumed"); } } contract FlashLoanConsumer is IFlashLoanRecipient { - FlashLoanAdapter internal adapter; - IERC20 internal token; - uint256 internal prevAmount; + FlashLoanAdapter internal immutable adapter; + uint256 public received; - constructor(FlashLoanAdapter adapter_, IERC20 token_) { + constructor(FlashLoanAdapter adapter_) { adapter = adapter_; - token = token_; } - function callFlashLoan() external { - prevAmount = token.balanceOf(address(this)); + function callFlashLoan(IERC20 token, uint256 amount, bytes memory data) external { IERC20[] memory tokens = new IERC20[](1); tokens[0] = token; uint256[] memory amounts = new uint256[](1); - amounts[0] = 1e6; - adapter.flashLoan(address(this), tokens, amounts, ""); + amounts[0] = amount; + adapter.flashLoan(address(this), tokens, amounts, data); } function receiveFlashLoan( IERC20[] memory tokens, uint256[] memory amounts, uint256[] memory fees, - bytes calldata + bytes memory ) external { - assert(token.balanceOf(address(this)) == prevAmount + amounts[0]); - assert(address(tokens[0]) == address(token)); + assert(msg.sender == address(adapter)); + received = tokens[0].balanceOf(address(this)); tokens[0].transfer(address(adapter), amounts[0] + fees[0]); } }