作者:Priyank Gupta. 编译:Cointime:QDD.
在本文中,你将学习如何构建和测试由Chainlink VRF服务驱动的智能合约。
在计算机上生成真正随机的数字是一个复杂的数学问题。然而,大多数编程语言今天要么具有原生支持生成随机数的功能,要么带有支持生成具有可接受确定性水平的随机数的库。
尽管在传统计算中生成可接受随机数是一个基本解决的问题,但在区块链上几乎不可能做到这一点。
是的,你可以使用keccak256函数将一堆区块和交易数据哈希在一起,但这只能算是临时的解决方法,绝对不能用于生产环境的代码。
如果你在Web3领域待了一段时间,你无疑听说过Chainlink。
Chainlink是一个去中心化的预言机网络,可以实时向区块链提供各种数据,以便智能合约开发人员可以访问可靠的链外数据,而不会损害合约的安全性。
在本文中,你将学习Chainlink的VRF服务,这是一个强大的工具,你可以安全地将随机化集成到智能合约中。
此外,我们将使用Foundry完成所有操作,这是市场上最新的智能合约开发框架之一。
我们将构建什么?
在本文中,我们将:
1. 使用Foundry建立开发环境,与Chainlink和OpenZeppelin的合约进行工作。
2. 将三个图像及其对应的JSON元数据上传到Pinata,这是一个用于IPFS的固定服务。
3. 设置一个ERC1155合约(不含VRF),用于从我们有限的图像集合中铸造多个代币。
4. 通过将Chainlink VRF集成到我们的合约中,随机化铸造功能。
5. 使用本地模拟合约测试我们的随机NFT铸造合约,现在由Chainlink的VRF提供支持。
6. 使用Forge部署和验证我们的合约到Mumbai测试网,使任何人都能从我们的合约中铸造NFT。
注:几个月前,Patrick Collins在推特上发布了一些看起来像健身照片的图片。
这些就是我们将在Mumbai测试网上将其转换为NFT的图片,获得了Patrick的授权。
我想,几乎没有什么比在Chainlink文章中看到Patrick更销售力了。这也是一个有趣的项目,有一个很酷的最终结果。
请跟随本文,到最后你将成为一位全新、闪亮的Patrick Collins NFT的所有者。
开始之前
本文将深入剖析Chainlink VRF,内容将会非常技术性。事实上,本文是我最初打算发布的内容的70%。
我建议你在第一次阅读时不要跟随文章操作,尤其是如果你之前没有使用过VRF。
相反,尝试理解我辛苦用简单的话解释的概念。
作为阅读到最后的奖励,你将能够铸造一张Patrick Collins的NFT :)
使用Foundry建立开发环境
Foundry是一个越来越受欢迎的智能合约开发框架。
这不是Foundry的入门课程。我建议你查阅Foundry的书籍,获得详细的参考资料,或者查看我为快速入门而创建的存储库。
安装Foundry后,请使用以下命令确保所有组件都是最新的:
foundryup
在一个新目录的新终端中,使用以下命令初始化一个新的Foundry项目:
forge init
我们在代码中使用了Chainlink和OpenZeppelin的智能合约库。要在你的Foundry项目中安装OpenZeppelin合约,请运行以下命令:
forge install Openzeppelin/openzeppelin-contracts
我们不需要安装包含Chainlink所有代码及其节点二进制文件的存储库。我们可以安装其精简版本,仅包含合约的版本,通过运行以下命令来安装:
forge install smartcontractkit/chainlink-brownie-contracts
默认情况下,Forge通过git子模块管理依赖项,我们不需要更改此行为(尽管我们可以)。你可以在lib目录中找到此项目的所有依赖项。
注:Foundry的设计是模块化的,它是四个不同的CLI工具的集合(目前为止)。它们是Forge、Cast、Anvil和Chisel。
在本文中,我主要会使用Forge和Anvil。
设置IPFS元数据
1. 前往Pinata网站,注册一个账户。对于我们的需求,我们只需要免费套餐即可。
2. 将你要创建代币的所有图像收集到一个文件夹中。我将Patrick的照片命名为1.png、2.png和3.png。我强烈建议使用简化的命名约定。
3. 将所有这些图像作为一个文件夹上传到Pinata。这意味着你将收到一个唯一的内容标识符(CID)。现在可以通过ipfs://CID/1.png来访问单个图像。我的图像文件夹可以通过此链接访问。
4. 接下来,我们将创建三个单独的JSON文件来存储与Opensea兼容的元数据。同样,我们将它们命名为1.json、2.json和3.json。你可以在Opensea的文档中详细了解Opensea的元数据标准。目前,1.json的内容如下所示。你可以通过此IPFS URL查看这三个JSON文件。
{
"name": "Patrick in the gym #1",
"description": "Call the mint function from this contract to get one of the three images from Patrick's gym photoshoot. This contract has a randomized mint function powered by Chainlink's VRF service.",
"image": "ipfs://QmQCRiKqzirEUBkjpoYJBKCBG4ynpknAjqH4Cp6rLTSTik/1.png",
"edition": 1,
"date": 1685971561,
"attributes": [
{
"trait_type": "Probability of getting this image.",
"value": "1%"
}
]
}
请注意概率值。这意味着我们希望铸造者只有1%的概率获得1.png。第二张和第三张图片的概率分别为33%和66%,并将通过Chainlink VRF来执行。
5. 最后,我们将这3个JSON文件作为一个文件夹上传到Pinata。这样,我们就可以获得一个CID来访问这3个文件。Opensea仅使用JSON元数据来显示NFT图像和相关属性。
一个通用的ERC1155合约
专业提示:在继续之前,我强烈建议你了解721和1155 NFT标准之间的区别。
在向智能合约添加随机性之前,让我们先设置一个通用的ERC1155智能合约。
1. 前往Openzeppelin Contracts Wizard,并使用以下配置设置一个基本的ERC1155合约。

注:我们的收藏的IPFS元数据可以通过ipfs://CID/{1、2或3}.json进行访问。这些数字也将是我们的图片的令牌ID。因此,我们将收藏的通用CID传递给智能合约,如下所示:
"ipfs://QmXN7twhiJF7pSttkvqxfok9o5p1QWJeCbwRTZvZ5RCzvz/{id}.json"
任何包含{id}的部分都将由诸如Opensea的客户端替换为令牌ID。
1. 在Foundry项目根目录下的src目录中创建一个名为nft.sol的文件。将代码粘贴到文件中。
2. 我做了一些修改。
1) 我删除了mintBatch函数,因为我不希望任何人从此合约中获得多个NFT。
2) 添加了一个名为name的公共字符串,并将其初始化为Patrick Through VRF。我们需要暴露一个名为name的公共字符串,以便Opensea能够为我们的收藏提供名称。在ERC721标准中,此变量会自动创建,但在ERC1155标准中不会。
3) 接下来,我创建了一个名为_minted的映射,用于跟踪已经铸造过NFT的所有地址。
4) 然后,我硬编码了mint函数的所有参数,除了tokenID。
5) 最后,我添加了一个简单的事件,每当我们的合约铸造一个NFT时会触发该事件。
此时,我们的合约如下所示。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol";
contract PatrickInTheGym is ERC1155, ERC1155Burnable, Ownable, ERC1155Supply {
mapping(address => bool) public _minted;
string public name = "Patrick Through VRF";
event TokenMinted(address indexed account, uint256 indexed id);
constructor() ERC1155("ipfs://QmXN7twhiJF7pSttkvqxfok9o5p1QWJeCbwRTZvZ5RCzvz/{id}.json") {}
function mint(uint256 id)
public
{
require(!_minted[msg.sender], "You can only mint once");
_minted[msg.sender] = true;
_mint(msg.sender, id, 1, "");
emit TokenMinted(msg.sender, id);
}
// The following functions are overrides required by Solidity.
function _beforeTokenTransfer(address operator, address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data)
internal
override(ERC1155, ERC1155Supply)
{
super._beforeTokenTransfer(operator, from, to, ids, amounts, data);
}
}
此合约允许任何人通过传入他们选择的tokenID调用合约的mint函数从我们的收藏中铸造一个NFT。
请牢记这一点。我将在下面进行扩展。
Foundry中的重映射
让我们编译合约,确保到目前为止一切顺利。要在Foundry中编译合约,请运行以下命令:
forge build
但是,Forge无法立即编译我们的合约,因为它无法理解我们的导入语句的格式。更准确地说,Forge不知道"@openzeppelin"是什么意思。
在终端中运行以下命令:
forge remappings > remappings.txt
此命令将在项目的根目录中创建一个名为remappings.txt的新文件,并使用Forge自动为你推断出的一些重映射填充该文件。现在,请确保将以下行添加到重映射文件中:
@openzeppelin/=lib/openzeppelin-contracts/
保存更改后,重新运行构建命令。这次,合约应该能够成功编译。
我们需要Chainlink VRF做什么?
我们的合约允许任何人通过传入他们选择的tokenID来铸造一个NFT。我们希望将Chainlink VRF集成到我们的合约中,以便mint函数能够随机铸造三张图片中的一张,并具有不同的概率水平。

这是我想到的解决方案。
1) 请求VRF在1和100之间(包括两端)生成一个随机数。
2) 如果返回的数字是100,就铸造一个tokenID设置为1的NFT。
3) 如果返回的数字可被3整除,就铸造一个tokenID设置为2的NFT。
4) 否则,铸造一个tokenID设置为3的NFT。
创建VRF订阅
Chainlink VRF目前为我们提供了两种请求随机性的方法:
1) 直接资助:该方法需要在使用合约中维护适当数量的LINK代币余额,以支付每次随机请求的费用。
2) 订阅:此方法创建一个特定的“订阅”,其中包含所需的LINK代币。可以根据所有者的意愿,使用此账户资助多个使用合约。
在本教程中,我们将选择订阅方法。
1) 前往faucets.chain.link,并向EOA请求一些LINK代币。
2) 前往vrf.chain.link,并在Mumbai测试网上创建一个新的订阅。我们很快将需要订阅ID。
3) 创建订阅后,请添加一些LINK代币。
4) 在部署智能合约后,我们将向我们的订阅添加一个使用合约。
VRF驱动的随机化
在src目录中创建一个名为nftVRF.sol的新文件。
准备好了。真正的事情现在开始。
首先,我们需要将一些Chainlink依赖项导入到我们的合约中。将以下导入添加到nftVRF.sol中:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol";
//Chainlink VRF imports
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
你可能需要像之前在重映射文件中一样配置Chainlink导入。
专业提示:Forge为我安装了Chainlink合约存储库的过时版本。我不明白为什么会发生这种情况。
如果你遇到同样的问题,请运行forge update lib/chainlink-brownie-contracts以更新此库。
1. VRFCoordinatorV2Interface是一个用于与所使用的链上部署的VRFCoordinator合约进行交互的接口。你可以在这里查看Mumbai测试网络的协调器合约。
在Solidity中,接口是一组标记为external的函数声明(而非定义)的集合。
当你的智能合约需要与另一个智能合约交互,并且你只需要了解另一个合约的函数签名时,接口非常有用。
要向Chainlink发送随机性请求,我们在此合约上调用requestRandomWords()函数。
你在vrf.chain.link上创建的订阅只是调用协调器合约上的createSubscription()函数的界面。
请查看接口代码以获得更好的理解。
2. VRFConsumerBaseV2是一个抽象合约。Chainlink协调器要求我们将此合约作为父合约继承,并实现一个名为fulfillRandomWords()的函数。
协调器在生成随机值后会调用fulfillRandomWords()函数。
注意:抽象合约与普通合约类似,但它并未完全实现。它可能有一些没有函数体(即未实现)的函数。至少有一个函数没有实现的合约被视为抽象合约。
在导入所有依赖项的同时,声明主要合约:
contract PatrickInTheGym is ERC1155,
ERC1155Burnable,
Ownable,
ERC1155Supply,
VRFConsumerBaseV2
{
}
你将立即看到整个代码都变成了红色错误。这是因为VRFConsumerBaseV2合约需要一个构造函数来初始化,而我们的合约在提供所有基本合约的构造函数参数之前无法编译。
让我们开始配置我们需要从Coordinator合约调用requestRandomWords()函数的所有变量。查看所有这些变量:
//Chainlink Variables
VRFCoordinatorV2Interface private immutable CoordinatorInterface;
uint64 private immutable _subscriptionId;
address private immutable _vrfCoordinatorV2Address;
bytes32 keyHash = 0x4b09e658ed251bcafeebbc69400383d49f344ace09b9576fe248bb02c003fe9f;
uint32 callbackGasLimit = 200000;
uint16 blockConfirmations = 10;
uint32 numWords = 1;
CoordinatorInterface只是VRFCoordinatorV2Interface的一个新实例。该实例将在构造函数中初始化。
订阅ID:用于保存LINK并为合约的随机性请求提供资金的订阅的唯一ID。
此值必须在构造函数中进行初始化。
协调器V2地址:特定链上的Chainlink VRF协调器合约的地址。
密钥散列/Gas通道:此哈希值表示你愿意支付的最大燃气价格。Chainlink VRF支持的主网通常有多个支持的“燃气通道”;然而,Mumbai测试网只有一个。
你可以在Chainlink的文档中查看该值。
回调Gas限制:该值指定协调器合约在调用fulfillRandomWords()以返回随机值时必须使用的最大燃气量。
块确认:该值设置协调器在发送我们的随机值之前等待的块数。该值越大,生成的随机数越安全。每个网络的最小和最大块确认在Chainlink的文档中指定。
字数:每个请求要获取的随机值数量。我们将每个请求调用一次。
在合约声明的下面添加这些值。
一个概念性的偏离
让我们花点时间仔细研究新的工作流程。与通用的1155合约相比,mint函数将经历重大变化,理解这些差异非常重要。
在新合约中将发生以下情况:
1. 用户在主合约上调用名为mint()的函数,但这不会直接为他们铸造一个NFT。相反,这个mint函数将在内部调用requestRandomWords(),告诉VRF Coordinator:
“嘿,伙计,我想要一个随机数。请等待10个块,然后给我一个随机数”。
2. 调用此函数会触发名为RandomWordsRequested的事件,该事件来自Coordinator合约;一个链下的VRF节点接收到该事件。
3. VRF节点将等待十个块(如我们所指定)后将随机数返回给Coordinator合约。
4. 然后,Coordinator将从我们的合约中调用fulfillRandomWords()函数,并执行包含的任何逻辑。
我们将在此函数内部铸造NFT。
注意:让我重复一遍。用户只会调用mint()函数,这将触发requestRandomWords()函数。
协调器合约调用fulfillRandomWords()函数,这使其成为“回调函数”。
看一下这个简略的图表。在编写其余代码时,这将变得更清晰。

结束合约
最后,让我们设置我们的构造函数。我们需要做两件事:
1. 将我们的订阅ID作为构造函数参数传递给主合约。
2. 通过传递协调器地址来初始化VRFConsumerBaseV2合约的构造函数。
我们的构造函数将如下所示:
constructor(uint64 subscriptionId, address vrfCoordinatorV2Address)
ERC1155("ipfs://QmXN7twhiJF7pSttkvqxfok9o5p1QWJeCbwRTZvZ5RCzvz/{id}.json")
VRFConsumerBaseV2(vrfCoordinatorV2Address)
{
_subscriptionId = subscriptionId;
_vrfCoordinatorV2Address= vrfCoordinatorV2Address;
CoordinatorInterface = VRFCoordinatorV2Interface(vrfCoordinatorV2Address);
}
注意:我还创建了名为CoordinatorInterface的Coordinator合约的新实例。这将使使用接口调用函数变得更简单。
接下来,我将声明合约所需的一些状态变量。现在,我们的合约看起来像这样:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol";
//Chainlink VRF imports
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
contract PatrickInTheGym is
ERC1155,
ERC1155Burnable,
Ownable,
ERC1155Supply,
VRFConsumerBaseV2
{
//Contract Variables and events
mapping(address => bool) public _minted;
string public name = "Patrick Through VRF";
mapping(uint256 => address) public _requestIdToMinter;
event RequestInitalized(uint256 indexed requestId, address indexed minter);
event NftMinted(uint256 indexed tokenID, address indexed minter);
//Chainlink Variables
VRFCoordinatorV2Interface private immutable CoordinatorInterface;
uint64 private immutable _subscriptionId;
address private immutable _vrfCoordinatorV2Address;
bytes32 keyHash = 0x4b09e658ed251bcafeebbc69400383d49f344ace09b9576fe248bb02c003fe9f;
uint32 callbackGasLimit = 200000;
uint16 blockConfirmations = 10;
uint32 numWords = 1;
constructor(
uint64 subscriptionId,
address vrfCoordinatorV2Address
)
ERC1155("ipfs://QmXN7twhiJF7pSttkvqxfok9o5p1QWJeCbwRTZvZ5RCzvz/{id}.json")
VRFConsumerBaseV2(vrfCoordinatorV2Address)
{
_subscriptionId = subscriptionId;
_vrfCoordinatorV2Address= vrfCoordinatorV2Address;
CoordinatorInterface = VRFCoordinatorV2Interface(vrfCoordinatorV2Address);
}
function mint() public returns (uint256 requestId) {
require(!_minted[msg.sender], "You can only mint once");
//Calling requestRandomWords from the coordinator contract
requestId = CoordinatorInterface.requestRandomWords(
keyHash,
_subscriptionId,
blockConfirmations,
callbackGasLimit,
numWords
);
// map the caller to their respective requestIDs.
_requestIdToMinter[requestId] = msg.sender;
// emit an event
emit RequestInitalized(requestId, msg.sender);
}
这个函数只能被尚未拥有我们合约中的 NFT 的地址调用。
在这里,我们调用了协调器合约的 requestRandomWords() 函数。该函数返回一个 uint256 类型的唯一变量,我们将其存储为 requestID。
调用 requestRandomWords() 函数将自动启动链下的随机数生成过程。
注意:为什么我们使用 _requestIdToMinter 映射?
因为全球可能会有很多人同时调用 mint 函数。在这种情况下,将 requestID 分配给铸币者非常有帮助,因为我们可以跟踪到到达的结果。
请创建一个名为 fulfillRandomWords() 的函数,将其放在 mint() 函数的下方,就像这样:
function fulfillRandomWords(
uint256 requestId,
uint256[] memory randomWords
) internal override {
// get the minter address
address minter = _requestIdToMinter[requestId];
// To generate a random number between 1 and 100 inclusive
uint256 randomNumber = (randomWords[0] % 100) + 1;
uint256 tokenId;
//manipulate the random number to get the tokenId with a variable probability
if(randomNumber == 100){
tokenId = 1;
} else if(randomNumber % 3 == 0) {
tokenId = 2;
} else {
tokenId = 3;
}
// Updating the mapping
_minted[minter] = true;
// Finally mint the token
_mint(minter, tokenId, 1, "");
// emit an event
emit NftMinted(tokenId, minter);
}
当协调器合约希望返回成功随机请求的结果时,上面的代码片段将被调用。
每当触发此函数时,它将向从我们合约调用 mint() 函数的人铸造一个随机的 NFT。
最后,从 ERC1155 标准中添加 _beforeTokenTransfer 函数。
// The following functions are overrides required by Solidity.
function _beforeTokenTransfer(
address operator,
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) internal override(ERC1155, ERC1155Supply) {
super._beforeTokenTransfer(operator, from, to, ids, amounts, data);
}
}
这就是最终的合约代码:
//Contract Variables and events
mapping(address => bool) public _minted;
string public name = "Patrick Through VRF";
mapping(uint256 => address) public _requestIdToMinter;
event RequestInitalized(uint256 indexed requestId, address indexed minter);
event NftMinted(uint256 indexed tokenID, address indexed minter);
//Chainlink Variables
VRFCoordinatorV2Interface private immutable CoordinatorInterface;
uint64 private immutable _subscriptionId;
address private immutable _vrfCoordinatorV2Address;
bytes32 keyHash = 0x4b09e658ed251bcafeebbc69400383d49f344ace09b9576fe248bb02c003fe9f;
uint32 callbackGasLimit = 200000;
uint16 blockConfirmations = 10;
uint32 numWords = 1;
constructor(
uint64 subscriptionId,
address vrfCoordinatorV2Address
)
ERC1155("ipfs://QmXN7twhiJF7pSttkvqxfok9o5p1QWJeCbwRTZvZ5RCzvz/{id}.json")
VRFConsumerBaseV2(vrfCoordinatorV2Address)
{
_subscriptionId = subscriptionId;
_vrfCoordinatorV2Address= vrfCoordinatorV2Address;
CoordinatorInterface = VRFCoordinatorV2Interface(vrfCoordinatorV2Address);
}
function mint() public returns (uint256 requestId) {
require(!_minted[msg.sender], "You can only mint once");
//Calling requestRandomWords from the coordinator contract
requestId = CoordinatorInterface.requestRandomWords(
keyHash,
_subscriptionId,
blockConfirmations,
callbackGasLimit,
numWords
);
// map the caller to their respective requestIDs.
_requestIdToMinter[requestId] = msg.sender;
// emit an event
emit RequestInitalized(requestId, msg.sender);
}
function fulfillRandomWords(
uint256 requestId,
uint256[] memory randomWords
) internal override {
// get the minter address
address minter = _requestIdToMinter[requestId];
// To generate a random number between 1 and 100 inclusive
uint256 randomNumber = (randomWords[0] % 100) + 1;
uint256 tokenId;
//manipulate the random number to get the tokenId with a variable probability
if(randomNumber == 100){
tokenId = 1;
} else if(randomNumber % 3 == 0) {
tokenId = 2;
} else {
tokenId = 3;
}
// Updating the mapping
_minted[minter] = true;
// Finally mint the token
_mint(minter, tokenId, 1, "");
// emit an event
emit NftMinted(tokenId, minter);
}
// The following functions are overrides required by Solidity.
function _beforeTokenTransfer(
address operator,
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) internal override(ERC1155, ERC1155Supply) {
super._beforeTokenTransfer(operator, from, to, ids, amounts, data);
}
}
运行forge build命令检查是否存在任何即时错误。合约应该能够成功编译。
专业提示:可能会出现需要更改keyHash、numWords或blockConfirmations等值的情况。建议通过一个公共函数进行访问,并使用onlyOwner修饰符保护,以便根据需要配置这些值。
让我们继续使用Forge的测试工具对合约进行测试。
使用模拟合约进行本地测试
Chainlink为我们提供了用于测试目的的VRFCoordinatorV2Mock合约。它模拟了实际的VRFCoordinatorV2合约的行为,使我们能够在本地测试基于VRF的合约。
在“test”目录中创建一个名为vrfTest1.t.sol的文件。
设置所需的导入项:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/nftVRF.sol";
import "../lib/chainlink-brownie-contracts/contracts/src/v0.8/mocks/VRFCoordinatorV2Mock.sol";
像这样初始化测试合约:
contract PatrickInTheGymTest is Test {
}
为测试合约声明一些状态变量:
//Creating instances of the main contract
//and the mock contract
PatrickInTheGym public patrickInTheGym;
VRFCoordinatorV2Mock public mock;
//To keep track of the number of NFTs
//of each tokenID
mapping(uint256 => uint256) supplytracker;
//This is a shorthand used to represent the full address
// address(1) == 0x0000000000000000000000000000000000000001
address alpha = address(1);
有关在Foundry中进行测试的一些概念:
1. 如果你的Solidity文件目录中的任何函数名称以字符串“test”开头,Forge将将其视为测试函数。因此,testVRF()是一个有效的测试函数名称,但VRFtest()不是。
2. Forge默认在新的EVM实例中运行所有测试函数。这意味着由于一个测试函数的状态更改对下一个测试函数的结果没有影响。
3. setup()是可以包含在Foundry测试套件中的特殊函数。每次在运行新的测试函数之前,Forge都会执行此函数。
4. 我们将使用setup()函数来“设置”我们需要测试的区块链状态。
在状态变量下方定义一个名为setup()的新函数:
function setUp() public {
//Can ignore this. Just sets some base values
// In real-world scenarios, you won't be deciding the
//constructor values of the coordinator contract anyways
mock = new VRFCoordinatorV2Mock(100000000000000000, 1000000000);
//Creating a new subscription through account 0x1
//Prank cheatcode explained below the code snippet
vm.prank(alpha);
uint64 subId = mock.createSubscription();
//funding the subscription with 1000 LINK
mock.fundSubscription(subId, 1000000000000000000000);
//Creating a new instance of the main consumer contract
patrickInTheGym = new PatrickInTheGym(subId, address(mock));
//Adding the consumer contract to the subscription
//Only owner of subscription can add consumers
vm.prank(alpha);
mock.addConsumer(subId, address(patrickInTheGym));
}
注意:Prank作弊代码是一种方便的方式,可以“模拟”从特定地址调用区块链的方法。
在Prank作弊代码下方的调用将使用指定的地址作为msg.sender进行执行。
现在最后,创建一个名为testRandomness()的函数,如下所示:
function testRandomness() public {
for (uint i = 1; i <= 1000; i++) {
//Creating a random address using the
//variable {i}
//Useful to call the mint function from a 100
//different addresses
address addr = address(bytes20(uint160(i)));
vm.prank(addr);
uint requestID = patrickInTheGym.mint();
//Have to impersonate the VRFCoordinatorV2Mock contract
//since only the VRFCoordinatorV2Mock contract
//can call the fulfillRandomWords function
vm.prank(address(mock));
mock.fulfillRandomWords(requestID,address(patrickInTheGym));
}
//Calling the total supply function on all tokenIDs
//to get a final tally, before logging the values.
supplytracker[1] = patrickInTheGym.totalSupply(1);
supplytracker[2] = patrickInTheGym.totalSupply(2);
supplytracker[3] = patrickInTheGym.totalSupply(3);
console2.log("Supply with tokenID 1 is " , supplytracker[1]);
console2.log("Supply with tokenID 2 is " , supplytracker[2]);
console2.log("Supply with tokenID 3 is " , supplytracker[3]);
}
你可以使用以下命令运行测试文件:
forge test --match-path test/vrfTest1.t.sol -vvvvv
Forge将在终端返回结果。你将看到在for循环运行1000次后的输出。
你可以看到百分比与我们期望的一致。
请注意,“test”函数只有在满足所有必需条件时才会通过。我们没有设置任何导致测试函数失败的条件。
这个测试阶段是一种粗略检查我们的VRF随机性是否有效的方法。
遗憾的是:我本打算在不变量测试部分包含一个完整的章节。想法是利用foundry-chainlink-toolkit编写一个更全面的测试套件。
该项目目的是为了与Forge一起快速启动本地Chainlink节点。但是,尽管我尽了最大努力,但我无法设置节点。
我将尽快撰写本教程的第二部分。
部署到Mumbai
由于我无法为你提供一个带有本地Chainlink节点的完整的不变量测试教程,让我们继续部署和验证我们的合约。
在Foundry项目的根目录中,创建一个.env文件。将.env文件填充以下值:
RPC_URL=
PRIVATE_KEY=
POLYGONSCAN_API_KEY=
你可以从Alchemy、Chainstack或Quicknode等服务获取RPC URL。你也可以使用公共的RPC URL,如果你愿意的话。
使用在Mumbai测试网上具有一些MATIC代币的私钥。
获取Polygonscan API密钥。主网资源管理器的密钥在Mumbai上也可以使用。
填充完所有值后,保存.env文件。在终端中运行以下命令将这些环境变量导入终端:
source .env
我们将创建一个部署脚本来将合约部署到区块链上。
在脚本文件夹中创建一个名为nftVRF.s.sol的文件。设置所需的导入项:
pragma solidity ^0.8.4;
import "forge-std/Script.sol";
import "../src/nftVRF.sol";
创建一个新的合约,继承Forge提供的Scripts.sol:
contract PatrickInTheGymDeploy is Script {
}
将合约填充如下:
function run() public {
//Forge可以直接从.env文件中读取私钥
uint PrivateKey = vm.envUint("PRIVATE_KEY");
//这个作弊代码将在链上广播包含的所有事务
vm.startBroadcast(PrivateKey);
//你的订阅ID将不同
//但Coordinator地址将保持不变
//除非你不是部署到Mumbai
PatrickInTheGym patrickInTheGym = new PatrickInTheGym(5125, 0x7a1BaC17Ccc5b313516C5E16fb24f7659aA5ebed);
vm.stopBroadcast();
}
脚本默认情况下在run()函数内执行。
我们可以通过在广播(Broadcast)作弊代码中创建新的实例来将合约部署到区块链上。
保存文件。
要执行脚本,请在终端中运行以下命令:
forge script script/nftVRF.s.sol:PatrickInTheGymDeploy \
--rpc-url $RPC_URL \
--broadcast -vvvv
Forge将在终端返回一个合约地址。在Mumbai Explorer中打开该地址。
要验证此合约,请运行以下命令:
注意:我在toml文件中将编译器版本配置为0.8.17。其他值是默认值。
forge verify-contract <YOUR_SMART_CONTRACT_ADDRESS> \
--chain-id 80001 \
--num-of-optimizations 200 \
--watch --compiler-version v0.8.17+commit.8df45f5f \
--constructor-args $(cast abi-encode "constructor(uint64, address)" 5125 0x7a1BaC17Ccc5b313516C5E16fb24f7659aA5ebed)
src/nftVRF.sol:PatrickInTheGym \
--etherscan-api-key $POLYGONSCAN_API_KEY
要获得自己的随机Patrick,请转到链接并调用mint函数。你会注意到,你无法选择令牌ID,这正是我们的目的所在。
你可以在Opensea上查看整个收藏。
截至目前,没有人能够铸造令牌ID为1的NFT。

关于验证的坦白:我以前用Forge在链上验证过合约很多次,但无论我怎么努力,这次我无法验证合约。
我不知道我做错了什么。
我通过REMIX将完全相同的合约部署到了Mumbai,并从那里验证了它。
如果你能找出我做错了什么,请随时联系我。
所有评论