# How to build and use ERC-777 tokens

Author: Markus Waas

Published: 2020-11-08T05:34:50.000Z

Updated: 2026-09-13T14:25:30.000Z

Source: [https://soliditydeveloper.com/erc-777](<https://soliditydeveloper.com/erc-777>)

## Compatibility and review

Before you start

Historical ERC-777 tutorial using Solidity 0.7.4 and OpenZeppelin Contracts 3. OpenZeppelin 5 removed ERC-777. The standard permits ERC-20 compatibility but does not require it; recipient-hook behavior depends on the transfer entry point.

[Official reference](<https://eips.ethereum.org/EIPS/eip-777>)

The new upgraded standard for ERC-20 tokens is becoming more and more popular. The implementation used here supports ERC-20 compatibility, you can easily create one using the Openzeppelin contracts and there are many interesting new features not available in ERC-20.

Should you upgrade from ERC-20? Well let's look into what ERC-777 is.

![xkcd upgrades](<https://cdn0.scrvt.com/b095ee27d37b3d7b6b150adba9ac6ec8/1218ace4160cbdb0/f8859cacf238/v/ccce1d8da156/worst_thing_that_could_happen_2x.png>)

## The Features of ERC-777

Let's explore all features with direct code examples that you can simply follow via Remix. Let's create an ERC-777 contract:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.7.4;

import "http://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.2.1-solc-0.7/contracts/token/ERC777/ERC777.sol";
import "http://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.2.1-solc-0.7/contracts/token/ERC777/IERC777Sender.sol";
import "http://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.2.1-solc-0.7/contracts/token/ERC777/IERC777Recipient.sol";
import "http://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.2.1-solc-0.7/contracts/introspection/ERC1820Implementer.sol";
import "http://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.2.1-solc-0.7/contracts/introspection/IERC1820Registry.sol";

contract TestERC777 is ERC777 {
    constructor(
        uint256 initialSupply,
        address[] memory defaultOperators
    ) ERC777("Gold", "GLD", defaultOperators) {
        _mint(msg.sender, initialSupply, "", "");
    }
}
```

Don't worry about the imports and defaultOperators yet, we will need them later on.

### 1. Similar to sending ETH + Data

Now that we have a deployed token, we can use it similarly to ERC-20. One difference though is a new transfer function. While in ERC-20 we used to do `token.transfer(receiver, amount)`, now with ERC-777 we do `token.send(receiver, amount, "")`.

Why this change?

First of all it's now more similar to the way of sending ETH via the [send function](<https://solidity.readthedocs.io/en/v0.7.4/types.html#members-of-addresses>). And then we also have a new `bytes data` field which enables one to send arbitrary data along with the transfer. This can be used freely and thus adds extra functionality in the token transfer call. In the regular transfer case you would just leave it empty.

### 2. Registry (EIP-1820)

This is technically its own standard as the 777 standard was getting too large. So a new standard [EIP-1820](<https://eips.ethereum.org/EIPS/eip-1820>) was created. One reason the standard was needed was to enable the hooks functionality (see below). But let's look at the 1820 standard now.

We have previously looked at the EIP-165 standard [here](<https://soliditydeveloper.com/eip-165>). As a quick recap EIP-165 allows smart contracts to register as implementing a specific interface. This can ensure no invalid smart contract addresses are used, for example preventing to send tokens to a contract that doesn't have functions to retrieve those tokens. In EIP-165 a contract must return `true` for the `supportsInterface(interfaceId)` function if it implements the given interface.

**So what's different in EIP-1820?**

#### Single Registry Contract

In contrast to EIP-165 we don't have contracts themselves implementing a supportsInterface function themselves. Instead there is a single registry contract. This registry is always deployed on the same address for every Ethereum network: `0x1820a4b7618bde71dce8cdc73aab6c95905fad24`.

How is it ensured that the registry is always available at the same address for each network?

The solution for this is quite interesting. The so-called 'Nick method' was first described [here](<https://medium.com/@weka/how-to-send-ether-to-11-440-people-187e332566b7>). The cryptographic signature for signed transactions in Ethereum consists of three values, v, r and s. Usually these have to be generated by a private key and the `ecrecover` function will retrieve the signer public key, i.e., the Ethereum address. But as it turns out you can choose these values at random and in 50% of the times will still get a valid signature.

This is being used by the EIP-1820 standard to define fixed values for v, r and s which produce a valid signature:

```javascript
{
    v: 27,
    r: '0x1820182018201820182018201820182018201820182018201820182018201820',
    s: '0x1820182018201820182018201820182018201820182018201820182018201820'
}
```

Now looking at these values, you can see that those are not random. So you also now know that nobody actually owns the private key that could have created this transaction signature. Since nobody owns the key and this signature is only valid to deploy the regular registry contract at the 1820... address, we have a guarantee that in every Ethereum network under the 0x1820a4b7618bde71dce8cdc73aab6c95905fad24 address:

- we can find the correct registry contract
- or no contract at all (not yet deployed)

Take a look at [https://etherscan.io/address/0x1820a4b7618bde71dce8cdc73aab6c95905fad24\#code](<https://etherscan.io/address/0x1820a4b7618bde71dce8cdc73aab6c95905fad24#code>) to see the registry in action for the mainnet.

#### Registering an Interface

Given the single registry contract, the manager of an address can register an implementer for that address by calling

```solidity
function setInterfaceImplementer(address _addr, bytes32 _interfaceHash, address _implementer)
```

This can be another smart contract, but even EOA (human controlled addresses) can register an interface for their own address. This was not possible with EIP-165.

#### Fully EIP-165 backwards compatible + Caching

And EIP-1820 comes fully backwards compatible. When you call the function:

```solidity
function getInterfaceImplementer(address _addr, bytes32 _interfaceHash) returns (address)
```

with an EIP-165 interface hash (padded with 28 zero bytes), the call is just forwarded =&gt; `_addr.supportsInterface(_interfaceHash)`. This also allows for some extra caching to save gas and store the EIP-165 call result for future usage.

### 3. Key feature: Hooks

Now that we understand the registry, we can look at the key feature of ERC-777 which are the new hooks. They allow someone to register a smart contract function to be executed every time tokens are sent from this address and/or received to this address.

Let's look at how one would implement both of these cases:

#### Send Hooks

First a send hook. This will be our hook that executes every time tokens are about to be sent from the given address. If someone wants to use this hook for their own address, they would call

```javascript
erc1820.setInterfaceImplementer(
    myAddress,
    TOKENS_SENDER_INTERFACE_HASH,
    usingERC777SenderHook
)
usingERC777SenderHook.registerHookForAccount(
    myAddress
)
```

Now every time tokens are sent from `myAddress`, the `tokensToSend` function will be executed. If you're curious how this is implemented in the ERC-777 token contract, take a look [here](<https://github.com/OpenZeppelin/openzeppelin-contracts/blob/85334995073fff746961155113b0906b689a044c/contracts/token/ERC777/ERC777.sol#L452-L455>):

```solidity
address implementer = registry.getInterfaceImplementer(
    from,
    TOKENS_SENDER_INTERFACE_HASH
);
if (implementer != address(0)) {
    IERC777Sender(implementer).tokensToSend(
        operator,
        from,
        to,
        amount,
        userData,
        operatorData
    );
}
```

```solidity
contract UsingERC777SenderHook is IERC777Sender, ERC1820Implementer {
    // keccak256("ERC777TokensSender")
    bytes32 constant private TOKENS_SENDER_INTERFACE_HASH =
        0x29ddb589b1fb5fc7cf394961c1adf5f8c6454761adf795e67fe149f658abe895;

    function registerHookForAccount(address account) public {
        _registerInterfaceForAddress(
            TOKENS_SENDER_INTERFACE_HASH,
            account
        );
    }

    function tokensToSend(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes calldata userData,
        bytes calldata operatorData
    ) external override {
        // this will be run for every registered
        // 'from' token transfers
    }
}
```

#### Receive Hook

Likewise we can create a hook that is run after the registered address has received tokens. In our example on the right we use this to register the contract itself as receiver and implementer.

Now every time tokens are received to our contract, the `tokensReceived` function will be executed. If you're curious how this is implemented in the ERC-777 token contract, take a look [here](<https://github.com/OpenZeppelin/openzeppelin-contracts/blob/85334995073fff746961155113b0906b689a044c/contracts/token/ERC777/ERC777.sol#L480-L485>):

```solidity
address implementer = registry.getInterfaceImplementer(
    to,
    TOKENS_RECIPIENT_INTERFACE_HASH
);

if (implementer != address(0)) {
    IERC777Recipient(implementer).tokensReceived(
        operator,
        from,
        to,
        amount,
        userData,
        operatorData
    );
} else {
    require(!to.isContract());
}
```

For ERC-777 `send` and `operatorSend`, a contract recipient must register the required receiver implementation. The ERC-20-compatible `transfer` and `transferFrom` paths have different compatibility rules and can send to an unregistered contract. Those paths can therefore still leave tokens stuck if the recipient has no withdrawal logic.

```solidity
contract UsingERC777ReceiverHook is IERC777Recipient {
    ERC777 public token;
    IERC1820Registry public registry
        = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);
    
    // keccak256('ERC777TokensRecipient')
    bytes32 constant private TOKENS_RECIPIENT_INTERFACE_HASH
        = 0xb281fc8c12954d22544db45de3159a39272895b169a852b314f9cc762e44c53b;
        
    mapping(address => uint256) private _balances;

    constructor() {
        token = new TestERC777(100 ether, new address[](0));
        token.transfer(msg.sender, 100 ether);
        
        registry.setInterfaceImplementer(
            address(this),
            TOKENS_RECIPIENT_INTERFACE_HASH,
            address(this)
        );
    }

    function tokensReceived(
        address /*operator*/,
        address from,
        address /*to*/,
        uint256 amount,
        bytes calldata /*userData*/,
        bytes calldata /*operatorData*/
    ) external override {
        require(msg.sender == address(token), "Invalid token");

        // like approve + transferFrom, but only one tx
        _balances[from] += amount;
    }
}
```

#### Approve + TransferFrom in one Transaction

Just like the [ERC20-Permit](<https://soliditydeveloper.com/erc20-permit>) standard, this also allows us to do approve + transferFrom in one transaction. What previously was

1. Approve 10 tokens to contract (`token.approve(contractAddress, 10e18)`)
2. Call function contractAddress.execute (internally using `token.transferFrom(msg.sender, address(this), 10e18)`).

Now becomes:

1. Send 10 tokens to contract (`token.send(contractAddress, 10e18, "")`). That's it, our registered `tokensReceived` function can internally call `execute`.

### 4. Operators Functionality

With ERC-777 you also get functionality to set operators. This can be a set of default operators defined in the constructor which will be able to transfer tokens on behalf of any address. Or they can be defined by the token holders themselves as being allowed to transfer on their behalfs.

Obviously you have to fully trust the operators. That's why for the most part you won't set a regular address as operator. Rather those operators are intended to be verified smart contracts such as an exchange, a cheque processor or an automatic charging system. Ensuring they cannot steal any money and behave in an expected manner.

### 5. Backwards Compatible

ERC-777 can be implemented alongside ERC-20, as in the historical OpenZeppelin implementation used here. The ERC-777 standard itself makes that compatibility optional. Check the actual token’s supported functions and events before treating it as an ERC-20, and remember that registered hooks can still run.

![Yo Dawg Hooks](<https://cdn0.scrvt.com/b095ee27d37b3d7b6b150adba9ac6ec8/7cb5145b7afb87d7/94db3698dcfe/v/54a514b936cb/hooks-meme.jpg>)

If you treat it as ERC-20 or not, any registered send or receive hooks will still be triggered regardless. People can abuse this for reentrancy attacks. This has happened earlier this year for 300k USD lost on Uniswap v1. Simple solution: use [reentrancy guards](<https://docs.openzeppelin.com/contracts/3.x/api/utils#ReentrancyGuard>).

## So should I use ERC-777 instead of ERC-20?

I wouldn't say this is a clear decision yet. Out in the wild the amount of popular ERC-777 tokens is still pretty small. There are additional risks involved with 777 as mentioned above with reentrancy. Also added complexity and people not being very familiar with it yet are reasons to not use it.

Ask yourself if any of the ERC-777 features would be of particular value to you. If not, stick to ERC-20. Otherwise you might want to give ERC-777 a try.
