// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; interface IAavePoolLike { function flashLoanSimple( address receiverAddress, address asset, uint256 amount, bytes calldata params, uint16 referralCode ) external; } interface IAaveFlashLoanSimpleReceiver { function executeOperation( address asset, uint256 amount, uint256 premium, address initiator, bytes calldata params ) external returns (bool); } interface IAaveDODOQuotePushSwapExactIn { function swapExactIn(address pool, address tokenIn, uint256 amountIn, uint256 minAmountOut) external returns (uint256 amountOut); } interface IAaveExternalUnwinder { function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data) external returns (uint256 amountOut); } interface IAaveAtomicBridgeCoordinator { struct CreateIntentParams { uint64 sourceChain; uint64 destinationChain; address assetIn; address assetOut; uint256 amountIn; uint256 minAmountOut; address recipient; uint256 deadline; bytes32 routeId; } function createIntent(CreateIntentParams calldata p) external returns (bytes32 obligationId); function submitCommitment(bytes32 obligationId, bytes32 settlementMode) external; function obligationEscrow() external view returns (address); } /** * @title AaveQuotePushFlashReceiver * @notice Aave V3 flashLoanSimple receiver for the quote-push workflow: * flash borrow quote -> buy PMM base -> unwind base externally -> repay lender, retaining any surplus. */ contract AaveQuotePushFlashReceiver is IAaveFlashLoanSimpleReceiver { using SafeERC20 for IERC20; address public immutable pool; struct QuotePushParams { address integration; address pmmPool; address baseToken; address externalUnwinder; uint256 minOutPmm; uint256 minOutUnwind; bytes unwindData; AtomicBridgeParams atomicBridge; } struct AtomicBridgeParams { address coordinator; uint64 sourceChain; uint64 destinationChain; address destinationAsset; uint256 bridgeAmount; uint256 minDestinationAmount; address destinationRecipient; uint256 destinationDeadline; bytes32 routeId; bytes32 settlementMode; bool submitCommitment; } error UntrustedPool(); error UntrustedInitiator(); error BadParams(); error InsufficientToRepay(); error InvalidAtomicBridge(); event QuotePushExecuted( address indexed quoteToken, address indexed baseToken, uint256 borrowedAmount, uint256 premium, uint256 baseOut, uint256 unwindOut, uint256 surplus ); event AtomicBridgeTriggered( bytes32 indexed obligationId, address indexed coordinator, address indexed destinationRecipient, uint256 bridgeAmount, uint256 minDestinationAmount ); constructor(address pool_) { pool = pool_; } function flashQuotePush(address asset, uint256 amount, QuotePushParams calldata params) external { IAavePoolLike(pool).flashLoanSimple(address(this), asset, amount, abi.encode(address(this), params), 0); } function executeOperation( address asset, uint256 amount, uint256 premium, address initiator, bytes calldata params ) external returns (bool) { if (msg.sender != pool) revert UntrustedPool(); (address expectedInitiator, QuotePushParams memory p) = abi.decode(params, (address, QuotePushParams)); if (initiator != expectedInitiator) revert UntrustedInitiator(); if ( p.integration == address(0) || p.pmmPool == address(0) || p.baseToken == address(0) || p.externalUnwinder == address(0) ) revert BadParams(); if (p.baseToken == asset) revert BadParams(); uint256 baseOut = _swapQuoteForBase(asset, amount, p.integration, p.pmmPool, p.minOutPmm); uint256 baseBal = IERC20(p.baseToken).balanceOf(address(this)); if (p.atomicBridge.coordinator != address(0)) { _triggerAtomicBridge(p.baseToken, baseBal, p.atomicBridge); } uint256 unwindOut = _unwindBaseIntoQuote(p.baseToken, asset, p.externalUnwinder, p.minOutUnwind, p.unwindData); uint256 surplus = _approveRepayment(asset, amount + premium); emit QuotePushExecuted(asset, p.baseToken, amount, premium, baseOut, unwindOut, surplus); return true; } function _swapQuoteForBase(address asset, uint256 amount, address integration, address pmmPool, uint256 minOutPmm) internal returns (uint256 baseOut) { IERC20(asset).forceApprove(integration, amount); baseOut = IAaveDODOQuotePushSwapExactIn(integration).swapExactIn(pmmPool, asset, amount, minOutPmm); } function _unwindBaseIntoQuote( address baseToken, address quoteToken, address externalUnwinder, uint256 minOutUnwind, bytes memory unwindData ) internal returns (uint256 unwindOut) { uint256 remainingBase = IERC20(baseToken).balanceOf(address(this)); IERC20(baseToken).forceApprove(externalUnwinder, remainingBase); unwindOut = IAaveExternalUnwinder(externalUnwinder).unwind(baseToken, quoteToken, remainingBase, minOutUnwind, unwindData); } function _approveRepayment(address quoteToken, uint256 need) internal returns (uint256 surplus) { IERC20 quote = IERC20(quoteToken); uint256 quoteBal = quote.balanceOf(address(this)); if (quoteBal < need) revert InsufficientToRepay(); surplus = quoteBal - need; quote.forceApprove(pool, need); } function _triggerAtomicBridge(address baseToken, uint256 baseBal, AtomicBridgeParams memory atomicBridge) internal { if ( atomicBridge.destinationAsset == address(0) || atomicBridge.destinationRecipient == address(0) || atomicBridge.bridgeAmount == 0 || atomicBridge.bridgeAmount > baseBal || atomicBridge.destinationDeadline <= block.timestamp ) revert InvalidAtomicBridge(); address escrowAddress = IAaveAtomicBridgeCoordinator(atomicBridge.coordinator).obligationEscrow(); IERC20(baseToken).forceApprove(escrowAddress, atomicBridge.bridgeAmount); bytes32 obligationId = IAaveAtomicBridgeCoordinator(atomicBridge.coordinator).createIntent( IAaveAtomicBridgeCoordinator.CreateIntentParams({ sourceChain: atomicBridge.sourceChain, destinationChain: atomicBridge.destinationChain, assetIn: baseToken, assetOut: atomicBridge.destinationAsset, amountIn: atomicBridge.bridgeAmount, minAmountOut: atomicBridge.minDestinationAmount, recipient: atomicBridge.destinationRecipient, deadline: atomicBridge.destinationDeadline, routeId: atomicBridge.routeId }) ); if (atomicBridge.submitCommitment) { IAaveAtomicBridgeCoordinator(atomicBridge.coordinator).submitCommitment( obligationId, atomicBridge.settlementMode ); } emit AtomicBridgeTriggered( obligationId, atomicBridge.coordinator, atomicBridge.destinationRecipient, atomicBridge.bridgeAmount, atomicBridge.minDestinationAmount ); } }