// SPDX-License-Identifier: MIT pragma solidity 0.8.28; import {AgentRegistry} from "../AgentRegistry.sol"; import {IAggregationRouter} from "./SwapAdapter.sol"; import {FlashEngine, IERC20, INPM} from "./lib/FlashEngine.sol"; /// @title AaveRevertAdapter — Aave↔Revert moves, split from RevertLeverageAdapter for EIP-170. contract AaveRevertAdapter is FlashEngine { bytes32 public constant OP_MOVE_TO_AAVE = "move_to_aave"; bytes32 public constant OP_MOVE_FROM_AAVE = "move_from_aave"; bytes32 public constant OP_INCREASE_FROM_AAVE = "increase_from_aave"; bytes32 internal constant ACT_TYPEHASH = keccak256( "Act(address account,address owner,bytes32 taskId,uint256 deadline,bytes32 operation," "bytes32 flashVenue,bytes32 revertVenue,bytes32 npmVenue,bytes32 aaveVenue,bytes32 routerVenue," "uint256 tokenId,uint256 amount,uint256 amount0,uint256 amount1,uint256 amount0Min,uint256 amount1Min," "address token0,address token1,uint24 fee,int24 tickLower,int24 tickUpper,uint16 bps," "uint256 minOut,uint256 borrowAmount,uint256 collateralAmount,address asset," "address amountFrom,bytes32 amountFromTask,uint16 maxLtvBps)" ); struct Act { address account; address owner; bytes32 taskId; uint256 deadline; bytes32 operation; bytes32 flashVenue; bytes32 revertVenue; bytes32 npmVenue; bytes32 aaveVenue; bytes32 routerVenue; uint256 tokenId; uint256 amount; uint256 amount0; uint256 amount1; uint256 amount0Min; uint256 amount1Min; address token0; address token1; uint24 fee; int24 tickLower; int24 tickUpper; uint16 bps; uint256 minOut; uint256 borrowAmount; uint256 collateralAmount; address asset; address amountFrom; bytes32 amountFromTask; uint16 maxLtvBps; } event Acted(address indexed account, bytes32 indexed operation, uint256 tokenId); error UnknownOperation(bytes32 operation); constructor(AgentRegistry registry_) FlashEngine(registry_) {} function _domainNameAndVersion() internal pure override returns (string memory, string memory) { return ("DeFiLoopsAaveRevertAdapter", "1"); } function digest(Act calldata a) public view returns (bytes32) { return _hashTypedData(keccak256(bytes.concat(_head(a), _mid(a), _tail(a)))); } function _head(Act calldata a) internal pure returns (bytes memory) { return abi.encode( ACT_TYPEHASH, a.account, a.owner, a.taskId, a.deadline, a.operation, a.flashVenue, a.revertVenue, a.npmVenue, a.aaveVenue ); } function _mid(Act calldata a) internal pure returns (bytes memory) { return abi.encode( a.routerVenue, a.tokenId, a.amount, a.amount0, a.amount1, a.amount0Min, a.amount1Min, a.token0, a.token1, a.fee ); } function _tail(Act calldata a) internal pure returns (bytes memory) { return abi.encode( a.tickLower, a.tickUpper, a.bps, a.minOut, a.borrowAmount, a.collateralAmount, a.asset, a.amountFrom, a.amountFromTask, a.maxLtvBps ); } function act(Act calldata a, bytes calldata signature, IAggregationRouter.Path[] calldata route) external { _consume(a.account, a.owner, a.taskId, a.deadline, digest(a), signature); _requireOperation(a.account, a.operation); uint256 flashAmt = _amountToSpend(a.account, a.amount, a.amountFrom, a.amountFromTask, a.asset); address balancer = _venueOf(a.account, a.flashVenue); _account = a.account; _flash(balancer, a.asset, flashAmt, abi.encode(a, route, flashAmt)); _account = address(0); emit Acted(a.account, a.operation, a.tokenId); } function _onFlash(address token, uint256 amount, uint256 fee, bytes memory data) internal override { (Act memory a, IAggregationRouter.Path[] memory route, uint256 flashAmt) = abi.decode(data, (Act, IAggregationRouter.Path[], uint256)); token; fee; address account = _account; _pushToAccount(a.asset, account, amount); if (a.operation == OP_MOVE_TO_AAVE) _moveToAave(a, flashAmt); else if (a.operation == OP_MOVE_FROM_AAVE) _moveFromAave(a, flashAmt); else if (a.operation == OP_INCREASE_FROM_AAVE) _increaseFromAave(a, flashAmt); else revert UnknownOperation(a.operation); uint256 owed = amount + fee; _pullFromAccount(a.asset, account, owed); route; } function _venues(Act memory a) internal view returns (address vault, address npm, address pool) { vault = _venueOf(_account, a.revertVenue); npm = _venueOf(_account, a.npmVenue); pool = _venueOf(_account, a.aaveVenue); } /// Fresh frame so `_moveToAave` does not overflow the stack (via_ir off). function _decreaseCollectAct( Act memory a, address npm, uint128 liq, address token0, address token1 ) internal returns (uint256 got0, uint256 got1) { // The tokens are PASSED and the deltas are RETURNED: every caller // already has the tokens and needs the deltas, `positions()` is a // twelve-field answer, and this adapter sits within a few hundred bytes // of EIP-170. return _decreaseCollect( _account, npm, a.tokenId, liq, a.deadline, a.amount0Min, a.amount1Min, token0, token1 ); } function _moveToAave(Act memory a, uint256 flashAmt) internal { address account = _account; (address vault, address npm, address pool) = _venues(a); (address token0, address token1,,,, uint128 liq) = _npmPosition(npm, a.tokenId); _revertRepayRemove(account, vault, a.asset, a.tokenId, flashAmt); (uint256 got0, uint256 got1) = _decreaseCollectAct(a, npm, liq, token0, token1); _aaveSupply(account, pool, token0, got0); _aaveSupply(account, pool, token1, got1); // Same class as SafeExit: the flash is repaid by a new Aave borrow — // charge the epoch for that debt open. _charge(account, a.asset, flashAmt); _aaveBorrow(account, pool, a.asset, flashAmt); _requireLtv(pool, account, a.maxLtvBps); } function _mintFromAct(Act memory a, address npm) internal returns (uint256 tokenId) { MintSpec memory m; m.npm = npm; m.token0 = a.token0; m.token1 = a.token1; m.fee = a.fee; m.tickLower = a.tickLower; m.tickUpper = a.tickUpper; m.amount0 = a.amount0; m.amount1 = a.amount1; // move_from_aave hashes these two into the Act and then minted with // zero floors, so the leg that takes the withdrawn collateral and puts // it into a Uni position accepted whatever ratio the pool offered at // that instant. m.amount0Min = a.amount0Min; m.amount1Min = a.amount1Min; m.deadline = a.deadline; uint256 used0; uint256 used1; (tokenId, used0, used1) = _mintNpm(m); _refundIfLess(_account, a.token1, a.amount1, used1); _refundIfLess(_account, a.token0, a.amount0, used0); } function _moveFromAave(Act memory a, uint256 flashAmt) internal { address account = _account; (address vault, address npm, address pool) = _venues(a); _aaveRepay(account, pool, a.asset, flashAmt); if (a.collateralAmount > 0) { _aaveWithdraw(account, pool, a.token0, a.amount0); if (a.amount1 > 0) _aaveWithdraw(account, pool, a.token1, a.amount1); } _charge(account, a.token0, a.amount0); if (a.amount1 > 0) _charge(account, a.token1, a.amount1); _approveFrom(account, a.token0, npm, a.amount0); _approveFrom(account, a.token1, npm, a.amount1); uint256 tokenId = _mintFromAct(a, npm); _requireNoResidual(account, a.token0, npm); _requireNoResidual(account, a.token1, npm); _revertCreateBorrow(account, npm, vault, a.asset, tokenId, flashAmt); _publishTokenId(account, a.taskId, tokenId); // H-1 OF THE ROUND-2 AUDIT: THIS SIGNED `maxLtvBps` AND DISCARDED IT. // // The field is hashed into the Act — so a plan reading the message // believes the leg is bounded — and `_requireLtv` was reached from // `_moveToAave` only. This arm takes collateral OUT of Aave // (`_aaveWithdraw` above) and leaves the debt where it is, which is the // half of the ratio that moves in the dangerous direction. Signing a // bound and dropping it is worse than offering none. // // `_requireLtv` takes the TIGHTER of the signed field and the owner's // ceiling, so this is both halves of the bound at once. _requireLtv(pool, account, a.maxLtvBps); } function _increaseFromAave(Act memory a, uint256 flashAmt) internal { address account = _account; (address vault, address npm, address pool) = _venues(a); (address token0, address token1,,,,) = _npmPosition(npm, a.tokenId); _revertRepayRemove(account, vault, a.asset, a.tokenId, flashAmt); if (a.collateralAmount > 0) _aaveWithdraw(account, pool, a.token0, a.collateralAmount); // BOTH LEGS, AS SIGNED — the 2026-10-09 Base campaign // (a gen-6 fork test, removed with gen 6; TestGen7AdaptersRevertInRangeAdds now runs the add in range). // // This zeroed whichever leg was the flash asset, "so it can repay the // loan". It never needed to: the flash is repaid out of the Revert // borrow `_revertCreateBorrow` re-opens for the whole `flashAmt` // below, not out of anything added here. What zeroing it did do was // make an in-range add impossible — a position the price is inside // takes both tokens in its own ratio, so with one leg zero the // position manager computes zero liquidity and the pool refuses the // mint. For the ordinary WETH/USDC position flashing USDC, that was // every in-range position. // // So the flash asset's leg is the account's own balance of it, like // the other leg, charged like the other leg. A plan that signs more // than the account holds fails in the position manager's transfer or, // if it ate into the flash, in the flash repayment — never quietly. // // THE EPOCH CAP, charged on the CEILING and refunded on the fill: // increaseLiquidity takes a ratio of the two desired amounts, so the // leftover must not burn the epoch. The refund was promised here // before and never made — the fill was not even decoded. _charge(account, token0, a.amount0); _charge(account, token1, a.amount1); (uint256 used0, uint256 used1) = _addLiquidity(a, npm, token0, token1); _refundIfLess(account, token0, a.amount0, used0); _refundIfLess(account, token1, a.amount1, used1); _requireNoResidual(account, token0, npm); _requireNoResidual(account, token1, npm); _revertCreateBorrow(account, npm, vault, a.asset, a.tokenId, flashAmt); // The same signed-and-discarded bound as `_moveFromAave` above — H-1. // The audit's note that "`_increaseFromAave` has no Aave repayment at // all — pure unbounded collateral removal" is exactly why this one // needs it: the `_aaveWithdraw` at the top is the whole of this arm's // effect on the Aave position. _requireLtv(pool, account, a.maxLtvBps); } /// Fresh frame so `_increaseFromAave` does not overflow the stack /// (via_ir off) once it also has to hold what the fill actually used. function _addLiquidity(Act memory a, address npm, address token0, address token1) internal returns (uint256 used0, uint256 used1) { address account = _account; _approveFrom(account, token0, npm, a.amount0); _approveFrom(account, token1, npm, a.amount1); bytes memory ret = _exec( account, npm, 0, abi.encodeCall( INPM.increaseLiquidity, (INPM.IncreaseLiquidityParams({ tokenId: a.tokenId, amount0Desired: a.amount0, amount1Desired: a.amount1, amount0Min: a.amount0Min, amount1Min: a.amount1Min, deadline: a.deadline })) ) ); (, used0, used1) = abi.decode(ret, (uint128, uint256, uint256)); } }