Lazy Minting NFT - Solidity, Hardhat
Tags: solidity, web3, nft, hardhat
Published: 2022-09-03 04:46:38

## What is NFT ? 
Non-fungible tokens, often referred to as NFTs, are blockchain-based tokens that each represent a unique asset like a piece of art, digital content, or media.

## Why Lazy Minting ?
Before understanding what is lazy minting lets understand why we really need it.When minting an NFT the owner needs to pay Gas fee which is a fee that the creator or the one who made the NFTs must pay in exchange for the computational energy needed to process and validate transactions on the blockchain.So with lazy minting we don't have to pay a gas fee when listing an NFT, We only pay gas fee when we actually mint the nft, once the asset is purchased and transfered on chain.

##How it works
Usually when we mint an NFT we call the contract function directly & mint NFT on Chain.But in case of Lazy minting,Creator prepares a cryptographic signature with their wallet private key.
That cryptographic signature is know as "Voucher" which is then used to redeem NFT.It may also include some additional information required while minting the NFT on Chain. 

## Tech Stack
Solidity & Hardhat for smart contract development
React Js & Tailwind CSS for dapp

##Let's start
By creating & understanding the signed voucher.In order to accomplish the signing we are going to use
[EIP-712: Typed structured data hashing and signing](https://eips.ethereum.org/EIPS/eip-712) 
Which allows us to standardize signing a typed data structure, Which then can be sent to smart contract to claim the NFT.

eg.
```
struct SomeVoucher {
  uint256 tokenId;
  uint256 someVariable;
  uint256 nftPrice;
  string uri;
  bytes signature;
}
```

Create a new directory called `lazymint`
```
> mkdir lazymint
> cd lazymint 
> yarn add -D hardhat

```

Next, Initialize hardhat development environment
```javascript
> yarn hardhat

888    888                      888 888               888
888    888                      888 888               888
888    888                      888 888               888
8888888888  8888b.  888d888 .d88888 88888b.   8888b.  888888
888    888     "88b 888P"  d88" 888 888 "88b     "88b 888
888    888 .d888888 888    888  888 888  888 .d888888 888
888    888 888  888 888    Y88b 888 888  888 888  888 Y88b.
888    888 "Y888888 888     "Y88888 888  888 "Y888888  "Y888

Welcome to Hardhat v2.10.2

? What do you want to do? … 
▸ Create a JavaScript project
  Create a TypeScript project
  Create an empty hardhat.config.js
  Quit

```
Select `Create a JavaScript project` and let it install all the dependencies. 

Next, lets install a package called [hardhat-deploy](https://github.com/wighawag/hardhat-deploy) which makes working with hardhat 2x easier & fun 👻

```
> yarn add -D hardhat-deploy
```
And add the following statement to your hardhat.config.js:
```
require('hardhat-deploy');

```
Next,
```
yarn add -D @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers ethers
```
All the hardhat.config.js changes can be found in my [REPO](https://github.com/Hussainzz/lazy-mint-contract).

Next, Create a new contract in contracts directory feel free to give any name, i'll call it `LazyMint.sol`

```javascript
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

contract LazyMint is ERC721, ERC721URIStorage, Ownable, EIP712, AccessControl {

    error OnlyMinter(address to);
    error NotEnoughValue(address to, uint256);
    error NoFundsToWithdraw(uint256 balance);
    error FailedToWithdraw(bool sent);

    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    string private constant SIGNING_DOMAIN = "Lazy-Domain";
    string private constant SIGNING_VERSION = "1";

    event NewMint(address indexed to, uint256 tokenId);
    event FundsWithdrawn(address indexed owner, uint256 amount);

    struct LazyMintVoucher{
        uint256 tokenId;
        uint256 price;
        string uri;
        bytes signature;
    }

    constructor(address minter) ERC721("LazyMint", "MTK") EIP712(SIGNING_DOMAIN, SIGNING_VERSION) {
        _setupRole(MINTER_ROLE, minter);
    }
    

    function mintNFT(address _to, LazyMintVoucher calldata _voucher) public payable {
        address signer = _verify(_voucher);
        if(hasRole(MINTER_ROLE, signer)){
            if(msg.value >= _voucher.price){
                _safeMint(_to, _voucher.tokenId);
                _setTokenURI(_voucher.tokenId, _voucher.uri);
                emit NewMint(_to, _voucher.tokenId);
            }else{
                revert NotEnoughValue(_to, msg.value);
            }
        }else{
            revert OnlyMinter(_to);
        }
    }

    function _hash(LazyMintVoucher calldata voucher) internal view returns(bytes32){
        return _hashTypedDataV4(keccak256(abi.encode(
            //function selector
            keccak256("LazyMintVoucher(uint256 tokenId,uint256 price,string uri)"),
            voucher.tokenId,
            voucher.price,
            keccak256(bytes(voucher.uri))
        )));
    }

    function _verify(LazyMintVoucher calldata voucher) internal view returns(address){
        bytes32 digest = _hash(voucher);
        //returns signer
        return ECDSA.recover(digest, voucher.signature);
    }

    // The following functions are overrides required by Solidity.

    function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControl, ERC721) returns (bool){
        return ERC721.supportsInterface(interfaceId) || AccessControl.supportsInterface(interfaceId);
    }

    function withdrawFunds() public payable onlyOwner{
        uint256 balance = address(this).balance;
        if(balance <= 0){revert NoFundsToWithdraw(balance);}
        (bool sent,) = msg.sender.call{value: balance}("");
        if(!sent){revert FailedToWithdraw(sent);}
        emit FundsWithdrawn(msg.sender, balance);
    }
}
```
Let's quickly go through the `LazyMint.sol` contract.
i am using `ERC721` & `ERC721URIStorage` from [OpenZeppelin Contracts](https://github.com/OpenZeppelin/openzeppelin-contracts)

**ERC721** - is a standard for representing ownership of non-fungible tokens, that is, where each token id is unique.

**ERC721URIStorage** - is an implementation of ERC721 that includes the metadata standard extensions ( IERC721Metadata ) as well as a mechanism for per-token metadata. 

In Simple words, contract implemented without `ERC721Storage` generates tokenURI for tokenId on fly by concatenating baseURI + tokenID.In case of contracts using `ERC721Storage` we provide the tokenURI(metadata) when minting the token which is then stored on-chain.

Next, i am using Ownable & AccessControl.
[**Ownable**](https://docs.openzeppelin.com/contracts/2.x/api/ownership) - which enables contract exclusive access to functions eg. OnlyOwner has access to certain functions.

[**AccessControl**](https://docs.openzeppelin.com/contracts/2.x/access-control) - Which allows us to assign certain roles to address like in our case,Specific address can sign vouchers to mint NFTs, for that we can create a role called `MINTER`. or an admin for the overall contract.

Next, i have defined some custom errors which were introduced in `solidity version 0.8.4`
```javascript
 error OnlyMinter(address to);
 error NotEnoughValue(address to, uint256);
 error NoFundsToWithdraw(uint256 balance);
 error FailedToWithdraw(bool sent);
```
Next, we define the minter role, signing domain & version.
so that when minting we can check that the signed voucher explained above was signed by minter address.And includes the same domain and version.
```javascript
 bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    string private constant SIGNING_DOMAIN = "Lazy-Domain";
    string private constant SIGNING_VERSION = "1";
```
Next, lets take a look at the Voucher which we defined in solidity using `struct`
```javascript
struct LazyMintVoucher{
   uint256 tokenId;
   uint256 price;
   string uri;
   bytes signature;
}
```
Which includes `tokenId` to mint, price, uri[usually a url pointing to metadata] for the nft,and signature.Let's Understand the mintNFT function to understand how this voucher does all the magic 🪄.

```javascript
function mintNFT(address _to, LazyMintVoucher calldata _voucher) public payable {
  address signer = _verify(_voucher);
  if(hasRole(MINTER_ROLE, signer)){
     if(msg.value >= _voucher.price){
        _safeMint(_to, _voucher.tokenId);
        _setTokenURI(_voucher.tokenId, _voucher.uri);
        emit NewMint(_to, _voucher.tokenId);
     }else{
         revert NotEnoughValue(_to, msg.value);
     }
  }else{
     revert OnlyMinter(_to);
  }
}
```
the important part of the voucher is signature which we sign off-chain with the Minters private key which has all the additional data in the same order defined in the above `struct`.Then in `mintNFT` function which expects two arguments `_to` & `_voucher`.
nft will be minted for `_to` address.And voucher basically helps redeem the NFT.First line in `mintNFT` function is to verify the signature from the voucher. to verify the signer we call a cryptographic function from `draft-EIP712.sol` called `_hashTypedDataV4` which takes in our hashed version of our voucher struct and the return value then can be used with a `recover` function from [`Elliptic Curve Digital Signature Algorithm (ECDSA) `](https://docs.openzeppelin.com/contracts/2.x/utilities) to get the signer address.We then compare the recovered signer to check if it matches our `MINTER` address & also check if that signer has the minter_role, If Yes ? we proceed and check the value(eth passed) matches the price value mentioned in the voucher. If yes ? we go ahead and mint the token and emit an event `emit NewMint(_to, _voucher.tokenId)`.

That's it for the Voucher Verification Magic Trick  🪄🪄🪄

Next, we have a `withdrawFunds` which allows only the contract owner to withdraw funds, if any.

## How to sign a voucher ?
Open [createSalesOrder](https://github.com/Hussainzz/lazy-mint-contract/blob/main/scripts/createSalesOrder.js), go to the scripts folder open `createSalesOrder.js` which is a simple script to create vouchers.

First we get the signer account.
```javascript
const [signer] = await ethers.getSigners();

```
Next, We need the signing domain & version these values should same as one defined in the contract.
```javascript
const SIGNING_DOMAIN = "Lazy-Domain";
const SIGNING_VERSION = "1";
const MINTER = signer;
```
As per EIP 712 we need a domain, which is made up of chainId, ContractAddress, domain & the version.

```javascript
const domain = {
  name: SIGNING_DOMAIN,
  version: SIGNING_VERSION,
  verifyingContract: lazyMint.address,
  chainId: network.config.chainId
}    
let voucher = {
  tokenId: i,
  price: PRICE,
  uri: META_DATA_URI
}
```
`META_DATA_URI` i have already pinned it to [ipfs](https://ipfs.io/ipfs/QmPE9bwcVXiCxtH5q5R3mC2DpDkyDbJUZKmNAbAkhZVfWE)

```javascript
`ghost_metadata.json`
{
    "description": "Boooooo",
    "image": "https://ipfs.io/ipfs/QmeueVyGRuTH939fPhGcPC8iF6HYhRixGBRmEgiZqFUvEW",
    "name": "Baby Ghost",
    "attributes": [
        {
            "trait_type": "cuteness",
            "value": 100
        }
    ]
}
```


Next, we need the `createVoucher` function open `voucherHelper.js`. In order to create a signed voucher we need three arguments `domain` `voucher` `types`. Now types is nothing but the `struct` voucher which we have defined in solidity smart contract.Make sure the name of the `struct` & also the order of variables match.

```javascript
const types = {
    LazyMintVoucher:[
        {name: "tokenId", type:"uint256"},
        {name: "price", type:"uint256"},
        {name: "uri", type:"string"}
    ]
}
```
Next, we sign the voucher with the MINTER(signer) wallet & get the signed voucher 🖋️
```javascript
const signature = await signer._signTypedData(domain, types, voucher);
```
In `createSalesOrder.js` at the end once i get the signed vouchers, In my case to keep things simple i am just saving the signed vouchers inside a file called `NFTVouchers.json` which i am creating directly in my dapp `'../lazymintdapp/src/NFTVouchers.json'`.Ideally in real life scenario you store these signed vouchers in your centralized DB 🤪

Done! 🎉🎉

##Contract Tests 
I have written some unit tests which can be found in `test/LazyMintUnit.test.js`
```
> yarn hardhat test
```
![hardhat unit tests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tml1ppmdxt9vct7iox9p.png)

##Deployed Contract & Demo

[https://ghost-mint.vercel.app/](https://ghost-mint.vercel.app/)

[https://mumbai.polygonscan.com/address/0x3077B8941e6337091FbB2E3216B1D5797B065C71](https://mumbai.polygonscan.com/address/0x3077B8941e6337091FbB2E3216B1D5797B065C71)

[**CODE REPO**](https://github.com/Hussainzz/lazy-mint-contract) Don't forget to drop a ⭐⭐⭐⭐⭐


![Ghost Dapp Demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/loeq2cev018q7kk3xsh2.png)


## LazyNFTs Dapp
i won't be going through the dapp completely.But for reactjs dapp development i used `create-react-app`.

**Packages**
- **Wagmi**: https://wagmi.sh/ for contract interaction, Cool and easy react hooks.And their documentation has lots of examples which makes it way easier to implement. 🦍

##Resources
There are multiple ways in which one can approach the lazy minting flow. this was all my learning experience.Here are some resources which help me understand lazy minting.

- https://www.alchemy.com/overviews/lazy-minting
- https://eips.ethereum.org/EIPS/eip-712
- https://medium.com/metamask/eip712-is-coming-what-to-expect-and-how-to-use-it-bb92fd1a7a26
- https://nftschool.dev/tutorial/lazy-minting

That's it on lazy minting. 👻 

![FunnyJIM](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0s5g9p3t8u26txmt80ou.gif)



[![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/guidelines/download-assets-sm-1.svg)](https://www.buymeacoffee.com/shkhussainb)