Skip to main content

How to Create and Deploy an ERC-777 Token

Created on
Updated on
Dec 17, 2024

14 min read

Overview

The most popular token standard to date has been ERC-20 (and for good reason). However, developers are constantly looking for new ways to innovate. The authors of ERC-777 proposed a new token standard (which is backward compatible with ERC-20) that introduces "operators", allowing approved addresses to manage tokens on behalf of holders, and implements send/receive hooks on token transfers.

In this guide, we'll walk through the ERC-777 implementation in detail, covering core concepts, and then learn how to create, deploy, and test your ERC-777 contract yourself using Foundry. If you don't know about Foundry already, check out this guide: Introduction to Foundry

Let's get started!

Prefer a visual walkthrough? Follow along with Sahil to learn how to create and deploy an ERC-777 token and explore the unique features of the ERC-777 token standard.
Subscribe to our YouTube channel for more videos!

What You Will Need


  • Intermediate understanding of Ethereum and smart contracts
  • A QuickNode account (create one here)
  • An Ethereum wallet with ETH on the Sepolia network (you can use the QuickNode Faucet to get some test ETH)
  • Node.js installed
  • Foundry installed

What You Will Do


  • Understand the ERC-777 token standard
  • Set up a development environment with Foundry
  • Create and deploy an ERC-777 smart contract
  • Test the smart contract locally
  • Deploy the smart contract to a Ethereum Sepolia testnet
DependencyVersion
node20.17
foundry0.8.10

What is ERC-777?

ERC-777 is an advanced token standard that builds upon ERC-20. Its key features include:


  1. Hooks: Allows token senders and recipients to execute custom logic before tokens are sent or received.
  2. Operators: Authorized addresses that can manage tokens on behalf of holders.
  3. ERC-1820 Registry: Used to register and lookup implementers of specific interfaces.
  4. Improved UX: Uses a send function instead of transfer, potentially eliminating the need for separate approve and transferFrom calls.
  5. Backward Compatibility: Maintains compatibility with ERC-20 functions and events.

To implement ERC-777's hook functionality:


  1. Register a smart contract address with the ERC-1820 Registry. The address of the registry on all chains is: 0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24
  2. Implement the ERC777TokensSender and ERC777TokensRecipient interfaces in the registered contract.

These hooks enable features like rejecting unwanted tokens or implementing complex transfer logic.


caution

It's important to note that while ERC-777 offers advanced features, it also introduces potential security risks, particularly related to reentrancy attacks (learn more here). OpenZeppelin, a leading smart contract library, has deprecated their support for ERC-777 due to these concerns. Developers should exercise caution when implementing ERC-777 tokens.

Hooks

Hooks allow custom logic execution before tokens are sent or received, with the following:


  • Use ERC-1820 registry for implementation lookup
  • Sender Hook: tokensToSend function, called before token movement
  • Recipient Hook: tokensReceived function, called after token movement
  • Can revert transactions, rejecting unwanted transfers
  • Receive additional data with transfers

Operators

Operators are addresses authorized to manage tokens on behalf of holders, with the following:


  • Token holders can authorize or revoke operators
  • Tokens can have default operators defined at creation
  • Operators use operatorSend and operatorBurn functions
  • Anyone can check if an address is an operator using isOperatorFor

In the next sections, we'll delve deeper into the ERC-777 by creating, testing, and deploying one ourselves.

Set up Project

Open a terminal and run the following commands to create a new project:

mkdir erc777-token
cd erc777-token
forge init

Next, install the OpenZeppelin library:

forge install OpenZeppelin/openzeppelin-contracts@v4.9.6

Then set up mappings by creating a remappings.txt file in the project's root directory (this automatically retrieves the inferred remappings for the project) and include the line below:

@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/

Next, we'll walk through creating an endpoint on QuickNode so we can deploy to a production testnet.

Create a QuickNode RPC Endpoint

To interact with the Ethereum network and deploy the smart contracts for this guide, we'll need an RPC endpoint. Follow these steps to create one using QuickNode:


  1. Sign up for an account at QuickNode
  2. Create a new endpoint for Ethereum Sepolia
  3. Keep the HTTP Provider URL handy for the next step

One method of configuring the RPC endpoint in this project is to also include it in the foundry.toml file, but if you plan to open source this code, we recommend keeping it out. Instead, let's create environment variables to include our RPC endpoint and private key (which we'll be using to deploy the smart contracts) which we'll create via the command-line window:

export RPC_URL=<Your RPC endpoint>
export PRIVATE_KEY=<Your wallets private key>

Fund Your Wallet via QuickNode Faucet

In order to deploy the ERC-777 contracts to Sepolia testnet, you'll need Sepolia testnet ETH to pay the gas fees. The Multi-Chain QuickNode Faucet makes it easy to obtain test ETH!

Navigate to the Multi-Chain QuickNode Faucet and connect your wallet (e.g., MetaMask, Coinbase Wallet) or paste in your wallet address to retrieve test ETH. Note that there is a mainnet balance requirement of 0.001 ETH on Ethereum Mainnet to use the EVM faucets. You can also tweet or login with your QuickNode account to get bonus testnet tokens!

Create the ERC-777 contract

Create a new file src/TestERC777.sol and add the following code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC777/ERC777.sol";

contract TestERC777 is ERC777 {
constructor(string memory name, string memory symbol, address[] memory defaultOperators)
ERC777(name, symbol, defaultOperators)
{}

function mint(address account, uint256 amount, bytes memory userData, bytes memory operatorData) public {
_mint(account, amount, userData, operatorData);
}
}

This contract extends from the ERC-777 implementation provided by OpenZeppelin. It sets the token name to "Gold", symbol to "GLD", and includes a mint function to mint tokens to a specified address. It also includes a defaultOperators argument which we'll use to pass in operators we want to set during contract deployment.

Create the ERC-777 Recipient contract

Next, to showcase the hook features that ERC-777 has, we'll create another contract src/TestERC777Recipient.sol, which will contain logic when a token is received.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC777/IERC777Recipient.sol";
import "@openzeppelin/contracts/utils/introspection/IERC1820Registry.sol";

contract TestERC777Recipient is IERC777Recipient {
IERC1820Registry private constant _ERC1820_REGISTRY = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);
bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient");

uint256 public receivedTokens;
uint256 public lastReceivedAmount;
address public lastOperator;
address public lastSender;

event TokensReceived(
address operator,
address from,
address to,
uint256 amount,
bytes userData,
bytes operatorData
);

constructor() {
_ERC1820_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 {
receivedTokens += amount;
lastReceivedAmount = amount;
lastOperator = operator;
lastSender = from;

emit TokensReceived(operator, from, to, amount, userData, operatorData);
}

function getReceivedTokens() public view returns (uint256) {
return receivedTokens;
}

function getLastReceivedInfo() public view returns (uint256, address, address) {
return (lastReceivedAmount, lastOperator, lastSender);
}
}

Let's recap the contract above. It implements the IERC777Recipient interface to handle incoming ERC777 token transfers. It registers itself with the ERC1820 registry to be recognized as an ERC777 token recipient. The contract tracks the total amount of tokens received, as well as details of the most recent transfer (amount, operator, and sender). When tokens are received, the tokensReceived function updates the contract's state and emits an event with transfer details. Additionally, the two public functions, getReceivedTokens and getLastReceivedInfo, allow external queries about the received tokens and the most recent transfer.

Note that the tokensReceived function is the actual hook that gets called whenever this contract receives ERC-777 tokens. It updates the contract's state (if it receives tokens) and emits an event.

Now, save the file and we'll move on to creating tests to everything is working as intended before deploying to a testnet.

Test the ERC-777 Functionality

Create a test file test/TestERC777.t.sol and input the following code:

1. Initialize Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC777/ERC777.sol";
import "@openzeppelin/contracts/utils/introspection/IERC1820Registry.sol";
import "../src/TestERC777Recipient.sol";
import "../src/TestERC777.sol";

contract ERC777SetupTest is Test {
TestERC777 public token;
TestERC777Recipient public recipient;
IERC1820Registry public erc1820Registry;
address public deployer;
address public user1;
address public user2;

bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient");

}

Above we import the necessary files like the TestERC777 contract and Test contract needed for Foundry testing. Then we declare the contract ERC777SetupTest and inherit the Test contract. Inside the contract we declare the TestERC777 and TestERC777Recipient contracts.

2. Set Up the Test Environment

Add the setUp function to initialize our test environment:

function setUp() public {
// Deploy mock ERC1820 registry
vm.etch(
address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24),
bytes(
hex"608060405234801561001057600080fd5b50600436106100a5576000357c010000000000000000000000000000000000000000000000000000000090048063a41e7d5111610078578063a41e7d51146101d4578063aabbb8ca1461020a578063b705676514610236578063f712f3e814610280576100a5565b806329965a1d146100aa5780633d584063146100e25780635df8122f1461012457806365ba36c114610152575b600080fd5b6100e0600480360360608110156100c057600080fd5b50600160a060020a038135811691602081013591604090910135166102b6565b005b610108600480360360208110156100f857600080fd5b5035600160a060020a0316610570565b60408051600160a060020a039092168252519081900360200190f35b6100e06004803603604081101561013a57600080fd5b50600160a060020a03813581169160200135166105bc565b6101c26004803603602081101561016857600080fd5b81019060208101813564010000000081111561018357600080fd5b82018360208201111561019557600080fd5b803590602001918460018302840111640100000000831117156101b757600080fd5b5090925090506106b3565b60408051918252519081900360200190f35b6100e0600480360360408110156101ea57600080fd5b508035600160a060020a03169060200135600160e060020a0319166106ee565b6101086004803603604081101561022057600080fd5b50600160a060020a038135169060200135610778565b61026c6004803603604081101561024c57600080fd5b508035600160a060020a03169060200135600160e060020a0319166107ef565b604080519115158252519081900360200190f35b61026c6004803603604081101561029657600080fd5b508035600160a060020a03169060200135600160e060020a0319166108aa565b6000600160a060020a038416156102cd57836102cf565b335b9050336102db82610570565b600160a060020a031614610339576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b6103428361092a565b15610397576040805160e560020a62461bcd02815260206004820152601a60248201527f4d757374206e6f7420626520616e204552433136352068617368000000000000604482015290519081900360640190fd5b600160a060020a038216158015906103b85750600160a060020a0382163314155b156104ff5760405160200180807f455243313832305f4143434550545f4d4147494300000000000000000000000081525060140190506040516020818303038152906040528051906020012082600160a060020a031663249cb3fa85846040518363ffffffff167c01000000000000000000000000000000000000000000000000000000000281526004018083815260200182600160a060020a0316600160a060020a031681526020019250505060206040518083038186803b15801561047e57600080fd5b505afa158015610492573d6000803e3d6000fd5b505050506040513d60208110156104a857600080fd5b5051146104ff576040805160e560020a62461bcd02815260206004820181905260248201527f446f6573206e6f7420696d706c656d656e742074686520696e74657266616365604482015290519081900360640190fd5b600160a060020a03818116600081815260208181526040808320888452909152808220805473ffffffffffffffffffffffffffffffffffffffff19169487169485179055518692917f93baa6efbd2244243bfee6ce4cfdd1d04fc4c0e9a786abd3a41313bd352db15391a450505050565b600160a060020a03818116600090815260016020526040812054909116151561059a5750806105b7565b50600160a060020a03808216600090815260016020526040902054165b919050565b336105c683610570565b600160a060020a031614610624576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b81600160a060020a031681600160a060020a0316146106435780610646565b60005b600160a060020a03838116600081815260016020526040808220805473ffffffffffffffffffffffffffffffffffffffff19169585169590951790945592519184169290917f605c2dbf762e5f7d60a546d42e7205dcb1b011ebc62a61736a57c9089d3a43509190a35050565b600082826040516020018083838082843780830192505050925050506040516020818303038152906040528051906020012090505b92915050565b6106f882826107ef565b610703576000610705565b815b600160a060020a03928316600081815260208181526040808320600160e060020a031996909616808452958252808320805473ffffffffffffffffffffffffffffffffffffffff19169590971694909417909555908152600284528181209281529190925220805460ff19166001179055565b600080600160a060020a038416156107905783610792565b335b905061079d8361092a565b156107c357826107ad82826108aa565b6107b85760006107ba565b815b925050506106e8565b600160a060020a0390811660009081526020818152604080832086845290915290205416905092915050565b6000808061081d857f01ffc9a70000000000000000000000000000000000000000000000000000000061094c565b909250905081158061082d575080155b1561083d576000925050506106e8565b61084f85600160e060020a031961094c565b909250905081158061086057508015155b15610870576000925050506106e8565b61087a858561094c565b909250905060018214801561088f5750806001145b1561089f576001925050506106e8565b506000949350505050565b600160a060020a0382166000908152600260209081526040808320600160e060020a03198516845290915281205460ff1615156108f2576108eb83836107ef565b90506106e8565b50600160a060020a03808316600081815260208181526040808320600160e060020a0319871684529091529020549091161492915050565b7bffffffffffffffffffffffffffffffffffffffffffffffffffffffff161590565b6040517f01ffc9a7000000000000000000000000000000000000000000000000000000008082526004820183905260009182919060208160248189617530fa90519096909550935050505056fea165627a7a72305820377f4a2d4301ede9949f163f319021a6e9c687c292a5e2b2c4734c126b524e6c0029"
)
);

erc1820Registry = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);

deployer = address(this);
user1 = address(0x1);
user2 = address(0x2);

// Deploy custom ERC777 token
address[] memory defaultOperators = new address[](0);
token = new TestERC777("Gold", "GLD", defaultOperators);

// Deploy ERC777Recipient
recipient = new TestERC777Recipient();

vm.label(address(token), "GoldToken");
vm.label(address(recipient), "TokenRecipient");
vm.label(user1, "User1");
vm.label(user2, "User2");
}

This function runs before each test, setting up a fresh state. It initializes the owner and operator addresses, creates the token with an initial supply, and deploys the ERC777Recipient contract.

3. Test Initial Setup

Let's add our first test to check if the setup is correct:

function testERC777Setup() public {
// Verify ERC1820 registry is deployed
assertTrue(address(erc1820Registry).code.length > 0, "ERC1820 registry should be deployed");

// Verify ERC777 token is deployed
assertTrue(address(token).code.length > 0, "ERC777 token should be deployed");

// Check token details
assertEq(token.name(), "Gold", "Token name should be Gold");
assertEq(token.symbol(), "GLD", "Token symbol should be GLD");
assertEq(token.totalSupply(), 0, "Initial supply should be 0");

// Mint some tokens to user1
token.mint(user1, 1000 * 10**18, "", "");

// Check balance
assertEq(token.balanceOf(user1), 1000 * 10**18, "User1 balance should be 1000 GLD");
}

4. Test Transfer Functionality

Now, let's test the token transfer functionality:

function testERC777TransferToAddress() public {
// Mint tokens to user1
token.mint(user1, 1000 * 10**18, "", "");

// User1 sends tokens to user2 (regular address)
vm.prank(user1);
token.send(user2, 100 * 10**18, "");

// Check token balances
assertEq(token.balanceOf(user1), 900 * 10**18, "User1 balance should be 900 GLD after sending");
assertEq(token.balanceOf(user2), 100 * 10**18, "User2 balance should be 100 GLD after receiving");
}

This snippet simulates user1 sending tokens to user2 and then checks the resulting balances.

5. Test Send Hook

Let's test the ERC777 send hook functionality:

function testERC777TransferToRegisteredRecipient() public {
// Mint tokens to user1
token.mint(user1, 1000 * 10**18, "", "");

vm.prank(address(recipient)); // The recipient must call this as it's its own manager by default
erc1820Registry.setManager(address(recipient), address(this));

// Register the recipient contract
vm.prank(address(this));
erc1820Registry.setInterfaceImplementer(address(this), _TOKENS_RECIPIENT_INTERFACE_HASH, address(this));

// User1 sends tokens to the recipient contract
vm.prank(user1);
token.send(address(recipient), 150 * 10**18, "");

// Check recipient contract state
assertEq(recipient.getReceivedTokens(), 150 * 10**18, "Recipient should have received 150 GLD");

(uint256 lastAmount, address lastOperator, address lastSender) = recipient.getLastReceivedInfo();
assertEq(lastAmount, 150 * 10**18, "Last received amount should be 150 GLD");
assertEq(lastOperator, user1, "Last operator should be user1");
assertEq(lastSender, user1, "Last sender should be user1");

// Check token balances
assertEq(token.balanceOf(user1), 850 * 10**18, "User1 balance should be 850 GLD after sending");
assertEq(token.balanceOf(address(recipient)), 150 * 10**18, "Recipient balance should be 150 GLD after receiving");
}

This test validates that the TestERC777Recipient contract correctly receives tokens, updates its state, and logs the necessary details about the transaction.

6. Test Operator Send

Now, let's test the functionality of an operator sending tokens on behalf of another user:

function testERC777OperatorSend() public {
// Mint tokens to user1
token.mint(user1, 1000 * 10**18, "", "");

// Set user2 as an operator for user1
vm.prank(user1);
token.authorizeOperator(user2);

// Ensure the recipient contract is properly set up
vm.prank(address(recipient));
erc1820Registry.setInterfaceImplementer(address(recipient), _TOKENS_RECIPIENT_INTERFACE_HASH, address(recipient));

// User2 sends tokens from user1 to the recipient contract
vm.prank(user2);
token.operatorSend(user1, address(recipient), 200 * 10**18, "", "");

// Check recipient contract state
assertEq(recipient.getReceivedTokens(), 200 * 10**18, "Recipient should have received 200 GLD");

(uint256 lastAmount, address lastOperator, address lastSender) = recipient.getLastReceivedInfo();
assertEq(lastAmount, 200 * 10**18, "Last received amount should be 200 GLD");
assertEq(lastOperator, user2, "Last operator should be user2");
assertEq(lastSender, user1, "Last sender should be user1");

// Check token balances
assertEq(token.balanceOf(user1), 800 * 10**18, "User1 balance should be 800 GLD after sending");
assertEq(token.balanceOf(address(recipient)), 200 * 10**18, "Recipient balance should be 200 GLD after receiving");
}

This test checks that an operator user2 can send tokens from the account of another user user1 and verifies that the recipient contract correctly handles the received tokens.

To run these tests, use the following command in your terminal:

forge test

You will see an output similar to the below:

ERC777-Foundry % forge test -vvv
[⠊] Compiling...
[⠘] Compiling 1 files with Solc 0.8.22
[⠃] Solc 0.8.22 finished in 1.76s
Compiler run successful!

Ran 4 tests for test/GLDToken.t.sol:ERC777SetupTest
[PASS] testERC777OperatorSend() (gas: 252056)
[PASS] testERC777Setup() (gas: 86202)
[PASS] testERC777TransferToAddress() (gas: 110695)
[PASS] testERC777TransferToRegisteredRecipient() (gas: 268863)
Suite result: ok. 4 passed; 0 failed; 0 skipped; finished in 6.20ms (1.13ms CPU time)

With our tests passed, let's deploy it to a live testnet.

Deploy to Testnet

Create a file called script/DeployERC777Contracts.s.sol and include the following code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "forge-std/Script.sol";
import "../src/TestERC777.sol";
import "../src/TestERC777Recipient.sol";

contract DeployERC777Contracts is Script {
IERC1820Registry constant _ERC1820_REGISTRY = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);
bytes32 constant _TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient");

function deployTestERC777() external {

vm.startBroadcast();

// Deploy TestERC777 token
address[] memory defaultOperators = new address[](0);
uint256 initialSupply = 1000000 * 10**18; // 1 million tokens with 18 decimals
TestERC777 token = new TestERC777("Gold", "GLD", defaultOperators);
token.mint(msg.sender, initialSupply, "", "");

vm.stopBroadcast();

console.log("TestERC777 token deployed at:", address(token));
console.log("Tokens minted to:", msg.sender);
console.log("Initial supply:", initialSupply);
}

function deployTestERC777Recipient() external {

vm.startBroadcast();

// Deploy TestERC777Recipient
TestERC777Recipient recipient = new TestERC777Recipient();

vm.stopBroadcast();

console.log("TestERC777Recipient deployed at:", address(recipient));
console.log("ERC1820 Registry address:", address(_ERC1820_REGISTRY));
}
}

Let's recap the script.

This script contains two separate deployment functions, each setting up a specific component of the ERC-777 token and a Recipient contract hook. Note that these are separate contracts that register themselves independently with the ERC-1820 registry. The registry then allows for dynamic interface detection, which is how the token contract knows whether an address (like your recipient contract) implements the required interfaces to receive tokens.

Now, execute each of the following terminal commands below for both the TestERC777 and ERC777Recipient contract.

forge script script/DeployERC777Contracts.s.sol:DeployERC777Contracts --sig "deployTestERC777()" --rpc-url $RPC_URL --broadcast -vvv --private-key $PRIVATE_KEY
forge script script/DeployERC777Contracts.s.sol:DeployERC777Contracts --sig "deployTestERC777Recipient()" --rpc-url $RPC_URL --broadcast -vvv --private-key $PRIVATE_KEY

The output shows the traces and displays the contract addresses:

...

Chain 11155111

Estimated gas price: 0.269606234 gwei

Estimated total gas used for script: 2035187

Estimated amount required: 0.000548699102555758 ETH

==========================
##
Sending transactions [0 - 1].
⠉ [00:00:00] [##################################################################################################################################################################################] 2/2 txes (0.0s)##
Waiting for receipts.
⠙ [00:00:07] [##############################################################################################################################################################################] 2/2 receipts (0.0s)
##### sepolia
✅ [Success]Hash: 0x12451b5a7e8cb5fe13389d01c9c86a2ac18610dd5d35932a810e39d263e8c935
Contract Address: 0x7258c917eBE13c54a71a81D50e9652a847dceA21
Block: 6572863
Paid: 0.000235367593825194 ETH (1481877 gas * 0.158830722 gwei

That's it! You've come a long way if you made it this far. ERC-777 is a complicated token standard but you should always reference the original EIP a

Wrap Up

In this guide, we covered the creation, testing, and deployment of an ERC-777 token and ERC-777 Recipient contract using Foundry. You now have a foundational understanding of how ERC-777 tokens work, and you've implemented both the token and recipient contracts to demonstrate the functionality of hooks and operators.

Stay up to date with the latest blockchain development tools and insights by following us on Twitter (@QuickNode) or joining our community on Discord. We're excited to see what you'll build next!

We ❤️ Feedback!

Let us know if you have any feedback or requests for new topics. We'd love to hear from you.

Share this guide