RobinbooMint
0xc0b3b1a0803eb6ca76fb30895772e169a74486c7
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
5,475 bytes
ABI entries
56
18 read · 9 write
License
none
Contract information
- Address
- 0xc0b3b1a0803eb6ca76fb30895772e169a74486c7
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0x46292B1EeB…a0B9E669Ae
- Creation tx
- 0xbea1594693…9670a3f37e
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (18)
MAX_MINT_UNITS() → uint32
MAX_REFUND_BATCH() → uint256
MAX_UNITS_PER_WALLET() → uint8
PUBLIC_INVENTORY() → uint256
RECENT_MINT_LIMIT() → uint256
TOKENS_PER_UNIT() → uint256
getRecentMints() → tuple[]
mintEnded() → bool
mintPrice() → uint256
minterAt(uint256) → address
minterCount() → uint256
minterInfo(address) → uint8, bool
owner() → address
pendingOwner() → address
settlementState() → uint8
token() → address
totalMintTransactions() → uint64
totalMintedUnits() → uint32
Events (8)
ETHReturnedFullRefundStartedMintEndedMintedOwnershipTransferStartedOwnershipTransferredProceedsTransferredTokenInitialized
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "owner_",
"type": "address"
},
{
"internalType": "uint256",
"name": "mintPrice_",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "ETHTransferFailed",
"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": "uint256",
"name": "actual",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "required",
"type": "uint256"
}
],
"name": "InvalidTokenInventory",
"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": [
{
"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": [
{
"internalType": "uint256",
"name": "requestedTotal",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "maximum",
"type": "uint256"
}
],
"name": "WalletLimitExceeded",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"inputs": [],
"name": "ZeroMintPrice",
"type": "error"
},
{
"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"
},
{
"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": "MAX_UNITS_PER_WALLET",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"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": "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": [
{
"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": "uint8",
"name": "mintedUnits",
"type": "uint8"
},
{
"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"
}
]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 {
uint8 mintedUnits;
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 = 16_000 ether;
uint32 public constant MAX_MINT_UNITS = 50_000;
uint8 public constant MAX_UNITS_PER_WALLET = 10;
uint256 public constant MAX_REFUND_BATCH = 100;
uint256 public constant RECENT_MINT_LIMIT = 50;
IERC20 public token;
uint256 public immutable mintPrice;
bool public mintEnded;
SettlementState public settlementState;
uint32 public totalMintedUnits;
uint64 public totalMintTransactions;
mapping(address => MinterInfo) public minterInfo;
address[] private minterList;
MintRecord[50] private recentMints;
error ZeroAddress();
error ZeroMintPrice();
error TokenAlreadyInitialized();
error TokenNotInitialized();
error InvalidTokenInventory(uint256 actual, uint256 required);
error MintAlreadyEnded();
error InvalidUnits();
error WalletLimitExceeded(uint256 requestedTotal, uint256 maximum);
error MintSupplyExceeded(uint256 requestedTotal, uint256 maximum);
error IncorrectPayment(uint256 sent, uint256 required);
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 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();
IERC20 candidate = IERC20(token_);
uint256 inventory = candidate.balanceOf(address(this));
if (inventory != PUBLIC_INVENTORY) {
revert InvalidTokenInventory(inventory, PUBLIC_INVENTORY);
}
token = candidate;
emit TokenInitialized(token_);
}
function mint(uint256 units) external payable nonReentrant {
if (address(token) == address(0)) revert TokenNotInitialized();
if (mintEnded) revert MintAlreadyEnded();
if (units == 0) revert InvalidUnits();
MinterInfo storage info = minterInfo[msg.sender];
uint256 requestedWalletTotal = uint256(info.mintedUnits) + units;
if (requestedWalletTotal > MAX_UNITS_PER_WALLET) {
revert WalletLimitExceeded(requestedWalletTotal, MAX_UNITS_PER_WALLET);
}
uint256 requestedGlobalTotal = uint256(totalMintedUnits) + units;
if (requestedGlobalTotal > MAX_MINT_UNITS) {
revert MintSupplyExceeded(requestedGlobalTotal, MAX_MINT_UNITS);
}
uint256 requiredPayment = units * mintPrice;
if (msg.value != requiredPayment) revert IncorrectPayment(msg.value, requiredPayment);
if (info.mintedUnits == 0) minterList.push(msg.sender);
info.mintedUnits = uint8(requestedWalletTotal);
totalMintedUnits = uint32(requestedGlobalTotal);
uint256 slot = uint256(totalMintTransactions) % RECENT_MINT_LIMIT;
recentMints[slot] = MintRecord(msg.sender, uint40(block.timestamp), uint8(units));
unchecked {
++totalMintTransactions;
}
if (requestedGlobalTotal == MAX_MINT_UNITS) {
mintEnded = true;
emit MintEnded(requestedGlobalTotal);
}
uint256 tokenAmount = units * TOKENS_PER_UNIT;
token.safeTransfer(msg.sender, tokenAmount);
emit Minted(msg.sender, units, tokenAmount, msg.value);
}
function endMint() external onlyOwner {
if (mintEnded) revert MintAlreadyEnded();
mintEnded = true;
emit MintEnded(totalMintedUnits);
}
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 _validateOwner(address owner_) private pure returns (address) {
if (owner_ == address(0)) revert ZeroAddress();
return owner_;
}
}
Chain explorer5458msChain node90ms