Bläddra i källkod

Feature/erc777 #1159 (#1684)

* IERC777 from specs, constants returned, up to defaultOperators. (#1159)

* IERC777 oprarator approvals (#1159)

* ERC777 oprarator approvals fixes and tests

* IERC777 send and receive with ERC820 (#1159)

* ERC777 Add burn functions and fix send functions (#1159)

* ERC777 Make expectEvent compatible with web3.js 1.0 (#1159)

* ERC777 Add ERC820 deploy script (#1159)

* ERC777 Complete implementation of ERC777 (#1159)

This implementation conforms to the current EIP

* ERC777 Update ERC820 Registry contract to final version (#1159)

* ERC777 Move contracts to 'drafts' folder (#1159)

* ERC777: Update to ERC1820 registry and linter error fix (#1159)

* ERC777: implement recent changes of EIP777 (#1159)

* ERC777 Fix formatting (#1159)

* ERC777 Update to solc 0.5.2 (#1159)

* ERC777 Fix travis CI errors (#1159)

* ERC777 Fix linter errors again... (#1159)

* ERC777 Fix unit test (#1159)

* ERC777 Fix unit test again (#1159)

* Remove extra newlines.

* Rename ERC777Base to ERC777.

* Remove 'Token' from contract names.

* Replace ops for operators.

* Move operator check out of _send.

* Remove ERC777Burnable.

* Remove ERC1820Client, now using the interface directly.

* Minor internal refactors in contracts.

* Delete extra test helpers.

* Simplified tests.

* Add basic 777 tests.

* Add granularity send test.

* Add first operator send tests.

* Add burn tests.

* Refactor send and burn tests.

* Improve send burn refactor.

* Greatly improve test module.

* Burn instead of send removed tokens.

* Add operator tests.

* Improve send tests under changing operators.

* Refactor and merge send and burn tests.

* Add missing and not-implemented tests.

* Make _burn private.

* Fix typo.

* Greatly improve tokensToSend tests.

* Refactor hook tests.

* Fix hook tests.

* Update openzeppelin-test-helpers and ERC1820 address.

* Fix natspec indentation.

* Make interface functions external.

* Remove redundant private revoke and authorize functions.

* Improved readability of if statement.

* Remove unnecessary asserts.

* Add non-one granularity test.

* Fix hook call order in _mint.

* Fix _mint not reverting on failure to implement tokensReceived.

* Remove special case in operatorFn when from is 0.

* Refactor ERC777SenderMock.

* Add tokensReceived tests.

* switch to updated ganache-cli-coverage fork

* Fix linter errors.

* Add mint tests.

* Fix linter errors.

* Fix tests.

* Update test/drafts/ERC777/ERC777.test.js

Co-Authored-By: nventuro <nicolas.venturo@gmail.com>

* Add changelog entry.
Bertrand Masius 6 år sedan
förälder
incheckning
5a2b349992

+ 3 - 1
CHANGELOG.md

@@ -3,7 +3,9 @@
 ## 2.3.0 (unreleased)
 
 ### New features:
- * `ERC1820`: added support for interacting with the [ERC1820](https://eips.ethereum.org/EIPS/eip-1820) registry contract (`IERC1820Registry`), as well as base contracts that can be registered as implementers there. ([#1677](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1677))
+ * `ERC1820`: added support for interacting with the [ERC1820](https://eips.ethereum.org/EIPS/eip-1820) registry contract (`IERC1820Registry`), as well as base contracts that can be registered as
+ implementers there. ([#1677](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1677))
+ * `ERC777`: initial support for the [ERC777 token](https://eips.ethereum.org/EIPS/eip-777), which has multiple improvements over `ERC20` such as built-in burning, a more  straightforward permission system, and optional sender and receiver hooks on transfer (mandatory for contracts!). ([#1684](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1684))
 
 ## 2.2.0 (2019-03-14)
 

+ 357 - 0
contracts/drafts/ERC777/ERC777.sol

@@ -0,0 +1,357 @@
+pragma solidity ^0.5.2;
+
+import "./IERC777.sol";
+import "./IERC777Recipient.sol";
+import "./IERC777Sender.sol";
+import "../../math/SafeMath.sol";
+import "../../utils/Address.sol";
+import "../IERC1820Registry.sol";
+
+/**
+ * @title ERC777 token implementation
+ * @author etsvigun <utgarda@gmail.com>, Bertrand Masius <github@catageeks.tk>
+ */
+contract ERC777 is IERC777 {
+    using SafeMath for uint256;
+    using Address for address;
+
+    IERC1820Registry private _erc1820 = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);
+
+    string private _name;
+
+    string private _symbol;
+
+    mapping(address => uint256) private _balances;
+
+    uint256 private _totalSupply;
+
+    uint256 private _granularity;
+
+    bytes32 constant private TOKENS_SENDER_INTERFACE_HASH = keccak256("ERC777TokensSender");
+    bytes32 constant private TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient");
+
+    // This isn't ever read from - it's only used to respond to the defaultOperators query.
+    address[] private _defaultOperatorsArray;
+
+    // Immutable, but accounts may revoke them (tracked in __revokedDefaultOperators).
+    mapping(address => bool) private _defaultOperators;
+
+    // For each account, a mapping of its operators and revoked default operators.
+    mapping(address => mapping(address => bool)) private _operators;
+    mapping(address => mapping(address => bool)) private _revokedDefaultOperators;
+
+    constructor(
+        string memory name,
+        string memory symbol,
+        uint256 granularity,
+        address[] memory defaultOperators
+    ) public {
+        require(granularity > 0);
+
+        _name = name;
+        _symbol = symbol;
+        _granularity = granularity;
+
+        _defaultOperatorsArray = defaultOperators;
+        for (uint256 i = 0; i < _defaultOperatorsArray.length; i++) {
+            _defaultOperators[_defaultOperatorsArray[i]] = true;
+        }
+
+        // register interface
+        _erc1820.setInterfaceImplementer(address(this), keccak256("ERC777Token"), address(this));
+    }
+
+    /**
+     * @dev Send the amount of tokens from the address msg.sender to the address to
+     * @param to address recipient address
+     * @param amount uint256 amount of tokens to transfer
+     * @param data bytes information attached to the send, and intended for the recipient (to)
+     */
+    function send(address to, uint256 amount, bytes calldata data) external {
+        _send(msg.sender, msg.sender, to, amount, data, "");
+    }
+
+    /**
+     * @dev Send the amount of tokens on behalf of the address from to the address to
+     * @param from address token holder address.
+     * @param to address recipient address
+     * @param amount uint256 amount of tokens to transfer
+     * @param data bytes information attached to the send, and intended for the recipient (to)
+     * @param operatorData bytes extra information provided by the operator (if any)
+     */
+    function operatorSend(
+        address from,
+        address to,
+        uint256 amount,
+        bytes calldata data,
+        bytes calldata operatorData
+    )
+    external
+    {
+        require(isOperatorFor(msg.sender, from));
+        _send(msg.sender, from, to, amount, data, operatorData);
+    }
+
+    /**
+     * @dev Burn the amount of tokens from the address msg.sender
+     * @param amount uint256 amount of tokens to transfer
+     * @param data bytes extra information provided by the token holder
+     */
+    function burn(uint256 amount, bytes calldata data) external {
+        _burn(msg.sender, msg.sender, amount, data, "");
+    }
+
+    /**
+     * @dev Burn the amount of tokens on behalf of the address from
+     * @param from address token holder address.
+     * @param amount uint256 amount of tokens to transfer
+     * @param data bytes extra information provided by the token holder
+     * @param operatorData bytes extra information provided by the operator (if any)
+     */
+    function operatorBurn(address from, uint256 amount, bytes calldata data, bytes calldata operatorData) external {
+        require(isOperatorFor(msg.sender, from));
+        _burn(msg.sender, from, amount, data, operatorData);
+    }
+
+    /**
+     * @dev Authorize an operator for the sender
+     * @param operator address to be authorized as operator
+     */
+    function authorizeOperator(address operator) external {
+        require(msg.sender != operator);
+
+        if (_defaultOperators[operator]) {
+            delete _revokedDefaultOperators[msg.sender][operator];
+        } else {
+            _operators[msg.sender][operator] = true;
+        }
+
+        emit AuthorizedOperator(operator, msg.sender);
+    }
+
+    /**
+     * @dev Revoke operator rights from one of the default operators
+     * @param operator address to revoke operator rights from
+     */
+    function revokeOperator(address operator) external {
+        require(operator != msg.sender);
+
+        if (_defaultOperators[operator]) {
+            _revokedDefaultOperators[msg.sender][operator] = true;
+        } else {
+            delete _operators[msg.sender][operator];
+        }
+
+        emit RevokedOperator(operator, msg.sender);
+    }
+
+    /**
+     * @return the name of the token.
+     */
+    function name() public view returns (string memory) {
+        return _name;
+    }
+
+    /**
+     * @return the symbol of the token.
+     */
+    function symbol() public view returns (string memory) {
+        return _symbol;
+    }
+
+    /**
+     * @dev Total number of tokens in existence
+     */
+    function totalSupply() public view returns (uint256) {
+        return _totalSupply;
+    }
+
+    /**
+     * @dev Gets the balance of the specified address.
+     * @param tokenHolder The address to query the balance of.
+        * @return uint256 representing the amount owned by the specified address.
+     */
+    function balanceOf(address tokenHolder) public view returns (uint256) {
+        return _balances[tokenHolder];
+    }
+
+    /**
+     * @dev Gets the token's granularity,
+     * i.e. the smallest number of tokens (in the basic unit)
+     * which may be minted, sent or burned at any time
+     * @return uint256 granularity
+     */
+    function granularity() public view returns (uint256) {
+        return _granularity;
+    }
+
+    /**
+     * @dev Get the list of default operators as defined by the token contract.
+     * @return address[] default operators
+     */
+    function defaultOperators() public view returns (address[] memory) {
+        return _defaultOperatorsArray;
+    }
+
+    /**
+     * @dev Indicate whether an address
+     * is an operator of the tokenHolder address
+     * @param operator address which may be an operator of tokenHolder
+     * @param tokenHolder address of a token holder which may have the operator
+     * address as an operator.
+     */
+    function isOperatorFor(
+        address operator,
+        address tokenHolder
+    ) public view returns (bool) {
+        return operator == tokenHolder ||
+            (_defaultOperators[operator] && !_revokedDefaultOperators[tokenHolder][operator]) ||
+            _operators[tokenHolder][operator];
+    }
+
+    /**
+     * @dev Mint tokens. Does not check authorization of operator
+     * @dev the caller may ckeck that operator is authorized before calling
+     * @param operator address operator requesting the operation
+     * @param to address token recipient address
+     * @param amount uint256 amount of tokens to mint
+     * @param userData bytes extra information defined by the token recipient (if any)
+     * @param operatorData bytes extra information provided by the operator (if any)
+     */
+    function _mint(
+        address operator,
+        address to,
+        uint256 amount,
+        bytes memory userData,
+        bytes memory operatorData
+    )
+    internal
+    {
+        require(to != address(0));
+        require((amount % _granularity) == 0);
+
+        // Update state variables
+        _totalSupply = _totalSupply.add(amount);
+        _balances[to] = _balances[to].add(amount);
+
+        _callTokensReceived(operator, address(0), to, amount, userData, operatorData);
+
+        emit Minted(operator, to, amount, userData, operatorData);
+    }
+
+    /**
+     * @dev Send tokens
+     * @param operator address operator requesting the transfer
+     * @param from address token holder address
+     * @param to address recipient address
+     * @param amount uint256 amount of tokens to transfer
+     * @param userData bytes extra information provided by the token holder (if any)
+     * @param operatorData bytes extra information provided by the operator (if any)
+     */
+    function _send(
+        address operator,
+        address from,
+        address to,
+        uint256 amount,
+        bytes memory userData,
+        bytes memory operatorData
+    )
+    private
+    {
+        require(from != address(0));
+        require(to != address(0));
+        require((amount % _granularity) == 0);
+
+        _callTokensToSend(operator, from, to, amount, userData, operatorData);
+
+        // Update state variables
+        _balances[from] = _balances[from].sub(amount);
+        _balances[to] = _balances[to].add(amount);
+
+        _callTokensReceived(operator, from, to, amount, userData, operatorData);
+
+        emit Sent(operator, from, to, amount, userData, operatorData);
+    }
+
+    /**
+     * @dev Burn tokens
+     * @param operator address operator requesting the operation
+     * @param from address token holder address
+     * @param amount uint256 amount of tokens to burn
+     * @param data bytes extra information provided by the token holder
+     * @param operatorData bytes extra information provided by the operator (if any)
+     */
+    function _burn(
+        address operator,
+        address from,
+        uint256 amount,
+        bytes memory data,
+        bytes memory operatorData
+    )
+    private
+    {
+        require(from != address(0));
+        require((amount % _granularity) == 0);
+
+        _callTokensToSend(operator, from, address(0), amount, data, operatorData);
+
+        // Update state variables
+        _totalSupply = _totalSupply.sub(amount);
+        _balances[from] = _balances[from].sub(amount);
+
+        emit Burned(operator, from, amount, data, operatorData);
+    }
+
+    /**
+     * @dev Call from.tokensToSend() if the interface is registered
+     * @param operator address operator requesting the transfer
+     * @param from address token holder address
+     * @param to address recipient address
+     * @param amount uint256 amount of tokens to transfer
+     * @param userData bytes extra information provided by the token holder (if any)
+     * @param operatorData bytes extra information provided by the operator (if any)
+     */
+    function _callTokensToSend(
+        address operator,
+        address from,
+        address to,
+        uint256 amount,
+        bytes memory userData,
+        bytes memory operatorData
+    )
+    private
+    {
+        address implementer = _erc1820.getInterfaceImplementer(from, TOKENS_SENDER_INTERFACE_HASH);
+        if (implementer != address(0)) {
+            IERC777Sender(implementer).tokensToSend(operator, from, to, amount, userData, operatorData);
+        }
+    }
+
+    /**
+     * @dev Call to.tokensReceived() if the interface is registered. Reverts if the recipient is a contract but
+     * tokensReceived() was not registered for the recipient
+     * @param operator address operator requesting the transfer
+     * @param from address token holder address
+     * @param to address recipient address
+     * @param amount uint256 amount of tokens to transfer
+     * @param userData bytes extra information provided by the token holder (if any)
+     * @param operatorData bytes extra information provided by the operator (if any)
+     */
+    function _callTokensReceived(
+        address operator,
+        address from,
+        address to,
+        uint256 amount,
+        bytes memory userData,
+        bytes memory operatorData
+    )
+    private
+    {
+        address implementer = _erc1820.getInterfaceImplementer(to, TOKENS_RECIPIENT_INTERFACE_HASH);
+        if (implementer != address(0)) {
+            IERC777Recipient(implementer).tokensReceived(operator, from, to, amount, userData, operatorData);
+        } else {
+            require(!to.isContract());
+        }
+    }
+}

+ 61 - 0
contracts/drafts/ERC777/IERC777.sol

@@ -0,0 +1,61 @@
+pragma solidity ^0.5.2;
+
+/**
+ * @title ERC777 token interface
+ * @dev See https://eips.ethereum.org/EIPS/eip-777
+ */
+interface IERC777 {
+    function authorizeOperator(address operator) external;
+
+    function revokeOperator(address operator) external;
+
+    function send(address to, uint256 amount, bytes calldata data) external;
+
+    function operatorSend(
+        address from,
+        address to,
+        uint256 amount,
+        bytes calldata data,
+        bytes calldata operatorData
+    ) external;
+
+    function burn(uint256 amount, bytes calldata data) external;
+
+    function operatorBurn(
+        address from,
+        uint256 amount,
+        bytes calldata data,
+        bytes calldata operatorData
+    ) external;
+
+    function name() external view returns (string memory);
+
+    function symbol() external view returns (string memory);
+
+    function totalSupply() external view returns (uint256);
+
+    function balanceOf(address owner) external view returns (uint256);
+
+    function granularity() external view returns (uint256);
+
+    function defaultOperators() external view returns (address[] memory);
+
+    function isOperatorFor(address operator, address tokenHolder) external view returns (bool);
+
+    event Sent(
+        address indexed operator,
+        address indexed from,
+        address indexed to,
+        uint256 amount,
+        bytes data,
+        bytes operatorData
+    );
+
+    event Minted(address indexed operator, address indexed to, uint256 amount, bytes data, bytes operatorData);
+
+    event Burned(address indexed operator, address indexed from, uint256 amount, bytes data, bytes operatorData);
+
+    event AuthorizedOperator(address indexed operator, address indexed tokenHolder);
+
+    event RevokedOperator(address indexed operator, address indexed tokenHolder);
+}

+ 16 - 0
contracts/drafts/ERC777/IERC777Recipient.sol

@@ -0,0 +1,16 @@
+pragma solidity ^0.5.2;
+
+/**
+ * @title ERC777 token recipient interface
+ * @dev See https://eips.ethereum.org/EIPS/eip-777
+ */
+interface IERC777Recipient {
+    function tokensReceived(
+        address operator,
+        address from,
+        address to,
+        uint amount,
+        bytes calldata userData,
+        bytes calldata operatorData
+    ) external;
+}

+ 16 - 0
contracts/drafts/ERC777/IERC777Sender.sol

@@ -0,0 +1,16 @@
+pragma solidity ^0.5.2;
+
+/**
+ * @title ERC777 token sender interface
+ * @dev See https://eips.ethereum.org/EIPS/eip-777
+ */
+interface IERC777Sender {
+    function tokensToSend(
+        address operator,
+        address from,
+        address to,
+        uint amount,
+        bytes calldata userData,
+        bytes calldata operatorData
+    ) external;
+}

+ 26 - 0
contracts/mocks/ERC777Mock.sol

@@ -0,0 +1,26 @@
+pragma solidity ^0.5.2;
+
+import "../drafts/ERC777/ERC777.sol";
+
+contract ERC777Mock is ERC777 {
+    constructor(
+        address initialHolder,
+        uint256 initialBalance,
+        string memory name,
+        string memory symbol,
+        uint256 granularity,
+        address[] memory defaultOperators
+    ) public ERC777(name, symbol, granularity, defaultOperators) {
+        _mint(msg.sender, initialHolder, initialBalance, "", "");
+    }
+
+    function mintInternal (
+        address operator,
+        address to,
+        uint256 amount,
+        bytes memory userData,
+        bytes memory operatorData
+    ) public {
+        _mint(operator, to, amount, userData, operatorData);
+    }
+}

+ 147 - 0
contracts/mocks/ERC777SenderRecipientMock.sol

@@ -0,0 +1,147 @@
+pragma solidity ^0.5.2;
+
+import "../drafts/ERC777/IERC777.sol";
+import "../drafts/ERC777/IERC777Sender.sol";
+import "../drafts/ERC777/IERC777Recipient.sol";
+import "../drafts/IERC1820Registry.sol";
+import "../drafts/ERC1820Implementer.sol";
+
+contract ERC777SenderRecipientMock is IERC777Sender, IERC777Recipient, ERC1820Implementer {
+    event TokensToSendCalled(
+        address operator,
+        address from,
+        address to,
+        uint256 amount,
+        bytes data,
+        bytes operatorData,
+        address token,
+        uint256 fromBalance,
+        uint256 toBalance
+    );
+
+    event TokensReceivedCalled(
+        address operator,
+        address from,
+        address to,
+        uint256 amount,
+        bytes data,
+        bytes operatorData,
+        address token,
+        uint256 fromBalance,
+        uint256 toBalance
+    );
+
+    bool private _shouldRevertSend;
+    bool private _shouldRevertReceive;
+
+    IERC1820Registry private _erc1820 = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);
+
+    bytes32 constant private TOKENS_SENDER_INTERFACE_HASH = keccak256("ERC777TokensSender");
+    bytes32 constant private TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient");
+
+    function tokensToSend(
+        address operator,
+        address from,
+        address to,
+        uint amount,
+        bytes calldata userData,
+        bytes calldata operatorData
+    ) external {
+        if (_shouldRevertSend) {
+            revert();
+        }
+
+        IERC777 token = IERC777(msg.sender);
+
+        uint256 fromBalance = token.balanceOf(from);
+        // when called due to burn, to will be the zero address, which will have a balance of 0
+        uint256 toBalance = token.balanceOf(to);
+
+        emit TokensToSendCalled(
+            operator,
+            from,
+            to,
+            amount,
+            userData,
+            operatorData,
+            address(token),
+            fromBalance,
+            toBalance
+        );
+    }
+
+    function tokensReceived(
+        address operator,
+        address from,
+        address to,
+        uint amount,
+        bytes calldata userData,
+        bytes calldata operatorData
+    ) external{
+        if (_shouldRevertReceive) {
+            revert();
+        }
+
+        IERC777 token = IERC777(msg.sender);
+
+        uint256 fromBalance = token.balanceOf(from);
+        // when called due to burn, to will be the zero address, which will have a balance of 0
+        uint256 toBalance = token.balanceOf(to);
+
+        emit TokensReceivedCalled(
+            operator,
+            from,
+            to,
+            amount,
+            userData,
+            operatorData,
+            address(token),
+            fromBalance,
+            toBalance
+        );
+    }
+
+    function senderFor(address account) public {
+        _registerInterfaceForAddress(TOKENS_SENDER_INTERFACE_HASH, account);
+
+        address self = address(this);
+        if (account == self) {
+            registerSender(self);
+        }
+    }
+
+    function registerSender(address sender) public {
+        _erc1820.setInterfaceImplementer(address(this), TOKENS_SENDER_INTERFACE_HASH, sender);
+    }
+
+    function recipientFor(address account) public {
+        _registerInterfaceForAddress(TOKENS_RECIPIENT_INTERFACE_HASH, account);
+
+        address self = address(this);
+        if (account == self) {
+            registerRecipient(self);
+        }
+    }
+
+    function registerRecipient(address recipient) public {
+        _erc1820.setInterfaceImplementer(address(this), TOKENS_RECIPIENT_INTERFACE_HASH, recipient);
+    }
+
+    function setShouldRevertSend(bool shouldRevert) public {
+        _shouldRevertSend = shouldRevert;
+    }
+
+    function setShouldRevertReceive(bool shouldRevert) public {
+        _shouldRevertReceive = shouldRevert;
+    }
+
+    function send(IERC777 token, address to, uint256 amount, bytes memory data) public {
+        // This is 777's send function, not the Solidity send function
+        token.send(to, amount, data); // solhint-disable-line check-send-result
+    }
+
+    function burn(IERC777 token, uint256 amount, bytes memory data) public {
+        token.burn(amount, data);
+    }
+}
+

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1777 - 3188
package-lock.json


+ 2 - 2
package.json

@@ -51,8 +51,8 @@
     "eslint-plugin-standard": "^3.1.0",
     "ethereumjs-util": "^6.0.0",
     "ganache-cli": "^6.4.1",
-    "ganache-cli-coverage": "github:Agusx1211/ganache-cli#c462b3fc48fe9b16756f7799885c0741114d9ed3",
-    "openzeppelin-test-helpers": "^0.3.0",
+    "ganache-cli-coverage": "https://github.com/frangio/ganache-cli/releases/download/v6.4.1-coverage/ganache-cli-coverage-6.4.1.tgz",
+    "openzeppelin-test-helpers": "^0.3.2",
     "solhint": "^1.5.0",
     "solidity-coverage": "github:rotcivegaf/solidity-coverage#5875f5b7bc74d447f3312c9c0e9fc7814b482477",
     "truffle": "^5.0.0"

+ 533 - 0
test/drafts/ERC777/ERC777.behavior.js

@@ -0,0 +1,533 @@
+const { BN, constants, expectEvent, shouldFail } = require('openzeppelin-test-helpers');
+const { ZERO_ADDRESS } = constants;
+
+const ERC777SenderRecipientMock = artifacts.require('ERC777SenderRecipientMock');
+
+function shouldBehaveLikeERC777DirectSendBurn (holder, recipient, data) {
+  shouldBehaveLikeERC777DirectSend(holder, recipient, data);
+  shouldBehaveLikeERC777DirectBurn(holder, data);
+}
+
+function shouldBehaveLikeERC777OperatorSendBurn (holder, recipient, operator, data, operatorData) {
+  shouldBehaveLikeERC777OperatorSend(holder, recipient, operator, data, operatorData);
+  shouldBehaveLikeERC777OperatorBurn(holder, operator, data, operatorData);
+}
+
+function shouldBehaveLikeERC777UnauthorizedOperatorSendBurn (holder, recipient, operator, data, operatorData) {
+  shouldBehaveLikeERC777UnauthorizedOperatorSend(holder, recipient, operator, data, operatorData);
+  shouldBehaveLikeERC777UnauthorizedOperatorBurn(holder, operator, data, operatorData);
+}
+
+function shouldBehaveLikeERC777DirectSend (holder, recipient, data) {
+  describe('direct send', function () {
+    context('when the sender has tokens', function () {
+      shouldDirectSendTokens(holder, recipient, new BN('0'), data);
+      shouldDirectSendTokens(holder, recipient, new BN('1'), data);
+
+      it('reverts when sending more than the balance', async function () {
+        const balance = await this.token.balanceOf(holder);
+        await shouldFail.reverting(this.token.send(recipient, balance.addn(1), data, { from: holder }));
+      });
+
+      it('reverts when sending to the zero address', async function () {
+        await shouldFail.reverting(this.token.send(ZERO_ADDRESS, new BN('1'), data, { from: holder }));
+      });
+    });
+
+    context('when the sender has no tokens', function () {
+      removeBalance(holder);
+
+      shouldDirectSendTokens(holder, recipient, new BN('0'), data);
+
+      it('reverts when sending a non-zero amount', async function () {
+        await shouldFail.reverting(this.token.send(recipient, new BN('1'), data, { from: holder }));
+      });
+    });
+  });
+}
+
+function shouldBehaveLikeERC777OperatorSend (holder, recipient, operator, data, operatorData) {
+  describe('operator send', function () {
+    context('when the sender has tokens', async function () {
+      shouldOperatorSendTokens(holder, operator, recipient, new BN('0'), data, operatorData);
+      shouldOperatorSendTokens(holder, operator, recipient, new BN('1'), data, operatorData);
+
+      it('reverts when sending more than the balance', async function () {
+        const balance = await this.token.balanceOf(holder);
+        await shouldFail.reverting(
+          this.token.operatorSend(holder, recipient, balance.addn(1), data, operatorData, { from: operator })
+        );
+      });
+
+      it('reverts when sending to the zero address', async function () {
+        await shouldFail.reverting(
+          this.token.operatorSend(
+            holder, ZERO_ADDRESS, new BN('1'), data, operatorData, { from: operator }
+          )
+        );
+      });
+    });
+
+    context('when the sender has no tokens', function () {
+      removeBalance(holder);
+
+      shouldOperatorSendTokens(holder, operator, recipient, new BN('0'), data, operatorData);
+
+      it('reverts when sending a non-zero amount', async function () {
+        await shouldFail.reverting(
+          this.token.operatorSend(holder, recipient, new BN('1'), data, operatorData, { from: operator })
+        );
+      });
+
+      it('reverts when sending from the zero address', async function () {
+        // This is not yet reflected in the spec
+        await shouldFail.reverting(
+          this.token.operatorSend(
+            ZERO_ADDRESS, recipient, new BN('0'), data, operatorData, { from: operator }
+          )
+        );
+      });
+    });
+  });
+}
+
+function shouldBehaveLikeERC777UnauthorizedOperatorSend (holder, recipient, operator, data, operatorData) {
+  describe('operator send', function () {
+    it('reverts', async function () {
+      await shouldFail.reverting(this.token.operatorSend(holder, recipient, new BN('0'), data, operatorData));
+    });
+  });
+}
+
+function shouldBehaveLikeERC777DirectBurn (holder, data) {
+  describe('direct burn', function () {
+    context('when the sender has tokens', function () {
+      shouldDirectBurnTokens(holder, new BN('0'), data);
+      shouldDirectBurnTokens(holder, new BN('1'), data);
+
+      it('reverts when burning more than the balance', async function () {
+        const balance = await this.token.balanceOf(holder);
+        await shouldFail.reverting(this.token.burn(balance.addn(1), data, { from: holder }));
+      });
+    });
+
+    context('when the sender has no tokens', function () {
+      removeBalance(holder);
+
+      shouldDirectBurnTokens(holder, new BN('0'), data);
+
+      it('reverts when burning a non-zero amount', async function () {
+        await shouldFail.reverting(this.token.burn(new BN('1'), data, { from: holder }));
+      });
+    });
+  });
+}
+
+function shouldBehaveLikeERC777OperatorBurn (holder, operator, data, operatorData) {
+  describe('operator burn', function () {
+    context('when the sender has tokens', async function () {
+      shouldOperatorBurnTokens(holder, operator, new BN('0'), data, operatorData);
+      shouldOperatorBurnTokens(holder, operator, new BN('1'), data, operatorData);
+
+      it('reverts when burning more than the balance', async function () {
+        const balance = await this.token.balanceOf(holder);
+        await shouldFail.reverting(
+          this.token.operatorBurn(holder, balance.addn(1), data, operatorData, { from: operator })
+        );
+      });
+    });
+
+    context('when the sender has no tokens', function () {
+      removeBalance(holder);
+
+      shouldOperatorBurnTokens(holder, operator, new BN('0'), data, operatorData);
+
+      it('reverts when burning a non-zero amount', async function () {
+        await shouldFail.reverting(
+          this.token.operatorBurn(holder, new BN('1'), data, operatorData, { from: operator })
+        );
+      });
+
+      it('reverts when burning from the zero address', async function () {
+        // This is not yet reflected in the spec
+        await shouldFail.reverting(
+          this.token.operatorBurn(
+            ZERO_ADDRESS, new BN('0'), data, operatorData, { from: operator }
+          )
+        );
+      });
+    });
+  });
+}
+
+function shouldBehaveLikeERC777UnauthorizedOperatorBurn (holder, operator, data, operatorData) {
+  describe('operator burn', function () {
+    it('reverts', async function () {
+      await shouldFail.reverting(this.token.operatorBurn(holder, new BN('0'), data, operatorData));
+    });
+  });
+}
+
+function shouldDirectSendTokens (from, to, amount, data) {
+  shouldSendTokens(from, null, to, amount, data, null);
+}
+
+function shouldOperatorSendTokens (from, operator, to, amount, data, operatorData) {
+  shouldSendTokens(from, operator, to, amount, data, operatorData);
+}
+
+function shouldSendTokens (from, operator, to, amount, data, operatorData) {
+  const operatorCall = operator !== null;
+
+  it(`${operatorCall ? 'operator ' : ''}can send an amount of ${amount}`, async function () {
+    const initialTotalSupply = await this.token.totalSupply();
+    const initialFromBalance = await this.token.balanceOf(from);
+    const initialToBalance = await this.token.balanceOf(to);
+
+    if (!operatorCall) {
+      const { logs } = await this.token.send(to, amount, data, { from });
+      expectEvent.inLogs(logs, 'Sent', {
+        operator: from,
+        from,
+        to,
+        amount,
+        data,
+        operatorData: null,
+      });
+    } else {
+      const { logs } = await this.token.operatorSend(from, to, amount, data, operatorData, { from: operator });
+      expectEvent.inLogs(logs, 'Sent', {
+        operator,
+        from,
+        to,
+        amount,
+        data,
+        operatorData,
+      });
+    }
+
+    const finalTotalSupply = await this.token.totalSupply();
+    const finalFromBalance = await this.token.balanceOf(from);
+    const finalToBalance = await this.token.balanceOf(to);
+
+    finalTotalSupply.should.be.bignumber.equal(initialTotalSupply);
+    finalToBalance.sub(initialToBalance).should.be.bignumber.equal(amount);
+    finalFromBalance.sub(initialFromBalance).should.be.bignumber.equal(amount.neg());
+  });
+}
+
+function shouldDirectBurnTokens (from, amount, data) {
+  shouldBurnTokens(from, null, amount, data, null);
+}
+
+function shouldOperatorBurnTokens (from, operator, amount, data, operatorData) {
+  shouldBurnTokens(from, operator, amount, data, operatorData);
+}
+
+function shouldBurnTokens (from, operator, amount, data, operatorData) {
+  const operatorCall = operator !== null;
+
+  it(`${operatorCall ? 'operator ' : ''}can burn an amount of ${amount}`, async function () {
+    const initialTotalSupply = await this.token.totalSupply();
+    const initialFromBalance = await this.token.balanceOf(from);
+
+    if (!operatorCall) {
+      const { logs } = await this.token.burn(amount, data, { from });
+      expectEvent.inLogs(logs, 'Burned', {
+        operator: from,
+        from,
+        amount,
+        data,
+        operatorData: null,
+      });
+    } else {
+      const { logs } = await this.token.operatorBurn(from, amount, data, operatorData, { from: operator });
+      expectEvent.inLogs(logs, 'Burned', {
+        operator,
+        from,
+        amount,
+        data,
+        operatorData,
+      });
+    }
+
+    const finalTotalSupply = await this.token.totalSupply();
+    const finalFromBalance = await this.token.balanceOf(from);
+
+    finalTotalSupply.sub(initialTotalSupply).should.be.bignumber.equal(amount.neg());
+    finalFromBalance.sub(initialFromBalance).should.be.bignumber.equal(amount.neg());
+  });
+}
+
+function shouldBehaveLikeERC777InternalMint (recipient, operator, amount, data, operatorData) {
+  shouldInternalMintTokens(operator, recipient, new BN('0'), data, operatorData);
+  shouldInternalMintTokens(operator, recipient, amount, data, operatorData);
+
+  it('reverts when minting tokens for the zero address', async function () {
+    await shouldFail.reverting(this.token.mintInternal(operator, ZERO_ADDRESS, amount, data, operatorData));
+  });
+}
+
+function shouldInternalMintTokens (operator, to, amount, data, operatorData) {
+  it(`can (internal) mint an amount of ${amount}`, async function () {
+    const initialTotalSupply = await this.token.totalSupply();
+    const initialToBalance = await this.token.balanceOf(to);
+
+    const { logs } = await this.token.mintInternal(operator, to, amount, data, operatorData);
+    expectEvent.inLogs(logs, 'Minted', {
+      operator,
+      to,
+      amount,
+      data,
+      operatorData,
+    });
+
+    const finalTotalSupply = await this.token.totalSupply();
+    const finalToBalance = await this.token.balanceOf(to);
+
+    finalTotalSupply.sub(initialTotalSupply).should.be.bignumber.equal(amount);
+    finalToBalance.sub(initialToBalance).should.be.bignumber.equal(amount);
+  });
+}
+
+function shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook (operator, amount, data, operatorData) {
+  context('when TokensRecipient reverts', function () {
+    beforeEach(async function () {
+      await this.tokensRecipientImplementer.setShouldRevertReceive(true);
+    });
+
+    it('send reverts', async function () {
+      await shouldFail.reverting(sendFromHolder(this.token, this.sender, this.recipient, amount, data));
+    });
+
+    it('operatorSend reverts', async function () {
+      await shouldFail.reverting(
+        this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { from: operator })
+      );
+    });
+
+    it('mint (internal) reverts', async function () {
+      await shouldFail.reverting(
+        this.token.mintInternal(operator, this.recipient, amount, data, operatorData)
+      );
+    });
+  });
+
+  context('when TokensRecipient does not revert', function () {
+    beforeEach(async function () {
+      await this.tokensRecipientImplementer.setShouldRevertSend(false);
+    });
+
+    it('TokensRecipient receives send data and is called after state mutation', async function () {
+      const { tx } = await sendFromHolder(this.token, this.sender, this.recipient, amount, data);
+
+      const postSenderBalance = await this.token.balanceOf(this.sender);
+      const postRecipientBalance = await this.token.balanceOf(this.recipient);
+
+      await assertTokensReceivedCalled(
+        this.token,
+        tx,
+        this.sender,
+        this.sender,
+        this.recipient,
+        amount,
+        data,
+        null,
+        postSenderBalance,
+        postRecipientBalance,
+      );
+    });
+
+    it('TokensRecipient receives operatorSend data and is called after state mutation', async function () {
+      const { tx } = await this.token.operatorSend(
+        this.sender, this.recipient, amount, data, operatorData,
+        { from: operator },
+      );
+
+      const postSenderBalance = await this.token.balanceOf(this.sender);
+      const postRecipientBalance = await this.token.balanceOf(this.recipient);
+
+      await assertTokensReceivedCalled(
+        this.token,
+        tx,
+        operator,
+        this.sender,
+        this.recipient,
+        amount,
+        data,
+        operatorData,
+        postSenderBalance,
+        postRecipientBalance,
+      );
+    });
+
+    it('TokensRecipient receives mint (internal) data and is called after state mutation', async function () {
+      const { tx } = await this.token.mintInternal(
+        operator, this.recipient, amount, data, operatorData,
+      );
+
+      const postRecipientBalance = await this.token.balanceOf(this.recipient);
+
+      await assertTokensReceivedCalled(
+        this.token,
+        tx,
+        operator,
+        ZERO_ADDRESS,
+        this.recipient,
+        amount,
+        data,
+        operatorData,
+        new BN('0'),
+        postRecipientBalance,
+      );
+    });
+  });
+}
+
+function shouldBehaveLikeERC777SendBurnWithSendHook (operator, amount, data, operatorData) {
+  context('when TokensSender reverts', function () {
+    beforeEach(async function () {
+      await this.tokensSenderImplementer.setShouldRevertSend(true);
+    });
+
+    it('send reverts', async function () {
+      await shouldFail.reverting(sendFromHolder(this.token, this.sender, this.recipient, amount, data));
+    });
+
+    it('operatorSend reverts', async function () {
+      await shouldFail.reverting(
+        this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { from: operator })
+      );
+    });
+
+    it('burn reverts', async function () {
+      await shouldFail.reverting(burnFromHolder(this.token, this.sender, amount, data));
+    });
+
+    it('operatorBurn reverts', async function () {
+      await shouldFail.reverting(
+        this.token.operatorBurn(this.sender, amount, data, operatorData, { from: operator })
+      );
+    });
+  });
+
+  context('when TokensSender does not revert', function () {
+    beforeEach(async function () {
+      await this.tokensSenderImplementer.setShouldRevertSend(false);
+    });
+
+    it('TokensSender receives send data and is called before state mutation', async function () {
+      const preSenderBalance = await this.token.balanceOf(this.sender);
+      const preRecipientBalance = await this.token.balanceOf(this.recipient);
+
+      const { tx } = await sendFromHolder(this.token, this.sender, this.recipient, amount, data);
+
+      await assertTokensToSendCalled(
+        this.token,
+        tx,
+        this.sender,
+        this.sender,
+        this.recipient,
+        amount,
+        data,
+        null,
+        preSenderBalance,
+        preRecipientBalance,
+      );
+    });
+
+    it('TokensSender receives operatorSend data and is called before state mutation', async function () {
+      const preSenderBalance = await this.token.balanceOf(this.sender);
+      const preRecipientBalance = await this.token.balanceOf(this.recipient);
+
+      const { tx } = await this.token.operatorSend(
+        this.sender, this.recipient, amount, data, operatorData,
+        { from: operator },
+      );
+
+      await assertTokensToSendCalled(
+        this.token,
+        tx,
+        operator,
+        this.sender,
+        this.recipient,
+        amount,
+        data,
+        operatorData,
+        preSenderBalance,
+        preRecipientBalance,
+      );
+    });
+
+    it('TokensSender receives burn data and is called before state mutation', async function () {
+      const preSenderBalance = await this.token.balanceOf(this.sender);
+
+      const { tx } = await burnFromHolder(this.token, this.sender, amount, data, { from: this.sender });
+
+      await assertTokensToSendCalled(
+        this.token, tx, this.sender, this.sender, ZERO_ADDRESS, amount, data, null, preSenderBalance
+      );
+    });
+
+    it('TokensSender receives operatorBurn data and is called before state mutation', async function () {
+      const preSenderBalance = await this.token.balanceOf(this.sender);
+
+      const { tx } = await this.token.operatorBurn(this.sender, amount, data, operatorData, { from: operator });
+
+      await assertTokensToSendCalled(
+        this.token, tx, operator, this.sender, ZERO_ADDRESS, amount, data, operatorData, preSenderBalance
+      );
+    });
+  });
+}
+
+function removeBalance (holder) {
+  beforeEach(async function () {
+    await this.token.burn(await this.token.balanceOf(holder), '0x', { from: holder });
+    (await this.token.balanceOf(holder)).should.be.bignumber.equal('0');
+  });
+}
+
+async function assertTokensReceivedCalled (token, txHash, operator, from, to, amount, data, operatorData, fromBalance,
+  toBalance = '0') {
+  await expectEvent.inTransaction(txHash, ERC777SenderRecipientMock, 'TokensReceivedCalled', {
+    operator, from, to, amount, data, operatorData, token: token.address, fromBalance, toBalance,
+  });
+}
+
+async function assertTokensToSendCalled (token, txHash, operator, from, to, amount, data, operatorData, fromBalance,
+  toBalance = '0') {
+  await expectEvent.inTransaction(txHash, ERC777SenderRecipientMock, 'TokensToSendCalled', {
+    operator, from, to, amount, data, operatorData, token: token.address, fromBalance, toBalance,
+  });
+}
+
+async function sendFromHolder (token, holder, to, amount, data) {
+  if ((await web3.eth.getCode(holder)).length <= '0x'.length) {
+    return token.send(to, amount, data, { from: holder });
+  } else {
+    // assume holder is ERC777SenderRecipientMock contract
+    return (await ERC777SenderRecipientMock.at(holder)).send(token.address, to, amount, data);
+  }
+}
+
+async function burnFromHolder (token, holder, amount, data) {
+  if ((await web3.eth.getCode(holder)).length <= '0x'.length) {
+    return token.burn(amount, data, { from: holder });
+  } else {
+    // assume holder is ERC777SenderRecipientMock contract
+    return (await ERC777SenderRecipientMock.at(holder)).burn(token.address, amount, data);
+  }
+}
+
+module.exports = {
+  shouldBehaveLikeERC777DirectSendBurn,
+  shouldBehaveLikeERC777OperatorSendBurn,
+  shouldBehaveLikeERC777UnauthorizedOperatorSendBurn,
+  shouldDirectSendTokens,
+  shouldDirectBurnTokens,
+  shouldBehaveLikeERC777InternalMint,
+  shouldInternalMintTokens,
+  shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook,
+  shouldBehaveLikeERC777SendBurnWithSendHook,
+};

+ 448 - 0
test/drafts/ERC777/ERC777.test.js

@@ -0,0 +1,448 @@
+const { BN, expectEvent, shouldFail, singletons } = require('openzeppelin-test-helpers');
+
+const {
+  shouldBehaveLikeERC777DirectSendBurn,
+  shouldBehaveLikeERC777OperatorSendBurn,
+  shouldBehaveLikeERC777UnauthorizedOperatorSendBurn,
+  shouldDirectSendTokens,
+  shouldDirectBurnTokens,
+  shouldBehaveLikeERC777InternalMint,
+  shouldInternalMintTokens,
+  shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook,
+  shouldBehaveLikeERC777SendBurnWithSendHook,
+} = require('./ERC777.behavior');
+
+const ERC777 = artifacts.require('ERC777Mock');
+const ERC777SenderRecipientMock = artifacts.require('ERC777SenderRecipientMock');
+
+contract('ERC777', function ([
+  _, registryFunder, holder, defaultOperatorA, defaultOperatorB, newOperator, anyone,
+]) {
+  const initialSupply = new BN('10000');
+  const name = 'ERC777Test';
+  const symbol = '777T';
+  const data = web3.utils.sha3('OZ777TestData');
+  const operatorData = web3.utils.sha3('OZ777TestOperatorData');
+
+  const defaultOperators = [defaultOperatorA, defaultOperatorB];
+
+  beforeEach(async function () {
+    this.erc1820 = await singletons.ERC1820Registry(registryFunder);
+  });
+
+  it('reverts with a granularity of zero', async function () {
+    await shouldFail.reverting(ERC777.new(holder, initialSupply, name, symbol, 0, []));
+  });
+
+  context('with a granularity of one', function () {
+    const granularity = new BN('1');
+
+    context('with default operators', function () {
+      beforeEach(async function () {
+        this.token = await ERC777.new(holder, initialSupply, name, symbol, granularity, defaultOperators);
+      });
+
+      it.skip('does not emit AuthorizedOperator events for default operators', async function () {
+        expectEvent.not.inConstructor(this.token, 'AuthorizedOperator'); // This helper needs to be implemented
+      });
+
+      describe('basic information', function () {
+        it('returns the name', async function () {
+          (await this.token.name()).should.equal(name);
+        });
+
+        it('returns the symbol', async function () {
+          (await this.token.symbol()).should.equal(symbol);
+        });
+
+        it('returns the granularity', async function () {
+          (await this.token.granularity()).should.be.bignumber.equal(granularity);
+        });
+
+        it('returns the default operators', async function () {
+          (await this.token.defaultOperators()).should.deep.equal(defaultOperators);
+        });
+
+        it('default operators are operators for all accounts', async function () {
+          for (const operator of defaultOperators) {
+            (await this.token.isOperatorFor(operator, anyone)).should.equal(true);
+          }
+        });
+
+        it('returns thte total supply', async function () {
+          (await this.token.totalSupply()).should.be.bignumber.equal(initialSupply);
+        });
+
+        it('is registered in the registry', async function () {
+          (await this.erc1820.getInterfaceImplementer(this.token.address, web3.utils.soliditySha3('ERC777Token')))
+            .should.equal(this.token.address);
+        });
+      });
+
+      describe('balanceOf', function () {
+        context('for an account with no tokens', function () {
+          it('returns zero', async function () {
+            (await this.token.balanceOf(anyone)).should.be.bignumber.equal('0');
+          });
+        });
+
+        context('for an account with tokens', function () {
+          it('returns their balance', async function () {
+            (await this.token.balanceOf(holder)).should.be.bignumber.equal(initialSupply);
+          });
+        });
+      });
+
+      context('with no ERC777TokensSender and no ERC777TokensRecipient implementers', function () {
+        describe('send/burn', function () {
+          shouldBehaveLikeERC777DirectSendBurn(holder, anyone, data);
+
+          context('with self operator', function () {
+            shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, holder, data, operatorData);
+          });
+
+          context('with first default operator', function () {
+            shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, defaultOperatorA, data, operatorData);
+          });
+
+          context('with second default operator', function () {
+            shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, defaultOperatorB, data, operatorData);
+          });
+
+          context('before authorizing a new operator', function () {
+            shouldBehaveLikeERC777UnauthorizedOperatorSendBurn(holder, anyone, newOperator, data, operatorData);
+          });
+
+          context('with new authorized operator', function () {
+            beforeEach(async function () {
+              await this.token.authorizeOperator(newOperator, { from: holder });
+            });
+
+            shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, newOperator, data, operatorData);
+
+            context('with revoked operator', function () {
+              beforeEach(async function () {
+                await this.token.revokeOperator(newOperator, { from: holder });
+              });
+
+              shouldBehaveLikeERC777UnauthorizedOperatorSendBurn(holder, anyone, newOperator, data, operatorData);
+            });
+          });
+        });
+
+        describe('mint (internal)', function () {
+          const to = anyone;
+          const amount = new BN('5');
+
+          context('with default operator', function () {
+            const operator = defaultOperatorA;
+
+            shouldBehaveLikeERC777InternalMint(to, operator, amount, data, operatorData);
+          });
+
+          context('with non operator', function () {
+            const operator = newOperator;
+
+            shouldBehaveLikeERC777InternalMint(to, operator, amount, data, operatorData);
+          });
+        });
+      });
+
+      describe('operator management', function () {
+        it('accounts are their own operator', async function () {
+          (await this.token.isOperatorFor(holder, holder)).should.equal(true);
+        });
+
+        it('reverts when self-authorizing', async function () {
+          await shouldFail.reverting(this.token.authorizeOperator(holder, { from: holder }));
+        });
+
+        it('reverts when self-revoking', async function () {
+          await shouldFail.reverting(this.token.revokeOperator(holder, { from: holder }));
+        });
+
+        it('non-operators can be revoked', async function () {
+          (await this.token.isOperatorFor(newOperator, holder)).should.equal(false);
+
+          const { logs } = await this.token.revokeOperator(newOperator, { from: holder });
+          expectEvent.inLogs(logs, 'RevokedOperator', { operator: newOperator, tokenHolder: holder });
+
+          (await this.token.isOperatorFor(newOperator, holder)).should.equal(false);
+        });
+
+        it('non-operators can be authorized', async function () {
+          (await this.token.isOperatorFor(newOperator, holder)).should.equal(false);
+
+          const { logs } = await this.token.authorizeOperator(newOperator, { from: holder });
+          expectEvent.inLogs(logs, 'AuthorizedOperator', { operator: newOperator, tokenHolder: holder });
+
+          (await this.token.isOperatorFor(newOperator, holder)).should.equal(true);
+        });
+
+        describe('new operators', function () {
+          beforeEach(async function () {
+            await this.token.authorizeOperator(newOperator, { from: holder });
+          });
+
+          it('are not added to the default operators list', async function () {
+            (await this.token.defaultOperators()).should.deep.equal(defaultOperators);
+          });
+
+          it('can be re-authorized', async function () {
+            const { logs } = await this.token.authorizeOperator(newOperator, { from: holder });
+            expectEvent.inLogs(logs, 'AuthorizedOperator', { operator: newOperator, tokenHolder: holder });
+
+            (await this.token.isOperatorFor(newOperator, holder)).should.equal(true);
+          });
+
+          it('can be revoked', async function () {
+            const { logs } = await this.token.revokeOperator(newOperator, { from: holder });
+            expectEvent.inLogs(logs, 'RevokedOperator', { operator: newOperator, tokenHolder: holder });
+
+            (await this.token.isOperatorFor(newOperator, holder)).should.equal(false);
+          });
+        });
+
+        describe('default operators', function () {
+          it('can be re-authorized', async function () {
+            const { logs } = await this.token.authorizeOperator(defaultOperatorA, { from: holder });
+            expectEvent.inLogs(logs, 'AuthorizedOperator', { operator: defaultOperatorA, tokenHolder: holder });
+
+            (await this.token.isOperatorFor(defaultOperatorA, holder)).should.equal(true);
+          });
+
+          it('can be revoked', async function () {
+            const { logs } = await this.token.revokeOperator(defaultOperatorA, { from: holder });
+            expectEvent.inLogs(logs, 'RevokedOperator', { operator: defaultOperatorA, tokenHolder: holder });
+
+            (await this.token.isOperatorFor(defaultOperatorA, holder)).should.equal(false);
+          });
+
+          it('cannot be revoked for themselves', async function () {
+            await shouldFail.reverting(this.token.revokeOperator(defaultOperatorA, { from: defaultOperatorA }));
+          });
+
+          context('with revoked default operator', function () {
+            beforeEach(async function () {
+              await this.token.revokeOperator(defaultOperatorA, { from: holder });
+            });
+
+            it('default operator is not revoked for other holders', async function () {
+              (await this.token.isOperatorFor(defaultOperatorA, anyone)).should.equal(true);
+            });
+
+            it('other default operators are not revoked', async function () {
+              (await this.token.isOperatorFor(defaultOperatorB, holder)).should.equal(true);
+            });
+
+            it('default operators list is not modified', async function () {
+              (await this.token.defaultOperators()).should.deep.equal(defaultOperators);
+            });
+
+            it('revoked default operator can be re-authorized', async function () {
+              const { logs } = await this.token.authorizeOperator(defaultOperatorA, { from: holder });
+              expectEvent.inLogs(logs, 'AuthorizedOperator', { operator: defaultOperatorA, tokenHolder: holder });
+
+              (await this.token.isOperatorFor(defaultOperatorA, holder)).should.equal(true);
+            });
+          });
+        });
+      });
+
+      describe('send and receive hooks', function () {
+        const amount = new BN('1');
+        const operator = defaultOperatorA;
+        // sender and recipient are stored inside 'this', since in some tests their addresses are determined dynamically
+
+        describe('tokensReceived', function () {
+          beforeEach(function () {
+            this.sender = holder;
+          });
+
+          context('with no ERC777TokensRecipient implementer', function () {
+            context('with contract recipient', function () {
+              beforeEach(async function () {
+                this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new();
+                this.recipient = this.tokensRecipientImplementer.address;
+
+                // Note that tokensRecipientImplementer doesn't implement the recipient interface for the recipient
+              });
+
+              it('send reverts', async function () {
+                await shouldFail.reverting(this.token.send(this.recipient, amount, data));
+              });
+
+              it('operatorSend reverts', async function () {
+                await shouldFail.reverting(
+                  this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { from: operator })
+                );
+              });
+
+              it('mint (internal) reverts', async function () {
+                await shouldFail.reverting(
+                  this.token.mintInternal(operator, this.recipient, amount, data, operatorData)
+                );
+              });
+            });
+          });
+
+          context('with ERC777TokensRecipient implementer', function () {
+            context('with contract as implementer for an externally owned account', function () {
+              beforeEach(async function () {
+                this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new();
+                this.recipient = anyone;
+
+                await this.tokensRecipientImplementer.recipientFor(this.recipient);
+
+                await this.erc1820.setInterfaceImplementer(
+                  this.recipient,
+                  web3.utils.soliditySha3('ERC777TokensRecipient'), this.tokensRecipientImplementer.address,
+                  { from: this.recipient },
+                );
+              });
+
+              shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData);
+            });
+
+            context('with contract as implementer for another contract', function () {
+              beforeEach(async function () {
+                this.recipientContract = await ERC777SenderRecipientMock.new();
+                this.recipient = this.recipientContract.address;
+
+                this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new();
+                await this.tokensRecipientImplementer.recipientFor(this.recipient);
+                await this.recipientContract.registerRecipient(this.tokensRecipientImplementer.address);
+              });
+
+              shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData);
+            });
+
+            context('with contract as implementer for itself', function () {
+              beforeEach(async function () {
+                this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new();
+                this.recipient = this.tokensRecipientImplementer.address;
+
+                await this.tokensRecipientImplementer.recipientFor(this.recipient);
+              });
+
+              shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData);
+            });
+          });
+        });
+
+        describe('tokensToSend', function () {
+          beforeEach(function () {
+            this.recipient = anyone;
+          });
+
+          context('with a contract as implementer for an externally owned account', function () {
+            beforeEach(async function () {
+              this.tokensSenderImplementer = await ERC777SenderRecipientMock.new();
+              this.sender = holder;
+
+              await this.tokensSenderImplementer.senderFor(this.sender);
+
+              await this.erc1820.setInterfaceImplementer(
+                this.sender,
+                web3.utils.soliditySha3('ERC777TokensSender'), this.tokensSenderImplementer.address,
+                { from: this.sender },
+              );
+            });
+
+            shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData);
+          });
+
+          context('with contract as implementer for another contract', function () {
+            beforeEach(async function () {
+              this.senderContract = await ERC777SenderRecipientMock.new();
+              this.sender = this.senderContract.address;
+
+              this.tokensSenderImplementer = await ERC777SenderRecipientMock.new();
+              await this.tokensSenderImplementer.senderFor(this.sender);
+              await this.senderContract.registerSender(this.tokensSenderImplementer.address);
+
+              // For the contract to be able to receive tokens (that it can later send), it must also implement the
+              // recipient interface.
+
+              await this.senderContract.recipientFor(this.sender);
+              await this.token.send(this.sender, amount, data, { from: holder });
+            });
+
+            shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData);
+          });
+
+          context('with a contract as implementer for itself', function () {
+            beforeEach(async function () {
+              this.tokensSenderImplementer = await ERC777SenderRecipientMock.new();
+              this.sender = this.tokensSenderImplementer.address;
+
+              await this.tokensSenderImplementer.senderFor(this.sender);
+
+              // For the contract to be able to receive tokens (that it can later send), it must also implement the
+              // recipient interface.
+
+              await this.tokensSenderImplementer.recipientFor(this.sender);
+              await this.token.send(this.sender, amount, data, { from: holder });
+            });
+
+            shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData);
+          });
+        });
+      });
+    });
+
+    context('with no default operators', function () {
+      beforeEach(async function () {
+        this.token = await ERC777.new(holder, initialSupply, name, symbol, granularity, []);
+      });
+
+      it('default operators list is empty', async function () {
+        (await this.token.defaultOperators()).should.deep.equal([]);
+      });
+    });
+  });
+
+  context('with granularity larger than 1', function () {
+    const granularity = new BN('4');
+
+    beforeEach(async function () {
+      initialSupply.mod(granularity).should.be.bignumber.equal('0');
+
+      this.token = await ERC777.new(holder, initialSupply, name, symbol, granularity, defaultOperators);
+    });
+
+    it('returns the granularity', async function () {
+      (await this.token.granularity()).should.be.bignumber.equal(granularity);
+    });
+
+    context('when the sender has tokens', function () {
+      const from = holder;
+
+      shouldDirectSendTokens(from, anyone, new BN('0'), data);
+      shouldDirectSendTokens(from, anyone, granularity, data);
+      shouldDirectSendTokens(from, anyone, granularity.muln(2), data);
+
+      it('reverts when sending an amount non-multiple of the granularity', async function () {
+        await shouldFail.reverting(this.token.send(anyone, granularity.subn(1), data, { from }));
+      });
+
+      shouldDirectBurnTokens(from, new BN('0'), data);
+      shouldDirectBurnTokens(from, granularity, data);
+      shouldDirectBurnTokens(from, granularity.muln(2), data);
+
+      it('reverts when burning an amount non-multiple of the granularity', async function () {
+        await shouldFail.reverting(this.token.burn(granularity.subn(1), data, { from }));
+      });
+    });
+
+    shouldInternalMintTokens(anyone, defaultOperatorA, new BN('0'), data, operatorData);
+    shouldInternalMintTokens(anyone, defaultOperatorA, granularity, data, operatorData);
+    shouldInternalMintTokens(anyone, defaultOperatorA, granularity.muln(2), data, operatorData);
+
+    it('reverts when minting an amount non-multiple of the granularity', async function () {
+      await shouldFail.reverting(
+        this.token.mintInternal(defaultOperatorA, anyone, granularity.subn(1), data, operatorData)
+      );
+    });
+  });
+});

Vissa filer visades inte eftersom för många filer har ändrats