Skip to content

Latest commit

 

History

History
360 lines (273 loc) · 16.2 KB

07-erc721x.md

File metadata and controls

360 lines (273 loc) · 16.2 KB
title actions requireLogin material
Backward Compatibility With ERC721 NFTs
checkAnswer
hints
true
editor
language startingCode answer
sol
ZombieCard.sol ERC721XToken.sol
pragma solidity ^0.4.25; import "./ERC721XToken.sol"; import "./Ownable.sol"; contract ZombieCard is ERC721XToken { mapping(uint => uint) internal tokenIdToIndividualSupply; // 1. Declare mapping and uint here event TokenAwarded(uint indexed tokenId, address claimer, uint amount); function name() external view returns (string) { return "ZombieCard"; } function symbol() external view returns (string) { return "ZCX"; } function individualSupply(uint _tokenId) public view returns (uint) { return tokenIdToIndividualSupply[_tokenId]; } function mintToken(uint _tokenId, uint _supply) public onlyOwner { require(!exists(_tokenId), "Error: Tried to mint duplicate token id"); _mint(_tokenId, msg.sender, _supply); tokenIdToIndividualSupply[_tokenId] = _supply; } function awardToken(uint _tokenId, address _to, uint _amount) public onlyOwner { require(exists(_tokenId), "TokenID has not been minted"); if (individualSupply(_tokenId) > 0) { require(_amount <= balanceOf(_from, _tokenId), "Quantity greater than remaining cards"); _updateTokenBalance(msg.sender, _tokenId, _amount, ObjectLib.Operations.SUB); } _updateTokenBalance(_to, _tokenId, _amount, ObjectLib.Operations.ADD); emit TokenAwarded(_tokenId, _to, _amount); } // 2. Define function here }
// Full implementation with all included files at https://github.com/loomnetwork/erc721x pragma solidity ^0.4.25; 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); } }
pragma solidity ^0.4.25; import "./ERC721XToken.sol"; import "./Ownable.sol"; contract ZombieCard is ERC721XToken { mapping(uint => uint) internal tokenIdToIndividualSupply; mapping(uint => uint) internal nftTokenIdToMouldId; uint nftTokenIdIndex = 1000000; event TokenAwarded(uint indexed tokenId, address claimer, uint amount); function name() external view returns (string) { return "ZombieCard"; } function symbol() external view returns (string) { return "ZCX"; } function individualSupply(uint _tokenId) public view returns (uint) { return tokenIdToIndividualSupply[_tokenId]; } function mintToken(uint _tokenId, uint _supply) public onlyOwner { require(!exists(_tokenId), "Error: Tried to mint duplicate token id"); _mint(_tokenId, msg.sender, _supply); tokenIdToIndividualSupply[_tokenId] = _supply; } function awardToken(uint _tokenId, address _to, uint _amount) public onlyOwner { require(exists(_tokenId), "TokenID has not been minted"); if (individualSupply(_tokenId) > 0) { require(_amount <= balanceOf(_from, _tokenId), "Quantity greater than remaining cards"); _updateTokenBalance(msg.sender, _tokenId, _amount, ObjectLib.Operations.SUB); } _updateTokenBalance(_to, _tokenId, _amount, ObjectLib.Operations.ADD); emit TokenAwarded(_tokenId, _to, _amount); } function convertToNFT(uint _tokenId, uint _amount) public { require(tokenType[_tokenId] == FT); require(_amount <= balanceOf(msg.sender, _tokenId), "You do not own enough tokens"); } }

Using ERC721x instead of ERC721 comes with some pretty significant benefits — things like batch minting of tokens, batch transfers, and multiple token classes in the same contract.

In Zombie Battleground, being able to batch mint and batch transfer resulted in a 1,000x savings in gas fees when we first delivered cards to our initial backers 😮

For this reason, we think that over time more and more wallets and marketplaces will switch over from traditional ERC721 to ERC721x to take advantage of these savings.

But in the meantime, we've also made ERC721x fully backward compatible, and provided a way for users to use their tokens on existing ERC721 wallets and marketplaces that don't yet support the new standard.

To do this, we'll have to implement some functions in our contract that convert tokens from Fungible to Non-Fungible, and vice versa.

Converting from FT to NFT

ERC721x was built to support both FTs and NFTs in the same contract.

The main difference in the implementation is that NFTs each have a unique tokenId, whereas FTs don't because they're interchangeable with each other (and giving each a unique token ID would make it more gas-expensive to do things like batch transfers).

So in order to make ERC721x tokens compatible with legacy wallets, we'll need to provide a way to convert an FT to an NFT — which will generate a unique ID for that token in the process.

With our Zombie Battleground implementation on Loom PlasmaChain, we do this when the user wants to transfer their token from PlasmaChain to Ethereum Mainnet.

But for our implementation in this lesson of CryptoZombies, we can simply write a method that performs the conversion — then the user can call it manually if they need to convert the token to an ERC721 to work with a legacy service.

Put it to the test

To start with, we'll need to declare some new variables in our contract where we'll store some information about the NFT.

  1. Declare a mapping of uint => uint called nftTokenIdToMouldId that is internal.

Since we're generating a new tokenId for the NFT, we need a way to remember what the original FT's tokenId was. We'll call this ID the card's "mould" — imagine it like an iron mould you would use to print new cards that are identical copies of each other.

  1. We'll need a way to generate new NFT token IDs. So below the mapping, declare a uint called nftTokenIdIndex and set it equal to 1000000.

We want to make sure our NFT token IDs don't collide with any FT IDs. So we'll start NFTs at one million, and reserve any ID less than that for FTs. It's unlikely we'll have more than one million cards in our game.

  1. At the bottom of our contract, declare a function named convertToNFT that takes a uint _tokenId and uint _amount as its arguments. It should be public.

  2. We'll want 2 require statements at the start of our function. First, we'll want to require that this token we're about to convert is a FT and not an NFT. You can do this with some code from ERC721XToken.sol: require(tokenType[_tokenId] == FT);

  3. Second, we want to require that msg.sender has a _tokenId balance that is greater than or equal to _amount. This is the same require statement we used in awardToken — but this time we'll want to output the error message "You do not own enough tokens".

    Note: Our answer checker is basic. It will only accept the answer if _amount comes first.

We'll finish implementing the rest of this function in the next chapter.