Skip to content
On this page

L1-12: Gated NFT(门控 NFT)

1. 问题

创建一个 ERC-721 NFT 合约,要求用户在铸造时必须支付指定的 ETH 费用。核心需求:可配置的铸造价格、最大供应量限制、铸造数量追踪、超额支付找零。

这本质上是「付费门槛」模式——用户需要通过经济投入来获得 NFT,同时合约需要确保不会超发,并正确处理用户多付的 ETH。

2. 原因

NFT 铸造的经济模型是每个 NFT 项目的基础。几乎所有 NFT 项目都需要:

  • 付费铸造:防止机器人批量免费领取(Sybil 攻击)
  • 供应量上限:创造稀缺性,维护价值
  • 价格可调:适应市场变化(预售 vs 公开铸造不同价格)

门控 NFT 是理解「payable + access control + supply cap」组合的第一课。相比白名单(Merkle Proof),付费门槛更简单直接,是进入 NFT 世界的起点。

3. 方案

合约架构

继承 OpenZeppelin ERC-721,添加支付验证逻辑:

solidity
contract GatedNFT is ERC721 {
    uint256 public mintPrice;
    uint256 public maxSupply;
    uint256 public totalSupply;
    uint256 private _tokenIdCounter;

    constructor(
        uint256 _mintPrice,
        uint256 _maxSupply
    ) ERC721("GatedNFT", "GTD") {
        mintPrice = _mintPrice;
        maxSupply = _maxSupply;
    }

    function mint() external payable {
        require(msg.value >= mintPrice, "Insufficient payment");
        require(totalSupply < maxSupply, "Max supply reached");

        uint256 tokenId = _tokenIdCounter;
        _tokenIdCounter++;
        _safeMint(msg.sender, tokenId);
        totalSupply++;

        // 找零:如果用户多付了,退差额
        if (msg.value > mintPrice) {
            payable(msg.sender).transfer(msg.value - mintPrice);
        }
    }
}

关键机制

  1. 铸造门槛msg.value >= mintPrice 确保经济门槛
  2. 供应上限totalSupply < maxSupply 防止超发
  3. 自增 ID_tokenIdCounter++ 为每个 NFT 分配唯一 ID
  4. 找零处理:超额支付时退还差额,避免用户资金损失

4. 遭遇的陷阱

4.1 找零中的重入风险

mint() 中先 _safeMint()transfer() 找零——如果 _safeMint() 触发了接收方的 onERC721Received 回调,此时 totalSupply 已增加但找零尚未完成。一个恶意的接收方合约可能在回调中重新调用 mint()

4.2 totalSupply 追踪不准确

OpenZeppelin ERC-721 内部没有追踪 totalSupply。如果手动维护的 totalSupply 计数器与实际的 balanceOf 分布不一致(例如通过 _burn 销毁),会导致供应上限逻辑出错。

4.3 price 更新的时间窗口

如果在 mint() 执行期间 mintPrice 被 owner 通过另一个交易修改,用户可能在不知情的情况下以更高的价格铸造。在高 Gas 环境下,交易可能在 mempool 中等待数分钟,价格可能已经变化。

5. 陷阱的原因

5.1

_safeMint() 内部会检查接收地址是否为合约。如果是合约,它会调用 onERC721Received(to, operator, from, tokenId, data)。这个回调给了攻击者一个执行窗口,虽然在此场景中攻击面有限(因为 mintPrice 已付、totalSupply 已增),但违反了 CEI 原则。

5.2

totalSupply 不是 ERC-721 标准的一部分,每个合约自行维护。如果使用 _burn() 销毁 NFT 而不减少 totalSupply,会导致实际可铸造数量少于预期。

5.3

Solidity 的交易是原子的——单个交易中的价格不会变。但如果依赖前端显示的价格,用户可能在点击「铸造」时看到的是旧价格。正确做法是合约使用交易当时的 mintPrice,前端需要监听 MintPriceChanged 事件。

6. 如何解决陷阱

  • 遵循 CEI 模式:先验证(Checks),再更新状态(Effects),最后做找零转账(Interactions)
  • 如果项目支持 NFT 销毁,在 _burn() 中也减少 totalSupply
  • 使用 nonReentrant modifier 保护 mint() 函数
  • 找零放在所有状态更新之后,或使用 pull-over-push 模式让用户自行提取超额付款

7. 技术要点

要点说明
payable mintmsg.value 验证付费门槛
供应上限totalSupply < maxSupply 防超发
自增 tokenId_tokenIdCounter++ 顺序分配
找零处理超额支付退还差额
ERC-721 安全铸造_safeMint 检查接收方是否为合约
访问控制Owner 可修改 mintPrice 适应市场

Built with AiAda