How to add ETH payments to NFTs

In this article we'll create an NFT contract where users can buy NFTs for a fixed price

When it comes to NFT contracts, depending on the specific project, we always want to use ERC721A if the user has the possibility of minting more then one NFT at time as it's way more cheaper then the normal OpenZeppelin standard integration.

We start by defining the license, solidity version and importing our contracts from the workspace image

I'm not using Ownable to show how to implement a similar system but you want to stick to the original implementation on real projects.

Let's continue by defining our contract, the storage and the constructor

We created our NFT contract called NFT heheboi
Inherited ERC721A and ERC721AQueryable (This one will give us some more useful functions expecially on the off-chain side of things, external dapps can be build without too much complexity as the data is served directly by the contract itself --> more info [here]LINK ERC721AEXT)

image Next we define our storage, the core storage of NFTs are inside ERC721A, here we just define

  • What's the price for a single mint
  • What is the max limit of NFTs minted
  • Who is the owner of the NFT contract
  • What is the base url for the metadata json
  • Is the contract fully decentralized? ![[KKTeam Lead Magnet/ERC721/How to create an NFT contract where users can buy NFT with ETH/easy/images/2.png]] Then we define the following errors
  • You cannot buy with less ETH then mintPrice
  • You cannot buy with more ETH then mintPrice
  • ETH failed to reach the owner address
  • Only the owner can do that action
  • This action is not permitted as the contract is decentralized
  • Max mint reached, nobody can create new NFTs

One single event to serve off-chain data about minting

Finally, we create our constructor to setup the storage on deployment with:

  • Name of the NFT contract
  • Symbol of the NFT contract
  • Price in WEI for a single mint
  • Max supply of NFTs
  • Base Url for the metadata json

We now jump on the main logics First we add our onlyOwner modifier, this will let only the owner call certain functions, only if the contract is centralized image

Create the mint function with all the checks before minting the NFTs to the user image

We leave the user choose who will receive the NFTs.
This enable someone to "gift" NFTs if he wants without doing two separate transactions.

For the URI, we override the _baseURI() function from ERC721A, pointing it to our baseURI storage variable.

setBaseUri instead will let the owner switch the metadata if needed.
It's a design choice how to set the metadata.
In this specific case, the metadata must be set once all the mints are done.
There's a whole topic about metadata that won't be covered in this article.
image

Lastly we write the two functions to change owner & decentralize the contract.
Once the contract is decentralized, the owner loses all his privileges (to change URI and the owner in this case) image

The contract is ready!
We must write some tests now... 16 exactly.
lesgoooo!

npx hardhat test

image

We're gonna use fixtures this time!
This means our test will restart from the original state we define, each time!

We import Chai & hardhat
Then we create a "describe" block with three global variables

  • nftName, the name of our contract
  • nftSymbol, the symbol of our contract
  • BaseURI, the url of our metadatas
  • maxMints, the number we want to fix the max supply
  • mintPrice, the price of a single NFT in WEI (number * 10**18) image

Finally we create our fixture, in that async function we define the state we want to have on each test.

We simply deploy the NFT contract and return it along with the ower&user.

The following tests give 100% coverage to the NFT contract.
(While 100% coverage is a good thing to do, it's not always a sign of security, all depends on the quality of the tests written)

npx hardhat coverage

image

First we test our deployment setup
Keep in mind the deployment is done by the fixture in the first row. image

Test the mint with an amount > 1
Checks:

  • if the mint event is emitted
  • if the balance of the user is incremented
  • if the ETH balance of the owner is incremented image

Test the metadata Checks:

  • metadata should revert if the token does not exists
  • We take the first user owned NFT and check it's metadata, it's ID 0 but this time should have the correct value (and not revert) image

Change the metadata base URL
Checks:

  • We check if the new metadata is correct image

Buy 1000 NFTs with 10 random accounts
This tests is an edge-case where only 10 different accounts mint all the supply.
Sticking to the same accounts during the tests it's not always a good thing, so we try with some random ones there.
Checks for each account:

  • Balance should be correct
  • Supply should reflect the mints image

Change owner address
Checks:

  • We check if the new owner address is correct image

From now on, all the following tests goes against a revert transaction.

We cannot buy if the max mint cap is reached
Checks:

  • Any call to mint() function should fail if the supply is 1000 image

We cannot but with a lower price
Checks:

  • Buying with a lower price should fail image

We cannot buy with an higher price
Checks:

  • Buying with an higher price should fail image

We cannot buy 0 NFTs
Checks:

  • Buying with 0 gasValue should fail (using ERC721A revert!) image

An user should not be able to change baseURI
Checks:

  • calling setBaseURI as user should fail image

An user should not be able to decentralize the contract
Checks:

  • calling decentralize() as user should fail image

An user cannot change the owner address
Checks:

  • calling setOwner() as user should fail image

This is an edge-case that won't likely happen but we have to test what happens if the ETH call fails (spoiler: nobody can mint)
Let's write a bad contract that reverts the tx if you send it ETH
image

  • The normal mint function should fail image

Owner cannot change baseURI after calling decentralization()
Checks:

  • Check if the new uri is correctly set
  • calling setBaseURI() as owner should fail image

Owner cannot change owner address after calling decentralization()
Checks:

  • calling setOwner() as owner should fail image

And we are done!