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.
- Node
v20.11.0
or greater - pnpm
- A wallet configured with Berachain Artio Network
$BERA
or Berachain Testnet Tokens in that wallet — see Berachain Faucet
Foundry
This guide requires Foundry to be installed. In a terminal window, run:
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:
Installing Dependencies
We will be leveraging a number of dependencies, so install them below:
Forge can remap dependencies to make imports more readable. So let’s remap our Solidity imports:
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 fromERC721Enumerable
, providing standard NFT functionality. Our contract additionally inherits fromIEntropyConsumer
, 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 returnedsequenceNumber
indexes the caller’s entitlement to mint the NFT for the random number corresponding to thesequenceNumber
- 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 theavailableTokenIds
array. UsedtokenIds
are swapped for the last element of the array to maintain a list of availabletokenIds
You may deletesrc/Counter.sol
,test/Counter.t.sol
andscript/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
populateENTROPY_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:
- User commits to a random number in the NFT contract's
requestMint
call - An off-chain service (bot) generates the second random number
- The bot sends the two random numbers to Pyth's
Entropy
contract through therevealWithCallBack
call. This ultimately invokes our NFT contract'sentropyCallback
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:
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.
🛠️ 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.