Skip to main content
PulseScanner.io

Address

0x100f0cc209e86d58f3acdde43392866d2f4e1a8d
Current Holdings
$0.00
TXs sent
not counted
First Active
2026-08-11
block 27,260,808
Last Active
34 days ago
block 27,290,477
Funded By
0x2de5…3fbb

Net worth historyi

12 snapshots · to block 27,463,673coverage change 26 Augcoverage change 27 Augcoverage change 27 Augcoverage change 27 Augcoverage change 27 Aug
exact matchPulseMonkeyBusinesssolc 0.8.26+commit.8a97fa7aruntime exact · creation exact
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/access/Ownable2Step.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

/**
 * @title Pulse Monkey Business
 * @notice 5,000 ERC-721 NFTs on PulseChain.
 *
 * Mint rules:
 * - 2,000,000 PLS per NFT
 * - maximum 20 NFTs per transaction
 * - no per-wallet holding or mint limit
 *
 * Metadata:
 * - tokenURI format: <baseURI><tokenId>.json
 * - baseURI can be permanently frozen by the owner
 *
 * Royalties:
 * - ERC-2981 royalty signal fixed at 2.5% (250 basis points)
 * - royalty receiver can be changed by the owner
 *
 * Token assignment:
 * - token IDs are drawn without replacement from 1..5000
 * - this prevents duplicate token IDs and makes the next token ID non-sequential
 * - the on-chain pseudo-random draw is NOT cryptographically/provably fair randomness
 */
contract PulseMonkeyBusiness is
    ERC721Enumerable,
    ERC2981,
    Ownable2Step,
    ReentrancyGuard
{
    using Strings for uint256;

    uint256 public constant MAX_SUPPLY = 5000;
    uint256 public constant MAX_PER_TX = 20;
    uint256 public constant mintPrice = 2_000_000 ether;

    uint96 public constant ROYALTY_BPS = 250; // 2.5% of 10,000

    bool public mintOpen;
    bool public metadataFrozen;

    address payable public treasury;
    address public royaltyReceiver;

    string private _collectionBaseURI;

    // Lazy Fisher-Yates shuffle. Values are stored encoded as value + 1 so
    // mapping zero can safely mean "not initialized".
    mapping(uint256 => uint256) private _tokenMatrix;

    event MintStateChanged(bool isOpen);
    event BaseURIChanged(string newBaseURI);
    event MetadataFrozen(string baseURI);
    event TreasuryChanged(address indexed oldTreasury, address indexed newTreasury);
    event RoyaltyReceiverChanged(address indexed oldReceiver, address indexed newReceiver);
    event MintBatch(address indexed minter, uint256 quantity, uint256 totalPaid);
    event Withdrawal(address indexed treasury, uint256 amount);

    error MintClosed();
    error InvalidQuantity();
    error SoldOut();
    error IncorrectPayment();
    error ZeroAddress();
    error MetadataIsFrozen();
    error BaseURINotSet();
    error InvalidTokenRange();
    error TransferFailed();

    constructor()
        ERC721("Pulse Monkey Business", "PMB")
        Ownable(msg.sender)
    {
        treasury = payable(msg.sender);
        royaltyReceiver = msg.sender;
        _setDefaultRoyalty(msg.sender, ROYALTY_BPS);
    }

    /**
     * @notice Mint 1-20 PMB NFTs.
     * @dev No per-wallet cap. Users may call this repeatedly while supply remains.
     */
    function mint(uint256 quantity) external payable nonReentrant {
        if (!mintOpen) revert MintClosed();
        if (quantity == 0 || quantity > MAX_PER_TX) revert InvalidQuantity();

        uint256 supply = totalSupply();
        if (supply + quantity > MAX_SUPPLY) revert SoldOut();

        uint256 requiredPayment = mintPrice * quantity;
        if (msg.value != requiredPayment) revert IncorrectPayment();

        for (uint256 i = 0; i < quantity; ) {
            uint256 tokenId = _drawTokenId(i);
            _safeMint(msg.sender, tokenId);

            unchecked {
                ++i;
            }
        }

        emit MintBatch(msg.sender, quantity, requiredPayment);
    }

    /**
     * @notice Returns true when a token ID has already been minted.
     * Useful for the website Collection view.
     */
    function isMinted(uint256 tokenId) public view returns (bool) {
        if (tokenId == 0 || tokenId > MAX_SUPPLY) return false;
        return _ownerOf(tokenId) != address(0);
    }

    /**
     * @notice Returns minted/unminted status for a consecutive range.
     * @dev Intended for website batching instead of 5,000 separate RPC calls.
     * Max 500 tokens per call.
     */
    function mintedStatus(
        uint256 startTokenId,
        uint256 count
    ) external view returns (bool[] memory statuses) {
        if (
            startTokenId == 0 ||
            count == 0 ||
            count > 500 ||
            startTokenId + count - 1 > MAX_SUPPLY
        ) {
            revert InvalidTokenRange();
        }

        statuses = new bool[](count);

        for (uint256 i = 0; i < count; ) {
            statuses[i] = _ownerOf(startTokenId + i) != address(0);

            unchecked {
                ++i;
            }
        }
    }

    /**
     * @notice All token IDs currently owned by a wallet.
     * This is what the PMB website's "My Wallet" section can call.
     */
    function walletOfOwner(
        address wallet
    ) external view returns (uint256[] memory tokenIds) {
        uint256 balance = balanceOf(wallet);
        tokenIds = new uint256[](balance);

        for (uint256 i = 0; i < balance; ) {
            tokenIds[i] = tokenOfOwnerByIndex(wallet, i);

            unchecked {
                ++i;
            }
        }
    }

    function maxSupply() external pure returns (uint256) {
        return MAX_SUPPLY;
    }

    function maxPerTransaction() external pure returns (uint256) {
        return MAX_PER_TX;
    }

    function unmintedCount() external view returns (uint256) {
        return MAX_SUPPLY - totalSupply();
    }

    /**
     * @notice Returns all minted token IDs.
     * For normal website operation, mintedStatus() in batches is preferred.
     */
    function mintedTokenIds() external view returns (uint256[] memory tokenIds) {
        uint256 supply = totalSupply();
        tokenIds = new uint256[](supply);

        for (uint256 i = 0; i < supply; ) {
            tokenIds[i] = tokenByIndex(i);

            unchecked {
                ++i;
            }
        }
    }

    // -------------------------
    // Metadata
    // -------------------------

    function tokenURI(
        uint256 tokenId
    ) public view override returns (string memory) {
        _requireOwned(tokenId);

        string memory base = _baseURI();
        if (bytes(base).length == 0) return "";

        return string.concat(base, tokenId.toString(), ".json");
    }

    function baseURI() external view returns (string memory) {
        return _collectionBaseURI;
    }

    function setBaseURI(string calldata newBaseURI) external onlyOwner {
        if (metadataFrozen) revert MetadataIsFrozen();
        _collectionBaseURI = newBaseURI;
        emit BaseURIChanged(newBaseURI);
    }

    function freezeMetadata() external onlyOwner {
        if (metadataFrozen) revert MetadataIsFrozen();
        if (bytes(_collectionBaseURI).length == 0) revert BaseURINotSet();

        metadataFrozen = true;
        emit MetadataFrozen(_collectionBaseURI);
    }

    function _baseURI() internal view override returns (string memory) {
        return _collectionBaseURI;
    }

    // -------------------------
    // Owner controls
    // -------------------------

    function setMintOpen(bool isOpen) external onlyOwner {
        // Prevent accidentally opening mint before metadata URI is configured.
        if (isOpen && bytes(_collectionBaseURI).length == 0) {
            revert BaseURINotSet();
        }

        mintOpen = isOpen;
        emit MintStateChanged(isOpen);
    }

    function setTreasury(address payable newTreasury) external onlyOwner {
        if (newTreasury == address(0)) revert ZeroAddress();

        address oldTreasury = treasury;
        treasury = newTreasury;

        emit TreasuryChanged(oldTreasury, newTreasury);
    }

    /**
     * @notice Changes who receives the fixed 2.5% ERC-2981 royalty.
     * The percentage itself remains fixed at 2.5%.
     */
    function setRoyaltyReceiver(address newReceiver) external onlyOwner {
        if (newReceiver == address(0)) revert ZeroAddress();

        address oldReceiver = royaltyReceiver;
        royaltyReceiver = newReceiver;
        _setDefaultRoyalty(newReceiver, ROYALTY_BPS);

        emit RoyaltyReceiverChanged(oldReceiver, newReceiver);
    }

    /**
     * @notice Withdraw all primary mint proceeds to the treasury.
     */
    function withdraw() external onlyOwner nonReentrant {
        uint256 amount = address(this).balance;

        (bool success, ) = treasury.call{value: amount}("");
        if (!success) revert TransferFailed();

        emit Withdrawal(treasury, amount);
    }

    // -------------------------
    // Token drawing
    // -------------------------

    function _drawTokenId(uint256 mintIndex) private returns (uint256) {
        uint256 remaining = MAX_SUPPLY - totalSupply();

        uint256 randomIndex = uint256(
            keccak256(
                abi.encodePacked(
                    block.prevrandao,
                    block.timestamp,
                    blockhash(block.number - 1),
                    msg.sender,
                    address(this),
                    totalSupply(),
                    mintIndex
                )
            )
        ) % remaining;

        uint256 encodedPicked = _tokenMatrix[randomIndex];
        uint256 pickedIndex = encodedPicked == 0
            ? randomIndex
            : encodedPicked - 1;

        uint256 lastIndex = remaining - 1;

        if (randomIndex != lastIndex) {
            uint256 encodedLast = _tokenMatrix[lastIndex];
            uint256 lastValue = encodedLast == 0
                ? lastIndex
                : encodedLast - 1;

            _tokenMatrix[randomIndex] = lastValue + 1;
        }

        delete _tokenMatrix[lastIndex];

        // Metadata/token IDs are 1 through 5,000.
        return pickedIndex + 1;
    }

    // -------------------------
    // ERC-165 interface support
    // -------------------------

    function supportsInterface(
        bytes4 interfaceId
    ) public view override(ERC721Enumerable, ERC2981) returns (bool) {
        return super.supportsInterface(interfaceId);
    }
}