chore: sync submodule state (parent ref update)
Made-with: Cursor
This commit is contained in:
181
contracts/treasury/StrategyExecutor138.sol
Normal file
181
contracts/treasury/StrategyExecutor138.sol
Normal file
@@ -0,0 +1,181 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts/access/AccessControl.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||
import "./TreasuryVault.sol";
|
||||
import "./CcipBridgeAdapter138.sol";
|
||||
|
||||
/**
|
||||
* @title StrategyExecutor138
|
||||
* @notice Single "brain" that can request moves from TreasuryVault and initiate export via CcipBridgeAdapter138.
|
||||
* @dev Token allowlist = canonical 138 list; no calldata-provided token/receiver for export. See docs/treasury/EXECUTOR_ALLOWLIST_MATRIX.md.
|
||||
*/
|
||||
contract StrategyExecutor138 is AccessControl, ReentrancyGuard {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
/// @notice Export trigger mode: daily sweep, threshold-based, or hybrid.
|
||||
enum ExportMode {
|
||||
Daily,
|
||||
Threshold,
|
||||
Hybrid
|
||||
}
|
||||
|
||||
/// @notice Policy for export (caps enforced by TreasuryVault; mode/minExportUsd for bot logic).
|
||||
struct ExportPolicy {
|
||||
ExportMode mode;
|
||||
uint256 minExportUsd;
|
||||
uint256 maxPerTxUsd;
|
||||
uint256 maxDailyUsd;
|
||||
uint256 rateLimitPerHour;
|
||||
uint256 cooldownBlocks;
|
||||
address exportAsset;
|
||||
uint64 destinationSelector;
|
||||
address destinationReceiver;
|
||||
}
|
||||
|
||||
bytes32 public constant KEEPER_ROLE = keccak256("KEEPER_ROLE");
|
||||
|
||||
TreasuryVault public immutable vault;
|
||||
CcipBridgeAdapter138 public immutable ccipAdapter;
|
||||
|
||||
mapping(address => bool) public allowedTokens;
|
||||
mapping(address => bool) public allowedRoutersOrLp;
|
||||
|
||||
uint256 public cooldownBlocks;
|
||||
uint256 public lastExportBlock;
|
||||
|
||||
ExportPolicy public exportPolicy;
|
||||
|
||||
uint256 public pendingIntentAmount;
|
||||
address public pendingIntentToken;
|
||||
|
||||
event ExportToMainnetRequested(uint256 amount, uint256 deadline);
|
||||
event CooldownBlocksSet(uint256 blocks);
|
||||
event ExportPolicySet(ExportMode mode, uint256 minExportUsd);
|
||||
event ExportIntentRecorded(address indexed token, uint256 amount);
|
||||
event PendingIntentProcessed(uint256 amount);
|
||||
|
||||
error TokenNotApproved();
|
||||
error RouterNotApproved();
|
||||
error CooldownNotElapsed();
|
||||
error ZeroAddress();
|
||||
error NoPendingIntent();
|
||||
error ExportsNotEnabled();
|
||||
error NotImplemented();
|
||||
|
||||
constructor(
|
||||
address _vault,
|
||||
address _ccipAdapter,
|
||||
address admin
|
||||
) {
|
||||
if (_vault == address(0) || _ccipAdapter == address(0)) revert ZeroAddress();
|
||||
vault = TreasuryVault(payable(_vault));
|
||||
ccipAdapter = CcipBridgeAdapter138(payable(_ccipAdapter));
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||||
_grantRole(KEEPER_ROLE, admin);
|
||||
}
|
||||
|
||||
function setToken(address token, bool approved) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
if (token == address(0)) revert ZeroAddress();
|
||||
allowedTokens[token] = approved;
|
||||
}
|
||||
|
||||
function setRouterOrLp(address routerOrLp, bool approved) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
if (routerOrLp == address(0)) revert ZeroAddress();
|
||||
allowedRoutersOrLp[routerOrLp] = approved;
|
||||
}
|
||||
|
||||
function setCooldownBlocks(uint256 blocks) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
cooldownBlocks = blocks;
|
||||
emit CooldownBlocksSet(blocks);
|
||||
}
|
||||
|
||||
function setExportPolicy(ExportPolicy calldata policy) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
exportPolicy = policy;
|
||||
if (policy.cooldownBlocks != cooldownBlocks) {
|
||||
cooldownBlocks = policy.cooldownBlocks;
|
||||
emit CooldownBlocksSet(policy.cooldownBlocks);
|
||||
}
|
||||
emit ExportPolicySet(policy.mode, policy.minExportUsd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Record intent to export when CCIP is not yet live. Process later with processPendingIntent().
|
||||
* Only one pending intent (overwrites previous). Call when ccipAdapter.exportsEnabled() is false.
|
||||
*/
|
||||
function recordExportIntent(address token, uint256 amount) external onlyRole(KEEPER_ROLE) {
|
||||
if (ccipAdapter.exportsEnabled()) revert ExportsNotEnabled();
|
||||
if (token == address(0)) revert ZeroAddress();
|
||||
if (!allowedTokens[token]) revert TokenNotApproved();
|
||||
pendingIntentToken = token;
|
||||
pendingIntentAmount = amount;
|
||||
emit ExportIntentRecorded(token, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Process the pending export intent once CCIP exports are enabled.
|
||||
*/
|
||||
function processPendingIntent(uint256 deadline)
|
||||
external
|
||||
payable
|
||||
nonReentrant
|
||||
onlyRole(KEEPER_ROLE)
|
||||
{
|
||||
if (!ccipAdapter.exportsEnabled()) revert ExportsNotEnabled();
|
||||
if (pendingIntentToken == address(0) || pendingIntentAmount == 0) revert NoPendingIntent();
|
||||
address token = pendingIntentToken;
|
||||
uint256 amount = pendingIntentAmount;
|
||||
pendingIntentToken = address(0);
|
||||
pendingIntentAmount = 0;
|
||||
if (block.timestamp > deadline) revert();
|
||||
if (cooldownBlocks != 0 && block.number < lastExportBlock + cooldownBlocks) revert CooldownNotElapsed();
|
||||
lastExportBlock = block.number;
|
||||
vault.requestTransfer(token, amount, address(this));
|
||||
IERC20(token).approve(address(ccipAdapter), amount);
|
||||
ccipAdapter.sendWeth9ToMainnet{value: msg.value}(amount, 0, deadline);
|
||||
emit ExportToMainnetRequested(amount, deadline);
|
||||
emit PendingIntentProcessed(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Harvest fees from LP/router. Stub for bot integration; implement when LP contracts are wired.
|
||||
*/
|
||||
function harvestFees() external view onlyRole(KEEPER_ROLE) {
|
||||
revert NotImplemented();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Rebalance LP positions. Stub for bot integration; implement when LP contracts are wired.
|
||||
*/
|
||||
function rebalanceLp() external view onlyRole(KEEPER_ROLE) {
|
||||
revert NotImplemented();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Request WETH9 from vault and send to mainnet via CCIP. Only allowed WETH9; no calldata destinations.
|
||||
* @param weth9Amount Amount of WETH9 to export.
|
||||
* @param deadline Revert if block.timestamp > deadline.
|
||||
*/
|
||||
function exportToMainnet(address weth9Token, uint256 weth9Amount, uint256 deadline)
|
||||
external
|
||||
payable
|
||||
nonReentrant
|
||||
onlyRole(KEEPER_ROLE)
|
||||
{
|
||||
if (weth9Token == address(0)) revert ZeroAddress();
|
||||
if (!allowedTokens[weth9Token]) revert TokenNotApproved();
|
||||
if (block.timestamp > deadline) revert();
|
||||
if (cooldownBlocks != 0 && block.number < lastExportBlock + cooldownBlocks) revert CooldownNotElapsed();
|
||||
|
||||
lastExportBlock = block.number;
|
||||
|
||||
vault.requestTransfer(weth9Token, weth9Amount, address(this));
|
||||
IERC20(weth9Token).approve(address(ccipAdapter), weth9Amount);
|
||||
|
||||
ccipAdapter.sendWeth9ToMainnet{value: msg.value}(weth9Amount, 0, deadline);
|
||||
emit ExportToMainnetRequested(weth9Amount, deadline);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user