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
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)
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
Create the mint function with all the checks before minting the NFTs to the user
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.
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)
The contract is ready!
We must write some tests now... 16 exactly.
lesgoooo!
npx hardhat test
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)
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
First we test our deployment setup
Keep in mind the deployment is done by the fixture in the first row.
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
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)
Change the metadata base URL
Checks:
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:
Change owner address
Checks:
From now on, all the following tests goes against a revert transaction.
We cannot buy if the max mint cap is reached
Checks:
We cannot but with a lower price
Checks:
We cannot buy with an higher price
Checks:
We cannot buy 0 NFTs
Checks:
An user should not be able to change baseURI
Checks:
An user should not be able to decentralize the contract
Checks:
An user cannot change the owner address
Checks:
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
Owner cannot change baseURI after calling decentralization()
Checks:
Owner cannot change owner address after calling decentralization()
Checks:
And we are done!