// 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 — a DIFFERENT PROTOCOL from v3, not a newer version. An asset /// here is a NUMERIC RESERVE ID belonging to the spoke, never an address: the id /// is looked up from the protocol's reserve table (AdapterCuration, keyed by the /// spoke's address), so a wrong id is a curation error, not something a signed /// message can carry. Copied from LoanShiftAdapter. interface IAaveV4Spoke { function supply(uint256 reserveId, uint256 amount, address onBehalfOf) external; function withdraw(uint256 reserveId, uint256 amount, address onBehalfOf) external; function borrow(uint256 reserveId, uint256 amount, address onBehalfOf) external; function repay(uint256 reserveId, uint256 amount, address onBehalfOf) external; /// The call that makes a v4 supply COUNT. Until it is called the supply is a /// deposit and nothing more, and borrowing against it fails with a /// health-factor error naming a position the protocol does not think exists. function setUsingAsCollateral(uint256 reserveId, bool useAsCollateral, address onBehalfOf) external; } interface IMorphoFlash { function flashLoan(address token, uint256 assets, bytes calldata data) external; } /// @title LeverageAaveV4Adapter — lever an Aave v4 position up in one transaction. /// /// The Aave v4 sibling of LeverageAaveV3Adapter, identical in shape and differing /// only where the protocol does: /// - assets are numeric reserve ids resolved from the protocol's curation /// (`AdapterCuration.useReserve`), never addresses; /// - a supply is a DEPOSIT until `setUsingAsCollateral` is called, so the /// enable sits between the supply and the borrow; /// - the LTV read comes from the spoke's threshold and health factor rather /// than from collateral/debt totals (see `_ltvBps`). /// /// Everything else — Morpho free flash, the four-gate callback with payload /// pinning, the untrusted-route swap floored by a measured delta, the cap on the /// owner's principal, the adapter-holds-nothing check, and the owner's LTV /// ceiling enforced after the borrow — is LoanShiftAdapter's, to the line. The /// curated market name supplies the token pair. contract LeverageAaveV4Adapter is BaseAdapter { uint256 internal constant BPS = 10_000; bytes32 public constant OP_LEVERAGE = "leverage_aave_v4"; bytes32 internal constant LEVERAGE_TYPEHASH = keccak256( "Leverage(address account,address owner,bytes32 taskId,uint256 deadline,bytes32 venue," "bytes32 router,bytes32 aaveVenue,bytes32 market,uint256 principalAmount," "uint256 flashAmount,uint256 minOut,uint256 borrowAmount,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 Leverage { 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 principalAmount; // owner's added collateral; zero is legal uint256 flashAmount; // loan asset flash-borrowed and swapped to collateral uint256 minOut; // floor on flash -> collateral, measured uint256 borrowAmount; // loan asset to borrow; 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 NoPositionData(address venue); 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 ("DeFiLoopsLeverageAaveV4Adapter", "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.aaveVenue ); } function _tail(Leverage calldata s) internal pure returns (bytes memory) { return abi.encode( s.market, 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); } /// Split across helpers so no frame runs the stack out under via_ir=false. 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; address spoke = _venueNotRouter(account, s.aaveVenue); // 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, spoke, assets, bought, preBase); } function _supplyBorrowSettle( Leverage memory s, IMorpho.MarketParams memory m, address account, address spoke, uint256 assets, uint256 bought, uint256 preBase ) internal { uint256 supplied = _supply(s, m, account, spoke, bought); uint256 borrowed = _borrowSettle(s, m, account, spoke, assets, preBase); emit Levered(account, s.market, s.principalAmount, assets, bought, supplied, borrowed); } /// Supply the bought collateral plus the owner's principal, then ENABLE it — /// a v4 supply is a deposit until `setUsingAsCollateral`, and borrowing /// against one that was never enabled fails naming a position the protocol /// does not think exists. function _supply( Leverage memory s, IMorpho.MarketParams memory m, address account, address spoke, uint256 bought ) internal returns (uint256 supplied) { supplied = bought + s.principalAmount; uint256 collId = curationOf(address(registry)).useReserve(spoke, m.collateralToken, true); _approveFrom(account, m.collateralToken, spoke, supplied); _exec(account, spoke, 0, abi.encodeCall(IAaveV4Spoke.supply, (collId, supplied, account))); _requireNoResidual(account, m.collateralToken, spoke); _exec( account, spoke, 0, abi.encodeCall(IAaveV4Spoke.setUsingAsCollateral, (collId, true, account)) ); } /// Borrow PRO-RATA — only the flash gap the swap left, capped at the owner's /// authorised `borrowAmount`. See LeverageAaveV3Adapter._supplyBorrowSettle. function _borrowSettle( Leverage memory s, IMorpho.MarketParams memory m, address account, address spoke, uint256 assets, uint256 preBase ) internal returns (uint256 needed) { uint256 unused = IERC20(m.loanToken).balanceOf(account) - preBase; needed = assets > unused ? assets - unused : 0; if (needed > s.borrowAmount) revert BorrowCeilingTooLow(needed, s.borrowAmount); if (needed != 0) { uint256 loanId = curationOf(address(registry)).useReserve(spoke, m.loanToken, true); _exec(account, spoke, 0, abi.encodeCall(IAaveV4Spoke.borrow, (loanId, needed, account))); } uint256 available = IERC20(m.loanToken).balanceOf(account) - preBase; if (available < assets) revert BorrowDoesNotCoverTheFlash(assets, available); _settle(m.loanToken, assets); _requireLtvCeiling(account, _ltvBps(spoke, account)); } /// Sell the flash into collateral, measured, floored. Copied from /// LoanShiftAdapter; the swap call is split out for the stack. 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); } /// v4 exposes a threshold and a health factor, not the amounts, so the ratio /// is threshold/HF. Copied from LoanShiftAdapter._aaveV4LtvBps. 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(); } }