如何在ERC721合约中设置白名单发售与公开发售的双供应量上限?
Hey James, this is a super common requirement for NFT launches, and you’re totally right that a single maxSupply variable can’t handle splitting mint limits between phases. Let’s walk through the implementation logic first, then jump into a Solidity code example that covers all your needs.
Core Implementation Logic
The key is to track minted counts separately for each phase, plus add guards for per-wallet limits and overall total supply. Here’s the breakdown:
- Track phase-specific minted amounts: Use two state variables to keep tabs on how many NFTs have been minted during the whitelist phase and public phase respectively.
- Define phase limits: Set a hard cap for the whitelist phase (
1500in your case) and calculate the public phase limit astotalMaxSupply - whitelistMaxSupply(which would be8500here). - Per-wallet mint limit: Use a mapping to track how many NFTs each wallet has minted, and enforce that this never exceeds your per-wallet cap (
10). - Phase state control: Add a state variable to track which sale phase is active (whitelist, public, or closed) so you can restrict minting to the correct phase.
Solidity Code Example
Here’s a complete example based on OpenZeppelin’s ERC721 implementation—this covers all your requirements:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract PhasedNFTSale is ERC721, Ownable { // Total maximum supply of NFTs uint256 public constant totalMaxSupply = 10000; // Maximum NFTs allowed to be minted during whitelist phase uint256 public constant whitelistMaxSupply = 1500; // Maximum NFTs a single wallet can mint in total uint256 public constant perWalletMax = 10; // Track minted counts per phase uint256 public whitelistMinted; uint256 public publicMinted; // Whitelist storage (could also use a Merkle tree for gas efficiency with large whitelists) mapping(address => bool) public isWhitelisted; // Sale phase enum enum SalePhase { Closed, Whitelist, Public } SalePhase public currentPhase; // Mapping to track how many NFTs each wallet has minted mapping(address => uint256) public walletMintedCount; constructor() ERC721("PhasedNFT", "PNFT") Ownable(msg.sender) { currentPhase = SalePhase.Closed; } // Owner-only function to add addresses to whitelist function addToWhitelist(address[] calldata addresses) external onlyOwner { for (uint256 i = 0; i < addresses.length; i++) { isWhitelisted[addresses[i]] = true; } } // Owner-only function to switch sale phases function setSalePhase(SalePhase newPhase) external onlyOwner { currentPhase = newPhase; } // Whitelist mint function function whitelistMint(uint256 quantity) external payable { // Check if we're in whitelist phase require(currentPhase == SalePhase.Whitelist, "Whitelist sale not active"); // Check if sender is whitelisted require(isWhitelisted[msg.sender], "Not whitelisted"); // Check per-wallet limit require(walletMintedCount[msg.sender] + quantity <= perWalletMax, "Exceeds per-wallet limit"); // Check whitelist phase supply limit require(whitelistMinted + quantity <= whitelistMaxSupply, "Whitelist supply exhausted"); // Check total supply limit require(whitelistMinted + publicMinted + quantity <= totalMaxSupply, "Total supply exhausted"); // Update mint counts walletMintedCount[msg.sender] += quantity; whitelistMinted += quantity; // Mint the NFTs for (uint256 i = 0; i < quantity; i++) { uint256 tokenId = whitelistMinted + publicMinted - quantity + i; _safeMint(msg.sender, tokenId); } } // Public mint function function publicMint(uint256 quantity) external payable { // Check if we're in public phase require(currentPhase == SalePhase.Public, "Public sale not active"); // Check per-wallet limit require(walletMintedCount[msg.sender] + quantity <= perWalletMax, "Exceeds per-wallet limit"); // Check public phase supply limit (total max minus whitelist max) require(publicMinted + quantity <= totalMaxSupply - whitelistMaxSupply, "Public supply exhausted"); // Check total supply limit require(whitelistMinted + publicMinted + quantity <= totalMaxSupply, "Total supply exhausted"); // Update mint counts walletMintedCount[msg.sender] += quantity; publicMinted += quantity; // Mint the NFTs for (uint256 i = 0; i < quantity; i++) { uint256 tokenId = whitelistMinted + publicMinted - quantity + i; _safeMint(msg.sender, tokenId); } } }
Key Details to Note
- Whitelist Storage: The example uses a simple mapping for whitelisting, which works fine for 2000 addresses. For larger whitelists, consider using a Merkle Tree to save gas (you can verify a proof instead of storing every address on-chain).
- Phase Control: The
setSalePhasefunction lets you toggle between closed, whitelist, and public phases—only the contract owner can call this, so you have full control over when each phase starts/ends. - Per-Wallet Limit: The
walletMintedCountmapping tracks total mints per wallet across both phases, so a user can’t mint 10 in whitelist and another 10 in public. - Redundant Supply Checks: We add checks for total supply alongside phase-specific limits to ensure we never exceed the 10000 total cap, even if there’s a bug in phase logic.
This setup gives you full control over each sale phase’s supply, enforces per-wallet limits, and stays within your total supply cap. Let me know if you need help adapting this to your specific stack!
内容的提问来源于stack exchange,提问作者JamesL

