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.

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.