From 7c850cc0827ba12351135a4bc1f89d23aa091fa0 Mon Sep 17 00:00:00 2001 From: Greg Hysen Date: Wed, 27 Feb 2019 09:55:43 -0800 Subject: [PATCH] Basic ERC1155 Implementation. Derived from reference implementation, with bug fixes. --- .gitignore | 2 + contracts/erc1155/.solhintignore | 3 + contracts/erc1155/CHANGELOG.json | 11 + contracts/erc1155/CHANGELOG.md | 6 + contracts/erc1155/DEPLOYS.json | 1 + contracts/erc1155/README.md | 73 ++++ contracts/erc1155/compiler.json | 28 ++ contracts/erc1155/contracts/src/ERC1155.sol | 161 ++++++++ .../erc1155/contracts/src/ERC1155Mintable.sol | 98 +++++ .../contracts/src/interfaces/IERC1155.sol | 124 +++++++ .../src/interfaces/IERC1155Receiver.sol | 55 +++ .../contracts/test/DummyERC1155Receiver.sol | 75 ++++ .../contracts/test/DummyERC1155Token.sol | 40 ++ .../contracts/test/InvalidERC1155Receiver.sol | 68 ++++ contracts/erc1155/package.json | 82 +++++ contracts/erc1155/src/artifacts.ts | 23 ++ contracts/erc1155/src/index.ts | 2 + contracts/erc1155/src/wrappers.ts | 12 + contracts/erc1155/test/erc1155_token.ts | 347 ++++++++++++++++++ contracts/erc1155/test/global_hooks.ts | 17 + contracts/erc1155/tsconfig.json | 15 + contracts/erc1155/tslint.json | 6 + 22 files changed, 1249 insertions(+) create mode 100644 contracts/erc1155/.solhintignore create mode 100644 contracts/erc1155/CHANGELOG.json create mode 100644 contracts/erc1155/CHANGELOG.md create mode 100644 contracts/erc1155/DEPLOYS.json create mode 100644 contracts/erc1155/README.md create mode 100644 contracts/erc1155/compiler.json create mode 100644 contracts/erc1155/contracts/src/ERC1155.sol create mode 100644 contracts/erc1155/contracts/src/ERC1155Mintable.sol create mode 100644 contracts/erc1155/contracts/src/interfaces/IERC1155.sol create mode 100644 contracts/erc1155/contracts/src/interfaces/IERC1155Receiver.sol create mode 100644 contracts/erc1155/contracts/test/DummyERC1155Receiver.sol create mode 100644 contracts/erc1155/contracts/test/DummyERC1155Token.sol create mode 100644 contracts/erc1155/contracts/test/InvalidERC1155Receiver.sol create mode 100644 contracts/erc1155/package.json create mode 100644 contracts/erc1155/src/artifacts.ts create mode 100644 contracts/erc1155/src/index.ts create mode 100644 contracts/erc1155/src/wrappers.ts create mode 100644 contracts/erc1155/test/erc1155_token.ts create mode 100644 contracts/erc1155/test/global_hooks.ts create mode 100644 contracts/erc1155/tsconfig.json create mode 100644 contracts/erc1155/tslint.json diff --git a/.gitignore b/.gitignore index c1c1bc29d2..203d8ad811 100644 --- a/.gitignore +++ b/.gitignore @@ -91,6 +91,7 @@ contracts/utils/generated-artifacts/ contracts/exchange-libs/generated-artifacts/ contracts/erc20/generated-artifacts/ contracts/erc721/generated-artifacts/ +contracts/erc1155/generated-artifacts/ contracts/extensions/generated-artifacts/ contracts/exchange-forwarder/generated-artifacts/ packages/sol-tracing-utils/test/fixtures/artifacts/ @@ -106,6 +107,7 @@ contracts/utils/generated-wrappers/ contracts/exchange-libs/generated-wrappers/ contracts/erc20/generated-wrappers/ contracts/erc721/generated-wrappers/ +contracts/erc1155/generated-wrappers/ contracts/extensions/generated-wrappers/ contracts/exchange-forwarder/generated-wrappers/ packages/metacoin/src/contract_wrappers diff --git a/contracts/erc1155/.solhintignore b/contracts/erc1155/.solhintignore new file mode 100644 index 0000000000..1e33ec53ba --- /dev/null +++ b/contracts/erc1155/.solhintignore @@ -0,0 +1,3 @@ +contracts/tokens/ZRXToken/ERC20Token_v1.sol +contracts/tokens/ZRXToken/Token_v1.sol +contracts/tokens/ZRXToken/UnlimitedAllowanceToken_v1.sol diff --git a/contracts/erc1155/CHANGELOG.json b/contracts/erc1155/CHANGELOG.json new file mode 100644 index 0000000000..9bf27d911c --- /dev/null +++ b/contracts/erc1155/CHANGELOG.json @@ -0,0 +1,11 @@ +[ + { + "version": "1.0.0", + "changes": [ + { + "note": "Created ERC1155 contracts package", + "pr": 0 + } + ] + } +] diff --git a/contracts/erc1155/CHANGELOG.md b/contracts/erc1155/CHANGELOG.md new file mode 100644 index 0000000000..1004e94271 --- /dev/null +++ b/contracts/erc1155/CHANGELOG.md @@ -0,0 +1,6 @@ + + +CHANGELOG diff --git a/contracts/erc1155/DEPLOYS.json b/contracts/erc1155/DEPLOYS.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/contracts/erc1155/DEPLOYS.json @@ -0,0 +1 @@ +[] diff --git a/contracts/erc1155/README.md b/contracts/erc1155/README.md new file mode 100644 index 0000000000..1ab7f991eb --- /dev/null +++ b/contracts/erc1155/README.md @@ -0,0 +1,73 @@ +## ERC1155 Tokens + +This package contains implementations of various [ERC1155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1155.md) tokens. Addresses of the deployed contracts can be found in the 0x [wiki](https://0xproject.com/wiki#Deployed-Addresses) or the [DEPLOYS](./DEPLOYS.json) file within this package. + +## Installation + +**Install** + +```bash +npm install @0x/contracts-erc1155 --save +``` + +## Bug bounty + +A bug bounty for the 2.0.0 contracts is ongoing! Instructions can be found [here](https://0xproject.com/wiki#Bug-Bounty). + +## Contributing + +We strongly recommend that the community help us make improvements and determine the future direction of the protocol. To report bugs within this package, please create an issue in this repository. + +For proposals regarding the 0x protocol's smart contract architecture, message format, or additional functionality, go to the [0x Improvement Proposals (ZEIPs)](https://github.com/0xProject/ZEIPs) repository and follow the contribution guidelines provided therein. + +Please read our [contribution guidelines](../../CONTRIBUTING.md) before getting started. + +### Install Dependencies + +If you don't have yarn workspaces enabled (Yarn < v1.0) - enable them: + +```bash +yarn config set workspaces-experimental true +``` + +Then install dependencies + +```bash +yarn install +``` + +### Build + +To build this package and all other monorepo packages that it depends on, run the following from the monorepo root directory: + +```bash +PKG=@0x/contracts-erc1155 yarn build +``` + +Or continuously rebuild on change: + +```bash +PKG=@0x/contracts-erc1155 yarn watch +``` + +### Clean + +```bash +yarn clean +``` + +### Lint + +```bash +yarn lint +``` + +### Run Tests + +```bash +yarn test +``` + +#### Testing options + +Contracts testing options like coverage, profiling, revert traces or backing node choosing - are described [here](../TESTING.md). diff --git a/contracts/erc1155/compiler.json b/contracts/erc1155/compiler.json new file mode 100644 index 0000000000..1ccae4dc54 --- /dev/null +++ b/contracts/erc1155/compiler.json @@ -0,0 +1,28 @@ +{ + "artifactsDir": "generated-artifacts", + "contractsDir": "contracts", + "useDockerisedSolc": true, + "compilerSettings": { + "optimizer": { "enabled": true, "runs": 1000000 }, + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap" + ] + } + } + }, + "contracts": [ + "src/ERC1155.sol", + "src/ERC1155Mintable.sol", + "src/interfaces/IERC1155.sol", + "src/interfaces/IERC1155Receiver.sol", + "test/DummyERC1155Receiver.sol", + "test/DummyERC1155Token.sol", + "test/InvalidERC1155Receiver.sol" + ] +} diff --git a/contracts/erc1155/contracts/src/ERC1155.sol b/contracts/erc1155/contracts/src/ERC1155.sol new file mode 100644 index 0000000000..9375a0dc30 --- /dev/null +++ b/contracts/erc1155/contracts/src/ERC1155.sol @@ -0,0 +1,161 @@ +pragma solidity ^0.5.3; + +import "./lib/SafeMath.sol"; +import "./lib/Address.sol"; +import "./interfaces/IERC1155.sol"; +import "./interfaces/IERC1155Receiver.sol"; + + +// A sample implementation of core ERC1155 function. +contract ERC1155 is + IERC1155, + SafeMath +{ + using Address for address; + + bytes4 constant public ERC1155_RECEIVED = 0xf23a6e61; + bytes4 constant public ERC1155_BATCH_RECEIVED = 0xbc197c81; + + // id => (owner => balance) + mapping (uint256 => mapping(address => uint256)) internal balances; + + // owner => (operator => approved) + mapping (address => mapping(address => bool)) internal operatorApproval; + + // Use a split bit implementation. + // Store the type in the upper 128 bits.. + uint256 constant TYPE_MASK = uint256(uint128(~0)) << 128; + + // ..and the non-fungible index in the lower 128 + uint256 constant NF_INDEX_MASK = uint128(~0); + + // The top bit is a flag to tell if this is a NFI. + uint256 constant TYPE_NF_BIT = 1 << 255; + + mapping (uint256 => address) nfOwners; + + // Only to make code clearer. Should not be functions + function isNonFungible(uint256 _id) public pure returns(bool) { + return _id & TYPE_NF_BIT == TYPE_NF_BIT; + } + function isFungible(uint256 _id) public pure returns(bool) { + return _id & TYPE_NF_BIT == 0; + } + function getNonFungibleIndex(uint256 _id) public pure returns(uint256) { + return _id & NF_INDEX_MASK; + } + function getNonFungibleBaseType(uint256 _id) public pure returns(uint256) { + return _id & TYPE_MASK; + } + function isNonFungibleBaseType(uint256 _id) public pure returns(bool) { + // A base type has the NF bit but does not have an index. + return (_id & TYPE_NF_BIT == TYPE_NF_BIT) && (_id & NF_INDEX_MASK == 0); + } + function isNonFungibleItem(uint256 _id) public pure returns(bool) { + // A base type has the NF bit but does has an index. + return (_id & TYPE_NF_BIT == TYPE_NF_BIT) && (_id & NF_INDEX_MASK != 0); + } + function ownerOf(uint256 _id) public view returns (address) { + return nfOwners[_id]; + } + + // overide + function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) external { + + require(_to != address(0x0), "cannot send to zero address"); + require(_from == msg.sender || operatorApproval[_from][msg.sender] == true, "Need operator approval for 3rd party transfers."); + + if (isNonFungible(_id)) { + require(nfOwners[_id] == _from); + nfOwners[_id] = _to; + // You could keep balance of NF type in base type id like so: + // uint256 baseType = getNonFungibleBaseType(_id); + // balances[baseType][_from] = balances[baseType][_from].safeSub(_value); + // balances[baseType][_to] = balances[baseType][_to].safeAdd(_value); + } else { + balances[_id][_from] = safeSub(balances[_id][_from], _value); + balances[_id][_to] = safeAdd(balances[_id][_to], _value); + } + + emit TransferSingle(msg.sender, _from, _to, _id, _value); + + if (_to.isContract()) { + require(IERC1155Receiver(_to).onERC1155Received(msg.sender, _from, _id, _value, _data) == ERC1155_RECEIVED); + } + } + + // overide + function safeBatchTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) external { + + require(_to != address(0x0), "cannot send to zero address"); + require(_ids.length == _values.length, "Array length must match"); + + // Only supporting a global operator approval allows us to do only 1 check and not to touch storage to handle allowances. + require(_from == msg.sender || operatorApproval[_from][msg.sender] == true, "Need operator approval for 3rd party transfers."); + + for (uint256 i = 0; i < _ids.length; ++i) { + // Cache value to local variable to reduce read costs. + uint256 id = _ids[i]; + uint256 value = _values[i]; + + if (isNonFungible(id)) { + require(nfOwners[id] == _from); + nfOwners[id] = _to; + } else { + balances[id][_from] = safeSub(balances[id][_from], value); + balances[id][_to] = safeAdd(value, balances[id][_to]); + } + } + + emit TransferBatch(msg.sender, _from, _to, _ids, _values); + + if (_to.isContract()) { + require(IERC1155Receiver(_to).onERC1155BatchReceived(msg.sender, _from, _ids, _values, _data) == ERC1155_BATCH_RECEIVED); + } + } + + function balanceOf(address _owner, uint256 _id) external view returns (uint256) { + if (isNonFungibleItem(_id)) + return nfOwners[_id] == _owner ? 1 : 0; + return balances[_id][_owner]; + } + + function balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids) external view returns (uint256[] memory) { + + require(_owners.length == _ids.length); + + uint256[] memory balances_ = new uint256[](_owners.length); + + for (uint256 i = 0; i < _owners.length; ++i) { + uint256 id = _ids[i]; + if (isNonFungibleItem(id)) { + balances_[i] = nfOwners[id] == _owners[i] ? 1 : 0; + } else { + balances_[i] = balances[id][_owners[i]]; + } + } + + return balances_; + } + + /** + @notice Enable or disable approval for a third party ("operator") to manage all of the caller's tokens. + @dev MUST emit the ApprovalForAll event on success. + @param _operator address to safeAdd to the set of authorized operators + @param _approved True if the operator is approved, false to revoke approval + */ + function setApprovalForAll(address _operator, bool _approved) external { + operatorApproval[msg.sender][_operator] = _approved; + emit ApprovalForAll(msg.sender, _operator, _approved); + } + + /** + @notice Queries the approval status of an operator for a given owner. + @param _owner The owner of the Tokens + @param _operator address of authorized operator + @return True if the operator is approved, false if not + */ + function isApprovedForAll(address _owner, address _operator) external view returns (bool) { + return operatorApproval[_owner][_operator]; + } +} diff --git a/contracts/erc1155/contracts/src/ERC1155Mintable.sol b/contracts/erc1155/contracts/src/ERC1155Mintable.sol new file mode 100644 index 0000000000..62c6dc907a --- /dev/null +++ b/contracts/erc1155/contracts/src/ERC1155Mintable.sol @@ -0,0 +1,98 @@ +pragma solidity ^0.5.3; + +import "./lib/SafeMath.sol"; +import "./ERC1155.sol"; + +/** + @dev Mintable form of ERC1155 + Shows how easy it is to mint new items +*/ +contract ERC1155Mintable is + ERC1155 +{ + + uint256 nonce; + mapping (uint256 => address) public creators; + mapping (uint256 => uint256) public maxIndex; + + modifier creatorOnly(uint256 _id) { + require(creators[_id] == msg.sender); + _; + } + + // This function only creates the type. + function create( + string calldata _uri, + bool _isNF + ) + external + returns (uint256 _type) + { + // Store the type in the upper 128 bits + _type = (++nonce << 128); + + // Set a flag if this is an NFI. + if (_isNF) + _type = _type | TYPE_NF_BIT; + + // This will allow restricted access to creators. + creators[_type] = msg.sender; + + // emit a Transfer event with Create semantic to help with discovery. + emit TransferSingle(msg.sender, address(0x0), address(0x0), _type, 0); + + if (bytes(_uri).length > 0) + emit URI(_uri, _type); + } + + function mintNonFungible(uint256 _type, address[] calldata _to) external creatorOnly(_type) { + + // No need to check this is a nf type rather than an id since + // creatorOnly() will only let a type pass through. + require(isNonFungible(_type)); + + // Index are 1-based. + uint256 index = maxIndex[_type] + 1; + + for (uint256 i = 0; i < _to.length; ++i) { + address dst = _to[i]; + uint256 id = _type | index + i; + + nfOwners[id] = dst; + + // You could use base-type id to store NF type balances if you wish. + // balances[_type][dst] = quantity.safeAdd(balances[_type][dst]); + + emit TransferSingle(msg.sender, address(0x0), dst, id, 1); + + if (dst.isContract()) { + require(IERC1155Receiver(dst).onERC1155Received(msg.sender, msg.sender, id, 1, '') == ERC1155_RECEIVED); + } + } + + maxIndex[_type] = safeAdd(_to.length, maxIndex[_type]); + } + + function mintFungible(uint256 _id, address[] calldata _to, uint256[] calldata _quantities) external creatorOnly(_id) { + + require(isFungible(_id)); + + for (uint256 i = 0; i < _to.length; ++i) { + + address to = _to[i]; + uint256 quantity = _quantities[i]; + + // Grant the items to the caller + balances[_id][to] = safeAdd(quantity, balances[_id][to]); + + // Emit the Transfer/Mint event. + // the 0x0 source address implies a mint + // It will also provide the circulating supply info. + emit TransferSingle(msg.sender, address(0x0), to, _id, quantity); + + if (to.isContract()) { + require(IERC1155Receiver(to).onERC1155Received(msg.sender, msg.sender, _id, quantity, '') == ERC1155_RECEIVED); + } + } + } +} diff --git a/contracts/erc1155/contracts/src/interfaces/IERC1155.sol b/contracts/erc1155/contracts/src/interfaces/IERC1155.sol new file mode 100644 index 0000000000..f60fb92542 --- /dev/null +++ b/contracts/erc1155/contracts/src/interfaces/IERC1155.sol @@ -0,0 +1,124 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.3; + + +/** + @title ERC-1155 Multi Token Standard + @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1155.md + Note: The ERC-165 identifier for this interface is 0xd9b67a26. + */ +interface IERC1155 { + /** + @dev Either TransferSingle or TransferBatch MUST emit when tokens are transferred, including zero value transfers as well as minting or burning. + Operator will always be msg.sender. + Either event from address `0x0` signifies a minting operation. + An event to address `0x0` signifies a burning or melting operation. + The total value transferred from address 0x0 minus the total value transferred to 0x0 may be used by clients and exchanges to be added to the "circulating supply" for a given token ID. + To define a token ID with no initial balance, the contract SHOULD emit the TransferSingle event from `0x0` to `0x0`, with the token creator as `_operator`. + */ + event TransferSingle(address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value); + + /** + @dev Either TransferSingle or TransferBatch MUST emit when tokens are transferred, including zero value transfers as well as minting or burning. + Operator will always be msg.sender. + Either event from address `0x0` signifies a minting operation. + An event to address `0x0` signifies a burning or melting operation. + The total value transferred from address 0x0 minus the total value transferred to 0x0 may be used by clients and exchanges to be added to the "circulating supply" for a given token ID. + To define multiple token IDs with no initial balance, this SHOULD emit the TransferBatch event from `0x0` to `0x0`, with the token creator as `_operator`. + */ + event TransferBatch(address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values); + + /** + @dev MUST emit when an approval is updated. + */ + event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); + + /** + @dev MUST emit when the URI is updated for a token ID. + URIs are defined in RFC 3986. + The URI MUST point a JSON file that conforms to the "ERC-1155 Metadata JSON Schema". + */ + event URI(string _value, uint256 indexed _id); + + /** + @notice Transfers value amount of an _id from the _from address to the _to address specified. + @dev MUST emit TransferSingle event on success. + Caller must be approved to manage the _from account's tokens (see isApprovedForAll). + MUST throw if `_to` is the zero address. + MUST throw if balance of sender for token `_id` is lower than the `_value` sent. + MUST throw on any other error. + When transfer is complete, this function MUST check if `_to` is a smart contract (code size > 0). If so, it MUST call `onERC1155Received` on `_to` and revert if the return value is not `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`. + @param _from Source address + @param _to Target address + @param _id ID of the token type + @param _value Transfer amount + @param _data Additional data with no specified format, sent in call to `_to` + */ + function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) external; + + /** + @notice Send multiple types of Tokens from a 3rd party in one transfer (with safety call). + @dev MUST emit TransferBatch event on success. + Caller must be approved to manage the _from account's tokens (see isApprovedForAll). + MUST throw if `_to` is the zero address. + MUST throw if length of `_ids` is not the same as length of `_values`. + MUST throw if any of the balance of sender for token `_ids` is lower than the respective `_values` sent. + MUST throw on any other error. + When transfer is complete, this function MUST check if `_to` is a smart contract (code size > 0). If so, it MUST call `onERC1155BatchReceived` on `_to` and revert if the return value is not `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`. + @param _from Source addresses + @param _to Target addresses + @param _ids IDs of each token type + @param _values Transfer amounts per token type + @param _data Additional data with no specified format, sent in call to `_to` + */ + function safeBatchTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) external; + + /** + @notice Get the balance of an account's Tokens. + @param _owner The address of the token holder + @param _id ID of the Token + @return The _owner's balance of the Token type requested + */ + function balanceOf(address _owner, uint256 _id) external view returns (uint256); + + /** + @notice Get the balance of multiple account/token pairs + @param _owners The addresses of the token holders + @param _ids ID of the Tokens + @return The _owner's balance of the Token types requested + */ + function balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids) external view returns (uint256[] memory); + + /** + @notice Enable or disable approval for a third party ("operator") to manage all of the caller's tokens. + @dev MUST emit the ApprovalForAll event on success. + @param _operator Address to add to the set of authorized operators + @param _approved True if the operator is approved, false to revoke approval + */ + function setApprovalForAll(address _operator, bool _approved) external; + + /** + @notice Queries the approval status of an operator for a given owner. + @param _owner The owner of the Tokens + @param _operator Address of authorized operator + @return True if the operator is approved, false if not + */ + function isApprovedForAll(address _owner, address _operator) external view returns (bool); +} diff --git a/contracts/erc1155/contracts/src/interfaces/IERC1155Receiver.sol b/contracts/erc1155/contracts/src/interfaces/IERC1155Receiver.sol new file mode 100644 index 0000000000..85c49a2cec --- /dev/null +++ b/contracts/erc1155/contracts/src/interfaces/IERC1155Receiver.sol @@ -0,0 +1,55 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.3; + + +interface IERC1155Receiver { + + /** + @notice Handle the receipt of a single ERC1155 token type + @dev The smart contract calls this function on the recipient + after a `safeTransferFrom`. This function MAY throw to revert and reject the + transfer. Return of other than the magic value MUST result in the + transaction being reverted + Note: the contract address is always the message sender + @param _operator The address which called `safeTransferFrom` function + @param _from The address which previously owned the token + @param _id An array containing the ids of the token being transferred + @param _value An array containing the amount of tokens being transferred + @param _data Additional data with no specified format + @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + */ + function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data) external returns(bytes4); + + /** + @notice Handle the receipt of multiple ERC1155 token types + @dev The smart contract calls this function on the recipient + after a `safeTransferFrom`. This function MAY throw to revert and reject the + transfer. Return of other than the magic value MUST result in the + transaction being reverted + Note: the contract address is always the message sender + @param _operator The address which called `safeTransferFrom` function + @param _from The address which previously owned the token + @param _ids An array containing ids of each token being transferred + @param _values An array containing amounts of each token being transferred + @param _data Additional data with no specified format + @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + */ + function onERC1155BatchReceived(address _operator, address _from, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) external returns(bytes4); +} diff --git a/contracts/erc1155/contracts/test/DummyERC1155Receiver.sol b/contracts/erc1155/contracts/test/DummyERC1155Receiver.sol new file mode 100644 index 0000000000..db88ca63c7 --- /dev/null +++ b/contracts/erc1155/contracts/test/DummyERC1155Receiver.sol @@ -0,0 +1,75 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.3; + +import "../src/interfaces/IERC1155Receiver.sol"; + +contract DummyERC1155Receiver is + IERC1155Receiver +{ + + bytes4 constant public ERC1155_RECEIVED = 0xf23a6e61; + bytes4 constant public ERC1155_BATCH_RECEIVED = 0xbc197c81; + + constructor () public {} + + event TokenReceived( + address operator, + address from, + uint256 tokenId, + uint256 tokenValue, + bytes data + ); + + event BatchTokenReceived( + address operator, + address from, + uint256[] tokenIds, + uint256[] tokenValues, + bytes data + ); + + function onERC1155Received( + address _operator, + address _from, + uint256 _id, + uint256 _value, + bytes calldata _data + ) + external + returns (bytes4) + { + emit TokenReceived(_operator, _from, _id, _value, _data); + return ERC1155_RECEIVED; + } + + function onERC1155BatchReceived( + address _operator, + address _from, + uint256[] calldata _ids, + uint256[] calldata _values, + bytes calldata _data + ) + external + returns (bytes4) + { + emit BatchTokenReceived(_operator, _from, _ids, _values, _data); + return ERC1155_BATCH_RECEIVED; + } +} diff --git a/contracts/erc1155/contracts/test/DummyERC1155Token.sol b/contracts/erc1155/contracts/test/DummyERC1155Token.sol new file mode 100644 index 0000000000..86a001ce8c --- /dev/null +++ b/contracts/erc1155/contracts/test/DummyERC1155Token.sol @@ -0,0 +1,40 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.3; + +import "../src/ERC1155Mintable.sol"; + +// solhint-disable no-empty-blocks +contract DummyERC1155Token is + ERC1155Mintable +{ + + string public name; + string public symbol; + + constructor ( + string memory _name, + string memory _symbol + ) + public + { + name = _name; + symbol = _symbol; + } +} diff --git a/contracts/erc1155/contracts/test/InvalidERC1155Receiver.sol b/contracts/erc1155/contracts/test/InvalidERC1155Receiver.sol new file mode 100644 index 0000000000..379ff97345 --- /dev/null +++ b/contracts/erc1155/contracts/test/InvalidERC1155Receiver.sol @@ -0,0 +1,68 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.3; + + +contract InvalidERC1155Receiver //is + //IERC1155Receiver +{ + /* + // Actual function signature is `onERC1155Received(address,address,uint256,bytes)` + bytes4 constant internal INVALID_ERC1155_RECEIVED = bytes4(keccak256("onERC1155Received(address,uint256,bytes)")); + + event TokenReceived( + address operator, + address from, + uint256 tokenId, + bytes data + ); + + /// @notice Handle the receipt of an NFT + /// @dev The ERC1155 smart contract calls this function on the recipient + /// after a `transfer`. This function MAY throw to revert and reject the + /// transfer. Return of other than the magic value MUST result in the + /// transaction being reverted. + /// Note: the contract address is always the message sender. + /// @param _operator The address which called `safeTransferFrom` function + /// @param _from The address which previously owned the token + /// @param _tokenId The NFT identifier which is being transferred + /// @param _data Additional data with no specified format + /// @return `bytes4(keccak256("onERC1155Received(address,address,uint256,bytes)"))` + /// unless throwing + function onERC1155Received( + address _operator, + address _from, + uint256 _tokenId, + bytes _data + ) + external + returns (bytes4) + { + emit TokenReceived( + _operator, + _from, + _tokenId, + _data + ); + return INVALID_ERC1155_RECEIVED; + } + */ + +} + diff --git a/contracts/erc1155/package.json b/contracts/erc1155/package.json new file mode 100644 index 0000000000..144726e2e4 --- /dev/null +++ b/contracts/erc1155/package.json @@ -0,0 +1,82 @@ +{ + "name": "@0x/contracts-erc1155", + "version": "1.0.0", + "engines": { + "node": ">=6.12" + }, + "description": "Token contracts used by 0x protocol", + "main": "lib/src/index.js", + "directories": { + "test": "test" + }, + "scripts": { + "build": "yarn pre_build && tsc -b", + "build:ci": "yarn build", + "pre_build": "run-s compile generate_contract_wrappers", + "test": "yarn run_mocha", + "rebuild_and_test": "run-s build test", + "test:coverage": "SOLIDITY_COVERAGE=true run-s build run_mocha coverage:report:text coverage:report:lcov", + "test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html", + "test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha", + "run_mocha": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit", + "compile": "sol-compiler", + "watch": "sol-compiler -w", + "clean": "shx rm -rf lib generated-artifacts generated-wrappers", + "generate_contract_wrappers": "abi-gen --abis ${npm_package_config_abis} --template ../../node_modules/@0x/abi-gen-templates/contract.handlebars --partials '../../node_modules/@0x/abi-gen-templates/partials/**/*.handlebars' --output generated-wrappers --backend ethers", + "lint": "tslint --format stylish --project . --exclude ./generated-wrappers/**/* --exclude ./generated-artifacts/**/* --exclude **/lib/**/* && yarn lint-contracts", + "coverage:report:text": "istanbul report text", + "coverage:report:html": "istanbul report html && open coverage/index.html", + "profiler:report:html": "istanbul report html && open coverage/index.html", + "coverage:report:lcov": "istanbul report lcov", + "test:circleci": "yarn test", + "contracts:gen": "contracts-gen", + "lint-contracts": "solhint -c ../.solhint.json contracts/**/**/**/**/*.sol" + }, + "config": { + "abis": "generated-artifacts/@(DummyERC1155Receiver|DummyERC1155Token|ERC1155|ERC1155Mintable|IERC1155|IERC1155Receiver|InvalidERC1155Receiver).json", + "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." + }, + "repository": { + "type": "git", + "url": "https://github.com/0xProject/0x-monorepo.git" + }, + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/0xProject/0x-monorepo/issues" + }, + "homepage": "https://github.com/0xProject/0x-monorepo/contracts/tokens/README.md", + "devDependencies": { + "@0x/abi-gen": "^2.0.4", + "@0x/contracts-gen": "^1.0.3", + "@0x/contracts-test-utils": "^3.0.5", + "@0x/dev-utils": "^2.1.1", + "@0x/sol-compiler": "^3.1.0", + "@0x/tslint-config": "^3.0.0", + "@types/lodash": "4.14.104", + "@types/node": "*", + "chai": "^4.0.1", + "chai-as-promised": "^7.1.0", + "chai-bignumber": "^3.0.0", + "dirty-chai": "^2.0.1", + "make-promises-safe": "^1.1.0", + "mocha": "^4.1.0", + "npm-run-all": "^4.1.2", + "shx": "^0.2.2", + "solhint": "^1.4.1", + "tslint": "5.11.0", + "typescript": "3.0.1" + }, + "dependencies": { + "@0x/base-contract": "^5.0.0", + "@0x/contracts-utils": "2.0.1", + "@0x/types": "^2.1.0", + "@0x/typescript-typings": "^4.1.0", + "@0x/utils": "^4.2.0", + "@0x/web3-wrapper": "^6.0.0", + "ethereum-types": "^2.1.0", + "lodash": "^4.17.11" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/contracts/erc1155/src/artifacts.ts b/contracts/erc1155/src/artifacts.ts new file mode 100644 index 0000000000..aa92b28b67 --- /dev/null +++ b/contracts/erc1155/src/artifacts.ts @@ -0,0 +1,23 @@ +/* + * ----------------------------------------------------------------------------- + * Warning: This file is auto-generated by contracts-gen. Don't edit manually. + * ----------------------------------------------------------------------------- + */ +import { ContractArtifact } from 'ethereum-types'; + +import * as DummyERC1155Receiver from '../generated-artifacts/DummyERC1155Receiver.json'; +import * as DummyERC1155Token from '../generated-artifacts/DummyERC1155Token.json'; +import * as ERC1155 from '../generated-artifacts/ERC1155.json'; +import * as ERC1155Mintable from '../generated-artifacts/ERC1155Mintable.json'; +import * as IERC1155 from '../generated-artifacts/IERC1155.json'; +import * as IERC1155Receiver from '../generated-artifacts/IERC1155Receiver.json'; +import * as InvalidERC1155Receiver from '../generated-artifacts/InvalidERC1155Receiver.json'; +export const artifacts = { + ERC1155: ERC1155 as ContractArtifact, + ERC1155Mintable: ERC1155Mintable as ContractArtifact, + IERC1155: IERC1155 as ContractArtifact, + IERC1155Receiver: IERC1155Receiver as ContractArtifact, + DummyERC1155Receiver: DummyERC1155Receiver as ContractArtifact, + DummyERC1155Token: DummyERC1155Token as ContractArtifact, + InvalidERC1155Receiver: InvalidERC1155Receiver as ContractArtifact, +}; diff --git a/contracts/erc1155/src/index.ts b/contracts/erc1155/src/index.ts new file mode 100644 index 0000000000..91dd7e0e3e --- /dev/null +++ b/contracts/erc1155/src/index.ts @@ -0,0 +1,2 @@ +export * from './wrappers'; +export * from './artifacts'; diff --git a/contracts/erc1155/src/wrappers.ts b/contracts/erc1155/src/wrappers.ts new file mode 100644 index 0000000000..a3ce828d20 --- /dev/null +++ b/contracts/erc1155/src/wrappers.ts @@ -0,0 +1,12 @@ +/* + * ----------------------------------------------------------------------------- + * Warning: This file is auto-generated by contracts-gen. Don't edit manually. + * ----------------------------------------------------------------------------- + */ +export * from '../generated-wrappers/dummy_erc1155_receiver'; +export * from '../generated-wrappers/dummy_erc1155_token'; +export * from '../generated-wrappers/erc1155'; +export * from '../generated-wrappers/erc1155_mintable'; +export * from '../generated-wrappers/i_erc1155_receiver'; +export * from '../generated-wrappers/ierc1155'; +export * from '../generated-wrappers/invalid_erc1155_receiver'; diff --git a/contracts/erc1155/test/erc1155_token.ts b/contracts/erc1155/test/erc1155_token.ts new file mode 100644 index 0000000000..5ca15380f2 --- /dev/null +++ b/contracts/erc1155/test/erc1155_token.ts @@ -0,0 +1,347 @@ + +import { + chaiSetup, + constants, + expectTransactionFailedAsync, + expectTransactionFailedWithoutReasonAsync, + LogDecoder, + provider, + txDefaults, + web3Wrapper, +} from '@0x/contracts-test-utils'; +import { BlockchainLifecycle } from '@0x/dev-utils'; +import { RevertReason } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import * as chai from 'chai'; +import { LogWithDecodedArgs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { + artifacts, + DummyERC1155ReceiverContract, + DummyERC1155ReceiverTokenReceivedEventArgs, + DummyERC1155TokenContract, + //DummyERC1155TokenTransferEventArgs, + InvalidERC1155ReceiverContract, + ERC1155TransferSingleEventArgs, + DummyERC1155ReceiverBatchTokenReceivedEventArgs +} from '../src'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +// tslint:disable:no-unnecessary-type-assertion +describe('ERC1155Token', () => { + + const DUMMY_FUNGIBLE_TOKEN_URI = 'FUN'; + const DUMMY_FUNGIBLE_TOKEN_IS_FUNGIBLE = false; + const DUMMY_NONFUNGIBLE_TOKEN_URI = 'NOFUN'; + const DUMMY_NONFUNGIBLE_TOKEN_IS_FUNGIBLE = true; + + let owner: string; + let spender: string; + const spenderInitialBalance = new BigNumber(500); + const receiverInitialBalance = new BigNumber(0); + let token: DummyERC1155TokenContract; + let erc1155Receiver: DummyERC1155ReceiverContract; + let logDecoder: LogDecoder; + const tokenId = new BigNumber(1); + let dummyNft: BigNumber; + const nftOwnerBalance = new BigNumber(1); + const nftNotOwnerBalance = new BigNumber(0); + + let dummyFungibleTokenType: BigNumber; + let dummyNonFungibleTokenType: BigNumber; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + owner = accounts[0]; + spender = accounts[1]; + token = await DummyERC1155TokenContract.deployFrom0xArtifactAsync( + artifacts.DummyERC1155Token, + provider, + txDefaults, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + ); + erc1155Receiver = await DummyERC1155ReceiverContract.deployFrom0xArtifactAsync( + artifacts.DummyERC1155Receiver, + provider, + txDefaults, + ); + + logDecoder = new LogDecoder(web3Wrapper, artifacts); + // Create & mint fungible token + let txReceipt = await logDecoder.getTxWithDecodedLogsAsync( + await token.create.sendTransactionAsync( + DUMMY_FUNGIBLE_TOKEN_URI, + DUMMY_FUNGIBLE_TOKEN_IS_FUNGIBLE + ) + ); + const createFungibleTokenLog = txReceipt.logs[0] as LogWithDecodedArgs; + dummyFungibleTokenType = createFungibleTokenLog.args._id; + await web3Wrapper.awaitTransactionSuccessAsync( + await token.mintFungible.sendTransactionAsync( + dummyFungibleTokenType, + [spender], + [spenderInitialBalance], + { from: owner } + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Create & mint non-fungible token + txReceipt = await logDecoder.getTxWithDecodedLogsAsync( + await token.create.sendTransactionAsync( + DUMMY_NONFUNGIBLE_TOKEN_URI, + DUMMY_NONFUNGIBLE_TOKEN_IS_FUNGIBLE + ) + ); + const createNonFungibleTokenLog = txReceipt.logs[0] as LogWithDecodedArgs; + dummyNonFungibleTokenType = createNonFungibleTokenLog.args._id; + await web3Wrapper.awaitTransactionSuccessAsync( + await token.mintNonFungible.sendTransactionAsync( + dummyNonFungibleTokenType, + [spender], + { from: owner } + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + dummyNft = dummyNonFungibleTokenType.plus(1); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('safeTransferFrom', () => { + it('should transfer fungible token if called by token owner', async () => { + // setup test parameters + const from = spender; + const to = erc1155Receiver.address; + const fromIdx = 0; + const toIdx = 1; + const participatingOwners = [from, to]; + const participatingTokens = [dummyFungibleTokenType, dummyFungibleTokenType]; + const tokenTypesToTransfer = [dummyFungibleTokenType]; + const valueToTransfer = new BigNumber(200); + const valuesToTransfer = [valueToTransfer]; + const callbackData = constants.NULL_BYTES; + // check balances before transfer + const balancesBeforeTransfer = await token.balanceOfBatch.callAsync(participatingOwners, participatingTokens); + expect(balancesBeforeTransfer[fromIdx]).to.be.bignumber.equal(spenderInitialBalance); + expect(balancesBeforeTransfer[toIdx]).to.be.bignumber.equal(receiverInitialBalance); + // execute transfer + await web3Wrapper.awaitTransactionSuccessAsync( + await token.safeTransferFrom.sendTransactionAsync(from, to, tokenTypesToTransfer[0], valuesToTransfer[0], callbackData, {from}), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // check balances after transfer + const balancesAfterTransfer = await token.balanceOfBatch.callAsync(participatingOwners, participatingTokens); + expect(balancesAfterTransfer[fromIdx]).to.be.bignumber.equal(balancesBeforeTransfer[fromIdx].minus(valueToTransfer)); + expect(balancesAfterTransfer[toIdx]).to.be.bignumber.equal(balancesBeforeTransfer[toIdx].plus(valueToTransfer)); + }); + it('should transfer non-fungible token if called by token owner', async () => { + // setup test parameters + const from = spender; + const to = erc1155Receiver.address; + const fromIdx = 0; + const toIdx = 1; + const participatingOwners = [from, to]; + const participatingTokens = [dummyNft, dummyNft]; + const tokenTypesToTransfer = [dummyNft]; + const valueToTransfer = new BigNumber(1); + const valuesToTransfer = [valueToTransfer]; + const callbackData = constants.NULL_BYTES; + // check balances before transfer + const balancesBeforeTransfer = await token.balanceOfBatch.callAsync(participatingOwners, participatingTokens); + expect(balancesBeforeTransfer[fromIdx]).to.be.bignumber.equal(nftOwnerBalance); + expect(balancesBeforeTransfer[toIdx]).to.be.bignumber.equal(nftNotOwnerBalance); + // execute transfer + await web3Wrapper.awaitTransactionSuccessAsync( + await token.safeTransferFrom.sendTransactionAsync(from, to, tokenTypesToTransfer[0], valuesToTransfer[0], callbackData, {from}), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // check balances after transfer + const balancesAfterTransfer = await token.balanceOfBatch.callAsync(participatingOwners, participatingTokens); + expect(balancesAfterTransfer[fromIdx]).to.be.bignumber.equal(nftNotOwnerBalance); + expect(balancesAfterTransfer[toIdx]).to.be.bignumber.equal(nftOwnerBalance); + }); + it('should trigger callback if transferring to a contract', async () => { + // setup test parameters + const from = spender; + const to = erc1155Receiver.address; + const fromIdxFungible = 0; + const toIdxFungible = 1; + const fromIdxNonFungible = 2; + const toIdxNonFungible = 3; + const participatingOwners = [from, to, from, to]; + const participatingTokens = [dummyFungibleTokenType, dummyFungibleTokenType, dummyNft, dummyNft]; + const tokenTypesToTransfer = [dummyFungibleTokenType, dummyNft]; + const fungibleValueToTransfer = new BigNumber(200); + const nonFungibleValueToTransfer = new BigNumber(1); + const valuesToTransfer = [fungibleValueToTransfer, nonFungibleValueToTransfer]; + const callbackData = '0x01020304'; + // check balances before transfer + const balancesBeforeTransfer = await token.balanceOfBatch.callAsync(participatingOwners, participatingTokens); + expect(balancesBeforeTransfer[fromIdxFungible]).to.be.bignumber.equal(spenderInitialBalance); + expect(balancesBeforeTransfer[toIdxFungible]).to.be.bignumber.equal(receiverInitialBalance); + expect(balancesBeforeTransfer[fromIdxNonFungible]).to.be.bignumber.equal(nftOwnerBalance); + expect(balancesBeforeTransfer[toIdxNonFungible]).to.be.bignumber.equal(nftNotOwnerBalance); + // execute transfer + const txReceipt = await logDecoder.getTxWithDecodedLogsAsync( + await token.safeTransferFrom.sendTransactionAsync(from, to, tokenTypesToTransfer[0], valuesToTransfer[0], callbackData, {from}) + ); + expect(txReceipt.logs.length).to.be.equal(2); + const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs; + // check callback logs + expect(receiverLog.args.operator).to.be.equal(from); + expect(receiverLog.args.from).to.be.equal(from); + expect(receiverLog.args.tokenId).to.be.bignumber.equal(tokenTypesToTransfer[0]); + expect(receiverLog.args.tokenValue).to.be.bignumber.equal(valuesToTransfer[0]); + expect(receiverLog.args.data).to.be.deep.equal(callbackData); + // check balances after transfer + const balancesAfterTransfer = await token.balanceOfBatch.callAsync(participatingOwners, participatingTokens); + expect(balancesAfterTransfer[fromIdxFungible]).to.be.bignumber.equal(balancesBeforeTransfer[fromIdxFungible].minus(fungibleValueToTransfer)); + expect(balancesAfterTransfer[toIdxFungible]).to.be.bignumber.equal(balancesBeforeTransfer[toIdxFungible].plus(fungibleValueToTransfer)); + }); + }); + describe('batchSafeTransferFrom', () => { + it('should transfer fungible tokens if called by token owner', async () => { + // setup test parameters + const from = spender; + const to = erc1155Receiver.address; + const fromIdx = 0; + const toIdx = 1; + const participatingOwners = [from, to]; + const participatingTokens = [dummyFungibleTokenType, dummyFungibleTokenType]; + const tokenTypesToTransfer = [dummyFungibleTokenType]; + const valueToTransfer = new BigNumber(200); + const valuesToTransfer = [valueToTransfer]; + const callbackData = constants.NULL_BYTES; + // check balances before transfer + const balancesBeforeTransfer = await token.balanceOfBatch.callAsync(participatingOwners, participatingTokens); + expect(balancesBeforeTransfer[fromIdx]).to.be.bignumber.equal(spenderInitialBalance); + expect(balancesBeforeTransfer[toIdx]).to.be.bignumber.equal(receiverInitialBalance); + // execute transfer + await web3Wrapper.awaitTransactionSuccessAsync( + await token.safeBatchTransferFrom.sendTransactionAsync(from, to, tokenTypesToTransfer, valuesToTransfer, callbackData, {from}), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // check balances after transfer + const balancesAfterTransfer = await token.balanceOfBatch.callAsync(participatingOwners, participatingTokens); + expect(balancesAfterTransfer[fromIdx]).to.be.bignumber.equal(balancesBeforeTransfer[fromIdx].minus(valueToTransfer)); + expect(balancesAfterTransfer[toIdx]).to.be.bignumber.equal(balancesBeforeTransfer[toIdx].plus(valueToTransfer)); + }); + it('should transfer non-fungible token if called by token owner', async () => { + // setup test parameters + const from = spender; + const to = erc1155Receiver.address; + const fromIdx = 0; + const toIdx = 1; + const participatingOwners = [from, to]; + const participatingTokens = [dummyNft, dummyNft]; + const tokenTypesToTransfer = [dummyNft]; + const valueToTransfer = new BigNumber(1); + const valuesToTransfer = [valueToTransfer]; + const callbackData = constants.NULL_BYTES; + // check balances before transfer + const balancesBeforeTransfer = await token.balanceOfBatch.callAsync(participatingOwners, participatingTokens); + expect(balancesBeforeTransfer[fromIdx]).to.be.bignumber.equal(nftOwnerBalance); + expect(balancesBeforeTransfer[toIdx]).to.be.bignumber.equal(nftNotOwnerBalance); + // execute transfer + await web3Wrapper.awaitTransactionSuccessAsync( + await token.safeBatchTransferFrom.sendTransactionAsync(from, to, tokenTypesToTransfer, valuesToTransfer, callbackData, {from}), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // check balances after transfer + const balancesAfterTransfer = await token.balanceOfBatch.callAsync(participatingOwners, participatingTokens); + expect(balancesAfterTransfer[fromIdx]).to.be.bignumber.equal(nftNotOwnerBalance); + expect(balancesAfterTransfer[toIdx]).to.be.bignumber.equal(nftOwnerBalance); + }); + it('should transfer mix of fungible / non-fungible tokens if called by token owner', async () => { + // setup test parameters + const from = spender; + const to = erc1155Receiver.address; + const fromIdxFungible = 0; + const toIdxFungible = 1; + const fromIdxNonFungible = 2; + const toIdxNonFungible = 3; + const participatingOwners = [from, to, from, to]; + const participatingTokens = [dummyFungibleTokenType, dummyFungibleTokenType, dummyNft, dummyNft]; + const tokenTypesToTransfer = [dummyFungibleTokenType, dummyNft]; + const fungibleValueToTransfer = new BigNumber(200); + const nonFungibleValueToTransfer = new BigNumber(1); + const valuesToTransfer = [fungibleValueToTransfer, nonFungibleValueToTransfer]; + const callbackData = constants.NULL_BYTES; + // check balances before transfer + const balancesBeforeTransfer = await token.balanceOfBatch.callAsync(participatingOwners, participatingTokens); + expect(balancesBeforeTransfer[fromIdxFungible]).to.be.bignumber.equal(spenderInitialBalance); + expect(balancesBeforeTransfer[toIdxFungible]).to.be.bignumber.equal(receiverInitialBalance); + expect(balancesBeforeTransfer[fromIdxNonFungible]).to.be.bignumber.equal(nftOwnerBalance); + expect(balancesBeforeTransfer[toIdxNonFungible]).to.be.bignumber.equal(nftNotOwnerBalance); + // execute transfer + await web3Wrapper.awaitTransactionSuccessAsync( + await token.safeBatchTransferFrom.sendTransactionAsync(from, to, tokenTypesToTransfer, valuesToTransfer, callbackData, {from}), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // check balances after transfer + const balancesAfterTransfer = await token.balanceOfBatch.callAsync(participatingOwners, participatingTokens); + expect(balancesAfterTransfer[fromIdxFungible]).to.be.bignumber.equal(balancesBeforeTransfer[fromIdxFungible].minus(fungibleValueToTransfer)); + expect(balancesAfterTransfer[toIdxFungible]).to.be.bignumber.equal(balancesBeforeTransfer[toIdxFungible].plus(fungibleValueToTransfer)); + expect(balancesAfterTransfer[fromIdxNonFungible]).to.be.bignumber.equal(nftNotOwnerBalance); + expect(balancesAfterTransfer[toIdxNonFungible]).to.be.bignumber.equal(nftOwnerBalance); + }); + it('should trigger callback if transferring to a contract', async () => { + // setup test parameters + const from = spender; + const to = erc1155Receiver.address; + const fromIdxFungible = 0; + const toIdxFungible = 1; + const fromIdxNonFungible = 2; + const toIdxNonFungible = 3; + const participatingOwners = [from, to, from, to]; + const participatingTokens = [dummyFungibleTokenType, dummyFungibleTokenType, dummyNft, dummyNft]; + const tokenTypesToTransfer = [dummyFungibleTokenType, dummyNft]; + const fungibleValueToTransfer = new BigNumber(200); + const nonFungibleValueToTransfer = new BigNumber(1); + const valuesToTransfer = [fungibleValueToTransfer, nonFungibleValueToTransfer]; + const callbackData = '0x01020304'; + // check balances before transfer + const balancesBeforeTransfer = await token.balanceOfBatch.callAsync(participatingOwners, participatingTokens); + expect(balancesBeforeTransfer[fromIdxFungible]).to.be.bignumber.equal(spenderInitialBalance); + expect(balancesBeforeTransfer[toIdxFungible]).to.be.bignumber.equal(receiverInitialBalance); + expect(balancesBeforeTransfer[fromIdxNonFungible]).to.be.bignumber.equal(nftOwnerBalance); + expect(balancesBeforeTransfer[toIdxNonFungible]).to.be.bignumber.equal(nftNotOwnerBalance); + // execute transfer + const txReceipt = await logDecoder.getTxWithDecodedLogsAsync( + await token.safeBatchTransferFrom.sendTransactionAsync(from, to, tokenTypesToTransfer, valuesToTransfer, callbackData, {from}) + ); + expect(txReceipt.logs.length).to.be.equal(2); + const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs; + // check callback logs + expect(receiverLog.args.operator).to.be.equal(from); + expect(receiverLog.args.from).to.be.equal(from); + expect(receiverLog.args.tokenIds.length).to.be.equal(2); + expect(receiverLog.args.tokenIds[0]).to.be.bignumber.equal(tokenTypesToTransfer[0]); + expect(receiverLog.args.tokenIds[1]).to.be.bignumber.equal(tokenTypesToTransfer[1]); + expect(receiverLog.args.tokenValues.length).to.be.equal(2); + expect(receiverLog.args.tokenValues[0]).to.be.bignumber.equal(valuesToTransfer[0]); + expect(receiverLog.args.tokenValues[1]).to.be.bignumber.equal(valuesToTransfer[1]); + expect(receiverLog.args.data).to.be.deep.equal(callbackData); + // check balances after transfer + const balancesAfterTransfer = await token.balanceOfBatch.callAsync(participatingOwners, participatingTokens); + expect(balancesAfterTransfer[fromIdxFungible]).to.be.bignumber.equal(balancesBeforeTransfer[fromIdxFungible].minus(fungibleValueToTransfer)); + expect(balancesAfterTransfer[toIdxFungible]).to.be.bignumber.equal(balancesBeforeTransfer[toIdxFungible].plus(fungibleValueToTransfer)); + expect(balancesAfterTransfer[fromIdxNonFungible]).to.be.bignumber.equal(nftNotOwnerBalance); + expect(balancesAfterTransfer[toIdxNonFungible]).to.be.bignumber.equal(nftOwnerBalance); + }); + }); +}); +// tslint:enable:no-unnecessary-type-assertion \ No newline at end of file diff --git a/contracts/erc1155/test/global_hooks.ts b/contracts/erc1155/test/global_hooks.ts new file mode 100644 index 0000000000..f8ace376a5 --- /dev/null +++ b/contracts/erc1155/test/global_hooks.ts @@ -0,0 +1,17 @@ +import { env, EnvVars } from '@0x/dev-utils'; + +import { coverage, profiler, provider } from '@0x/contracts-test-utils'; +before('start web3 provider', () => { + provider.start(); +}); +after('generate coverage report', async () => { + if (env.parseBoolean(EnvVars.SolidityCoverage)) { + const coverageSubprovider = coverage.getCoverageSubproviderSingleton(); + await coverageSubprovider.writeCoverageAsync(); + } + if (env.parseBoolean(EnvVars.SolidityProfiler)) { + const profilerSubprovider = profiler.getProfilerSubproviderSingleton(); + await profilerSubprovider.writeProfilerOutputAsync(); + } + provider.stop(); +}); diff --git a/contracts/erc1155/tsconfig.json b/contracts/erc1155/tsconfig.json new file mode 100644 index 0000000000..5bb1c5c707 --- /dev/null +++ b/contracts/erc1155/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true }, + "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"], + "files": [ + "generated-artifacts/DummyERC1155Receiver.json", + "generated-artifacts/DummyERC1155Token.json", + "generated-artifacts/ERC1155.json", + "generated-artifacts/ERC1155Mintable.json", + "generated-artifacts/IERC1155.json", + "generated-artifacts/IERC1155Receiver.json", + "generated-artifacts/InvalidERC1155Receiver.json" + ], + "exclude": ["./deploy/solc/solc_bin"] +} diff --git a/contracts/erc1155/tslint.json b/contracts/erc1155/tslint.json new file mode 100644 index 0000000000..1bb3ac2a22 --- /dev/null +++ b/contracts/erc1155/tslint.json @@ -0,0 +1,6 @@ +{ + "extends": ["@0x/tslint-config"], + "rules": { + "custom-no-magic-numbers": false + } +}