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
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