From 0f3201d72a181c3dc7bafba9a82c141f91463ce9 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Thu, 26 Jul 2018 16:19:10 -0700 Subject: Optimize ERC721Token --- .../test/DummyERC721Token/DummyERC721Token.sol | 56 +-- .../src/2.0.0/tokens/ERC721Token/ERC721Token.sol | 554 ++++++++------------- .../2.0.0/tokens/ERC721Token/IERC721Receiver.sol | 81 ++- .../src/2.0.0/tokens/ERC721Token/IERC721Token.sol | 198 +++++--- .../tokens/ERC721Token/MintableERC721Token.sol | 83 +++ 5 files changed, 462 insertions(+), 510 deletions(-) create mode 100644 packages/contracts/src/2.0.0/tokens/ERC721Token/MintableERC721Token.sol (limited to 'packages/contracts') diff --git a/packages/contracts/src/2.0.0/test/DummyERC721Token/DummyERC721Token.sol b/packages/contracts/src/2.0.0/test/DummyERC721Token/DummyERC721Token.sol index 627746a52..7c64005bd 100644 --- a/packages/contracts/src/2.0.0/test/DummyERC721Token/DummyERC721Token.sol +++ b/packages/contracts/src/2.0.0/test/DummyERC721Token/DummyERC721Token.sol @@ -18,59 +18,35 @@ pragma solidity 0.4.24; -import "../../tokens/ERC721Token/ERC721Token.sol"; +import "../../tokens/ERC721Token/MintableERC721Token.sol"; import "../../utils/Ownable/Ownable.sol"; // solhint-disable no-empty-blocks contract DummyERC721Token is Ownable, - ERC721Token + MintableERC721Token { - /** - * @dev Constructor passes its arguments to the base ERC721Token constructor - * @param name of token - * @param symbol of token - */ - constructor ( - string name, - string symbol - ) - public - ERC721Token(name, symbol) - {} - - /** - * @dev Function to mint a new token - * @dev Reverts if the given token ID already exists - * @param to address the beneficiary that will own the minted token - * @param tokenId uint256 ID of the token to be minted by the msg.sender - */ - function mint(address to, uint256 tokenId) - public + /// @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 onlyOwner { - require( - !exists(tokenId), - "Token with tokenId already exists." - ); - _mint(to, tokenId); + _mint(_to, _tokenId); } - /** - * @dev Function to burn a token - * @dev Reverts if the given token ID doesn't exist - * @param tokenId uint256 ID of the token to be minted by the msg.sender - */ - function burn(address owner, uint256 tokenId) - public + /// @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) + external onlyOwner { - require( - exists(tokenId), - "Token with tokenId does not exist." - ); - _burn(owner, tokenId); + _burn(_owner, _tokenId); } } diff --git a/packages/contracts/src/2.0.0/tokens/ERC721Token/ERC721Token.sol b/packages/contracts/src/2.0.0/tokens/ERC721Token/ERC721Token.sol index 60603aa19..94e084731 100644 --- a/packages/contracts/src/2.0.0/tokens/ERC721Token/ERC721Token.sol +++ b/packages/contracts/src/2.0.0/tokens/ERC721Token/ERC721Token.sol @@ -1,26 +1,19 @@ /* -The MIT License (MIT) -Copyright (c) 2016 Smart Contract Solutions, Inc. + Copyright 2018 ZeroEx Intl. -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: + 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 -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. + 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. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ pragma solidity 0.4.24; @@ -30,179 +23,250 @@ import "./IERC721Receiver.sol"; import "../../utils/SafeMath/SafeMath.sol"; -/** - * @title ERC721 Non-Fungible Token Standard basic implementation - * @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md - * Modified from https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/token/ERC721/ERC721BasicToken.sol - */ contract ERC721Token is IERC721Token, SafeMath { - // Equals to `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))` - // which can be also obtained as `ERC721Receiver(0).onERC721Received.selector` - bytes4 constant internal ERC721_RECEIVED = 0xf0b9e5ba; + // Function selector for ERC721Receiver.onERC721Received + // 0x150b7a02 + bytes4 constant internal ERC721_RECEIVED = bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")); - // Mapping from token ID to owner - mapping (uint256 => address) internal tokenOwner; + // Mapping of tokenId => owner + mapping (uint256 => address) internal owners; - // Mapping from token ID to approved address - mapping (uint256 => address) internal tokenApprovals; + // Mapping of tokenId => approved address + mapping (uint256 => address) internal approvals; - // Mapping from owner to number of owned token - mapping (address => uint256) internal ownedTokensCount; + // Mapping of owner => number of tokens owned + mapping (address => uint256) internal balances; - // Mapping from owner to operator approvals + // Mapping of owner => operator => approved mapping (address => mapping (address => bool)) internal operatorApprovals; - /** - * @dev Guarantees msg.sender is owner of the given token - * @param _tokenId uint256 ID of the token to validate its ownership belongs to msg.sender - */ - modifier onlyOwnerOf(uint256 _tokenId) { - require(ownerOf(_tokenId) == msg.sender); - _; - } - - /** - * @dev Checks msg.sender can transfer a token, by being owner, approved, or operator - * @param _tokenId uint256 ID of the token to validate - */ - modifier canTransfer(uint256 _tokenId) { - require(isApprovedOrOwner(msg.sender, _tokenId)); - _; + /// @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 + { + 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" + ); + } } - constructor ( - string _name, - string _symbol) - public + /// @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 { - name_ = _name; - symbol_ = _symbol; + 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" + ); + } } - /** - * @dev Gets the token name - * @return string representing the token name - */ - function name() - public - view - returns (string) + /// @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 { - return name_; + address owner = ownerOf(_tokenId); + require( + msg.sender == owner || isApprovedForAll(owner, msg.sender), + "ERC721_INVALID_SENDER" + ); + + approvals[_tokenId] = _approved; + emit Approval( + owner, + _approved, + _tokenId + ); } - /** - * @dev Gets the token symbol - * @return string representing the token symbol - */ - function symbol() - public - view - returns (string) + /// @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 { - return symbol_; + operatorApprovals[msg.sender][_operator] = _approved; + emit ApprovalForAll( + msg.sender, + _operator, + _approved + ); } - - /** - * @dev Gets the balance of the specified address - * @param _owner address to query the balance of - * @return uint256 representing the amount owned by the passed address - */ + + /// @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) - public + external view returns (uint256) { - require(_owner != address(0)); - return ownedTokensCount[_owner]; + require( + _owner != address(0), + "ERC721_ZERO_OWNER" + ); + return balances[_owner]; } - /** - * @dev Gets the owner of the specified token ID - * @param _tokenId uint256 ID of the token to query the owner of - * @return owner address currently marked as the owner of the given token ID - */ - function ownerOf(uint256 _tokenId) - public - view - returns (address) - { - address owner = tokenOwner[_tokenId]; - require(owner != address(0)); - return owner; - } - - /** - * @dev Returns whether the specified token exists - * @param _tokenId uint256 ID of the token to query the existance of - * @return whether the token exists - */ - function exists(uint256 _tokenId) + /// @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 - view - returns (bool) { - address owner = tokenOwner[_tokenId]; - return owner != address(0); - } + require( + _to != address(0), + "ERC721_ZERO_TO_ADDRESS" + ); - /** - * @dev Approves another address to transfer the given token ID - * @dev The zero address indicates there is no approved address. - * @dev There can only be one approved address per token at a given time. - * @dev Can only be called by the token owner or an approved operator. - * @param _to address to be approved for the given token ID - * @param _tokenId uint256 ID of the token to be approved - */ - function approve(address _to, uint256 _tokenId) - public - { address owner = ownerOf(_tokenId); - require(_to != owner); - require(msg.sender == owner || isApprovedForAll(owner, msg.sender)); - - if (getApproved(_tokenId) != address(0) || _to != address(0)) { - tokenApprovals[_tokenId] = _to; - emit Approval(owner, _to, _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 + ); } - /** - * @dev Gets the approved address for a token ID, or zero if no address set - * @param _tokenId uint256 ID of the token to query the approval of - * @return address currently approved for a the given token ID - */ - function getApproved(uint256 _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) { - return tokenApprovals[_tokenId]; + address owner = owners[_tokenId]; + require( + owner != address(0), + "ERC721_ZERO_OWNER" + ); + return owner; } - /** - * @dev Sets or unsets the approval of a given operator - * @dev An operator is allowed to transfer all tokens of the sender on their behalf - * @param _to operator address to set the approval - * @param _approved representing the status of the approval to be set - */ - function setApprovalForAll(address _to, bool _approved) + /// @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) { - require(_to != msg.sender); - operatorApprovals[msg.sender][_to] = _approved; - emit ApprovalForAll(msg.sender, _to, _approved); + return approvals[_tokenId]; } - /** - * @dev Tells whether an operator is approved by a given owner - * @param _owner owner address which you want to query the approval of - * @param _operator operator address which you want to query the approval of - * @return bool whether the given operator is approved by the given owner - */ + /// @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 @@ -210,198 +274,4 @@ contract ERC721Token is { return operatorApprovals[_owner][_operator]; } - - /** - * @dev Transfers the ownership of a given token ID to another address - * @dev Usage of this method is discouraged, use `safeTransferFrom` whenever possible - * @dev Requires the msg sender to be the owner, approved, or operator - * @param _from current owner of the token - * @param _to address to receive the ownership of the given token ID - * @param _tokenId uint256 ID of the token to be transferred - */ - function transferFrom(address _from, address _to, uint256 _tokenId) - public - canTransfer(_tokenId) - { - require(_from != address(0)); - require(_to != address(0)); - - clearApproval(_from, _tokenId); - removeTokenFrom(_from, _tokenId); - addTokenTo(_to, _tokenId); - - emit Transfer(_from, _to, _tokenId); - } - - /** - * @dev Safely transfers the ownership of a given token ID to another address - * @dev If the target address is a contract, it must implement `onERC721Received`, - * which is called upon a safe transfer, and return the magic value - * `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))`; otherwise, - * the transfer is reverted. - * @dev Requires the msg sender to be the owner, approved, or operator - * @param _from current owner of the token - * @param _to address to receive the ownership of the given token ID - * @param _tokenId uint256 ID of the token to be transferred - */ - function safeTransferFrom( - address _from, - address _to, - uint256 _tokenId) - public - canTransfer(_tokenId) - { - // solium-disable-next-line arg-overflow - safeTransferFrom(_from, _to, _tokenId, ""); - } - - /** - * @dev Safely transfers the ownership of a given token ID to another address - * @dev If the target address is a contract, it must implement `onERC721Received`, - * which is called upon a safe transfer, and return the magic value - * `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))`; otherwise, - * the transfer is reverted. - * @dev Requires the msg sender to be the owner, approved, or operator - * @param _from current owner of the token - * @param _to address to receive the ownership of the given token ID - * @param _tokenId uint256 ID of the token to be transferred - * @param _data bytes data to send along with a safe transfer check - */ - function safeTransferFrom( - address _from, - address _to, - uint256 _tokenId, - bytes _data) - public - canTransfer(_tokenId) - { - transferFrom(_from, _to, _tokenId); - // solium-disable-next-line arg-overflow - require(checkAndCallSafeTransfer(_from, _to, _tokenId, _data)); - } - - /** - * @dev Returns whether the given spender can transfer a given token ID - * @param _spender address of the spender to query - * @param _tokenId uint256 ID of the token to be transferred - * @return bool whether the msg.sender is approved for the given token ID, - * is an operator of the owner, or is the owner of the token - */ - function isApprovedOrOwner(address _spender, uint256 _tokenId) - internal - view - returns (bool) - { - address owner = ownerOf(_tokenId); - return _spender == owner || getApproved(_tokenId) == _spender || isApprovedForAll(owner, _spender); - } - - /** - * @dev Internal function to mint a new token - * @dev Reverts if the given token ID already exists - * @param _to The address that will own the minted token - * @param _tokenId uint256 ID of the token to be minted by the msg.sender - */ - function _mint(address _to, uint256 _tokenId) - internal - { - require(_to != address(0)); - addTokenTo(_to, _tokenId); - emit Transfer(address(0), _to, _tokenId); - } - - /** - * @dev Internal function to burn a specific token - * @dev Reverts if the token does not exist - * @param _tokenId uint256 ID of the token being burned by the msg.sender - */ - function _burn(address _owner, uint256 _tokenId) - internal - { - clearApproval(_owner, _tokenId); - removeTokenFrom(_owner, _tokenId); - emit Transfer(_owner, address(0), _tokenId); - } - - /** - * @dev Internal function to clear current approval of a given token ID - * @dev Reverts if the given address is not indeed the owner of the token - * @param _owner owner of the token - * @param _tokenId uint256 ID of the token to be transferred - */ - function clearApproval(address _owner, uint256 _tokenId) - internal - { - require(ownerOf(_tokenId) == _owner); - if (tokenApprovals[_tokenId] != address(0)) { - tokenApprovals[_tokenId] = address(0); - emit Approval(_owner, address(0), _tokenId); - } - } - - /** - * @dev Internal function to add a token ID to the list of a given address - * @param _to address representing the new owner of the given token ID - * @param _tokenId uint256 ID of the token to be added to the tokens list of the given address - */ - function addTokenTo(address _to, uint256 _tokenId) - internal - { - require(tokenOwner[_tokenId] == address(0)); - tokenOwner[_tokenId] = _to; - ownedTokensCount[_to] = safeAdd(ownedTokensCount[_to], 1); - } - - /** - * @dev Internal function to remove a token ID from the list of a given address - * @param _from address representing the previous owner of the given token ID - * @param _tokenId uint256 ID of the token to be removed from the tokens list of the given address - */ - function removeTokenFrom(address _from, uint256 _tokenId) - internal - { - require(ownerOf(_tokenId) == _from); - ownedTokensCount[_from] = safeSub(ownedTokensCount[_from], 1); - tokenOwner[_tokenId] = address(0); - } - - /** - * @dev Internal function to invoke `onERC721Received` on a target address - * @dev The call is not executed if the target address is not a contract - * @param _from address representing the previous owner of the given token ID - * @param _to target address that will receive the tokens - * @param _tokenId uint256 ID of the token to be transferred - * @param _data bytes optional data to send along with the call - * @return whether the call correctly returned the expected magic value - */ - function checkAndCallSafeTransfer( - address _from, - address _to, - uint256 _tokenId, - bytes _data) - internal - returns (bool) - { - if (!isContract(_to)) { - return true; - } - bytes4 retval = IERC721Receiver(_to).onERC721Received(_from, _tokenId, _data); - return (retval == ERC721_RECEIVED); - } - - function isContract(address addr) - internal - view - returns (bool) - { - uint256 size; - // XXX Currently there is no better way to check if there is a contract in an address - // than to check the size of the code at that address. - // See https://ethereum.stackexchange.com/a/14016/36603 - // for more details about how this works. - // TODO Check this again before the Serenity release, because all addresses will be - // contracts then. - assembly { size := extcodesize(addr) } // solium-disable-line security/no-inline-assembly - return size > 0; - } } diff --git a/packages/contracts/src/2.0.0/tokens/ERC721Token/IERC721Receiver.sol b/packages/contracts/src/2.0.0/tokens/ERC721Token/IERC721Receiver.sol index f2e8f3c88..8e0e32ab2 100644 --- a/packages/contracts/src/2.0.0/tokens/ERC721Token/IERC721Receiver.sol +++ b/packages/contracts/src/2.0.0/tokens/ERC721Token/IERC721Receiver.sol @@ -1,61 +1,44 @@ /* -The MIT License (MIT) - -Copyright (c) 2016 Smart Contract Solutions, Inc. - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + 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; -/** - * @title ERC721 token receiver interface - * @dev Interface for any contract that wants to support safeTransfers - * rom ERC721 asset contracts. - * Modified from https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/token/ERC721/ERC721Receiver.sol - */ contract IERC721Receiver { - /** - * @dev Magic value to be returned upon successful reception of an NFT - * Equals to `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))`, - * which can be also obtained as `ERC721Receiver(0).onERC721Received.selector` - */ - bytes4 constant internal ERC721_RECEIVED = 0xf0b9e5ba; - - /** - * @notice Handle the receipt of an NFT - * @dev The ERC721 smart contract calls this function on the recipient - * after a `safetransfer`. This function MAY throw to revert and reject the - * transfer. This function MUST use 50,000 gas or less. Return of other - * than the magic value MUST result in the transaction being reverted. - * Note: the contract address is always the message sender. - * @param _from The sending address - * @param _tokenId The NFT identifier which is being transfered - * @param _data Additional data with no specified format - * @return `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))` - */ + + /// @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) - public + bytes _data + ) + external returns (bytes4); } diff --git a/packages/contracts/src/2.0.0/tokens/ERC721Token/IERC721Token.sol b/packages/contracts/src/2.0.0/tokens/ERC721Token/IERC721Token.sol index 4d57ece38..ac992c80d 100644 --- a/packages/contracts/src/2.0.0/tokens/ERC721Token/IERC721Token.sol +++ b/packages/contracts/src/2.0.0/tokens/ERC721Token/IERC721Token.sol @@ -1,118 +1,158 @@ /* -The MIT License (MIT) - -Copyright (c) 2016 Smart Contract Solutions, Inc. - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + 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; -/** - * @title ERC721 Non-Fungible Token Standard basic interface - * @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md - * Modified from https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/token/ERC721/ERC721Basic.sol - */ contract IERC721Token { - string internal name_; - string internal symbol_; + /// @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 _tokenId + 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 _tokenId + 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 ); - function name() - public - view - returns (string); - - function symbol() - public - view - returns (string); + /// @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) - public + external view - returns (uint256 _balance); + 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 _owner); + returns (address); - function exists(uint256 _tokenId) + /// @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 (bool _exists); - - function approve(address _to, uint256 _tokenId) - public; - - function getApproved(uint256 _tokenId) - public - view - returns (address _operator); - - function setApprovalForAll(address _operator, bool _approved) - public; - + 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); - - function transferFrom( - address _from, - address _to, - uint256 _tokenId - ) - public; - - function safeTransferFrom( - address _from, - address _to, - uint256 _tokenId - ) - public; - - function safeTransferFrom( - address _from, - address _to, - uint256 _tokenId, - bytes _data - ) - public; } diff --git a/packages/contracts/src/2.0.0/tokens/ERC721Token/MintableERC721Token.sol b/packages/contracts/src/2.0.0/tokens/ERC721Token/MintableERC721Token.sol new file mode 100644 index 000000000..85d192779 --- /dev/null +++ b/packages/contracts/src/2.0.0/tokens/ERC721Token/MintableERC721Token.sol @@ -0,0 +1,83 @@ +/* + + 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 + ); + } +} -- cgit v1.2.3 From bb3c34589130c6453f4a7ec5a96c75f729b24a67 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Thu, 26 Jul 2018 16:30:07 -0700 Subject: Update ERC20Token --- .../2.0.0/test/DummyERC20Token/DummyERC20Token.sol | 8 +-- .../DummyERC721Receiver/DummyERC721Receiver.sol | 4 ++ .../contracts/src/2.0.0/test/Mintable/Mintable.sol | 2 +- .../src/2.0.0/tokens/ERC20Token/ERC20Token.sol | 61 ++++++++++++++++---- .../src/2.0.0/tokens/ERC20Token/IERC20Token.sol | 65 +++++++++++++--------- .../UnlimitedAllowanceToken.sol | 20 +++++-- 6 files changed, 113 insertions(+), 47 deletions(-) (limited to 'packages/contracts') diff --git a/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol b/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol index 9272b18a8..20f36db31 100644 --- a/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol +++ b/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol @@ -41,7 +41,7 @@ contract DummyERC20Token is name = _name; symbol = _symbol; decimals = _decimals; - totalSupply = _totalSupply; + _totalSupply = _totalSupply; balances[msg.sender] = _totalSupply; } @@ -49,11 +49,11 @@ contract DummyERC20Token is public onlyOwner { - uint256 currBalance = balanceOf(_target); + uint256 currBalance = balances[_target]; if (_value < currBalance) { - totalSupply = safeSub(totalSupply, safeSub(currBalance, _value)); + _totalSupply = safeSub(_totalSupply, safeSub(currBalance, _value)); } else { - totalSupply = safeAdd(totalSupply, safeSub(_value, currBalance)); + _totalSupply = safeAdd(_totalSupply, safeSub(_value, currBalance)); } balances[_target] = _value; } diff --git a/packages/contracts/src/2.0.0/test/DummyERC721Receiver/DummyERC721Receiver.sol b/packages/contracts/src/2.0.0/test/DummyERC721Receiver/DummyERC721Receiver.sol index 5dce74a14..a29ef078f 100644 --- a/packages/contracts/src/2.0.0/test/DummyERC721Receiver/DummyERC721Receiver.sol +++ b/packages/contracts/src/2.0.0/test/DummyERC721Receiver/DummyERC721Receiver.sol @@ -32,6 +32,10 @@ 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 from, uint256 tokenId, diff --git a/packages/contracts/src/2.0.0/test/Mintable/Mintable.sol b/packages/contracts/src/2.0.0/test/Mintable/Mintable.sol index 767cc8d25..8e2ecbf40 100644 --- a/packages/contracts/src/2.0.0/test/Mintable/Mintable.sol +++ b/packages/contracts/src/2.0.0/test/Mintable/Mintable.sol @@ -38,6 +38,6 @@ contract Mintable is "Minting more than 100000000000000000000 is not allowed." ); balances[msg.sender] = safeAdd(_value, balances[msg.sender]); - totalSupply = safeAdd(totalSupply, _value); + _totalSupply = safeAdd(_totalSupply, _value); } } diff --git a/packages/contracts/src/2.0.0/tokens/ERC20Token/ERC20Token.sol b/packages/contracts/src/2.0.0/tokens/ERC20Token/ERC20Token.sol index d9950145d..563c84b5b 100644 --- a/packages/contracts/src/2.0.0/tokens/ERC20Token/ERC20Token.sol +++ b/packages/contracts/src/2.0.0/tokens/ERC20Token/ERC20Token.sol @@ -21,15 +21,21 @@ pragma solidity 0.4.24; import "./IERC20Token.sol"; -contract ERC20Token is IERC20Token { +contract ERC20Token is + IERC20Token +{ mapping (address => uint256) internal balances; mapping (address => mapping (address => uint256)) internal allowed; - uint256 public totalSupply; + uint256 internal _totalSupply; + /// @dev send `value` token to `to` from `msg.sender` + /// @param _to The address of the recipient + /// @param _value The amount of token to be transferred + /// @return True if transfer was successful function transfer(address _to, uint256 _value) - public + external returns (bool) { require( @@ -38,7 +44,7 @@ contract ERC20Token is IERC20Token { ); require( balances[_to] + _value >= balances[_to], - "OVERFLOW" + "UINT256_OVERFLOW" ); balances[msg.sender] -= _value; balances[_to] += _value; @@ -46,8 +52,17 @@ contract ERC20Token is IERC20Token { return true; } - function transferFrom(address _from, address _to, uint256 _value) - public + /// @dev send `value` token to `to` from `from` on the condition it is approved by `from` + /// @param _from The address of the sender + /// @param _to The address of the recipient + /// @param _value The amount of token to be transferred + /// @return True if transfer was successful + function transferFrom( + address _from, + address _to, + uint256 _value + ) + external returns (bool) { require( @@ -60,7 +75,7 @@ contract ERC20Token is IERC20Token { ); require( balances[_to] + _value >= balances[_to], - "OVERFLOW" + "UINT256_OVERFLOW" ); balances[_to] += _value; balances[_from] -= _value; @@ -69,25 +84,49 @@ contract ERC20Token is IERC20Token { return true; } + /// @dev `msg.sender` approves `_spender` to spend `_value` tokens + /// @param _spender The address of the account able to transfer the tokens + /// @param _value The amount of wei to be approved for transfer + /// @return Always true if enough call has enough gas to complete execution function approve(address _spender, uint256 _value) - public + external returns (bool) { allowed[msg.sender][_spender] = _value; - emit Approval(msg.sender, _spender, _value); + emit Approval( + msg.sender, + _spender, + _value + ); return true; } + /// @dev Query total supply of token + /// @return Total supply of token + function totalSupply() + external + view + returns (uint256) + { + return _totalSupply; + } + + /// @dev Query the balance of owner + /// @param _owner The address from which the balance will be retrieved + /// @return Balance of owner function balanceOf(address _owner) - public + external view returns (uint256) { return balances[_owner]; } + /// @param _owner The address of the account owning tokens + /// @param _spender The address of the account able to transfer the tokens + /// @return Amount of remaining tokens allowed to spent function allowance(address _owner, address _spender) - public + external view returns (uint256) { diff --git a/packages/contracts/src/2.0.0/tokens/ERC20Token/IERC20Token.sol b/packages/contracts/src/2.0.0/tokens/ERC20Token/IERC20Token.sol index 5ee5e1011..a752d3869 100644 --- a/packages/contracts/src/2.0.0/tokens/ERC20Token/IERC20Token.sol +++ b/packages/contracts/src/2.0.0/tokens/ERC20Token/IERC20Token.sol @@ -21,54 +21,67 @@ pragma solidity 0.4.24; contract IERC20Token { - /// @notice send `value` token to `to` from `msg.sender` + // solhint-disable-next-line no-simple-event-func-name + event Transfer( + address indexed _from, + address indexed _to, + uint256 _value + ); + + event Approval( + address indexed _owner, + address indexed _spender, + uint256 _value + ); + + /// @dev send `value` token to `to` from `msg.sender` /// @param _to The address of the recipient /// @param _value The amount of token to be transferred - /// @return Whether the transfer was successful or not + /// @return True if transfer was successful function transfer(address _to, uint256 _value) - public + external returns (bool); - /// @notice send `value` token to `to` from `from` on the condition it is approved by `from` + /// @dev send `value` token to `to` from `from` on the condition it is approved by `from` /// @param _from The address of the sender /// @param _to The address of the recipient /// @param _value The amount of token to be transferred - /// @return Whether the transfer was successful or not - function transferFrom(address _from, address _to, uint256 _value) - public + /// @return True if transfer was successful + function transferFrom( + address _from, + address _to, + uint256 _value + ) + external returns (bool); - /// @notice `msg.sender` approves `_spender` to spend `_value` tokens + /// @dev `msg.sender` approves `_spender` to spend `_value` tokens /// @param _spender The address of the account able to transfer the tokens /// @param _value The amount of wei to be approved for transfer - /// @return Whether the approval was successful or not + /// @return Always true if enough call has enough gas to complete execution function approve(address _spender, uint256 _value) - public + external returns (bool); + /// @dev Query total supply of token + /// @return Total supply of token + function totalSupply() + external + view + returns (uint256); + /// @param _owner The address from which the balance will be retrieved - /// @return The balance + /// @return Balance of owner function balanceOf(address _owner) - public view + external + view returns (uint256); /// @param _owner The address of the account owning tokens /// @param _spender The address of the account able to transfer the tokens /// @return Amount of remaining tokens allowed to spent function allowance(address _owner, address _spender) - public view + external + view returns (uint256); - - // solhint-disable-next-line no-simple-event-func-name - event Transfer( - address indexed _from, - address indexed _to, - uint256 _value - ); - - event Approval( - address indexed _owner, - address indexed _spender, - uint256 _value - ); } diff --git a/packages/contracts/src/2.0.0/tokens/UnlimitedAllowanceToken/UnlimitedAllowanceToken.sol b/packages/contracts/src/2.0.0/tokens/UnlimitedAllowanceToken/UnlimitedAllowanceToken.sol index 9feb5c914..07b85ddf4 100644 --- a/packages/contracts/src/2.0.0/tokens/UnlimitedAllowanceToken/UnlimitedAllowanceToken.sol +++ b/packages/contracts/src/2.0.0/tokens/UnlimitedAllowanceToken/UnlimitedAllowanceToken.sol @@ -21,7 +21,9 @@ pragma solidity 0.4.24; import "../ERC20Token/ERC20Token.sol"; -contract UnlimitedAllowanceToken is ERC20Token { +contract UnlimitedAllowanceToken is + ERC20Token +{ uint256 constant internal MAX_UINT = 2**256 - 1; @@ -30,8 +32,12 @@ contract UnlimitedAllowanceToken is ERC20Token { /// @param _to Address to transfer to. /// @param _value Amount to transfer. /// @return Success of transfer. - function transferFrom(address _from, address _to, uint256 _value) - public + function transferFrom( + address _from, + address _to, + uint256 _value + ) + external returns (bool) { uint256 allowance = allowed[_from][msg.sender]; @@ -45,14 +51,18 @@ contract UnlimitedAllowanceToken is ERC20Token { ); require( balances[_to] + _value >= balances[_to], - "OVERFLOW" + "UINT256_OVERFLOW" ); balances[_to] += _value; balances[_from] -= _value; if (allowance < MAX_UINT) { allowed[_from][msg.sender] -= _value; } - emit Transfer(_from, _to, _value); + emit Transfer( + _from, + _to, + _value + ); return true; } } -- cgit v1.2.3 From 2743eee044edfd3dafc012e31f60ecf4ee0ce30f Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Thu, 26 Jul 2018 16:30:44 -0700 Subject: Update checking for erc721 existence --- packages/contracts/test/utils/erc721_wrapper.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'packages/contracts') diff --git a/packages/contracts/test/utils/erc721_wrapper.ts b/packages/contracts/test/utils/erc721_wrapper.ts index d023b4d02..0c12130a3 100644 --- a/packages/contracts/test/utils/erc721_wrapper.ts +++ b/packages/contracts/test/utils/erc721_wrapper.ts @@ -35,8 +35,6 @@ export class ERC721Wrapper { artifacts.DummyERC721Token, this._provider, txDefaults, - constants.DUMMY_TOKEN_NAME, - constants.DUMMY_TOKEN_SYMBOL, ), ); } @@ -81,7 +79,8 @@ export class ERC721Wrapper { } public async doesTokenExistAsync(tokenAddress: string, tokenId: BigNumber): Promise { const tokenContract = this._getTokenContractFromAssetData(tokenAddress); - const doesExist = await tokenContract.exists.callAsync(tokenId); + const owner = await tokenContract.ownerOf.callAsync(tokenId); + const doesExist = owner !== constants.NULL_ADDRESS; return doesExist; } public async approveProxyAsync(tokenAddress: string, tokenId: BigNumber): Promise { -- cgit v1.2.3 From f5459164d283c4b55a0514faf3213334c45fea50 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Thu, 26 Jul 2018 17:18:43 -0700 Subject: Restructure directories --- .../2.0.0/test/DummyERC20Token/DummyERC20Token.sol | 24 ++++++-- .../contracts/src/2.0.0/test/Mintable/Mintable.sol | 43 ------------- .../src/2.0.0/tokens/ERC20Token/ERC20Token.sol | 18 +++++- .../src/2.0.0/tokens/ERC20Token/IERC20Token.sol | 4 +- .../2.0.0/tokens/ERC20Token/MintableERC20Token.sol | 61 +++++++++++++++++++ .../ERC20Token/UnlimitedAllowanceERC20Token.sol | 71 ++++++++++++++++++++++ .../UnlimitedAllowanceToken.sol | 68 --------------------- 7 files changed, 170 insertions(+), 119 deletions(-) delete mode 100644 packages/contracts/src/2.0.0/test/Mintable/Mintable.sol create mode 100644 packages/contracts/src/2.0.0/tokens/ERC20Token/MintableERC20Token.sol create mode 100644 packages/contracts/src/2.0.0/tokens/ERC20Token/UnlimitedAllowanceERC20Token.sol delete mode 100644 packages/contracts/src/2.0.0/tokens/UnlimitedAllowanceToken/UnlimitedAllowanceToken.sol (limited to 'packages/contracts') diff --git a/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol b/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol index 20f36db31..79a023f73 100644 --- a/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol +++ b/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol @@ -18,13 +18,13 @@ pragma solidity 0.4.24; -import "../Mintable/Mintable.sol"; import "../../utils/Ownable/Ownable.sol"; +import "../../tokens/ERC20Token/MintableERC20Token.sol"; contract DummyERC20Token is - Mintable, - Ownable + Ownable, + MintableERC20Token { string public name; string public symbol; @@ -45,8 +45,11 @@ contract DummyERC20Token is balances[msg.sender] = _totalSupply; } + /// @dev Sets the balance of target address + /// @param _target Address or which balance will be updated + /// @param _value New balance of target address function setBalance(address _target, uint256 _value) - public + external onlyOwner { uint256 currBalance = balances[_target]; @@ -57,4 +60,17 @@ contract DummyERC20Token is } balances[_target] = _value; } + + /// @dev Mints new tokens for sender + /// @param _value Amount of tokens to mint + function mint(uint256 _value) + external + { + require( + _value <= 100000000000000000000, + "VALUE_TOO_LARGE" + ); + + _mint(msg.sender, _value); + } } diff --git a/packages/contracts/src/2.0.0/test/Mintable/Mintable.sol b/packages/contracts/src/2.0.0/test/Mintable/Mintable.sol deleted file mode 100644 index 8e2ecbf40..000000000 --- a/packages/contracts/src/2.0.0/test/Mintable/Mintable.sol +++ /dev/null @@ -1,43 +0,0 @@ -/* - - 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 "../../tokens/UnlimitedAllowanceToken/UnlimitedAllowanceToken.sol"; -import "../../utils/SafeMath/SafeMath.sol"; - - -/* - * Mintable - * Base contract that creates a mintable UnlimitedAllowanceToken - */ -contract Mintable is - UnlimitedAllowanceToken, - SafeMath -{ - function mint(uint256 _value) - public - { - require( - _value <= 100000000000000000000, - "Minting more than 100000000000000000000 is not allowed." - ); - balances[msg.sender] = safeAdd(_value, balances[msg.sender]); - _totalSupply = safeAdd(_totalSupply, _value); - } -} diff --git a/packages/contracts/src/2.0.0/tokens/ERC20Token/ERC20Token.sol b/packages/contracts/src/2.0.0/tokens/ERC20Token/ERC20Token.sol index 563c84b5b..db2d09b9d 100644 --- a/packages/contracts/src/2.0.0/tokens/ERC20Token/ERC20Token.sol +++ b/packages/contracts/src/2.0.0/tokens/ERC20Token/ERC20Token.sol @@ -46,9 +46,16 @@ contract ERC20Token is balances[_to] + _value >= balances[_to], "UINT256_OVERFLOW" ); + balances[msg.sender] -= _value; balances[_to] += _value; - emit Transfer(msg.sender, _to, _value); + + emit Transfer( + msg.sender, + _to, + _value + ); + return true; } @@ -77,10 +84,17 @@ contract ERC20Token is balances[_to] + _value >= balances[_to], "UINT256_OVERFLOW" ); + balances[_to] += _value; balances[_from] -= _value; allowed[_from][msg.sender] -= _value; - emit Transfer(_from, _to, _value); + + emit Transfer( + _from, + _to, + _value + ); + return true; } diff --git a/packages/contracts/src/2.0.0/tokens/ERC20Token/IERC20Token.sol b/packages/contracts/src/2.0.0/tokens/ERC20Token/IERC20Token.sol index a752d3869..462be17fd 100644 --- a/packages/contracts/src/2.0.0/tokens/ERC20Token/IERC20Token.sol +++ b/packages/contracts/src/2.0.0/tokens/ERC20Token/IERC20Token.sol @@ -21,13 +21,13 @@ pragma solidity 0.4.24; contract IERC20Token { - // solhint-disable-next-line no-simple-event-func-name + // solhint-disable no-simple-event-func-name event Transfer( address indexed _from, address indexed _to, uint256 _value ); - + event Approval( address indexed _owner, address indexed _spender, diff --git a/packages/contracts/src/2.0.0/tokens/ERC20Token/MintableERC20Token.sol b/packages/contracts/src/2.0.0/tokens/ERC20Token/MintableERC20Token.sol new file mode 100644 index 000000000..cd1c7b4bb --- /dev/null +++ b/packages/contracts/src/2.0.0/tokens/ERC20Token/MintableERC20Token.sol @@ -0,0 +1,61 @@ +/* + + 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 "../../utils/SafeMath/SafeMath.sol"; +import "./UnlimitedAllowanceERC20Token.sol"; + + +contract MintableERC20Token is + SafeMath, + UnlimitedAllowanceERC20Token +{ + + /// @dev Mints new tokens + /// @param _to Address of the beneficiary that will own the minted token + /// @param _value Amount of tokens to mint + function _mint(address _to, uint256 _value) + internal + { + balances[_to] = safeAdd(_value, balances[_to]); + _totalSupply = safeAdd(_totalSupply, _value); + + emit Transfer( + address(0), + _to, + _value + ); + } + + /// @dev Mints new tokens + /// @param _owner Owner of tokens that will be burned + /// @param _value Amount of tokens to burn + function _burn(address _owner, uint256 _value) + internal + { + balances[_owner] = safeSub(balances[_owner], _value); + _totalSupply = safeSub(_totalSupply, _value); + + emit Transfer( + _owner, + address(0), + _value + ); + } +} diff --git a/packages/contracts/src/2.0.0/tokens/ERC20Token/UnlimitedAllowanceERC20Token.sol b/packages/contracts/src/2.0.0/tokens/ERC20Token/UnlimitedAllowanceERC20Token.sol new file mode 100644 index 000000000..e6f7c063e --- /dev/null +++ b/packages/contracts/src/2.0.0/tokens/ERC20Token/UnlimitedAllowanceERC20Token.sol @@ -0,0 +1,71 @@ +/* + + 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 "../ERC20Token/ERC20Token.sol"; + + +contract UnlimitedAllowanceERC20Token is + ERC20Token +{ + + uint256 constant internal MAX_UINT = 2**256 - 1; + + /// @dev ERC20 transferFrom, modified such that an allowance of MAX_UINT represents an unlimited allowance. See https://github.com/ethereum/EIPs/issues/717 + /// @param _from Address to transfer from. + /// @param _to Address to transfer to. + /// @param _value Amount to transfer. + /// @return Success of transfer. + function transferFrom( + address _from, + address _to, + uint256 _value + ) + external + returns (bool) + { + uint256 allowance = allowed[_from][msg.sender]; + require( + balances[_from] >= _value, + "ERC20_INSUFFICIENT_BALANCE" + ); + require( + allowance >= _value, + "ERC20_INSUFFICIENT_ALLOWANCE" + ); + require( + balances[_to] + _value >= balances[_to], + "UINT256_OVERFLOW" + ); + + balances[_to] += _value; + balances[_from] -= _value; + if (allowance < MAX_UINT) { + allowed[_from][msg.sender] -= _value; + } + + emit Transfer( + _from, + _to, + _value + ); + + return true; + } +} diff --git a/packages/contracts/src/2.0.0/tokens/UnlimitedAllowanceToken/UnlimitedAllowanceToken.sol b/packages/contracts/src/2.0.0/tokens/UnlimitedAllowanceToken/UnlimitedAllowanceToken.sol deleted file mode 100644 index 07b85ddf4..000000000 --- a/packages/contracts/src/2.0.0/tokens/UnlimitedAllowanceToken/UnlimitedAllowanceToken.sol +++ /dev/null @@ -1,68 +0,0 @@ -/* - - 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 "../ERC20Token/ERC20Token.sol"; - - -contract UnlimitedAllowanceToken is - ERC20Token -{ - - uint256 constant internal MAX_UINT = 2**256 - 1; - - /// @dev ERC20 transferFrom, modified such that an allowance of MAX_UINT represents an unlimited allowance. See https://github.com/ethereum/EIPs/issues/717 - /// @param _from Address to transfer from. - /// @param _to Address to transfer to. - /// @param _value Amount to transfer. - /// @return Success of transfer. - function transferFrom( - address _from, - address _to, - uint256 _value - ) - external - returns (bool) - { - uint256 allowance = allowed[_from][msg.sender]; - require( - balances[_from] >= _value, - "ERC20_INSUFFICIENT_BALANCE" - ); - require( - allowance >= _value, - "ERC20_INSUFFICIENT_ALLOWANCE" - ); - require( - balances[_to] + _value >= balances[_to], - "UINT256_OVERFLOW" - ); - balances[_to] += _value; - balances[_from] -= _value; - if (allowance < MAX_UINT) { - allowed[_from][msg.sender] -= _value; - } - emit Transfer( - _from, - _to, - _value - ); - return true; - } -} -- cgit v1.2.3 From 7a6e6473866dae794b9876f900e5d6f5efd15911 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Fri, 27 Jul 2018 13:28:16 -0700 Subject: Add constructor back to DummyERC721Token --- .../src/2.0.0/test/DummyERC721Token/DummyERC721Token.sol | 12 ++++++++++++ packages/contracts/test/utils/erc721_wrapper.ts | 2 ++ 2 files changed, 14 insertions(+) (limited to 'packages/contracts') diff --git a/packages/contracts/src/2.0.0/test/DummyERC721Token/DummyERC721Token.sol b/packages/contracts/src/2.0.0/test/DummyERC721Token/DummyERC721Token.sol index 7c64005bd..e995cc967 100644 --- a/packages/contracts/src/2.0.0/test/DummyERC721Token/DummyERC721Token.sol +++ b/packages/contracts/src/2.0.0/test/DummyERC721Token/DummyERC721Token.sol @@ -27,6 +27,18 @@ 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 diff --git a/packages/contracts/test/utils/erc721_wrapper.ts b/packages/contracts/test/utils/erc721_wrapper.ts index 0c12130a3..743d10706 100644 --- a/packages/contracts/test/utils/erc721_wrapper.ts +++ b/packages/contracts/test/utils/erc721_wrapper.ts @@ -35,6 +35,8 @@ export class ERC721Wrapper { artifacts.DummyERC721Token, this._provider, txDefaults, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, ), ); } -- cgit v1.2.3 From 4dd59a370d77e6aee2a95fc2e3b04646dd4f1f02 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Mon, 30 Jul 2018 17:14:00 -0700 Subject: Add tests for ERC20 token with no return values --- packages/contracts/compiler.json | 1 + packages/contracts/package.json | 12 +- .../DummyERC20Token/DummyNoReturnERC20Token.sol | 116 ++++++++++++++++++ packages/contracts/test/exchange/core.ts | 133 +++++++++++++++++++++ packages/contracts/test/exchange/fill_order.ts | 2 +- packages/contracts/test/utils/artifacts.ts | 2 + 6 files changed, 261 insertions(+), 5 deletions(-) create mode 100644 packages/contracts/src/2.0.0/test/DummyERC20Token/DummyNoReturnERC20Token.sol (limited to 'packages/contracts') diff --git a/packages/contracts/compiler.json b/packages/contracts/compiler.json index ad35fc5b3..0d938b8ea 100644 --- a/packages/contracts/compiler.json +++ b/packages/contracts/compiler.json @@ -23,6 +23,7 @@ "DummyERC20Token", "DummyERC721Receiver", "DummyERC721Token", + "DummyNoReturnERC20Token", "ERC20Proxy", "ERC20Token", "ERC721Token", diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 8bd808a27..29a440bcc 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -20,11 +20,14 @@ "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", + "run_mocha": + "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit", "compile": "sol-compiler --contracts-dir src", "clean": "shx rm -rf lib generated_contract_wrappers", - "generate_contract_wrappers": "abi-gen --abis ${npm_package_config_abis} --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output generated_contract_wrappers --backend ethers", - "lint": "tslint --project . --exclude **/src/generated_contract_wrappers/**/* --exclude **/lib/**/* && yarn lint-contracts", + "generate_contract_wrappers": + "abi-gen --abis ${npm_package_config_abis} --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output generated_contract_wrappers --backend ethers", + "lint": + "tslint --project . --exclude **/src/generated_contract_wrappers/**/* --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", @@ -33,7 +36,8 @@ "lint-contracts": "solhint src/2.0.0/**/**/**/**/*.sol" }, "config": { - "abis": "../migrations/artifacts/2.0.0/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|ERC20Proxy|ERC721Proxy|Forwarder|Exchange|ExchangeWrapper|IAssetData|IAssetProxy|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|TestAssetProxyOwner|TestAssetProxyDispatcher|TestConstants|TestExchangeInternals|TestLibBytes|TestLibs|TestSignatureValidator|Validator|Wallet|TokenRegistry|Whitelist|WETH9|ZRXToken).json" + "abis": + "../migrations/artifacts/2.0.0/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|DummyNoReturnERC20Token|ERC20Proxy|ERC721Proxy|Forwarder|Exchange|ExchangeWrapper|IAssetData|IAssetProxy|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|TestAssetProxyOwner|TestAssetProxyDispatcher|TestConstants|TestExchangeInternals|TestLibBytes|TestLibs|TestSignatureValidator|Validator|Wallet|TokenRegistry|Whitelist|WETH9|ZRXToken).json" }, "repository": { "type": "git", diff --git a/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyNoReturnERC20Token.sol b/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyNoReturnERC20Token.sol new file mode 100644 index 000000000..79156d3dd --- /dev/null +++ b/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyNoReturnERC20Token.sol @@ -0,0 +1,116 @@ +/* + + 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 "./DummyERC20Token.sol"; + + +// solhint-disable no-empty-blocks +contract DummyNoReturnERC20Token is + DummyERC20Token +{ + + constructor ( + string _name, + string _symbol, + uint256 _decimals, + uint256 _totalSupply + ) + public + DummyERC20Token( + _name, + _symbol, + _decimals, + _totalSupply + ) + {} + + /// @dev send `value` token to `to` from `msg.sender` + /// @param _to The address of the recipient + /// @param _value The amount of token to be transferred + function transfer(address _to, uint256 _value) + external + returns (bool) + { + require( + balances[msg.sender] >= _value, + "ERC20_INSUFFICIENT_BALANCE" + ); + require( + balances[_to] + _value >= balances[_to], + "UINT256_OVERFLOW" + ); + + balances[msg.sender] -= _value; + balances[_to] += _value; + + emit Transfer( + msg.sender, + _to, + _value + ); + + // HACK: This contract will not compile if we remove `returns (bool)`, so we manually return no data + assembly { + return(0, 0) + } + } + + /// @dev send `value` token to `to` from `from` on the condition it is approved by `from` + /// @param _from The address of the sender + /// @param _to The address of the recipient + /// @param _value The amount of token to be transferred + function transferFrom( + address _from, + address _to, + uint256 _value + ) + external + returns (bool) + { + require( + balances[_from] >= _value, + "ERC20_INSUFFICIENT_BALANCE" + ); + require( + allowed[_from][msg.sender] >= _value, + "ERC20_INSUFFICIENT_ALLOWANCE" + ); + require( + balances[_to] + _value >= balances[_to], + "UINT256_OVERFLOW" + ); + + balances[_to] += _value; + balances[_from] -= _value; + allowed[_from][msg.sender] -= _value; + + emit Transfer( + _from, + _to, + _value + ); + + // HACK: This contract will not compile if we remove `returns (bool)`, so we manually return no data + assembly { + return(0, 0) + } + } +} + diff --git a/packages/contracts/test/exchange/core.ts b/packages/contracts/test/exchange/core.ts index b324988cc..bc2bad749 100644 --- a/packages/contracts/test/exchange/core.ts +++ b/packages/contracts/test/exchange/core.ts @@ -10,6 +10,7 @@ import * as _ from 'lodash'; import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; import { DummyERC721TokenContract } from '../../generated_contract_wrappers/dummy_erc721_token'; +import { DummyNoReturnERC20TokenContract } from '../../generated_contract_wrappers/dummy_no_return_erc20_token'; import { ERC20ProxyContract } from '../../generated_contract_wrappers/erc20_proxy'; import { ERC721ProxyContract } from '../../generated_contract_wrappers/erc721_proxy'; import { ExchangeCancelEventArgs, ExchangeContract } from '../../generated_contract_wrappers/exchange'; @@ -39,6 +40,7 @@ describe('Exchange core', () => { let erc20TokenB: DummyERC20TokenContract; let zrxToken: DummyERC20TokenContract; let erc721Token: DummyERC721TokenContract; + let noReturnErc20Token: DummyNoReturnERC20TokenContract; let exchange: ExchangeContract; let erc20Proxy: ERC20ProxyContract; let erc721Proxy: ERC721ProxyContract; @@ -161,6 +163,137 @@ describe('Exchange core', () => { }); }); + describe('Testing exchange of ERC20 tokens with no return values', () => { + before(async () => { + noReturnErc20Token = await DummyNoReturnERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.DummyNoReturnERC20Token, + provider, + txDefaults, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + constants.DUMMY_TOKEN_DECIMALS, + constants.DUMMY_TOKEN_TOTAL_SUPPLY, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await noReturnErc20Token.setBalance.sendTransactionAsync(makerAddress, constants.INITIAL_ERC20_BALANCE), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await noReturnErc20Token.approve.sendTransactionAsync( + erc20Proxy.address, + constants.INITIAL_ERC20_ALLOWANCE, + { from: makerAddress }, + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + }); + it('should transfer the correct amounts when makerAssetAmount === takerAssetAmount', async () => { + signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(noReturnErc20Token.address), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + }); + + const initialMakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(makerAddress); + const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const initialTakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(takerAddress); + const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + const initialFeeRecipientZrxBalance = await zrxToken.balanceOf.callAsync(feeRecipientAddress); + + await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress); + + const finalMakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(makerAddress); + const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const finalTakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(takerAddress); + const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + const finalFeeRecipientZrxBalance = await zrxToken.balanceOf.callAsync(feeRecipientAddress); + + expect(finalMakerBalanceA).to.be.bignumber.equal(initialMakerBalanceA.minus(signedOrder.makerAssetAmount)); + expect(finalMakerBalanceB).to.be.bignumber.equal(initialMakerBalanceB.plus(signedOrder.takerAssetAmount)); + expect(finalTakerBalanceA).to.be.bignumber.equal(initialTakerBalanceA.plus(signedOrder.makerAssetAmount)); + expect(finalTakerBalanceB).to.be.bignumber.equal(initialTakerBalanceB.minus(signedOrder.takerAssetAmount)); + expect(finalMakerZrxBalance).to.be.bignumber.equal(initialMakerZrxBalance.minus(signedOrder.makerFee)); + expect(finalTakerZrxBalance).to.be.bignumber.equal(initialTakerZrxBalance.minus(signedOrder.takerFee)); + expect(finalFeeRecipientZrxBalance).to.be.bignumber.equal( + initialFeeRecipientZrxBalance.plus(signedOrder.makerFee.plus(signedOrder.takerFee)), + ); + }); + it('should transfer the correct amounts when makerAssetAmount > takerAssetAmount', async () => { + signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(noReturnErc20Token.address), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + }); + + const initialMakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(makerAddress); + const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const initialTakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(takerAddress); + const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + const initialFeeRecipientZrxBalance = await zrxToken.balanceOf.callAsync(feeRecipientAddress); + + await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress); + + const finalMakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(makerAddress); + const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const finalTakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(takerAddress); + const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + const finalFeeRecipientZrxBalance = await zrxToken.balanceOf.callAsync(feeRecipientAddress); + + expect(finalMakerBalanceA).to.be.bignumber.equal(initialMakerBalanceA.minus(signedOrder.makerAssetAmount)); + expect(finalMakerBalanceB).to.be.bignumber.equal(initialMakerBalanceB.plus(signedOrder.takerAssetAmount)); + expect(finalTakerBalanceA).to.be.bignumber.equal(initialTakerBalanceA.plus(signedOrder.makerAssetAmount)); + expect(finalTakerBalanceB).to.be.bignumber.equal(initialTakerBalanceB.minus(signedOrder.takerAssetAmount)); + expect(finalMakerZrxBalance).to.be.bignumber.equal(initialMakerZrxBalance.minus(signedOrder.makerFee)); + expect(finalTakerZrxBalance).to.be.bignumber.equal(initialTakerZrxBalance.minus(signedOrder.takerFee)); + expect(finalFeeRecipientZrxBalance).to.be.bignumber.equal( + initialFeeRecipientZrxBalance.plus(signedOrder.makerFee.plus(signedOrder.takerFee)), + ); + }); + it('should transfer the correct amounts when makerAssetAmount < takerAssetAmount', async () => { + signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(noReturnErc20Token.address), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18), + }); + + const initialMakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(makerAddress); + const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const initialTakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(takerAddress); + const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + const initialFeeRecipientZrxBalance = await zrxToken.balanceOf.callAsync(feeRecipientAddress); + + await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress); + + const finalMakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(makerAddress); + const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const finalTakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(takerAddress); + const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + const finalFeeRecipientZrxBalance = await zrxToken.balanceOf.callAsync(feeRecipientAddress); + + expect(finalMakerBalanceA).to.be.bignumber.equal(initialMakerBalanceA.minus(signedOrder.makerAssetAmount)); + expect(finalMakerBalanceB).to.be.bignumber.equal(initialMakerBalanceB.plus(signedOrder.takerAssetAmount)); + expect(finalTakerBalanceA).to.be.bignumber.equal(initialTakerBalanceA.plus(signedOrder.makerAssetAmount)); + expect(finalTakerBalanceB).to.be.bignumber.equal(initialTakerBalanceB.minus(signedOrder.takerAssetAmount)); + expect(finalMakerZrxBalance).to.be.bignumber.equal(initialMakerZrxBalance.minus(signedOrder.makerFee)); + expect(finalTakerZrxBalance).to.be.bignumber.equal(initialTakerZrxBalance.minus(signedOrder.takerFee)); + expect(finalFeeRecipientZrxBalance).to.be.bignumber.equal( + initialFeeRecipientZrxBalance.plus(signedOrder.makerFee.plus(signedOrder.takerFee)), + ); + }); + }); + describe('cancelOrder', () => { beforeEach(async () => { erc20Balances = await erc20Wrapper.getBalancesAsync(); diff --git a/packages/contracts/test/exchange/fill_order.ts b/packages/contracts/test/exchange/fill_order.ts index e79e2239e..b1e08324f 100644 --- a/packages/contracts/test/exchange/fill_order.ts +++ b/packages/contracts/test/exchange/fill_order.ts @@ -231,7 +231,7 @@ describe('FillOrder Tests', () => { }); }); - describe('Testing Exchange of ERC721 Tokens', () => { + describe('Testing exchange of ERC721 Tokens', () => { it('should successfully exchange a single token between the maker and taker (via fillOrder)', async () => { const fillScenario = { ...defaultFillScenario, diff --git a/packages/contracts/test/utils/artifacts.ts b/packages/contracts/test/utils/artifacts.ts index e608ee174..65f72015f 100644 --- a/packages/contracts/test/utils/artifacts.ts +++ b/packages/contracts/test/utils/artifacts.ts @@ -4,6 +4,7 @@ import * as AssetProxyOwner from '../../artifacts/AssetProxyOwner.json'; import * as DummyERC20Token from '../../artifacts/DummyERC20Token.json'; import * as DummyERC721Receiver from '../../artifacts/DummyERC721Receiver.json'; import * as DummyERC721Token from '../../artifacts/DummyERC721Token.json'; +import * as DummyNoReturnERC20Token from '../../artifacts/DummyNoReturnERC20Token.json'; import * as ERC20Proxy from '../../artifacts/ERC20Proxy.json'; import * as ERC721Proxy from '../../artifacts/ERC721Proxy.json'; import * as Exchange from '../../artifacts/Exchange.json'; @@ -32,6 +33,7 @@ export const artifacts = { DummyERC20Token: (DummyERC20Token as any) as ContractArtifact, DummyERC721Receiver: (DummyERC721Receiver as any) as ContractArtifact, DummyERC721Token: (DummyERC721Token as any) as ContractArtifact, + DummyNoReturnERC20Token: (DummyNoReturnERC20Token as any) as ContractArtifact, ERC20Proxy: (ERC20Proxy as any) as ContractArtifact, ERC721Proxy: (ERC721Proxy as any) as ContractArtifact, Exchange: (Exchange as any) as ContractArtifact, -- cgit v1.2.3 From 1d70724bcf8ee594317274d26b033e69937aacf0 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Tue, 31 Jul 2018 13:24:53 -0700 Subject: Fix DummyERC721Receiver --- .../DummyERC721Receiver/DummyERC721Receiver.sol | 64 +++++++++++----------- 1 file changed, 32 insertions(+), 32 deletions(-) (limited to 'packages/contracts') diff --git a/packages/contracts/src/2.0.0/test/DummyERC721Receiver/DummyERC721Receiver.sol b/packages/contracts/src/2.0.0/test/DummyERC721Receiver/DummyERC721Receiver.sol index a29ef078f..ac95e47bd 100644 --- a/packages/contracts/src/2.0.0/test/DummyERC721Receiver/DummyERC721Receiver.sol +++ b/packages/contracts/src/2.0.0/test/DummyERC721Receiver/DummyERC721Receiver.sol @@ -1,26 +1,19 @@ /* -The MIT License (MIT) -Copyright (c) 2016 Smart Contract Solutions, Inc. + Copyright 2018 ZeroEx Intl. -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: + 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 -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. + 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. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ pragma solidity 0.4.24; @@ -37,32 +30,39 @@ contract DummyERC721Receiver is 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 `safetransfer`. This function MAY throw to revert and reject the - * transfer. This function MUST use 50,000 gas or less. Return of other - * than the magic value MUST result in the transaction being reverted. - * Note: the contract address is always the message sender. - * @param _from The sending address - * @param _tokenId The NFT identifier which is being transfered - * @param _data Additional data with no specified format - * @return `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))` - */ + /// @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 ) - public + external returns (bytes4) { - emit TokenReceived(_from, _tokenId, _data); + emit TokenReceived( + _operator, + _from, + _tokenId, + _data + ); return ERC721_RECEIVED; } } -- cgit v1.2.3 From 6dde6d7cb7b9e69fc8f9a7660dfa0141ff6db4b2 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Thu, 9 Aug 2018 17:21:36 -0700 Subject: Increase max mintable amount --- packages/contracts/src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/contracts') diff --git a/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol b/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol index 79a023f73..c6797c0d8 100644 --- a/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol +++ b/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol @@ -67,7 +67,7 @@ contract DummyERC20Token is external { require( - _value <= 100000000000000000000, + _value <= 10000000000000000000000, "VALUE_TOO_LARGE" ); -- cgit v1.2.3 From a82e36c1d473d7fde5af52683f7a04f74a1c63c1 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Sun, 12 Aug 2018 19:33:37 -0500 Subject: Add ERC721Receiver that returns incorrect value --- packages/contracts/compiler.json | 1 + packages/contracts/package.json | 2 +- .../DummyERC721Receiver/InvalidERC721Receiver.sol | 66 ++++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 packages/contracts/src/2.0.0/test/DummyERC721Receiver/InvalidERC721Receiver.sol (limited to 'packages/contracts') diff --git a/packages/contracts/compiler.json b/packages/contracts/compiler.json index 0d938b8ea..60605e23b 100644 --- a/packages/contracts/compiler.json +++ b/packages/contracts/compiler.json @@ -33,6 +33,7 @@ "Forwarder", "IAssetData", "IAssetProxy", + "InvalidERC721Receiver", "IValidator", "IWallet", "MixinAuthorizable", diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 29a440bcc..b25b33c20 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -37,7 +37,7 @@ }, "config": { "abis": - "../migrations/artifacts/2.0.0/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|DummyNoReturnERC20Token|ERC20Proxy|ERC721Proxy|Forwarder|Exchange|ExchangeWrapper|IAssetData|IAssetProxy|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|TestAssetProxyOwner|TestAssetProxyDispatcher|TestConstants|TestExchangeInternals|TestLibBytes|TestLibs|TestSignatureValidator|Validator|Wallet|TokenRegistry|Whitelist|WETH9|ZRXToken).json" + "../migrations/artifacts/2.0.0/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|DummyNoReturnERC20Token|ERC20Proxy|ERC721Proxy|Forwarder|Exchange|ExchangeWrapper|IAssetData|IAssetProxy|InvalidERC721Receiver|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|TestAssetProxyOwner|TestAssetProxyDispatcher|TestConstants|TestExchangeInternals|TestLibBytes|TestLibs|TestSignatureValidator|Validator|Wallet|TokenRegistry|Whitelist|WETH9|ZRXToken).json" }, "repository": { "type": "git", diff --git a/packages/contracts/src/2.0.0/test/DummyERC721Receiver/InvalidERC721Receiver.sol b/packages/contracts/src/2.0.0/test/DummyERC721Receiver/InvalidERC721Receiver.sol new file mode 100644 index 000000000..309633bf5 --- /dev/null +++ b/packages/contracts/src/2.0.0/test/DummyERC721Receiver/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 "../../tokens/ERC721Token/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; + } +} -- cgit v1.2.3 From 0b9a9d92afc48745b2e4b438f31640c362882f78 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Sun, 12 Aug 2018 19:34:03 -0500 Subject: Add tests for ERC721Token --- packages/contracts/test/tokens/erc721_token.ts | 278 +++++++++++++++++++++ .../test/tokens/unlimited_allowance_token.ts | 2 +- packages/contracts/test/utils/artifacts.ts | 2 + packages/contracts/test/utils/log_decoder.ts | 1 - 4 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 packages/contracts/test/tokens/erc721_token.ts (limited to 'packages/contracts') diff --git a/packages/contracts/test/tokens/erc721_token.ts b/packages/contracts/test/tokens/erc721_token.ts new file mode 100644 index 000000000..0981e0f15 --- /dev/null +++ b/packages/contracts/test/tokens/erc721_token.ts @@ -0,0 +1,278 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { RevertReason } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import { LogWithDecodedArgs } from 'ethereum-types'; + +import { DummyERC721ReceiverContract } from '../../generated_contract_wrappers/dummy_erc721_receiver'; +import { + DummyERC721TokenContract, + DummyERC721TokenTransferEventArgs, +} from '../../generated_contract_wrappers/dummy_erc721_token'; +import { + InvalidERC721ReceiverContract, + InvalidERC721ReceiverTokenReceivedEventArgs, +} from '../../generated_contract_wrappers/invalid_erc721_receiver'; +import { artifacts } from '../utils/artifacts'; +import { expectContractCallFailed, expectContractCallFailedWithoutReasonAsync } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { LogDecoder } from '../utils/log_decoder'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +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, token.address); + 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 expectContractCallFailed( + 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 expectContractCallFailed( + 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 expectContractCallFailed( + 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 expectContractCallFailed( + 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; + 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; + 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 is 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; + 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; + 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 expectContractCallFailedWithoutReasonAsync( + 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 expectContractCallFailed( + 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; + const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs; + 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; + 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 expectContractCallFailedWithoutReasonAsync( + 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 expectContractCallFailed( + 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; + const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs; + 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); + }); + }); +}); diff --git a/packages/contracts/test/tokens/unlimited_allowance_token.ts b/packages/contracts/test/tokens/unlimited_allowance_token.ts index 81d931fc5..f2725b408 100644 --- a/packages/contracts/test/tokens/unlimited_allowance_token.ts +++ b/packages/contracts/test/tokens/unlimited_allowance_token.ts @@ -17,7 +17,7 @@ const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); describe('UnlimitedAllowanceToken', () => { let owner: string; let spender: string; - const MAX_MINT_VALUE = new BigNumber(100000000000000000000); + const MAX_MINT_VALUE = new BigNumber(10000000000000000000000); let token: DummyERC20TokenContract; before(async () => { diff --git a/packages/contracts/test/utils/artifacts.ts b/packages/contracts/test/utils/artifacts.ts index 65f72015f..e86ad5406 100644 --- a/packages/contracts/test/utils/artifacts.ts +++ b/packages/contracts/test/utils/artifacts.ts @@ -11,6 +11,7 @@ import * as Exchange from '../../artifacts/Exchange.json'; import * as ExchangeWrapper from '../../artifacts/ExchangeWrapper.json'; import * as Forwarder from '../../artifacts/Forwarder.json'; import * as IAssetProxy from '../../artifacts/IAssetProxy.json'; +import * as InvalidERC721Receiver from '../../artifacts/InvalidERC721Receiver.json'; import * as MixinAuthorizable from '../../artifacts/MixinAuthorizable.json'; import * as MultiSigWallet from '../../artifacts/MultiSigWallet.json'; import * as MultiSigWalletWithTimeLock from '../../artifacts/MultiSigWalletWithTimeLock.json'; @@ -41,6 +42,7 @@ export const artifacts = { EtherToken: (EtherToken as any) as ContractArtifact, Forwarder: (Forwarder as any) as ContractArtifact, IAssetProxy: (IAssetProxy as any) as ContractArtifact, + InvalidERC721Receiver: (InvalidERC721Receiver as any) as ContractArtifact, MixinAuthorizable: (MixinAuthorizable as any) as ContractArtifact, MultiSigWallet: (MultiSigWallet as any) as ContractArtifact, MultiSigWalletWithTimeLock: (MultiSigWalletWithTimeLock as any) as ContractArtifact, diff --git a/packages/contracts/test/utils/log_decoder.ts b/packages/contracts/test/utils/log_decoder.ts index 5a4801319..31e8679be 100644 --- a/packages/contracts/test/utils/log_decoder.ts +++ b/packages/contracts/test/utils/log_decoder.ts @@ -48,7 +48,6 @@ export class LogDecoder { } public async getTxWithDecodedLogsAsync(txHash: string): Promise { const tx = await this._web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); - tx.logs = _.filter(tx.logs, log => log.address === this._contractAddress); tx.logs = _.map(tx.logs, log => this.decodeLogOrThrow(log)); return tx; } -- cgit v1.2.3 From a4d6bc3190049996e68076eaf56762c05a71c88e Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Sun, 12 Aug 2018 19:56:09 -0500 Subject: Fix tests that assumed logs were being filtered --- packages/contracts/test/asset_proxy/proxies.ts | 2 +- packages/contracts/test/multisig/asset_proxy_owner.ts | 4 ++-- packages/contracts/test/utils/exchange_wrapper.ts | 3 +++ packages/contracts/test/utils/fill_order_combinatorial_utils.ts | 6 +++++- 4 files changed, 11 insertions(+), 4 deletions(-) (limited to 'packages/contracts') diff --git a/packages/contracts/test/asset_proxy/proxies.ts b/packages/contracts/test/asset_proxy/proxies.ts index 62215f08d..a35426190 100644 --- a/packages/contracts/test/asset_proxy/proxies.ts +++ b/packages/contracts/test/asset_proxy/proxies.ts @@ -271,7 +271,7 @@ describe('Asset Transfer Proxies', () => { }), ); // Verify that no log was emitted by erc721 receiver - expect(tx.logs.length).to.be.equal(0); + expect(tx.logs.length).to.be.equal(1); // Verify transfer was successful const newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); expect(newOwnerMakerAsset).to.be.bignumber.equal(erc721Receiver.address); diff --git a/packages/contracts/test/multisig/asset_proxy_owner.ts b/packages/contracts/test/multisig/asset_proxy_owner.ts index 6b98605d3..9515941ff 100644 --- a/packages/contracts/test/multisig/asset_proxy_owner.ts +++ b/packages/contracts/test/multisig/asset_proxy_owner.ts @@ -422,7 +422,7 @@ describe('AssetProxyOwner', () => { await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); const execRes = await multiSigWrapper.executeRemoveAuthorizedAddressAtIndexAsync(txId, owners[0]); - const execLog = execRes.logs[0] as LogWithDecodedArgs; + const execLog = execRes.logs[1] as LogWithDecodedArgs; expect(execLog.args.transactionId).to.be.bignumber.equal(txId); const tx = await testAssetProxyOwner.transactions.callAsync(txId); @@ -449,7 +449,7 @@ describe('AssetProxyOwner', () => { await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); const execRes = await multiSigWrapper.executeRemoveAuthorizedAddressAtIndexAsync(txId, owners[0]); - const execLog = execRes.logs[0] as LogWithDecodedArgs; + const execLog = execRes.logs[1] as LogWithDecodedArgs; expect(execLog.args.transactionId).to.be.bignumber.equal(txId); const tx = await testAssetProxyOwner.transactions.callAsync(txId); diff --git a/packages/contracts/test/utils/exchange_wrapper.ts b/packages/contracts/test/utils/exchange_wrapper.ts index d57592d6d..f2b9e76d7 100644 --- a/packages/contracts/test/utils/exchange_wrapper.ts +++ b/packages/contracts/test/utils/exchange_wrapper.ts @@ -266,4 +266,7 @@ export class ExchangeWrapper { ); return data; } + public getExchangeAddress(): string { + return this._exchange.address; + } } diff --git a/packages/contracts/test/utils/fill_order_combinatorial_utils.ts b/packages/contracts/test/utils/fill_order_combinatorial_utils.ts index f18ad0dd3..a9318571c 100644 --- a/packages/contracts/test/utils/fill_order_combinatorial_utils.ts +++ b/packages/contracts/test/utils/fill_order_combinatorial_utils.ts @@ -504,7 +504,11 @@ export class FillOrderCombinatorialUtils { const actFilledTakerAmount = await this.exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash); expect(actFilledTakerAmount).to.be.bignumber.equal(expFilledTakerAmount, 'filledTakerAmount'); - expect(txReceipt.logs.length).to.be.equal(1, 'logs length'); + const exchangeLogs = _.filter( + txReceipt.logs, + txLog => txLog.address === this.exchangeWrapper.getExchangeAddress(), + ); + expect(exchangeLogs.length).to.be.equal(1, 'logs length'); // tslint:disable-next-line:no-unnecessary-type-assertion const log = txReceipt.logs[0] as LogWithDecodedArgs; expect(log.args.makerAddress).to.be.equal(makerAddress, 'log.args.makerAddress'); -- cgit v1.2.3 From 633e6c38c8fe47a8a1ee6fc406704aefa6ed0b3a Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Sun, 12 Aug 2018 22:20:43 -0700 Subject: Fix linting errors --- packages/contracts/test/asset_proxy/proxies.ts | 2 +- packages/contracts/test/exchange/dispatcher.ts | 2 +- packages/contracts/test/exchange/signature_validator.ts | 2 +- packages/contracts/test/tokens/erc721_token.ts | 5 +++-- packages/contracts/test/utils/exchange_wrapper.ts | 2 +- packages/contracts/test/utils/forwarder_wrapper.ts | 2 +- packages/contracts/test/utils/log_decoder.ts | 4 +--- packages/contracts/test/utils/multi_sig_wrapper.ts | 2 +- 8 files changed, 10 insertions(+), 11 deletions(-) (limited to 'packages/contracts') diff --git a/packages/contracts/test/asset_proxy/proxies.ts b/packages/contracts/test/asset_proxy/proxies.ts index a35426190..76a020222 100644 --- a/packages/contracts/test/asset_proxy/proxies.ts +++ b/packages/contracts/test/asset_proxy/proxies.ts @@ -261,7 +261,7 @@ describe('Asset Transfer Proxies', () => { erc721Receiver.address, amount, ); - const logDecoder = new LogDecoder(web3Wrapper, erc721Receiver.address); + const logDecoder = new LogDecoder(web3Wrapper); const tx = await logDecoder.getTxWithDecodedLogsAsync( await web3Wrapper.sendTransactionAsync({ to: erc721Proxy.address, diff --git a/packages/contracts/test/exchange/dispatcher.ts b/packages/contracts/test/exchange/dispatcher.ts index 81d142ca4..81871a680 100644 --- a/packages/contracts/test/exchange/dispatcher.ts +++ b/packages/contracts/test/exchange/dispatcher.ts @@ -145,7 +145,7 @@ describe('AssetProxyDispatcher', () => { }); it('should log an event with correct arguments when an asset proxy is registered', async () => { - const logDecoder = new LogDecoder(web3Wrapper, assetProxyDispatcher.address); + const logDecoder = new LogDecoder(web3Wrapper); const txReceipt = await logDecoder.getTxWithDecodedLogsAsync( await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }), ); diff --git a/packages/contracts/test/exchange/signature_validator.ts b/packages/contracts/test/exchange/signature_validator.ts index 56a06c247..62aba45b8 100644 --- a/packages/contracts/test/exchange/signature_validator.ts +++ b/packages/contracts/test/exchange/signature_validator.ts @@ -65,7 +65,7 @@ describe('MixinSignatureValidator', () => { txDefaults, signerAddress, ); - signatureValidatorLogDecoder = new LogDecoder(web3Wrapper, signatureValidator.address); + signatureValidatorLogDecoder = new LogDecoder(web3Wrapper); await web3Wrapper.awaitTransactionSuccessAsync( await signatureValidator.setSignatureValidatorApproval.sendTransactionAsync(testValidator.address, true, { from: signerAddress, diff --git a/packages/contracts/test/tokens/erc721_token.ts b/packages/contracts/test/tokens/erc721_token.ts index 0981e0f15..d61db7fbd 100644 --- a/packages/contracts/test/tokens/erc721_token.ts +++ b/packages/contracts/test/tokens/erc721_token.ts @@ -23,7 +23,7 @@ import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; 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; @@ -53,7 +53,7 @@ describe('ERC721Token', () => { provider, txDefaults, ); - logDecoder = new LogDecoder(web3Wrapper, token.address); + logDecoder = new LogDecoder(web3Wrapper); await web3Wrapper.awaitTransactionSuccessAsync( await token.mint.sendTransactionAsync(owner, tokenId, { from: owner }), constants.AWAIT_TRANSACTION_MINED_MS, @@ -276,3 +276,4 @@ describe('ERC721Token', () => { }); }); }); +// tslint:enable:no-unnecessary-type-assertion diff --git a/packages/contracts/test/utils/exchange_wrapper.ts b/packages/contracts/test/utils/exchange_wrapper.ts index f2b9e76d7..619d43994 100644 --- a/packages/contracts/test/utils/exchange_wrapper.ts +++ b/packages/contracts/test/utils/exchange_wrapper.ts @@ -17,7 +17,7 @@ export class ExchangeWrapper { constructor(exchangeContract: ExchangeContract, provider: Provider) { this._exchange = exchangeContract; this._web3Wrapper = new Web3Wrapper(provider); - this._logDecoder = new LogDecoder(this._web3Wrapper, this._exchange.address); + this._logDecoder = new LogDecoder(this._web3Wrapper); } public async fillOrderAsync( signedOrder: SignedOrder, diff --git a/packages/contracts/test/utils/forwarder_wrapper.ts b/packages/contracts/test/utils/forwarder_wrapper.ts index 5b9a63ddf..de247a878 100644 --- a/packages/contracts/test/utils/forwarder_wrapper.ts +++ b/packages/contracts/test/utils/forwarder_wrapper.ts @@ -58,7 +58,7 @@ export class ForwarderWrapper { constructor(contractInstance: ForwarderContract, provider: Provider) { this._forwarderContract = contractInstance; this._web3Wrapper = new Web3Wrapper(provider); - this._logDecoder = new LogDecoder(this._web3Wrapper, this._forwarderContract.address); + this._logDecoder = new LogDecoder(this._web3Wrapper); } public async marketSellOrdersWithEthAsync( orders: SignedOrder[], diff --git a/packages/contracts/test/utils/log_decoder.ts b/packages/contracts/test/utils/log_decoder.ts index 31e8679be..6064ef0f7 100644 --- a/packages/contracts/test/utils/log_decoder.ts +++ b/packages/contracts/test/utils/log_decoder.ts @@ -16,7 +16,6 @@ import { constants } from './constants'; export class LogDecoder { private readonly _web3Wrapper: Web3Wrapper; - private readonly _contractAddress: string; private readonly _abiDecoder: AbiDecoder; public static wrapLogBigNumbers(log: any): any { const argNames = _.keys(log.args); @@ -27,9 +26,8 @@ export class LogDecoder { } } } - constructor(web3Wrapper: Web3Wrapper, contractAddress: string) { + constructor(web3Wrapper: Web3Wrapper) { this._web3Wrapper = web3Wrapper; - this._contractAddress = contractAddress; const abiArrays: AbiDefinition[][] = []; _.forEach(artifacts, (artifact: ContractArtifact) => { const compilerOutput = artifact.compilerOutput; diff --git a/packages/contracts/test/utils/multi_sig_wrapper.ts b/packages/contracts/test/utils/multi_sig_wrapper.ts index 8c8055d4a..e0c27b839 100644 --- a/packages/contracts/test/utils/multi_sig_wrapper.ts +++ b/packages/contracts/test/utils/multi_sig_wrapper.ts @@ -16,7 +16,7 @@ export class MultiSigWrapper { constructor(multiSigContract: MultiSigWalletContract, provider: Provider) { this._multiSig = multiSigContract; this._web3Wrapper = new Web3Wrapper(provider); - this._logDecoder = new LogDecoder(this._web3Wrapper, this._multiSig.address); + this._logDecoder = new LogDecoder(this._web3Wrapper); } public async submitTransactionAsync( destination: string, -- cgit v1.2.3 From 09d3d15db093555fa41b24bb3d08da44b08adba2 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Sun, 12 Aug 2018 22:41:37 -0700 Subject: Fix Geth tests --- packages/contracts/test/tokens/erc721_token.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'packages/contracts') diff --git a/packages/contracts/test/tokens/erc721_token.ts b/packages/contracts/test/tokens/erc721_token.ts index d61db7fbd..bdf4a28a4 100644 --- a/packages/contracts/test/tokens/erc721_token.ts +++ b/packages/contracts/test/tokens/erc721_token.ts @@ -14,7 +14,7 @@ import { InvalidERC721ReceiverTokenReceivedEventArgs, } from '../../generated_contract_wrappers/invalid_erc721_receiver'; import { artifacts } from '../utils/artifacts'; -import { expectContractCallFailed, expectContractCallFailedWithoutReasonAsync } from '../utils/assertions'; +import { expectTransactionFailedAsync, expectTransactionFailedWithoutReasonAsync } from '../utils/assertions'; import { chaiSetup } from '../utils/chai_setup'; import { constants } from '../utils/constants'; import { LogDecoder } from '../utils/log_decoder'; @@ -71,7 +71,7 @@ describe('ERC721Token', () => { const from = owner; const to = erc721Receiver.address; const unownedTokenId = new BigNumber(2); - await expectContractCallFailed( + await expectTransactionFailedAsync( token.transferFrom.sendTransactionAsync(from, to, unownedTokenId), RevertReason.Erc721ZeroOwner, ); @@ -79,7 +79,7 @@ describe('ERC721Token', () => { it('should revert if transferring to a null address', async () => { const from = owner; const to = constants.NULL_ADDRESS; - await expectContractCallFailed( + await expectTransactionFailedAsync( token.transferFrom.sendTransactionAsync(from, to, tokenId), RevertReason.Erc721ZeroToAddress, ); @@ -87,7 +87,7 @@ describe('ERC721Token', () => { it('should revert if the from address does not own the token', async () => { const from = spender; const to = erc721Receiver.address; - await expectContractCallFailed( + await expectTransactionFailedAsync( token.transferFrom.sendTransactionAsync(from, to, tokenId), RevertReason.Erc721OwnerMismatch, ); @@ -95,7 +95,7 @@ describe('ERC721Token', () => { 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 expectContractCallFailed( + await expectTransactionFailedAsync( token.transferFrom.sendTransactionAsync(from, to, tokenId, { from: spender }), RevertReason.Erc721InvalidSpender, ); @@ -178,7 +178,7 @@ describe('ERC721Token', () => { ); const from = owner; const to = contract.address; - await expectContractCallFailedWithoutReasonAsync( + await expectTransactionFailedWithoutReasonAsync( token.safeTransferFrom1.sendTransactionAsync(from, to, tokenId), ); }); @@ -190,7 +190,7 @@ describe('ERC721Token', () => { ); const from = owner; const to = invalidErc721Receiver.address; - await expectContractCallFailed( + await expectTransactionFailedAsync( token.safeTransferFrom1.sendTransactionAsync(from, to, tokenId), RevertReason.Erc721InvalidSelector, ); @@ -239,7 +239,7 @@ describe('ERC721Token', () => { ); const from = owner; const to = contract.address; - await expectContractCallFailedWithoutReasonAsync( + await expectTransactionFailedWithoutReasonAsync( token.safeTransferFrom2.sendTransactionAsync(from, to, tokenId, data), ); }); @@ -251,7 +251,7 @@ describe('ERC721Token', () => { ); const from = owner; const to = invalidErc721Receiver.address; - await expectContractCallFailed( + await expectTransactionFailedAsync( token.safeTransferFrom2.sendTransactionAsync(from, to, tokenId, data), RevertReason.Erc721InvalidSelector, ); -- cgit v1.2.3 From 8bce73dc679cb05353835d011b8e1dfcff7f4670 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Wed, 15 Aug 2018 10:55:48 -0700 Subject: Fix typos, run prettier, and allow anyone to call on DummyERC721Token --- .../src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol | 3 ++- .../src/2.0.0/test/DummyERC721Token/DummyERC721Token.sol | 3 +-- .../contracts/src/2.0.0/tokens/ERC20Token/ERC20Token.sol | 2 +- .../src/2.0.0/tokens/ERC20Token/IERC20Token.sol | 2 +- .../src/2.0.0/tokens/ERC721Token/ERC721Token.sol | 2 +- packages/contracts/test/tokens/erc721_token.ts | 16 ++++++++-------- 6 files changed, 14 insertions(+), 14 deletions(-) (limited to 'packages/contracts') diff --git a/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol b/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol index c6797c0d8..412c5d1ad 100644 --- a/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol +++ b/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol @@ -29,6 +29,7 @@ contract DummyERC20Token is string public name; string public symbol; uint256 public decimals; + uint256 public constant MAX_MINT_AMOUNT = 10000000000000000000000; constructor ( string _name, @@ -67,7 +68,7 @@ contract DummyERC20Token is external { require( - _value <= 10000000000000000000000, + _value <= MAX_MINT_AMOUNT, "VALUE_TOO_LARGE" ); diff --git a/packages/contracts/src/2.0.0/test/DummyERC721Token/DummyERC721Token.sol b/packages/contracts/src/2.0.0/test/DummyERC721Token/DummyERC721Token.sol index e995cc967..ac9068d1d 100644 --- a/packages/contracts/src/2.0.0/test/DummyERC721Token/DummyERC721Token.sol +++ b/packages/contracts/src/2.0.0/test/DummyERC721Token/DummyERC721Token.sol @@ -46,13 +46,12 @@ contract DummyERC721Token is /// @param _tokenId ID of the token to be minted by the msg.sender function mint(address _to, uint256 _tokenId) external - onlyOwner { _mint(_to, _tokenId); } /// @dev Function to burn a token - /// Reverts if the given token ID doesn't exist + /// 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) diff --git a/packages/contracts/src/2.0.0/tokens/ERC20Token/ERC20Token.sol b/packages/contracts/src/2.0.0/tokens/ERC20Token/ERC20Token.sol index db2d09b9d..5ef5ee7ce 100644 --- a/packages/contracts/src/2.0.0/tokens/ERC20Token/ERC20Token.sol +++ b/packages/contracts/src/2.0.0/tokens/ERC20Token/ERC20Token.sol @@ -101,7 +101,7 @@ contract ERC20Token is /// @dev `msg.sender` approves `_spender` to spend `_value` tokens /// @param _spender The address of the account able to transfer the tokens /// @param _value The amount of wei to be approved for transfer - /// @return Always true if enough call has enough gas to complete execution + /// @return Always true if the call has enough gas to complete execution function approve(address _spender, uint256 _value) external returns (bool) diff --git a/packages/contracts/src/2.0.0/tokens/ERC20Token/IERC20Token.sol b/packages/contracts/src/2.0.0/tokens/ERC20Token/IERC20Token.sol index 462be17fd..258d47393 100644 --- a/packages/contracts/src/2.0.0/tokens/ERC20Token/IERC20Token.sol +++ b/packages/contracts/src/2.0.0/tokens/ERC20Token/IERC20Token.sol @@ -58,7 +58,7 @@ contract IERC20Token { /// @dev `msg.sender` approves `_spender` to spend `_value` tokens /// @param _spender The address of the account able to transfer the tokens /// @param _value The amount of wei to be approved for transfer - /// @return Always true if enough call has enough gas to complete execution + /// @return Always true if the call has enough gas to complete execution function approve(address _spender, uint256 _value) external returns (bool); diff --git a/packages/contracts/src/2.0.0/tokens/ERC721Token/ERC721Token.sol b/packages/contracts/src/2.0.0/tokens/ERC721Token/ERC721Token.sol index 94e084731..530f080c0 100644 --- a/packages/contracts/src/2.0.0/tokens/ERC721Token/ERC721Token.sol +++ b/packages/contracts/src/2.0.0/tokens/ERC721Token/ERC721Token.sol @@ -45,7 +45,7 @@ contract ERC721Token is /// @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 + /// 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 diff --git a/packages/contracts/test/tokens/erc721_token.ts b/packages/contracts/test/tokens/erc721_token.ts index bdf4a28a4..e61fd7d51 100644 --- a/packages/contracts/test/tokens/erc721_token.ts +++ b/packages/contracts/test/tokens/erc721_token.ts @@ -4,15 +4,15 @@ import { BigNumber } from '@0xproject/utils'; import * as chai from 'chai'; import { LogWithDecodedArgs } from 'ethereum-types'; -import { DummyERC721ReceiverContract } from '../../generated_contract_wrappers/dummy_erc721_receiver'; +import { + DummyERC721ReceiverContract, + DummyERC721ReceiverTokenReceivedEventArgs, +} from '../../generated_contract_wrappers/dummy_erc721_receiver'; import { DummyERC721TokenContract, DummyERC721TokenTransferEventArgs, } from '../../generated_contract_wrappers/dummy_erc721_token'; -import { - InvalidERC721ReceiverContract, - InvalidERC721ReceiverTokenReceivedEventArgs, -} from '../../generated_contract_wrappers/invalid_erc721_receiver'; +import { InvalidERC721ReceiverContract } from '../../generated_contract_wrappers/invalid_erc721_receiver'; import { artifacts } from '../utils/artifacts'; import { expectTransactionFailedAsync, expectTransactionFailedWithoutReasonAsync } from '../utils/assertions'; import { chaiSetup } from '../utils/chai_setup'; @@ -132,7 +132,7 @@ describe('ERC721Token', () => { expect(log.args._to).to.be.equal(to); expect(log.args._tokenId).to.be.bignumber.equal(tokenId); }); - it('should transfer the token is spender is individually approved', async () => { + 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, @@ -204,7 +204,7 @@ describe('ERC721Token', () => { const newOwner = await token.ownerOf.callAsync(tokenId); expect(newOwner).to.be.equal(to); const transferLog = txReceipt.logs[0] as LogWithDecodedArgs; - const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs; + const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs; 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); @@ -265,7 +265,7 @@ describe('ERC721Token', () => { const newOwner = await token.ownerOf.callAsync(tokenId); expect(newOwner).to.be.equal(to); const transferLog = txReceipt.logs[0] as LogWithDecodedArgs; - const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs; + const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs; 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); -- cgit v1.2.3 From da3dc7affc2ad07b94a3b6c44f636f6fcbd4c3f3 Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Mon, 20 Aug 2018 13:56:19 -0700 Subject: fix(sol-cov): Remove old files and update config.contractsDir --- packages/contracts/compiler.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/contracts') diff --git a/packages/contracts/compiler.json b/packages/contracts/compiler.json index 60605e23b..09c47e6af 100644 --- a/packages/contracts/compiler.json +++ b/packages/contracts/compiler.json @@ -1,6 +1,6 @@ { "artifactsDir": "../migrations/artifacts/2.0.0", - "contractsDir": "src/contracts", + "contractsDir": "src/", "compilerSettings": { "optimizer": { "enabled": true, -- cgit v1.2.3 From 1ba26ea5e8034699315b39f2f55917abea963555 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Tue, 21 Aug 2018 10:50:09 -0700 Subject: Remove redundant constructor args --- packages/contracts/src/2.0.0/forwarder/Forwarder.sol | 4 ---- packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol | 12 ++++++++---- packages/contracts/test/forwarder/forwarder.ts | 2 -- 3 files changed, 8 insertions(+), 10 deletions(-) (limited to 'packages/contracts') diff --git a/packages/contracts/src/2.0.0/forwarder/Forwarder.sol b/packages/contracts/src/2.0.0/forwarder/Forwarder.sol index 5b88b05b1..6b17bb29b 100644 --- a/packages/contracts/src/2.0.0/forwarder/Forwarder.sol +++ b/packages/contracts/src/2.0.0/forwarder/Forwarder.sol @@ -37,16 +37,12 @@ contract Forwarder is constructor ( address _exchange, - address _etherToken, - address _zrxToken, bytes memory _zrxAssetData, bytes memory _wethAssetData ) public LibConstants( _exchange, - _etherToken, - _zrxToken, _zrxAssetData, _wethAssetData ) diff --git a/packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol b/packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol index c26d7902c..fb9691fe8 100644 --- a/packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol +++ b/packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol @@ -18,6 +18,7 @@ pragma solidity 0.4.24; +import "../../utils/LibBytes/LibBytes.sol"; import "../../protocol/Exchange/interfaces/IExchange.sol"; import "../../tokens/EtherToken/IEtherToken.sol"; import "../../tokens/ERC20Token/IERC20Token.sol"; @@ -25,6 +26,8 @@ import "../../tokens/ERC20Token/IERC20Token.sol"; contract LibConstants { + using LibBytes for bytes; + bytes4 constant internal ERC20_DATA_ID = bytes4(keccak256("ERC20Token(address)")); bytes4 constant internal ERC721_DATA_ID = bytes4(keccak256("ERC721Token(address,uint256)")); uint256 constant internal MAX_UINT = 2**256 - 1; @@ -42,17 +45,18 @@ contract LibConstants { constructor ( address _exchange, - address _etherToken, - address _zrxToken, bytes memory _zrxAssetData, bytes memory _wethAssetData ) public { EXCHANGE = IExchange(_exchange); - ETHER_TOKEN = IEtherToken(_etherToken); - ZRX_TOKEN = IERC20Token(_zrxToken); ZRX_ASSET_DATA = _zrxAssetData; WETH_ASSET_DATA = _wethAssetData; + + address etherToken = _wethAssetData.readAddress(16); + address zrxToken = _zrxAssetData.readAddress(16); + ETHER_TOKEN = IEtherToken(etherToken); + ZRX_TOKEN = IERC20Token(zrxToken); } } diff --git a/packages/contracts/test/forwarder/forwarder.ts b/packages/contracts/test/forwarder/forwarder.ts index 28ffdeabe..18101d684 100644 --- a/packages/contracts/test/forwarder/forwarder.ts +++ b/packages/contracts/test/forwarder/forwarder.ts @@ -131,8 +131,6 @@ describe(ContractName.Forwarder, () => { provider, txDefaults, exchangeInstance.address, - wethContract.address, - zrxToken.address, zrxAssetData, wethAssetData, ); -- cgit v1.2.3 From be67c25b0a3dcf2eaf91c39e4d8a8cdb1215bbe0 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Tue, 14 Aug 2018 15:39:10 -0700 Subject: Add OrderValidator contract --- .../src/2.0.0/extensions/Forwarder/Forwarder.sol | 51 ++++ .../src/2.0.0/extensions/Forwarder/MixinAssets.sol | 144 +++++++++++ .../extensions/Forwarder/MixinExchangeWrapper.sol | 263 +++++++++++++++++++++ .../extensions/Forwarder/MixinForwarderCore.sol | 213 +++++++++++++++++ .../src/2.0.0/extensions/Forwarder/MixinWeth.sol | 114 +++++++++ .../extensions/Forwarder/interfaces/IAssets.sol | 34 +++ .../extensions/Forwarder/interfaces/IForwarder.sol | 30 +++ .../Forwarder/interfaces/IForwarderCore.sol | 80 +++++++ .../extensions/Forwarder/libs/LibConstants.sol | 62 +++++ .../Forwarder/libs/LibForwarderErrors.sol | 34 +++ .../2.0.0/extensions/Forwarder/mixins/MAssets.sol | 54 +++++ .../Forwarder/mixins/MExchangeWrapper.sol | 87 +++++++ .../2.0.0/extensions/Forwarder/mixins/MWeth.sol | 41 ++++ .../extensions/OrderValidator/OrderValidator.sol | 140 +++++++++++ .../contracts/src/2.0.0/forwarder/Forwarder.sol | 51 ---- .../contracts/src/2.0.0/forwarder/MixinAssets.sol | 144 ----------- .../src/2.0.0/forwarder/MixinExchangeWrapper.sol | 263 --------------------- .../src/2.0.0/forwarder/MixinForwarderCore.sol | 213 ----------------- .../contracts/src/2.0.0/forwarder/MixinWeth.sol | 114 --------- .../src/2.0.0/forwarder/interfaces/IAssets.sol | 34 --- .../src/2.0.0/forwarder/interfaces/IForwarder.sol | 30 --- .../2.0.0/forwarder/interfaces/IForwarderCore.sol | 80 ------- .../src/2.0.0/forwarder/libs/LibConstants.sol | 62 ----- .../2.0.0/forwarder/libs/LibForwarderErrors.sol | 34 --- .../src/2.0.0/forwarder/mixins/MAssets.sol | 54 ----- .../2.0.0/forwarder/mixins/MExchangeWrapper.sol | 87 ------- .../contracts/src/2.0.0/forwarder/mixins/MWeth.sol | 41 ---- 27 files changed, 1347 insertions(+), 1207 deletions(-) create mode 100644 packages/contracts/src/2.0.0/extensions/Forwarder/Forwarder.sol create mode 100644 packages/contracts/src/2.0.0/extensions/Forwarder/MixinAssets.sol create mode 100644 packages/contracts/src/2.0.0/extensions/Forwarder/MixinExchangeWrapper.sol create mode 100644 packages/contracts/src/2.0.0/extensions/Forwarder/MixinForwarderCore.sol create mode 100644 packages/contracts/src/2.0.0/extensions/Forwarder/MixinWeth.sol create mode 100644 packages/contracts/src/2.0.0/extensions/Forwarder/interfaces/IAssets.sol create mode 100644 packages/contracts/src/2.0.0/extensions/Forwarder/interfaces/IForwarder.sol create mode 100644 packages/contracts/src/2.0.0/extensions/Forwarder/interfaces/IForwarderCore.sol create mode 100644 packages/contracts/src/2.0.0/extensions/Forwarder/libs/LibConstants.sol create mode 100644 packages/contracts/src/2.0.0/extensions/Forwarder/libs/LibForwarderErrors.sol create mode 100644 packages/contracts/src/2.0.0/extensions/Forwarder/mixins/MAssets.sol create mode 100644 packages/contracts/src/2.0.0/extensions/Forwarder/mixins/MExchangeWrapper.sol create mode 100644 packages/contracts/src/2.0.0/extensions/Forwarder/mixins/MWeth.sol create mode 100644 packages/contracts/src/2.0.0/extensions/OrderValidator/OrderValidator.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/Forwarder.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/MixinAssets.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/MixinWeth.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/interfaces/IAssets.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/interfaces/IForwarder.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/interfaces/IForwarderCore.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/libs/LibForwarderErrors.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/mixins/MAssets.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/mixins/MExchangeWrapper.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/mixins/MWeth.sol (limited to 'packages/contracts') diff --git a/packages/contracts/src/2.0.0/extensions/Forwarder/Forwarder.sol b/packages/contracts/src/2.0.0/extensions/Forwarder/Forwarder.sol new file mode 100644 index 000000000..6b17bb29b --- /dev/null +++ b/packages/contracts/src/2.0.0/extensions/Forwarder/Forwarder.sol @@ -0,0 +1,51 @@ +/* + + 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; +pragma experimental ABIEncoderV2; + +import "./MixinWeth.sol"; +import "./MixinForwarderCore.sol"; +import "./libs/LibConstants.sol"; +import "./MixinAssets.sol"; +import "./MixinExchangeWrapper.sol"; + + +// solhint-disable no-empty-blocks +contract Forwarder is + LibConstants, + MixinWeth, + MixinAssets, + MixinExchangeWrapper, + MixinForwarderCore +{ + + constructor ( + address _exchange, + bytes memory _zrxAssetData, + bytes memory _wethAssetData + ) + public + LibConstants( + _exchange, + _zrxAssetData, + _wethAssetData + ) + MixinForwarderCore() + {} +} diff --git a/packages/contracts/src/2.0.0/extensions/Forwarder/MixinAssets.sol b/packages/contracts/src/2.0.0/extensions/Forwarder/MixinAssets.sol new file mode 100644 index 000000000..d6a38aa6e --- /dev/null +++ b/packages/contracts/src/2.0.0/extensions/Forwarder/MixinAssets.sol @@ -0,0 +1,144 @@ +/* + + 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 "../../utils/LibBytes/LibBytes.sol"; +import "../../utils/Ownable/Ownable.sol"; +import "../../tokens/ERC20Token/IERC20Token.sol"; +import "../../tokens/ERC721Token/IERC721Token.sol"; +import "./libs/LibConstants.sol"; +import "./mixins/MAssets.sol"; + + +contract MixinAssets is + Ownable, + LibConstants, + MAssets +{ + + using LibBytes for bytes; + + bytes4 constant internal ERC20_TRANSFER_SELECTOR = bytes4(keccak256("transfer(address,uint256)")); + + /// @dev Withdraws assets from this contract. The contract requires a ZRX balance in order to + /// function optimally, and this function allows the ZRX to be withdrawn by owner. It may also be + /// used to withdraw assets that were accidentally sent to this contract. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of ERC20 token to withdraw. + function withdrawAsset( + bytes assetData, + uint256 amount + ) + external + onlyOwner + { + transferAssetToSender(assetData, amount); + } + + /// @dev Transfers given amount of asset to sender. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of asset to transfer to sender. + function transferAssetToSender( + bytes memory assetData, + uint256 amount + ) + internal + { + bytes4 proxyId = assetData.readBytes4(0); + + if (proxyId == ERC20_DATA_ID) { + transferERC20Token(assetData, amount); + } else if (proxyId == ERC721_DATA_ID) { + transferERC721Token(assetData, amount); + } else { + revert("UNSUPPORTED_ASSET_PROXY"); + } + } + + /// @dev Decodes ERC20 assetData and transfers given amount to sender. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of asset to transfer to sender. + function transferERC20Token( + bytes memory assetData, + uint256 amount + ) + internal + { + address token = assetData.readAddress(16); + + // Transfer tokens. + // We do a raw call so we can check the success separate + // from the return data. + bool success = token.call(abi.encodeWithSelector( + ERC20_TRANSFER_SELECTOR, + msg.sender, + amount + )); + require( + success, + "TRANSFER_FAILED" + ); + + // Check return data. + // If there is no return data, we assume the token incorrectly + // does not return a bool. In this case we expect it to revert + // on failure, which was handled above. + // If the token does return data, we require that it is a single + // value that evaluates to true. + assembly { + if returndatasize { + success := 0 + if eq(returndatasize, 32) { + // First 64 bytes of memory are reserved scratch space + returndatacopy(0, 0, 32) + success := mload(0) + } + } + } + require( + success, + "TRANSFER_FAILED" + ); + } + + /// @dev Decodes ERC721 assetData and transfers given amount to sender. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of asset to transfer to sender. + function transferERC721Token( + bytes memory assetData, + uint256 amount + ) + internal + { + require( + amount == 1, + "INVALID_AMOUNT" + ); + // Decode asset data. + address token = assetData.readAddress(16); + uint256 tokenId = assetData.readUint256(36); + + // Perform transfer. + IERC721Token(token).transferFrom( + address(this), + msg.sender, + tokenId + ); + } +} diff --git a/packages/contracts/src/2.0.0/extensions/Forwarder/MixinExchangeWrapper.sol b/packages/contracts/src/2.0.0/extensions/Forwarder/MixinExchangeWrapper.sol new file mode 100644 index 000000000..218713d3c --- /dev/null +++ b/packages/contracts/src/2.0.0/extensions/Forwarder/MixinExchangeWrapper.sol @@ -0,0 +1,263 @@ +/* + + 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; +pragma experimental ABIEncoderV2; + +import "./libs/LibConstants.sol"; +import "./mixins/MExchangeWrapper.sol"; +import "../../protocol/Exchange/libs/LibAbiEncoder.sol"; +import "../../protocol/Exchange/libs/LibOrder.sol"; +import "../../protocol/Exchange/libs/LibFillResults.sol"; +import "../../protocol/Exchange/libs/LibMath.sol"; + + +contract MixinExchangeWrapper is + LibAbiEncoder, + LibFillResults, + LibMath, + LibConstants, + MExchangeWrapper +{ + + /// @dev Fills the input order. + /// Returns false if the transaction would otherwise revert. + /// @param order Order struct containing order specifications. + /// @param takerAssetFillAmount Desired amount of takerAsset to sell. + /// @param signature Proof that order has been created by maker. + /// @return Amounts filled and fees paid by maker and taker. + function fillOrderNoThrow( + LibOrder.Order memory order, + uint256 takerAssetFillAmount, + bytes memory signature + ) + internal + returns (FillResults memory fillResults) + { + // ABI encode calldata for `fillOrder` + bytes memory fillOrderCalldata = abiEncodeFillOrder( + order, + takerAssetFillAmount, + signature + ); + + address exchange = address(EXCHANGE); + + // Call `fillOrder` and handle any exceptions gracefully + assembly { + let success := call( + gas, // forward all gas, TODO: look into gas consumption of assert/throw + exchange, // call address of Exchange contract + 0, // transfer 0 wei + add(fillOrderCalldata, 32), // pointer to start of input (skip array length in first 32 bytes) + mload(fillOrderCalldata), // length of input + fillOrderCalldata, // write output over input + 128 // output size is 128 bytes + ) + switch success + case 0 { + mstore(fillResults, 0) + mstore(add(fillResults, 32), 0) + mstore(add(fillResults, 64), 0) + mstore(add(fillResults, 96), 0) + } + case 1 { + mstore(fillResults, mload(fillOrderCalldata)) + mstore(add(fillResults, 32), mload(add(fillOrderCalldata, 32))) + mstore(add(fillResults, 64), mload(add(fillOrderCalldata, 64))) + mstore(add(fillResults, 96), mload(add(fillOrderCalldata, 96))) + } + } + return fillResults; + } + + /// @dev Synchronously executes multiple calls of fillOrder until total amount of WETH has been sold by taker. + /// Returns false if the transaction would otherwise revert. + /// @param orders Array of order specifications. + /// @param wethSellAmount Desired amount of WETH to sell. + /// @param signatures Proofs that orders have been signed by makers. + /// @return Amounts filled and fees paid by makers and taker. + function marketSellWeth( + LibOrder.Order[] memory orders, + uint256 wethSellAmount, + bytes[] memory signatures + ) + internal + returns (FillResults memory totalFillResults) + { + bytes memory makerAssetData = orders[0].makerAssetData; + bytes memory wethAssetData = WETH_ASSET_DATA; + + uint256 ordersLength = orders.length; + for (uint256 i = 0; i != ordersLength; i++) { + + // We assume that asset being bought by taker is the same for each order. + // We assume that asset being sold by taker is WETH for each order. + orders[i].makerAssetData = makerAssetData; + orders[i].takerAssetData = wethAssetData; + + // Calculate the remaining amount of WETH to sell + uint256 remainingTakerAssetFillAmount = safeSub(wethSellAmount, totalFillResults.takerAssetFilledAmount); + + // Attempt to sell the remaining amount of WETH + FillResults memory singleFillResults = fillOrderNoThrow( + orders[i], + remainingTakerAssetFillAmount, + signatures[i] + ); + + // Update amounts filled and fees paid by maker and taker + addFillResults(totalFillResults, singleFillResults); + + // Stop execution if the entire amount of takerAsset has been sold + if (totalFillResults.takerAssetFilledAmount >= wethSellAmount) { + break; + } + } + return totalFillResults; + } + + /// @dev Synchronously executes multiple fill orders in a single transaction until total amount is bought by taker. + /// Returns false if the transaction would otherwise revert. + /// The asset being sold by taker must always be WETH. + /// @param orders Array of order specifications. + /// @param makerAssetFillAmount Desired amount of makerAsset to buy. + /// @param signatures Proofs that orders have been signed by makers. + /// @return Amounts filled and fees paid by makers and taker. + function marketBuyExactAmountWithWeth( + LibOrder.Order[] memory orders, + uint256 makerAssetFillAmount, + bytes[] memory signatures + ) + internal + returns (FillResults memory totalFillResults) + { + bytes memory makerAssetData = orders[0].makerAssetData; + bytes memory wethAssetData = WETH_ASSET_DATA; + + uint256 ordersLength = orders.length; + for (uint256 i = 0; i != ordersLength; i++) { + + // We assume that asset being bought by taker is the same for each order. + // We assume that asset being sold by taker is WETH for each order. + orders[i].makerAssetData = makerAssetData; + orders[i].takerAssetData = wethAssetData; + + // Calculate the remaining amount of makerAsset to buy + uint256 remainingMakerAssetFillAmount = safeSub(makerAssetFillAmount, totalFillResults.makerAssetFilledAmount); + + // Convert the remaining amount of makerAsset to buy into remaining amount + // of takerAsset to sell, assuming entire amount can be sold in the current order + uint256 remainingTakerAssetFillAmount = getPartialAmount( + orders[i].takerAssetAmount, + orders[i].makerAssetAmount, + remainingMakerAssetFillAmount + ); + + // Attempt to sell the remaining amount of takerAsset + FillResults memory singleFillResults = fillOrderNoThrow( + orders[i], + remainingTakerAssetFillAmount, + signatures[i] + ); + + // Update amounts filled and fees paid by maker and taker + addFillResults(totalFillResults, singleFillResults); + + // Stop execution if the entire amount of makerAsset has been bought + uint256 makerAssetFilledAmount = totalFillResults.makerAssetFilledAmount; + if (makerAssetFilledAmount >= makerAssetFillAmount) { + break; + } + } + + require( + makerAssetFilledAmount >= makerAssetFillAmount, + "COMPLETE_FILL_FAILED" + ); + return totalFillResults; + } + + /// @dev Buys zrxBuyAmount of ZRX fee tokens, taking into account ZRX fees for each order. This will guarantee + /// that at least zrxBuyAmount of ZRX is purchased (sometimes slightly over due to rounding issues). + /// It is possible that a request to buy 200 ZRX will require purchasing 202 ZRX + /// as 2 ZRX is required to purchase the 200 ZRX fee tokens. This guarantees at least 200 ZRX for future purchases. + /// The asset being sold by taker must always be WETH. + /// @param orders Array of order specifications containing ZRX as makerAsset and WETH as takerAsset. + /// @param zrxBuyAmount Desired amount of ZRX to buy. + /// @param signatures Proofs that orders have been created by makers. + /// @return totalFillResults Amounts filled and fees paid by maker and taker. + function marketBuyExactZrxWithWeth( + LibOrder.Order[] memory orders, + uint256 zrxBuyAmount, + bytes[] memory signatures + ) + internal + returns (FillResults memory totalFillResults) + { + // Do nothing if zrxBuyAmount == 0 + if (zrxBuyAmount == 0) { + return totalFillResults; + } + + bytes memory zrxAssetData = ZRX_ASSET_DATA; + bytes memory wethAssetData = WETH_ASSET_DATA; + uint256 zrxPurchased = 0; + + uint256 ordersLength = orders.length; + for (uint256 i = 0; i != ordersLength; i++) { + + // All of these are ZRX/WETH, so we can drop the respective assetData from calldata. + orders[i].makerAssetData = zrxAssetData; + orders[i].takerAssetData = wethAssetData; + + // Calculate the remaining amount of ZRX to buy. + uint256 remainingZrxBuyAmount = safeSub(zrxBuyAmount, zrxPurchased); + + // Convert the remaining amount of ZRX to buy into remaining amount + // of WETH to sell, assuming entire amount can be sold in the current order. + uint256 remainingWethSellAmount = getPartialAmount( + orders[i].takerAssetAmount, + safeSub(orders[i].makerAssetAmount, orders[i].takerFee), // our exchange rate after fees + remainingZrxBuyAmount + ); + + // Attempt to sell the remaining amount of WETH. + FillResults memory singleFillResult = fillOrderNoThrow( + orders[i], + safeAdd(remainingWethSellAmount, 1), // we add 1 wei to the fill amount to make up for rounding errors + signatures[i] + ); + + // Update amounts filled and fees paid by maker and taker. + addFillResults(totalFillResults, singleFillResult); + zrxPurchased = safeSub(totalFillResults.makerAssetFilledAmount, totalFillResults.takerFeePaid); + + // Stop execution if the entire amount of ZRX has been bought. + if (zrxPurchased >= zrxBuyAmount) { + break; + } + } + + require( + zrxPurchased >= zrxBuyAmount, + "COMPLETE_FILL_FAILED" + ); + return totalFillResults; + } +} diff --git a/packages/contracts/src/2.0.0/extensions/Forwarder/MixinForwarderCore.sol b/packages/contracts/src/2.0.0/extensions/Forwarder/MixinForwarderCore.sol new file mode 100644 index 000000000..42cec4d36 --- /dev/null +++ b/packages/contracts/src/2.0.0/extensions/Forwarder/MixinForwarderCore.sol @@ -0,0 +1,213 @@ +/* + + 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; +pragma experimental ABIEncoderV2; + +import "./libs/LibConstants.sol"; +import "./mixins/MWeth.sol"; +import "./mixins/MAssets.sol"; +import "./mixins/MExchangeWrapper.sol"; +import "./interfaces/IForwarderCore.sol"; +import "../../utils/LibBytes/LibBytes.sol"; +import "../../protocol/Exchange/libs/LibOrder.sol"; +import "../../protocol/Exchange/libs/LibFillResults.sol"; +import "../../protocol/Exchange/libs/LibMath.sol"; + + +contract MixinForwarderCore is + LibFillResults, + LibMath, + LibConstants, + MWeth, + MAssets, + MExchangeWrapper, + IForwarderCore +{ + + using LibBytes for bytes; + + /// @dev Constructor approves ERC20 proxy to transfer ZRX and WETH on this contract's behalf. + constructor () + public + { + address proxyAddress = EXCHANGE.getAssetProxy(ERC20_DATA_ID); + if (proxyAddress != address(0)) { + ETHER_TOKEN.approve(proxyAddress, MAX_UINT); + ZRX_TOKEN.approve(proxyAddress, MAX_UINT); + } + } + + /// @dev Purchases as much of orders' makerAssets as possible by selling up to 95% of transaction's ETH value. + /// Any ZRX required to pay fees for primary orders will automatically be purchased by this contract. + /// 5% of ETH value is reserved for paying fees to order feeRecipients (in ZRX) and forwarding contract feeRecipient (in ETH). + /// Any ETH not spent will be refunded to sender. + /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. + /// @param signatures Proofs that orders have been created by makers. + /// @param feeOrders Array of order specifications containing ZRX as makerAsset and WETH as takerAsset. Used to purchase ZRX for primary order fees. + /// @param feeSignatures Proofs that feeOrders have been created by makers. + /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. + /// @param feeRecipient Address that will receive ETH when orders are filled. + /// @return Amounts filled and fees paid by maker and taker for both sets of orders. + function marketSellOrdersWithEth( + LibOrder.Order[] memory orders, + bytes[] memory signatures, + LibOrder.Order[] memory feeOrders, + bytes[] memory feeSignatures, + uint256 feePercentage, + address feeRecipient + ) + public + payable + returns ( + FillResults memory orderFillResults, + FillResults memory feeOrderFillResults + ) + { + // Convert ETH to WETH. + convertEthToWeth(); + + uint256 wethSellAmount; + uint256 zrxBuyAmount; + uint256 makerAssetAmountPurchased; + if (orders[0].makerAssetData.equals(ZRX_ASSET_DATA)) { + // Calculate amount of WETH that won't be spent on ETH fees. + wethSellAmount = getPartialAmount( + PERCENTAGE_DENOMINATOR, + safeAdd(PERCENTAGE_DENOMINATOR, feePercentage), + msg.value + ); + // Market sell available WETH. + // ZRX fees are paid with this contract's balance. + orderFillResults = marketSellWeth( + orders, + wethSellAmount, + signatures + ); + // The fee amount must be deducted from the amount transfered back to sender. + makerAssetAmountPurchased = safeSub(orderFillResults.makerAssetFilledAmount, orderFillResults.takerFeePaid); + } else { + // 5% of WETH is reserved for filling feeOrders and paying feeRecipient. + wethSellAmount = getPartialAmount( + MAX_WETH_FILL_PERCENTAGE, + PERCENTAGE_DENOMINATOR, + msg.value + ); + // Market sell 95% of WETH. + // ZRX fees are payed with this contract's balance. + orderFillResults = marketSellWeth( + orders, + wethSellAmount, + signatures + ); + // Buy back all ZRX spent on fees. + zrxBuyAmount = orderFillResults.takerFeePaid; + feeOrderFillResults = marketBuyExactZrxWithWeth( + feeOrders, + zrxBuyAmount, + feeSignatures + ); + makerAssetAmountPurchased = orderFillResults.makerAssetFilledAmount; + } + + // Transfer feePercentage of total ETH spent on primary orders to feeRecipient. + // Refund remaining ETH to msg.sender. + transferEthFeeAndRefund( + orderFillResults.takerAssetFilledAmount, + feeOrderFillResults.takerAssetFilledAmount, + feePercentage, + feeRecipient + ); + + // Transfer purchased assets to msg.sender. + transferAssetToSender(orders[0].makerAssetData, makerAssetAmountPurchased); + } + + /// @dev Attempt to purchase makerAssetFillAmount of makerAsset by selling ETH provided with transaction. + /// Any ZRX required to pay fees for primary orders will automatically be purchased by this contract. + /// Any ETH not spent will be refunded to sender. + /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. + /// @param makerAssetFillAmount Desired amount of makerAsset to purchase. + /// @param signatures Proofs that orders have been created by makers. + /// @param feeOrders Array of order specifications containing ZRX as makerAsset and WETH as takerAsset. Used to purchase ZRX for primary order fees. + /// @param feeSignatures Proofs that feeOrders have been created by makers. + /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. + /// @param feeRecipient Address that will receive ETH when orders are filled. + /// @return Amounts filled and fees paid by maker and taker for both sets of orders. + function marketBuyOrdersWithEth( + LibOrder.Order[] memory orders, + uint256 makerAssetFillAmount, + bytes[] memory signatures, + LibOrder.Order[] memory feeOrders, + bytes[] memory feeSignatures, + uint256 feePercentage, + address feeRecipient + ) + public + payable + returns ( + FillResults memory orderFillResults, + FillResults memory feeOrderFillResults + ) + { + // Convert ETH to WETH. + convertEthToWeth(); + + uint256 zrxBuyAmount; + uint256 makerAssetAmountPurchased; + if (orders[0].makerAssetData.equals(ZRX_ASSET_DATA)) { + // If the makerAsset is ZRX, it is not necessary to pay fees out of this + // contracts's ZRX balance because fees are factored into the price of the order. + orderFillResults = marketBuyExactZrxWithWeth( + orders, + makerAssetFillAmount, + signatures + ); + // The fee amount must be deducted from the amount transfered back to sender. + makerAssetAmountPurchased = safeSub(orderFillResults.makerAssetFilledAmount, orderFillResults.takerFeePaid); + } else { + // Attemp to purchase desired amount of makerAsset. + // ZRX fees are payed with this contract's balance. + orderFillResults = marketBuyExactAmountWithWeth( + orders, + makerAssetFillAmount, + signatures + ); + // Buy back all ZRX spent on fees. + zrxBuyAmount = orderFillResults.takerFeePaid; + feeOrderFillResults = marketBuyExactZrxWithWeth( + feeOrders, + zrxBuyAmount, + feeSignatures + ); + makerAssetAmountPurchased = orderFillResults.makerAssetFilledAmount; + } + + // Transfer feePercentage of total ETH spent on primary orders to feeRecipient. + // Refund remaining ETH to msg.sender. + transferEthFeeAndRefund( + orderFillResults.takerAssetFilledAmount, + feeOrderFillResults.takerAssetFilledAmount, + feePercentage, + feeRecipient + ); + + // Transfer purchased assets to msg.sender. + transferAssetToSender(orders[0].makerAssetData, makerAssetAmountPurchased); + } +} diff --git a/packages/contracts/src/2.0.0/extensions/Forwarder/MixinWeth.sol b/packages/contracts/src/2.0.0/extensions/Forwarder/MixinWeth.sol new file mode 100644 index 000000000..93e85e599 --- /dev/null +++ b/packages/contracts/src/2.0.0/extensions/Forwarder/MixinWeth.sol @@ -0,0 +1,114 @@ +/* + + 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 "../../protocol/Exchange/libs/LibMath.sol"; +import "./libs/LibConstants.sol"; +import "./mixins/MWeth.sol"; + + +contract MixinWeth is + LibMath, + LibConstants, + MWeth +{ + + /// @dev Default payabale function, this allows us to withdraw WETH + function () + public + payable + { + require( + msg.sender == address(ETHER_TOKEN), + "DEFAULT_FUNCTION_WETH_CONTRACT_ONLY" + ); + } + + /// @dev Converts message call's ETH value into WETH. + function convertEthToWeth() + internal + { + require( + msg.value > 0, + "INVALID_MSG_VALUE" + ); + ETHER_TOKEN.deposit.value(msg.value)(); + } + + /// @dev Transfers feePercentage of WETH spent on primary orders to feeRecipient. + /// Refunds any excess ETH to msg.sender. + /// @param wethSoldExcludingFeeOrders Amount of WETH sold when filling primary orders. + /// @param wethSoldForZrx Amount of WETH sold when purchasing ZRX required for primary order fees. + /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. + /// @param feeRecipient Address that will receive ETH when orders are filled. + function transferEthFeeAndRefund( + uint256 wethSoldExcludingFeeOrders, + uint256 wethSoldForZrx, + uint256 feePercentage, + address feeRecipient + ) + internal + { + // Ensure feePercentage is less than 5%. + require( + feePercentage <= MAX_FEE_PERCENTAGE, + "FEE_PERCENTAGE_TOO_LARGE" + ); + + // Ensure that no extra WETH owned by this contract has been sold. + uint256 wethSold = safeAdd(wethSoldExcludingFeeOrders, wethSoldForZrx); + require( + wethSold <= msg.value, + "OVERSOLD_WETH" + ); + + // Calculate amount of WETH that hasn't been sold. + uint256 wethRemaining = safeSub(msg.value, wethSold); + + // Calculate ETH fee to pay to feeRecipient. + uint256 ethFee = getPartialAmount( + feePercentage, + PERCENTAGE_DENOMINATOR, + wethSoldExcludingFeeOrders + ); + + // Ensure fee is less than amount of WETH remaining. + require( + ethFee <= wethRemaining, + "INSUFFICIENT_ETH_REMAINING" + ); + + // Do nothing if no WETH remaining + if (wethRemaining > 0) { + // Convert remaining WETH to ETH + ETHER_TOKEN.withdraw(wethRemaining); + + // Pay ETH to feeRecipient + if (ethFee > 0) { + feeRecipient.transfer(ethFee); + } + + // Refund remaining ETH to msg.sender. + uint256 ethRefund = safeSub(wethRemaining, ethFee); + if (ethRefund > 0) { + msg.sender.transfer(ethRefund); + } + } + } +} diff --git a/packages/contracts/src/2.0.0/extensions/Forwarder/interfaces/IAssets.sol b/packages/contracts/src/2.0.0/extensions/Forwarder/interfaces/IAssets.sol new file mode 100644 index 000000000..1e034c003 --- /dev/null +++ b/packages/contracts/src/2.0.0/extensions/Forwarder/interfaces/IAssets.sol @@ -0,0 +1,34 @@ +/* + + 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 IAssets { + + /// @dev Withdraws assets from this contract. The contract requires a ZRX balance in order to + /// function optimally, and this function allows the ZRX to be withdrawn by owner. It may also be + /// used to withdraw assets that were accidentally sent to this contract. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of ERC20 token to withdraw. + function withdrawAsset( + bytes assetData, + uint256 amount + ) + external; +} diff --git a/packages/contracts/src/2.0.0/extensions/Forwarder/interfaces/IForwarder.sol b/packages/contracts/src/2.0.0/extensions/Forwarder/interfaces/IForwarder.sol new file mode 100644 index 000000000..f5a26e2ba --- /dev/null +++ b/packages/contracts/src/2.0.0/extensions/Forwarder/interfaces/IForwarder.sol @@ -0,0 +1,30 @@ +/* + + 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; +pragma experimental ABIEncoderV2; + +import "./IForwarderCore.sol"; +import "./IAssets.sol"; + + +// solhint-disable no-empty-blocks +contract IForwarder is + IForwarderCore, + IAssets +{} diff --git a/packages/contracts/src/2.0.0/extensions/Forwarder/interfaces/IForwarderCore.sol b/packages/contracts/src/2.0.0/extensions/Forwarder/interfaces/IForwarderCore.sol new file mode 100644 index 000000000..74c7da01d --- /dev/null +++ b/packages/contracts/src/2.0.0/extensions/Forwarder/interfaces/IForwarderCore.sol @@ -0,0 +1,80 @@ +/* + + 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; +pragma experimental ABIEncoderV2; + +import "../../../protocol/Exchange/libs/LibOrder.sol"; +import "../../../protocol/Exchange/libs/LibFillResults.sol"; + + +contract IForwarderCore { + + /// @dev Purchases as much of orders' makerAssets as possible by selling up to 95% of transaction's ETH value. + /// Any ZRX required to pay fees for primary orders will automatically be purchased by this contract. + /// 5% of ETH value is reserved for paying fees to order feeRecipients (in ZRX) and forwarding contract feeRecipient (in ETH). + /// Any ETH not spent will be refunded to sender. + /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. + /// @param signatures Proofs that orders have been created by makers. + /// @param feeOrders Array of order specifications containing ZRX as makerAsset and WETH as takerAsset. Used to purchase ZRX for primary order fees. + /// @param feeSignatures Proofs that feeOrders have been created by makers. + /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. + /// @param feeRecipient Address that will receive ETH when orders are filled. + /// @return Amounts filled and fees paid by maker and taker for both sets of orders. + function marketSellOrdersWithEth( + LibOrder.Order[] memory orders, + bytes[] memory signatures, + LibOrder.Order[] memory feeOrders, + bytes[] memory feeSignatures, + uint256 feePercentage, + address feeRecipient + ) + public + payable + returns ( + LibFillResults.FillResults memory orderFillResults, + LibFillResults.FillResults memory feeOrderFillResults + ); + + /// @dev Attempt to purchase makerAssetFillAmount of makerAsset by selling ETH provided with transaction. + /// Any ZRX required to pay fees for primary orders will automatically be purchased by this contract. + /// Any ETH not spent will be refunded to sender. + /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. + /// @param makerAssetFillAmount Desired amount of makerAsset to purchase. + /// @param signatures Proofs that orders have been created by makers. + /// @param feeOrders Array of order specifications containing ZRX as makerAsset and WETH as takerAsset. Used to purchase ZRX for primary order fees. + /// @param feeSignatures Proofs that feeOrders have been created by makers. + /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. + /// @param feeRecipient Address that will receive ETH when orders are filled. + /// @return Amounts filled and fees paid by maker and taker for both sets of orders. + function marketBuyOrdersWithEth( + LibOrder.Order[] memory orders, + uint256 makerAssetFillAmount, + bytes[] memory signatures, + LibOrder.Order[] memory feeOrders, + bytes[] memory feeSignatures, + uint256 feePercentage, + address feeRecipient + ) + public + payable + returns ( + LibFillResults.FillResults memory orderFillResults, + LibFillResults.FillResults memory feeOrderFillResults + ); +} diff --git a/packages/contracts/src/2.0.0/extensions/Forwarder/libs/LibConstants.sol b/packages/contracts/src/2.0.0/extensions/Forwarder/libs/LibConstants.sol new file mode 100644 index 000000000..704e42ce3 --- /dev/null +++ b/packages/contracts/src/2.0.0/extensions/Forwarder/libs/LibConstants.sol @@ -0,0 +1,62 @@ +/* + + 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 "../../../utils/LibBytes/LibBytes.sol"; +import "../../../protocol/Exchange/interfaces/IExchange.sol"; +import "../../../tokens/EtherToken/IEtherToken.sol"; +import "../../../tokens/ERC20Token/IERC20Token.sol"; + + +contract LibConstants { + + using LibBytes for bytes; + + bytes4 constant internal ERC20_DATA_ID = bytes4(keccak256("ERC20Token(address)")); + bytes4 constant internal ERC721_DATA_ID = bytes4(keccak256("ERC721Token(address,uint256)")); + uint256 constant internal MAX_UINT = 2**256 - 1; + uint256 constant internal PERCENTAGE_DENOMINATOR = 10**18; + uint256 constant internal MAX_FEE_PERCENTAGE = 5 * PERCENTAGE_DENOMINATOR / 100; // 5% + uint256 constant internal MAX_WETH_FILL_PERCENTAGE = 95 * PERCENTAGE_DENOMINATOR / 100; // 95% + + // solhint-disable var-name-mixedcase + IExchange internal EXCHANGE; + IEtherToken internal ETHER_TOKEN; + IERC20Token internal ZRX_TOKEN; + bytes internal ZRX_ASSET_DATA; + bytes internal WETH_ASSET_DATA; + // solhint-enable var-name-mixedcase + + constructor ( + address _exchange, + bytes memory _zrxAssetData, + bytes memory _wethAssetData + ) + public + { + EXCHANGE = IExchange(_exchange); + ZRX_ASSET_DATA = _zrxAssetData; + WETH_ASSET_DATA = _wethAssetData; + + address etherToken = _wethAssetData.readAddress(16); + address zrxToken = _zrxAssetData.readAddress(16); + ETHER_TOKEN = IEtherToken(etherToken); + ZRX_TOKEN = IERC20Token(zrxToken); + } +} diff --git a/packages/contracts/src/2.0.0/extensions/Forwarder/libs/LibForwarderErrors.sol b/packages/contracts/src/2.0.0/extensions/Forwarder/libs/LibForwarderErrors.sol new file mode 100644 index 000000000..fb3ade1db --- /dev/null +++ b/packages/contracts/src/2.0.0/extensions/Forwarder/libs/LibForwarderErrors.sol @@ -0,0 +1,34 @@ +/* + + 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. + +*/ + +// solhint-disable +pragma solidity 0.4.24; + + +/// This contract is intended to serve as a reference, but is not actually used for efficiency reasons. +contract LibForwarderErrors { + string constant FEE_PERCENTAGE_TOO_LARGE = "FEE_PROPORTION_TOO_LARGE"; // Provided fee percentage greater than 5%. + string constant INSUFFICIENT_ETH_REMAINING = "INSUFFICIENT_ETH_REMAINING"; // Not enough ETH remaining to pay feeRecipient. + string constant OVERSOLD_WETH = "OVERSOLD_WETH"; // More WETH sold than provided with current message call. + string constant COMPLETE_FILL_FAILED = "COMPLETE_FILL_FAILED"; // Desired purchase amount not completely filled (required for ZRX fees only). + string constant TRANSFER_FAILED = "TRANSFER_FAILED"; // Asset transfer failed. + string constant UNSUPPORTED_ASSET_PROXY = "UNSUPPORTED_ASSET_PROXY"; // Proxy in assetData not supported. + string constant DEFAULT_FUNCTION_WETH_CONTRACT_ONLY = "DEFAULT_FUNCTION_WETH_CONTRACT_ONLY"; // Fallback function may only be used for WETH withdrawals. + string constant INVALID_MSG_VALUE = "INVALID_MSG_VALUE"; // msg.value must be greater than 0. + string constant INVALID_AMOUNT = "INVALID_AMOUNT"; // Amount must equal 1. +} diff --git a/packages/contracts/src/2.0.0/extensions/Forwarder/mixins/MAssets.sol b/packages/contracts/src/2.0.0/extensions/Forwarder/mixins/MAssets.sol new file mode 100644 index 000000000..83636432a --- /dev/null +++ b/packages/contracts/src/2.0.0/extensions/Forwarder/mixins/MAssets.sol @@ -0,0 +1,54 @@ +/* + + 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/IAssets.sol"; + + +contract MAssets is + IAssets +{ + + /// @dev Transfers given amount of asset to sender. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of asset to transfer to sender. + function transferAssetToSender( + bytes memory assetData, + uint256 amount + ) + internal; + + /// @dev Decodes ERC20 assetData and transfers given amount to sender. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of asset to transfer to sender. + function transferERC20Token( + bytes memory assetData, + uint256 amount + ) + internal; + + /// @dev Decodes ERC721 assetData and transfers given amount to sender. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of asset to transfer to sender. + function transferERC721Token( + bytes memory assetData, + uint256 amount + ) + internal; +} diff --git a/packages/contracts/src/2.0.0/extensions/Forwarder/mixins/MExchangeWrapper.sol b/packages/contracts/src/2.0.0/extensions/Forwarder/mixins/MExchangeWrapper.sol new file mode 100644 index 000000000..13c26b03a --- /dev/null +++ b/packages/contracts/src/2.0.0/extensions/Forwarder/mixins/MExchangeWrapper.sol @@ -0,0 +1,87 @@ +/* + + 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; +pragma experimental ABIEncoderV2; + +import "../../../protocol/Exchange/libs/LibOrder.sol"; +import "../../../protocol/Exchange/libs/LibFillResults.sol"; + + +contract MExchangeWrapper { + + /// @dev Fills the input order. + /// Returns false if the transaction would otherwise revert. + /// @param order Order struct containing order specifications. + /// @param takerAssetFillAmount Desired amount of takerAsset to sell. + /// @param signature Proof that order has been created by maker. + /// @return Amounts filled and fees paid by maker and taker. + function fillOrderNoThrow( + LibOrder.Order memory order, + uint256 takerAssetFillAmount, + bytes memory signature + ) + internal + returns (LibFillResults.FillResults memory fillResults); + + /// @dev Synchronously executes multiple calls of fillOrder until total amount of WETH has been sold by taker. + /// Returns false if the transaction would otherwise revert. + /// @param orders Array of order specifications. + /// @param wethSellAmount Desired amount of WETH to sell. + /// @param signatures Proofs that orders have been signed by makers. + /// @return Amounts filled and fees paid by makers and taker. + function marketSellWeth( + LibOrder.Order[] memory orders, + uint256 wethSellAmount, + bytes[] memory signatures + ) + internal + returns (LibFillResults.FillResults memory totalFillResults); + + /// @dev Synchronously executes multiple fill orders in a single transaction until total amount is bought by taker. + /// Returns false if the transaction would otherwise revert. + /// The asset being sold by taker must always be WETH. + /// @param orders Array of order specifications. + /// @param makerAssetFillAmount Desired amount of makerAsset to buy. + /// @param signatures Proofs that orders have been signed by makers. + /// @return Amounts filled and fees paid by makers and taker. + function marketBuyExactAmountWithWeth( + LibOrder.Order[] memory orders, + uint256 makerAssetFillAmount, + bytes[] memory signatures + ) + internal + returns (LibFillResults.FillResults memory totalFillResults); + + /// @dev Buys zrxBuyAmount of ZRX fee tokens, taking into account ZRX fees for each order. This will guarantee + /// that at least zrxBuyAmount of ZRX is purchased (sometimes slightly over due to rounding issues). + /// It is possible that a request to buy 200 ZRX will require purchasing 202 ZRX + /// as 2 ZRX is required to purchase the 200 ZRX fee tokens. This guarantees at least 200 ZRX for future purchases. + /// The asset being sold by taker must always be WETH. + /// @param orders Array of order specifications containing ZRX as makerAsset and WETH as takerAsset. + /// @param zrxBuyAmount Desired amount of ZRX to buy. + /// @param signatures Proofs that orders have been created by makers. + /// @return totalFillResults Amounts filled and fees paid by maker and taker. + function marketBuyExactZrxWithWeth( + LibOrder.Order[] memory orders, + uint256 zrxBuyAmount, + bytes[] memory signatures + ) + internal + returns (LibFillResults.FillResults memory totalFillResults); +} diff --git a/packages/contracts/src/2.0.0/extensions/Forwarder/mixins/MWeth.sol b/packages/contracts/src/2.0.0/extensions/Forwarder/mixins/MWeth.sol new file mode 100644 index 000000000..88e77be4e --- /dev/null +++ b/packages/contracts/src/2.0.0/extensions/Forwarder/mixins/MWeth.sol @@ -0,0 +1,41 @@ +/* + + 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 MWeth { + + /// @dev Converts message call's ETH value into WETH. + function convertEthToWeth() + internal; + + /// @dev Transfers feePercentage of WETH spent on primary orders to feeRecipient. + /// Refunds any excess ETH to msg.sender. + /// @param wethSoldExcludingFeeOrders Amount of WETH sold when filling primary orders. + /// @param wethSoldForZrx Amount of WETH sold when purchasing ZRX required for primary order fees. + /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. + /// @param feeRecipient Address that will receive ETH when orders are filled. + function transferEthFeeAndRefund( + uint256 wethSoldExcludingFeeOrders, + uint256 wethSoldForZrx, + uint256 feePercentage, + address feeRecipient + ) + internal; +} diff --git a/packages/contracts/src/2.0.0/extensions/OrderValidator/OrderValidator.sol b/packages/contracts/src/2.0.0/extensions/OrderValidator/OrderValidator.sol new file mode 100644 index 000000000..dcbbd7fa5 --- /dev/null +++ b/packages/contracts/src/2.0.0/extensions/OrderValidator/OrderValidator.sol @@ -0,0 +1,140 @@ +/* + + 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; +pragma experimental ABIEncoderV2; + +import "../../protocol/Exchange/interfaces/IExchange.sol"; +import "../../protocol/Exchange/libs/LibOrder.sol"; +import "../../tokens/ERC20Token/IERC20Token.sol"; +import "../../tokens/ERC721Token/IERC721Token.sol"; +import "../../utils/LibBytes/LibBytes.sol"; + + +contract OrderValidator { + + bytes4 constant internal ERC20_DATA_ID = bytes4(keccak256("ERC20Token(address)")); + bytes4 constant internal ERC721_DATA_ID = bytes4(keccak256("ERC721Token(address,uint256)")); + + using LibBytes for bytes; + + struct TraderInfo { + uint256 makerBalance; // Maker's balance of makerAsset + uint256 makerAllowance; // Maker's allowance to corresponding AssetProxy + uint256 takerBalance; // Taker's balance of takerAsset + uint256 takerAllowance; // Taker's allowance to corresponding AssetProxy + } + + // Exchange contract. + // solhint-disable-next-line var-name-mixedcase + IExchange internal EXCHANGE; + + constructor (address _exchange) + public + { + EXCHANGE = IExchange(_exchange); + } + + /// @dev Fetches information for order and maker/taker of order. + /// @param order The order structure. + /// @param takerAddress Address that will be filling the order. + /// @return OrderInfo and TraderInfo instances for given order. + function getOrderAndTraderInfo(LibOrder.Order memory order, address takerAddress) + public + view + returns (LibOrder.OrderInfo memory orderInfo, TraderInfo memory traderInfo) + { + orderInfo = EXCHANGE.getOrderInfo(order); + traderInfo = getTraderInfo(order, takerAddress); + return (orderInfo, traderInfo); + } + + /// @dev Fetches information for all passed in orders and the makers/takers of each order. + /// @param orders Array of order specifications. + /// @param takerAddresses Array of taker addresses corresponding to each order. + /// @return Arrays of OrderInfo and TraderInfo instances that correspond to each order. + function getOrdersAndTradersInfo(LibOrder.Order[] memory orders, address[] memory takerAddresses) + public + view + returns (LibOrder.OrderInfo[] memory ordersInfo, TraderInfo[] memory tradersInfo) + { + ordersInfo = EXCHANGE.getOrdersInfo(orders); + tradersInfo = getTradersInfo(orders, takerAddresses); + return (ordersInfo, tradersInfo); + } + + /// @dev Fetches balance and allowances for maker and taker of order. + /// @param order The order structure. + /// @param takerAddress Address that will be filling the order. + /// @return Balances and allowances of maker and taker of order. + function getTraderInfo(LibOrder.Order memory order, address takerAddress) + public + view + returns (TraderInfo memory traderInfo) + { + (traderInfo.makerBalance, traderInfo.makerAllowance) = getBalanceAndAllowance(order.makerAddress, order.makerAssetData); + (traderInfo.takerBalance, traderInfo.takerAllowance) = getBalanceAndAllowance(takerAddress, order.takerAssetData); + return traderInfo; + } + + /// @dev Fetches balances and allowances of maker and taker for each provided order. + /// @param orders Array of order specifications. + /// @param takerAddresses Array of taker addresses corresponding to each order. + /// @return Array of balances and allowances for maker and taker of each order. + function getTradersInfo(LibOrder.Order[] memory orders, address[] memory takerAddresses) + public + view + returns (TraderInfo[] memory) + { + uint256 ordersLength = orders.length; + TraderInfo[] memory tradersInfo = new TraderInfo[](ordersLength); + for (uint256 i = 0; i != ordersLength; i++) { + tradersInfo[i] = getTraderInfo(orders[i], takerAddresses[i]); + } + return tradersInfo; + } + + /// @dev Fetches token balances and allowances of an address to given assetProxy. Supports ERC20 and ERC721. + /// @param target Address to fetch balances and allowances of. + /// @param assetData Encoded data that can be decoded by a specified proxy contract when transferring asset. + /// @return Balance of asset and allowance set to given proxy of asset. + /// For ERC721 tokens, these values will always be 1 or 0. + function getBalanceAndAllowance(address target, bytes memory assetData) + public + view + returns (uint256 balance, uint256 allowance) + { + bytes4 assetProxyId = assetData.readBytes4(0); + address token = assetData.readAddress(16); + address assetProxy = EXCHANGE.getAssetProxy(assetProxyId); + + if (assetProxyId == ERC20_DATA_ID) { + balance = IERC20Token(token).balanceOf(target); + allowance = IERC20Token(token).allowance(target, assetProxy); + } else if (assetProxyId == ERC721_DATA_ID) { + uint256 tokenId = assetData.readUint256(36); + address owner = IERC721Token(token).ownerOf(tokenId); + balance = target == owner ? 1 : 0; + bool isApproved = IERC721Token(token).isApprovedForAll(target, assetProxy) || IERC721Token(token).getApproved(tokenId) == assetProxy; + allowance = isApproved ? 1 : 0; + } else { + revert("UNSUPPORTED_ASSET_PROXY"); + } + return (balance, allowance); + } +} diff --git a/packages/contracts/src/2.0.0/forwarder/Forwarder.sol b/packages/contracts/src/2.0.0/forwarder/Forwarder.sol deleted file mode 100644 index 6b17bb29b..000000000 --- a/packages/contracts/src/2.0.0/forwarder/Forwarder.sol +++ /dev/null @@ -1,51 +0,0 @@ -/* - - 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; -pragma experimental ABIEncoderV2; - -import "./MixinWeth.sol"; -import "./MixinForwarderCore.sol"; -import "./libs/LibConstants.sol"; -import "./MixinAssets.sol"; -import "./MixinExchangeWrapper.sol"; - - -// solhint-disable no-empty-blocks -contract Forwarder is - LibConstants, - MixinWeth, - MixinAssets, - MixinExchangeWrapper, - MixinForwarderCore -{ - - constructor ( - address _exchange, - bytes memory _zrxAssetData, - bytes memory _wethAssetData - ) - public - LibConstants( - _exchange, - _zrxAssetData, - _wethAssetData - ) - MixinForwarderCore() - {} -} diff --git a/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol b/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol deleted file mode 100644 index e06f9a8e3..000000000 --- a/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol +++ /dev/null @@ -1,144 +0,0 @@ -/* - - 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 "../utils/LibBytes/LibBytes.sol"; -import "../utils/Ownable/Ownable.sol"; -import "../tokens/ERC20Token/IERC20Token.sol"; -import "../tokens/ERC721Token/IERC721Token.sol"; -import "./libs/LibConstants.sol"; -import "./mixins/MAssets.sol"; - - -contract MixinAssets is - Ownable, - LibConstants, - MAssets -{ - - using LibBytes for bytes; - - bytes4 constant internal ERC20_TRANSFER_SELECTOR = bytes4(keccak256("transfer(address,uint256)")); - - /// @dev Withdraws assets from this contract. The contract requires a ZRX balance in order to - /// function optimally, and this function allows the ZRX to be withdrawn by owner. It may also be - /// used to withdraw assets that were accidentally sent to this contract. - /// @param assetData Byte array encoded for the respective asset proxy. - /// @param amount Amount of ERC20 token to withdraw. - function withdrawAsset( - bytes assetData, - uint256 amount - ) - external - onlyOwner - { - transferAssetToSender(assetData, amount); - } - - /// @dev Transfers given amount of asset to sender. - /// @param assetData Byte array encoded for the respective asset proxy. - /// @param amount Amount of asset to transfer to sender. - function transferAssetToSender( - bytes memory assetData, - uint256 amount - ) - internal - { - bytes4 proxyId = assetData.readBytes4(0); - - if (proxyId == ERC20_DATA_ID) { - transferERC20Token(assetData, amount); - } else if (proxyId == ERC721_DATA_ID) { - transferERC721Token(assetData, amount); - } else { - revert("UNSUPPORTED_TOKEN_PROXY"); - } - } - - /// @dev Decodes ERC20 assetData and transfers given amount to sender. - /// @param assetData Byte array encoded for the respective asset proxy. - /// @param amount Amount of asset to transfer to sender. - function transferERC20Token( - bytes memory assetData, - uint256 amount - ) - internal - { - address token = assetData.readAddress(16); - - // Transfer tokens. - // We do a raw call so we can check the success separate - // from the return data. - bool success = token.call(abi.encodeWithSelector( - ERC20_TRANSFER_SELECTOR, - msg.sender, - amount - )); - require( - success, - "TRANSFER_FAILED" - ); - - // Check return data. - // If there is no return data, we assume the token incorrectly - // does not return a bool. In this case we expect it to revert - // on failure, which was handled above. - // If the token does return data, we require that it is a single - // value that evaluates to true. - assembly { - if returndatasize { - success := 0 - if eq(returndatasize, 32) { - // First 64 bytes of memory are reserved scratch space - returndatacopy(0, 0, 32) - success := mload(0) - } - } - } - require( - success, - "TRANSFER_FAILED" - ); - } - - /// @dev Decodes ERC721 assetData and transfers given amount to sender. - /// @param assetData Byte array encoded for the respective asset proxy. - /// @param amount Amount of asset to transfer to sender. - function transferERC721Token( - bytes memory assetData, - uint256 amount - ) - internal - { - require( - amount == 1, - "INVALID_AMOUNT" - ); - // Decode asset data. - address token = assetData.readAddress(16); - uint256 tokenId = assetData.readUint256(36); - - // Perform transfer. - IERC721Token(token).transferFrom( - address(this), - msg.sender, - tokenId - ); - } -} diff --git a/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol b/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol deleted file mode 100644 index 4584bb840..000000000 --- a/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol +++ /dev/null @@ -1,263 +0,0 @@ -/* - - 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; -pragma experimental ABIEncoderV2; - -import "./libs/LibConstants.sol"; -import "./mixins/MExchangeWrapper.sol"; -import "../protocol/Exchange/libs/LibAbiEncoder.sol"; -import "../protocol/Exchange/libs/LibOrder.sol"; -import "../protocol/Exchange/libs/LibFillResults.sol"; -import "../protocol/Exchange/libs/LibMath.sol"; - - -contract MixinExchangeWrapper is - LibAbiEncoder, - LibFillResults, - LibMath, - LibConstants, - MExchangeWrapper -{ - - /// @dev Fills the input order. - /// Returns false if the transaction would otherwise revert. - /// @param order Order struct containing order specifications. - /// @param takerAssetFillAmount Desired amount of takerAsset to sell. - /// @param signature Proof that order has been created by maker. - /// @return Amounts filled and fees paid by maker and taker. - function fillOrderNoThrow( - LibOrder.Order memory order, - uint256 takerAssetFillAmount, - bytes memory signature - ) - internal - returns (FillResults memory fillResults) - { - // ABI encode calldata for `fillOrder` - bytes memory fillOrderCalldata = abiEncodeFillOrder( - order, - takerAssetFillAmount, - signature - ); - - address exchange = address(EXCHANGE); - - // Call `fillOrder` and handle any exceptions gracefully - assembly { - let success := call( - gas, // forward all gas, TODO: look into gas consumption of assert/throw - exchange, // call address of Exchange contract - 0, // transfer 0 wei - add(fillOrderCalldata, 32), // pointer to start of input (skip array length in first 32 bytes) - mload(fillOrderCalldata), // length of input - fillOrderCalldata, // write output over input - 128 // output size is 128 bytes - ) - switch success - case 0 { - mstore(fillResults, 0) - mstore(add(fillResults, 32), 0) - mstore(add(fillResults, 64), 0) - mstore(add(fillResults, 96), 0) - } - case 1 { - mstore(fillResults, mload(fillOrderCalldata)) - mstore(add(fillResults, 32), mload(add(fillOrderCalldata, 32))) - mstore(add(fillResults, 64), mload(add(fillOrderCalldata, 64))) - mstore(add(fillResults, 96), mload(add(fillOrderCalldata, 96))) - } - } - return fillResults; - } - - /// @dev Synchronously executes multiple calls of fillOrder until total amount of WETH has been sold by taker. - /// Returns false if the transaction would otherwise revert. - /// @param orders Array of order specifications. - /// @param wethSellAmount Desired amount of WETH to sell. - /// @param signatures Proofs that orders have been signed by makers. - /// @return Amounts filled and fees paid by makers and taker. - function marketSellWeth( - LibOrder.Order[] memory orders, - uint256 wethSellAmount, - bytes[] memory signatures - ) - internal - returns (FillResults memory totalFillResults) - { - bytes memory makerAssetData = orders[0].makerAssetData; - bytes memory wethAssetData = WETH_ASSET_DATA; - - uint256 ordersLength = orders.length; - for (uint256 i = 0; i != ordersLength; i++) { - - // We assume that asset being bought by taker is the same for each order. - // We assume that asset being sold by taker is WETH for each order. - orders[i].makerAssetData = makerAssetData; - orders[i].takerAssetData = wethAssetData; - - // Calculate the remaining amount of WETH to sell - uint256 remainingTakerAssetFillAmount = safeSub(wethSellAmount, totalFillResults.takerAssetFilledAmount); - - // Attempt to sell the remaining amount of WETH - FillResults memory singleFillResults = fillOrderNoThrow( - orders[i], - remainingTakerAssetFillAmount, - signatures[i] - ); - - // Update amounts filled and fees paid by maker and taker - addFillResults(totalFillResults, singleFillResults); - - // Stop execution if the entire amount of takerAsset has been sold - if (totalFillResults.takerAssetFilledAmount >= wethSellAmount) { - break; - } - } - return totalFillResults; - } - - /// @dev Synchronously executes multiple fill orders in a single transaction until total amount is bought by taker. - /// Returns false if the transaction would otherwise revert. - /// The asset being sold by taker must always be WETH. - /// @param orders Array of order specifications. - /// @param makerAssetFillAmount Desired amount of makerAsset to buy. - /// @param signatures Proofs that orders have been signed by makers. - /// @return Amounts filled and fees paid by makers and taker. - function marketBuyExactAmountWithWeth( - LibOrder.Order[] memory orders, - uint256 makerAssetFillAmount, - bytes[] memory signatures - ) - internal - returns (FillResults memory totalFillResults) - { - bytes memory makerAssetData = orders[0].makerAssetData; - bytes memory wethAssetData = WETH_ASSET_DATA; - - uint256 ordersLength = orders.length; - for (uint256 i = 0; i != ordersLength; i++) { - - // We assume that asset being bought by taker is the same for each order. - // We assume that asset being sold by taker is WETH for each order. - orders[i].makerAssetData = makerAssetData; - orders[i].takerAssetData = wethAssetData; - - // Calculate the remaining amount of makerAsset to buy - uint256 remainingMakerAssetFillAmount = safeSub(makerAssetFillAmount, totalFillResults.makerAssetFilledAmount); - - // Convert the remaining amount of makerAsset to buy into remaining amount - // of takerAsset to sell, assuming entire amount can be sold in the current order - uint256 remainingTakerAssetFillAmount = getPartialAmount( - orders[i].takerAssetAmount, - orders[i].makerAssetAmount, - remainingMakerAssetFillAmount - ); - - // Attempt to sell the remaining amount of takerAsset - FillResults memory singleFillResults = fillOrderNoThrow( - orders[i], - remainingTakerAssetFillAmount, - signatures[i] - ); - - // Update amounts filled and fees paid by maker and taker - addFillResults(totalFillResults, singleFillResults); - - // Stop execution if the entire amount of makerAsset has been bought - uint256 makerAssetFilledAmount = totalFillResults.makerAssetFilledAmount; - if (makerAssetFilledAmount >= makerAssetFillAmount) { - break; - } - } - - require( - makerAssetFilledAmount >= makerAssetFillAmount, - "COMPLETE_FILL_FAILED" - ); - return totalFillResults; - } - - /// @dev Buys zrxBuyAmount of ZRX fee tokens, taking into account ZRX fees for each order. This will guarantee - /// that at least zrxBuyAmount of ZRX is purchased (sometimes slightly over due to rounding issues). - /// It is possible that a request to buy 200 ZRX will require purchasing 202 ZRX - /// as 2 ZRX is required to purchase the 200 ZRX fee tokens. This guarantees at least 200 ZRX for future purchases. - /// The asset being sold by taker must always be WETH. - /// @param orders Array of order specifications containing ZRX as makerAsset and WETH as takerAsset. - /// @param zrxBuyAmount Desired amount of ZRX to buy. - /// @param signatures Proofs that orders have been created by makers. - /// @return totalFillResults Amounts filled and fees paid by maker and taker. - function marketBuyExactZrxWithWeth( - LibOrder.Order[] memory orders, - uint256 zrxBuyAmount, - bytes[] memory signatures - ) - internal - returns (FillResults memory totalFillResults) - { - // Do nothing if zrxBuyAmount == 0 - if (zrxBuyAmount == 0) { - return totalFillResults; - } - - bytes memory zrxAssetData = ZRX_ASSET_DATA; - bytes memory wethAssetData = WETH_ASSET_DATA; - uint256 zrxPurchased = 0; - - uint256 ordersLength = orders.length; - for (uint256 i = 0; i != ordersLength; i++) { - - // All of these are ZRX/WETH, so we can drop the respective assetData from calldata. - orders[i].makerAssetData = zrxAssetData; - orders[i].takerAssetData = wethAssetData; - - // Calculate the remaining amount of ZRX to buy. - uint256 remainingZrxBuyAmount = safeSub(zrxBuyAmount, zrxPurchased); - - // Convert the remaining amount of ZRX to buy into remaining amount - // of WETH to sell, assuming entire amount can be sold in the current order. - uint256 remainingWethSellAmount = getPartialAmount( - orders[i].takerAssetAmount, - safeSub(orders[i].makerAssetAmount, orders[i].takerFee), // our exchange rate after fees - remainingZrxBuyAmount - ); - - // Attempt to sell the remaining amount of WETH. - FillResults memory singleFillResult = fillOrderNoThrow( - orders[i], - safeAdd(remainingWethSellAmount, 1), // we add 1 wei to the fill amount to make up for rounding errors - signatures[i] - ); - - // Update amounts filled and fees paid by maker and taker. - addFillResults(totalFillResults, singleFillResult); - zrxPurchased = safeSub(totalFillResults.makerAssetFilledAmount, totalFillResults.takerFeePaid); - - // Stop execution if the entire amount of ZRX has been bought. - if (zrxPurchased >= zrxBuyAmount) { - break; - } - } - - require( - zrxPurchased >= zrxBuyAmount, - "COMPLETE_FILL_FAILED" - ); - return totalFillResults; - } -} diff --git a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol deleted file mode 100644 index 93cbf79be..000000000 --- a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol +++ /dev/null @@ -1,213 +0,0 @@ -/* - - 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; -pragma experimental ABIEncoderV2; - -import "./libs/LibConstants.sol"; -import "./mixins/MWeth.sol"; -import "./mixins/MAssets.sol"; -import "./mixins/MExchangeWrapper.sol"; -import "./interfaces/IForwarderCore.sol"; -import "../utils/LibBytes/LibBytes.sol"; -import "../protocol/Exchange/libs/LibOrder.sol"; -import "../protocol/Exchange/libs/LibFillResults.sol"; -import "../protocol/Exchange/libs/LibMath.sol"; - - -contract MixinForwarderCore is - LibFillResults, - LibMath, - LibConstants, - MWeth, - MAssets, - MExchangeWrapper, - IForwarderCore -{ - - using LibBytes for bytes; - - /// @dev Constructor approves ERC20 proxy to transfer ZRX and WETH on this contract's behalf. - constructor () - public - { - address proxyAddress = EXCHANGE.getAssetProxy(ERC20_DATA_ID); - if (proxyAddress != address(0)) { - ETHER_TOKEN.approve(proxyAddress, MAX_UINT); - ZRX_TOKEN.approve(proxyAddress, MAX_UINT); - } - } - - /// @dev Purchases as much of orders' makerAssets as possible by selling up to 95% of transaction's ETH value. - /// Any ZRX required to pay fees for primary orders will automatically be purchased by this contract. - /// 5% of ETH value is reserved for paying fees to order feeRecipients (in ZRX) and forwarding contract feeRecipient (in ETH). - /// Any ETH not spent will be refunded to sender. - /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. - /// @param signatures Proofs that orders have been created by makers. - /// @param feeOrders Array of order specifications containing ZRX as makerAsset and WETH as takerAsset. Used to purchase ZRX for primary order fees. - /// @param feeSignatures Proofs that feeOrders have been created by makers. - /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. - /// @param feeRecipient Address that will receive ETH when orders are filled. - /// @return Amounts filled and fees paid by maker and taker for both sets of orders. - function marketSellOrdersWithEth( - LibOrder.Order[] memory orders, - bytes[] memory signatures, - LibOrder.Order[] memory feeOrders, - bytes[] memory feeSignatures, - uint256 feePercentage, - address feeRecipient - ) - public - payable - returns ( - FillResults memory orderFillResults, - FillResults memory feeOrderFillResults - ) - { - // Convert ETH to WETH. - convertEthToWeth(); - - uint256 wethSellAmount; - uint256 zrxBuyAmount; - uint256 makerAssetAmountPurchased; - if (orders[0].makerAssetData.equals(ZRX_ASSET_DATA)) { - // Calculate amount of WETH that won't be spent on ETH fees. - wethSellAmount = getPartialAmount( - PERCENTAGE_DENOMINATOR, - safeAdd(PERCENTAGE_DENOMINATOR, feePercentage), - msg.value - ); - // Market sell available WETH. - // ZRX fees are paid with this contract's balance. - orderFillResults = marketSellWeth( - orders, - wethSellAmount, - signatures - ); - // The fee amount must be deducted from the amount transfered back to sender. - makerAssetAmountPurchased = safeSub(orderFillResults.makerAssetFilledAmount, orderFillResults.takerFeePaid); - } else { - // 5% of WETH is reserved for filling feeOrders and paying feeRecipient. - wethSellAmount = getPartialAmount( - MAX_WETH_FILL_PERCENTAGE, - PERCENTAGE_DENOMINATOR, - msg.value - ); - // Market sell 95% of WETH. - // ZRX fees are payed with this contract's balance. - orderFillResults = marketSellWeth( - orders, - wethSellAmount, - signatures - ); - // Buy back all ZRX spent on fees. - zrxBuyAmount = orderFillResults.takerFeePaid; - feeOrderFillResults = marketBuyExactZrxWithWeth( - feeOrders, - zrxBuyAmount, - feeSignatures - ); - makerAssetAmountPurchased = orderFillResults.makerAssetFilledAmount; - } - - // Transfer feePercentage of total ETH spent on primary orders to feeRecipient. - // Refund remaining ETH to msg.sender. - transferEthFeeAndRefund( - orderFillResults.takerAssetFilledAmount, - feeOrderFillResults.takerAssetFilledAmount, - feePercentage, - feeRecipient - ); - - // Transfer purchased assets to msg.sender. - transferAssetToSender(orders[0].makerAssetData, makerAssetAmountPurchased); - } - - /// @dev Attempt to purchase makerAssetFillAmount of makerAsset by selling ETH provided with transaction. - /// Any ZRX required to pay fees for primary orders will automatically be purchased by this contract. - /// Any ETH not spent will be refunded to sender. - /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. - /// @param makerAssetFillAmount Desired amount of makerAsset to purchase. - /// @param signatures Proofs that orders have been created by makers. - /// @param feeOrders Array of order specifications containing ZRX as makerAsset and WETH as takerAsset. Used to purchase ZRX for primary order fees. - /// @param feeSignatures Proofs that feeOrders have been created by makers. - /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. - /// @param feeRecipient Address that will receive ETH when orders are filled. - /// @return Amounts filled and fees paid by maker and taker for both sets of orders. - function marketBuyOrdersWithEth( - LibOrder.Order[] memory orders, - uint256 makerAssetFillAmount, - bytes[] memory signatures, - LibOrder.Order[] memory feeOrders, - bytes[] memory feeSignatures, - uint256 feePercentage, - address feeRecipient - ) - public - payable - returns ( - FillResults memory orderFillResults, - FillResults memory feeOrderFillResults - ) - { - // Convert ETH to WETH. - convertEthToWeth(); - - uint256 zrxBuyAmount; - uint256 makerAssetAmountPurchased; - if (orders[0].makerAssetData.equals(ZRX_ASSET_DATA)) { - // If the makerAsset is ZRX, it is not necessary to pay fees out of this - // contracts's ZRX balance because fees are factored into the price of the order. - orderFillResults = marketBuyExactZrxWithWeth( - orders, - makerAssetFillAmount, - signatures - ); - // The fee amount must be deducted from the amount transfered back to sender. - makerAssetAmountPurchased = safeSub(orderFillResults.makerAssetFilledAmount, orderFillResults.takerFeePaid); - } else { - // Attemp to purchase desired amount of makerAsset. - // ZRX fees are payed with this contract's balance. - orderFillResults = marketBuyExactAmountWithWeth( - orders, - makerAssetFillAmount, - signatures - ); - // Buy back all ZRX spent on fees. - zrxBuyAmount = orderFillResults.takerFeePaid; - feeOrderFillResults = marketBuyExactZrxWithWeth( - feeOrders, - zrxBuyAmount, - feeSignatures - ); - makerAssetAmountPurchased = orderFillResults.makerAssetFilledAmount; - } - - // Transfer feePercentage of total ETH spent on primary orders to feeRecipient. - // Refund remaining ETH to msg.sender. - transferEthFeeAndRefund( - orderFillResults.takerAssetFilledAmount, - feeOrderFillResults.takerAssetFilledAmount, - feePercentage, - feeRecipient - ); - - // Transfer purchased assets to msg.sender. - transferAssetToSender(orders[0].makerAssetData, makerAssetAmountPurchased); - } -} diff --git a/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol b/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol deleted file mode 100644 index e07940776..000000000 --- a/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol +++ /dev/null @@ -1,114 +0,0 @@ -/* - - 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 "../protocol/Exchange/libs/LibMath.sol"; -import "./libs/LibConstants.sol"; -import "./mixins/MWeth.sol"; - - -contract MixinWeth is - LibMath, - LibConstants, - MWeth -{ - - /// @dev Default payabale function, this allows us to withdraw WETH - function () - public - payable - { - require( - msg.sender == address(ETHER_TOKEN), - "DEFAULT_FUNCTION_WETH_CONTRACT_ONLY" - ); - } - - /// @dev Converts message call's ETH value into WETH. - function convertEthToWeth() - internal - { - require( - msg.value > 0, - "INVALID_MSG_VALUE" - ); - ETHER_TOKEN.deposit.value(msg.value)(); - } - - /// @dev Transfers feePercentage of WETH spent on primary orders to feeRecipient. - /// Refunds any excess ETH to msg.sender. - /// @param wethSoldExcludingFeeOrders Amount of WETH sold when filling primary orders. - /// @param wethSoldForZrx Amount of WETH sold when purchasing ZRX required for primary order fees. - /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. - /// @param feeRecipient Address that will receive ETH when orders are filled. - function transferEthFeeAndRefund( - uint256 wethSoldExcludingFeeOrders, - uint256 wethSoldForZrx, - uint256 feePercentage, - address feeRecipient - ) - internal - { - // Ensure feePercentage is less than 5%. - require( - feePercentage <= MAX_FEE_PERCENTAGE, - "FEE_PERCENTAGE_TOO_LARGE" - ); - - // Ensure that no extra WETH owned by this contract has been sold. - uint256 wethSold = safeAdd(wethSoldExcludingFeeOrders, wethSoldForZrx); - require( - wethSold <= msg.value, - "OVERSOLD_WETH" - ); - - // Calculate amount of WETH that hasn't been sold. - uint256 wethRemaining = safeSub(msg.value, wethSold); - - // Calculate ETH fee to pay to feeRecipient. - uint256 ethFee = getPartialAmount( - feePercentage, - PERCENTAGE_DENOMINATOR, - wethSoldExcludingFeeOrders - ); - - // Ensure fee is less than amount of WETH remaining. - require( - ethFee <= wethRemaining, - "INSUFFICIENT_ETH_REMAINING" - ); - - // Do nothing if no WETH remaining - if (wethRemaining > 0) { - // Convert remaining WETH to ETH - ETHER_TOKEN.withdraw(wethRemaining); - - // Pay ETH to feeRecipient - if (ethFee > 0) { - feeRecipient.transfer(ethFee); - } - - // Refund remaining ETH to msg.sender. - uint256 ethRefund = safeSub(wethRemaining, ethFee); - if (ethRefund > 0) { - msg.sender.transfer(ethRefund); - } - } - } -} diff --git a/packages/contracts/src/2.0.0/forwarder/interfaces/IAssets.sol b/packages/contracts/src/2.0.0/forwarder/interfaces/IAssets.sol deleted file mode 100644 index 1e034c003..000000000 --- a/packages/contracts/src/2.0.0/forwarder/interfaces/IAssets.sol +++ /dev/null @@ -1,34 +0,0 @@ -/* - - 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 IAssets { - - /// @dev Withdraws assets from this contract. The contract requires a ZRX balance in order to - /// function optimally, and this function allows the ZRX to be withdrawn by owner. It may also be - /// used to withdraw assets that were accidentally sent to this contract. - /// @param assetData Byte array encoded for the respective asset proxy. - /// @param amount Amount of ERC20 token to withdraw. - function withdrawAsset( - bytes assetData, - uint256 amount - ) - external; -} diff --git a/packages/contracts/src/2.0.0/forwarder/interfaces/IForwarder.sol b/packages/contracts/src/2.0.0/forwarder/interfaces/IForwarder.sol deleted file mode 100644 index f5a26e2ba..000000000 --- a/packages/contracts/src/2.0.0/forwarder/interfaces/IForwarder.sol +++ /dev/null @@ -1,30 +0,0 @@ -/* - - 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; -pragma experimental ABIEncoderV2; - -import "./IForwarderCore.sol"; -import "./IAssets.sol"; - - -// solhint-disable no-empty-blocks -contract IForwarder is - IForwarderCore, - IAssets -{} diff --git a/packages/contracts/src/2.0.0/forwarder/interfaces/IForwarderCore.sol b/packages/contracts/src/2.0.0/forwarder/interfaces/IForwarderCore.sol deleted file mode 100644 index 3ecbb133b..000000000 --- a/packages/contracts/src/2.0.0/forwarder/interfaces/IForwarderCore.sol +++ /dev/null @@ -1,80 +0,0 @@ -/* - - 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; -pragma experimental ABIEncoderV2; - -import "../../protocol/Exchange/libs/LibOrder.sol"; -import "../../protocol/Exchange/libs/LibFillResults.sol"; - - -contract IForwarderCore { - - /// @dev Purchases as much of orders' makerAssets as possible by selling up to 95% of transaction's ETH value. - /// Any ZRX required to pay fees for primary orders will automatically be purchased by this contract. - /// 5% of ETH value is reserved for paying fees to order feeRecipients (in ZRX) and forwarding contract feeRecipient (in ETH). - /// Any ETH not spent will be refunded to sender. - /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. - /// @param signatures Proofs that orders have been created by makers. - /// @param feeOrders Array of order specifications containing ZRX as makerAsset and WETH as takerAsset. Used to purchase ZRX for primary order fees. - /// @param feeSignatures Proofs that feeOrders have been created by makers. - /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. - /// @param feeRecipient Address that will receive ETH when orders are filled. - /// @return Amounts filled and fees paid by maker and taker for both sets of orders. - function marketSellOrdersWithEth( - LibOrder.Order[] memory orders, - bytes[] memory signatures, - LibOrder.Order[] memory feeOrders, - bytes[] memory feeSignatures, - uint256 feePercentage, - address feeRecipient - ) - public - payable - returns ( - LibFillResults.FillResults memory orderFillResults, - LibFillResults.FillResults memory feeOrderFillResults - ); - - /// @dev Attempt to purchase makerAssetFillAmount of makerAsset by selling ETH provided with transaction. - /// Any ZRX required to pay fees for primary orders will automatically be purchased by this contract. - /// Any ETH not spent will be refunded to sender. - /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. - /// @param makerAssetFillAmount Desired amount of makerAsset to purchase. - /// @param signatures Proofs that orders have been created by makers. - /// @param feeOrders Array of order specifications containing ZRX as makerAsset and WETH as takerAsset. Used to purchase ZRX for primary order fees. - /// @param feeSignatures Proofs that feeOrders have been created by makers. - /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. - /// @param feeRecipient Address that will receive ETH when orders are filled. - /// @return Amounts filled and fees paid by maker and taker for both sets of orders. - function marketBuyOrdersWithEth( - LibOrder.Order[] memory orders, - uint256 makerAssetFillAmount, - bytes[] memory signatures, - LibOrder.Order[] memory feeOrders, - bytes[] memory feeSignatures, - uint256 feePercentage, - address feeRecipient - ) - public - payable - returns ( - LibFillResults.FillResults memory orderFillResults, - LibFillResults.FillResults memory feeOrderFillResults - ); -} diff --git a/packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol b/packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol deleted file mode 100644 index fb9691fe8..000000000 --- a/packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol +++ /dev/null @@ -1,62 +0,0 @@ -/* - - 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 "../../utils/LibBytes/LibBytes.sol"; -import "../../protocol/Exchange/interfaces/IExchange.sol"; -import "../../tokens/EtherToken/IEtherToken.sol"; -import "../../tokens/ERC20Token/IERC20Token.sol"; - - -contract LibConstants { - - using LibBytes for bytes; - - bytes4 constant internal ERC20_DATA_ID = bytes4(keccak256("ERC20Token(address)")); - bytes4 constant internal ERC721_DATA_ID = bytes4(keccak256("ERC721Token(address,uint256)")); - uint256 constant internal MAX_UINT = 2**256 - 1; - uint256 constant internal PERCENTAGE_DENOMINATOR = 10**18; - uint256 constant internal MAX_FEE_PERCENTAGE = 5 * PERCENTAGE_DENOMINATOR / 100; // 5% - uint256 constant internal MAX_WETH_FILL_PERCENTAGE = 95 * PERCENTAGE_DENOMINATOR / 100; // 95% - - // solhint-disable var-name-mixedcase - IExchange internal EXCHANGE; - IEtherToken internal ETHER_TOKEN; - IERC20Token internal ZRX_TOKEN; - bytes internal ZRX_ASSET_DATA; - bytes internal WETH_ASSET_DATA; - // solhint-enable var-name-mixedcase - - constructor ( - address _exchange, - bytes memory _zrxAssetData, - bytes memory _wethAssetData - ) - public - { - EXCHANGE = IExchange(_exchange); - ZRX_ASSET_DATA = _zrxAssetData; - WETH_ASSET_DATA = _wethAssetData; - - address etherToken = _wethAssetData.readAddress(16); - address zrxToken = _zrxAssetData.readAddress(16); - ETHER_TOKEN = IEtherToken(etherToken); - ZRX_TOKEN = IERC20Token(zrxToken); - } -} diff --git a/packages/contracts/src/2.0.0/forwarder/libs/LibForwarderErrors.sol b/packages/contracts/src/2.0.0/forwarder/libs/LibForwarderErrors.sol deleted file mode 100644 index cdfb77a0b..000000000 --- a/packages/contracts/src/2.0.0/forwarder/libs/LibForwarderErrors.sol +++ /dev/null @@ -1,34 +0,0 @@ -/* - - 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. - -*/ - -// solhint-disable -pragma solidity 0.4.24; - - -/// This contract is intended to serve as a reference, but is not actually used for efficiency reasons. -contract LibForwarderErrors { - string constant FEE_PERCENTAGE_TOO_LARGE = "FEE_PROPORTION_TOO_LARGE"; // Provided fee percentage greater than 5%. - string constant INSUFFICIENT_ETH_REMAINING = "INSUFFICIENT_ETH_REMAINING"; // Not enough ETH remaining to pay feeRecipient. - string constant OVERSOLD_WETH = "OVERSOLD_WETH"; // More WETH sold than provided with current message call. - string constant COMPLETE_FILL_FAILED = "COMPLETE_FILL_FAILED"; // Desired purchase amount not completely filled (required for ZRX fees only). - string constant TRANSFER_FAILED = "TRANSFER_FAILED"; // Asset transfer failed. - string constant UNSUPPORTED_TOKEN_PROXY = "UNSUPPORTED_TOKEN_PROXY"; // Proxy in assetData not supported. - string constant DEFAULT_FUNCTION_WETH_CONTRACT_ONLY = "DEFAULT_FUNCTION_WETH_CONTRACT_ONLY"; // Fallback function may only be used for WETH withdrawals. - string constant INVALID_MSG_VALUE = "INVALID_MSG_VALUE"; // msg.value must be greater than 0. - string constant INVALID_AMOUNT = "INVALID_AMOUNT"; // Amount must equal 1. -} diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MAssets.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MAssets.sol deleted file mode 100644 index 83636432a..000000000 --- a/packages/contracts/src/2.0.0/forwarder/mixins/MAssets.sol +++ /dev/null @@ -1,54 +0,0 @@ -/* - - 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/IAssets.sol"; - - -contract MAssets is - IAssets -{ - - /// @dev Transfers given amount of asset to sender. - /// @param assetData Byte array encoded for the respective asset proxy. - /// @param amount Amount of asset to transfer to sender. - function transferAssetToSender( - bytes memory assetData, - uint256 amount - ) - internal; - - /// @dev Decodes ERC20 assetData and transfers given amount to sender. - /// @param assetData Byte array encoded for the respective asset proxy. - /// @param amount Amount of asset to transfer to sender. - function transferERC20Token( - bytes memory assetData, - uint256 amount - ) - internal; - - /// @dev Decodes ERC721 assetData and transfers given amount to sender. - /// @param assetData Byte array encoded for the respective asset proxy. - /// @param amount Amount of asset to transfer to sender. - function transferERC721Token( - bytes memory assetData, - uint256 amount - ) - internal; -} diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MExchangeWrapper.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MExchangeWrapper.sol deleted file mode 100644 index 360dea0e4..000000000 --- a/packages/contracts/src/2.0.0/forwarder/mixins/MExchangeWrapper.sol +++ /dev/null @@ -1,87 +0,0 @@ -/* - - 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; -pragma experimental ABIEncoderV2; - -import "../../protocol/Exchange/libs/LibOrder.sol"; -import "../../protocol/Exchange/libs/LibFillResults.sol"; - - -contract MExchangeWrapper { - - /// @dev Fills the input order. - /// Returns false if the transaction would otherwise revert. - /// @param order Order struct containing order specifications. - /// @param takerAssetFillAmount Desired amount of takerAsset to sell. - /// @param signature Proof that order has been created by maker. - /// @return Amounts filled and fees paid by maker and taker. - function fillOrderNoThrow( - LibOrder.Order memory order, - uint256 takerAssetFillAmount, - bytes memory signature - ) - internal - returns (LibFillResults.FillResults memory fillResults); - - /// @dev Synchronously executes multiple calls of fillOrder until total amount of WETH has been sold by taker. - /// Returns false if the transaction would otherwise revert. - /// @param orders Array of order specifications. - /// @param wethSellAmount Desired amount of WETH to sell. - /// @param signatures Proofs that orders have been signed by makers. - /// @return Amounts filled and fees paid by makers and taker. - function marketSellWeth( - LibOrder.Order[] memory orders, - uint256 wethSellAmount, - bytes[] memory signatures - ) - internal - returns (LibFillResults.FillResults memory totalFillResults); - - /// @dev Synchronously executes multiple fill orders in a single transaction until total amount is bought by taker. - /// Returns false if the transaction would otherwise revert. - /// The asset being sold by taker must always be WETH. - /// @param orders Array of order specifications. - /// @param makerAssetFillAmount Desired amount of makerAsset to buy. - /// @param signatures Proofs that orders have been signed by makers. - /// @return Amounts filled and fees paid by makers and taker. - function marketBuyExactAmountWithWeth( - LibOrder.Order[] memory orders, - uint256 makerAssetFillAmount, - bytes[] memory signatures - ) - internal - returns (LibFillResults.FillResults memory totalFillResults); - - /// @dev Buys zrxBuyAmount of ZRX fee tokens, taking into account ZRX fees for each order. This will guarantee - /// that at least zrxBuyAmount of ZRX is purchased (sometimes slightly over due to rounding issues). - /// It is possible that a request to buy 200 ZRX will require purchasing 202 ZRX - /// as 2 ZRX is required to purchase the 200 ZRX fee tokens. This guarantees at least 200 ZRX for future purchases. - /// The asset being sold by taker must always be WETH. - /// @param orders Array of order specifications containing ZRX as makerAsset and WETH as takerAsset. - /// @param zrxBuyAmount Desired amount of ZRX to buy. - /// @param signatures Proofs that orders have been created by makers. - /// @return totalFillResults Amounts filled and fees paid by maker and taker. - function marketBuyExactZrxWithWeth( - LibOrder.Order[] memory orders, - uint256 zrxBuyAmount, - bytes[] memory signatures - ) - internal - returns (LibFillResults.FillResults memory totalFillResults); -} diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MWeth.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MWeth.sol deleted file mode 100644 index 88e77be4e..000000000 --- a/packages/contracts/src/2.0.0/forwarder/mixins/MWeth.sol +++ /dev/null @@ -1,41 +0,0 @@ -/* - - 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 MWeth { - - /// @dev Converts message call's ETH value into WETH. - function convertEthToWeth() - internal; - - /// @dev Transfers feePercentage of WETH spent on primary orders to feeRecipient. - /// Refunds any excess ETH to msg.sender. - /// @param wethSoldExcludingFeeOrders Amount of WETH sold when filling primary orders. - /// @param wethSoldForZrx Amount of WETH sold when purchasing ZRX required for primary order fees. - /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. - /// @param feeRecipient Address that will receive ETH when orders are filled. - function transferEthFeeAndRefund( - uint256 wethSoldExcludingFeeOrders, - uint256 wethSoldForZrx, - uint256 feePercentage, - address feeRecipient - ) - internal; -} -- cgit v1.2.3 From 0918f954219199cb6ee5efd29a296c1f412b2888 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Sun, 19 Aug 2018 20:03:59 -0700 Subject: Don't throw if ERC721 token isn't owned --- .../extensions/OrderValidator/OrderValidator.sol | 52 +++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) (limited to 'packages/contracts') diff --git a/packages/contracts/src/2.0.0/extensions/OrderValidator/OrderValidator.sol b/packages/contracts/src/2.0.0/extensions/OrderValidator/OrderValidator.sol index dcbbd7fa5..1c9cc6978 100644 --- a/packages/contracts/src/2.0.0/extensions/OrderValidator/OrderValidator.sol +++ b/packages/contracts/src/2.0.0/extensions/OrderValidator/OrderValidator.sol @@ -124,17 +124,67 @@ contract OrderValidator { address assetProxy = EXCHANGE.getAssetProxy(assetProxyId); if (assetProxyId == ERC20_DATA_ID) { + // Query balance balance = IERC20Token(token).balanceOf(target); + + // Query allowance allowance = IERC20Token(token).allowance(target, assetProxy); } else if (assetProxyId == ERC721_DATA_ID) { uint256 tokenId = assetData.readUint256(36); - address owner = IERC721Token(token).ownerOf(tokenId); + + // Query owner of tokenId + address owner = getERC721TokenOwner(token, tokenId); + + // Set balance to 1 if tokenId is owned by target balance = target == owner ? 1 : 0; + + // Check if ERC721Proxy is approved to spend tokenId bool isApproved = IERC721Token(token).isApprovedForAll(target, assetProxy) || IERC721Token(token).getApproved(tokenId) == assetProxy; + + // Set alowance to 1 if ERC721Proxy is approved to spend tokenId allowance = isApproved ? 1 : 0; } else { revert("UNSUPPORTED_ASSET_PROXY"); } return (balance, allowance); } + + /// @dev Calls `token.ownerOf(tokenId)`, but returns a null owner instead of reverting on an unowned token. + /// @param token Address of ERC721 token. + /// @param tokenId The identifier for the specific NFT. + /// @return Owner of tokenId or null address if unowned. + function getERC721TokenOwner(address token, uint256 tokenId) + public + view + returns (address owner) + { + assembly { + // load free memory pointer + let cdStart := mload(64) + + // bytes4(keccak256(ownerOf(uint256))) = 0x6352211e + mstore(cdStart, 0x6352211e00000000000000000000000000000000000000000000000000000000) + mstore(add(cdStart, 4), tokenId) + + // staticcall `ownerOf(tokenId)` + // `ownerOf` will revert if tokenId is not owned + let success := staticcall( + gas, // forward all gas + token, // call token contract + cdStart, // start of calldata + 36, // length of input is 36 bytes + cdStart, // write output over input + 32 // size of output is 32 bytes + ) + + // Success implies that tokenId is owned + // Copy owner from return data if successful + if success { + owner := mload(cdStart) + } + } + + // Owner initialized to address(0), no need to modify if call is unsuccessful + return owner; + } } -- cgit v1.2.3 From dafb282432f51b670e5117e70e7a15cd64aea899 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Sun, 19 Aug 2018 20:43:19 -0700 Subject: Add tests --- packages/contracts/compiler.json | 1 + packages/contracts/package.json | 2 +- packages/contracts/test/extensions/forwarder.ts | 994 +++++++++++++++++++++ .../contracts/test/extensions/order_validator.ts | 447 +++++++++ packages/contracts/test/forwarder/forwarder.ts | 994 --------------------- packages/contracts/test/utils/artifacts.ts | 2 + 6 files changed, 1445 insertions(+), 995 deletions(-) create mode 100644 packages/contracts/test/extensions/forwarder.ts create mode 100644 packages/contracts/test/extensions/order_validator.ts delete mode 100644 packages/contracts/test/forwarder/forwarder.ts (limited to 'packages/contracts') diff --git a/packages/contracts/compiler.json b/packages/contracts/compiler.json index 09c47e6af..16524231c 100644 --- a/packages/contracts/compiler.json +++ b/packages/contracts/compiler.json @@ -39,6 +39,7 @@ "MixinAuthorizable", "MultiSigWallet", "MultiSigWalletWithTimeLock", + "OrderValidator", "TestAssetProxyOwner", "TestAssetProxyDispatcher", "TestConstants", diff --git a/packages/contracts/package.json b/packages/contracts/package.json index b25b33c20..81014537f 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -37,7 +37,7 @@ }, "config": { "abis": - "../migrations/artifacts/2.0.0/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|DummyNoReturnERC20Token|ERC20Proxy|ERC721Proxy|Forwarder|Exchange|ExchangeWrapper|IAssetData|IAssetProxy|InvalidERC721Receiver|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|TestAssetProxyOwner|TestAssetProxyDispatcher|TestConstants|TestExchangeInternals|TestLibBytes|TestLibs|TestSignatureValidator|Validator|Wallet|TokenRegistry|Whitelist|WETH9|ZRXToken).json" + "../migrations/artifacts/2.0.0/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|DummyNoReturnERC20Token|ERC20Proxy|ERC721Proxy|Forwarder|Exchange|ExchangeWrapper|IAssetData|IAssetProxy|InvalidERC721Receiver|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|OrderValidator|TestAssetProxyOwner|TestAssetProxyDispatcher|TestConstants|TestExchangeInternals|TestLibBytes|TestLibs|TestSignatureValidator|Validator|Wallet|TokenRegistry|Whitelist|WETH9|ZRXToken).json" }, "repository": { "type": "git", diff --git a/packages/contracts/test/extensions/forwarder.ts b/packages/contracts/test/extensions/forwarder.ts new file mode 100644 index 000000000..18101d684 --- /dev/null +++ b/packages/contracts/test/extensions/forwarder.ts @@ -0,0 +1,994 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { assetDataUtils } from '@0xproject/order-utils'; +import { RevertReason, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as chai from 'chai'; +import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; + +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; +import { DummyERC721TokenContract } from '../../generated_contract_wrappers/dummy_erc721_token'; +import { ExchangeContract } from '../../generated_contract_wrappers/exchange'; +import { ForwarderContract } from '../../generated_contract_wrappers/forwarder'; +import { WETH9Contract } from '../../generated_contract_wrappers/weth9'; +import { artifacts } from '../utils/artifacts'; +import { expectTransactionFailedAsync } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { ERC20Wrapper } from '../utils/erc20_wrapper'; +import { ERC721Wrapper } from '../utils/erc721_wrapper'; +import { ExchangeWrapper } from '../utils/exchange_wrapper'; +import { ForwarderWrapper } from '../utils/forwarder_wrapper'; +import { OrderFactory } from '../utils/order_factory'; +import { ContractName, ERC20BalancesByOwner } from '../utils/types'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +const DECIMALS_DEFAULT = 18; +const MAX_WETH_FILL_PERCENTAGE = 95; + +describe(ContractName.Forwarder, () => { + let makerAddress: string; + let owner: string; + let takerAddress: string; + let feeRecipientAddress: string; + let otherAddress: string; + let defaultMakerAssetAddress: string; + let zrxAssetData: string; + + let weth: DummyERC20TokenContract; + let zrxToken: DummyERC20TokenContract; + let erc721Token: DummyERC721TokenContract; + let forwarderContract: ForwarderContract; + let wethContract: WETH9Contract; + let forwarderWrapper: ForwarderWrapper; + let exchangeWrapper: ExchangeWrapper; + + let orderWithoutFee: SignedOrder; + let orderWithFee: SignedOrder; + let feeOrder: SignedOrder; + let orderFactory: OrderFactory; + let erc20Wrapper: ERC20Wrapper; + let erc20Balances: ERC20BalancesByOwner; + let tx: TransactionReceiptWithDecodedLogs; + + let erc721MakerAssetIds: BigNumber[]; + let takerEthBalanceBefore: BigNumber; + let feePercentage: BigNumber; + let gasPrice: BigNumber; + + before(async () => { + await blockchainLifecycle.startAsync(); + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress, otherAddress] = accounts); + + const txHash = await web3Wrapper.sendTransactionAsync({ from: accounts[0], to: accounts[0], value: 0 }); + const transaction = await web3Wrapper.getTransactionByHashAsync(txHash); + gasPrice = new BigNumber(transaction.gasPrice); + + const erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); + erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); + + const numDummyErc20ToDeploy = 3; + let erc20TokenA; + [erc20TokenA, zrxToken] = await erc20Wrapper.deployDummyTokensAsync( + numDummyErc20ToDeploy, + constants.DUMMY_TOKEN_DECIMALS, + ); + const erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + + [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); + const erc721Proxy = await erc721Wrapper.deployProxyAsync(); + await erc721Wrapper.setBalancesAndAllowancesAsync(); + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + erc721MakerAssetIds = erc721Balances[makerAddress][erc721Token.address]; + + wethContract = await WETH9Contract.deployFrom0xArtifactAsync(artifacts.EtherToken, provider, txDefaults); + weth = new DummyERC20TokenContract(wethContract.abi, wethContract.address, provider); + erc20Wrapper.addDummyTokenContract(weth); + + const wethAssetData = assetDataUtils.encodeERC20AssetData(wethContract.address); + zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync( + artifacts.Exchange, + provider, + txDefaults, + zrxAssetData, + ); + const exchangeContract = new ExchangeContract(exchangeInstance.abi, exchangeInstance.address, provider); + exchangeWrapper = new ExchangeWrapper(exchangeContract, provider); + await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); + await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner); + + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, { + from: owner, + }); + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, { + from: owner, + }); + + defaultMakerAssetAddress = erc20TokenA.address; + const defaultTakerAssetAddress = wethContract.address; + const defaultOrderParams = { + exchangeAddress: exchangeInstance.address, + makerAddress, + feeRecipientAddress, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), DECIMALS_DEFAULT), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT), + makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), + }; + const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)]; + orderFactory = new OrderFactory(privateKey, defaultOrderParams); + + const forwarderInstance = await ForwarderContract.deployFrom0xArtifactAsync( + artifacts.Forwarder, + provider, + txDefaults, + exchangeInstance.address, + zrxAssetData, + wethAssetData, + ); + forwarderContract = new ForwarderContract(forwarderInstance.abi, forwarderInstance.address, provider); + forwarderWrapper = new ForwarderWrapper(forwarderContract, provider); + const zrxDepositAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.transfer.sendTransactionAsync(forwarderContract.address, zrxDepositAmount), + ); + erc20Wrapper.addTokenOwnerAddress(forwarderInstance.address); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + erc20Balances = await erc20Wrapper.getBalancesAsync(); + takerEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + orderWithoutFee = await orderFactory.newSignedOrderAsync(); + feeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + orderWithFee = await orderFactory.newSignedOrderAsync({ + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('marketSellOrdersWithEth without extra fees', () => { + it('should fill a single order', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const makerAssetFillAmount = primaryTakerAssetFillAmount + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should fill multiple orders', async () => { + const secondOrderWithoutFee = await orderFactory.newSignedOrderAsync(); + const ordersWithoutFee = [orderWithoutFee, secondOrderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const ethValue = ordersWithoutFee[0].takerAssetAmount.plus( + ordersWithoutFee[1].takerAssetAmount.dividedToIntegerBy(2), + ); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const firstTakerAssetFillAmount = ordersWithoutFee[0].takerAssetAmount; + const secondTakerAssetFillAmount = primaryTakerAssetFillAmount.minus(firstTakerAssetFillAmount); + + const makerAssetFillAmount = ordersWithoutFee[0].makerAssetAmount.plus( + ordersWithoutFee[1].makerAssetAmount + .times(secondTakerAssetFillAmount) + .dividedToIntegerBy(ordersWithoutFee[1].takerAssetAmount), + ); + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should fill the order and pay ZRX fees from a single feeOrder', async () => { + const ordersWithFee = [orderWithFee]; + const feeOrders = [feeOrder]; + const ethValue = orderWithFee.takerAssetAmount.dividedToIntegerBy(2); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const makerAssetFillAmount = primaryTakerAssetFillAmount + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + const feeAmount = ForwarderWrapper.getPercentageOfValue( + orderWithFee.takerFee.dividedToIntegerBy(2), + MAX_WETH_FILL_PERCENTAGE, + ); + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const totalEthSpent = primaryTakerAssetFillAmount + .plus(wethSpentOnFeeOrders) + .plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should fill the orders and pay ZRX from multiple feeOrders', async () => { + const ordersWithFee = [orderWithFee]; + const ethValue = orderWithFee.takerAssetAmount; + const makerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const makerAssetAmount = orderWithFee.takerFee.dividedToIntegerBy(2); + const takerAssetAmount = feeOrder.takerAssetAmount + .times(makerAssetAmount) + .dividedToIntegerBy(feeOrder.makerAssetAmount); + + const firstFeeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + makerAssetAmount, + takerAssetAmount, + }); + const secondFeeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + makerAssetAmount, + takerAssetAmount, + }); + const feeOrders = [firstFeeOrder, secondFeeOrder]; + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const makerAssetFillAmount = primaryTakerAssetFillAmount + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + const feeAmount = ForwarderWrapper.getPercentageOfValue(orderWithFee.takerFee, MAX_WETH_FILL_PERCENTAGE); + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const totalEthSpent = primaryTakerAssetFillAmount + .plus(wethSpentOnFeeOrders) + .plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should fill the order when token is ZRX with fees', async () => { + orderWithFee = await orderFactory.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + const ordersWithFee = [orderWithFee]; + const feeOrders: SignedOrder[] = []; + const ethValue = orderWithFee.takerAssetAmount.dividedToIntegerBy(2); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2); + const totalEthSpent = ethValue.plus(gasPrice.times(tx.gasUsed)); + const takerFeePaid = orderWithFee.takerFee.dividedToIntegerBy(2); + const makerFeePaid = orderWithFee.makerFee.dividedToIntegerBy(2); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFillAmount).minus(makerFeePaid), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].plus(makerAssetFillAmount).minus(takerFeePaid), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(ethValue), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[forwarderContract.address][zrxToken.address], + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should refund remaining ETH if amount is greater than takerAssetAmount', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const ethValue = orderWithoutFee.takerAssetAmount.times(2); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const totalEthSpent = orderWithoutFee.takerAssetAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + }); + it('should revert if ZRX cannot be fully repurchased', async () => { + orderWithFee = await orderFactory.newSignedOrderAsync({ + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), DECIMALS_DEFAULT), + }); + const ordersWithFee = [orderWithFee]; + feeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + const feeOrders = [feeOrder]; + const ethValue = orderWithFee.takerAssetAmount; + return expectTransactionFailedAsync( + forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { + value: ethValue, + from: takerAddress, + }), + RevertReason.CompleteFillFailed, + ); + }); + it('should not fill orders with different makerAssetData than the first order', async () => { + const makerAssetId = erc721MakerAssetIds[0]; + const erc721SignedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + }); + const erc20SignedOrder = await orderFactory.newSignedOrderAsync(); + const ordersWithoutFee = [erc20SignedOrder, erc721SignedOrder]; + const feeOrders: SignedOrder[] = []; + const ethValue = erc20SignedOrder.takerAssetAmount.plus(erc721SignedOrder.takerAssetAmount); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const totalEthSpent = erc20SignedOrder.takerAssetAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + }); + }); + describe('marketSellOrdersWithEth with extra fees', () => { + it('should fill the order and send fee to feeRecipient', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const ethValue = orderWithoutFee.takerAssetAmount.div(2); + + const baseFeePercentage = 2; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + tx = await forwarderWrapper.marketSellOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + { + value: ethValue, + from: takerAddress, + }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const makerAssetFillAmount = primaryTakerAssetFillAmount + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + const ethSpentOnFee = ForwarderWrapper.getPercentageOfValue(primaryTakerAssetFillAmount, baseFeePercentage); + const totalEthSpent = primaryTakerAssetFillAmount.plus(ethSpentOnFee).plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(feeRecipientEthBalanceAfter).to.be.bignumber.equal(feeRecipientEthBalanceBefore.plus(ethSpentOnFee)); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should fail if the fee is set too high', async () => { + const ethValue = orderWithoutFee.takerAssetAmount.div(2); + const baseFeePercentage = 6; + feePercentage = ForwarderWrapper.getPercentageOfValue(ethValue, baseFeePercentage); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + await expectTransactionFailedAsync( + forwarderWrapper.marketSellOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + { from: takerAddress, value: ethValue, gasPrice }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ), + RevertReason.FeePercentageTooLarge, + ); + }); + it('should fail if there is not enough ETH remaining to pay the fee', async () => { + const ethValue = orderWithoutFee.takerAssetAmount.div(2); + const baseFeePercentage = 5; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + const ordersWithFee = [orderWithFee]; + const feeOrders = [feeOrder]; + await expectTransactionFailedAsync( + forwarderWrapper.marketSellOrdersWithEthAsync( + ordersWithFee, + feeOrders, + { from: takerAddress, value: ethValue, gasPrice }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ), + RevertReason.InsufficientEthRemaining, + ); + }); + }); + describe('marketBuyOrdersWithEth without extra fees', () => { + it('should buy the exact amount of makerAsset in a single order', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should buy the exact amount of makerAsset in multiple orders', async () => { + const secondOrderWithoutFee = await orderFactory.newSignedOrderAsync(); + const ordersWithoutFee = [orderWithoutFee, secondOrderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = ordersWithoutFee[0].makerAssetAmount.plus( + ordersWithoutFee[1].makerAssetAmount.dividedToIntegerBy(2), + ); + const ethValue = ordersWithoutFee[0].takerAssetAmount.plus( + ordersWithoutFee[1].takerAssetAmount.dividedToIntegerBy(2), + ); + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should buy the exact amount of makerAsset and return excess ETH', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount; + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ethValue.dividedToIntegerBy(2); + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should buy the exact amount of makerAsset and pay ZRX from feeOrders', async () => { + const ordersWithFee = [orderWithFee]; + const feeOrders = [feeOrder]; + const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithFee.takerAssetAmount; + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = orderWithFee.takerAssetAmount.dividedToIntegerBy(2); + const feeAmount = orderWithFee.takerFee.dividedToIntegerBy(2); + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const totalEthSpent = primaryTakerAssetFillAmount + .plus(wethSpentOnFeeOrders) + .plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should buy slightly greater than makerAssetAmount when buying ZRX', async () => { + orderWithFee = await orderFactory.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + const ordersWithFee = [orderWithFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithFee.takerAssetAmount; + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getWethForFeeOrders( + makerAssetFillAmount, + ordersWithFee, + ); + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + const makerAssetFilledAmount = orderWithFee.makerAssetAmount + .times(primaryTakerAssetFillAmount) + .dividedToIntegerBy(orderWithFee.takerAssetAmount); + const takerFeePaid = orderWithFee.takerFee + .times(primaryTakerAssetFillAmount) + .dividedToIntegerBy(orderWithFee.takerAssetAmount); + const makerFeePaid = orderWithFee.makerFee + .times(primaryTakerAssetFillAmount) + .dividedToIntegerBy(orderWithFee.takerAssetAmount); + const totalZrxPurchased = makerAssetFilledAmount.minus(takerFeePaid); + // Up to 1 wei worth of ZRX will be overbought per order + const maxOverboughtZrx = new BigNumber(1) + .times(orderWithFee.makerAssetAmount) + .dividedToIntegerBy(orderWithFee.takerAssetAmount); + + expect(totalZrxPurchased).to.be.bignumber.gte(makerAssetFillAmount); + expect(totalZrxPurchased).to.be.bignumber.lte(makerAssetFillAmount.plus(maxOverboughtZrx)); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFilledAmount).minus(makerFeePaid), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].plus(totalZrxPurchased), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[forwarderContract.address][zrxToken.address], + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should revert if the amount of ETH sent is too low to fill the makerAssetAmount', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(4); + return expectTransactionFailedAsync( + forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }), + RevertReason.CompleteFillFailed, + ); + }); + it('should buy an ERC721 asset from a single order', async () => { + const makerAssetId = erc721MakerAssetIds[0]; + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + }); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = new BigNumber(1); + const ethValue = orderWithFee.takerAssetAmount; + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + from: takerAddress, + value: ethValue, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + expect(newOwner).to.be.bignumber.equal(takerAddress); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should revert if buying an ERC721 asset when later orders contain different makerAssetData', async () => { + const makerAssetId = erc721MakerAssetIds[0]; + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + }); + const differentMakerAssetDataOrder = await orderFactory.newSignedOrderAsync(); + const ordersWithoutFee = [orderWithoutFee, differentMakerAssetDataOrder]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = new BigNumber(1).plus(differentMakerAssetDataOrder.makerAssetAmount); + const ethValue = orderWithFee.takerAssetAmount; + return expectTransactionFailedAsync( + forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }), + RevertReason.CompleteFillFailed, + ); + }); + it('should buy an ERC721 asset and pay ZRX fees from a single fee order', async () => { + const makerAssetId = erc721MakerAssetIds[0]; + orderWithFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + const ordersWithFee = [orderWithFee]; + const feeOrders = [feeOrder]; + const makerAssetFillAmount = orderWithFee.makerAssetAmount; + const primaryTakerAssetFillAmount = orderWithFee.takerAssetAmount; + const feeAmount = orderWithFee.takerFee; + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const ethValue = primaryTakerAssetFillAmount.plus(wethSpentOnFeeOrders); + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const totalEthSpent = ethValue.plus(gasPrice.times(tx.gasUsed)); + + expect(newOwner).to.be.bignumber.equal(takerAddress); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should buy an ERC721 asset and pay ZRX fees from multiple fee orders', async () => { + const makerAssetId = erc721MakerAssetIds[0]; + orderWithFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + const ordersWithFee = [orderWithFee]; + const makerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const makerAssetAmount = orderWithFee.takerFee.dividedToIntegerBy(2); + const takerAssetAmount = feeOrder.takerAssetAmount + .times(makerAssetAmount) + .dividedToIntegerBy(feeOrder.makerAssetAmount); + + const firstFeeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + makerAssetAmount, + takerAssetAmount, + }); + const secondFeeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + makerAssetAmount, + takerAssetAmount, + }); + const feeOrders = [firstFeeOrder, secondFeeOrder]; + + const makerAssetFillAmount = orderWithFee.makerAssetAmount; + const primaryTakerAssetFillAmount = orderWithFee.takerAssetAmount; + const feeAmount = orderWithFee.takerFee; + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const ethValue = primaryTakerAssetFillAmount.plus(wethSpentOnFeeOrders); + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const totalEthSpent = ethValue.plus(gasPrice.times(tx.gasUsed)); + + expect(newOwner).to.be.bignumber.equal(takerAddress); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + }); + describe('marketBuyOrdersWithEth with extra fees', () => { + it('should buy an asset and send fee to feeRecipient', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount; + + const baseFeePercentage = 2; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + makerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); + const ethSpentOnFee = ForwarderWrapper.getPercentageOfValue(primaryTakerAssetFillAmount, baseFeePercentage); + const totalEthSpent = primaryTakerAssetFillAmount.plus(ethSpentOnFee).plus(gasPrice.times(tx.gasUsed)); + + expect(feeRecipientEthBalanceAfter).to.be.bignumber.equal(feeRecipientEthBalanceBefore.plus(ethSpentOnFee)); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should fail if the fee is set too high', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount; + + const baseFeePercentage = 6; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + await expectTransactionFailedAsync( + forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + makerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ), + RevertReason.FeePercentageTooLarge, + ); + }); + it('should fail if there is not enough ETH remaining to pay the fee', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); + + const baseFeePercentage = 2; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + await expectTransactionFailedAsync( + forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + makerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ), + RevertReason.InsufficientEthRemaining, + ); + }); + }); + describe('withdrawAsset', () => { + it('should allow owner to withdraw ERC20 tokens', async () => { + const zrxWithdrawAmount = erc20Balances[forwarderContract.address][zrxToken.address]; + await forwarderWrapper.withdrawAssetAsync(zrxAssetData, zrxWithdrawAmount, { from: owner }); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances[owner][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[owner][zrxToken.address].plus(zrxWithdrawAmount), + ); + expect(newBalances[forwarderContract.address][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[forwarderContract.address][zrxToken.address].minus(zrxWithdrawAmount), + ); + }); + it('should revert if not called by owner', async () => { + const zrxWithdrawAmount = erc20Balances[forwarderContract.address][zrxToken.address]; + await expectTransactionFailedAsync( + forwarderWrapper.withdrawAssetAsync(zrxAssetData, zrxWithdrawAmount, { from: makerAddress }), + RevertReason.OnlyContractOwner, + ); + }); + }); +}); +// tslint:disable:max-file-line-count +// tslint:enable:no-unnecessary-type-assertion diff --git a/packages/contracts/test/extensions/order_validator.ts b/packages/contracts/test/extensions/order_validator.ts new file mode 100644 index 000000000..22a140465 --- /dev/null +++ b/packages/contracts/test/extensions/order_validator.ts @@ -0,0 +1,447 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { assetDataUtils, orderHashUtils } from '@0xproject/order-utils'; +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import * as _ from 'lodash'; + +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; +import { DummyERC721TokenContract } from '../../generated_contract_wrappers/dummy_erc721_token'; +import { ERC20ProxyContract } from '../../generated_contract_wrappers/erc20_proxy'; +import { ERC721ProxyContract } from '../../generated_contract_wrappers/erc721_proxy'; +import { ExchangeContract } from '../../generated_contract_wrappers/exchange'; +import { OrderValidatorContract } from '../../generated_contract_wrappers/order_validator'; +import { artifacts } from '../utils/artifacts'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { ERC20Wrapper } from '../utils/erc20_wrapper'; +import { ERC721Wrapper } from '../utils/erc721_wrapper'; +import { ExchangeWrapper } from '../utils/exchange_wrapper'; +import { OrderFactory } from '../utils/order_factory'; +import { OrderStatus } from '../utils/types'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('OrderValidator', () => { + let makerAddress: string; + let owner: string; + let takerAddress: string; + let erc20AssetData: string; + let erc721AssetData: string; + + let erc20Token: DummyERC20TokenContract; + let erc721Token: DummyERC721TokenContract; + let exchange: ExchangeContract; + let orderValidator: OrderValidatorContract; + let erc20Proxy: ERC20ProxyContract; + let erc721Proxy: ERC721ProxyContract; + + let signedOrder: SignedOrder; + let signedOrder2: SignedOrder; + let orderFactory: OrderFactory; + + const tokenId = new BigNumber(123456789); + const tokenId2 = new BigNumber(987654321); + const ERC721_BALANCE = new BigNumber(1); + const ERC721_ALLOWANCE = new BigNumber(1); + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const usedAddresses = ([owner, makerAddress, takerAddress] = _.slice(accounts, 0, 3)); + + const erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); + const erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); + + const numDummyErc20ToDeploy = 1; + [erc20Token] = await erc20Wrapper.deployDummyTokensAsync(numDummyErc20ToDeploy, constants.DUMMY_TOKEN_DECIMALS); + erc20Proxy = await erc20Wrapper.deployProxyAsync(); + + [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); + erc721Proxy = await erc721Wrapper.deployProxyAsync(); + + exchange = await ExchangeContract.deployFrom0xArtifactAsync( + artifacts.Exchange, + provider, + txDefaults, + assetDataUtils.encodeERC20AssetData(erc20Token.address), + ); + const exchangeWrapper = new ExchangeWrapper(exchange, provider); + await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); + await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner); + + orderValidator = await OrderValidatorContract.deployFrom0xArtifactAsync( + artifacts.OrderValidator, + provider, + txDefaults, + exchange.address, + ); + + erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20Token.address); + erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, tokenId); + const defaultOrderParams = { + ...constants.STATIC_ORDER_PARAMS, + exchangeAddress: exchange.address, + makerAddress, + feeRecipientAddress: constants.NULL_ADDRESS, + makerAssetData: erc20AssetData, + takerAssetData: erc721AssetData, + }; + const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)]; + orderFactory = new OrderFactory(privateKey, defaultOrderParams); + }); + + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('getBalanceAndAllowance', () => { + describe('getERC721TokenOwner', async () => { + it('should return the null address when tokenId is not owned', async () => { + const tokenOwner = await orderValidator.getERC721TokenOwner.callAsync(makerAddress, tokenId); + expect(tokenOwner).to.be.equal(constants.NULL_ADDRESS); + }); + it('should return the owner address when tokenId is owned', async () => { + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.mint.sendTransactionAsync(makerAddress, tokenId), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const tokenOwner = await orderValidator.getERC721TokenOwner.callAsync(erc721Token.address, tokenId); + expect(tokenOwner).to.be.equal(makerAddress); + }); + }); + describe('ERC20 assetData', () => { + it('should return the correct balances and allowances when both values are 0', async () => { + const [newBalance, newAllowance] = await orderValidator.getBalanceAndAllowance.callAsync( + makerAddress, + erc20AssetData, + ); + expect(constants.ZERO_AMOUNT).to.be.bignumber.equal(newBalance); + expect(constants.ZERO_AMOUNT).to.be.bignumber.equal(newAllowance); + }); + it('should return the correct balance and allowance when both values are non-zero', async () => { + const balance = new BigNumber(123); + const allowance = new BigNumber(456); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Token.setBalance.sendTransactionAsync(makerAddress, balance), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Token.approve.sendTransactionAsync(erc20Proxy.address, allowance, { + from: makerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const [newBalance, newAllowance] = await orderValidator.getBalanceAndAllowance.callAsync( + makerAddress, + erc20AssetData, + ); + expect(balance).to.be.bignumber.equal(newBalance); + expect(allowance).to.be.bignumber.equal(newAllowance); + }); + }); + describe('ERC721 assetData', () => { + it('should return a balance of 0 when the tokenId is not owned by target', async () => { + const [newBalance] = await orderValidator.getBalanceAndAllowance.callAsync( + makerAddress, + erc721AssetData, + ); + expect(newBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should return an allowance of 0 when no approval is set', async () => { + const [, newAllowance] = await orderValidator.getBalanceAndAllowance.callAsync( + makerAddress, + erc721AssetData, + ); + expect(newAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should return a balance of 1 when the tokenId is owned by target', async () => { + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.mint.sendTransactionAsync(makerAddress, tokenId), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const [newBalance] = await orderValidator.getBalanceAndAllowance.callAsync( + makerAddress, + erc721AssetData, + ); + expect(newBalance).to.be.bignumber.equal(ERC721_BALANCE); + }); + it('should return an allowance of 1 when ERC721Proxy is approved for all', async () => { + const isApproved = true; + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.setApprovalForAll.sendTransactionAsync(erc721Proxy.address, isApproved, { + from: makerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const [, newAllowance] = await orderValidator.getBalanceAndAllowance.callAsync( + makerAddress, + erc721AssetData, + ); + expect(newAllowance).to.be.bignumber.equal(ERC721_ALLOWANCE); + }); + it('should return an allowance of 1 when ERC721Proxy is approved for specific tokenId', async () => { + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.mint.sendTransactionAsync(makerAddress, tokenId), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.approve.sendTransactionAsync(erc721Proxy.address, tokenId, { + from: makerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const [, newAllowance] = await orderValidator.getBalanceAndAllowance.callAsync( + makerAddress, + erc721AssetData, + ); + expect(newAllowance).to.be.bignumber.equal(ERC721_ALLOWANCE); + }); + }); + }); + describe('getTraderInfo', () => { + beforeEach(async () => { + signedOrder = await orderFactory.newSignedOrderAsync(); + }); + it('should return the correct info when no balances or allowances are set', async () => { + const traderInfo = await orderValidator.getTraderInfo.callAsync(signedOrder, takerAddress); + expect(traderInfo.makerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo.makerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo.takerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo.takerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should return the correct info when balances and allowances are set', async () => { + const makerBalance = new BigNumber(123); + const makerAllowance = new BigNumber(456); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Token.setBalance.sendTransactionAsync(makerAddress, makerBalance), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Token.approve.sendTransactionAsync(erc20Proxy.address, makerAllowance, { + from: makerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.mint.sendTransactionAsync(takerAddress, tokenId), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.approve.sendTransactionAsync(erc721Proxy.address, tokenId, { + from: takerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const traderInfo = await orderValidator.getTraderInfo.callAsync(signedOrder, takerAddress); + expect(traderInfo.makerBalance).to.be.bignumber.equal(makerBalance); + expect(traderInfo.makerAllowance).to.be.bignumber.equal(makerAllowance); + expect(traderInfo.takerBalance).to.be.bignumber.equal(ERC721_BALANCE); + expect(traderInfo.takerAllowance).to.be.bignumber.equal(ERC721_ALLOWANCE); + }); + }); + describe('getTradersInfo', () => { + beforeEach(async () => { + signedOrder = await orderFactory.newSignedOrderAsync(); + signedOrder2 = await orderFactory.newSignedOrderAsync({ + takerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, tokenId2), + }); + }); + it('should return the correct info when no balances or allowances have been set', async () => { + const orders = [signedOrder, signedOrder2]; + const takers = [takerAddress, takerAddress]; + const [traderInfo1, traderInfo2] = await orderValidator.getTradersInfo.callAsync(orders, takers); + expect(traderInfo1.makerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.makerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.takerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.takerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo2.makerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo2.makerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo2.takerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo2.takerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should return the correct info when balances and allowances are set', async () => { + const makerBalance = new BigNumber(123); + const makerAllowance = new BigNumber(456); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Token.setBalance.sendTransactionAsync(makerAddress, makerBalance), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Token.approve.sendTransactionAsync(erc20Proxy.address, makerAllowance, { + from: makerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const isApproved = true; + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.setApprovalForAll.sendTransactionAsync(erc721Proxy.address, isApproved, { + from: takerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.mint.sendTransactionAsync(takerAddress, tokenId2), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const orders = [signedOrder, signedOrder2]; + const takers = [takerAddress, takerAddress]; + const [traderInfo1, traderInfo2] = await orderValidator.getTradersInfo.callAsync(orders, takers); + + expect(traderInfo1.makerBalance).to.be.bignumber.equal(makerBalance); + expect(traderInfo1.makerAllowance).to.be.bignumber.equal(makerAllowance); + expect(traderInfo1.takerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.takerAllowance).to.be.bignumber.equal(ERC721_ALLOWANCE); + expect(traderInfo2.makerBalance).to.be.bignumber.equal(makerBalance); + expect(traderInfo2.makerAllowance).to.be.bignumber.equal(makerAllowance); + expect(traderInfo2.takerBalance).to.be.bignumber.equal(ERC721_BALANCE); + expect(traderInfo2.takerAllowance).to.be.bignumber.equal(ERC721_ALLOWANCE); + }); + }); + describe('getOrderAndTraderInfo', () => { + beforeEach(async () => { + signedOrder = await orderFactory.newSignedOrderAsync(); + }); + it('should return the correct info when no balances or allowances are set', async () => { + const [orderInfo, traderInfo] = await orderValidator.getOrderAndTraderInfo.callAsync( + signedOrder, + takerAddress, + ); + const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder); + expect(orderInfo.orderStatus).to.be.equal(OrderStatus.FILLABLE); + expect(orderInfo.orderHash).to.be.equal(expectedOrderHash); + expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo.makerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo.makerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo.takerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo.takerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should return the correct info when balances and allowances are set', async () => { + const makerBalance = new BigNumber(123); + const makerAllowance = new BigNumber(456); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Token.setBalance.sendTransactionAsync(makerAddress, makerBalance), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Token.approve.sendTransactionAsync(erc20Proxy.address, makerAllowance, { + from: makerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.mint.sendTransactionAsync(takerAddress, tokenId), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.approve.sendTransactionAsync(erc721Proxy.address, tokenId, { + from: takerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const [orderInfo, traderInfo] = await orderValidator.getOrderAndTraderInfo.callAsync( + signedOrder, + takerAddress, + ); + const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder); + expect(orderInfo.orderStatus).to.be.equal(OrderStatus.FILLABLE); + expect(orderInfo.orderHash).to.be.equal(expectedOrderHash); + expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo.makerBalance).to.be.bignumber.equal(makerBalance); + expect(traderInfo.makerAllowance).to.be.bignumber.equal(makerAllowance); + expect(traderInfo.takerBalance).to.be.bignumber.equal(ERC721_BALANCE); + expect(traderInfo.takerAllowance).to.be.bignumber.equal(ERC721_ALLOWANCE); + }); + }); + describe('getOrdersAndTradersInfo', () => { + beforeEach(async () => { + signedOrder = await orderFactory.newSignedOrderAsync(); + signedOrder2 = await orderFactory.newSignedOrderAsync({ + takerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, tokenId2), + }); + }); + it('should return the correct info when no balances or allowances have been set', async () => { + const orders = [signedOrder, signedOrder2]; + const takers = [takerAddress, takerAddress]; + const [ + [orderInfo1, orderInfo2], + [traderInfo1, traderInfo2], + ] = await orderValidator.getOrdersAndTradersInfo.callAsync(orders, takers); + const expectedOrderHash1 = orderHashUtils.getOrderHashHex(signedOrder); + const expectedOrderHash2 = orderHashUtils.getOrderHashHex(signedOrder2); + expect(orderInfo1.orderStatus).to.be.equal(OrderStatus.FILLABLE); + expect(orderInfo1.orderHash).to.be.equal(expectedOrderHash1); + expect(orderInfo1.orderTakerAssetFilledAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(orderInfo2.orderStatus).to.be.equal(OrderStatus.FILLABLE); + expect(orderInfo2.orderHash).to.be.equal(expectedOrderHash2); + expect(orderInfo2.orderTakerAssetFilledAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.makerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.makerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.takerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.takerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo2.makerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo2.makerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo2.takerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo2.takerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should return the correct info when balances and allowances are set', async () => { + const makerBalance = new BigNumber(123); + const makerAllowance = new BigNumber(456); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Token.setBalance.sendTransactionAsync(makerAddress, makerBalance), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Token.approve.sendTransactionAsync(erc20Proxy.address, makerAllowance, { + from: makerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const isApproved = true; + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.setApprovalForAll.sendTransactionAsync(erc721Proxy.address, isApproved, { + from: takerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.mint.sendTransactionAsync(takerAddress, tokenId2), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const orders = [signedOrder, signedOrder2]; + const takers = [takerAddress, takerAddress]; + const [ + [orderInfo1, orderInfo2], + [traderInfo1, traderInfo2], + ] = await orderValidator.getOrdersAndTradersInfo.callAsync(orders, takers); + const expectedOrderHash1 = orderHashUtils.getOrderHashHex(signedOrder); + const expectedOrderHash2 = orderHashUtils.getOrderHashHex(signedOrder2); + expect(orderInfo1.orderStatus).to.be.equal(OrderStatus.FILLABLE); + expect(orderInfo1.orderHash).to.be.equal(expectedOrderHash1); + expect(orderInfo1.orderTakerAssetFilledAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(orderInfo2.orderStatus).to.be.equal(OrderStatus.FILLABLE); + expect(orderInfo2.orderHash).to.be.equal(expectedOrderHash2); + expect(orderInfo2.orderTakerAssetFilledAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.makerBalance).to.be.bignumber.equal(makerBalance); + expect(traderInfo1.makerAllowance).to.be.bignumber.equal(makerAllowance); + expect(traderInfo1.takerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.takerAllowance).to.be.bignumber.equal(ERC721_ALLOWANCE); + expect(traderInfo2.makerBalance).to.be.bignumber.equal(makerBalance); + expect(traderInfo2.makerAllowance).to.be.bignumber.equal(makerAllowance); + expect(traderInfo2.takerBalance).to.be.bignumber.equal(ERC721_BALANCE); + expect(traderInfo2.takerAllowance).to.be.bignumber.equal(ERC721_ALLOWANCE); + }); + }); +}); diff --git a/packages/contracts/test/forwarder/forwarder.ts b/packages/contracts/test/forwarder/forwarder.ts deleted file mode 100644 index 18101d684..000000000 --- a/packages/contracts/test/forwarder/forwarder.ts +++ /dev/null @@ -1,994 +0,0 @@ -import { BlockchainLifecycle } from '@0xproject/dev-utils'; -import { assetDataUtils } from '@0xproject/order-utils'; -import { RevertReason, SignedOrder } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as chai from 'chai'; -import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; - -import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; -import { DummyERC721TokenContract } from '../../generated_contract_wrappers/dummy_erc721_token'; -import { ExchangeContract } from '../../generated_contract_wrappers/exchange'; -import { ForwarderContract } from '../../generated_contract_wrappers/forwarder'; -import { WETH9Contract } from '../../generated_contract_wrappers/weth9'; -import { artifacts } from '../utils/artifacts'; -import { expectTransactionFailedAsync } from '../utils/assertions'; -import { chaiSetup } from '../utils/chai_setup'; -import { constants } from '../utils/constants'; -import { ERC20Wrapper } from '../utils/erc20_wrapper'; -import { ERC721Wrapper } from '../utils/erc721_wrapper'; -import { ExchangeWrapper } from '../utils/exchange_wrapper'; -import { ForwarderWrapper } from '../utils/forwarder_wrapper'; -import { OrderFactory } from '../utils/order_factory'; -import { ContractName, ERC20BalancesByOwner } from '../utils/types'; -import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); -const DECIMALS_DEFAULT = 18; -const MAX_WETH_FILL_PERCENTAGE = 95; - -describe(ContractName.Forwarder, () => { - let makerAddress: string; - let owner: string; - let takerAddress: string; - let feeRecipientAddress: string; - let otherAddress: string; - let defaultMakerAssetAddress: string; - let zrxAssetData: string; - - let weth: DummyERC20TokenContract; - let zrxToken: DummyERC20TokenContract; - let erc721Token: DummyERC721TokenContract; - let forwarderContract: ForwarderContract; - let wethContract: WETH9Contract; - let forwarderWrapper: ForwarderWrapper; - let exchangeWrapper: ExchangeWrapper; - - let orderWithoutFee: SignedOrder; - let orderWithFee: SignedOrder; - let feeOrder: SignedOrder; - let orderFactory: OrderFactory; - let erc20Wrapper: ERC20Wrapper; - let erc20Balances: ERC20BalancesByOwner; - let tx: TransactionReceiptWithDecodedLogs; - - let erc721MakerAssetIds: BigNumber[]; - let takerEthBalanceBefore: BigNumber; - let feePercentage: BigNumber; - let gasPrice: BigNumber; - - before(async () => { - await blockchainLifecycle.startAsync(); - const accounts = await web3Wrapper.getAvailableAddressesAsync(); - const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress, otherAddress] = accounts); - - const txHash = await web3Wrapper.sendTransactionAsync({ from: accounts[0], to: accounts[0], value: 0 }); - const transaction = await web3Wrapper.getTransactionByHashAsync(txHash); - gasPrice = new BigNumber(transaction.gasPrice); - - const erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); - erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); - - const numDummyErc20ToDeploy = 3; - let erc20TokenA; - [erc20TokenA, zrxToken] = await erc20Wrapper.deployDummyTokensAsync( - numDummyErc20ToDeploy, - constants.DUMMY_TOKEN_DECIMALS, - ); - const erc20Proxy = await erc20Wrapper.deployProxyAsync(); - await erc20Wrapper.setBalancesAndAllowancesAsync(); - - [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); - const erc721Proxy = await erc721Wrapper.deployProxyAsync(); - await erc721Wrapper.setBalancesAndAllowancesAsync(); - const erc721Balances = await erc721Wrapper.getBalancesAsync(); - erc721MakerAssetIds = erc721Balances[makerAddress][erc721Token.address]; - - wethContract = await WETH9Contract.deployFrom0xArtifactAsync(artifacts.EtherToken, provider, txDefaults); - weth = new DummyERC20TokenContract(wethContract.abi, wethContract.address, provider); - erc20Wrapper.addDummyTokenContract(weth); - - const wethAssetData = assetDataUtils.encodeERC20AssetData(wethContract.address); - zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); - const exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync( - artifacts.Exchange, - provider, - txDefaults, - zrxAssetData, - ); - const exchangeContract = new ExchangeContract(exchangeInstance.abi, exchangeInstance.address, provider); - exchangeWrapper = new ExchangeWrapper(exchangeContract, provider); - await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); - await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner); - - await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, { - from: owner, - }); - await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, { - from: owner, - }); - - defaultMakerAssetAddress = erc20TokenA.address; - const defaultTakerAssetAddress = wethContract.address; - const defaultOrderParams = { - exchangeAddress: exchangeInstance.address, - makerAddress, - feeRecipientAddress, - makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), - takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress), - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), DECIMALS_DEFAULT), - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT), - makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), - }; - const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)]; - orderFactory = new OrderFactory(privateKey, defaultOrderParams); - - const forwarderInstance = await ForwarderContract.deployFrom0xArtifactAsync( - artifacts.Forwarder, - provider, - txDefaults, - exchangeInstance.address, - zrxAssetData, - wethAssetData, - ); - forwarderContract = new ForwarderContract(forwarderInstance.abi, forwarderInstance.address, provider); - forwarderWrapper = new ForwarderWrapper(forwarderContract, provider); - const zrxDepositAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18); - await web3Wrapper.awaitTransactionSuccessAsync( - await zrxToken.transfer.sendTransactionAsync(forwarderContract.address, zrxDepositAmount), - ); - erc20Wrapper.addTokenOwnerAddress(forwarderInstance.address); - }); - after(async () => { - await blockchainLifecycle.revertAsync(); - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - erc20Balances = await erc20Wrapper.getBalancesAsync(); - takerEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - orderWithoutFee = await orderFactory.newSignedOrderAsync(); - feeOrder = await orderFactory.newSignedOrderAsync({ - makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), - }); - orderWithFee = await orderFactory.newSignedOrderAsync({ - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), - }); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - - describe('marketSellOrdersWithEth without extra fees', () => { - it('should fill a single order', async () => { - const ordersWithoutFee = [orderWithoutFee]; - const feeOrders: SignedOrder[] = []; - const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); - - tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { - value: ethValue, - from: takerAddress, - }); - const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); - const newBalances = await erc20Wrapper.getBalancesAsync(); - - const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( - ethValue, - MAX_WETH_FILL_PERCENTAGE, - ); - const makerAssetFillAmount = primaryTakerAssetFillAmount - .times(orderWithoutFee.makerAssetAmount) - .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); - const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); - - expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); - expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), - ); - expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), - ); - expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), - ); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); - expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( - constants.ZERO_AMOUNT, - ); - expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); - }); - it('should fill multiple orders', async () => { - const secondOrderWithoutFee = await orderFactory.newSignedOrderAsync(); - const ordersWithoutFee = [orderWithoutFee, secondOrderWithoutFee]; - const feeOrders: SignedOrder[] = []; - const ethValue = ordersWithoutFee[0].takerAssetAmount.plus( - ordersWithoutFee[1].takerAssetAmount.dividedToIntegerBy(2), - ); - - tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { - value: ethValue, - from: takerAddress, - }); - const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); - const newBalances = await erc20Wrapper.getBalancesAsync(); - - const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( - ethValue, - MAX_WETH_FILL_PERCENTAGE, - ); - const firstTakerAssetFillAmount = ordersWithoutFee[0].takerAssetAmount; - const secondTakerAssetFillAmount = primaryTakerAssetFillAmount.minus(firstTakerAssetFillAmount); - - const makerAssetFillAmount = ordersWithoutFee[0].makerAssetAmount.plus( - ordersWithoutFee[1].makerAssetAmount - .times(secondTakerAssetFillAmount) - .dividedToIntegerBy(ordersWithoutFee[1].takerAssetAmount), - ); - const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); - expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); - expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), - ); - expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), - ); - expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), - ); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); - expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( - constants.ZERO_AMOUNT, - ); - expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); - }); - it('should fill the order and pay ZRX fees from a single feeOrder', async () => { - const ordersWithFee = [orderWithFee]; - const feeOrders = [feeOrder]; - const ethValue = orderWithFee.takerAssetAmount.dividedToIntegerBy(2); - - tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { - value: ethValue, - from: takerAddress, - }); - const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); - const newBalances = await erc20Wrapper.getBalancesAsync(); - - const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( - ethValue, - MAX_WETH_FILL_PERCENTAGE, - ); - const makerAssetFillAmount = primaryTakerAssetFillAmount - .times(orderWithoutFee.makerAssetAmount) - .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); - const feeAmount = ForwarderWrapper.getPercentageOfValue( - orderWithFee.takerFee.dividedToIntegerBy(2), - MAX_WETH_FILL_PERCENTAGE, - ); - const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); - const totalEthSpent = primaryTakerAssetFillAmount - .plus(wethSpentOnFeeOrders) - .plus(gasPrice.times(tx.gasUsed)); - - expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); - expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), - ); - expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), - ); - expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), - ); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); - expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( - constants.ZERO_AMOUNT, - ); - expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); - }); - it('should fill the orders and pay ZRX from multiple feeOrders', async () => { - const ordersWithFee = [orderWithFee]; - const ethValue = orderWithFee.takerAssetAmount; - const makerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); - const makerAssetAmount = orderWithFee.takerFee.dividedToIntegerBy(2); - const takerAssetAmount = feeOrder.takerAssetAmount - .times(makerAssetAmount) - .dividedToIntegerBy(feeOrder.makerAssetAmount); - - const firstFeeOrder = await orderFactory.newSignedOrderAsync({ - makerAssetData, - makerAssetAmount, - takerAssetAmount, - }); - const secondFeeOrder = await orderFactory.newSignedOrderAsync({ - makerAssetData, - makerAssetAmount, - takerAssetAmount, - }); - const feeOrders = [firstFeeOrder, secondFeeOrder]; - - tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { - value: ethValue, - from: takerAddress, - }); - const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); - const newBalances = await erc20Wrapper.getBalancesAsync(); - - const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( - ethValue, - MAX_WETH_FILL_PERCENTAGE, - ); - const makerAssetFillAmount = primaryTakerAssetFillAmount - .times(orderWithoutFee.makerAssetAmount) - .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); - const feeAmount = ForwarderWrapper.getPercentageOfValue(orderWithFee.takerFee, MAX_WETH_FILL_PERCENTAGE); - const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); - const totalEthSpent = primaryTakerAssetFillAmount - .plus(wethSpentOnFeeOrders) - .plus(gasPrice.times(tx.gasUsed)); - - expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); - expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), - ); - expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), - ); - expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), - ); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); - expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( - constants.ZERO_AMOUNT, - ); - expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); - }); - it('should fill the order when token is ZRX with fees', async () => { - orderWithFee = await orderFactory.newSignedOrderAsync({ - makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), - }); - const ordersWithFee = [orderWithFee]; - const feeOrders: SignedOrder[] = []; - const ethValue = orderWithFee.takerAssetAmount.dividedToIntegerBy(2); - - tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { - value: ethValue, - from: takerAddress, - }); - const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); - const newBalances = await erc20Wrapper.getBalancesAsync(); - - const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2); - const totalEthSpent = ethValue.plus(gasPrice.times(tx.gasUsed)); - const takerFeePaid = orderWithFee.takerFee.dividedToIntegerBy(2); - const makerFeePaid = orderWithFee.makerFee.dividedToIntegerBy(2); - - expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); - expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFillAmount).minus(makerFeePaid), - ); - expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[takerAddress][zrxToken.address].plus(makerAssetFillAmount).minus(takerFeePaid), - ); - expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][weth.address].plus(ethValue), - ); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); - expect(newBalances[forwarderContract.address][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[forwarderContract.address][zrxToken.address], - ); - expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); - }); - it('should refund remaining ETH if amount is greater than takerAssetAmount', async () => { - const ordersWithoutFee = [orderWithoutFee]; - const feeOrders: SignedOrder[] = []; - const ethValue = orderWithoutFee.takerAssetAmount.times(2); - - tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { - value: ethValue, - from: takerAddress, - }); - const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const totalEthSpent = orderWithoutFee.takerAssetAmount.plus(gasPrice.times(tx.gasUsed)); - - expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); - }); - it('should revert if ZRX cannot be fully repurchased', async () => { - orderWithFee = await orderFactory.newSignedOrderAsync({ - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), DECIMALS_DEFAULT), - }); - const ordersWithFee = [orderWithFee]; - feeOrder = await orderFactory.newSignedOrderAsync({ - makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), - }); - const feeOrders = [feeOrder]; - const ethValue = orderWithFee.takerAssetAmount; - return expectTransactionFailedAsync( - forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { - value: ethValue, - from: takerAddress, - }), - RevertReason.CompleteFillFailed, - ); - }); - it('should not fill orders with different makerAssetData than the first order', async () => { - const makerAssetId = erc721MakerAssetIds[0]; - const erc721SignedOrder = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: new BigNumber(1), - makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), - }); - const erc20SignedOrder = await orderFactory.newSignedOrderAsync(); - const ordersWithoutFee = [erc20SignedOrder, erc721SignedOrder]; - const feeOrders: SignedOrder[] = []; - const ethValue = erc20SignedOrder.takerAssetAmount.plus(erc721SignedOrder.takerAssetAmount); - - tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { - value: ethValue, - from: takerAddress, - }); - const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const totalEthSpent = erc20SignedOrder.takerAssetAmount.plus(gasPrice.times(tx.gasUsed)); - - expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); - }); - }); - describe('marketSellOrdersWithEth with extra fees', () => { - it('should fill the order and send fee to feeRecipient', async () => { - const ordersWithoutFee = [orderWithoutFee]; - const feeOrders: SignedOrder[] = []; - const ethValue = orderWithoutFee.takerAssetAmount.div(2); - - const baseFeePercentage = 2; - feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); - const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); - tx = await forwarderWrapper.marketSellOrdersWithEthAsync( - ordersWithoutFee, - feeOrders, - { - value: ethValue, - from: takerAddress, - }, - { feePercentage, feeRecipient: feeRecipientAddress }, - ); - const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); - const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); - const newBalances = await erc20Wrapper.getBalancesAsync(); - - const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( - ethValue, - MAX_WETH_FILL_PERCENTAGE, - ); - const makerAssetFillAmount = primaryTakerAssetFillAmount - .times(orderWithoutFee.makerAssetAmount) - .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); - const ethSpentOnFee = ForwarderWrapper.getPercentageOfValue(primaryTakerAssetFillAmount, baseFeePercentage); - const totalEthSpent = primaryTakerAssetFillAmount.plus(ethSpentOnFee).plus(gasPrice.times(tx.gasUsed)); - - expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); - expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), - ); - expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), - ); - expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), - ); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); - expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( - constants.ZERO_AMOUNT, - ); - expect(feeRecipientEthBalanceAfter).to.be.bignumber.equal(feeRecipientEthBalanceBefore.plus(ethSpentOnFee)); - expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); - }); - it('should fail if the fee is set too high', async () => { - const ethValue = orderWithoutFee.takerAssetAmount.div(2); - const baseFeePercentage = 6; - feePercentage = ForwarderWrapper.getPercentageOfValue(ethValue, baseFeePercentage); - const ordersWithoutFee = [orderWithoutFee]; - const feeOrders: SignedOrder[] = []; - await expectTransactionFailedAsync( - forwarderWrapper.marketSellOrdersWithEthAsync( - ordersWithoutFee, - feeOrders, - { from: takerAddress, value: ethValue, gasPrice }, - { feePercentage, feeRecipient: feeRecipientAddress }, - ), - RevertReason.FeePercentageTooLarge, - ); - }); - it('should fail if there is not enough ETH remaining to pay the fee', async () => { - const ethValue = orderWithoutFee.takerAssetAmount.div(2); - const baseFeePercentage = 5; - feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); - const ordersWithFee = [orderWithFee]; - const feeOrders = [feeOrder]; - await expectTransactionFailedAsync( - forwarderWrapper.marketSellOrdersWithEthAsync( - ordersWithFee, - feeOrders, - { from: takerAddress, value: ethValue, gasPrice }, - { feePercentage, feeRecipient: feeRecipientAddress }, - ), - RevertReason.InsufficientEthRemaining, - ); - }); - }); - describe('marketBuyOrdersWithEth without extra fees', () => { - it('should buy the exact amount of makerAsset in a single order', async () => { - const ordersWithoutFee = [orderWithoutFee]; - const feeOrders: SignedOrder[] = []; - const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); - const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); - - tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { - value: ethValue, - from: takerAddress, - }); - const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); - const newBalances = await erc20Wrapper.getBalancesAsync(); - - const primaryTakerAssetFillAmount = ethValue; - const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); - - expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); - expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), - ); - expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), - ); - expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), - ); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); - expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( - constants.ZERO_AMOUNT, - ); - expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); - }); - it('should buy the exact amount of makerAsset in multiple orders', async () => { - const secondOrderWithoutFee = await orderFactory.newSignedOrderAsync(); - const ordersWithoutFee = [orderWithoutFee, secondOrderWithoutFee]; - const feeOrders: SignedOrder[] = []; - const makerAssetFillAmount = ordersWithoutFee[0].makerAssetAmount.plus( - ordersWithoutFee[1].makerAssetAmount.dividedToIntegerBy(2), - ); - const ethValue = ordersWithoutFee[0].takerAssetAmount.plus( - ordersWithoutFee[1].takerAssetAmount.dividedToIntegerBy(2), - ); - - tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { - value: ethValue, - from: takerAddress, - }); - const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); - const newBalances = await erc20Wrapper.getBalancesAsync(); - - const primaryTakerAssetFillAmount = ethValue; - const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); - - expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); - expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), - ); - expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), - ); - expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), - ); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); - expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( - constants.ZERO_AMOUNT, - ); - expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); - }); - it('should buy the exact amount of makerAsset and return excess ETH', async () => { - const ordersWithoutFee = [orderWithoutFee]; - const feeOrders: SignedOrder[] = []; - const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); - const ethValue = orderWithoutFee.takerAssetAmount; - - tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { - value: ethValue, - from: takerAddress, - }); - const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); - const newBalances = await erc20Wrapper.getBalancesAsync(); - - const primaryTakerAssetFillAmount = ethValue.dividedToIntegerBy(2); - const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); - - expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); - expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), - ); - expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), - ); - expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), - ); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); - expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( - constants.ZERO_AMOUNT, - ); - expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); - }); - it('should buy the exact amount of makerAsset and pay ZRX from feeOrders', async () => { - const ordersWithFee = [orderWithFee]; - const feeOrders = [feeOrder]; - const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2); - const ethValue = orderWithFee.takerAssetAmount; - - tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { - value: ethValue, - from: takerAddress, - }); - const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); - const newBalances = await erc20Wrapper.getBalancesAsync(); - - const primaryTakerAssetFillAmount = orderWithFee.takerAssetAmount.dividedToIntegerBy(2); - const feeAmount = orderWithFee.takerFee.dividedToIntegerBy(2); - const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); - const totalEthSpent = primaryTakerAssetFillAmount - .plus(wethSpentOnFeeOrders) - .plus(gasPrice.times(tx.gasUsed)); - - expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); - expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), - ); - expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), - ); - expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), - ); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); - expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( - constants.ZERO_AMOUNT, - ); - expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); - }); - it('should buy slightly greater than makerAssetAmount when buying ZRX', async () => { - orderWithFee = await orderFactory.newSignedOrderAsync({ - makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), - }); - const ordersWithFee = [orderWithFee]; - const feeOrders: SignedOrder[] = []; - const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2); - const ethValue = orderWithFee.takerAssetAmount; - tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { - value: ethValue, - from: takerAddress, - }); - const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); - const newBalances = await erc20Wrapper.getBalancesAsync(); - - const primaryTakerAssetFillAmount = ForwarderWrapper.getWethForFeeOrders( - makerAssetFillAmount, - ordersWithFee, - ); - const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); - const makerAssetFilledAmount = orderWithFee.makerAssetAmount - .times(primaryTakerAssetFillAmount) - .dividedToIntegerBy(orderWithFee.takerAssetAmount); - const takerFeePaid = orderWithFee.takerFee - .times(primaryTakerAssetFillAmount) - .dividedToIntegerBy(orderWithFee.takerAssetAmount); - const makerFeePaid = orderWithFee.makerFee - .times(primaryTakerAssetFillAmount) - .dividedToIntegerBy(orderWithFee.takerAssetAmount); - const totalZrxPurchased = makerAssetFilledAmount.minus(takerFeePaid); - // Up to 1 wei worth of ZRX will be overbought per order - const maxOverboughtZrx = new BigNumber(1) - .times(orderWithFee.makerAssetAmount) - .dividedToIntegerBy(orderWithFee.takerAssetAmount); - - expect(totalZrxPurchased).to.be.bignumber.gte(makerAssetFillAmount); - expect(totalZrxPurchased).to.be.bignumber.lte(makerAssetFillAmount.plus(maxOverboughtZrx)); - expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); - expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFilledAmount).minus(makerFeePaid), - ); - expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[takerAddress][zrxToken.address].plus(totalZrxPurchased), - ); - expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), - ); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); - expect(newBalances[forwarderContract.address][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[forwarderContract.address][zrxToken.address], - ); - expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); - }); - it('should revert if the amount of ETH sent is too low to fill the makerAssetAmount', async () => { - const ordersWithoutFee = [orderWithoutFee]; - const feeOrders: SignedOrder[] = []; - const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); - const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(4); - return expectTransactionFailedAsync( - forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { - value: ethValue, - from: takerAddress, - }), - RevertReason.CompleteFillFailed, - ); - }); - it('should buy an ERC721 asset from a single order', async () => { - const makerAssetId = erc721MakerAssetIds[0]; - orderWithoutFee = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: new BigNumber(1), - makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), - }); - const ordersWithoutFee = [orderWithoutFee]; - const feeOrders: SignedOrder[] = []; - const makerAssetFillAmount = new BigNumber(1); - const ethValue = orderWithFee.takerAssetAmount; - - tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { - from: takerAddress, - value: ethValue, - }); - const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); - const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); - const newBalances = await erc20Wrapper.getBalancesAsync(); - - const primaryTakerAssetFillAmount = ethValue; - const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); - expect(newOwner).to.be.bignumber.equal(takerAddress); - expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); - expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), - ); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); - expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( - constants.ZERO_AMOUNT, - ); - expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); - }); - it('should revert if buying an ERC721 asset when later orders contain different makerAssetData', async () => { - const makerAssetId = erc721MakerAssetIds[0]; - orderWithoutFee = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: new BigNumber(1), - makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), - }); - const differentMakerAssetDataOrder = await orderFactory.newSignedOrderAsync(); - const ordersWithoutFee = [orderWithoutFee, differentMakerAssetDataOrder]; - const feeOrders: SignedOrder[] = []; - const makerAssetFillAmount = new BigNumber(1).plus(differentMakerAssetDataOrder.makerAssetAmount); - const ethValue = orderWithFee.takerAssetAmount; - return expectTransactionFailedAsync( - forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { - value: ethValue, - from: takerAddress, - }), - RevertReason.CompleteFillFailed, - ); - }); - it('should buy an ERC721 asset and pay ZRX fees from a single fee order', async () => { - const makerAssetId = erc721MakerAssetIds[0]; - orderWithFee = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: new BigNumber(1), - makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), - }); - const ordersWithFee = [orderWithFee]; - const feeOrders = [feeOrder]; - const makerAssetFillAmount = orderWithFee.makerAssetAmount; - const primaryTakerAssetFillAmount = orderWithFee.takerAssetAmount; - const feeAmount = orderWithFee.takerFee; - const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); - const ethValue = primaryTakerAssetFillAmount.plus(wethSpentOnFeeOrders); - - tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { - value: ethValue, - from: takerAddress, - }); - const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); - const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); - const newBalances = await erc20Wrapper.getBalancesAsync(); - - const totalEthSpent = ethValue.plus(gasPrice.times(tx.gasUsed)); - - expect(newOwner).to.be.bignumber.equal(takerAddress); - expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); - expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), - ); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); - expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( - constants.ZERO_AMOUNT, - ); - expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); - }); - it('should buy an ERC721 asset and pay ZRX fees from multiple fee orders', async () => { - const makerAssetId = erc721MakerAssetIds[0]; - orderWithFee = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: new BigNumber(1), - makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), - }); - const ordersWithFee = [orderWithFee]; - const makerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); - const makerAssetAmount = orderWithFee.takerFee.dividedToIntegerBy(2); - const takerAssetAmount = feeOrder.takerAssetAmount - .times(makerAssetAmount) - .dividedToIntegerBy(feeOrder.makerAssetAmount); - - const firstFeeOrder = await orderFactory.newSignedOrderAsync({ - makerAssetData, - makerAssetAmount, - takerAssetAmount, - }); - const secondFeeOrder = await orderFactory.newSignedOrderAsync({ - makerAssetData, - makerAssetAmount, - takerAssetAmount, - }); - const feeOrders = [firstFeeOrder, secondFeeOrder]; - - const makerAssetFillAmount = orderWithFee.makerAssetAmount; - const primaryTakerAssetFillAmount = orderWithFee.takerAssetAmount; - const feeAmount = orderWithFee.takerFee; - const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); - const ethValue = primaryTakerAssetFillAmount.plus(wethSpentOnFeeOrders); - - tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { - value: ethValue, - from: takerAddress, - }); - const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); - const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); - const newBalances = await erc20Wrapper.getBalancesAsync(); - - const totalEthSpent = ethValue.plus(gasPrice.times(tx.gasUsed)); - - expect(newOwner).to.be.bignumber.equal(takerAddress); - expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); - expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), - ); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); - expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( - constants.ZERO_AMOUNT, - ); - expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); - }); - }); - describe('marketBuyOrdersWithEth with extra fees', () => { - it('should buy an asset and send fee to feeRecipient', async () => { - const ordersWithoutFee = [orderWithoutFee]; - const feeOrders: SignedOrder[] = []; - const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); - const ethValue = orderWithoutFee.takerAssetAmount; - - const baseFeePercentage = 2; - feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); - const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); - tx = await forwarderWrapper.marketBuyOrdersWithEthAsync( - ordersWithoutFee, - feeOrders, - makerAssetFillAmount, - { - value: ethValue, - from: takerAddress, - }, - { feePercentage, feeRecipient: feeRecipientAddress }, - ); - const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); - const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); - const newBalances = await erc20Wrapper.getBalancesAsync(); - - const primaryTakerAssetFillAmount = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); - const ethSpentOnFee = ForwarderWrapper.getPercentageOfValue(primaryTakerAssetFillAmount, baseFeePercentage); - const totalEthSpent = primaryTakerAssetFillAmount.plus(ethSpentOnFee).plus(gasPrice.times(tx.gasUsed)); - - expect(feeRecipientEthBalanceAfter).to.be.bignumber.equal(feeRecipientEthBalanceBefore.plus(ethSpentOnFee)); - expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); - expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), - ); - expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), - ); - expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), - ); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); - expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( - constants.ZERO_AMOUNT, - ); - expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); - }); - it('should fail if the fee is set too high', async () => { - const ordersWithoutFee = [orderWithoutFee]; - const feeOrders: SignedOrder[] = []; - const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); - const ethValue = orderWithoutFee.takerAssetAmount; - - const baseFeePercentage = 6; - feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); - await expectTransactionFailedAsync( - forwarderWrapper.marketBuyOrdersWithEthAsync( - ordersWithoutFee, - feeOrders, - makerAssetFillAmount, - { - value: ethValue, - from: takerAddress, - }, - { feePercentage, feeRecipient: feeRecipientAddress }, - ), - RevertReason.FeePercentageTooLarge, - ); - }); - it('should fail if there is not enough ETH remaining to pay the fee', async () => { - const ordersWithoutFee = [orderWithoutFee]; - const feeOrders: SignedOrder[] = []; - const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); - const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); - - const baseFeePercentage = 2; - feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); - await expectTransactionFailedAsync( - forwarderWrapper.marketBuyOrdersWithEthAsync( - ordersWithoutFee, - feeOrders, - makerAssetFillAmount, - { - value: ethValue, - from: takerAddress, - }, - { feePercentage, feeRecipient: feeRecipientAddress }, - ), - RevertReason.InsufficientEthRemaining, - ); - }); - }); - describe('withdrawAsset', () => { - it('should allow owner to withdraw ERC20 tokens', async () => { - const zrxWithdrawAmount = erc20Balances[forwarderContract.address][zrxToken.address]; - await forwarderWrapper.withdrawAssetAsync(zrxAssetData, zrxWithdrawAmount, { from: owner }); - const newBalances = await erc20Wrapper.getBalancesAsync(); - expect(newBalances[owner][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[owner][zrxToken.address].plus(zrxWithdrawAmount), - ); - expect(newBalances[forwarderContract.address][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[forwarderContract.address][zrxToken.address].minus(zrxWithdrawAmount), - ); - }); - it('should revert if not called by owner', async () => { - const zrxWithdrawAmount = erc20Balances[forwarderContract.address][zrxToken.address]; - await expectTransactionFailedAsync( - forwarderWrapper.withdrawAssetAsync(zrxAssetData, zrxWithdrawAmount, { from: makerAddress }), - RevertReason.OnlyContractOwner, - ); - }); - }); -}); -// tslint:disable:max-file-line-count -// tslint:enable:no-unnecessary-type-assertion diff --git a/packages/contracts/test/utils/artifacts.ts b/packages/contracts/test/utils/artifacts.ts index e86ad5406..21dd932c6 100644 --- a/packages/contracts/test/utils/artifacts.ts +++ b/packages/contracts/test/utils/artifacts.ts @@ -15,6 +15,7 @@ import * as InvalidERC721Receiver from '../../artifacts/InvalidERC721Receiver.js import * as MixinAuthorizable from '../../artifacts/MixinAuthorizable.json'; import * as MultiSigWallet from '../../artifacts/MultiSigWallet.json'; import * as MultiSigWalletWithTimeLock from '../../artifacts/MultiSigWalletWithTimeLock.json'; +import * as OrderValidator from '../../artifacts/OrderValidator.json'; import * as TestAssetProxyDispatcher from '../../artifacts/TestAssetProxyDispatcher.json'; import * as TestAssetProxyOwner from '../../artifacts/TestAssetProxyOwner.json'; import * as TestConstants from '../../artifacts/TestConstants.json'; @@ -46,6 +47,7 @@ export const artifacts = { MixinAuthorizable: (MixinAuthorizable as any) as ContractArtifact, MultiSigWallet: (MultiSigWallet as any) as ContractArtifact, MultiSigWalletWithTimeLock: (MultiSigWalletWithTimeLock as any) as ContractArtifact, + OrderValidator: (OrderValidator as any) as ContractArtifact, TestAssetProxyOwner: (TestAssetProxyOwner as any) as ContractArtifact, TestAssetProxyDispatcher: (TestAssetProxyDispatcher as any) as ContractArtifact, TestConstants: (TestConstants as any) as ContractArtifact, -- cgit v1.2.3 From 7fa5b474ebac649b6a23c8b9108e07c117dcc00b Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Tue, 21 Aug 2018 10:15:45 -0700 Subject: Add ZRX balances and allowances to TraderInfo --- .../extensions/OrderValidator/OrderValidator.sol | 23 +++-- .../contracts/test/extensions/order_validator.ts | 109 ++++++++++++++++++++- 2 files changed, 122 insertions(+), 10 deletions(-) (limited to 'packages/contracts') diff --git a/packages/contracts/src/2.0.0/extensions/OrderValidator/OrderValidator.sol b/packages/contracts/src/2.0.0/extensions/OrderValidator/OrderValidator.sol index 1c9cc6978..8f4ce9ccc 100644 --- a/packages/contracts/src/2.0.0/extensions/OrderValidator/OrderValidator.sol +++ b/packages/contracts/src/2.0.0/extensions/OrderValidator/OrderValidator.sol @@ -34,20 +34,26 @@ contract OrderValidator { using LibBytes for bytes; struct TraderInfo { - uint256 makerBalance; // Maker's balance of makerAsset - uint256 makerAllowance; // Maker's allowance to corresponding AssetProxy - uint256 takerBalance; // Taker's balance of takerAsset - uint256 takerAllowance; // Taker's allowance to corresponding AssetProxy + uint256 makerBalance; // Maker's balance of makerAsset + uint256 makerAllowance; // Maker's allowance to corresponding AssetProxy + uint256 takerBalance; // Taker's balance of takerAsset + uint256 takerAllowance; // Taker's allowance to corresponding AssetProxy + uint256 makerZrxBalance; // Maker's balance of ZRX + uint256 makerZrxAllowance; // Maker's allowance of ZRX to ERC20Proxy + uint256 takerZrxBalance; // Taker's balance of ZRX + uint256 takerZrxAllowance; // Taker's allowance of ZRX to ERC20Proxy } - // Exchange contract. - // solhint-disable-next-line var-name-mixedcase + // solhint-disable var-name-mixedcase IExchange internal EXCHANGE; + bytes internal ZRX_ASSET_DATA; + // solhint-enable var-name-mixedcase - constructor (address _exchange) + constructor (address _exchange, bytes memory _zrxAssetData) public { EXCHANGE = IExchange(_exchange); + ZRX_ASSET_DATA = _zrxAssetData; } /// @dev Fetches information for order and maker/taker of order. @@ -89,6 +95,9 @@ contract OrderValidator { { (traderInfo.makerBalance, traderInfo.makerAllowance) = getBalanceAndAllowance(order.makerAddress, order.makerAssetData); (traderInfo.takerBalance, traderInfo.takerAllowance) = getBalanceAndAllowance(takerAddress, order.takerAssetData); + bytes memory zrxAssetData = ZRX_ASSET_DATA; + (traderInfo.makerZrxBalance, traderInfo.makerZrxAllowance) = getBalanceAndAllowance(order.makerAddress, zrxAssetData); + (traderInfo.takerZrxBalance, traderInfo.takerZrxAllowance) = getBalanceAndAllowance(takerAddress, zrxAssetData); return traderInfo; } diff --git a/packages/contracts/test/extensions/order_validator.ts b/packages/contracts/test/extensions/order_validator.ts index 22a140465..a3b704f27 100644 --- a/packages/contracts/test/extensions/order_validator.ts +++ b/packages/contracts/test/extensions/order_validator.ts @@ -33,6 +33,7 @@ describe('OrderValidator', () => { let erc721AssetData: string; let erc20Token: DummyERC20TokenContract; + let zrxToken: DummyERC20TokenContract; let erc721Token: DummyERC721TokenContract; let exchange: ExchangeContract; let orderValidator: OrderValidatorContract; @@ -62,18 +63,22 @@ describe('OrderValidator', () => { const erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); const erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); - const numDummyErc20ToDeploy = 1; - [erc20Token] = await erc20Wrapper.deployDummyTokensAsync(numDummyErc20ToDeploy, constants.DUMMY_TOKEN_DECIMALS); + const numDummyErc20ToDeploy = 2; + [erc20Token, zrxToken] = await erc20Wrapper.deployDummyTokensAsync( + numDummyErc20ToDeploy, + constants.DUMMY_TOKEN_DECIMALS, + ); erc20Proxy = await erc20Wrapper.deployProxyAsync(); [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); erc721Proxy = await erc721Wrapper.deployProxyAsync(); + const zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); exchange = await ExchangeContract.deployFrom0xArtifactAsync( artifacts.Exchange, provider, txDefaults, - assetDataUtils.encodeERC20AssetData(erc20Token.address), + zrxAssetData, ); const exchangeWrapper = new ExchangeWrapper(exchange, provider); await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); @@ -84,6 +89,7 @@ describe('OrderValidator', () => { provider, txDefaults, exchange.address, + zrxAssetData, ); erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20Token.address); @@ -221,10 +227,16 @@ describe('OrderValidator', () => { expect(traderInfo.makerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); expect(traderInfo.takerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); expect(traderInfo.takerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo.makerZrxBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo.makerZrxAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo.takerZrxBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo.takerZrxAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); it('should return the correct info when balances and allowances are set', async () => { const makerBalance = new BigNumber(123); const makerAllowance = new BigNumber(456); + const makerZrxBalance = new BigNumber(789); + const takerZrxAllowance = new BigNumber(987); await web3Wrapper.awaitTransactionSuccessAsync( await erc20Token.setBalance.sendTransactionAsync(makerAddress, makerBalance), constants.AWAIT_TRANSACTION_MINED_MS, @@ -235,6 +247,16 @@ describe('OrderValidator', () => { }), constants.AWAIT_TRANSACTION_MINED_MS, ); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.setBalance.sendTransactionAsync(makerAddress, makerZrxBalance), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.approve.sendTransactionAsync(erc20Proxy.address, takerZrxAllowance, { + from: takerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); await web3Wrapper.awaitTransactionSuccessAsync( await erc721Token.mint.sendTransactionAsync(takerAddress, tokenId), constants.AWAIT_TRANSACTION_MINED_MS, @@ -250,6 +272,10 @@ describe('OrderValidator', () => { expect(traderInfo.makerAllowance).to.be.bignumber.equal(makerAllowance); expect(traderInfo.takerBalance).to.be.bignumber.equal(ERC721_BALANCE); expect(traderInfo.takerAllowance).to.be.bignumber.equal(ERC721_ALLOWANCE); + expect(traderInfo.makerZrxBalance).to.be.bignumber.equal(makerZrxBalance); + expect(traderInfo.makerZrxAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo.takerZrxBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo.takerZrxAllowance).to.be.bignumber.equal(takerZrxAllowance); }); }); describe('getTradersInfo', () => { @@ -267,14 +293,24 @@ describe('OrderValidator', () => { expect(traderInfo1.makerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); expect(traderInfo1.takerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); expect(traderInfo1.takerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.makerZrxBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.makerZrxAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.takerZrxBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.takerZrxAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); expect(traderInfo2.makerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); expect(traderInfo2.makerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); expect(traderInfo2.takerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); expect(traderInfo2.takerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo2.makerZrxBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo2.makerZrxAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo2.takerZrxBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo2.takerZrxAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); it('should return the correct info when balances and allowances are set', async () => { const makerBalance = new BigNumber(123); const makerAllowance = new BigNumber(456); + const makerZrxBalance = new BigNumber(789); + const takerZrxAllowance = new BigNumber(987); await web3Wrapper.awaitTransactionSuccessAsync( await erc20Token.setBalance.sendTransactionAsync(makerAddress, makerBalance), constants.AWAIT_TRANSACTION_MINED_MS, @@ -285,6 +321,16 @@ describe('OrderValidator', () => { }), constants.AWAIT_TRANSACTION_MINED_MS, ); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.setBalance.sendTransactionAsync(makerAddress, makerZrxBalance), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.approve.sendTransactionAsync(erc20Proxy.address, takerZrxAllowance, { + from: takerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); const isApproved = true; await web3Wrapper.awaitTransactionSuccessAsync( await erc721Token.setApprovalForAll.sendTransactionAsync(erc721Proxy.address, isApproved, { @@ -304,10 +350,18 @@ describe('OrderValidator', () => { expect(traderInfo1.makerAllowance).to.be.bignumber.equal(makerAllowance); expect(traderInfo1.takerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); expect(traderInfo1.takerAllowance).to.be.bignumber.equal(ERC721_ALLOWANCE); + expect(traderInfo1.makerZrxBalance).to.be.bignumber.equal(makerZrxBalance); + expect(traderInfo1.makerZrxAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.takerZrxBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.takerZrxAllowance).to.be.bignumber.equal(takerZrxAllowance); expect(traderInfo2.makerBalance).to.be.bignumber.equal(makerBalance); expect(traderInfo2.makerAllowance).to.be.bignumber.equal(makerAllowance); expect(traderInfo2.takerBalance).to.be.bignumber.equal(ERC721_BALANCE); expect(traderInfo2.takerAllowance).to.be.bignumber.equal(ERC721_ALLOWANCE); + expect(traderInfo2.makerZrxBalance).to.be.bignumber.equal(makerZrxBalance); + expect(traderInfo2.makerZrxAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo2.takerZrxBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo2.takerZrxAllowance).to.be.bignumber.equal(takerZrxAllowance); }); }); describe('getOrderAndTraderInfo', () => { @@ -327,10 +381,16 @@ describe('OrderValidator', () => { expect(traderInfo.makerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); expect(traderInfo.takerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); expect(traderInfo.takerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo.makerZrxBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo.makerZrxAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo.takerZrxBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo.takerZrxAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); it('should return the correct info when balances and allowances are set', async () => { const makerBalance = new BigNumber(123); const makerAllowance = new BigNumber(456); + const makerZrxBalance = new BigNumber(789); + const takerZrxAllowance = new BigNumber(987); await web3Wrapper.awaitTransactionSuccessAsync( await erc20Token.setBalance.sendTransactionAsync(makerAddress, makerBalance), constants.AWAIT_TRANSACTION_MINED_MS, @@ -341,6 +401,16 @@ describe('OrderValidator', () => { }), constants.AWAIT_TRANSACTION_MINED_MS, ); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.setBalance.sendTransactionAsync(makerAddress, makerZrxBalance), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.approve.sendTransactionAsync(erc20Proxy.address, takerZrxAllowance, { + from: takerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); await web3Wrapper.awaitTransactionSuccessAsync( await erc721Token.mint.sendTransactionAsync(takerAddress, tokenId), constants.AWAIT_TRANSACTION_MINED_MS, @@ -363,6 +433,10 @@ describe('OrderValidator', () => { expect(traderInfo.makerAllowance).to.be.bignumber.equal(makerAllowance); expect(traderInfo.takerBalance).to.be.bignumber.equal(ERC721_BALANCE); expect(traderInfo.takerAllowance).to.be.bignumber.equal(ERC721_ALLOWANCE); + expect(traderInfo.makerZrxBalance).to.be.bignumber.equal(makerZrxBalance); + expect(traderInfo.makerZrxAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo.takerZrxBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo.takerZrxAllowance).to.be.bignumber.equal(takerZrxAllowance); }); }); describe('getOrdersAndTradersInfo', () => { @@ -391,14 +465,24 @@ describe('OrderValidator', () => { expect(traderInfo1.makerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); expect(traderInfo1.takerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); expect(traderInfo1.takerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.makerZrxBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.makerZrxAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.takerZrxBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.takerZrxAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); expect(traderInfo2.makerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); expect(traderInfo2.makerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); expect(traderInfo2.takerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); expect(traderInfo2.takerAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo2.makerZrxBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo2.makerZrxAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo2.takerZrxBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo2.takerZrxAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); it('should return the correct info when balances and allowances are set', async () => { const makerBalance = new BigNumber(123); const makerAllowance = new BigNumber(456); + const makerZrxBalance = new BigNumber(789); + const takerZrxAllowance = new BigNumber(987); await web3Wrapper.awaitTransactionSuccessAsync( await erc20Token.setBalance.sendTransactionAsync(makerAddress, makerBalance), constants.AWAIT_TRANSACTION_MINED_MS, @@ -409,6 +493,16 @@ describe('OrderValidator', () => { }), constants.AWAIT_TRANSACTION_MINED_MS, ); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.setBalance.sendTransactionAsync(makerAddress, makerZrxBalance), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.approve.sendTransactionAsync(erc20Proxy.address, takerZrxAllowance, { + from: takerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); const isApproved = true; await web3Wrapper.awaitTransactionSuccessAsync( await erc721Token.setApprovalForAll.sendTransactionAsync(erc721Proxy.address, isApproved, { @@ -438,10 +532,19 @@ describe('OrderValidator', () => { expect(traderInfo1.makerAllowance).to.be.bignumber.equal(makerAllowance); expect(traderInfo1.takerBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); expect(traderInfo1.takerAllowance).to.be.bignumber.equal(ERC721_ALLOWANCE); + expect(traderInfo1.makerZrxBalance).to.be.bignumber.equal(makerZrxBalance); + expect(traderInfo1.makerZrxAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.takerZrxBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo1.takerZrxAllowance).to.be.bignumber.equal(takerZrxAllowance); expect(traderInfo2.makerBalance).to.be.bignumber.equal(makerBalance); expect(traderInfo2.makerAllowance).to.be.bignumber.equal(makerAllowance); expect(traderInfo2.takerBalance).to.be.bignumber.equal(ERC721_BALANCE); expect(traderInfo2.takerAllowance).to.be.bignumber.equal(ERC721_ALLOWANCE); + expect(traderInfo2.makerZrxBalance).to.be.bignumber.equal(makerZrxBalance); + expect(traderInfo2.makerZrxAllowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo2.takerZrxBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(traderInfo2.takerZrxAllowance).to.be.bignumber.equal(takerZrxAllowance); }); }); }); +// tslint:disable:max-file-line-count -- cgit v1.2.3 From 3760eb5bafcc0980b3a42dd5f10e745390702b16 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Tue, 21 Aug 2018 18:14:22 -0700 Subject: Add getBalancesAndAllowances --- .../extensions/OrderValidator/OrderValidator.sol | 19 ++++++++ .../contracts/test/extensions/order_validator.ts | 50 ++++++++++++++++++++++ 2 files changed, 69 insertions(+) (limited to 'packages/contracts') diff --git a/packages/contracts/src/2.0.0/extensions/OrderValidator/OrderValidator.sol b/packages/contracts/src/2.0.0/extensions/OrderValidator/OrderValidator.sol index 8f4ce9ccc..a18345245 100644 --- a/packages/contracts/src/2.0.0/extensions/OrderValidator/OrderValidator.sol +++ b/packages/contracts/src/2.0.0/extensions/OrderValidator/OrderValidator.sol @@ -158,6 +158,25 @@ contract OrderValidator { return (balance, allowance); } + /// @dev Fetches token balances and allowances of an address for each given assetProxy. Supports ERC20 and ERC721. + /// @param target Address to fetch balances and allowances of. + /// @param assetData Array of encoded byte arrays that can be decoded by a specified proxy contract when transferring asset. + /// @return Balances and allowances of assets. + /// For ERC721 tokens, these values will always be 1 or 0. + function getBalancesAndAllowances(address target, bytes[] memory assetData) + public + view + returns (uint256[] memory, uint256[] memory) + { + uint256 length = assetData.length; + uint256[] memory balances = new uint256[](length); + uint256[] memory allowances = new uint256[](length); + for (uint256 i = 0; i != length; i++) { + (balances[i], allowances[i]) = getBalanceAndAllowance(target, assetData[i]); + } + return (balances, allowances); + } + /// @dev Calls `token.ownerOf(tokenId)`, but returns a null owner instead of reverting on an unowned token. /// @param token Address of ERC721 token. /// @param tokenId The identifier for the specific NFT. diff --git a/packages/contracts/test/extensions/order_validator.ts b/packages/contracts/test/extensions/order_validator.ts index a3b704f27..3a57cfc37 100644 --- a/packages/contracts/test/extensions/order_validator.ts +++ b/packages/contracts/test/extensions/order_validator.ts @@ -217,6 +217,56 @@ describe('OrderValidator', () => { }); }); }); + describe('getBalancesAndAllowances', () => { + it('should return the correct balances and allowances when all values are 0', async () => { + const [ + [erc20Balance, erc721Balance], + [erc20Allowance, erc721Allowance], + ] = await orderValidator.getBalancesAndAllowances.callAsync(makerAddress, [ + erc20AssetData, + erc721AssetData, + ]); + expect(erc20Balance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(erc721Balance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(erc20Allowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(erc721Allowance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should return the correct balances and allowances when balances and allowances are non-zero', async () => { + const balance = new BigNumber(123); + const allowance = new BigNumber(456); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Token.setBalance.sendTransactionAsync(makerAddress, balance), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Token.approve.sendTransactionAsync(erc20Proxy.address, allowance, { + from: makerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.mint.sendTransactionAsync(makerAddress, tokenId), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.approve.sendTransactionAsync(erc721Proxy.address, tokenId, { + from: makerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const [ + [erc20Balance, erc721Balance], + [erc20Allowance, erc721Allowance], + ] = await orderValidator.getBalancesAndAllowances.callAsync(makerAddress, [ + erc20AssetData, + erc721AssetData, + ]); + expect(erc20Balance).to.be.bignumber.equal(balance); + expect(erc721Balance).to.be.bignumber.equal(ERC721_BALANCE); + expect(erc20Allowance).to.be.bignumber.equal(allowance); + expect(erc721Allowance).to.be.bignumber.equal(ERC721_ALLOWANCE); + }); + }); describe('getTraderInfo', () => { beforeEach(async () => { signedOrder = await orderFactory.newSignedOrderAsync(); -- cgit v1.2.3