Creating Provenance on the Blockchain

Building an ERC-721 token on Ethereum with Solidity, Truffle, IPFS, and Pinata

Working in blockchain often requires one to explain amorphous concepts in a meaningful way. In my latest encounter with this challenge, I had to describe Non-Fungible Tokens (NFTs) to my 65+ year old mother. Having been a Product Manager in the tech industry, I am familiar with presenting complexities to individuals from a gambit of demographics and backgrounds. What I have found to be the most effective approach is to contextualize the concept to match the audience. In this case, my mother is a wonderful cook with an arsenal of recipes that span generations of our family. Today, these recipes are kept safe in a small, wooden chest stashed away in one of her various hiding places so that they remain “in the family.” Emotionally, these recipes are our family’s treasured possessions. Physically, they are a collection of data and a perfect vessel for establishing context for my mother to understand the wonderful world of NFTs.

The story I wove that day centered on the following question: What if you could store these recipes in perpetuity yet retain your ownership over them and even pass that ownership down as you see fit?

To illustrate the concept, I built and deployed a Non-Fungible Token based on the ERC-721 standard to the Ethereum blockchain using Solidity, Truffle, IPFS, and Pinata where my mother and I could see and interact with it using MetaMask.

To follow along, you can find the source code on my GitHub.

Creating our Non-Fungible Token

$ mkdir erc721-token-recipe
$ cd erc721-token-recipe/
$ npm init -y

We first create our project’s root directory. We then run npm init to create a package.json file and use the -y option to populate it with the default information.

$ truffle init

The truffle init command creates a variety of files and folders within our project necessary to use the tools provided by Truffle.

Now that we have set up the environment, we are ready to start writing the token! The token itself is a Solidity file that defines a smart contract, a core component of the Ethereum blockchain. Like the Internet, Ethereum is maintained and improved by a consortium of individuals. Improvements to the blockchain are submitted as proposals to undergo the Ethereum Improvement Proposals (EIP) process. Under this process, changes to the standards governing smart contracts are submitted as an Ethereum Request for Comments (ERC). In 2018, ERC-721 Non-Fungible Token Standard Accepted was created setting the standard for creating tokens on Ethereum of subjective value, e.g. a Holographic Charizard trading card != a Magikarp trading card, which built on the original ERC-20 standard that defined fungible tokens, e.g. 1 ETH = 1 ETH.

The specifics of ERC-721 tokens are defined in the standard. Rather than writing this ourselves, we will use OpenZeppelin who provides an implementation of the ERC-721 standard and adds security to ensure it is safe to use. Run the following installation in the root directory of our project.

$ npm install @openzeppelin/contracts

This command creates a new folder and installs all of the prebuilt contracts provided by OpenZeppelin. Let’s use this to build our NFT now.

$ touch contracts/FamilyRecipe.sol

In the FamilyRecipe.sol file, we define the smart contract.

pragma solidity ^0.6.4;
import ‘@openzeppelin/contracts/token/ERC721/ERC721.sol’;
import ‘@openzeppelin/contracts/utils/Counters.sol’;
contract FamilyRecipe is ERC721{
using Counters for Counters.Counter;
Counters.Counter private _counter;
mapping(string => uint8) public hashes;

constructor() public ERC721(‘FamilyRecipe’, ‘REC’) {

function mintREC(address recipient, string memory hash, string memory metadata) public returns(uint256) {
hashes[hash] != 1,
“Hash has already been used!”
hashes[hash] = 1;
uint256 newRECId = _counter.current();
_mint(recipient, newRECId);
_setTokenURI(newRECId, metadata);
return newRECId;

As you can see, we first import OpenZeppelin’s implementation of the ERC-721 token, aptly named ERC721, as well as another function that we will use later in our smart contract. We then define our contract instance naming it FamilyRecipe and use the is keyword provided by Solidity to declare it as an instance of ERC721 so that it will inherit the properties and methods of the standard.

We now define the functionality of our smart contract. We first include the Counters library within our contract. This library provides counters that can only be incremented or decremented by one to achieve a variety of use cases. We will use this functionality to issue IDs for our ERC-721 token. We include this library with the using keyword provided by Solidity. This keyword allows us to use a library within a contract.

$ open node_modules/\@openzeppelin/contracts/utils/Counters.sol

We can see by opening the Counters.sol file that Counters is indeed a library and it provides methods like current and increment which we will use later in our contract. Writing using Counters for Counters.Counter allows us to use any of the methods described in the Counters library on any value of type Counter within our FamilyRecipe contract. We do this in the proceeding line where we declare a private variable named _counter of type Counters.Counter which we will use to uniquely identify each of our tokens as they are minted. We then declare a public variable hashes of type mapping which will enable us to track that freshly minted tokens do not contain the same data as previously minted tokens.

Next, we add a constructor function for our FamilyRecipe contract. This function will run when our contract is deployed. Our contract inherits a constructor function from its ERC721 parent which we will repurpose for our contract. Let’s take a look at this function.

$ open node_modules/\@openzeppelin/contracts/token/ERC721/ERC721.sol

Opening the ERC721.sol file reveals its contents.

constructor (string memory name_, string memory symbol_) public {
_name = name_;
_symbol = symbol_;

The inherited constructor takes two string variables as arguments. These will be used as the name and symbol when viewing these with Ethereum clients like MetaMask. We pass ‘FamilyRecipe’ and ‘REC’ as arguments here.

Now it’s time to define a function to mint our REC tokens! There are a few pieces of information that we need to mint the token, so we add these as parameters to our mintREC function. A key property of Non-Fungible Tokens as indicated in the standard is that “each asset is distinct (non-fungible)…” We are going to ensure that every REC token represents a non-fungible asset using hashes. Hashes are created as a result of a hashing function which takes large amounts of data and condenses them into an alphanumeric string to represent that data. Any change to the underlying data will result in a different hash. We create a simple verification mechanism that requires that any hash passed to our minting function has not already been used by checking its value in the hashes mapping to 1. If the check passes, our function continues to process by assigning the hash a value of 1 in the hashes mapping so that it will revert the transaction if another REC token is attempted to be minted with that same hash.

Next, we assign the _counter property of our REC token a value using the increment method described above which will increment the _value property of the Counters library by one. We then create a new, local variable newRECId of type uint256 and assign it to the current value of the _value property using the current method also described above. We go about assigning the REC token an ID in this manner because the Counters library warns that the _value variable should never be accessed directly. Instead, we create a new instance of the Counter class, use the methods of that instance to increment _value, and assign our token an ID based on the value of that instance.

Next, we call the _mint method which our contract inherited from its ERC721 parent. Let’s take a look at this method.

$ open node_modules/\@openzeppelin/contracts/token/ERC721/ERC721.sol

Opening the ERC721.sol file reveals its contents.

function _mint(address to, uint256 tokenId) internal virtual {
require(to != address(0), “ERC721: mint to the zero address”);
require(!_exists(tokenId), “ERC721: token already minted”);
_beforeTokenTransfer(address(0), to, tokenId);
_tokenOwners.set(tokenId, to);
emit Transfer(address(0), to, tokenId);

The result of this method call is the creation of a REC token by reassigning ownership of that token from the address(0), i.e. the genesis of the Ethereum blockchain for which it is assumed that nobody now or ever will have the private key, to the address of the recipient of that token including the token’s ID. The definition of this method includes a view requirements such as that the token isn’t being minted to address(0) which would effectively make the token unusable and that the ID of the token doesn’t match an existing token. To call the _mint method, we must pass the address of the recipient and the token’s ID as arguments.

After minting the token, we call the _setTokenURI method which our contract inherited from its ERC721 parent. Let’s also take a look at this method.

$ open node_modules/\@openzeppelin/contracts/token/ERC721/ERC721.sol

Opening the ERC721.sol file reveals its contents.

function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual {
require(_exists(tokenId), “ERC721Metadata: URI set of nonexistent token”);
_tokenURIs[tokenId] = _tokenURI;

The result of this function is that our newly minted REC token is bound via its ID to the token’s metadata which asserts ownership and defines various properties.

And finally, if all requirements of our mintREC function are met, we are returned the ID of the new REC token which we can use to gather multitudes of data about that token. We will dive deeper into this in a later section.

Deploying our Non-Fungible Token to the Ethereum blockchain

The first step when deploying to the Ethereum blockchain is to compile our contracts down into the .abi and .bin files necessary for deployment. Truffle provides tools for us to do this, but first we are going to need to define our configuration needs. We will do so in the truffle-cofig.js file that was created when we initiated our Truffle project in the Creating our Non-Fungible Token section above. Replace the boilerplate code in that file with the following trimmed down code.

module.exports = {
networks: {
development: {
host: “”,
port: 9545,
network_id: “*”,
compilers: {
solc: {
version: “0.6.4”

In the configuration above, we point Truffle to the correct blockchain instance which will be located at which is Truffle’s default port. We also define the version of the Solidity compiler with the version that we defined in our smart contract using the pragma keyword.

Truffle adds a step after compilation which they refer to as migration. The first migration that our application runs was predefined when we initiated our Truffle project and can be found in the migrations directory in the 1_initial_migration.js file. This deploys the Migrations contract that also came in the initialization of Truffle. To deploy our FamilyRecipe contract, we will need to add a second migration file.

$ touch migrations/2_deploy_contracts.js

Let’s add the directions for the migration to this new file.

var FamilyRecipe = artifacts.require(‘FamilyRecipe’);
module.exports = function(deployer) {

Alright we are ready to deploy! Let’s first spin up a local instance of Ethereum using Truffle.

$ truffle developTruffle Develop started at
(0) 0x762fceb6848a20f118026fa4d63868a97381e575
(1) 0x726a3af4a354d3fa5246e6e9a767376d9c1a9971
(2) 0xb1e65a460863574d8971779ed416e209962e2135
(3) 0xd2fe247261350da249bdb21a2f63023f66204a50
(4) 0x0ec1e4a1041a05e1b6a896c1e354a98ed32eaca8
(5) 0xe03266378dbf4cc42d738d56d772796988e51f99
(6) 0x3a3734a9f15992b07c227e554ece641ef61a3a58
(7) 0x1eff998f196950a503762d6ed5d4cea9475026a5
(8) 0x90b398adc7dbd68c83de4fcbd676308e5c62ce6c
(9) 0xe3b5f3d0b3cf76aace406b7e269ed83ac63bd68f
Private Keys:
(0) c66b56212b0c19539f2d93aaeca2538578ad7f8e847cf7c6a043733614ebd3b4
(1) 37e9b79f9115c41f9bd100b6d791f130c61a52132ae2e47920432287a379c4d6
(2) 17168cf9944a9f36af688fe00f5e0f27b966e65b594d2d4ed4c3fff287690913
(3) 635dcc6b3130d29ccda99e0649f68a18e2076ddb0f074f79f5b787ee1c030c64
(4) 3bda71f106891ab04f170718c409f48c96c80b1c7a69ab06c66598f387d40f44
(5) f2a21eaa515294ee9c4b8276c829b531929adaf0913612dd6af4bbd47552a864
(6) 0ca58d43481dfafe60936f07d964f4a168e3a6b6334b929c9b082f8cb16b2317
(7) 19556056dfcade57602923452c1a4fed90e5270eebd67cabe4f87c9ab2ac4fe2
(8) bfe5ad396421add15e4ddd69630e19c7d0816cb05d9b8aade38c2932f25ef284
(9) 7ae79d9aa69e6ea2c1f1c1cc75489eb3025575d57471f8d6216f0bc89a3b7fc0
Mnemonic: awkward jump actress purity solid about number loud gossip intact cherry problem

Now that we have a local instance of the Ethereum blockchain, we are ready to deploy our smart contract. We do so within the Truffle development console.

truffle(develop)> truffle compile
truffle(develop)> truffle migrate
Compiling your contracts…
> Everything is up to date, there is nothing to compile.
Starting migrations…
> Network name: ‘develop’
> Network id: 5777
> Block gas limit: 6721975 (0x6691b7)
Replacing ‘Migrations’
— — — — — — — — — — —
> transaction hash: 0x4fec9e3fe34d4fe7c6f356ea999d0942cc3ff70224d20224aea01f83b7e1a068
> Blocks: 0 Seconds: 0
> contract address: 0xe2fdE3f8265BEa9A019746Ea21B771fBa06EC10d
> block number: 1
> block timestamp: 1610664526
> account: 0x762fCeb6848a20F118026Fa4D63868A97381E575
> balance: 99.99615682
> gas used: 192159 (0x2ee9f)
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.00384318 ETH
> Saving migration to chain.
> Saving artifacts
— — — — — — — — — — — — — — — — — — -
> Total cost: 0.00384318 ETH
Deploying ‘FamilyRecipe’
— — — — — — — — — — — —
> transaction hash: 0x31aaaf53ed323bc6d4ae00f9030324f25837ada2d0239ec2d23226cd5064eac1
> Blocks: 0 Seconds: 0
> contract address: 0x5F0f4aa2D79b323fe1810c60349Ef069454688Dd
> block number: 3
> block timestamp: 1610664530
> account: 0x762fCeb6848a20F118026Fa4D63868A97381E575
> balance: 99.93938832
> gas used: 2796087 (0x2aaa37)
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.05592174 ETH
> Saving migration to chain.
> Saving artifacts
— — — — — — — — — — — — — — — — — — -
> Total cost: 0.05592174 ETH
> Total deployments: 2
> Final cost: 0.05976492 ETH

Truffle will respond to a successful deployment with the information above. We can see that our account at address 0x762fCeb6848a20F118026Fa4D63868A97381E575 used 192,159 units of gas at 20 gwei per unit = 3,843,180 gwei which is how the subtotal came to 0.00384318 ETH. Likewise, our second migration used 2,796,087 in gas at 20 gwei per unit = 55,921,740 gwei or 0.05592174 ETH. In total, we spent 0.05976492 ETH to deploy our FamilyRecipe smart contract to the Ethereum blockchain which at a current trading price of 1 ETH = $1,197.28 USD means that the deployment cost $71.55 USD. On Ethereum’s mainnet, this gas price fluctuates significantly, e.g. on June 11, 2020 the minimum gas price was 0.00035 gwei while the maximum gas price was 508,034,850 gwei according to Etherscan, which is pushing many to devise ways to make blockchain development cheaper and keep the price more stable. What our purchase has bought us is essentially a digital mint capable of producing new REC tokens on demand.

Next, we will use our freshly deployed FamilyRecipe smart contract to mint a new REC token!

Creating a digital asset and minting our Non-Fungible Token

function mintREC(address recipient, string memory hash, string memory metadata) public returns(uint256) {

The first parameter is a reference to the address of the individual who will receive the REC token. The second and third parameters are related to the content itself. Recall that what we are trying to achieve is to ascribe ownership to a digital asset. The second parameter represents that digital asset as a hash. The third parameter represents the metadata relevant to that digital asset and defines who is the owner.

We need somewhere to host our digital asset, in this case it’s my mother’s recipe for arguably the most delicious hermit cookies to ever come out of the oven. One benefit of the blockchain is that it is decentralized meaning that we are not deploying our application to a single server or a group of servers owned by a single entity. We can also apply this value system to our digital asset. Unfortunately, today, it is not economically viable to host something like a photograph on the blockchain. As we saw in the Deploying our Non-Fungible Token to the Ethereum blockchain section, deployments to the blockchain have a price. We paid 0.05976492 ETH ($71.55 USD) to deploy our smart contract which was only 879 Bytes (0.879 KB) in size. The size of the file containing our recipe is 430 KB in size. At the time of this writing, the average block size on the Ethereum blockchain was roughly 43,000 Bytes (43 KB) according to Etherscan. That means that it would take 10 blocks for us to store our recipe which means ten massive transactions which puts it well out of my price range. Luckily, there is another option for decentralized storage: a project called the InterPlanetary File System (IPFS).

The simplest way that I have found to interact with IPFS is via a project called Pinata which has built an API and toolkit. In order to use it, you will need to create an account with Pinata. After creating your account, you will need to generate an API Key after which Pinata will present you with a Pinata API Key, Pinata API Secret, and the JWT. Make sure to write down your Pinata API Key and Pinata API Secret as we will need them next!

Back in our project, we will build a small program that will upload our recipe to IPFS using the API provided by Pinata.

$ mkdir utils
$ touch utils/uploadFile.js

Let’s add the code for our program into the new uploadFile.js file.

const pinataApiKey = '{YOUR_API_KEY}';
const pinataApiSecret = '{YOUR_API_SECRET}';
const axios = require(‘axios’);
const fs = require(‘fs’);
const FormData = require(‘form-data’);
var args = process.argv.slice(2);
var filePath = args[0];
const uploadFile = async () => {
const url = ‘';
let data = new FormData();
data.append(‘file’, fs.createReadStream(filePath));
const res = await, data, {
maxContentLength: ‘Infinity’,
headers: {
“Content-Type”: `multipart/form-data;
pinata_api_key: pinataApiKey,
pinata_secret_api_key: pinataApiSecret,

Our uploadFile program uses the axios REST client, so we need to install that.

$ npm install axios

Alright we are ready to point our program to the recipe and upload that to IPFS.

$ node utils/uploadFile.js ./recipe.png

If all is successful, Pinata will respond with a JSON object containing a hash from IPFS representing our file and a timestamp of when the file was uploaded.

IpfsHash: ‘QmfXZzR5LvqmBnAFNRR8nCKZGaHazMEr6BK4vd9xoSQ3nf’,
PinSize: 429769,
Timestamp: ‘2021–01–15T21:52:33.124Z’

What’s really cool about this is that if we ran the same program again with our recipe file that the hash and the timestamp would be exactly the same because the underlying data is the same!

In addition to the API, Pinata has also built the “Pin Explorer” which provides a gateway to view files uploaded to IPFS. We can see the uploaded recipe here:

Now that we have uploaded our digital asset to decentralized storage and generated a hash to uniquely represent it, we need to declare ownership over the asset. The ERC-721 standard achieves this via metadata, i.e. data that describes data. The standard requires that this metadata is a JSON file and made accessible via a URI. This ensures that the tokenURI method described by the standard enables anyone using Ethereum to interrogate the NFT for its name and for details about the asset which the token represents. Let’s create the JSON file for our asset now.

$ touch recipe_metadata.json

And populate the file.

“name”: “Family Hermit Recipe”,
“hash”: “QmfXZzR5LvqmBnAFNRR8nCKZGaHazMEr6BK4vd9xoSQ3nf”,
“cook”: “Mama”

As this is a file, we are also going to upload this to IPFS via Pinata using our uploadFile program.

$ node utils/uploadFile.js ./recipe_metadata.json

Which should result in the creation of a new hash and timestamp.

IpfsHash: ‘QmTAXrF6y5k3jksmYAoSyS8oknNXyDhNBijj6djp7jBxAX’,
PinSize: 123,
Timestamp: ‘2021–01–17T16:08:55.446Z’

Great, we now have all three parameters that we need to mint our first REC token! Let’s launch return to our Truffle development server and get going. First, we create new variables to store each parameter. We will grab a random address from Truffle to be the owner of the token.

truffle(develop)> recipient = ‘0xe3b5f3d0b3cf76aace406b7e269ed83ac63bd68f’;
truffle(develop)> hash = ‘QmfXZzR5LvqmBnAFNRR8nCKZGaHazMEr6BK4vd9xoSQ3nf’;
truffle(develop)> metadata = ‘ipfs://QmTAXrF6y5k3jksmYAoSyS8oknNXyDhNBijj6djp7jBxAX’;

We check the balance of the receiving address which should be zero because we have not yet minted a REC token.

truffle(develop)> contract.methods.balanceOf(recipient).call(console.log);null 0

Great, let’s get minting!

truffle(develop)> var tokenId;
truffle(develop)> contract.methods.mintREC(recipient, hash, metadata).send({from:recipient,gas: 6721975,gasPrice: ‘30000000’}).then((res)=>{});

We call our mintREC function providing the necessary parameters and dictating the terms of our transaction. We then store the ID of the newly minted token in a newly created tokenId variable.

Let’s check the balance of the receiving address which should now be 1 as we have a newly minted REC token.

truffle(develop)> contract.methods.balanceOf(recipient).call(console.log);null 1

We can double-check this using the ownerOf method defined by the ERC-721 standard to find the owner of an NFT which should return the same address that we passed to our mintREC function via the recipient variable.

truffle(develop)> recipient‘0xe3b5f3d0b3cf76aace406b7e269ed83ac63bd68f’truffle(develop)> contract.methods.ownerOf(tokenId).call(console.log);null 0xe3b5f3d0b3CF76AAce406b7e269Ed83aC63Bd68f

So now we have verified ownership of the token. Let’s take a look at the asset that the token represents. We can do this using the tokenURI method mentioned earlier in this section.

truffle(develop)> contract.methods.tokenURI(tokenId).call(console.log);null ipfs://QmTAXrF6y5k3jksmYAoSyS8oknNXyDhNBijj6djp7jBxAX

Cool! The final utility of NFTs that we will inspect is the transfer of ownership from one recipient to the next. This enables use cases like the sale of a digital asset or even something less transactional, more human like the passing down of a treasured amongst one another.

truffle(develop)> newRecipient = ‘0x90b398adc7dbd68c83de4fcbd676308e5c62ce6c’;
truffle(develop)> contract.methods.safeTransferFrom(recipient, newRecipient, tokenId).send({from:recipient,gas: 6721975,gasPrice: ‘30000000’});
truffle(develop)> contract.methods.ownerOf(tokenId).call(console.log);
null 0x90B398AdC7dbd68c83de4fCBd676308e5c62CE6ctruffle(develop)> contract.methods.balanceOf(recipient).call(console.log);null 0truffle(develop)> contract.methods.balanceOf(newRecipient).call(console.log);null 1

Success, the person behind the address is the new owner of the REC token containing our recipe!

What were we talking about, again?

I am really excited about the future of Non-Fungible Tokens and their potential impact. Already innovators like SuperRare, CryptoKitties, Audius, and OpenSea are using NFTs to put control back into the hands of creators in markets like art, gaming, music, and collectibles. If you have read my other blogs, you know that I have a huge, squishy soft spot for blockchain and its applications to solve problems for small business owners including sole proprietors, the self-employed, solopreneurs, and other believers and participants of the passion economy. I believe that blockchain, and specifically Ethereum and smart contracts, will have a massive impact on key problems they face from on-time payment collection to access to lending. I see NFTs helping to solve a suite of other challenges from intellectual property ownership to tokenization of assets that aren’t represented in the current financial system!

Note: As an example of the benefits of NFTs for intellectual property ownership, an article from CoinDesk reports an individual’s attempt to claim copy right ownership over the Bitcoin white paper. This white paper was published anonymously in 2008 by an individual under the pseudonym “Satoshi Nakamoto” whose true identity remains unknown although the individual’s public key is known. Had the Bitcoin white paper been published as an NFT complete with the author’s information and a timestamp, ownership would be clear and verifiable. Although, there is a good debate to be had over whether this is antithetical to the value system behind Bitcoin.

Understanding these technologies is fundamental to seeing their impact, and sharing that knowledge is fundamental to universal adoption of these technologies and achievement of that vision. There are many outlets to staying updated on what is becoming known as the “NFT Revolution” including from sources like Decrypt. I hope that this blog helped you to understand NFTs a bit better and gave you a tool to spread that knowledge!

If you know someone who you would like to share the knowledge of Non-Fungible Tokens, clone the source code from my GitHub of the token that we built throughout this blog and run it on your local machine.

About the author

In his spare time, Colin can be found checking out an REI camp bundle for a weekend away in the woods, field testing the latest HOKAs, hunting fresh powder on the mountain, or surfing the Horror section on Netflix.

Connect with Colin —

Problem solver wielding JavaScript and Python as my tools. Builder of RESTful web services and progressive web applications. Scholar of the newly possible.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store