// 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} 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); } /// The Aave v3 calls this adapter makes, in Aave's own argument order. /// /// The argument-order warnings are copied verbatim from LoanShiftAdapter, /// because copying one call to make another is exactly how a supply lands on the /// referral slot: `supply`'s onBehalfOf is THIRD and `borrow`'s is FIFTH. interface IAavePool { function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external; function borrow( address asset, uint256 amount, uint256 interestRateMode, uint16 referralCode, address onBehalfOf ) external; /// Six aggregate words in the pool's BASE CURRENCY. Only the first two are /// read: total collateral and total debt, the ratio the owner's ceiling is /// stated in. function getUserAccountData(address user) external view returns (uint256, uint256, uint256, uint256, uint256, uint256); } /// Morpho's flash loan, the free one. See LoanShiftAdapter for why the ADAPTER /// takes the loan and not the account, and why the lender is Morpho and never /// Balancer. interface IMorphoFlash { function flashLoan(address token, uint256 assets, bytes calldata data) external; } /// @title LeverageAaveV3Adapter — lever an Aave v3 position up in one transaction. /// /// # What it does /// /// Increasing leverage by hand means buying more collateral you cannot afford: /// borrow against what you have, swap the borrow into collateral, supply it, /// borrow again — and each round is a smaller step, several transactions, and a /// position that is briefly whatever the last incomplete step left it. A flash /// loan collapses the loop into one atomic move: /// /// flash the loan asset -> swap it into collateral -> supply (with any /// principal the owner adds) -> borrow the loan asset -> repay the flash. /// /// The position ends with more collateral and more debt, at the leverage the /// owner asked for, and never passes through an intermediate state anybody can /// interrupt. /// /// # Why this mirrors LoanShiftAdapter and does not extend FlashEngine /// /// LoanShift is the only adapter that both takes a Morpho flash AND sells through /// the first-party aggregation router (`SwapAdapter.IAggregationRouter`), with /// the route as an untrusted separate argument floored by a measured balance /// delta. That is exactly the shape a leverage needs, so this follows it to the /// line: same flash gating (payload pinning, callback-spent), same `_sell` / /// `_sellAndMeasure`, same "the adapter ends holding nothing" delta check. It is /// single-venue (Aave v3) on purpose — one venue per contract keeps every one of /// these under the 24KB EIP-170 limit that split loan.shift from loan.close. /// /// # What a compromised agent can and cannot do /// /// The agent signs the `Leverage` message freely on the production path. It /// cannot drain: the swap is floored by `minOut` against the measured delta, the /// principal the owner adds is charged against the epoch cap, the flash nets to /// zero, and — the load-bearing one — the resulting position is checked against /// the OWNER'S LTV ceiling after the borrow. A bad route that buys little /// collateral for a large borrow lands as a high LTV and reverts the whole /// transaction. See `_requireLtvCeiling`. contract LeverageAaveV3Adapter is BaseAdapter { /// Aave's variable-rate mode. Stable rate is deprecated and never offered. uint256 internal constant BPS = 10_000; uint256 internal constant AAVE_VARIABLE = 2; /// What A authorises at install. Distinct from a plain `borrow`: an owner who /// allowed borrowing has not thereby allowed something to lever the whole /// position up in one shot. Distinct per venue so authorising it here does /// not authorise the v4 or Morpho leverage adapters. bytes32 public constant OP_LEVERAGE = "leverage_aave_v3"; 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 curated market supplies the token PAIR, exactly as it does for an Aave // leg of a shift: "the name is what supplies the token pair either way". A // plan names one from the protocol's curated set and never passes a pair of // addresses, which would let it point a lever at assets nobody approved. // Read from AdapterCuration (`useMarket`), the one curation table, found // through the registry — this contract holds no address of its own for it. struct Leverage { address account; address owner; bytes32 taskId; uint256 deadline; /// WHICH Morpho, by name — the flash lender. bytes32 venue; /// WHICH aggregation router, by name. bytes32 router; /// The Aave v3 pool, by the name the account installed. bytes32 aaveVenue; /// The curated market name, which supplies (collateralToken, loanToken). bytes32 market; /// Extra collateral the owner adds from the account on top of what the /// flash buys. Zero is legal: a pure lever-up of existing holdings. uint256 principalAmount; /// The loan asset to flash-borrow and swap into collateral. uint256 flashAmount; /// The floor on the flash -> collateral sale, against the measured delta. uint256 minOut; /// The loan asset to borrow once the collateral is supplied. Must cover /// the flash repay; the surplus, if any, stays in the account. uint256 borrowAmount; uint16 maxFeeBps; /// Accept a SHORT FILL on the flash -> collateral swap. Default (false) is /// fill-or-kill, which is the safe choice for a flash: the borrow repays /// the flash and the position is sized from what this buys, so a partial /// fill lands a different position than signed. True lets the router take /// what the price supports; the minOut floor and the borrow-covers-flash /// check still apply, so an underfill that cannot repay the flash reverts. /// Signed, so a compromised agent cannot flip it. bool partialFill; } /// The owner's LTV ceiling is the whole protection on the resulting leverage, /// so it is NOT a signed field: a number B fills in is a number a compromised /// B chooses. The ceiling lives on the account (`ltvCeilingOf`) and only the /// owner can tighten it. See LoanShiftAdapter's H-1 note. 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); /// The pro-rata borrow needed to repay the flash exceeds what the owner /// authorised as `borrowAmount`. Under-authorised, not a fill problem. 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 ); // Transient across the flash, cleared on the way out. Same set and the same // gating as LoanShiftAdapter. 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 ("DeFiLoopsLeverageAaveV3Adapter", "1"); } /// Split across two encodes for the reason LoanShift's is: fourteen value /// fields run the stack out under `via_ir=false`, and the concatenation is /// byte-identical to what one encode would produce. 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 ); } /// Lever an Aave v3 position up. /// /// The route is a separate argument for the reason it is in SwapAdapter: it is /// large, it is untrusted, and signing it would pin the plan to a block's /// liquidity. function leverage( Leverage calldata s, bytes calldata signature, IAggregationRouter.Path[] calldata route ) external { if (_inFlight) revert ReentrantLeverage(); if (s.flashAmount == 0) revert NothingToFlash(); // The flash buys collateral, so there is always a sale and it always // needs a route and a floor. A message that omitted either would sell // into nothing or accept any price. 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); // A market whose two legs are the same asset has no swap to make and no // leverage to take — it is a rate position, which is what the neutral // shift is for, not this. if (m.collateralToken == m.loanToken) revert SameAsset(m.collateralToken); // WHAT LEAVES THE ACCOUNT IRREVERSIBLY is the principal the owner adds: // the flash returns within the transaction, the borrow is an inflow the // LTV ceiling bounds, and the swap converts the flash inside the account. // Charge the cap against the collateral the owner actually contributes. 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); // An adapter that keeps a balance is an adapter somebody can drain, and a // DELTA rather than an absolute balance so a stray donation cannot brick // it. See LoanShiftAdapter for the full account of why this is `>` and not // `!=`. uint256 kept = IERC20(m.loanToken).balanceOf(address(this)); if (kept > held) revert AdapterRetainedFunds(m.loanToken, kept - held); } /// Morpho's callback, gated four ways exactly as LoanShift gates it: the /// caller is the lender we called, we are in flight, the payload is the one we /// encoded (H-7), and this is the first time we have seen it. 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; address pool = _venueNotRouter(account, s.aaveVenue); // The owner's own holding of the loan asset before the flash arrives. The // pro-rata borrow below is sized to leave this untouched: a leverage // spends ONLY the flash and what the swap returns, never the account's // other funds. uint256 preBase = IERC20(m.loanToken).balanceOf(account); // The flash arrives at the adapter; hand it to the account so the account // is the one that sells, supplies and borrows. _transferThis(m.loanToken, account, assets); // Sell the flash into collateral, floored by the signed minOut against the // account's measured balance delta. Untrusted route, exactly as in // SwapAdapter / LoanShift. uint256 bought = _sellAndMeasure(s, route, m, account, pool); _supplyBorrowSettle(s, m, account, pool, assets, bought, preBase); } /// Supply what the swap returned (plus the owner's principal), then borrow /// PRO-RATA: only what the flash still needs after the unused flash the swap /// left. On a full fill the swap consumes the whole flash, `unused` is zero /// and the borrow equals the flash; on a short fill (`partialFill`) the swap /// consumed less, so `unused` is the remainder and the borrow scales down with /// it — the position keeps its leverage ratio instead of over-borrowing /// against less collateral. `borrowAmount` is the owner's authorised CEILING. function _supplyBorrowSettle( Leverage memory s, IMorpho.MarketParams memory m, address account, address pool, uint256 assets, uint256 bought, uint256 preBase ) internal { uint256 supplied = bought + s.principalAmount; _approveFrom(account, m.collateralToken, pool, supplied); _exec( account, pool, 0, abi.encodeCall(IAavePool.supply, (m.collateralToken, supplied, account, 0)) ); _requireNoResidual(account, m.collateralToken, pool); // The flash the swap did not consume is still in the account (over // preBase); borrow only the gap, capped at the authorised ceiling. 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, pool, 0, abi.encodeCall(IAavePool.borrow, (m.loanToken, needed, AAVE_VARIABLE, 0, account)) ); } uint256 available = IERC20(m.loanToken).balanceOf(account) - preBase; if (available < assets) revert BorrowDoesNotCoverTheFlash(assets, available); _settle(m.loanToken, assets); // The owner's ceiling, against Aave's own numbers after the borrow — the // whole protection on the leverage the message asked for. _requireLtvCeiling(account, _aaveLtvBps(pool, account)); emit Levered(account, s.market, s.principalAmount, assets, bought, supplied, needed); } /// Sell the flash-loaned asset into collateral and report what ACTUALLY /// arrived. Copied to the line from LoanShiftAdapter: approve the router for /// exactly the input, swap, clear the residual allowance so no standing /// transferFrom right survives, measure the delta, enforce the floor. function _sellAndMeasure( Leverage memory s, IAggregationRouter.Path[] memory route, IMorpho.MarketParams memory m, address account, address // pool, unused here; kept so the signature reads with _run ) internal returns (uint256 delivered) { address router = _venueRouter(account, s.router); uint256 before = IERC20(m.collateralToken).balanceOf(account); _approveFrom(account, m.loanToken, router, s.flashAmount); _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, // The owner's signed choice. False (default) is // fill-or-kill; true lets the router fill what the price // supports, with the minOut floor and the // borrow-covers-flash check as the backstops. partiallyFillable: s.partialFill }), route ) ) ); _requireNoResidual(account, m.loanToken, router); delivered = IERC20(m.collateralToken).balanceOf(account) - before; if (delivered < s.minOut) revert DeliveredLessThanFloor(s.minOut, delivered); } /// Hand the loan back and let Morpho pull it. Copied from LoanShiftAdapter. function _settle(address loanToken, uint256 assets) internal { _exec(_account, loanToken, 0, abi.encodeCall(IERC20.transfer, (address(this), assets))); _approveThis(loanToken, _morpho, assets); } function _aaveLtvBps(address pool, address account) internal view returns (uint256) { (uint256 collateral, uint256 debt,,,,) = IAavePool(pool).getUserAccountData(account); if (collateral == 0) return debt == 0 ? 0 : type(uint256).max; return (debt * BPS + collateral - 1) / collateral; } /// Approve from THIS contract (the flash settle), not from the account. Empty /// returndata is success — USDT-class tokens return nothing. Copied from /// LoanShiftAdapter, EXTCODESIZE-guarded per M-16. 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(); } /// Approve from the ACCOUNT. Copied from LoanShiftAdapter. 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(); } }