// 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 {LoanHealth} from "./lib/LoanHealth.sol"; import {IMorpho} 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); } /// Aave v4's spoke. Copied from LoanShiftAdapter; assets are numeric reserve ids. interface IAaveV4Spoke { function withdraw(uint256 reserveId, uint256 amount, address onBehalfOf) external; function repay(uint256 reserveId, uint256 amount, address onBehalfOf) external; } interface IMorphoFlash { function flashLoan(address token, uint256 assets, bytes calldata data) external; } /// @title DeleverageAaveV4Adapter — reduce an Aave v4 position's leverage in one /// transaction, without closing it. /// /// The Aave v4 sibling of DeleverageAaveV3Adapter. The flow is the same — flash /// the loan asset, repay, withdraw the freed collateral, sell it back into the /// loan asset, repay the flash — and differs only where v4 does: /// - assets are numeric reserve ids (`AdapterCuration.useReserve`), never /// addresses; /// - v4's `repay` takes an EXACT amount and has no `max` sentinel, so the flash /// is the amount repaid — deleverage's natural PARTIAL shape (repay some, /// keep the position open), which is exactly what v4 cannot express for a /// whole-position MOVE but can for an in-place reduction; /// - there is NO aToken clamp: v4 holds the position on the spoke rather than /// in a rebasing token, so there is no rounded balance to cap the withdraw /// against and nothing to refund. /// /// Everything else — Morpho free flash, 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 LoanShift's. contract DeleverageAaveV4Adapter is BaseAdapter { uint256 internal constant BPS = 10_000; bytes32 public constant OP_DELEVERAGE = "deleverage_aave_v4"; bytes32 internal constant DELEVERAGE_TYPEHASH = keccak256( "Deleverage(address account,address owner,bytes32 taskId,uint256 deadline,bytes32 venue," "bytes32 router,bytes32 aaveVenue,bytes32 market,uint256 flashAmount," "uint256 collateralAmount,uint256 minOut,uint16 maxFeeBps,bool partialFill)" ); // The token PAIR comes from the curated market, and each token's reserve id // from the protocol's reserve table keyed by the spoke — both AdapterCuration, // found through the registry. struct Deleverage { address account; address owner; bytes32 taskId; uint256 deadline; bytes32 venue; // WHICH Morpho, the flash lender bytes32 router; // WHICH aggregation router bytes32 aaveVenue; // the Aave v4 spoke, by name bytes32 market; // curated name -> (collateralToken, loanToken) uint256 flashAmount; // loan asset flashed and repaid into the debt 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 NoPositionData(address venue); 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 LtvRoseOnDeleverage(uint256 beforeBps, uint256 afterBps); 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 ("DeFiLoopsDeleverageAaveV4Adapter", "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.aaveVenue ); } function _tail(Deleverage calldata s) internal pure returns (bytes memory) { return abi.encode( s.market, s.flashAmount, 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; address spoke = _venueNotRouter(account, s.aaveVenue); uint256 base = IERC20(m.loanToken).balanceOf(account); uint256 ltvBefore = _ltvBps(spoke, account); _repayAndWithdraw(s, m, account, spoke, assets); uint256 delivered = _sellAndMeasure(s, route, m, account); _finish(s, m, account, spoke, assets, base, delivered, ltvBefore); } function _repayAndWithdraw( Deleverage memory s, IMorpho.MarketParams memory m, address account, address spoke, uint256 assets ) internal { uint256 loanId = curationOf(address(registry)).useReserve(spoke, m.loanToken, false); _transferThis(m.loanToken, account, assets); _approveFrom(account, m.loanToken, spoke, assets); // v4 repay takes an exact amount — no `max`. The flash is what is repaid. _exec(account, spoke, 0, abi.encodeCall(IAaveV4Spoke.repay, (loanId, assets, account))); _requireNoResidual(account, m.loanToken, spoke); // No aToken clamp: v4 holds the position on the spoke, not in a rebasing // token, so there is no rounded balance to cap against. uint256 collId = curationOf(address(registry)).useReserve(spoke, m.collateralToken, false); _exec(account, spoke, 0, abi.encodeCall(IAaveV4Spoke.withdraw, (collId, s.collateralAmount, account))); } function _finish( Deleverage memory s, IMorpho.MarketParams memory m, address account, address spoke, 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 = _ltvBps(spoke, account); // Improving but still over the owner ceiling must still be allowed. 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); } function _ltvBps(address spoke, address account) internal view returns (uint256) { (bool ok, bytes memory ret) = spoke.staticcall( abi.encodeWithSelector(bytes4(keccak256("getUserAccountData(address)")), account) ); if (!ok || ret.length < 192) revert NoPositionData(spoke); uint256 thresholdWad; uint256 hfWad; assembly { thresholdWad := mload(add(ret, 64)) hfWad := mload(add(ret, 96)) } // `LoanHealth.v4LtvBps` is internal, so this inlines and links nothing. return LoanHealth.v4LtvBps(thresholdWad, hfWad); } 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(); } }