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.
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
Dependency | Version |
---|---|
node | 20.17 |
foundry | 0.8.10 |
What is ERC-777?
ERC-777 is an advanced token standard that builds upon ERC-20. Its key features include:
- Hooks: Allows token senders and recipients to execute custom logic before tokens are sent or received.
- Operators: Authorized addresses that can manage tokens on behalf of holders.
- ERC-1820 Registry: Used to register and lookup implementers of specific interfaces.
- Improved UX: Uses a
send
function instead oftransfer
, potentially eliminating the need for separateapprove
andtransferFrom
calls. - Backward Compatibility: Maintains compatibility with ERC-20 functions and events.
To implement ERC-777's hook functionality:
- Register a smart contract address with the ERC-1820 Registry. The address of the registry on all chains is:
0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24
- Implement the
ERC777TokensSender
andERC777TokensRecipient
interfaces in the registered contract.
These hooks enable features like rejecting unwanted tokens or implementing complex transfer logic.
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
andoperatorBurn
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:
- Sign up for an account at QuickNode
- Create a new endpoint for Ethereum Sepolia
- 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.