// 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 and the reads a deleverage 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); /// Bring the market's totals to this block, so a debt share can be priced. function accrueInterest(IMorpho.MarketParams calldata m) external; function market(bytes32 id) external view returns ( uint128 totalSupplyAssets, uint128 totalSupplyShares, uint128 totalBorrowAssets, uint128 totalBorrowShares, uint128 lastUpdate, uint128 fee ); } /// @title DeleverageMorphoAdapter — reduce a Morpho Blue position's leverage in /// one transaction, without closing it. /// /// The Morpho sibling of DeleverageAaveV3Adapter. Same flow — flash the loan /// asset, repay, withdraw the freed collateral, sell it back into the loan asset, /// repay the flash — and differs only where Morpho does: /// - the flash lender and the position are the SAME Morpho (one `venue`); /// - `repayAll` clears the debt by SHARES, not assets: a Morpho debt is held in /// shares that accrue, so repaying a fixed amount leaves dust and dust debt /// blocks the withdraw. The flash is the CEILING; the share repay takes what /// it needs and whatever the flash did not cover comes out of the account and /// is charged to the epoch cap; /// - `repay` ends with `bytes data` and `withdrawCollateral` ends with an /// `address receiver` — the argument shapes are not the ones the names /// suggest, so they are written out. /// /// Everything else — the four-gate callback, the untrusted route floored by a /// measured delta, the cap on the withdrawn collateral, the adapter-holds-nothing /// check, and the owner's LTV ceiling — is LoanShiftAdapter's, to the line. contract DeleverageMorphoAdapter is BaseAdapter { uint256 internal constant BPS = 10_000; uint256 internal constant MAX_DEBT_SLACK_BPS = 500; uint256 internal constant SLACK_BPS_BASE = 10_000; bytes32 public constant OP_DELEVERAGE = "deleverage_morpho"; error DebtExceedsFlash(uint256 due, uint256 ceiling); error LtvRoseOnDeleverage(uint256 beforeBps, uint256 afterBps); bytes32 internal constant DELEVERAGE_TYPEHASH = keccak256( "Deleverage(address account,address owner,bytes32 taskId,uint256 deadline,bytes32 venue," "bytes32 router,bytes32 market,uint256 flashAmount,bool repayAll,uint256 collateralAmount," "uint256 minOut,uint16 maxFeeBps,bool partialFill)" ); // The market's five words come from the curated market, read from // AdapterCuration through the registry. struct Deleverage { 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 flashAmount; // loan asset flashed; a ceiling when repayAll bool repayAll; // clear the debt by shares (leaves an unlevered position) uint256 collateralAmount; // collateral withdrawn and sold uint256 minOut; // floor on collateral -> loan, measured 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 ReentrantDeleverage(); error DeliveredLessThanFloor(uint256 floor, uint256 delivered); error SaleDidNotCoverTheFlash(uint256 flash, uint256 gained); error AdapterRetainedFunds(address asset, uint256 amount); error SameAsset(address token); event Deleveraged( address indexed account, bytes32 indexed market, uint256 repaid, uint256 withdrawn, uint256 delivered, uint256 surplus ); 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 ("DeFiLoopsDeleverageMorphoAdapter", "1"); } function digest(Deleverage calldata s) public view returns (bytes32) { return _hashTypedData(keccak256(bytes.concat(_head(s), _tail(s)))); } function _head(Deleverage calldata s) internal pure returns (bytes memory) { return abi.encode( DELEVERAGE_TYPEHASH, s.account, s.owner, s.taskId, s.deadline, s.venue, s.router, s.market ); } function _tail(Deleverage calldata s) internal pure returns (bytes memory) { return abi.encode( s.flashAmount, s.repayAll, s.collateralAmount, s.minOut, s.maxFeeBps, s.partialFill ); } function deleverage( Deleverage calldata s, bytes calldata signature, IAggregationRouter.Path[] calldata route ) external { if (_inFlight) revert ReentrantDeleverage(); 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_DELEVERAGE); IMorpho.MarketParams memory m = curationOf(address(registry)).useMarket(s.account, s.market, false); if (m.collateralToken == m.loanToken) revert SameAsset(m.collateralToken); _charge(s.account, m.collateralToken, s.collateralAmount); 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 { (Deleverage memory s, IAggregationRouter.Path[] memory route, IMorpho.MarketParams memory m) = abi.decode(data, (Deleverage, IAggregationRouter.Path[], IMorpho.MarketParams)); address account = _account; // Pre-flash holding of the loan asset: the settle coverage below is a gain // over this, so the owner's own balance is never swept. uint256 base = IERC20(m.loanToken).balanceOf(account); // Accrue before both LTV snapshots so a mid-callback accrue cannot look // like a rise and revert a real deleverage. IMorphoFlash(_morpho).accrueInterest(m); uint256 ltvBefore = _morphoLtvBps(_morpho, m, account); _repayAndWithdraw(s, m, account, assets); uint256 delivered = _sellAndMeasure(s, route, m, account); _finish(s, m, account, assets, base, delivered, ltvBefore); } function _repayAndWithdraw( Deleverage memory s, IMorpho.MarketParams memory m, address account, uint256 assets ) internal { address morpho = _morpho; _transferThis(m.loanToken, account, assets); if (s.repayAll) { // Approve what the SHARES cost, not what was flashed — the two differ // by the interest accrued since the plan was written, and an approval // one wei short fails as a bare "transferFrom reverted". Repaying by // assets instead would leave dust debt that blocks the withdraw. // H-8: slack-bound `due` and charge the flash shortfall (as the // comment below always claimed). uint256 due = _debtOf(morpho, m, account); uint256 ceiling = assets + (assets * MAX_DEBT_SLACK_BPS) / SLACK_BPS_BASE; if (due > ceiling) revert DebtExceedsFlash(due, ceiling); if (due > assets) _charge(account, m.loanToken, due - assets); _approveFrom(account, m.loanToken, morpho, due); (, uint128 shares,) = IMorphoFlash(morpho).position(keccak256(abi.encode(m)), account); _exec(account, morpho, 0, abi.encodeCall(IMorpho.repay, (m, 0, shares, account, bytes("")))); } else { _approveFrom(account, m.loanToken, morpho, assets); _exec(account, morpho, 0, abi.encodeCall(IMorpho.repay, (m, assets, 0, account, bytes("")))); } _exec( account, morpho, 0, abi.encodeCall(IMorpho.withdrawCollateral, (m, s.collateralAmount, account, account)) ); _requireNoResidual(account, m.loanToken, morpho); } function _finish( Deleverage memory s, IMorpho.MarketParams memory m, address account, uint256 assets, uint256 base, uint256 delivered, uint256 ltvBefore ) internal { uint256 bal = IERC20(m.loanToken).balanceOf(account); uint256 gained = bal < base ? 0 : bal - base; if (gained < assets) revert SaleDidNotCoverTheFlash(assets, gained); _settle(m.loanToken, assets); uint256 ltvAfter = _morphoLtvBps(_morpho, m, account); // Deleverage must not raise LTV — only the absolute ceiling used to be // checked, which made repay≪withdraw synthetic leverage up to the cap. // Improving but still over the owner ceiling must still be allowed // (price-moved positions cannot otherwise deleverage down). if (ltvAfter > ltvBefore) revert LtvRoseOnDeleverage(ltvBefore, ltvAfter); emit Deleveraged(account, s.market, assets, s.collateralAmount, delivered, gained - assets); } function _sellAndMeasure( Deleverage 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.loanToken).balanceOf(account); _approveFrom(account, m.collateralToken, router, s.collateralAmount); _sell(s, route, m, account, router); _requireNoResidual(account, m.collateralToken, router); delivered = IERC20(m.loanToken).balanceOf(account) - before; if (delivered < s.minOut) revert DeliveredLessThanFloor(s.minOut, delivered); } function _sell( Deleverage 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.collateralToken, tokenOut: m.loanToken, recipient: account, amountIn: s.collateralAmount, 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); } /// What the whole debt costs to clear at THIS block. accrue first, or the /// totals are stale and the answer is short by exactly the interest that /// matters. Rounded up (Morpho rounds debt up in its favour). function _debtOf(address morpho, IMorpho.MarketParams memory m, address account) internal returns (uint256) { IMorphoFlash(morpho).accrueInterest(m); bytes32 id = keccak256(abi.encode(m)); (, uint128 shares,) = IMorphoFlash(morpho).position(id, account); return _assetsFor(morpho, id, shares); } 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 _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; } 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(); } }