222 lines
8.9 KiB
Solidity
222 lines
8.9 KiB
Solidity
// 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;
|
|
}
|
|
}
|