diff options
Diffstat (limited to 'contracts/extensions')
50 files changed, 8201 insertions, 0 deletions
diff --git a/contracts/extensions/CHANGELOG.json b/contracts/extensions/CHANGELOG.json new file mode 100644 index 000000000..40e25565e --- /dev/null +++ b/contracts/extensions/CHANGELOG.json @@ -0,0 +1,37 @@ +[ + { + "version": "1.2.0", + "changes": [ + { + "note": "Added Dutch Auction Wrapper", + "pr": 1465 + } + ] + }, + { + "version": "1.1.0", + "changes": [ + { + "note": "Added Balance Threshold Filter", + "pr": 1383 + }, + { + "note": "Add OrderMatcher", + "pr": 1117 + }, + { + "note": "Add OrderValidator", + "pr": 1464 + } + ] + }, + { + "timestamp": 1544741676, + "version": "1.0.2", + "changes": [ + { + "note": "Dependencies updated" + } + ] + } +] diff --git a/contracts/extensions/CHANGELOG.md b/contracts/extensions/CHANGELOG.md new file mode 100644 index 000000000..716353d05 --- /dev/null +++ b/contracts/extensions/CHANGELOG.md @@ -0,0 +1,10 @@ +<!-- +changelogUtils.file is auto-generated using the monorepo-scripts package. Don't edit directly. +Edit the package's CHANGELOG.json file only. +--> + +CHANGELOG + +## v1.0.2 - _December 13, 2018_ + + * Dependencies updated diff --git a/contracts/extensions/DEPLOYS.json b/contracts/extensions/DEPLOYS.json new file mode 100644 index 000000000..1a093bf77 --- /dev/null +++ b/contracts/extensions/DEPLOYS.json @@ -0,0 +1,31 @@ +[ + { + "name": "Forwarder", + "version": "1.1.0", + "changes": [ + { + "note": "Round up when calculating remaining amounts in marketBuy functions", + "pr": 1162, + "networks": { + "1": "0x5468a1dc173652ee28d249c271fa9933144746b1", + "3": "0x2240dab907db71e64d3e0dba4800c83b5c502d4e", + "42": "0x17992e4ffb22730138e4b62aaa6367fa9d3699a6" + } + } + ] + }, + { + "name": "Forwarder", + "version": "1.0.0", + "changes": [ + { + "note": "protocol v2 deploy", + "networks": { + "1": "0x7afc2d5107af94c462a194d2c21b5bdd238709d6", + "3": "0x3983e204b12b3c02fb0638caf2cd406a62e0ead3", + "42": "0xd85e2fa7e7e252b27b01bf0d65c946959d2f45b8" + } + } + ] + } +] diff --git a/contracts/extensions/README.md b/contracts/extensions/README.md new file mode 100644 index 000000000..820f6e78a --- /dev/null +++ b/contracts/extensions/README.md @@ -0,0 +1,69 @@ +## Contract extensions + +Smart contracts that implement extensions for the 0x protocol. + +## Usage + +Contract extensions of the protocol can be found in the [contracts](./contracts) directory. This directory contains contracts that interact with the 2.0.0 contracts and will be used in production, such as the [Forwarder](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/forwarder-specification.md) contract. + +## Bug bounty + +A bug bounty for the 2.0.0 contracts is ongoing! Instructions can be found [here](https://0xproject.com/wiki#Bug-Bounty). + +## Contributing + +We strongly recommend that the community help us make improvements and determine the future direction of the protocol. To report bugs within this package, please create an issue in this repository. + +For proposals regarding the 0x protocol's smart contract architecture, message format, or additional functionality, go to the [0x Improvement Proposals (ZEIPs)](https://github.com/0xProject/ZEIPs) repository and follow the contribution guidelines provided therein. + +Please read our [contribution guidelines](../../CONTRIBUTING.md) before getting started. + +### Install Dependencies + +If you don't have yarn workspaces enabled (Yarn < v1.0) - enable them: + +```bash +yarn config set workspaces-experimental true +``` + +Then install dependencies + +```bash +yarn install +``` + +### Build + +To build this package and all other monorepo packages that it depends on, run the following from the monorepo root directory: + +```bash +PKG=@0x/contracts-extensions yarn build +``` + +Or continuously rebuild on change: + +```bash +PKG=@0x/contracts-extensions yarn watch +``` + +### Clean + +```bash +yarn clean +``` + +### Lint + +```bash +yarn lint +``` + +### Run Tests + +```bash +yarn test +``` + +#### Testing options + +Contracts testing options like coverage, profiling, revert traces or backing node choosing - are described [here](../TESTING.md). diff --git a/contracts/extensions/compiler.json b/contracts/extensions/compiler.json new file mode 100644 index 000000000..2bb468724 --- /dev/null +++ b/contracts/extensions/compiler.json @@ -0,0 +1,22 @@ +{ + "artifactsDir": "./generated-artifacts", + "contractsDir": "./contracts", + "compilerSettings": { + "optimizer": { + "enabled": true, + "runs": 1000000 + }, + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap" + ] + } + } + }, + "contracts": ["BalanceThresholdFilter", "DutchAuction", "Forwarder", "OrderMatcher", "OrderValidator"] +} diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/BalanceThresholdFilter.sol b/contracts/extensions/contracts/BalanceThresholdFilter/BalanceThresholdFilter.sol new file mode 100644 index 000000000..16cacd461 --- /dev/null +++ b/contracts/extensions/contracts/BalanceThresholdFilter/BalanceThresholdFilter.sol @@ -0,0 +1,45 @@ +/* + + 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 "@0x/contracts-interfaces/contracts/protocol/Exchange/IExchange.sol"; +import "./interfaces/IThresholdAsset.sol"; +import "./MixinBalanceThresholdFilterCore.sol"; + + +contract BalanceThresholdFilter is + MixinBalanceThresholdFilterCore +{ + + /// @dev Constructs BalanceThresholdFilter. + /// @param exchange Address of 0x exchange. + /// @param thresholdAsset The asset that must be held by makers/takers. + /// @param balanceThreshold The minimum balance of `thresholdAsset` that must be held by makers/takers. + constructor( + address exchange, + address thresholdAsset, + uint256 balanceThreshold + ) + public + { + EXCHANGE = IExchange(exchange); + THRESHOLD_ASSET = IThresholdAsset(thresholdAsset); + BALANCE_THRESHOLD = balanceThreshold; + } +} diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/MixinBalanceThresholdFilterCore.sol b/contracts/extensions/contracts/BalanceThresholdFilter/MixinBalanceThresholdFilterCore.sol new file mode 100644 index 000000000..da050bf72 --- /dev/null +++ b/contracts/extensions/contracts/BalanceThresholdFilter/MixinBalanceThresholdFilterCore.sol @@ -0,0 +1,135 @@ +/* + + 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/LICENSE2.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 "@0x/contracts-libs/contracts/libs/LibExchangeSelectors.sol"; +import "@0x/contracts-libs/contracts/libs/LibOrder.sol"; +import "./mixins/MBalanceThresholdFilterCore.sol"; +import "./MixinExchangeCalldata.sol"; + + +contract MixinBalanceThresholdFilterCore is + MBalanceThresholdFilterCore, + MixinExchangeCalldata, + LibOrder, + LibExchangeSelectors +{ + + /// @dev Executes an Exchange transaction iff the maker and taker meet + /// the hold at least `BALANCE_THRESHOLD` of the asset `THRESHOLD_ASSET` OR + /// the exchange function is a cancellation. + /// Supported Exchange functions: + /// batchFillOrders + /// batchFillOrdersNoThrow + /// batchFillOrKillOrders + /// fillOrder + /// fillOrderNoThrow + /// fillOrKillOrder + /// marketBuyOrders + /// marketBuyOrdersNoThrow + /// marketSellOrders + /// marketSellOrdersNoThrow + /// matchOrders + /// cancelOrder + /// batchCancelOrders + /// cancelOrdersUpTo + /// Trying to call any other exchange function will throw. + /// @param salt Arbitrary number to ensure uniqueness of transaction hash. + /// @param signerAddress Address of transaction signer. + /// @param signedExchangeTransaction AbiV2 encoded calldata. + /// @param signature Proof of signer transaction by signer. + function executeTransaction( + uint256 salt, + address signerAddress, + bytes signedExchangeTransaction, + bytes signature + ) + external + { + // Get accounts whose balances must be validated + address[] memory addressesToValidate = getAddressesToValidate(signerAddress); + + // Validate account balances + uint256 balanceThreshold = BALANCE_THRESHOLD; + IThresholdAsset thresholdAsset = THRESHOLD_ASSET; + for (uint256 i = 0; i < addressesToValidate.length; ++i) { + uint256 addressBalance = thresholdAsset.balanceOf(addressesToValidate[i]); + require( + addressBalance >= balanceThreshold, + "AT_LEAST_ONE_ADDRESS_DOES_NOT_MEET_BALANCE_THRESHOLD" + ); + } + emit ValidatedAddresses(addressesToValidate); + + // All addresses are valid. Execute exchange function. + EXCHANGE.executeTransaction( + salt, + signerAddress, + signedExchangeTransaction, + signature + ); + } + + /// @dev Constructs an array of addresses to be validated. + /// Addresses depend on which Exchange function is to be called + /// (defined by `signedExchangeTransaction` above). + /// @param signerAddress Address of transaction signer. + /// @return addressesToValidate Array of addresses to validate. + function getAddressesToValidate(address signerAddress) + internal pure + returns (address[] memory addressesToValidate) + { + bytes4 exchangeFunctionSelector = bytes4(exchangeCalldataload(0)); + // solhint-disable expression-indent + if ( + exchangeFunctionSelector == BATCH_FILL_ORDERS_SELECTOR || + exchangeFunctionSelector == BATCH_FILL_ORDERS_NO_THROW_SELECTOR || + exchangeFunctionSelector == BATCH_FILL_OR_KILL_ORDERS_SELECTOR || + exchangeFunctionSelector == MARKET_BUY_ORDERS_SELECTOR || + exchangeFunctionSelector == MARKET_BUY_ORDERS_NO_THROW_SELECTOR || + exchangeFunctionSelector == MARKET_SELL_ORDERS_SELECTOR || + exchangeFunctionSelector == MARKET_SELL_ORDERS_NO_THROW_SELECTOR + ) { + addressesToValidate = loadMakerAddressesFromOrderArray(0); + addressesToValidate = addressesToValidate.append(signerAddress); + } else if ( + exchangeFunctionSelector == FILL_ORDER_SELECTOR || + exchangeFunctionSelector == FILL_ORDER_NO_THROW_SELECTOR || + exchangeFunctionSelector == FILL_OR_KILL_ORDER_SELECTOR + ) { + address makerAddress = loadMakerAddressFromOrder(0); + addressesToValidate = addressesToValidate.append(makerAddress); + addressesToValidate = addressesToValidate.append(signerAddress); + } else if (exchangeFunctionSelector == MATCH_ORDERS_SELECTOR) { + address leftMakerAddress = loadMakerAddressFromOrder(0); + addressesToValidate = addressesToValidate.append(leftMakerAddress); + address rightMakerAddress = loadMakerAddressFromOrder(1); + addressesToValidate = addressesToValidate.append(rightMakerAddress); + addressesToValidate = addressesToValidate.append(signerAddress); + } else if ( + exchangeFunctionSelector != CANCEL_ORDER_SELECTOR && + exchangeFunctionSelector != BATCH_CANCEL_ORDERS_SELECTOR && + exchangeFunctionSelector != CANCEL_ORDERS_UP_TO_SELECTOR + ) { + revert("INVALID_OR_BLOCKED_EXCHANGE_SELECTOR"); + } + // solhint-enable expression-indent + return addressesToValidate; + } +} diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/MixinExchangeCalldata.sol b/contracts/extensions/contracts/BalanceThresholdFilter/MixinExchangeCalldata.sol new file mode 100644 index 000000000..a15e95a9d --- /dev/null +++ b/contracts/extensions/contracts/BalanceThresholdFilter/MixinExchangeCalldata.sol @@ -0,0 +1,103 @@ + + /* + + 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/LICENSE2.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 "./mixins/MExchangeCalldata.sol"; +import "@0x/contracts-libs/contracts/libs/LibAddressArray.sol"; + + +contract MixinExchangeCalldata is + MExchangeCalldata +{ + + using LibAddressArray for address[]; + + /// @dev Emulates the `calldataload` opcode on the embedded Exchange calldata, + /// which is accessed through `signedExchangeTransaction`. + /// @param offset Offset into the Exchange calldata. + /// @return value Corresponding 32 byte value stored at `offset`. + function exchangeCalldataload(uint256 offset) + internal pure + returns (bytes32 value) + { + assembly { + // Pointer to exchange transaction + // 0x04 for calldata selector + // 0x40 to access `signedExchangeTransaction`, which is the third parameter + let exchangeTxPtr := calldataload(0x44) + + // Offset into Exchange calldata + // We compute this by adding 0x24 to the `exchangeTxPtr` computed above. + // 0x04 for calldata selector + // 0x20 for length field of `signedExchangeTransaction` + let exchangeCalldataOffset := add(exchangeTxPtr, add(0x24, offset)) + value := calldataload(exchangeCalldataOffset) + } + return value; + } + + /// @dev Convenience function that skips the 4 byte selector when loading + /// from the embedded Exchange calldata. + /// @param offset Offset into the Exchange calldata (minus the 4 byte selector) + /// @return value Corresponding 32 byte value stored at `offset` + 4. + function loadExchangeData(uint256 offset) + internal pure + returns (bytes32 value) + { + value = exchangeCalldataload(offset + 4); + return value; + } + + /// @dev Extracts the maker address from an order stored in the Exchange calldata + /// (which is embedded in `signedExchangeTransaction`). + /// @param orderParamIndex Index of the order in the Exchange function's signature. + /// @return makerAddress The extracted maker address. + function loadMakerAddressFromOrder(uint256 orderParamIndex) + internal pure + returns (address makerAddress) + { + uint256 orderOffsetInBytes = orderParamIndex * 32; + uint256 orderPtr = uint256(loadExchangeData(orderOffsetInBytes)); + makerAddress = address(loadExchangeData(orderPtr)); + return makerAddress; + } + + /// @dev Extracts the maker addresses from an array of orders stored in the Exchange calldata + /// (which is embedded in `signedExchangeTransaction`). + /// @param orderArrayParamIndex Index of the order array in the Exchange function's signature + /// @return makerAddresses The extracted maker addresses. + function loadMakerAddressesFromOrderArray(uint256 orderArrayParamIndex) + internal pure + returns (address[] makerAddresses) + { + uint256 orderArrayOffsetInBytes = orderArrayParamIndex * 32; + uint256 orderArrayPtr = uint256(loadExchangeData(orderArrayOffsetInBytes)); + uint256 orderArrayLength = uint256(loadExchangeData(orderArrayPtr)); + uint256 orderArrayLengthInBytes = orderArrayLength * 32; + uint256 orderArrayElementPtr = orderArrayPtr + 32; + uint256 orderArrayElementEndPtr = orderArrayElementPtr + orderArrayLengthInBytes; + for (uint orderPtrOffset = orderArrayElementPtr; orderPtrOffset < orderArrayElementEndPtr; orderPtrOffset += 32) { + uint256 orderPtr = uint256(loadExchangeData(orderPtrOffset)); + address makerAddress = address(loadExchangeData(orderPtr + orderArrayElementPtr)); + makerAddresses = makerAddresses.append(makerAddress); + } + return makerAddresses; + } +} diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/interfaces/IBalanceThresholdFilterCore.sol b/contracts/extensions/contracts/BalanceThresholdFilter/interfaces/IBalanceThresholdFilterCore.sol new file mode 100644 index 000000000..4a1bf1fb2 --- /dev/null +++ b/contracts/extensions/contracts/BalanceThresholdFilter/interfaces/IBalanceThresholdFilterCore.sol @@ -0,0 +1,55 @@ + +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.4.24; + + +contract IBalanceThresholdFilterCore { + + /// @dev Executes an Exchange transaction iff the maker and taker meet + /// the hold at least `BALANCE_THRESHOLD` of the asset `THRESHOLD_ASSET` OR + /// the exchange function is a cancellation. + /// Supported Exchange functions: + /// - batchFillOrders + /// - batchFillOrdersNoThrow + /// - batchFillOrKillOrders + /// - fillOrder + /// - fillOrderNoThrow + /// - fillOrKillOrder + /// - marketBuyOrders + /// - marketBuyOrdersNoThrow + /// - marketSellOrders + /// - marketSellOrdersNoThrow + /// - matchOrders + /// - cancelOrder + /// - batchCancelOrders + /// - cancelOrdersUpTo + /// Trying to call any other exchange function will throw. + /// @param salt Arbitrary number to ensure uniqueness of transaction hash. + /// @param signerAddress Address of transaction signer. + /// @param signedExchangeTransaction AbiV2 encoded calldata. + /// @param signature Proof of signer transaction by signer. + function executeTransaction( + uint256 salt, + address signerAddress, + bytes signedExchangeTransaction, + bytes signature + ) + external; +} diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/interfaces/IThresholdAsset.sol b/contracts/extensions/contracts/BalanceThresholdFilter/interfaces/IThresholdAsset.sol new file mode 100644 index 000000000..f78f9c2de --- /dev/null +++ b/contracts/extensions/contracts/BalanceThresholdFilter/interfaces/IThresholdAsset.sol @@ -0,0 +1,31 @@ +/* + + 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 IThresholdAsset { + + /// @param _owner The address from which the balance will be retrieved + /// @return Balance of owner + function balanceOf(address _owner) + external + view + returns (uint256); + +} diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/mixins/MBalanceThresholdFilterCore.sol b/contracts/extensions/contracts/BalanceThresholdFilter/mixins/MBalanceThresholdFilterCore.sol new file mode 100644 index 000000000..074686e8d --- /dev/null +++ b/contracts/extensions/contracts/BalanceThresholdFilter/mixins/MBalanceThresholdFilterCore.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 "@0x/contracts-interfaces/contracts/protocol/Exchange/IExchange.sol"; +import "../interfaces/IThresholdAsset.sol"; +import "../interfaces/IBalanceThresholdFilterCore.sol"; + + +contract MBalanceThresholdFilterCore is + IBalanceThresholdFilterCore +{ + + // Points to 0x exchange contract + // solhint-disable var-name-mixedcase + IExchange internal EXCHANGE; + + // The asset that must be held by makers/takers + IThresholdAsset internal THRESHOLD_ASSET; + + // The minimum balance of `THRESHOLD_ASSET` that must be held by makers/takers + uint256 internal BALANCE_THRESHOLD; + // solhint-enable var-name-mixedcase + + // Addresses that hold at least `BALANCE_THRESHOLD` of `THRESHOLD_ASSET` + event ValidatedAddresses ( + address[] addresses + ); + + /// @dev Constructs an array of addresses to be validated. + /// Addresses depend on which Exchange function is to be called + /// (defined by `signedExchangeTransaction` above). + /// @param signerAddress Address of transaction signer. + /// @return addressesToValidate Array of addresses to validate. + function getAddressesToValidate(address signerAddress) + internal pure + returns (address[] memory addressesToValidate); +} diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/mixins/MExchangeCalldata.sol b/contracts/extensions/contracts/BalanceThresholdFilter/mixins/MExchangeCalldata.sol new file mode 100644 index 000000000..40536d820 --- /dev/null +++ b/contracts/extensions/contracts/BalanceThresholdFilter/mixins/MExchangeCalldata.sol @@ -0,0 +1,56 @@ + + /* + + 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/LICENSE2.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 MExchangeCalldata { + + /// @dev Emulates the `calldataload` opcode on the embedded Exchange calldata, + /// which is accessed through `signedExchangeTransaction`. + /// @param offset Offset into the Exchange calldata. + /// @return value Corresponding 32 byte value stored at `offset`. + function exchangeCalldataload(uint256 offset) + internal pure + returns (bytes32 value); + + /// @dev Convenience function that skips the 4 byte selector when loading + /// from the embedded Exchange calldata. + /// @param offset Offset into the Exchange calldata (minus the 4 byte selector) + /// @return value Corresponding 32 byte value stored at `offset` + 4. + function loadExchangeData(uint256 offset) + internal pure + returns (bytes32 value); + + /// @dev Extracts the maker address from an order stored in the Exchange calldata + /// (which is embedded in `signedExchangeTransaction`). + /// @param orderParamIndex Index of the order in the Exchange function's signature. + /// @return makerAddress The extracted maker address. + function loadMakerAddressFromOrder(uint256 orderParamIndex) + internal pure + returns (address makerAddress); + + /// @dev Extracts the maker addresses from an array of orders stored in the Exchange calldata + /// (which is embedded in `signedExchangeTransaction`). + /// @param orderArrayParamIndex Index of the order array in the Exchange function's signature + /// @return makerAddresses The extracted maker addresses. + function loadMakerAddressesFromOrderArray(uint256 orderArrayParamIndex) + internal pure + returns (address[] makerAddresses); +} diff --git a/contracts/extensions/contracts/DutchAuction/DutchAuction.sol b/contracts/extensions/contracts/DutchAuction/DutchAuction.sol new file mode 100644 index 000000000..9c9f3990a --- /dev/null +++ b/contracts/extensions/contracts/DutchAuction/DutchAuction.sol @@ -0,0 +1,205 @@ +/* + + 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 "@0x/contracts-interfaces/contracts/protocol/Exchange/IExchange.sol"; +import "@0x/contracts-libs/contracts/libs/LibOrder.sol"; +import "@0x/contracts-tokens/contracts/tokens/ERC20Token/IERC20Token.sol"; +import "@0x/contracts-utils/contracts/utils/LibBytes/LibBytes.sol"; +import "@0x/contracts-utils/contracts/utils/SafeMath/SafeMath.sol"; + + +contract DutchAuction is + SafeMath +{ + using LibBytes for bytes; + + // solhint-disable var-name-mixedcase + IExchange internal EXCHANGE; + + struct AuctionDetails { + uint256 beginTimeSeconds; // Auction begin unix timestamp: sellOrder.makerAssetData + uint256 endTimeSeconds; // Auction end unix timestamp: sellOrder.expiryTimeSeconds + uint256 beginAmount; // Auction begin amount: sellOrder.makerAssetData + uint256 endAmount; // Auction end amount: sellOrder.takerAssetAmount + uint256 currentAmount; // Calculated amount given block.timestamp + uint256 currentTimeSeconds; // block.timestamp + } + + constructor (address _exchange) + public + { + EXCHANGE = IExchange(_exchange); + } + + /// @dev Matches the buy and sell orders at an amount given the following: the current block time, the auction + /// start time and the auction begin amount. The sell order is a an order at the lowest amount + /// at the end of the auction. Excess from the match is transferred to the seller. + /// Over time the price moves from beginAmount to endAmount given the current block.timestamp. + /// sellOrder.expiryTimeSeconds is the end time of the auction. + /// sellOrder.takerAssetAmount is the end amount of the auction (lowest possible amount). + /// sellOrder.makerAssetData is the ABI encoded Asset Proxy data with the following data appended + /// buyOrder.makerAssetData is the buyers bid on the auction, must meet the amount for the current block timestamp + /// (uint256 beginTimeSeconds, uint256 beginAmount). + /// This function reverts in the following scenarios: + /// * Auction has not started (auctionDetails.currentTimeSeconds < auctionDetails.beginTimeSeconds) + /// * Auction has expired (auctionDetails.endTimeSeconds < auctionDetails.currentTimeSeconds) + /// * Amount is invalid: Buy order amount is too low (buyOrder.makerAssetAmount < auctionDetails.currentAmount) + /// * Amount is invalid: Invalid begin amount (auctionDetails.beginAmount > auctionDetails.endAmount) + /// * Any failure in the 0x Match Orders + /// @param buyOrder The Buyer's order. This order is for the current expected price of the auction. + /// @param sellOrder The Seller's order. This order is for the lowest amount (at the end of the auction). + /// @param buySignature Proof that order was created by the buyer. + /// @param sellSignature Proof that order was created by the seller. + /// @return matchedFillResults amounts filled and fees paid by maker and taker of matched orders. + function matchOrders( + LibOrder.Order memory buyOrder, + LibOrder.Order memory sellOrder, + bytes memory buySignature, + bytes memory sellSignature + ) + public + returns (LibFillResults.MatchedFillResults memory matchedFillResults) + { + AuctionDetails memory auctionDetails = getAuctionDetails(sellOrder); + // Ensure the auction has not yet started + require( + auctionDetails.currentTimeSeconds >= auctionDetails.beginTimeSeconds, + "AUCTION_NOT_STARTED" + ); + // Ensure the auction has not expired. This will fail later in 0x but we can save gas by failing early + require( + sellOrder.expirationTimeSeconds > auctionDetails.currentTimeSeconds, + "AUCTION_EXPIRED" + ); + // Validate the buyer amount is greater than the current auction amount + require( + buyOrder.makerAssetAmount >= auctionDetails.currentAmount, + "INVALID_AMOUNT" + ); + // Match orders, maximally filling `buyOrder` + matchedFillResults = EXCHANGE.matchOrders( + buyOrder, + sellOrder, + buySignature, + sellSignature + ); + // The difference in sellOrder.takerAssetAmount and current amount is given as spread to the matcher + // This may include additional spread from the buyOrder.makerAssetAmount and the currentAmount. + // e.g currentAmount is 30, sellOrder.takerAssetAmount is 10 and buyOrder.makerAssetamount is 40. + // 10 (40-30) is returned to the buyer, 20 (30-10) sent to the seller and 10 has previously + // been transferred to the seller during matchOrders + uint256 leftMakerAssetSpreadAmount = matchedFillResults.leftMakerAssetSpreadAmount; + if (leftMakerAssetSpreadAmount > 0) { + // ERC20 Asset data itself is encoded as follows: + // + // | Area | Offset | Length | Contents | + // |----------|--------|---------|-------------------------------------| + // | Header | 0 | 4 | function selector | + // | Params | | 1 * 32 | function parameters: | + // | | 4 | 12 | 1. token address padding | + // | | 16 | 20 | 2. token address | + bytes memory assetData = sellOrder.takerAssetData; + address token = assetData.readAddress(16); + // Calculate the excess from the buy order. This can occur if the buyer sends in a higher + // amount than the calculated current amount + uint256 buyerExcessAmount = safeSub(buyOrder.makerAssetAmount, auctionDetails.currentAmount); + uint256 sellerExcessAmount = safeSub(leftMakerAssetSpreadAmount, buyerExcessAmount); + // Return the difference between auctionDetails.currentAmount and sellOrder.takerAssetAmount + // to the seller + if (sellerExcessAmount > 0) { + IERC20Token(token).transfer(sellOrder.makerAddress, sellerExcessAmount); + } + // Return the difference between buyOrder.makerAssetAmount and auctionDetails.currentAmount + // to the buyer + if (buyerExcessAmount > 0) { + IERC20Token(token).transfer(buyOrder.makerAddress, buyerExcessAmount); + } + } + return matchedFillResults; + } + + /// @dev Calculates the Auction Details for the given order + /// @param order The sell order + /// @return AuctionDetails + function getAuctionDetails( + LibOrder.Order memory order + ) + public + returns (AuctionDetails memory auctionDetails) + { + uint256 makerAssetDataLength = order.makerAssetData.length; + // It is unknown the encoded data of makerAssetData, we assume the last 64 bytes + // are the Auction Details encoding. + // Auction Details is encoded as follows: + // + // | Area | Offset | Length | Contents | + // |----------|--------|---------|-------------------------------------| + // | Params | | 2 * 32 | parameters: | + // | | -64 | 32 | 1. auction begin unix timestamp | + // | | -32 | 32 | 2. auction begin begin amount | + // ERC20 asset data length is 4+32, 64 for auction details results in min length 100 + require( + makerAssetDataLength >= 100, + "INVALID_ASSET_DATA" + ); + uint256 auctionBeginTimeSeconds = order.makerAssetData.readUint256(makerAssetDataLength - 64); + uint256 auctionBeginAmount = order.makerAssetData.readUint256(makerAssetDataLength - 32); + // Ensure the auction has a valid begin time + require( + order.expirationTimeSeconds > auctionBeginTimeSeconds, + "INVALID_BEGIN_TIME" + ); + uint256 auctionDurationSeconds = order.expirationTimeSeconds-auctionBeginTimeSeconds; + // Ensure the auction goes from high to low + uint256 minAmount = order.takerAssetAmount; + require( + auctionBeginAmount > minAmount, + "INVALID_AMOUNT" + ); + uint256 amountDelta = auctionBeginAmount-minAmount; + // solhint-disable-next-line not-rely-on-time + uint256 timestamp = block.timestamp; + auctionDetails.beginTimeSeconds = auctionBeginTimeSeconds; + auctionDetails.endTimeSeconds = order.expirationTimeSeconds; + auctionDetails.beginAmount = auctionBeginAmount; + auctionDetails.endAmount = minAmount; + auctionDetails.currentTimeSeconds = timestamp; + + uint256 remainingDurationSeconds = order.expirationTimeSeconds-timestamp; + if (timestamp < auctionBeginTimeSeconds) { + // If the auction has not yet begun the current amount is the auctionBeginAmount + auctionDetails.currentAmount = auctionBeginAmount; + } else if (timestamp >= order.expirationTimeSeconds) { + // If the auction has ended the current amount is the minAmount. + // Auction end time is guaranteed by 0x Exchange due to the order expiration + auctionDetails.currentAmount = minAmount; + } else { + auctionDetails.currentAmount = safeAdd( + minAmount, + safeDiv( + safeMul(remainingDurationSeconds, amountDelta), + auctionDurationSeconds + ) + ); + } + return auctionDetails; + } +} diff --git a/contracts/extensions/contracts/Forwarder/Forwarder.sol b/contracts/extensions/contracts/Forwarder/Forwarder.sol new file mode 100644 index 000000000..94dec40ed --- /dev/null +++ b/contracts/extensions/contracts/Forwarder/Forwarder.sol @@ -0,0 +1,50 @@ +/* + + 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/contracts/extensions/contracts/Forwarder/MixinAssets.sol b/contracts/extensions/contracts/Forwarder/MixinAssets.sol new file mode 100644 index 000000000..116cdf267 --- /dev/null +++ b/contracts/extensions/contracts/Forwarder/MixinAssets.sol @@ -0,0 +1,143 @@ +/* + + 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 "@0x/contracts-utils/contracts/utils/LibBytes/LibBytes.sol"; +import "@0x/contracts-utils/contracts/utils/Ownable/Ownable.sol"; +import "@0x/contracts-tokens/contracts/tokens/ERC20Token/IERC20Token.sol"; +import "@0x/contracts-tokens/contracts/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/contracts/extensions/contracts/Forwarder/MixinExchangeWrapper.sol b/contracts/extensions/contracts/Forwarder/MixinExchangeWrapper.sol new file mode 100644 index 000000000..cab26741d --- /dev/null +++ b/contracts/extensions/contracts/Forwarder/MixinExchangeWrapper.sol @@ -0,0 +1,260 @@ +/* + + 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 "@0x/contracts-libs/contracts/libs/LibAbiEncoder.sol"; +import "@0x/contracts-libs/contracts/libs/LibOrder.sol"; +import "@0x/contracts-libs/contracts/libs/LibFillResults.sol"; +import "@0x/contracts-libs/contracts/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 + 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 + ) + if success { + 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))) + } + } + // fillResults values will be 0 by default if call was unsuccessful + 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. + // We round up because the exchange rate computed by fillOrder rounds in favor + // of the Maker. In this case we want to overestimate the amount of takerAsset. + uint256 remainingTakerAssetFillAmount = getPartialAmountCeil( + 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. + // We round up because the exchange rate computed by fillOrder rounds in favor + // of the Maker. In this case we want to overestimate the amount of takerAsset. + uint256 remainingWethSellAmount = getPartialAmountCeil( + 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], + remainingWethSellAmount, + 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/contracts/extensions/contracts/Forwarder/MixinForwarderCore.sol b/contracts/extensions/contracts/Forwarder/MixinForwarderCore.sol new file mode 100644 index 000000000..11c0147a5 --- /dev/null +++ b/contracts/extensions/contracts/Forwarder/MixinForwarderCore.sol @@ -0,0 +1,214 @@ +/* + + 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 "@0x/contracts-utils/contracts/utils/LibBytes/LibBytes.sol"; +import "@0x/contracts-libs/contracts/libs/LibOrder.sol"; +import "@0x/contracts-libs/contracts/libs/LibFillResults.sol"; +import "@0x/contracts-libs/contracts/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); + require( + proxyAddress != address(0), + "UNREGISTERED_ASSET_PROXY" + ); + 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 = getPartialAmountFloor( + 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 = getPartialAmountFloor( + 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/contracts/extensions/contracts/Forwarder/MixinWeth.sol b/contracts/extensions/contracts/Forwarder/MixinWeth.sol new file mode 100644 index 000000000..25a35f47b --- /dev/null +++ b/contracts/extensions/contracts/Forwarder/MixinWeth.sol @@ -0,0 +1,113 @@ +/* + + 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 "@0x/contracts-libs/contracts/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 = getPartialAmountFloor( + 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/contracts/extensions/contracts/Forwarder/interfaces/IAssets.sol b/contracts/extensions/contracts/Forwarder/interfaces/IAssets.sol new file mode 100644 index 000000000..cebfd3706 --- /dev/null +++ b/contracts/extensions/contracts/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/contracts/extensions/contracts/Forwarder/interfaces/IForwarder.sol b/contracts/extensions/contracts/Forwarder/interfaces/IForwarder.sol new file mode 100644 index 000000000..6ce8a1d31 --- /dev/null +++ b/contracts/extensions/contracts/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/contracts/extensions/contracts/Forwarder/interfaces/IForwarderCore.sol b/contracts/extensions/contracts/Forwarder/interfaces/IForwarderCore.sol new file mode 100644 index 000000000..7f62722e7 --- /dev/null +++ b/contracts/extensions/contracts/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 "@0x/contracts-libs/contracts/libs/LibOrder.sol"; +import "@0x/contracts-libs/contracts/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/contracts/extensions/contracts/Forwarder/libs/LibConstants.sol b/contracts/extensions/contracts/Forwarder/libs/LibConstants.sol new file mode 100644 index 000000000..0d2f6e36d --- /dev/null +++ b/contracts/extensions/contracts/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 "@0x/contracts-utils/contracts/utils/LibBytes/LibBytes.sol"; +import "@0x/contracts-interfaces/contracts/protocol/Exchange/IExchange.sol"; +import "@0x/contracts-tokens/contracts/tokens/EtherToken/IEtherToken.sol"; +import "@0x/contracts-tokens/contracts/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/contracts/extensions/contracts/Forwarder/libs/LibForwarderErrors.sol b/contracts/extensions/contracts/Forwarder/libs/LibForwarderErrors.sol new file mode 100644 index 000000000..7a95b78a0 --- /dev/null +++ b/contracts/extensions/contracts/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/contracts/extensions/contracts/Forwarder/mixins/MAssets.sol b/contracts/extensions/contracts/Forwarder/mixins/MAssets.sol new file mode 100644 index 000000000..1757b37fb --- /dev/null +++ b/contracts/extensions/contracts/Forwarder/mixins/MAssets.sol @@ -0,0 +1,53 @@ +/* + + 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/contracts/extensions/contracts/Forwarder/mixins/MExchangeWrapper.sol b/contracts/extensions/contracts/Forwarder/mixins/MExchangeWrapper.sol new file mode 100644 index 000000000..143f888b1 --- /dev/null +++ b/contracts/extensions/contracts/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 "@0x/contracts-libs/contracts/libs/LibOrder.sol"; +import "@0x/contracts-libs/contracts/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/contracts/extensions/contracts/Forwarder/mixins/MWeth.sol b/contracts/extensions/contracts/Forwarder/mixins/MWeth.sol new file mode 100644 index 000000000..15d66942f --- /dev/null +++ b/contracts/extensions/contracts/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/contracts/extensions/contracts/OrderMatcher/MixinAssets.sol b/contracts/extensions/contracts/OrderMatcher/MixinAssets.sol new file mode 100644 index 000000000..f0f91cfc0 --- /dev/null +++ b/contracts/extensions/contracts/OrderMatcher/MixinAssets.sol @@ -0,0 +1,195 @@ +/* + + 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 "@0x/contracts-utils/contracts/utils/LibBytes/LibBytes.sol"; +import "@0x/contracts-utils/contracts/utils/Ownable/Ownable.sol"; +import "@0x/contracts-tokens/contracts/tokens/ERC20Token/IERC20Token.sol"; +import "@0x/contracts-tokens/contracts/tokens/ERC721Token/IERC721Token.sol"; +import "./mixins/MAssets.sol"; +import "./libs/LibConstants.sol"; + + +contract MixinAssets is + MAssets, + Ownable, + LibConstants +{ + using LibBytes for bytes; + + /// @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 asset to withdraw. + function withdrawAsset( + bytes assetData, + uint256 amount + ) + external + onlyOwner + { + transferAssetToSender(assetData, amount); + } + + /// @dev Approves or disapproves an AssetProxy to spend asset. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of asset to approve for respective proxy. + function approveAssetProxy( + bytes assetData, + uint256 amount + ) + external + onlyOwner + { + bytes4 proxyId = assetData.readBytes4(0); + + if (proxyId == ERC20_DATA_ID) { + approveERC20Token(assetData, amount); + } else if (proxyId == ERC721_DATA_ID) { + approveERC721Token(assetData, amount); + } else { + revert("UNSUPPORTED_ASSET_PROXY"); + } + } + + /// @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 + { + // 4 byte id + 12 0 bytes before ABI encoded token address. + 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. + // 4 byte id + 12 0 bytes before ABI encoded token address. + address token = assetData.readAddress(16); + // 4 byte id + 32 byte ABI encoded token address before token id. + uint256 tokenId = assetData.readUint256(36); + + // Perform transfer. + IERC721Token(token).transferFrom( + address(this), + msg.sender, + tokenId + ); + } + + /// @dev Sets approval for ERC20 AssetProxy. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of asset to approve for respective proxy. + function approveERC20Token( + bytes memory assetData, + uint256 amount + ) + internal + { + address token = assetData.readAddress(16); + require( + IERC20Token(token).approve(ERC20_PROXY_ADDRESS, amount), + "APPROVAL_FAILED" + ); + } + + /// @dev Sets approval for ERC721 AssetProxy. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of asset to approve for respective proxy. + function approveERC721Token( + bytes memory assetData, + uint256 amount + ) + internal + { + address token = assetData.readAddress(16); + bool approval = amount >= 1; + IERC721Token(token).setApprovalForAll(ERC721_PROXY_ADDRESS, approval); + } +} diff --git a/contracts/extensions/contracts/OrderMatcher/MixinMatchOrders.sol b/contracts/extensions/contracts/OrderMatcher/MixinMatchOrders.sol new file mode 100644 index 000000000..1787deb59 --- /dev/null +++ b/contracts/extensions/contracts/OrderMatcher/MixinMatchOrders.sol @@ -0,0 +1,86 @@ +/* + + 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 "@0x/contracts-libs/contracts/libs/LibOrder.sol"; +import "@0x/contracts-libs/contracts/libs/LibFillResults.sol"; +import "@0x/contracts-utils/contracts/utils/Ownable/Ownable.sol"; + + +contract MixinMatchOrders is + Ownable, + LibConstants +{ + /// @dev Match two complementary orders that have a profitable spread. + /// Each order is filled at their respective price point. However, the calculations are + /// carried out as though the orders are both being filled at the right order's price point. + /// The profit made by the left order is then used to fill the right order as much as possible. + /// This results in a spread being taken in terms of both assets. The spread is held within this contract. + /// @param leftOrder First order to match. + /// @param rightOrder Second order to match. + /// @param leftSignature Proof that order was created by the left maker. + /// @param rightSignature Proof that order was created by the right maker. + function matchOrders( + LibOrder.Order memory leftOrder, + LibOrder.Order memory rightOrder, + bytes memory leftSignature, + bytes memory rightSignature + ) + public + onlyOwner + { + // Match orders, maximally filling `leftOrder` + LibFillResults.MatchedFillResults memory matchedFillResults = EXCHANGE.matchOrders( + leftOrder, + rightOrder, + leftSignature, + rightSignature + ); + + uint256 leftMakerAssetSpreadAmount = matchedFillResults.leftMakerAssetSpreadAmount; + uint256 rightOrderTakerAssetAmount = rightOrder.takerAssetAmount; + + // Do not attempt to call `fillOrder` if no spread was taken or `rightOrder` has been completely filled + if (leftMakerAssetSpreadAmount == 0 || matchedFillResults.right.takerAssetFilledAmount == rightOrderTakerAssetAmount) { + return; + } + + // The `assetData` fields of the `rightOrder` could have been null for the `matchOrders` call. We reassign them before calling `fillOrder`. + rightOrder.makerAssetData = leftOrder.takerAssetData; + rightOrder.takerAssetData = leftOrder.makerAssetData; + + // Query `rightOrder` info to check if it has been completely filled + // We need to make this check in case the `rightOrder` was partially filled before the `matchOrders` call + LibOrder.OrderInfo memory orderInfo = EXCHANGE.getOrderInfo(rightOrder); + + // Do not attempt to call `fillOrder` if order has been completely filled + if (orderInfo.orderTakerAssetFilledAmount == rightOrderTakerAssetAmount) { + return; + } + + // We do not need to pass in a signature since it was already validated in the `matchOrders` call + EXCHANGE.fillOrder( + rightOrder, + leftMakerAssetSpreadAmount, + "" + ); + } +} diff --git a/contracts/extensions/contracts/OrderMatcher/OrderMatcher.sol b/contracts/extensions/contracts/OrderMatcher/OrderMatcher.sol new file mode 100644 index 000000000..4879b7bca --- /dev/null +++ b/contracts/extensions/contracts/OrderMatcher/OrderMatcher.sol @@ -0,0 +1,38 @@ +/* + + 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 "@0x/contracts-utils/contracts/utils/Ownable/Ownable.sol"; +import "./libs/LibConstants.sol"; +import "./MixinMatchOrders.sol"; +import "./MixinAssets.sol"; + + +// solhint-disable no-empty-blocks +contract OrderMatcher is + MixinMatchOrders, + MixinAssets +{ + constructor (address _exchange) + public + LibConstants(_exchange) + Ownable() + {} +} diff --git a/contracts/extensions/contracts/OrderMatcher/interfaces/IAssets.sol b/contracts/extensions/contracts/OrderMatcher/interfaces/IAssets.sol new file mode 100644 index 000000000..9fcc0023a --- /dev/null +++ b/contracts/extensions/contracts/OrderMatcher/interfaces/IAssets.sol @@ -0,0 +1,43 @@ +/* + + 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 asset to withdraw. + function withdrawAsset( + bytes assetData, + uint256 amount + ) + external; + + /// @dev Approves or disapproves an AssetProxy to spend asset. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of asset to approve for respective proxy. + function approveAssetProxy( + bytes assetData, + uint256 amount + ) + external; +} diff --git a/contracts/extensions/contracts/OrderMatcher/interfaces/IMatchOrders.sol b/contracts/extensions/contracts/OrderMatcher/interfaces/IMatchOrders.sol new file mode 100644 index 000000000..1443c73b9 --- /dev/null +++ b/contracts/extensions/contracts/OrderMatcher/interfaces/IMatchOrders.sol @@ -0,0 +1,43 @@ +/* + + 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 "@0x/contracts-libs/contracts/libs/LibOrder.sol"; + + +contract IMatchOrders { + + /// @dev Match two complementary orders that have a profitable spread. + /// Each order is filled at their respective price point. However, the calculations are + /// carried out as though the orders are both being filled at the right order's price point. + /// The profit made by the left order is then used to fill the right order as much as possible. + /// This results in a spread being taken in terms of both assets. The spread is held within this contract. + /// @param leftOrder First order to match. + /// @param rightOrder Second order to match. + /// @param leftSignature Proof that order was created by the left maker. + /// @param rightSignature Proof that order was created by the right maker. + function matchOrders( + LibOrder.Order memory leftOrder, + LibOrder.Order memory rightOrder, + bytes memory leftSignature, + bytes memory rightSignature + ) + public; +} diff --git a/contracts/extensions/contracts/OrderMatcher/interfaces/IOrderMatcher.sol b/contracts/extensions/contracts/OrderMatcher/interfaces/IOrderMatcher.sol new file mode 100644 index 000000000..75f26dca6 --- /dev/null +++ b/contracts/extensions/contracts/OrderMatcher/interfaces/IOrderMatcher.sol @@ -0,0 +1,31 @@ +/* + + 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 "@0x/contract-utils/contracts/utils/Ownable/IOwnable.sol"; +import "./IMatchOrders.sol"; +import "./IAssets.sol"; + + +// solhint-disable no-empty-blocks +contract IOrderMatcher is + IOwnable, + IMatchOrders, + IAssets +{} diff --git a/contracts/extensions/contracts/OrderMatcher/libs/LibConstants.sol b/contracts/extensions/contracts/OrderMatcher/libs/LibConstants.sol new file mode 100644 index 000000000..c1a86a9c7 --- /dev/null +++ b/contracts/extensions/contracts/OrderMatcher/libs/LibConstants.sol @@ -0,0 +1,56 @@ +/* + + 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 "@0x/contracts-interfaces/contracts/protocol/Exchange/IExchange.sol"; + + +contract LibConstants { + + // bytes4(keccak256("transfer(address,uint256)")) + bytes4 constant internal ERC20_TRANSFER_SELECTOR = 0xa9059cbb; + // bytes4(keccak256("ERC20Token(address)")) + bytes4 constant internal ERC20_DATA_ID = 0xf47261b0; + // bytes4(keccak256("ERC721Token(address,uint256)")) + bytes4 constant internal ERC721_DATA_ID = 0x02571792; + + // solhint-disable var-name-mixedcase + IExchange internal EXCHANGE; + address internal ERC20_PROXY_ADDRESS; + address internal ERC721_PROXY_ADDRESS; + // solhint-enable var-name-mixedcase + + constructor (address _exchange) + public + { + EXCHANGE = IExchange(_exchange); + + ERC20_PROXY_ADDRESS = EXCHANGE.getAssetProxy(ERC20_DATA_ID); + require( + ERC20_PROXY_ADDRESS != address(0), + "UNREGISTERED_ASSET_PROXY" + ); + + ERC721_PROXY_ADDRESS = EXCHANGE.getAssetProxy(ERC721_DATA_ID); + require( + ERC721_PROXY_ADDRESS != address(0), + "UNREGISTERED_ASSET_PROXY" + ); + } +} diff --git a/contracts/extensions/contracts/OrderMatcher/mixins/MAssets.sol b/contracts/extensions/contracts/OrderMatcher/mixins/MAssets.sol new file mode 100644 index 000000000..6ea7a3290 --- /dev/null +++ b/contracts/extensions/contracts/OrderMatcher/mixins/MAssets.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 "../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; + + /// @dev Sets approval for ERC20 AssetProxy. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of asset to approve for respective proxy. + function approveERC20Token( + bytes memory assetData, + uint256 amount + ) + internal; + + /// @dev Sets approval for ERC721 AssetProxy. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of asset to approve for respective proxy. + function approveERC721Token( + bytes memory assetData, + uint256 amount + ) + internal; +} diff --git a/contracts/extensions/contracts/OrderValidator/OrderValidator.sol b/contracts/extensions/contracts/OrderValidator/OrderValidator.sol new file mode 100644 index 000000000..33dd1326c --- /dev/null +++ b/contracts/extensions/contracts/OrderValidator/OrderValidator.sol @@ -0,0 +1,218 @@ +/* + + 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 "@0x/contracts-interfaces/contracts/protocol/Exchange/IExchange.sol"; +import "@0x/contracts-libs/contracts/libs/LibOrder.sol"; +import "@0x/contracts-tokens/contracts/tokens/ERC20Token/IERC20Token.sol"; +import "@0x/contracts-tokens/contracts/tokens/ERC721Token/IERC721Token.sol"; +import "@0x/contracts-utils/contracts/utils/LibBytes/LibBytes.sol"; + + +contract OrderValidator { + + 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)")); + + 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 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 + } + + // solhint-disable var-name-mixedcase + IExchange internal EXCHANGE; + bytes internal ZRX_ASSET_DATA; + // solhint-enable var-name-mixedcase + + constructor (address _exchange, bytes memory _zrxAssetData) + public + { + EXCHANGE = IExchange(_exchange); + ZRX_ASSET_DATA = _zrxAssetData; + } + + /// @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); + bytes memory zrxAssetData = ZRX_ASSET_DATA; + (traderInfo.makerZrxBalance, traderInfo.makerZrxAllowance) = getBalanceAndAllowance(order.makerAddress, zrxAssetData); + (traderInfo.takerZrxBalance, traderInfo.takerZrxAllowance) = getBalanceAndAllowance(takerAddress, zrxAssetData); + 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) { + // 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); + + // 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); + + // Set alowance to 1 if ERC721Proxy is approved to spend tokenId + allowance = isApproved ? 1 : 0; + } else { + revert("UNSUPPORTED_ASSET_PROXY"); + } + 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. + /// @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; + } +} diff --git a/contracts/extensions/package.json b/contracts/extensions/package.json new file mode 100644 index 000000000..aee995645 --- /dev/null +++ b/contracts/extensions/package.json @@ -0,0 +1,95 @@ +{ + "name": "@0x/contracts-extensions", + "version": "1.0.2", + "engines": { + "node": ">=6.12" + }, + "description": "Smart contract extensions of 0x protocol", + "main": "lib/src/index.js", + "directories": { + "test": "test" + }, + "scripts": { + "build": "yarn pre_build && tsc -b", + "build:ci": "yarn build", + "pre_build": "run-s compile generate_contract_wrappers", + "test": "yarn run_mocha", + "rebuild_and_test": "run-s build test", + "test:coverage": "SOLIDITY_COVERAGE=true run-s build run_mocha coverage:report:text coverage:report:lcov", + "test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html", + "test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha", + "run_mocha": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit", + "compile": "sol-compiler", + "watch": "sol-compiler -w", + "clean": "shx rm -rf lib generated-artifacts generated-wrappers", + "generate_contract_wrappers": "abi-gen --abis ${npm_package_config_abis} --template ../../node_modules/@0x/abi-gen-templates/contract.handlebars --partials '../../node_modules/@0x/abi-gen-templates/partials/**/*.handlebars' --output generated-wrappers --backend ethers", + "lint": "tslint --format stylish --project . --exclude ./generated-wrappers/**/* --exclude ./generated-artifacts/**/* --exclude **/lib/**/* && yarn lint-contracts", + "coverage:report:text": "istanbul report text", + "coverage:report:html": "istanbul report html && open coverage/index.html", + "profiler:report:html": "istanbul report html && open coverage/index.html", + "coverage:report:lcov": "istanbul report lcov", + "test:circleci": "yarn test", + "lint-contracts": "solhint -c ../.solhint.json contracts/**/**/**/**/*.sol" + }, + "config": { + "abis": "generated-artifacts/@(BalanceThresholdFilter|DutchAuction|Forwarder|OrderMatcher|OrderValidator).json" + }, + "repository": { + "type": "git", + "url": "https://github.com/0xProject/0x-monorepo.git" + }, + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/0xProject/0x-monorepo/issues" + }, + "homepage": "https://github.com/0xProject/0x-monorepo/contracts/extensions/README.md", + "devDependencies": { + "@0x/abi-gen": "^1.0.19", + "@0x/contract-wrappers": "^4.1.3", + "@0x/contracts-test-utils": "^1.0.2", + "@0x/dev-utils": "^1.0.21", + "@0x/sol-compiler": "^1.1.16", + "@0x/sol-cov": "^2.1.16", + "@0x/subproviders": "^2.1.8", + "@0x/tslint-config": "^2.0.0", + "@types/bn.js": "^4.11.0", + "@types/lodash": "4.14.104", + "@types/node": "*", + "@types/yargs": "^10.0.0", + "chai": "^4.0.1", + "chai-as-promised": "^7.1.0", + "chai-bignumber": "^2.0.1", + "dirty-chai": "^2.0.1", + "ethereumjs-abi": "0.6.5", + "make-promises-safe": "^1.1.0", + "mocha": "^4.1.0", + "npm-run-all": "^4.1.2", + "shx": "^0.2.2", + "solc": "^0.4.24", + "solhint": "^1.4.1", + "tslint": "5.11.0", + "typescript": "3.0.1", + "yargs": "^10.0.3" + }, + "dependencies": { + "@0x/base-contract": "^3.0.10", + "@0x/contracts-interfaces": "^1.0.2", + "@0x/contracts-libs": "^1.0.2", + "@0x/contracts-protocol": "^2.1.59", + "@0x/contracts-tokens": "^1.0.2", + "@0x/contracts-utils": "^1.0.2", + "@0x/order-utils": "^3.0.7", + "@0x/types": "^1.4.1", + "@0x/typescript-typings": "^3.0.6", + "@0x/utils": "^2.0.8", + "@0x/web3-wrapper": "^3.2.1", + "@types/js-combinatorics": "^0.5.29", + "bn.js": "^4.11.8", + "ethereum-types": "^1.1.4", + "ethereumjs-util": "^5.1.1", + "lodash": "^4.17.5" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/contracts/extensions/src/artifacts/index.ts b/contracts/extensions/src/artifacts/index.ts new file mode 100644 index 000000000..329d0f3f3 --- /dev/null +++ b/contracts/extensions/src/artifacts/index.ts @@ -0,0 +1,15 @@ +import { ContractArtifact } from 'ethereum-types'; + +import * as BalanceThresholdFilter from '../../generated-artifacts/BalanceThresholdFilter.json'; +import * as DutchAuction from '../../generated-artifacts/DutchAuction.json'; +import * as Forwarder from '../../generated-artifacts/Forwarder.json'; +import * as OrderMatcher from '../../generated-artifacts/OrderMatcher.json'; +import * as OrderValidator from '../../generated-artifacts/OrderValidator.json'; + +export const artifacts = { + BalanceThresholdFilter: BalanceThresholdFilter as ContractArtifact, + DutchAuction: DutchAuction as ContractArtifact, + Forwarder: Forwarder as ContractArtifact, + OrderMatcher: OrderMatcher as ContractArtifact, + OrderValidator: OrderValidator as ContractArtifact, +}; diff --git a/contracts/extensions/src/index.ts b/contracts/extensions/src/index.ts new file mode 100644 index 000000000..d55f08ea2 --- /dev/null +++ b/contracts/extensions/src/index.ts @@ -0,0 +1,2 @@ +export * from './artifacts'; +export * from './wrappers'; diff --git a/contracts/extensions/src/wrappers/index.ts b/contracts/extensions/src/wrappers/index.ts new file mode 100644 index 000000000..65aec3ccd --- /dev/null +++ b/contracts/extensions/src/wrappers/index.ts @@ -0,0 +1,5 @@ +export * from '../../generated-wrappers/balance_threshold_filter'; +export * from '../../generated-wrappers/dutch_auction'; +export * from '../../generated-wrappers/forwarder'; +export * from '../../generated-wrappers/order_matcher'; +export * from '../../generated-wrappers/order_validator'; diff --git a/contracts/extensions/test/extensions/balance_threshold_filter.ts b/contracts/extensions/test/extensions/balance_threshold_filter.ts new file mode 100644 index 000000000..07199d60b --- /dev/null +++ b/contracts/extensions/test/extensions/balance_threshold_filter.ts @@ -0,0 +1,1644 @@ +import { BlockchainLifecycle } from '@0x/dev-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { Order, RevertReason, SignedOrder } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import * as chai from 'chai'; +import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { + artifacts as protocolArtifacts, + ERC20Wrapper, + ERC721Wrapper, + ExchangeContract, + ExchangeWrapper, +} from '@0x/contracts-protocol'; +import { + chaiSetup, + constants, + ContractName, + ERC20BalancesByOwner, + expectTransactionFailedAsync, + OrderFactory, + OrderStatus, + provider, + TransactionFactory, + txDefaults, + web3Wrapper, +} from '@0x/contracts-test-utils'; +import { DummyERC20TokenContract } from '@0x/contracts-tokens'; + +import { BalanceThresholdFilterContract } from '../../generated-wrappers/balance_threshold_filter'; +import { artifacts } from '../../src/artifacts'; +import { BalanceThresholdWrapper } from '../utils/balance_threshold_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +const DECIMALS_DEFAULT = 18; + +interface ValidatedAddressesLog { + args: { addresses: string[] }; +} + +describe(ContractName.BalanceThresholdFilter, () => { + const takerAssetAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(500), DECIMALS_DEFAULT); + const makerAssetAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(1000), DECIMALS_DEFAULT); + const takerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(250), DECIMALS_DEFAULT); + + let validMakerAddress: string; + let validMakerAddress2: string; + let owner: string; + let validTakerAddress: string; + let feeRecipientAddress: string; + let invalidAddress: string; + let defaultMakerAssetAddress: string; + let defaultTakerAssetAddress: string; + let zrxAssetData: string; + let zrxToken: DummyERC20TokenContract; + let exchangeInstance: ExchangeContract; + let exchangeWrapper: ExchangeWrapper; + + let orderFactory: OrderFactory; + let orderFactory2: OrderFactory; + let invalidOrderFactory: OrderFactory; + let erc20Wrapper: ERC20Wrapper; + let erc20Balances: ERC20BalancesByOwner; + let erc20TakerBalanceThresholdWrapper: BalanceThresholdWrapper; + let erc721TakerBalanceThresholdWrapper: BalanceThresholdWrapper; + let erc721MakerBalanceThresholdWrapper: BalanceThresholdWrapper; + let erc721NonValidBalanceThresholdWrapper: BalanceThresholdWrapper; + + let defaultOrderParams: Partial<Order>; + let validSignedOrder: SignedOrder; + let validSignedOrder2: SignedOrder; + + let erc721BalanceThresholdFilterInstance: BalanceThresholdFilterContract; + let erc20BalanceThresholdFilterInstance: BalanceThresholdFilterContract; + + const assertValidatedAddressesLog = async ( + txReceipt: TransactionReceiptWithDecodedLogs, + expectedValidatedAddresses: string[], + ) => { + expect(txReceipt.logs.length).to.be.gte(1); + const validatedAddressesLog = (txReceipt.logs[0] as any) as ValidatedAddressesLog; + const validatedAddresses = validatedAddressesLog.args.addresses; + // @HACK-hysz: Nested addresses are not translated to lower-case but this will change once + // the new ABI Encoder/Decoder is used by the contract templates. + const validatedAddressesNormalized: string[] = []; + _.each(validatedAddresses, address => { + const normalizedAddress = _.toLower(address); + validatedAddressesNormalized.push(normalizedAddress); + }); + expect(validatedAddressesNormalized).to.be.deep.equal(expectedValidatedAddresses); + }; + + before(async () => { + // Create accounts + await blockchainLifecycle.startAsync(); + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const usedAddresses = ([ + owner, + validMakerAddress, + validMakerAddress2, + validTakerAddress, + feeRecipientAddress, + invalidAddress, + ] = accounts); + const takerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(validTakerAddress)]; + const makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(validMakerAddress)]; + const secondMakerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(validMakerAddress2)]; + const invalidAddressPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(invalidAddress)]; + // Create wrappers + erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); + const validAddresses = _.cloneDeepWith(usedAddresses); + _.remove(validAddresses, (address: string) => { + return address === invalidAddress; + }); + const erc721Wrapper = new ERC721Wrapper(provider, validAddresses, owner); + // Deploy ERC20 tokens + const numDummyErc20ToDeploy = 4; + let erc20TokenA: DummyERC20TokenContract; + let erc20TokenB: DummyERC20TokenContract; + let erc20BalanceThresholdAsset: DummyERC20TokenContract; + [erc20TokenA, erc20TokenB, zrxToken, erc20BalanceThresholdAsset] = await erc20Wrapper.deployDummyTokensAsync( + numDummyErc20ToDeploy, + constants.DUMMY_TOKEN_DECIMALS, + ); + defaultMakerAssetAddress = erc20TokenA.address; + defaultTakerAssetAddress = erc20TokenB.address; + zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + // Create proxies + const erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + // Deploy Exchange contract + exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync( + protocolArtifacts.Exchange, + provider, + txDefaults, + zrxAssetData, + ); + exchangeWrapper = new ExchangeWrapper(exchangeInstance, provider); + // Register proxies + await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, { + from: owner, + }); + // Deploy Balance Threshold Filters + // One uses an ERC721 token as its balance threshold asset; the other uses an ERC20 + const erc721alanceThreshold = new BigNumber(1); + await erc721Wrapper.deployProxyAsync(); + const [erc721BalanceThresholdAsset] = await erc721Wrapper.deployDummyTokensAsync(); + await erc721Wrapper.setBalancesAndAllowancesAsync(); + erc721BalanceThresholdFilterInstance = await BalanceThresholdFilterContract.deployFrom0xArtifactAsync( + artifacts.BalanceThresholdFilter, + provider, + txDefaults, + exchangeInstance.address, + erc721BalanceThresholdAsset.address, + erc721alanceThreshold, + ); + const erc20BalanceThreshold = Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 10); + erc20BalanceThresholdFilterInstance = await BalanceThresholdFilterContract.deployFrom0xArtifactAsync( + artifacts.BalanceThresholdFilter, + provider, + txDefaults, + exchangeInstance.address, + erc20BalanceThresholdAsset.address, + erc20BalanceThreshold, + ); + // Default order parameters + defaultOrderParams = { + exchangeAddress: exchangeInstance.address, + feeRecipientAddress, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress), + makerAssetAmount, + takerAssetAmount, + makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), DECIMALS_DEFAULT), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(150), DECIMALS_DEFAULT), + senderAddress: erc721BalanceThresholdFilterInstance.address, + }; + // Create two order factories with valid makers (who meet the threshold balance), and + // one factory for an invalid address (that does not meet the threshold balance) + // Valid order factory #1 + const defaultOrderParams1 = { + makerAddress: validMakerAddress, + ...defaultOrderParams, + }; + orderFactory = new OrderFactory(makerPrivateKey, defaultOrderParams1); + // Valid order factory #2 + const defaultOrderParams2 = { + makerAddress: validMakerAddress2, + ...defaultOrderParams, + }; + orderFactory2 = new OrderFactory(secondMakerPrivateKey, defaultOrderParams2); + // Invalid order factory + const defaultNonValidOrderParams = { + makerAddress: invalidAddress, + ...defaultOrderParams, + }; + invalidOrderFactory = new OrderFactory(invalidAddressPrivateKey, defaultNonValidOrderParams); + // Create Balance Thresold Wrappers + erc20TakerBalanceThresholdWrapper = new BalanceThresholdWrapper( + erc20BalanceThresholdFilterInstance, + exchangeInstance, + new TransactionFactory(takerPrivateKey, exchangeInstance.address), + provider, + ); + erc721TakerBalanceThresholdWrapper = new BalanceThresholdWrapper( + erc721BalanceThresholdFilterInstance, + exchangeInstance, + new TransactionFactory(takerPrivateKey, exchangeInstance.address), + provider, + ); + erc721MakerBalanceThresholdWrapper = new BalanceThresholdWrapper( + erc721BalanceThresholdFilterInstance, + exchangeInstance, + new TransactionFactory(makerPrivateKey, exchangeInstance.address), + provider, + ); + erc721NonValidBalanceThresholdWrapper = new BalanceThresholdWrapper( + erc721BalanceThresholdFilterInstance, + exchangeInstance, + new TransactionFactory(invalidAddressPrivateKey, exchangeInstance.address), + provider, + ); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('General Sanity Checks', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + validSignedOrder2 = await orderFactory2.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both maker/taker when both maker and taker exceed the balance threshold of an ERC20 token', async () => { + const validSignedOrderERC20Sender = await orderFactory.newSignedOrderAsync({ + ...defaultOrderParams, + makerAddress: validMakerAddress, + senderAddress: erc20TakerBalanceThresholdWrapper.getBalanceThresholdAddress(), + }); + // Execute a valid fill + const txReceipt = await erc20TakerBalanceThresholdWrapper.fillOrderAsync( + validSignedOrderERC20Sender, + validTakerAddress, + { takerAssetFillAmount }, + ); + // Assert validated addresses + const expectedValidatedAddresseses = [validSignedOrder.makerAddress, validTakerAddress]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerAssetFillAmount = takerAssetFillAmount + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const makerFeePaid = validSignedOrder.makerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid), + ); + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)), + ); + }); + it('should revert if the Exchange transaction function is not supported', async () => { + // Create signed order without the fillOrder function selector + const salt = new BigNumber(0); + const badSelectorHex = '0x00000000'; + const signatureHex = '0x'; + // Call valid forwarder + return expectTransactionFailedAsync( + erc721BalanceThresholdFilterInstance.executeTransaction.sendTransactionAsync( + salt, + validTakerAddress, + badSelectorHex, + signatureHex, + ), + RevertReason.InvalidOrBlockedExchangeSelector, + ); + }); + it('should revert if senderAddress is not set to the valid forwarding contract', async () => { + // Create signed order with incorrect senderAddress + const notBalanceThresholdFilterAddress = zrxToken.address; + const signedOrderWithBadSenderAddress = await orderFactory.newSignedOrderAsync({ + senderAddress: notBalanceThresholdFilterAddress, + }); + // Call valid forwarder + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.fillOrderAsync(signedOrderWithBadSenderAddress, validTakerAddress, { + takerAssetFillAmount, + }), + RevertReason.FailedExecution, + ); + }); + }); + + describe('batchFillOrders', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + validSignedOrder2 = await orderFactory2.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both makers/taker when both maker and taker meet the balance threshold', async () => { + // Execute a valid fill + const orders = [validSignedOrder, validSignedOrder2]; + const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount]; + const txReceipt = await erc721TakerBalanceThresholdWrapper.batchFillOrdersAsync(orders, validTakerAddress, { + takerAssetFillAmounts, + }); + // Assert validated addresses + const expectedValidatedAddresseses = [ + validSignedOrder.makerAddress, + validSignedOrder2.makerAddress, + validTakerAddress, + ]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const cumulativeTakerAssetFillAmount = takerAssetFillAmount.times(2); + const makerAssetFillAmount = takerAssetFillAmount + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const makerFeePaid = validSignedOrder.makerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount) + .times(2); + // Maker #1 + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid), + ); + // Maker #2 + expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid), + ); + // Taker + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount), + ); + + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount.times(2)), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + // Fee recipient + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.times(2).add(takerFeePaid)), + ); + }); + it('should revert if one maker does not meet the balance threshold', async () => { + // Create order set with one non-valid maker address + const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount]; + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + makerAddress: invalidAddress, + }); + const orders = [validSignedOrder, signedOrderWithBadMakerAddress]; + // Execute transaction + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.batchFillOrdersAsync(orders, validTakerAddress, { + takerAssetFillAmounts, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + const orders = [validSignedOrder, validSignedOrder2]; + const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount]; + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.batchFillOrdersAsync(orders, invalidAddress, { + takerAssetFillAmounts, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + }); + + describe('batchFillOrdersNoThrow', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + validSignedOrder2 = await orderFactory2.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both makers/taker when both maker and taker meet the balance threshold', async () => { + // Execute a valid fill + const orders = [validSignedOrder, validSignedOrder2]; + const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount]; + const txReceipt = await erc721TakerBalanceThresholdWrapper.batchFillOrdersNoThrowAsync( + orders, + validTakerAddress, + { + takerAssetFillAmounts, + // HACK(albrow): We need to hardcode the gas estimate here because + // the Geth gas estimator doesn't work with the way we use + // delegatecall and swallow errors. + gas: 600000, + }, + ); + // Assert validated addresses + const expectedValidatedAddresseses = [ + validSignedOrder.makerAddress, + validSignedOrder2.makerAddress, + validTakerAddress, + ]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const cumulativeTakerAssetFillAmount = takerAssetFillAmount.times(2); + const makerAssetFillAmount = takerAssetFillAmount + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const makerFeePaid = validSignedOrder.makerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount) + .times(2); + // Maker #1 + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid), + ); + // Maker #2 + expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid), + ); + // Taker + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount), + ); + + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount.times(2)), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + // Fee recipient + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.times(2).add(takerFeePaid)), + ); + }); + it('should revert if one maker does not meet the balance threshold', async () => { + // Create order set with one non-valid maker address + const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount]; + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + makerAddress: invalidAddress, + }); + const orders = [validSignedOrder, signedOrderWithBadMakerAddress]; + // Execute transaction + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.batchFillOrdersNoThrowAsync(orders, validTakerAddress, { + takerAssetFillAmounts, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + const orders = [validSignedOrder, validSignedOrder2]; + const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount]; + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.batchFillOrdersNoThrowAsync(orders, invalidAddress, { + takerAssetFillAmounts, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + }); + + describe('batchFillOrKillOrders', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + validSignedOrder2 = await orderFactory2.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both makers/taker when both makers and taker meet the balance threshold', async () => { + // Execute a valid fill + const orders = [validSignedOrder, validSignedOrder2]; + const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount]; + const txReceipt = await erc721TakerBalanceThresholdWrapper.batchFillOrKillOrdersAsync( + orders, + validTakerAddress, + { takerAssetFillAmounts }, + ); + // Assert validated addresses + const expectedValidatedAddresseses = [ + validSignedOrder.makerAddress, + validSignedOrder2.makerAddress, + validTakerAddress, + ]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const cumulativeTakerAssetFillAmount = takerAssetFillAmount.times(2); + const makerAssetFillAmount = takerAssetFillAmount + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const makerFeePaid = validSignedOrder.makerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount) + .times(2); + // Maker #1 + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid), + ); + // Maker #2 + expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid), + ); + // Taker + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount), + ); + + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount.times(2)), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + // Fee recipient + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.times(2).add(takerFeePaid)), + ); + }); + it('should revert if one maker does not meet the balance threshold', async () => { + // Create order set with one non-valid maker address + const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount]; + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + makerAddress: invalidAddress, + }); + const orders = [validSignedOrder, signedOrderWithBadMakerAddress]; + // Execute transaction + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.batchFillOrKillOrdersAsync(orders, validTakerAddress, { + takerAssetFillAmounts, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + const orders = [validSignedOrder, validSignedOrder2]; + const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount]; + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.batchFillOrKillOrdersAsync(orders, invalidAddress, { + takerAssetFillAmounts, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if one takerAssetFillAmount is not fully filled', async () => { + const tooBigTakerAssetFillAmount = validSignedOrder.takerAssetAmount.times(2); + const orders = [validSignedOrder, validSignedOrder2]; + const takerAssetFillAmounts = [takerAssetFillAmount, tooBigTakerAssetFillAmount]; + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.batchFillOrKillOrdersAsync(orders, validTakerAddress, { + takerAssetFillAmounts, + }), + RevertReason.FailedExecution, + ); + }); + }); + + describe('fillOrder', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both maker/taker when both maker and taker meet the balance threshold', async () => { + // Execute a valid fill + const txReceipt = await erc721TakerBalanceThresholdWrapper.fillOrderAsync( + validSignedOrder, + validTakerAddress, + { takerAssetFillAmount }, + ); + // Assert validated addresses + const expectedValidatedAddresseses = [validSignedOrder.makerAddress, validTakerAddress]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerAssetFillAmount = takerAssetFillAmount + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const makerFeePaid = validSignedOrder.makerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid), + ); + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)), + ); + }); + it('should revert if maker does not meet the balance threshold', async () => { + // Create signed order with non-valid maker address + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + senderAddress: erc721BalanceThresholdFilterInstance.address, + makerAddress: invalidAddress, + }); + // Execute transaction + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.fillOrderAsync(signedOrderWithBadMakerAddress, validTakerAddress, { + takerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.fillOrderAsync(validSignedOrder, invalidAddress, { + takerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + }); + + describe('fillOrderNoThrow', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both maker/taker when both maker and taker meet the balance threshold', async () => { + // Execute a valid fill + const txReceipt = await erc721TakerBalanceThresholdWrapper.fillOrderNoThrowAsync( + validSignedOrder, + validTakerAddress, + { + takerAssetFillAmount, + // HACK(albrow): We need to hardcode the gas estimate here because + // the Geth gas estimator doesn't work with the way we use + // delegatecall and swallow errors. + gas: 600000, + }, + ); + // Assert validated addresses + const expectedValidatedAddresseses = [validSignedOrder.makerAddress, validTakerAddress]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerAssetFillAmount = takerAssetFillAmount + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const makerFeePaid = validSignedOrder.makerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid), + ); + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)), + ); + }); + it('should revert if maker does not meet the balance threshold', async () => { + // Create signed order with non-valid maker address + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + senderAddress: erc721BalanceThresholdFilterInstance.address, + makerAddress: invalidAddress, + }); + // Execute transaction + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.fillOrderNoThrowAsync( + signedOrderWithBadMakerAddress, + validTakerAddress, + { takerAssetFillAmount }, + ), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.fillOrderNoThrowAsync(validSignedOrder, invalidAddress, { + takerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + }); + + describe('fillOrKillOrder', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both maker/taker when both maker and taker meet the balance threshold', async () => { + // Execute a valid fill + const takerAssetFillAmount_ = validSignedOrder.takerAssetAmount; + const txReceipt = await erc721TakerBalanceThresholdWrapper.fillOrKillOrderAsync( + validSignedOrder, + validTakerAddress, + { takerAssetFillAmount: takerAssetFillAmount_ }, + ); + // Assert validated addresses + const expectedValidatedAddresseses = [validSignedOrder.makerAddress, validTakerAddress]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerAssetFillAmount = takerAssetFillAmount_ + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const makerFeePaid = validSignedOrder.makerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount_), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid), + ); + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount_), + ); + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)), + ); + }); + it('should revert if maker does not meet the balance threshold', async () => { + // Create signed order with non-valid maker address + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + senderAddress: erc721BalanceThresholdFilterInstance.address, + makerAddress: invalidAddress, + }); + // Execute transaction + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.fillOrKillOrderAsync( + signedOrderWithBadMakerAddress, + validTakerAddress, + { takerAssetFillAmount }, + ), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.fillOrKillOrderAsync(validSignedOrder, invalidAddress, { + takerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if takerAssetFillAmount is not fully filled', async () => { + const tooBigTakerAssetFillAmount = validSignedOrder.takerAssetAmount.times(2); + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.fillOrKillOrderAsync(validSignedOrder, validTakerAddress, { + takerAssetFillAmount: tooBigTakerAssetFillAmount, + }), + RevertReason.FailedExecution, + ); + }); + }); + + describe('marketSellOrders', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + validSignedOrder2 = await orderFactory2.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both makers/taker when both makers and taker meet the balance threshold', async () => { + // Execute a valid fill + const orders = [validSignedOrder, validSignedOrder2]; + const cumulativeTakerAssetFillAmount = validSignedOrder.takerAssetAmount.plus(takerAssetFillAmount); + const txReceipt = await erc721TakerBalanceThresholdWrapper.marketSellOrdersAsync( + orders, + validTakerAddress, + { takerAssetFillAmount: cumulativeTakerAssetFillAmount }, + ); + // Assert validated addresses + const expectedValidatedAddresseses = [ + validSignedOrder.makerAddress, + validSignedOrder2.makerAddress, + validTakerAddress, + ]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerAssetFillAmount2 = takerAssetFillAmount + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const makerFeePaid2 = validSignedOrder2.makerFee + .times(makerAssetFillAmount2) + .dividedToIntegerBy(validSignedOrder2.makerAssetAmount); + const takerFeePaid2 = validSignedOrder2.takerFee + .times(makerAssetFillAmount2) + .dividedToIntegerBy(validSignedOrder2.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee.plus(takerFeePaid2); + const cumulativeMakerAssetFillAmount = validSignedOrder.makerAssetAmount.plus(makerAssetFillAmount2); + // Maker #1 + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(validSignedOrder.makerAssetAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(validSignedOrder.takerAssetAmount), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(validSignedOrder.makerFee), + ); + // Maker #2 + expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount2), + ); + expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid2), + ); + // Taker + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(cumulativeMakerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + // Fee recipient + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address] + .add(validSignedOrder.makerFee) + .add(makerFeePaid2) + .add(takerFeePaid), + ); + }); + it('should revert if one maker does not meet the balance threshold', async () => { + // Create order set with one non-valid maker address + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + makerAddress: invalidAddress, + }); + const orders = [validSignedOrder, signedOrderWithBadMakerAddress]; + // Execute transaction + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.marketSellOrdersAsync(orders, validTakerAddress, { + takerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + const orders = [validSignedOrder, validSignedOrder2]; + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.marketSellOrdersAsync(orders, invalidAddress, { + takerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + }); + + describe('marketSellOrdersNoThrow', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + validSignedOrder2 = await orderFactory2.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both makers/taker when both makers and taker meet the balance threshold', async () => { + // Execute a valid fill + const orders = [validSignedOrder, validSignedOrder2]; + const cumulativeTakerAssetFillAmount = validSignedOrder.takerAssetAmount.plus(takerAssetFillAmount); + const txReceipt = await erc721TakerBalanceThresholdWrapper.marketSellOrdersNoThrowAsync( + orders, + validTakerAddress, + { + takerAssetFillAmount: cumulativeTakerAssetFillAmount, + // HACK(albrow): We need to hardcode the gas estimate here because + // the Geth gas estimator doesn't work with the way we use + // delegatecall and swallow errors. + gas: 600000, + }, + ); + // Assert validated addresses + const expectedValidatedAddresseses = [ + validSignedOrder.makerAddress, + validSignedOrder2.makerAddress, + validTakerAddress, + ]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerAssetFillAmount2 = takerAssetFillAmount + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const makerFeePaid2 = validSignedOrder2.makerFee + .times(makerAssetFillAmount2) + .dividedToIntegerBy(validSignedOrder2.makerAssetAmount); + const takerFeePaid2 = validSignedOrder2.takerFee + .times(makerAssetFillAmount2) + .dividedToIntegerBy(validSignedOrder2.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee.plus(takerFeePaid2); + const cumulativeMakerAssetFillAmount = validSignedOrder.makerAssetAmount.plus(makerAssetFillAmount2); + // Maker #1 + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(validSignedOrder.makerAssetAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(validSignedOrder.takerAssetAmount), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(validSignedOrder.makerFee), + ); + // Maker #2 + expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount2), + ); + expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid2), + ); + // Taker + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(cumulativeMakerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + // Fee recipient + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address] + .add(validSignedOrder.makerFee) + .add(makerFeePaid2) + .add(takerFeePaid), + ); + }); + it('should revert if one maker does not meet the balance threshold', async () => { + // Create order set with one non-valid maker address + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + makerAddress: invalidAddress, + }); + const orders = [validSignedOrder, signedOrderWithBadMakerAddress]; + // Execute transaction + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.marketSellOrdersNoThrowAsync(orders, validTakerAddress, { + takerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + const orders = [validSignedOrder, validSignedOrder2]; + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.marketSellOrdersNoThrowAsync(orders, invalidAddress, { + takerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + }); + + describe('marketBuyOrders', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + validSignedOrder2 = await orderFactory2.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both makers/taker when both makers and taker meet the balance threshold', async () => { + // Execute a valid fill + const orders = [validSignedOrder, validSignedOrder2]; + const cumulativeTakerAssetFillAmount = validSignedOrder.takerAssetAmount.plus(takerAssetFillAmount); + const makerAssetFillAmount2 = takerAssetFillAmount + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const cumulativeMakerAssetFillAmount = validSignedOrder.makerAssetAmount.plus(makerAssetFillAmount2); + const txReceipt = await erc721TakerBalanceThresholdWrapper.marketBuyOrdersAsync(orders, validTakerAddress, { + makerAssetFillAmount: cumulativeMakerAssetFillAmount, + }); + // Assert validated addresses + const expectedValidatedAddresseses = [ + validSignedOrder.makerAddress, + validSignedOrder2.makerAddress, + validTakerAddress, + ]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerFeePaid2 = validSignedOrder2.makerFee + .times(makerAssetFillAmount2) + .dividedToIntegerBy(validSignedOrder2.makerAssetAmount); + const takerFeePaid2 = validSignedOrder2.takerFee + .times(makerAssetFillAmount2) + .dividedToIntegerBy(validSignedOrder2.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee.plus(takerFeePaid2); + // Maker #1 + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(validSignedOrder.makerAssetAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(validSignedOrder.takerAssetAmount), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(validSignedOrder.makerFee), + ); + // Maker #2 + expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount2), + ); + expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid2), + ); + // Taker + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(cumulativeMakerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + // Fee recipient + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address] + .add(validSignedOrder.makerFee) + .add(makerFeePaid2) + .add(takerFeePaid), + ); + }); + it('should revert if one maker does not meet the balance threshold', async () => { + // Create order set with one non-valid maker address + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + makerAddress: invalidAddress, + }); + const orders = [validSignedOrder, signedOrderWithBadMakerAddress]; + // Execute transaction + const dummyMakerAssetFillAmount = new BigNumber(0); + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.marketBuyOrdersAsync(orders, validTakerAddress, { + makerAssetFillAmount: dummyMakerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + const orders = [validSignedOrder, validSignedOrder2]; + const dummyMakerAssetFillAmount = new BigNumber(0); + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.marketBuyOrdersAsync(orders, invalidAddress, { + makerAssetFillAmount: dummyMakerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + }); + + describe('marketBuyOrdersNoThrowAsync', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + validSignedOrder2 = await orderFactory2.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both makers/taker when both makers and taker meet the balance threshold', async () => { + // Execute a valid fill + const orders = [validSignedOrder, validSignedOrder2]; + const cumulativeTakerAssetFillAmount = validSignedOrder.takerAssetAmount.plus(takerAssetFillAmount); + const makerAssetFillAmount2 = takerAssetFillAmount + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const cumulativeMakerAssetFillAmount = validSignedOrder.makerAssetAmount.plus(makerAssetFillAmount2); + const txReceipt = await erc721TakerBalanceThresholdWrapper.marketBuyOrdersNoThrowAsync( + orders, + validTakerAddress, + { + makerAssetFillAmount: cumulativeMakerAssetFillAmount, + // HACK(albrow): We need to hardcode the gas estimate here because + // the Geth gas estimator doesn't work with the way we use + // delegatecall and swallow errors. + gas: 600000, + }, + ); + // Assert validated addresses + const expectedValidatedAddresseses = [ + validSignedOrder.makerAddress, + validSignedOrder2.makerAddress, + validTakerAddress, + ]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerFeePaid2 = validSignedOrder2.makerFee + .times(makerAssetFillAmount2) + .dividedToIntegerBy(validSignedOrder2.makerAssetAmount); + const takerFeePaid2 = validSignedOrder2.takerFee + .times(makerAssetFillAmount2) + .dividedToIntegerBy(validSignedOrder2.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee.plus(takerFeePaid2); + // Maker #1 + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(validSignedOrder.makerAssetAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(validSignedOrder.takerAssetAmount), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(validSignedOrder.makerFee), + ); + // Maker #2 + expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount2), + ); + expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid2), + ); + // Taker + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(cumulativeMakerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + // Fee recipient + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address] + .add(validSignedOrder.makerFee) + .add(makerFeePaid2) + .add(takerFeePaid), + ); + }); + it('should revert if one maker does not meet the balance threshold', async () => { + // Create order set with one non-valid maker address + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + makerAddress: invalidAddress, + }); + const orders = [validSignedOrder, signedOrderWithBadMakerAddress]; + // Execute transaction + const dummyMakerAssetFillAmount = new BigNumber(0); + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.marketBuyOrdersNoThrowAsync(orders, validTakerAddress, { + makerAssetFillAmount: dummyMakerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + const orders = [validSignedOrder, validSignedOrder2]; + const dummyMakerAssetFillAmount = new BigNumber(0); + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.marketBuyOrdersNoThrowAsync(orders, invalidAddress, { + makerAssetFillAmount: dummyMakerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + }); + + describe('matchOrders', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + validSignedOrder2 = await orderFactory2.newSignedOrderAsync(); + }); + it('Should transfer correct amounts when both makers and taker meet the balance threshold', async () => { + // Test values/results taken from Match Orders test: + // 'Should transfer correct amounts when right order is fully filled and values pass isRoundingErrorFloor but fail isRoundingErrorCeil' + // Create orders to match + const signedOrderLeft = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(17), 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(98), 0), + makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18), + feeRecipientAddress, + }); + const signedOrderRight = await orderFactory2.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(75), 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0), + makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18), + feeRecipientAddress, + }); + // Compute expected transfer amounts + const expectedTransferAmounts = { + // Left Maker + amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0), + amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(75), 0), + feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber('76.4705882352941176'), 16), // 76.47% + // Right Maker + amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(75), 0), + amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0), + feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100% + // Taker + amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), 0), + feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber('76.5306122448979591'), 16), // 76.53% + feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100% + }; + const txReceipt = await erc721TakerBalanceThresholdWrapper.matchOrdersAsync( + signedOrderLeft, + signedOrderRight, + validTakerAddress, + ); + // Assert validated addresses + const expectedValidatedAddresseses = [ + signedOrderLeft.makerAddress, + signedOrderRight.makerAddress, + validTakerAddress, + ]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect( + newBalances[signedOrderLeft.makerAddress][defaultMakerAssetAddress], + 'Checking left maker egress ERC20 account balance', + ).to.be.bignumber.equal( + erc20Balances[signedOrderLeft.makerAddress][defaultMakerAssetAddress].sub( + expectedTransferAmounts.amountSoldByLeftMaker, + ), + ); + expect( + newBalances[signedOrderRight.makerAddress][defaultTakerAssetAddress], + 'Checking right maker ingress ERC20 account balance', + ).to.be.bignumber.equal( + erc20Balances[signedOrderRight.makerAddress][defaultTakerAssetAddress].sub( + expectedTransferAmounts.amountSoldByRightMaker, + ), + ); + expect( + newBalances[validTakerAddress][defaultMakerAssetAddress], + 'Checking taker ingress ERC20 account balance', + ).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add( + expectedTransferAmounts.amountReceivedByTaker, + ), + ); + expect( + newBalances[signedOrderLeft.makerAddress][defaultTakerAssetAddress], + 'Checking left maker ingress ERC20 account balance', + ).to.be.bignumber.equal( + erc20Balances[signedOrderLeft.makerAddress][defaultTakerAssetAddress].add( + expectedTransferAmounts.amountBoughtByLeftMaker, + ), + ); + expect( + newBalances[signedOrderRight.makerAddress][defaultMakerAssetAddress], + 'Checking right maker egress ERC20 account balance', + ).to.be.bignumber.equal( + erc20Balances[signedOrderRight.makerAddress][defaultMakerAssetAddress].add( + expectedTransferAmounts.amountBoughtByRightMaker, + ), + ); + // Paid fees + expect( + newBalances[signedOrderLeft.makerAddress][zrxToken.address], + 'Checking left maker egress ERC20 account fees', + ).to.be.bignumber.equal( + erc20Balances[signedOrderLeft.makerAddress][zrxToken.address].minus( + expectedTransferAmounts.feePaidByLeftMaker, + ), + ); + expect( + newBalances[signedOrderRight.makerAddress][zrxToken.address], + 'Checking right maker egress ERC20 account fees', + ).to.be.bignumber.equal( + erc20Balances[signedOrderRight.makerAddress][zrxToken.address].minus( + expectedTransferAmounts.feePaidByRightMaker, + ), + ); + expect( + newBalances[validTakerAddress][zrxToken.address], + 'Checking taker egress ERC20 account fees', + ).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address] + .minus(expectedTransferAmounts.feePaidByTakerLeft) + .sub(expectedTransferAmounts.feePaidByTakerRight), + ); + // Received fees + expect( + newBalances[signedOrderLeft.feeRecipientAddress][zrxToken.address], + 'Checking left fee recipient ingress ERC20 account fees', + ).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address] + .add(expectedTransferAmounts.feePaidByLeftMaker) + .add(expectedTransferAmounts.feePaidByRightMaker) + .add(expectedTransferAmounts.feePaidByTakerLeft) + .add(expectedTransferAmounts.feePaidByTakerRight), + ); + }); + it('should revert if left maker does not meet the balance threshold', async () => { + // Create signed order with non-valid maker address + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + senderAddress: erc721BalanceThresholdFilterInstance.address, + makerAddress: invalidAddress, + }); + // Execute transaction + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.matchOrdersAsync( + validSignedOrder, + signedOrderWithBadMakerAddress, + validTakerAddress, + ), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if right maker does not meet the balance threshold', async () => { + // Create signed order with non-valid maker address + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + senderAddress: erc721BalanceThresholdFilterInstance.address, + makerAddress: invalidAddress, + }); + // Execute transaction + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.matchOrdersAsync( + signedOrderWithBadMakerAddress, + validSignedOrder, + validTakerAddress, + ), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.matchOrdersAsync( + validSignedOrder, + validSignedOrder, + invalidAddress, + ), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + }); + + describe('cancelOrder', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + validSignedOrder2 = await orderFactory2.newSignedOrderAsync(); + }); + it('Should successfully cancel order if maker meets balance threshold', async () => { + // Verify order is not cancelled + const orderInfoBeforeCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync( + validSignedOrder, + ); + expect(orderInfoBeforeCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE); + // Cancel + const txReceipt = await erc721MakerBalanceThresholdWrapper.cancelOrderAsync( + validSignedOrder, + validSignedOrder.makerAddress, + ); + // Assert validated addresses + const expectedValidatedAddresseses: string[] = []; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check that order was cancelled + const orderInfoAfterCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync( + validSignedOrder, + ); + expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.CANCELLED); + }); + it('Should successfully cancel order if maker does not meet balance threshold', async () => { + // Create order where maker does not meet balance threshold + const signedOrderWithBadMakerAddress = await invalidOrderFactory.newSignedOrderAsync({}); + // Verify order is not cancelled + const orderInfoBeforeCancelling = await erc721NonValidBalanceThresholdWrapper.getOrderInfoAsync( + signedOrderWithBadMakerAddress, + ); + expect(orderInfoBeforeCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE); + // Cancel + const txReceipt = await erc721NonValidBalanceThresholdWrapper.cancelOrderAsync( + signedOrderWithBadMakerAddress, + signedOrderWithBadMakerAddress.makerAddress, + ); + // Assert validated addresses + const expectedValidatedAddresseses: string[] = []; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check that order was cancelled + const orderInfoAfterCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync( + signedOrderWithBadMakerAddress, + ); + expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.CANCELLED); + }); + }); + + describe('batchCancelOrders', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + }); + it('Should successfully batch cancel orders if maker meets balance threshold', async () => { + // Create orders to cancel + const validSignedOrders = [ + await orderFactory.newSignedOrderAsync(), + await orderFactory.newSignedOrderAsync(), + await orderFactory.newSignedOrderAsync(), + ]; + // Verify orders are not cancelled + _.each(validSignedOrders, async signedOrder => { + const orderInfoBeforeCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync( + signedOrder, + ); + return expect(orderInfoBeforeCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE); + }); + // Cancel + const txReceipt = await erc721MakerBalanceThresholdWrapper.batchCancelOrdersAsync( + validSignedOrders, + validSignedOrders[0].makerAddress, + ); + // Assert validated addresses + const expectedValidatedAddresseses: string[] = []; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check that order was cancelled + _.each(validSignedOrders, async signedOrder => { + const orderInfoAfterCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync( + signedOrder, + ); + return expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.CANCELLED); + }); + }); + it('Should successfully batch cancel order if maker does not meet balance threshold', async () => { + // Create orders to cancel + const invalidSignedOrders = [ + await invalidOrderFactory.newSignedOrderAsync(), + await invalidOrderFactory.newSignedOrderAsync(), + await invalidOrderFactory.newSignedOrderAsync(), + ]; + // Verify orders are not cancelled + _.each(invalidSignedOrders, async signedOrder => { + const orderInfoBeforeCancelling = await erc721NonValidBalanceThresholdWrapper.getOrderInfoAsync( + signedOrder, + ); + return expect(orderInfoBeforeCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE); + }); + // Cancel + const txReceipt = await erc721NonValidBalanceThresholdWrapper.batchCancelOrdersAsync( + invalidSignedOrders, + invalidAddress, + ); + // Assert validated addresses + const expectedValidatedAddresseses: string[] = []; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check that order was cancelled + _.each(invalidSignedOrders, async signedOrder => { + const orderInfoAfterCancelling = await erc721NonValidBalanceThresholdWrapper.getOrderInfoAsync( + signedOrder, + ); + return expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.CANCELLED); + }); + }); + }); + + describe('cancelOrdersUpTo', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + }); + it('Should successfully batch cancel orders if maker meets balance threshold', async () => { + // Create orders to cancel + const validSignedOrders = [ + await orderFactory.newSignedOrderAsync({ salt: new BigNumber(0) }), + await orderFactory.newSignedOrderAsync({ salt: new BigNumber(1) }), + await orderFactory.newSignedOrderAsync({ salt: new BigNumber(2) }), + ]; + // Verify orders are not cancelled + _.each(validSignedOrders, async signedOrder => { + const orderInfoBeforeCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync( + signedOrder, + ); + return expect(orderInfoBeforeCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE); + }); + // Cancel + const cancelOrdersUpToThisSalt = new BigNumber(1); + const txReceipt = await erc721MakerBalanceThresholdWrapper.cancelOrdersUpToAsync( + cancelOrdersUpToThisSalt, + validSignedOrders[0].makerAddress, + ); + // Assert validated addresses + const expectedValidatedAddresseses: string[] = []; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check that order was cancelled + _.each(validSignedOrders, async (signedOrder, salt: number) => { + const orderInfoAfterCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync( + signedOrder, + ); + const saltAsBigNumber = new BigNumber(salt); + if (saltAsBigNumber.lessThanOrEqualTo(cancelOrdersUpToThisSalt)) { + return expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.CANCELLED); + } else { + return expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE); + } + }); + }); + it('Should successfully batch cancel order if maker does not meet balance threshold', async () => { + // Create orders to cancel + const invalidSignedOrders = [ + await invalidOrderFactory.newSignedOrderAsync({ salt: new BigNumber(0) }), + await invalidOrderFactory.newSignedOrderAsync({ salt: new BigNumber(1) }), + await invalidOrderFactory.newSignedOrderAsync({ salt: new BigNumber(2) }), + ]; + // Verify orders are not cancelled + _.each(invalidSignedOrders, async signedOrder => { + const orderInfoBeforeCancelling = await erc721NonValidBalanceThresholdWrapper.getOrderInfoAsync( + signedOrder, + ); + return expect(orderInfoBeforeCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE); + }); + // Cancel + const cancelOrdersUpToThisSalt = new BigNumber(1); + const txReceipt = await erc721NonValidBalanceThresholdWrapper.cancelOrdersUpToAsync( + cancelOrdersUpToThisSalt, + invalidAddress, + ); + // Assert validated addresses + const expectedValidatedAddresseses: string[] = []; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check that order was cancelled + _.each(invalidSignedOrders, async (signedOrder, salt: number) => { + const orderInfoAfterCancelling = await erc721NonValidBalanceThresholdWrapper.getOrderInfoAsync( + signedOrder, + ); + const saltAsBigNumber = new BigNumber(salt); + if (saltAsBigNumber.lessThanOrEqualTo(cancelOrdersUpToThisSalt)) { + return expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.CANCELLED); + } else { + return expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE); + } + }); + }); + }); +}); +// tslint:disable:max-file-line-count +// tslint:enable:no-unnecessary-type-assertion diff --git a/contracts/extensions/test/extensions/dutch_auction.ts b/contracts/extensions/test/extensions/dutch_auction.ts new file mode 100644 index 000000000..22b3caa16 --- /dev/null +++ b/contracts/extensions/test/extensions/dutch_auction.ts @@ -0,0 +1,378 @@ +import { DutchAuctionWrapper } from '@0x/contract-wrappers'; +import { + artifacts as protocolArtifacts, + ERC20Wrapper, + ERC721Wrapper, + ExchangeContract, + ExchangeWrapper, +} from '@0x/contracts-protocol'; +import { + chaiSetup, + constants, + ContractName, + ERC20BalancesByOwner, + expectTransactionFailedAsync, + getLatestBlockTimestampAsync, + OrderFactory, + provider, + txDefaults, + web3Wrapper, +} from '@0x/contracts-test-utils'; +import { + artifacts as tokensArtifacts, + DummyERC20TokenContract, + DummyERC721TokenContract, + WETH9Contract, +} from '@0x/contracts-tokens'; +import { BlockchainLifecycle } from '@0x/dev-utils'; +import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils'; +import { RevertReason, SignedOrder } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import * as chai from 'chai'; +import * as _ from 'lodash'; + +import { DutchAuctionContract } from '../../generated-wrappers/dutch_auction'; +import { artifacts } from '../../src/artifacts'; +import { DutchAuctionTestWrapper } from '../utils/dutch_auction_test_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +const DECIMALS_DEFAULT = 18; + +describe(ContractName.DutchAuction, () => { + let makerAddress: string; + let owner: string; + let takerAddress: string; + let feeRecipientAddress: string; + let defaultMakerAssetAddress: string; + + let zrxToken: DummyERC20TokenContract; + let erc20TokenA: DummyERC20TokenContract; + let erc721Token: DummyERC721TokenContract; + let dutchAuctionContract: DutchAuctionContract; + let wethContract: WETH9Contract; + + let sellerOrderFactory: OrderFactory; + let buyerOrderFactory: OrderFactory; + let erc20Wrapper: ERC20Wrapper; + let erc20Balances: ERC20BalancesByOwner; + let currentBlockTimestamp: number; + let auctionBeginTimeSeconds: BigNumber; + let auctionEndTimeSeconds: BigNumber; + let auctionBeginAmount: BigNumber; + let auctionEndAmount: BigNumber; + let sellOrder: SignedOrder; + let buyOrder: SignedOrder; + let erc721MakerAssetIds: BigNumber[]; + const tenMinutesInSeconds = 10 * 60; + + let dutchAuctionTestWrapper: DutchAuctionTestWrapper; + let defaultERC20MakerAssetData: string; + + before(async () => { + await blockchainLifecycle.startAsync(); + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress] = accounts); + + erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); + + const numDummyErc20ToDeploy = 2; + [erc20TokenA, zrxToken] = await erc20Wrapper.deployDummyTokensAsync( + numDummyErc20ToDeploy, + constants.DUMMY_TOKEN_DECIMALS, + ); + const erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + + const erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); + [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(tokensArtifacts.WETH9, provider, txDefaults); + erc20Wrapper.addDummyTokenContract(wethContract as any); + + const zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync( + protocolArtifacts.Exchange, + provider, + txDefaults, + zrxAssetData, + ); + const exchangeWrapper = new ExchangeWrapper(exchangeInstance, 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, + }); + + const dutchAuctionInstance = await DutchAuctionContract.deployFrom0xArtifactAsync( + artifacts.DutchAuction, + provider, + txDefaults, + exchangeInstance.address, + ); + dutchAuctionContract = new DutchAuctionContract( + dutchAuctionInstance.abi, + dutchAuctionInstance.address, + provider, + ); + dutchAuctionTestWrapper = new DutchAuctionTestWrapper(dutchAuctionInstance, provider); + + defaultMakerAssetAddress = erc20TokenA.address; + const defaultTakerAssetAddress = wethContract.address; + + // Set up taker WETH balance and allowance + await web3Wrapper.awaitTransactionSuccessAsync( + await wethContract.deposit.sendTransactionAsync({ + from: takerAddress, + value: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), DECIMALS_DEFAULT), + }), + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await wethContract.approve.sendTransactionAsync( + erc20Proxy.address, + constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + { from: takerAddress }, + ), + ); + web3Wrapper.abiDecoder.addABI(exchangeInstance.abi); + web3Wrapper.abiDecoder.addABI(zrxToken.abi); + erc20Wrapper.addTokenOwnerAddress(dutchAuctionContract.address); + + currentBlockTimestamp = await getLatestBlockTimestampAsync(); + // Default auction begins 10 minutes ago + auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp).minus(tenMinutesInSeconds); + // Default auction ends 10 from now + auctionEndTimeSeconds = new BigNumber(currentBlockTimestamp).plus(tenMinutesInSeconds); + auctionBeginAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT); + auctionEndAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT); + + // Default sell order and buy order are exact mirrors + const sellerDefaultOrderParams = { + salt: generatePseudoRandomSalt(), + exchangeAddress: exchangeInstance.address, + makerAddress, + feeRecipientAddress, + // taker address or sender address should be set to the ducth auction contract + takerAddress: dutchAuctionContract.address, + makerAssetData: DutchAuctionWrapper.encodeDutchAuctionAssetData( + assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), + auctionBeginTimeSeconds, + auctionBeginAmount, + ), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), DECIMALS_DEFAULT), + takerAssetAmount: auctionEndAmount, + expirationTimeSeconds: auctionEndTimeSeconds, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + }; + // Default buy order is for the auction begin price + const buyerDefaultOrderParams = { + ...sellerDefaultOrderParams, + makerAddress: takerAddress, + makerAssetData: sellerDefaultOrderParams.takerAssetData, + takerAssetData: sellerDefaultOrderParams.makerAssetData, + makerAssetAmount: auctionBeginAmount, + takerAssetAmount: sellerDefaultOrderParams.makerAssetAmount, + }; + const makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)]; + const takerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(takerAddress)]; + sellerOrderFactory = new OrderFactory(makerPrivateKey, sellerDefaultOrderParams); + buyerOrderFactory = new OrderFactory(takerPrivateKey, buyerDefaultOrderParams); + defaultERC20MakerAssetData = assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + erc20Balances = await erc20Wrapper.getBalancesAsync(); + sellOrder = await sellerOrderFactory.newSignedOrderAsync(); + buyOrder = await buyerOrderFactory.newSignedOrderAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('matchOrders', () => { + it('should be worth the begin price at the begining of the auction', async () => { + auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp + 2); + const makerAssetData = DutchAuctionWrapper.encodeDutchAuctionAssetData( + defaultERC20MakerAssetData, + auctionBeginTimeSeconds, + auctionBeginAmount, + ); + sellOrder = await sellerOrderFactory.newSignedOrderAsync({ makerAssetData }); + const auctionDetails = await dutchAuctionTestWrapper.getAuctionDetailsAsync(sellOrder); + expect(auctionDetails.currentTimeSeconds).to.be.bignumber.lte(auctionBeginTimeSeconds); + expect(auctionDetails.currentAmount).to.be.bignumber.equal(auctionBeginAmount); + expect(auctionDetails.beginAmount).to.be.bignumber.equal(auctionBeginAmount); + }); + it('should be be worth the end price at the end of the auction', async () => { + auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp - tenMinutesInSeconds * 2); + auctionEndTimeSeconds = new BigNumber(currentBlockTimestamp - tenMinutesInSeconds); + const makerAssetData = DutchAuctionWrapper.encodeDutchAuctionAssetData( + defaultERC20MakerAssetData, + auctionBeginTimeSeconds, + auctionBeginAmount, + ); + sellOrder = await sellerOrderFactory.newSignedOrderAsync({ + makerAssetData, + expirationTimeSeconds: auctionEndTimeSeconds, + }); + const auctionDetails = await dutchAuctionTestWrapper.getAuctionDetailsAsync(sellOrder); + expect(auctionDetails.currentTimeSeconds).to.be.bignumber.gte(auctionEndTimeSeconds); + expect(auctionDetails.currentAmount).to.be.bignumber.equal(auctionEndAmount); + expect(auctionDetails.beginAmount).to.be.bignumber.equal(auctionBeginAmount); + }); + it('should match orders at current amount and send excess to buyer', async () => { + const beforeAuctionDetails = await dutchAuctionTestWrapper.getAuctionDetailsAsync(sellOrder); + buyOrder = await buyerOrderFactory.newSignedOrderAsync({ + makerAssetAmount: beforeAuctionDetails.currentAmount.times(2), + }); + await dutchAuctionTestWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress); + const afterAuctionDetails = await dutchAuctionTestWrapper.getAuctionDetailsAsync(sellOrder); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances[dutchAuctionContract.address][wethContract.address]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + // HACK gte used here due to a bug in ganache where the timestamp can change + // between multiple calls to the same block. Which can move the amount in our case + // ref: https://github.com/trufflesuite/ganache-core/issues/111 + expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.gte( + erc20Balances[makerAddress][wethContract.address].plus(afterAuctionDetails.currentAmount), + ); + expect(newBalances[takerAddress][wethContract.address]).to.be.bignumber.gte( + erc20Balances[takerAddress][wethContract.address].minus(beforeAuctionDetails.currentAmount), + ); + }); + it('maker fees on sellOrder are paid to the fee receipient', async () => { + sellOrder = await sellerOrderFactory.newSignedOrderAsync({ + makerFee: new BigNumber(1), + }); + await dutchAuctionTestWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress); + const afterAuctionDetails = await dutchAuctionTestWrapper.getAuctionDetailsAsync(sellOrder); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.gte( + erc20Balances[makerAddress][wethContract.address].plus(afterAuctionDetails.currentAmount), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].plus(sellOrder.makerFee), + ); + }); + it('maker fees on buyOrder are paid to the fee receipient', async () => { + buyOrder = await buyerOrderFactory.newSignedOrderAsync({ + makerFee: new BigNumber(1), + }); + await dutchAuctionTestWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const afterAuctionDetails = await dutchAuctionTestWrapper.getAuctionDetailsAsync(sellOrder); + expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.gte( + erc20Balances[makerAddress][wethContract.address].plus(afterAuctionDetails.currentAmount), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].plus(buyOrder.makerFee), + ); + }); + it('should revert when auction expires', async () => { + auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp - tenMinutesInSeconds * 2); + auctionEndTimeSeconds = new BigNumber(currentBlockTimestamp - tenMinutesInSeconds); + const makerAssetData = DutchAuctionWrapper.encodeDutchAuctionAssetData( + defaultERC20MakerAssetData, + auctionBeginTimeSeconds, + auctionBeginAmount, + ); + sellOrder = await sellerOrderFactory.newSignedOrderAsync({ + expirationTimeSeconds: auctionEndTimeSeconds, + makerAssetData, + }); + return expectTransactionFailedAsync( + dutchAuctionTestWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress), + RevertReason.AuctionExpired, + ); + }); + it('cannot be filled for less than the current price', async () => { + buyOrder = await buyerOrderFactory.newSignedOrderAsync({ + makerAssetAmount: sellOrder.takerAssetAmount, + }); + return expectTransactionFailedAsync( + dutchAuctionTestWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress), + RevertReason.AuctionInvalidAmount, + ); + }); + it('auction begin amount must be higher than final amount ', async () => { + sellOrder = await sellerOrderFactory.newSignedOrderAsync({ + takerAssetAmount: auctionBeginAmount.plus(1), + }); + return expectTransactionFailedAsync( + dutchAuctionTestWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress), + RevertReason.AuctionInvalidAmount, + ); + }); + it('begin time is less than end time', async () => { + auctionBeginTimeSeconds = new BigNumber(auctionEndTimeSeconds).plus(tenMinutesInSeconds); + const makerAssetData = DutchAuctionWrapper.encodeDutchAuctionAssetData( + defaultERC20MakerAssetData, + auctionBeginTimeSeconds, + auctionBeginAmount, + ); + sellOrder = await sellerOrderFactory.newSignedOrderAsync({ + expirationTimeSeconds: auctionEndTimeSeconds, + makerAssetData, + }); + return expectTransactionFailedAsync( + dutchAuctionTestWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress), + RevertReason.AuctionInvalidBeginTime, + ); + }); + it('asset data contains auction parameters', async () => { + sellOrder = await sellerOrderFactory.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), + }); + return expectTransactionFailedAsync( + dutchAuctionTestWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress), + RevertReason.InvalidAssetData, + ); + }); + + describe('ERC721', () => { + it('should match orders when ERC721', async () => { + const makerAssetId = erc721MakerAssetIds[0]; + const erc721MakerAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId); + const makerAssetData = DutchAuctionWrapper.encodeDutchAuctionAssetData( + erc721MakerAssetData, + auctionBeginTimeSeconds, + auctionBeginAmount, + ); + sellOrder = await sellerOrderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData, + }); + buyOrder = await buyerOrderFactory.newSignedOrderAsync({ + takerAssetAmount: new BigNumber(1), + takerAssetData: sellOrder.makerAssetData, + }); + await dutchAuctionTestWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress); + const afterAuctionDetails = await dutchAuctionTestWrapper.getAuctionDetailsAsync(sellOrder); + const newBalances = await erc20Wrapper.getBalancesAsync(); + // HACK gte used here due to a bug in ganache where the timestamp can change + // between multiple calls to the same block. Which can move the amount in our case + // ref: https://github.com/trufflesuite/ganache-core/issues/111 + expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.gte( + erc20Balances[makerAddress][wethContract.address].plus(afterAuctionDetails.currentAmount), + ); + const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); + expect(newOwner).to.be.bignumber.equal(takerAddress); + }); + }); + }); +}); diff --git a/contracts/extensions/test/extensions/forwarder.ts b/contracts/extensions/test/extensions/forwarder.ts new file mode 100644 index 000000000..69939ed04 --- /dev/null +++ b/contracts/extensions/test/extensions/forwarder.ts @@ -0,0 +1,1291 @@ +import { + artifacts as protocolArtifacts, + ERC20Wrapper, + ERC721Wrapper, + ExchangeContract, + ExchangeWrapper, +} from '@0x/contracts-protocol'; +import { + chaiSetup, + constants, + ContractName, + ERC20BalancesByOwner, + expectContractCreationFailedAsync, + expectTransactionFailedAsync, + OrderFactory, + provider, + sendTransactionResult, + txDefaults, + web3Wrapper, +} from '@0x/contracts-test-utils'; +import { + artifacts as tokenArtifacts, + DummyERC20TokenContract, + DummyERC721TokenContract, + WETH9Contract, +} from '@0x/contracts-tokens'; +import { BlockchainLifecycle } from '@0x/dev-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { RevertReason, SignedOrder } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import * as chai from 'chai'; +import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; + +import { ForwarderContract } from '../../generated-wrappers/forwarder'; +import { artifacts } from '../../src/artifacts'; + +import { ForwarderWrapper } from '../utils/forwarder_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 defaultMakerAssetAddress: string; + let zrxAssetData: string; + let wethAssetData: string; + + let weth: DummyERC20TokenContract; + let zrxToken: DummyERC20TokenContract; + let erc20TokenA: 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] = 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; + [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(tokenArtifacts.WETH9, provider, txDefaults); + weth = new DummyERC20TokenContract(wethContract.abi, wethContract.address, provider); + erc20Wrapper.addDummyTokenContract(weth); + + wethAssetData = assetDataUtils.encodeERC20AssetData(wethContract.address); + zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync( + protocolArtifacts.Exchange, + provider, + txDefaults, + zrxAssetData, + ); + exchangeWrapper = new ExchangeWrapper(exchangeInstance, 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('constructor', () => { + it('should revert if assetProxy is unregistered', async () => { + const exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync( + protocolArtifacts.Exchange, + provider, + txDefaults, + zrxAssetData, + ); + return expectContractCreationFailedAsync( + (ForwarderContract.deployFrom0xArtifactAsync( + artifacts.Forwarder, + provider, + txDefaults, + exchangeInstance.address, + zrxAssetData, + wethAssetData, + ) as any) as sendTransactionResult, + RevertReason.UnregisteredAssetProxy, + ); + }); + }); + 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); + }); + it('Should buy slightly greater MakerAsset when exchange rate is rounded', async () => { + // The 0x Protocol contracts round the exchange rate in favor of the Maker. + // In this case, the taker must round up how much they're going to spend, which + // in turn increases the amount of MakerAsset being purchased. + // Example: + // The taker wants to buy 5 units of the MakerAsset at a rate of 3M/2T. + // For every 2 units of TakerAsset, the taker will receive 3 units of MakerAsset. + // To purchase 5 units, the taker must spend 10/3 = 3.33 units of TakerAssset. + // However, the Taker can only spend whole units. + // Spending floor(10/3) = 3 units will yield a profit of Floor(3*3/2) = Floor(4.5) = 4 units of MakerAsset. + // Spending ceil(10/3) = 4 units will yield a profit of Floor(4*3/2) = 6 units of MakerAsset. + // + // The forwarding contract will opt for the second option, which overbuys, to ensure the taker + // receives at least the amount of MakerAsset they requested. + // + // Construct test case using values from example above + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber('30'), + takerAssetAmount: new BigNumber('20'), + makerAssetData: assetDataUtils.encodeERC20AssetData(erc20TokenA.address), + takerAssetData: assetDataUtils.encodeERC20AssetData(weth.address), + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + }); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const desiredMakerAssetFillAmount = new BigNumber('5'); + const makerAssetFillAmount = new BigNumber('6'); + const ethValue = new BigNumber('4'); + // Execute test case + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + desiredMakerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + ); + // Fetch end balances and construct expected outputs + 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)); + // Validate test case + expect(makerAssetFillAmount).to.be.bignumber.greaterThan(desiredMakerAssetFillAmount); + 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 slightly greater MakerAsset when exchange rate is rounded, and MakerAsset is ZRX', async () => { + // See the test case above for a detailed description of this case. + // The difference here is that the MakerAsset is ZRX. We expect the same result as above, + // but this tests a different code path. + // + // Construct test case using values from example above + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber('30'), + takerAssetAmount: new BigNumber('20'), + makerAssetData: zrxAssetData, + takerAssetData: assetDataUtils.encodeERC20AssetData(weth.address), + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + }); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const desiredMakerAssetFillAmount = new BigNumber('5'); + const makerAssetFillAmount = new BigNumber('6'); + const ethValue = new BigNumber('4'); + // Execute test case + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + desiredMakerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + ); + // Fetch end balances and construct expected outputs + 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)); + // Validate test case + expect(makerAssetFillAmount).to.be.bignumber.greaterThan(desiredMakerAssetFillAmount); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].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(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('Should buy slightly greater MakerAsset when exchange rate is rounded (Regression Test)', async () => { + // Order taken from a transaction on mainnet that failed due to a rounding error. + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber('268166666666666666666'), + takerAssetAmount: new BigNumber('219090625878836371'), + makerAssetData: assetDataUtils.encodeERC20AssetData(erc20TokenA.address), + takerAssetData: assetDataUtils.encodeERC20AssetData(weth.address), + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + }); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + // The taker will receive more than the desired amount of makerAsset due to rounding + const desiredMakerAssetFillAmount = new BigNumber('5000000000000000000'); + const ethValue = new BigNumber('4084971271824171'); + const makerAssetFillAmount = ethValue + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + // Execute test case + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + desiredMakerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + ); + // Fetch end balances and construct expected outputs + 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)); + // Validate test case + expect(makerAssetFillAmount).to.be.bignumber.greaterThan(desiredMakerAssetFillAmount); + 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 slightly greater MakerAsset when exchange rate is rounded, and MakerAsset is ZRX (Regression Test)', async () => { + // Order taken from a transaction on mainnet that failed due to a rounding error. + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber('268166666666666666666'), + takerAssetAmount: new BigNumber('219090625878836371'), + makerAssetData: zrxAssetData, + takerAssetData: assetDataUtils.encodeERC20AssetData(weth.address), + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + }); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + // The taker will receive more than the desired amount of makerAsset due to rounding + const desiredMakerAssetFillAmount = new BigNumber('5000000000000000000'); + const ethValue = new BigNumber('4084971271824171'); + const makerAssetFillAmount = ethValue + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + // Execute test case + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + desiredMakerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + ); + // Fetch end balances and construct expected outputs + 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)); + // Validate test case + expect(makerAssetFillAmount).to.be.bignumber.greaterThan(desiredMakerAssetFillAmount); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].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(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('Should buy correct MakerAsset when exchange rate is NOT rounded, and MakerAsset is ZRX (Regression Test)', async () => { + // An extra unit of TakerAsset was sent to the exchange contract to account for rounding errors, in Forwarder v1. + // Specifically, the takerFillAmount was calculated using Floor(desiredMakerAmount * exchangeRate) + 1 + // We have since changed this to be Ceil(desiredMakerAmount * exchangeRate) + // These calculations produce different results when `desiredMakerAmount * exchangeRate` is an integer. + // + // This test verifies that `ceil` is sufficient: + // Let TakerAssetAmount = MakerAssetAmount * 2 + // -> exchangeRate = TakerAssetAmount / MakerAssetAmount = (2*MakerAssetAmount)/MakerAssetAmount = 2 + // .: desiredMakerAmount * exchangeRate is an integer. + // + // Construct test case using values from example above + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber('30'), + takerAssetAmount: new BigNumber('60'), + makerAssetData: zrxAssetData, + takerAssetData: assetDataUtils.encodeERC20AssetData(weth.address), + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + }); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = new BigNumber('5'); + const ethValue = new BigNumber('10'); + // Execute test case + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + // Fetch end balances and construct expected outputs + 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)); + // Validate test case + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].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(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/contracts/extensions/test/extensions/order_matcher.ts b/contracts/extensions/test/extensions/order_matcher.ts new file mode 100644 index 000000000..acb46ced4 --- /dev/null +++ b/contracts/extensions/test/extensions/order_matcher.ts @@ -0,0 +1,818 @@ +import { + artifacts as protocolArtifacts, + ERC20ProxyContract, + ERC20Wrapper, + ERC721ProxyContract, + ExchangeContract, + ExchangeFillEventArgs, + ExchangeWrapper, +} from '@0x/contracts-protocol'; +import { + chaiSetup, + constants, + ERC20BalancesByOwner, + expectContractCreationFailedAsync, + expectTransactionFailedAsync, + LogDecoder, + OrderFactory, + provider, + sendTransactionResult, + txDefaults, + web3Wrapper, +} from '@0x/contracts-test-utils'; +import { artifacts as tokenArtifacts, DummyERC20TokenContract, DummyERC721TokenContract } from '@0x/contracts-tokens'; +import { BlockchainLifecycle } from '@0x/dev-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { RevertReason } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import * as chai from 'chai'; +import { LogWithDecodedArgs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { OrderMatcherContract } from '../../generated-wrappers/order_matcher'; +import { artifacts } from '../../src/artifacts'; + +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +chaiSetup.configure(); +const expect = chai.expect; +// tslint:disable:no-unnecessary-type-assertion +describe('OrderMatcher', () => { + let makerAddressLeft: string; + let makerAddressRight: string; + let owner: string; + let takerAddress: string; + let feeRecipientAddressLeft: string; + let feeRecipientAddressRight: string; + + let erc20TokenA: DummyERC20TokenContract; + let erc20TokenB: DummyERC20TokenContract; + let zrxToken: DummyERC20TokenContract; + let exchange: ExchangeContract; + let erc20Proxy: ERC20ProxyContract; + let erc721Proxy: ERC721ProxyContract; + let orderMatcher: OrderMatcherContract; + + let erc20BalancesByOwner: ERC20BalancesByOwner; + let exchangeWrapper: ExchangeWrapper; + let erc20Wrapper: ERC20Wrapper; + let orderFactoryLeft: OrderFactory; + let orderFactoryRight: OrderFactory; + + let leftMakerAssetData: string; + let leftTakerAssetData: string; + let defaultERC20MakerAssetAddress: string; + let defaultERC20TakerAssetAddress: string; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + // Create accounts + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + // Hack(albrow): Both Prettier and TSLint insert a trailing comma below + // but that is invalid syntax as of TypeScript version >= 2.8. We don't + // have the right fine-grained configuration options in TSLint, + // Prettier, or TypeScript, to reconcile this, so we will just have to + // wait for them to sort it out. We disable TSLint and Prettier for + // this part of the code for now. This occurs several times in this + // file. See https://github.com/prettier/prettier/issues/4624. + // prettier-ignore + const usedAddresses = ([ + owner, + makerAddressLeft, + makerAddressRight, + takerAddress, + feeRecipientAddressLeft, + // tslint:disable-next-line:trailing-comma + feeRecipientAddressRight + ] = _.slice(accounts, 0, 6)); + // Create wrappers + erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); + // Deploy ERC20 token & ERC20 proxy + const numDummyErc20ToDeploy = 3; + [erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync( + numDummyErc20ToDeploy, + constants.DUMMY_TOKEN_DECIMALS, + ); + erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + // Deploy ERC721 proxy + erc721Proxy = await ERC721ProxyContract.deployFrom0xArtifactAsync( + protocolArtifacts.ERC721Proxy, + provider, + txDefaults, + ); + // Depoy exchange + exchange = await ExchangeContract.deployFrom0xArtifactAsync( + protocolArtifacts.Exchange, + provider, + txDefaults, + assetDataUtils.encodeERC20AssetData(zrxToken.address), + ); + exchangeWrapper = new ExchangeWrapper(exchange, provider); + await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); + await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner); + // Authorize ERC20 trades by exchange + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Deploy OrderMatcher + orderMatcher = await OrderMatcherContract.deployFrom0xArtifactAsync( + artifacts.OrderMatcher, + provider, + txDefaults, + exchange.address, + ); + // Set default addresses + defaultERC20MakerAssetAddress = erc20TokenA.address; + defaultERC20TakerAssetAddress = erc20TokenB.address; + leftMakerAssetData = assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress); + leftTakerAssetData = assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress); + // Set OrderMatcher balances and allowances + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20TokenA.setBalance.sendTransactionAsync(orderMatcher.address, constants.INITIAL_ERC20_BALANCE, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20TokenB.setBalance.sendTransactionAsync(orderMatcher.address, constants.INITIAL_ERC20_BALANCE, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await orderMatcher.approveAssetProxy.sendTransactionAsync( + leftMakerAssetData, + constants.INITIAL_ERC20_ALLOWANCE, + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await orderMatcher.approveAssetProxy.sendTransactionAsync( + leftTakerAssetData, + constants.INITIAL_ERC20_ALLOWANCE, + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Create default order parameters + const defaultOrderParamsLeft = { + ...constants.STATIC_ORDER_PARAMS, + makerAddress: makerAddressLeft, + exchangeAddress: exchange.address, + makerAssetData: leftMakerAssetData, + takerAssetData: leftTakerAssetData, + feeRecipientAddress: feeRecipientAddressLeft, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + }; + const defaultOrderParamsRight = { + ...constants.STATIC_ORDER_PARAMS, + makerAddress: makerAddressRight, + exchangeAddress: exchange.address, + makerAssetData: leftTakerAssetData, + takerAssetData: leftMakerAssetData, + feeRecipientAddress: feeRecipientAddressRight, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + }; + const privateKeyLeft = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressLeft)]; + orderFactoryLeft = new OrderFactory(privateKeyLeft, defaultOrderParamsLeft); + const privateKeyRight = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressRight)]; + orderFactoryRight = new OrderFactory(privateKeyRight, defaultOrderParamsRight); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('constructor', () => { + it('should revert if assetProxy is unregistered', async () => { + const exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync( + protocolArtifacts.Exchange, + provider, + txDefaults, + constants.NULL_BYTES, + ); + return expectContractCreationFailedAsync( + (OrderMatcherContract.deployFrom0xArtifactAsync( + artifacts.OrderMatcher, + provider, + txDefaults, + exchangeInstance.address, + ) as any) as sendTransactionResult, + RevertReason.UnregisteredAssetProxy, + ); + }); + }); + describe('matchOrders', () => { + beforeEach(async () => { + erc20BalancesByOwner = await erc20Wrapper.getBalancesAsync(); + }); + it('should revert if not called by owner', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + }); + const data = exchange.matchOrders.getABIEncodedTransactionData( + signedOrderLeft, + signedOrderRight, + signedOrderLeft.signature, + signedOrderRight.signature, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + data, + to: orderMatcher.address, + from: takerAddress, + gas: constants.MAX_MATCH_ORDERS_GAS, + }), + RevertReason.OnlyContractOwner, + ); + }); + it('should transfer the correct amounts when orders completely fill each other', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + }); + // Match signedOrderLeft with signedOrderRight + const expectedTransferAmounts = { + // Left Maker + amountSoldByLeftMaker: signedOrderLeft.makerAssetAmount, + amountBoughtByLeftMaker: signedOrderLeft.takerAssetAmount, + // Right Maker + amountSoldByRightMaker: signedOrderRight.makerAssetAmount, + amountBoughtByRightMaker: signedOrderRight.takerAssetAmount, + // Taker + leftMakerAssetSpreadAmount: signedOrderLeft.makerAssetAmount.minus(signedOrderRight.takerAssetAmount), + }; + const initialLeftMakerAssetTakerBalance = await erc20TokenA.balanceOf.callAsync(orderMatcher.address); + const data = exchange.matchOrders.getABIEncodedTransactionData( + signedOrderLeft, + signedOrderRight, + signedOrderLeft.signature, + signedOrderRight.signature, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + data, + to: orderMatcher.address, + from: owner, + gas: constants.MAX_MATCH_ORDERS_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newLeftMakerAssetTakerBalance = await erc20TokenA.balanceOf.callAsync(orderMatcher.address); + const newErc20Balances = await erc20Wrapper.getBalancesAsync(); + expect(newErc20Balances[makerAddressLeft][defaultERC20MakerAssetAddress]).to.be.bignumber.equal( + erc20BalancesByOwner[makerAddressLeft][defaultERC20MakerAssetAddress].minus( + expectedTransferAmounts.amountSoldByLeftMaker, + ), + ); + expect(newErc20Balances[makerAddressRight][defaultERC20TakerAssetAddress]).to.be.bignumber.equal( + erc20BalancesByOwner[makerAddressRight][defaultERC20TakerAssetAddress].minus( + expectedTransferAmounts.amountSoldByRightMaker, + ), + ); + expect(newErc20Balances[makerAddressLeft][defaultERC20TakerAssetAddress]).to.be.bignumber.equal( + erc20BalancesByOwner[makerAddressLeft][defaultERC20TakerAssetAddress].plus( + expectedTransferAmounts.amountBoughtByLeftMaker, + ), + ); + expect(newErc20Balances[makerAddressRight][defaultERC20MakerAssetAddress]).to.be.bignumber.equal( + erc20BalancesByOwner[makerAddressRight][defaultERC20MakerAssetAddress].plus( + expectedTransferAmounts.amountBoughtByRightMaker, + ), + ); + expect(newLeftMakerAssetTakerBalance).to.be.bignumber.equal( + initialLeftMakerAssetTakerBalance.plus(expectedTransferAmounts.leftMakerAssetSpreadAmount), + ); + }); + it('should transfer the correct amounts when orders completely fill each other and taker doesnt take a profit', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + }); + // Match signedOrderLeft with signedOrderRight + const expectedTransferAmounts = { + // Left Maker + amountSoldByLeftMaker: signedOrderLeft.makerAssetAmount, + amountBoughtByLeftMaker: signedOrderLeft.takerAssetAmount, + // Right Maker + amountSoldByRightMaker: signedOrderRight.makerAssetAmount, + amountBoughtByRightMaker: signedOrderRight.takerAssetAmount, + }; + const initialLeftMakerAssetTakerBalance = await erc20TokenA.balanceOf.callAsync(orderMatcher.address); + const data = exchange.matchOrders.getABIEncodedTransactionData( + signedOrderLeft, + signedOrderRight, + signedOrderLeft.signature, + signedOrderRight.signature, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + data, + to: orderMatcher.address, + from: owner, + gas: constants.MAX_MATCH_ORDERS_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newLeftMakerAssetTakerBalance = await erc20TokenA.balanceOf.callAsync(orderMatcher.address); + const newErc20Balances = await erc20Wrapper.getBalancesAsync(); + expect(newErc20Balances[makerAddressLeft][defaultERC20MakerAssetAddress]).to.be.bignumber.equal( + erc20BalancesByOwner[makerAddressLeft][defaultERC20MakerAssetAddress].minus( + expectedTransferAmounts.amountSoldByLeftMaker, + ), + ); + expect(newErc20Balances[makerAddressRight][defaultERC20TakerAssetAddress]).to.be.bignumber.equal( + erc20BalancesByOwner[makerAddressRight][defaultERC20TakerAssetAddress].minus( + expectedTransferAmounts.amountSoldByRightMaker, + ), + ); + expect(newErc20Balances[makerAddressLeft][defaultERC20TakerAssetAddress]).to.be.bignumber.equal( + erc20BalancesByOwner[makerAddressLeft][defaultERC20TakerAssetAddress].plus( + expectedTransferAmounts.amountBoughtByLeftMaker, + ), + ); + expect(newErc20Balances[makerAddressRight][defaultERC20MakerAssetAddress]).to.be.bignumber.equal( + erc20BalancesByOwner[makerAddressRight][defaultERC20MakerAssetAddress].plus( + expectedTransferAmounts.amountBoughtByRightMaker, + ), + ); + expect(newLeftMakerAssetTakerBalance).to.be.bignumber.equal(initialLeftMakerAssetTakerBalance); + }); + it('should transfer the correct amounts when left order is completely filled and right order would be partially filled', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(20), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(4), 18), + }); + // Match signedOrderLeft with signedOrderRight + const expectedTransferAmounts = { + // Left Maker + amountSoldByLeftMaker: signedOrderLeft.makerAssetAmount, + amountBoughtByLeftMaker: signedOrderLeft.takerAssetAmount, + // Right Maker + amountSoldByRightMaker: signedOrderRight.makerAssetAmount, + amountBoughtByRightMaker: signedOrderRight.takerAssetAmount, + // Taker + leftMakerAssetSpreadAmount: signedOrderLeft.makerAssetAmount.minus(signedOrderRight.takerAssetAmount), + leftTakerAssetSpreadAmount: signedOrderRight.makerAssetAmount.minus(signedOrderLeft.takerAssetAmount), + }; + const initialLeftMakerAssetTakerBalance = await erc20TokenA.balanceOf.callAsync(orderMatcher.address); + const initialLeftTakerAssetTakerBalance = await erc20TokenB.balanceOf.callAsync(orderMatcher.address); + // Match signedOrderLeft with signedOrderRight + const data = exchange.matchOrders.getABIEncodedTransactionData( + signedOrderLeft, + signedOrderRight, + signedOrderLeft.signature, + signedOrderRight.signature, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + data, + to: orderMatcher.address, + from: owner, + gas: constants.MAX_MATCH_ORDERS_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newLeftMakerAssetTakerBalance = await erc20TokenA.balanceOf.callAsync(orderMatcher.address); + const newLeftTakerAssetTakerBalance = await erc20TokenB.balanceOf.callAsync(orderMatcher.address); + const newErc20Balances = await erc20Wrapper.getBalancesAsync(); + expect(newErc20Balances[makerAddressLeft][defaultERC20MakerAssetAddress]).to.be.bignumber.equal( + erc20BalancesByOwner[makerAddressLeft][defaultERC20MakerAssetAddress].minus( + expectedTransferAmounts.amountSoldByLeftMaker, + ), + ); + expect(newErc20Balances[makerAddressRight][defaultERC20TakerAssetAddress]).to.be.bignumber.equal( + erc20BalancesByOwner[makerAddressRight][defaultERC20TakerAssetAddress].minus( + expectedTransferAmounts.amountSoldByRightMaker, + ), + ); + expect(newErc20Balances[makerAddressLeft][defaultERC20TakerAssetAddress]).to.be.bignumber.equal( + erc20BalancesByOwner[makerAddressLeft][defaultERC20TakerAssetAddress].plus( + expectedTransferAmounts.amountBoughtByLeftMaker, + ), + ); + expect(newErc20Balances[makerAddressRight][defaultERC20MakerAssetAddress]).to.be.bignumber.equal( + erc20BalancesByOwner[makerAddressRight][defaultERC20MakerAssetAddress].plus( + expectedTransferAmounts.amountBoughtByRightMaker, + ), + ); + expect(newLeftMakerAssetTakerBalance).to.be.bignumber.equal( + initialLeftMakerAssetTakerBalance.plus(expectedTransferAmounts.leftMakerAssetSpreadAmount), + ); + expect(newLeftTakerAssetTakerBalance).to.be.bignumber.equal( + initialLeftTakerAssetTakerBalance.plus(expectedTransferAmounts.leftTakerAssetSpreadAmount), + ); + }); + it('should not call fillOrder when rightOrder is completely filled after matchOrders call and orders were never partially filled', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + }); + const data = exchange.matchOrders.getABIEncodedTransactionData( + signedOrderLeft, + signedOrderRight, + signedOrderLeft.signature, + signedOrderRight.signature, + ); + const logDecoder = new LogDecoder(web3Wrapper, { ...artifacts, ...tokenArtifacts, ...protocolArtifacts }); + const txReceipt = await logDecoder.getTxWithDecodedLogsAsync( + await web3Wrapper.sendTransactionAsync({ + data, + to: orderMatcher.address, + from: owner, + gas: constants.MAX_MATCH_ORDERS_GAS, + }), + ); + const fillLogs = _.filter( + txReceipt.logs, + log => (log as LogWithDecodedArgs<ExchangeFillEventArgs>).event === 'Fill', + ); + // Only 2 Fill logs should exist for `matchOrders` call. `fillOrder` should not have been called and should not have emitted a Fill event. + expect(fillLogs.length).to.be.equal(2); + }); + it('should not call fillOrder when rightOrder is completely filled after matchOrders call and orders were initially partially filled', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + }); + await exchangeWrapper.fillOrderAsync(signedOrderLeft, takerAddress, { + takerAssetFillAmount: signedOrderLeft.takerAssetAmount.dividedToIntegerBy(5), + }); + await exchangeWrapper.fillOrderAsync(signedOrderRight, takerAddress, { + takerAssetFillAmount: signedOrderRight.takerAssetAmount.dividedToIntegerBy(5), + }); + const data = exchange.matchOrders.getABIEncodedTransactionData( + signedOrderLeft, + signedOrderRight, + signedOrderLeft.signature, + signedOrderRight.signature, + ); + const logDecoder = new LogDecoder(web3Wrapper, { ...artifacts, ...tokenArtifacts, ...protocolArtifacts }); + const txReceipt = await logDecoder.getTxWithDecodedLogsAsync( + await web3Wrapper.sendTransactionAsync({ + data, + to: orderMatcher.address, + from: owner, + gas: constants.MAX_MATCH_ORDERS_GAS, + }), + ); + const fillLogs = _.filter( + txReceipt.logs, + log => (log as LogWithDecodedArgs<ExchangeFillEventArgs>).event === 'Fill', + ); + // Only 2 Fill logs should exist for `matchOrders` call. `fillOrder` should not have been called and should not have emitted a Fill event. + expect(fillLogs.length).to.be.equal(2); + }); + it('should only take a spread in rightMakerAsset if entire leftMakerAssetSpread amount can be used to fill rightOrder after matchOrders call', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.9), 18), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(990), 18), + }); + const initialLeftMakerAssetSpreadAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(1.09), 18); + const leftTakerAssetSpreadAmount = initialLeftMakerAssetSpreadAmount + .times(signedOrderRight.makerAssetAmount) + .dividedToIntegerBy(signedOrderRight.takerAssetAmount); + // Match signedOrderLeft with signedOrderRight + const expectedTransferAmounts = { + // Left Maker + amountSoldByLeftMaker: signedOrderLeft.makerAssetAmount, + amountBoughtByLeftMaker: signedOrderLeft.takerAssetAmount, + // Right Maker + amountSoldByRightMaker: signedOrderLeft.takerAssetAmount.plus(leftTakerAssetSpreadAmount), + amountBoughtByRightMaker: signedOrderLeft.makerAssetAmount, + // Taker + leftMakerAssetSpreadAmount: constants.ZERO_AMOUNT, + leftTakerAssetSpreadAmount, + }; + const initialLeftMakerAssetTakerBalance = await erc20TokenA.balanceOf.callAsync(orderMatcher.address); + const initialLeftTakerAssetTakerBalance = await erc20TokenB.balanceOf.callAsync(orderMatcher.address); + // Match signedOrderLeft with signedOrderRight + const data = exchange.matchOrders.getABIEncodedTransactionData( + signedOrderLeft, + signedOrderRight, + signedOrderLeft.signature, + signedOrderRight.signature, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + data, + to: orderMatcher.address, + from: owner, + gas: constants.MAX_MATCH_ORDERS_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newLeftMakerAssetTakerBalance = await erc20TokenA.balanceOf.callAsync(orderMatcher.address); + const newLeftTakerAssetTakerBalance = await erc20TokenB.balanceOf.callAsync(orderMatcher.address); + const newErc20Balances = await erc20Wrapper.getBalancesAsync(); + expect(newErc20Balances[makerAddressLeft][defaultERC20MakerAssetAddress]).to.be.bignumber.equal( + erc20BalancesByOwner[makerAddressLeft][defaultERC20MakerAssetAddress].minus( + expectedTransferAmounts.amountSoldByLeftMaker, + ), + ); + expect(newErc20Balances[makerAddressRight][defaultERC20TakerAssetAddress]).to.be.bignumber.equal( + erc20BalancesByOwner[makerAddressRight][defaultERC20TakerAssetAddress].minus( + expectedTransferAmounts.amountSoldByRightMaker, + ), + ); + expect(newErc20Balances[makerAddressLeft][defaultERC20TakerAssetAddress]).to.be.bignumber.equal( + erc20BalancesByOwner[makerAddressLeft][defaultERC20TakerAssetAddress].plus( + expectedTransferAmounts.amountBoughtByLeftMaker, + ), + ); + expect(newErc20Balances[makerAddressRight][defaultERC20MakerAssetAddress]).to.be.bignumber.equal( + erc20BalancesByOwner[makerAddressRight][defaultERC20MakerAssetAddress].plus( + expectedTransferAmounts.amountBoughtByRightMaker, + ), + ); + expect(newLeftMakerAssetTakerBalance).to.be.bignumber.equal( + initialLeftMakerAssetTakerBalance.plus(expectedTransferAmounts.leftMakerAssetSpreadAmount), + ); + expect(newLeftTakerAssetTakerBalance).to.be.bignumber.equal( + initialLeftTakerAssetTakerBalance.plus(expectedTransferAmounts.leftTakerAssetSpreadAmount), + ); + }); + it("should succeed if rightOrder's makerAssetData and takerAssetData are not provided", async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(20), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(4), 18), + }); + // Match signedOrderLeft with signedOrderRight + const expectedTransferAmounts = { + // Left Maker + amountSoldByLeftMaker: signedOrderLeft.makerAssetAmount, + amountBoughtByLeftMaker: signedOrderLeft.takerAssetAmount, + // Right Maker + amountSoldByRightMaker: signedOrderRight.makerAssetAmount, + amountBoughtByRightMaker: signedOrderRight.takerAssetAmount, + // Taker + leftMakerAssetSpreadAmount: signedOrderLeft.makerAssetAmount.minus(signedOrderRight.takerAssetAmount), + leftTakerAssetSpreadAmount: signedOrderRight.makerAssetAmount.minus(signedOrderLeft.takerAssetAmount), + }; + const initialLeftMakerAssetTakerBalance = await erc20TokenA.balanceOf.callAsync(orderMatcher.address); + const initialLeftTakerAssetTakerBalance = await erc20TokenB.balanceOf.callAsync(orderMatcher.address); + // Match signedOrderLeft with signedOrderRight + signedOrderRight.makerAssetData = constants.NULL_BYTES; + signedOrderRight.takerAssetData = constants.NULL_BYTES; + const data = exchange.matchOrders.getABIEncodedTransactionData( + signedOrderLeft, + signedOrderRight, + signedOrderLeft.signature, + signedOrderRight.signature, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + data, + to: orderMatcher.address, + from: owner, + gas: constants.MAX_MATCH_ORDERS_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newLeftMakerAssetTakerBalance = await erc20TokenA.balanceOf.callAsync(orderMatcher.address); + const newLeftTakerAssetTakerBalance = await erc20TokenB.balanceOf.callAsync(orderMatcher.address); + const newErc20Balances = await erc20Wrapper.getBalancesAsync(); + expect(newErc20Balances[makerAddressLeft][defaultERC20MakerAssetAddress]).to.be.bignumber.equal( + erc20BalancesByOwner[makerAddressLeft][defaultERC20MakerAssetAddress].minus( + expectedTransferAmounts.amountSoldByLeftMaker, + ), + ); + expect(newErc20Balances[makerAddressRight][defaultERC20TakerAssetAddress]).to.be.bignumber.equal( + erc20BalancesByOwner[makerAddressRight][defaultERC20TakerAssetAddress].minus( + expectedTransferAmounts.amountSoldByRightMaker, + ), + ); + expect(newErc20Balances[makerAddressLeft][defaultERC20TakerAssetAddress]).to.be.bignumber.equal( + erc20BalancesByOwner[makerAddressLeft][defaultERC20TakerAssetAddress].plus( + expectedTransferAmounts.amountBoughtByLeftMaker, + ), + ); + expect(newErc20Balances[makerAddressRight][defaultERC20MakerAssetAddress]).to.be.bignumber.equal( + erc20BalancesByOwner[makerAddressRight][defaultERC20MakerAssetAddress].plus( + expectedTransferAmounts.amountBoughtByRightMaker, + ), + ); + expect(newLeftMakerAssetTakerBalance).to.be.bignumber.equal( + initialLeftMakerAssetTakerBalance.plus(expectedTransferAmounts.leftMakerAssetSpreadAmount), + ); + expect(newLeftTakerAssetTakerBalance).to.be.bignumber.equal( + initialLeftTakerAssetTakerBalance.plus(expectedTransferAmounts.leftTakerAssetSpreadAmount), + ); + }); + it('should revert with the correct reason if matchOrders call reverts', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + }); + signedOrderRight.signature = `0xff${signedOrderRight.signature.slice(4)}`; + const data = exchange.matchOrders.getABIEncodedTransactionData( + signedOrderLeft, + signedOrderRight, + signedOrderLeft.signature, + signedOrderRight.signature, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + data, + to: orderMatcher.address, + from: owner, + gas: constants.MAX_MATCH_ORDERS_GAS, + }), + RevertReason.InvalidOrderSignature, + ); + }); + it('should revert with the correct reason if fillOrder call reverts', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(20), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(4), 18), + }); + // Matcher will not have enough allowance to fill rightOrder + await web3Wrapper.awaitTransactionSuccessAsync( + await orderMatcher.approveAssetProxy.sendTransactionAsync(leftMakerAssetData, constants.ZERO_AMOUNT, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const data = exchange.matchOrders.getABIEncodedTransactionData( + signedOrderLeft, + signedOrderRight, + signedOrderLeft.signature, + signedOrderRight.signature, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + data, + to: orderMatcher.address, + from: owner, + gas: constants.MAX_MATCH_ORDERS_GAS, + }), + RevertReason.TransferFailed, + ); + }); + }); + describe('withdrawAsset', () => { + it('should allow owner to withdraw ERC20 tokens', async () => { + const erc20AWithdrawAmount = await erc20TokenA.balanceOf.callAsync(orderMatcher.address); + expect(erc20AWithdrawAmount).to.be.bignumber.gt(constants.ZERO_AMOUNT); + await web3Wrapper.awaitTransactionSuccessAsync( + await orderMatcher.withdrawAsset.sendTransactionAsync(leftMakerAssetData, erc20AWithdrawAmount, { + from: owner, + }), + ); + const newBalance = await erc20TokenA.balanceOf.callAsync(orderMatcher.address); + expect(newBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should allow owner to withdraw ERC721 tokens', async () => { + const erc721Token = await DummyERC721TokenContract.deployFrom0xArtifactAsync( + tokenArtifacts.DummyERC721Token, + provider, + txDefaults, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + ); + const tokenId = new BigNumber(1); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.mint.sendTransactionAsync(orderMatcher.address, tokenId, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const assetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, tokenId); + const withdrawAmount = new BigNumber(1); + await web3Wrapper.awaitTransactionSuccessAsync( + await orderMatcher.withdrawAsset.sendTransactionAsync(assetData, withdrawAmount, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const erc721Owner = await erc721Token.ownerOf.callAsync(tokenId); + expect(erc721Owner).to.be.equal(owner); + }); + it('should revert if not called by owner', async () => { + const erc20AWithdrawAmount = await erc20TokenA.balanceOf.callAsync(orderMatcher.address); + expect(erc20AWithdrawAmount).to.be.bignumber.gt(constants.ZERO_AMOUNT); + await expectTransactionFailedAsync( + orderMatcher.withdrawAsset.sendTransactionAsync(leftMakerAssetData, erc20AWithdrawAmount, { + from: takerAddress, + }), + RevertReason.OnlyContractOwner, + ); + }); + }); + describe('approveAssetProxy', () => { + it('should be able to set an allowance for ERC20 tokens', async () => { + const allowance = new BigNumber(55465465426546); + await web3Wrapper.awaitTransactionSuccessAsync( + await orderMatcher.approveAssetProxy.sendTransactionAsync(leftMakerAssetData, allowance, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newAllowance = await erc20TokenA.allowance.callAsync(orderMatcher.address, erc20Proxy.address); + expect(newAllowance).to.be.bignumber.equal(allowance); + }); + it('should be able to approve an ERC721 token by passing in allowance = 1', async () => { + const erc721Token = await DummyERC721TokenContract.deployFrom0xArtifactAsync( + tokenArtifacts.DummyERC721Token, + provider, + txDefaults, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + ); + const assetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, constants.ZERO_AMOUNT); + const allowance = new BigNumber(1); + await web3Wrapper.awaitTransactionSuccessAsync( + await orderMatcher.approveAssetProxy.sendTransactionAsync(assetData, allowance, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const isApproved = await erc721Token.isApprovedForAll.callAsync(orderMatcher.address, erc721Proxy.address); + expect(isApproved).to.be.equal(true); + }); + it('should be able to approve an ERC721 token by passing in allowance > 1', async () => { + const erc721Token = await DummyERC721TokenContract.deployFrom0xArtifactAsync( + tokenArtifacts.DummyERC721Token, + provider, + txDefaults, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + ); + const assetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, constants.ZERO_AMOUNT); + const allowance = new BigNumber(2); + await web3Wrapper.awaitTransactionSuccessAsync( + await orderMatcher.approveAssetProxy.sendTransactionAsync(assetData, allowance, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const isApproved = await erc721Token.isApprovedForAll.callAsync(orderMatcher.address, erc721Proxy.address); + expect(isApproved).to.be.equal(true); + }); + it('should revert if not called by owner', async () => { + const approval = new BigNumber(1); + await expectTransactionFailedAsync( + orderMatcher.approveAssetProxy.sendTransactionAsync(leftMakerAssetData, approval, { + from: takerAddress, + }), + RevertReason.OnlyContractOwner, + ); + }); + }); +}); +// tslint:disable:max-file-line-count +// tslint:enable:no-unnecessary-type-assertion diff --git a/contracts/extensions/test/extensions/order_validator.ts b/contracts/extensions/test/extensions/order_validator.ts new file mode 100644 index 000000000..82a6b937f --- /dev/null +++ b/contracts/extensions/test/extensions/order_validator.ts @@ -0,0 +1,609 @@ +import { + artifacts as protocolArtifacts, + ERC20ProxyContract, + ERC20Wrapper, + ERC721ProxyContract, + ERC721Wrapper, + ExchangeContract, + ExchangeWrapper, +} from '@0x/contracts-protocol'; +import { + chaiSetup, + constants, + OrderFactory, + OrderStatus, + provider, + txDefaults, + web3Wrapper, +} from '@0x/contracts-test-utils'; +import { DummyERC20TokenContract, DummyERC721TokenContract } from '@0x/contracts-tokens'; +import { BlockchainLifecycle } from '@0x/dev-utils'; +import { assetDataUtils, orderHashUtils } from '@0x/order-utils'; +import { SignedOrder } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import * as chai from 'chai'; +import * as _ from 'lodash'; + +import { OrderValidatorContract } from '../../generated-wrappers/order_validator'; +import { artifacts } from '../../src/artifacts/index'; + +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 zrxToken: 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 = 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( + protocolArtifacts.Exchange, + provider, + txDefaults, + zrxAssetData, + ); + 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, + zrxAssetData, + ); + + 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 0 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(constants.ZERO_AMOUNT); + }); + }); + }); + 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, + ); + const isApproved = true; + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.setApprovalForAll.sendTransactionAsync(erc721Proxy.address, isApproved, { + 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(); + }); + 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); + 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, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Token.approve.sendTransactionAsync(erc20Proxy.address, makerAllowance, { + from: makerAddress, + }), + 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, + ); + const isApproved = true; + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.setApprovalForAll.sendTransactionAsync(erc721Proxy.address, isApproved, { + 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); + 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', () => { + 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(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, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Token.approve.sendTransactionAsync(erc20Proxy.address, makerAllowance, { + from: makerAddress, + }), + 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, { + 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(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', () => { + 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); + 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, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Token.approve.sendTransactionAsync(erc20Proxy.address, makerAllowance, { + from: makerAddress, + }), + 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, + ); + const isApproved = true; + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.setApprovalForAll.sendTransactionAsync(erc721Proxy.address, isApproved, { + 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); + 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', () => { + 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(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, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Token.approve.sendTransactionAsync(erc20Proxy.address, makerAllowance, { + from: makerAddress, + }), + 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, { + 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(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 diff --git a/contracts/extensions/test/global_hooks.ts b/contracts/extensions/test/global_hooks.ts new file mode 100644 index 000000000..f8ace376a --- /dev/null +++ b/contracts/extensions/test/global_hooks.ts @@ -0,0 +1,17 @@ +import { env, EnvVars } from '@0x/dev-utils'; + +import { coverage, profiler, provider } from '@0x/contracts-test-utils'; +before('start web3 provider', () => { + provider.start(); +}); +after('generate coverage report', async () => { + if (env.parseBoolean(EnvVars.SolidityCoverage)) { + const coverageSubprovider = coverage.getCoverageSubproviderSingleton(); + await coverageSubprovider.writeCoverageAsync(); + } + if (env.parseBoolean(EnvVars.SolidityProfiler)) { + const profilerSubprovider = profiler.getProfilerSubproviderSingleton(); + await profilerSubprovider.writeProfilerOutputAsync(); + } + provider.stop(); +}); diff --git a/contracts/extensions/test/utils/balance_threshold_wrapper.ts b/contracts/extensions/test/utils/balance_threshold_wrapper.ts new file mode 100644 index 000000000..28a4ef011 --- /dev/null +++ b/contracts/extensions/test/utils/balance_threshold_wrapper.ts @@ -0,0 +1,283 @@ +import { artifacts as protocolArtifacts, ExchangeContract } from '@0x/contracts-protocol'; +import { + FillResults, + formatters, + LogDecoder, + OrderInfo, + orderUtils, + TransactionFactory, +} from '@0x/contracts-test-utils'; +import { artifacts as tokensArtifacts } from '@0x/contracts-tokens'; +import { SignedOrder } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import { Provider, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { BalanceThresholdFilterContract } from '../../generated-wrappers/balance_threshold_filter'; +import { artifacts } from '../../src/artifacts'; + +export class BalanceThresholdWrapper { + private readonly _balanceThresholdFilter: BalanceThresholdFilterContract; + private readonly _signerTransactionFactory: TransactionFactory; + private readonly _exchange: ExchangeContract; + private readonly _web3Wrapper: Web3Wrapper; + private readonly _logDecoder: LogDecoder; + constructor( + balanceThresholdFilter: BalanceThresholdFilterContract, + exchangeContract: ExchangeContract, + signerTransactionFactory: TransactionFactory, + provider: Provider, + ) { + this._balanceThresholdFilter = balanceThresholdFilter; + this._exchange = exchangeContract; + this._signerTransactionFactory = signerTransactionFactory; + this._web3Wrapper = new Web3Wrapper(provider); + this._logDecoder = new LogDecoder(this._web3Wrapper, { + ...artifacts, + ...tokensArtifacts, + ...protocolArtifacts, + }); + } + public async fillOrderAsync( + signedOrder: SignedOrder, + from: string, + opts: { takerAssetFillAmount?: BigNumber } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount); + const data = this._exchange.fillOrder.getABIEncodedTransactionData( + params.order, + params.takerAssetFillAmount, + params.signature, + ); + const txReceipt = this._executeTransactionAsync(data, from); + return txReceipt; + } + public async fillOrKillOrderAsync( + signedOrder: SignedOrder, + from: string, + opts: { takerAssetFillAmount?: BigNumber } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount); + const data = this._exchange.fillOrKillOrder.getABIEncodedTransactionData( + params.order, + params.takerAssetFillAmount, + params.signature, + ); + const txReceipt = this._executeTransactionAsync(data, from); + return txReceipt; + } + public async fillOrderNoThrowAsync( + signedOrder: SignedOrder, + from: string, + opts: { takerAssetFillAmount?: BigNumber; gas?: number } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount); + const data = this._exchange.fillOrderNoThrow.getABIEncodedTransactionData( + params.order, + params.takerAssetFillAmount, + params.signature, + ); + const txReceipt = this._executeTransactionAsync(data, from, opts.gas); + return txReceipt; + } + public async batchFillOrdersAsync( + orders: SignedOrder[], + from: string, + opts: { takerAssetFillAmounts?: BigNumber[] } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts); + const data = this._exchange.batchFillOrders.getABIEncodedTransactionData( + params.orders, + params.takerAssetFillAmounts, + params.signatures, + ); + const txReceipt = this._executeTransactionAsync(data, from); + return txReceipt; + } + public async batchFillOrKillOrdersAsync( + orders: SignedOrder[], + from: string, + opts: { takerAssetFillAmounts?: BigNumber[] } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts); + const data = this._exchange.batchFillOrKillOrders.getABIEncodedTransactionData( + params.orders, + params.takerAssetFillAmounts, + params.signatures, + ); + const txReceipt = this._executeTransactionAsync(data, from); + return txReceipt; + } + public async batchFillOrdersNoThrowAsync( + orders: SignedOrder[], + from: string, + opts: { takerAssetFillAmounts?: BigNumber[]; gas?: number } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts); + const data = this._exchange.batchFillOrKillOrders.getABIEncodedTransactionData( + params.orders, + params.takerAssetFillAmounts, + params.signatures, + ); + const txReceipt = this._executeTransactionAsync(data, from, opts.gas); + return txReceipt; + } + public async marketSellOrdersAsync( + orders: SignedOrder[], + from: string, + opts: { takerAssetFillAmount: BigNumber }, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createMarketSellOrders(orders, opts.takerAssetFillAmount); + const data = this._exchange.marketSellOrders.getABIEncodedTransactionData( + params.orders, + params.takerAssetFillAmount, + params.signatures, + ); + const txReceipt = this._executeTransactionAsync(data, from); + return txReceipt; + } + public async marketSellOrdersNoThrowAsync( + orders: SignedOrder[], + from: string, + opts: { takerAssetFillAmount: BigNumber; gas?: number }, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createMarketSellOrders(orders, opts.takerAssetFillAmount); + const data = this._exchange.marketSellOrdersNoThrow.getABIEncodedTransactionData( + params.orders, + params.takerAssetFillAmount, + params.signatures, + ); + const txReceipt = this._executeTransactionAsync(data, from, opts.gas); + return txReceipt; + } + public async marketBuyOrdersAsync( + orders: SignedOrder[], + from: string, + opts: { makerAssetFillAmount: BigNumber }, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createMarketBuyOrders(orders, opts.makerAssetFillAmount); + const data = this._exchange.marketBuyOrders.getABIEncodedTransactionData( + params.orders, + params.makerAssetFillAmount, + params.signatures, + ); + const txReceipt = this._executeTransactionAsync(data, from); + return txReceipt; + } + public async marketBuyOrdersNoThrowAsync( + orders: SignedOrder[], + from: string, + opts: { makerAssetFillAmount: BigNumber; gas?: number }, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createMarketBuyOrders(orders, opts.makerAssetFillAmount); + const data = this._exchange.marketBuyOrdersNoThrow.getABIEncodedTransactionData( + params.orders, + params.makerAssetFillAmount, + params.signatures, + ); + const txReceipt = this._executeTransactionAsync(data, from, opts.gas); + return txReceipt; + } + public async cancelOrderAsync(signedOrder: SignedOrder, from: string): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createCancel(signedOrder); + const data = this._exchange.cancelOrder.getABIEncodedTransactionData(params.order); + const txReceipt = this._executeTransactionAsync(data, from); + return txReceipt; + } + public async batchCancelOrdersAsync( + orders: SignedOrder[], + from: string, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createBatchCancel(orders); + const data = this._exchange.batchCancelOrders.getABIEncodedTransactionData(params.orders); + const txReceipt = this._executeTransactionAsync(data, from); + return txReceipt; + } + public async cancelOrdersUpToAsync(salt: BigNumber, from: string): Promise<TransactionReceiptWithDecodedLogs> { + const data = this._exchange.cancelOrdersUpTo.getABIEncodedTransactionData(salt); + const txReceipt = this._executeTransactionAsync(data, from); + return txReceipt; + } + public async getTakerAssetFilledAmountAsync(orderHashHex: string): Promise<BigNumber> { + const filledAmount = await this._exchange.filled.callAsync(orderHashHex); + return filledAmount; + } + public async isCancelledAsync(orderHashHex: string): Promise<boolean> { + const isCancelled = await this._exchange.cancelled.callAsync(orderHashHex); + return isCancelled; + } + public async getOrderEpochAsync(makerAddress: string, senderAddress: string): Promise<BigNumber> { + const orderEpoch = await this._exchange.orderEpoch.callAsync(makerAddress, senderAddress); + return orderEpoch; + } + public async getOrderInfoAsync(signedOrder: SignedOrder): Promise<OrderInfo> { + const orderInfo = await this._exchange.getOrderInfo.callAsync(signedOrder); + return orderInfo; + } + public async getOrdersInfoAsync(signedOrders: SignedOrder[]): Promise<OrderInfo[]> { + const ordersInfo = (await this._exchange.getOrdersInfo.callAsync(signedOrders)) as OrderInfo[]; + return ordersInfo; + } + public async matchOrdersAsync( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + from: string, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createMatchOrders(signedOrderLeft, signedOrderRight); + const data = await this._exchange.matchOrders.getABIEncodedTransactionData( + params.left, + params.right, + params.leftSignature, + params.rightSignature, + ); + const txReceipt = this._executeTransactionAsync(data, from); + return txReceipt; + } + public async getFillOrderResultsAsync( + signedOrder: SignedOrder, + from: string, + opts: { takerAssetFillAmount?: BigNumber } = {}, + ): Promise<FillResults> { + const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount); + const fillResults = await this._exchange.fillOrder.callAsync( + params.order, + params.takerAssetFillAmount, + params.signature, + { from }, + ); + return fillResults; + } + public abiEncodeFillOrder(signedOrder: SignedOrder, opts: { takerAssetFillAmount?: BigNumber } = {}): string { + const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount); + const data = this._exchange.fillOrder.getABIEncodedTransactionData( + params.order, + params.takerAssetFillAmount, + params.signature, + ); + return data; + } + public getBalanceThresholdAddress(): string { + return this._balanceThresholdFilter.address; + } + public getExchangeAddress(): string { + return this._exchange.address; + } + private async _executeTransactionAsync( + abiEncodedExchangeTxData: string, + from: string, + gas?: number, + ): Promise<TransactionReceiptWithDecodedLogs> { + const signedExchangeTx = this._signerTransactionFactory.newSignedTransaction(abiEncodedExchangeTxData); + const txOpts = _.isUndefined(gas) ? { from } : { from, gas }; + const txHash = await this._balanceThresholdFilter.executeTransaction.sendTransactionAsync( + signedExchangeTx.salt, + signedExchangeTx.signerAddress, + signedExchangeTx.data, + signedExchangeTx.signature, + txOpts, + ); + const txReceipt = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return txReceipt; + } +} diff --git a/contracts/extensions/test/utils/dutch_auction_test_wrapper.ts b/contracts/extensions/test/utils/dutch_auction_test_wrapper.ts new file mode 100644 index 000000000..c1e2f2070 --- /dev/null +++ b/contracts/extensions/test/utils/dutch_auction_test_wrapper.ts @@ -0,0 +1,62 @@ +import { artifacts as protocolArtifacts } from '@0x/contracts-protocol'; +import { LogDecoder } from '@0x/contracts-test-utils'; +import { artifacts as tokensArtifacts } from '@0x/contracts-tokens'; +import { DutchAuctionDetails, SignedOrder } from '@0x/types'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import { Provider, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { DutchAuctionContract } from '../../generated-wrappers/dutch_auction'; +import { artifacts } from '../../src/artifacts'; + +export class DutchAuctionTestWrapper { + private readonly _dutchAuctionContract: DutchAuctionContract; + private readonly _web3Wrapper: Web3Wrapper; + private readonly _logDecoder: LogDecoder; + + constructor(contractInstance: DutchAuctionContract, provider: Provider) { + this._dutchAuctionContract = contractInstance; + this._web3Wrapper = new Web3Wrapper(provider); + this._logDecoder = new LogDecoder(this._web3Wrapper, { + ...artifacts, + ...tokensArtifacts, + ...protocolArtifacts, + }); + } + /** + * Matches the buy and sell orders at an amount given the following: the current block time, the auction + * start time and the auction begin amount. The sell order is a an order at the lowest amount + * at the end of the auction. Excess from the match is transferred to the seller. + * Over time the price moves from beginAmount to endAmount given the current block.timestamp. + * @param buyOrder The Buyer's order. This order is for the current expected price of the auction. + * @param sellOrder The Seller's order. This order is for the lowest amount (at the end of the auction). + * @param from Address the transaction is being sent from. + * @return Transaction receipt with decoded logs. + */ + public async matchOrdersAsync( + buyOrder: SignedOrder, + sellOrder: SignedOrder, + from: string, + ): Promise<TransactionReceiptWithDecodedLogs> { + const txHash = await this._dutchAuctionContract.matchOrders.sendTransactionAsync( + buyOrder, + sellOrder, + buyOrder.signature, + sellOrder.signature, + { + from, + }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + /** + * Calculates the Auction Details for the given order + * @param sellOrder The Seller's order. This order is for the lowest amount (at the end of the auction). + * @return The dutch auction details. + */ + public async getAuctionDetailsAsync(sellOrder: SignedOrder): Promise<DutchAuctionDetails> { + const auctionDetails = await this._dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); + return auctionDetails; + } +} diff --git a/contracts/extensions/test/utils/forwarder_wrapper.ts b/contracts/extensions/test/utils/forwarder_wrapper.ts new file mode 100644 index 000000000..9e44ff6b9 --- /dev/null +++ b/contracts/extensions/test/utils/forwarder_wrapper.ts @@ -0,0 +1,124 @@ +import { artifacts as protocolArtifacts } from '@0x/contracts-protocol'; +import { constants, formatters, LogDecoder, MarketSellOrders } from '@0x/contracts-test-utils'; +import { artifacts as tokensArtifacts } from '@0x/contracts-tokens'; +import { SignedOrder } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import { Provider, TransactionReceiptWithDecodedLogs, TxDataPayable } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { ForwarderContract } from '../../generated-wrappers/forwarder'; +import { artifacts } from '../../src/artifacts'; + +export class ForwarderWrapper { + private readonly _web3Wrapper: Web3Wrapper; + private readonly _forwarderContract: ForwarderContract; + private readonly _logDecoder: LogDecoder; + public static getPercentageOfValue(value: BigNumber, percentage: number): BigNumber { + const numerator = constants.PERCENTAGE_DENOMINATOR.times(percentage).dividedToIntegerBy(100); + const newValue = value.times(numerator).dividedToIntegerBy(constants.PERCENTAGE_DENOMINATOR); + return newValue; + } + public static getWethForFeeOrders(feeAmount: BigNumber, feeOrders: SignedOrder[]): BigNumber { + let wethAmount = new BigNumber(0); + let remainingFeeAmount = feeAmount; + _.forEach(feeOrders, feeOrder => { + const feeAvailable = feeOrder.makerAssetAmount.minus(feeOrder.takerFee); + if (!remainingFeeAmount.isZero() && feeAvailable.gt(remainingFeeAmount)) { + wethAmount = wethAmount.plus( + feeOrder.takerAssetAmount + .times(remainingFeeAmount) + .dividedBy(feeAvailable) + .ceil(), + ); + remainingFeeAmount = new BigNumber(0); + } else if (!remainingFeeAmount.isZero()) { + wethAmount = wethAmount.plus(feeOrder.takerAssetAmount); + remainingFeeAmount = remainingFeeAmount.minus(feeAvailable); + } + }); + return wethAmount; + } + private static _createOptimizedOrders(signedOrders: SignedOrder[]): MarketSellOrders { + _.forEach(signedOrders, (signedOrder, index) => { + signedOrder.takerAssetData = constants.NULL_BYTES; + if (index > 0) { + signedOrder.makerAssetData = constants.NULL_BYTES; + } + }); + const params = formatters.createMarketSellOrders(signedOrders, constants.ZERO_AMOUNT); + return params; + } + private static _createOptimizedZrxOrders(signedOrders: SignedOrder[]): MarketSellOrders { + _.forEach(signedOrders, signedOrder => { + signedOrder.makerAssetData = constants.NULL_BYTES; + signedOrder.takerAssetData = constants.NULL_BYTES; + }); + const params = formatters.createMarketSellOrders(signedOrders, constants.ZERO_AMOUNT); + return params; + } + constructor(contractInstance: ForwarderContract, provider: Provider) { + this._forwarderContract = contractInstance; + this._web3Wrapper = new Web3Wrapper(provider); + this._logDecoder = new LogDecoder(this._web3Wrapper, { + ...artifacts, + ...tokensArtifacts, + ...protocolArtifacts, + }); + } + public async marketSellOrdersWithEthAsync( + orders: SignedOrder[], + feeOrders: SignedOrder[], + txData: TxDataPayable, + opts: { feePercentage?: BigNumber; feeRecipient?: string } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = ForwarderWrapper._createOptimizedOrders(orders); + const feeParams = ForwarderWrapper._createOptimizedZrxOrders(feeOrders); + const feePercentage = _.isUndefined(opts.feePercentage) ? constants.ZERO_AMOUNT : opts.feePercentage; + const feeRecipient = _.isUndefined(opts.feeRecipient) ? constants.NULL_ADDRESS : opts.feeRecipient; + const txHash = await this._forwarderContract.marketSellOrdersWithEth.sendTransactionAsync( + params.orders, + params.signatures, + feeParams.orders, + feeParams.signatures, + feePercentage, + feeRecipient, + txData, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async marketBuyOrdersWithEthAsync( + orders: SignedOrder[], + feeOrders: SignedOrder[], + makerAssetFillAmount: BigNumber, + txData: TxDataPayable, + opts: { feePercentage?: BigNumber; feeRecipient?: string } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = ForwarderWrapper._createOptimizedOrders(orders); + const feeParams = ForwarderWrapper._createOptimizedZrxOrders(feeOrders); + const feePercentage = _.isUndefined(opts.feePercentage) ? constants.ZERO_AMOUNT : opts.feePercentage; + const feeRecipient = _.isUndefined(opts.feeRecipient) ? constants.NULL_ADDRESS : opts.feeRecipient; + const txHash = await this._forwarderContract.marketBuyOrdersWithEth.sendTransactionAsync( + params.orders, + makerAssetFillAmount, + params.signatures, + feeParams.orders, + feeParams.signatures, + feePercentage, + feeRecipient, + txData, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async withdrawAssetAsync( + assetData: string, + amount: BigNumber, + txData: TxDataPayable, + ): Promise<TransactionReceiptWithDecodedLogs> { + const txHash = await this._forwarderContract.withdrawAsset.sendTransactionAsync(assetData, amount, txData); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } +} diff --git a/contracts/extensions/tsconfig.json b/contracts/extensions/tsconfig.json new file mode 100644 index 000000000..ed9b4fbe1 --- /dev/null +++ b/contracts/extensions/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "outDir": "lib", + "rootDir": ".", + "resolveJsonModule": true + }, + "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"], + "files": [ + "./generated-artifacts/BalanceThresholdFilter.json", + "./generated-artifacts/DutchAuction.json", + "./generated-artifacts/Forwarder.json", + "./generated-artifacts/OrderMatcher.json", + "./generated-artifacts/OrderValidator.json" + ], + "exclude": ["./deploy/solc/solc_bin"] +} diff --git a/contracts/extensions/tslint.json b/contracts/extensions/tslint.json new file mode 100644 index 000000000..1bb3ac2a2 --- /dev/null +++ b/contracts/extensions/tslint.json @@ -0,0 +1,6 @@ +{ + "extends": ["@0x/tslint-config"], + "rules": { + "custom-no-magic-numbers": false + } +} |