如何通过balanceOf()实现Solidity合约NFT铸造限额
NFT限量铸造限额失效问题修复方案
你的铸造限额失效问题分合约层核心bug和前端逻辑瑕疵两部分,按以下步骤修复即可正常运行:
1. 合约层修复(根本原因)
你最初的合约代码存在致命逻辑遗漏:mint函数里虽然做了walletMints的限额校验,但从来没有在铸造成功前更新walletMints映射的计数,导致每次调用mint时读到的用户已铸造数量永远是初始值0,自然无法拦截重复铸造。
❌ 错误思路提示:不要用
balanceOf做限额判断,用户把铸造得到的NFT转出后,balanceOf数值会下降,攻击者可以通过转走NFT反复铸造突破限额。必须用独立的walletMints映射记录地址累计铸造量,这个值只增不减,才是安全的。
修正后的完整mint函数代码:
function mint(uint256 quantity_) public payable{ require(isPublicMintEnabled, 'minting not enabled'); require(msg.value == quantity_ * mintPrice, 'wrong mint value'); require(totalSupply + quantity_ <= maxSupply, 'sold out'); require(walletMints[msg.sender] + quantity_ <= maxPerWallet, 'exceed max wallet'); // 新增:校验通过后立即更新用户铸造计数,必须在_safeMint前执行 walletMints[msg.sender] += quantity_; for(uint256 i = 0; i < quantity_; i++){ uint256 newTokenId = totalSupply + 1; totalSupply++; _safeMint(msg.sender, newTokenId); } }
2. 前端逻辑修复
你调整后的前端代码存在三个问题:
- 合约返回的计数是BigNumber类型,直接和普通数字比较会出现类型判断错误
- 只判断了已铸造数量小于上限,没有把本次要铸造的数量计入校验,会出现用户已铸1个、本次铸2个仍然超过限额的漏洞
- 没有提前获取签名者地址,直接取
signer.address在部分ethers版本下会返回未解析的promise对象
修正后的铸造逻辑代码:
const MainMint = ({ accounts}) =>{ const [mintAmount] = useState(1); // 和合约maxPerWallet保持一致的单地址铸造上限 const MAX_PER_WALLET = 2; const isConnected = Boolean(accounts[0]); async function handleMint(){ if(!window.ethereum || !isConnected) return; const provider = new ethers.providers.Web3Provider(window.ethereum); const signer = provider.getSigner(); // 显式获取用户地址,避免版本兼容问题 const userAddress = await signer.getAddress(); const contract = new ethers.Contract( NFTAddress, NFT.abi, signer ); try{ // 读取已铸造数量,转换为JS普通数字再做比较 const mintedRes = await contract.walletMints(userAddress); const currentMinted = mintedRes.toNumber(); if(currentMinted + mintAmount > MAX_PER_WALLET){ console.log(`单地址最多铸造${MAX_PER_WALLET}个,你已铸造${currentMinted}个`); return; } // 0成本测试网铸造保留0即可,主网部署替换为实际单NFT价格 const payValue = ethers.utils.parseEther((0 * mintAmount).toString()); const tx = await contract.mint(BigNumber.from(mintAmount), { value: payValue }); console.log('铸造交易已发送,交易哈希:', tx.hash); // 等待交易上链后再更新前端状态 await tx.wait(); console.log('铸造成功'); } catch(err){ console.log("铸造失败: ", err); } } // 其余组件渲染逻辑保持原有实现即可 }
关键提示
- 前端的限额校验仅用于优化用户体验,所有安全规则必须在合约层实现,前端代码可以被用户随意篡改、绕过,只靠前端拦截没有任何安全保障
- 建议给合约的
mint函数加上重入保护修饰器,避免潜在的安全风险 - 部署合约前在本地测试网多做边界场景测试:比如铸造到上限后再尝试铸造、转走NFT后再尝试铸造,确认规则符合预期
内容的提问来源于stack exchange,提问作者Ken Del Pen
相关产品推荐
相关产品推荐

