You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NFT白名单:如何实现单地址可变铸造额度

用离线签名方案实现可变额度白名单的NFT铸造合约

直接用链上mapping(address => uint)存储每个地址的铸造额度,确实会因为批量写入的gas成本过高(以太坊上每写一个存储slot要几千gas,几百条就是几十万甚至上百万gas)而不划算。离线签名授权是目前最常用的替代方案,能同时降低项目方和铸造者的成本,具体实现思路如下:

核心逻辑

项目方不需要提前把所有白名单地址和额度写到链上,而是离线为每个白名单地址生成专属的授权签名,用户铸造时携带这个签名给合约验证,合约只需要验证签名有效性、用户当前铸造数量是否超出签名中的额度即可。

关键步骤

  1. 项目方离线生成签名

    • 为每个白名单地址生成包含以下信息的授权内容:用户地址、可铸造最大额度、签名过期时间(可选,防止旧签名滥用)、合约地址(可选,防止签名被其他合约复用)。
    • 用项目方的私钥对这些内容的哈希值进行签名,得到签名数据。
    • 把签名和对应的授权信息分发给白名单用户(比如通过前端页面、邮件等)。
  2. 用户铸造时验证签名

    • 用户调用铸造函数时,传入自己要铸造的数量、授权信息、签名数据。
    • 合约先验证签名是否由项目方的私钥生成,再检查当前用户已铸造数量 + 本次铸造数量是否不超过签名中的额度,最后执行铸造逻辑。

合约代码示例

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract WhitelistNFT is ERC721 {
    // 项目方的签名地址(预存在合约中)
    address public immutable signer;
    // 记录每个用户已铸造的数量(仅当用户实际铸造时才写入,成本极低)
    mapping(address => uint256) public mintedCount;
    // 下一个要铸造的tokenID
    uint256 public nextTokenId;

    // 授权信息结构体
    struct MintAllowance {
        address user;
        uint256 maxAmount;
        uint256 deadline; // 签名过期时间(Unix时间戳)
        address contractAddr; // 绑定合约地址,防止签名跨合约复用
    }

    constructor(address _signer) ERC721("WhitelistNFT", "WNFT") {
        signer = _signer;
    }

    // 验证签名有效性
    function verifySignature(MintAllowance calldata allowance, bytes calldata signature) public view returns (bool) {
        // 生成消息哈希,包含所有关键信息防止篡改
        bytes32 messageHash = keccak256(abi.encode(
            allowance.user,
            allowance.maxAmount,
            allowance.deadline,
            allowance.contractAddr
        ));
        // 转换为以太坊签名标准的哈希(添加前缀)
        bytes32 ethSignedHash = keccak256(abi.encodePacked(
            "\x19Ethereum Signed Message:\n32",
            messageHash
        ));

        // 恢复签名者地址
        address recovered = ecrecover(ethSignedHash, signature[64], bytes32(signature[0:32]), bytes32(signature[32:64]));
        // 验证签名者是项目方、签名未过期、用户匹配、合约地址匹配
        return recovered == signer 
            && block.timestamp <= allowance.deadline 
            && allowance.user == msg.sender 
            && allowance.contractAddr == address(this);
    }

    // 铸造函数
    function mint(uint256 amount, MintAllowance calldata allowance, bytes calldata signature) external payable {
        // 1. 验证签名
        require(verifySignature(allowance, signature), "Invalid/expired signature");
        // 2. 检查是否超过额度
        require(mintedCount[msg.sender] + amount <= allowance.maxAmount, "Exceeds mint limit");
        // 3. 检查支付金额(如果是付费铸造)
        // require(msg.value == amount * PRICE_PER_NFT, "Incorrect ETH amount");

        // 执行铸造
        for (uint256 i = 0; i < amount; i++) {
            _safeMint(msg.sender, nextTokenId);
            nextTokenId++;
        }
        // 更新已铸造数量
        mintedCount[msg.sender] += amount;
    }
}

成本优势

  • 项目方:不需要执行任何链上批量写入操作,只需要离线生成签名(本地用脚本即可完成,比如用ethers.js或web3.js),几乎零gas成本,不管白名单有几百还是几千条都能轻松处理。
  • 用户:铸造时只需要多传递几个参数和签名,gas成本仅比普通铸造略高一点,远低于项目方提前批量写入白名单带来的间接成本(比如项目方可能会把成本转嫁到用户身上)。

注意事项

  • 必须保留mintedCount的mapping:用来记录用户已铸造的数量,防止用户重复使用同一个签名多次铸造,超出额度。这个mapping只有用户实际铸造时才会写入,所以整体gas成本比预先批量写入所有白名单地址低很多。
  • 签名要加入过期时间:避免项目方取消白名单后,用户还能用旧签名铸造。
  • 签名要绑定合约地址:防止攻击者把签名拿到其他同逻辑的合约上复用。
  • 项目方要妥善保管私钥:一旦私钥泄露,攻击者可以伪造任意地址的授权签名,造成损失。

内容的提问来源于stack exchange,提问作者agonist_

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 00:57:13