// SPDX-License-Identifier: MIT pragma solidity 0.8.28; import {BaseAdapter} from "../BaseAdapter.sol"; import {AgentRegistry} from "../AgentRegistry.sol"; import {curationOf} from "../AdapterCuration.sol"; import {IMorpho, IMorphoOracle} from "./MorphoAdapter.sol"; import {IAggregationRouter} from "./SwapAdapter.sol"; interface IERC20 { function approve(address spender, uint256 amount) external returns (bool); function transfer(address to, uint256 amount) external returns (bool); function balanceOf(address owner) external view returns (uint256); } /// Morpho's flash loan (free) and the reads a leverage needs. Copied from /// LoanShiftAdapter; the flash lender is Morpho and never Balancer. interface IMorphoFlash { function flashLoan(address token, uint256 assets, bytes calldata data) external; function position(bytes32 id, address user) external view returns (uint256 supplyShares, uint128 borrowShares, uint128 collateral); /// The totals a borrow share is valued against. The borrow this leverage just /// made accrued the market, so these are current without an explicit accrue. function market(bytes32 id) external view returns ( uint128 totalSupplyAssets, uint128 totalSupplyShares, uint128 totalBorrowAssets, uint128 totalBorrowShares, uint128 lastUpdate, uint128 fee ); } /// @title LeverageMorphoAdapter — lever a Morpho Blue position up in one /// transaction. /// /// The Morpho sibling of LeverageAaveV3Adapter. Same flow — flash the loan asset, /// swap it into collateral, supply, borrow, repay the flash — and differs only /// where Morpho does: /// - the flash lender and the position are the SAME Morpho (one `venue`), /// rather than Morpho-flash against an Aave position; /// - `supplyCollateral` ends with `bytes data` and `borrow` ends with an /// `address receiver` — the two argument shapes are not the ones the names /// suggest, so they are written out with the interface's warning; /// - the LTV read is Morpho's own arithmetic (borrowShares -> assets over /// collateral * oracle.price() / 1e36). /// /// Everything else — the four-gate callback with payload pinning, the untrusted /// route floored by a measured delta, the cap on the owner's principal, the /// adapter-holds-nothing check, and the owner's LTV ceiling after the borrow — is /// LoanShiftAdapter's, to the line. contract LeverageMorphoAdapter is BaseAdapter { uint256 internal constant BPS = 10_000; bytes32 public constant OP_LEVERAGE = "leverage_morpho"; bytes32 internal constant LEVERAGE_TYPEHASH = keccak256( "Leverage(address account,address owner,bytes32 taskId,uint256 deadline,bytes32 venue," "bytes32 router,bytes32 market,uint256 principalAmount,uint256 flashAmount,uint256 minOut," "uint256 borrowAmount,uint16 maxFeeBps,bool partialFill)" ); // The token pair AND the market's five words come from the curated market, // read from AdapterCuration through the registry. struct Leverage { address account; address owner; bytes32 taskId; uint256 deadline; bytes32 venue; // WHICH Morpho — flash lender AND the position's market bytes32 router; // WHICH aggregation router bytes32 market; // curated name -> full MarketParams uint256 principalAmount; // owner's added collateral; zero is legal uint256 flashAmount; // loan asset flashed and swapped to collateral uint256 minOut; // floor on flash -> collateral, measured uint256 borrowAmount; // loan asset borrowed; must cover the flash repay uint16 maxFeeBps; bool partialFill; // accept a short fill on the swap; signed. See v3. } error ApproveFailed(); error NotAToken(address token); error NoRoute(); error NoFloor(); error NothingToFlash(); error NotFlashLender(address caller); error PayloadNotOurs(); error CallbackAlreadyRan(); error NotInFlight(); error ReentrantLeverage(); error DeliveredLessThanFloor(uint256 floor, uint256 delivered); error BorrowDoesNotCoverTheFlash(uint256 flash, uint256 available); error BorrowCeilingTooLow(uint256 needed, uint256 ceiling); error AdapterRetainedFunds(address asset, uint256 amount); error SameAsset(address token); event Levered( address indexed account, bytes32 indexed market, uint256 principal, uint256 flashed, uint256 bought, uint256 supplied, uint256 borrowed ); address internal _account; address internal _morpho; bool internal _inFlight; bytes32 internal _payload; bool internal _callbackSpent; constructor(AgentRegistry registry_) BaseAdapter(registry_) {} function _domainNameAndVersion() internal pure override returns (string memory, string memory) { return ("DeFiLoopsLeverageMorphoAdapter", "1"); } function digest(Leverage calldata s) public view returns (bytes32) { return _hashTypedData(keccak256(bytes.concat(_head(s), _tail(s)))); } function _head(Leverage calldata s) internal pure returns (bytes memory) { return abi.encode( LEVERAGE_TYPEHASH, s.account, s.owner, s.taskId, s.deadline, s.venue, s.router, s.market ); } function _tail(Leverage calldata s) internal pure returns (bytes memory) { return abi.encode( s.principalAmount, s.flashAmount, s.minOut, s.borrowAmount, s.maxFeeBps, s.partialFill ); } function leverage( Leverage calldata s, bytes calldata signature, IAggregationRouter.Path[] calldata route ) external { if (_inFlight) revert ReentrantLeverage(); if (s.flashAmount == 0) revert NothingToFlash(); if (route.length == 0) revert NoRoute(); if (s.minOut == 0) revert NoFloor(); _consume(s.account, s.owner, s.taskId, s.deadline, digest(s), signature); _requireOperation(s.account, OP_LEVERAGE); IMorpho.MarketParams memory m = curationOf(address(registry)).useMarket(s.account, s.market, true); if (m.collateralToken == m.loanToken) revert SameAsset(m.collateralToken); if (s.principalAmount != 0) { _charge(s.account, m.collateralToken, s.principalAmount); } address morpho = _venueNotRouter(s.account, s.venue); _account = s.account; _morpho = morpho; _inFlight = true; uint256 held = IERC20(m.loanToken).balanceOf(address(this)); bytes memory payload = abi.encode(s, route, m); _payload = keccak256(payload); _callbackSpent = false; IMorphoFlash(morpho).flashLoan(m.loanToken, s.flashAmount, payload); _inFlight = false; _payload = bytes32(0); uint256 kept = IERC20(m.loanToken).balanceOf(address(this)); if (kept > held) revert AdapterRetainedFunds(m.loanToken, kept - held); } function onMorphoFlashLoan(uint256 assets, bytes calldata data) external { if (msg.sender != _morpho) revert NotFlashLender(msg.sender); if (!_inFlight) revert NotInFlight(); if (keccak256(data) != _payload) revert PayloadNotOurs(); if (_callbackSpent) revert CallbackAlreadyRan(); _callbackSpent = true; _run(assets, data); } function _run(uint256 assets, bytes calldata data) internal { (Leverage memory s, IAggregationRouter.Path[] memory route, IMorpho.MarketParams memory m) = abi.decode(data, (Leverage, IAggregationRouter.Path[], IMorpho.MarketParams)); address account = _account; // Owner's pre-existing loan asset; the pro-rata borrow leaves it untouched. uint256 preBase = IERC20(m.loanToken).balanceOf(account); _transferThis(m.loanToken, account, assets); uint256 bought = _sellAndMeasure(s, route, m, account); _supplyBorrowSettle(s, m, account, assets, bought, preBase); } /// Supply what the swap returned (plus principal), then borrow PRO-RATA: only /// the flash gap the swap left, capped at the authorised `borrowAmount`. See /// LeverageAaveV3Adapter._supplyBorrowSettle. function _supplyBorrowSettle( Leverage memory s, IMorpho.MarketParams memory m, address account, uint256 assets, uint256 bought, uint256 preBase ) internal { address morpho = _morpho; uint256 supplied = bought + s.principalAmount; _approveFrom(account, m.collateralToken, morpho, supplied); _exec(account, morpho, 0, abi.encodeCall(IMorpho.supplyCollateral, (m, supplied, account, bytes("")))); _requireNoResidual(account, m.collateralToken, morpho); uint256 unused = IERC20(m.loanToken).balanceOf(account) - preBase; uint256 needed = assets > unused ? assets - unused : 0; if (needed > s.borrowAmount) revert BorrowCeilingTooLow(needed, s.borrowAmount); if (needed != 0) { _exec(account, morpho, 0, abi.encodeCall(IMorpho.borrow, (m, needed, 0, account, account))); } uint256 available = IERC20(m.loanToken).balanceOf(account) - preBase; if (available < assets) revert BorrowDoesNotCoverTheFlash(assets, available); _settle(m.loanToken, assets); _requireLtvCeiling(account, _morphoLtvBps(morpho, m, account)); emit Levered(account, s.market, s.principalAmount, assets, bought, supplied, needed); } function _sellAndMeasure( Leverage memory s, IAggregationRouter.Path[] memory route, IMorpho.MarketParams memory m, address account ) internal returns (uint256 delivered) { address router = _venueRouter(account, s.router); uint256 before = IERC20(m.collateralToken).balanceOf(account); _approveFrom(account, m.loanToken, router, s.flashAmount); _sell(s, route, m, account, router); _requireNoResidual(account, m.loanToken, router); delivered = IERC20(m.collateralToken).balanceOf(account) - before; if (delivered < s.minOut) revert DeliveredLessThanFloor(s.minOut, delivered); } function _sell( Leverage memory s, IAggregationRouter.Path[] memory route, IMorpho.MarketParams memory m, address account, address router ) internal { _exec( account, router, 0, abi.encodeCall( IAggregationRouter.swap, ( IAggregationRouter.SwapDescription({ tokenIn: m.loanToken, tokenOut: m.collateralToken, recipient: account, amountIn: s.flashAmount, minAmountOut: s.minOut, deadline: s.deadline, maxFeeBps: s.maxFeeBps, partiallyFillable: s.partialFill }), route ) ) ); } function _settle(address loanToken, uint256 assets) internal { _exec(_account, loanToken, 0, abi.encodeCall(IERC20.transfer, (address(this), assets))); _approveThis(loanToken, _morpho, assets); } /// Morpho's own arithmetic: borrowShares valued up against this block's /// totals over collateral * oracle.price() / 1e36. Copied from /// LoanShiftAdapter._morphoLtvBps; the borrow just happened so the totals are /// current without an explicit accrue. function _morphoLtvBps(address morpho, IMorpho.MarketParams memory m, address account) internal view returns (uint256) { bytes32 id = keccak256(abi.encode(m)); (, uint128 shares, uint128 collateral) = IMorphoFlash(morpho).position(id, account); if (shares == 0) return 0; if (collateral == 0) return type(uint256).max; uint256 value = (uint256(collateral) * IMorphoOracle(m.oracle).price()) / 1e36; if (value == 0) return type(uint256).max; return (_assetsFor(morpho, id, shares) * BPS + value - 1) / value; } /// Borrow shares -> assets at this block's totals, Morpho's SharesMathLib /// (VIRTUAL_SHARES 1e6, VIRTUAL_ASSETS 1), rounded UP as Morpho rounds debt /// up. The borrow just accrued the market, so no explicit accrue is needed. function _assetsFor(address morpho, bytes32 id, uint256 shares) internal view returns (uint256) { (,, uint128 totalAssets, uint128 totalShares,,) = IMorphoFlash(morpho).market(id); uint256 num = shares * (uint256(totalAssets) + 1); uint256 den = uint256(totalShares) + 1e6; return (num + den - 1) / den; } function _approveThis(address token, address spender, uint256 amount) internal { _okTokenCall(token, abi.encodeCall(IERC20.approve, (spender, amount))); } function _transferThis(address token, address to, uint256 amount) internal { _okTokenCall(token, abi.encodeCall(IERC20.transfer, (to, amount))); } function _okTokenCall(address token, bytes memory data) internal { if (token.code.length == 0) revert NotAToken(token); (bool ok, bytes memory ret) = token.call(data); if (!ok) { assembly { revert(add(ret, 32), mload(ret)) } } if (ret.length != 0 && !abi.decode(ret, (bool))) revert ApproveFailed(); } function _approveFrom(address account, address asset, address spender, uint256 amount) internal { bytes memory ok = _exec(account, asset, 0, abi.encodeCall(IERC20.approve, (spender, amount))); if (ok.length != 0 && !abi.decode(ok, (bool))) revert ApproveFailed(); } }