diff --git a/src/interfaces/morpho/IMorpho.sol b/src/interfaces/morpho/IMorpho.sol new file mode 100644 index 0000000..ef2acb1 --- /dev/null +++ b/src/interfaces/morpho/IMorpho.sol @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +type Id is bytes32; + +struct MarketParams { + address loanToken; + address collateralToken; + address oracle; + address irm; + uint256 lltv; +} + +/// @dev Warning: For `feeRecipient`, `supplyShares` does not contain the accrued shares since the last interest +/// accrual. +struct Position { + uint256 supplyShares; + uint128 borrowShares; + uint128 collateral; +} + +/// @dev Warning: `totalSupplyAssets` does not contain the accrued interest since the last interest accrual. +/// @dev Warning: `totalBorrowAssets` does not contain the accrued interest since the last interest accrual. +/// @dev Warning: `totalSupplyShares` does not contain the additional shares accrued by `feeRecipient` since the last +/// interest accrual. +struct Market { + uint128 totalSupplyAssets; + uint128 totalSupplyShares; + uint128 totalBorrowAssets; + uint128 totalBorrowShares; + uint128 lastUpdate; + uint128 fee; +} + +struct Authorization { + address authorizer; + address authorized; + bool isAuthorized; + uint256 nonce; + uint256 deadline; +} + +struct Signature { + uint8 v; + bytes32 r; + bytes32 s; +} + +/// @dev This interface is used for factorizing IMorphoStaticTyping and IMorpho. +/// @dev Consider using the IMorpho interface instead of this one. +interface IMorphoBase { + /// @notice The EIP-712 domain separator. + /// @dev Warning: Every EIP-712 signed message based on this domain separator can be reused on chains sharing the + /// same chain id and on forks because the domain separator would be the same. + function DOMAIN_SEPARATOR() external view returns (bytes32); + + /// @notice The owner of the contract. + /// @dev It has the power to change the owner. + /// @dev It has the power to set fees on markets and set the fee recipient. + /// @dev It has the power to enable but not disable IRMs and LLTVs. + function owner() external view returns (address); + + /// @notice The fee recipient of all markets. + /// @dev The recipient receives the fees of a given market through a supply position on that market. + function feeRecipient() external view returns (address); + + /// @notice Whether the `irm` is enabled. + function isIrmEnabled(address irm) external view returns (bool); + + /// @notice Whether the `lltv` is enabled. + function isLltvEnabled(uint256 lltv) external view returns (bool); + + /// @notice Whether `authorized` is authorized to modify `authorizer`'s position on all markets. + /// @dev Anyone is authorized to modify their own positions, regardless of this variable. + function isAuthorized(address authorizer, address authorized) external view returns (bool); + + /// @notice The `authorizer`'s current nonce. Used to prevent replay attacks with EIP-712 signatures. + function nonce(address authorizer) external view returns (uint256); + + /// @notice Sets `newOwner` as `owner` of the contract. + /// @dev Warning: No two-step transfer ownership. + /// @dev Warning: The owner can be set to the zero address. + function setOwner(address newOwner) external; + + /// @notice Enables `irm` as a possible IRM for market creation. + /// @dev Warning: It is not possible to disable an IRM. + function enableIrm(address irm) external; + + /// @notice Enables `lltv` as a possible LLTV for market creation. + /// @dev Warning: It is not possible to disable a LLTV. + function enableLltv(uint256 lltv) external; + + /// @notice Sets the `newFee` for the given market `marketParams`. + /// @param newFee The new fee, scaled by WAD. + /// @dev Warning: The recipient can be the zero address. + function setFee(MarketParams memory marketParams, uint256 newFee) external; + + /// @notice Sets `newFeeRecipient` as `feeRecipient` of the fee. + /// @dev Warning: If the fee recipient is set to the zero address, fees will accrue there and will be lost. + /// @dev Modifying the fee recipient will allow the new recipient to claim any pending fees not yet accrued. To + /// ensure that the current recipient receives all due fees, accrue interest manually prior to making any changes. + function setFeeRecipient(address newFeeRecipient) external; + + /// @notice Creates the market `marketParams`. + /// @dev Here is the list of assumptions on the market's dependencies (tokens, IRM and oracle) that guarantees + /// Morpho behaves as expected: + /// - The token should be ERC-20 compliant, except that it can omit return values on `transfer` and `transferFrom`. + /// - The token balance of Morpho should only decrease on `transfer` and `transferFrom`. In particular, tokens with + /// burn functions are not supported. + /// - The token should not re-enter Morpho on `transfer` nor `transferFrom`. + /// - The token balance of the sender (resp. receiver) should decrease (resp. increase) by exactly the given amount + /// on `transfer` and `transferFrom`. In particular, tokens with fees on transfer are not supported. + /// - The IRM should not re-enter Morpho. + /// - The oracle should return a price with the correct scaling. + /// - The oracle price should not be able to change instantly such that the new price is less than the old price + /// multiplied by LLTV*LIF. In particular, if the loan asset is a vault that can receive donations, the oracle + /// should not price its shares using the AUM. + /// @dev Here is a list of assumptions on the market's dependencies which, if broken, could break Morpho's liveness + /// properties (funds could get stuck): + /// - The token should not revert on `transfer` and `transferFrom` if balances and approvals are right. + /// - The amount of assets supplied and borrowed should not be too high (max ~1e32), otherwise the number of shares + /// might not fit within 128 bits. + /// - The IRM should not revert on `borrowRate`. + /// - The IRM should not return a very high borrow rate (otherwise the computation of `interest` in + /// `_accrueInterest` can overflow). + /// - The oracle should not revert `price`. + /// - The oracle should not return a very high price (otherwise the computation of `maxBorrow` in `_isHealthy` or of + /// `assetsRepaid` in `liquidate` can overflow). + /// @dev The borrow share price of a market with less than 1e4 assets borrowed can be decreased by manipulations, to + /// the point where `totalBorrowShares` is very large and borrowing overflows. + function createMarket(MarketParams memory marketParams) external; + + /// @notice Supplies `assets` or `shares` on behalf of `onBehalf`, optionally calling back the caller's + /// `onMorphoSupply` function with the given `data`. + /// @dev Either `assets` or `shares` should be zero. Most use cases should rely on `assets` as an input so the + /// caller is guaranteed to have `assets` tokens pulled from their balance, but the possibility to mint a specific + /// amount of shares is given for full compatibility and precision. + /// @dev Supplying a large amount can revert for overflow. + /// @dev Supplying an amount of shares may lead to supply more or fewer assets than expected due to slippage. + /// Consider using the `assets` parameter to avoid this. + /// @param marketParams The market to supply assets to. + /// @param assets The amount of assets to supply. + /// @param shares The amount of shares to mint. + /// @param onBehalf The address that will own the increased supply position. + /// @param data Arbitrary data to pass to the `onMorphoSupply` callback. Pass empty data if not needed. + /// @return assetsSupplied The amount of assets supplied. + /// @return sharesSupplied The amount of shares minted. + function supply( + MarketParams memory marketParams, + uint256 assets, + uint256 shares, + address onBehalf, + bytes memory data + ) external returns (uint256 assetsSupplied, uint256 sharesSupplied); + + /// @notice Withdraws `assets` or `shares` on behalf of `onBehalf` and sends the assets to `receiver`. + /// @dev Either `assets` or `shares` should be zero. To withdraw max, pass the `shares`'s balance of `onBehalf`. + /// @dev `msg.sender` must be authorized to manage `onBehalf`'s positions. + /// @dev Withdrawing an amount corresponding to more shares than supplied will revert for underflow. + /// @dev It is advised to use the `shares` input when withdrawing the full position to avoid reverts due to + /// conversion roundings between shares and assets. + /// @param marketParams The market to withdraw assets from. + /// @param assets The amount of assets to withdraw. + /// @param shares The amount of shares to burn. + /// @param onBehalf The address of the owner of the supply position. + /// @param receiver The address that will receive the withdrawn assets. + /// @return assetsWithdrawn The amount of assets withdrawn. + /// @return sharesWithdrawn The amount of shares burned. + function withdraw( + MarketParams memory marketParams, + uint256 assets, + uint256 shares, + address onBehalf, + address receiver + ) external returns (uint256 assetsWithdrawn, uint256 sharesWithdrawn); + + /// @notice Borrows `assets` or `shares` on behalf of `onBehalf` and sends the assets to `receiver`. + /// @dev Either `assets` or `shares` should be zero. Most use cases should rely on `assets` as an input so the + /// caller is guaranteed to borrow `assets` of tokens, but the possibility to mint a specific amount of shares is + /// given for full compatibility and precision. + /// @dev `msg.sender` must be authorized to manage `onBehalf`'s positions. + /// @dev Borrowing a large amount can revert for overflow. + /// @dev Borrowing an amount of shares may lead to borrow fewer assets than expected due to slippage. + /// Consider using the `assets` parameter to avoid this. + /// @param marketParams The market to borrow assets from. + /// @param assets The amount of assets to borrow. + /// @param shares The amount of shares to mint. + /// @param onBehalf The address that will own the increased borrow position. + /// @param receiver The address that will receive the borrowed assets. + /// @return assetsBorrowed The amount of assets borrowed. + /// @return sharesBorrowed The amount of shares minted. + function borrow( + MarketParams memory marketParams, + uint256 assets, + uint256 shares, + address onBehalf, + address receiver + ) external returns (uint256 assetsBorrowed, uint256 sharesBorrowed); + + /// @notice Repays `assets` or `shares` on behalf of `onBehalf`, optionally calling back the caller's + /// `onMorphoRepay` function with the given `data`. + /// @dev Either `assets` or `shares` should be zero. To repay max, pass the `shares`'s balance of `onBehalf`. + /// @dev Repaying an amount corresponding to more shares than borrowed will revert for underflow. + /// @dev It is advised to use the `shares` input when repaying the full position to avoid reverts due to conversion + /// roundings between shares and assets. + /// @dev An attacker can front-run a repay with a small repay making the transaction revert for underflow. + /// @param marketParams The market to repay assets to. + /// @param assets The amount of assets to repay. + /// @param shares The amount of shares to burn. + /// @param onBehalf The address of the owner of the debt position. + /// @param data Arbitrary data to pass to the `onMorphoRepay` callback. Pass empty data if not needed. + /// @return assetsRepaid The amount of assets repaid. + /// @return sharesRepaid The amount of shares burned. + function repay( + MarketParams memory marketParams, + uint256 assets, + uint256 shares, + address onBehalf, + bytes memory data + ) external returns (uint256 assetsRepaid, uint256 sharesRepaid); + + /// @notice Supplies `assets` of collateral on behalf of `onBehalf`, optionally calling back the caller's + /// `onMorphoSupplyCollateral` function with the given `data`. + /// @dev Interest are not accrued since it's not required and it saves gas. + /// @dev Supplying a large amount can revert for overflow. + /// @param marketParams The market to supply collateral to. + /// @param assets The amount of collateral to supply. + /// @param onBehalf The address that will own the increased collateral position. + /// @param data Arbitrary data to pass to the `onMorphoSupplyCollateral` callback. Pass empty data if not needed. + function supplyCollateral( + MarketParams memory marketParams, + uint256 assets, + address onBehalf, + bytes memory data + ) external; + + /// @notice Withdraws `assets` of collateral on behalf of `onBehalf` and sends the assets to `receiver`. + /// @dev `msg.sender` must be authorized to manage `onBehalf`'s positions. + /// @dev Withdrawing an amount corresponding to more collateral than supplied will revert for underflow. + /// @param marketParams The market to withdraw collateral from. + /// @param assets The amount of collateral to withdraw. + /// @param onBehalf The address of the owner of the collateral position. + /// @param receiver The address that will receive the collateral assets. + function withdrawCollateral( + MarketParams memory marketParams, + uint256 assets, + address onBehalf, + address receiver + ) external; + + /// @notice Liquidates the given `repaidShares` of debt asset or seize the given `seizedAssets` of collateral on the + /// given market `marketParams` of the given `borrower`'s position, optionally calling back the caller's + /// `onMorphoLiquidate` function with the given `data`. + /// @dev Either `seizedAssets` or `repaidShares` should be zero. + /// @dev Seizing more than the collateral balance will underflow and revert without any error message. + /// @dev Repaying more than the borrow balance will underflow and revert without any error message. + /// @dev An attacker can front-run a liquidation with a small repay making the transaction revert for underflow. + /// @param marketParams The market of the position. + /// @param borrower The owner of the position. + /// @param seizedAssets The amount of collateral to seize. + /// @param repaidShares The amount of shares to repay. + /// @param data Arbitrary data to pass to the `onMorphoLiquidate` callback. Pass empty data if not needed. + /// @return The amount of assets seized. + /// @return The amount of assets repaid. + function liquidate( + MarketParams memory marketParams, + address borrower, + uint256 seizedAssets, + uint256 repaidShares, + bytes memory data + ) external returns (uint256, uint256); + + /// @notice Executes a flash loan. + /// @dev Flash loans have access to the whole balance of the contract (the liquidity and deposited collateral of all + /// markets combined, plus donations). + /// @dev Warning: Not ERC-3156 compliant but compatibility is easily reached: + /// - `flashFee` is zero. + /// - `maxFlashLoan` is the token's balance of this contract. + /// - The receiver of `assets` is the caller. + /// @param token The token to flash loan. + /// @param assets The amount of assets to flash loan. + /// @param data Arbitrary data to pass to the `onMorphoFlashLoan` callback. + function flashLoan(address token, uint256 assets, bytes calldata data) external; + + /// @notice Sets the authorization for `authorized` to manage `msg.sender`'s positions. + /// @param authorized The authorized address. + /// @param newIsAuthorized The new authorization status. + function setAuthorization(address authorized, bool newIsAuthorized) external; + + /// @notice Sets the authorization for `authorization.authorized` to manage `authorization.authorizer`'s positions. + /// @dev Warning: Reverts if the signature has already been submitted. + /// @dev The signature is malleable, but it has no impact on the security here. + /// @dev The nonce is passed as argument to be able to revert with a different error message. + /// @param authorization The `Authorization` struct. + /// @param signature The signature. + function setAuthorizationWithSig(Authorization calldata authorization, Signature calldata signature) external; + + /// @notice Accrues interest for the given market `marketParams`. + function accrueInterest(MarketParams memory marketParams) external; + + /// @notice Returns the data stored on the different `slots`. + function extSloads(bytes32[] memory slots) external view returns (bytes32[] memory); +} + +/// @dev This interface is inherited by Morpho so that function signatures are checked by the compiler. +/// @dev Consider using the IMorpho interface instead of this one. +interface IMorphoStaticTyping is IMorphoBase { + /// @notice The state of the position of `user` on the market corresponding to `id`. + /// @dev Warning: For `feeRecipient`, `supplyShares` does not contain the accrued shares since the last interest + /// accrual. + function position( + Id id, + address user + ) external view returns (uint256 supplyShares, uint128 borrowShares, uint128 collateral); + + /// @notice The state of the market corresponding to `id`. + /// @dev Warning: `totalSupplyAssets` does not contain the accrued interest since the last interest accrual. + /// @dev Warning: `totalBorrowAssets` does not contain the accrued interest since the last interest accrual. + /// @dev Warning: `totalSupplyShares` does not contain the accrued shares by `feeRecipient` since the last interest + /// accrual. + function market( + Id id + ) + external + view + returns ( + uint128 totalSupplyAssets, + uint128 totalSupplyShares, + uint128 totalBorrowAssets, + uint128 totalBorrowShares, + uint128 lastUpdate, + uint128 fee + ); + + /// @notice The market params corresponding to `id`. + /// @dev This mapping is not used in Morpho. It is there to enable reducing the cost associated to calldata on layer + /// 2s by creating a wrapper contract with functions that take `id` as input instead of `marketParams`. + function idToMarketParams( + Id id + ) external view returns (address loanToken, address collateralToken, address oracle, address irm, uint256 lltv); +} + +/// @title IMorpho +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @dev Use this interface for Morpho to have access to all the functions with the appropriate function signatures. +interface IMorpho is IMorphoBase { + /// @notice The state of the position of `user` on the market corresponding to `id`. + /// @dev Warning: For `feeRecipient`, `p.supplyShares` does not contain the accrued shares since the last interest + /// accrual. + function position(Id id, address user) external view returns (Position memory p); + + /// @notice The state of the market corresponding to `id`. + /// @dev Warning: `m.totalSupplyAssets` does not contain the accrued interest since the last interest accrual. + /// @dev Warning: `m.totalBorrowAssets` does not contain the accrued interest since the last interest accrual. + /// @dev Warning: `m.totalSupplyShares` does not contain the accrued shares by `feeRecipient` since the last + /// interest accrual. + function market(Id id) external view returns (Market memory m); + + /// @notice The market params corresponding to `id`. + /// @dev This mapping is not used in Morpho. It is there to enable reducing the cost associated to calldata on layer + /// 2s by creating a wrapper contract with functions that take `id` as input instead of `marketParams`. + function idToMarketParams(Id id) external view returns (MarketParams memory); +} diff --git a/src/interfaces/morpho/IMorphoCallbacks.sol b/src/interfaces/morpho/IMorphoCallbacks.sol new file mode 100644 index 0000000..b4c7898 --- /dev/null +++ b/src/interfaces/morpho/IMorphoCallbacks.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +/// @title IMorphoLiquidateCallback +/// @notice Interface that liquidators willing to use `liquidate`'s callback must implement. +interface IMorphoLiquidateCallback { + /// @notice Callback called when a liquidation occurs. + /// @dev The callback is called only if data is not empty. + /// @param repaidAssets The amount of repaid assets. + /// @param data Arbitrary data passed to the `liquidate` function. + function onMorphoLiquidate(uint256 repaidAssets, bytes calldata data) external; +} + +/// @title IMorphoRepayCallback +/// @notice Interface that users willing to use `repay`'s callback must implement. +interface IMorphoRepayCallback { + /// @notice Callback called when a repayment occurs. + /// @dev The callback is called only if data is not empty. + /// @param assets The amount of repaid assets. + /// @param data Arbitrary data passed to the `repay` function. + function onMorphoRepay(uint256 assets, bytes calldata data) external; +} + +/// @title IMorphoSupplyCallback +/// @notice Interface that users willing to use `supply`'s callback must implement. +interface IMorphoSupplyCallback { + /// @notice Callback called when a supply occurs. + /// @dev The callback is called only if data is not empty. + /// @param assets The amount of supplied assets. + /// @param data Arbitrary data passed to the `supply` function. + function onMorphoSupply(uint256 assets, bytes calldata data) external; +} + +/// @title IMorphoSupplyCollateralCallback +/// @notice Interface that users willing to use `supplyCollateral`'s callback must implement. +interface IMorphoSupplyCollateralCallback { + /// @notice Callback called when a supply of collateral occurs. + /// @dev The callback is called only if data is not empty. + /// @param assets The amount of supplied collateral. + /// @param data Arbitrary data passed to the `supplyCollateral` function. + function onMorphoSupplyCollateral(uint256 assets, bytes calldata data) external; +} + +/// @title IMorphoFlashLoanCallback +/// @notice Interface that users willing to use `flashLoan`'s callback must implement. +interface IMorphoFlashLoanCallback { + /// @notice Callback called when a flash loan occurs. + /// @dev The callback is called only if data is not empty. + /// @param assets The amount of assets that was flash loaned. + /// @param data Arbitrary data passed to the `flashLoan` function. + function onMorphoFlashLoan(uint256 assets, bytes calldata data) external; +} diff --git a/src/strategy/MorphoLoopStrategy.sol b/src/strategy/MorphoLoopStrategy.sol new file mode 100644 index 0000000..afbd3f4 --- /dev/null +++ b/src/strategy/MorphoLoopStrategy.sol @@ -0,0 +1,552 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import {AccessControlEnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {IStrategy} from "src/interfaces/IStrategy.sol"; +import {StvStETHPool} from "src/StvStETHPool.sol"; +import {WithdrawalQueue} from "src/WithdrawalQueue.sol"; +import {IWstETH} from "src/interfaces/core/IWstETH.sol"; +import {IStETH} from "src/interfaces/core/IStETH.sol"; +import {IStrategyCallForwarder} from "src/interfaces/IStrategyCallForwarder.sol"; +import {StrategyCallForwarderRegistry} from "src/strategy/StrategyCallForwarderRegistry.sol"; +import {FeaturePausable} from "src/utils/FeaturePausable.sol"; + +import {IMorpho, MarketParams, Id, Position} from "src/interfaces/morpho/IMorpho.sol"; +import {IMorphoRepayCallback, IMorphoSupplyCallback} from "src/interfaces/morpho/IMorphoCallbacks.sol"; +import {IWETH} from "src/interfaces/erc20/IWETH.sol"; + +contract MorphoLoopStrategy is + IStrategy, + IMorphoRepayCallback, + IMorphoSupplyCallback, + AccessControlEnumerableUpgradeable, + FeaturePausable, + StrategyCallForwarderRegistry +{ + StvStETHPool private immutable POOL_; + + IStETH public immutable STETH; + IWETH public immutable WETH; + IWstETH public immutable WSTETH; + + IMorpho public immutable MORPHO; + + // Morpho market parameters stored as individual immutables + address public immutable MARKET_LOAN_TOKEN; + address public immutable MARKET_COLLATERAL_TOKEN; + address public immutable MARKET_ORACLE; + address public immutable MARKET_IRM; + uint256 public immutable MARKET_LLTV; + Id public immutable MARKET_ID; + + // Maximum leverage in basis points (e.g., 30000 = 3x) + uint256 public immutable MAX_LEVERAGE_BP; + + // ACL + bytes32 public constant SUPPLY_FEATURE = keccak256("SUPPLY_FEATURE"); + bytes32 public constant SUPPLY_PAUSE_ROLE = keccak256("SUPPLY_PAUSE_ROLE"); + bytes32 public constant SUPPLY_RESUME_ROLE = keccak256("SUPPLY_RESUME_ROLE"); + + struct LoopSupplyParams { + uint256 targetLeverageBp; // Target leverage (10000-30000 = 1x-3x) + } + + struct LoopExitParams { + uint256 collateralToWithdraw; // Amount of wstETH collateral to withdraw + } + + // Temporary storage for callback context + struct CallbackContext { + address callForwarder; + uint256 borrowAmount; + bool isRepayCallback; + } + + CallbackContext private _callbackContext; + + event LoopExecuted( + address indexed user, + uint256 initialCollateral, + uint256 finalCollateral, + uint256 totalDebt, + uint256 actualLeverageBp + ); + + event ExitRequested(address indexed user, bytes32 requestId, uint256 collateralWithdrawn, uint256 debtRepaid); + + error InvalidLeverage(); + error UnauthorizedCallback(); + error InsufficientCollateral(); + error InsufficientDeposit(); + error NoActiveContext(); + error ZeroArgument(string name); + + constructor( + bytes32 _strategyId, + address _strategyCallForwarderImpl, + address _pool, + address _morpho, + address _weth, + MarketParams memory _marketParams, + uint256 _maxLeverageBp + ) StrategyCallForwarderRegistry(_strategyId, _strategyCallForwarderImpl) { + if (_pool == address(0)) revert ZeroArgument("_pool"); + if (_morpho == address(0)) revert ZeroArgument("_morpho"); + if (_weth == address(0)) revert ZeroArgument("_weth"); + if (_marketParams.loanToken != _weth) revert ZeroArgument("loan token must be WETH"); + + POOL_ = StvStETHPool(payable(_pool)); + WSTETH = IWstETH(POOL_.WSTETH()); + STETH = IStETH(POOL_.STETH()); + MORPHO = IMorpho(_morpho); + WETH = IWETH(_weth); + + // Store market params as individual immutables + MARKET_LOAN_TOKEN = _marketParams.loanToken; + MARKET_COLLATERAL_TOKEN = _marketParams.collateralToken; + MARKET_ORACLE = _marketParams.oracle; + MARKET_IRM = _marketParams.irm; + MARKET_LLTV = _marketParams.lltv; + MARKET_ID = Id.wrap(keccak256(abi.encode(_marketParams))); + MAX_LEVERAGE_BP = _maxLeverageBp; + + require(_marketParams.collateralToken == address(WSTETH), "Collateral must be wstETH"); + + _disableInitializers(); + _pauseFeature(SUPPLY_FEATURE); + } + + function initialize(address _admin) external initializer { + if (_admin == address(0)) revert ZeroArgument("_admin"); + + __AccessControlEnumerable_init(); + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + } + + function POOL() external view returns (address) { + return address(POOL_); + } + + /** + * @notice Reconstructs the MarketParams struct from immutable fields + * @return MarketParams struct for use with Morpho functions + */ + function getMarketParams() public view returns (MarketParams memory) { + return + MarketParams({ + loanToken: MARKET_LOAN_TOKEN, + collateralToken: MARKET_COLLATERAL_TOKEN, + oracle: MARKET_ORACLE, + irm: MARKET_IRM, + lltv: MARKET_LLTV + }); + } + + receive() external payable {} + + // ================================================================================= + // SUPPLY WITH ATOMIC LOOPING + // ================================================================================= + + /** + * @inheritdoc IStrategy + */ + function supply( + address _referral, + uint256 _wstethToMint, + bytes calldata _params + ) external payable returns (uint256 stv) { + _checkFeatureNotPaused(SUPPLY_FEATURE); + + LoopSupplyParams memory params = abi.decode(_params, (LoopSupplyParams)); + if (params.targetLeverageBp < 10000) revert InvalidLeverage(); + if (params.targetLeverageBp > MAX_LEVERAGE_BP) revert InvalidLeverage(); + + IStrategyCallForwarder callForwarder = _getOrCreateCallForwarder(msg.sender); + + // 1. Deposit ETH to pool and get stvETH + if (msg.value > 0) { + stv = POOL_.depositETH{value: msg.value}(address(callForwarder), _referral); + } + + // 2. Mint initial wstETH from pool + callForwarder.doCall(address(POOL_), abi.encodeWithSelector(POOL_.mintWsteth.selector, _wstethToMint)); + + // 3. Execute atomic leverage loop via Morpho callback + _executeAtomicLoop(callForwarder, _wstethToMint, params); + + emit StrategySupplied(msg.sender, _referral, msg.value, stv, _wstethToMint, _params); + } + + function _executeAtomicLoop( + IStrategyCallForwarder callForwarder, + uint256 initialWsteth, + LoopSupplyParams memory params + ) internal { + // Calculate how much WETH to borrow based on target leverage + // Formula: borrowAmount = initialValue * (targetLeverage - 1) + // Example: For 2x leverage (20000 bp), borrow = initialValue * 1 + uint256 initialValueInEth = WSTETH.getStETHByWstETH(initialWsteth); + uint256 targetBorrowAmount = (initialValueInEth * (params.targetLeverageBp - 10000)) / 10000; + + // Apply safety factor based on LLTV to avoid immediate liquidation + // Leave some buffer for interest accrual and price movements + uint256 safetyFactorBp = 9500; // 95% of theoretical max + targetBorrowAmount = (targetBorrowAmount * safetyFactorBp) / 10000; + + // Set up callback context for the borrow callback + _callbackContext = CallbackContext({ + callForwarder: address(callForwarder), + borrowAmount: targetBorrowAmount, + isRepayCallback: false + }); + + // Approve wstETH to Morpho for collateral supply + callForwarder.doCall( + address(WSTETH), + abi.encodeWithSelector(WSTETH.approve.selector, address(MORPHO), type(uint256).max) + ); + + // Supply initial wstETH as collateral + callForwarder.doCall( + address(MORPHO), + abi.encodeCall( + MORPHO.supplyCollateral, + (getMarketParams(), initialWsteth, address(callForwarder), new bytes(0)) + ) + ); + + // Execute atomic borrow with callback + // The callback will: receive WETH → unwrap to ETH → stake for stETH → wrap to wstETH → supply collateral + // By the time borrow() returns, the position will be properly collateralized + callForwarder.doCall( + address(MORPHO), + abi.encodeCall( + MORPHO.borrow, + (getMarketParams(), targetBorrowAmount, 0, address(callForwarder), address(this)) + ) + ); + + // Clear callback context + delete _callbackContext; + + // Get final position for event emission + Position memory position = MORPHO.position(MARKET_ID, address(callForwarder)); + + uint256 actualLeverageBp = (position.collateral * 10000) / initialWsteth; + + emit LoopExecuted(msg.sender, initialWsteth, position.collateral, position.borrowShares, actualLeverageBp); + } + + // ================================================================================= + // MORPHO CALLBACKS - ATOMIC LEVERAGE MAGIC + // ================================================================================= + + /** + * @notice Morpho callback executed during supply + * @dev Called BEFORE the supply is finalized + */ + function onMorphoSupply(uint256 assets, bytes calldata data) external { + if (msg.sender != address(MORPHO)) revert UnauthorizedCallback(); + // Not used in current implementation + } + + /** + * @notice Morpho callback executed during repay + * @dev This is the KEY callback for atomic leveraged looping + * @dev Morpho calls this AFTER sending borrowed WETH but BEFORE checking collateral + * @dev This allows us to: + * 1. Receive borrowed WETH + * 2. Convert WETH → ETH → stETH → wstETH + * 3. Supply wstETH as collateral + * 4. Return to Morpho with position now properly collateralized + */ + function onMorphoRepay(uint256 repaidAssets, bytes calldata data) external { + if (msg.sender != address(MORPHO)) revert UnauthorizedCallback(); + + CallbackContext memory ctx = _callbackContext; + if (ctx.callForwarder == address(0)) revert NoActiveContext(); + if (ctx.isRepayCallback) return; // Only process during leverage, not deleverage + + // At this point, we have received WETH from the borrow + uint256 wethBalance = WETH.balanceOf(address(this)); + + // 1. Unwrap WETH to ETH + WETH.withdraw(wethBalance); + + // 2. Stake ETH with Lido to get stETH + uint256 stethReceived = STETH.submit{value: wethBalance}(address(0)); + + // 3. Approve and wrap stETH to wstETH + STETH.approve(address(WSTETH), stethReceived); + uint256 wstethReceived = WSTETH.wrap(stethReceived); + + // 4. Supply the new wstETH as additional collateral to Morpho + // This happens on behalf of the user's call forwarder + WSTETH.approve(address(MORPHO), wstethReceived); + MORPHO.supplyCollateral(getMarketParams(), wstethReceived, ctx.callForwarder, new bytes(0)); + + // Position is now properly collateralized, the borrow will succeed when we return + } + + // ================================================================================= + // EXIT STRATEGY (DELEVERAGE) + // ================================================================================= + + /** + * @inheritdoc IStrategy + */ + function requestExitByWsteth(uint256 _wstethAmount, bytes calldata _params) external returns (bytes32 requestId) { + LoopExitParams memory params = abi.decode(_params, (LoopExitParams)); + IStrategyCallForwarder callForwarder = _getOrCreateCallForwarder(msg.sender); + + // Get current Morpho position + Position memory position = MORPHO.position(MARKET_ID, address(callForwarder)); + uint256 currentCollateral = position.collateral; + uint256 borrowShares = position.borrowShares; + + // Determine how much collateral to withdraw + uint256 collateralToWithdraw = params.collateralToWithdraw > 0 + ? Math.min(params.collateralToWithdraw, currentCollateral) + : Math.min(_wstethAmount, currentCollateral); + + // Calculate proportional debt to repay to maintain health factor + // For full exit: repay all debt and withdraw all collateral + uint256 debtToRepay; + if (collateralToWithdraw == currentCollateral) { + // Full exit - repay all debt + debtToRepay = type(uint256).max; // Will repay all shares + } else { + // Partial exit - calculate proportional repayment + // Need to maintain safe LTV after withdrawal + uint256 remainingCollateral = currentCollateral - collateralToWithdraw; + uint256 remainingCollateralValue = WSTETH.getStETHByWstETH(remainingCollateral); + uint256 maxSafeBorrow = (((remainingCollateralValue * MARKET_LLTV) / 1e18) * 9000) / 10000; // 90% of max + + // Convert current borrow shares to assets + // TODO: Need to get actual borrow amount from Morpho market state + debtToRepay = 0; // Calculate based on market state + } + + // Execute deleverage + _executeDeleverage(callForwarder, collateralToWithdraw, debtToRepay); + + requestId = keccak256(abi.encodePacked(msg.sender, block.timestamp, _wstethAmount)); + + emit ExitRequested(msg.sender, requestId, collateralToWithdraw, debtToRepay); + emit StrategyExitRequested(msg.sender, requestId, _wstethAmount, _params); + } + + function _executeDeleverage( + IStrategyCallForwarder callForwarder, + uint256 collateralAmount, + uint256 debtAmount + ) internal { + // 1. Withdraw wstETH collateral from Morpho + if (collateralAmount > 0) { + callForwarder.doCall( + address(MORPHO), + abi.encodeCall( + MORPHO.withdrawCollateral, + (getMarketParams(), collateralAmount, address(callForwarder), address(callForwarder)) + ) + ); + } + + // 2. Convert wstETH to WETH for debt repayment + if (debtAmount > 0) { + // Unwrap wstETH → stETH + bytes memory unwrapResult = callForwarder.doCall( + address(WSTETH), + abi.encodeWithSelector(WSTETH.unwrap.selector, collateralAmount) + ); + uint256 stethAmount = abi.decode(unwrapResult, (uint256)); + + // For immediate repayment, we need ETH + // Option 1: Use Curve to swap stETH → ETH (most liquid) + // Option 2: Request withdrawal from Lido (requires waiting) + // For now, assume we swap via Curve or similar + // uint256 ethReceived = _swapStethToEth(stethAmount); + + // 3. Wrap ETH to WETH + // callForwarder.doCallWithValue( + // address(WETH), + // abi.encodeWithSelector(WETH.deposit.selector), + // ethReceived + // ); + + // 4. Approve and repay debt to Morpho + callForwarder.doCall( + address(WETH), + abi.encodeWithSelector(WETH.approve.selector, address(MORPHO), type(uint256).max) + ); + + callForwarder.doCall( + address(MORPHO), + abi.encodeCall(MORPHO.repay, (getMarketParams(), 0, debtAmount, address(callForwarder), new bytes(0))) + ); + } + } + + /** + * @inheritdoc IStrategy + */ + function finalizeRequestExit(bytes32 _requestId) external pure { + // Exits are synchronous in this implementation + // No async finalization needed + revert("Exits are synchronous"); + } + + // ================================================================================= + // HELPER VIEWS + // ================================================================================= + + /** + * @inheritdoc IStrategy + */ + function mintedStethSharesOf(address _user) external view returns (uint256) { + IStrategyCallForwarder callForwarder = getStrategyCallForwarderAddress(_user); + return POOL_.mintedStethSharesOf(address(callForwarder)); + } + + /** + * @inheritdoc IStrategy + */ + function remainingMintingCapacitySharesOf(address _user, uint256 _ethToFund) external view returns (uint256) { + IStrategyCallForwarder callForwarder = getStrategyCallForwarderAddress(_user); + return POOL_.remainingMintingCapacitySharesOf(address(callForwarder), _ethToFund); + } + + /** + * @inheritdoc IStrategy + */ + function wstethOf(address _user) external view returns (uint256) { + IStrategyCallForwarder callForwarder = getStrategyCallForwarderAddress(_user); + return WSTETH.balanceOf(address(callForwarder)); + } + + /** + * @inheritdoc IStrategy + */ + function stvOf(address _user) external view returns (uint256) { + IStrategyCallForwarder callForwarder = getStrategyCallForwarderAddress(_user); + return POOL_.balanceOf(address(callForwarder)); + } + + /** + * @notice Returns the Morpho position details for a user + * @param _user The user address + * @return supplyShares The amount of supply shares + * @return borrowShares The amount of borrow shares + * @return collateral The amount of wstETH collateral + */ + function morphoPositionOf( + address _user + ) external view returns (uint256 supplyShares, uint256 borrowShares, uint256 collateral) { + IStrategyCallForwarder callForwarder = getStrategyCallForwarderAddress(_user); + Position memory position = MORPHO.position(MARKET_ID, address(callForwarder)); + supplyShares = position.supplyShares; + borrowShares = position.borrowShares; + collateral = position.collateral; + } + + /** + * @notice Calculates the current leverage ratio for a user + * @param _user The user address + * @return leverageBp The leverage in basis points (10000 = 1x) + * @dev This is a simplified calculation and may not be fully accurate + */ + function currentLeverageOf(address _user) external view returns (uint256 leverageBp) { + IStrategyCallForwarder callForwarder = getStrategyCallForwarderAddress(_user); + Position memory position = MORPHO.position(MARKET_ID, address(callForwarder)); + + // For now, return a simple ratio + // TODO: Implement proper leverage calculation based on initial deposit tracking + if (position.collateral == 0) return 10000; + + leverageBp = 10000; // Placeholder + } + + // ================================================================================= + // POOL OPERATIONS + // ================================================================================= + + /** + * @inheritdoc IStrategy + */ + function requestWithdrawalFromPool( + address _recipient, + uint256 _stvToWithdraw, + uint256 _stethSharesToRebalance + ) external returns (uint256 requestId) { + IStrategyCallForwarder callForwarder = _getOrCreateCallForwarder(msg.sender); + + bytes memory data = callForwarder.doCall( + address(POOL_.WITHDRAWAL_QUEUE()), + abi.encodeWithSelector( + WithdrawalQueue.requestWithdrawal.selector, + _recipient, + _stvToWithdraw, + _stethSharesToRebalance + ) + ); + + requestId = abi.decode(data, (uint256)); + } + + /** + * @inheritdoc IStrategy + */ + function burnWsteth(uint256 _wstethToBurn) external { + IStrategyCallForwarder callForwarder = _getOrCreateCallForwarder(msg.sender); + + callForwarder.doCall( + address(WSTETH), + abi.encodeWithSelector(WSTETH.approve.selector, address(POOL_), _wstethToBurn) + ); + + callForwarder.doCall(address(POOL_), abi.encodeWithSelector(StvStETHPool.burnWsteth.selector, _wstethToBurn)); + } + + // ================================================================================= + // PAUSE/RESUME + // ================================================================================= + + /** + * @notice Pause supply operations + */ + function pauseSupply() external { + _checkRole(SUPPLY_PAUSE_ROLE, msg.sender); + _pauseFeature(SUPPLY_FEATURE); + } + + /** + * @notice Resume supply operations + */ + function resumeSupply() external { + _checkRole(SUPPLY_RESUME_ROLE, msg.sender); + _resumeFeature(SUPPLY_FEATURE); + } + + // ================================================================================= + // RECOVERY + // ================================================================================= + + /** + * @notice Recovers ERC20 tokens from the call forwarder + * @param _token The token to recover + * @param _recipient The recipient of the tokens + * @param _amount The amount of tokens to recover + */ + function recoverERC20(address _token, address _recipient, uint256 _amount) external { + if (_token == address(0)) revert ZeroArgument("_token"); + if (_recipient == address(0)) revert ZeroArgument("_recipient"); + if (_amount == 0) revert ZeroArgument("_amount"); + + IStrategyCallForwarder callForwarder = _getOrCreateCallForwarder(msg.sender); + callForwarder.doCall(_token, abi.encodeWithSelector(IERC20.transfer.selector, _recipient, _amount)); + } +}