Использование Chainlink VRF версии 2 в не очень специальном контракте NFT

… и развертывание его на Polygon с помощью Hardhat.

Как следует из названия, мы будем использовать версию 2 Chainlink VRF в смарт-контракте и посмотрим, как ее можно использовать для добавления верифицируемой случайности в смарт-контракт. В процессе работы мы создадим контракт NFT, а затем развернем его в Mumbai Testnet компании Polygon. В качестве вишенки на вершине мы посмотрим, как вывести список майнинга NFT из смарт-контракта на OpenSea. Наши изображения NFT не будут иметь таких ограничений и могут варьироваться от фотографии вашего профиля до редкого покемона или, возможно, мифического дьявольского фрукта, но мы не будем фокусироваться на части «прикрепления metadata.json», поскольку это выходит за рамки данной статьи.

В этой статье предполагается, что вы знакомы с Alchemy и имеете URL RPC Mumbai Testnet для нее или что-то эквивалентное этому. Кроме того, вам необходимо иметь ключ API Polygonscan, если вы хотите пройти часть проверки. По этим причинам не стесняйтесь просматривать ресурсы по этим темам, прежде чем начать.

Многое нужно охватить, а слов слишком мало. Итак, давайте приступим.

Chainlink VRF — это предложение от компании Chainlink, которое предоставляет смарт-контракту способ внести случайность в смарт-контракт. Как многие читатели, возможно, знают, до появления VRF для придания псевдослучайности использовался хэш блока и/или метка времени или что-то в этом роде.

Это был хороший первый шаг. Но у него был огромный очевидный недостаток. Его можно было предсказать. Из-за этого многие смарт-контракты эксплуатировались — особенно игры на ранних стадиях блокчейна, которые использовали NFT или даже беттинг. Поскольку человек мог эффективно распределять время своих вызовов, он мог в итоге получить гораздо большее состояние. Это не только было несправедливо, но и нанесло удар по всей экосистеме Web 3 — то, что должно было работать без доверия и быть «неэксплуатируемым» (по крайней мере, так это представлялось в первые дни), теперь таковым не являлось.

Chainlink предприняла шаг, чтобы продвинуть экосистему дальше. Конечно, были и другие претенденты, такие как Provable (позже переименованный в Oracalize), но VRF от Chainlink превзошел их всех и стал основным для внедрения внецепочечной «проверяемой» (отсюда и название Verifiable Random Function) случайности.

VRF — это оракул, который используется для одной и только одной цели — получения случайного числа (чисел). В первой версии смарт-контракт мог запрашивать у оракула только одно случайное число (uint256). Это было изменено во второй версии, где смарт-контракт мог запрашивать несколько и получать массив чисел uint256.

Верхний предел количества случайных чисел, которые могут быть запрошены за один раз, устанавливается контрактом VRF-координатора для конкретной цепочки. Например, в случае контракта координатора VRF на Rinkeby, Mumbai и других тестовых сетях, верхний предел составляет 500 на момент написания статьи.

Вы можете подумать, что это дает большую свободу действий для разработчиков. Но, как однажды сказал дядюшка Бутерин:

«С большим доступом к данным приходит большая стоимость газа».

В документации Chainlink указано, что на один номер uint256 уходит примерно 20 тысяч газа. На практике это может отличаться. И хотя это может не иметь значения на L2, таких как Polygon, или сайдчейнах и форках, таких как BSC и Avax, на мой взгляд, это отличный способ продемонстрировать ответственность в условиях ограниченного газа, как в Eth 1.0.

Более того, в отличие от версии 1, в Chainlink VRF v2 появился портал Subscription Manager. По сути, здесь вы создаете подписку и добавляете к ней средства. Вы получаете идентификатор подписки, который нужно использовать в смарт-контракте, как мы увидим ниже. Затем вы добавляете контракт потребителя (адрес контракта, который будет использовать Chainlink VRF) и все.

Честно говоря, весь этот процесс напоминает мне использование службы Chainlink Oracle для запроса API, где вам нужно setFulfillmentPermission().

Создание подписки

Прежде чем написать смарт-контракт, нам необходимо создать подписку на конкретную цепочку, к которой мы хотим получить доступ через Chainlink VRF. Эта функция в Chainlink VRF — способ объединить все смарт-контракты в одной консоли, и, на мой взгляд, это хороший шаг, сделанный в версии 2.

Перейдите на портал менеджера подписок VRF и нажмите на кнопку Create Subscription. Это создаст транзакцию, которая приведет к созданию подписки. На следующем этапе вам нужно добавить средства и пополнить подписку токенами LINK.

После подтверждения транзакции появится запрос на добавление потребителя. На этом мы пока остановимся. Теперь у вас должен быть идентификатор подписки. Он будет использоваться смарт-контрактом, поэтому обязательно запишите его.

Написание контракта

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

import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract OurNFTContract is ERC721, ERC721URIStorage, VRFConsumerBaseV2 {
    using Counters for Counters.Counter;
    VRFCoordinatorV2Interface COORDINATOR;


    uint64 subscriptionId;
    address vrfCoordinator = 0x7a1BaC17Ccc5b313516C5E16fb24f7659aA5ebed;
    bytes32 keyHash = 0x4b09e658ed251bcafeebbc69400383d49f344ace09b9576fe248bb02c003fe9f;
    uint32 callbackGasLimit = 200000;
    uint16 requestConfirmations = 3;
    uint32 numWords =  2;

    struct NFTStat {
        bool exists;
        address originalOwner;
        address currentOwner;
        string characterName;
        uint8 power;
        uint8 specialPower;
        uint16 cooldown;
        uint256 rarity;
    }

    mapping(uint256 => string) requestToCharacterName;
    mapping(uint256 => address) requestToSender;

    Counters.Counter private _tokenIdCounter;


    mapping(uint256 => NFTStat) private characterRegistry;

    event ReceivedRandomness( uint256 reqId, uint256 n1, uint256 n2);
    event RequestedRandomness( uint256 reqId, address invoker, string name);

    constructor(uint64 _subscriptionId) ERC721("OurNFTContract", "ONC") VRFConsumerBaseV2(vrfCoordinator) {
        COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
        subscriptionId = _subscriptionId;
    }

...

}
Вход в полноэкранный режим Выход из полноэкранного режима

Контракт не будет очень сложным. Изюминка будет заключаться в части майнинга. Мы будем использовать контракт ERC721 от Openzeppelin вместе с другими необходимыми контрактами из пакета и контрактов Chainlink. В этом примере мы будем наследовать от ERC721, ERC721URIStorage (используется для хранения URI соответствующих JSON метаданных NFT) и VRFConsumerBaseV2 (отсылка к тому, как нужно наследовать ChainlinkClient для запроса любого API с помощью Oracle Contract).

В строках, следующих за конструктором, мы определяем несколько важных переменных.

  1. COORDINATOR используется для использования VRF-координатора (это похоже на то, когда вы используете интерфейс IERC20 и передаете адрес контракта ERC20 для вызова определенной функции на этом контракте ERC20). Здесь COORDINATOR используется для взаимодействия с контрактом координатора в Mumbai Testnet.
  2. Параметры subscriptionId, vrfCoordinator, keyHash, callbackGasLimit, requestConfirmations, numWords используются для формулировки запроса, который мы пошлем контракту-координатору. Большинство из них можно инициализировать через конструктор, чтобы обеспечить более динамичный метод в контракте, но здесь мы инициализируем их фиксированными значениями. subscriptionId будет получен из Subscription Manager, в то время как vrfCoordinator и keyHash будут меняться в зависимости от цепочки. Следующие три параметра могут быть изменены в зависимости от потребностей и будут определять предел газа функции выполнения, количество подтверждений, которые должен ждать координатор, и количество слов для обратной отправки соответственно.
  3. Структура NFTStat используется для отслеживания первоначального владельца и текущих владельцев токена. Поскольку NFT будет размещен на OpenSea, его непосредственной функцией будет возможность его продажи. Если это так, то NFTStat — удобная вещь для отслеживания существования, текущих и первоначальных владельцев, имени персонажа, силы, специальной силы и статистики cooldown. characterRegistry сопоставляет каждый токен, отчеканенный из этого персонажа, с соответствующим статом.
  4. requestToCharacterName и requestToSender — удобные сопоставления. Чеканка не будет мгновенной. Когда пользователь вызовет функцию safeMint(), указав имя персонажа, функция отправит запрос контракту координатора. Контракт координатора будет отправлять обратно запрошенные случайные слова через методы fulfillRandomWords(). Поскольку у нас не будет доступа к предоставленному имени персонажа и адресу пользователя (msg.sender не будет работать внутри функции выполнения), мы сопоставим их с идентификатором запроса, который отправляется координатору, и позже получим доступ к нему внутри функции fulfillRandomWords().
  5. _tokenIdCounter отслеживает ID следующего майнируемого токена и используется при майнинге NFT.
  6. После завершения выполнения функции fulfillRandomWords() она выдаст случайные слова (числа здесь называются словами), которые были получены на определенный ID запроса, используя событие ReceivedRandomness.

Конструктор получает ID подписки, который мы получаем от менеджера подписки, и инициализирует координатора, передавая в VRFCoordinatorV2Interface адрес vrfCoordinator. На этом наша начальная часть завершена.

Далее, как показано ниже в функции safeMint(), мы передаем имя символа. Это один из атрибутов нашего NFT. Функция safeMint() возвращает идентификатор запроса, который отправляется в контракт координатора. Это поможет нам отслеживать запрос и от контракта координатора.

contract OurNFTContract is ERC721, ERC721URIStorage, VRFConsumerBaseV2 {

...

 // Assumes the subscription is funded sufficiently.
    function safeMint(string calldata name) public returns(uint256 requestId) {
        // Will revert if subscription is not set and funded.
        requestId = COORDINATOR.requestRandomWords(
        keyHash,
        subscriptionId,
        requestConfirmations,
        callbackGasLimit,
        numWords
        );
        requestToCharacterName[requestId] = name;
        requestToSender[requestId] = msg.sender;
        emit RequestedRandomness(requestId, msg.sender, name);
    }

    function fulfillRandomWords(
        uint256 requestId,
        uint256[] memory randomWords
    ) internal override {
        uint256 w1 = randomWords[0];
        uint256 w2 = randomWords[1];
        uint8 p = uint8(w2 % 10);
        uint8 sp = uint8(w2 % 100 /10);
        uint16 c = uint16(w2 % 1000 / 100);


        address sender = requestToSender[requestId];
        string memory name = requestToCharacterName[requestId];
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(sender, tokenId);

        characterRegistry[tokenId] = NFTStat(
            true,
            sender,
            sender,
            name,
            p,
            sp,
            c,
            w1 % 10000
        );
        emit ReceivedRandomness(requestId, w1, w2);

    }

...
}
Вход в полноэкранный режим Выход из полноэкранного режима

Одно из отличий, которое некоторые читатели могут заметить выше, это изменение requestID с bytes32 на uint256. Это один из нюансов v2 и, на мой взгляд, тонкий, но хороший шаг. Мы получаем requestId от COORDINATOR в вышеуказанной функции и затем используем его для сопоставления предоставленного имени персонажа и адреса майнера в сопоставлениях requestToCharacterName и requestToSender. Наконец, функция испускает событие RequestedRandomness.

Приведенная выше функция fulfillRandomness() вызывается Координатором и получает массив requestId и randomWords. Первый параметр — это тот же ID запроса, который мы получили, вызвав safeMint(), а второй содержит 2 случайных числа, которые мы запросили.

Здесь следует отметить, что хотя вы можете послать запрос Координатору на получение случайных чисел через VRF в любом методе контракта, метод fulfillRandomness с его текущим прототипом функции является обязательным. В противном случае вы получите от компилятора уведомление «Вам необходимо преобразовать ваш контракт в абстрактный». Это происходит потому, что VRFConsumerBaseV2, который мы унаследовали, делает его обязательным. Координатор будет вызывать эту функцию только при отправке результатов от службы VRF.

Поэтому здесь мы проводим фактическую чеканку NFT. Мы берем два случайных числа, отправленных обратно, а затем вычисляем мощность, специальную мощность и период охлаждения из второго случайного числа. Последние 4 цифры первого случайного числа используются в качестве редкости.

Как вы, возможно, знаете, использование десятичной системы счисления в Solidity не является нашей роскошью, поэтому последние 4 цифры преобразуются в число с двумя цифрами справа от десятичной точки. Мы получаем обратно адрес майнера и имя символа.

Затем мы увеличиваем tokenIdCounter и затем майним NFT с tokenId на этот адрес. В characterRegistry записываются статистические данные, которые мы хотим записать для этого NFT. Наконец, мы испускаем событие ReceivedRandomness, которое показывает нам два uint256 случайных числа, которые мы получили от VRF для нашего конкретного requestId.

Функция getCharacter() ниже является полезной функцией, которая предоставляет нам метаданные нашего отчеканенного NFT. Она будет использоваться сервером метаданных.

contract OurNFTContract is ERC721, ERC721URIStorage, VRFConsumerBaseV2 {

...

function getCharacter(uint256 tokenId)
    public
    view
    returns (
        address,
        address,
        string memory,
        uint8,
        uint8,
        uint16
    ) {
        require(characterRegistry[tokenId].exists == true, "Character does not Exist.");
        return (
            characterRegistry[tokenId].currentOwner,
            characterRegistry[tokenId].originalOwner,
            characterRegistry[tokenId].characterName,
            characterRegistry[tokenId].power,
            characterRegistry[tokenId].specialPower,
            characterRegistry[tokenId].cooldown
        );
    }

...

}
Вход в полноэкранный режим Выход из полноэкранного режима

Это еще не все. Помните, как мы хотели отслеживать статистику нашего НМТ по мере того, как он переходит из рук в руки? Можно предположить, что любой приличный смарт-контракт будет вызывать safeTransferFrom(), но некоторые разработчики могут не захотеть добавлять дополнительную защиту onERC721Received(), реализуя интерфейс IERC721Receiver на своем рынке (см. мою статью о рынке НМТ). Поэтому нам нужно переопределить функцию transferFrom() также, как показано ниже.

contract OurNFTContract is ERC721, ERC721URIStorage, VRFConsumerBaseV2 {

...

    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public virtual override {
        super.transferFrom(from, to, tokenId);
        characterRegistry[tokenId].currentOwner = to;
    }

    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public virtual override {
        super.safeTransferFrom(from, to, tokenId, "");
        characterRegistry[tokenId].currentOwner = to;
    }

    function setTokenURI(uint256 tokenId, string memory _tokenURI) public {
        require(
            _isApprovedOrOwner(_msgSender(), tokenId),
            "ERC721: transfer caller is not owner nor approved"
        );
        _setTokenURI(tokenId, _tokenURI);
    }

...

}
Вход в полноэкранный режим Выход из полноэкранного режима

Мы переопределяем функции transferFrom() и safeTransferFrom() в приведенном выше коде. Мы не хотим реализовывать свою новую пользовательскую логику. Мы просто хотим записать смену владельца в нашем characterRegistry, чтобы мы могли отслеживать текущего и первоначального владельца.

Вот почему мы используем ключевое слово super для вызова transferFrom() и safeTransferFrom() для использования оригинальной логики передачи и затем добавляем часть, которая изменяет статистику текущего владельца к адресу, на который отправляется NFT в characterRegistry в соответствующих методах.

Использование ключевого слова virtual в прототипе функции позволяет дальнейшее переопределение, если кто-то захочет унаследовать наш контракт и использовать его в качестве основы для своего контракта.

Наконец, мы определяем функции setTokenURI и tokenURI, как показано ниже. Первая функция setTokenURI будет использоваться для хранения ссылки на файл metadata.json. Этот файл будет сгенерирован после майнинга НФТ. Функция tokenURI будет вызываться OpenSea для получения URL файла metadata.json. Этот файл metadata.json может быть размещен на платформе по вашему выбору и может варьироваться от вашего собственного Google Drive (не рекомендуется) до AWS S3 или GCP storage bucket и IPFS (предпочтительно).

contract OurNFTContract is ERC721, ERC721URIStorage, VRFConsumerBaseV2 {

...

    function setTokenURI(uint256 tokenId, string memory _tokenURI) public {
        require(
            _isApprovedOrOwner(_msgSender(), tokenId),
            "ERC721: transfer caller is not owner nor approved"
        );
        _setTokenURI(tokenId, _tokenURI);
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string)
    {
        require( _isApprovedOrOwner(_msgSender(), tokenId),"Not Permitted");
        return super.tokenURI(tokenId);
    }

...

}
Вход в полноэкранный режим Выход из полноэкранного режима

На этом мы завершаем этап написания смарт-контракта.

Развертывание и проверка на Polygon Mumbai Testnet

Развертывание и верификация частей — это просто. Если вы используете Hardhat (я использую его в этом проекте), вы можете выполнить следующие шаги

У вас должен быть ключ API Polygonscan. Зарегистрируйтесь на Polygon Scan и создайте его. Создайте файл .env и добавьте следующие переменные окружения, как показано ниже.

POLYGONSCAN_API_KEY=<YOUR API POLYGONSCAN KEY>
MUMBAI_URL=https://polygon-mumbai.g.alchemy.com/v2/<YOUR API POLYGONSCAN KEY>
PRIVATE_KEY=<WALLET PRIVATE KEY HERE>
REPORT_GAS=true

Войти в полноэкранный режим Выйти из полноэкранного режима

Замените части на соответствующие ключи. Далее откройте файл hardhat.config.js и в разделе networks добавьте следующее.

    mumbai: {
      url: process.env.MUMBAI_URL || "",
      accounts: [process.env.PRIVATE_KEY]
    }
Войти в полноэкранный режим Выйти из полноэкранного режима

Если у вас нет свойств networks в module.exports, добавьте одно из них следующим образом.

  networks: {
    mumbai: {
      url: process.env.MUMBAI_URL || "",
      accounts: [process.env.PRIVATE_KEY]
    }
  },
Вход в полноэкранный режим Выйти из полноэкранного режима

Также необходимо добавить ключ Polygonscan в этот модуль.exports в свойство etherscan следующим образом:

  etherscan: {
    // apiKey: process.env.ETHERSCAN_API_KEY,
    apiKey: process.env.POLYGONSCAN_API_KEY,
  },
Войти в полноэкранный режим Выйти из полноэкранного режима

Этого должно быть достаточно.

Перейдите в папку scripts вашего проекта Hardhat и создайте файл с именем deploy.js и поместите в него следующий код.

// We require the Hardhat Runtime Environment explicitly here. This is optional
// but useful for running the script in a standalone fashion through `node <script>`.
//
// When running the script with `npx hardhat run <script>` you'll find the Hardhat
// Runtime Environment's members available in the global scope.
const hre = require("hardhat");

async function main() {
  // Hardhat always runs the compile task when running scripts with its command
  // line interface.
  //
  // If this script is run directly using `node` you may want to call compile
  // manually to make sure everything is compiled
  // await hre.run('compile');

  // We get the contract to deploy
  const OurNFTContract = await hre.ethers.getContractFactory("OurNFTContract");
  const ourNftContract = await OurNFTContract.deploy(414);

  await ourNftContract.deployed();

  console.log("Contract deployed to:", ourNftContract.address);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Вход в полноэкранный режим Выход из полноэкранного режима

Выше приведена модифицированная версия шаблонного кода, предоставляемого при инициализации проекта hardhat. В строке номер 17 мы получаем наш контракт (с именем OurNFTContract) с помощью метода getContractFactory из эфиров, предоставляемых hardhat. В следующей строке мы развертываем наш контракт с помощью OurNFTContract.deploy().

Не забудьте заменить 414 на ваш идентификатор подписки. Метод deploy в смарт-контракте принимает аргументы конструктора, которыми в нашем случае будет ID подписки, полученный от нашего менеджера подписки.

Запустите npx hardhat run scripts/deploy.js -network mumbai, чтобы развернуть смарт-контракт. После развертывания мы получим адрес контракта на нашей консоли, как показано ниже:

После развертывания контракта выполните команду npx hardhat verify -network mumbai <Your Deployed Contract Address> <Subscription ID> для проверки контракта. В случае, если он говорит, что отсутствует модуль hardhat-etherscan, просто запустите npm I @nomiclabs/hardhat-etherscan, а затем повторно выполните команду проверки.

Вы должны получить результат, похожий на изображение выше.

Добавление потребителя

После проверки контракта вернитесь на портал подписки VRF и добавьте этот контракт в качестве потребителя. Это важный шаг. По сути, он создает пособие для вашего смарт-контракта. Таким образом, когда safeMint() будет вызван и контракту-координатору будет отправлен запрос от нашего смарт-контракта, он не потерпит неудачу. Это хороший способ обезопасить свою подписку, поскольку в подписке будут токены mainnet LINK, и это предотвратит рассылку спама.

В качестве хорошей практики, если у вас есть потребитель, которым вы больше не пользуетесь, вы должны удалить его. Если кто-то непреднамеренно или намеренно использует вызов Chainlink VRF из этого потребительского контракта, то ваш баланс LINK на подписке будет использован для финансирования этого запроса.

Чеканка НФТ

Поскольку на данный момент мы развернули и проверили наш контракт, мы можем просто перейти на Polygonscan и попытаться майнить NFT. Я уже сделал это, и вот результаты.


На изображении выше показано событие, которое возникло при вызове функции safeMint(). Как вы видите, она послала запрос и выдала событие, содержащее requestId, начинающееся с 16 (это целое число uint256 и поэтому представлено в экспоненциальной форме), адрес кошелька вызывающего, начинающийся с 0xe61, и имя персонажа Raze.


На изображении выше показано событие, испускаемое методом fulfillRandomWords(), которое содержит requestId (совпадающий с предыдущим изображением) и два возвращаемых целых числа uint256.


На приведенном выше изображении показан NFT, который был отчеканен. На контракте был вызван метод getCharacter(), который показывает свойства символа.

Листинг на OpenSea

Чтобы разместить контракт NFT на OpenSea, просто перейдите по адресу testnets.opensea.io/get-listed/step-two, выберите тестнет Mumbai и введите адрес контракта. Нажмите на кнопку «Отправить», чтобы внести ваш контракт в список OpenSea. Все NFT, добытые на основе этого контракта, будут отображаться на OpenSea. Это будет не очень красиво, если вы не прикрепите metadata.json к вашему токену командой setTokenURI после майнинга.

Вот и все, друзья!

В этой статье мы затронули много тем. Мы вкратце обсудили службу Chainlink VRF. Мы создали подписку на Chainlink VRF с помощью менеджера подписок. Мы написали смарт-контракт NFT, который использовал Chainlink VRF v2 для предоставления случайных свойств NFT, а затем развернули смарт-контракт на Polygon’s Mumbai Testnet.

Код для этого был открыт на GitHub.

Я бы с удовольствием изучил, как протестировать такой смарт-контракт с помощью макета смарт-контракта VRF Coordinator, предоставленного Chainlink, но давайте пока отложим это до будущей статьи. Давайте закончим на этом.

Если вам понравилась статья, пожалуйста, не стесняйтесь делиться и нажимать на эти прекрасные кнопки, которые показывают любовь. Это помогает мне поддерживать мотивацию. Если у вас есть рекомендации по темам, свяжитесь со мной в Twitter или по электронной почте. А пока продолжайте разрабатывать потрясающие вещи на Web3 и WAGMI!

Оцените статью
Procodings.ru
Добавить комментарий