Understanding ERC-777 token operator contracts
· Jim McDonald · 11 minutes read · 2315 words ·The previous article⧉ described ERC-777 token contracts, and spent some time discussing token operator contracts. This article provides more information about token operator contracts in ERC-777 through a set of example use cases with the fictional company ACME.
To recap: token operator contracts are contracts that call operatorSend()
on an ERC-777 token contract. Their primary function is to provide additional features to users without requiring changes to the token contract itself.
ACME’s ERC-777 token
The company ACME decides to launch an ERC-777 token. They want the token to have the following features for all users:
- ability for holders to bulk send tokens at a reduced gas price
- ability for holders to transfer tokens without requiring Ether in the holding account
One option is for ACME to take an existing ERC-777 token contract and alter it to provide these features, however this involves significant time and money and requires the entire contract to be audited for potential security issues. As such, rather than change the ERC-777 token contract ACME want to add the features as token operator contracts.
Bulk send token operator
Bulk sending allows tokens to be sent from a token holder to multiple recipients in a single transaction. It is a much more efficient manner of sending tokens than a single transaction for each recipient, and can be used for distributing dividends, airdrops, and similar. To bulk send tokens a token holder provides a list of recipients and the number of tokens to send each recipient to the token operator contract.
A sample bulk send token operator contract is shown below:
contract BulkSend {
function send(IERC777 _token, address[] _recipients, uint256 _amount, bytes _data) public {
for (uint256 i = 0; i < _recipients.length; i++) {
_token.operatorSend(msg.sender, _recipients[i], _amount, _data, "");
}
}
It requires a definition of the ERC-777 operatorSend()
function, which is as follows:
contract IERC777 {
function operatorSend(address from, address to, uint256 amount, bytes data, bytes operatorData) public;
}
A complete version of the above contract, with annotations and an additional function to send different amounts to different recipients, can be found here⧉ .
Once the token operator contract is deployed it can be referenced when deploying the ACME token contract, allowing all ACME token holders to carry out bulk sends.
Note that the bulk send token operator contract can be used by any ERC-777 token contract, not just ACME. Equally, if a bulk send token operator contract has already been written and deployed then ACME does not need to deploy its own version. This means that only a single copy of the contract needs to be deployed for all tokens and users to benefit, reducing gas costs and network load.
Etherless transfer token operator
Etherless transfer allows tokens to be sent from a token holder even if the holder does not have Ether in the account that holds the tokens. The ERC-777 token contract requires the user submitting a send()
transaction to also be the token holder, but if the token holder does not have any Ether in the account holding the tokens it is not possible for them to submit such a transaction.
Etherless transfer changes the requirement from the submitter needing to be the token holder to the submitter needing to supply an authority from the token holder. The specific definition of the authority is up to the particular contract, but the important point is that on presentation of the authority it allows movement of tokens on behalf of the token holder.
For the purposes of this example ACME choose to use a signature-based authority. The token holder signs information defining the details of the transfer (primarily the token contract address, the amount of tokens, and the recipient of the tokens) and anyone with this information and signature is considered authorised to carry out the transfer.
The key pieces of an Etherless transfer token operator contract using signature as the authority are shown below:
contract SignatureAuthority {
mapping(bytes32=>bool) private usedHashes;
function send(IERC777 _token, address _holder, address _recipient, uint256 _amount, bytes _data, uint256 _nonce, bytes _signature) public {
bytes32 hash = keccak256(abi.encodePacked(_token, _holder, _recipient, _amount, _data, _nonce));
require(!usedHashes[hash], "tokens already sent");
address signatory = signer(hash, _signature);
require(signatory != 0, "signatory is invalid");
require(signatory == _holder, "signatory is not the holder");
usedHashes[hash] = true;
_token.operatorSend(_holder, _recipient, _amount, _data, "");
}
}
A complete version of the above contract, with annotations and the signer()
function, can be found here⧉
. The tests for the contract contain examples of how to create the signatures and can be found here⧉
.
As per the bulk send example above, this token operator contract can be used with any ERC-777 token and only a single copy needs to be deployed on the blockchain to be available for use.
Deploying the ACME token contract
Having defined the token operator contracts that will provide the additional features for ACME’s token, ACME carries out the following steps to deploy the token contract:
- obtains a standard ERC-777 token contract
- obtains the address of the bulk sender token operator contract, either by deploying it themselves (if they wrote the contract) or by obtaining the address of the already-deployed contract
- obtains the address of the etherless transfer token operator contract through the same methods as described for the bulk sender token operator contract
- deploys the ERC-777 token contract with suitable parameters (name, symbol, granularity etc.) and the addresses of the token operator contracts as default operators
t this point the token contract is deployed. Token holders can carry out normal token operations directly against the ERC-777 token contract, or they can send transactions to the token operator contracts for the advanced features as explained above. Holders can interact with the token contract directly as well as through the operators, as shown below:
But ACME wants to do more. It plans for an initial coin offering (ICO) with the following distribution of tokens:
- 10,000 tokens will be available as an ICO at a fixed price
- An amount of tokens will be given to staff at the time of the ICO; these will be locked up for six months after the ICO date
The features are required for ACME’s ICO but it is not desirable for all token holders to be bound by the same rules. As such the token operator contracts are not defined as default operators but instead can be assigned to individual accounts as will be seen below.
Fixed price sale token operator
ACME’s ICO is on a fixed price basis. Fixed price sale allows anyone to transfer tokens away from the holder provided they send enough Ether in their transaction. The holder sets a price in Ether for each token and the purchaser submits a transaction to the token operator contract which calculates how many tokens to send depending on the Ether sent in the transaction. For example, if the holder sets the price at 0.1 Ether per token and a transaction is submitted with a value of 5 Ether this will result in 50 tokens being sent to the submitter.
The key pieces of a fixed price sale token operator contract are shown below:
contract FixedPriceSeller {
mapping(address=>mapping(address=>uint256)) pricePerToken;
function setPricePerToken(IERC777 _token, uint256 _pricePerToken) public {
pricePerToken[_token][msg.sender] = _pricePerToken;
}
function send(IERC777 _token, address _holder) public payable {
uint256 amount = msg.value.mul(1000000000000000000).div(pricePerToken[_token][_holder]);
_token.operatorSend(_holder, msg.sender, amount, "", "");
_holder.transfer(msg.value);
}
}
A complete version of the above contract, with annotations and suitable error checking, can be found here⧉ .
As per the prior examples, this token operator contract can be used with any ERC-777 token and only a single copy needs to be deployed to be available for use.
As mentioned above, the features provided by this token operator contract are required for the ICO event. To enable it the following operations are to be undertaken:
- ACME sends 10,000 tokens to an address for which ACME holds the private key (the ICO address)
- ACME sets the sale price of the tokens to 0.5 Ether each by submitting a
transaction setPricePerToken(<token contract address>, 0.5 ether)
from the ICO address to the token operator contract address - ACME allows the token operator contract to sell tokens held by the ICO address by submitting a transaction
authorizeOperator(<token operator contract address>)
from the ICO address to the ACME token contract address
At this point anyone can submit a transaction to the fixed price token operator contract with the required Ether and they will be returned tokens, until all 10,000 tokens have been purchased at which point the ICO finishes.
Fixed time lockup token operator
ACME grants 2,000 tokens to its staff that are locked up for a given time period. Fixed time lockup makes tokens unavailable for transfer until a predefined time. The holder creates a list of recipients and the number of tokens each are allowed, and sets the time period before which tokens cannot be transferred. Once the time period has expired the recipients can transfer tokens to their designated accounts.
This is a slightly more complicated operator in that it has two separate conditions: a time constraint and allowance constraint. The key pieces of a fixed time lockup token operator contract are shown below:
contract FixedTimeLockup {
mapping(address=>mapping(address=>uint256)) releases;
mapping(address=>mapping(address=>mapping(address=>uint256))) allowances;
function setReleaseTimestamp(IERC777 _token, uint256 _timestamp) public {
releases[address(_token)][msg.sender] = _timestamp;
}
function setAllowance(IERC777 _token, address _recipient, uint256 _allowance) public {
allowances[address(_token)][msg.sender][_recipient] = _allowance;
}
function send(IERC777 _token, address _holder, address _recipient, uint256 _amount) public {
require(releases[address(_token)][_holder] <= now, "not yet released");
allowances[address(_token)][_holder][msg.sender] = allowances[address(_token)][_holder][msg.sender].sub(_amount);
_token.operatorSend(_holder, _recipient, _amount, "", "");
}
}
A complete version of the above contract, which is a composite token operator contract built from individual fixed time release and allowance token operator contracts, with annotations and suitable error checking, can be found here⧉ .
As per the prior examples, this token operator contract can be used with any ERC-777 token and only a single copy needs to be deployed to be available for use.
As mentioned above, the features provided by this token operator contract are required for the employee token grant. To enable it the following operations are to be undertaken:
- ACME sends 2,000 tokens to an address for which ACME holds the private key (the grant address)
- ACME sets the lockup expiry date of the tokens to 1st November 2019 by submitting a transaction
setReleaseTimestamp(<token contract address>, 1572566400)
from the grant address to the token operator contract address (1572566500 is the unix timestamp for 1st November 2019) - ACME sets the token allowances for individual members of its staff by submitting a transaction
setAllowance(<token contract address>, <staff address>, <amount>)
from the grant address to the token operator contract address - ACME allows the token operator contract to disburse tokens held by the grant address by submitting a transaction
authorizeOperator(<token operator contract address>)
from the grant address to the ACME token contract address
After the lockup period has expired members of staff can claim their allowance by submitting a transaction send(<token contract address>, <grant address>, <recipient address>, <grant amount>)
from their individual staff address to the token operator contract address.
In addition to holders’ existing ability to interact with the token contract non-holders can now also interact with the contract to buy tokens through the ICO and obtain their grants, as shown below:
Using token operator contracts ACME has added significant functionality to its token contract without requiring any changes to the token contract itself. Additional features, for example a secondary offering, can be added at later dates following a similar process.
Benefits of Token Operator Contracts
A number of example token operator contracts have been outlined above, but what are the benefits to token contract creators and token holders compared to simply adding the features to the token contract to begin with? Some of the key benefits are as follows:
Token operator contracts are focused. Each contract is provides a single feature and as such it is easier to write. ICOs, lockups, whitelists and more can be programmed in a few lines of code. This focus also makes it easier to verify their correctness.
Token control contracts are generic. A single token operator contract provides a feature that can be used independently by every holder of every ERC-777 token. This allows well-known and battle-tested token operator contracts to be used without having to redeploy new instances for each holder or token.
Token operator contracts are egalitarian. They do not require “superuser” or “admin” privileges to carry out their work, resulting in less privileged code and as a result fewer security vulnerabilities.
Token operator contracts are selectable. Individual holders can add token operator contracts to their holdings, and likewise remove default operators. The token holder is always in control of which features are enabled or not.
In addition to the above, token operator contracts allow users to choose the features available to their token holdings. This is especially important as new features come to the fore (e.g. ERC-712⧉ ), because their functions can be used by token holders without having to rely on the token contract creator taking action.
Drawbacks of Token Operator Contracts
Token operator contracts are deployed at their own addresses, which means that an understanding of the advanced features of token contracts are not immediately obvious to users. It is expected that over time blockchain explorers will be able to query token contracts automatically to understand which features are provided and show an appropriate user interface to use such features.
The ERC-777 specification does not allow changes to the set of default operators for any given token. As such, decisions about which token operator contracts to use must be made prior to the token contract being deployed.
Any weaknesses in token operator contracts can be magnified when the same token operator contract is used by multiple token contracts. Careful auditing of token operator contracts is highly encouraged.