Deep Dive — OpenZeppelin’s ERC777 Implementation
OpenZeppelin recently published their implementation of the surging fungible-token standard ERC777. The purpose of ERC777 is to improve upon ERC20 while maintaining backward compatibility. The contract comes with two hooks, tokensToSend and tokensReceived, that addresses may implement to control and revert token operations. Accounts can now receive funds and a notification within a single transaction, supplanting the two-step process (approve/transferFrom) in ERC20. Let’s jump right in.
A glaring difference between 777 and ERC20 is the addition of operators. Token holders can authorize and revoke trusted entities to act on their behalf. Contract deployers may define default operators who can move tokens for all addresses. Notice that send is used in place of transfer and transferFrom, mirroring the transfer of Ether.
We begin with the contract definition and variable declarations. ERC777 inherits from the interface defined in the EIP as well as from the ERC20 interface. An introspection registry (ERC1820) where contracts and regular addresses publish the functionality they implement, is required. The two hardcoded hashes will see use later when we call our send/receive hooks.
The constructor intakes three arguments: the name of the token, the symbol of the token (DAI, BAT…etc), and an array defaultOperators to hold a list of addresses. Private variables are assigned. The contract then proclaims its ERC777/ERC20 interfaces with the registry.
The view functions look as expected. ERC20 compliance requires the implementation of decimals.
We’ve arrived at send, the quintessential method which moves tokens between accounts. The _send call nested inside will also be called by operatorSend (which we will get to later).
_send requires that from and to cannot be the zero address. Notice that _move is the one who moves the needle (and emit two events, one for each token standard). _callTokensToSend & _callTokensReceived is the duo responsible for calling the previously mentioned hook functions.
The Send Hook
_callTokensToSend first checks with the introspection registry that the from address in our transaction implements the send-hook. This enables us to call IERC777Sender(implementer).tokensToSend. Upon firing, the from address should receive a prompt (This will depend on the wallet implementation of the interface) allowing the sender to revert the transaction.
The Receive Hook
_callTokensReceived should feel familiar. The else if at the end will revert if requireReceptionAck is true && to is a contract. requireReceptionAck is false only when an ERC20 function (transfer, transferFrom) calls _callTokensReceived. Recall that ERC777 inherits from ERC20. Upon invoking tokensReceived, the receiver will get a notification that someone is sending them some tokens, and allowing the receiver to revert.
A 777-derived contract is meant to call _mint since the authors chose to remain agnostic in token creation methodologies. State variables are updated to reflect the minting. The receiver of the newly minted coins gets notified so long as the receive-hook is at the ready. Finally, the method emits the Minted (ERC777) and Transfer (ERC20) events.
The functions burn and operatorBurn both invoke the underlying _burn method. The send-hook allows the burner to revert. _totalSupply and _balances[from] are updated appropriately.
Operator send / burn
As expected, these functions call the _send and _burn methods on behalf of the token holder.
The authorizeOperator & revokeOperator both require msg.sender to not be the argument supplied. Both functions check to see if the operator is part of the list of default operators so that it may modify the corresponding array.
isOperatorFor checks if a given user authorizes a given operator.
All of the ERC20 functions are implemented.