Hi, fren! gm. ☀️
In this blog post, we're going to cover some of the most common ERC token standards that are used for the creation of fungible, non-fungible, and semi-fungible tokens.
So, without further ado, let's get started!
First of all, WTH is ERC? 🤔
ERC stands for Ethereum Request for Comments, and it is a proposal document that developers write which is then reviewed by the Ethereum community using a process called 'Ethereum Improvement Proposal' or EIP.
What is ERC-20?
ERC-20 is the 20th proposal, which was proposed in 2015 by Fabian Vogelsteller and was integrated into the Ethereum blockchain in 2017. It is the most commonly used token standard in the Ethereum ecosystem.
The tokens created using the ERC-20 standard have three properties:
🔹 They are Fungible: meaning all tokens can be used interchangeably and each token holds the same value. Think of a Dollar bill 💵
🔹 They are transferrable: the tokens can be sent from one address to another.
🔹 They have a fixed supply.
The Need for ERC-20
So back in the day when everyone was trying to come up with their own fungible tokens, there were no set rules or guidelines for developing tokens. So, everyone had to reinvent the wheel to create their own tokens.
As a result, all the tokens were different from each other. This was extremely painful for developers to work with other tokens, and for exchanges and wallets to list and handle different tokens on their platforms.
The ERC-20 introduced a standard (or a set of rules) that allowed developers to create their own 'Fungible' tokens on the Ethereum blockchain. It laid out a set of guidelines that all Ethereum-based tokens must adhere to.
Today, wallets like Metamask and exchanges like Binance use the ERC-20 standard to list various standardized tokens onto their platforms and handle the exchange of value between them.
The Technical Aspects
ERC-20 provides a standard interface for fungible tokens.
So, what is an Interface? An interface defines rules that a program must follow.
In this case, it contains a set of functions and events that a smart contract must implement. While an interface doesn't specify how the functions or events should be implemented, it specifies what functions or events should be implemented.
The ERC-20 standard defines three optional and six mandatory functions that a smart contract should implement to create a fungible token.
The three optional ones are:
🔹 name()
: returns the name of the token - e.g. ShurikenToken
.
function name() public view returns (string)
🔹 symbol()
: returns the symbol of the token - e.g. CST
.
function symbol() public view returns (string)
🔹 decimals()
: returns the number of decimals places the token uses. More on decimals here.
function decimals() public view returns (uint8)
The six mandatory functions are:
🔹 totalSupply()
: determines the total supply of tokens. Once this limit is reached, the smart contract will refuse to mint new tokens.
function totalSupply() public view returns (uint256)
🔹 balanceOf()
: takes in _owner
address parameter and returns the number of tokens a given address holds.
function balanceOf(address _owner) public view returns (uint256 balance)
🔹 transfer()
: transfers a certain number of tokens from the total supply to a user address.
function transfer(address _to, uint256 _value) public returns (bool success)
🔹 transferFrom()
: allows users to transfer tokens from one address to another.
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)
🔹 approve()
: verifies whether a smart contract is allowed to allocate a certain amount of tokens to a user considering the total supply, making sure that there are no extra or missing tokens.
function approve(address _spender, uint256 _value) public returns (bool success)
🔹 allowance()
: how much an address is allowed to spend. Essentially it checks whether a user has enough balance to make transactions to another user.
function allowance(address _owner, address _spender) public view returns (uint256 remaining)
Apart from these functions, the smart contract should also emit two events:
🔹 Transfer
: this event is emitted when some amount of tokens are transferred from one wallet to another. It gives the address of both the sender and recipient as well as the number of tokens transferred.
event Transfer(address indexed from, address indexed to, uint256 value);
🔹 Approval
: this event is emitted when the approve() function is successfully executed.
event Approval(address indexed _owner, address indexed _spender, uint256 _value)
ℹ️ If you're unfamiliar with Events in Solidity, here's a fantastic blog post by Patrick Collins on the Chainlink blog.
The ERC-20 token standard provides a basic structure for the creation of fungible tokens in a uniform manner and takes out the complexities that used to be introduced with the "reinventing the wheel" approach in the earlier days.
Now that the significance of ERC-20 is clear, let's move on to another common token standard that powered the world of something we know as NFTs.
What is ERC-721?
While the ERC-20 token standard caters to the creation of fungible tokens, ERC-721 is the token standard for creating non-fungible tokens (NFTs). It was proposed by William Entriken, Dieter Shirley, Jacob Evans, and Nastassia Sachs from the CryptoKitties team in 2018.
ERC-721 defines a minimum interface a smart contract must implement to allow unique tokens to be managed, owned, and traded.
Unlike the ERC-20 standard, where there is no concept of "uniqueness", all tokens that are created using the ERC-721 standard are "non-fungible" or unique, and each NFT is linked to its respective owner and possesses its metadata and a unique tokenId
.
Image Credit: ERC721.org
The use cases for the ERC-721 can be both digital & physical ownership. We can own digital assets like digital art, music NFTs, digital land like Decentraland, as well as physical assets like tokenized house deeds & real art.
The ERC-721 Functions and Events
The ERC-721 standard has the following functions that a smart contract should implement:
🔹 ownerOf()
: takes in a _tokenId
and returns the address of the owner of the NFT with that particular _tokenId
.
function ownerOf(uint256 _tokenId) external view returns (address);
🔹 transferFrom()
: transfers the ownership of an NFT from one address ( _from
) to another ( _to
).
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
🔹 safeTransferFrom()
: transfer the ownership of an NFT from one address to another. It also checks if an address receiving the NFT is an ERC-721 receiver (i.e. whether it is capable of receiving the token or not) making sure that the tokens aren't lost permanently.
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
🔹 approve()
: approves another entity the permission to transfer an NFT on behalf of the owner. This allows NFT platforms like OpenSea to transfer ownership of NFTs you listed for sale when someone buys it.
function approve(address _approved, uint256 _tokenId) external payable;
🔹 setApprovalForAll()
: enables or disables approval for a third party to manage all NFTs of a caller (i.e. msg.sender
).
function setApprovalForAll(address _operator, bool _approved) external;
🔹 getApproved()
: takes in a _tokenID
and gets the approved address for a single NFT with that particular ID.
function getApproved(uint256 _tokenId) external view returns (address);
🔹 isApprovedForAll()
: queries if an address is an authorized operator for another address and returns true
or false
given the approval status of the operator.
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
ERC-20 compatible functions:
🔹 name()
and symbol()
: used to define the name & symbol of a token. These optional functions help in providing compatibility with the ERC-20 standard and make it easier for existing wallets to display information about the token.
function name() external view returns (string _name);
function symbol() external view returns (string _symbol);
🔹 totalSupply()
: determines the total supply of tokens. In the case of ERC-721, the total supply doesn't necessarily have to be a constant.
function totalSupply() external view returns (uint256);
🔹 balanceOf()
: returns the count of all the NFTs a given address owns.
function balanceOf(address _owner) external view returns (uint256);
Apart from these functions, the smart contract should also emit the following events:
🔹 Transfer
: this event is emitted when the ownership of an NFT is transferred, or a new NFT is created.
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
🔹 Approval
: this event is emitted when the approved address for an NFT is changed (whenever approve() function is executed).
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
🔹 ApprovalForAll
: this event is emitted when an operator is enabled or disabled for an owner. This operator can manage all NFTs of an owner.
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
So, while the ERC-721 token standard allows us to create unique "non-fungible" tokens, it does have its shortcomings.
One key issue is that since all the tokens are unique, each token has to be sent in its own individual transaction. You can't just transfer 10 NFTs in a single transaction like you would normally do with ERC-20 tokens. As a result, it makes the entire process costly.
And what if I don't want all my tokens to be unique? 🤔
Think of an FPS game. Let's say the game has different types of unique skins for different weapons: Swords, Guns, throwables, etc, and an in-game currency called NinjaCoins. Let's say a user owns 1 gun, 10 swords, and 10,000 NinjaCoins.
Now, if we work with the ERC-721 standard, we have to write a different smart contract for each unique item or NFT, plus we have to write another smart contract for our NinjaCoins (that will be an ERC-20 token since it's fungible).
What if there was a single token standard that could facilitate the creation of both fungible and non-fungible tokens in a single deployed smart contract?
Enter ERC-1155! 😎
What is ERC-1155?
The ERC-1155 token standard was introduced by Witek Radomski & the Enjin team in 2018.
The ERC-1155 token standard combines the abilities of both ERC-20 and ERC-721 and tackles their shortcomings by providing a standard interface for smart contracts to manage multiple token types.
Some key features of the ERC-1155 token standard are:
- 🛠 Ability to create fungible, non-fungible, and semi-fungible tokens with a single smart contract.
- 🔀 Ability to do batch transfers: sending multiple tokens in a single-transactions.
- ⛽️ Gas efficient.
- 📦 Requires less storage space.
One key difference between the ERC-20, ERC-721, and ERC-1155 is:
🔹 The ERC-20 maps different addresses to the number of tokens they hold,
🔹 The ERC-721 maps unique token IDs to owners, and
🔹 The ERC-1155 has a nested mapping of IDs to owners to amount of tokens they own.
Image Credit: OpenSea blog
The ERC-1155 Functions and Events
The ERC-1155 token standard has the following functions that a smart contract should implement:
🔹 safeTransferFrom()
: transfers _value amount of an _id from one address ( _from
) to another ( _to
).
function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) external;
🔹 safeBatchTransferFrom()
: allows the batch transfer of tokens. This function works pretty similarly to the transferFrom()
function in the ERC-20 standard. The only difference being we pass the _values
and _ids
as an array.
// ERC-20
function transferFrom(address from, address to, uint256 value) external returns (bool);
// ERC-1155
function safeBatchTransferFrom(
address _from,
address _to,
uint256[] calldata _ids,
uint256[] calldata _values,
bytes calldata _data
) external;
For example, let's say we want to transfer 10 swords, 5 guns, and 100 NinjaCoins to an address. Let's say the IDs for swords, guns, and NinjaCoins are 2, 3, and 4 respectively.
So, _ids
= [2, 3, 4] and _values
= [10, 5, 100]
So, the resulting transfer with the safeBatchTransferFrom()
function would be:
Transfer 10 tokens with id 2 from _from
to _to
.
Transfer 5 tokens with id 3 from _from
to _to
.
Transfer 100 tokens with id 4 from _from
to _to
.
🔹 balanceOf()
: takes in an _owner
and _id
and gets the balance of an account's tokens of that respective token id.
function balanceOf(address _owner, uint256 _id) external view returns (uint256);
🔹 balanceOfBatch()
: takes in an array of _owners
addresses and _ids
, and gets the balance of 'each' owner/id pair.
function balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids) external view returns (uint256[] memory);
For example, given _ids
= [2, 3, 4] and _owners
= [0xd1dc..., 0x7c2d..., 0x1bdc...], the return value will be:
[
balanceOf(0xd1dc...),
balanceOf(0x7c2d...),
balanceOf(0x1bdc...)
]
🔹 setApprovalForAll()
: enables or disables approval for a third party ("operator") to manage all of the caller's tokens.
function setApprovalForAll(address _operator, bool _approved) external;
It returns true
if the operator is approved, else false
.
💡 The approvals in ERC-1155 are slightly different than ERC-20. Instead of approving specific amounts, you set an operator to approved or not approved via
setApprovalForAll()
.
🔹 isApprovedForAll()
: queries the current approval status of an operator for a given owner and returns true
or false
given the approval status of the operator. Similar to the isApprovedForAll()
function in the ERC-721 standard.
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
Apart from these functions, the smart contract should also emit the following events:
🔹 TransferSingle
& TransferBatch
: either of these events must be emitted when tokens are transferred (i.e. when safeTransferFrom()
or safeBatchTransferFrom()
functions are executed).
event TransferSingle(address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value);
event TransferBatch(address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values);
🔹 ApprovalForAll
: this event is emitted when approval for a second party or operator address to manage all tokens for an owner address is enabled or disabled (i.e. when setApprovalForAll()
function is executed).
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
🔹 URI
: must be emitted when the URI for a token ID is updated.
event URI(string _value, uint256 indexed _id);
Conclusion
That's it for now, fren!
We've covered a lot about the three most common ERC token standards in this blog post.
We learned how the ERC-20 standard provides a basic structure for the creation of fungible tokens in a uniform manner, and how ERC-721 tackled the shortcomings & inefficiencies of the ERC-20 standard by providing a standard interface for creating & managing non-fungible tokens.
And then how the ERC-1155 standard sort of combined the abilities of both ERC-20 and ERC-721 by tackling their shortcomings such as storage redundancy and gas inefficiency, providing a "best of both worlds" experience.
But it does not mean that the ERC-1155 standard does not need improvement. The ever-evolving nature of the Ethereum ecosystem demands improvements in token standards as new advancements are made.
Prefer a TL;DR Thread?
I've got you covered! 🙌🏻
Here's a TL;DR version of this blog post. While you're at it, please consider following me @cryptoshuriken for more such content.
Image: A thread on ERC Token Standards - CryptoShuriken
Recommended Resources
- EIP-20 Token Standard - Ethereum Improvement Proposals
- ERC-20 Tokens, Explained - CoinTelegraph
- EIP-721 Token Standard - Ethereum Improvement Proposals
- EIP-1155 Token Standard - Ethereum Improvement Proposals
- The Non-Fungible Token Bible - OpenSea Blog
- Ethereum Developer Docs
- OpenZeppelin Docs
If you enjoyed reading this article, please consider subscribing here so that you get a notification in your mail whenever I post new stuff. Subscribing is free, and it gives me a ton of motivation to keep going.
#WAGMI ✌🏻
Top comments (0)