// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {AaveQuotePushFlashReceiver} from "./AaveQuotePushFlashReceiver.sol"; interface IQuotePushSweepableReceiver { function owner() external view returns (address); function quoteSurplusBalance(address quoteToken, uint256 reserveRetained) external view returns (uint256 surplus); function sweepQuoteSurplus(address quoteToken, address to, uint256 reserveRetained) external returns (uint256 amount); function transferOwnership(address newOwner) external; } /** * @title QuotePushTreasuryManager * @notice Minimal treasury manager for Aave quote-push retained surplus. * Intended flow: * 1. Receiver retains quote surplus after flash repayment. * 2. This manager, as receiver owner, harvests that surplus into itself. * 3. Owner/operator distributes quote to gas and recycle recipients. * * Gas replenishment is still an operator policy in quote units; converting * quote into native gas token remains off-chain / external. */ contract QuotePushTreasuryManager is Ownable { using SafeERC20 for IERC20; IQuotePushSweepableReceiver public immutable receiver; IERC20 public immutable quoteToken; address public operator; address public gasRecipient; address public recycleRecipient; uint256 public receiverReserveRetained; uint256 public managerReserveRetained; error BadConfig(); error Unauthorized(); error NothingToHarvest(); error InsufficientAvailable(uint256 available, uint256 requested); error ReceiverNotOwnedByManager(); event OperatorUpdated(address indexed previousOperator, address indexed newOperator); event RecipientsUpdated(address indexed gasRecipient, address indexed recycleRecipient); event ReservesUpdated(uint256 receiverReserveRetained, uint256 managerReserveRetained); event ReceiverSurplusHarvested(address indexed token, uint256 amount, uint256 receiverReserveRetained); event QuoteDistributed(address indexed token, address indexed to, uint256 amount, bytes32 purpose); event TokenRescued(address indexed token, address indexed to, uint256 amount); event ManagedCycleExecuted( address indexed flashAsset, uint256 flashAmount, uint256 harvested, uint256 gasAmount, uint256 recycleAmount ); event ManagedReceiverOwnershipTransferred(address indexed previousOwner, address indexed newOwner); modifier onlyOwnerOrOperator() { if (msg.sender != owner() && msg.sender != operator) revert Unauthorized(); _; } constructor( address initialOwner, address receiver_, address quoteToken_, address operator_, address gasRecipient_, address recycleRecipient_, uint256 receiverReserveRetained_, uint256 managerReserveRetained_ ) Ownable(initialOwner) { if ( initialOwner == address(0) || receiver_ == address(0) || quoteToken_ == address(0) || gasRecipient_ == address(0) || recycleRecipient_ == address(0) ) revert BadConfig(); receiver = IQuotePushSweepableReceiver(receiver_); quoteToken = IERC20(quoteToken_); operator = operator_; gasRecipient = gasRecipient_; recycleRecipient = recycleRecipient_; receiverReserveRetained = receiverReserveRetained_; managerReserveRetained = managerReserveRetained_; } function receiverOwner() public view returns (address) { return receiver.owner(); } function isReceiverOwnedByManager() public view returns (bool) { return receiver.owner() == address(this); } function quoteBalance() public view returns (uint256) { return quoteToken.balanceOf(address(this)); } function availableQuote() public view returns (uint256 available) { uint256 balance = quoteBalance(); if (balance > managerReserveRetained) { available = balance - managerReserveRetained; } } function receiverSweepableQuote() public view returns (uint256) { return receiver.quoteSurplusBalance(address(quoteToken), receiverReserveRetained); } function setOperator(address newOperator) external onlyOwner { emit OperatorUpdated(operator, newOperator); operator = newOperator; } function setRecipients(address gasRecipient_, address recycleRecipient_) external onlyOwner { if (gasRecipient_ == address(0) || recycleRecipient_ == address(0)) revert BadConfig(); gasRecipient = gasRecipient_; recycleRecipient = recycleRecipient_; emit RecipientsUpdated(gasRecipient_, recycleRecipient_); } function setReserves(uint256 receiverReserveRetained_, uint256 managerReserveRetained_) external onlyOwner { receiverReserveRetained = receiverReserveRetained_; managerReserveRetained = managerReserveRetained_; emit ReservesUpdated(receiverReserveRetained_, managerReserveRetained_); } function harvestReceiverSurplus() public onlyOwnerOrOperator returns (uint256 amount) { amount = receiver.sweepQuoteSurplus(address(quoteToken), address(this), receiverReserveRetained); if (amount == 0) revert NothingToHarvest(); emit ReceiverSurplusHarvested(address(quoteToken), amount, receiverReserveRetained); } function runManagedCycle( address flashAsset, uint256 flashAmount, AaveQuotePushFlashReceiver.QuotePushParams calldata params, bool harvestSurplus, uint256 gasHoldbackTargetRaw ) external onlyOwnerOrOperator returns (uint256 harvested, uint256 gasAmount, uint256 recycleAmount) { if (!isReceiverOwnedByManager()) revert ReceiverNotOwnedByManager(); AaveQuotePushFlashReceiver(address(receiver)).flashQuotePush(flashAsset, flashAmount, params); if (harvestSurplus) { uint256 sweepable = receiverSweepableQuote(); if (sweepable > 0) { harvested = harvestReceiverSurplus(); } } uint256 available = availableQuote(); gasAmount = _min(available, gasHoldbackTargetRaw); recycleAmount = available - gasAmount; if (gasAmount > 0 || recycleAmount > 0) { _distributeConfigured(gasAmount, recycleAmount); } emit ManagedCycleExecuted(flashAsset, flashAmount, harvested, gasAmount, recycleAmount); } function distributeQuote(address to, uint256 amount, bytes32 purpose) external onlyOwnerOrOperator { _distributeQuote(to, amount, purpose); } function distributeToConfiguredRecipients(uint256 gasAmount, uint256 recycleAmount) external onlyOwnerOrOperator { _distributeConfigured(gasAmount, recycleAmount); } function transferManagedReceiverOwnership(address newOwner) external onlyOwner { if (newOwner == address(0)) revert BadConfig(); if (!isReceiverOwnedByManager()) revert ReceiverNotOwnedByManager(); address previousOwner = receiver.owner(); receiver.transferOwnership(newOwner); emit ManagedReceiverOwnershipTransferred(previousOwner, newOwner); } function rescueToken(address token, address to, uint256 amount) external onlyOwner { if (token == address(0) || to == address(0) || amount == 0) revert BadConfig(); if (token == address(quoteToken)) { _requireAvailable(amount); } IERC20(token).safeTransfer(to, amount); emit TokenRescued(token, to, amount); } function _distributeQuote(address to, uint256 amount, bytes32 purpose) internal { if (to == address(0) || amount == 0) revert BadConfig(); _requireAvailable(amount); quoteToken.safeTransfer(to, amount); emit QuoteDistributed(address(quoteToken), to, amount, purpose); } function _distributeConfigured(uint256 gasAmount, uint256 recycleAmount) internal { uint256 requested = gasAmount + recycleAmount; _requireAvailable(requested); if (gasAmount > 0) { quoteToken.safeTransfer(gasRecipient, gasAmount); emit QuoteDistributed(address(quoteToken), gasRecipient, gasAmount, bytes32("gas")); } if (recycleAmount > 0) { quoteToken.safeTransfer(recycleRecipient, recycleAmount); emit QuoteDistributed(address(quoteToken), recycleRecipient, recycleAmount, bytes32("recycle")); } } function _requireAvailable(uint256 requested) internal view { uint256 available = availableQuote(); if (requested > available) revert InsufficientAvailable(available, requested); } function _min(uint256 a, uint256 b) internal pure returns (uint256) { return a < b ? a : b; } }