Appearance
L1-12: Gated NFT(ゲーテッドNFT)
1. 問題
ユーザーがミント時に指定されたETH手数料を支払う必要があるERC-721 NFTコントラクトを作成します。中核要件:設定可能なミント価格、最大供給量制限、ミント数追跡、過払い時の釣り銭返金。
これは本質的に「ペイウォール」パターンです——ユーザーはNFTを取得するために経済的価値を投入する必要があり、同時にコントラクトは過剰発行を防ぎ、ユーザーが過払いしたETHを正しく処理する必要があります。
2. 理由
NFTミントの経済モデルは、すべてのNFTプロジェクトの基盤です。ほぼすべてのNFTプロジェクトに必要なもの:
- 有料ミント:ボットによる大量の無料請求(Sybil攻撃)を防止
- 供給量上限:希少性を創出し、価値を維持
- 価格調整可能:市場の変化に適応(プレセールとパブリックミントで異なる価格)
ゲーテッドNFTは「payable + アクセス制御 + 供給上限」の組み合わせを理解する最初のレッスンです。ホワイトリスト(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);
}
}
}
主要メカニズム
- ミント閾値:
msg.value >= mintPriceが経済的障壁を強制 - 供給上限:
totalSupply < maxSupplyが過剰発行を防止 - 自動インクリメントID:
_tokenIdCounter++が各NFTに一意のIDを割り当て - 釣り銭処理:過払い時に差額を返金し、ユーザーの資金損失を防止
4. 遭遇した落とし穴
4.1 釣り銭返金におけるリエントランシーリスク
mint() 内で _safeMint() の後に transfer() で釣り銭を返金——_safeMint() が受信側の onERC721Received コールバックをトリガーした場合、その時点で totalSupply は既に増加しているが釣り銭返金はまだ完了していません。悪意のある受信側コントラクトがコールバック中に mint() を再呼び出しする可能性があります。
4.2 totalSupply 追跡の不正確さ
OpenZeppelin ERC-721 は内部的に totalSupply を追跡しません。手動で管理する totalSupply カウンターが実際の balanceOf 分布と矛盾する場合(例:_burn による)、供給上限ロジックが破綻します。
4.3 価格更新の時間窓
mint() 実行中に mintPrice がオーナーによって別のトランザクションで変更された場合、ユーザーは知らないうちにより高い価格でミントする可能性があります。高ガス環境では、トランザクションが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モディファイアを使用してmint()関数を保護する- 釣り銭返金はすべての状態更新の後に行うか、または「pull-over-push」パターンを使用してユーザーが超過支払いを自身で引き出せるようにする
7. 技術的ポイント
| ポイント | 説明 |
|---|---|
| payable ミント | msg.value が支払い障壁を検証 |
| 供給上限 | totalSupply < maxSupply が過剰発行を防止 |
| 自動インクリメント tokenId | _tokenIdCounter++ が順次IDを割り当て |
| 釣り銭処理 | 過払い時に差額を返金 |
| ERC-721 安全なミント | _safeMint が受信側がコントラクトかどうかをチェック |
| アクセス制御 | オーナーが市場に適応して mintPrice を変更可能 |