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