WifReflections
0x6466d378375826be149994be0562d40f77cca173
Verification
Verified
v0.8.20+commit.a1b79de6
Type
Contract
6,968 bytes
ABI entries
47
19 read · 12 write
License
none
Contract information
- Address
- 0x6466d378375826be149994be0562d40f77cca173
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.20+commit.a1b79de6
- Optimization
- Enabled
- Creator
- 0xbF551eED83…f18600B7C5
- Creation tx
- 0x5b83ad5cca…20d72af6b6
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (19)
budgetRemaining() → uint256
currentEpoch() → uint256
eligibleCount(uint256) → uint256
emissionPerEpoch() → uint256
epochInterval() → uint256
epochsRemaining() → uint256
excluded(address) → bool
keeper() → address
lastEpochStart() → uint256
minHolding() → uint256
nextEpochIn() → uint256
owner() → address
paid(uint256, address) → bool
pendingFor(address) → uint256
phase() → uint8
snapshotBalance(uint256, address) → uint256
token() → address
totalEligible(uint256) → uint256
totalPaid(uint256) → uint256
Events (9)
ConfigUpdatedDistributedEpochFinalizedEpochStartedExcludedSetKeeperUpdatedOwnershipTransferredSnapshotSealedSnapshotted
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "token_",
"type": "address"
},
{
"internalType": "uint256",
"name": "epochInterval_",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "emissionPerEpoch_",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minHolding_",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "currentEpoch_",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "lastEpochStart_",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "EmptySnapshot",
"type": "error"
},
{
"inputs": [],
"name": "NotKeeper",
"type": "error"
},
{
"inputs": [],
"name": "TooSoon",
"type": "error"
},
{
"inputs": [],
"name": "WrongPhase",
"type": "error"
},
{
"inputs": [],
"name": "ZeroConfig",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "epochInterval",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "emissionPerEpoch",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "minHolding",
"type": "uint256"
}
],
"name": "ConfigUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "epoch",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "holdersPaid",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amountPaid",
"type": "uint256"
}
],
"name": "Distributed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "epoch",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "totalPaid",
"type": "uint256"
}
],
"name": "EpochFinalized",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "epoch",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "emission",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "minHolding",
"type": "uint256"
}
],
"name": "EpochStarted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "isExcluded",
"type": "bool"
}
],
"name": "ExcludedSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "keeper",
"type": "address"
}
],
"name": "KeeperUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "epoch",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "totalEligible",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "eligibleCount",
"type": "uint256"
}
],
"name": "SnapshotSealed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "epoch",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "added",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "totalEligible",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "eligibleCount",
"type": "uint256"
}
],
"name": "Snapshotted",
"type": "event"
},
{
"inputs": [],
"name": "budgetRemaining",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "currentEpoch",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "holders",
"type": "address[]"
}
],
"name": "distribute",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "eligibleCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "emissionPerEpoch",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "epochInterval",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "epochsRemaining",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "excluded",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "finalizeEpoch",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "keeper",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "lastEpochStart",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "minHolding",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "nextEpochIn",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "paid",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "holder",
"type": "address"
}
],
"name": "pendingFor",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "phase",
"outputs": [
{
"internalType": "enum WifReflections.Phase",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "sealSnapshot",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "epochInterval_",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "emissionPerEpoch_",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minHolding_",
"type": "uint256"
}
],
"name": "setConfig",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
},
{
"internalType": "bool",
"name": "isExcluded",
"type": "bool"
}
],
"name": "setExcluded",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "keeper_",
"type": "address"
}
],
"name": "setKeeper",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "holders",
"type": "address[]"
}
],
"name": "snapshot",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "snapshotBalance",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "startEpoch",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "token",
"outputs": [
{
"internalType": "contract IERC20",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "totalEligible",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "totalPaid",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "withdrawEth",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token_",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "withdrawToken",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
/**
* @title WifReflections
* @notice Timed, pro-rata token reflections for holders of a standard
* (tax-free) ERC20 — built for Sentry-launched tokens, whose vanilla
* token contract can't do fee-on-transfer reflections by design.
*
* V2 of SentryReflections (0xd51b038E...54EE0), same mechanics with two
* changes: (1) EIP-7702-delegated EOAs (code = 23-byte 0xef0100||addr
* designator) count as normal wallets — Robinhood-app users delegate
* their EOAs and were silently skipped by V1's code check; real
* contracts (pools, lockers, Safes) remain excluded. (2) The
* constructor seeds the epoch counter and schedule so a migration
* continues the V1 cadence instead of resetting it.
*
* The contract is funded with the reward budget (e.g. 20M WIF) and
* pays it out over fixed epochs. Each epoch:
*
* 1. startEpoch() — anyone may ring the bell once the
* interval has elapsed; opens a snapshot.
* 2. snapshot(holders[]) — keeper submits candidate holders in
* batches. THE CONTRACT reads each
* balanceOf() itself, applies the
* minimum-holding bar and the exclusion
* list, and records the eligible balance.
* The keeper can therefore omit someone
* (visible on-chain) but can never
* inflate a balance, double-count a
* wallet, or invent a holder.
* 3. sealSnapshot() — locks the eligible set + total.
* 4. distribute(holders[]) — batched pro-rata payout:
* amount = emission * snapBal / totalEligible.
* Contract-enforced: a holder is paid at
* most once, the epoch never pays more
* than emissionPerEpoch, and payouts only
* come from the funded budget.
*
* Off-chain, a keeper (Sentry's cron) sources the holder list from the
* chain explorer / subgraph and submits it — the math and all spending
* limits live here. Exclusions (the Uniswap pool, the LP locker, dead
* addresses, the distributor itself) are owner-maintained and public.
*
* The minimum holding is denominated in TOKENS. A USD bar ("$100")
* moves with price, so the owner/keeper periodically re-derives the
* token amount from the pool price and calls setMinHolding — on-chain
* USD oracles don't exist on this chain yet.
*
* ETH sent to the contract is held for future use (ETH reflections or
* keeper-gas top-ups) and is owner-withdrawable; nothing here requires
* the contract to hold ETH.
*/
contract WifReflections is Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;
IERC20 public immutable token;
/// @notice Seconds between epochs (e.g. 7 days).
uint256 public epochInterval;
/// @notice Reward tokens paid out per epoch, split pro-rata.
uint256 public emissionPerEpoch;
/// @notice Minimum token balance to be eligible (the "$100 bar").
uint256 public minHolding;
/// @notice Addresses that never receive reflections (pool, locker, ...).
mapping(address => bool) public excluded;
/// @notice Optional dedicated keeper; owner always allowed.
address public keeper;
enum Phase {
Idle, // between epochs
Snapshotting, // startEpoch called, balances being recorded
Distributing // snapshot sealed, payouts in progress
}
uint256 public currentEpoch; // 0 = none yet
uint256 public lastEpochStart; // timestamp startEpoch last succeeded
Phase public phase;
// Per-epoch snapshot state. Keyed by epoch id so stale entries from
// prior epochs can never leak into the current one.
mapping(uint256 => mapping(address => uint256)) public snapshotBalance;
mapping(uint256 => mapping(address => bool)) public paid;
mapping(uint256 => uint256) public totalEligible;
mapping(uint256 => uint256) public eligibleCount;
mapping(uint256 => uint256) public totalPaid;
event EpochStarted(uint256 indexed epoch, uint256 emission, uint256 minHolding);
event Snapshotted(uint256 indexed epoch, uint256 added, uint256 totalEligible, uint256 eligibleCount);
event SnapshotSealed(uint256 indexed epoch, uint256 totalEligible, uint256 eligibleCount);
event Distributed(uint256 indexed epoch, uint256 holdersPaid, uint256 amountPaid);
event EpochFinalized(uint256 indexed epoch, uint256 totalPaid);
event ExcludedSet(address indexed account, bool isExcluded);
event ConfigUpdated(uint256 epochInterval, uint256 emissionPerEpoch, uint256 minHolding);
event KeeperUpdated(address keeper);
error NotKeeper();
error WrongPhase();
error TooSoon();
error EmptySnapshot();
error ZeroConfig();
modifier onlyKeeper() {
if (msg.sender != owner() && msg.sender != keeper) revert NotKeeper();
_;
}
constructor(
address token_,
uint256 epochInterval_,
uint256 emissionPerEpoch_,
uint256 minHolding_,
uint256 currentEpoch_,
uint256 lastEpochStart_
) {
if (token_ == address(0) || epochInterval_ == 0 || emissionPerEpoch_ == 0) revert ZeroConfig();
token = IERC20(token_);
epochInterval = epochInterval_;
emissionPerEpoch = emissionPerEpoch_;
minHolding = minHolding_;
// Migration seeds: continue a predecessor's epoch numbering and
// schedule (0, 0 for a fresh start).
currentEpoch = currentEpoch_;
lastEpochStart = lastEpochStart_;
// The distributor must never count its own reward pool as a
// holder balance.
excluded[address(this)] = true;
excluded[address(0)] = true;
excluded[0x000000000000000000000000000000000000dEaD] = true;
}
/* ── Epoch lifecycle ─────────────────────────────────────────────── */
/// @notice Open the next epoch. Callable by anyone (the schedule is
/// the guard, not the caller), but snapshotting stays keeper-only.
function startEpoch() external {
if (phase != Phase.Idle) revert WrongPhase();
if (block.timestamp < lastEpochStart + epochInterval) revert TooSoon();
// Refuse to open an epoch the budget can't cover — leftover dust
// below one full emission stays withdrawable by the owner.
require(token.balanceOf(address(this)) >= emissionPerEpoch, "budget exhausted");
currentEpoch += 1;
lastEpochStart = block.timestamp;
phase = Phase.Snapshotting;
emit EpochStarted(currentEpoch, emissionPerEpoch, minHolding);
}
/// @notice Record eligible balances for a batch of candidate
/// holders. Balances are read on-chain HERE — the keeper only
/// supplies addresses. Ineligible/duplicate/excluded entries are
/// skipped, not reverted, so one bad address can't wedge a batch.
function snapshot(address[] calldata holders) external onlyKeeper {
if (phase != Phase.Snapshotting) revert WrongPhase();
uint256 epoch = currentEpoch;
uint256 added;
uint256 addedBalance;
for (uint256 i = 0; i < holders.length; i++) {
address holder = holders[i];
if (excluded[holder]) continue;
if (snapshotBalance[epoch][holder] != 0) continue; // already counted
// EOAs only: pools, lockers, routers, and other contracts
// never earn reflections. An EIP-7702-delegated EOA carries
// the 23-byte designator 0xef0100||delegate — that is still
// a person's wallet (Robinhood-app users delegate), so it
// stays eligible.
if (!_isEoa(holder)) continue;
uint256 bal = token.balanceOf(holder);
if (bal < minHolding) continue;
snapshotBalance[epoch][holder] = bal;
addedBalance += bal;
added += 1;
}
totalEligible[epoch] += addedBalance;
eligibleCount[epoch] += added;
emit Snapshotted(epoch, added, totalEligible[epoch], eligibleCount[epoch]);
}
/// @notice Lock the snapshot and open distribution.
function sealSnapshot() external onlyKeeper {
if (phase != Phase.Snapshotting) revert WrongPhase();
if (totalEligible[currentEpoch] == 0) revert EmptySnapshot();
phase = Phase.Distributing;
emit SnapshotSealed(currentEpoch, totalEligible[currentEpoch], eligibleCount[currentEpoch]);
}
/// @notice Pay a batch of snapshotted holders their pro-rata share:
/// emission * snapshotBalance / totalEligible. Each holder pays out
/// at most once per epoch; un-snapshotted addresses are skipped.
function distribute(address[] calldata holders) external onlyKeeper nonReentrant {
if (phase != Phase.Distributing) revert WrongPhase();
uint256 epoch = currentEpoch;
uint256 tot = totalEligible[epoch];
uint256 emission = emissionPerEpoch;
uint256 paidNow;
uint256 count;
for (uint256 i = 0; i < holders.length; i++) {
address holder = holders[i];
uint256 bal = snapshotBalance[epoch][holder];
if (bal == 0 || paid[epoch][holder]) continue;
paid[epoch][holder] = true;
uint256 amount = (emission * bal) / tot;
if (amount == 0) continue;
paidNow += amount;
count += 1;
token.safeTransfer(holder, amount);
}
totalPaid[epoch] += paidNow;
// Pro-rata shares of a fixed emission can never sum past the
// emission; this assert documents the invariant distribution
// relies on (rounding dust stays in the pool for later epochs).
assert(totalPaid[epoch] <= emission);
emit Distributed(epoch, count, paidNow);
}
/// @notice Close the epoch. Distribution doesn't need to have
/// reached every snapshotted holder (a wallet gone to zero pays
/// zero anyway); whatever wasn't sent stays in the budget.
function finalizeEpoch() external onlyKeeper {
if (phase != Phase.Distributing) revert WrongPhase();
phase = Phase.Idle;
emit EpochFinalized(currentEpoch, totalPaid[currentEpoch]);
}
/// @dev True for plain EOAs and for EIP-7702-delegated EOAs (whose
/// code is exactly the 23-byte designator 0xef0100 || delegate).
function _isEoa(address account) private view returns (bool) {
bytes memory code = account.code;
if (code.length == 0) return true;
return code.length == 23 && code[0] == 0xef && code[1] == 0x01 && code[2] == 0x00;
}
/* ── Views ───────────────────────────────────────────────────────── */
/// @notice Seconds until the next epoch can start (0 = ready).
function nextEpochIn() external view returns (uint256) {
uint256 nextAt = lastEpochStart + epochInterval;
return block.timestamp >= nextAt ? 0 : nextAt - block.timestamp;
}
/// @notice Remaining reward budget in the pool.
function budgetRemaining() external view returns (uint256) {
return token.balanceOf(address(this));
}
/// @notice Full epochs the current budget can still fund.
function epochsRemaining() external view returns (uint256) {
return token.balanceOf(address(this)) / emissionPerEpoch;
}
/// @notice What `holder` would receive this epoch (0 before seal).
function pendingFor(address holder) external view returns (uint256) {
uint256 epoch = currentEpoch;
if (phase != Phase.Distributing || paid[epoch][holder]) return 0;
uint256 tot = totalEligible[epoch];
if (tot == 0) return 0;
return (emissionPerEpoch * snapshotBalance[epoch][holder]) / tot;
}
/* ── Admin ───────────────────────────────────────────────────────── */
function setExcluded(address account, bool isExcluded) external onlyOwner {
excluded[account] = isExcluded;
emit ExcludedSet(account, isExcluded);
}
/// @notice Update schedule/economics. Only between epochs so a
/// mid-epoch change can't skew a snapshot already in progress.
function setConfig(
uint256 epochInterval_,
uint256 emissionPerEpoch_,
uint256 minHolding_
) external onlyOwner {
if (phase != Phase.Idle) revert WrongPhase();
if (epochInterval_ == 0 || emissionPerEpoch_ == 0) revert ZeroConfig();
epochInterval = epochInterval_;
emissionPerEpoch = emissionPerEpoch_;
minHolding = minHolding_;
emit ConfigUpdated(epochInterval_, emissionPerEpoch_, minHolding_);
}
function setKeeper(address keeper_) external onlyOwner {
keeper = keeper_;
emit KeeperUpdated(keeper_);
}
/// @notice Owner exit: pull unused budget (or any stray token) out.
/// Reflections are a grant from the funder, not user deposits, so a
/// withdrawal path is appropriate here (unlike the LP locker).
function withdrawToken(address token_, uint256 amount, address to) external onlyOwner {
IERC20(token_).safeTransfer(to, amount);
}
function withdrawEth(uint256 amount, address to) external onlyOwner {
(bool ok,) = to.call{value: amount}("");
require(ok, "eth send failed");
}
receive() external payable {}
}
Chain explorer1323msChain node81ms