RobinbooMint
0x44e226ca2c30c0393793492c96dd80f1b872157f
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
6,425 bytes
ABI entries
66
20 read · 11 write
License
none
Contract information
- Address
- 0x44e226ca2c30c0393793492c96dd80f1b872157f
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0x46292B1EeB…a0B9E669Ae
- Creation tx
- 0x21ccc455ca…96a525f11a
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (20)
MAX_MINT_UNITS() → uint32
MAX_REFUND_BATCH() → uint256
PUBLIC_INVENTORY() → uint256
RECENT_MINT_LIMIT() → uint256
TOKENS_PER_UNIT() → uint256
claimableUnits(address) → uint256
getRecentMints() → tuple[]
isClaimOpen() → bool
lastMintBlock(address) → uint256
mintEnded() → bool
mintPrice() → uint256
minterAt(uint256) → address
minterCount() → uint256
minterInfo(address) → uint32, uint32, bool
owner() → address
pendingOwner() → address
settlementState() → uint8
token() → address
totalMintTransactions() → uint64
totalMintedUnits() → uint32
Events (10)
ClaimedETHReturnedFullRefundStartedMintEndedMintedOwnershipTransferStartedOwnershipTransferredProceedsTransferredTokenInitializedTokenWithdrawn
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "owner_",
"type": "address"
},
{
"internalType": "uint256",
"name": "mintPrice_",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "blockNumber",
"type": "uint256"
}
],
"name": "AlreadyMintedThisBlock",
"type": "error"
},
{
"inputs": [],
"name": "ClaimNotOpen",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "ETHTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "EmptyTokenBalance",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "sent",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "required",
"type": "uint256"
}
],
"name": "IncorrectPayment",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "startIndex",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "count",
"type": "uint256"
}
],
"name": "InvalidRefundRange",
"type": "error"
},
{
"inputs": [
{
"internalType": "enum RobinbooMint.SettlementState",
"name": "current",
"type": "uint8"
}
],
"name": "InvalidSettlementState",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "InvalidTokenContract",
"type": "error"
},
{
"inputs": [],
"name": "InvalidUnits",
"type": "error"
},
{
"inputs": [],
"name": "MintAlreadyEnded",
"type": "error"
},
{
"inputs": [],
"name": "MintNotEnded",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "requestedTotal",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "maximum",
"type": "uint256"
}
],
"name": "MintSupplyExceeded",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "MinterIndexOutOfBounds",
"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": "uint256",
"name": "count",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "maximum",
"type": "uint256"
}
],
"name": "RefundBatchTooLarge",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "TokenAlreadyInitialized",
"type": "error"
},
{
"inputs": [],
"name": "TokenNotInitialized",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"inputs": [],
"name": "ZeroMintPrice",
"type": "error"
},
{
"inputs": [],
"name": "ZeroTokenAmount",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "units",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
}
],
"name": "Claimed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "minter",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "units",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "ETHReturned",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "minterCount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "totalMintedUnits",
"type": "uint256"
}
],
"name": "FullRefundStarted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "totalMintedUnits",
"type": "uint256"
}
],
"name": "MintEnded",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "buyer",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "units",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "booAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethAmount",
"type": "uint256"
}
],
"name": "Minted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferStarted",
"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": "recipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "ProceedsTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "TokenInitialized",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "TokenWithdrawn",
"type": "event"
},
{
"inputs": [],
"name": "MAX_MINT_UNITS",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_REFUND_BATCH",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "PUBLIC_INVENTORY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "RECENT_MINT_LIMIT",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TOKENS_PER_UNIT",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "claim",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "claimableUnits",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "endMint",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "getRecentMints",
"outputs": [
{
"components": [
{
"internalType": "address",
"name": "buyer",
"type": "address"
},
{
"internalType": "uint40",
"name": "timestamp",
"type": "uint40"
},
{
"internalType": "uint8",
"name": "units",
"type": "uint8"
}
],
"internalType": "struct RobinbooMint.MintRecord[]",
"name": "records",
"type": "tuple[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token_",
"type": "address"
}
],
"name": "initializeToken",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "isClaimOpen",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "lastMintBlock",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "units",
"type": "uint256"
}
],
"name": "mint",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "mintEnded",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "mintPrice",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "minterAt",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "minterCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "minterInfo",
"outputs": [
{
"internalType": "uint32",
"name": "mintedUnits",
"type": "uint32"
},
{
"internalType": "uint32",
"name": "claimedUnits",
"type": "uint32"
},
{
"internalType": "bool",
"name": "ethReturned",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "startIndex",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "count",
"type": "uint256"
}
],
"name": "refundBatch",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "settlementState",
"outputs": [
{
"internalType": "enum RobinbooMint.SettlementState",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "startFullRefund",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "token",
"outputs": [
{
"internalType": "contract IERC20",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalMintTransactions",
"outputs": [
{
"internalType": "uint64",
"name": "",
"type": "uint64"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalMintedUnits",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address payable",
"name": "recipient",
"type": "address"
}
],
"name": "transferProceeds",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "withdrawToken",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
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 { Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract RobinbooMint is Ownable2Step, ReentrancyGuard {
using SafeERC20 for IERC20;
enum SettlementState {
Pending,
Refunding,
Transferred
}
struct MinterInfo {
uint32 mintedUnits;
uint32 claimedUnits;
bool ethReturned;
}
struct MintRecord {
address buyer;
uint40 timestamp;
uint8 units;
}
uint256 public constant PUBLIC_INVENTORY = 800_000_000 ether;
uint256 public constant TOKENS_PER_UNIT = 8_000 ether;
uint32 public constant MAX_MINT_UNITS = 100_000;
uint256 public constant MAX_REFUND_BATCH = 100;
uint256 public constant RECENT_MINT_LIMIT = 50;
IERC20 public token;
// Kept in camelCase to preserve the public ABI used by existing scripts and the frontend.
// forge-lint: disable-next-line(screaming-snake-case-immutable)
uint256 public immutable mintPrice;
bool public mintEnded;
SettlementState public settlementState;
uint32 public totalMintedUnits;
uint64 public totalMintTransactions;
mapping(address => MinterInfo) public minterInfo;
mapping(address => uint256) public lastMintBlock;
address[] private minterList;
MintRecord[50] private recentMints;
error ZeroAddress();
error ZeroMintPrice();
error TokenAlreadyInitialized();
error InvalidTokenContract(address token);
error MintAlreadyEnded();
error InvalidUnits();
error AlreadyMintedThisBlock(uint256 blockNumber);
error MintSupplyExceeded(uint256 requestedTotal, uint256 maximum);
error IncorrectPayment(uint256 sent, uint256 required);
error ClaimNotOpen();
error NothingToClaim();
error TokenNotInitialized();
error EmptyTokenBalance();
error ZeroTokenAmount();
error MinterIndexOutOfBounds(uint256 index);
error MintNotEnded();
error InvalidSettlementState(SettlementState current);
error InvalidRefundRange(uint256 startIndex, uint256 count);
error RefundBatchTooLarge(uint256 count, uint256 maximum);
error ETHTransferFailed(address recipient);
event TokenInitialized(address indexed token);
event Minted(address indexed buyer, uint256 units, uint256 booAmount, uint256 ethAmount);
event Claimed(address indexed account, uint256 units, uint256 tokenAmount);
event TokenWithdrawn(address indexed recipient, uint256 amount);
event MintEnded(uint256 totalMintedUnits);
event FullRefundStarted(uint256 minterCount, uint256 totalMintedUnits);
event ETHReturned(address indexed minter, uint256 units, uint256 amount);
event ProceedsTransferred(address indexed recipient, uint256 amount);
constructor(address owner_, uint256 mintPrice_) Ownable(_validateOwner(owner_)) {
if (mintPrice_ == 0) revert ZeroMintPrice();
mintPrice = mintPrice_;
}
function initializeToken(address token_) external onlyOwner {
if (token_ == address(0)) revert ZeroAddress();
if (address(token) != address(0)) revert TokenAlreadyInitialized();
if (token_.code.length == 0) revert InvalidTokenContract(token_);
token = IERC20(token_);
emit TokenInitialized(token_);
}
function mint(uint256 units) external payable nonReentrant {
if (mintEnded) revert MintAlreadyEnded();
if (units != 1) revert InvalidUnits();
if (lastMintBlock[msg.sender] == block.number) {
revert AlreadyMintedThisBlock(block.number);
}
MinterInfo storage info = minterInfo[msg.sender];
uint256 requestedGlobalTotal = uint256(totalMintedUnits) + 1;
if (requestedGlobalTotal > MAX_MINT_UNITS) {
revert MintSupplyExceeded(requestedGlobalTotal, MAX_MINT_UNITS);
}
if (msg.value != mintPrice) revert IncorrectPayment(msg.value, mintPrice);
if (info.mintedUnits == 0) minterList.push(msg.sender);
unchecked {
++info.mintedUnits;
}
lastMintBlock[msg.sender] = block.number;
// Safe because requestedGlobalTotal is bounded by MAX_MINT_UNITS (100,000).
// forge-lint: disable-next-line(unsafe-typecast)
totalMintedUnits = uint32(requestedGlobalTotal);
uint256 slot = uint256(totalMintTransactions) % RECENT_MINT_LIMIT;
recentMints[slot] =
MintRecord({ buyer: msg.sender, timestamp: uint40(block.timestamp), units: 1 });
unchecked {
++totalMintTransactions;
}
if (requestedGlobalTotal == MAX_MINT_UNITS && _hasTokenBalance()) _endMint();
emit Minted(msg.sender, 1, TOKENS_PER_UNIT, msg.value);
}
function isClaimOpen() public view returns (bool) {
return mintEnded && address(token) != address(0);
}
function claimableUnits(address account) public view returns (uint256) {
MinterInfo storage info = minterInfo[account];
return uint256(info.mintedUnits) - uint256(info.claimedUnits);
}
function claim() external nonReentrant {
if (!isClaimOpen()) revert ClaimNotOpen();
MinterInfo storage info = minterInfo[msg.sender];
uint256 units = uint256(info.mintedUnits) - uint256(info.claimedUnits);
if (units == 0) revert NothingToClaim();
info.claimedUnits = info.mintedUnits;
uint256 tokenAmount = units * TOKENS_PER_UNIT;
token.safeTransfer(msg.sender, tokenAmount);
emit Claimed(msg.sender, units, tokenAmount);
}
function endMint() external onlyOwner {
if (mintEnded) revert MintAlreadyEnded();
if (address(token) == address(0)) revert TokenNotInitialized();
if (token.balanceOf(address(this)) == 0) revert EmptyTokenBalance();
_endMint();
}
function withdrawToken(address recipient, uint256 amount) external onlyOwner nonReentrant {
_requireMintEnded();
if (address(token) == address(0)) revert TokenNotInitialized();
if (recipient == address(0)) revert ZeroAddress();
if (amount == 0) revert ZeroTokenAmount();
token.safeTransfer(recipient, amount);
emit TokenWithdrawn(recipient, amount);
}
function startFullRefund() external onlyOwner nonReentrant {
_requireMintEnded();
if (settlementState != SettlementState.Pending) {
revert InvalidSettlementState(settlementState);
}
settlementState = SettlementState.Refunding;
emit FullRefundStarted(minterList.length, totalMintedUnits);
}
function refundBatch(uint256 startIndex, uint256 count) external onlyOwner nonReentrant {
_requireMintEnded();
if (settlementState != SettlementState.Refunding) {
revert InvalidSettlementState(settlementState);
}
if (count == 0) revert InvalidRefundRange(startIndex, count);
if (count > MAX_REFUND_BATCH) revert RefundBatchTooLarge(count, MAX_REFUND_BATCH);
uint256 length = minterList.length;
if (startIndex >= length) revert InvalidRefundRange(startIndex, count);
uint256 endIndex = startIndex + count;
if (endIndex > length) endIndex = length;
for (uint256 i = startIndex; i < endIndex; ++i) {
address minter = minterList[i];
MinterInfo storage info = minterInfo[minter];
if (info.ethReturned) continue;
uint256 units = info.mintedUnits;
uint256 amount = units * mintPrice;
info.ethReturned = true;
(bool success,) = payable(minter).call{ value: amount }("");
if (!success) revert ETHTransferFailed(minter);
emit ETHReturned(minter, units, amount);
}
}
function transferProceeds(address payable recipient) external onlyOwner nonReentrant {
_requireMintEnded();
if (settlementState != SettlementState.Pending) {
revert InvalidSettlementState(settlementState);
}
if (recipient == address(0)) revert ZeroAddress();
settlementState = SettlementState.Transferred;
uint256 amount = address(this).balance;
(bool success,) = recipient.call{ value: amount }("");
if (!success) revert ETHTransferFailed(recipient);
emit ProceedsTransferred(recipient, amount);
}
function getRecentMints() external view returns (MintRecord[] memory records) {
uint256 transactionCount = totalMintTransactions;
uint256 resultLength = transactionCount;
if (resultLength > RECENT_MINT_LIMIT) resultLength = RECENT_MINT_LIMIT;
records = new MintRecord[](resultLength);
for (uint256 i; i < resultLength; ++i) {
uint256 slot = (transactionCount - 1 - i) % RECENT_MINT_LIMIT;
records[i] = recentMints[slot];
}
}
function minterCount() external view returns (uint256) {
return minterList.length;
}
function minterAt(uint256 index) external view returns (address) {
if (index >= minterList.length) revert MinterIndexOutOfBounds(index);
return minterList[index];
}
function _requireMintEnded() private view {
if (!mintEnded) revert MintNotEnded();
}
function _hasTokenBalance() private view returns (bool) {
if (address(token) == address(0)) return false;
try token.balanceOf(address(this)) returns (uint256 balance) {
return balance > 0;
} catch {
return false;
}
}
function _endMint() private {
mintEnded = true;
emit MintEnded(totalMintedUnits);
}
function _validateOwner(address owner_) private pure returns (address) {
if (owner_ == address(0)) revert ZeroAddress();
return owner_;
}
}
Chain explorer5833msChain node101ms