Simple ERC777 token example

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-solidity/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-solidity/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')({ web3 });

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]) {
  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,
    });
  });
});

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-solidity/contracts/token/ERC777/IERC777.sol";
import "openzeppelin-solidity/contracts/introspection/IERC1820Registry.sol";
import "openzeppelin-solidity/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')({ web3 });

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);
  });
});

I have deployed to ganache-cli and Ropsten and Rinkeby.

The Simple ERC777 token example can be found on GitHub.

7 Likes

This is great, thanks for sharing it!

2 Likes

Documentation for the OpenZeppelin ERC777 implementation:

2 Likes

@abcoathup what if we move this to the #general:guides-and-tutorials category? :slight_smile:

2 Likes

Updated to add a recipient contract.

1 Like

Hi @abcoathup, thanks for sharing, I have an issue with the Recipient Smart Contract.
In tokensReceived( ) function, if I emit events, they are not fired.

Am I missing something ?

1 Like

Hi @maverick193 welcome to the community :wave:

Great question. I suspect you might be checking for an event to be directly emitted, when the event is emitted from the token received contract after being called by the ERC777 token contract.

I updated the example to include an event being fired to show how this works.

    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);
    }

We can test for the event being emitted in the transaction using OpenZeppelin test helpers expectEvent.inTransaction which can test for events indirectly emitted. See the documentation for details.

  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);
  });
1 Like

Hi @abcoathup, awesome !! it works !!
Now I understand my mistake, in my test script, I was expecting to catch the event using expectEvent.inLogs (log checking)

With expectEvent.inTransaction, I am able to test the event (indirectly emitted).

Thanks again for your answer and your time !!

1 Like

Hi @maverick193 glad I could help. I only found out about how to test for indirect events recently.