WEB3DEV

Cover image for Aleatoriedade imprevisível para cunhar NFT com a VRF da Chainlink
Panegali
Panegali

Posted on

Aleatoriedade imprevisível para cunhar NFT com a VRF da Chainlink

Tabela de Conteúdos

  • Introdução.

  • Implementação

Obter aleatoriedade da Chainlink
Encontrar um ID de token aleatório
Escrever a função Mint

  • Testes de escrita

Mocking a VRF da Chainlink
Ciclo de Teste de Solicitação e Recebimento de Aleatoriedade
Teste de Estágio

Introdução

Blockchains como Ethereum e Bitcoin são transparentes e determinísticas. Isso significa que gerar aleatoriedade imprevisível à prova de adulteração é uma tarefa desafiadora. No entanto, continua sendo um aspecto crucial em muitos aplicativos de blockchain, como determinar funções de governança em DAOs, giveaways (sorteios), jogos play-to-earn (jogar para ganhar), gerando traços aleatórios de NFT, distribuindo ativos de forma justa e muito mais.

Uma maneira simples de conseguir isso seria gerar aleatoriedade usando uma variável disponível globalmente como block.difficulty ou block.timestamp como fonte de entropia. Mas essa forma de gerar aleatoriedade nunca é uma boa ideia se você estiver construindo algo sério. Por quê? Porquê estas podem ser manipuladas pelos mineradores.

Então, a solução seria usar uma rede de oráculos que pode gerar aleatoriedade fora da cadeia (off-chain), mas fornecer uma prova criptográfica na cadeia (on-chain). E a VRF (função aleatória verificável) da Chainlink faz exatamente isto. Portanto, gostaria de orientá-lo em uma implementação simples de VRF em um contrato de cunhagem de NFT para distribuir NFTs raros de maneira justa entre os cunhadores. A maioria dos projetos de arte NFT gera características NFT por meio de computação fora da cadeia e armazena imagens NFT em um armazenamento distribuído como o IPFS. No entanto, isto representa um risco dos mineradores roubarem NFTs raros, dificultando assim a distribuição justa de tokens. Portanto, para resolver esse problema, podemos escrever um contrato inteligente que permitirá que os mineradores obtenham um ID de token aleatório usando a VRF da Chainlink.

Eu vou escrever o contrato de cunhagem e testes usando hardhat e typescript. Além disso, vou testar o contrato inteligente usando mocks chainlink (mocks são objetos que simulam o comportamento de objetos reais de forma controlada). Aqui está um link para o repositório do GitHub contendo os contratos, testes e scripts de implantação.

Implementação

Obtendo a aleatoriedade da Chainlink

Cunhar um ID de token aleatório primeiro requer um valor aleatório da Chainlink. Então, vamos dar uma olhada rápida em como fazer isso. Existem dois contratos que serão importantes para obter a aleatoriedade. O primeiro é VRFCoordinatorV2. Como o nome sugere, este contrato é responsável por coordenar todo o processo de solicitação e recebimento da aleatoriedade. Você pode ler mais informações sobre o ciclo de solicitação e recebimento de dados aqui. E o outro é VRFConsumerBaseV2.sol. Para solicitar um valor aleatório da chainlink, seu contrato (contrato que consome aleatoriedade) deve herdar esse contrato e implementar a função fulfillRandomWords. O construtor (constructor) do VRFConsumerBaseV2 usa o endereço vrfCoordinator como um argumento. Você pode encontrar as redes suportadas e os endereços do VRFCoordinator aqui.

import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";

contract VRFMinting is VRFConsumberBaseV2 {

    event RandomnessRequestFulfilled(
      uint256 requestId, 
      uint256[] randomWords
    )

    constructor(
      address _vrfCoordinator
    ) VRFConsumerBaseV2(_vrfCoordinator){}

    function fulfillRandomWords(
      uint256 _requestId, 
      uint256[] memory _randomWords
    ) internal override {
      emit RandomnessRequestFulfilled(_requestId, _randomWords);
    }

}
Enter fullscreen mode Exit fullscreen mode

Aqui, implementei a função fulfillRandomWords definida em VRFConsumerBaseV2. Esta é a função de retorno de chamada da VRF, que é chamada quando a solicitação de aleatoriedade é atendida. Isso só pode ser chamado pelo contrato VRFCoordinatorV2 pré-definido no contrato consumer. Como visto aqui, se algum outro contrato tentar chamar isso, ele será revertido com o erro OnlyCoordinatorCanFulfill.

Para enviar a solicitação de aleatoriedade, o contrato consumer deve chamar a função reqeustRandomWords no VRFCoordinatorV2 com os argumentos abaixo.

function requestRandomness() external returns (RequestStatus memory) {
   // Será revertida se a assinatura não for definida e financiada.
   uint256 requestId = vrfCoordinator.requestRandomWords(
       keyHash,
       subscriptionId,
       requestConfirmations,
       callbackGasLimit,
       numWords
   )
   emit RandomnessRequestSent(requestId, numWords);
}
Enter fullscreen mode Exit fullscreen mode
  • keyHash: usado para indicar o limite de preço do gás para a solicitação de aleatoriedade. Encontre as chaves de hashes disponíveis aqui.
  • subscriptionId: você precisa criar uma assinatura e financiá-la com alguns tokens testnet LINK para obter um ID de assinatura.
  • requestConfirmations: O número de confirmações de bloco que o serviço de VRF aguardará para responder. As confirmações de Máximo e Mínimo podem ser encontradas aqui.
  • callbackGasLimit: o contrato VRFCoordinator usará essa quantidade de gás ao chamar a função callback (rawFulfillRandomWords) no contrato vrfConsumerBaseV2. (ver callWithExactGas)

VRFCoordinatorV2.sol#L567
bytes memory resp = abi.encodeWithSelector(v.rawFulfillRandomWords.selector, requestId, randomWords)
VRFCoordinatorV2.sol#L575
bool success = callWithExactGas(rc.callbackGasLimit, rc.sender,resp);

  • numWords: O número de números aleatórios a serem solicitados. Os valores máximos podem ser encontrados aqui.

Você também pode direcionar tokens LINK de fundos para solicitar aleatoriedade em vez de gerenciar assinaturas. Você pode ler mais sobre isso aqui.

Encontrar um ID de token aleatório

O número aleatório que estamos obtendo da Chainlink é um Integer (inteiro) de 32 bytes. O fornecimento máximo de token desse contrato específico é 3333. O índice inicial do token é 0. Portanto, para obter um ID de token aleatório, podemos dividir o integer aleatório de 32 bytes por 3333 e obter o restante. uint16 randomId = randomInt % 3333

Portanto, para evitar colisões entre IDs de tokens aleatórios, precisamos verificar se esse ID já foi cunhado ou não.

 function getRandomTokenId(address requester)
        public
        view
        returns (uint256 randomTokenId)
    {
        RequestStatus memory requestStatus = getRandomnessRequestState(requester);
        require(requestStatus.fulfilled, "Pedido não atendido");
        uint256 randomWord = requestStatus.randomWords[0];
        uint256 randomTokenIdFirst = randomWord % maxSupply; // 3333 não é um id de token
        uint256 stopValue = randomTokenIdFirst;
        if (_exists(randomTokenIdFirst)) {
            while (
                _exists(randomTokenIdFirst) &&
                randomTokenIdFirst < maxSupply - 1
            ) {
                randomTokenIdFirst = (randomTokenIdFirst + 1);
            }
            if (_exists(randomTokenIdFirst)) {
                // randomTokenIdFirst deve ser 3332 aqui
                randomTokenIdFirst = 0;
                while (
                    _exists(randomTokenIdFirst) &&
                    randomTokenIdFirst < stopValue
                ) {
                    randomTokenIdFirst = (randomTokenIdFirst + 1);
                }
                if (_exists(randomTokenIdFirst)) {
                    revert NoTokenIdAvailable();
                } else if (!_exists(randomTokenIdFirst)) {
                    randomTokenId = randomTokenIdFirst;
                }
            } else if (!_exists(randomTokenIdFirst)) {
                randomTokenId = randomTokenIdFirst;
            }
        } else if (!_exists(randomTokenIdFirst)) {
            randomTokenId = randomTokenIdFirst;
        }
    }
Enter fullscreen mode Exit fullscreen mode

A função acima obterá um ID de token aleatório e, se o ID já estiver cunhado, ele procurará o próximo ID de token disponível. Você pode ver que adicionei uma instrução require para verificar se a solicitação de aleatoriedade foi atendida. Dê uma olhada no código aqui.

Escrevendo a função Mint

function mint(uint256 _mintAmount)
        public
        payable
        mintCompliance(_mintAmount)
        mintPriceCompliance(_mintAmount)
    {
        require(
            saleStatus == SaleStatus.PUBLIC_MINT,
            "A venda pública não está habilitada!"
        );

        require(
            (totalPublicMintByAddress[_msgSender()] + _mintAmount) <=
                maxMintAmountPerWalletPublic,
            "Cunhagem pública máxima excedida!"
        );

        uint256 tokenId = getRandomTokenId(_msgSender());

        totalPublicMintByAddress[_msgSender()] += _mintAmount;
        _safeMint(_msgSender(), tokenId);
    }
Enter fullscreen mode Exit fullscreen mode

Eu usei a extensão ERC721 Enumerable do OpenZeppelin aqui. A função _safeMint recebe dois argumentos. O endereço para o qual o NFT deve ser transferido e o ID do token. Como você pode ver, tudo o que resta fazer agora é passar o ID do token aleatório gerado para a função mint.

Testes de Redação

Mocking VRFCoordinatorV2 da Chainlink

Como eu disse anteriormente, todas as requisições de aleatoriedade são coordenadas pelo contrato VRFCoordinator. Já existem contratos coordinator implantados na rede principal e nas redes de teste Ethereum. Mas eu queria testar todo esse processo em um ambiente local isolado. Portanto, implantei este contrato mock no repositório GitHub da chainlink para imitar o coordinator.

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

import "@chainlink/contracts/src/v0.8/mocks/VRFCoordinatorV2Mock.sol";
Enter fullscreen mode Exit fullscreen mode

Você pode encontrar os scripts de implantação escritos usando o plug-in hardhat-deploy aqui. Esses scripts também lidam com a criação e o financiamento de assinaturas VRF em testes mock.

O contrato simulado usa o id de solicitação retornado da função requestRandomWords como uma semente (seed) para gerar um número aleatório falso.

VRFCoordinatorV2Mock.sol#L113
for (uint256 i = 0; i < req.numWords; i++)
{
  _words[i] = uint256(keccak256(abi.encode(_requestId, i)));
}
Enter fullscreen mode Exit fullscreen mode

Para imitar o comportamento do Coordinator, temos que chamar a função fulfillRandomWords em nossos scripts de teste.

Ciclo de solicitação e recebimento de aleatoriedade de teste

import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { expect } from "chai";
import { deployments, ethers } from "hardhat";
import { VRFMinting, VRFCoordinatorV2Mock } from "../typechain";

export const POINT_ONE_LINK = `100000000000000000`;
export const LINK_PER_GAS = 1e9; // 0.000000001 LINK per gas

describe("Teste de aleatoriedade Solicitação/Recebimento", function () {
  let owner: SignerWithAddress;
  let vrfCoordinatorV2Mock: VRFCoordinatorV2Mock;
  let vrfMinting: VRFMinting;

  before(async function () {
    [owner] = await ethers.getSigners();
  });

  beforeEach(async function () {
    await deployments.fixture(["mocks", "VRFMinting"]);
    vrfCoordinatorV2Mock = await ethers.getContract("VRFCoordinatorV2Mock");
    vrfMinting = await ethers.getContract("VRFMinting");
  });

  it("Deve solicitar aleatoriedade: evento VRFCoordinator", async function () {
    await expect(vrfMinting.requestRandomness()).to.emit(
      vrfCoordinatorV2Mock,
      "RandomWordsRequested"
    );
  });

  it("Deve solicitar aleatoriedade: Evento do Contrato VRFMinting", async function () {
    await expect(vrfMinting.requestRandomness()).to.emit(
      vrfMinting,
      "RandomnessRequestSent"
    );
  });

  it("Deve solicitar aleatoriedade e obter um resultado", async function () {
    const tx = await vrfMinting.requestRandomness();
    const txReceipt = await tx.wait(1);
    if (!txReceipt.events) return;
    const requestId = await vrfMinting.addressToRequestId(owner.address);

    // simular a chamada de retorno da rede de oráculos
    await expect(
      vrfCoordinatorV2Mock.fulfillRandomWords(requestId, vrfMinting.address)
    ).to.emit(vrfMinting, "RandomnessRequestFulfilled");
    const randomnessRequestState = await vrfMinting.getRandomnessRequestState(
      owner.address
    );
    expect(randomnessRequestState[0]).to.equal(true);
  });

});
Enter fullscreen mode Exit fullscreen mode

Aqui eu testei se os resultados esperados são retornados no ciclo de solicitação e resposta de aleatoriedade. Você pode encontrar este e outros contratos de teste no repositório do Github.

Teste de Estágio

Use o bloco de teste abaixo ao testar em uma rede de teste como Goerli em vez de localhost.

it("Deve ser bem-sucedido o evento de retorno de chamada", async function () {
    await new Promise(async (resolve, reject) => {
      vrfMinting.once("RandomnessRequestFulfilled", async () => {
        console.log("evento disparado RandomnessRequestFulfilled!");
        const randomnessRequestState =
          await vrfMinting.getRandomnessRequestState(owner.address);
        try {
          expect(randomnessRequestState[0]).to.equal(true);
          expect(randomnessRequestState[1]).to.equal(true);

          resolve(true);
        } catch (e) {
          reject(e);
        }
      });
      await vrfMinting.requestRandomness();
    });
  });
Enter fullscreen mode Exit fullscreen mode

Ao testar em uma rede de teste como Goerli, não sabemos exatamente quando a solicitação de aleatoriedade será atendida. Portanto, não podemos esperar que nosso contrato emita o evento RandomnessRequestFulfilled instantaneamente como no ambiente local. Portanto, criei uma promessa (promise) para resolver quando o evento for emitido e os resultados esperados retornados.

Junte-se ao canal Coinmonks no Telegram e Youtube, aprenda sobre negociação e investimento em criptomoedas

Além disso, leia também


Artigo original escrito por Akalanka Pathirage e traduzido por Marcelo Panegali

Latest comments (0)