diff --git a/contracts/mocks/ERC1155Mock.sol b/contracts/mocks/ERC1155Mock.sol new file mode 100644 index 00000000000..e7b3c549a04 --- /dev/null +++ b/contracts/mocks/ERC1155Mock.sol @@ -0,0 +1,33 @@ +pragma solidity ^0.5.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); + } +} diff --git a/contracts/mocks/ERC1155ReceiverMock.sol b/contracts/mocks/ERC1155ReceiverMock.sol new file mode 100644 index 00000000000..ffbf9a5532d --- /dev/null +++ b/contracts/mocks/ERC1155ReceiverMock.sol @@ -0,0 +1,58 @@ +pragma solidity ^0.5.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 + 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 + returns(bytes4) + { + require(!_batReverts, "ERC1155ReceiverMock: reverting on batch receive"); + emit BatchReceived(operator, from, ids, values, data, gasleft()); + return _batRetval; + } +} diff --git a/contracts/token/ERC1155/ERC1155.sol b/contracts/token/ERC1155/ERC1155.sol new file mode 100644 index 00000000000..1e6163f57cc --- /dev/null +++ b/contracts/token/ERC1155/ERC1155.sol @@ -0,0 +1,295 @@ +pragma solidity ^0.5.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; + + constructor() + public + { + _registerInterface( + ERC1155(0).safeTransferFrom.selector ^ + ERC1155(0).safeBatchTransferFrom.selector ^ + ERC1155(0).balanceOf.selector ^ + ERC1155(0).balanceOfBatch.selector ^ + ERC1155(0).setApprovalForAll.selector ^ + ERC1155(0).isApprovedForAll.selector + ); + } + + /** + @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 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 + 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 { + 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 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 + { + 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 + { + 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 { + 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 { + 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 { + 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 { + 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 + { + 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 + { + if(to.isContract()) { + require( + IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, values, data) == + IERC1155Receiver(to).onERC1155BatchReceived.selector, + "ERC1155: got unknown value from onERC1155BatchReceived" + ); + } + } +} diff --git a/contracts/token/ERC1155/ERC1155Holder.sol b/contracts/token/ERC1155/ERC1155Holder.sol new file mode 100644 index 00000000000..465f8b60791 --- /dev/null +++ b/contracts/token/ERC1155/ERC1155Holder.sol @@ -0,0 +1,17 @@ +pragma solidity ^0.5.0; + +import "./ERC1155Receiver.sol"; + +contract ERC1155Holder is ERC1155Receiver { + + function onERC1155Received(address, address, uint256, uint256, bytes calldata) external returns (bytes4) + { + return this.onERC1155Received.selector; + } + + + function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) external returns (bytes4) + { + return this.onERC1155BatchReceived.selector; + } +} diff --git a/contracts/token/ERC1155/ERC1155Receiver.sol b/contracts/token/ERC1155/ERC1155Receiver.sol new file mode 100644 index 00000000000..87c4dcca0b1 --- /dev/null +++ b/contracts/token/ERC1155/ERC1155Receiver.sol @@ -0,0 +1,13 @@ +pragma solidity ^0.5.0; + +import "./IERC1155Receiver.sol"; +import "../../introspection/ERC165.sol"; + +contract ERC1155Receiver is ERC165, IERC1155Receiver { + constructor() public { + _registerInterface( + ERC1155Receiver(0).onERC1155Received.selector ^ + ERC1155Receiver(0).onERC1155BatchReceived.selector + ); + } +} diff --git a/contracts/token/ERC1155/IERC1155.sol b/contracts/token/ERC1155/IERC1155.sol new file mode 100644 index 00000000000..87bb10d9bb9 --- /dev/null +++ b/contracts/token/ERC1155/IERC1155.sol @@ -0,0 +1,29 @@ +pragma solidity ^0.5.0; + +import "../../introspection/IERC165.sol"; + +/** + @title ERC-1155 Multi Token Standard basic interface + @dev See https://eips.ethereum.org/EIPS/eip-1155 + */ +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 returns (uint256); + + function balanceOfBatch(address[] memory accounts, uint256[] memory ids) public view returns (uint256[] memory); + + function setApprovalForAll(address operator, bool approved) external; + + function isApprovedForAll(address account, address operator) external view returns (bool); + + function safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes calldata data) external; + + function safeBatchTransferFrom(address from, address to, uint256[] calldata ids, uint256[] calldata values, bytes calldata data) external; +} diff --git a/contracts/token/ERC1155/IERC1155Receiver.sol b/contracts/token/ERC1155/IERC1155Receiver.sol new file mode 100644 index 00000000000..90f710972a2 --- /dev/null +++ b/contracts/token/ERC1155/IERC1155Receiver.sol @@ -0,0 +1,56 @@ +pragma solidity ^0.5.0; + +import "../../introspection/IERC165.sol"; + +/** + @title ERC-1155 Multi Token Receiver Interface + @dev See https://eips.ethereum.org/EIPS/eip-1155 +*/ +contract 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); +} diff --git a/contracts/token/ERC1155/README.md b/contracts/token/ERC1155/README.md new file mode 100644 index 00000000000..c729f69688d --- /dev/null +++ b/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`. diff --git a/test/introspection/SupportsInterface.behavior.js b/test/introspection/SupportsInterface.behavior.js index ce9b7e1f536..83b1511b023 100644 --- a/test/introspection/SupportsInterface.behavior.js +++ b/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 = {}; diff --git a/test/token/ERC1155/ERC1155.behavior.js b/test/token/ERC1155/ERC1155.behavior.js new file mode 100644 index 00000000000..e0700d5e6f8 --- /dev/null +++ b/test/token/ERC1155/ERC1155.behavior.js @@ -0,0 +1,703 @@ +const { BN, constants, expectEvent, expectRevert } = require('openzeppelin-test-helpers'); +const { ZERO_ADDRESS } = constants; +const { shouldSupportInterfaces } = require('../../introspection/SupportsInterface.behavior'); + +const ERC1155ReceiverMock = artifacts.require('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 () { + (await this.token.balanceOf( + firstTokenHolder, + firstTokenId + )).should.be.bignumber.equal('0'); + + (await this.token.balanceOf( + secondTokenHolder, + secondTokenId + )).should.be.bignumber.equal('0'); + + (await this.token.balanceOf( + firstTokenHolder, + unknownTokenId + )).should.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 () { + (await this.token.balanceOf( + firstTokenHolder, + firstTokenId + )).should.be.bignumber.equal(firstAmount); + + (await this.token.balanceOf( + secondTokenHolder, + secondTokenId + )).should.be.bignumber.equal(secondAmount); + + (await this.token.balanceOf( + firstTokenHolder, + unknownTokenId + )).should.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] + ); + result.should.be.an('array'); + result[0].should.be.a.bignumber.equal('0'); + result[1].should.be.a.bignumber.equal('0'); + result[2].should.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] + ); + result.should.be.an('array'); + result[0].should.be.a.bignumber.equal(secondAmount); + result[1].should.be.a.bignumber.equal(firstAmount); + result[2].should.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 () { + (await this.token.isApprovedForAll(multiTokenHolder, proxy)).should.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 }); + (await this.token.isApprovedForAll(multiTokenHolder, proxy)).should.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); + newBalance.should.be.a.bignumber.equal('0'); + }); + + it('credits transferred balance to receiver', async function () { + const newBalance = await this.token.balanceOf(this.toWhom, id); + newBalance.should.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); + balance1.should.be.a.bignumber.equal(secondAmount); + + const balance2 = await this.token.balanceOf(recipient, secondTokenId); + balance2.should.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); + balance.should.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) { + newBalance.should.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++) { + newBalances[i].should.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); + balance1.should.be.a.bignumber.equal('0'); + const balance2 = await this.token.balanceOf(proxy, secondTokenId); + balance2.should.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, +}; diff --git a/test/token/ERC1155/ERC1155.test.js b/test/token/ERC1155/ERC1155.test.js new file mode 100644 index 00000000000..71c62b5301f --- /dev/null +++ b/test/token/ERC1155/ERC1155.test.js @@ -0,0 +1,218 @@ +const { + BN, + constants, + expectEvent, + expectRevert, +} = require('openzeppelin-test-helpers'); +const { ZERO_ADDRESS } = constants; + +const { shouldBehaveLikeERC1155 } = require('./ERC1155.behavior'); +const ERC1155Mock = artifacts.require('ERC1155Mock'); + +contract('ERC1155', function ([, creator, tokenHolder, tokenBatchHolder, ...accounts]) { + beforeEach(async function () { + this.token = await ERC1155Mock.new({ from: creator }); + }); + + shouldBehaveLikeERC1155(accounts); + + 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 () { + (await this.token.balanceOf( + tokenHolder, + tokenId + )).should.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++) { + holderBatchBalances[i].should.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 () { + (await this.token.balanceOf( + tokenHolder, + tokenId + )).should.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++) { + holderBatchBalances[i].should.be.bignumber.equal(mintAmounts[i].sub(burnAmounts[i])); + } + }); + }); + }); + }); +}); diff --git a/test/token/ERC1155/ERC1155Holder.test.js b/test/token/ERC1155/ERC1155Holder.test.js new file mode 100644 index 00000000000..de2b909c49c --- /dev/null +++ b/test/token/ERC1155/ERC1155Holder.test.js @@ -0,0 +1,41 @@ +const { BN } = require('openzeppelin-test-helpers'); + +const ERC1155Holder = artifacts.require('ERC1155Holder'); +const ERC1155Mock = artifacts.require('ERC1155Mock'); + +contract('ERC1155Holder', function ([creator]) { + 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 }, + ); + + (await multiToken.balanceOf(holder.address, multiTokenIds[0])).should.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++) { + (await multiToken.balanceOf(holder.address, multiTokenIds[i])).should.be.bignumber.equal(multiTokenAmounts[i]); + } + }); +});