// 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. Copied /// with LoanShiftAdapter's warnings: `withdraw`'s third argument is a RECEIVER /// and `repay`'s is an onBehalfOf — copying one to make the other is how a /// withdrawal ends up sent somewhere nobody chose. interface IAavePool { function repay(address asset, uint256 amount, uint256 interestRateMode, address onBehalfOf) external returns (uint256); function withdraw(address asset, uint256 amount, address to) external returns (uint256); /// Aave 3.2, for capping a withdraw at the aToken balance. Not on the classic /// V3 pools — `_aTokenOf` falls back to `getReserveData` there. function getReserveAToken(address asset) external view returns (address); function getUserAccountData(address user) external view returns (uint256, uint256, uint256, uint256, uint256, uint256); } interface IMorphoFlash { function flashLoan(address token, uint256 assets, bytes calldata data) external; } /// @title DeleverageAaveV3Adapter — reduce an Aave v3 position's leverage in one /// transaction, without closing it. /// /// # What it does /// /// Reducing leverage by hand is the mirror of raising it and just as awkward: to /// take collateral out you must first repay debt you do not have the cash for. A /// flash loan supplies it for the length of one transaction: /// /// flash the loan asset -> repay part of the debt -> withdraw collateral the /// repay just freed -> swap the collateral back into the loan asset -> repay /// the flash. /// /// The position ends smaller — less collateral, less debt — and stays OPEN. The /// surplus of the sale over the flash, if any, stays in the account as the loan /// asset. Fully unwinding a position into cash is a different operation with a /// different authorisation; that is `loan.close`. /// /// # Why this mirrors LoanShiftAdapter /// /// The unwind half is `LoanShiftAdapter._unwindAave` to the line — repay by /// amount or `max`, then a withdraw CAPPED at the aToken balance (Aave's scaled /// balance rounds down, so "all of it" is one wei short and reverts), refunding /// the epoch cap for the wei the clamp shaved. The sell is `_sell` / /// `_sellAndMeasure`, floored by a measured delta against an untrusted route. The /// flash gating (payload pinning, callback-spent) and the adapter-holds-nothing /// delta check are identical. Single-venue on purpose: one venue per contract /// keeps each under the 24KB EIP-170 limit. /// /// # What a compromised agent can and cannot do /// /// Deleveraging moves value nowhere the agent controls — adapters deliver only to /// the account, never to an address — so the surplus lands in the account and the /// position shrinks. The swap is floored by `minOut`; the collateral pulled is /// charged against the epoch cap; and the resulting position is still checked /// against the owner's LTV ceiling, so a perverse "deleverage" that withdrew far /// more collateral than it repaid — leaving the position MORE levered — reverts. contract DeleverageAaveV3Adapter is BaseAdapter { uint256 internal constant BPS = 10_000; uint256 internal constant AAVE_VARIABLE = 2; /// Aave's "all of it" for repay. uint256 internal constant AAVE_ALL = type(uint256).max; /// Distinct per venue: authorising deleverage here does not authorise the v4 /// or Morpho ones, nor a plain repay/withdraw. bytes32 public constant OP_DELEVERAGE = "deleverage_aave_v3"; 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,bool repayAll," "uint256 collateralAmount,uint256 minOut,uint16 maxFeeBps,bool partialFill)" ); // The curated market supplies the token PAIR, read from AdapterCuration. See // LeverageAaveV3Adapter. struct Deleverage { 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; /// The loan asset to flash-borrow and repay into the debt. With /// `repayAll` this is a CEILING on the flash and the repay takes what it /// needs; otherwise it is the exact debt to clear. uint256 flashAmount; /// Clear the debt ENTIRELY (Aave works `max` out itself). Deleverage's /// ordinary use is PARTIAL — leave this false and pass the amount to /// repay. `loan.close` is the path for taking the whole position to cash. bool repayAll; /// How much collateral to withdraw and sell. Signed, not derived from a /// ratio this contract computed — a ratio needs a price, and a price this /// contract chose is a price nobody agreed to. uint256 collateralAmount; /// The floor on the collateral -> loan sale, against the measured delta. uint256 minOut; uint16 maxFeeBps; /// Accept a SHORT FILL on the collateral -> loan sale. Default (false) is /// fill-or-kill; true lets the router fill what the price supports, with /// the minOut floor and the sale-covers-flash check as the backstops. A /// partial fill leaves the unsold collateral in the account. Signed, so a /// compromised agent cannot flip it. bool partialFill; } error ApproveFailed(); error NotAToken(address token); error NoAToken(address asset); 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 ("DeFiLoopsDeleverageAaveV3Adapter", "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.repayAll, s.collateralAmount, s.minOut, s.maxFeeBps, s.partialFill ); } /// Reduce an Aave v3 position's leverage. The route is a separate untrusted /// argument, as in SwapAdapter / LoanShift. function deleverage( Deleverage calldata s, bytes calldata signature, IAggregationRouter.Path[] calldata route ) external { if (_inFlight) revert ReentrantDeleverage(); if (s.flashAmount == 0) revert NothingToFlash(); // The withdrawn collateral is always sold back into the loan asset, so // there is always a sale and it always needs a route and a floor. 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); // The collateral withdrawn and sold is what leaves the position; the // flash returns within the transaction. Charge the cap against it, and // the callback refunds the wei the aToken clamp shaves off. 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 for the reason LoanShift splits _run/_unwind/_finish: /// repay + clamp + withdraw + sell + settle + ceiling in one frame runs the /// stack out with `via_ir=false`. 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 pool = _venueNotRouter(account, s.aaveVenue); // The account's own holding of the loan asset before anything moves. // Everything after is a gain over it, so the owner's existing balance is // never swept into the flash repay. uint256 base = IERC20(m.loanToken).balanceOf(account); uint256 ltvBefore = _aaveLtvBps(pool, account); _repayAndWithdraw(s, m, account, pool, assets); uint256 delivered = _sellAndMeasure(s, route, m, account); _finish(s, m, account, pool, assets, base, delivered, ltvBefore); } /// Repay the debt with the flash, then withdraw the collateral it freed — /// clamped at the aToken balance, refunding the cap for the wei the clamp /// shaves. Writes the clamped amount back into `s` (a memory struct passed by /// reference), so `_sellAndMeasure` sells exactly what was withdrawn. function _repayAndWithdraw( Deleverage memory s, IMorpho.MarketParams memory m, address account, address pool, uint256 assets ) internal { _transferThis(m.loanToken, account, assets); _approveFrom(account, m.loanToken, pool, assets); _exec( account, pool, 0, abi.encodeCall( IAavePool.repay, (m.loanToken, s.repayAll ? AAVE_ALL : assets, AAVE_VARIABLE, account) ) ); // `max` repays only the actual debt, which can be less than the approved // `assets`; clear the remaining allowance so none survives the call. _requireNoResidual(account, m.loanToken, pool); // Aave's scaled aToken balance rounds down, so a plan naming the number it // supplied is one wei short. Refund the epoch cap for the shaved wei — see // LoanShiftAdapter._unwindAave. uint256 want = s.collateralAmount; uint256 have = IERC20(_aTokenOf(pool, m.collateralToken)).balanceOf(account); if (have < want) { _refund(account, m.collateralToken, want - have, want); s.collateralAmount = have; } // Third argument is a RECEIVER, not an onBehalfOf. _exec( account, pool, 0, abi.encodeCall(IAavePool.withdraw, (m.collateralToken, s.collateralAmount, account)) ); } /// The sale must cover the flash; the excess is the owner's. Then settle, and /// hold the resulting position to the owner's ceiling. function _finish( Deleverage memory s, IMorpho.MarketParams memory m, address account, address pool, uint256 assets, uint256 base, uint256 delivered, uint256 ltvBefore ) internal { // The sale (plus any flash surplus left when `repayAll` repaid less than // it was handed) must cover the flash. The gain over `base` is what the // account settles from; the excess above the flash is the owner's. 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); // Refuse a raise: repay≪withdraw used to be synthetic leverage up to the // ceiling because only `_requireLtvCeiling` ran. Improving but still over // the owner ceiling must still be allowed (close policy). uint256 ltvAfter = _aaveLtvBps(pool, account); if (ltvAfter > ltvBefore) revert LtvRoseOnDeleverage(ltvBefore, ltvAfter); emit Deleveraged(account, s.market, assets, s.collateralAmount, delivered, gained - assets); } /// Sell the collateral and report what ACTUALLY arrived. Copied to the line /// from LoanShiftAdapter._sellAndMeasure / _sell. 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); _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, // 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 // sale-covers-flash check as the backstops. partiallyFillable: s.partialFill }), route ) ) ); _requireNoResidual(account, m.collateralToken, router); delivered = IERC20(m.loanToken).balanceOf(account) - before; if (delivered < s.minOut) revert DeliveredLessThanFloor(s.minOut, delivered); } 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; } /// WHICH aTOKEN REPRESENTS AN ASSET, both ways Aave answers it. Copied from /// LoanShiftAdapter._aTokenOf — two paths because the chains disagree. function _aTokenOf(address pool, address asset) internal view returns (address aToken) { (bool ok, bytes memory ret) = pool.staticcall(abi.encodeWithSelector(IAavePool.getReserveAToken.selector, asset)); if (ok && ret.length >= 32) { aToken = abi.decode(ret, (address)); if (aToken != address(0)) return aToken; } (ok, ret) = pool.staticcall( abi.encodeWithSelector(bytes4(keccak256("getReserveData(address)")), asset) ); if (!ok || ret.length < 288) revert NoAToken(asset); assembly { aToken := mload(add(ret, 288)) } } 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(); } }