Skip to main content

4. Smart contract improvements

In this chapter of the tutorial, we will work on setting a maximum supply for the collection and adding a price to mint tokens from the contract.

But first, let's improve our developer quality of life by making it easy to read and call our smart contract by verifying it on Energi Block Explorer.

4.1 Verifying your smart contract on Energi Block Explorer

Verifying a smart contract has several benefits: It improves our quality of life as developers since we can directly read and interact with a verified smart contract on Energi Block Explorer. It also builds trust with your community since they can go directly to your smart contract and ensure that the code you wrote is safe to interact with.

Hardhat and Energi Block Explorer have made it very easy to verify smart contracts by providing an extension package that automatically adds the appropriate verification tasks to the Hardhat CLI. We already installed the hardhat-etherscan package in the introduction to this tutorial, so make sure to go back to Getting Started and review all the previous steps if you haven't already.

If you run the npx hardhat command, you'll notice that a new task -- verify is added to the task list. Run the following command to verify the contract.

export NFT_CONTRACT_ADDRESS=0x5A106e0E52B0F60101BAeBC255c1E5d5D9fA0ABd
npx hardhat verify $NFT_CONTRACT_ADDRESS
Response
Nothing to compile
Compiling 1 file with 0.8.18
Successfully submitted source code for contract
contracts/NFT.sol:NFT at 0x5A106e0E52B0F60101BAeBC255c1E5d5D9fA0ABd
for verification on the block explorer. Waiting for verification result...

Successfully verified contract Foo on Etherscan.
https://explorer.test.energi.network/address/0x5A106e0E52B0F60101BAeBC255c1E5d5D9fA0ABd/contracts

You can now browse over to that generated Energi Block Explorer link and view your code on the decentralized web!

You can even make smart contract calls directly after connecting to a web3 wallet:

4.2 Setting a token supply limit

Many NFT projects like to limit the total supply of mintable tokens for various reasons. Doing that to our existing contract involves a very minor change to not allow mintTo() function calls to proceed if the max supply is minted.

contracts/NFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract NFT is ERC721 {
using Counters for Counters.Counter;

// Constants
uint256 public constant TOTAL_SUPPLY = 10_000;

Counters.Counter private currentTokenId;

/// @dev Base token URI used as a prefix by tokenURI().
string public baseTokenURI;

constructor() ERC721("NFTTutorial", "NFT") {
baseTokenURI = "";
}

function mintTo(address recipient) public returns (uint256) {
uint256 tokenId = currentTokenId.current();
require(tokenId < TOTAL_SUPPLY, "Max supply reached");

currentTokenId.increment();
uint256 newItemId = currentTokenId.current();
_safeMint(recipient, newItemId);
return newItemId;
}

/// @dev Returns an URI for a given token ID
function _baseURI() internal view virtual override returns (string memory) {
return baseTokenURI;
}

/// @dev Sets the base token URI prefix.
function setBaseTokenURI(string memory _baseTokenURI) public {
baseTokenURI = _baseTokenURI;
}
}

The require in line 24 will cause the function to not succeed in executing (and not charge users money) if the condition passed to it resolves to false.

4.3 Setting a price for minting your NFT

Many projects like to charge a cost to mint from their contract. Charging a specific amount to call a function is relatively easy, with a few caveats:

contracts/NFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract NFT is ERC721 {
using Counters for Counters.Counter;

// Constants
uint256 public constant TOTAL_SUPPLY = 10_000;
uint256 public constant MINT_PRICE = 0.08 ether;

Counters.Counter private currentTokenId;

/// @dev Base token URI used as a prefix by tokenURI().
string public baseTokenURI;

constructor() ERC721("NFTTutorial", "NFT") {
baseTokenURI = "";
}

function mintTo(address recipient) public payable returns (uint256) {
uint256 tokenId = currentTokenId.current();
require(tokenId < TOTAL_SUPPLY, "Max supply reached");
require(msg.value == MINT_PRICE, "Transaction value did not equal the mint price");

currentTokenId.increment();
uint256 newItemId = currentTokenId.current();
_safeMint(recipient, newItemId);
return newItemId;
}

/// @dev Returns an URI for a given token ID
function _baseURI() internal view virtual override returns (string memory) {
return baseTokenURI;
}

/// @dev Sets the base token URI prefix.
function setBaseTokenURI(string memory _baseTokenURI) public {
baseTokenURI = _baseTokenURI;
}
}

In addition to the new constant (line 12) and the new require() line 26, you should also add the payable modifier (line 23) to the function itself.

4.4 Withdrawing Funds

Your contract now charges 0.08 NRG to call the mintTo() function. Every time users call that function, NRG will be transferred to the smart contract address. Unfortunately, that NRG is now "locked" in that smart contract, without an easy way for you to transfer that NRG out of that contract. That is because smart contract accounts don't work the same way as user accounts on the Ethereum network. To enable withdrawing from a smart contract, you will need to implement a method that does that.

Unfortunately, smart contract development in Solidity is prone to abuse from an exploit called the Reentrency Problem. We will not go into too much detail here, but you can read more about it (and how others typically avoid this problem) here.

Thankfully, OpenZeppelin has implemented several solutions to protect against reentrancy exploits that work out-of-the-box for most use cases. To keep things simple, we will use their PullPayment (line 6) implementation in our NFT.sol smart contract.

contracts/NFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/security/PullPayment.sol";

contract NFT is ERC721, PullPayment {
using Counters for Counters.Counter;

// Constants
uint256 public constant TOTAL_SUPPLY = 10_000;
uint256 public constant MINT_PRICE = 0.08 ether;

Counters.Counter private currentTokenId;

/// @dev Base token URI used as a prefix by tokenURI().
string public baseTokenURI;

constructor() ERC721("NFTTutorial", "NFT") {
baseTokenURI = "";
}

function mintTo(address recipient) public payable returns (uint256) {
uint256 tokenId = currentTokenId.current();
require(tokenId < TOTAL_SUPPLY, "Max supply reached");
require(msg.value == MINT_PRICE, "Transaction value did not equal the mint price");

currentTokenId.increment();
uint256 newItemId = currentTokenId.current();
_safeMint(recipient, newItemId);
return newItemId;
}

/// @dev Returns an URI for a given token ID
function _baseURI() internal view virtual override returns (string memory) {
return baseTokenURI;
}

/// @dev Sets the base token URI prefix.
function setBaseTokenURI(string memory _baseTokenURI) public {
baseTokenURI = _baseTokenURI;
}
}

There is not much changed here, other than importing the PullPayment.sol (line 6) dependency and making our NFT contract extend that contract (line 8 - 22). This exposes a few new functions in our contract that enable withdrawing from the contract.

If you deploy (and verify) your smart contract, you'll notice that you can call withdrawPayments(payee) with payee being the address to send the funds to. If the smart contract has any funds in it, they will be send to that address.

4.5 Roles and Access

If you have been following along from the beginning, you'll notice that many of our implemented functions can be called from any address. This poses several dangerous security vulnerabilities, such as users other than yourself being able to withdraw funds from the smart contract.

Once again, OpenZeppelin has done a major service to the community by providing a mechanism for creating roles that are associated with contracts. Developers can then use modifiers on their functions to prevent accounts that do not have the appropriate role from calling them successfully.

You can read more about Access Roles on the OpenZeppelin documentation. For the sake of this tutorial, we will focus on the much simpler Ownable helper, but the two systems work similarly.

Making your contract Ownable (line 10) exposes a few new functions as well as a new modifier: onlyOwner (line 48 - 50). Adding this modifier to your functions will make it so that only you (or the owner) will be able to call that function. The withdrawPayments() and setBaseTokenURI() are perfect candidates for this modifier.

contracts/NFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/security/PullPayment.sol";
import "@openzeppelin/contracts/access/Ownable.sol";


contract NFT is ERC721, PullPayment, Ownable {
using Counters for Counters.Counter;

// Constants
uint256 public constant TOTAL_SUPPLY = 10_000;
uint256 public constant MINT_PRICE = 0.08 ether;

Counters.Counter private currentTokenId;

/// @dev Base token URI used as a prefix by tokenURI().
string public baseTokenURI;

constructor() ERC721("NFTTutorial", "NFT") {
baseTokenURI = "";
}

function mintTo(address recipient) public payable returns (uint256) {
uint256 tokenId = currentTokenId.current();
require(tokenId < TOTAL_SUPPLY, "Max supply reached");
require(msg.value == MINT_PRICE, "Transaction value did not equal the mint price");

currentTokenId.increment();
uint256 newItemId = currentTokenId.current();
_safeMint(recipient, newItemId);
return newItemId;
}

/// @dev Returns an URI for a given token ID
function _baseURI() internal view virtual override returns (string memory) {
return baseTokenURI;
}

/// @dev Sets the base token URI prefix.
function setBaseTokenURI(string memory _baseTokenURI) public onlyOwner {
baseTokenURI = _baseTokenURI;
}

/// @dev Overridden in order to make it an onlyOwner function
function withdrawPayments(address payable payee) public override onlyOwner virtual {
super.withdrawPayments(payee);
}
}

This is all that's needed! These two functions are now protected against non-owners calling them. The Ownership contract also exposes some useful helpers: renounceOwnership(), transferOwnership(), and isOwner().

4.6 Verify Contract

Ve

Deploy Smart Contract
npx hardhat deploy
Response
Contract deployed to address: 0x2A6D543610119f92775d3afe44A58816d11F0Da9
Verify Smart Contract
npx hardhat verify 0x2A6D543610119f92775d3afe44A58816d11F0Da9

If you get the following error message, go to Energi Block Explorer to validate the contract.

Response
Nothing to compile
Error in plugin @nomiclabs/hardhat-etherscan: The address provided as argument contains a contract, but its bytecode doesn't match any of your local contracts.

Possible causes are:
- Contract code changed after the deployment was executed. This includes code for seemingly unrelated contracts.
- A solidity file was added, moved, deleted or renamed after the deployment was executed. This includes files for seemingly unrelated contracts.
- Solidity compiler settings were modified after the deployment was executed (like the optimizer, target EVM, etc.).
- The given address is wrong.
- The selected network (energiTestnet) is wrong.

For more info run Hardhat with --show-stack-traces

On Energi Block Explorer look up the contract:

https://explorer.test.energi.network/address/0xe167F814661f3cf77e21737e766D4A70Ac8cD88e/contracts

Click Verify & Publish in the Code tab:

Select Via Flattened Source Code and click Next:

Select the "Compiler" version, select "No" Optimization, paste the smart contract and then scroll to the bottom of the page and click Verify & publish:

Once the contract is verified, you will see Write Contract. Select the tab. You will see the withdrawPayments(payee) function to send fund to the "payee".

https://explorer.test.energi.network/address/0x2A6D543610119f92775d3afe44A58816d11F0Da9/contracts

4.7 Wrapping up

The 4 chapters covered many topics in details. If you have any questions or need help, reach out to Energi Support.

All the scripts are available on the project repo on Gitlab under the chapter_four branch.

Future tutorials will feature more advanced topics such as optimizations you can make to reduce gas costs, setting up a minting website, and using the GonnaMakeIt SDK to create listings.