Introduction

This guide is an introduction to Ethereum smart contract development. It was created by Saravanan Vijayakumaran for the participants of the 2022 ACM Winter School on Digital Trust organized by IITB Trust Lab.

Acknowledgment

The content in the hardhat workflow, ERC-20, and NFT chapters is based on tutorials in the Alchemy Docs.

Prerequisites

Ethereum Testnets

Ethereum smart contracts are programs that are installed on the Ethereum blockchain. Installing a smart contract is also called deployment. Deploying smart contracts on Ethereum costs ether. The amount of ether required depends on the size of the smart contract.

Using real ether for learning smart contract development is not feasible. Fortunately, the Ethereum ecosystem operates testnets. These are versions of the Ethereum blockchain that are used for testing purposes. The main Ethereum blockchain is called mainnet. Unlike mainnet ether, testnet ether has no value. It is available for free from websites called faucets.

In this guide, we will be using the Goerli testnet (https://goerli.etherscan.io/) for Ethereum smart contract development. The domain name in the previous URL corresponds to Etherscan, a blockchain explorer which allows users to explore blockchain data in a browser. The blocks on Ethereum mainnet can be explored at https://etherscan.io/.

Node Providers

To deploy contracts to Ethereum, we need access to an Ethereum node. The node software continually downloads blocks and tracks the tip of the Ethereum blockchain. While it is possible to run an Ethereum node on our own computers, it will consume bandwidth, disk space, and CPU cycles.

It is more convenient to connect to a remote node operated by a node provider like Alchemy, Infura, or QuickNode. These are Infrastructure-as-a-Service (IaaS) companies that offer access to Ethereum nodes for a fee. All of them offer a free tier account that is sufficient for learning purposes.

See this article for more information about node providers: Alchemy Blog: What is a Blockchain Node Provider

Prerequisites for Smart Contract Development

Before beginning smart contract development, you need to complete the following steps:

  1. Install an Ethereum wallet and create an address to store testnet ether
  2. Request testnet ether from a faucet into your address
  3. Create an account with a node provider and generate an API key

The details of how to complete these steps are described in the next sections. Keep reading!

Note: In this guide, we will be using two different workflows for smart contract development. One will use the Remix online IDE and the other will use Hardhat.

The Remix workflow does not require a node provider API key (step 3 in the above list can be skipped). Remix will use the node provider of the Metamask wallet (which is installed as a browser plugin). The Hardhat workflow does require the API key.

While the Remix workflow is easier to setup, the Hardhat workflow is more convenient for testing and deploying smart contracts.

Install Metamask

Metamask is an Ethereum wallet that can be installed as a browswer plugin. Go to https://metamask.io/download/ and install it for your browser.

  1. Once you install the Metamask extension, you should see an orange fox icon in your browser's extension list. Click on it to get started with setting up your wallet.

  2. You should eventually arrive at the following screen

    Metamask create wallet screen

  3. Click on "Create a wallet" and set a password.

  4. The Secret Recovery Phrase (SRP) is the seed randomness from which all your private keys will be created. Metamask will show you the SRP for your new wallet.

    • Make sure you save your SRP by writing it down or downloading it.
    • If you lose it, you will lose access to all the Ethereum accounts you create in Metamask.
  5. Metamask will show you the words in your SRP and ask you to choose them in the right order. This is to confirm that you have stored the SRP.

  6. Once your SRP is confirmed, Metamask will show you the wallet screen with a new created Ethereum account. This account will have no founds. We will fund it in the next section.

  7. You can copy your Metamask address by following the step described in this page.

Get Testnet Ether

For the lab exercises described in this guide, you need to have testnet ether in your wallet. You will get testnet ether from faucet websites. To view your testnet ether balances, you need to add a testnet configuration to Metamask.

Configuring Goerli Testnet in Metamask

By default, the Metamask wallet displays account balances on Ethereum mainnet. This is indicated at the top of the Metamask interface.

Metamask Mainnet

You will be configuring Metamask to interact with the Goerli tesnet. Do the following steps:

  1. Open Metamask and click on the dropdown labelled Ethereum mainnet. Click the Show/hide test networks link.

    Metamask show/hide test networks

  2. Set the Show test networks button to ON.

    Metamask show test networks button

  3. The network dropdown menu should now show the Goerli test network

    Metamask networks dropdown

  4. Choose the Goerli test network from the dropdown. Metamask should now denominate your balance in GoerliETH.

    Metamask Goerli ETH

Requesting Goerli Tesnet Ether From Faucets

Faucets are websites that send small amounts of testnet ether on request. They are called faucets because they drip ether. The faucet operators themselves acquire the testnet ether by participating in the testnet consensus protocol.

  1. Click on the account name in Metmask to copy the address to your clipboard.. To request testnet ether, you have to paste your account address in the faucet website.

    Metamask Copy Account Address

  2. To prevent bots or malicious users from draining all the testnet ether available, faucets have mechanisms for rate-limiting requests. Here are some faucets along with their rate-limiting mechanisms. Go to one of them.

    • Paradigm Faucet: https://faucet.paradigm.xyz/
      • Requires a Twitter account that must have at least 1 Tweet, 50 followers, and be older than 1 month. You need to authorize this faucet to read your Twitter profile, to check the previous conditions hold.
      • Drips 0.1 ETH per user every 24 hours.
    • Alchemy Faucet: https://goerlifaucet.com/
      • Requires a free Alchemy account. The faucet will check that you are logged in.
      • Drips 0.2 ETH per user every 24 hours.
  3. Paste the account address into the textbox of the faucet and request the testnet ether. The picture below shows the Alchemy faucet interface.

    Alchemy Goerli Faucet

  4. If the faucet's ether transfer succeeds, you should see a non-zero GoerliETH balance in Metamask.

    Metamask wallet funded

Get Node Provider API Key

NOTE: Node provider API key is required only for the lab exercises based on Hardhat.

We will using Alchemy as our node provider. To generate an Alchemy API key, do the following:

  1. Create a free account on Alchemy.

  2. Login into Alchemy.

  3. Create a new app by choosing the option on the Apps dropdown menu.

  4. Choose Goerli in the NETWORK dropdown of the Create App page.

    Alchemy Create Goerli App

  5. Click the Create app button. Your new Alchemy app should appear in the list of Apps at https://dashboard.alchemy.com/apps.

  6. Click on the VIEW KEY button to get the API key.

    Alchemy Apps List

Remix Workflow

Remix is an IDE for Ethereum smart contract development that can run in a browser. It is available at https://remix.ethereum.org. The Remix documentation is available at https://remix-ide.readthedocs.io/.

The layout of the Remix IDE is described at https://remix-ide.readthedocs.io/en/latest/layout.html. Read this page. We will be using the components described on that page in the rest of this chapter.

Compiling a Contract

Do the following in the Remix IDE:

  1. Create a new blank workspace using the File Explorer.
  2. Create a new file and name it Storage.sol.
  3. Copy the below code and paste it into Storage.sol. This code is from the Solidity tutorial.
    // SPDX-License-Identifier: GPL-3.0
    pragma solidity >=0.4.16 <0.9.0;
    
    contract SimpleStorage {
        uint storedData;
    
        function set(uint x) public {
            storedData = x;
        }
    
        function get() public view returns (uint) {
            return storedData;
        }
    }
    
  4. Click on the Solidity compiler icon and click on the Compile Storage.sol to compile the contract.
  5. Introduce a bug in Storage.sol by changing the line uint storeData; to uint store;. Click the Compile Storage.sol button again and check that errors are reported.
  6. Undo the bug and continue with the next section.

Deploying a Contract

Do the following in the Remix IDE:

  1. Click on the Deploy & run transactions icon.

  2. In the ENVIRONMENT dropdown, choose Injected Provider - Metamask.

    Remix Environment Setting

  3. A Metamask window will popup asking you to select the account you wish to connect to Remix. Choose the appropriate account and click Next followed by Connect. At this point, the ACCOUNT field in Remix will be populated by your account address from Metamask.

  4. Click the Deploy button. A Metamask window will popup asking you to confirm the contract deployment.

    Remix Contract Deployment Metamask Popup

  5. Click the Confirm button. After a few seconds, the contract will be deployed. You will see a notification in the Remix terminal. The contract will also appear in the Deployed Contracts list in the Remix Side Panel.

Interacting with the Contract

Do the following in the Remix IDE:

  1. Go to the Deployed Contracts section of the Remix Side Panel.

  2. In the section corresponding to SimpleStorage, you should see two buttons labeled set and get.

  3. Click the get button. It should return the value 0. The string uint256: 0 will appear below the get button.

  4. Enter an integer value (say 32) into the input box next to the set button and click it.

  5. A Metamask window will popup asking you to confirm the set transaction. Click the Confirm button.

    Remix Simple Storage Set Tx Metamask Popup

  6. You will see a notification in the Remix terminal that the transaction is pending. After a few seconds, there will be another notification in the Remix terminal that the transaction was included in a block.

  7. Click the get button. It should return the value 32. The string uint256: 32 will appear below the get button.

Hardhat Workflow

Hardhat is a development environment for Ethereum smart contract development. It has a rich ecosystem of tools and plugins which have made it a popular choice among developers. The Hardhat documentation can be found at https://hardhat.org/docs.

Acknowledgment: The contents on this chapter are based on the Hello World smart contract tutorial by Alchemy.

Setting up a Hardhat Project

To setup a project that uses Hardhat for Ethereum contract development, do the following:

  1. Install a recent version of Node.js. We need the npm and npx commands for the rest of this chapter.
  2. Create a new directory and enter it.
    mkdir hello-world
    cd hello-world
    
  3. Initialize a new Node.js project.
    npm init -y
    
    The directory should contain a single file called package.json.
  4. Install Hardhat by running the following command in the hello-world directory.
    npm install --save-dev hardhat
    
    The package.json file will now have a hardhat section under devDependencies.
  5. Create a Hardhat project by running the following command. Choose the Create an empty hardhat.config.js option.
    npx hardhat
    
    The directory will have a file called hardhat.config.js with the following contents.
    /** @type import('hardhat/config').HardhatUserConfig */
    module.exports = {
        solidity: "0.8.17",
    };
    
    The number 0.8.17 specifies the version of the Solidity compiler.

Compiling a Contract

To add a contract and compile it using Hardhat, do the following:

  1. In the project directory, create a contracts directory.

    mkdir contracts
    
  2. Create a new file in the contracts directory called HelloWorld.sol.

  3. Copy and paste the following code into HelloWorld.sol.

    // SPDX-License-Identifier: MIT
    pragma solidity >=0.7.3;
    
    // Defines a contract named `HelloWorld`.
    contract HelloWorld {
    
        // Event emitted when update function is called
        event UpdatedMessages(string oldStr, string newStr);
    
        // Declares a state variable `message` of type `string`.
        string public message;
    
        // A constructor is a special function that is only executed upon contract creation.
        constructor(string memory initMessage) {
            message = initMessage;
        }
    
        // A public function that accepts a string argument and updates the `message` storage variable.
        function update(string memory newMessage) public {
            string memory oldMsg = message;
            message = newMessage;
            emit UpdatedMessages(oldMsg, newMessage);
        }
    }
    
  4. Compile the contract by running the following command.

    npx hardhat compile
    

    You should see a message saying Compiled 1 Solidity file successfully.

    The command also creates a directory called artifacts that has some JSON files. The HelloWorld.json has the contract's code and application binary interface (ABI). These will be used for contract deployment and interaction.

    Hardhat Compile Artifacts

Deploying a Contract

To deploy the HelloWorld.sol contract, do the following:

  1. Install the dotenv package in your project directory.

    npm install dotenv --save
    
  2. Create a file called .env in the project directory with the following contents.

    API_URL = "https://eth-goerli.g.alchemy.com/v2/your-api-key"
    PRIVATE_KEY = "your-metamask-private-key"
    

    Follow these instructions to export your private key from Metamask. The API_URL needs to be copied from your Alchemy account.

    NOTE: If you are going to push the project code to a public Github/Gitlab repository, remember to add the .env file to your .gitignore.

  3. Install Ethers.js by running the following command

    npm install --save-dev @nomiclabs/hardhat-ethers "ethers@^5.0.0"
    
  4. Update the hardhat.config.js file to have the following content.

    /**
    * @type import('hardhat/config').HardhatUserConfig
    */
    
    require('dotenv').config();
    require("@nomiclabs/hardhat-ethers");
    
    const { API_URL, PRIVATE_KEY } = process.env;
    
    module.exports = {
    solidity: "0.8.17",
    defaultNetwork: "goerli",
    networks: {
        hardhat: {},
        goerli: {
            url: API_URL,
            accounts: [`0x${PRIVATE_KEY}`]
        }
    },
    }
    
  5. Create a directory called scripts

    mkdir scripts
    
  6. Create a file called deploy.js in the scripts directory with the following content.

    async function main() {
        const HelloWorld = await ethers.getContractFactory("HelloWorld");
    
        // Start deployment, returning a promise that resolves to a contract object
        const hello_world = await HelloWorld.deploy("Hello World!");   
        console.log("Contract deployed to address:", hello_world.address);
    }
    
    main()
    .then(() => process.exit(0))
    .catch(error => {
        console.error(error);
        process.exit(1);
    });
    
  7. Deploy the contract by running the following command.

    npx hardhat run scripts/deploy.js --network goerli
    

    You should see a message of the following form. The address will be different in your case.

    Contract deployed to address: 0xD1aEf927a80301b63dE477afe2410F25bf8Baf6a
    

    Save the contract address somewhere. It will be used in the next section.

Interacting with the Contract

In this section, we will interact with the HelloWorld contract that deployed in the previous section. Do the following:

  1. Add API_KEY and CONTRACT_ADDRESS variables to the .env file. Your .env file should look like the following:

    API_URL = "https://eth-goerli.g.alchemy.com/v2/<your-api-key>"
    API_KEY = "<your-api-key>"
    PRIVATE_KEY = "<your-metamask-private-key>"
    CONTRACT_ADDRESS = "0x<your contract address>"
    

    Note that the API_KEY is the string at the end of your API_URL. The CONTRACT_ADDRESS is the address that was output at the end of the deployment workflow in the previous section.

  2. Create a file called interact.js in the scripts directory with the following content.

    const API_KEY = process.env.API_KEY;
    const PRIVATE_KEY = process.env.PRIVATE_KEY;
    const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS;
    
    const contract = require("../artifacts/contracts/HelloWorld.sol/HelloWorld.json");
    
    // provider - Alchemy
    const alchemyProvider = new ethers.providers.AlchemyProvider(network="goerli", API_KEY);
    
    // signer - your account
    const signer = new ethers.Wallet(PRIVATE_KEY, alchemyProvider);
    
    // contract instance
    const helloWorldContract = new ethers.Contract(CONTRACT_ADDRESS, contract.abi, signer);
    
    async function main() {
        const message = await helloWorldContract.message();
        console.log("The message is: " + message); 
    
        console.log("Updating the message...");
        const tx = await helloWorldContract.update("Hello Again World!");
        await tx.wait();
    
        const newMessage = await helloWorldContract.message();
        console.log("The new message is: " + newMessage); 
    }
    
    main();
    
  3. Run the following command to update the message in the HelloWorld contract.

    npx hardhat run scripts/interact.js --network goerli
    

    You should see the following output after about half a minute. The delay is because the script waits for the transaction included in a new block in the Goerli testnet.

    The message is: Hello World!
    Updating the message...
    The new message is: Hello Again World!
    
  4. The interact.js script above requires you to enter the message in the script itself. It would be nice if we could specify the message on the command line. This can be done by using Hardhat as a library in a standalone Node.js script. Create a new file called interact-cli.js in the scripts directory with the following content.

    const hre = require("hardhat");
    const API_KEY = process.env.API_KEY;
    const PRIVATE_KEY = process.env.PRIVATE_KEY;
    const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS;
    
    const contract = require("../artifacts/contracts/HelloWorld.sol/HelloWorld.json");
    
    // provider - Alchemy
    const alchemyProvider = new hre.ethers.providers.AlchemyProvider(network="goerli", API_KEY);
    
    // signer - your account
    const signer = new hre.ethers.Wallet(PRIVATE_KEY, alchemyProvider);
    
    // contract instance
    const helloWorldContract = new hre.ethers.Contract(CONTRACT_ADDRESS, contract.abi, signer);
    
    async function main() {
        await hre.run("compile");
    
        const args = process.argv;
        if (args.length != 3) {
            console.log("Provide a message argument (in single quotes)")
            process.exit(0);
        }
        const new_message_arg = args[2];
    
        const message = await helloWorldContract.message();
        console.log("The message is: " + message); 
    
        console.log("Updating the message...");
        const tx = await helloWorldContract.update(new_message_arg);
        await tx.wait();
    
        const newMessage = await helloWorldContract.message();
        console.log("The new message is: " + newMessage);
    }
    
    main().catch((error) => {
        console.error(error);
        process.exitCode = 1;
    });
    
  5. Run the following command to update the message stored in the HelloWorld contract from the command line. Note the single quotes surrounding the new message.

    node scripts/interact-cli.js 'Hello World 3!'
    

    You should see the following output:

    Nothing to compile
    The message is: Hello World Again!
    Updating the message...
    The new message is: Hello World 3!
    

ERC-20 Token

The ERC-20 token standard is a specification for fungible tokens on Ethereum. A token is said to be fungible if one unit of the token can be replaced with any other unit.

For example, a 10 rupee note is fungible in the sense it can be exchanged for any other 10 rupee note. Ether is itself a fungible token. Each unit of a non-fungible token (NFTs) has a unique identifier and cannot be exchanged for another unit.

Note: The ERC in ERC-20 stands for Ethereum Request for Comments. They are Ethereum's analog of Request for Comments (RFCs), that are used to specify Internet standards. When the Github issue to discuss the fungible token standard was created in the Ethereum Improvement Proposals repository, it was assigned issue number 20. Hence the name ERC-20.

The full ERC-20 token specification can be found in Ethereum Improvement Proposal 20 (EIP-20).

In this chapter, you will be deploying your own ERC-20 token on Goerli testnet.

Acknowledgment: The contents on this chapter are based on the How to Create an ERC-20 Token tutorial by Alchemy.

Setting up the ERC-20 Project

To setup a project that uses Hardhat for ERC-20 contract development, do the following:

  1. Create a new directory and enter it.

    mkdir my-token
    cd my-token
    
  2. Initialize a new Node.js project.

    npm init -y
    

    The directory should contain a single file called package.json.

  3. Install Hardhat by running the following command in the my-token directory.

    npm install --save-dev hardhat
    

    The package.json file will now have a hardhat section under devDependencies.

  4. Create a Hardhat project by running the following command. Choose the Create an empty hardhat.config.js option.

    npx hardhat
    

    The directory will have a file called hardhat.config.js with the following contents.

    /** @type import('hardhat/config').HardhatUserConfig */
    module.exports = {
        solidity: "0.8.17",
    };
    

    The number 0.8.17 specifies the version of the Solidity compiler.

  5. Install the dotenv package in your project directory.

    npm install dotenv --save
    
  6. Create a file called .env in the project directory with the following contents.

    API_URL = "https://eth-goerli.g.alchemy.com/v2/your-api-key"
    PRIVATE_KEY = "your-metamask-private-key"
    

    Follow these instructions to export your private key from Metamask. The API_URL needs to be copied from your Alchemy account.

    NOTE: If you are going to push the project code to a public Github/Gitlab repository, remember to add the .env file to your .gitignore.

  7. Install Ethers.js by running the following command

    npm install --save-dev @nomiclabs/hardhat-ethers "ethers@^5.0.0"
    
  8. Update the hardhat.config.js file to have the following content.

    /**
    * @type import('hardhat/config').HardhatUserConfig
    */
    
    require('dotenv').config();
    require("@nomiclabs/hardhat-ethers");
    
    const { API_URL, PRIVATE_KEY } = process.env;
    
    module.exports = {
    solidity: "0.8.17",
    defaultNetwork: "goerli",
    networks: {
        hardhat: {},
        goerli: {
            url: API_URL,
            accounts: [`0x${PRIVATE_KEY}`]
        }
    },
    }
    
  9. Your ERC-20 token will be based on the implementation by OpenZeppelin. Install the Node.js package containing OpenZeppelin's contracts by running the following command in the project directory.

    npm install @openzeppelin/contracts
    

    The installed contracts can be found in the node_modules directory in your project directory. The path will be node_modules/@openzeppelin/contracts/. We will be inheriting the ERC-20 implementation at node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol.

Deploying the Contract

To deploy your ERC-20 token, do the following:

  1. In the project directory, create a contracts directory.

    mkdir contracts
    
  2. Create a new file in the contracts directory called MyToken.sol.

  3. Copy and paste the following code into MyToken.sol.

    //SPDX-License-Identifier: Unlicense
    pragma solidity ^0.8.0;
    
    import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
    
    contract WinterSchoolToken is ERC20 {
        uint constant _initial_supply = 100 * (10**18);
    
        /* ERC 20 constructor takes in two strings:
         1. The name of your token name
         2. A symbol for your token
        */
        constructor() ERC20("Winter School Token", "WST") {
            _mint(msg.sender, _initial_supply);
        }
    }
    
  4. Compile the contract by running the following command.

    npx hardhat compile
    

    You should see a message saying Compiled 1 Solidity file successfully.

  5. Create a directory called scripts

    mkdir scripts
    
  6. Create a file called deploy.js in the scripts directory with the following content.

    async function main() {
        const MyToken = await ethers.getContractFactory("WinterSchoolToken");
    
        // Start deployment, returning a promise that resolves to a contract object
        const my_token = await MyToken.deploy();   
        console.log("ERC-20 contract deployed to address:", my_token.address);
    }
    
    main()
    .then(() => process.exit(0))
    .catch(error => {
        console.error(error);
        process.exit(1);
    });
    
  7. Deploy the contract by running the following command.

    npx hardhat run scripts/deploy.js --network goerli
    

    You should see a message of the following form. The address will be different in your case.

    ERC-20 contract deployed to address: 0xbb8Ab9564596Ccbfe0C6eD49D7FdB056eE741CE5
    
  8. Go to https://goerli.etherscan.io/token/[Your Token Address] to see the token details. Notice that you have to enter the address of the newly created token in the URL.

Customize and Launch Your Token (optional)

  1. The Winter School Token launched in the previous section has three characteristics that can be customized.

    • Token name: Winter School Token
    • Token symbol: WST
    • Initial supply: 100 WST

    Note: The _initial_supply variable in MyToken.sol is set to 100 * (10**18) which equals \(100 \times 10^{18}\). This corresponds to 100 WST tokens because the default value for the number of decimals in OpenZeppelin's ERC20 implementation is 18. This is the same with Ether where 1 ETH = \(10^{18}\) wei.

  2. *Customize your token by changing the values in the constructor arguments and the initial supply. The following lines in MyToken.sol need to be changed.

    uint constant _initial_supply = ...;
    ...
    constructor() ERC20("Winter School Token", "WST") {
    
  3. Run the following commands to lauch your customized token contract.

    npx hardhat compile
    npx hardhat run scripts/deploy.js --network goerli
    

    Note: If you change the name of the contract from WinterSchoolToken to something else, remember to enter the new name as the argument to the ethers.getContractFactory in scripts/deploy.js.

  4. Go to https://goerli.etherscan.io/token/[Your Token Address] to see the token details.

Transfer Tokens using Metamask

  1. To transfer the newly created token using Metamask, you have to import it into Metamask. Click the Import tokens link in Metamask.

    Metamask Import Token Link

  2. Enter your token contract address in the textbox labeled Token contract address. When you move to the next input box, the Token symbol and Token decimal fields get automatically populated by Metamask. Click on Add custom token.

    Metamask Import Tokens Dialog

  3. The new token's balance should appear in Metamask.

    Metamask Tokens Display

  4. To transfer the new tokens, we need a recipient address. Create a new account in Metamask by clicking Create account in the account menu.

    Metamask Tokens Display

  5. Let the name of the new account be Account 2. Copy the address of Account 2.

  6. Switch back to Account 1. Click on the token's account balance to reveal the tranfer screen.

  7. Click on Send and paste Account 2's address. Specify an amount of tokens to transfer. You will have to click on Confirm to send the transfer transaction.

  8. You will have to import the token in Account 2 to see its balance (the same as step 1).

ERC-721 Tokens aka Non-Fungible Tokens

The ERC-721 token standard is a specification for non-fungible tokens (NFTs) on Ethereum. A token is said to be non-fungible if each unit of the token has a unique identifier and cannot be exchanged for another unit.

Note: The ERC in ERC-721 stands for Ethereum Request for Comments. They are Ethereum's analog of Request for Comments (RFCs), that are used to specify Internet standards. When the Github issue to discuss the non-fungible token standard was created in the Ethereum Improvement Proposals repository, it was assigned issue number 721. Hence the name ERC-721.

The full ERC-721 token specification can be found in Ethereum Improvement Proposal 721 (EIP-721).

In this chapter, you will be deploying your own NFT on Goerli testnet.

Acknowledgment: The contents on this chapter are based on the How to Create an NFT on Ethereum Tutorial by Alchemy.

Setting up the ERC-721 Project

To setup a project that uses Hardhat for ERC-721 contract development, do the following:

  1. Create a new directory and enter it.

    mkdir my-nft
    cd my-nft
    
  2. Initialize a new Node.js project.

    npm init -y
    

    The directory should contain a single file called package.json.

  3. Install Hardhat by running the following command in the my-nft directory.

    npm install --save-dev hardhat
    

    The package.json file will now have a hardhat section under devDependencies.

  4. Create a Hardhat project by running the following command. Choose the Create an empty hardhat.config.js option.

    npx hardhat
    

    The directory will have a file called hardhat.config.js with the following contents.

    /** @type import('hardhat/config').HardhatUserConfig */
    module.exports = {
        solidity: "0.8.17",
    };
    

    The number 0.8.17 specifies the version of the Solidity compiler.

  5. Install the dotenv package in your project directory.

    npm install dotenv --save
    
  6. Create a file called .env in the project directory with the following contents.

    API_URL = "https://eth-goerli.g.alchemy.com/v2/your-api-key"
    API_KEY = "your-api-key"
    PRIVATE_KEY = "your-metamask-private-key"
    

    Follow these instructions to export your private key from Metamask. The API_URL and API_KEY values need to be copied from your Alchemy account.

    NOTE: If you are going to push the project code to a public Github/Gitlab repository, remember to add the .env file to your .gitignore.

  7. Install Ethers.js by running the following command

    npm install --save-dev @nomiclabs/hardhat-ethers "ethers@^5.0.0"
    
  8. Update the hardhat.config.js file to have the following content.

    /**
    * @type import('hardhat/config').HardhatUserConfig
    */
    
    require('dotenv').config();
    require("@nomiclabs/hardhat-ethers");
    
    const { API_URL, PRIVATE_KEY } = process.env;
    
    module.exports = {
    solidity: "0.8.17",
    defaultNetwork: "goerli",
    networks: {
        hardhat: {},
        goerli: {
            url: API_URL,
            accounts: [`0x${PRIVATE_KEY}`]
        }
    },
    }
    
  9. Your ERC-721 token will be based on the implementation by OpenZeppelin. Install the Node.js package containing OpenZeppelin's contracts by running the following command in the project directory.

    npm install @openzeppelin/contracts
    

    The installed contracts can be found in the node_modules directory in your project directory. The path will be node_modules/@openzeppelin/contracts/. We will be inheriting the ERC-20 implementation at node_modules/@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol.

Deploying the NFT (ERC-721) Contract

To deploy your ERC-721 token, do the following:

  1. In the project directory, create a contracts directory.

    mkdir contracts
    
  2. Create a new file in the contracts directory called MyNFT.sol.

  3. Copy and paste the following code into MyNFT.sol.

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.17;
    
    import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
    import "@openzeppelin/contracts/utils/Counters.sol";
    import "@openzeppelin/contracts/access/Ownable.sol";
    
    contract WinterSchoolNFT is ERC721URIStorage, Ownable {
        using Counters for Counters.Counter;
        Counters.Counter private _tokenIds;
    
        constructor() ERC721("Winter School NFT", "WSNFT") {}
    
        function mintNFT(address recipient, string memory tokenURI)
            public
            onlyOwner
            returns (uint256)
        {
            _tokenIds.increment();
    
            uint256 newItemId = _tokenIds.current();
            _mint(recipient, newItemId);
            _setTokenURI(newItemId, tokenURI);
    
            return newItemId;
        }
    }
    
  4. Compile the contract by running the following command.

    npx hardhat compile
    

    You should see a message saying Compiled 1 Solidity file successfully.

  5. Create a directory called scripts

    mkdir scripts
    
  6. Create a file called deploy.js in the scripts directory with the following content.

    async function main() {
        // Grab the contract factory 
        const MyNFT = await ethers.getContractFactory("WinterSchoolNFT");
    
        // Start deployment, returning a promise that resolves to a contract object
        const myNFT = await MyNFT.deploy(); // Instance of the contract 
        console.log("ERC-721 contract deployed to address:", myNFT.address);
    }
    
    main()
    .then(() => process.exit(0))
    .catch(error => {
        console.error(error);
        process.exit(1);
    });
    
  7. Deploy the contract by running the following command.

    npx hardhat run scripts/deploy.js --network goerli
    

    You should see a message of the following form. The address will be different in your case.

    ERC-721 contract deployed to address: 0x6898E26AD18e2DeA803E578a1F29C0f86bF3276a
    
  8. IMPORTANT: Create a variable called CONTRACT_ADDRESS in your .env file with value equal to your deployed contract address. Your .env file should look like this.

    API_URL = "https://eth-goerli.g.alchemy.com/v2/your-api-key"
    API_KEY = "your-api-key"
    PRIVATE_KEY = "your-metamask-private-key"
    CONTRACT_ADDRESS = "your-deployed-contract-address"
    
  9. Go to https://goerli.etherscan.io/token/[Your Token Address] to see the token details. Notice that you have to enter the address of the newly created token in the URL.

Customize and Launch Your Token (optional)

  1. The Winter School NFT launched in the previous section has two characteristics that can be customized.

    • NFT Collection name: Winter School NFT
    • NFT Collection symbol: WSNST
  2. *Customize your NFT by changing the values in the constructor arguments. The following line in MyNFT.sol needs to be changed.

    constructor() ERC721("Winter School NFT", "WSNFT") {}
    
  3. Run the following commands to lauch your customized token contract.

    npx hardhat compile
    npx hardhat run scripts/deploy.js --network goerli
    

    Note: If you change the name of the contract from WinterSchoolNFT to something else, remember to enter the new name as the argument to the ethers.getContractFactory in scripts/deploy.js.

  4. Go to https://goerli.etherscan.io/token/[Your Token Address] to see the token details.

Minting an NFT

To mint an NFT, do the following:

  1. Choose an image for your NFT. We will use the following image:

    Winter School NFT #0

  2. Create a free account in Pinata and upload your image there.

    Note: We will need to specify an image location when we mint our NFT. This image will be displayed by NFT explorer websites like OpenSea. While we could upload an image to a regular website and use that URL, the image will become inaccessible if the website goes down. Or someone could change the image located at that URL leading to undesirable effects.
    The convention in the NFT community is to upload the image to the Inter Planetary File System (IPFS). IPFS is a peer-to-peer network for storing files. IPFS using content addressing to identify files. This means that a cryptographic hash of a file's contents identify the file. Such file identifiers are called Content Identifiers (CIDs).

  3. Copy your image's CID. It will look like QmcssgBH1SeDTmmEw2N6JqzTzfNebo5tCLbzuk3gxfiuk6. For example, the image we will be using is available on the Pinata interface at https://gateway.pinata.cloud/ipfs/QmcssgBH1SeDTmmEw2N6JqzTzfNebo5tCLbzuk3gxfiuk6. The CID corresponds to the alphanumeric string starting with Qm....

  4. Create a NFT metadata JSON file in the following format. In the value corresponding to the image key, enter the CID of your image after the ipfs://. You can also enter other values for the name, description, and external_url keys.

    {
        "name": "ACM Winter School Attendee #0",
        "description": "NFT given to an attendee of the 2022 ACM Winter School on Topics in Digital Trust, organized by Trust Lab, IIT Bombay",
        "external_url": "https://trustlab.iitb.ac.in/event/winter-school-2022",
        "image": "ipfs://QmcssgBH1SeDTmmEw2N6JqzTzfNebo5tCLbzuk3gxfiuk6"
    }
    
  5. Upload the metadata JSON file to Pinata and get its CID. For example, the above file is at location https://gateway.pinata.cloud/ipfs/QmfHMf1Qe5o9TRW1HgSoGqcFNosKLmBprbvH4T3SM3w5Hy. The CID corresponds to QmfHMf1Qe5o9TRW1HgSoGqcFNosKLmBprbvH4T3SM3w5Hy.

  6. Create a file called mint-nft.js in the scripts directory with the following content.

    const hre = require("hardhat");
    const API_KEY = process.env.API_KEY;
    const privateKey = process.env.PRIVATE_KEY;
    const contractAddress = process.env.CONTRACT_ADDRESS;
    
    
    // Define an Alchemy Provider
    const provider = new hre.ethers.providers.AlchemyProvider('goerli', API_KEY)
    
    // Get contract ABI file
    const contract = require("../artifacts/contracts/MyNFT.sol/WinterSchoolNFT.json");
    
    // Create a signer
    const signer = new hre.ethers.Wallet(privateKey, provider)
    
    // Get contract ABI and address
    const abi = contract.abi
    
    // Create a contract instance
    const myNftContract = new hre.ethers.Contract(contractAddress, abi, signer)
    
    async function main() {
        const args = process.argv;
        if (args.length != 3) {
            console.log("Provide an IPFS hash of the NFT metadata as an argument")
            process.exit(0);
        }
        const ipfsHash = args[2];
        const tokenUri = "ipfs://" + ipfsHash;
    
        console.log("Minting NFT...");
        let nftTxn = await myNftContract.mintNFT(signer.address, tokenUri)
        await nftTxn.wait()
        console.log(`NFT Minted! Check it out at: https://goerli.etherscan.io/tx/${nftTxn.hash}`)
    }
        
    main().catch((error) => {
        console.error(error);
        process.exitCode = 1;
    });
    
  7. Run the following command with the <metadata-ipfs-hash> value replaced with your metadata file's CID. Don't use the image CID.

    node scripts/mint-nft.js <metadata-ipfs-hash>
    

    After about a half a minute, you should see a message saying

    NFT Minted! Check it out at: https://goerli.etherscan.io/tx/0xa9da090c5de3eb59e649cc04dd362bd4d09f454c8a37197d107f1603e4c910f5
    

Viewing the NFTs on OpenSea

To see your NFT collection on OpenSea, do the following:

  1. Go to https://testnets.opensea.io/
  2. Enter your contract address in the search bar.
  3. Your NFT collection should show up. For example, the NFT collection created in this guide is available here.