前言本文档详细介绍基于Solidity开发NFT盲盒智能合约的全流程,核心集成ChainlinkVRF(可验证随机函数)实现公平随机的盲盒开启逻辑,包含合约开发、本地测试、部署脚本等实操内容。同时补充ChainlinkVRF的核心概念、前置准备及使用方法,帮助开发者快速理解随机数在区块链中的
本文档详细介绍基于Solidity开发NFT盲盒智能合约的全流程,核心集成Chainlink VRF(可验证随机函数)实现公平随机的盲盒开启逻辑,包含合约开发、本地测试、部署脚本等实操内容。同时补充Chainlink VRF的核心概念、前置准备及使用方法,帮助开发者快速理解随机数在区块链中的实现原理与落地方式。补充说明:
未使用正式 Chainlink 网络、借助 MockVRF 规避 LINK 支付前置内容
1.1 核心技术栈介绍
Chainlink VRF(Verifiable Random Function,可验证随机函数)是Chainlink预言机网络提供的核心服务之一,用于生成链上可验证的安全随机数。其核心优势在于“可验证性”——每一个生成的随机数都附带加密证明,开发者可在合约中验证随机数的真实性与公平性,杜绝被篡改的可能。
相较于链上伪随机数(如基于区块哈希、时间戳生成),VRF通过去中心化预言机节点生成随机数,避免了区块信息可预测导致的随机数被操纵问题,广泛应用于NFT盲盒、游戏抽奖、去中心化博彩等需要公平随机逻辑的场景。
Chainlink VRF的工作流程分为4个核心步骤,确保随机数的安全性与可验证性:
VRF智能合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {VRFCoordinatorV2_5Mock} from "@chainlink/contracts/src/v0.8/vrf/mocks/VRFCoordinatorV2_5Mock.sol";
// 显式继承,这样 Hardhat 会强制生成名为 "MockVRF" 的 Artifact
contract MockVRF is VRFCoordinatorV2_5Mock {
constructor(
uint96 _baseFee,
uint96 _gasPriceUint,
int256 _weiPerUnitLink
) VRFCoordinatorV2_5Mock(_baseFee, _gasPriceUint, _weiPerUnitLink) {}
}
NFT盲盒智能合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
// 引入 OpenZeppelin 5.4.0 标准库
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
// 引入 Chainlink 1.5.0 异步随机数库
import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";
/**
* @title 盲盒 NFT 智能合约
* @notice 这是一个在本地环境运行的示例,结合了 OpenZeppelin 和 Chainlink VRF
*/
contract BlindBoxNFT is ERC721, VRFConsumerBaseV2Plus {
using Strings for uint256;
// 随机数相关状态变量
uint256 private _nextTokenId;
uint256 public constant MAX_SUPPLY = 1000;
string public baseTokenURI;
// VRF 配置参数 (本地测试使用模拟值)
uint256 public s_subscriptionId;
bytes32 public keyHash; // 本地测试不关心具体值
uint32 public callbackGasLimit = 500000;
uint16 public requestConfirmations = 3;
uint32 public numWords = 1;
// 映射:请求ID -> 铸造者地址
mapping(uint256 => address) public s_requestIdToSender;
// 映射:TokenID -> 稀有度权重 (示例)
mapping(uint256 => uint256) public tokenRarity;
event MintRequested(uint256 indexed requestId, address indexed requester);
event MetadataRevealed(uint256 indexed tokenId, uint256 rarity);
/**
* @param vrfCoordinator 本地测试时传入 Mock 合约的地址
* @param subscriptionId VRF 订阅 ID
*/
constructor(
address vrfCoordinator,
uint256 subscriptionId,
string memory _initialBaseURI
)
ERC721("Mystery Box NFT", "MBN")
VRFConsumerBaseV2Plus(vrfCoordinator)
{
s_subscriptionId = subscriptionId;
baseTokenURI = _initialBaseURI;
}
/**
* @notice 用户发起盲盒开启请求
*/
function requestMint() external returns (uint256 requestId) {
require(_nextTokenId < MAX_SUPPLY, "Sold out");
// 构造 Chainlink VRF 请求
requestId = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: keyHash,
subId: s_subscriptionId,
requestConfirmations: requestConfirmations,
callbackGasLimit: callbackGasLimit,
numWords: numWords,
extraArgs: VRFV2PlusClient._argsToBytes(
VRFV2PlusClient.ExtraArgsV1({nativePayment: false}) // 本地测试不支付原生币
)
})
);
s_requestIdToSender[requestId] = msg.sender;
emit MintRequested(requestId, msg.sender);
}
/**
* @notice Chainlink 预言机回调此函数
* @dev 这是逻辑真正执行的地方,包括确定稀有度和铸造代币
*/
function fulfillRandomWords(
uint256 requestId,
uint256[] calldata randomWords
) internal override {
address buyer = s_requestIdToSender[requestId];
uint256 tokenId = _nextTokenId++;
// 使用随机数决定稀有度 (例如 0-99 分布)
uint256 randomResult = randomWords[0] % 100;
tokenRarity[tokenId] = randomResult;
_safeMint(buyer, tokenId);
emit MetadataRevealed(tokenId, randomResult);
}
// 重写基础 URI 逻辑
function _baseURI() internal view override returns (string memory) {
return baseTokenURI;
}
function setBaseURI(string memory _newBaseURI) external onlyOwner {
baseTokenURI = _newBaseURI;
}
}
import { network, artifacts } from "hardhat";
import { parseEther, parseEventLogs, getAddress } from "viem";
async function main() {
console.log(`--- 开始在网络: ${network.name} 部署 ---`);
// 1. 初始化客户端
const { viem } = await network.connect();
const [deployer] = await viem.getWalletClients();
const publicClient = await viem.getPublicClient();
const deployerAddress = deployer.account.address;
// 2. 部署 MockVRF 合约
console.log("正在部署 MockVRF...");
const BASE_FEE = parseEther("0.1");
const GAS_PRICE_LINK = 1000000000n;
const WEI_PER_UNIT_LINK = 3000000000000000n;
const MockVRFArtifact = await artifacts.readArtifact("MockVRF");
const vrfHash = await deployer.deployContract({
abi: MockVRFArtifact.abi,
bytecode: MockVRFArtifact.bytecode as `0x${string}`,
args: [BASE_FEE, GAS_PRICE_LINK, WEI_PER_UNIT_LINK],
});
const vrfReceipt = await publicClient.waitForTransactionReceipt({ hash: vrfHash });
const vrfAddress = vrfReceipt.contractAddress!;
console.log("MockVRF 部署成功,地址:", vrfAddress);
// 3. 创建 VRF 订阅 (必须动态获取 subId)
console.log("正在创建 VRF 订阅...");
const createSubHash = await deployer.writeContract({
address: vrfAddress,
abi: MockVRFArtifact.abi,
functionName: "createSubscription",
});
const createSubReceipt = await publicClient.waitForTransactionReceipt({ hash: createSubHash });
// 从日志中解析订阅 ID (VRF V2.5 核心步骤)
const subLogs = parseEventLogs({
abi: MockVRFArtifact.abi,
eventName: "SubscriptionCreated",
logs: createSubReceipt.logs,
});
const subId = subLogs[0].args.subId;
console.log("动态获取的订阅 ID:", subId.toString());
// 4. 为订阅充值
console.log("正在为订阅充值...");
await deployer.writeContract({
address: vrfAddress,
abi: MockVRFArtifact.abi,
functionName: "fundSubscription",
args: [subId, parseEther("100")],
});
// 5. 部署 BlindBoxNFT 合约
console.log("正在部署 BlindBoxNFT...");
const ipfsjsonuri = "https://zygomorphic-magenta-bobolink.myfilebase.com/ipfs/QmQT8VpmWQVhUhoDCEK1mdHXaFaJ3KawkRxHm96GUhrXLB";
const NFTArtifact = await artifacts.readArtifact("BlindBoxNFT");
const nftHash = await deployer.deployContract({
abi: NFTArtifact.abi,
bytecode: NFTArtifact.bytecode as `0x${string}`,
args: [vrfAddress, subId, ipfsjsonuri],
});
const nftReceipt = await publicClient.waitForTransactionReceipt({ hash: nftHash });
const nftAddress = nftReceipt.contractAddress!;
console.log("BlindBoxNFT 部署成功,地址:", nftAddress);
// 6. 【关键】将 NFT 合约添加为消费者
console.log("正在授权 NFT 合约为 VRF 消费者...");
await deployer.writeContract({
address: vrfAddress,
abi: MockVRFArtifact.abi,
functionName: "addConsumer",
args: [subId, nftAddress],
});
console.log("--- 部署与配置任务全部完成 ---");
console.log({
network: network.name,
mockVRF: vrfAddress,
blindBoxNFT: nftAddress,
subscriptionId: subId.toString(),
});
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { parseEther, parseEventLogs } from 'viem';
import { network } from "hardhat";
describe("BlindBoxNFT 核心功能测试 (2026版)", async function () {
// 1. 部署环境准备
async function deployFixture() {
const { viem } = await network.connect();
const publicClient = await viem.getPublicClient();
const [owner, user] = await viem.getWalletClients();
// 部署 MockVRF (参数:基础费用, Gas单价, Link汇率)
const BASE_FEE = parseEther("0.1");
const GAS_PRICE_LINK = 1000000000n;
const WEI_PER_UNIT_LINK = 3000000000000000n;
const mockVRF = await viem.deployContract("MockVRF", [BASE_FEE, GAS_PRICE_LINK, WEI_PER_UNIT_LINK]);
// 2. 创建订阅并动态获取 subId
const createTx = await mockVRF.write.createSubscription();
const createReceipt = await publicClient.waitForTransactionReceipt({ hash: createTx });
// 从 MockVRF 的事件中解析 subId
const subLogs = parseEventLogs({
abi: mockVRF.abi,
eventName: 'SubscriptionCreated',
logs: createReceipt.logs,
});
const subId = subLogs[0].args.subId;
console.log("创建的订阅 ID:", subId.toString());
// 3. 充值订阅
await mockVRF.write.fundSubscription([subId, parseEther("100")]);
// 4. 部署 NFT 合约
const ipfsUri = "https://zygomorphic-magenta-bobolink.myfilebase.com/ipfs/QmQT8VpmWQVhUhoDCEK1mdHXaFaJ3KawkRxHm96GUhrXLB";
const nftContract = await viem.deployContract("BlindBoxNFT", [
mockVRF.address,
subId,
ipfsUri
]);
// 5. 【关键】授权 NFT 合约作为消费者
await mockVRF.write.addConsumer([subId, nftContract.address]);
return { mockVRF, nftContract, owner, user, publicClient, subId };
}
it("完整流程测试:请求随机数 -> 预言机回调 -> 盲盒开启成功", async function () {
const { mockVRF, nftContract, user, publicClient } = await deployFixture();
console.log("正在部署合约...", nftContract);
// --- Step 1: 用户发起开启盲盒请求 ---
console.log("正在请求开启盲盒...");
const requestTx = await nftContract.write.requestMint({ account: user.account });
const requestReceipt = await publicClient.waitForTransactionReceipt({ hash: requestTx });
// --- Step 2: 动态解析生成的 requestId ---
const mintLogs = parseEventLogs({
abi: nftContract.abi,
eventName: 'MintRequested',
logs: requestReceipt.logs,
});
const requestId = mintLogs[0].args.requestId;
console.log("获取到 Request ID:", requestId.toString());
// --- Step 3: 模拟 Chainlink 预言机节点回调 ---
// 这一步会触发 BlindBoxNFT 的 fulfillRandomWords
console.log("正在模拟预言机回调...");
const fulfillTx = await mockVRF.write.fulfillRandomWords([
requestId,
nftContract.address
]);
await publicClient.waitForTransactionReceipt({ hash: fulfillTx });
// --- Step 4: 结果验证 ---
// 1. 验证用户是否收到了 NFT (TokenID 0)
const nftOwner = await nftContract.read.ownerOf([0n]);
assert.equal(nftOwner.toLowerCase(), user.account.address.toLowerCase(), "NFT 铸造失败,拥有者不符");
// 2. 验证稀有度数据是否写入成功
const rarity = await nftContract.read.tokenRarity([0n]);
console.log("生成的 NFT 稀有度权重为:", rarity.toString());
assert.ok(rarity >= 0n && rarity < 100n, "稀有度计算逻辑异常");
// 3. 验证 URI 拼接
const tokenURI = await nftContract.read.tokenURI([0n]);
console.log("Token URI:", tokenURI);
assert.ok(tokenURI.endsWith("0"), "TokenURI 拼接错误");
console.log("测试全部通过!");
});
});
至此,本方案完整构建了 NFT 盲盒合约的开发闭环:基于 Solidity 语言,结合 OpenZeppelin 合约库与 Chainlink VRF(可验证随机函数)实现核心合约开发,通过 Hardhat+viem 完成合约的部署与测试,并利用 IPFS 实现 NFT 元数据的去中心化存储。其中,Chainlink VRF 从根本上解决了区块链链上随机数的安全问题,借助可验证的随机数机制,确保盲盒稀有度分配的公平性,从技术层面杜绝了盲盒稀有度被人为操纵的可能性。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!