Home

Lesson 5: ERC721 & Crypto-Collectibles

Tokens on Ethereum

If you've been in the Ethereum space for any amount of time, you've probably heard people talking about tokens — specifically ERC20 tokens.

A token on Ethereum is basically just a smart contract that follows some common rules — namely it implements a standard set of functions that all other token contracts share, such as transferFrom(address _from, address _to, uint256 _tokenId) and balanceOf(address _owner).

Internally the smart contract usually has a mapping, mapping(address => uint256) balances, that keeps track of how much balance each address has.

So basically a token is just a contract that keeps track of who owns how much of that token, and some functions so those users can transfer their tokens to other addresses.

Why does it matter?

Since all ERC20 tokens share the same set of functions with the same names, they can all be interacted with in the same ways.

This means if you build an application that is capable of interacting with one ERC20 token, it's also capable of interacting with any ERC20 token. That way more tokens can easily be added to your app in the future without needing to be custom coded. You could simply plug in the new token contract address, and boom, your app has another token it can use.

One example of this would be an exchange. When an exchange adds a new ERC20 token, really it just needs to add another smart contract it talks to. Users can tell that contract to send tokens to the exchange's wallet address, and the exchange can tell the contract to send the tokens back out to users when they request a withdraw.

The exchange only needs to implement this transfer logic once, then when it wants to add a new ERC20 token, it's simply a matter of adding the new contract address to its database.

Other token standards

ERC20 tokens are really cool for tokens that act like currencies. But they're not particularly useful for representing zombies in our zombie game.

For one, zombies aren't divisible like currencies — I can send you 0.237 ETH, but transfering you 0.237 of a zombie doesn't really make sense.

Secondly, all zombies are not created equal. Your Level 2 zombie "Steve" is totally not equal to my Level 732 zombie "H4XF13LD MORRIS 💯💯😎💯💯". (Not even close, Steve).

There's another token standard that's a much better fit for crypto-collectibles like CryptoZombies — and they're called ERC721 tokens.

ERC721 tokens are not interchangeable since each one is assumed to be unique, and are not divisible. You can only trade them in whole units, and each one has a unique ID. So these are a perfect fit for making our zombies tradeable.

Note that using a standard like ERC721 has the benefit that we don't have to implement the auction or escrow logic within our contract that determines how players can trade / sell our zombies. If we conform to the spec, someone else could build an exchange platform for crypto-tradable ERC721 assets, and our ERC721 zombies would be usable on that platform. So there are clear benefits to using a token standard instead of rolling your own trading logic.

Implementing ERC721 tokens

A lot of this section of the code was working through the implementation of the required functions for the inheritance.

Head to lesson 5 to see how we implemented the inheritance, but for some insight on functions like transferFrom and approve:

pragma solidity >=0.5.0 <0.6.0; import "./zombieattack.sol"; import "./erc721.sol"; contract ZombieOwnership is ZombieAttack, ERC721 { mapping (uint => address) zombieApprovals; function balanceOf(address _owner) external view returns (uint256) { return ownerZombieCount[_owner]; } function ownerOf(uint256 _tokenId) external view returns (address) { return zombieToOwner[_tokenId]; } function _transfer(address _from, address _to, uint256 _tokenId) private { ownerZombieCount[_to]++; ownerZombieCount[_from]--; zombieToOwner[_tokenId] = _to; emit Transfer(_from, _to, _tokenId); } function transferFrom(address _from, address _to, uint256 _tokenId) external payable { require (zombieToOwner[_tokenId] == msg.sender || zombieApprovals[_tokenId] == msg.sender); _transfer(_from, _to, _tokenId); } function approve(address _approved, uint256 _tokenId) external payable onlyOwnerOf(_tokenId) { zombieApprovals[_tokenId] = _approved; emit Approval(msg.sender, _approved, _tokenId); } }

Contract security enhancements: overflows and underflows

To prevent this, OpenZeppelin has created a library called SafeMath that prevents these issues by default.

A library is a special type of contract in Solidity. One of the things it is useful for is to attach functions to native data types.

For example, with the SafeMath library, we'll use the syntax using SafeMath for uint256. The SafeMath library has 4 functions — add, sub, mul, and div. And now we can access these functions from uint256 as follows:

using SafeMath for uint256; uint256 a = 5; uint256 b = a.add(3); // 5 + 3 = 8 uint256 c = a.mul(2); // 5 * 2 = 10

Here is a look at code behind SafeMath:

library SafeMath { function mul(uint256 a, uint256 b) internal pure returns (uint256) { if (a == 0) { return 0; } uint256 c = a * b; assert(c / a == b); return c; } function div(uint256 a, uint256 b) internal pure returns (uint256) { // assert(b > 0); // Solidity automatically throws when dividing by 0 uint256 c = a / b; // assert(a == b * c + a % b); // There is no case in which this doesn't hold return c; } function sub(uint256 a, uint256 b) internal pure returns (uint256) { assert(b <= a); return a - b; } function add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; assert(c >= a); return c; } }

First we have the library keyword — libraries are similar to contracts but with a few differences. For our purposes, libraries allow us to use the using keyword, which automatically tacks on all of the library's methods to another data type.

using SafeMath for uint; // now we can use these methods on any uint uint test = 2; test = test.mul(3); // test now equals 6 test = test.add(5); // test now equals 11

Note that the mul and add functions each require 2 arguments, but when we declare using SafeMath for uint, the uint we call the function on (test) is automatically passed in as the first argument.

assert is similar to require, where it will throw an error if false. The difference between assert and require is that require will refund the user the rest of their gas when a function fails, whereas assert will not. So most of the time you want to use require in your code; assert is typically used when something has gone horribly wrong with the code (like a uint overflow).

Here is some code that exemplifies using SafeMath code for different uint data types:

pragma solidity >=0.5.0 <0.6.0; import "./ownable.sol"; import "./safemath.sol"; contract ZombieFactory is Ownable { using SafeMath for uint256; using SafeMath32 for uint32; using SafeMath16 for uint16; event NewZombie(uint zombieId, string name, uint dna); uint dnaDigits = 16; uint dnaModulus = 10 ** dnaDigits; uint cooldownTime = 1 days; struct Zombie { string name; uint dna; uint32 level; uint32 readyTime; uint16 winCount; uint16 lossCount; } Zombie[] public zombies; mapping (uint => address) public zombieToOwner; mapping (address => uint) ownerZombieCount; function _createZombie(string memory _name, uint _dna) internal { // Note: We chose not to prevent the year 2038 problem... So don't need // worry about overflows on readyTime. Our app is screwed in 2038 anyway ;) uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime), 0, 0)) - 1; zombieToOwner[id] = msg.sender; ownerZombieCount[msg.sender] = ownerZombieCount[msg.sender].add(1); emit NewZombie(id, _name, _dna); } function _generateRandomDna(string memory _str) private view returns (uint) { uint rand = uint(keccak256(abi.encodePacked(_str))); return rand % dnaModulus; } function createRandomZombie(string memory _name) public { require(ownerZombieCount[msg.sender] == 0); uint randDna = _generateRandomDna(_name); randDna = randDna - randDna % 100; _createZombie(_name, randDna); } }

Comments

The standard in the Solidity community is to use a format called natspec, which looks like this:

/// @title A contract for basic math operations /// @author H4XF13LD MORRIS 💯💯😎💯💯 /// @notice For now, this contract just adds a multiply function contract Math { /// @notice Multiplies 2 numbers together /// @param x the first uint. /// @param y the second uint. /// @return z the product of (x * y) /// @dev This function does not currently check for overflows function multiply(uint x, uint y) returns (uint z) { // This is just a normal comment, and won't get picked up by natspec z = x * y; } }

Most are straightforward but @notice explains to a user what the contract / function does and @dev is for explaining extra details to developers.

Repository

https://github.com/okeeffed/developer-notes-nextjs/content/ethereum/Crypto-Zombies/Solidity-Path/Lesson-5-ERC721-Crypto-Collectibles

Sections


Related