Address
0x100f0cc209e86d58f3acdde43392866d2f4e1a8dCurrent 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
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);
}
}