// SPDX-License-Identifier: MIT pragma solidity 0.8.30; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; interface IAgentID { function handleOf(address wallet) external view returns (string memory); } /// @notice Free BSC collection. All mint paths require a registered Binance Agent Wallet. /// @dev The phases, lists and metadata directory are fixed at construction. contract GenesisWorkers is ERC721 { uint256 public constant MAX_SUPPLY = 2000; IAgentID public constant REGISTRY = IAgentID(0x60B9262456dE011F7d15a54CdD8c48bAaa8B022F); enum Phase { Closed, WL, FCFS, Public } enum Result { Ready, Inactive, SoldOut, AlreadyMinted, NoAgentID, StageFull, NotListed } uint64 public immutable wlStart; uint64 public immutable fcfsStart; uint64 public immutable publicStart; uint64 public immutable endTime; bytes32 public immutable wlRoot; bytes32 public immutable fcfsRoot; uint16 public immutable wlCap; uint16 public immutable fcfsCap; string private metadataDirectory; uint256 public totalMinted; mapping(address => bool) public claimed; mapping(Phase => uint256) public mintedInPhase; event Claimed(address indexed wallet, uint256 indexed tokenId, Phase indexed phase); error InvalidConfiguration(); error PhaseChanged(); error CannotMint(Result reason); constructor(uint64[4] memory times, uint16[2] memory caps, bytes32[2] memory roots, string memory directory) ERC721("Process ID Genesis Workers","PID") { bytes memory uri = bytes(directory); if (block.chainid != 56 || address(REGISTRY).code.length == 0 || times[0] <= block.timestamp || times[0] >= times[1] || times[1] >= times[2] || times[2] >= times[3] || caps[0] == 0 || caps[1] == 0 || uint256(caps[0]) + caps[1] >= MAX_SUPPLY || roots[0] == 0 || roots[1] == 0 || uri.length == 0 || uri[uri.length - 1] != 0x2f) revert InvalidConfiguration(); wlStart=times[0]; fcfsStart=times[1]; publicStart=times[2]; endTime=times[3]; wlCap=caps[0]; fcfsCap=caps[1]; wlRoot=roots[0]; fcfsRoot=roots[1]; metadataDirectory=directory; } function phase() public view returns (Phase) { if (block.timestamp < wlStart || block.timestamp >= endTime) return Phase.Closed; if (block.timestamp < fcfsStart) return Phase.WL; if (block.timestamp < publicStart) return Phase.FCFS; return Phase.Public; } function eligibility(address wallet, bytes32[] calldata proof) public view returns (Phase stage, Result reason) { stage=phase(); if (stage == Phase.Closed) return (stage, Result.Inactive); if (totalMinted >= MAX_SUPPLY) return (stage, Result.SoldOut); if (claimed[wallet]) return (stage, Result.AlreadyMinted); if (wallet == address(0) || bytes(REGISTRY.handleOf(wallet)).length == 0) return (stage, Result.NoAgentID); if (stage != Phase.Public) { (uint256 cap,bytes32 root) = stage == Phase.WL ? (uint256(wlCap),wlRoot) : (uint256(fcfsCap),fcfsRoot); if (mintedInPhase[stage] >= cap) return (stage, Result.StageFull); bytes32 leaf=keccak256(bytes.concat(keccak256(abi.encode(block.chainid,uint8(stage),wallet)))); if (!MerkleProof.verifyCalldata(proof,root,leaf)) return (stage, Result.NotListed); } return (stage,Result.Ready); } function mint(Phase expected, bytes32[] calldata proof) external returns (uint256 tokenId) { (Phase stage,Result reason)=eligibility(msg.sender,proof); if (stage != expected) revert PhaseChanged(); if (reason != Result.Ready) revert CannotMint(reason); claimed[msg.sender]=true; tokenId=++totalMinted; ++mintedInPhase[stage]; _safeMint(msg.sender,tokenId); emit Claimed(msg.sender,tokenId,stage); } function tokenURI(uint256 tokenId) public view override returns (string memory) { _requireOwned(tokenId); return string.concat(metadataDirectory,Strings.toString(tokenId),".json"); } }