GenesisBurnDropV3
0x9d4c15e1c88cb44cc8f0ff50968f05f6bc13200a
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
4,844 bytes
ABI entries
36
7 read · 9 write
License
none
Contract information
- Address
- 0x9d4c15e1c88cb44cc8f0ff50968f05f6bc13200a
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x4e3C4f0bF9…16FE22298c
- Creation tx
- 0x7ccd633366…0ff0a8a2cd
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (7)
BURN_ADDRESS() → address
MAX_BPS() → uint16
airdropReserve(address) → uint256
claimable(address, address) → uint256
owner() → address
pendingClaims(address) → address[], uint256[]
totalUnclaimed(address) → uint256
Events (8)
AllocatedAllocationRevokedClaimedEthRescuedOwnershipTransferredProcessSkippedProcessedTokenRescued
ABI
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "EthTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientReserve",
"type": "error"
},
{
"inputs": [],
"name": "InvalidBps",
"type": "error"
},
{
"inputs": [],
"name": "LengthMismatch",
"type": "error"
},
{
"inputs": [],
"name": "NothingToClaim",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "OwnableInvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "OwnableUnauthorizedAccount",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "recipientCount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "total",
"type": "uint256"
}
],
"name": "Allocated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "user",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "AllocationRevoked",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "user",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "Claimed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "EthRescued",
"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": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "bytes",
"name": "reason",
"type": "bytes"
}
],
"name": "ProcessSkipped",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "pending",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "burned",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "reserved",
"type": "uint256"
}
],
"name": "Processed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "TokenRescued",
"type": "event"
},
{
"inputs": [],
"name": "BURN_ADDRESS",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "airdropReserve",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address[]",
"name": "recipients",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"name": "allocate",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "claim",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "claimToken",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "user",
"type": "address"
}
],
"name": "claimable",
"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": "address",
"name": "user",
"type": "address"
}
],
"name": "pendingClaims",
"outputs": [
{
"internalType": "address[]",
"name": "tokens",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "tokens",
"type": "address[]"
},
{
"internalType": "uint16",
"name": "burnBps",
"type": "uint16"
}
],
"name": "process",
"outputs": [
{
"internalType": "uint256[]",
"name": "burned",
"type": "uint256[]"
},
{
"internalType": "uint256[]",
"name": "reserved",
"type": "uint256[]"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "rescueEth",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "rescueToken",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address[]",
"name": "users",
"type": "address[]"
}
],
"name": "revoke",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "totalUnclaimed",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/// @notice Burn-and-claim vault for creator tokens on Robinhood Chain.
/// Tokens are sent here by plain transfer. `process` splits each token's
/// untracked balance by a caller-chosen burn percentage (0–100% in bps):
/// the burn share goes to the dead address, the rest is held in a per-token
/// reserve. The owner then `allocate`s the reserve to GENESIS holders as
/// claimable balances — nothing is pushed. Each holder calls `claim()` once
/// to receive every token allocated to their address.
/// @dev Unclaimed allocations remain in the contract balance; `process`
/// subtracts them (`totalUnclaimed`) so they are never re-burned. LaunchToken
/// restriction windows can make `process` (burn) or `claim` (transfer out)
/// revert until the window ends; `claimToken` lets a user skip a stuck token.
contract GenesisBurnDropV3 is Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;
error ZeroAddress();
error LengthMismatch();
error InsufficientReserve();
error EthTransferFailed();
error NothingToClaim();
error InvalidBps();
event Processed(address indexed token, uint256 pending, uint256 burned, uint256 reserved);
event ProcessSkipped(address indexed token, bytes reason);
event Allocated(address indexed token, uint256 recipientCount, uint256 total);
event Claimed(address indexed user, address indexed token, uint256 amount);
event AllocationRevoked(address indexed token, address indexed user, uint256 amount);
event TokenRescued(address indexed token, address indexed to, uint256 amount);
event EthRescued(address indexed to, uint256 amount);
uint16 public constant MAX_BPS = 10_000;
address public constant BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD;
/// @notice Kept share of each token's processed deposits, not yet allocated.
mapping(address token => uint256) public airdropReserve;
/// @notice Sum of all allocated-but-unclaimed amounts per token.
mapping(address token => uint256) public totalUnclaimed;
/// @notice Claimable amount per token per user.
mapping(address token => mapping(address user => uint256)) public claimable;
/// @dev Tokens ever allocated to a user and not yet swept by claim().
mapping(address user => address[]) internal _userTokens;
mapping(address user => mapping(address token => bool)) internal _listed;
constructor() Ownable(msg.sender) {}
/// @notice Split each token's untracked balance (balance minus reserve and
/// minus unclaimed allocations): burn `burnBps` (0–10_000, e.g. 5_000 =
/// 50%, 0 = no burn) to the dead address, add the remainder to the
/// reserve. Re-processing with no new deposit is a no-op; a codeless token
/// address is skipped so one bad entry can't sink the batch.
function process(address[] calldata tokens, uint16 burnBps)
external
onlyOwner
nonReentrant
returns (uint256[] memory burned, uint256[] memory reserved)
{
if (burnBps > MAX_BPS) revert InvalidBps();
burned = new uint256[](tokens.length);
reserved = new uint256[](tokens.length);
for (uint256 i = 0; i < tokens.length; i++) {
address token = tokens[i];
if (token.code.length == 0) {
emit ProcessSkipped(token, "no code");
continue;
}
uint256 obligations = airdropReserve[token] + totalUnclaimed[token];
uint256 pending = IERC20(token).balanceOf(address(this)) - obligations;
if (pending == 0) {
emit ProcessSkipped(token, "nothing pending");
continue;
}
uint256 burnAmt = (pending * burnBps) / MAX_BPS;
if (burnAmt > 0) IERC20(token).safeTransfer(BURN_ADDRESS, burnAmt);
// Re-read instead of `reserve += pending - burnAmt` so accounting
// stays exact for fee-on-transfer tokens.
uint256 newReserve = IERC20(token).balanceOf(address(this)) - totalUnclaimed[token];
uint256 added = newReserve - airdropReserve[token];
airdropReserve[token] = newReserve;
burned[i] = burnAmt;
reserved[i] = added;
emit Processed(token, pending, burnAmt, added);
}
}
/// @notice Credit a batch of the token's reserve to recipients as claimable
/// balances. Amounts must sum to no more than the reserve; leftovers stay
/// reserved for later batches or runs. Nothing is transferred here.
function allocate(address token, address[] calldata recipients, uint256[] calldata amounts)
external
onlyOwner
nonReentrant
{
if (recipients.length != amounts.length) revert LengthMismatch();
uint256 total = 0;
for (uint256 i = 0; i < amounts.length; i++) {
total += amounts[i];
}
uint256 reserve = airdropReserve[token];
if (total > reserve) revert InsufficientReserve();
airdropReserve[token] = reserve - total;
totalUnclaimed[token] += total;
for (uint256 i = 0; i < recipients.length; i++) {
address user = recipients[i];
uint256 amount = amounts[i];
if (amount == 0) continue;
claimable[token][user] += amount;
if (!_listed[user][token]) {
_listed[user][token] = true;
_userTokens[user].push(token);
}
}
emit Allocated(token, recipients.length, total);
}
/// @notice Claim every token allocated to the caller in one transaction.
function claim() external nonReentrant {
address[] storage list = _userTokens[msg.sender];
uint256 n = list.length;
if (n == 0) revert NothingToClaim();
bool claimedAny = false;
for (uint256 i = n; i > 0; i--) {
address token = list[i - 1];
uint256 amount = claimable[token][msg.sender];
if (amount > 0) {
claimable[token][msg.sender] = 0;
totalUnclaimed[token] -= amount;
IERC20(token).safeTransfer(msg.sender, amount);
emit Claimed(msg.sender, token, amount);
claimedAny = true;
}
_listed[msg.sender][token] = false;
list.pop();
}
if (!claimedAny) revert NothingToClaim();
}
/// @notice Claim a single token — escape hatch if one token in the full
/// claim reverts (e.g. its LaunchToken restriction window is still active).
function claimToken(address token) external nonReentrant {
uint256 amount = claimable[token][msg.sender];
if (amount == 0) revert NothingToClaim();
claimable[token][msg.sender] = 0;
totalUnclaimed[token] -= amount;
IERC20(token).safeTransfer(msg.sender, amount);
emit Claimed(msg.sender, token, amount);
}
/// @notice All tokens with a nonzero claimable balance for `user`.
function pendingClaims(address user)
external
view
returns (address[] memory tokens, uint256[] memory amounts)
{
address[] storage list = _userTokens[user];
uint256 count = 0;
for (uint256 i = 0; i < list.length; i++) {
if (claimable[list[i]][user] > 0) count++;
}
tokens = new address[](count);
amounts = new uint256[](count);
uint256 j = 0;
for (uint256 i = 0; i < list.length; i++) {
uint256 amount = claimable[list[i]][user];
if (amount > 0) {
tokens[j] = list[i];
amounts[j] = amount;
j++;
}
}
}
/// @notice Owner pulls stale unclaimed allocations back into the reserve.
function revoke(address token, address[] calldata users) external onlyOwner {
for (uint256 i = 0; i < users.length; i++) {
uint256 amount = claimable[token][users[i]];
if (amount == 0) continue;
claimable[token][users[i]] = 0;
totalUnclaimed[token] -= amount;
airdropReserve[token] += amount;
emit AllocationRevoked(token, users[i], amount);
}
}
/// @notice Owner escape hatch for stuck or mistaken deposits. Can raid the
/// reserve and even allocated funds — owner-trusted by design; the reserve
/// is reconciled down so accounting never exceeds the actual balance.
function rescueToken(address token, address to, uint256 amount) external onlyOwner {
if (to == address(0)) revert ZeroAddress();
IERC20(token).safeTransfer(to, amount);
uint256 remaining = IERC20(token).balanceOf(address(this));
uint256 unclaimed = totalUnclaimed[token];
uint256 maxReserve = remaining > unclaimed ? remaining - unclaimed : 0;
if (airdropReserve[token] > maxReserve) airdropReserve[token] = maxReserve;
emit TokenRescued(token, to, amount);
}
/// @notice Owner escape hatch for stray ETH (nothing here ever needs it).
function rescueEth(address to) external onlyOwner {
if (to == address(0)) revert ZeroAddress();
uint256 amount = address(this).balance;
(bool ok,) = to.call{value: amount}("");
if (!ok) revert EthTransferFailed();
emit EthRescued(to, amount);
}
receive() external payable {}
}
Chain explorer3891msChain node82ms