182 lines
6.7 KiB
Solidity
182 lines
6.7 KiB
Solidity
// 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);
|
|
}
|
|
}
|