Files
smom-dbis-138/contracts/treasury/ReceiverExecutorMainnet.sol
2026-03-02 12:14:09 -08:00

100 lines
4.5 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";
/**
* @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 {}
}