// 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"; /** * @title ReceiverExecutorMainnet * @notice Receives WETH9 from CCIP (via mainnet CCIPWETH9Bridge transfer). Unwraps to ETH or swaps to canonical USDC/USDT only. * @dev Only hardcoded WETH9, USDC, USDT. No calldata-provided stablecoin address. * WETH9 is expected to be transferred only from the mainnet CCIPWETH9Bridge (the bridge receives from CCIP Router and transfers here). * Use setExpectedWeth9Source() to record the bridge address for operator/off-chain checks; no on-chain transfer restriction. * See docs/treasury/EXECUTOR_ALLOWLIST_MATRIX.md and EXPORT_STATE_MACHINE.md. */ contract ReceiverExecutorMainnet is AccessControl, ReentrancyGuard { using SafeERC20 for IERC20; address public constant WETH9_MAINNET = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address public constant USDC_MAINNET = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; address public constant USDT_MAINNET = 0xdAC17F958D2ee523a2206206994597C13D831ec7; bytes32 public constant KEEPER_ROLE = keccak256("KEEPER_ROLE"); /// @notice When set, operator should treat WETH9 as valid only when transferred from this address (e.g. mainnet CCIPWETH9Bridge). address public expectedWeth9Source; event Unwrapped(uint256 amount); event SwappedToUsdc(uint256 amountIn, uint256 amountOut); event SwappedToUsdt(uint256 amountIn, uint256 amountOut); error ZeroAmount(); error InsufficientOutput(); event ExpectedWeth9SourceSet(address indexed source); constructor(address admin) { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(KEEPER_ROLE, admin); } /// @notice Set the address from which WETH9 is expected (e.g. mainnet CCIPWETH9Bridge). For documentation and off-chain checks only; no on-chain transfer restriction. function setExpectedWeth9Source(address source) external onlyRole(DEFAULT_ADMIN_ROLE) { expectedWeth9Source = source; emit ExpectedWeth9SourceSet(source); } /// @notice Returns the address from which WETH9 is expected. Zero means not configured. function getExpectedWeth9Source() external view returns (address) { return expectedWeth9Source; } function unwrapWeth9(uint256 amount, address recipient) external nonReentrant onlyRole(KEEPER_ROLE) { if (amount == 0) revert ZeroAmount(); if (recipient == address(0)) revert("ReceiverExecutorMainnet: zero recipient"); (bool ok,) = WETH9_MAINNET.call( abi.encodeWithSignature("withdraw(uint256)", amount) ); require(ok, "ReceiverExecutorMainnet: withdraw failed"); (bool sent,) = payable(recipient).call{value: amount}(""); require(sent, "ReceiverExecutorMainnet: ETH send failed"); emit Unwrapped(amount); } function swapWeth9ToUsdc(address router, uint256 amountIn, uint256 minOut, bytes calldata data) external nonReentrant onlyRole(KEEPER_ROLE) { if (amountIn == 0) revert ZeroAmount(); if (router == address(0)) revert("ReceiverExecutorMainnet: zero router"); uint256 balanceBefore = IERC20(USDC_MAINNET).balanceOf(address(this)); IERC20(WETH9_MAINNET).approve(router, amountIn); (bool ok,) = router.call(data); require(ok, "ReceiverExecutorMainnet: swap failed"); uint256 amountOut = IERC20(USDC_MAINNET).balanceOf(address(this)) - balanceBefore; if (amountOut < minOut) revert InsufficientOutput(); emit SwappedToUsdc(amountIn, amountOut); } function swapWeth9ToUsdt(address router, uint256 amountIn, uint256 minOut, bytes calldata data) external nonReentrant onlyRole(KEEPER_ROLE) { if (amountIn == 0) revert ZeroAmount(); if (router == address(0)) revert("ReceiverExecutorMainnet: zero router"); uint256 balanceBefore = IERC20(USDT_MAINNET).balanceOf(address(this)); IERC20(WETH9_MAINNET).approve(router, amountIn); (bool ok,) = router.call(data); require(ok, "ReceiverExecutorMainnet: swap failed"); uint256 amountOut = IERC20(USDT_MAINNET).balanceOf(address(this)) - balanceBefore; if (amountOut < minOut) revert InsufficientOutput(); emit SwappedToUsdt(amountIn, amountOut); } receive() external payable {} }