chore: sync submodule state (parent ref update)
Made-with: Cursor
This commit is contained in:
99
contracts/treasury/ReceiverExecutorMainnet.sol
Normal file
99
contracts/treasury/ReceiverExecutorMainnet.sol
Normal file
@@ -0,0 +1,99 @@
|
||||
// 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 {}
|
||||
}
|
||||
Reference in New Issue
Block a user