100 lines
4.5 KiB
Solidity
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 {}
|
|
}
|