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. (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.

14 Likes

This is great, thanks for sharing it!

3 Likes

Documentation for the OpenZeppelin ERC777 implementation:

4 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

4 posts were split to a new topic: ERC777 events

A post was split to a new topic: ERC777 hooks

Good help, thank you very much

1 Like

I have updated to use OpenZeppelin Test Helpers 0.5

1 Like

A post was split to a new topic: Recipient error

A post was split to a new topic: Is there a simple ERC20 token example?

A post was split to a new topic: How to build a buyable ERC777?

A post was split to a new topic: In ERC777 example why are the token, recipient and sender contracts separate?

Hi! I followed this example to implement an ERC777 + ERC777 Recipient contract.

I’m having issues, deploying the recipient.

_erc1820.setInterfaceImplementer(
            address(this),
            TOKENS_RECIPIENT_INTERFACE_HASH,
            address(this)
        );

This call fails for some reason but I can’t figure out why.

This might be helpful - it’s the transaction in tenderly.

I can provide the code if tenderly isn’t enough :slight_smile:

Thanks in advance!

A post was split to a new topic: Warning: Could not decode event

Hi I'm trying to create a contract that will accept the UNI token and emit an event. I have setup metamask with an account which have few tokens and will transfer UNI token to my contract. I don't want to create any custom tokens.
I'm just using the implementation of IERC777Recipient piece from your example.

So within this code what must be passed to the constructor when I deploy the contract?

constructor (address token) public {
        _token = IERC777(**token**);     //what must be passed from remix here?

        _erc1820.setInterfaceImplementer(address(this), TOKENS_RECIPIENT_INTERFACE_HASH, address(this));
    }

And will implementing just IERC777Recipient will be enough?

UNI is not an ERC777 token, so this will have no effect for receiving UNI token.

Continuing the discussion from Simple ERC777 token example:

In the contract Simple777Sender.sol what does the function _registerInterfaceForAddress(TOKENS_SENDER_INTERFACE_HASH, account) inside the sendFor function do? and where exactly is the function implemented?

Hi everyone! I have a really revolutionary project, and despite the fact that I am not a programmer, I try to nail down this code and learn in the process. But I am stuck because the existing code of the token (ERC20) must be changed to ERC777. The token script is this, ``` ...it sends some fixed part of every transaction to the "lottery" contracts, but as the smart contracts doesn't get notified and can't automatically do the action which is needed, I have to change the token to ERC777. Can anyone help me with this?

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol";

    contract Apros39 is ERC20 {
    using SafeMath for uint256;
    uint Royalty = 5;
    address public owner;
    //mapping(address => bool) public offRoyalty;
   

    constructor() ERC20("Apros39", "APROS39") {
    _mint(msg.sender, 10000* 10 ** 18);
    owner = msg.sender;
   // offRoyalty[msg.sender] == true;
}

    modifier onlyOwner(){
        require(msg.sender == owner);
        _;
}
    struct Destination {
    address payable lotteryAddress;
    string name;
}
    
    Destination[] public destinations;
  

    function addDestination(address payable lotteryAddress, string memory name) public onlyOwner {
    destinations.push(Destination(lotteryAddress, name));
    }
  


    function transfer(address recipient, uint256 amount) public override returns (bool) {
     uint royaltyFee = amount.mul(Royalty) / 100;
     for(uint i = 0; i < destinations.length; i++) {
       _transfer(_msgSender(), destinations[i].lotteryAddress, royaltyFee / destinations.length);
     }
       _transfer(_msgSender(), recipient, amount.sub(royaltyFee));
     
      return true;
     }
}`

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC777/ERC777.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol";

contract Apros78 is ERC777 {
using SafeMath for uint256;
address public owner;
uint Royalty = 5;
bytes32 constant public TOKENS_SENDER_INTERFACE_HASH = keccak256("ERC777TokensSender");
event DoneStuff(address operator, address from, address to, uint256 amount, bytes userData, bytes operatorData);
//mapping(address => bool) public offRoyalty;
//event TransferReceived(address _from, uint _amount);

 constructor () ERC777("Apros78", "APO78", new address[](0)) {
    _mint(msg.sender, 1000000 * 10 ** 18, "", "");
    owner = msg.sender;
}
    modifier onlyOwner(){
    require(msg.sender == owner);
    _;

}

struct Destination {
address payable lotteryAddress;
string name;

}

Destination[] public destinations;
function addDestination(address payable lotteryAddress, string memory name) public onlyOwner {
destinations.push(Destination(lotteryAddress, name));

}

  function tokensToSend(address operator, address from, address to, uint256 amount,
    bytes calldata userData,
    bytes calldata operatorData
) public override returns (bool) {
uint royaltyFee = amount.mul(Royalty) / 100;
 for(uint i = 0; i < destinations.length; i++) {
  send(_msgSender(), destinations[i].lotteryAddress, royaltyFee / destinations.length);
 }
  send(_msgSender(), from, amount.sub(royaltyFee));
  emit DoneStuff(operator, from, to, amount, userData, operatorData);
  return true;
 }

}