ERC-721 tokens: What they are? How do they work? How can we use them?

ERC-20 vs. ERC-721

At the height of the ICO bubble in 2017, ERC-20 tokens were everywhere. They were used as a form of crowdfunding by tech companies, with some claiming future use of those tokens on their platform.

ERC-20 tokens are like currency. Every dollar is valued the same as every other dollar. Exchanging a one-dollar note for another, and you essentially have the same thing. This is what’s known as a fungible token.

ERC-721 tokens are like collectables. Every Pokemon card is not the same as every other Pokemon card. Even cards representing the same Pokemon are valued differently. Some cards are in better condition than others, some are limited or special edition, etc. One Pikachu card does not always equal another Pikachu card. This is what’s known as a non-fungible token.

But How?

The ERC-721 standard describes the interface that any non-fungible token must adhere to to be considered ERC-721.

Fortunately, we don’t need to create new code to satisfy this standard every time we want to create an ERC-721. Community-maintained libraries exist that cut this corner for us. OpenZeppelin provides a great library that fills in the majority of blanks.

Let’s take a look at how we would create a very limited imitation of a digital Pocket Monsters game using OpenZeppelin. We’ll call our game Ethermon.

ERC-721 Ethermon Game

First of all, let’s outline some assumptions about each Ethermon:

Each is owned by someone.

They start at level one.

You battle other Pokemon.

You gain levels by battling.

We also need some logic around the battles. For simplicity, let’s assume that if one Ethermon attacks another, the winner will be determined by which Ethermon is the higher level. If the levels are the same, the attacker wins. Winners of battles go two levels up, losers go one level up.

Project creation

Please note, we’re going to use the Truffle Suite in this walkthrough.

Start by creating a new folder and initializing the Truffle project.

mkdir ethermon

cd ethermon/

truffle init

Use OpenZeppelin

To use OpenZeppelin, we need to import the library via npm. Let’s initialize npm, then get the correct version of OpenZeppelin. We’re using the latest stable version, which at time of writing is version 2.5.0 . Make sure you have Solidity compiler version 0.5.5 .

npm init

npm install @openzeppelin/contracts@2.5.0 --save

Extend ERC-721

In our contracts/ folder, let’s create a new file called Ethermon.sol . To include all of the functionality written for us in the OpenZeppelin code, we need to import and extend ERC721.sol .

Figure 1 shows us what Ethermon.sol should look like at this point:

pragma solidity ^0.5.5; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract Ethermon is ERC721 { }

Figure 1: Ethermon.sol

We ensure our contract compiles properly by running truffle compile . Next up, let’s write our migration script so we can deploy it to our local Blockchain. Create a new migration file in migrations/ called 2_deploy_contracts.js . Paste in the contents of Figure 2.

const Ethermon = artifacts.require("Ethermon"); module.exports = function(deployer) { deployer.deploy(Ethermon); };

Figure 2: deploy_contracts.js

Ensure that your truffle-config.js is set up correctly for your local Blockchain. You can test this by running truffle test .

Write Ethermon logic

We need the Ethermon contract to be able to:

Create new monsters. Assign monsters to their owners. Let owners battle their monsters against other monsters.

Let’s begin by tackling step 1. We need our Ethermon contract to store an array of all the monsters in existence. That monster data needs to store a few pieces of information, like name and level, so we’re going to use a struct.

Figure 3 shows what our contract should look like at this stage.

pragma solidity ^0.5.5; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract Ethermon is ERC721 { struct Monster { string name; uint level; } Monster[] public monsters; address public gameOwner; constructor() public { gameOwner = msg.sender; } function createNewMonster(string memory _name, address _to) public { require(msg.sender == gameOwner, "Only game owner can create new monsters"); uint id = monsters.length; monsters.push(Monster(_name, 1)); _safeMint(_to, id); } }

Figure 3: Creating new monsters

Our Monster struct is defined on line 7 and our array on line 12. We’ve also added a gameOwner variable to store the address that deployed the Ethermon contract. Line 19 shows the definition of our createNewMonster() function.

Firstly, it checks that the function was called by the owner of the game. Then it generates an ID using the number of monsters in existence, pushes a new monster to our array, and finally uses _safeMint() to assign the monster ID to the owner of it. This covers steps 1 and 2.

_safeMint() is a function provided to us by the ERC-721 contract that ours extends from. It safely assigns the ID to an address by checking that the ID does not already exist.

As it stands, we’re able to create new monsters and assign them to owners. On to step 3: battle logic.

Battle logic

As mentioned earlier, our battle logic determines how many levels a monster goes up. The higher-level monster wins and goes up two levels, the loser goes up one. If they are the same level, the attacker wins. Figure 4 shows what our contract looks like when we add the code for battles.

pragma solidity ^0.5.5; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract Ethermon is ERC721 { struct Monster { string name; uint level; } Monster[] public monsters; address public gameOwner; constructor() public { gameOwner = msg.sender; } function battle(uint _attackingMonster, uint _defendingMonster) public { Monster storage attacker = monsters[_attackingMonster]; Monster storage defender = monsters[_defendingMonster]; if (attacker.level >= defender.level) { attacker.level += 2; defender.level += 1; } else{ attacker.level += 1; attacker.level += 2; } } function createNewMonster(string memory _name, address _to) public { require(msg.sender == gameOwner, "Only game owner can create new monsters"); uint id = monsters.length; monsters.push(Monster(_name, 1)); _safeMint(_to, id); } }

Figure 4: Ethermon.sol with Battle function added

Line 19 now shows the battle function logic. Currently, anyone can call battle() . However, we need to limit the function so only the owner of the attacking monster can initiate a battle. For this, we can add a modifier that utilizes the ownerOf() function in ERC721.sol . Figure 5 shows us what that looks like.

pragma solidity ^0.5.5; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract Ethermon is ERC721 { struct Monster { string name; uint level; } Monster[] public monsters; address public gameOwner; constructor() public { gameOwner = msg.sender; } modifier onlyOwnerOf(uint _monsterId) { require(ownerOf(_monsterId) == msg.sender, "Must be owner of monster to battle"); _; } function battle(uint _attackingMonster, uint _defendingMonster) public onlyOwnerOf(_attackingMonster) { Monster storage attacker = monsters[_attackingMonster]; Monster storage defender = monsters[_defendingMonster]; if (attacker.level >= defender.level) { attacker.level += 2; defender.level += 1; } else{ attacker.level += 1; attacker.level += 2; } } function createNewMonster(string memory _name, address _to) public { require(msg.sender == gameOwner, "Only game owner can create new monsters"); uint id = monsters.length; monsters.push(Monster(_name, 1)); _safeMint(_to, id); } }

Figure 5: Finished Contract

There we have it! A very basic but working monster battling game built using ERC-721.

We can create new monsters and assign owners. From there, owners of monsters can fight others to level-up their monsters.

Next Steps

Learn More

If you’re interested in Blockchain Development, I write tutorials, walkthroughs, hints, and tips on how to get started and build a portfolio. Check out this evolving list of Blockchain Development Resources.

If you enjoyed this post and want to learn more about Smart Contract Security, Blockchain Development or the Blockchain Space in general, I highly recommend signing up to the Blockgeeks platform. They have courses on a wide range of topics in the industry, from Coding to Marketing to Trading. It has proven to be an invaluable tool for my development in the Blockchain space.