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