Browse Source

ERC1155 feature pending tasks (#2014)

* Initial ERC1155 implementation with some tests (#1803)

* Initial ERC1155 implementation with some tests

* Remove mocked isERC1155TokenReceiver

* Revert reason edit nit

* Remove parameters associated with isERC1155TokenReceiver call

* Add tests for approvals and single transfers

* Add tests for transferring to contracts

* Add tests for batch transfers

* Make expectEvent.inTransaction tests async

* Renamed "owner" to "account" and "holder"

* Document unspecified balanceOfBatch reversion on zero behavior

* Ensure accounts can't set their own operator status

* Specify descriptive messages for underflow errors

* Bring SafeMath.add calls in line with OZ style

* Explicitly prevent _burn on the zero account

* Implement batch minting/burning

* Refactored operator approval check into isApprovedForAll calls

* Renamed ERC1155TokenReceiver to ERC1155Receiver

* Added ERC1155Holder

* Fix lint issues

* Migrate tests to @openzeppelin/test-environment

* Port ERC 1155 branch to Solidity 0.6 (and current master) (#2130)

* port ERC1155 to Solidity 0.6

* make ERC1155 constructor more similar to ERC721 one

* also migrate mock contracts to Solidity 0.6

* mark all non-view functions as virtual

Co-authored-by: Alan Lu <alanlu1023@gmail.com>
Co-authored-by: Nicolás Venturo <nicolas.venturo@gmail.com>
Co-authored-by: Robert Kaiser <kairo@kairo.at>
Francisco Giordano 5 years ago
parent
commit
956d6632d9

+ 33 - 0
contracts/mocks/ERC1155Mock.sol

@@ -0,0 +1,33 @@
+pragma solidity ^0.6.0;
+
+import "../token/ERC1155/ERC1155.sol";
+
+/**
+ * @title ERC1155Mock
+ * This mock just publicizes internal functions for testing purposes
+ */
+contract ERC1155Mock is ERC1155 {
+    function mint(address to, uint256 id, uint256 value, bytes memory data) public {
+        _mint(to, id, value, data);
+    }
+
+    function mintBatch(address to, uint256[] memory ids, uint256[] memory values, bytes memory data) public {
+        _mintBatch(to, ids, values, data);
+    }
+
+    function burn(address owner, uint256 id, uint256 value) public {
+        _burn(owner, id, value);
+    }
+
+    function burnBatch(address owner, uint256[] memory ids, uint256[] memory values) public {
+        _burnBatch(owner, ids, values);
+    }
+
+    function doSafeTransferAcceptanceCheck(address operator, address from, address to, uint256 id, uint256 value, bytes memory data) public {
+        _doSafeTransferAcceptanceCheck(operator, from, to, id, value, data);
+    }
+
+    function doSafeBatchTransferAcceptanceCheck(address operator, address from, address to, uint256[] memory ids, uint256[] memory values, bytes memory data) public {
+        _doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, values, data);
+    }
+}

+ 60 - 0
contracts/mocks/ERC1155ReceiverMock.sol

@@ -0,0 +1,60 @@
+pragma solidity ^0.6.0;
+
+import "../token/ERC1155/IERC1155Receiver.sol";
+import "./ERC165Mock.sol";
+
+contract ERC1155ReceiverMock is IERC1155Receiver, ERC165Mock {
+    bytes4 private _recRetval;
+    bool private _recReverts;
+    bytes4 private _batRetval;
+    bool private _batReverts;
+
+    event Received(address operator, address from, uint256 id, uint256 value, bytes data, uint256 gas);
+    event BatchReceived(address operator, address from, uint256[] ids, uint256[] values, bytes data, uint256 gas);
+
+    constructor (
+        bytes4 recRetval,
+        bool recReverts,
+        bytes4 batRetval,
+        bool batReverts
+    )
+        public
+    {
+        _recRetval = recRetval;
+        _recReverts = recReverts;
+        _batRetval = batRetval;
+        _batReverts = batReverts;
+    }
+
+    function onERC1155Received(
+        address operator,
+        address from,
+        uint256 id,
+        uint256 value,
+        bytes calldata data
+    )
+        external
+        override
+        returns(bytes4)
+    {
+        require(!_recReverts, "ERC1155ReceiverMock: reverting on receive");
+        emit Received(operator, from, id, value, data, gasleft());
+        return _recRetval;
+    }
+
+    function onERC1155BatchReceived(
+        address operator,
+        address from,
+        uint256[] calldata ids,
+        uint256[] calldata values,
+        bytes calldata data
+    )
+        external
+        override
+        returns(bytes4)
+    {
+        require(!_batReverts, "ERC1155ReceiverMock: reverting on batch receive");
+        emit BatchReceived(operator, from, ids, values, data, gasleft());
+        return _batRetval;
+    }
+}

+ 307 - 0
contracts/token/ERC1155/ERC1155.sol

@@ -0,0 +1,307 @@
+pragma solidity ^0.6.0;
+
+import "./IERC1155.sol";
+import "./IERC1155Receiver.sol";
+import "../../math/SafeMath.sol";
+import "../../utils/Address.sol";
+import "../../introspection/ERC165.sol";
+
+/**
+ * @title Standard ERC1155 token
+ *
+ * @dev Implementation of the basic standard multi-token.
+ * See https://eips.ethereum.org/EIPS/eip-1155
+ * Originally based on code by Enjin: https://github.com/enjin/erc-1155
+ */
+contract ERC1155 is ERC165, IERC1155
+{
+    using SafeMath for uint256;
+    using Address for address;
+
+    // Mapping from token ID to account balances
+    mapping (uint256 => mapping(address => uint256)) private _balances;
+
+    // Mapping from account to operator approvals
+    mapping (address => mapping(address => bool)) private _operatorApprovals;
+
+    /*
+     *     bytes4(keccak256('balanceOf(address,uint256)')) == 0x00fdd58e
+     *     bytes4(keccak256('balanceOfBatch(address[],uint256[])')) == 0x4e1273f4
+     *     bytes4(keccak256('setApprovalForAll(address,bool)')) == 0xa22cb465
+     *     bytes4(keccak256('isApprovedForAll(address,address)')) == 0xe985e9c5
+     *     bytes4(keccak256('safeTransferFrom(address,address,uint256,uint256,bytes)')) == 0xf242432a
+     *     bytes4(keccak256('safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)')) == 0x2eb2c2d6
+     *
+     *     => 0x00fdd58e ^ 0x4e1273f4 ^ 0xa22cb465 ^
+     *        0xe985e9c5 ^ 0xf242432a ^ 0x2eb2c2d6 == 0xd9b67a26
+     */
+    bytes4 private constant _INTERFACE_ID_ERC1155 = 0xd9b67a26;
+
+    constructor() public {
+        // register the supported interfaces to conform to ERC1155 via ERC165
+        _registerInterface(_INTERFACE_ID_ERC1155);
+    }
+
+    /**
+        @dev Get the specified address' balance for token with specified ID.
+
+        Attempting to query the zero account for a balance will result in a revert.
+
+        @param account The address of the token holder
+        @param id ID of the token
+        @return The account's balance of the token type requested
+     */
+    function balanceOf(address account, uint256 id) public view override returns (uint256) {
+        require(account != address(0), "ERC1155: balance query for the zero address");
+        return _balances[id][account];
+    }
+
+    /**
+        @dev Get the balance of multiple account/token pairs.
+
+        If any of the query accounts is the zero account, this query will revert.
+
+        @param accounts The addresses of the token holders
+        @param ids IDs of the tokens
+        @return Balances for each account and token id pair
+     */
+    function balanceOfBatch(
+        address[] memory accounts,
+        uint256[] memory ids
+    )
+        public
+        view
+        override
+        returns (uint256[] memory)
+    {
+        require(accounts.length == ids.length, "ERC1155: accounts and IDs must have same lengths");
+
+        uint256[] memory batchBalances = new uint256[](accounts.length);
+
+        for (uint256 i = 0; i < accounts.length; ++i) {
+            require(accounts[i] != address(0), "ERC1155: some address in batch balance query is zero");
+            batchBalances[i] = _balances[ids[i]][accounts[i]];
+        }
+
+        return batchBalances;
+    }
+
+    /**
+     * @dev Sets or unsets the approval of a given operator.
+     *
+     * An operator is allowed to transfer all tokens of the sender on their behalf.
+     *
+     * Because an account already has operator privileges for itself, this function will revert
+     * if the account attempts to set the approval status for itself.
+     *
+     * @param operator address to set the approval
+     * @param approved representing the status of the approval to be set
+     */
+    function setApprovalForAll(address operator, bool approved) external override virtual {
+        require(msg.sender != operator, "ERC1155: cannot set approval status for self");
+        _operatorApprovals[msg.sender][operator] = approved;
+        emit ApprovalForAll(msg.sender, operator, approved);
+    }
+
+    /**
+        @notice Queries the approval status of an operator for a given account.
+        @param account   The account of the Tokens
+        @param operator  Address of authorized operator
+        @return           True if the operator is approved, false if not
+    */
+    function isApprovedForAll(address account, address operator) public view override returns (bool) {
+        return _operatorApprovals[account][operator];
+    }
+
+    /**
+        @dev Transfers `value` amount of an `id` from the `from` address to the `to` address specified.
+        Caller must be approved to manage the tokens being transferred out of the `from` account.
+        If `to` is a smart contract, will call `onERC1155Received` on `to` and act appropriately.
+        @param from Source address
+        @param to Target address
+        @param id ID of the token type
+        @param value Transfer amount
+        @param data Data forwarded to `onERC1155Received` if `to` is a contract receiver
+    */
+    function safeTransferFrom(
+        address from,
+        address to,
+        uint256 id,
+        uint256 value,
+        bytes calldata data
+    )
+        external
+        override
+        virtual
+    {
+        require(to != address(0), "ERC1155: target address must be non-zero");
+        require(
+            from == msg.sender || isApprovedForAll(from, msg.sender) == true,
+            "ERC1155: need operator approval for 3rd party transfers"
+        );
+
+        _balances[id][from] = _balances[id][from].sub(value, "ERC1155: insufficient balance for transfer");
+        _balances[id][to] = _balances[id][to].add(value);
+
+        emit TransferSingle(msg.sender, from, to, id, value);
+
+        _doSafeTransferAcceptanceCheck(msg.sender, from, to, id, value, data);
+    }
+
+    /**
+        @dev Transfers `values` amount(s) of `ids` from the `from` address to the
+        `to` address specified. Caller must be approved to manage the tokens being
+        transferred out of the `from` account. If `to` is a smart contract, will
+        call `onERC1155BatchReceived` on `to` and act appropriately.
+        @param from Source address
+        @param to Target address
+        @param ids IDs of each token type
+        @param values Transfer amounts per token type
+        @param data Data forwarded to `onERC1155Received` if `to` is a contract receiver
+    */
+    function safeBatchTransferFrom(
+        address from,
+        address to,
+        uint256[] calldata ids,
+        uint256[] calldata values,
+        bytes calldata data
+    )
+        external
+        override
+        virtual
+    {
+        require(ids.length == values.length, "ERC1155: IDs and values must have same lengths");
+        require(to != address(0), "ERC1155: target address must be non-zero");
+        require(
+            from == msg.sender || isApprovedForAll(from, msg.sender) == true,
+            "ERC1155: need operator approval for 3rd party transfers"
+        );
+
+        for (uint256 i = 0; i < ids.length; ++i) {
+            uint256 id = ids[i];
+            uint256 value = values[i];
+
+            _balances[id][from] = _balances[id][from].sub(
+                value,
+                "ERC1155: insufficient balance of some token type for transfer"
+            );
+            _balances[id][to] = _balances[id][to].add(value);
+        }
+
+        emit TransferBatch(msg.sender, from, to, ids, values);
+
+        _doSafeBatchTransferAcceptanceCheck(msg.sender, from, to, ids, values, data);
+    }
+
+    /**
+     * @dev Internal function to mint an amount of a token with the given ID
+     * @param to The address that will own the minted token
+     * @param id ID of the token to be minted
+     * @param value Amount of the token to be minted
+     * @param data Data forwarded to `onERC1155Received` if `to` is a contract receiver
+     */
+    function _mint(address to, uint256 id, uint256 value, bytes memory data) internal virtual {
+        require(to != address(0), "ERC1155: mint to the zero address");
+
+        _balances[id][to] = _balances[id][to].add(value);
+        emit TransferSingle(msg.sender, address(0), to, id, value);
+
+        _doSafeTransferAcceptanceCheck(msg.sender, address(0), to, id, value, data);
+    }
+
+    /**
+     * @dev Internal function to batch mint amounts of tokens with the given IDs
+     * @param to The address that will own the minted token
+     * @param ids IDs of the tokens to be minted
+     * @param values Amounts of the tokens to be minted
+     * @param data Data forwarded to `onERC1155Received` if `to` is a contract receiver
+     */
+    function _mintBatch(address to, uint256[] memory ids, uint256[] memory values, bytes memory data) internal virtual {
+        require(to != address(0), "ERC1155: batch mint to the zero address");
+        require(ids.length == values.length, "ERC1155: minted IDs and values must have same lengths");
+
+        for(uint i = 0; i < ids.length; i++) {
+            _balances[ids[i]][to] = values[i].add(_balances[ids[i]][to]);
+        }
+
+        emit TransferBatch(msg.sender, address(0), to, ids, values);
+
+        _doSafeBatchTransferAcceptanceCheck(msg.sender, address(0), to, ids, values, data);
+    }
+
+    /**
+     * @dev Internal function to burn an amount of a token with the given ID
+     * @param account Account which owns the token to be burnt
+     * @param id ID of the token to be burnt
+     * @param value Amount of the token to be burnt
+     */
+    function _burn(address account, uint256 id, uint256 value) internal virtual {
+        require(account != address(0), "ERC1155: attempting to burn tokens on zero account");
+
+        _balances[id][account] = _balances[id][account].sub(
+            value,
+            "ERC1155: attempting to burn more than balance"
+        );
+        emit TransferSingle(msg.sender, account, address(0), id, value);
+    }
+
+    /**
+     * @dev Internal function to batch burn an amounts of tokens with the given IDs
+     * @param account Account which owns the token to be burnt
+     * @param ids IDs of the tokens to be burnt
+     * @param values Amounts of the tokens to be burnt
+     */
+    function _burnBatch(address account, uint256[] memory ids, uint256[] memory values) internal virtual {
+        require(account != address(0), "ERC1155: attempting to burn batch of tokens on zero account");
+        require(ids.length == values.length, "ERC1155: burnt IDs and values must have same lengths");
+
+        for(uint i = 0; i < ids.length; i++) {
+            _balances[ids[i]][account] = _balances[ids[i]][account].sub(
+                values[i],
+                "ERC1155: attempting to burn more than balance for some token"
+            );
+        }
+
+        emit TransferBatch(msg.sender, account, address(0), ids, values);
+    }
+
+    function _doSafeTransferAcceptanceCheck(
+        address operator,
+        address from,
+        address to,
+        uint256 id,
+        uint256 value,
+        bytes memory data
+    )
+        internal
+        virtual
+    {
+        if(to.isContract()) {
+            require(
+                IERC1155Receiver(to).onERC1155Received(operator, from, id, value, data) ==
+                    IERC1155Receiver(to).onERC1155Received.selector,
+                "ERC1155: got unknown value from onERC1155Received"
+            );
+        }
+    }
+
+    function _doSafeBatchTransferAcceptanceCheck(
+        address operator,
+        address from,
+        address to,
+        uint256[] memory ids,
+        uint256[] memory values,
+        bytes memory data
+    )
+        internal
+        virtual
+    {
+        if(to.isContract()) {
+            require(
+                IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, values, data) ==
+                    IERC1155Receiver(to).onERC1155BatchReceived.selector,
+                "ERC1155: got unknown value from onERC1155BatchReceived"
+            );
+        }
+    }
+}

+ 17 - 0
contracts/token/ERC1155/ERC1155Holder.sol

@@ -0,0 +1,17 @@
+pragma solidity ^0.6.0;
+
+import "./ERC1155Receiver.sol";
+
+contract ERC1155Holder is ERC1155Receiver {
+
+    function onERC1155Received(address, address, uint256, uint256, bytes calldata) external override virtual returns (bytes4)
+    {
+        return this.onERC1155Received.selector;
+    }
+
+
+    function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) external override virtual returns (bytes4)
+    {
+        return this.onERC1155BatchReceived.selector;
+    }
+}

+ 13 - 0
contracts/token/ERC1155/ERC1155Receiver.sol

@@ -0,0 +1,13 @@
+pragma solidity ^0.6.0;
+
+import "./IERC1155Receiver.sol";
+import "../../introspection/ERC165.sol";
+
+abstract contract ERC1155Receiver is ERC165, IERC1155Receiver {
+    constructor() public {
+        _registerInterface(
+            ERC1155Receiver(0).onERC1155Received.selector ^
+            ERC1155Receiver(0).onERC1155BatchReceived.selector
+        );
+    }
+}

+ 29 - 0
contracts/token/ERC1155/IERC1155.sol

@@ -0,0 +1,29 @@
+pragma solidity ^0.6.0;
+
+import "../../introspection/IERC165.sol";
+
+/**
+    @title ERC-1155 Multi Token Standard basic interface
+    @dev See https://eips.ethereum.org/EIPS/eip-1155
+ */
+abstract contract IERC1155 is IERC165 {
+    event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value);
+
+    event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values);
+
+    event ApprovalForAll(address indexed account, address indexed operator, bool approved);
+
+    event URI(string value, uint256 indexed id);
+
+    function balanceOf(address account, uint256 id) public view virtual returns (uint256);
+
+    function balanceOfBatch(address[] memory accounts, uint256[] memory ids) public view virtual returns (uint256[] memory);
+
+    function setApprovalForAll(address operator, bool approved) external virtual;
+
+    function isApprovedForAll(address account, address operator) external view virtual returns (bool);
+
+    function safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes calldata data) external virtual;
+
+    function safeBatchTransferFrom(address from, address to, uint256[] calldata ids, uint256[] calldata values, bytes calldata data) external virtual;
+}

+ 56 - 0
contracts/token/ERC1155/IERC1155Receiver.sol

@@ -0,0 +1,56 @@
+pragma solidity ^0.6.0;
+
+import "../../introspection/IERC165.sol";
+
+/**
+    @title ERC-1155 Multi Token Receiver Interface
+    @dev See https://eips.ethereum.org/EIPS/eip-1155
+*/
+interface IERC1155Receiver is IERC165 {
+
+    /**
+        @dev Handles the receipt of a single ERC1155 token type. This function is
+        called at the end of a `safeTransferFrom` after the balance has been updated.
+        To accept the transfer, this must return
+        `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`
+        (i.e. 0xf23a6e61, or its own function selector).
+        @param operator The address which initiated the transfer (i.e. msg.sender)
+        @param from The address which previously owned the token
+        @param id The ID of the token being transferred
+        @param value The amount of tokens being transferred
+        @param data Additional data with no specified format
+        @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed
+    */
+    function onERC1155Received(
+        address operator,
+        address from,
+        uint256 id,
+        uint256 value,
+        bytes calldata data
+    )
+        external
+        returns(bytes4);
+
+    /**
+        @dev Handles the receipt of a multiple ERC1155 token types. This function
+        is called at the end of a `safeBatchTransferFrom` after the balances have
+        been updated. To accept the transfer(s), this must return
+        `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`
+        (i.e. 0xbc197c81, or its own function selector).
+        @param operator The address which initiated the batch transfer (i.e. msg.sender)
+        @param from The address which previously owned the token
+        @param ids An array containing ids of each token being transferred (order and length must match values array)
+        @param values An array containing amounts of each token being transferred (order and length must match ids array)
+        @param data Additional data with no specified format
+        @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed
+    */
+    function onERC1155BatchReceived(
+        address operator,
+        address from,
+        uint256[] calldata ids,
+        uint256[] calldata values,
+        bytes calldata data
+    )
+        external
+        returns(bytes4);
+}

+ 12 - 0
contracts/token/ERC1155/README.md

@@ -0,0 +1,12 @@
+---
+sections:
+  - title: Core
+    contracts:
+      - IERC1155
+      - ERC1155
+      - IERC1155Receiver
+---
+
+This set of interfaces and contracts are all related to the [ERC1155 Multi Token Standard](https://eips.ethereum.org/EIPS/eip-1155).
+
+The EIP consists of two interfaces which fulfill different roles, found here as `IERC1155`  and `IERC1155Receiver`. Only `IERC1155` is required for a contract to be ERC1155 compliant. The basic functionality is implemented in `ERC1155`.

+ 8 - 0
test/introspection/SupportsInterface.behavior.js

@@ -27,6 +27,14 @@ const INTERFACES = {
     'symbol()',
     'tokenURI(uint256)',
   ],
+  ERC1155: [
+    'balanceOf(address,uint256)',
+    'balanceOfBatch(address[],uint256[])',
+    'setApprovalForAll(address,bool)',
+    'isApprovedForAll(address,address)',
+    'safeTransferFrom(address,address,uint256,uint256,bytes)',
+    'safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)',
+  ],
 };
 
 const INTERFACE_IDS = {};

+ 708 - 0
test/token/ERC1155/ERC1155.behavior.js

@@ -0,0 +1,708 @@
+const { contract } = require('@openzeppelin/test-environment');
+
+const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
+const { ZERO_ADDRESS } = constants;
+
+const { expect } = require('chai');
+
+const { shouldSupportInterfaces } = require('../../introspection/SupportsInterface.behavior');
+
+const ERC1155ReceiverMock = contract.fromArtifact('ERC1155ReceiverMock');
+
+function shouldBehaveLikeERC1155 ([minter, firstTokenHolder, secondTokenHolder, multiTokenHolder, recipient, proxy]) {
+  const firstTokenId = new BN(1);
+  const secondTokenId = new BN(2);
+  const unknownTokenId = new BN(3);
+
+  const firstAmount = new BN(1000);
+  const secondAmount = new BN(2000);
+
+  const RECEIVER_SINGLE_MAGIC_VALUE = '0xf23a6e61';
+  const RECEIVER_BATCH_MAGIC_VALUE = '0xbc197c81';
+
+  describe('like an ERC1155', function () {
+    describe('balanceOf', function () {
+      it('reverts when queried about the zero address', async function () {
+        await expectRevert(
+          this.token.balanceOf(ZERO_ADDRESS, firstTokenId),
+          'ERC1155: balance query for the zero address'
+        );
+      });
+
+      context('when accounts don\'t own tokens', function () {
+        it('returns zero for given addresses', async function () {
+          expect(await this.token.balanceOf(
+            firstTokenHolder,
+            firstTokenId
+          )).to.be.bignumber.equal('0');
+
+          expect(await this.token.balanceOf(
+            secondTokenHolder,
+            secondTokenId
+          )).to.be.bignumber.equal('0');
+
+          expect(await this.token.balanceOf(
+            firstTokenHolder,
+            unknownTokenId
+          )).to.be.bignumber.equal('0');
+        });
+      });
+
+      context('when accounts own some tokens', function () {
+        beforeEach(async function () {
+          await this.token.mint(firstTokenHolder, firstTokenId, firstAmount, '0x', {
+            from: minter,
+          });
+          await this.token.mint(
+            secondTokenHolder,
+            secondTokenId,
+            secondAmount,
+            '0x',
+            {
+              from: minter,
+            }
+          );
+        });
+
+        it('returns the amount of tokens owned by the given addresses', async function () {
+          expect(await this.token.balanceOf(
+            firstTokenHolder,
+            firstTokenId
+          )).to.be.bignumber.equal(firstAmount);
+
+          expect(await this.token.balanceOf(
+            secondTokenHolder,
+            secondTokenId
+          )).to.be.bignumber.equal(secondAmount);
+
+          expect(await this.token.balanceOf(
+            firstTokenHolder,
+            unknownTokenId
+          )).to.be.bignumber.equal('0');
+        });
+      });
+    });
+
+    describe('balanceOfBatch', function () {
+      it('reverts when input arrays don\'t match up', async function () {
+        await expectRevert(
+          this.token.balanceOfBatch(
+            [firstTokenHolder, secondTokenHolder, firstTokenHolder, secondTokenHolder],
+            [firstTokenId, secondTokenId, unknownTokenId]
+          ),
+          'ERC1155: accounts and IDs must have same lengths'
+        );
+      });
+
+      it('reverts when one of the addresses is the zero address', async function () {
+        await expectRevert(
+          this.token.balanceOfBatch(
+            [firstTokenHolder, secondTokenHolder, ZERO_ADDRESS],
+            [firstTokenId, secondTokenId, unknownTokenId]
+          ),
+          'ERC1155: some address in batch balance query is zero'
+        );
+      });
+
+      context('when accounts don\'t own tokens', function () {
+        it('returns zeros for each account', async function () {
+          const result = await this.token.balanceOfBatch(
+            [firstTokenHolder, secondTokenHolder, firstTokenHolder],
+            [firstTokenId, secondTokenId, unknownTokenId]
+          );
+          expect(result).to.be.an('array');
+          expect(result[0]).to.be.a.bignumber.equal('0');
+          expect(result[1]).to.be.a.bignumber.equal('0');
+          expect(result[2]).to.be.a.bignumber.equal('0');
+        });
+      });
+
+      context('when accounts own some tokens', function () {
+        beforeEach(async function () {
+          await this.token.mint(firstTokenHolder, firstTokenId, firstAmount, '0x', {
+            from: minter,
+          });
+          await this.token.mint(
+            secondTokenHolder,
+            secondTokenId,
+            secondAmount,
+            '0x',
+            {
+              from: minter,
+            }
+          );
+        });
+
+        it('returns amounts owned by each account in order passed', async function () {
+          const result = await this.token.balanceOfBatch(
+            [secondTokenHolder, firstTokenHolder, firstTokenHolder],
+            [secondTokenId, firstTokenId, unknownTokenId]
+          );
+          expect(result).to.be.an('array');
+          expect(result[0]).to.be.a.bignumber.equal(secondAmount);
+          expect(result[1]).to.be.a.bignumber.equal(firstAmount);
+          expect(result[2]).to.be.a.bignumber.equal('0');
+        });
+      });
+    });
+
+    describe('setApprovalForAll', function () {
+      let logs;
+      beforeEach(async function () {
+        ({ logs } = await this.token.setApprovalForAll(proxy, true, { from: multiTokenHolder }));
+      });
+
+      it('sets approval status which can be queried via isApprovedForAll', async function () {
+        expect(await this.token.isApprovedForAll(multiTokenHolder, proxy)).to.be.equal(true);
+      });
+
+      it('emits an ApprovalForAll log', function () {
+        expectEvent.inLogs(logs, 'ApprovalForAll', { account: multiTokenHolder, operator: proxy, approved: true });
+      });
+
+      it('can unset approval for an operator', async function () {
+        await this.token.setApprovalForAll(proxy, false, { from: multiTokenHolder });
+        expect(await this.token.isApprovedForAll(multiTokenHolder, proxy)).to.be.equal(false);
+      });
+
+      it('reverts if attempting to approve self as an operator', async function () {
+        await expectRevert(
+          this.token.setApprovalForAll(multiTokenHolder, true, { from: multiTokenHolder }),
+          'ERC1155: cannot set approval status for self'
+        );
+      });
+    });
+
+    describe('safeTransferFrom', function () {
+      beforeEach(async function () {
+        await this.token.mint(multiTokenHolder, firstTokenId, firstAmount, '0x', {
+          from: minter,
+        });
+        await this.token.mint(
+          multiTokenHolder,
+          secondTokenId,
+          secondAmount,
+          '0x',
+          {
+            from: minter,
+          }
+        );
+      });
+
+      it('reverts when transferring more than balance', async function () {
+        await expectRevert(
+          this.token.safeTransferFrom(
+            multiTokenHolder,
+            recipient,
+            firstTokenId,
+            firstAmount.addn(1),
+            '0x',
+            { from: multiTokenHolder },
+          ),
+          'ERC1155: insufficient balance for transfer'
+        );
+      });
+
+      it('reverts when transferring to zero address', async function () {
+        await expectRevert(
+          this.token.safeTransferFrom(
+            multiTokenHolder,
+            ZERO_ADDRESS,
+            firstTokenId,
+            firstAmount,
+            '0x',
+            { from: multiTokenHolder },
+          ),
+          'ERC1155: target address must be non-zero'
+        );
+      });
+
+      function transferWasSuccessful ({ operator, from, id, value }) {
+        it('debits transferred balance from sender', async function () {
+          const newBalance = await this.token.balanceOf(from, id);
+          expect(newBalance).to.be.a.bignumber.equal('0');
+        });
+
+        it('credits transferred balance to receiver', async function () {
+          const newBalance = await this.token.balanceOf(this.toWhom, id);
+          expect(newBalance).to.be.a.bignumber.equal(value);
+        });
+
+        it('emits a TransferSingle log', function () {
+          expectEvent.inLogs(this.transferLogs, 'TransferSingle', {
+            operator,
+            from,
+            to: this.toWhom,
+            id,
+            value,
+          });
+        });
+      }
+
+      context('when called by the multiTokenHolder', async function () {
+        beforeEach(async function () {
+          this.toWhom = recipient;
+          ({ logs: this.transferLogs } =
+            await this.token.safeTransferFrom(multiTokenHolder, recipient, firstTokenId, firstAmount, '0x', {
+              from: multiTokenHolder,
+            }));
+        });
+
+        transferWasSuccessful.call(this, {
+          operator: multiTokenHolder,
+          from: multiTokenHolder,
+          id: firstTokenId,
+          value: firstAmount,
+        });
+
+        it('preserves existing balances which are not transferred by multiTokenHolder', async function () {
+          const balance1 = await this.token.balanceOf(multiTokenHolder, secondTokenId);
+          expect(balance1).to.be.a.bignumber.equal(secondAmount);
+
+          const balance2 = await this.token.balanceOf(recipient, secondTokenId);
+          expect(balance2).to.be.a.bignumber.equal('0');
+        });
+      });
+
+      context('when called by an operator on behalf of the multiTokenHolder', function () {
+        context('when operator is not approved by multiTokenHolder', function () {
+          beforeEach(async function () {
+            await this.token.setApprovalForAll(proxy, false, { from: multiTokenHolder });
+          });
+
+          it('reverts', async function () {
+            await expectRevert(
+              this.token.safeTransferFrom(multiTokenHolder, recipient, firstTokenId, firstAmount, '0x', {
+                from: proxy,
+              }),
+              'ERC1155: need operator approval for 3rd party transfers'
+            );
+          });
+        });
+
+        context('when operator is approved by multiTokenHolder', function () {
+          beforeEach(async function () {
+            this.toWhom = recipient;
+            await this.token.setApprovalForAll(proxy, true, { from: multiTokenHolder });
+            ({ logs: this.transferLogs } =
+              await this.token.safeTransferFrom(multiTokenHolder, recipient, firstTokenId, firstAmount, '0x', {
+                from: proxy,
+              }));
+          });
+
+          transferWasSuccessful.call(this, {
+            operator: proxy,
+            from: multiTokenHolder,
+            id: firstTokenId,
+            value: firstAmount,
+          });
+
+          it('preserves operator\'s balances not involved in the transfer', async function () {
+            const balance = await this.token.balanceOf(proxy, firstTokenId);
+            expect(balance).to.be.a.bignumber.equal('0');
+          });
+        });
+      });
+
+      context('when sending to a valid receiver', function () {
+        beforeEach(async function () {
+          this.receiver = await ERC1155ReceiverMock.new(
+            RECEIVER_SINGLE_MAGIC_VALUE, false,
+            RECEIVER_BATCH_MAGIC_VALUE, false,
+          );
+        });
+
+        context('without data', function () {
+          beforeEach(async function () {
+            this.toWhom = this.receiver.address;
+            this.transferReceipt = await this.token.safeTransferFrom(
+              multiTokenHolder,
+              this.receiver.address,
+              firstTokenId,
+              firstAmount,
+              '0x',
+              { from: multiTokenHolder }
+            );
+            ({ logs: this.transferLogs } = this.transferReceipt);
+          });
+
+          transferWasSuccessful.call(this, {
+            operator: multiTokenHolder,
+            from: multiTokenHolder,
+            id: firstTokenId,
+            value: firstAmount,
+          });
+
+          it('should call onERC1155Received', async function () {
+            await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'Received', {
+              operator: multiTokenHolder,
+              from: multiTokenHolder,
+              id: firstTokenId,
+              value: firstAmount,
+              data: null,
+            });
+          });
+        });
+
+        context('with data', function () {
+          const data = '0xf00dd00d';
+          beforeEach(async function () {
+            this.toWhom = this.receiver.address;
+            this.transferReceipt = await this.token.safeTransferFrom(
+              multiTokenHolder,
+              this.receiver.address,
+              firstTokenId,
+              firstAmount,
+              data,
+              { from: multiTokenHolder }
+            );
+            ({ logs: this.transferLogs } = this.transferReceipt);
+          });
+
+          transferWasSuccessful.call(this, {
+            operator: multiTokenHolder,
+            from: multiTokenHolder,
+            id: firstTokenId,
+            value: firstAmount,
+          });
+
+          it('should call onERC1155Received', async function () {
+            await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'Received', {
+              operator: multiTokenHolder,
+              from: multiTokenHolder,
+              id: firstTokenId,
+              value: firstAmount,
+              data,
+            });
+          });
+        });
+      });
+
+      context('to a receiver contract returning unexpected value', function () {
+        beforeEach(async function () {
+          this.receiver = await ERC1155ReceiverMock.new(
+            '0x00c0ffee', false,
+            RECEIVER_BATCH_MAGIC_VALUE, false,
+          );
+        });
+
+        it('reverts', async function () {
+          await expectRevert(
+            this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', {
+              from: multiTokenHolder,
+            }),
+            'ERC1155: got unknown value from onERC1155Received'
+          );
+        });
+      });
+
+      context('to a receiver contract that reverts', function () {
+        beforeEach(async function () {
+          this.receiver = await ERC1155ReceiverMock.new(
+            RECEIVER_SINGLE_MAGIC_VALUE, true,
+            RECEIVER_BATCH_MAGIC_VALUE, false,
+          );
+        });
+
+        it('reverts', async function () {
+          await expectRevert(
+            this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', {
+              from: multiTokenHolder,
+            }),
+            'ERC1155ReceiverMock: reverting on receive'
+          );
+        });
+      });
+
+      context('to a contract that does not implement the required function', function () {
+        it('reverts', async function () {
+          const invalidReceiver = this.token;
+          await expectRevert.unspecified(
+            this.token.safeTransferFrom(multiTokenHolder, invalidReceiver.address, firstTokenId, firstAmount, '0x', {
+              from: multiTokenHolder,
+            })
+          );
+        });
+      });
+    });
+
+    describe('safeBatchTransferFrom', function () {
+      beforeEach(async function () {
+        await this.token.mint(multiTokenHolder, firstTokenId, firstAmount, '0x', {
+          from: minter,
+        });
+        await this.token.mint(
+          multiTokenHolder,
+          secondTokenId,
+          secondAmount,
+          '0x',
+          {
+            from: minter,
+          }
+        );
+      });
+
+      it('reverts when transferring amount more than any of balances', async function () {
+        await expectRevert(
+          this.token.safeBatchTransferFrom(
+            multiTokenHolder, recipient,
+            [firstTokenId, secondTokenId],
+            [firstAmount, secondAmount.addn(1)],
+            '0x', { from: multiTokenHolder }
+          ),
+          'ERC1155: insufficient balance of some token type for transfer'
+        );
+      });
+
+      it('reverts when ids array length doesn\'t match amounts array length', async function () {
+        await expectRevert(
+          this.token.safeBatchTransferFrom(
+            multiTokenHolder, recipient,
+            [firstTokenId],
+            [firstAmount, secondAmount],
+            '0x', { from: multiTokenHolder }
+          ),
+          'ERC1155: IDs and values must have same lengths'
+        );
+      });
+
+      it('reverts when transferring to zero address', async function () {
+        await expectRevert(
+          this.token.safeBatchTransferFrom(
+            multiTokenHolder, ZERO_ADDRESS,
+            [firstTokenId, secondTokenId],
+            [firstAmount, secondAmount],
+            '0x', { from: multiTokenHolder }
+          ),
+          'ERC1155: target address must be non-zero'
+        );
+      });
+
+      function batchTransferWasSuccessful ({ operator, from, ids, values }) {
+        it('debits transferred balances from sender', async function () {
+          const newBalances = await this.token.balanceOfBatch(new Array(ids.length).fill(from), ids);
+          for (const newBalance of newBalances) {
+            expect(newBalance).to.be.a.bignumber.equal('0');
+          }
+        });
+
+        it('credits transferred balances to receiver', async function () {
+          const newBalances = await this.token.balanceOfBatch(new Array(ids.length).fill(this.toWhom), ids);
+          for (let i = 0; i < newBalances.length; i++) {
+            expect(newBalances[i]).to.be.a.bignumber.equal(values[i]);
+          }
+        });
+
+        it('emits a TransferBatch log', function () {
+          expectEvent.inLogs(this.transferLogs, 'TransferBatch', {
+            operator,
+            from,
+            to: this.toWhom,
+            // ids,
+            // values,
+          });
+        });
+      }
+
+      context('when called by the multiTokenHolder', async function () {
+        beforeEach(async function () {
+          this.toWhom = recipient;
+          ({ logs: this.transferLogs } =
+            await this.token.safeBatchTransferFrom(
+              multiTokenHolder, recipient,
+              [firstTokenId, secondTokenId],
+              [firstAmount, secondAmount],
+              '0x', { from: multiTokenHolder }
+            ));
+        });
+
+        batchTransferWasSuccessful.call(this, {
+          operator: multiTokenHolder,
+          from: multiTokenHolder,
+          ids: [firstTokenId, secondTokenId],
+          values: [firstAmount, secondAmount],
+        });
+      });
+
+      context('when called by an operator on behalf of the multiTokenHolder', function () {
+        context('when operator is not approved by multiTokenHolder', function () {
+          beforeEach(async function () {
+            await this.token.setApprovalForAll(proxy, false, { from: multiTokenHolder });
+          });
+
+          it('reverts', async function () {
+            await expectRevert(
+              this.token.safeBatchTransferFrom(
+                multiTokenHolder, recipient,
+                [firstTokenId, secondTokenId],
+                [firstAmount, secondAmount],
+                '0x', { from: proxy }
+              ),
+              'ERC1155: need operator approval for 3rd party transfers'
+            );
+          });
+        });
+
+        context('when operator is approved by multiTokenHolder', function () {
+          beforeEach(async function () {
+            this.toWhom = recipient;
+            await this.token.setApprovalForAll(proxy, true, { from: multiTokenHolder });
+            ({ logs: this.transferLogs } =
+              await this.token.safeBatchTransferFrom(
+                multiTokenHolder, recipient,
+                [firstTokenId, secondTokenId],
+                [firstAmount, secondAmount],
+                '0x', { from: proxy },
+              ));
+          });
+
+          batchTransferWasSuccessful.call(this, {
+            operator: proxy,
+            from: multiTokenHolder,
+            ids: [firstTokenId, secondTokenId],
+            values: [firstAmount, secondAmount],
+          });
+
+          it('preserves operator\'s balances not involved in the transfer', async function () {
+            const balance1 = await this.token.balanceOf(proxy, firstTokenId);
+            expect(balance1).to.be.a.bignumber.equal('0');
+            const balance2 = await this.token.balanceOf(proxy, secondTokenId);
+            expect(balance2).to.be.a.bignumber.equal('0');
+          });
+        });
+      });
+
+      context('when sending to a valid receiver', function () {
+        beforeEach(async function () {
+          this.receiver = await ERC1155ReceiverMock.new(
+            RECEIVER_SINGLE_MAGIC_VALUE, false,
+            RECEIVER_BATCH_MAGIC_VALUE, false,
+          );
+        });
+
+        context('without data', function () {
+          beforeEach(async function () {
+            this.toWhom = this.receiver.address;
+            this.transferReceipt = await this.token.safeBatchTransferFrom(
+              multiTokenHolder, this.receiver.address,
+              [firstTokenId, secondTokenId],
+              [firstAmount, secondAmount],
+              '0x', { from: multiTokenHolder },
+            );
+            ({ logs: this.transferLogs } = this.transferReceipt);
+          });
+
+          batchTransferWasSuccessful.call(this, {
+            operator: multiTokenHolder,
+            from: multiTokenHolder,
+            ids: [firstTokenId, secondTokenId],
+            values: [firstAmount, secondAmount],
+          });
+
+          it('should call onERC1155BatchReceived', async function () {
+            await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'BatchReceived', {
+              operator: multiTokenHolder,
+              from: multiTokenHolder,
+              // ids: [firstTokenId, secondTokenId],
+              // values: [firstAmount, secondAmount],
+              data: null,
+            });
+          });
+        });
+
+        context('with data', function () {
+          const data = '0xf00dd00d';
+          beforeEach(async function () {
+            this.toWhom = this.receiver.address;
+            this.transferReceipt = await this.token.safeBatchTransferFrom(
+              multiTokenHolder, this.receiver.address,
+              [firstTokenId, secondTokenId],
+              [firstAmount, secondAmount],
+              data, { from: multiTokenHolder },
+            );
+            ({ logs: this.transferLogs } = this.transferReceipt);
+          });
+
+          batchTransferWasSuccessful.call(this, {
+            operator: multiTokenHolder,
+            from: multiTokenHolder,
+            ids: [firstTokenId, secondTokenId],
+            values: [firstAmount, secondAmount],
+          });
+
+          it('should call onERC1155Received', async function () {
+            await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'BatchReceived', {
+              operator: multiTokenHolder,
+              from: multiTokenHolder,
+              // ids: [firstTokenId, secondTokenId],
+              // values: [firstAmount, secondAmount],
+              data,
+            });
+          });
+        });
+      });
+
+      context('to a receiver contract returning unexpected value', function () {
+        beforeEach(async function () {
+          this.receiver = await ERC1155ReceiverMock.new(
+            RECEIVER_SINGLE_MAGIC_VALUE, false,
+            RECEIVER_SINGLE_MAGIC_VALUE, false,
+          );
+        });
+
+        it('reverts', async function () {
+          await expectRevert(
+            this.token.safeBatchTransferFrom(
+              multiTokenHolder, this.receiver.address,
+              [firstTokenId, secondTokenId],
+              [firstAmount, secondAmount],
+              '0x', { from: multiTokenHolder },
+            ),
+            'ERC1155: got unknown value from onERC1155BatchReceived'
+          );
+        });
+      });
+
+      context('to a receiver contract that reverts', function () {
+        beforeEach(async function () {
+          this.receiver = await ERC1155ReceiverMock.new(
+            RECEIVER_SINGLE_MAGIC_VALUE, false,
+            RECEIVER_BATCH_MAGIC_VALUE, true,
+          );
+        });
+
+        it('reverts', async function () {
+          await expectRevert(
+            this.token.safeBatchTransferFrom(
+              multiTokenHolder, this.receiver.address,
+              [firstTokenId, secondTokenId],
+              [firstAmount, secondAmount],
+              '0x', { from: multiTokenHolder },
+            ),
+            'ERC1155ReceiverMock: reverting on batch receive'
+          );
+        });
+      });
+
+      context('to a contract that does not implement the required function', function () {
+        it('reverts', async function () {
+          const invalidReceiver = this.token;
+          await expectRevert.unspecified(
+            this.token.safeBatchTransferFrom(
+              multiTokenHolder, invalidReceiver.address,
+              [firstTokenId, secondTokenId],
+              [firstAmount, secondAmount],
+              '0x', { from: multiTokenHolder },
+            )
+          );
+        });
+      });
+    });
+
+    shouldSupportInterfaces(['ERC165', 'ERC1155']);
+  });
+}
+
+module.exports = {
+  shouldBehaveLikeERC1155,
+};

+ 219 - 0
test/token/ERC1155/ERC1155.test.js

@@ -0,0 +1,219 @@
+const { accounts, contract } = require('@openzeppelin/test-environment');
+
+const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
+const { ZERO_ADDRESS } = constants;
+
+const { expect } = require('chai');
+
+const { shouldBehaveLikeERC1155 } = require('./ERC1155.behavior');
+const ERC1155Mock = contract.fromArtifact('ERC1155Mock');
+
+describe('ERC1155', function () {
+  const [creator, tokenHolder, tokenBatchHolder, ...otherAccounts] = accounts;
+
+  beforeEach(async function () {
+    this.token = await ERC1155Mock.new({ from: creator });
+  });
+
+  shouldBehaveLikeERC1155(otherAccounts);
+
+  describe('internal functions', function () {
+    const tokenId = new BN(1990);
+    const mintAmount = new BN(9001);
+    const burnAmount = new BN(3000);
+
+    const tokenBatchIds = [new BN(2000), new BN(2010), new BN(2020)];
+    const mintAmounts = [new BN(5000), new BN(10000), new BN(42195)];
+    const burnAmounts = [new BN(5000), new BN(9001), new BN(195)];
+
+    const data = '0xcafebabe';
+
+    describe('_mint(address, uint256, uint256, bytes memory)', function () {
+      it('reverts with a null destination address', async function () {
+        await expectRevert(
+          this.token.mint(ZERO_ADDRESS, tokenId, mintAmount, data),
+          'ERC1155: mint to the zero address'
+        );
+      });
+
+      context('with minted tokens', function () {
+        beforeEach(async function () {
+          ({ logs: this.logs } = await this.token.mint(
+            tokenHolder,
+            tokenId,
+            mintAmount,
+            data,
+            { from: creator }
+          ));
+        });
+
+        it('emits a TransferSingle event', function () {
+          expectEvent.inLogs(this.logs, 'TransferSingle', {
+            operator: creator,
+            from: ZERO_ADDRESS,
+            to: tokenHolder,
+            id: tokenId,
+            value: mintAmount,
+          });
+        });
+
+        it('credits the minted amount of tokens', async function () {
+          expect(await this.token.balanceOf(
+            tokenHolder,
+            tokenId
+          )).to.be.bignumber.equal(mintAmount);
+        });
+      });
+    });
+
+    describe('_mintBatch(address, uint256[] memory, uint256[] memory, bytes memory)', function () {
+      it('reverts with a null destination address', async function () {
+        await expectRevert(
+          this.token.mintBatch(ZERO_ADDRESS, tokenBatchIds, mintAmounts, data),
+          'ERC1155: batch mint to the zero address'
+        );
+      });
+
+      it('reverts if length of inputs do not match', async function () {
+        await expectRevert(
+          this.token.mintBatch(tokenBatchHolder, tokenBatchIds, mintAmounts.slice(1), data),
+          'ERC1155: minted IDs and values must have same lengths'
+        );
+      });
+
+      context('with minted batch of tokens', function () {
+        beforeEach(async function () {
+          ({ logs: this.logs } = await this.token.mintBatch(
+            tokenBatchHolder,
+            tokenBatchIds,
+            mintAmounts,
+            data,
+            { from: creator }
+          ));
+        });
+
+        it('emits a TransferBatch event', function () {
+          expectEvent.inLogs(this.logs, 'TransferBatch', {
+            operator: creator,
+            from: ZERO_ADDRESS,
+            to: tokenBatchHolder,
+            // ids: tokenBatchIds,
+            // values: mintAmounts,
+          });
+        });
+
+        it('credits the minted batch of tokens', async function () {
+          const holderBatchBalances = await this.token.balanceOfBatch(
+            new Array(tokenBatchIds.length).fill(tokenBatchHolder),
+            tokenBatchIds
+          );
+
+          for (let i = 0; i < holderBatchBalances.length; i++) {
+            expect(holderBatchBalances[i]).to.be.bignumber.equal(mintAmounts[i]);
+          }
+        });
+      });
+    });
+
+    describe('_burn(address, uint256, uint256)', function () {
+      it('reverts when burning the zero account\'s tokens', async function () {
+        await expectRevert(
+          this.token.burn(ZERO_ADDRESS, tokenId, mintAmount),
+          'ERC1155: attempting to burn tokens on zero account'
+        );
+      });
+
+      it('reverts when burning a non-existent token id', async function () {
+        await expectRevert(
+          this.token.burn(tokenHolder, tokenId, mintAmount),
+          'ERC1155: attempting to burn more than balance'
+        );
+      });
+
+      context('with minted-then-burnt tokens', function () {
+        beforeEach(async function () {
+          await this.token.mint(tokenHolder, tokenId, mintAmount, data);
+          ({ logs: this.logs } = await this.token.burn(
+            tokenHolder,
+            tokenId,
+            burnAmount,
+            { from: creator }
+          ));
+        });
+
+        it('emits a TransferSingle event', function () {
+          expectEvent.inLogs(this.logs, 'TransferSingle', {
+            operator: creator,
+            from: tokenHolder,
+            to: ZERO_ADDRESS,
+            id: tokenId,
+            value: burnAmount,
+          });
+        });
+
+        it('accounts for both minting and burning', async function () {
+          expect(await this.token.balanceOf(
+            tokenHolder,
+            tokenId
+          )).to.be.bignumber.equal(mintAmount.sub(burnAmount));
+        });
+      });
+    });
+
+    describe('_burnBatch(address, uint256[] memory, uint256[] memory)', function () {
+      it('reverts when burning the zero account\'s tokens', async function () {
+        await expectRevert(
+          this.token.burnBatch(ZERO_ADDRESS, tokenBatchIds, burnAmounts),
+          'ERC1155: attempting to burn batch of tokens on zero account'
+        );
+      });
+
+      it('reverts if length of inputs do not match', async function () {
+        await expectRevert(
+          this.token.burnBatch(tokenBatchHolder, tokenBatchIds, burnAmounts.slice(1)),
+          'ERC1155: burnt IDs and values must have same lengths'
+        );
+      });
+
+      it('reverts when burning a non-existent token id', async function () {
+        await expectRevert(
+          this.token.burnBatch(tokenBatchHolder, tokenBatchIds, burnAmounts),
+          'ERC1155: attempting to burn more than balance for some token'
+        );
+      });
+
+      context('with minted-then-burnt tokens', function () {
+        beforeEach(async function () {
+          await this.token.mintBatch(tokenBatchHolder, tokenBatchIds, mintAmounts, data);
+          ({ logs: this.logs } = await this.token.burnBatch(
+            tokenBatchHolder,
+            tokenBatchIds,
+            burnAmounts,
+            { from: creator }
+          ));
+        });
+
+        it('emits a TransferBatch event', function () {
+          expectEvent.inLogs(this.logs, 'TransferBatch', {
+            operator: creator,
+            from: tokenBatchHolder,
+            to: ZERO_ADDRESS,
+            // ids: tokenBatchIds,
+            // values: burnAmounts,
+          });
+        });
+
+        it('accounts for both minting and burning', async function () {
+          const holderBatchBalances = await this.token.balanceOfBatch(
+            new Array(tokenBatchIds.length).fill(tokenBatchHolder),
+            tokenBatchIds
+          );
+
+          for (let i = 0; i < holderBatchBalances.length; i++) {
+            expect(holderBatchBalances[i]).to.be.bignumber.equal(mintAmounts[i].sub(burnAmounts[i]));
+          }
+        });
+      });
+    });
+  });
+});

+ 46 - 0
test/token/ERC1155/ERC1155Holder.test.js

@@ -0,0 +1,46 @@
+const { accounts, contract } = require('@openzeppelin/test-environment');
+const { BN } = require('@openzeppelin/test-helpers');
+
+const ERC1155Holder = contract.fromArtifact('ERC1155Holder');
+const ERC1155Mock = contract.fromArtifact('ERC1155Mock');
+
+const { expect } = require('chai');
+
+describe('ERC1155Holder', function () {
+  const [creator] = accounts;
+
+  it('receives ERC1155 tokens', async function () {
+    const multiToken = await ERC1155Mock.new({ from: creator });
+    const multiTokenIds = [new BN(1), new BN(2), new BN(3)];
+    const multiTokenAmounts = [new BN(1000), new BN(2000), new BN(3000)];
+    await multiToken.mintBatch(creator, multiTokenIds, multiTokenAmounts, '0x', { from: creator });
+
+    const transferData = '0xf00dbabe';
+
+    const holder = await ERC1155Holder.new();
+
+    await multiToken.safeTransferFrom(
+      creator,
+      holder.address,
+      multiTokenIds[0],
+      multiTokenAmounts[0],
+      transferData,
+      { from: creator },
+    );
+
+    expect(await multiToken.balanceOf(holder.address, multiTokenIds[0])).to.be.bignumber.equal(multiTokenAmounts[0]);
+
+    await multiToken.safeBatchTransferFrom(
+      creator,
+      holder.address,
+      multiTokenIds.slice(1),
+      multiTokenAmounts.slice(1),
+      transferData,
+      { from: creator },
+    );
+
+    for (let i = 1; i < multiTokenIds.length; i++) {
+      expect(await multiToken.balanceOf(holder.address, multiTokenIds[i])).to.be.bignumber.equal(multiTokenAmounts[i]);
+    }
+  });
+});