(Part 16) Ethereum Solidity - ERC721 Third Party Approval, Transfer, Events And Full Implementation(PT 16)
Repository
https://github.com/igormuba/EthereumSolidityClasses/tree/master/class16
What Will I Learn?
- ERC721 approval and authorizations
- Implementing transfers called by third parties
- Full implementation of ERC721
Requirements
- Internet connection
- Code editor
- Browser
Difficulty
- Intermediate
Tutorial Contents
On this tutorial, we will work on the approval functions from the ERC721 token standard. Previously we have implemented a fully functional ERC721 token, but the contract we have developed still lacks the approval functions.
The approval functions are used for token owners to allow third parties (like someone else or some app) to manage one of their tokens, or all of their assets. We will work a lot with mapping here.
Approved addresses
We need to set a mapping to keep track of what address has been approved to manage said token. Luckily for us, we have already implemented something very similar to this functionality with the mapping
mapping(uint => address) public ownership;
It is just a matter of either refactor the code or implement the new functionality to change it
NFT Approval
Now, we implement the function that will add an approved address for an NFT (Non-Fungible Token)
According to the ERC721 documentation
/// @notice Set or reaffirm the approved address for an NFT
/// @dev The zero address indicates there is no approved address.
/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized
/// operator of the current owner.
/// @param _approved The new approved NFT controller
/// @param _tokenId The NFT to approve
function approve(address _approved, uint256 _tokenId) external payable;
We have a very similar function for own internal use, but we must implement one with the standard naming so that third-party apps can know how to work with our tokens
function approve(address _approved, uint256 _tokenId) external payable{
require(msg.sender == ownership[_tokenId]);
ownership[_tokenId]=_approved;
tokens[_tokenId].owner=_approved;
}
With the function above we are effectively changing the ownership of a token, or, adding an approved address
The require
is a security measure, so that, only the current owner of a token can change the approval of the token.
Approval for all
The next step is to set a global approval so that an address (contract or wallet) can manage all the tokens from an account
For this, we will use a mapping of mappings mapping(address=>mapping(address=>bool)) public allowedAddresses;
At first glance, how the mapping of mappings works is not very intuitive, but it is not hard either to understand.
It maps many addresses to one address, so we can set more than one approved address to manage someone's funds
Now, to work with that, when you pass an address to allowedAddresses
it returns a mapping, so after that, you need to pass another address to get a boolean. At first, you might think that working with that would look like allowedAddresses[msg.sender[_operator]]
, but that is wrong, look at the function implemented below to see how it actually works
function setApprovalForAll(address _operator, bool _approved) external{
allowedAddresses[msg.sender][_operator]= _approved;
}
Here we do not need to set the require because we are setting on the mapping mapping(address=>mapping(address=>bool)) public allowedAddresses;
the caller of the functions allowed addresses, so you can't approve an address for someone else
Get approved
Now, we need to see who is the owner of a token with a function called getApproved
to fit on the standard. We already have a function used internally on our contract that looks like that, but we need, once again, to implement a new function to fit on the standard.
function getApproved(uint256 _tokenId) external view returns (address){
return ownership[_tokenId];
}
Is approved for all
This function has a very similar use as the above, but this time, we will use the mapping of mapping we have implemented before, so when we call the function isApprovedForAll
passing to it 2 arguments, first, the owner of the tokens, second, the address we want to see if it is authorized to manage the tokens of the owner, we will get a boolean return telling if the second address is authorized to manage the firsts tokens.
function isApprovedForAll(address _owner, address _operator) external view returns (bool){
return allowedAddresses[_owner][_operator];
}
Allowing an approved address to manage the tokens
Now, we need to refactor the code to allow for an approved address to manage the funds from an address that allowed him to manage.
The first function we will refactor is the one to change the price we want for a token, we will change the require so that it allows the allowed address to manage the token value
function changeValue(uint _id, uint newValue) public{
require(ownership[_id]==msg.sender||allowedAddresses[ownership[_id]][msg.sender]);
tokens[_id].value=newValue;
}
So
ownership[_id]==msg.sender
if the caller is the owner or
allowedAddresses[ownership[_id]][msg.sender]
the caller is allowed by the owner
tokens[_id].value=newValue;
the price is updated
The || stands for "or", so you can guess the piece of code allowedAddresses[ownership[_id]][msg.sender]
is the key for easily refactoring all the other transfer functions implemented before, like in
function transferFrom(address _from, address _to, uint256 _id) external payable{
if (msg.sender==tokens[_id].owner||allowedAddresses[ownership[_id]][msg.sender]){
uniqueTokens[msg.sender]+=1;
uniqueTokens[tokens[_id].owner]-=1;
tokens[_id].owner = msg.sender;
ownership[_id] = msg.sender;
}else{
require(msg.value>=tokens[_id].value);
balance[tokens[_id].owner]+=msg.value;
uniqueTokens[msg.sender]+=1;
uniqueTokens[tokens[_id].owner]-=1;
tokens[_id].owner = msg.sender;
ownership[_id] = msg.sender;
}
}
The events
The last thing we need to do now is to implement the events when a token transfer, creation or approval happens.
On the approve
function, before the change of ownership, I have added the call for the event
emit Approval(ownership[_tokenId], _approved, _tokenId);
On the setApprovedForAll
, the call for the event
emit ApprovalForAll(msg.sender, _operator, _approved);
On the createToken
, before the currentId
variable is automatically changed, I am calling the event
emit Transfer(msg.sender, msg.sender, currentId);
on the buyToken
function, before the change of ownership
emit Transfer(tokens[_id].owner, msg.sender, _id);
And on all the transfer functions
emit Transfer(_from, _to, _id);
How the code looks
This is the end code, totally compliant with the ERC721 standard and with a few security features implemented.
Please, note that if you want to implement this in a real production environment I recommend you to study and implement your own security measures, ad the security features I have implemented are superficial and obvious, nothing fancy, and a real hacker that really wants to exploit your code would exploit more advanced techniques than the ones I have covered on my tutorial series
The code is too big and would look ugly pasted here, but you can read the full implementation here
https://github.com/igormuba/EthereumSolidityClasses/blob/master/class16/erc721.sol
Curriculum
- (Part 15) Ethereum Solidity - ERC165, ERC721, Their Relation, Balances And Transfer Functions(PT 15)
- (Part 14) Ethereum Solidity - Token Uniqueness, Non-Fungibility And Transactions With Unique Tokens(PT 14)
- (Part 13) Ethereum Solidity In Truffle - Testnet Environment Function Calls And Truffle Testing(PT 13)
- (Part 12) Ethereum Solidity - Using Truffle, Ganache And Zeppelin To Deploy(PT 12)
- (Part 11) Ethereum Solidity - Multisig Contract As Bank,k Multiple Users, And Where To Implement App Logic(PT 11)
- (Part 10) Ethereum Solidity - Multiple inheritances, Diamond Problem And Function Polymorphism(PT 10)
- (Part 9) Ethereum Solidity Assembly - Return, Memory, Hexadecimal, Pointers, And Memory Addressing(PT 9)
- (Part 8) Ethereum Solidity - Assembly, Reducing Costs And Creation Your Low-Level Expression(PT 8)
- (Part 7) Ethereum Solidity - Fallback, Ethereum Fractions, And Collateral Backed Contract(PT 7)
- (Part 6) Ethereum Solidity - Custom Variable Functionalities, Libraries, Using Libraries For Security(PT 6)
- (Part 5) Ethereum Solidity - Custom Access Modifiers, Security Breach Alerts, Assert And Require(PT 5)
- (Part 4) Ethereum Solidity Development - Inheritance, Working With Multiple Contracts And Simple Mutability(PT 4)
- (Part 3) Ethereum Solidity Development - Contract Mutability, DelegateCall And Calling Functions By Address(PT 3)
- (Part 2) Ethereum Solidity Development - Deploying, Security And ERC20 Compliance(PT 2)
- (Part 1) Ethereum Solidity Development - Getting Started + Lower Level Explanation (PT 1)
Beneficiaries
This post has as beneficiaries
@utopian.pay with 5%
using the SteemPeak beneficiary tool
Thank you for your contribution. Another beautiful and useful piece of work.
The only suggestion I have is pertaining showing some outcome of implementing and running those functions, which would be pretty helpful to your readers.
Your contribution has been evaluated according to Utopian policies and guidelines, as well as a predefined set of questions pertaining to the category.
To view those questions and the relevant answers related to your post, click here.
Need help? Chat with us on Discord.
[utopian-moderator]
Thank you for your review, @mcfarhat! Keep up the good work!
Hi @igormuba!
Your post was upvoted by @steem-ua, new Steem dApp, using UserAuthority for algorithmic post curation!
Your post is eligible for our upvote, thanks to our collaboration with @utopian-io!
Feel free to join our @steem-ua Discord server
Hey, @igormuba!
Thanks for contributing on Utopian.
We’re already looking forward to your next contribution!
Get higher incentives and support Utopian.io!
Simply set @utopian.pay as a 5% (or higher) payout beneficiary on your contribution post (via SteemPlus or Steeditor).
Want to chat? Join us on Discord https://discord.gg/h52nFrV.
Vote for Utopian Witness!