OpenZeppelin 2.3.0 release candidate includes an implementation of ERC777.
I created a simple ERC777 token inheriting from the OpenZeppelin implementation, based on the SimpleToken example.
A key point to note is that in a testing environment an ERC777 token requires deploying an ERC1820 registry using @openzeppelin/test-helpers. (Thanks @frangio) The same applies when deploying to a private network. (We can use the OpenZeppelin Test Helpers to deploy the ERC1820 registry to a private network as well as local testnets)
This example smart contract merely sets a name, symbol and has no default operators. All tokens are pre-assigned to the creator using the internal mint function. Based on SimpleToken.sol
ERC777 Token Smart Contract
Simple777Token.sol
pragma solidity ^0.5.0;
import "@openzeppelin/contracts/token/ERC777/ERC777.sol";
/**
* @title Simple777Token
* @dev Very simple ERC777 Token example, where all tokens are pre-assigned to the creator.
* Note they can later distribute these tokens as they wish using `transfer` and other
* `ERC20` or `ERC777` functions.
* Based on https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/examples/SimpleToken.sol
*/
contract Simple777Token is ERC777 {
/**
* @dev Constructor that gives msg.sender all of existing tokens.
*/
constructor () public ERC777("Simple777Token", "S7", new address[](0)) {
_mint(msg.sender, msg.sender, 10000 * 10 ** 18, "", "");
}
}
Migration
2_deploy.js
const Simple777Token = artifacts.require('Simple777Token');
require('@openzeppelin/test-helpers/configure')({ provider: web3.currentProvider, environment: 'truffle' });
const { singletons } = require('@openzeppelin/test-helpers');
module.exports = async function (deployer, network, accounts) {
if (network === 'development') {
// In a test environment an ERC777 token requires deploying an ERC1820 registry
await singletons.ERC1820Registry(accounts[0]);
}
await deployer.deploy(Simple777Token);
};
Test
Simple777Token.test.js
Tests are based on SimpleToken.test.js
// Based on https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/test/examples/SimpleToken.test.js
const { expectEvent, singletons, constants } = require('@openzeppelin/test-helpers');
const { ZERO_ADDRESS } = constants;
const Simple777Token = artifacts.require('Simple777Token');
contract('Simple777Token', function ([_, registryFunder, creator, operator]) {
beforeEach(async function () {
this.erc1820 = await singletons.ERC1820Registry(registryFunder);
this.token = await Simple777Token.new({ from: creator });
});
it('has a name', async function () {
(await this.token.name()).should.equal('Simple777Token');
});
it('has a symbol', async function () {
(await this.token.symbol()).should.equal('S7');
});
it('assigns the initial total supply to the creator', async function () {
const totalSupply = await this.token.totalSupply();
const creatorBalance = await this.token.balanceOf(creator);
creatorBalance.should.be.bignumber.equal(totalSupply);
await expectEvent.inConstruction(this.token, 'Transfer', {
from: ZERO_ADDRESS,
to: creator,
value: totalSupply,
});
});
it('allows operator burn', async function () {
const creatorBalance = await this.token.balanceOf(creator);
const data = web3.utils.sha3('Simple777Data');
const operatorData = web3.utils.sha3('Simple777OperatorData');
await this.token.authorizeOperator(operator, { from: creator });
await this.token.operatorBurn(creator, creatorBalance, data, operatorData, { from: operator });
(await this.token.balanceOf(creator)).should.be.bignumber.equal("0");
});
});
For a smart contract to receive ERC777 tokens, it needs to implement the tokensReceived
hook and register with ERC1820 registry as an ERC777TokensRecipient
ERC777 Recipient Smart Contract
Simple777Recipient.sol
The following is a simple recipient example based on the OpenZeppelin ERC777SenderRecipientMock.sol
(Optional) The contract accepts only one type of ERC777 token (this is not a requirement for ERC777), set in the constructor.
The constructor registers with ERC1820 registry as an interface implementer of ERC777TokensRecipient
.
The contract implements the tokensReceived
function from IERC777Recipient.sol
pragma solidity ^0.5.0;
import "@openzeppelin/contracts/token/ERC777/IERC777.sol";
import "@openzeppelin/contracts/introspection/IERC1820Registry.sol";
import "@openzeppelin/contracts/token/ERC777/IERC777Recipient.sol";
/**
* @title Simple777Recipient
* @dev Very simple ERC777 Recipient
*/
contract Simple777Recipient is IERC777Recipient {
IERC1820Registry private _erc1820 = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);
bytes32 constant private TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient");
IERC777 private _token;
event DoneStuff(address operator, address from, address to, uint256 amount, bytes userData, bytes operatorData);
constructor (address token) public {
_token = IERC777(token);
_erc1820.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 {
require(msg.sender == address(_token), "Simple777Recipient: Invalid token");
// do stuff
emit DoneStuff(operator, from, to, amount, userData, operatorData);
}
}
Migration
2_deploy.js
Updated to deploy Simple777Recipient
const Simple777Token = artifacts.require('Simple777Token');
const Simple777Recipient = artifacts.require('Simple777Recipient');
require('@openzeppelin/test-helpers/configure')({ provider: web3.currentProvider, environment: 'truffle' });
const { singletons } = require('@openzeppelin/test-helpers');
module.exports = async function (deployer, network, accounts) {
if (network === 'development') {
// In a test environment an ERC777 token requires deploying an ERC1820 registry
await singletons.ERC1820Registry(accounts[0]);
}
await deployer.deploy(Simple777Token);
const token = await Simple777Token.deployed();
await deployer.deploy(Simple777Recipient, token.address);
};
Test
Simple777Recipient.test.js
const { singletons, BN, expectEvent } = require('@openzeppelin/test-helpers');
const Simple777Token = artifacts.require('Simple777Token');
const Simple777Recipient = artifacts.require('Simple777Recipient');
contract('Simple777Recipient', function ([_, registryFunder, creator, holder]) {
const data = web3.utils.sha3('777TestData');
beforeEach(async function () {
this.erc1820 = await singletons.ERC1820Registry(registryFunder);
this.token = await Simple777Token.new({ from: creator });
const amount = new BN(10000);
await this.token.send(holder, amount, data, { from: creator });
this.recipient = await Simple777Recipient.new(this.token.address, { from: creator });
});
it('sends to a contract from an externally-owned account', async function () {
const amount = new BN(1000);
const receipt = await this.token.send(this.recipient.address, amount, data, { from: holder });
await expectEvent.inTransaction(receipt.tx, Simple777Recipient, 'DoneStuff', { from: holder, to: this.recipient.address, amount: amount, userData: data, operatorData: null });
const recipientBalance = await this.token.balanceOf(this.recipient.address);
recipientBalance.should.be.bignumber.equal(amount);
});
});
ERC777 Sender Smart Contract
Simple777Sender.sol
The following is a simple recipient example based on the OpenZeppelin ERC777SenderRecipientMock.sol
pragma solidity ^0.5.0;
import "@openzeppelin/contracts/token/ERC777/IERC777.sol";
import "@openzeppelin/contracts/introspection/IERC1820Registry.sol";
import "@openzeppelin/contracts/introspection/ERC1820Implementer.sol";
import "@openzeppelin/contracts/token/ERC777/IERC777Sender.sol";
contract Simple777Sender is IERC777Sender, ERC1820Implementer {
bytes32 constant public TOKENS_SENDER_INTERFACE_HASH = keccak256("ERC777TokensSender");
event DoneStuff(address operator, address from, address to, uint256 amount, bytes userData, bytes operatorData);
function senderFor(address account) public {
_registerInterfaceForAddress(TOKENS_SENDER_INTERFACE_HASH, account);
}
function tokensToSend(
address operator,
address from,
address to,
uint256 amount,
bytes calldata userData,
bytes calldata operatorData
) external {
// do stuff
emit DoneStuff(operator, from, to, amount, userData, operatorData);
}
}
Test
Simple777Sender.test.js
const { singletons, BN, expectEvent } = require('@openzeppelin/test-helpers');
const Simple777Token = artifacts.require('Simple777Token');
const Simple777Sender = artifacts.require('Simple777Sender');
contract('Simple777Sender', function ([_, registryFunder, creator, holder, recipient]) {
const data = web3.utils.sha3('777TestData');
beforeEach(async function () {
this.erc1820 = await singletons.ERC1820Registry(registryFunder);
this.token = await Simple777Token.new({ from: creator });
const amount = new BN(10000);
await this.token.send(holder, amount, data, { from: creator });
this.sender = await Simple777Sender.new({ from: creator });
});
it('sends from an externally-owned account', async function () {
const amount = new BN(1000);
const tokensSenderInterfaceHash = await this.sender.TOKENS_SENDER_INTERFACE_HASH();
await this.sender.senderFor(holder);
await this.erc1820.setInterfaceImplementer(holder, tokensSenderInterfaceHash, this.sender.address, { from: holder });
const receipt = await this.token.send(recipient, amount, data, { from: holder });
await expectEvent.inTransaction(receipt.tx, Simple777Sender, 'DoneStuff', { from: holder, to: recipient, amount: amount, userData: data, operatorData: null });
const recipientBalance = await this.token.balanceOf(recipient);
recipientBalance.should.be.bignumber.equal(amount);
});
});
I have deployed to ganache-cli and Ropsten and Rinkeby.
The Simple ERC777 token example can be found on GitHub.