Add managed quote-push treasury workflows
This commit is contained in:
@@ -4,6 +4,7 @@ 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);
|
||||
@@ -11,6 +12,7 @@ interface IQuotePushSweepableReceiver {
|
||||
function sweepQuoteSurplus(address quoteToken, address to, uint256 reserveRetained)
|
||||
external
|
||||
returns (uint256 amount);
|
||||
function transferOwnership(address newOwner) external;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,6 +42,7 @@ contract QuotePushTreasuryManager is Ownable {
|
||||
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);
|
||||
@@ -47,6 +50,14 @@ contract QuotePushTreasuryManager is Ownable {
|
||||
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();
|
||||
@@ -124,22 +135,49 @@ contract QuotePushTreasuryManager is Ownable {
|
||||
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 {
|
||||
uint256 requested = gasAmount + recycleAmount;
|
||||
_requireAvailable(requested);
|
||||
_distributeConfigured(gasAmount, recycleAmount);
|
||||
}
|
||||
|
||||
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 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 {
|
||||
@@ -158,8 +196,26 @@ contract QuotePushTreasuryManager is Ownable {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// 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";
|
||||
import {ISwapRouter} from "../bridge/trustless/interfaces/ISwapRouter.sol";
|
||||
|
||||
interface IDODOTwoHopSwapExactIn {
|
||||
function swapExactIn(address pool, address tokenIn, uint256 amountIn, uint256 minAmountOut)
|
||||
external
|
||||
returns (uint256 amountOut);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title TwoHopDodoToUniswapV3MultiHopExternalUnwinder
|
||||
* @notice Unwinds through two DODO PMM hops followed by a final Uniswap V3 exactInput path.
|
||||
* @dev `data` =
|
||||
* abi.encode(
|
||||
* address poolA,
|
||||
* address poolB,
|
||||
* address midToken,
|
||||
* uint256 minMidOut,
|
||||
* address intermediateToken,
|
||||
* uint256 minIntermediateOut,
|
||||
* bytes uniswapPath
|
||||
* )
|
||||
* Route shape:
|
||||
* tokenIn --(poolA via DODO)--> midToken
|
||||
* midToken --(poolB via DODO)--> intermediateToken
|
||||
* intermediateToken --(Uniswap V3 path)--> tokenOut
|
||||
*/
|
||||
contract TwoHopDodoToUniswapV3MultiHopExternalUnwinder {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
address public immutable integration;
|
||||
address public immutable router;
|
||||
|
||||
error BadParams();
|
||||
|
||||
constructor(address integration_, address router_) {
|
||||
if (integration_ == address(0) || router_ == address(0)) revert BadParams();
|
||||
integration = integration_;
|
||||
router = router_;
|
||||
}
|
||||
|
||||
function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data)
|
||||
external
|
||||
returns (uint256 amountOut)
|
||||
{
|
||||
if (tokenIn == address(0) || tokenOut == address(0) || tokenIn == tokenOut || amountIn == 0) revert BadParams();
|
||||
|
||||
(
|
||||
address poolA,
|
||||
address poolB,
|
||||
address midToken,
|
||||
uint256 minMidOut,
|
||||
address intermediateToken,
|
||||
uint256 minIntermediateOut,
|
||||
bytes memory uniswapPath
|
||||
) = abi.decode(data, (address, address, address, uint256, address, uint256, bytes));
|
||||
|
||||
if (poolA == address(0) || poolB == address(0) || midToken == address(0) || intermediateToken == address(0)) {
|
||||
revert BadParams();
|
||||
}
|
||||
if (midToken == tokenIn || midToken == tokenOut || intermediateToken == tokenIn || intermediateToken == midToken) {
|
||||
revert BadParams();
|
||||
}
|
||||
if (uniswapPath.length < 43) revert BadParams();
|
||||
|
||||
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
|
||||
IERC20(tokenIn).forceApprove(integration, amountIn);
|
||||
|
||||
uint256 midOut = IDODOTwoHopSwapExactIn(integration).swapExactIn(poolA, tokenIn, amountIn, minMidOut);
|
||||
if (midOut == 0) revert BadParams();
|
||||
|
||||
IERC20(midToken).forceApprove(integration, midOut);
|
||||
uint256 intermediateOut =
|
||||
IDODOTwoHopSwapExactIn(integration).swapExactIn(poolB, midToken, midOut, minIntermediateOut);
|
||||
if (intermediateOut == 0) revert BadParams();
|
||||
|
||||
IERC20(intermediateToken).forceApprove(router, intermediateOut);
|
||||
amountOut = ISwapRouter(router).exactInput(
|
||||
ISwapRouter.ExactInputParams({
|
||||
path: uniswapPath,
|
||||
recipient: msg.sender,
|
||||
deadline: block.timestamp + 300,
|
||||
amountIn: intermediateOut,
|
||||
amountOutMinimum: minAmountOut
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user