Deciphering Token Standards in Ethereum Part-IV — The Failure of ERC777

A technical deep dive of ERC777 token standard and understanding why it failed.

Deciphering Token Standards in Ethereum Part-IV — The Failure of ERC777

Here I am, with another ERC token standard. Previously we talked about ERC20, ERC721, and ERC1155. And now it’s time for ERC777.

You already know what an ERC is and why we need them for tokens in the Ethereum ecosystem, if not then check out my previous articles on this topic here.

Although ERC777 was considered an official standard, it wasn’t able to draw attention and adoption. There are various reasons for that, we will discuss all of them in this article. But before discussing the reasons for failure we need to understand the ERC completely.

What is ERC777 and why do we need it?


A Brief History of Fungible Tokens

As we are already aware, ERC20 was the first token standard proposed, and it has very limited functionality, this is obvious because that was the earliest point in the Ethereum ecosystem.
One of the major disadvantages you might already know is about sending the tokens to a smart contract, that was not designed to handle any ERC20 token. In this case, the tokens get stuck forever in the smart contract.

In the present time, there are millions of dollars worth of tokens stuck in smart contracts.

Another issue that is considered a bad UX and a security vulnerability about ERC20 is the approve and transferFrom functionality. Which makes the user sign two transactions, and also leads to a security vulnerability of double spending by the spender.

So what was the solution to it?

Before ERC777, ERC223 was proposed.

It wasn’t a very complex standard, instead, it just had two additional functionalities.

To check if the recipient address is a contract and if it is, then check if it can receive the tokens by calling a function tokenReceived. It was very similar to the concept of ERC721TokenReceiver

ERC223 also claimed to remove the need for users to call approve and then transferFrom by defining a new function that can transfer the tokens and tell the recipient contract that the tokens have been transferred.

Meanwhile, we got a new ERC that is ERC777 which contains some more functionalities along with what ERC223 was trying to implement. We can’t say that ERC777 is an extended version of ERC20 because it implements the standard in a very different way. Although it is here to fix the ERC20 standard, it’s not fully derived from it, unlike ERC223.

What new feature does it offer?

Well, it has a concept of hooks similar to the tokenReceived in ERC223 but ERC777 offers this hook for both receiving and sending tokens. These hooks are introduced with much more functionality like reverting on receiving or sending tokens. We will talk about this later.

Apart from hooks, it has a concept of operators similar to ERC721 that allows a third-party address to manage the tokens on behalf of the holder.
Eliminating the process of increasing allowance and decreasing allowance.

Let’s discuss this in detail.

First of all, we will look at the interface, and then delve deep into it. This way we can understand the functionalities it offers more technically rather than reading them in theories.

Interface of ERC777

interface ERC777Token {
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function totalSupply() external view returns (uint256);
    function balanceOf(address holder) external view returns (uint256);
    function granularity() external view returns (uint256);

    function defaultOperators() external view returns (address[] memory);
    function isOperatorFor(
        address operator,
        address holder
    ) external view returns (bool);
    function authorizeOperator(address operator) external;
    function revokeOperator(address operator) external;

    function send(address to, uint256 amount, bytes calldata data) external;
    function operatorSend(
        address from,
        address to,
        uint256 amount,
        bytes calldata data,
        bytes calldata operatorData
    ) external;

    function burn(uint256 amount, bytes calldata data) external;
    function operatorBurn(
        address from,
        uint256 amount,
        bytes calldata data,
        bytes calldata operatorData
    ) external;

//every time there is a transfer of tokens
    event Sent(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256 amount,
        bytes data,
        bytes operatorData
    );
//whenever tokens are minted
    event Minted(
        address indexed operator,
        address indexed to,
        uint256 amount,
        bytes data,
        bytes operatorData
    );
//whenever tokens are burned
    event Burned(
        address indexed operator,
        address indexed from,
        uint256 amount,
        bytes data,
        bytes operatorData
    );
//When someone authorizes an operator
    event AuthorizedOperator(
        address indexed operator,
        address indexed holder
    );
// When someone revokes and operator
    event RevokedOperator(address indexed operator, address indexed holder);
}

Functions

There are 13 functions. The interface seems pretty much similar to ERC20 but with a few additional functionalities, let’s discuss all of them.

View functions are discussed first

  1. name
    This function should return the name of the token. Pretty obvious :’-)
function name() external view returns (string memory);

2. symbol
This should return the symbol of the token.
For example, ETH for Ether, BNB for Binance coin, etc.

function symbol() external view returns (string memory);

3. totalSupply
This should return the total amount of minted tokens that are in circulation. In circulation means excluding the number of tokens that are burned.

function totalSupply() external view returns (uint256);

4. balanceOf
As the name suggests this will return the balance of any address. Yeah, the same way we do in ERC20, recording balances in a mapping.

function balanceOf(address holder) external view returns (uint256);

5. granularity
This is something that replaces the concept of decimals in ERC20.
Granularity means the smallest fraction of the token.
Every value of tokens that will be passed in the mint, burn, and send functions should be multiple of the granularity, the same goes with the balance too. By default, granularity should always return 1, unless there is some solid reason to make it larger.
Like the decimals in ERC20, granularity also should be immutable and set at creation time. It can’t be zero.

function granularity() external view returns (uint256);
The concept of Operators is pretty similar to the operators in ERC721.
It is different from the ERC20 approve function. As in the approve/Allowance functionality, the holder could assign a value to any other address but in ERC777 if you assign an operator then that address can spend all your tokens. You don't need to assign them new values every time.

6. defaultOperators
Default operators refer to the addresses that are being set at the creation time of the contract. Default operators can spend everyone’s tokens.
This function should return an array of all default operators.
One more thing to keep in mind is that default operators can’t be changed later.

function defaultOperators() external view returns (address[] memory);

7.isOperatorFor
This function should return if any address is an operator for the given address, passing both addresses as arguments.
Remember this is different from default operators as these operators are assigned by the token holder.

function isOperatorFor(address operator,address holder) external view returns (bool);

8. authorizeOperator
This function should allow users to assign any address as an operator.
The holder shouldn’t be able to assign his address as an operator.

function authorizeOperator(address operator) external;

9. revokeOperator
As suggested by the name, this function should be used to revoke an operator.

function revokeOperator(address operator) external;
Before getting into the transfers of tokens, one thing to keep in mind is that every function that has a movement of tokens is supposed to call the hooks.

10. send
Don’t get confused with the name, it’s the same as the transfer function we all know, but the name is changed to send. One more difference is the number of parameters. There is an additional parameter, data. We can define the hooks in a way that they execute based on the data given in this send function.
This mechanism seems familiar, right? Yes, it is similar to the transfers of Ether (The native currency of Ethereum). It is also mentioned in the official ERC that “This standard uses the same philosophy as Ether”.

function send(address to, uint256 amount, bytes calldata data) external;

11. operatorSend
This function is equivalent to the transferFrom function in ERC20.
An authorized operator can send tokens on behalf of another address using this function. Notice there is an extra operatorData parameter too that the operator may use to send some data with the transaction.
The holder of tokens can also use this function and in this case, this should work like the send function, with the additional parameter of operatorData

function operatorSend(
        address from,
        address to,
        uint256 amount,
        bytes calldata data,
        bytes calldata operatorData
    ) external;

12. burn
The holder should be able to burn their token using this function. As usual, this should decrease the totalSupply and the balance of the holder as well.
There shouldn’t be any increment in the balance for the zero address, although we assume that the tokens are dumped into the zero address.
This function should also call the hooks.

function burn(uint256 amount, bytes calldata data) external;

13. operatorBurn
Whenever an operator needs to burn the tokens on behalf of the holder, they should use this function.
The functionality will be the same as burning it’s just that an operator should be able to call this function.

function operatorBurn(
        address from,
        uint256 amount,
        bytes calldata data,
        bytes calldata operatorData
    ) external;


Seeing the interface, we already have concluded that this is not similar to ERC20, although the core idea remains the same, the implementation is quite different.

Now one of the most significant features of ERC777 is that it introduces the concept of hooks in fungible tokens and utilizes the possibilities offered by ERC1820.
But before understanding the relation between both ERCs we need to know what exactly ERC1820 offers.

ERC1820

ERC1820 is also called the registry contract. To better understand this, you need to recall ERC165 which we talked about in my ERC721 article.
In short, ERC165 is used by contracts to prove that they implement a certain interface.
Every contract implements ERC165 separately, but what if we have a single contract deployed on the blockchain and use it as a registry contract?

This is the concept used by ERC1820, i.e. every chain will have a single registry contract where every other contract or EOA can register itself with the respective interface that they are implementing.

How ERC777 is related to ERC1820?

Remember we talked about hooks earlier? This is where ERC1820 comes in. Hooks play an important role in every token standard. As they allow us to define some custom logic during the transfers of tokens. The logic can be anything like “Mint an NFT when someone sent X amount of tokens” etc.

We have 2 different hooks in ERC777,tokensReceived and tokensToSend. As suggested by the name tokensToSendwill be called for the sender, and tokensReceivedwill be executed on the receiver’s side. Let’s see them one by one.

tokensToSend hook

Whenever there is a transfer of tokens, the transfer function should query the ERC1820 registry contract if the sender implements the ERC777TokensSender interface, if it does, then the tokensToSend hook should be called. This hook should allow the sender to revert the transaction (this depends on the wallet implementation of the interface).

tokensReceived hook

After the transfer of tokens, it should be checked if the recipient implements the ERC777TokensRecipient interface. If it does then the tokensReceived hook should be called.

The imperative thing to note is that, if the recipient is a contract and it doesn’t implement the ERC777TokensRecipient interface, the call should revert. But as we know ERC777 remains backward compatible with ERC20, if this function was called via a transfer or transferFrom function then it should keep processing even if the interface is not implemented by the receiver.

These hooks can also be used in rejecting the incoming or outgoing tokens, just by reverting the hook, when any condition provided is true. Authors claimed that this can be used to reject spam tokens.

As of now we already know pretty much everything about ERC777.

But it failed!

Despite offering more functionalities and fixing the approval/transfer issue of ERC20, why does no one use this standard and why is it not recommended? Openzeppelin also removed the ERC777 library.

One thing is very clear till now, ERC777 is much more complex than ERC20, but we need to make sure that adding complexity to something should provide value worth the complexity.
This is where the community never liked ERC777. It is considered over-engineered.

Calling the sender hook before updating the states as I mentioned earlier, is also a vulnerable practice as it violates the Checks, Effects, and Interaction (CEI) pattern in solidity. And there are many more concerns raised by developers.

In the end, using ERC777 is not advised, instead we can use other ERCs, like ERC2612, ERC1363, etc. which are extensions for ERC20.

That’s it for this post, hope you understood the logic of ERC777 and why isn’t it used, feel free to ask any questions you might have HERE.