Fair Random NFT Mints with Pyth Entropy on Berachain

Beary Cucumber
Beary Cucumber
8 min read
Fair Random NFT Mints with Pyth Entropy on Berachain

Building a Fairly Minted NFT Collection with Pyth

Pyth Entropy allows developers to easily generate secure random numbers on the blockchain.

Verifiable randomness has useful applications in NFTs. Exploiting prior knowledge of the location of rares in a set (e.g. through insider knowledge or sleuthing) can compromise the fairness of a mint. An effective way to combat this is to distribute the mints completely randomly. True randomness is difficult to obtain natively, due to the deterministic nature of most blockchain protocols.

In this article, we show you how to leverage Pyth Entropy to create NFT mints which are provably fair for your users.


📋 Requirements

Before we move on, make sure you have the following installed and setup on your computer.

Foundry

This guide requires Foundry to be installed. In a terminal window, run:

curl -L https://foundry.paradigm.xyz | bash;

foundryup;

# foundryup installs the 'forge' and 'cast' binaries, used later

Installing Foundry To Your Computer

For more installation instructions, see Foundry’s installation guide. For more details using Foundry on Berachain, see this guide.


Creating Our Pyth Entropy Project

First, we’re going to set up your development environment using Foundry.

Start by creating a new project folder and initializing Foundry:

forge init pyth-entropy --no-git --no-commit;

cd pyth-entropy;

# We observe the following basic layout
# .
# ├── foundry.toml
# ├── script
# │   └── Counter.s.sol
# ├── src
# │   └── Counter.sol
# └── test
#     └── Counter.t.sol

Initial Forge Project Template

Installing Dependencies

We will be leveraging a number of dependencies, so install them below:

# FROM: ./pyth-entropy

pnpm init;
pnpm add web3 dotenv @pythnetwork/entropy-sdk-solidity @openzeppelin/contracts --ignore-workspace; 

# web3 - interact with EVM blockchains with JavaScript
# dotenv - manage environment variables
# @pythnetwork/entropy-sdk-solidity - Pyth Entropy interfaces 
# @openzeppelin/contracts - audited smart contract libraries

Install Contract Dependencies & SDKs

Forge can remap dependencies to make imports more readable. So let’s remap our Solidity imports:

# FROM: ./pyth-entropy

echo "remappings = ['@pythnetwork/entropy-sdk-solidity/=node_modules/@pythnetwork/entropy-sdk-solidity', '@openzeppelin/contracts/=node_modules/@openzeppelin/contracts']" >> ./foundry.toml

Foundry Toml File For Remapping

Writing the NFT Minting Contract

Now we’re ready to jump into the exciting part of generating verifiable randomness in a Solidity smart contract, using Pyth.

Create a new file ./src/EntropyNFT.sol and paste in the following:

pragma solidity ^0.8.13;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@pythnetwork/entropy-sdk-solidity/IEntropy.sol";
import "@pythnetwork/entropy-sdk-solidity/IEntropyConsumer.sol";

contract EntropyNFT is ERC721Enumerable, IEntropyConsumer {
    event NumberRequested(uint64 sequenceNumber, address minter);
    event Minted(uint64 sequenceNumber, address minter, uint256 tokenId);

    IEntropy entropy;
    address provider;
    uint256 public constant MAX_SUPPLY = 500;
    uint256 public nextIndex;
    uint256[] private availableTokenIds;

    // Mapping of sequence numbers to minter addresses
    mapping(uint64 => address) public sequenceNumberToMinter;

    constructor(
        address _entropy,
        address _provider
    ) ERC721("EntropyNFT", "eNFT") {
        entropy = IEntropy(_entropy);
        provider = _provider;
        initializeAvailableTokenIds();
    }

    // Step 1 of 2: Request a new random number for minting
    // Returns sequence number used to obtain random number from Pyth
    function requestMint(bytes32 userRandomNumber) external payable {
        require(nextIndex < MAX_SUPPLY, "Reached max supply");

        uint128 requestFee = entropy.getFee(provider);
        require(msg.value >= requestFee, "not enough fees");

        uint64 sequenceNumber = entropy.requestWithCallback{value: requestFee}(
            provider,
            userRandomNumber
        );

        sequenceNumberToMinter[sequenceNumber] = msg.sender;

        emit NumberRequested(sequenceNumber, msg.sender);
    }

    // Step 2 of 2: Fulfill mint request on Pyth callback
    function entropyCallback(
        uint64 sequenceNumber,
        address,
        bytes32 randomNumber
    ) internal override {
        address minter = sequenceNumberToMinter[sequenceNumber];
        uint256 randomIndex = uint256(randomNumber) % availableTokenIds.length;
        uint256 tokenId = availableTokenIds[randomIndex];

        // Swap-and-pop to replace minted tokenId
        availableTokenIds[randomIndex] = availableTokenIds[
            availableTokenIds.length - 1
        ];
        availableTokenIds.pop();
        nextIndex++;

        _safeMint(minter, tokenId);
        emit Minted(sequenceNumber, minter, tokenId);
    }

    // Initialize array of available token IDs
    function initializeAvailableTokenIds() private {
        for (uint256 i = 0; i < MAX_SUPPLY; i++) {
            availableTokenIds.push(i);
        }
    }

    // This method is required by the IEntropyConsumer interface
    function getEntropy() internal view override returns (address) {
        return address(entropy);
    }
}

Now let’s break this contract down:

  • The EntropyNFT contract inherits from ERC721Enumerable, providing standard NFT functionality. Our contract additionally inherits from IEntropyConsumer, which implements the callback once the random number request is fulfilled.
  • IEntropy.sol provides interfaces for interacting with the Pyth Entropy contract. The constructor argument _entropy hooks up the deployed Entropy contract to this interface.
  • The contract defines a maximum supply of 500 NFTs and initializes an array availableTokenIds from [0…499]
  • The requestMint function allows users to request a random number from Entropy by providing a user commitment (more below) and paying the required fee. The returned sequenceNumber indexes the caller’s entitlement to mint the NFT for the random number corresponding to the sequenceNumber
  • The entropyCallback function is called when a decentralized keeper bot fulfills a random number request which ultimately mints the NFT with a unique token ID. This is achieved by taking the modulo of the random number and the remaining number of NFTs to be minted, ensuring that the random index is in bounds of the availableTokenIds array. Used tokenIds are swapped for the last element of the array to maintain a list of available tokenIds
You may delete src/Counter.soltest/Counter.t.sol and script/Counter.s.sol 
which were generated with the project

Setting up for Deployment

Create an ./env file at the project root and populate it with the following:

RPC_URL=https://artio.rpc.berachain.com/
ENTROPY_NFT_ADDRESS=YOUR_ENTROPY_NFT_ADDRESS
PRIVATE_KEY=YOUR_PRIVATE_KEY
ENTROPY_ADDRESS=0x26DD80569a8B23768A1d80869Ed7339e07595E85
PROVIDER_ADDRESS=0x6CC14824Ea2918f5De5C2f75A9Da968ad4BD6344

Fill in your deployer wallet PRIVATE_KEY and then load these environment variables into your terminal session:

# FROM: ./pyth-entropy

source .env;

Deploying to Berachain Testnet

First, compile the smart contract:

# FROM: ./pyth-entropy

forge build;

You will notice a number of build outputs appearing in the ./out directory.

We will be leveraging the forge create command for deploying our new contract on Berachain Testnet (read more about the command here):

# FROM: ./pyth-entropy

forge create src/EntropyNFT.sol:EntropyNFT  \
--private-key $PRIVATE_KEY \
--rpc-url $RPC_URL \
--constructor-args $ENTROPY_ADDRESS $PROVIDER_ADDRESS;

# [Example output]
# Deployer: <YOUR_WALLET_ADDRESS>
# Deployed to: <YOUR_DEPLOYED_CONTRACT>
# Transaction hash: <CONTRACT_DEPLOYMENT_TX_HASH>
In .env populate ENTROPY_NFT_ADDRESS with your deployed contract address

You are required to have $BERA to pay for the deployment fees. Faucet funds are available at https://artio.faucet.berachain.com/.

Minting Your Randomized NFT

Now let’s interact with our new NFT contract using JavaScript. Let’s create a new folder to house our scripts (we will have two):

# FROM ./pyth-entropy

mkdir app;
cd app;
touch requestMint.js;

In ./app/requestMint.js paste in the following code:

const { Web3 } = require("web3");
const EntropyNFTAbi = require("../out/EntropyNFT.sol/EntropyNFT.json");
const EntropyAbi = require("@pythnetwork/entropy-sdk-solidity/abis/IEntropy.json");
require("dotenv").config({ path: "../.env" });

async function requestMint() {
  // Step 1: initialize wallet & web3 contracts
  const web3 = new Web3(process.env["RPC_URL"]);
  const { address } = web3.eth.accounts.wallet.add(
    process.env["PRIVATE_KEY"]
  )[0];

  const entropyNFTContract = new web3.eth.Contract(
    EntropyNFTAbi.abi,
    process.env["ENTROPY_NFT_ADDRESS"]
  );

  const entropyContract = new web3.eth.Contract(
    EntropyAbi,
    process.env["ENTROPY_ADDRESS"]
  );

  // Step 2: generate user random number, request mint
  console.log("Generating user random number and commitment...");
  const userRandomNumber = web3.utils.randomHex(32);
  console.log(`User Random Number: ${userRandomNumber}`);

  console.log("Fetching request fee...");
  const fee = await entropyContract.methods
    .getFee(process.env["PROVIDER_ADDRESS"])
    .call();
  console.log(`Request Fee: ${fee}`);

  console.log("Requesting NFT mint...");
  const requestReceipt = await entropyNFTContract.methods
    .requestMint(userRandomNumber)
    .send({ value: fee, from: address });
  console.log(`Request Transaction Hash: ${requestReceipt.transactionHash}`);

  const sequenceNumber =
    requestReceipt.events.NumberRequested.returnValues.sequenceNumber;
  console.log(`Sequence Number: ${sequenceNumber}`);

  // Step 3: Poll for new Minted events emitted by EntropyNFT
  // Stops polling when same sequenceNumber is fulfilled 
  const intervalId = setInterval(async () => {
    currentBlock = await web3.eth.getBlockNumber();

    const events = await entropyNFTContract.getPastEvents("Minted", {
      fromBlock: currentBlock - 5n,
      toBlock: currentBlock,
    });
    
    // Find the event with the same sequence number as the request.
    const event = events.find(
      (event) => event.returnValues.sequenceNumber === sequenceNumber
    );
    
    // If the event is found, log the result and stop polling.
    if (event !== undefined) {
      const values = events[0].returnValues
      console.log(`✅ NFT ID ${values.tokenId} minted to ${values.minter}, based on sequenceNumber ${values.sequenceNumber}`)
      clearInterval(intervalId);
    }
  }, 2000);

}

requestMint();

Now run:

# FROM: ./pyth-entropy/app

node requestMint.js;

# [Expected Output]:
# Generating user random number and commitment...
# User Random Number: <0xUSER_RANDOM_NUMBER>
# Fetching request fee...
# Request Fee: 101
# Requesting NFT mint...
# Request Transaction Hash: <0xTX_HASH>
# Sequence Number: 116
# ✅ NFT ID 168 minted to <YOUR_WALLET_ADDRESS>, based on sequenceNumber 116

The requestMint script executes two main steps:

Step 1: We start by initializing the user wallet and the web3 contracts for interacting with the NFT and Entropy contracts.

Step 2: A random number is generated and passed into EntropyNFT's requestMint function alongside a required fee. We receive a sequenceNumber from the Entropy contract in return.

Step 3: After submitting the request, an automated Pyth service will generate a random number, which executes the entropyCallback function on our NFT contract. This portion of our script monitors our contract for the Minted event, which signifies that the Entropy request has been fulfilled and that a verifiably random NFT has been minted to your wallet, using the random number.

Congratulations! You’ve successfully leveraged Pyth’s Entropy service to provide a provably fair NFT minting process 🎉

Entropy Recap 🔮

Pyth Entropy extends the classical commit/reveal scheme for generating random numbers. First, two parties individually commit to a secret value (generated random numbers). At the reveal phase, a final random number is generated based on a hash of the two random numbers.

In the context of our NFT contract, the random number generation works as follows, placing emphasis on the different parties involved:

  1. User commits to a random number in the NFT contract's requestMint call
  2. An off-chain service (bot) generates the second random number
  3. The bot sends the two random numbers to Pyth's Entropy contract through the revealWithCallBack call. This ultimately invokes our NFT contract's entropyCallback to mint the randomized NFT to the user

Note: from the user's perspective, their initial transaction requesting the random number (requestMint) will not mint the NFT. Rather, the user receives the NFT in a subsequent call initiated by the bot.

There are a lot of moving parts, so here’s a flow diagram which helps break down the interactions:

Pyth Entropy Sequence Diagram
Learn the details behind Entropy's design in Pyth's docs

🐻 Full Code Repository

If you want to see the final code and see other guides, check out Berachain Pyth Entropy Guide Code.

guides/apps/pyth-entropy at main · berachain/guides
A demonstration of different contracts, languages, and libraries that work with Berachain EVM. - berachain/guides

https://github.com/berachain/guides/tree/main/apps/pyth-entropy


🛠️ Want To Build More?

Want to build more on Berachain and see more examples. Take a look at our Berachain GitHub Guides Repo for a wide variety of implementations that include NextJS, Hardhat, Viem, Foundry, and more.

If you’re looking to dive deeper into the details, take a look at our Berachain Docs.


Looking For Dev Support?

Make sure to join our Berachain Discord server and check out our developer channels to ask questions.

❤️ Don’t forget to show some love for this article by sharing it.