Artists and content creators have a unique potential to monetize their work due to blockchain technology, especially NFTs.
Artists are no longer reliant on galleries or auction houses to sell their art. Instead, they can sell it directly to the consumer as an NFT, allowing them to keep a more significant portion of the profit.
This article will guide us through building and deploying an NFT whitelist smart contract, enabling us to add, remove, validate, and verify if a user is part of a project’s whitelist.
Prerequisites
Make sure to have Node.js or npm installed on your computer. If you don’t, click here.
Project Setup and Installation
Let’s create a new folder/directory for our project, whitelist-project
in the terminal. We’ll work in this directory through the course of this tutorial. In the directory we just created, run the following commands:
npm init -y
npm install --save-dev hardhat
Let’s get a sample project by running the command below:
npx hardhat
We’ll go with the following options:
- A sample project.
- Accept all other requests.
Having hardhat-waffle
and hardhat-ethers
installed is required for the sample project.
Just in case it didn’t install automatically, we will install it manually with the following command:
npm install --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers
To make sure everything is working, run the following code:
npx hardhat test
If everything is working as it should, you’ll see a passed test result in your console:
Now, delete sample-test.js
from the test folder, sample-script.js
from the scripts
folder, and Greeter.sol
from the contracts
folder.
The folders themselves should not be deleted.
We’ll create a Whitelist.sol
file inside the contracts
directory. When using Hardhat, file layout is crucial, so pay attention! We’ll start with the most basic structure of any contract.
pragma solidity ^0.8.0;
import "hardhat/console.sol";
contract Whitelist {
constructor() {
console.log("Hello! from Whitelist Contract");
}
}
To build and deploy our smart contract, we’ll navigate to the scripts
folder, create a new run.js
file, and update it with the following code snippet:
const main = async () => {
const whitelistContractFactory = await hre.ethers.getContractFactory(
"Whitelist"
);
const whitelistContract = await whitelistContractFactory.deploy();
await whitelistContract.deployed();
console.log("Whitelist Contract deployed to: ", whitelistContract.address);
};
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();
In the code snippet above, we’ve created a script
that lets us deploy the smart contract we wrote earlier.
Let’s run it with the following command:
npx hardhat run scripts/run.js
You should see something similar to this:
Now, we have a working smart contract. Let’s deploy it to our local network.
In the scripts
folder, let’s create a deploy.js
file and copy and paste the code below:
const main = async () => {
const [deployer] = await hre.ethers.getSigners();
const accountBalance = await deployer.getBalance();
console.log("Deploying contracts with account: ", deployer.address);
console.log("Account balance: ", accountBalance.toString());
const Token = await hre.ethers.getContractFactory("Whitelist");
const portal = await Token.deploy();
await portal.deployed();
console.log("Whitelist address: ", portal.address);
};
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.error(error);
process.exit(1);
}
};
runMain();
Before deploying, let’s make sure our local blockchain node is running in a separate terminal with the following command:
npx hardhat node
Next, we’ll deploy our smart contract:
npx hardhat run scripts/deploy.js --network localhost
We should have something like this:
Building a Whitelist
In this section, we’ll update the smart contract Whitelist.sol
and deploy.js
files respectively.
Update the Whitelist.sol
file with the following code snippet:
pragma solidity ^0.8.0;
contract Whitelist {
uint256 public maxNumberOfWhitelistedAddresses;
uint256 public numberOfAddressesWhitelisted;
address owner;
mapping(address => bool) whitelistedAddresses;
constructor(uint256 _maxWhitelistedAddresses) {
owner = msg.sender;
maxNumberOfWhitelistedAddresses = _maxWhitelistedAddresses;
}
modifier onlyOwner() {
require(msg.sender == owner, "Error: Caller is not the owner");
_;
}
function addUserAddressToWhitelist(address _addressToWhitelist)
public
onlyOwner
{
require(
!whitelistedAddresses[_addressToWhitelist],
"Error: Sender already been whitelisted"
);
require(
numberOfAddressesWhitelisted < maxNumberOfWhitelistedAddresses,
"Error: Whitelist Limit exceeded"
);
whitelistedAddresses[_addressToWhitelist] = true;
numberOfAddressesWhitelisted += 1;
}
function verifyUserAddress(address _whitelistedAddress)
public
view
returns (bool)
{
bool userIsWhitelisted = whitelistedAddresses[_whitelistedAddress];
return userIsWhitelisted;
}
function isWhitelisted(address _whitelistedAddress)
public
view
returns (bool)
{
return whitelistedAddresses[_whitelistedAddress];
}
function removeUserAddressFromWhitelist(address _addressToRemove)
public
onlyOwner
{
require(
whitelistedAddresses[_addressToRemove],
"Error: Sender is not whitelisted"
);
whitelistedAddresses[_addressToRemove] = false;
numberOfAddressesWhitelisted -= 1;
}
function getNumberOfWhitelistedAddresses() public view returns (uint256) {
return numberOfAddressesWhitelisted;
}
function getMaxNumberOfWhitelistedAddresses()
public
view
returns (uint256)
{
return maxNumberOfWhitelistedAddresses;
}
function getOwner() public view returns (address) {
return owner;
}
}
Update the deploy.js
file in the script
directory with the following code snippet:
const main = async () => {
const portal = await Token.deploy(5);
await portal.deployed();
console.log("Whitelist address: ", portal.address);
};
In the code snippet above, we updated the deploy.js
script by specifying 5
in the constructor as the maximum number of addresses to be whitelisted.
Smart Contract Unit Testing
In this section, we’ll write a basic test to test out the most critical functions we’ll use.
To do so, we’ll create a whitelist-test.js
file inside the test
directory and write the following code:
const { expect, use } = require("chai");
const { ethers } = require("hardhat");
describe("Whitelist", async () => {
let whitelist;
let whitelistContract;
before(async () => {
whitelist = await ethers.getContractFactory("Whitelist");
whitelistContract = await whitelist.deploy(5);
});
it("should deploy", async () => {
expect(whitelistContract.address).to.be.a("string");
expect(whitelistContract.address).to.not.be.null;
});
it("should allow address to be added to whitelist", async () => {
const whitelistAddress = "0x0000000000000000000000000000000000000000";
await whitelistContract.addUserAddressToWhitelist(whitelistAddress);
const isWhitelisted = await whitelistContract.isWhitelisted(
whitelistAddress
);
expect(isWhitelisted).to.be.true;
});
it("should not allow address to be added to whitelist if already whitelisted", async () => {
const whitelistAddress = "0x0000000000000000000000000000000000000009";
await whitelistContract.addUserAddressToWhitelist(whitelistAddress);
const isWhitelisted = await whitelistContract.isWhitelisted(
whitelistAddress
);
expect(isWhitelisted).to.be.true;
});
it("should allow address to be removed from whitelist if already whitelisted", async () => {
const whitelistAddress = "0x0000000000000000000000000000000000000009";
await whitelistContract.removeUserAddressFromWhitelist(whitelistAddress);
const isWhitelisted = await whitelistContract.isWhitelisted(
whitelistAddress
);
expect(isWhitelisted).to.be.false;
});
it("should not allow address to be removed from whitelist if not whitelisted", async () => {
const whitelistAddress = "0x0000000000000000000000000000000000000000";
await whitelistContract.removeUserAddressFromWhitelist(whitelistAddress);
const isWhitelisted = await whitelistContract.isWhitelisted(
whitelistAddress
);
expect(isWhitelisted).to.be.false;
});
it("should return number of whitelisted addresses", async () => {
const whitelistAddress = "0x0000000000000000000000000000000000000000";
await whitelistContract.addUserAddressToWhitelist(whitelistAddress);
const numberOfWhitelistedAddresses =
await whitelistContract.getNumberOfWhitelistedAddresses();
expect(numberOfWhitelistedAddresses).to.equal(1);
});
it("should return the maximum number of whitelisted addresses", async () => {
const maxNumberOfWhitelistedAddresses =
await whitelistContract.getMaxNumberOfWhitelistedAddresses();
expect(maxNumberOfWhitelistedAddresses).to.equal(5);
});
it("should return the owner of the contract", async () => {
const owner = await whitelistContract.getOwner();
expect(owner).to.be.a("string");
expect(owner).to.not.be.null;
});
});
Next, let’s run the test with the following command:
npx hardhat test
We should have something similar to the image below:
RPC(Remote Procedure Call) Setup
Let’s set up an RPC and deploy the contract to the blockchain.
Before deploying to the blockchain, we’ll need to create an Alchemy account.
We’ll publish our contract creation transaction with Alchemy. The transaction will be mined and added to the blockchain as a valid transaction.
After you sign up, we’ll create an app like the one below. Remember to switch the network to Mumbai, where we’ll be deploying.
We’ll need to grab our keys, as shown below, and store them for later use:
To use the Testnet, we’ll need some fake MATIC tokens in our Testnet account, so we’ll request some from Polygon Mumbai using a faucet.
This “fake” MATIC can only be used on the Testnet.
We can grab some MATIC tokens here.
Let us update the hardhat.config.js
file in the root project directory:
require("@nomiclabs/hardhat-waffle");
require("dotenv").config();
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
const accounts = await hre.ethers.getSigners();
for (const account of accounts) {
console.log(account.address);
}
});
module.exports = {
solidity: "0.8.4",
networks: {
mumbai: {
url: process.env.STAGING_ALCHEMY_KEY,
accounts: [process.env.PRIVATE_KEY],
},
},
};
In the code snippet above, some keys were read from the .env
file, as well as the import
at the top of require("dotenv").config()
. This implies that we need to install the dotenv
package and also create a .env
file using the command below:
npm install -D dotenv
touch .env
Inside the .env
file, let’s add the following keys:
STAGING_ALCHEMY_KEY=
PRIVATE_KEY=
In the code above, we need to put our private key. Fortunately, getting our private key isn’t that hard. Check out this post.
Smart Contract Deployment to Polygon Network
It’s time to deploy our application on the Polygon network.
Let’s run the command below to deploy our contract to a blockchain network:
npx hardhat run scripts/deploy.js --network mumbai
We should have something similar to this:
We can verify our contract deployment on Polygon Mumbai Network.
Here’s the link to the repository so that you can check the code or in case you missed anything:
Conclusion
We built a whitelist smart contract in this article and deployed it to the Polygon Testnet.
This article is a part of the Hashnode Web3 blog, where a team of curated writers brings out new resources to help you discover the universe of web3. Check us out for more on NFTs, DAOs, blockchains, and the decentralized future.