aboutsummaryrefslogtreecommitdiffstats
path: root/contracts/erc721
diff options
context:
space:
mode:
Diffstat (limited to 'contracts/erc721')
-rw-r--r--contracts/erc721/.solhintignore3
-rw-r--r--contracts/erc721/CHANGELOG.json56
-rw-r--r--contracts/erc721/CHANGELOG.md26
-rw-r--r--contracts/erc721/DEPLOYS.json16
-rw-r--r--contracts/erc721/README.md74
-rw-r--r--contracts/erc721/compiler.json30
-rw-r--r--contracts/erc721/contracts/src/ERC721Token.sol277
-rw-r--r--contracts/erc721/contracts/src/MintableERC721Token.sol82
-rw-r--r--contracts/erc721/contracts/src/interfaces/IERC721Receiver.sol44
-rw-r--r--contracts/erc721/contracts/src/interfaces/IERC721Token.sol158
-rw-r--r--contracts/erc721/contracts/test/DummyERC721Receiver.sol67
-rw-r--r--contracts/erc721/contracts/test/DummyERC721Token.sol63
-rw-r--r--contracts/erc721/contracts/test/InvalidERC721Receiver.sol66
-rw-r--r--contracts/erc721/package.json89
-rw-r--r--contracts/erc721/src/artifacts/index.ts20
-rw-r--r--contracts/erc721/src/index.ts2
-rw-r--r--contracts/erc721/src/wrappers/index.ts7
-rw-r--r--contracts/erc721/test/erc721_token.ts282
-rw-r--r--contracts/erc721/test/global_hooks.ts17
-rw-r--r--contracts/erc721/tsconfig.json19
-rw-r--r--contracts/erc721/tslint.json6
21 files changed, 1404 insertions, 0 deletions
diff --git a/contracts/erc721/.solhintignore b/contracts/erc721/.solhintignore
new file mode 100644
index 000000000..1e33ec53b
--- /dev/null
+++ b/contracts/erc721/.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/erc721/CHANGELOG.json b/contracts/erc721/CHANGELOG.json
new file mode 100644
index 000000000..724ab1e4a
--- /dev/null
+++ b/contracts/erc721/CHANGELOG.json
@@ -0,0 +1,56 @@
+[
+ {
+ "version": "2.0.0",
+ "changes": [
+ {
+ "note": "Upgrade the bignumber.js to v8.0.2",
+ "pr": 1517
+ }
+ ]
+ },
+ {
+ "timestamp": 1547747677,
+ "version": "1.0.6",
+ "changes": [
+ {
+ "note": "Dependencies updated"
+ }
+ ]
+ },
+ {
+ "timestamp": 1547561734,
+ "version": "1.0.5",
+ "changes": [
+ {
+ "note": "Dependencies updated"
+ }
+ ]
+ },
+ {
+ "timestamp": 1547225310,
+ "version": "1.0.4",
+ "changes": [
+ {
+ "note": "Dependencies updated"
+ }
+ ]
+ },
+ {
+ "timestamp": 1547040760,
+ "version": "1.0.3",
+ "changes": [
+ {
+ "note": "Dependencies updated"
+ }
+ ]
+ },
+ {
+ "timestamp": 1544741676,
+ "version": "1.0.2",
+ "changes": [
+ {
+ "note": "Dependencies updated"
+ }
+ ]
+ }
+]
diff --git a/contracts/erc721/CHANGELOG.md b/contracts/erc721/CHANGELOG.md
new file mode 100644
index 000000000..59d0804da
--- /dev/null
+++ b/contracts/erc721/CHANGELOG.md
@@ -0,0 +1,26 @@
+<!--
+changelogUtils.file is auto-generated using the monorepo-scripts package. Don't edit directly.
+Edit the package's CHANGELOG.json file only.
+-->
+
+CHANGELOG
+
+## v1.0.6 - _January 17, 2019_
+
+ * Dependencies updated
+
+## v1.0.5 - _January 15, 2019_
+
+ * Dependencies updated
+
+## v1.0.4 - _January 11, 2019_
+
+ * Dependencies updated
+
+## v1.0.3 - _January 9, 2019_
+
+ * Dependencies updated
+
+## v1.0.2 - _December 13, 2018_
+
+ * Dependencies updated
diff --git a/contracts/erc721/DEPLOYS.json b/contracts/erc721/DEPLOYS.json
new file mode 100644
index 000000000..5ff58c035
--- /dev/null
+++ b/contracts/erc721/DEPLOYS.json
@@ -0,0 +1,16 @@
+[
+ {
+ "name": "ZRXToken",
+ "version": "1.0.0",
+ "changes": [
+ {
+ "note": "protocol v1 deploy",
+ "networks": {
+ "1": "0xe41d2489571d322189246dafa5ebde1f4699f498",
+ "3": "0xff67881f8d12f372d91baae9752eb3631ff0ed00",
+ "42": "0x2002d3812f58e35f0ea1ffbf80a75a38c32175fa"
+ }
+ }
+ ]
+ }
+]
diff --git a/contracts/erc721/README.md b/contracts/erc721/README.md
new file mode 100644
index 000000000..2d1db92f0
--- /dev/null
+++ b/contracts/erc721/README.md
@@ -0,0 +1,74 @@
+## Token contracts
+
+Token smart contracts that are used in the 0x protocol. Addresses of the deployed contracts can be found in the 0x [wiki](https://0xproject.com/wiki#Deployed-Addresses) or the [CHANGELOG](./CHANGELOG.json) of this package.
+
+## Usage
+
+Token contracts that make up and interact with version 2.0.0 of the protocol can be found in the [contracts](./contracts) directory. The contents of this directory are broken down into the following subdirectories:
+
+- [tokens](./contracts/tokens)
+ - This directory contains implementations of different tokens and token standards, including [wETH](https://weth.io/), ZRX, [ERC20](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md), and [ERC721](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md).
+- [test](./contracts/test)
+ - This directory contains mocks and other contracts that are used solely for testing contracts within the other directories.
+
+## 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-tokens yarn build
+```
+
+Or continuously rebuild on change:
+
+```bash
+PKG=@0x/contracts-tokens 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/erc721/compiler.json b/contracts/erc721/compiler.json
new file mode 100644
index 000000000..c5f07ddb8
--- /dev/null
+++ b/contracts/erc721/compiler.json
@@ -0,0 +1,30 @@
+{
+ "artifactsDir": "./generated-artifacts",
+ "contractsDir": "./contracts",
+ "compilerSettings": {
+ "optimizer": {
+ "enabled": true,
+ "runs": 1000000
+ },
+ "outputSelection": {
+ "*": {
+ "*": [
+ "abi",
+ "evm.bytecode.object",
+ "evm.bytecode.sourceMap",
+ "evm.deployedBytecode.object",
+ "evm.deployedBytecode.sourceMap"
+ ]
+ }
+ }
+ },
+ "contracts": [
+ "DummyERC721Receiver",
+ "InvalidERC721Receiver",
+ "DummyERC721Token",
+ "ERC721Token",
+ "IERC721Receiver",
+ "IERC721Token",
+ "MintableERC721Token"
+ ]
+}
diff --git a/contracts/erc721/contracts/src/ERC721Token.sol b/contracts/erc721/contracts/src/ERC721Token.sol
new file mode 100644
index 000000000..32f70ccf8
--- /dev/null
+++ b/contracts/erc721/contracts/src/ERC721Token.sol
@@ -0,0 +1,277 @@
+/*
+
+ 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.4.24;
+
+import "./interfaces/IERC721Token.sol";
+import "./interfaces/IERC721Receiver.sol";
+import "@0x/contracts-utils/contracts/src/SafeMath.sol";
+
+
+contract ERC721Token is
+ IERC721Token,
+ SafeMath
+{
+ // Function selector for ERC721Receiver.onERC721Received
+ // 0x150b7a02
+ bytes4 constant internal ERC721_RECEIVED = bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));
+
+ // Mapping of tokenId => owner
+ mapping (uint256 => address) internal owners;
+
+ // Mapping of tokenId => approved address
+ mapping (uint256 => address) internal approvals;
+
+ // Mapping of owner => number of tokens owned
+ mapping (address => uint256) internal balances;
+
+ // Mapping of owner => operator => approved
+ mapping (address => mapping (address => bool)) internal operatorApprovals;
+
+ /// @notice Transfers the ownership of an NFT from one address to another address
+ /// @dev Throws unless `msg.sender` is the current owner, an authorized
+ /// operator, or the approved address for this NFT. Throws if `_from` is
+ /// not the current owner. Throws if `_to` is the zero address. Throws if
+ /// `_tokenId` is not a valid NFT. When transfer is complete, this function
+ /// checks if `_to` is a smart contract (code size > 0). If so, it calls
+ /// `onERC721Received` on `_to` and throws if the return value is not
+ /// `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.
+ /// @param _from The current owner of the NFT
+ /// @param _to The new owner
+ /// @param _tokenId The NFT to transfer
+ /// @param _data Additional data with no specified format, sent in call to `_to`
+ function safeTransferFrom(
+ address _from,
+ address _to,
+ uint256 _tokenId,
+ bytes _data
+ )
+ external
+ {
+ transferFrom(
+ _from,
+ _to,
+ _tokenId
+ );
+
+ uint256 receiverCodeSize;
+ assembly {
+ receiverCodeSize := extcodesize(_to)
+ }
+ if (receiverCodeSize > 0) {
+ bytes4 selector = IERC721Receiver(_to).onERC721Received(
+ msg.sender,
+ _from,
+ _tokenId,
+ _data
+ );
+ require(
+ selector == ERC721_RECEIVED,
+ "ERC721_INVALID_SELECTOR"
+ );
+ }
+ }
+
+ /// @notice Transfers the ownership of an NFT from one address to another address
+ /// @dev This works identically to the other function with an extra data parameter,
+ /// except this function just sets data to "".
+ /// @param _from The current owner of the NFT
+ /// @param _to The new owner
+ /// @param _tokenId The NFT to transfer
+ function safeTransferFrom(
+ address _from,
+ address _to,
+ uint256 _tokenId
+ )
+ external
+ {
+ transferFrom(
+ _from,
+ _to,
+ _tokenId
+ );
+
+ uint256 receiverCodeSize;
+ assembly {
+ receiverCodeSize := extcodesize(_to)
+ }
+ if (receiverCodeSize > 0) {
+ bytes4 selector = IERC721Receiver(_to).onERC721Received(
+ msg.sender,
+ _from,
+ _tokenId,
+ ""
+ );
+ require(
+ selector == ERC721_RECEIVED,
+ "ERC721_INVALID_SELECTOR"
+ );
+ }
+ }
+
+ /// @notice Change or reaffirm the approved address for an NFT
+ /// @dev The zero address indicates there is no approved address.
+ /// Throws unless `msg.sender` is the current NFT owner, or an authorized
+ /// operator of the current owner.
+ /// @param _approved The new approved NFT controller
+ /// @param _tokenId The NFT to approve
+ function approve(address _approved, uint256 _tokenId)
+ external
+ {
+ address owner = ownerOf(_tokenId);
+ require(
+ msg.sender == owner || isApprovedForAll(owner, msg.sender),
+ "ERC721_INVALID_SENDER"
+ );
+
+ approvals[_tokenId] = _approved;
+ emit Approval(
+ owner,
+ _approved,
+ _tokenId
+ );
+ }
+
+ /// @notice Enable or disable approval for a third party ("operator") to manage
+ /// all of `msg.sender`'s assets
+ /// @dev Emits the ApprovalForAll event. The contract MUST allow
+ /// multiple operators per owner.
+ /// @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
+ {
+ operatorApprovals[msg.sender][_operator] = _approved;
+ emit ApprovalForAll(
+ msg.sender,
+ _operator,
+ _approved
+ );
+ }
+
+ /// @notice Count all NFTs assigned to an owner
+ /// @dev NFTs assigned to the zero address are considered invalid, and this
+ /// function throws for queries about the zero address.
+ /// @param _owner An address for whom to query the balance
+ /// @return The number of NFTs owned by `_owner`, possibly zero
+ function balanceOf(address _owner)
+ external
+ view
+ returns (uint256)
+ {
+ require(
+ _owner != address(0),
+ "ERC721_ZERO_OWNER"
+ );
+ return balances[_owner];
+ }
+
+ /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE
+ /// TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE
+ /// THEY MAY BE PERMANENTLY LOST
+ /// @dev Throws unless `msg.sender` is the current owner, an authorized
+ /// operator, or the approved address for this NFT. Throws if `_from` is
+ /// not the current owner. Throws if `_to` is the zero address. Throws if
+ /// `_tokenId` is not a valid NFT.
+ /// @param _from The current owner of the NFT
+ /// @param _to The new owner
+ /// @param _tokenId The NFT to transfer
+ function transferFrom(
+ address _from,
+ address _to,
+ uint256 _tokenId
+ )
+ public
+ {
+ require(
+ _to != address(0),
+ "ERC721_ZERO_TO_ADDRESS"
+ );
+
+ address owner = ownerOf(_tokenId);
+ require(
+ _from == owner,
+ "ERC721_OWNER_MISMATCH"
+ );
+
+ address spender = msg.sender;
+ address approvedAddress = getApproved(_tokenId);
+ require(
+ spender == owner ||
+ isApprovedForAll(owner, spender) ||
+ approvedAddress == spender,
+ "ERC721_INVALID_SPENDER"
+ );
+
+ if (approvedAddress != address(0)) {
+ approvals[_tokenId] = address(0);
+ }
+
+ owners[_tokenId] = _to;
+ balances[_from] = safeSub(balances[_from], 1);
+ balances[_to] = safeAdd(balances[_to], 1);
+
+ emit Transfer(
+ _from,
+ _to,
+ _tokenId
+ );
+ }
+
+ /// @notice Find the owner of an NFT
+ /// @dev NFTs assigned to zero address are considered invalid, and queries
+ /// about them do throw.
+ /// @param _tokenId The identifier for an NFT
+ /// @return The address of the owner of the NFT
+ function ownerOf(uint256 _tokenId)
+ public
+ view
+ returns (address)
+ {
+ address owner = owners[_tokenId];
+ require(
+ owner != address(0),
+ "ERC721_ZERO_OWNER"
+ );
+ return owner;
+ }
+
+ /// @notice Get the approved address for a single NFT
+ /// @dev Throws if `_tokenId` is not a valid NFT.
+ /// @param _tokenId The NFT to find the approved address for
+ /// @return The approved address for this NFT, or the zero address if there is none
+ function getApproved(uint256 _tokenId)
+ public
+ view
+ returns (address)
+ {
+ return approvals[_tokenId];
+ }
+
+ /// @notice Query if an address is an authorized operator for another address
+ /// @param _owner The address that owns the NFTs
+ /// @param _operator The address that acts on behalf of the owner
+ /// @return True if `_operator` is an approved operator for `_owner`, false otherwise
+ function isApprovedForAll(address _owner, address _operator)
+ public
+ view
+ returns (bool)
+ {
+ return operatorApprovals[_owner][_operator];
+ }
+}
diff --git a/contracts/erc721/contracts/src/MintableERC721Token.sol b/contracts/erc721/contracts/src/MintableERC721Token.sol
new file mode 100644
index 000000000..27326d857
--- /dev/null
+++ b/contracts/erc721/contracts/src/MintableERC721Token.sol
@@ -0,0 +1,82 @@
+/*
+
+ 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.4.24;
+
+import "./ERC721Token.sol";
+
+
+contract MintableERC721Token is
+ ERC721Token
+{
+ /// @dev Function to mint a new token
+ /// Reverts if the given token ID already exists
+ /// @param _to Address of the beneficiary that will own the minted token
+ /// @param _tokenId ID of the token to be minted by the msg.sender
+ function _mint(address _to, uint256 _tokenId)
+ internal
+ {
+ require(
+ _to != address(0),
+ "ERC721_ZERO_TO_ADDRESS"
+ );
+
+ address owner = owners[_tokenId];
+ require(
+ owner == address(0),
+ "ERC721_OWNER_ALREADY_EXISTS"
+ );
+
+ owners[_tokenId] = _to;
+ balances[_to] = safeAdd(balances[_to], 1);
+
+ emit Transfer(
+ address(0),
+ _to,
+ _tokenId
+ );
+ }
+
+ /// @dev Function to burn a token
+ /// Reverts if the given token ID doesn't exist
+ /// @param _owner Owner of token with given token ID
+ /// @param _tokenId ID of the token to be burned by the msg.sender
+ function _burn(address _owner, uint256 _tokenId)
+ internal
+ {
+ require(
+ _owner != address(0),
+ "ERC721_ZERO_OWNER_ADDRESS"
+ );
+
+ address owner = owners[_tokenId];
+ require(
+ owner == _owner,
+ "ERC721_OWNER_MISMATCH"
+ );
+
+ owners[_tokenId] = address(0);
+ balances[_owner] = safeSub(balances[_owner], 1);
+
+ emit Transfer(
+ _owner,
+ address(0),
+ _tokenId
+ );
+ }
+}
diff --git a/contracts/erc721/contracts/src/interfaces/IERC721Receiver.sol b/contracts/erc721/contracts/src/interfaces/IERC721Receiver.sol
new file mode 100644
index 000000000..c3ad35ede
--- /dev/null
+++ b/contracts/erc721/contracts/src/interfaces/IERC721Receiver.sol
@@ -0,0 +1,44 @@
+/*
+
+ 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.4.24;
+
+
+contract IERC721Receiver {
+
+ /// @notice Handle the receipt of an NFT
+ /// @dev The ERC721 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("onERC721Received(address,address,uint256,bytes)"))`
+ /// unless throwing
+ function onERC721Received(
+ address _operator,
+ address _from,
+ uint256 _tokenId,
+ bytes _data
+ )
+ external
+ returns (bytes4);
+}
diff --git a/contracts/erc721/contracts/src/interfaces/IERC721Token.sol b/contracts/erc721/contracts/src/interfaces/IERC721Token.sol
new file mode 100644
index 000000000..56a3314df
--- /dev/null
+++ b/contracts/erc721/contracts/src/interfaces/IERC721Token.sol
@@ -0,0 +1,158 @@
+/*
+
+ 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.4.24;
+
+
+contract IERC721Token {
+
+ /// @dev This emits when ownership of any NFT changes by any mechanism.
+ /// This event emits when NFTs are created (`from` == 0) and destroyed
+ /// (`to` == 0). Exception: during contract creation, any number of NFTs
+ /// may be created and assigned without emitting Transfer. At the time of
+ /// any transfer, the approved address for that NFT (if any) is reset to none.
+ event Transfer(
+ address indexed _from,
+ address indexed _to,
+ uint256 indexed _tokenId
+ );
+
+ /// @dev This emits when the approved address for an NFT is changed or
+ /// reaffirmed. The zero address indicates there is no approved address.
+ /// When a Transfer event emits, this also indicates that the approved
+ /// address for that NFT (if any) is reset to none.
+ event Approval(
+ address indexed _owner,
+ address indexed _approved,
+ uint256 indexed _tokenId
+ );
+
+ /// @dev This emits when an operator is enabled or disabled for an owner.
+ /// The operator can manage all NFTs of the owner.
+ event ApprovalForAll(
+ address indexed _owner,
+ address indexed _operator,
+ bool _approved
+ );
+
+ /// @notice Transfers the ownership of an NFT from one address to another address
+ /// @dev Throws unless `msg.sender` is the current owner, an authorized
+ /// perator, or the approved address for this NFT. Throws if `_from` is
+ /// not the current owner. Throws if `_to` is the zero address. Throws if
+ /// `_tokenId` is not a valid NFT. When transfer is complete, this function
+ /// checks if `_to` is a smart contract (code size > 0). If so, it calls
+ /// `onERC721Received` on `_to` and throws if the return value is not
+ /// `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.
+ /// @param _from The current owner of the NFT
+ /// @param _to The new owner
+ /// @param _tokenId The NFT to transfer
+ /// @param _data Additional data with no specified format, sent in call to `_to`
+ function safeTransferFrom(
+ address _from,
+ address _to,
+ uint256 _tokenId,
+ bytes _data
+ )
+ external;
+
+ /// @notice Transfers the ownership of an NFT from one address to another address
+ /// @dev This works identically to the other function with an extra data parameter,
+ /// except this function just sets data to "".
+ /// @param _from The current owner of the NFT
+ /// @param _to The new owner
+ /// @param _tokenId The NFT to transfer
+ function safeTransferFrom(
+ address _from,
+ address _to,
+ uint256 _tokenId
+ )
+ external;
+
+ /// @notice Change or reaffirm the approved address for an NFT
+ /// @dev The zero address indicates there is no approved address.
+ /// Throws unless `msg.sender` is the current NFT owner, or an authorized
+ /// operator of the current owner.
+ /// @param _approved The new approved NFT controller
+ /// @param _tokenId The NFT to approve
+ function approve(address _approved, uint256 _tokenId)
+ external;
+
+ /// @notice Enable or disable approval for a third party ("operator") to manage
+ /// all of `msg.sender`'s assets
+ /// @dev Emits the ApprovalForAll event. The contract MUST allow
+ /// multiple operators per owner.
+ /// @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 Count all NFTs assigned to an owner
+ /// @dev NFTs assigned to the zero address are considered invalid, and this
+ /// function throws for queries about the zero address.
+ /// @param _owner An address for whom to query the balance
+ /// @return The number of NFTs owned by `_owner`, possibly zero
+ function balanceOf(address _owner)
+ external
+ view
+ returns (uint256);
+
+ /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE
+ /// TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE
+ /// THEY MAY BE PERMANENTLY LOST
+ /// @dev Throws unless `msg.sender` is the current owner, an authorized
+ /// operator, or the approved address for this NFT. Throws if `_from` is
+ /// not the current owner. Throws if `_to` is the zero address. Throws if
+ /// `_tokenId` is not a valid NFT.
+ /// @param _from The current owner of the NFT
+ /// @param _to The new owner
+ /// @param _tokenId The NFT to transfer
+ function transferFrom(
+ address _from,
+ address _to,
+ uint256 _tokenId
+ )
+ public;
+
+ /// @notice Find the owner of an NFT
+ /// @dev NFTs assigned to zero address are considered invalid, and queries
+ /// about them do throw.
+ /// @param _tokenId The identifier for an NFT
+ /// @return The address of the owner of the NFT
+ function ownerOf(uint256 _tokenId)
+ public
+ view
+ returns (address);
+
+ /// @notice Get the approved address for a single NFT
+ /// @dev Throws if `_tokenId` is not a valid NFT.
+ /// @param _tokenId The NFT to find the approved address for
+ /// @return The approved address for this NFT, or the zero address if there is none
+ function getApproved(uint256 _tokenId)
+ public
+ view
+ returns (address);
+
+ /// @notice Query if an address is an authorized operator for another address
+ /// @param _owner The address that owns the NFTs
+ /// @param _operator The address that acts on behalf of the owner
+ /// @return True if `_operator` is an approved operator for `_owner`, false otherwise
+ function isApprovedForAll(address _owner, address _operator)
+ public
+ view
+ returns (bool);
+}
diff --git a/contracts/erc721/contracts/test/DummyERC721Receiver.sol b/contracts/erc721/contracts/test/DummyERC721Receiver.sol
new file mode 100644
index 000000000..b5de01aa5
--- /dev/null
+++ b/contracts/erc721/contracts/test/DummyERC721Receiver.sol
@@ -0,0 +1,67 @@
+/*
+
+ 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.4.24;
+
+import "../src/interfaces/IERC721Receiver.sol";
+
+
+contract DummyERC721Receiver is
+ IERC721Receiver
+{
+ // Function selector for ERC721Receiver.onERC721Received
+ // 0x150b7a02
+ bytes4 constant internal ERC721_RECEIVED = bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));
+
+ event TokenReceived(
+ address operator,
+ address from,
+ uint256 tokenId,
+ bytes data
+ );
+
+ /// @notice Handle the receipt of an NFT
+ /// @dev The ERC721 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("onERC721Received(address,address,uint256,bytes)"))`
+ /// unless throwing
+ function onERC721Received(
+ address _operator,
+ address _from,
+ uint256 _tokenId,
+ bytes _data
+ )
+ external
+ returns (bytes4)
+ {
+ emit TokenReceived(
+ _operator,
+ _from,
+ _tokenId,
+ _data
+ );
+ return ERC721_RECEIVED;
+ }
+}
diff --git a/contracts/erc721/contracts/test/DummyERC721Token.sol b/contracts/erc721/contracts/test/DummyERC721Token.sol
new file mode 100644
index 000000000..41dc8ea18
--- /dev/null
+++ b/contracts/erc721/contracts/test/DummyERC721Token.sol
@@ -0,0 +1,63 @@
+/*
+
+ 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.4.24;
+
+import "../src/MintableERC721Token.sol";
+import "@0x/contracts-utils/contracts/src/Ownable.sol";
+
+
+// solhint-disable no-empty-blocks
+contract DummyERC721Token is
+ Ownable,
+ MintableERC721Token
+{
+ string public name;
+ string public symbol;
+
+ constructor (
+ string _name,
+ string _symbol
+ )
+ public
+ {
+ name = _name;
+ symbol = _symbol;
+ }
+
+ /// @dev Function to mint a new token
+ /// Reverts if the given token ID already exists
+ /// @param _to Address of the beneficiary that will own the minted token
+ /// @param _tokenId ID of the token to be minted by the msg.sender
+ function mint(address _to, uint256 _tokenId)
+ external
+ {
+ _mint(_to, _tokenId);
+ }
+
+ /// @dev Function to burn a token
+ /// Reverts if the given token ID doesn't exist or not called by contract owner
+ /// @param _owner Owner of token with given token ID
+ /// @param _tokenId ID of the token to be burned by the msg.sender
+ function burn(address _owner, uint256 _tokenId)
+ external
+ onlyOwner
+ {
+ _burn(_owner, _tokenId);
+ }
+}
diff --git a/contracts/erc721/contracts/test/InvalidERC721Receiver.sol b/contracts/erc721/contracts/test/InvalidERC721Receiver.sol
new file mode 100644
index 000000000..ea59dedc5
--- /dev/null
+++ b/contracts/erc721/contracts/test/InvalidERC721Receiver.sol
@@ -0,0 +1,66 @@
+/*
+
+ 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.4.24;
+
+import "../src/interfaces/IERC721Receiver.sol";
+
+
+contract InvalidERC721Receiver is
+ IERC721Receiver
+{
+ // Actual function signature is `onERC721Received(address,address,uint256,bytes)`
+ bytes4 constant internal INVALID_ERC721_RECEIVED = bytes4(keccak256("onERC721Received(address,uint256,bytes)"));
+
+ event TokenReceived(
+ address operator,
+ address from,
+ uint256 tokenId,
+ bytes data
+ );
+
+ /// @notice Handle the receipt of an NFT
+ /// @dev The ERC721 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("onERC721Received(address,address,uint256,bytes)"))`
+ /// unless throwing
+ function onERC721Received(
+ address _operator,
+ address _from,
+ uint256 _tokenId,
+ bytes _data
+ )
+ external
+ returns (bytes4)
+ {
+ emit TokenReceived(
+ _operator,
+ _from,
+ _tokenId,
+ _data
+ );
+ return INVALID_ERC721_RECEIVED;
+ }
+}
diff --git a/contracts/erc721/package.json b/contracts/erc721/package.json
new file mode 100644
index 000000000..84b09a708
--- /dev/null
+++ b/contracts/erc721/package.json
@@ -0,0 +1,89 @@
+{
+ "name": "@0x/contracts-erc721",
+ "version": "1.0.6",
+ "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",
+ "lint-contracts": "solhint -c ../.solhint.json contracts/**/**/**/**/*.sol"
+ },
+ "config": {
+ "abis": "generated-artifacts/@(DummyERC721Receiver|InvalidERC721Receiver|DummyERC721Token|ERC721Token|IERC721Receiver|IERC721Token|MintableERC721Token).json"
+ },
+ "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": "^1.0.22",
+ "@0x/contracts-test-utils": "^2.0.1",
+ "@0x/dev-utils": "^1.0.24",
+ "@0x/sol-compiler": "^2.0.2",
+ "@0x/subproviders": "^2.1.11",
+ "@0x/tslint-config": "^2.0.2",
+ "@types/bn.js": "^4.11.0",
+ "@types/lodash": "4.14.104",
+ "@types/node": "*",
+ "@types/yargs": "^10.0.0",
+ "chai": "^4.0.1",
+ "chai-as-promised": "^7.1.0",
+ "chai-bignumber": "^3.0.0",
+ "dirty-chai": "^2.0.1",
+ "ethereumjs-abi": "0.6.5",
+ "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",
+ "yargs": "^10.0.3"
+ },
+ "dependencies": {
+ "@0x/base-contract": "^3.0.13",
+ "@0x/contracts-exchange-libs": "^1.0.6",
+ "@0x/contracts-utils": "^1.0.6",
+ "@0x/order-utils": "^3.1.2",
+ "@0x/types": "^1.5.2",
+ "@0x/typescript-typings": "^3.0.8",
+ "@0x/utils": "^3.0.1",
+ "@0x/web3-wrapper": "^3.2.4",
+ "@types/js-combinatorics": "^0.5.29",
+ "bn.js": "^4.11.8",
+ "ethereum-types": "^1.1.6",
+ "ethereumjs-util": "^5.1.1",
+ "lodash": "^4.17.5"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/contracts/erc721/src/artifacts/index.ts b/contracts/erc721/src/artifacts/index.ts
new file mode 100644
index 000000000..8d5ec8a46
--- /dev/null
+++ b/contracts/erc721/src/artifacts/index.ts
@@ -0,0 +1,20 @@
+import { ContractArtifact } from 'ethereum-types';
+
+import * as DummyERC721Receiver from '../../generated-artifacts/DummyERC721Receiver.json';
+import * as DummyERC721Token from '../../generated-artifacts/DummyERC721Token.json';
+import * as ERC721Token from '../../generated-artifacts/ERC721Token.json';
+import * as IERC721Receiver from '../../generated-artifacts/IERC721Receiver.json';
+import * as IERC721Token from '../../generated-artifacts/IERC721Token.json';
+import * as InvalidERC721Receiver from '../../generated-artifacts/InvalidERC721Receiver.json';
+import * as MintableERC721Token from '../../generated-artifacts/MintableERC721Token.json';
+
+// tslint:disable:no-unnecessary-type-assertion
+export const artifacts = {
+ DummyERC721Receiver: DummyERC721Receiver as ContractArtifact,
+ InvalidERC721Receiver: InvalidERC721Receiver as ContractArtifact,
+ DummyERC721Token: DummyERC721Token as ContractArtifact,
+ ERC721Token: ERC721Token as ContractArtifact,
+ IERC721Receiver: IERC721Receiver as ContractArtifact,
+ IERC721Token: IERC721Token as ContractArtifact,
+ MintableERC721Token: MintableERC721Token as ContractArtifact,
+};
diff --git a/contracts/erc721/src/index.ts b/contracts/erc721/src/index.ts
new file mode 100644
index 000000000..91dd7e0e3
--- /dev/null
+++ b/contracts/erc721/src/index.ts
@@ -0,0 +1,2 @@
+export * from './wrappers';
+export * from './artifacts';
diff --git a/contracts/erc721/src/wrappers/index.ts b/contracts/erc721/src/wrappers/index.ts
new file mode 100644
index 000000000..ba6c51885
--- /dev/null
+++ b/contracts/erc721/src/wrappers/index.ts
@@ -0,0 +1,7 @@
+export * from '../../generated-wrappers/mintable_erc721_token';
+export * from '../../generated-wrappers/invalid_erc721_receiver';
+export * from '../../generated-wrappers/i_erc721_token';
+export * from '../../generated-wrappers/i_erc721_receiver';
+export * from '../../generated-wrappers/erc721_token';
+export * from '../../generated-wrappers/dummy_erc721_token';
+export * from '../../generated-wrappers/dummy_erc721_receiver';
diff --git a/contracts/erc721/test/erc721_token.ts b/contracts/erc721/test/erc721_token.ts
new file mode 100644
index 000000000..13332cd35
--- /dev/null
+++ b/contracts/erc721/test/erc721_token.ts
@@ -0,0 +1,282 @@
+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 {
+ artifacts,
+ DummyERC721ReceiverContract,
+ DummyERC721ReceiverTokenReceivedEventArgs,
+ DummyERC721TokenContract,
+ DummyERC721TokenTransferEventArgs,
+ InvalidERC721ReceiverContract,
+} from '../src';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+// tslint:disable:no-unnecessary-type-assertion
+describe('ERC721Token', () => {
+ let owner: string;
+ let spender: string;
+ let token: DummyERC721TokenContract;
+ let erc721Receiver: DummyERC721ReceiverContract;
+ let logDecoder: LogDecoder;
+ const tokenId = new BigNumber(1);
+ 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 DummyERC721TokenContract.deployFrom0xArtifactAsync(
+ artifacts.DummyERC721Token,
+ provider,
+ txDefaults,
+ constants.DUMMY_TOKEN_NAME,
+ constants.DUMMY_TOKEN_SYMBOL,
+ );
+ erc721Receiver = await DummyERC721ReceiverContract.deployFrom0xArtifactAsync(
+ artifacts.DummyERC721Receiver,
+ provider,
+ txDefaults,
+ );
+ logDecoder = new LogDecoder(web3Wrapper, artifacts);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await token.mint.sendTransactionAsync(owner, tokenId, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+
+ describe('transferFrom', () => {
+ it('should revert if the tokenId is not owner', async () => {
+ const from = owner;
+ const to = erc721Receiver.address;
+ const unownedTokenId = new BigNumber(2);
+ await expectTransactionFailedAsync(
+ token.transferFrom.sendTransactionAsync(from, to, unownedTokenId),
+ RevertReason.Erc721ZeroOwner,
+ );
+ });
+ it('should revert if transferring to a null address', async () => {
+ const from = owner;
+ const to = constants.NULL_ADDRESS;
+ await expectTransactionFailedAsync(
+ token.transferFrom.sendTransactionAsync(from, to, tokenId),
+ RevertReason.Erc721ZeroToAddress,
+ );
+ });
+ it('should revert if the from address does not own the token', async () => {
+ const from = spender;
+ const to = erc721Receiver.address;
+ await expectTransactionFailedAsync(
+ token.transferFrom.sendTransactionAsync(from, to, tokenId),
+ RevertReason.Erc721OwnerMismatch,
+ );
+ });
+ it('should revert if spender does not own the token, is not approved, and is not approved for all', async () => {
+ const from = owner;
+ const to = erc721Receiver.address;
+ await expectTransactionFailedAsync(
+ token.transferFrom.sendTransactionAsync(from, to, tokenId, { from: spender }),
+ RevertReason.Erc721InvalidSpender,
+ );
+ });
+ it('should transfer the token if called by owner', async () => {
+ const from = owner;
+ const to = erc721Receiver.address;
+ const txReceipt = await logDecoder.getTxWithDecodedLogsAsync(
+ await token.transferFrom.sendTransactionAsync(from, to, tokenId),
+ );
+ const newOwner = await token.ownerOf.callAsync(tokenId);
+ expect(newOwner).to.be.equal(to);
+ const log = txReceipt.logs[0] as LogWithDecodedArgs<DummyERC721TokenTransferEventArgs>;
+ expect(log.args._from).to.be.equal(from);
+ expect(log.args._to).to.be.equal(to);
+ expect(log.args._tokenId).to.be.bignumber.equal(tokenId);
+ });
+ it('should transfer the token if spender is approved for all', async () => {
+ const isApproved = true;
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await token.setApprovalForAll.sendTransactionAsync(spender, isApproved),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ const from = owner;
+ const to = erc721Receiver.address;
+ const txReceipt = await logDecoder.getTxWithDecodedLogsAsync(
+ await token.transferFrom.sendTransactionAsync(from, to, tokenId),
+ );
+ const newOwner = await token.ownerOf.callAsync(tokenId);
+ expect(newOwner).to.be.equal(to);
+ const log = txReceipt.logs[0] as LogWithDecodedArgs<DummyERC721TokenTransferEventArgs>;
+ expect(log.args._from).to.be.equal(from);
+ expect(log.args._to).to.be.equal(to);
+ expect(log.args._tokenId).to.be.bignumber.equal(tokenId);
+ });
+ it('should transfer the token if spender is individually approved', async () => {
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await token.approve.sendTransactionAsync(spender, tokenId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ const from = owner;
+ const to = erc721Receiver.address;
+ const txReceipt = await logDecoder.getTxWithDecodedLogsAsync(
+ await token.transferFrom.sendTransactionAsync(from, to, tokenId),
+ );
+ const newOwner = await token.ownerOf.callAsync(tokenId);
+ expect(newOwner).to.be.equal(to);
+
+ const approvedAddress = await token.getApproved.callAsync(tokenId);
+ expect(approvedAddress).to.be.equal(constants.NULL_ADDRESS);
+ const log = txReceipt.logs[0] as LogWithDecodedArgs<DummyERC721TokenTransferEventArgs>;
+ expect(log.args._from).to.be.equal(from);
+ expect(log.args._to).to.be.equal(to);
+ expect(log.args._tokenId).to.be.bignumber.equal(tokenId);
+ });
+ });
+ describe('safeTransferFrom without data', () => {
+ it('should transfer token to a non-contract address if called by owner', async () => {
+ const from = owner;
+ const to = spender;
+ const txReceipt = await logDecoder.getTxWithDecodedLogsAsync(
+ await token.safeTransferFrom1.sendTransactionAsync(from, to, tokenId),
+ );
+ const newOwner = await token.ownerOf.callAsync(tokenId);
+ expect(newOwner).to.be.equal(to);
+ const log = txReceipt.logs[0] as LogWithDecodedArgs<DummyERC721TokenTransferEventArgs>;
+ expect(log.args._from).to.be.equal(from);
+ expect(log.args._to).to.be.equal(to);
+ expect(log.args._tokenId).to.be.bignumber.equal(tokenId);
+ });
+ it('should revert if transferring to a contract address without onERC721Received', async () => {
+ const contract = await DummyERC721TokenContract.deployFrom0xArtifactAsync(
+ artifacts.DummyERC721Token,
+ provider,
+ txDefaults,
+ constants.DUMMY_TOKEN_NAME,
+ constants.DUMMY_TOKEN_SYMBOL,
+ );
+ const from = owner;
+ const to = contract.address;
+ await expectTransactionFailedWithoutReasonAsync(
+ token.safeTransferFrom1.sendTransactionAsync(from, to, tokenId),
+ );
+ });
+ it('should revert if onERC721Received does not return the correct value', async () => {
+ const invalidErc721Receiver = await InvalidERC721ReceiverContract.deployFrom0xArtifactAsync(
+ artifacts.InvalidERC721Receiver,
+ provider,
+ txDefaults,
+ );
+ const from = owner;
+ const to = invalidErc721Receiver.address;
+ await expectTransactionFailedAsync(
+ token.safeTransferFrom1.sendTransactionAsync(from, to, tokenId),
+ RevertReason.Erc721InvalidSelector,
+ );
+ });
+ it('should transfer to contract and call onERC721Received with correct return value', async () => {
+ const from = owner;
+ const to = erc721Receiver.address;
+ const txReceipt = await logDecoder.getTxWithDecodedLogsAsync(
+ await token.safeTransferFrom1.sendTransactionAsync(from, to, tokenId),
+ );
+ const newOwner = await token.ownerOf.callAsync(tokenId);
+ expect(newOwner).to.be.equal(to);
+ const transferLog = txReceipt.logs[0] as LogWithDecodedArgs<DummyERC721TokenTransferEventArgs>;
+ const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs<DummyERC721ReceiverTokenReceivedEventArgs>;
+ expect(transferLog.args._from).to.be.equal(from);
+ expect(transferLog.args._to).to.be.equal(to);
+ expect(transferLog.args._tokenId).to.be.bignumber.equal(tokenId);
+ expect(receiverLog.args.operator).to.be.equal(owner);
+ expect(receiverLog.args.from).to.be.equal(from);
+ expect(receiverLog.args.tokenId).to.be.bignumber.equal(tokenId);
+ expect(receiverLog.args.data).to.be.equal(constants.NULL_BYTES);
+ });
+ });
+ describe('safeTransferFrom with data', () => {
+ const data = '0x0102030405060708090a0b0c0d0e0f';
+ it('should transfer token to a non-contract address if called by owner', async () => {
+ const from = owner;
+ const to = spender;
+ const txReceipt = await logDecoder.getTxWithDecodedLogsAsync(
+ await token.safeTransferFrom2.sendTransactionAsync(from, to, tokenId, data),
+ );
+ const newOwner = await token.ownerOf.callAsync(tokenId);
+ expect(newOwner).to.be.equal(to);
+ const log = txReceipt.logs[0] as LogWithDecodedArgs<DummyERC721TokenTransferEventArgs>;
+ expect(log.args._from).to.be.equal(from);
+ expect(log.args._to).to.be.equal(to);
+ expect(log.args._tokenId).to.be.bignumber.equal(tokenId);
+ });
+ it('should revert if transferring to a contract address without onERC721Received', async () => {
+ const contract = await DummyERC721TokenContract.deployFrom0xArtifactAsync(
+ artifacts.DummyERC721Token,
+ provider,
+ txDefaults,
+ constants.DUMMY_TOKEN_NAME,
+ constants.DUMMY_TOKEN_SYMBOL,
+ );
+ const from = owner;
+ const to = contract.address;
+ await expectTransactionFailedWithoutReasonAsync(
+ token.safeTransferFrom2.sendTransactionAsync(from, to, tokenId, data),
+ );
+ });
+ it('should revert if onERC721Received does not return the correct value', async () => {
+ const invalidErc721Receiver = await InvalidERC721ReceiverContract.deployFrom0xArtifactAsync(
+ artifacts.InvalidERC721Receiver,
+ provider,
+ txDefaults,
+ );
+ const from = owner;
+ const to = invalidErc721Receiver.address;
+ await expectTransactionFailedAsync(
+ token.safeTransferFrom2.sendTransactionAsync(from, to, tokenId, data),
+ RevertReason.Erc721InvalidSelector,
+ );
+ });
+ it('should transfer to contract and call onERC721Received with correct return value', async () => {
+ const from = owner;
+ const to = erc721Receiver.address;
+ const txReceipt = await logDecoder.getTxWithDecodedLogsAsync(
+ await token.safeTransferFrom2.sendTransactionAsync(from, to, tokenId, data),
+ );
+ const newOwner = await token.ownerOf.callAsync(tokenId);
+ expect(newOwner).to.be.equal(to);
+ const transferLog = txReceipt.logs[0] as LogWithDecodedArgs<DummyERC721TokenTransferEventArgs>;
+ const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs<DummyERC721ReceiverTokenReceivedEventArgs>;
+ expect(transferLog.args._from).to.be.equal(from);
+ expect(transferLog.args._to).to.be.equal(to);
+ expect(transferLog.args._tokenId).to.be.bignumber.equal(tokenId);
+ expect(receiverLog.args.operator).to.be.equal(owner);
+ expect(receiverLog.args.from).to.be.equal(from);
+ expect(receiverLog.args.tokenId).to.be.bignumber.equal(tokenId);
+ expect(receiverLog.args.data).to.be.equal(data);
+ });
+ });
+});
+// tslint:enable:no-unnecessary-type-assertion
diff --git a/contracts/erc721/test/global_hooks.ts b/contracts/erc721/test/global_hooks.ts
new file mode 100644
index 000000000..f8ace376a
--- /dev/null
+++ b/contracts/erc721/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/erc721/tsconfig.json b/contracts/erc721/tsconfig.json
new file mode 100644
index 000000000..098c3a7f3
--- /dev/null
+++ b/contracts/erc721/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../tsconfig",
+ "compilerOptions": {
+ "outDir": "lib",
+ "rootDir": ".",
+ "resolveJsonModule": true
+ },
+ "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"],
+ "files": [
+ "./generated-artifacts/DummyERC721Receiver.json",
+ "./generated-artifacts/InvalidERC721Receiver.json",
+ "./generated-artifacts/DummyERC721Token.json",
+ "./generated-artifacts/ERC721Token.json",
+ "./generated-artifacts/IERC721Receiver.json",
+ "./generated-artifacts/IERC721Token.json",
+ "./generated-artifacts/MintableERC721Token.json"
+ ],
+ "exclude": ["./deploy/solc/solc_bin"]
+}
diff --git a/contracts/erc721/tslint.json b/contracts/erc721/tslint.json
new file mode 100644
index 000000000..1bb3ac2a2
--- /dev/null
+++ b/contracts/erc721/tslint.json
@@ -0,0 +1,6 @@
+{
+ "extends": ["@0x/tslint-config"],
+ "rules": {
+ "custom-no-magic-numbers": false
+ }
+}