Files
smom-dbis-138/contracts/flash/QuotePushTreasuryManager.sol
2026-04-13 21:37:33 -07:00

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;
}
}