How to Create an Off-Chain NFT Allowlist

Learn how to create off-chain allowlists for NFT PFP Projects

In a previous article, we explored the problems that allowlists solve (namely, generating and streamlining demand in your project) and implemented an on-chain version.

2392

Mekaverse, one of the most popular NFT projects in 2021, used off-chain allowlists to streamline demand

Although secure and fully functional, the on-chain solution comes with its caveats. In an on-chain implementation, the project owner (or contract) must upload and store all allowlisted addresses on the blockchain. If your project operates on a platform like Ethereum, this could mean a fortune in gas fees.

Fortunately, you can bypass these fees without compromising on security. This article will explore how to implement and store an allowlist off-chain using digital signatures and OpenZeppelin’s ECDSA library.

Creating the Off-Chain Allowlist

Step 1: Install Node and npm

In case you haven’t already, install node and npm on your local machine.

Ensure that node is at least v14 or higher by entering the following command in your terminal:

bash
$node -v

Step 2: Create a Hardhat project

We’re going to set up our project using Hardhat, the industry-standard development environment for Ethereum smart contracts. Additionally, we’ll also install OpenZeppelin contracts.

To set up Hardhat, run the following commands in your terminal:

bash
$mkdir offchain-allowlist && cd offchain-allowlist
>npm init -y
>npm install --save-dev hardhat
>npx hardhat

Choose Create a Javascript project from the menu and accept all defaults. To ensure everything is installed correctly, run the following command in your terminal:

bash
$npx hardhat test

To install OpenZeppelin:

bash
$npm install @openzeppelin/contracts

Step 3: Write the smart contract

Now, let’s write a basic NFT smart contract that can operate with an off-chain allowlist. To do this, we need two things:

  1. A Solidity mapping signatureUsed to check if a particular signature has been used before.
  2. A function recoverSigner that returns the public address of the wallet used to create a particular signature (or signed message).

Open the project in your favorite code editor (e.g., VS Code), and create a new file called NFTAllowlist.sol in the contracts folder. Add the following code to this file:

NFTAllowlist.sol
1//SPDX-License-Identifier: MIT
2pragma solidity ^0.8.4;
3
4import "hardhat/console.sol";
5import "@openzeppelin/contracts/utils/Counters.sol";
6import "@openzeppelin/contracts/access/Ownable.sol";
7import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
8import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
9
10contract NFTAllowlist is ERC721Enumerable, Ownable {
11 using Counters for Counters.Counter;
12
13 Counters.Counter private _tokenIds;
14
15 // Signature tracker
16 mapping(bytes => bool) public signatureUsed;
17
18 constructor() ERC721("NFT Allowlist Demo", "NAD") {
19 console.log("Contract has been deployed!");
20 }
21
22 // Allowlist addresses
23 function recoverSigner(bytes32 hash,
24 bytes memory signature)
25 public pure returns (address) {
26 bytes32 messageDigest = keccak256(
27 abi.encodePacked(
28 "\x19Ethereum Signed Message:\n32",
29 hash
30 )
31 );
32 return ECDSA.recover(messageDigest, signature);
33 }
34
35 // Presale mint
36 function preSale(uint _count, bytes32 hash, bytes memory signature) public {
37 require(recoverSigner(hash, signature) == owner(), "Address is not allowlisted");
38 require(!signatureUsed[signature], "Signature has already been used.");
39
40 for (uint i = 0; i < _count; i++) {
41 _mintSingleNFT();
42 }
43
44 signatureUsed[signature] = true;
45 }
46
47 function _mintSingleNFT() private {
48 uint newTokenID = _tokenIds.current();
49 _safeMint(msg.sender, newTokenID);
50 _tokenIds.increment();
51 }
52}

Notice that the preSale and recoverSigner functions take in a hash and a signature as arguments. The former is the hashed version of any message that we want to sign, and the latter is the hashed message signed using a wallet’s private key. We will generate both these values in the next step.

Compile the contract and make sure everything is working by running:

bash
$npx hardhat compile

Step 4: Create the Off-Chain Allowlist

Let’s now write a script that allows us to implement an allowlist off-chain. To do this, create a new file called run.js in the scripts folder, then add the following code:

run.js
1const { ethers } = require("hardhat");
2const hre = require("hardhat");
3
4async function main() {
5
6 // Define a list of allowlisted wallets
7 const allowlistedAddresses = [
8 '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
9 '0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc',
10 '0x90f79bf6eb2c4f870365e785982e1f101e93b906',
11 '0x15d34aaf54267db7d7c367839aaf71a00a2c6a65',
12 '0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc',
13 ];
14
15 // Select an allowlisted address to mint NFT
16 const selectedAddress = '0x90f79bf6eb2c4f870365e785982e1f101e93b906'
17
18 // Define wallet that will be used to sign messages
19 const walletAddress = '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266';
20 const privateKey = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
21 const signer = new ethers.Wallet(privateKey);
22 console.log("Wallet used to sign messages: ", signer.address, "\n");
23
24 let messageHash, signature;
25
26 // Check if selected address is in allowlist
27 // If yes, sign the wallet's address
28 if (allowlistedAddresses.includes(selectedAddress)) {
29 console.log("Address is allowlisted! Minting should be possible.");
30
31 // Compute message hash
32 messageHash = ethers.utils.id(selectedAddress);
33 console.log("Message Hash: ", messageHash);
34
35 // Sign the message hash
36 let messageBytes = ethers.utils.arrayify(messageHash);
37 signature = await signer.signMessage(messageBytes);
38 console.log("Signature: ", signature, "\n");
39 }
40
41 const factory = await hre.ethers.getContractFactory("NFTAllowlist");
42 const [owner, address1, address2] = await hre.ethers.getSigners();
43 const contract = await factory.deploy();
44
45 await contract.deployed();
46 console.log("Contract deployed to: ", contract.address);
47 console.log("Contract deployed by (Owner/Signing Wallet): ", owner.address, "\n");
48
49 recover = await contract.recoverSigner(messageHash, signature);
50 console.log("Message was signed by: ", recover.toString());
51
52 let txn;
53 txn = await contract.preSale(2, messageHash, signature);
54 await txn.wait();
55 console.log("NFTs minted successfully!");
56
57}
58
59main()
60 .then(() => process.exit(0))
61 .catch((error) => {
62 console.error(error);
63 process.exit(1);
64 });

Run the script using the following command:

bash
$npx hardhat run scripts/run.js

You should see output that looks something like this:

bash
$Wallet used to sign messages: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
>
>Address is allowlisted! Minting should be possible.
>Message Hash: 0x52d01c65d2e6acff550def14b5ce5bf353ac7ad53b132fc531c5d085d77c4ee3
>Signature: 0x55d2baf93dff9184dea51cc81c7837c0d65e01962d7292f03afa80cedf3dcdb948ada6cae3bc49ae56d3546019ebd62cc56efe1ef9c946a7c4fe66f21596e6c91c
>
>Contract has been deployed!
>Contract deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
>Contract deployed by (Owner/Signing Wallet): 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
>
>Message was signed by: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
>NFTs minted successfully!

Notice that we did not store allowlisted wallets on-chain. Instead, we stored them locally and performed the following steps:

  1. Check if the selected wallet is allowlisted.
  2. If allowlisted, sign the hashed version of the wallet’s public address using a secret private key.
  3. Pass the hashed address and the signature to the minting function of the smart contract.
  4. In the minting function, recover the signer and check if the signer is the owner of the smart contract. If yes, allow mint. Otherwise, return an error.

Conclusion

Congratulations! You now know how to implement an off-chain NFT allowlist.

If you enjoyed this tutorial about creating on-chain allowlists, tweet us at [@Alchemy] and give us a shoutout!

Don’t forget to join our Discord server to meet other blockchain devs, builders, and entrepreneurs!

Ready to start building your NFT collection?

Create a free Alchemy account and do share your project with us!