Understanding ERC-777 token contracts
· Jim McDonald · 18 minutes read · 3829 words ·ERC-777⧉ is a new token contract standard that fixes some security issues with ERC-20⧉ and provides the ability to extend functionality for contract creators as well as token holders and recipients without requiring changes to the token contract itself. As with any progression, ERC-777 takes ideas from multiple sources including ERC-20, ERC-223 and others, resulting in a next-generation standard that provides great features for both developers and users.
This article takes a look at token contracts and explains the features,functions and usage of the ERC-777 token contract. Note that this article does not cover ERC-20; a separate article⧉ provides information on understading ERC-20 token contracts.
What is a token contract?
A token contract is a smart contract that contains a map of account addresses and their balances, as shown below. The balance represents a value that is defined by the contract creator: one token contract might use balances to represent physical objects, another monetary value, and a third the holder’s reputation.The unit of this balance is commonly called a token.
It should be noted that an end user could have any number of addresses that contain tokens. There are various reasons for this, including the user wanting to separate their holding to separate logical accounts (savings, tax, spending, etc.) or representing separate sources (ICO, investments, service payments, etc.).
When tokens are transferred from one account to another the token contract updates the balance of the two accounts. For example, a transfer of 10 tokens from \(0x2299…3ab7\) to \(0x1f59…3492\) would result in the balances being updated as shown below:
The total supply of tokens can be increased by minting new tokens (usually a function reserved for the token contract owner). For example, minting 100 tokens to \(0x4ba5..ae22\) would result in the balances being updated as shown below:
The total supply of tokens can be decreased by burning existing tokens (available to any token holder if the contract allows it). For example, \(0x4919…413d\) burning 50 tokens would result in the balances being updated as shown below:
Simple token contracts hold the above information in a mapping of address to balance. When more complex scenarios come in to play, such as providing dividends, then alternative or additional structures will often be more powerful. Regardless of the implementation details, however, the view of token balances to the outside world should always look like the diagrams shown above.
Operators in an ERC-777 token contract
ERC-777 token contracts introduce the concept of operators. An operator is a third party that can act on behalf of a token holder, specifically with the ability to move tokens from the holder’s address. Note that because operators are very powerful they should be added with care.
Each address contains a list of operators they have authorised, as shown below:
In the above example holder \(0x1f59…3492\) has two operators, and two other holders have one operator each. It is, of course, valid for a holder to have no operators.
A simple example of using operators is where a user has tokens on multiple addresses and has to manage them individually. Traditionally, to send tokens from one address to another requires the sender’s address to hold an amount of Ether that can be used as gas. This often requires the sender to carry out multiple transactions as they move Ether from one account to another prior to being able to send tokens, as shown below:
Here address \(0x93f1…1b09\) sends Ether to address \(0x1f59…3492\), waits for the transaction to complete, and then \(0x1f59…3492\) sends tokens to \(0x4ba5…ae22\). Requiring multiple steps results in a bad user experience and higher network load.
Operators allow one account to hold Ether and others to hold tokens, and the Ether-holding account to carry out token transfers. Following on from the previous example, if \(0x1f59…3492\) made \(0x93f1…1b09\) an operator the process of sending tokens from \(0x1f59…3492\) to \(0x4ba5…ae22\) could be simplified to the following:
This reduces the user burden significantly, as well as allowing them to control their Ether funds from a single operator account whilst retaining the separation of tokens in multiple holder accounts.
It is possible for operators to be contracts (known as “token operator contracts”), and it is also possible for token operator contracts to be defined for all holders at contract creation time. The combination of operators being usable for all token holders at the same time as being restricted to the functionality of the smart contract allows token contracts to easily extend the features they offer their holders. The full power of token operator contracts is explored in a later section.
The definition of an ERC-777 token contract
When an ERC-777 token contract is deployed on to the Ethereum it is given an address, known as the token address. The token will contain a number of parameters that define its operation.
First, it is important to understand that there is no central registry for token contracts so the uniqueness of a particular name or symbol is not guaranteed. As such, the best method of taking and retaining a unique identity is to publicise your token. Once you have created a token contract you should ask for it to be added to common sites such as Etherscan, MyEtherWallet, MyCrypto and CoinMarketCap, although be sure to follow the instructions provided by each site for your best chance of the submission being accepted.
The name of the token contract is the long name by which it should be known, for example “My token”. There are no restrictions on the length of this name but long names are likely to be truncated in some wallet applications so it’s best to keep the name relatively short.
The symbol of the token contract is the short symbol by which it should be known, for example “MYT”. It is broadly equivalent to a stock ticker, and although it has no restriction on its size it is usually 3 or 4 characters in length.
Solidity (the main Ethereum programming language) does not support decimal numbers, but fractional tokens are a common requirement. The solution adopted by ERC-777 is to represent all tokens internally as an integer \(10^{18}\) times their actual size. So for example what the end user sees as \(1.2345\) tokens is internally represented as \(1.2345\times10^{18}\). This results in tokens being divisible to \(0.000000000000000001\) of a token whilst still being represented internally as an integer number, as shown below:
Some token contract creators may want their tokens to be less divisible than that shown above, for example a user creating a token contract representing software licenses might not want to allow partial licenses or a user creating a token contract representing physical gold with 1 token representing 1Kg might not want to allow transfers of less than 0.01Kg.
The granularity of the token contract is the minimum divisible unit of the token, expressed in terms of the internal representation. Following on with the above examples, the license token should have a granularity of \(10^{18}\) (because \(\frac{10^{18}}{10^{18}} \equiv 1\)) and the gold token should have a granularity of \(10^{16}\) (because \(\frac{10^{16}}{10^{18}} \equiv 0.01\)).
It is expected that the vast majority of token contracts will have a granularity of 1, meaning that the token can be divided all the way down to \(\frac{1}{10^{18}}\), or \(0.000000000000000001\). A different granularity should only be chosen if there is a specific requirement for a less divisible token, as per the examples given above.
It is instructive to examine the difference between ERC-777’s granularity and ERC-20’s decimals. Although both achieve the same aim of providing variable divisibility of tokens, ERC-20 does so by moving the decimal place around depending on the value of decimals whereas ERC-777’s position of the decimal place is fixed. This makes it easier to display ERC-777 token values in user interfaces as they always put the decimal point at the same place and just throw away trailing 0s.
Functions of an ERC-777 token contract
ERC-777 token contracts come with a number of functions to allow users to find out the balances of accounts as well as to transfer them from one account to another under varying conditions. These functions are described below.
The totalSupply()
function states the number of tokens held by all addresses. This value can be increased by minting new tokens and decreased by burning existing tokens.
The balanceOf()
function states the number of tokens held by a given address. Note that anyone can query any address’ balance, as all data on the blockchain is public.
The send()
function sends a number of tokens from the message sender to another address. Sending tokens in ERC-777 provides significant feature enhancements over ERC-20, and is covered in more detail in a later section.
The burn()
function destroys a number of tokens held by the message sender. Burning tokens in ERC-777 provides significant feature enhancements over ERC-20, and is covered in more detail in a later section.
The authorizeOperator()
function allows the message sender to delegate full control of their tokens to another address.
The revokeOperator()
functions removes an existing operator for the message sender’s tokens.
The isOperatorFor()
function states if a given address is an operator for a given token holder.
The operatorSend()
function sends a number of tokens from one account to another, as long as the sender has operator privileges on the originating account.
The defaultOperators()
function provides the list of token operator contracts to which the token contract has delegated full control of all tokens; see the “Token operator contracts” section below for more detail on this powerful feature.
Events of an ERC-777 token contract
ERC-777 defines a number of events that can be used to track individual and overall information about a token contract.
The Minted()
event is emitted whenever new tokens are minted. The event includes information about how many tokens were minted, and to which address they have been given.
The Burned()
event is emitted whenever existing tokens are burned. The event includes information about how many tokens were burned, and from which address they came.
The Sent()
event is emitted whenever tokens are moved from one address to another. The event includes information about how many tokens were moved, and the holder and recipient addresses.
In addition to the above events two administration events are also part of the standard. The AuthorizedOperator() event is emitted whenever a user adds an operator for their address, and the RevokedOperator() event is emitted whenever a user removes an operator for their address. Note that these events do not indicate any change in number or ownership of tokens.
Anatomy of an ERC-777 token contract send
The process of sending ERC-777 tokens from one address to another consists of several steps, and this is where the additional functionality and security of this standard over others comes in to play.
The common flow for sending a token is shown below:
These steps are as follows: * Validate: ensures that the input parameters are valid. This includes checks that the holder has enough tokens to send the requested amount, and that the amount to send is a multiple of the token’s granularity * Authorise: ensures that the sender has the right to send the tokens. This requires the sender to be the holder of the tokens or an operator for the sending address * Send: carries out the token transfer, updating the token contract’s information about the holdings for each address Log: emits events that contain details of the operation that has taken place
ERC-777 adds two steps to the flow, as shown below:
It can be seen that ERC-777 adds the steps tokensToSend()
and tokensReceived()
to the common flow.
tokensToSend()
is called after the transaction’s information has been validated and authorised but before the contract’s holdings have been updated
tokensReceived()
is called after the contract’s holdings have been updated
At first glance it does not appear that these additional steps add much to the process. However, the power of tokensToSend()
and tokensReceived()
is that they are not defined by the token contract but in separate contracts by the token sender and recipient, respectively. They give full control to the sender and recipient to decide whether or not to allow the transaction to complete, as well as allowing them to carry out more advanced functions.
tokensToSend()
allows token holders to supply conditions and actions of the form “before tokens leave this account…” and tokensReceived()
allows token receivers to supply conditions and actions of the form “when tokens arrive at this account…”
The purpose of tokensToSend()
Imagine a situation where the Chief Financial Officer (CFO) of a company sets the rules for transferring funds of multiple currencies out of the company. The CFO allows the finance director (FD) to spend funds as long as they follow company rules, whilst retaining full control over both the rules and the funds. The situation is shown graphically below:
This setup can be achieved easily if the funds are ERC-777 tokens. The steps that need to be undertaken are:
- the CFO’s rules are encoded in to a token control contract, and the token control contract is applied to the company’s holding address
- the CFO authorises the FD as an operator for the company’s holding address
- the FD sends funds using
operatorSend()
What rules can the CFO put in place? Pretty much anything that can be encoded in a smart contract, but to give some examples:
- the operator might only be allowed to spend certain funds (tokens)
- the operator might have daily/weekly/monthly spend limits
- the operator might only be able to send funds to a set of authorised recipients
- the operator might only be allowed to spend funds if they provide a reference to an invoice that has been approved by the CFO
and many more. It is important to understand that there can be multiple sets of rules for different ERC-777 tokens held by the company, or that the same set of rules can apply across multiple ERC-777 tokens. This allows the CFO to build the rules that suit his company, and for the FD to have no choice but to abide by them.
It is also possible to have multiple operators for a single address, so for example if the FD has a deputy they can also have access to the funds under the same (or other) restrictions as the FD.
The purpose of tokensToSend()
, then, is to control funds leaving one or more accounts by placing holder-defined rules around the transaction. Colloquially, they allow users to complete the sentence “Before tokens leave my account…”. The rules are defined in a token control contract. The same token control contract can be used for multiple ERC-777 token contracts and across multiple accounts to provide consistent rules.
The purpose of tokensReceived()
Similar to tokensToSend()
, tokensReceived()
is informed of tokens that have been transferred to an account. Continuing the example from the last section, the company has an accounts department that receives payments. When a payment is received it needs to be matched to an invoice if appropriate, and allocated to the correct department internally. When funds are received the accounts department carry out the following procedure:
- if the received funds come with an invoice reference number, match it to the invoice and credit the appropriate department
- otherwise, if the received funds come from a known sender, credit the appropriate department
- otherwise, keep the funds in a holding account and investigate
As with tokensToSend()
the procedure above is an example and could be virtually anything, for example there might be a discount for early payment (or surcharge for late payment), there might be restrictions on which currencies are accepted, etc. This requires a single step:
- The department’s procedures are encoded in to a token control contract, and the token control contract is applied to the company’s receiving address
tokensReceived()
is generally less complex than tokensToSend()
for two reasons. First, it generally only contains a single actor (the receiving address) whereas sending involves an operator. Second, users are generally more concerned about funds leaving their account than those entering it. Nevertheless, tokensReceived()
is a very powerful feature that can help larger environments such as exchanges manage funds that are received.
The purpose of tokensReceived()
, then, is to control funds entering one or more accounts by placing holder-defined rules around the transaction. Colloquially, they allow users to complete the sentence “When tokens enter my account…”. The rules are defined in a token control contract. The same token control contract can be used for multiple ERC-777 token contracts and across multiple accounts to provide consistent rules.
Requirements for tokensToSend()
and tokensReceived()
tokensToSend()
is optional; if not present then sending tokens will proceed as per the common flow outlined above. tokensReceived()
is optional unless the recipient account is a contract, in which case it is mandatory. Forcing any contract that receives tokens to implement tokensReceived()
ensures that tokens are only sent to contracts that actively state they can process them. This was the primary goal of ERC-223, although ERC-777 provides additional safeguards by forcing the receiver to register its ability to receive ERC-777 tokens with the ERC-1820 register.
Token operator contracts
As mentioned previously, token operator contracts are contracts that call operatorSend()
on an ERC-777 token contract. The power of token operator contracts is in their ability to extend the functions of ERC-777 without requiring changes to the ERC-777 token contract itself.
When a holder wants to send tokens to another address they do so by calling send()
directly on the token contract as shown below:
However any user can also send tokens by calling token operator contracts that are allowed to send tokens on a holder’s behalf, as shown below:
Token operator contracts can be enabled for all holders at the time of contract creation (known as default operators) or for individual holders whenever they so desire.
Token operator contracts can provide additional features for token holders. For example: bulk sending of tokens is a common requirement but not specified in the ERC-777 standard. It would be possible to add a bulk send function to an ERC-777 token contract before deploying it but that results in a custom token contract and introduces the chance that an error is made.
Instead, a standalone token operator contract for bulk send can be written and deployed separately. The token operator contract would accept transactions from the token holder with details about which token to send to which recipients and repeatedly call operatorSend()
to send the tokens.
To enable the bulk transfer functionality a standard ERC-777 token contract is deployed and the bulk send operator contract is specified as a default operator during deployment. Now any holder of the token can call the send()
function on the token operator contract to send multiple tokens from their account in a single transaction. Alternatively, if the token contract does not specify the bulk send operator contract as a default operator a holder can configure it for just their account.
Note that in the above diagram the token operator contracts have a single send()
function, but complex token operator contracts can have multiple functions. For example, a bulk send token operator contract might have functions for sending the same number of tokens to multiple recipients, different numbers of tokens to each recipient, etc.
Moving beyond the above example, it is also possible for the user calling the token operator contract to be someone other than the holder of the tokens. Although it might sound dangerous for someone who is not the holder of the tokens to send them to another account there are many situations where this is in fact useful. Separating the holder of the token from the user requesting transfer of the token allows a much wider range of functionality, for example:
- sending tokens with authority of the holder (“etherless transfer”)
- sending tokens in return for payment (initial coin offering, distributed exchange)
- sending tokens once other conditions are met (performance-based rewards, time-locked tokens)
Put simply, a token operator contract can change the rules for when tokens can be transferred from one account to another. This is a powerful feature that requires trust in the token operator contract, and it is envisaged that there will be a number of well-known token operator contracts deployed on the main Ethereum network that provide specific functionality. Token contract creators and individual holders will be able to add features by simply selecting the token operator contracts they want, resulting in faster and more secure tokens.
Token operator contracts will be explored in much greater depth in the next article.
The difference between token operator and token control contracts
At first glance token operator contracts might sound very similar to token control contracts’ tokensToSend()
method, however there are a number of differences.
Token operator contracts are optional; any token holder can bypass their functionality by calling send()
directly. Token control contracts are mandatory and cannot be bypassed.
Token operator contracts can be called by anyone. Token control contracts are called as part of a send()
or operatorSend()
operation so can only be called by the token holder (or an operator for the holder).
In general, the focus of token operator contracts is on extending the functionality of token contracts whereas with token control contracts the focus is on controlling the flow of tokens from an account.
A summary of the differences between token operator contracts and token control contracts is shown below:
Item | Operator contract | Control contract |
---|---|---|
Configured by | Contract owner | Token holder |
Token holder | ||
Use if present | Optional | Mandatory |
Used by | Everyone | Token holder |
Focus | Extending functionality | Restricting transfers |
Compatibility with ERC-20
The eagle-eyed reader might have noticed that although ERC-20 and ERC-777 provide much the same functionality there is no overlap in terms of function names; ERC-20 uses transfer()
/approve()
/transferFrom()
and ERC-777 uses send()
/operatorSend()
. This means that it is possible for a single token contract to provide both ERC-20 and ERC-777 functions. Specific details of how to act and which events to emit are detailed within the ERC-777 standard.
ERC-777 implementations
ERC-777 comes with a reference implementation⧉ , that also includes a version that provides ERC-20 compatibility⧉ . Samples of token operator⧉ and token⧉ control⧉ contracts are available separately.
Further information on ERC-777
A companion article⧉ provides a more in-depth look at ERC-777 token operator contracts, using multiple examples to show how they can be used to extend the functionality of the basic ERC-777 token contract.