Add managed quote-push treasury workflows
This commit is contained in:
@@ -7,6 +7,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import {QuotePushTreasuryManager} from "../../contracts/flash/QuotePushTreasuryManager.sol";
|
||||
import {AaveQuotePushFlashReceiver} from "../../contracts/flash/AaveQuotePushFlashReceiver.sol";
|
||||
|
||||
contract MockTreasuryToken is ERC20 {
|
||||
constructor() ERC20("Mock Treasury Token", "MTT") {}
|
||||
@@ -42,7 +43,49 @@ contract MockSweepableReceiver is Ownable {
|
||||
uint256 balance = token.balanceOf(address(this));
|
||||
require(balance > reserveRetained, "nothing to sweep");
|
||||
amount = balance - reserveRetained;
|
||||
token.safeTransfer(to, amount);
|
||||
IERC20(address(token)).safeTransfer(to, amount);
|
||||
}
|
||||
}
|
||||
|
||||
contract MockManagedCycleReceiver is Ownable {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
MockTreasuryToken internal immutable token;
|
||||
uint256 internal simulatedSurplusRaw;
|
||||
|
||||
constructor(address initialOwner, MockTreasuryToken token_) Ownable(initialOwner) {
|
||||
token = token_;
|
||||
}
|
||||
|
||||
function setSimulatedSurplusRaw(uint256 amount) external {
|
||||
simulatedSurplusRaw = amount;
|
||||
}
|
||||
|
||||
function flashQuotePush(address asset, uint256, AaveQuotePushFlashReceiver.QuotePushParams calldata) external {
|
||||
require(asset == address(token), "wrong asset");
|
||||
if (simulatedSurplusRaw > 0) {
|
||||
token.mint(address(this), simulatedSurplusRaw);
|
||||
}
|
||||
}
|
||||
|
||||
function quoteSurplusBalance(address quoteToken, uint256 reserveRetained) external view returns (uint256 surplus) {
|
||||
require(quoteToken == address(token), "wrong token");
|
||||
uint256 balance = token.balanceOf(address(this));
|
||||
if (balance > reserveRetained) {
|
||||
surplus = balance - reserveRetained;
|
||||
}
|
||||
}
|
||||
|
||||
function sweepQuoteSurplus(address quoteToken, address to, uint256 reserveRetained)
|
||||
external
|
||||
onlyOwner
|
||||
returns (uint256 amount)
|
||||
{
|
||||
require(quoteToken == address(token), "wrong token");
|
||||
uint256 balance = token.balanceOf(address(this));
|
||||
require(balance > reserveRetained, "nothing to sweep");
|
||||
amount = balance - reserveRetained;
|
||||
IERC20(address(token)).safeTransfer(to, amount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +93,8 @@ contract QuotePushTreasuryManagerTest is Test {
|
||||
MockTreasuryToken internal token;
|
||||
MockSweepableReceiver internal receiver;
|
||||
QuotePushTreasuryManager internal manager;
|
||||
MockManagedCycleReceiver internal cycleReceiver;
|
||||
QuotePushTreasuryManager internal cycleManager;
|
||||
|
||||
address internal constant OPERATOR = address(0xBEEF);
|
||||
address internal constant GAS_RECIPIENT = address(0xCAFE);
|
||||
@@ -64,6 +109,12 @@ contract QuotePushTreasuryManagerTest is Test {
|
||||
|
||||
receiver.transferOwnership(address(manager));
|
||||
token.mint(address(receiver), 1_000_000);
|
||||
|
||||
cycleReceiver = new MockManagedCycleReceiver(address(this), token);
|
||||
cycleManager = new QuotePushTreasuryManager(
|
||||
address(this), address(cycleReceiver), address(token), OPERATOR, GAS_RECIPIENT, RECYCLE_RECIPIENT, 10_000, 5_000
|
||||
);
|
||||
cycleReceiver.transferOwnership(address(cycleManager));
|
||||
}
|
||||
|
||||
function testHarvestReceiverSurplusKeepsReceiverReserve() public {
|
||||
@@ -106,4 +157,83 @@ contract QuotePushTreasuryManagerTest is Test {
|
||||
|
||||
assertEq(other.balanceOf(address(this)), 42, "owner should be able to rescue unrelated tokens");
|
||||
}
|
||||
|
||||
function testRunManagedCycleHarvestsAndDistributesAtomically() public {
|
||||
cycleReceiver.setSimulatedSurplusRaw(80_000);
|
||||
|
||||
AaveQuotePushFlashReceiver.QuotePushParams memory params = AaveQuotePushFlashReceiver.QuotePushParams({
|
||||
integration: address(0x1111),
|
||||
pmmPool: address(0x2222),
|
||||
baseToken: address(0x3333),
|
||||
externalUnwinder: address(0x4444),
|
||||
minOutPmm: 1,
|
||||
minOutUnwind: 1,
|
||||
unwindData: "",
|
||||
atomicBridge: AaveQuotePushFlashReceiver.AtomicBridgeParams({
|
||||
coordinator: address(0),
|
||||
sourceChain: 0,
|
||||
destinationChain: 0,
|
||||
destinationAsset: address(0),
|
||||
bridgeAmount: 0,
|
||||
minDestinationAmount: 0,
|
||||
destinationRecipient: address(0),
|
||||
destinationDeadline: 0,
|
||||
routeId: bytes32(0),
|
||||
settlementMode: bytes32(0),
|
||||
submitCommitment: false
|
||||
})
|
||||
});
|
||||
|
||||
vm.prank(OPERATOR);
|
||||
(uint256 harvested, uint256 gasAmount, uint256 recycleAmount) =
|
||||
cycleManager.runManagedCycle(address(token), 20_000, params, true, 15_000);
|
||||
|
||||
assertEq(harvested, 70_000, "receiver reserve should stay behind");
|
||||
assertEq(gasAmount, 15_000, "gas holdback should consume configured slice");
|
||||
assertEq(recycleAmount, 50_000, "remaining available quote should recycle");
|
||||
assertEq(token.balanceOf(GAS_RECIPIENT), 15_000, "gas recipient receives holdback");
|
||||
assertEq(token.balanceOf(RECYCLE_RECIPIENT), 50_000, "recycle recipient receives remaining quote");
|
||||
assertEq(token.balanceOf(address(cycleReceiver)), 10_000, "receiver reserve is retained");
|
||||
assertEq(token.balanceOf(address(cycleManager)), 5_000, "manager reserve is retained");
|
||||
}
|
||||
|
||||
function testOwnerCanTransferManagedReceiverOwnership() public {
|
||||
address newOwner = address(0xABCD);
|
||||
|
||||
manager.transferManagedReceiverOwnership(newOwner);
|
||||
|
||||
assertEq(manager.receiverOwner(), newOwner, "receiver ownership should move to requested address");
|
||||
assertFalse(manager.isReceiverOwnedByManager(), "manager should no longer own receiver");
|
||||
}
|
||||
|
||||
function testRunManagedCycleRevertsWhenManagerDoesNotOwnReceiver() public {
|
||||
AaveQuotePushFlashReceiver.QuotePushParams memory params = AaveQuotePushFlashReceiver.QuotePushParams({
|
||||
integration: address(0x1111),
|
||||
pmmPool: address(0x2222),
|
||||
baseToken: address(0x3333),
|
||||
externalUnwinder: address(0x4444),
|
||||
minOutPmm: 1,
|
||||
minOutUnwind: 1,
|
||||
unwindData: "",
|
||||
atomicBridge: AaveQuotePushFlashReceiver.AtomicBridgeParams({
|
||||
coordinator: address(0),
|
||||
sourceChain: 0,
|
||||
destinationChain: 0,
|
||||
destinationAsset: address(0),
|
||||
bridgeAmount: 0,
|
||||
minDestinationAmount: 0,
|
||||
destinationRecipient: address(0),
|
||||
destinationDeadline: 0,
|
||||
routeId: bytes32(0),
|
||||
settlementMode: bytes32(0),
|
||||
submitCommitment: false
|
||||
})
|
||||
});
|
||||
|
||||
cycleManager.transferManagedReceiverOwnership(address(this));
|
||||
|
||||
vm.prank(OPERATOR);
|
||||
vm.expectRevert(QuotePushTreasuryManager.ReceiverNotOwnedByManager.selector);
|
||||
cycleManager.runManagedCycle(address(token), 20_000, params, true, 15_000);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user