forked from CryptozombiesHQ/cryptozombie-lessons
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
35 changed files
with
6,259 additions
and
1,383 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
--- | ||
title: "ERC721x: Multi Fungible Tokens" | ||
header: "ERC721x: Multi Fungible Tokens" | ||
roadmap: roadmap.jpg | ||
|
||
--- | ||
|
||
Welcome, CryptoZombie! | ||
|
||
In this intermediate lesson, we will be learning about **ERC721x Multi-Fungible Tokens**. | ||
|
||
What is a multi-fungible token, you ask? | ||
|
||
With Non-Fungible Tokens (NFTs) like ERC721, every item is totally 100% unique. But Multi-Fungible Tokens recognize that in most games, you will want to have *multiple identical copies of the same item*. | ||
|
||
For example, your Master Sword is probably exactly the same as my Master Sword — they look the same, do the same damage, and have the exact same functionality. Thus we would call these items "fungible", because we could trade our Master Swords without either one of us minding — there is no difference between them. | ||
|
||
Of course, this isn't always the case. For some items, maybe we do want them to be one of a kind. | ||
|
||
Thus, ERC721x gives you the flexibility to do both — and to manage all the items in your game, both fungible and non-fungible, within one single smart contract. And best of all, it's fully backward-compatible with ERC721. | ||
|
||
In this lesson, you will learn how to use it in your game. | ||
|
||
Are you ready to get started? |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,303 @@ | ||
--- | ||
title: Background & Getting Started | ||
actions: ['checkAnswer', 'hints'] | ||
requireLogin: true | ||
material: | ||
editor: | ||
language: sol | ||
startingCode: | ||
"ZombieToken.sol": | | ||
//start here | ||
"ERC721XToken.sol": | | ||
// Full implementation with all included files at https://github.com/loomnetwork/erc721x | ||
pragma solidity ^0.4.24; | ||
import "./../../Interfaces/ERC721X.sol"; | ||
import "./../../Interfaces/ERC721XReceiver.sol"; | ||
import "./ERC721XTokenNFT.sol"; | ||
import "openzeppelin-solidity/contracts/AddressUtils.sol"; | ||
import "./../../Libraries/ObjectsLib.sol"; | ||
// Additional features over NFT token that is compatible with batch transfers | ||
contract ERC721XToken is ERC721X, ERC721XTokenNFT { | ||
using ObjectLib for ObjectLib.Operations; | ||
using AddressUtils for address; | ||
bytes4 internal constant ERC721X_RECEIVED = 0x660b3370; | ||
bytes4 internal constant ERC721X_BATCH_RECEIVE_SIG = 0xe9e5be6a; | ||
event BatchTransfer(address from, address to, uint256[] tokenTypes, uint256[] amounts); | ||
modifier isOperatorOrOwner(address _from) { | ||
require((msg.sender == _from) || operators[_from][msg.sender], "msg.sender is neither _from nor operator"); | ||
_; | ||
} | ||
function implementsERC721X() public pure returns (bool) { | ||
return true; | ||
} | ||
/** | ||
* @dev transfer objects from different tokenIds to specified address | ||
* @param _from The address to BatchTransfer objects from. | ||
* @param _to The address to batchTransfer objects to. | ||
* @param _tokenIds Array of tokenIds to update balance of | ||
* @param _amounts Array of amount of object per type to be transferred. | ||
* Note: Arrays should be sorted so that all tokenIds in a same bin are adjacent (more efficient). | ||
*/ | ||
function _batchTransferFrom(address _from, address _to, uint256[] _tokenIds, uint256[] _amounts) | ||
internal | ||
isOperatorOrOwner(_from) | ||
{ | ||
// Requirements | ||
require(_tokenIds.length == _amounts.length, "Inconsistent array length between args"); | ||
require(_to != address(0), "Invalid recipient"); | ||
if (tokenType[_tokenIds[0]] == NFT) { | ||
tokenOwner[_tokenIds[0]] = _to; | ||
emit Transfer(_from, _to, _tokenIds[0]); | ||
} | ||
// Load first bin and index where the object balance exists | ||
(uint256 bin, uint256 index) = ObjectLib.getTokenBinIndex(_tokenIds[0]); | ||
// Balance for current bin in memory (initialized with first transfer) | ||
// Written with bad library syntax instead of as below to bypass stack limit error | ||
uint256 balFrom = ObjectLib.updateTokenBalance( | ||
packedTokenBalance[_from][bin], index, _amounts[0], ObjectLib.Operations.SUB | ||
); | ||
uint256 balTo = ObjectLib.updateTokenBalance( | ||
packedTokenBalance[_to][bin], index, _amounts[0], ObjectLib.Operations.ADD | ||
); | ||
// Number of transfers to execute | ||
uint256 nTransfer = _tokenIds.length; | ||
// Last bin updated | ||
uint256 lastBin = bin; | ||
for (uint256 i = 1; i < nTransfer; i++) { | ||
// If we're transferring an NFT we additionally should update the tokenOwner and emit the corresponding event | ||
if (tokenType[_tokenIds[i]] == NFT) { | ||
tokenOwner[_tokenIds[i]] = _to; | ||
emit Transfer(_from, _to, _tokenIds[i]); | ||
} | ||
(bin, index) = _tokenIds[i].getTokenBinIndex(); | ||
// If new bin | ||
if (bin != lastBin) { | ||
// Update storage balance of previous bin | ||
packedTokenBalance[_from][lastBin] = balFrom; | ||
packedTokenBalance[_to][lastBin] = balTo; | ||
// Load current bin balance in memory | ||
balFrom = packedTokenBalance[_from][bin]; | ||
balTo = packedTokenBalance[_to][bin]; | ||
// Bin will be the most recent bin | ||
lastBin = bin; | ||
} | ||
// Update memory balance | ||
balFrom = balFrom.updateTokenBalance(index, _amounts[i], ObjectLib.Operations.SUB); | ||
balTo = balTo.updateTokenBalance(index, _amounts[i], ObjectLib.Operations.ADD); | ||
} | ||
// Update storage of the last bin visited | ||
packedTokenBalance[_from][bin] = balFrom; | ||
packedTokenBalance[_to][bin] = balTo; | ||
// Emit batchTransfer event | ||
emit BatchTransfer(_from, _to, _tokenIds, _amounts); | ||
} | ||
function batchTransferFrom(address _from, address _to, uint256[] _tokenIds, uint256[] _amounts) public { | ||
// Batch Transfering | ||
_batchTransferFrom(_from, _to, _tokenIds, _amounts); | ||
} | ||
/** | ||
* @dev transfer objects from different tokenIds to specified address | ||
* @param _from The address to BatchTransfer objects from. | ||
* @param _to The address to batchTransfer objects to. | ||
* @param _tokenIds Array of tokenIds to update balance of | ||
* @param _amounts Array of amount of object per type to be transferred. | ||
* @param _data Data to pass to onERC721XReceived() function if recipient is contract | ||
* Note: Arrays should be sorted so that all tokenIds in a same bin are adjacent (more efficient). | ||
*/ | ||
function safeBatchTransferFrom( | ||
address _from, | ||
address _to, | ||
uint256[] _tokenIds, | ||
uint256[] _amounts, | ||
bytes _data | ||
) | ||
public | ||
{ | ||
// Batch Transfering | ||
_batchTransferFrom(_from, _to, _tokenIds, _amounts); | ||
// Pass data if recipient is contract | ||
if (_to.isContract()) { | ||
bytes4 retval = ERC721XReceiver(_to).onERC721XBatchReceived( | ||
msg.sender, _from, _tokenIds, _amounts, _data | ||
); | ||
require(retval == ERC721X_BATCH_RECEIVE_SIG); | ||
} | ||
} | ||
function transfer(address _to, uint256 _tokenId, uint256 _amount) public { | ||
_transferFrom(msg.sender, _to, _tokenId, _amount); | ||
} | ||
function transferFrom(address _from, address _to, uint256 _tokenId, uint256 _amount) public { | ||
_transferFrom(_from, _to, _tokenId, _amount); | ||
} | ||
function _transferFrom(address _from, address _to, uint256 _tokenId, uint256 _amount) | ||
internal | ||
isOperatorOrOwner(_from) | ||
{ | ||
require(tokenType[_tokenId] == FT); | ||
require(_amount <= balanceOf(_from, _tokenId), "Quantity greater than from balance"); | ||
require(_to != address(0), "Invalid to address"); | ||
_updateTokenBalance(_from, _tokenId, _amount, ObjectLib.Operations.SUB); | ||
_updateTokenBalance(_to, _tokenId, _amount, ObjectLib.Operations.ADD); | ||
emit TransferWithQuantity(_from, _to, _tokenId, _amount); | ||
} | ||
function safeTransferFrom(address _from, address _to, uint256 _tokenId, uint256 _amount) public { | ||
safeTransferFrom(_from, _to, _tokenId, _amount, ""); | ||
} | ||
function safeTransferFrom(address _from, address _to, uint256 _tokenId, uint256 _amount, bytes _data) public { | ||
_transferFrom(_from, _to, _tokenId, _amount); | ||
require( | ||
checkAndCallSafeTransfer(_from, _to, _tokenId, _amount, _data), | ||
"Sent to a contract which is not an ERC721X receiver" | ||
); | ||
} | ||
function _mint(uint256 _tokenId, address _to, uint256 _supply) internal { | ||
// If the token doesn't exist, add it to the tokens array | ||
if (!exists(_tokenId)) { | ||
tokenType[_tokenId] = FT; | ||
allTokens.push(_tokenId); | ||
} else { | ||
// if the token exists, it must be a FT | ||
require(tokenType[_tokenId] == FT, "Not a FT"); | ||
} | ||
_updateTokenBalance(_to, _tokenId, _supply, ObjectLib.Operations.ADD); | ||
emit TransferWithQuantity(address(this), _to, _tokenId, _supply); | ||
} | ||
function checkAndCallSafeTransfer( | ||
address _from, | ||
address _to, | ||
uint256 _tokenId, | ||
uint256 _amount, | ||
bytes _data | ||
) | ||
internal | ||
returns (bool) | ||
{ | ||
if (!_to.isContract()) { | ||
return true; | ||
} | ||
bytes4 retval = ERC721XReceiver(_to).onERC721XReceived( | ||
msg.sender, _from, _tokenId, _amount, _data); | ||
return(retval == ERC721X_RECEIVED); | ||
} | ||
} | ||
answer: > | ||
pragma solidity ^0.4.25; | ||
import "./ERC721XToken.sol"; | ||
contract ZombieCard is ERC721XToken { | ||
} | ||
--- | ||
|
||
In this lesson, we're going to be building the token contract for an example blockchain game. | ||
|
||
It is roughly based on the way we implemented ERC721x in our own game at Loom Network, <a href="https://loom.games/" target=_blank>Zombie Battleground</a> — a blockchain-based collectible card game similar to Magic: The Gathering or Hearthstone. | ||
|
||
This way you'll get to see what a real-world use case looks like, and be more than equipped to adapt the implementation to your own game after you complete this lesson. | ||
|
||
For our first chapter, let's start with some background to make sure we're all on the same page... | ||
|
||
## Background: What is a Token, Anyway? | ||
|
||
Referring to in-game items as "tokens" can be a bit misleading, since the word "token" implies something that would be spent like a currency. | ||
|
||
But the word **_token_** on Ethereum simply refers to a standard for smart contracts that all implement the same common functions, such as `transfer(address _to, uint256 _value)` and `balanceOf(address _owner)`. | ||
|
||
Yes, you can transfer and have a balance of a currency — but you can also transfer and have an inventory of items in a game, like swords or playing cards. | ||
|
||
Even though a sword in a game isn't spendable like a currency — we *do* want these same basic functionalities for anything that a player can own.c | ||
|
||
Thus the concept of a "token" is useful for abstracting out these common methods so they can be applied into all items we put into our game — this way all game items on Ethereum share a common interface, and can be supported by any wallet or marketplace that adheres to the same token standard. | ||
|
||
## Different Token Standards: ERC20 and ERC721 | ||
|
||
The most commonly known token standard is the **_ERC20 token_**. These tokens act like currencies. But they're not particularly useful for representing digital items, such as cards in a card trading game. | ||
|
||
First of all, cards aren't divisible like currencies — you can't own 2/5ths of a playing card. | ||
|
||
Second, not all cards are created equal, so they're not interchangeable. You wouldn't want to exchange your **Z-Virus** card for my **Ozmoziz**. | ||
|
||
There's another token standard that's a much better fit for crypto-collectibles — and they're called **_ERC721 tokens._** | ||
|
||
**_ERC721 tokens_** are **not** interchangeable. Each item is assumed to be unique, and has a unique ID. They're also not divisible — you can only trade them in whole units. | ||
|
||
So these are a better fit for making our card trading game. | ||
|
||
You noticed that I said they're a **better** fit — not perfect, right? 😉 | ||
|
||
## ERC721x — Best of Both Worlds | ||
|
||
The issue with ERC721 for in-game items is that most games **do** have some items that are identical. | ||
|
||
For example, say our card game has 300 different cards. But each of these cards needs more than one copy — otherwise not many people could play our game! | ||
|
||
Multiple copies of the same card ARE (for the most part) interchangeable. So it makes sense to have a token standard that takes this into account. | ||
|
||
ERC721x handles this use case by allowing you to define multiple tokens (think of them as card templates), and for each card you can define things like the total number of cards that will exist. | ||
|
||
You'll see how, as we walk you through step by step in the coming chapters 👨🏫 | ||
|
||
## Getting Started: Using ERC721x In Your Project | ||
|
||
When implementing a new ERC721x token, there's no need to start from scratch. Just like you would start your ERC20 token by copying a template from somewhere like <a href="https://github.com/OpenZeppelin/openzeppelin-solidity" target=_blank>Open Zeppelin</a> and making any changes you need, for ERC721x you can start by including our implementation into your project. | ||
|
||
To include it in your future projects, you can follow the instructions from our GitHub <a href="https://github.com/loomnetwork/erc721x" target=_blank>here</a>. But for this lesson, we've already gone ahead and included `ERC721XToken.sol` into your project. | ||
|
||
Don't worry about understanding all the code yet — we'll be walking you through step by step the pieces you need to understand. | ||
|
||
# Put It to the Test | ||
|
||
Let's start with the basics: we'll create a new contract called "ZombieCard" that inherits the logic from `ERC721XToken`. | ||
|
||
1. First, declare at the top that we're using `pragma solidity ^0.4.25`. | ||
|
||
2. After declaring the pragma, `import` the file `./ERC721XToken.sol` | ||
|
||
3. Next, declare a new `contract` named "ZombieCard". It should inherit from `ERC721XToken` using the keyword `is`. Leave the body of the contract empty for now. | ||
|
||
> Note: If you can't remember the basics and are getting lost, you can review <a href="https://cryptozombies.io/en/lesson/1/chapter/2">Lesson 1 Chapter 2</a> and <a href="https://cryptozombies.io/en/lesson/2/chapter/5">Lesson 2 Chapter 5</a>. |
Oops, something went wrong.