From 814518dd8094d03908d77e39faa21b8758f1552b Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Mon, 9 Jul 2018 21:09:45 -0700 Subject: Refactor forwarding contract architecture, remove batch functions --- .../contracts/src/2.0.0/forwarder/Forwarder.sol | 13 +- .../contracts/src/2.0.0/forwarder/MixinAssets.sol | 169 +++++++ .../src/2.0.0/forwarder/MixinConstants.sol | 4 + .../src/2.0.0/forwarder/MixinErrorMessages.sol | 18 +- .../src/2.0.0/forwarder/MixinExpectedResults.sol | 161 ------- .../contracts/src/2.0.0/forwarder/MixinFees.sol | 126 ----- .../src/2.0.0/forwarder/MixinForwarderCore.sol | 519 ++++++++------------- .../src/2.0.0/forwarder/MixinMarketBuyZrx.sol | 83 ---- .../src/2.0.0/forwarder/MixinTransfer.sol | 118 ----- .../contracts/src/2.0.0/forwarder/MixinWeth.sol | 109 +++++ .../src/2.0.0/forwarder/interfaces/IAssets.sol | 53 +++ .../forwarder/interfaces/IExpectedResults.sol | 66 --- .../src/2.0.0/forwarder/interfaces/IForwarder.sol | 4 +- .../2.0.0/forwarder/interfaces/IForwarderCore.sol | 69 +-- .../src/2.0.0/forwarder/mixins/MAssets.sol | 50 ++ .../src/2.0.0/forwarder/mixins/MConstants.sol | 4 + .../2.0.0/forwarder/mixins/MExpectedResults.sol | 42 -- .../contracts/src/2.0.0/forwarder/mixins/MFees.sol | 63 --- .../src/2.0.0/forwarder/mixins/MForwarderCore.sol | 76 ++- .../src/2.0.0/forwarder/mixins/MMarketBuyZrx.sol | 42 -- .../src/2.0.0/forwarder/mixins/MTransfer.sol | 46 -- .../contracts/src/2.0.0/forwarder/mixins/MWeth.sol | 43 ++ 22 files changed, 706 insertions(+), 1172 deletions(-) create mode 100644 packages/contracts/src/2.0.0/forwarder/MixinAssets.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/MixinExpectedResults.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/MixinFees.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/MixinMarketBuyZrx.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/MixinTransfer.sol create mode 100644 packages/contracts/src/2.0.0/forwarder/MixinWeth.sol create mode 100644 packages/contracts/src/2.0.0/forwarder/interfaces/IAssets.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/interfaces/IExpectedResults.sol create mode 100644 packages/contracts/src/2.0.0/forwarder/mixins/MAssets.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/mixins/MExpectedResults.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/mixins/MFees.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/mixins/MMarketBuyZrx.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/mixins/MTransfer.sol create mode 100644 packages/contracts/src/2.0.0/forwarder/mixins/MWeth.sol diff --git a/packages/contracts/src/2.0.0/forwarder/Forwarder.sol b/packages/contracts/src/2.0.0/forwarder/Forwarder.sol index 546e7f22c..4405d5e46 100644 --- a/packages/contracts/src/2.0.0/forwarder/Forwarder.sol +++ b/packages/contracts/src/2.0.0/forwarder/Forwarder.sol @@ -19,20 +19,17 @@ pragma solidity 0.4.24; pragma experimental ABIEncoderV2; -import "./MixinFees.sol"; +import "./MixinWeth.sol"; import "./MixinForwarderCore.sol"; import "./MixinConstants.sol"; -import "./MixinMarketBuyZrx.sol"; -import "./MixinExpectedResults.sol"; -import "./MixinTransfer.sol"; +import "./MixinAssets.sol"; +// solhint-disable no-empty-blocks contract Forwarder is MixinConstants, - MixinExpectedResults, - MixinFees, - MixinMarketBuyZrx, - MixinTransfer, + MixinWeth, + MixinAssets, MixinForwarderCore { diff --git a/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol b/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol new file mode 100644 index 000000000..325af5518 --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol @@ -0,0 +1,169 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; + +import "../utils/LibBytes/LibBytes.sol"; +import "../utils/Ownable/Ownable.sol"; +import "../tokens/ERC20Token/IERC20Token.sol"; +import "../tokens/ERC721Token/IERC721Token.sol"; +import "./mixins/MAssets.sol"; +import "./mixins/MConstants.sol"; + + +contract MixinAssets is + Ownable, + MConstants, + MAssets +{ + + using LibBytes for bytes; + + bytes4 constant internal ERC20_TRANSFER_SELECTOR = bytes4(keccak256("transfer(address,uint256)")); + bytes4 constant internal ERC721_RECEIVED = bytes4(keccak256("onERC721Received(address,uint256,bytes)")); + bytes4 constant internal ERC721_RECEIVED_OPERATOR = bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")); + + /// @dev Withdraws ERC20 tokens 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 tokens that were accidentally sent to this contract. + /// @param token Address of ERC20 token to withdraw. + /// @param amount Amount of ERC20 token to withdraw. + function withdrawERC20( + address token, + uint256 amount + ) + external + onlyOwner + { + require( + IERC20Token(token).transfer(msg.sender, amount), + "WITHDRAWAL_FAILED" + ); + } + + function onERC721Received( + address, + uint256, + bytes memory + ) + public + pure + returns(bytes4) + { + return ERC721_RECEIVED; + } + + function onERC721Received( + address, + address, + uint256, + bytes memory + ) + public + pure + returns(bytes4) + { + return ERC721_RECEIVED_OPERATOR; + } + + /// @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 transferPurchasedAssetToSender( + 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); + } else { + revert("UNSUPPORTED_TOKEN_PROXY"); + } + } + + /// @dev Decodes ERC20 assetData and transfers given amount to sender. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of asset to transfer to sender. + function transferERC20Token( + bytes memory assetData, + uint256 amount + ) + internal + { + address token = assetData.readAddress(16); + + // Transfer tokens. + // We do a raw call so we can check the success separate + // from the return data. + bool success = token.call(abi.encodeWithSelector( + ERC20_TRANSFER_SELECTOR, + msg.sender, + amount + )); + require( + success, + "TRANSFER_FAILED" + ); + + // Check return data. + // If there is no return data, we assume the token incorrectly + // does not return a bool. In this case we expect it to revert + // on failure, which was handled above. + // If the token does return data, we require that it is a single + // value that evaluates to true. + assembly { + if returndatasize { + success := 0 + if eq(returndatasize, 32) { + // First 64 bytes of memory are reserved scratch space + returndatacopy(0, 0, 32) + success := mload(0) + } + } + } + require( + success, + "TRANSFER_FAILED" + ); + } + + /// @dev Decodes ERC721 assetData and transfers given amount to sender. + /// @param assetData Byte array encoded for the respective asset proxy. + function transferERC721Token(bytes memory assetData) + internal + { + // Decode asset data. + address token = assetData.readAddress(16); + uint256 tokenId = assetData.readUint256(36); + bytes memory receiverData = assetData.readBytesWithLength(100); + + // Perform transfer. + // TODO: Do we want to use `transferFrom` here? + IERC721Token(token).safeTransferFrom( + address(this), + msg.sender, + tokenId, + receiverData + ); + } +} diff --git a/packages/contracts/src/2.0.0/forwarder/MixinConstants.sol b/packages/contracts/src/2.0.0/forwarder/MixinConstants.sol index 2b064d579..4f95c796b 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinConstants.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinConstants.sol @@ -25,6 +25,10 @@ contract MixinConstants is MConstants { + bytes4 constant internal ERC20_DATA_ID = bytes4(keccak256("ERC20Token(address)")); + bytes4 constant internal ERC721_DATA_ID = bytes4(keccak256("ERC721Token(address,uint256,bytes)")); + uint256 constant internal MAX_UINT = 2**256 - 1; + constructor ( address _exchange, address _etherToken, diff --git a/packages/contracts/src/2.0.0/forwarder/MixinErrorMessages.sol b/packages/contracts/src/2.0.0/forwarder/MixinErrorMessages.sol index 1b3e3f488..91758eadc 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinErrorMessages.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinErrorMessages.sol @@ -22,14 +22,12 @@ pragma solidity 0.4.24; /// This contract is intended to serve as a reference, but is not actually used for efficiency reasons. contract MixinErrorMessages { - string constant VALUE_GREATER_THAN_ZERO = "VALUE_GREATER_THAN_ZERO"; - string constant FEE_PROPORTION_TOO_LARGE = "FEE_PROPORTION_TOO_LARGE"; - string constant TAKER_ASSET_ZRX_REQUIRED = "TAKER_ASSET_ZRX_REQUIRED"; - string constant TAKER_ASSET_WETH_REQUIRED = "TAKER_ASSET_WETH_REQUIRED"; - string constant SAME_ASSET_TYPE_REQUIRED = "SAME_ASSET_TYPE_REQUIRED"; - string constant UNACCEPTABLE_THRESHOLD = "UNACCEPTABLE_THRESHOLD"; - string constant UNSUPPORTED_TOKEN_PROXY = "UNSUPPORTED_TOKEN_PROXY"; - string constant ASSET_AMOUNT_MATCH_ORDER_SIZE = "ASSET_AMOUNT_MUST_MATCH_ORDER_SIZE"; - string constant DEFAULT_FUNCTION_WETH_CONTRACT_ONLY = "DEFAULT_FUNCTION_WETH_CONTRACT_ONLY"; - string constant INVALID_MSG_VALUE = "INVALID_MSG_VALUE"; + string constant FEE_PERCENTAGE_TOO_LARGE = "FEE_PROPORTION_TOO_LARGE"; // Provided fee percentage greater than 5%. + string constant MAX_FEE_EXCEEDED = "MAX_FEE_EXCEEDED"; // Not enough ETH remaining to pay feeRecipient. + string constant OVERSOLD_WETH = "OVERSOLD_WETH"; // More WETH sold than provided with current message call. + string constant COMPLETE_FILL_FAILED = "COMPLETE_FILL_FAILED"; // Desired purchase amount not completely filled (required for ZRX fees only). + string constant TRANSFER_FAILED = "TRANSFER_FAILED"; // Asset transfer failed. + string constant UNSUPPORTED_TOKEN_PROXY = "UNSUPPORTED_TOKEN_PROXY"; // Proxy in assetData not supported. + string constant DEFAULT_FUNCTION_WETH_CONTRACT_ONLY = "DEFAULT_FUNCTION_WETH_CONTRACT_ONLY"; // Fallback function may only be used for WETH withdrawals. + string constant INVALID_MSG_VALUE = "INVALID_MSG_VALUE"; // msg.value must be greater than 0. } diff --git a/packages/contracts/src/2.0.0/forwarder/MixinExpectedResults.sol b/packages/contracts/src/2.0.0/forwarder/MixinExpectedResults.sol deleted file mode 100644 index a575c9675..000000000 --- a/packages/contracts/src/2.0.0/forwarder/MixinExpectedResults.sol +++ /dev/null @@ -1,161 +0,0 @@ -/* - - Copyright 2018 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity 0.4.24; -pragma experimental ABIEncoderV2; - -import "../utils/LibBytes/LibBytes.sol"; -import "../protocol/Exchange/libs/LibFillResults.sol"; -import "../protocol/Exchange/libs/LibMath.sol"; -import "../protocol/Exchange/libs/LibOrder.sol"; -import "./mixins/MConstants.sol"; -import "./mixins/MExpectedResults.sol"; - - -contract MixinExpectedResults is - LibMath, - LibFillResults, - MConstants, - MExpectedResults -{ - - /// @dev Calculates a total FillResults for buying makerAssetFillAmount over all orders. - /// Including the fees required to be paid. - /// @param orders An array of Order struct containing order specifications. - /// @param makerAssetFillAmount A number representing the amount of this order to fill. - /// @return totalFillResults Amounts filled and fees paid by maker and taker. - function calculateMarketBuyResults( - LibOrder.Order[] memory orders, - uint256 makerAssetFillAmount - ) - public - view - returns (FillResults memory totalFillResults) - { - for (uint256 i = 0; i < orders.length; i++) { - uint256 remainingMakerAssetFillAmount = safeSub(makerAssetFillAmount, totalFillResults.makerAssetFilledAmount); - uint256 remainingTakerAssetFillAmount = getPartialAmount( - orders[i].takerAssetAmount, - orders[i].makerAssetAmount, - remainingMakerAssetFillAmount - ); - FillResults memory singleFillResult = calculateFillResults(orders[i], remainingTakerAssetFillAmount); - addFillResults(totalFillResults, singleFillResult); - if (totalFillResults.makerAssetFilledAmount == makerAssetFillAmount) { - break; - } - } - return totalFillResults; - } - - /// @dev Calculates a FillResults total for selling takerAssetFillAmount over all orders. - /// Including the fees required to be paid. - /// @param orders An array of Order struct containing order specifications. - /// @param takerAssetFillAmount A number representing the amount of this order to fill. - /// @return totalFillResults Amounts filled and fees paid by maker and taker. - function calculateMarketSellResults( - LibOrder.Order[] memory orders, - uint256 takerAssetFillAmount - ) - public - view - returns (FillResults memory totalFillResults) - { - for (uint256 i = 0; i < orders.length; i++) { - uint256 remainingTakerAssetFillAmount = safeSub(takerAssetFillAmount, totalFillResults.takerAssetFilledAmount); - FillResults memory singleFillResult = calculateFillResults(orders[i], remainingTakerAssetFillAmount); - addFillResults(totalFillResults, singleFillResult); - if (totalFillResults.takerAssetFilledAmount == takerAssetFillAmount) { - break; - } - } - return totalFillResults; - } - - /// @dev Calculates fill results for buyFeeTokens. This handles fees on buying ZRX - /// so the end result is the expected amount of ZRX (not less after fees). - /// @param orders An array of Order struct containing order specifications. - /// @param zrxFillAmount A number representing the amount zrx to buy - /// @return totalFillResults Expected fill result amounts from buying fees - function calculateMarketBuyZrxResults( - LibOrder.Order[] memory orders, - uint256 zrxFillAmount - ) - public - view - returns (FillResults memory totalFillResults) - { - for (uint256 i = 0; i < orders.length; i++) { - uint256 remainingZrxFillAmount = safeSub(zrxFillAmount, totalFillResults.makerAssetFilledAmount); - // Convert the remaining amount of makerToken to buy into remaining amount - // of takerToken to sell, assuming entire amount can be sold in the current order - uint256 remainingWethSellAmount = getPartialAmount( - orders[i].takerAssetAmount, - safeSub(orders[i].makerAssetAmount, orders[i].takerFee), // our exchange rate after fees - remainingZrxFillAmount - ); - FillResults memory singleFillResult = calculateFillResults(orders[i], safeAdd(remainingWethSellAmount, 1)); - - singleFillResult.makerAssetFilledAmount = safeSub(singleFillResult.makerAssetFilledAmount, singleFillResult.takerFeePaid); - addFillResults(totalFillResults, singleFillResult); - // As we compensate for the rounding issue above have slightly more ZRX than the requested zrxFillAmount - if (totalFillResults.makerAssetFilledAmount >= zrxFillAmount) { - break; - } - } - return totalFillResults; - } - - /// @dev Simulates the 0x Exchange fillOrder validation and calculations, without performing any state changes. - /// @param order An Order struct containing order specifications. - /// @param takerAssetFillAmount A number representing the amount of this order to fill. - /// @return fillResults Amounts filled and fees paid by maker and taker. - function calculateFillResults( - LibOrder.Order memory order, - uint256 takerAssetFillAmount - ) - internal - view - returns (FillResults memory fillResults) - { - LibOrder.OrderInfo memory orderInfo = EXCHANGE.getOrderInfo(order); - if (orderInfo.orderStatus != uint8(LibOrder.OrderStatus.FILLABLE)) { - return fillResults; - } - uint256 remainingTakerAssetAmount = safeSub(order.takerAssetAmount, orderInfo.orderTakerAssetFilledAmount); - uint256 takerAssetFilledAmount = min256(takerAssetFillAmount, remainingTakerAssetAmount); - - fillResults.takerAssetFilledAmount = takerAssetFilledAmount; - fillResults.makerAssetFilledAmount = getPartialAmount( - takerAssetFilledAmount, - order.takerAssetAmount, - order.makerAssetAmount - ); - fillResults.makerFeePaid = getPartialAmount( - takerAssetFilledAmount, - order.takerAssetAmount, - order.makerFee - ); - fillResults.takerFeePaid = getPartialAmount( - takerAssetFilledAmount, - order.takerAssetAmount, - order.takerFee - ); - return fillResults; - } -} diff --git a/packages/contracts/src/2.0.0/forwarder/MixinFees.sol b/packages/contracts/src/2.0.0/forwarder/MixinFees.sol deleted file mode 100644 index 8ea00a1d5..000000000 --- a/packages/contracts/src/2.0.0/forwarder/MixinFees.sol +++ /dev/null @@ -1,126 +0,0 @@ -/* - - Copyright 2018 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity 0.4.24; - -import "../protocol/Exchange/libs/LibMath.sol"; -import "./mixins/MConstants.sol"; -import "./mixins/MFees.sol"; - - -contract MixinFees is - LibMath, - MConstants, - MFees -{ - - uint16 constant public PERCENTAGE_DENOMINATOR = 10000; // 9800 == 98%, 10000 == 100% - uint16 constant public MAX_FEE = 1000; // 10% - uint16 constant public ALLOWABLE_EXCHANGE_PERCENTAGE = 9500; // 95% - - /// @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 Pays the feeRecipient feeProportion of the total takerEthAmount, denominated in ETH - /// @param takerEthAmount The total amount that was transacted in WETH, fees are calculated from this value. - /// @param feeProportion The proportion of fees - /// @param feeRecipient The recipient of the fees - /// @return ethFeeAmount Amount of ETH paid to feeRecipient as fee. - function payEthFee( - uint256 takerEthAmount, - uint16 feeProportion, - address feeRecipient - ) - internal - returns (uint256 ethFeeAmount) - { - if (feeProportion > 0 && feeRecipient != address(0)) { - require( - feeProportion <= MAX_FEE, - "FEE_PROPORTION_TOO_LARGE" - ); - // 1.5% is 150, allowing for 2 decimal precision, i.e 0.05% is 5 - ethFeeAmount = getPartialAmount( - feeProportion, - PERCENTAGE_DENOMINATOR, - takerEthAmount - ); - feeRecipient.transfer(ethFeeAmount); - } - return ethFeeAmount; - } - - /// @dev Withdraws the remaining WETH, deduct and pay fees from this amount based on the takerTokenAmount to the feeRecipient. - /// If a user overpaid ETH initially, the fees are calculated from the amount traded and deducted from withdrawAmount. - /// Any remaining ETH is sent back to the user. - /// @param ethWithdrawAmount The amount to withdraw from the WETH contract. - /// @param wethAmountSold The total amount that was transacted in WETH, fees are calculated from this value. - /// @param feeProportion The proportion of fees - /// @param feeRecipient The recipient of the fees - function withdrawPayAndDeductEthFee( - uint256 ethWithdrawAmount, - uint256 wethAmountSold, - uint16 feeProportion, - address feeRecipient - ) - internal - { - // Return all of the excess WETH if any after deducting fees on the amount - if (ethWithdrawAmount > 0) { - ETHER_TOKEN.withdraw(ethWithdrawAmount); - // Fees proportional to the amount traded - uint256 ethFeeAmount = payEthFee( - wethAmountSold, - feeProportion, - feeRecipient - ); - uint256 unspentEthAmount = safeSub(ethWithdrawAmount, ethFeeAmount); - if (unspentEthAmount > 0) { - msg.sender.transfer(unspentEthAmount); - } - } - } - - /// @dev Checks whether the amount of tokens sold against the amount of tokens requested - /// is within a certain threshold. This ensures the caller gets a fair deal when - /// performing any token fee abstraction. Threshold is 95%. If fee abstraction costs more than - /// 5% of the total transaction, we return false. - /// @param requestedSellAmount The amount the user requested, or sent in to a payable function - /// @param tokenAmountSold The amount of the token that was sold after fee abstraction - /// @return bool of whether this is within an acceptable threshold - function isAcceptableThreshold(uint256 requestedSellAmount, uint256 tokenAmountSold) - internal - pure - returns (bool) - { - uint256 acceptableSellAmount = getPartialAmount( - ALLOWABLE_EXCHANGE_PERCENTAGE, - PERCENTAGE_DENOMINATOR, - requestedSellAmount - ); - return tokenAmountSold >= acceptableSellAmount; - } -} diff --git a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol index cb0ed5422..1a0687ab5 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol @@ -19,30 +19,25 @@ pragma solidity 0.4.24; pragma experimental ABIEncoderV2; -import "../utils/LibBytes/LibBytes.sol"; -import "./mixins/MFees.sol"; -import "./mixins/MMarketBuyZrx.sol"; -import "./mixins/MExpectedResults.sol"; -import "./mixins/MTransfer.sol"; +import "./mixins/MWeth.sol"; +import "./mixins/MAssets.sol"; import "./mixins/MConstants.sol"; import "./mixins/MForwarderCore.sol"; import "../protocol/Exchange/libs/LibOrder.sol"; import "../protocol/Exchange/libs/LibFillResults.sol"; +import "../protocol/Exchange/libs/LibMath.sol"; contract MixinForwarderCore is LibFillResults, + LibMath, MConstants, - MExpectedResults, - MFees, - MMarketBuyZrx, - MTransfer, + MWeth, + MAssets, MForwarderCore { - 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; + /// @dev Constructor approves ERC20 proxy to transfer ZRX and WETH on this contract's behalf. constructor () public { @@ -53,377 +48,257 @@ contract MixinForwarderCore is } } - /// @dev Market sells ETH for ERC20 tokens, performing fee abstraction if required. This does not support ERC721 tokens. This function is payable - /// and will convert all incoming ETH into WETH and perform the trade on behalf of the caller. - /// This function allows for a deduction of a proportion of incoming ETH sent to the feeRecipient. - /// The caller is sent all tokens from the operation. - /// If the purchased token amount does not meet an acceptable threshold then this function reverts. - /// @param orders An array of Order struct containing order specifications. - /// @param signatures An array of Proof that order has been created by maker. - /// @param feeOrders An array of Order struct containing order specifications for fees. - /// @param feeSignatures An array of Proof that order has been created by maker for the fee orders. - /// @param feeProportion A proportion deducted off the incoming ETH and sent to feeRecipient. The maximum value for this - /// is 1000, aka 10%. Supports up to 2 decimal places. I.e 0.59% is 59. - /// @param feeRecipient An address of the fee recipient whom receives feeProportion of ETH. - /// @return FillResults amounts filled and fees paid by maker and taker. - function marketSellEthForERC20( + /// @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, - uint16 feeProportion, + uint256 feePercentage, address feeRecipient ) public payable - returns (FillResults memory totalFillResults) + returns ( + FillResults memory orderFillResults, + FillResults memory feeOrderFillResults + ) { - uint256 takerEthAmount = msg.value; - require( - takerEthAmount > 0, - "VALUE_GREATER_THAN_ZERO" - ); - // Deduct the fee from the total amount of ETH sent in - uint256 ethFeeAmount = payEthFee( - takerEthAmount, - feeProportion, - feeRecipient + // Convert ETH to WETH. + // 5% of WETH is reserved for filling feeOrders and paying feeRecipient. + uint256 wethAvailable = convertEthToWeth(); + + // Attempt to sell 95% of WETH. + // ZRX fees are payed with this contract's balance. + marketSellEth( + orders, + wethAvailable, + signatures ); - uint256 wethSellAmount = safeSub(takerEthAmount, ethFeeAmount); - // Deposit the remaining to be used for trading - ETHER_TOKEN.deposit.value(wethSellAmount)(); - // Populate the known assetData, as it is always WETH the caller can provide null bytes to save gas - // marketSellOrders fills the remaining - address makerTokenAddress = LibBytes.readAddress(orders[0].makerAssetData, 16); - orders[0].takerAssetData = WETH_ASSET_DATA; - if (makerTokenAddress == address(ZRX_TOKEN)) { - // If this is ZRX then we market sell from the orders, rather than a 2 step of buying ZRX fees from feeOrders - // then buying ZRX from orders - totalFillResults = marketSellEthForZRXInternal( - orders, - signatures, - wethSellAmount - ); - } else { - totalFillResults = marketSellEthForERC20Internal( - orders, - signatures, - feeOrders, - feeSignatures, - wethSellAmount - ); - } - // Prevent accidental WETH owned by this contract and it being spent - require( - takerEthAmount >= totalFillResults.takerAssetFilledAmount, - "INVALID_MSG_VALUE" + // Buy back all ZRX spent on fees. + feeOrderFillResults = marketBuyZrx( + feeOrders, + orderFillResults.takerFeePaid, + feeSignatures ); - // Ensure no WETH is left in this contract + + // Ensure that no extra WETH owned by this contract has been sold. + uint256 totalWethSold = safeAdd(orderFillResults.takerAssetFilledAmount, feeOrderFillResults.takerAssetFilledAmount); require( - wethSellAmount == totalFillResults.takerAssetFilledAmount, - "UNACCEPTABLE_THRESHOLD" + totalWethSold <= msg.value, + "OVERSOLD_WETH" ); - // Transfer all tokens to msg.sender - transferERC20Token( - makerTokenAddress, - msg.sender, - totalFillResults.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 ); - return totalFillResults; + + // Transfer purchased assets to msg.sender. + transferPurchasedAssetToSender(orders[0].makerAssetData, orderFillResults.makerAssetFilledAmount); } - /// @dev Buys the exact amount of assets (ERC20 and ERC721), performing fee abstraction if required. - /// All order assets must be of the same type. Deducts a proportional fee to fee recipient. - /// This function is payable and will convert all incoming ETH into WETH and perform the trade on behalf of the caller. - /// The caller is sent all assets from the fill of orders. This function will revert unless the requested amount of assets are purchased. - /// Any excess ETH sent will be returned to the caller - /// @param orders An array of Order struct containing order specifications. - /// @param signatures An array of Proof that order has been created by maker. - /// @param feeOrders An array of Order struct containing order specifications for fees. - /// @param makerTokenFillAmount The amount of maker asset to buy. - /// @param feeSignatures An array of Proof that order has been created by maker for the fee orders. - /// @param feeProportion A proportion deducted off the ETH spent and sent to feeRecipient. The maximum value for this - /// is 1000, aka 10%. Supports up to 2 decimal places. I.e 0.59% is 59. - /// @param feeRecipient An address of the fee recipient whom receives feeProportion of ETH. - /// @return FillResults amounts filled and fees paid by maker and taker. - function marketBuyTokensWithEth( + /// @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 makerTokenFillAmount, - uint16 feeProportion, + uint256 feePercentage, address feeRecipient ) public payable - returns (FillResults memory totalFillResults) + returns ( + FillResults memory orderFillResults, + FillResults memory feeOrderFillResults + ) { - uint256 takerEthAmount = msg.value; - require( - takerEthAmount > 0, - "VALUE_GREATER_THAN_ZERO" - ); - require( - makerTokenFillAmount > 0, - "VALUE_GREATER_THAN_ZERO" + // Convert ETH to WETH. + convertEthToWeth(); + + // Attemp to purchase desired amount of makerAsset. + // ZRX fees are payed with this contract's balance. + orderFillResults = marketBuyAsset( + orders, + makerAssetFillAmount, + signatures ); - bytes4 assetDataId = LibBytes.readBytes4(orders[0].makerAssetData, 0); - require( - assetDataId == ERC20_DATA_ID || assetDataId == ERC721_DATA_ID, - "UNSUPPORTED_TOKEN_PROXY" + + // Buy back all ZRX spent on fees. + feeOrderFillResults = marketBuyZrx( + feeOrders, + orderFillResults.takerFeePaid, + feeSignatures ); - ETHER_TOKEN.deposit.value(takerEthAmount)(); - if (assetDataId == ERC20_DATA_ID) { - totalFillResults = marketBuyERC20TokensInternal( - orders, - signatures, - feeOrders, - feeSignatures, - makerTokenFillAmount - ); - } else if (assetDataId == ERC721_DATA_ID) { - totalFillResults = batchBuyERC721TokensInternal( - orders, - signatures, - feeOrders, - feeSignatures - ); - } - // Prevent accidental WETH owned by this contract and it being spent + // Ensure that no extra WETH owned by this contract has been sold. + uint256 totalWethSold = safeAdd(orderFillResults.takerAssetFilledAmount, feeOrderFillResults.takerAssetFilledAmount); require( - takerEthAmount >= totalFillResults.takerAssetFilledAmount, - "INVALID_MSG_VALUE" + totalWethSold <= msg.value, + "OVERSOLD_WETH" ); - withdrawPayAndDeductEthFee( - safeSub(takerEthAmount, totalFillResults.takerAssetFilledAmount), - totalFillResults.takerAssetFilledAmount, - feeProportion, + + // Transfer feePercentage of total ETH spent on primary orders to feeRecipient. + // Refund remaining ETH to msg.sender. + transferEthFeeAndRefund( + orderFillResults.takerAssetFilledAmount, + feeOrderFillResults.takerAssetFilledAmount, + feePercentage, feeRecipient ); - return totalFillResults; + + // Transfer purchased assets to msg.sender. + transferPurchasedAssetToSender(orders[0].makerAssetData, orderFillResults.makerAssetFilledAmount); } - /// @dev Market sells WETH for ERC20 tokens. - /// @param orders An array of Order struct containing order specifications. - /// @param signatures An array of Proof that order has been created by maker. - /// @param feeOrders An array of Order struct containing order specifications for fees. - /// @param feeSignatures An array of Proof that order has been created by maker for the fee orders. - /// @param wethSellAmount The amount of WETH to sell. - /// @return FillResults amounts filled and fees paid by maker and taker. - function marketSellEthForERC20Internal( + /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. + /// @param wethSellAmount Desired amount of WETH to sell. + /// @param signatures Proofs that orders have been created by makers. + /// @return Amounts filled and fees paid by maker and taker. + function marketSellEth( LibOrder.Order[] memory orders, - bytes[] memory signatures, - LibOrder.Order[] memory feeOrders, - bytes[] memory feeSignatures, - uint256 wethSellAmount + uint256 wethSellAmount, + bytes[] memory signatures ) internal - returns (FillResults memory totalFillResults) + returns (FillResults memory fillResults) { - uint256 remainingWethSellAmount = wethSellAmount; - FillResults memory calculatedMarketSellResults = calculateMarketSellResults(orders, wethSellAmount); - if (calculatedMarketSellResults.takerFeePaid > 0) { - // Fees are required for these orders. Buy enough ZRX to cover the future market buy - FillResults memory feeTokensResults = marketBuyZrxInternal( - feeOrders, - feeSignatures, - calculatedMarketSellResults.takerFeePaid - ); - // Ensure the token abstraction was fair if fees were proportionally too high, we fail - require( - isAcceptableThreshold( - wethSellAmount, - safeSub(wethSellAmount, feeTokensResults.takerAssetFilledAmount) - ), - "UNACCEPTABLE_THRESHOLD" - ); - remainingWethSellAmount = safeSub(remainingWethSellAmount, feeTokensResults.takerAssetFilledAmount); - totalFillResults.takerFeePaid = feeTokensResults.takerFeePaid; - totalFillResults.takerAssetFilledAmount = feeTokensResults.takerAssetFilledAmount; + // `marketSellOrders` uses the first order's takerAssetData for all passed in orders. + orders[0].takerAssetData = WETH_ASSET_DATA; + + // All orders are required to have the same makerAssetData. We save on gas by reusing the makerAssetData of the first order. + for (uint256 i = 0; i < orders.length; i++) { + orders[i].makerAssetData = orders[0].makerAssetData; } - // Make our market sell to buy the requested tokens with the remaining balance - FillResults memory requestedTokensResults = EXCHANGE.marketSellOrders( + + // Sell WETH until entire amount has been sold or all orders have been filled. + fillResults = EXCHANGE.marketSellOrdersNoThrow( orders, - remainingWethSellAmount, + wethSellAmount, signatures ); - // Update our return FillResult with the market sell - addFillResults(totalFillResults, requestedTokensResults); - return totalFillResults; + + return fillResults; } - /// @dev Market sells WETH for ZRX tokens. - /// @param orders An array of Order struct containing order specifications. - /// @param signatures An array of Proof that order has been created by maker. - /// @param wethSellAmount The amount of WETH to sell. - /// @return FillResults amounts filled and fees paid by maker and taker. - function marketSellEthForZRXInternal( + /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. + /// @param makerAssetFillAmount Desired amount of makerAsset to buy. + /// @param signatures Proofs that orders have been created by makers. + /// @return Amounts filled and fees paid by maker and taker. + function marketBuyAsset( LibOrder.Order[] memory orders, - bytes[] memory signatures, - uint256 wethSellAmount + uint256 makerAssetFillAmount, + bytes[] memory signatures ) internal - returns (FillResults memory totalFillResults) + returns (FillResults memory fillResults) { - // Make our market sell to buy the requested tokens with the remaining balance - totalFillResults = EXCHANGE.marketSellOrders( + bytes memory wethAssetData = WETH_ASSET_DATA; + + // All orders are required to have WETH as takerAssetData. We save on gas by populating the orders here, rather than passing in any extra calldata. + for (uint256 i = 0; i < orders.length; i++) { + orders[i].takerAssetData = wethAssetData; + } + + // Purchase makerAsset until entire amount has been bought or all orders have been filled. + fillResults = EXCHANGE.marketBuyOrdersNoThrow( orders, - wethSellAmount, + makerAssetFillAmount, signatures ); - // Exchange does not special case ZRX in the makerAssetFilledAmount, if fees were deducted then using this amount - // for future transfers is invalid. - uint256 zrxAmountBought = safeSub(totalFillResults.makerAssetFilledAmount, totalFillResults.takerFeePaid); - require( - isAcceptableThreshold(totalFillResults.makerAssetFilledAmount, zrxAmountBought), - "UNACCEPTABLE_THRESHOLD" - ); - totalFillResults.makerAssetFilledAmount = zrxAmountBought; - return totalFillResults; + + return fillResults; } - /// @dev Buys an exact amount of an ERC20 token using WETH. - /// @param orders Orders to fill. The maker asset is the ERC20 token to buy. The taker asset is WETH. - /// @param signatures Proof that the orders were created by their respective makers. - /// @param feeOrders to fill. The maker asset is ZRX and the taker asset is WETH. - /// @param feeSignatures Proof that the feeOrders were created by their respective makers. - /// @param makerTokenFillAmount Amount of the ERC20 token to buy. - /// @return totalFillResults Aggregated fill results of buying the ERC20 and ZRX tokens. - function marketBuyERC20TokensInternal( + /// @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. + /// @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 marketBuyZrx( LibOrder.Order[] memory orders, - bytes[] memory signatures, - LibOrder.Order[] memory feeOrders, - bytes[] memory feeSignatures, - uint256 makerTokenFillAmount + uint256 zrxBuyAmount, + bytes[] memory signatures ) internal - returns (LibFillResults.FillResults memory totalFillResults) + returns (FillResults memory totalFillResults) { - // We read the maker token address to check if it is ZRX and later use it for transfer - address makerTokenAddress = LibBytes.readAddress(orders[0].makerAssetData, 16); - // We assume that asset being bought by taker is the same for each order. - // Rather than passing this in as calldata, we copy the makerAssetData from the first order onto all later orders. - orders[0].takerAssetData = WETH_ASSET_DATA; - // We can short cut here for effeciency and use buyFeeTokensInternal if maker asset token is ZRX - // this buys us exactly that amount taking into account the fees. This saves gas and calculates the rate correctly - FillResults memory marketBuyResults; - if (makerTokenAddress == address(ZRX_TOKEN)) { - marketBuyResults = marketBuyZrxInternal( - orders, - signatures, - makerTokenFillAmount - ); - // When buying ZRX we round up which can result in a small margin excess - require( - marketBuyResults.makerAssetFilledAmount >= makerTokenFillAmount, - "UNACCEPTABLE_THRESHOLD" - ); - addFillResults(totalFillResults, marketBuyResults); - require( - isAcceptableThreshold( - safeAdd(totalFillResults.makerAssetFilledAmount, totalFillResults.takerFeePaid), // Total ZRX - totalFillResults.makerAssetFilledAmount // amount going to msg.sender - ), - "UNACCEPTABLE_THRESHOLD" - ); - } else { - FillResults memory calculatedMarketBuyResults = calculateMarketBuyResults(orders, makerTokenFillAmount); - if (calculatedMarketBuyResults.takerFeePaid > 0) { - // Fees are required for these orders. Buy enough ZRX to cover the future market buy - FillResults memory zrxMarketBuyResults = marketBuyZrxInternal( - feeOrders, - feeSignatures, - calculatedMarketBuyResults.takerFeePaid + // Do nothing if zrxBuyAmount == 0 + if (zrxBuyAmount > 0) { + + bytes memory zrxAssetData = ZRX_ASSET_DATA; + bytes memory wethAssetData = WETH_ASSET_DATA; + + for (uint256 i = 0; i < orders.length; 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 = safeAdd( + safeSub(zrxBuyAmount, totalFillResults.makerAssetFilledAmount), + totalFillResults.takerFeePaid + ); + + // Convert the remaining amount of ZRX to buy into remaining amount + // of WETH to sell, assuming entire amount can be sold in the current order. + uint256 remainingWethSellAmount = getPartialAmount( + orders[i].takerAssetAmount, + safeSub(orders[i].makerAssetAmount, orders[i].takerFee), // our exchange rate after fees + remainingZrxBuyAmount + ); + + // Attempt to sell the remaining amount of WETH. + FillResults memory singleFillResult = EXCHANGE.fillOrderNoThrow( + orders[i], + remainingWethSellAmount, + signatures[i] ); - totalFillResults.takerAssetFilledAmount = zrxMarketBuyResults.takerAssetFilledAmount; - totalFillResults.takerFeePaid = zrxMarketBuyResults.takerFeePaid; + + // Update amounts filled and fees paid by maker and taker. + addFillResults(totalFillResults, singleFillResult); + + // Stop execution if the entire amount of ZRX has been bought. + if (totalFillResults.makerAssetFilledAmount == zrxBuyAmount) { + break; + } } - // Make our market buy of the requested tokens with the remaining balance - marketBuyResults = EXCHANGE.marketBuyOrders( - orders, - makerTokenFillAmount, - signatures - ); - require( - marketBuyResults.makerAssetFilledAmount == makerTokenFillAmount, - "UNACCEPTABLE_THRESHOLD" - ); - addFillResults(totalFillResults, marketBuyResults); - require( - isAcceptableThreshold( - totalFillResults.takerAssetFilledAmount, - marketBuyResults.takerAssetFilledAmount - ), - "UNACCEPTABLE_THRESHOLD" - ); - } - // Transfer all purchased tokens to msg.sender - transferERC20Token( - makerTokenAddress, - msg.sender, - marketBuyResults.makerAssetFilledAmount - ); - return totalFillResults; - } - /// @dev Buys an all of the ERC721 tokens in the orders. - /// @param orders Orders to fill. The maker asset is the ERC721 token to buy. The taker asset is WETH. - /// @param signatures Proof that the orders were created by their respective makers. - /// @param feeOrders to fill. The maker asset is ZRX and the taker asset is WETH. - /// @param feeSignatures Proof that the feeOrders were created by their respective makers. - /// @return totalFillResults Aggregated fill results of buying the ERC721 tokens and ZRX tokens. - function batchBuyERC721TokensInternal( - LibOrder.Order[] memory orders, - bytes[] memory signatures, - LibOrder.Order[] memory feeOrders, - bytes[] memory feeSignatures - ) - internal - returns (LibFillResults.FillResults memory totalFillResults) - { - uint256 totalZrxFeeAmount; - uint256 ordersLength = orders.length; - uint256[] memory takerAssetFillAmounts = new uint256[](ordersLength); - for (uint256 i = 0; i < ordersLength; i++) { - // Total up the fees - totalZrxFeeAmount = safeAdd(totalZrxFeeAmount, orders[i].takerFee); - // We assume that asset being bought by taker is the same for each order. - // Rather than passing this in as calldata, we set the takerAssetData as WETH asset data - orders[i].takerAssetData = WETH_ASSET_DATA; - // Populate takerAssetFillAmounts for later batchFill - takerAssetFillAmounts[i] = orders[i].takerAssetAmount; - } - if (totalZrxFeeAmount > 0) { - // Fees are required for these orders. Buy enough ZRX to cover the future fill - FillResults memory zrxMarketBuyResults = marketBuyZrxInternal( - feeOrders, - feeSignatures, - totalZrxFeeAmount - ); - totalFillResults.takerFeePaid = zrxMarketBuyResults.takerFeePaid; - totalFillResults.takerAssetFilledAmount = zrxMarketBuyResults.takerAssetFilledAmount; - } - FillResults memory batchFillResults = EXCHANGE.batchFillOrKillOrders( - orders, - takerAssetFillAmounts, - signatures - ); - addFillResults(totalFillResults, batchFillResults); - require( - isAcceptableThreshold( - totalFillResults.takerAssetFilledAmount, - batchFillResults.takerAssetFilledAmount - ), - "UNACCEPTABLE_THRESHOLD" - ); - // Transfer all of the tokens filled from the batchFill - for (i = 0; i < ordersLength; i++) { - transferERC721Token( - orders[i].makerAssetData, - msg.sender + // Ensure that all ZRX spent while filling primary orders has been repurchased. + require( + totalFillResults.makerAssetFilledAmount == zrxBuyAmount, + "COMPLETE_FILL_FAILED" ); } return totalFillResults; diff --git a/packages/contracts/src/2.0.0/forwarder/MixinMarketBuyZrx.sol b/packages/contracts/src/2.0.0/forwarder/MixinMarketBuyZrx.sol deleted file mode 100644 index e272f8aad..000000000 --- a/packages/contracts/src/2.0.0/forwarder/MixinMarketBuyZrx.sol +++ /dev/null @@ -1,83 +0,0 @@ -/* - - Copyright 2018 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity 0.4.24; -pragma experimental ABIEncoderV2; - -import "../protocol/Exchange/libs/LibFillResults.sol"; -import "../protocol/Exchange/libs/LibOrder.sol"; -import "../protocol/Exchange/libs/LibMath.sol"; -import "./mixins/MConstants.sol"; -import "./mixins/MMarketBuyZrx.sol"; - - -contract MixinMarketBuyZrx is - LibMath, - LibFillResults, - MConstants, - MMarketBuyZrx -{ - - /// @dev Buys zrxBuyAmount of ZRX fee tokens, taking into account the fees on buying fee tokens. This will guarantee - /// At least zrxBuyAmount of ZRX fee tokens are purchased (sometimes slightly over due to rounding issues). - /// It is possible that a request to buy 200 ZRX fee tokens will require purchasing 202 ZRX tokens - /// As 2 ZRX is required to purchase the 200 ZRX fee tokens. This guarantees at least 200 ZRX for future purchases. - /// @param orders An array of Order struct containing order specifications for fees. - /// @param signatures An array of Proof that order has been created by maker for the fee orders. - /// @param zrxBuyAmount The number of requested ZRX fee tokens. - /// @return totalFillResults Amounts filled and fees paid by maker and taker. makerTokenAmount is the zrx amount deducted of fees - function marketBuyZrxInternal( - LibOrder.Order[] memory orders, - bytes[] memory signatures, - uint256 zrxBuyAmount - ) - internal - returns (FillResults memory totalFillResults) - { - for (uint256 i = 0; i < orders.length; i++) { - // All of these are ZRX/WETH, we can drop the respective assetData from callData - orders[i].makerAssetData = ZRX_ASSET_DATA; - orders[i].takerAssetData = WETH_ASSET_DATA; - // Calculate the remaining amount of makerToken to buy - uint256 remainingZrxBuyAmount = safeSub(zrxBuyAmount, totalFillResults.makerAssetFilledAmount); - // Convert the remaining amount of makerToken to buy into remaining amount - // of takerToken to sell, assuming entire amount can be sold in the current order - uint256 remainingWethSellAmount = getPartialAmount( - orders[i].takerAssetAmount, - safeSub(orders[i].makerAssetAmount, orders[i].takerFee), // our exchange rate after fees - remainingZrxBuyAmount - ); - // Attempt to sell the remaining amount of takerToken - // Round up the amount to ensure we don't under buy by a fractional amount - FillResults memory singleFillResult = EXCHANGE.fillOrder( - orders[i], - safeAdd(remainingWethSellAmount, 1), - signatures[i] - ); - // We didn't buy the full amount when buying ZRX as some were taken for fees - singleFillResult.makerAssetFilledAmount = safeSub(singleFillResult.makerAssetFilledAmount, singleFillResult.takerFeePaid); - // Update amounts filled and fees paid by maker and taker - addFillResults(totalFillResults, singleFillResult); - // Stop execution if the entire amount of makerToken has been bought - if (totalFillResults.makerAssetFilledAmount >= zrxBuyAmount) { - break; - } - } - return totalFillResults; - } -} diff --git a/packages/contracts/src/2.0.0/forwarder/MixinTransfer.sol b/packages/contracts/src/2.0.0/forwarder/MixinTransfer.sol deleted file mode 100644 index bebfc976b..000000000 --- a/packages/contracts/src/2.0.0/forwarder/MixinTransfer.sol +++ /dev/null @@ -1,118 +0,0 @@ -/* - - Copyright 2018 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity 0.4.24; - -import "../utils/LibBytes/LibBytes.sol"; -import "../tokens/ERC721Token/IERC721Token.sol"; -import "./mixins/MTransfer.sol"; - - -contract MixinTransfer is - MTransfer -{ - - using LibBytes for bytes; - - bytes4 constant internal ERC20_TRANSFER_SELECTOR = bytes4(keccak256("transfer(address,uint256)")); - bytes4 constant internal ERC721_RECEIVED = bytes4(keccak256("onERC721Received(address,uint256,bytes)")); - bytes4 constant internal ERC721_RECEIVED_OPERATOR = bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")); - - function onERC721Received( - address, - uint256, - bytes memory - ) - public - pure - returns(bytes4) - { - return ERC721_RECEIVED; - } - - function onERC721Received( - address, - address, - uint256, - bytes memory - ) - public - pure - returns(bytes4) - { - return ERC721_RECEIVED_OPERATOR; - } - - function transferERC20Token( - address token, - address to, - uint256 amount - ) - internal - { - // 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, - to, - 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" - ); - } - - function transferERC721Token( - bytes memory assetData, - address to - ) - internal - { - // Decode asset data. - address token = assetData.readAddress(16); - uint256 tokenId = assetData.readUint256(36); - IERC721Token(token).transferFrom( - address(this), - to, - tokenId - ); - } -} diff --git a/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol b/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol new file mode 100644 index 000000000..cbc81a11c --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol @@ -0,0 +1,109 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; + +import "../protocol/Exchange/libs/LibMath.sol"; +import "./mixins/MConstants.sol"; +import "./mixins/MWeth.sol"; + + +contract MixinWeth is + LibMath, + MConstants, + MWeth +{ + + uint256 constant internal PERCENTAGE_DENOMINATOR = 10000; // 9800 == 98%, 10000 == 100% + uint256 constant internal MAX_FEE_PERCENTAGE = 500; // 5% + uint256 constant internal MAX_WETH_FILL_PERCENTAGE = 9500; // 95% + + /// @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. + /// @return 95% of ETH converted to WETH. + function convertEthToWeth() + internal + returns (uint256 wethAvailable) + { + require( + msg.value > 0, + "INVALID_MSG_VALUE" + ); + + ETHER_TOKEN.deposit.value(msg.value)(); + wethAvailable = getPartialAmount( + MAX_WETH_FILL_PERCENTAGE, + PERCENTAGE_DENOMINATOR, + msg.value + ); + return wethAvailable; + } + + /// @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 + { + uint256 wethRemaining = safeSub( + msg.value, + safeAdd(wethSoldExcludingFeeOrders, wethSoldForZrx) + ); + ETHER_TOKEN.withdraw(wethRemaining); + + require( + feePercentage <= MAX_FEE_PERCENTAGE, + "FEE_PERCENTAGE_TOO_LARGE" + ); + uint256 ethFee = getPartialAmount( + feePercentage, + PERCENTAGE_DENOMINATOR, + wethSoldExcludingFeeOrders + ); + require( + ethFee < wethRemaining, + "MAX_FEE_EXCEEDED" + ); + if (ethFee > 0) { + feeRecipient.transfer(ethFee); + } + + uint256 ethRefund = safeSub(wethRemaining, ethFee); + if (ethRefund > 0) { + msg.sender.transfer(ethRefund); + } + } +} diff --git a/packages/contracts/src/2.0.0/forwarder/interfaces/IAssets.sol b/packages/contracts/src/2.0.0/forwarder/interfaces/IAssets.sol new file mode 100644 index 000000000..27adb1221 --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/interfaces/IAssets.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; + + +contract IAssets { + + /// @dev Withdraws ERC20 tokens 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 tokens that were accidentally sent to this contract. + /// @param token Address of ERC20 token to withdraw. + /// @param amount Amount of ERC20 token to withdraw. + function withdrawERC20( + address token, + uint256 amount + ) + external; + + function onERC721Received( + address, + uint256, + bytes memory + ) + public + pure + returns(bytes4); + + function onERC721Received( + address, + address, + uint256, + bytes memory + ) + public + pure + returns(bytes4); +} \ No newline at end of file diff --git a/packages/contracts/src/2.0.0/forwarder/interfaces/IExpectedResults.sol b/packages/contracts/src/2.0.0/forwarder/interfaces/IExpectedResults.sol deleted file mode 100644 index 89187b750..000000000 --- a/packages/contracts/src/2.0.0/forwarder/interfaces/IExpectedResults.sol +++ /dev/null @@ -1,66 +0,0 @@ -/* - - Copyright 2018 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity 0.4.24; -pragma experimental ABIEncoderV2; - -import "../../protocol/Exchange/libs/LibFillResults.sol"; -import "../../protocol/Exchange/libs/LibOrder.sol"; - - -contract IExpectedResults { - - /// @dev Calculates a total FillResults for buying makerAssetFillAmount over all orders. - /// Including the fees required to be paid. - /// @param orders An array of Order struct containing order specifications. - /// @param makerAssetFillAmount A number representing the amount of this order to fill. - /// @return totalFillResults Amounts filled and fees paid by maker and taker. - function calculateMarketBuyResults( - LibOrder.Order[] memory orders, - uint256 makerAssetFillAmount - ) - public - view - returns (LibFillResults.FillResults memory totalFillResults); - - /// @dev Calculates a FillResults total for selling takerAssetFillAmount over all orders. - /// Including the fees required to be paid. - /// @param orders An array of Order struct containing order specifications. - /// @param takerAssetFillAmount A number representing the amount of this order to fill. - /// @return totalFillResults Amounts filled and fees paid by maker and taker. - function calculateMarketSellResults( - LibOrder.Order[] memory orders, - uint256 takerAssetFillAmount - ) - public - view - returns (LibFillResults.FillResults memory totalFillResults); - - /// @dev Calculates fill results for buyFeeTokens. This handles fees on buying ZRX - /// so the end result is the expected amount of ZRX (not less after fees). - /// @param orders An array of Order struct containing order specifications. - /// @param zrxFillAmount A number representing the amount zrx to buy - /// @return totalFillResults Expected fill result amounts from buying fees - function calculateMarketBuyZrxResults( - LibOrder.Order[] memory orders, - uint256 zrxFillAmount - ) - public - view - returns (LibFillResults.FillResults memory totalFillResults); -} diff --git a/packages/contracts/src/2.0.0/forwarder/interfaces/IForwarder.sol b/packages/contracts/src/2.0.0/forwarder/interfaces/IForwarder.sol index 745dd29a9..f5a26e2ba 100644 --- a/packages/contracts/src/2.0.0/forwarder/interfaces/IForwarder.sol +++ b/packages/contracts/src/2.0.0/forwarder/interfaces/IForwarder.sol @@ -20,11 +20,11 @@ pragma solidity 0.4.24; pragma experimental ABIEncoderV2; import "./IForwarderCore.sol"; -import "./IExpectedResults.sol"; +import "./IAssets.sol"; // solhint-disable no-empty-blocks contract IForwarder is IForwarderCore, - IExpectedResults + IAssets {} diff --git a/packages/contracts/src/2.0.0/forwarder/interfaces/IForwarderCore.sol b/packages/contracts/src/2.0.0/forwarder/interfaces/IForwarderCore.sol index 7ac2a8af3..3ecbb133b 100644 --- a/packages/contracts/src/2.0.0/forwarder/interfaces/IForwarderCore.sol +++ b/packages/contracts/src/2.0.0/forwarder/interfaces/IForwarderCore.sol @@ -25,55 +25,56 @@ import "../../protocol/Exchange/libs/LibFillResults.sol"; contract IForwarderCore { - /// @dev Market sells ETH for ERC20 tokens, performing fee abstraction if required. This does not support ERC721 tokens. This function is payable - /// and will convert all incoming ETH into WETH and perform the trade on behalf of the caller. - /// This function allows for a deduction of a proportion of incoming ETH sent to the feeRecipient. - /// The caller is sent all tokens from the operation. - /// If the purchased token amount does not meet an acceptable threshold then this function reverts. - /// @param orders An array of Order struct containing order specifications. - /// @param signatures An array of Proof that order has been created by maker. - /// @param feeOrders An array of Order struct containing order specifications for fees. - /// @param feeSignatures An array of Proof that order has been created by maker for the fee orders. - /// @param feeProportion A proportion deducted off the incoming ETH and sent to feeRecipient. The maximum value for this - /// is 1000, aka 10%. Supports up to 2 decimal places. I.e 0.59% is 59. - /// @param feeRecipient An address of the fee recipient whom receives feeProportion of ETH. - /// @return FillResults amounts filled and fees paid by maker and taker. - function marketSellEthForERC20( + /// @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, - uint16 feeProportion, + uint256 feePercentage, address feeRecipient ) public payable - returns (LibFillResults.FillResults memory totalFillResults); + returns ( + LibFillResults.FillResults memory orderFillResults, + LibFillResults.FillResults memory feeOrderFillResults + ); - /// @dev Buys the exact amount of assets (ERC20 and ERC721), performing fee abstraction if required. - /// All order assets must be of the same type. Deducts a proportional fee to fee recipient. - /// This function is payable and will convert all incoming ETH into WETH and perform the trade on behalf of the caller. - /// The caller is sent all assets from the fill of orders. This function will revert unless the requested amount of assets are purchased. - /// Any excess ETH sent will be returned to the caller - /// @param orders An array of Order struct containing order specifications. - /// @param signatures An array of Proof that order has been created by maker. - /// @param feeOrders An array of Order struct containing order specifications for fees. - /// @param makerTokenFillAmount The amount of maker asset to buy. - /// @param feeSignatures An array of Proof that order has been created by maker for the fee orders. - /// @param feeProportion A proportion deducted off the ETH spent and sent to feeRecipient. The maximum value for this - /// is 1000, aka 10%. Supports up to 2 decimal places. I.e 0.59% is 59. - /// @param feeRecipient An address of the fee recipient whom receives feeProportion of ETH. - /// @return FillResults amounts filled and fees paid by maker and taker. - function marketBuyTokensWithEth( + /// @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 makerTokenFillAmount, - uint16 feeProportion, + uint256 feePercentage, address feeRecipient ) public payable - returns (LibFillResults.FillResults memory totalFillResults); + returns ( + LibFillResults.FillResults memory orderFillResults, + LibFillResults.FillResults memory feeOrderFillResults + ); } diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MAssets.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MAssets.sol new file mode 100644 index 000000000..b69f7482d --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/mixins/MAssets.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; + +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 transferPurchasedAssetToSender( + 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. + function transferERC721Token(bytes memory assetData) + internal; +} diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MConstants.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MConstants.sol index 348bf169e..bbc4969d6 100644 --- a/packages/contracts/src/2.0.0/forwarder/mixins/MConstants.sol +++ b/packages/contracts/src/2.0.0/forwarder/mixins/MConstants.sol @@ -25,6 +25,10 @@ import "../../tokens/ERC20Token/IERC20Token.sol"; contract MConstants { + bytes4 constant internal ERC20_DATA_ID = bytes4(keccak256("ERC20Token(address)")); + bytes4 constant internal ERC721_DATA_ID = bytes4(keccak256("ERC721Token(address,uint256,bytes)")); + uint256 constant internal MAX_UINT = 2**256 - 1; + // solhint-disable var-name-mixedcase IExchange internal EXCHANGE; IEtherToken internal ETHER_TOKEN; diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MExpectedResults.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MExpectedResults.sol deleted file mode 100644 index cf03bb32e..000000000 --- a/packages/contracts/src/2.0.0/forwarder/mixins/MExpectedResults.sol +++ /dev/null @@ -1,42 +0,0 @@ -/* - - Copyright 2018 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity 0.4.24; -pragma experimental ABIEncoderV2; - -import "../../protocol/Exchange/libs/LibFillResults.sol"; -import "../../protocol/Exchange/libs/LibOrder.sol"; -import "../interfaces/IExpectedResults.sol"; - - -contract MExpectedResults is - IExpectedResults -{ - - /// @dev Simulates the 0x Exchange fillOrder validation and calculations, without performing any state changes. - /// @param order An Order struct containing order specifications. - /// @param takerAssetFillAmount A number representing the amount of this order to fill. - /// @return fillResults Amounts filled and fees paid by maker and taker. - function calculateFillResults( - LibOrder.Order memory order, - uint256 takerAssetFillAmount - ) - internal - view - returns (LibFillResults.FillResults memory fillResults); -} diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MFees.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MFees.sol deleted file mode 100644 index f332637ea..000000000 --- a/packages/contracts/src/2.0.0/forwarder/mixins/MFees.sol +++ /dev/null @@ -1,63 +0,0 @@ -/* - - Copyright 2018 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity 0.4.24; - - -contract MFees { - - /// @dev Pays the feeRecipient feeProportion of the total takerEthAmount, denominated in ETH - /// @param takerEthAmount The total amount that was transacted in WETH, fees are calculated from this value. - /// @param feeProportion The proportion of fees - /// @param feeRecipient The recipient of the fees - /// @return ethFeeAmount Amount of ETH paid to feeRecipient as fee. - function payEthFee( - uint256 takerEthAmount, - uint16 feeProportion, - address feeRecipient - ) - internal - returns (uint256 ethFeeAmount); - - /// @dev Withdraws the remaining WETH, deduct and pay fees from this amount based on the takerTokenAmount to the feeRecipient. - /// If a user overpaid ETH initially, the fees are calculated from the amount traded and deducted from withdrawAmount. - /// Any remaining ETH is sent back to the user. - /// @param ethWithdrawAmount The amount to withdraw from the WETH contract. - /// @param wethAmountSold The total amount that was transacted in WETH, fees are calculated from this value. - /// @param feeProportion The proportion of fees - /// @param feeRecipient The recipient of the fees - function withdrawPayAndDeductEthFee( - uint256 ethWithdrawAmount, - uint256 wethAmountSold, - uint16 feeProportion, - address feeRecipient - ) - internal; - - /// @dev Checks whether the amount of tokens sold against the amount of tokens requested - /// is within a certain threshold. This ensures the caller gets a fair deal when - /// performing any token fee abstraction. Threshold is 95%. If fee abstraction costs more than - /// 5% of the total transaction, we return false. - /// @param requestedSellAmount The amount the user requested, or sent in to a payable function - /// @param tokenAmountSold The amount of the token that was sold after fee abstraction - /// @return bool of whether this is within an acceptable threshold - function isAcceptableThreshold(uint256 requestedSellAmount, uint256 tokenAmountSold) - internal - pure - returns (bool); -} diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MForwarderCore.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MForwarderCore.sol index 4a54e76b1..83915c738 100644 --- a/packages/contracts/src/2.0.0/forwarder/mixins/MForwarderCore.sol +++ b/packages/contracts/src/2.0.0/forwarder/mixins/MForwarderCore.sol @@ -28,64 +28,42 @@ contract MForwarderCore is IForwarderCore { - /// @dev Market sells WETH for ERC20 tokens. - /// @param orders An array of Order struct containing order specifications. - /// @param signatures An array of Proof that order has been created by maker. - /// @param feeOrders An array of Order struct containing order specifications for fees. - /// @param feeSignatures An array of Proof that order has been created by maker for the fee orders. - /// @param wethSellAmount The amount of WETH to sell. - /// @return FillResults amounts filled and fees paid by maker and taker. - function marketSellEthForERC20Internal( + /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. + /// @param wethSellAmount Desired amount of WETH to sell. + /// @param signatures Proofs that orders have been created by makers. + /// @return Amounts filled and fees paid by maker and taker. + function marketSellEth( LibOrder.Order[] memory orders, - bytes[] memory signatures, - LibOrder.Order[] memory feeOrders, - bytes[] memory feeSignatures, - uint256 wethSellAmount + uint256 wethSellAmount, + bytes[] memory signatures ) internal - returns (LibFillResults.FillResults memory totalFillResults); + returns (LibFillResults.FillResults memory fillResults); - /// @dev Market sells WETH for ZRX tokens. - /// @param orders An array of Order struct containing order specifications. - /// @param signatures An array of Proof that order has been created by maker. - /// @param wethSellAmount The amount of WETH to sell. - /// @return FillResults amounts filled and fees paid by maker and taker. - function marketSellEthForZRXInternal( + /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. + /// @param makerAssetFillAmount Desired amount of makerAsset to buy. + /// @param signatures Proofs that orders have been created by makers. + /// @return Amounts filled and fees paid by maker and taker. + function marketBuyAsset( LibOrder.Order[] memory orders, - bytes[] memory signatures, - uint256 wethSellAmount + uint256 makerAssetFillAmount, + bytes[] memory signatures ) internal - returns (LibFillResults.FillResults memory totalFillResults); - - /// @dev Buys an exact amount of an ERC20 token using WETH. - /// @param orders Orders to fill. The maker asset is the ERC20 token to buy. The taker asset is WETH. - /// @param signatures Proof that the orders were created by their respective makers. - /// @param feeOrders to fill. The maker asset is ZRX and the taker asset is WETH. - /// @param feeSignatures Proof that the feeOrders were created by their respective makers. - /// @param makerTokenFillAmount Amount of the ERC20 token to buy. - /// @return totalFillResults Aggregated fill results of buying the ERC20 and ZRX tokens. - function marketBuyERC20TokensInternal( - LibOrder.Order[] memory orders, - bytes[] memory signatures, - LibOrder.Order[] memory feeOrders, - bytes[] memory feeSignatures, - uint256 makerTokenFillAmount - ) - internal - returns (LibFillResults.FillResults memory totalFillResults); + returns (LibFillResults.FillResults memory fillResults); - /// @dev Buys an all of the ERC721 tokens in the orders. - /// @param orders Orders to fill. The maker asset is the ERC721 token to buy. The taker asset is WETH. - /// @param signatures Proof that the orders were created by their respective makers. - /// @param feeOrders to fill. The maker asset is ZRX and the taker asset is WETH. - /// @param feeSignatures Proof that the feeOrders were created by their respective makers. - /// @return totalFillResults Aggregated fill results of buying the ERC721 tokens and ZRX tokens. - function batchBuyERC721TokensInternal( + /// @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. + /// @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 marketBuyZrx( LibOrder.Order[] memory orders, - bytes[] memory signatures, - LibOrder.Order[] memory feeOrders, - bytes[] memory feeSignatures + uint256 zrxBuyAmount, + bytes[] memory signatures ) internal returns (LibFillResults.FillResults memory totalFillResults); diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MMarketBuyZrx.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MMarketBuyZrx.sol deleted file mode 100644 index 3501ef001..000000000 --- a/packages/contracts/src/2.0.0/forwarder/mixins/MMarketBuyZrx.sol +++ /dev/null @@ -1,42 +0,0 @@ -/* - - Copyright 2018 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity 0.4.24; - -import "../../protocol/Exchange/libs/LibFillResults.sol"; -import "../../protocol/Exchange/libs/LibOrder.sol"; - - -contract MMarketBuyZrx { - - /// @dev Buys zrxBuyAmount of ZRX fee tokens, taking into account the fees on buying fee tokens. This will guarantee - /// At least zrxBuyAmount of ZRX fee tokens are purchased (sometimes slightly over due to rounding issues). - /// It is possible that a request to buy 200 ZRX fee tokens will require purchasing 202 ZRX tokens - /// As 2 ZRX is required to purchase the 200 ZRX fee tokens. This guarantees at least 200 ZRX for future purchases. - /// @param orders An array of Order struct containing order specifications for fees. - /// @param signatures An array of Proof that order has been created by maker for the fee orders. - /// @param zrxBuyAmount The number of requested ZRX fee tokens. - /// @return totalFillResults Amounts filled and fees paid by maker and taker. makerTokenAmount is the zrx amount deducted of fees - function marketBuyZrxInternal( - LibOrder.Order[] memory orders, - bytes[] memory signatures, - uint256 zrxBuyAmount - ) - internal - returns (LibFillResults.FillResults memory totalFillResults); -} diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MTransfer.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MTransfer.sol deleted file mode 100644 index 418a6288b..000000000 --- a/packages/contracts/src/2.0.0/forwarder/mixins/MTransfer.sol +++ /dev/null @@ -1,46 +0,0 @@ -/* - - Copyright 2018 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity 0.4.24; - - -contract MTransfer { - - function onERC721Received(address, uint256, bytes memory) - public - pure - returns(bytes4); - - function onERC721Received(address, address, uint256, bytes memory) - public - pure - returns(bytes4); - - function transferERC20Token( - address token, - address to, - uint256 amount - ) - internal; - - function transferERC721Token( - bytes memory assetData, - address to - ) - internal; -} diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MWeth.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MWeth.sol new file mode 100644 index 000000000..7cbc115e9 --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/mixins/MWeth.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 MWeth { + + /// @dev Converts message call's ETH value into WETH. + /// @return 95% of ETH converted to WETH. + function convertEthToWeth() + internal + returns (uint256 wethAvailable); + + /// @dev Transfers feePercentage of WETH spent on primary orders to feeRecipient. + /// Refunds any excess ETH to msg.sender. + /// @param wethSoldExcludingFeeOrders Amount of WETH sold when filling primary orders. + /// @param wethSoldForZrx Amount of WETH sold when purchasing ZRX required for primary order fees. + /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. + /// @param feeRecipient Address that will receive ETH when orders are filled. + function transferEthFeeAndRefund( + uint256 wethSoldExcludingFeeOrders, + uint256 wethSoldForZrx, + uint256 feePercentage, + address feeRecipient + ) + internal; +} -- cgit v1.2.3 From 6fb157488cf337a1b3c96fc4a9b04d81098bd27d Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Tue, 10 Jul 2018 15:41:33 -0700 Subject: Update transferEthFeeAndRefund, add check to ERC721 transfer --- .../src/2.0.0/forwarder/LibForwarderErrors.sol | 34 ++++++++++++++++++ .../contracts/src/2.0.0/forwarder/MixinAssets.sol | 12 +++++-- .../src/2.0.0/forwarder/MixinErrorMessages.sol | 33 ------------------ .../src/2.0.0/forwarder/MixinForwarderCore.sol | 13 ++++--- .../contracts/src/2.0.0/forwarder/MixinWeth.sol | 40 +++++++++++++++------- .../src/2.0.0/forwarder/mixins/MAssets.sol | 6 +++- .../src/2.0.0/protocol/Exchange/libs/LibMath.sol | 6 ++-- 7 files changed, 86 insertions(+), 58 deletions(-) create mode 100644 packages/contracts/src/2.0.0/forwarder/LibForwarderErrors.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/MixinErrorMessages.sol diff --git a/packages/contracts/src/2.0.0/forwarder/LibForwarderErrors.sol b/packages/contracts/src/2.0.0/forwarder/LibForwarderErrors.sol new file mode 100644 index 000000000..69108738a --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/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 MAX_FEE_EXCEEDED = "MAX_FEE_EXCEEDED"; // Not enough ETH remaining to pay feeRecipient. + string constant OVERSOLD_WETH = "OVERSOLD_WETH"; // More WETH sold than provided with current message call. + string constant COMPLETE_FILL_FAILED = "COMPLETE_FILL_FAILED"; // Desired purchase amount not completely filled (required for ZRX fees only). + string constant TRANSFER_FAILED = "TRANSFER_FAILED"; // Asset transfer failed. + string constant UNSUPPORTED_TOKEN_PROXY = "UNSUPPORTED_TOKEN_PROXY"; // Proxy in assetData not supported. + string constant DEFAULT_FUNCTION_WETH_CONTRACT_ONLY = "DEFAULT_FUNCTION_WETH_CONTRACT_ONLY"; // Fallback function may only be used for WETH withdrawals. + string constant INVALID_MSG_VALUE = "INVALID_MSG_VALUE"; // msg.value must be greater than 0. + string constant INVALID_AMOUNT = "INVALID_AMOUNT"; // Amount must equal 1. +} diff --git a/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol b/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol index 325af5518..cd150764e 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol @@ -95,7 +95,7 @@ contract MixinAssets is if (proxyId == ERC20_DATA_ID) { transferERC20Token(assetData, amount); } else if (proxyId == ERC721_DATA_ID) { - transferERC721Token(assetData); + transferERC721Token(assetData, amount); } else { revert("UNSUPPORTED_TOKEN_PROXY"); } @@ -149,9 +149,17 @@ contract MixinAssets is /// @dev Decodes ERC721 assetData and transfers given amount to sender. /// @param assetData Byte array encoded for the respective asset proxy. - function transferERC721Token(bytes memory assetData) + /// @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); diff --git a/packages/contracts/src/2.0.0/forwarder/MixinErrorMessages.sol b/packages/contracts/src/2.0.0/forwarder/MixinErrorMessages.sol deleted file mode 100644 index 91758eadc..000000000 --- a/packages/contracts/src/2.0.0/forwarder/MixinErrorMessages.sol +++ /dev/null @@ -1,33 +0,0 @@ -/* - - Copyright 2018 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -// solhint-disable -pragma solidity 0.4.24; - - -/// This contract is intended to serve as a reference, but is not actually used for efficiency reasons. -contract MixinErrorMessages { - string constant FEE_PERCENTAGE_TOO_LARGE = "FEE_PROPORTION_TOO_LARGE"; // Provided fee percentage greater than 5%. - string constant MAX_FEE_EXCEEDED = "MAX_FEE_EXCEEDED"; // Not enough ETH remaining to pay feeRecipient. - string constant OVERSOLD_WETH = "OVERSOLD_WETH"; // More WETH sold than provided with current message call. - string constant COMPLETE_FILL_FAILED = "COMPLETE_FILL_FAILED"; // Desired purchase amount not completely filled (required for ZRX fees only). - string constant TRANSFER_FAILED = "TRANSFER_FAILED"; // Asset transfer failed. - string constant UNSUPPORTED_TOKEN_PROXY = "UNSUPPORTED_TOKEN_PROXY"; // Proxy in assetData not supported. - string constant DEFAULT_FUNCTION_WETH_CONTRACT_ONLY = "DEFAULT_FUNCTION_WETH_CONTRACT_ONLY"; // Fallback function may only be used for WETH withdrawals. - string constant INVALID_MSG_VALUE = "INVALID_MSG_VALUE"; // msg.value must be greater than 0. -} diff --git a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol index 1a0687ab5..88ec5f472 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol @@ -80,7 +80,7 @@ contract MixinForwarderCore is // Attempt to sell 95% of WETH. // ZRX fees are payed with this contract's balance. - marketSellEth( + orderFillResults = marketSellEth( orders, wethAvailable, signatures @@ -258,6 +258,7 @@ contract MixinForwarderCore is bytes memory zrxAssetData = ZRX_ASSET_DATA; bytes memory wethAssetData = WETH_ASSET_DATA; + uint256 zrxPurchased = 0; for (uint256 i = 0; i < orders.length; i++) { @@ -266,10 +267,7 @@ contract MixinForwarderCore is orders[i].takerAssetData = wethAssetData; // Calculate the remaining amount of ZRX to buy. - uint256 remainingZrxBuyAmount = safeAdd( - safeSub(zrxBuyAmount, totalFillResults.makerAssetFilledAmount), - totalFillResults.takerFeePaid - ); + 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. @@ -288,16 +286,17 @@ contract MixinForwarderCore is // 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 (totalFillResults.makerAssetFilledAmount == zrxBuyAmount) { + if (zrxPurchased == zrxBuyAmount) { break; } } // Ensure that all ZRX spent while filling primary orders has been repurchased. require( - totalFillResults.makerAssetFilledAmount == zrxBuyAmount, + zrxPurchased == zrxBuyAmount, "COMPLETE_FILL_FAILED" ); } diff --git a/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol b/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol index cbc81a11c..1d0c315f5 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol @@ -78,32 +78,46 @@ contract MixinWeth is ) internal { + // Ensure feePercentage is less than 5%. + require( + feePercentage <= MAX_FEE_PERCENTAGE, + "FEE_PERCENTAGE_TOO_LARGE" + ); + + // Calculate amount of WETH that hasn't been sold. uint256 wethRemaining = safeSub( msg.value, safeAdd(wethSoldExcludingFeeOrders, wethSoldForZrx) ); - ETHER_TOKEN.withdraw(wethRemaining); - require( - feePercentage <= MAX_FEE_PERCENTAGE, - "FEE_PERCENTAGE_TOO_LARGE" - ); + // Calculate ETH fee to pay to feeRecipient. uint256 ethFee = getPartialAmount( feePercentage, PERCENTAGE_DENOMINATOR, wethSoldExcludingFeeOrders ); + + // Ensure fee is less than amount of WETH remaining. require( - ethFee < wethRemaining, + ethFee <= wethRemaining, "MAX_FEE_EXCEEDED" ); - if (ethFee > 0) { - feeRecipient.transfer(ethFee); - } - - uint256 ethRefund = safeSub(wethRemaining, ethFee); - if (ethRefund > 0) { - msg.sender.transfer(ethRefund); + + // Do nothing if no WETH remaining + if (wethRemaining > 0) { + // Convert remaining WETH to ETH + ETHER_TOKEN.withdraw(wethRemaining); + + // Pay ETH to feeRecipient + if (ethFee > 0) { + feeRecipient.transfer(ethFee); + } + + // Refund remaining ETH to msg.sender. + uint256 ethRefund = safeSub(wethRemaining, ethFee); + if (ethRefund > 0) { + msg.sender.transfer(ethRefund); + } } } } diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MAssets.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MAssets.sol index b69f7482d..340ee0bcb 100644 --- a/packages/contracts/src/2.0.0/forwarder/mixins/MAssets.sol +++ b/packages/contracts/src/2.0.0/forwarder/mixins/MAssets.sol @@ -45,6 +45,10 @@ contract MAssets is /// @dev Decodes ERC721 assetData and transfers given amount to sender. /// @param assetData Byte array encoded for the respective asset proxy. - function transferERC721Token(bytes memory assetData) + /// @param amount Amount of asset to transfer to sender. + function transferERC721Token( + bytes memory assetData, + uint256 amount + ) internal; } diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibMath.sol b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibMath.sol index 46c13d390..fa09da6ac 100644 --- a/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibMath.sol +++ b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibMath.sol @@ -33,7 +33,8 @@ contract LibMath is function getPartialAmount( uint256 numerator, uint256 denominator, - uint256 target) + uint256 target + ) internal pure returns (uint256 partialAmount) @@ -53,7 +54,8 @@ contract LibMath is function isRoundingError( uint256 numerator, uint256 denominator, - uint256 target) + uint256 target + ) internal pure returns (bool isError) -- cgit v1.2.3 From 66ab0100554d4b044c7e12c667bb2afd57773302 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Fri, 13 Jul 2018 14:06:48 -0700 Subject: Update percentage constants --- .../contracts/src/2.0.0/forwarder/MixinWeth.sol | 6 ++--- .../protocol/Exchange/MixinWrapperFunctions.sol | 1 + .../src/2.0.0/protocol/Exchange/libs/LibOrder.sol | 28 +++++++++++----------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol b/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol index 1d0c315f5..44cd9cdf9 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol @@ -29,9 +29,9 @@ contract MixinWeth is MWeth { - uint256 constant internal PERCENTAGE_DENOMINATOR = 10000; // 9800 == 98%, 10000 == 100% - uint256 constant internal MAX_FEE_PERCENTAGE = 500; // 5% - uint256 constant internal MAX_WETH_FILL_PERCENTAGE = 9500; // 95% + 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% /// @dev Default payabale function, this allows us to withdraw WETH function () diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol b/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol index 567f26c52..0abf53d02 100644 --- a/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol +++ b/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol @@ -32,6 +32,7 @@ contract MixinWrapperFunctions is LibAbiEncoder, MExchangeCore { + /// @dev Fills the input order. Reverts if exact takerAssetFillAmount not filled. /// @param order Order struct containing order specifications. /// @param takerAssetFillAmount Desired amount of takerAsset to sell. diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibOrder.sol b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibOrder.sol index 68f2c8aed..4031ff26b 100644 --- a/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibOrder.sol +++ b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibOrder.sol @@ -103,20 +103,20 @@ contract LibOrder is bytes32 takerAssetDataHash = keccak256(order.takerAssetData); // Assembly for more efficiently computing: - // keccak256(abi.encode( - // order.makerAddress, - // order.takerAddress, - // order.feeRecipientAddress, - // order.senderAddress, - // order.makerAssetAmount, - // order.takerAssetAmount, - // order.makerFee, - // order.takerFee, - // order.expirationTimeSeconds, - // order.salt, - // keccak256(order.makerAssetData), - // keccak256(order.takerAssetData) - // )); + // keccak256(abi.encode( + // order.makerAddress, + // order.takerAddress, + // order.feeRecipientAddress, + // order.senderAddress, + // order.makerAssetAmount, + // order.takerAssetAmount, + // order.makerFee, + // order.takerFee, + // order.expirationTimeSeconds, + // order.salt, + // keccak256(order.makerAssetData), + // keccak256(order.takerAssetData) + // )); assembly { // Backup -- cgit v1.2.3 From ec5f768f9b9a333fc577e0885cd15f261997a367 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Fri, 13 Jul 2018 16:19:50 -0700 Subject: Rename marketSellEth => marketSellWeth --- packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol | 4 ++-- packages/contracts/src/2.0.0/forwarder/mixins/MForwarderCore.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol index 88ec5f472..19b1c357b 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol @@ -80,7 +80,7 @@ contract MixinForwarderCore is // Attempt to sell 95% of WETH. // ZRX fees are payed with this contract's balance. - orderFillResults = marketSellEth( + orderFillResults = marketSellWeth( orders, wethAvailable, signatures @@ -182,7 +182,7 @@ contract MixinForwarderCore is /// @param wethSellAmount Desired amount of WETH to sell. /// @param signatures Proofs that orders have been created by makers. /// @return Amounts filled and fees paid by maker and taker. - function marketSellEth( + function marketSellWeth( LibOrder.Order[] memory orders, uint256 wethSellAmount, bytes[] memory signatures diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MForwarderCore.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MForwarderCore.sol index 83915c738..9dee15481 100644 --- a/packages/contracts/src/2.0.0/forwarder/mixins/MForwarderCore.sol +++ b/packages/contracts/src/2.0.0/forwarder/mixins/MForwarderCore.sol @@ -32,7 +32,7 @@ contract MForwarderCore is /// @param wethSellAmount Desired amount of WETH to sell. /// @param signatures Proofs that orders have been created by makers. /// @return Amounts filled and fees paid by maker and taker. - function marketSellEth( + function marketSellWeth( LibOrder.Order[] memory orders, uint256 wethSellAmount, bytes[] memory signatures -- cgit v1.2.3 From 799ff2a5c392383c8b245ae53057593acc2534ce Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Tue, 17 Jul 2018 11:32:44 -0700 Subject: Fix rounding error issues, use different logic when makerAsset is ZRX --- .../src/2.0.0/forwarder/LibForwarderErrors.sol | 1 + .../src/2.0.0/forwarder/MixinConstants.sol | 5 +- .../src/2.0.0/forwarder/MixinForwarderCore.sol | 121 +++++++++++++++------ .../contracts/src/2.0.0/forwarder/MixinWeth.sol | 13 --- .../src/2.0.0/forwarder/mixins/MConstants.sol | 3 + .../contracts/src/2.0.0/forwarder/mixins/MWeth.sol | 4 +- 6 files changed, 95 insertions(+), 52 deletions(-) diff --git a/packages/contracts/src/2.0.0/forwarder/LibForwarderErrors.sol b/packages/contracts/src/2.0.0/forwarder/LibForwarderErrors.sol index 69108738a..af85c75e3 100644 --- a/packages/contracts/src/2.0.0/forwarder/LibForwarderErrors.sol +++ b/packages/contracts/src/2.0.0/forwarder/LibForwarderErrors.sol @@ -31,4 +31,5 @@ contract LibForwarderErrors { 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. + string constant INVALID_ORDERS_LENGTH = "INVALID_ORDERS_LENGTH"; // Length of orders must be greater than 1. } diff --git a/packages/contracts/src/2.0.0/forwarder/MixinConstants.sol b/packages/contracts/src/2.0.0/forwarder/MixinConstants.sol index 4f95c796b..e430aba41 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinConstants.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinConstants.sol @@ -28,7 +28,10 @@ contract MixinConstants is bytes4 constant internal ERC20_DATA_ID = bytes4(keccak256("ERC20Token(address)")); bytes4 constant internal ERC721_DATA_ID = bytes4(keccak256("ERC721Token(address,uint256,bytes)")); 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% + constructor ( address _exchange, address _etherToken, diff --git a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol index 19b1c357b..9dc203373 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol @@ -23,6 +23,7 @@ import "./mixins/MWeth.sol"; import "./mixins/MAssets.sol"; import "./mixins/MConstants.sol"; import "./mixins/MForwarderCore.sol"; +import "../utils/LibBytes/LibBytes.sol"; import "../protocol/Exchange/libs/LibOrder.sol"; import "../protocol/Exchange/libs/LibFillResults.sol"; import "../protocol/Exchange/libs/LibMath.sol"; @@ -37,6 +38,8 @@ contract MixinForwarderCore is MForwarderCore { + using LibBytes for bytes; + /// @dev Constructor approves ERC20 proxy to transfer ZRX and WETH on this contract's behalf. constructor () public @@ -74,24 +77,54 @@ contract MixinForwarderCore is FillResults memory feeOrderFillResults ) { - // Convert ETH to WETH. - // 5% of WETH is reserved for filling feeOrders and paying feeRecipient. - uint256 wethAvailable = convertEthToWeth(); - - // Attempt to sell 95% of WETH. - // ZRX fees are payed with this contract's balance. - orderFillResults = marketSellWeth( - orders, - wethAvailable, - signatures + require( + orders.length > 0, + "INVALID_ORDERS_LENGTH" ); - // Buy back all ZRX spent on fees. - feeOrderFillResults = marketBuyZrx( - feeOrders, - orderFillResults.takerFeePaid, - feeSignatures - ); + // Convert ETH to WETH. + convertEthToWeth(); + + uint256 makerAssetAmountPurchased; + uint256 wethAvailable; + if (orders[0].makerAssetData.equals(ZRX_ASSET_DATA)) { + // Calculate amount of WETH that won't be spent on ETH fees. + wethAvailable = getPartialAmount( + PERCENTAGE_DENOMINATOR, + safeAdd(PERCENTAGE_DENOMINATOR, feePercentage), + msg.value + ); + // Market sell available WETH. + // ZRX fees are paid with this contract's balance. + orderFillResults = marketSellWeth( + orders, + wethAvailable, + 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. + wethAvailable = getPartialAmount( + MAX_WETH_FILL_PERCENTAGE, + PERCENTAGE_DENOMINATOR, + msg.value + ); + // Market sell 95% of WETH. + // ZRX fees are payed with this contract's balance. + orderFillResults = marketSellWeth( + orders, + wethAvailable, + signatures + ); + // Buy back all ZRX spent on fees. + feeOrderFillResults = marketBuyZrx( + feeOrders, + orderFillResults.takerFeePaid, + feeSignatures + ); + makerAssetAmountPurchased = orderFillResults.makerAssetFilledAmount; + } // Ensure that no extra WETH owned by this contract has been sold. uint256 totalWethSold = safeAdd(orderFillResults.takerAssetFilledAmount, feeOrderFillResults.takerAssetFilledAmount); @@ -110,7 +143,7 @@ contract MixinForwarderCore is ); // Transfer purchased assets to msg.sender. - transferPurchasedAssetToSender(orders[0].makerAssetData, orderFillResults.makerAssetFilledAmount); + transferPurchasedAssetToSender(orders[0].makerAssetData, makerAssetAmountPurchased); } /// @dev Attempt to purchase makerAssetFillAmount of makerAsset by selling ETH provided with transaction. @@ -140,23 +173,41 @@ contract MixinForwarderCore is FillResults memory feeOrderFillResults ) { + require( + orders.length > 0, + "INVALID_ORDERS_LENGTH" + ); + // Convert ETH to WETH. convertEthToWeth(); - // Attemp to purchase desired amount of makerAsset. - // ZRX fees are payed with this contract's balance. - orderFillResults = marketBuyAsset( - orders, - makerAssetFillAmount, - signatures - ); - - // Buy back all ZRX spent on fees. - feeOrderFillResults = marketBuyZrx( - feeOrders, - orderFillResults.takerFeePaid, - feeSignatures - ); + 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 = marketBuyZrx( + 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 = marketBuyAsset( + orders, + makerAssetFillAmount, + signatures + ); + // Buy back all ZRX spent on fees. + feeOrderFillResults = marketBuyZrx( + feeOrders, + orderFillResults.takerFeePaid, + feeSignatures + ); + makerAssetAmountPurchased = orderFillResults.makerAssetFilledAmount; + } // Ensure that no extra WETH owned by this contract has been sold. uint256 totalWethSold = safeAdd(orderFillResults.takerAssetFilledAmount, feeOrderFillResults.takerAssetFilledAmount); @@ -175,7 +226,7 @@ contract MixinForwarderCore is ); // Transfer purchased assets to msg.sender. - transferPurchasedAssetToSender(orders[0].makerAssetData, orderFillResults.makerAssetFilledAmount); + transferPurchasedAssetToSender(orders[0].makerAssetData, makerAssetAmountPurchased); } /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. @@ -280,7 +331,7 @@ contract MixinForwarderCore is // Attempt to sell the remaining amount of WETH. FillResults memory singleFillResult = EXCHANGE.fillOrderNoThrow( orders[i], - remainingWethSellAmount, + safeAdd(remainingWethSellAmount, 1), signatures[i] ); @@ -289,14 +340,14 @@ contract MixinForwarderCore is zrxPurchased = safeSub(totalFillResults.makerAssetFilledAmount, totalFillResults.takerFeePaid); // Stop execution if the entire amount of ZRX has been bought. - if (zrxPurchased == zrxBuyAmount) { + if (zrxPurchased >= zrxBuyAmount) { break; } } // Ensure that all ZRX spent while filling primary orders has been repurchased. require( - zrxPurchased == zrxBuyAmount, + zrxPurchased >= zrxBuyAmount, "COMPLETE_FILL_FAILED" ); } diff --git a/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol b/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol index 44cd9cdf9..b566c3ef6 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol @@ -29,10 +29,6 @@ contract MixinWeth is MWeth { - 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% - /// @dev Default payabale function, this allows us to withdraw WETH function () public @@ -45,23 +41,14 @@ contract MixinWeth is } /// @dev Converts message call's ETH value into WETH. - /// @return 95% of ETH converted to WETH. function convertEthToWeth() internal - returns (uint256 wethAvailable) { require( msg.value > 0, "INVALID_MSG_VALUE" ); - ETHER_TOKEN.deposit.value(msg.value)(); - wethAvailable = getPartialAmount( - MAX_WETH_FILL_PERCENTAGE, - PERCENTAGE_DENOMINATOR, - msg.value - ); - return wethAvailable; } /// @dev Transfers feePercentage of WETH spent on primary orders to feeRecipient. diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MConstants.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MConstants.sol index bbc4969d6..712a11c5d 100644 --- a/packages/contracts/src/2.0.0/forwarder/mixins/MConstants.sol +++ b/packages/contracts/src/2.0.0/forwarder/mixins/MConstants.sol @@ -28,6 +28,9 @@ contract MConstants { bytes4 constant internal ERC20_DATA_ID = bytes4(keccak256("ERC20Token(address)")); bytes4 constant internal ERC721_DATA_ID = bytes4(keccak256("ERC721Token(address,uint256,bytes)")); 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; diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MWeth.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MWeth.sol index 7cbc115e9..88e77be4e 100644 --- a/packages/contracts/src/2.0.0/forwarder/mixins/MWeth.sol +++ b/packages/contracts/src/2.0.0/forwarder/mixins/MWeth.sol @@ -22,10 +22,8 @@ pragma solidity 0.4.24; contract MWeth { /// @dev Converts message call's ETH value into WETH. - /// @return 95% of ETH converted to WETH. function convertEthToWeth() - internal - returns (uint256 wethAvailable); + internal; /// @dev Transfers feePercentage of WETH spent on primary orders to feeRecipient. /// Refunds any excess ETH to msg.sender. -- cgit v1.2.3 From 1f0e819756ea247e592a8a05c53561894c8ca87a Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Tue, 17 Jul 2018 11:33:51 -0700 Subject: Fix minimal tests --- packages/contracts/test/forwarder/forwarder.ts | 1248 +++++++++++--------- packages/contracts/test/utils/constants.ts | 2 + packages/contracts/test/utils/forwarder_wrapper.ts | 226 +--- packages/types/src/index.ts | 2 +- 4 files changed, 748 insertions(+), 730 deletions(-) diff --git a/packages/contracts/test/forwarder/forwarder.ts b/packages/contracts/test/forwarder/forwarder.ts index 0256d7d81..f2966fe75 100644 --- a/packages/contracts/test/forwarder/forwarder.ts +++ b/packages/contracts/test/forwarder/forwarder.ts @@ -47,10 +47,10 @@ describe(ContractName.Forwarder, () => { let forwarderWrapper: ForwarderWrapper; let exchangeWrapper: ExchangeWrapper; - let signedOrder: SignedOrder; - let signedOrders: SignedOrder[]; + let orderWithoutFee: SignedOrder; + let ordersWithoutFee: SignedOrder[]; let orderWithFee: SignedOrder; - let signedOrdersWithFee: SignedOrder[]; + let ordersWithFee: SignedOrder[]; let feeOrder: SignedOrder; let feeOrders: SignedOrder[]; let orderFactory: OrderFactory; @@ -59,7 +59,9 @@ describe(ContractName.Forwarder, () => { let tx: TransactionReceiptWithDecodedLogs; let erc721MakerAssetIds: BigNumber[]; - let feeProportion: number = 0; + let takerEthBalanceBefore: BigNumber; + let feePercentage: BigNumber; + const MAX_WETH_FILL_PERCENTAGE = 95; before(async () => { await blockchainLifecycle.startAsync(); @@ -135,7 +137,11 @@ describe(ContractName.Forwarder, () => { wethAssetData, ); forwarderContract = new ForwarderContract(forwarderInstance.abi, forwarderInstance.address, provider); - forwarderWrapper = new ForwarderWrapper(forwarderContract, provider, zrxToken.address); + 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); web3Wrapper.abiDecoder.addABI(forwarderContract.abi); @@ -146,673 +152,785 @@ describe(ContractName.Forwarder, () => { }); beforeEach(async () => { await blockchainLifecycle.startAsync(); - feeProportion = 0; erc20Balances = await erc20Wrapper.getBalancesAsync(); - signedOrder = await orderFactory.newSignedOrderAsync(); - signedOrders = [signedOrder]; + 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), }); - feeOrders = [feeOrder]; orderWithFee = await orderFactory.newSignedOrderAsync({ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), }); - signedOrdersWithFee = [orderWithFee]; }); afterEach(async () => { await blockchainLifecycle.revertAsync(); }); - describe('calculations', () => { - it('throws if partially filled orders passed in are not enough to satisfy requested amount', async () => { - feeOrders = [feeOrder]; - const makerTokenFillAmount = feeOrder.makerAssetAmount.div(2); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - feeOrders, - [], - feeProportion, - makerTokenFillAmount, - ); - // Fill the feeOrder - tx = await forwarderWrapper.marketBuyTokensWithEthAsync(feeOrders, [], makerTokenFillAmount, { - from: takerAddress, - value: fillAmountWei, - }); - return expect( - forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - feeOrders, - [], - feeProportion, - makerTokenFillAmount, - ), - ).to.be.rejectedWith('Unable to satisfy makerAssetFillAmount with provided orders'); - }); - it('throws if orders passed are cancelled', async () => { - tx = await exchangeWrapper.cancelOrderAsync(feeOrder, makerAddress); - // Cancel the feeOrder - return expect( - forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - feeOrders, - [], - feeProportion, - feeOrder.makerAssetAmount.div(2), - ), - ).to.be.rejectedWith('Unable to satisfy makerAssetFillAmount with provided orders'); - }); - }); - describe('marketSellEthForERC20 without extra fees', () => { + + describe('marketSellOrdersWithEth without extra fees', () => { it('should fill the order', async () => { - const fillAmount = signedOrder.takerAssetAmount.div(2); - const makerBalanceBefore = erc20Balances[makerAddress][defaultMakerAssetAddress]; - const takerBalanceBefore = erc20Balances[takerAddress][defaultMakerAssetAddress]; + ordersWithoutFee = [orderWithoutFee]; feeOrders = []; - tx = await forwarderWrapper.marketSellEthForERC20Async(signedOrders, feeOrders, { - value: fillAmount, + 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 makerBalanceAfter = newBalances[makerAddress][defaultMakerAssetAddress]; - const takerBalanceAfter = newBalances[takerAddress][defaultMakerAssetAddress]; - const makerTokenFillAmount = fillAmount - .times(signedOrder.makerAssetAmount) - .dividedToIntegerBy(signedOrder.takerAssetAmount); - - expect(makerBalanceAfter).to.be.bignumber.equal(makerBalanceBefore.minus(makerTokenFillAmount)); - expect(takerBalanceAfter).to.be.bignumber.equal(takerBalanceBefore.plus(makerTokenFillAmount)); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(new BigNumber(0)); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const makerAssetFillAmount = primaryTakerAssetFillAmount + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + const totalEthSpent = primaryTakerAssetFillAmount.plus(DEFAULT_GAS_PRICE.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 perform fee abstraction', async () => { - const fillAmount = signedOrder.takerAssetAmount.div(4); - const takerBalanceBefore = erc20Balances[takerAddress][defaultMakerAssetAddress]; - tx = await forwarderWrapper.marketSellEthForERC20Async(signedOrdersWithFee, feeOrders, { - value: fillAmount, + it('should fill the order and pay ZRX fees from feeOrders', async () => { + ordersWithFee = [orderWithFee]; + 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 takerBalanceAfter = newBalances[takerAddress][defaultMakerAssetAddress]; - const acceptPercentage = 98; - const acceptableThreshold = takerBalanceBefore.plus(fillAmount.times(acceptPercentage).dividedBy(100)); - const isWithinThreshold = takerBalanceAfter.greaterThanOrEqualTo(acceptableThreshold); - expect(isWithinThreshold).to.be.true(); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(new BigNumber(0)); + 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(DEFAULT_GAS_PRICE.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), }); - signedOrdersWithFee = [orderWithFee]; + ordersWithFee = [orderWithFee]; feeOrders = []; - const fillAmount = signedOrder.takerAssetAmount.div(4); - const takerBalanceBefore = erc20Balances[takerAddress][zrxToken.address]; - tx = await forwarderWrapper.marketSellEthForERC20Async(signedOrdersWithFee, feeOrders, { - value: fillAmount, + 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 takerBalanceAfter = newBalances[takerAddress][zrxToken.address]; - const acceptPercentage = 98; - const acceptableThreshold = takerBalanceBefore.plus(fillAmount.times(acceptPercentage).dividedBy(100)); - const isWithinThreshold = takerBalanceAfter.greaterThanOrEqualTo(acceptableThreshold); - expect(isWithinThreshold).to.be.true(); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(new BigNumber(0)); + const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2); + const totalEthSpent = ethValue.plus(DEFAULT_GAS_PRICE.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 fail if sent an ETH amount too high', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + it('should refund remaining ETH if amount is greater than takerAssetAmount', async () => { + ordersWithoutFee = [orderWithoutFee]; + const ethValue = orderWithoutFee.takerAssetAmount.times(2); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { + value: ethValue, + from: takerAddress, }); - const fillAmount = signedOrder.takerAssetAmount.times(2); - return expectTransactionFailedAsync( - forwarderWrapper.marketSellEthForERC20Async(signedOrdersWithFee, feeOrders, { - value: fillAmount, - from: takerAddress, - }), - RevertReason.UnacceptableThreshold, - ); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const totalEthSpent = orderWithoutFee.takerAssetAmount.plus(DEFAULT_GAS_PRICE.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); }); - it('should fail if fee abstraction amount is too high', async () => { + it('should revert if ZRX cannot be fully repurchased', async () => { orderWithFee = await orderFactory.newSignedOrderAsync({ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), DECIMALS_DEFAULT), }); - signedOrdersWithFee = [orderWithFee]; + ordersWithFee = [orderWithFee]; feeOrder = await orderFactory.newSignedOrderAsync({ makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), }); feeOrders = [feeOrder]; - const fillAmount = signedOrder.takerAssetAmount.div(4); + const ethValue = orderWithFee.takerAssetAmount; return expectTransactionFailedAsync( - forwarderWrapper.marketSellEthForERC20Async(signedOrdersWithFee, feeOrders, { - value: fillAmount, + forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { + value: ethValue, from: takerAddress, }), - RevertReason.TransferFailed, + RevertReason.CompleteFillFailed, ); }); - it('throws when mixed ERC721 and ERC20 assets with ERC20 first', async () => { + 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(); - signedOrders = [erc20SignedOrder, erc721SignedOrder]; - const fillAmountWei = erc20SignedOrder.takerAssetAmount.plus(erc721SignedOrder.takerAssetAmount); - return expectTransactionFailedAsync( - forwarderWrapper.marketSellEthForERC20Async(signedOrders, feeOrders, { - from: takerAddress, - value: fillAmountWei, - }), - RevertReason.InvalidOrderSignature, - ); + ordersWithoutFee = [erc20SignedOrder, erc721SignedOrder]; + 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(DEFAULT_GAS_PRICE.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); }); }); - describe('marketSellEthForERC20 with extra fees', () => { - it('should fill the order and send fee to fee recipient', async () => { - const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); - const fillAmount = signedOrder.takerAssetAmount.div(2); - feeProportion = 150; // 1.5% + describe('marketSellOrdersWithEth with extra fees', () => { + it('should fill the order and send fee to feeRecipient', async () => { + ordersWithoutFee = [orderWithoutFee]; feeOrders = []; - tx = await forwarderWrapper.marketSellEthForERC20Async( - signedOrders, + 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, - value: fillAmount, - gasPrice: DEFAULT_GAS_PRICE, - }, - { - feeProportion, - feeRecipient: feeRecipientAddress, }, + { 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 makerBalanceBefore = erc20Balances[makerAddress][defaultMakerAssetAddress]; - const makerBalanceAfter = newBalances[makerAddress][defaultMakerAssetAddress]; - const takerBalanceAfter = newBalances[takerAddress][defaultMakerAssetAddress]; - const afterEthBalance = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); - const takerBoughtAmount = takerBalanceAfter.minus(erc20Balances[takerAddress][defaultMakerAssetAddress]); - expect(makerBalanceAfter).to.be.bignumber.equal(makerBalanceBefore.minus(takerBoughtAmount)); - expect(afterEthBalance).to.be.bignumber.equal( - initEthBalance.plus(fillAmount.times(feeProportion).dividedBy(10000)), + 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(DEFAULT_GAS_PRICE.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[forwarderContract.address][weth.address]).to.be.bignumber.equal(new BigNumber(0)); + 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 initEthBalance = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); - const fillAmount = signedOrder.takerAssetAmount.div(2); - feeProportion = 1500; // 15.0% + const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + const ethValue = orderWithoutFee.takerAssetAmount.div(2); + const baseFeePercentage = 6; + feePercentage = ForwarderWrapper.getPercentageOfValue(ethValue, baseFeePercentage); feeOrders = []; await expectTransactionFailedAsync( - forwarderWrapper.marketSellEthForERC20Async( - signedOrders, + forwarderWrapper.marketSellOrdersWithEthAsync( + ordersWithoutFee, feeOrders, - { from: takerAddress, value: fillAmount, gasPrice: DEFAULT_GAS_PRICE }, - { feeProportion, feeRecipient: feeRecipientAddress }, + { from: takerAddress, value: ethValue, gasPrice: DEFAULT_GAS_PRICE }, + { feePercentage, feeRecipient: feeRecipientAddress }, ), - RevertReason.FeeProportionTooLarge, + RevertReason.FeePercentageTooLarge, ); - const afterEthBalance = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); - expect(afterEthBalance).to.be.bignumber.equal(initEthBalance); + const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + expect(feeRecipientEthBalanceAfter).to.be.bignumber.equal(feeRecipientEthBalanceBefore); }); }); - describe('marketBuyTokensWithEth', () => { + describe('marketBuyOrdersWithEth without extra fees', () => { it('should buy the exact amount of assets', async () => { - const makerAssetAmount = signedOrder.makerAssetAmount.div(2); - const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const balancesBefore = await erc20Wrapper.getBalancesAsync(); - const rate = signedOrder.makerAssetAmount.dividedBy(signedOrder.takerAssetAmount); - const fillAmountWei = makerAssetAmount.dividedToIntegerBy(rate); + ordersWithoutFee = [orderWithoutFee]; feeOrders = []; - tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, from: takerAddress, - value: fillAmountWei, - gasPrice: DEFAULT_GAS_PRICE, }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); const newBalances = await erc20Wrapper.getBalancesAsync(); - const takerBalanceBefore = balancesBefore[takerAddress][defaultMakerAssetAddress]; - const takerBalanceAfter = newBalances[takerAddress][defaultMakerAssetAddress]; - const afterEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const expectedEthBalanceAfterGasCosts = initEthBalance.minus(fillAmountWei).minus(tx.gasUsed); - expect(takerBalanceAfter).to.be.bignumber.eq(takerBalanceBefore.plus(makerAssetAmount)); - expect(afterEthBalance).to.be.bignumber.eq(expectedEthBalanceAfterGasCosts); + + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(DEFAULT_GAS_PRICE.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 assets and return excess ETH', async () => { - const makerAssetAmount = signedOrder.makerAssetAmount.div(2); - const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const balancesBefore = await erc20Wrapper.getBalancesAsync(); - const rate = signedOrder.makerAssetAmount.dividedBy(signedOrder.takerAssetAmount); - const fillAmount = makerAssetAmount.dividedToIntegerBy(rate); - const excessFillAmount = fillAmount.times(2); - feeOrders = []; - tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { - from: takerAddress, - value: excessFillAmount, - gasPrice: DEFAULT_GAS_PRICE, - }); - const newBalances = await erc20Wrapper.getBalancesAsync(); - const takerBalanceBefore = balancesBefore[takerAddress][defaultMakerAssetAddress]; - const takerBalanceAfter = newBalances[takerAddress][defaultMakerAssetAddress]; - const afterEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const expectedEthBalanceAfterGasCosts = initEthBalance.minus(fillAmount).minus(tx.gasUsed); - expect(takerBalanceAfter).to.be.bignumber.eq(takerBalanceBefore.plus(makerAssetAmount)); - expect(afterEthBalance).to.be.bignumber.eq(expectedEthBalanceAfterGasCosts); - }); - it('should buy the exact amount of assets with fee abstraction', async () => { - const makerAssetAmount = signedOrder.makerAssetAmount.div(2); - const balancesBefore = await erc20Wrapper.getBalancesAsync(); - const rate = signedOrder.makerAssetAmount.dividedBy(signedOrder.takerAssetAmount); - const fillAmount = makerAssetAmount.dividedToIntegerBy(rate); - const excessFillAmount = fillAmount.times(2); - tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrdersWithFee, feeOrders, makerAssetAmount, { - from: takerAddress, - value: excessFillAmount, - }); - const newBalances = await erc20Wrapper.getBalancesAsync(); - const takerBalanceBefore = balancesBefore[takerAddress][defaultMakerAssetAddress]; - const takerBalanceAfter = newBalances[takerAddress][defaultMakerAssetAddress]; - expect(takerBalanceAfter).to.be.bignumber.eq(takerBalanceBefore.plus(makerAssetAmount)); - }); - it('should buy the exact amount of assets when buying zrx with fee abstraction', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), - }); - signedOrdersWithFee = [signedOrder]; + ordersWithoutFee = [orderWithoutFee]; feeOrders = []; - const makerAssetAmount = signedOrder.makerAssetAmount.div(2); - const takerWeiBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const balancesBefore = await erc20Wrapper.getBalancesAsync(); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrdersWithFee, - feeOrders, - feeProportion, - makerAssetAmount, - ); - tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrdersWithFee, feeOrders, makerAssetAmount, { + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount; + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, from: takerAddress, - value: fillAmountWei, - gasPrice: DEFAULT_GAS_PRICE, }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); const newBalances = await erc20Wrapper.getBalancesAsync(); - const takerTokenBalanceBefore = balancesBefore[takerAddress][zrxToken.address]; - const takerTokenBalanceAfter = newBalances[takerAddress][zrxToken.address]; - const takerWeiBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const expectedCostAfterGas = fillAmountWei.plus(tx.gasUsed); - expect(takerTokenBalanceAfter).to.be.bignumber.greaterThan(takerTokenBalanceBefore.plus(makerAssetAmount)); - expect(takerWeiBalanceAfter).to.be.bignumber.equal(takerWeiBalanceBefore.minus(expectedCostAfterGas)); - }); - it('throws if fees are higher than 5% when buying zrx', async () => { - const highFeeZRXOrder = await orderFactory.newSignedOrderAsync({ - makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - makerAssetAmount: signedOrder.makerAssetAmount, - takerFee: signedOrder.makerAssetAmount.times(0.06), - }); - signedOrdersWithFee = [highFeeZRXOrder]; - feeOrders = []; - const makerAssetAmount = signedOrder.makerAssetAmount.div(2); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrdersWithFee, - feeOrders, - feeProportion, - makerAssetAmount, + + const primaryTakerAssetFillAmount = ethValue.dividedToIntegerBy(2); + const totalEthSpent = primaryTakerAssetFillAmount.plus(DEFAULT_GAS_PRICE.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), ); - return expectTransactionFailedAsync( - forwarderWrapper.marketBuyTokensWithEthAsync(signedOrdersWithFee, feeOrders, makerAssetAmount, { - from: takerAddress, - value: fillAmountWei, - }), - RevertReason.UnacceptableThreshold, + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), ); - }); - it('throws if fees are higher than 5% when buying erc20', async () => { - const highFeeERC20Order = await orderFactory.newSignedOrderAsync({ - takerFee: signedOrder.makerAssetAmount.times(0.06), - }); - signedOrdersWithFee = [highFeeERC20Order]; - feeOrders = [feeOrder]; - const makerAssetAmount = signedOrder.makerAssetAmount.div(2); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrdersWithFee, - feeOrders, - feeProportion, - makerAssetAmount, + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), ); - return expectTransactionFailedAsync( - forwarderWrapper.marketBuyTokensWithEthAsync(signedOrdersWithFee, feeOrders, makerAssetAmount, { - from: takerAddress, - value: fillAmountWei, - }), - RevertReason.UnacceptableThreshold as any, + 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('throws if makerAssetAmount is 0', async () => { - const makerAssetAmount = new BigNumber(0); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrdersWithFee, - feeOrders, - feeProportion, - makerAssetAmount, - ); - return expectTransactionFailedAsync( - forwarderWrapper.marketBuyTokensWithEthAsync(signedOrdersWithFee, feeOrders, makerAssetAmount, { - from: takerAddress, - value: fillAmountWei, - }), - RevertReason.ValueGreaterThanZero as any, - ); - }); - it('throws if the amount of ETH sent in is less than the takerAssetFilledAmount', async () => { - const makerAssetAmount = signedOrder.makerAssetAmount; - const fillAmount = signedOrder.takerAssetAmount.div(2); - const zero = new BigNumber(0); - // Deposit enough taker balance to fill the order - const wethDepositTxHash = await wethContract.deposit.sendTransactionAsync({ + it('should buy the exact amount of assets with fee abstraction', async () => { + ordersWithFee = [orderWithFee]; + feeOrders = [feeOrder]; + const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount; + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { + value: ethValue, from: takerAddress, - value: signedOrder.takerAssetAmount, }); - await web3Wrapper.awaitTransactionSuccessAsync(wethDepositTxHash); - // Transfer all of this WETH to the forwarding contract - const wethTransferTxHash = await wethContract.transfer.sendTransactionAsync( - forwarderContract.address, - signedOrder.takerAssetAmount, - { from: takerAddress }, - ); - await web3Wrapper.awaitTransactionSuccessAsync(wethTransferTxHash); - // We use the contract directly to get around wrapper validations and calculations - const formattedOrders = formatters.createMarketSellOrders(signedOrders, zero); - const formattedFeeOrders = formatters.createMarketSellOrders(feeOrders, zero); - return expectTransactionFailedAsync( - forwarderContract.marketBuyTokensWithEth.sendTransactionAsync( - formattedOrders.orders, - formattedOrders.signatures, - formattedFeeOrders.orders, - formattedFeeOrders.signatures, - makerAssetAmount, - zero, - constants.NULL_ADDRESS, - { value: fillAmount, from: takerAddress }, - ), - RevertReason.InvalidMsgValue, + 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(DEFAULT_GAS_PRICE.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), ); - }); - }); - describe('marketBuyTokensWithEth - ERC721', async () => { - it('buys ERC721 assets', async () => { - const makerAssetId = erc721MakerAssetIds[0]; - signedOrder = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: new BigNumber(1), - makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), - }); - feeOrders = []; - signedOrders = [signedOrder]; - const makerAssetAmount = new BigNumber(signedOrders.length); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrders, - feeOrders, - feeProportion, - makerAssetAmount, + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), ); - tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { - from: takerAddress, - value: fillAmountWei, - }); - const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); - expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress); + 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('buys ERC721 assets with fee abstraction', async () => { - const makerAssetId = erc721MakerAssetIds[0]; - signedOrder = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: new BigNumber(1), + it('should buy the exact amount of assets when buying ZRX with fee abstraction', async () => { + orderWithFee = await orderFactory.newSignedOrderAsync({ + makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address), takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), - makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), }); - signedOrders = [signedOrder]; - const makerAssetAmount = new BigNumber(signedOrders.length); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrders, - feeOrders, - feeProportion, - makerAssetAmount, - ); - tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { + ordersWithFee = [orderWithFee]; + feeOrders = []; + const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithFee.takerAssetAmount; + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { + value: ethValue, from: takerAddress, - value: fillAmountWei, }); - const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); - expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress); - }); - it('buys ERC721 assets with fee abstraction and pays fee to fee recipient', async () => { - const makerAssetId = erc721MakerAssetIds[0]; - signedOrder = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: new BigNumber(1), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), - makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), - }); - signedOrders = [signedOrder]; - feeProportion = 100; - const initTakerBalanceWei = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const initFeeRecipientBalanceWei = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); - const makerAssetAmount = new BigNumber(signedOrders.length); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrders, - feeOrders, - feeProportion, - makerAssetAmount, - ); - tx = await forwarderWrapper.marketBuyTokensWithEthAsync( - signedOrders, - feeOrders, - makerAssetAmount, - { - from: takerAddress, - value: fillAmountWei, - gasPrice: DEFAULT_GAS_PRICE, - }, - { - feeProportion, - feeRecipient: feeRecipientAddress, - }, + 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 afterFeeRecipientEthBalance = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); - const afterTakerBalanceWei = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const takerFilledAmount = initTakerBalanceWei.minus(afterTakerBalanceWei).plus(tx.gasUsed); - const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); - expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress); - const balanceDiff = afterFeeRecipientEthBalance.minus(initFeeRecipientBalanceWei); - expect(takerFilledAmount.dividedToIntegerBy(balanceDiff)).to.be.bignumber.equal(101); - expect(takerFilledAmount.minus(balanceDiff).dividedToIntegerBy(balanceDiff)).to.be.bignumber.equal(100); - }); - it('buys multiple ERC721 assets with fee abstraction and pays fee to fee recipient', async () => { - const makerAssetId1 = erc721MakerAssetIds[0]; - const makerAssetId2 = erc721MakerAssetIds[1]; - const signedOrder1 = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: new BigNumber(1), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), DECIMALS_DEFAULT), - makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId1), - }); - const signedOrder2 = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: new BigNumber(1), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(4), DECIMALS_DEFAULT), - makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId2), - }); - signedOrders = [signedOrder1, signedOrder2]; - feeProportion = 10; - const makerAssetAmount = new BigNumber(signedOrders.length); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrders, - feeOrders, - feeProportion, - makerAssetAmount, + const totalEthSpent = primaryTakerAssetFillAmount.plus(DEFAULT_GAS_PRICE.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); + + 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), ); - tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { - from: takerAddress, - value: fillAmountWei, - }); - const newOwnerTakerAsset1 = await erc721Token.ownerOf.callAsync(makerAssetId1); - expect(newOwnerTakerAsset1).to.be.bignumber.equal(takerAddress); - const newOwnerTakerAsset2 = await erc721Token.ownerOf.callAsync(makerAssetId2); - expect(newOwnerTakerAsset2).to.be.bignumber.equal(takerAddress); - }); - it('buys ERC721 assets with fee abstraction and handles fee orders filled and excess eth', async () => { - const makerAssetId = erc721MakerAssetIds[0]; - feeProportion = 0; - // In this scenario a total of 6 ZRX fees need to be paid. - // There are two fee orders, but the first fee order is partially filled while - // the Forwarding contract tx is in the mempool. - const erc721MakerAssetAmount = new BigNumber(1); - signedOrder = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: erc721MakerAssetAmount, - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(6), DECIMALS_DEFAULT), - makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), - }); - signedOrders = [signedOrder]; - const firstFeeOrder = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(8), DECIMALS_DEFAULT), - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.1), DECIMALS_DEFAULT), - makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), - }); - const secondFeeOrder = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(8), DECIMALS_DEFAULT), - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.12), DECIMALS_DEFAULT), - makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), - }); - feeOrders = [firstFeeOrder, secondFeeOrder]; - const makerAssetAmount = new BigNumber(signedOrders.length); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrders, - feeOrders, - feeProportion, - erc721MakerAssetAmount, - ); - // Simulate another otherAddress user partially filling firstFeeOrder - const firstFeeOrderFillAmount = firstFeeOrder.makerAssetAmount.div(2); - tx = await forwarderWrapper.marketBuyTokensWithEthAsync([firstFeeOrder], [], firstFeeOrderFillAmount, { - from: otherAddress, - value: fillAmountWei, - }); - // For tests we calculate how much this should've cost given that firstFeeOrder was filled - const expectedFillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrders, - feeOrders, - feeProportion, - erc721MakerAssetAmount, - ); - // With 4 ZRX remaining in firstFeeOrder, the secondFeeOrder will need to be filled to make up - // the total amount of fees required (6) - // Since the fee orders can be filled while the transaction is pending the user safely sends in - // extra ether to cover any slippage - const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const slippageFillAmountWei = fillAmountWei.times(2); - tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { - from: takerAddress, - value: slippageFillAmountWei, - gasPrice: DEFAULT_GAS_PRICE, - }); - const afterEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const expectedEthBalanceAfterGasCosts = initEthBalance.minus(expectedFillAmountWei).minus(tx.gasUsed); - const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); - expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress); - expect(afterEthBalance).to.be.bignumber.equal(expectedEthBalanceAfterGasCosts); - }); - it('buys ERC721 assets with fee abstraction and handles fee orders filled', async () => { - const makerAssetId = erc721MakerAssetIds[0]; - feeProportion = 0; - // In this scenario a total of 6 ZRX fees need to be paid. - // There are two fee orders, but the first fee order is partially filled while - // the Forwarding contract tx is in the mempool. - const erc721MakerAssetAmount = new BigNumber(1); - signedOrder = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: erc721MakerAssetAmount, - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(6), DECIMALS_DEFAULT), - makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), - }); - const zrxMakerAssetAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(8), DECIMALS_DEFAULT); - signedOrders = [signedOrder]; - const firstFeeOrder = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: zrxMakerAssetAmount, - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.1), DECIMALS_DEFAULT), - makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), - }); - const secondFeeOrder = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: zrxMakerAssetAmount, - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.12), DECIMALS_DEFAULT), - makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), - }); - feeOrders = [firstFeeOrder, secondFeeOrder]; - const makerAssetAmount = new BigNumber(signedOrders.length); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrders, - feeOrders, - feeProportion, - erc721MakerAssetAmount, - ); - // Simulate another otherAddress user partially filling firstFeeOrder - const firstFeeOrderFillAmount = firstFeeOrder.makerAssetAmount.div(2); - tx = await forwarderWrapper.marketBuyTokensWithEthAsync([firstFeeOrder], [], firstFeeOrderFillAmount, { - from: otherAddress, - value: fillAmountWei, - }); - const expectedFillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrders, - feeOrders, - feeProportion, - erc721MakerAssetAmount, + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].plus(makerAssetFilledAmount).minus(takerFeePaid), ); - tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { - from: takerAddress, - value: expectedFillAmountWei, - }); - const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); - expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress); - }); - it('throws when mixed ERC721 and ERC20 assets', 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(); - signedOrders = [erc721SignedOrder, erc20SignedOrder]; - const makerAssetAmount = new BigNumber(signedOrders.length); - const fillAmountWei = erc20SignedOrder.takerAssetAmount.plus(erc721SignedOrder.takerAssetAmount); - return expectTransactionFailedAsync( - forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { - from: takerAddress, - value: fillAmountWei, - }), - RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), ); - }); - it('throws when mixed ERC721 and ERC20 assets with ERC20 first', 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(); - signedOrders = [erc20SignedOrder, erc721SignedOrder]; - const makerAssetAmount = new BigNumber(signedOrders.length); - const fillAmountWei = erc20SignedOrder.takerAssetAmount.plus(erc721SignedOrder.takerAssetAmount); - return expectTransactionFailedAsync( - forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { - from: takerAddress, - value: fillAmountWei, - }), - RevertReason.InvalidTakerAmount, + 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('throws if fees are higher than 5% when buying zrx', async () => { + // const highFeeZRXOrder = orderFactory.newSignedOrder({ + // makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address), + // makerAssetAmount: orderWithoutFee.makerAssetAmount, + // takerFee: orderWithoutFee.makerAssetAmount.times(0.06), + // }); + // ordersWithFee = [highFeeZRXOrder]; + // feeOrders = []; + // const makerAssetAmount = orderWithoutFee.makerAssetAmount.div(2); + // const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( + // ordersWithFee, + // feeOrders, + // feePercentage, + // makerAssetAmount, + // ); + // return expectTransactionFailedAsync( + // forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetAmount, { + // from: takerAddress, + // value: fillAmountWei, + // }), + // RevertReason.UnacceptableThreshold, + // ); + // }); + // it('throws if fees are higher than 5% when buying erc20', async () => { + // const highFeeERC20Order = orderFactory.newSignedOrder({ + // takerFee: orderWithoutFee.makerAssetAmount.times(0.06), + // }); + // ordersWithFee = [highFeeERC20Order]; + // feeOrders = [feeOrder]; + // const makerAssetAmount = orderWithoutFee.makerAssetAmount.div(2); + // const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( + // ordersWithFee, + // feeOrders, + // feePercentage, + // makerAssetAmount, + // ); + // return expectTransactionFailedAsync( + // forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetAmount, { + // from: takerAddress, + // value: fillAmountWei, + // }), + // RevertReason.UnacceptableThreshold as any, + // ); + // }); + // it('throws if makerAssetAmount is 0', async () => { + // const makerAssetAmount = new BigNumber(0); + // const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( + // ordersWithFee, + // feeOrders, + // feePercentage, + // makerAssetAmount, + // ); + // return expectTransactionFailedAsync( + // forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetAmount, { + // from: takerAddress, + // value: fillAmountWei, + // }), + // RevertReason.ValueGreaterThanZero as any, + // ); + // }); + // it('throws if the amount of ETH sent in is less than the takerAssetFilledAmount', async () => { + // const makerAssetAmount = orderWithoutFee.makerAssetAmount; + // const primaryTakerAssetFillAmount = orderWithoutFee.takerAssetAmount.div(2); + // const zero = new BigNumber(0); + // // Deposit enough taker balance to fill the order + // const wethDepositTxHash = await wethContract.deposit.sendTransactionAsync({ + // from: takerAddress, + // value: orderWithoutFee.takerAssetAmount, + // }); + // await web3Wrapper.awaitTransactionSuccessAsync(wethDepositTxHash); + // // Transfer all of this WETH to the forwarding contract + // const wethTransferTxHash = await wethContract.transfer.sendTransactionAsync( + // forwarderContract.address, + // orderWithoutFee.takerAssetAmount, + // { from: takerAddress }, + // ); + // await web3Wrapper.awaitTransactionSuccessAsync(wethTransferTxHash); + // // We use the contract directly to get around wrapper validations and calculations + // const formattedOrders = formatters.createMarketSellOrders(signedOrders, zero); + // const formattedFeeOrders = formatters.createMarketSellOrders(feeOrders, zero); + // return expectTransactionFailedAsync( + // forwarderContract.marketBuyOrdersWithEth.sendTransactionAsync( + // formattedOrders.orders, + // formattedOrders.signatures, + // formattedFeeOrders.orders, + // formattedFeeOrders.signatures, + // makerAssetAmount, + // zero, + // constants.NULL_ADDRESS, + // { value: primaryTakerAssetFillAmount, from: takerAddress }, + // ), + // RevertReason.InvalidMsgValue, + // ); + // }); }); + // describe('marketBuyOrdersWithEth - ERC721', async () => { + // it('buys ERC721 assets', async () => { + // const makerAssetId = erc721MakerAssetIds[0]; + // orderWithoutFee = orderFactory.newSignedOrder({ + // makerAssetAmount: new BigNumber(1), + // makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + // }); + // feeOrders = []; + // signedOrders = [orderWithoutFee]; + // const makerAssetAmount = new BigNumber(signedOrders.length); + // const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( + // signedOrders, + // feeOrders, + // feePercentage, + // makerAssetAmount, + // ); + // tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { + // from: takerAddress, + // value: fillAmountWei, + // }); + // const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); + // expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress); + // }); + // it('buys ERC721 assets with fee abstraction', async () => { + // const makerAssetId = erc721MakerAssetIds[0]; + // orderWithoutFee = orderFactory.newSignedOrder({ + // makerAssetAmount: new BigNumber(1), + // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + // makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + // }); + // signedOrders = [orderWithoutFee]; + // const makerAssetAmount = new BigNumber(signedOrders.length); + // const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( + // signedOrders, + // feeOrders, + // feePercentage, + // makerAssetAmount, + // ); + // tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { + // from: takerAddress, + // value: fillAmountWei, + // }); + // const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); + // expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress); + // }); + // it('buys ERC721 assets with fee abstraction and pays fee to fee recipient', async () => { + // const makerAssetId = erc721MakerAssetIds[0]; + // orderWithoutFee = orderFactory.newSignedOrder({ + // makerAssetAmount: new BigNumber(1), + // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + // makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + // }); + // signedOrders = [orderWithoutFee]; + // feePercentage = 100; + // const initTakerBalanceWei = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + // const initFeeRecipientBalanceWei = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + // const makerAssetAmount = new BigNumber(signedOrders.length); + // const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( + // signedOrders, + // feeOrders, + // feePercentage, + // makerAssetAmount, + // ); + // tx = await forwarderWrapper.marketBuyOrdersWithEthAsync( + // signedOrders, + // feeOrders, + // makerAssetAmount, + // { + // from: takerAddress, + // value: fillAmountWei, + // gasPrice: DEFAULT_GAS_PRICE, + // }, + // { + // feePercentage, + // feeRecipient: feeRecipientAddress, + // }, + // ); + // const afterFeeRecipientEthBalance = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + // const afterTakerBalanceWei = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + // const takerFilledAmount = initTakerBalanceWei.minus(afterTakerBalanceWei).plus(tx.gasUsed); + // const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); + // expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress); + // const balanceDiff = afterFeeRecipientEthBalance.minus(initFeeRecipientBalanceWei); + // expect(takerFilledAmount.dividedToIntegerBy(balanceDiff)).to.be.bignumber.equal(101); + // expect(takerFilledAmount.minus(balanceDiff).dividedToIntegerBy(balanceDiff)).to.be.bignumber.equal(100); + // }); + // it('buys multiple ERC721 assets with fee abstraction and pays fee to fee recipient', async () => { + // const makerAssetId1 = erc721MakerAssetIds[0]; + // const makerAssetId2 = erc721MakerAssetIds[1]; + // const signedOrder1 = orderFactory.newSignedOrder({ + // makerAssetAmount: new BigNumber(1), + // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), DECIMALS_DEFAULT), + // makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId1), + // }); + // const signedOrder2 = orderFactory.newSignedOrder({ + // makerAssetAmount: new BigNumber(1), + // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(4), DECIMALS_DEFAULT), + // makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId2), + // }); + // signedOrders = [signedOrder1, signedOrder2]; + // feePercentage = 10; + // const makerAssetAmount = new BigNumber(signedOrders.length); + // const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( + // signedOrders, + // feeOrders, + // feePercentage, + // makerAssetAmount, + // ); + // tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { + // from: takerAddress, + // value: fillAmountWei, + // }); + // const newOwnerTakerAsset1 = await erc721Token.ownerOf.callAsync(makerAssetId1); + // expect(newOwnerTakerAsset1).to.be.bignumber.equal(takerAddress); + // const newOwnerTakerAsset2 = await erc721Token.ownerOf.callAsync(makerAssetId2); + // expect(newOwnerTakerAsset2).to.be.bignumber.equal(takerAddress); + // }); + // it('buys ERC721 assets with fee abstraction and handles fee orders filled and excess eth', async () => { + // const makerAssetId = erc721MakerAssetIds[0]; + // feePercentage = 0; + // // In this scenario a total of 6 ZRX fees need to be paid. + // // There are two fee orders, but the first fee order is partially filled while + // // the Forwarding contract tx is in the mempool. + // const erc721MakerAssetAmount = new BigNumber(1); + // orderWithoutFee = orderFactory.newSignedOrder({ + // makerAssetAmount: erc721MakerAssetAmount, + // takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT), + // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(6), DECIMALS_DEFAULT), + // makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + // }); + // signedOrders = [orderWithoutFee]; + // const firstFeeOrder = orderFactory.newSignedOrder({ + // makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(8), DECIMALS_DEFAULT), + // takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.1), DECIMALS_DEFAULT), + // makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address), + // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), + // }); + // const secondFeeOrder = orderFactory.newSignedOrder({ + // makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(8), DECIMALS_DEFAULT), + // takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.12), DECIMALS_DEFAULT), + // makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address), + // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), + // }); + // feeOrders = [firstFeeOrder, secondFeeOrder]; + // const makerAssetAmount = new BigNumber(signedOrders.length); + // const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( + // signedOrders, + // feeOrders, + // feePercentage, + // erc721MakerAssetAmount, + // ); + // // Simulate another otherAddress user partially filling firstFeeOrder + // const firstFeeOrderFillAmount = firstFeeOrder.makerAssetAmount.div(2); + // tx = await forwarderWrapper.marketBuyOrdersWithEthAsync([firstFeeOrder], [], firstFeeOrderFillAmount, { + // from: otherAddress, + // value: fillAmountWei, + // }); + // // For tests we calculate how much this should've cost given that firstFeeOrder was filled + // const expectedFillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( + // signedOrders, + // feeOrders, + // feePercentage, + // erc721MakerAssetAmount, + // ); + // // With 4 ZRX remaining in firstFeeOrder, the secondFeeOrder will need to be filled to make up + // // the total amount of fees required (6) + // // Since the fee orders can be filled while the transaction is pending the user safely sends in + // // extra ether to cover any slippage + // const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + // const slippageFillAmountWei = fillAmountWei.times(2); + // tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { + // from: takerAddress, + // value: slippageFillAmountWei, + // gasPrice: DEFAULT_GAS_PRICE, + // }); + // const afterEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + // const expectedEthBalanceAfterGasCosts = initEthBalance.minus(expectedFillAmountWei).minus(tx.gasUsed); + // const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); + // expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress); + // expect(afterEthBalance).to.be.bignumber.equal(expectedEthBalanceAfterGasCosts); + // }); + // it('buys ERC721 assets with fee abstraction and handles fee orders filled', async () => { + // const makerAssetId = erc721MakerAssetIds[0]; + // feePercentage = 0; + // // In this scenario a total of 6 ZRX fees need to be paid. + // // There are two fee orders, but the first fee order is partially filled while + // // the Forwarding contract tx is in the mempool. + // const erc721MakerAssetAmount = new BigNumber(1); + // orderWithoutFee = orderFactory.newSignedOrder({ + // makerAssetAmount: erc721MakerAssetAmount, + // takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT), + // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(6), DECIMALS_DEFAULT), + // makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + // }); + // const zrxMakerAssetAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(8), DECIMALS_DEFAULT); + // signedOrders = [orderWithoutFee]; + // const firstFeeOrder = orderFactory.newSignedOrder({ + // makerAssetAmount: zrxMakerAssetAmount, + // takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.1), DECIMALS_DEFAULT), + // makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address), + // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), + // }); + // const secondFeeOrder = orderFactory.newSignedOrder({ + // makerAssetAmount: zrxMakerAssetAmount, + // takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.12), DECIMALS_DEFAULT), + // makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address), + // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), + // }); + // feeOrders = [firstFeeOrder, secondFeeOrder]; + // const makerAssetAmount = new BigNumber(signedOrders.length); + // const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( + // signedOrders, + // feeOrders, + // feePercentage, + // erc721MakerAssetAmount, + // ); + // // Simulate another otherAddress user partially filling firstFeeOrder + // const firstFeeOrderFillAmount = firstFeeOrder.makerAssetAmount.div(2); + // tx = await forwarderWrapper.marketBuyOrdersWithEthAsync([firstFeeOrder], [], firstFeeOrderFillAmount, { + // from: otherAddress, + // value: fillAmountWei, + // }); + // const expectedFillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( + // signedOrders, + // feeOrders, + // feePercentage, + // erc721MakerAssetAmount, + // ); + // tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { + // from: takerAddress, + // value: expectedFillAmountWei, + // }); + // const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); + // expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress); + // }); + // it('throws when mixed ERC721 and ERC20 assets', async () => { + // const makerAssetId = erc721MakerAssetIds[0]; + // const erc721SignedOrder = orderFactory.newSignedOrder({ + // makerAssetAmount: new BigNumber(1), + // makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + // }); + // const erc20SignedOrder = orderFactory.newSignedOrder(); + // signedOrders = [erc721SignedOrder, erc20SignedOrder]; + // const makerAssetAmount = new BigNumber(signedOrders.length); + // const fillAmountWei = erc20SignedOrder.takerAssetAmount.plus(erc721SignedOrder.takerAssetAmount); + // return expectTransactionFailedAsync( + // forwarderWrapper.marketBuyOrdersWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { + // from: takerAddress, + // value: fillAmountWei, + // }), + // RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + // ); + // }); + // it('throws when mixed ERC721 and ERC20 assets with ERC20 first', async () => { + // const makerAssetId = erc721MakerAssetIds[0]; + // const erc721SignedOrder = orderFactory.newSignedOrder({ + // makerAssetAmount: new BigNumber(1), + // makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + // }); + // const erc20SignedOrder = orderFactory.newSignedOrder(); + // signedOrders = [erc20SignedOrder, erc721SignedOrder]; + // const makerAssetAmount = new BigNumber(signedOrders.length); + // const fillAmountWei = erc20SignedOrder.takerAssetAmount.plus(erc721SignedOrder.takerAssetAmount); + // return expectTransactionFailedAsync( + // forwarderWrapper.marketBuyOrdersWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { + // from: takerAddress, + // value: fillAmountWei, + // }), + // RevertReason.InvalidTakerAmount, + // ); + // }); + // }); }); // tslint:disable:max-file-line-count // tslint:enable:no-unnecessary-type-assertion diff --git a/packages/contracts/test/utils/constants.ts b/packages/contracts/test/utils/constants.ts index 7dac38f56..65eaee398 100644 --- a/packages/contracts/test/utils/constants.ts +++ b/packages/contracts/test/utils/constants.ts @@ -49,4 +49,6 @@ export const constants = { takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18), }, WORD_LENGTH: 32, + ZERO_AMOUNT: new BigNumber(0), + PERCENTAGE_DENOMINATOR: new BigNumber(10).pow(18), }; diff --git a/packages/contracts/test/utils/forwarder_wrapper.ts b/packages/contracts/test/utils/forwarder_wrapper.ts index e39df14b1..773ddf897 100644 --- a/packages/contracts/test/utils/forwarder_wrapper.ts +++ b/packages/contracts/test/utils/forwarder_wrapper.ts @@ -1,5 +1,4 @@ -import { assetDataUtils } from '@0xproject/order-utils'; -import { AssetProxyId, SignedOrder } from '@0xproject/types'; +import { SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import { Provider, TransactionReceiptWithDecodedLogs, TxDataPayable } from 'ethereum-types'; @@ -12,209 +11,108 @@ import { formatters } from './formatters'; import { LogDecoder } from './log_decoder'; import { MarketSellOrders } from './types'; -const DEFAULT_FEE_PROPORTION = 0; -const PERCENTAGE_DENOMINATOR = 10000; -const ZERO_AMOUNT = new BigNumber(0); -const INSUFFICENT_ORDERS_FOR_MAKER_AMOUNT = 'Unable to satisfy makerAssetFillAmount with provided orders'; - export class ForwarderWrapper { private readonly _web3Wrapper: Web3Wrapper; private readonly _forwarderContract: ForwarderContract; private readonly _logDecoder: LogDecoder; - private readonly _zrxAddress: string; - private static _createOptimizedSellOrders(signedOrders: SignedOrder[]): MarketSellOrders { - const marketSellOrders = formatters.createMarketSellOrders(signedOrders, ZERO_AMOUNT); - const assetDataId = assetDataUtils.decodeAssetProxyId(signedOrders[0].makerAssetData); - // Contract will fill this in for us as all of the assetData is assumed to be the same - for (let i = 0; i < signedOrders.length; i++) { - if (i !== 0 && assetDataId === AssetProxyId.ERC20) { - // Forwarding contract will fill this in from the first order - marketSellOrders.orders[i].makerAssetData = constants.NULL_BYTES; + 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.gte(remainingFeeAmount)) { + wethAmount = wethAmount + .plus(feeOrder.takerAssetAmount.times(remainingFeeAmount).dividedToIntegerBy(feeAvailable)) + .plus(1); + remainingFeeAmount = new BigNumber(0); + } else if (!remainingFeeAmount.isZero()) { + wethAmount = wethAmount.plus(feeOrder.takerAssetAmount).plus(1); + remainingFeeAmount = remainingFeeAmount.minus(feeAvailable); } - marketSellOrders.orders[i].takerAssetData = constants.NULL_BYTES; - } - return marketSellOrders; + }); + return wethAmount; } - private static _createOptimizedZRXSellOrders(signedOrders: SignedOrder[]): MarketSellOrders { - const marketSellOrders = formatters.createMarketSellOrders(signedOrders, ZERO_AMOUNT); - // Contract will fill this in for us as all of the assetData is assumed to be the same - for (let i = 0; i < signedOrders.length; i++) { - marketSellOrders.orders[i].makerAssetData = constants.NULL_BYTES; - marketSellOrders.orders[i].takerAssetData = constants.NULL_BYTES; - } - return marketSellOrders; + 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 _calculateAdditionalFeeProportionAmount(feeProportion: number, fillAmountWei: BigNumber): BigNumber { - if (feeProportion > 0) { - // Add to the total ETH transaction to ensure all NFTs can be filled after fees - // 150 = 1.5% = 0.015 - const denominator = new BigNumber(1).minus(new BigNumber(feeProportion).dividedBy(PERCENTAGE_DENOMINATOR)); - return fillAmountWei.dividedBy(denominator).round(0, BigNumber.ROUND_FLOOR); - } - return fillAmountWei; + 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, zrxAddress: string) { + constructor(contractInstance: ForwarderContract, provider: Provider) { this._forwarderContract = contractInstance; this._web3Wrapper = new Web3Wrapper(provider); this._logDecoder = new LogDecoder(this._web3Wrapper, this._forwarderContract.address); - // this._web3Wrapper.abiDecoder.addABI(contractInstance.abi); - this._zrxAddress = zrxAddress; } - public async marketBuyTokensWithEthAsync( + public async marketSellOrdersWithEthAsync( orders: SignedOrder[], feeOrders: SignedOrder[], - makerTokenBuyAmount: BigNumber, txData: TxDataPayable, - opts: { feeProportion?: number; feeRecipient?: string } = {}, + opts: { feePercentage?: BigNumber; feeRecipient?: string } = {}, ): Promise { - const params = ForwarderWrapper._createOptimizedSellOrders(orders); - const feeParams = ForwarderWrapper._createOptimizedZRXSellOrders(feeOrders); - const feeProportion = _.isUndefined(opts.feeProportion) ? DEFAULT_FEE_PROPORTION : opts.feeProportion; + 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: string = await this._forwarderContract.marketBuyTokensWithEth.sendTransactionAsync( + const txHash = await this._forwarderContract.marketSellOrdersWithEth.sendTransactionAsync( params.orders, params.signatures, feeParams.orders, feeParams.signatures, - makerTokenBuyAmount, - feeProportion, + feePercentage, feeRecipient, txData, ); const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); return tx; } - public async marketSellEthForERC20Async( + public async marketBuyOrdersWithEthAsync( orders: SignedOrder[], feeOrders: SignedOrder[], + makerAssetFillAmount: BigNumber, txData: TxDataPayable, - opts: { feeProportion?: number; feeRecipient?: string } = {}, + opts: { feePercentage?: BigNumber; feeRecipient?: string } = {}, ): Promise { - const assetDataId = assetDataUtils.decodeAssetProxyId(orders[0].makerAssetData); - if (assetDataId !== AssetProxyId.ERC20) { - throw new Error('Asset type not supported by marketSellEthForERC20'); - } - const params = ForwarderWrapper._createOptimizedSellOrders(orders); - const feeParams = ForwarderWrapper._createOptimizedZRXSellOrders(feeOrders); - const feeProportion = _.isUndefined(opts.feeProportion) ? DEFAULT_FEE_PROPORTION : opts.feeProportion; + 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: string = await this._forwarderContract.marketSellEthForERC20.sendTransactionAsync( + const txHash = await this._forwarderContract.marketBuyOrdersWithEth.sendTransactionAsync( params.orders, + makerAssetFillAmount, params.signatures, feeParams.orders, feeParams.signatures, - feeProportion, + feePercentage, feeRecipient, txData, ); const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); return tx; } - public async calculateMarketBuyFillAmountWeiAsync( - orders: SignedOrder[], - feeOrders: SignedOrder[], - feeProportion: number, - makerAssetFillAmount: BigNumber, - ): Promise { - const assetProxyId = assetDataUtils.decodeAssetProxyId(orders[0].makerAssetData); - switch (assetProxyId) { - case AssetProxyId.ERC20: { - const fillAmountWei = this._calculateMarketBuyERC20FillAmountAsync( - orders, - feeOrders, - feeProportion, - makerAssetFillAmount, - ); - return fillAmountWei; - } - case AssetProxyId.ERC721: { - const fillAmountWei = await this._calculateMarketBuyERC721FillAmountAsync( - orders, - feeOrders, - feeProportion, - ); - return fillAmountWei; - } - default: - throw new Error(`Invalid Asset Proxy Id: ${assetProxyId}`); - } - } - private async _calculateMarketBuyERC20FillAmountAsync( - orders: SignedOrder[], - feeOrders: SignedOrder[], - feeProportion: number, - makerAssetFillAmount: BigNumber, - ): Promise { - const makerAssetData = assetDataUtils.decodeAssetDataOrThrow(orders[0].makerAssetData); - const makerAssetToken = makerAssetData.tokenAddress; - const params = formatters.createMarketBuyOrders(orders, makerAssetFillAmount); - - let fillAmountWei; - if (makerAssetToken === this._zrxAddress) { - // If buying ZRX we buy the tokens and fees from the ZRX order in one step - const expectedBuyFeeTokensFillResults = await this._forwarderContract.calculateMarketBuyZrxResults.callAsync( - params.orders, - makerAssetFillAmount, - ); - if (expectedBuyFeeTokensFillResults.makerAssetFilledAmount.lessThan(makerAssetFillAmount)) { - throw new Error(INSUFFICENT_ORDERS_FOR_MAKER_AMOUNT); - } - fillAmountWei = expectedBuyFeeTokensFillResults.takerAssetFilledAmount; - } else { - const expectedMarketBuyFillResults = await this._forwarderContract.calculateMarketBuyResults.callAsync( - params.orders, - makerAssetFillAmount, - ); - if (expectedMarketBuyFillResults.makerAssetFilledAmount.lessThan(makerAssetFillAmount)) { - throw new Error(INSUFFICENT_ORDERS_FOR_MAKER_AMOUNT); - } - fillAmountWei = expectedMarketBuyFillResults.takerAssetFilledAmount; - const expectedFeeAmount = expectedMarketBuyFillResults.takerFeePaid; - if (expectedFeeAmount.greaterThan(ZERO_AMOUNT)) { - const expectedFeeFillFillAmountWei = await this._calculateMarketBuyERC20FillAmountAsync( - feeOrders, - [], - DEFAULT_FEE_PROPORTION, - expectedFeeAmount, - ); - fillAmountWei = fillAmountWei.plus(expectedFeeFillFillAmountWei); - } - } - fillAmountWei = ForwarderWrapper._calculateAdditionalFeeProportionAmount(feeProportion, fillAmountWei); - return fillAmountWei; - } - private async _calculateMarketBuyERC721FillAmountAsync( - orders: SignedOrder[], - feeOrders: SignedOrder[], - feeProportion: number, - ): Promise { - // Total cost when buying ERC721 is the total cost of all ERC721 orders + any fee abstraction - let fillAmountWei = _.reduce( - orders, - (totalAmount: BigNumber, order: SignedOrder) => { - return totalAmount.plus(order.takerAssetAmount); - }, - ZERO_AMOUNT, - ); - const totalFees = _.reduce( - orders, - (totalAmount: BigNumber, order: SignedOrder) => { - return totalAmount.plus(order.takerFee); - }, - ZERO_AMOUNT, - ); - if (totalFees.greaterThan(ZERO_AMOUNT)) { - // Calculate the ZRX fee abstraction cost - const emptyFeeOrders: SignedOrder[] = []; - const expectedFeeAmountWei = await this._calculateMarketBuyERC20FillAmountAsync( - feeOrders, - emptyFeeOrders, - DEFAULT_FEE_PROPORTION, - totalFees, - ); - fillAmountWei = fillAmountWei.plus(expectedFeeAmountWei); - } - fillAmountWei = ForwarderWrapper._calculateAdditionalFeeProportionAmount(feeProportion, fillAmountWei); - return fillAmountWei; + public async withdrawERC20Async( + tokenAddress: string, + amount: BigNumber, + txData: TxDataPayable, + ): Promise { + const txHash = await this._forwarderContract.withdrawERC20.sendTransactionAsync(tokenAddress, amount, txData); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; } } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 19f2b1a23..b96bdb182 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -216,7 +216,7 @@ export enum RevertReason { Erc20InsufficientBalance = 'ERC20_INSUFFICIENT_BALANCE', Erc20InsufficientAllowance = 'ERC20_INSUFFICIENT_ALLOWANCE', UnacceptableThreshold = 'UNACCEPTABLE_THRESHOLD', - FeeProportionTooLarge = 'FEE_PROPORTION_TOO_LARGE', + FeePercentageTooLarge = 'FEE_PERCENTAGE_TOO_LARGE', ValueGreaterThanZero = 'VALUE_GREATER_THAN_ZERO', InvalidMsgValue = 'INVALID_MSG_VALUE', } -- cgit v1.2.3 From 3506ec1caa2a6c7ee2d68970553ca53d8ceb5c64 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Tue, 17 Jul 2018 11:51:18 -0700 Subject: Use transferFrom instead of safeTransferFrom --- packages/contracts/src/2.0.0/forwarder/MixinAssets.sol | 7 ++----- packages/contracts/test/forwarder/forwarder.ts | 1 - 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol b/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol index cd150764e..44809ed85 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol @@ -163,15 +163,12 @@ contract MixinAssets is // Decode asset data. address token = assetData.readAddress(16); uint256 tokenId = assetData.readUint256(36); - bytes memory receiverData = assetData.readBytesWithLength(100); // Perform transfer. - // TODO: Do we want to use `transferFrom` here? - IERC721Token(token).safeTransferFrom( + IERC721Token(token).transferFrom( address(this), msg.sender, - tokenId, - receiverData + tokenId ); } } diff --git a/packages/contracts/test/forwarder/forwarder.ts b/packages/contracts/test/forwarder/forwarder.ts index f2966fe75..7b110b61f 100644 --- a/packages/contracts/test/forwarder/forwarder.ts +++ b/packages/contracts/test/forwarder/forwarder.ts @@ -18,7 +18,6 @@ import { constants } from '../utils/constants'; import { ERC20Wrapper } from '../utils/erc20_wrapper'; import { ERC721Wrapper } from '../utils/erc721_wrapper'; import { ExchangeWrapper } from '../utils/exchange_wrapper'; -import { formatters } from '../utils/formatters'; import { ForwarderWrapper } from '../utils/forwarder_wrapper'; import { OrderFactory } from '../utils/order_factory'; import { ContractName, ERC20BalancesByOwner } from '../utils/types'; -- cgit v1.2.3 From 4636d5fbc2ad3350278dc9fa2e780c2afa08d3d9 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Tue, 17 Jul 2018 16:58:00 -0700 Subject: Store orders length in varible before looping over orders --- .../src/2.0.0/forwarder/MixinForwarderCore.sol | 9 ++++--- .../protocol/Exchange/MixinWrapperFunctions.sol | 30 ++++++++++++++-------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol index 9dc203373..561507ce4 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol @@ -245,7 +245,8 @@ contract MixinForwarderCore is orders[0].takerAssetData = WETH_ASSET_DATA; // All orders are required to have the same makerAssetData. We save on gas by reusing the makerAssetData of the first order. - for (uint256 i = 0; i < orders.length; i++) { + uint256 ordersLength = orders.length; + for (uint256 i = 0; i < ordersLength; i++) { orders[i].makerAssetData = orders[0].makerAssetData; } @@ -274,7 +275,8 @@ contract MixinForwarderCore is bytes memory wethAssetData = WETH_ASSET_DATA; // All orders are required to have WETH as takerAssetData. We save on gas by populating the orders here, rather than passing in any extra calldata. - for (uint256 i = 0; i < orders.length; i++) { + uint256 ordersLength = orders.length; + for (uint256 i = 0; i < ordersLength; i++) { orders[i].takerAssetData = wethAssetData; } @@ -311,7 +313,8 @@ contract MixinForwarderCore is bytes memory wethAssetData = WETH_ASSET_DATA; uint256 zrxPurchased = 0; - for (uint256 i = 0; i < orders.length; i++) { + 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; diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol b/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol index 0abf53d02..adb56799a 100644 --- a/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol +++ b/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol @@ -119,7 +119,8 @@ contract MixinWrapperFunctions is public returns (FillResults memory totalFillResults) { - for (uint256 i = 0; i < orders.length; i++) { + uint256 ordersLength = orders.length; + for (uint256 i = 0; i < ordersLength; i++) { FillResults memory singleFillResults = fillOrder( orders[i], takerAssetFillAmounts[i], @@ -144,7 +145,8 @@ contract MixinWrapperFunctions is public returns (FillResults memory totalFillResults) { - for (uint256 i = 0; i < orders.length; i++) { + uint256 ordersLength = orders.length; + for (uint256 i = 0; i < ordersLength; i++) { FillResults memory singleFillResults = fillOrKillOrder( orders[i], takerAssetFillAmounts[i], @@ -170,7 +172,8 @@ contract MixinWrapperFunctions is public returns (FillResults memory totalFillResults) { - for (uint256 i = 0; i < orders.length; i++) { + uint256 ordersLength = orders.length; + for (uint256 i = 0; i < ordersLength; i++) { FillResults memory singleFillResults = fillOrderNoThrow( orders[i], takerAssetFillAmounts[i], @@ -196,7 +199,8 @@ contract MixinWrapperFunctions is { bytes memory takerAssetData = orders[0].takerAssetData; - for (uint256 i = 0; i < orders.length; i++) { + uint256 ordersLength = orders.length; + for (uint256 i = 0; i < ordersLength; i++) { // We assume that asset being sold by taker is the same for each order. // Rather than passing this in as calldata, we use the takerAssetData from the first order in all later orders. @@ -239,7 +243,8 @@ contract MixinWrapperFunctions is { bytes memory takerAssetData = orders[0].takerAssetData; - for (uint256 i = 0; i < orders.length; i++) { + uint256 ordersLength = orders.length; + for (uint256 i = 0; i < ordersLength; i++) { // We assume that asset being sold by taker is the same for each order. // Rather than passing this in as calldata, we use the takerAssetData from the first order in all later orders. @@ -281,7 +286,8 @@ contract MixinWrapperFunctions is { bytes memory makerAssetData = orders[0].makerAssetData; - for (uint256 i = 0; i < orders.length; i++) { + 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. // Rather than passing this in as calldata, we copy the makerAssetData from the first order onto all later orders. @@ -332,7 +338,8 @@ contract MixinWrapperFunctions is { bytes memory makerAssetData = orders[0].makerAssetData; - for (uint256 i = 0; i < orders.length; i++) { + 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. // Rather than passing this in as calldata, we copy the makerAssetData from the first order onto all later orders. @@ -372,7 +379,8 @@ contract MixinWrapperFunctions is function batchCancelOrders(LibOrder.Order[] memory orders) public { - for (uint256 i = 0; i < orders.length; i++) { + uint256 ordersLength = orders.length; + for (uint256 i = 0; i < ordersLength; i++) { cancelOrder(orders[i]); } } @@ -385,9 +393,9 @@ contract MixinWrapperFunctions is view returns (LibOrder.OrderInfo[] memory) { - uint256 length = orders.length; - LibOrder.OrderInfo[] memory ordersInfo = new LibOrder.OrderInfo[](length); - for (uint256 i = 0; i < length; i++) { + uint256 ordersLength = orders.length; + LibOrder.OrderInfo[] memory ordersInfo = new LibOrder.OrderInfo[](ordersLength); + for (uint256 i = 0; i < ordersLength; i++) { ordersInfo[i] = getOrderInfo(orders[i]); } return ordersInfo; -- cgit v1.2.3 From 0a976a3fb8792b83eaae621c0c78607c90344b8e Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Wed, 18 Jul 2018 13:31:29 -0700 Subject: Get actual gasPrice from transaction instead of setting default --- packages/contracts/test/forwarder/forwarder.ts | 69 +++++++++++++------------- packages/web3-wrapper/src/web3_wrapper.ts | 14 ++++++ 2 files changed, 48 insertions(+), 35 deletions(-) diff --git a/packages/contracts/test/forwarder/forwarder.ts b/packages/contracts/test/forwarder/forwarder.ts index 7b110b61f..f10f22556 100644 --- a/packages/contracts/test/forwarder/forwarder.ts +++ b/packages/contracts/test/forwarder/forwarder.ts @@ -27,8 +27,6 @@ chaiSetup.configure(); const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); const DECIMALS_DEFAULT = 18; -// Set a gasPrice so when checking balance of msg.sender we can accurately calculate gasPrice*gasUsed -const DEFAULT_GAS_PRICE = new BigNumber(1); describe(ContractName.Forwarder, () => { let makerAddress: string; @@ -60,6 +58,8 @@ describe(ContractName.Forwarder, () => { let erc721MakerAssetIds: BigNumber[]; let takerEthBalanceBefore: BigNumber; let feePercentage: BigNumber; + let gasPrice: BigNumber; + const MAX_WETH_FILL_PERCENTAGE = 95; before(async () => { @@ -67,6 +67,10 @@ describe(ContractName.Forwarder, () => { const accounts = await web3Wrapper.getAvailableAddressesAsync(); const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress, otherAddress] = accounts); + const txHash = await web3Wrapper.sendTransactionAsync({ from: accounts[0], to: accounts[0], value: 0 }); + const transaction = await web3Wrapper.getTransactionByHashAsync(txHash); + gasPrice = new BigNumber(transaction.gasPrice); + const erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); @@ -142,9 +146,6 @@ describe(ContractName.Forwarder, () => { await zrxToken.transfer.sendTransactionAsync(forwarderContract.address, zrxDepositAmount), ); erc20Wrapper.addTokenOwnerAddress(forwarderInstance.address); - - web3Wrapper.abiDecoder.addABI(forwarderContract.abi); - web3Wrapper.abiDecoder.addABI(exchangeInstance.abi); }); after(async () => { await blockchainLifecycle.revertAsync(); @@ -187,7 +188,7 @@ describe(ContractName.Forwarder, () => { const makerAssetFillAmount = primaryTakerAssetFillAmount .times(orderWithoutFee.makerAssetAmount) .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); - const totalEthSpent = primaryTakerAssetFillAmount.plus(DEFAULT_GAS_PRICE.times(tx.gasUsed)); + 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( @@ -232,7 +233,7 @@ describe(ContractName.Forwarder, () => { const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); const totalEthSpent = primaryTakerAssetFillAmount .plus(wethSpentOnFeeOrders) - .plus(DEFAULT_GAS_PRICE.times(tx.gasUsed)); + .plus(gasPrice.times(tx.gasUsed)); expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( @@ -268,7 +269,7 @@ describe(ContractName.Forwarder, () => { const newBalances = await erc20Wrapper.getBalancesAsync(); const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2); - const totalEthSpent = ethValue.plus(DEFAULT_GAS_PRICE.times(tx.gasUsed)); + const totalEthSpent = ethValue.plus(gasPrice.times(tx.gasUsed)); const takerFeePaid = orderWithFee.takerFee.dividedToIntegerBy(2); const makerFeePaid = orderWithFee.makerFee.dividedToIntegerBy(2); @@ -297,7 +298,7 @@ describe(ContractName.Forwarder, () => { from: takerAddress, }); const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const totalEthSpent = orderWithoutFee.takerAssetAmount.plus(DEFAULT_GAS_PRICE.times(tx.gasUsed)); + const totalEthSpent = orderWithoutFee.takerAssetAmount.plus(gasPrice.times(tx.gasUsed)); expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); }); @@ -335,7 +336,7 @@ describe(ContractName.Forwarder, () => { from: takerAddress, }); const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const totalEthSpent = erc20SignedOrder.takerAssetAmount.plus(DEFAULT_GAS_PRICE.times(tx.gasUsed)); + const totalEthSpent = erc20SignedOrder.takerAssetAmount.plus(gasPrice.times(tx.gasUsed)); expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); }); @@ -371,9 +372,7 @@ describe(ContractName.Forwarder, () => { .times(orderWithoutFee.makerAssetAmount) .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); const ethSpentOnFee = ForwarderWrapper.getPercentageOfValue(primaryTakerAssetFillAmount, baseFeePercentage); - const totalEthSpent = primaryTakerAssetFillAmount - .plus(ethSpentOnFee) - .plus(DEFAULT_GAS_PRICE.times(tx.gasUsed)); + 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( @@ -402,7 +401,7 @@ describe(ContractName.Forwarder, () => { forwarderWrapper.marketSellOrdersWithEthAsync( ordersWithoutFee, feeOrders, - { from: takerAddress, value: ethValue, gasPrice: DEFAULT_GAS_PRICE }, + { from: takerAddress, value: ethValue, gasPrice }, { feePercentage, feeRecipient: feeRecipientAddress }, ), RevertReason.FeePercentageTooLarge, @@ -427,7 +426,7 @@ describe(ContractName.Forwarder, () => { const newBalances = await erc20Wrapper.getBalancesAsync(); const primaryTakerAssetFillAmount = ethValue; - const totalEthSpent = primaryTakerAssetFillAmount.plus(DEFAULT_GAS_PRICE.times(tx.gasUsed)); + 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( @@ -460,7 +459,7 @@ describe(ContractName.Forwarder, () => { const newBalances = await erc20Wrapper.getBalancesAsync(); const primaryTakerAssetFillAmount = ethValue.dividedToIntegerBy(2); - const totalEthSpent = primaryTakerAssetFillAmount.plus(DEFAULT_GAS_PRICE.times(tx.gasUsed)); + 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( @@ -497,7 +496,7 @@ describe(ContractName.Forwarder, () => { const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); const totalEthSpent = primaryTakerAssetFillAmount .plus(wethSpentOnFeeOrders) - .plus(DEFAULT_GAS_PRICE.times(tx.gasUsed)); + .plus(gasPrice.times(tx.gasUsed)); expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( @@ -517,7 +516,7 @@ describe(ContractName.Forwarder, () => { }); it('should buy the exact amount of assets when buying ZRX with fee abstraction', async () => { orderWithFee = await orderFactory.newSignedOrderAsync({ - makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address), + makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), }); ordersWithFee = [orderWithFee]; @@ -536,7 +535,7 @@ describe(ContractName.Forwarder, () => { makerAssetFillAmount, ordersWithFee, ); - const totalEthSpent = primaryTakerAssetFillAmount.plus(DEFAULT_GAS_PRICE.times(tx.gasUsed)); + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); const makerAssetFilledAmount = orderWithFee.makerAssetAmount .times(primaryTakerAssetFillAmount) .dividedToIntegerBy(orderWithFee.takerAssetAmount); @@ -565,7 +564,7 @@ describe(ContractName.Forwarder, () => { }); // it('throws if fees are higher than 5% when buying zrx', async () => { // const highFeeZRXOrder = orderFactory.newSignedOrder({ - // makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address), + // makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), // makerAssetAmount: orderWithoutFee.makerAssetAmount, // takerFee: orderWithoutFee.makerAssetAmount.times(0.06), // }); @@ -663,7 +662,7 @@ describe(ContractName.Forwarder, () => { // const makerAssetId = erc721MakerAssetIds[0]; // orderWithoutFee = orderFactory.newSignedOrder({ // makerAssetAmount: new BigNumber(1), - // makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + // makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), // }); // feeOrders = []; // signedOrders = [orderWithoutFee]; @@ -686,7 +685,7 @@ describe(ContractName.Forwarder, () => { // orderWithoutFee = orderFactory.newSignedOrder({ // makerAssetAmount: new BigNumber(1), // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), - // makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + // makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), // }); // signedOrders = [orderWithoutFee]; // const makerAssetAmount = new BigNumber(signedOrders.length); @@ -708,7 +707,7 @@ describe(ContractName.Forwarder, () => { // orderWithoutFee = orderFactory.newSignedOrder({ // makerAssetAmount: new BigNumber(1), // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), - // makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + // makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), // }); // signedOrders = [orderWithoutFee]; // feePercentage = 100; @@ -728,7 +727,7 @@ describe(ContractName.Forwarder, () => { // { // from: takerAddress, // value: fillAmountWei, - // gasPrice: DEFAULT_GAS_PRICE, + // gasPrice: gasPrice, // }, // { // feePercentage, @@ -750,12 +749,12 @@ describe(ContractName.Forwarder, () => { // const signedOrder1 = orderFactory.newSignedOrder({ // makerAssetAmount: new BigNumber(1), // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), DECIMALS_DEFAULT), - // makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId1), + // makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId1), // }); // const signedOrder2 = orderFactory.newSignedOrder({ // makerAssetAmount: new BigNumber(1), // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(4), DECIMALS_DEFAULT), - // makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId2), + // makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId2), // }); // signedOrders = [signedOrder1, signedOrder2]; // feePercentage = 10; @@ -786,19 +785,19 @@ describe(ContractName.Forwarder, () => { // makerAssetAmount: erc721MakerAssetAmount, // takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT), // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(6), DECIMALS_DEFAULT), - // makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + // makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), // }); // signedOrders = [orderWithoutFee]; // const firstFeeOrder = orderFactory.newSignedOrder({ // makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(8), DECIMALS_DEFAULT), // takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.1), DECIMALS_DEFAULT), - // makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address), + // makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), // }); // const secondFeeOrder = orderFactory.newSignedOrder({ // makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(8), DECIMALS_DEFAULT), // takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.12), DECIMALS_DEFAULT), - // makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address), + // makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), // }); // feeOrders = [firstFeeOrder, secondFeeOrder]; @@ -831,7 +830,7 @@ describe(ContractName.Forwarder, () => { // tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { // from: takerAddress, // value: slippageFillAmountWei, - // gasPrice: DEFAULT_GAS_PRICE, + // gasPrice: gasPrice, // }); // const afterEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress); // const expectedEthBalanceAfterGasCosts = initEthBalance.minus(expectedFillAmountWei).minus(tx.gasUsed); @@ -850,20 +849,20 @@ describe(ContractName.Forwarder, () => { // makerAssetAmount: erc721MakerAssetAmount, // takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT), // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(6), DECIMALS_DEFAULT), - // makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + // makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), // }); // const zrxMakerAssetAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(8), DECIMALS_DEFAULT); // signedOrders = [orderWithoutFee]; // const firstFeeOrder = orderFactory.newSignedOrder({ // makerAssetAmount: zrxMakerAssetAmount, // takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.1), DECIMALS_DEFAULT), - // makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address), + // makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), // }); // const secondFeeOrder = orderFactory.newSignedOrder({ // makerAssetAmount: zrxMakerAssetAmount, // takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.12), DECIMALS_DEFAULT), - // makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address), + // makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), // }); // feeOrders = [firstFeeOrder, secondFeeOrder]; @@ -897,7 +896,7 @@ describe(ContractName.Forwarder, () => { // const makerAssetId = erc721MakerAssetIds[0]; // const erc721SignedOrder = orderFactory.newSignedOrder({ // makerAssetAmount: new BigNumber(1), - // makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + // makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), // }); // const erc20SignedOrder = orderFactory.newSignedOrder(); // signedOrders = [erc721SignedOrder, erc20SignedOrder]; @@ -915,7 +914,7 @@ describe(ContractName.Forwarder, () => { // const makerAssetId = erc721MakerAssetIds[0]; // const erc721SignedOrder = orderFactory.newSignedOrder({ // makerAssetAmount: new BigNumber(1), - // makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + // makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), // }); // const erc20SignedOrder = orderFactory.newSignedOrder(); // signedOrders = [erc20SignedOrder, erc721SignedOrder]; diff --git a/packages/web3-wrapper/src/web3_wrapper.ts b/packages/web3-wrapper/src/web3_wrapper.ts index 883a99bb4..dd35e2094 100644 --- a/packages/web3-wrapper/src/web3_wrapper.ts +++ b/packages/web3-wrapper/src/web3_wrapper.ts @@ -14,6 +14,7 @@ import { Provider, RawLogEntry, TraceParams, + Transaction, TransactionReceipt, TransactionReceiptWithDecodedLogs, TransactionTrace, @@ -220,6 +221,19 @@ export class Web3Wrapper { } return transactionReceipt; } + /** + * Retrieves the transaction data for a given transaction + * @param txHash Transaction hash + * @returns The raw transaction data + */ + public async getTransactionByHashAsync(txHash: string): Promise { + assert.isHexString('txHash', txHash); + const transaction = await this._sendRawPayloadAsync({ + method: 'eth_getTransactionByHash', + params: [txHash], + }); + return transaction; + } /** * Retrieves an accounts Ether balance in wei * @param owner Account whose balance you wish to check -- cgit v1.2.3 From d8099d53fe6ab7d2662ecd20219648016b12bcaa Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Wed, 18 Jul 2018 13:37:22 -0700 Subject: Update web3Wrapper CHANGELOG --- packages/web3-wrapper/CHANGELOG.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/web3-wrapper/CHANGELOG.json b/packages/web3-wrapper/CHANGELOG.json index d707ef486..975f83037 100644 --- a/packages/web3-wrapper/CHANGELOG.json +++ b/packages/web3-wrapper/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "1.1.0", + "changes": [ + { + "note": "Add `getTransactionByHashAsync` method", + "pr": 847 + } + ] + }, { "timestamp": 1532357734, "version": "1.0.1", -- cgit v1.2.3 From e90ed01105d58c008e31db7a5cfde57716c3c976 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Wed, 18 Jul 2018 16:02:16 -0700 Subject: Add assertValidFillResults --- .../src/2.0.0/forwarder/MixinForwarderCore.sol | 149 ++++++++++++--------- 1 file changed, 88 insertions(+), 61 deletions(-) diff --git a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol index 561507ce4..7a136b018 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol @@ -85,11 +85,12 @@ contract MixinForwarderCore is // Convert ETH to WETH. convertEthToWeth(); + uint256 wethSellAmount; + uint256 zrxBuyAmount; uint256 makerAssetAmountPurchased; - uint256 wethAvailable; if (orders[0].makerAssetData.equals(ZRX_ASSET_DATA)) { // Calculate amount of WETH that won't be spent on ETH fees. - wethAvailable = getPartialAmount( + wethSellAmount = getPartialAmount( PERCENTAGE_DENOMINATOR, safeAdd(PERCENTAGE_DENOMINATOR, feePercentage), msg.value @@ -98,14 +99,14 @@ contract MixinForwarderCore is // ZRX fees are paid with this contract's balance. orderFillResults = marketSellWeth( orders, - wethAvailable, + 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. - wethAvailable = getPartialAmount( + wethSellAmount = getPartialAmount( MAX_WETH_FILL_PERCENTAGE, PERCENTAGE_DENOMINATOR, msg.value @@ -114,23 +115,24 @@ contract MixinForwarderCore is // ZRX fees are payed with this contract's balance. orderFillResults = marketSellWeth( orders, - wethAvailable, + wethSellAmount, signatures ); // Buy back all ZRX spent on fees. + zrxBuyAmount = orderFillResults.takerFeePaid; feeOrderFillResults = marketBuyZrx( feeOrders, - orderFillResults.takerFeePaid, + zrxBuyAmount, feeSignatures ); makerAssetAmountPurchased = orderFillResults.makerAssetFilledAmount; } - // Ensure that no extra WETH owned by this contract has been sold. - uint256 totalWethSold = safeAdd(orderFillResults.takerAssetFilledAmount, feeOrderFillResults.takerAssetFilledAmount); - require( - totalWethSold <= msg.value, - "OVERSOLD_WETH" + // Ensure that all ZRX fees have been repurchased and no extra WETH owned by this contract has been sold. + assertValidFillResults( + orderFillResults, + feeOrderFillResults, + zrxBuyAmount ); // Transfer feePercentage of total ETH spent on primary orders to feeRecipient. @@ -181,6 +183,7 @@ contract MixinForwarderCore is // 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 @@ -201,19 +204,20 @@ contract MixinForwarderCore is signatures ); // Buy back all ZRX spent on fees. + zrxBuyAmount = orderFillResults.takerFeePaid; feeOrderFillResults = marketBuyZrx( feeOrders, - orderFillResults.takerFeePaid, + zrxBuyAmount, feeSignatures ); makerAssetAmountPurchased = orderFillResults.makerAssetFilledAmount; } - // Ensure that no extra WETH owned by this contract has been sold. - uint256 totalWethSold = safeAdd(orderFillResults.takerAssetFilledAmount, feeOrderFillResults.takerAssetFilledAmount); - require( - totalWethSold <= msg.value, - "OVERSOLD_WETH" + // Ensure that all ZRX fees have been repurchased and no extra WETH owned by this contract has been sold. + assertValidFillResults( + orderFillResults, + feeOrderFillResults, + zrxBuyAmount ); // Transfer feePercentage of total ETH spent on primary orders to feeRecipient. @@ -307,53 +311,76 @@ contract MixinForwarderCore is returns (FillResults memory totalFillResults) { // Do nothing if zrxBuyAmount == 0 - if (zrxBuyAmount > 0) { - - bytes memory zrxAssetData = ZRX_ASSET_DATA; - bytes memory wethAssetData = WETH_ASSET_DATA; - uint256 zrxPurchased = 0; - - uint256 ordersLength = orders.length; - for (uint256 i = 0; i < ordersLength; i++) { - - // All of these are ZRX/WETH, so we can drop the respective assetData from calldata. - orders[i].makerAssetData = zrxAssetData; - orders[i].takerAssetData = wethAssetData; - - // Calculate the remaining amount of ZRX to buy. - uint256 remainingZrxBuyAmount = safeSub(zrxBuyAmount, zrxPurchased); - - // Convert the remaining amount of ZRX to buy into remaining amount - // of WETH to sell, assuming entire amount can be sold in the current order. - uint256 remainingWethSellAmount = getPartialAmount( - orders[i].takerAssetAmount, - safeSub(orders[i].makerAssetAmount, orders[i].takerFee), // our exchange rate after fees - remainingZrxBuyAmount - ); - - // Attempt to sell the remaining amount of WETH. - FillResults memory singleFillResult = EXCHANGE.fillOrderNoThrow( - orders[i], - safeAdd(remainingWethSellAmount, 1), - 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; - } - } + if (zrxBuyAmount == 0) { + return totalFillResults; + } + + bytes memory zrxAssetData = ZRX_ASSET_DATA; + bytes memory wethAssetData = WETH_ASSET_DATA; + uint256 zrxPurchased = 0; + + uint256 ordersLength = orders.length; + for (uint256 i = 0; i < ordersLength; i++) { + + // All of these are ZRX/WETH, so we can drop the respective assetData from calldata. + orders[i].makerAssetData = zrxAssetData; + orders[i].takerAssetData = wethAssetData; + + // Calculate the remaining amount of ZRX to buy. + uint256 remainingZrxBuyAmount = safeSub(zrxBuyAmount, zrxPurchased); + + // Convert the remaining amount of ZRX to buy into remaining amount + // of WETH to sell, assuming entire amount can be sold in the current order. + uint256 remainingWethSellAmount = getPartialAmount( + orders[i].takerAssetAmount, + safeSub(orders[i].makerAssetAmount, orders[i].takerFee), // our exchange rate after fees + remainingZrxBuyAmount + ); - // Ensure that all ZRX spent while filling primary orders has been repurchased. - require( - zrxPurchased >= zrxBuyAmount, - "COMPLETE_FILL_FAILED" + // Attempt to sell the remaining amount of WETH. + FillResults memory singleFillResult = EXCHANGE.fillOrderNoThrow( + orders[i], + safeAdd(remainingWethSellAmount, 1), + 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; + } } + return totalFillResults; } + + /// @dev Ensures that all ZRX fees have been repurchased and no extra WETH owned by this contract has been sold. + /// @param orderFillResults Amounts filled and fees paid for primary orders. + /// @param feeOrderFillResults Amounts filled and fees paid for fee orders. + /// @param zrxBuyAmount The amount of ZRX that needed to be repurchased after filling primary orders. + function assertValidFillResults( + FillResults memory orderFillResults, + FillResults memory feeOrderFillResults, + uint256 zrxBuyAmount + ) + internal + view + { + // Ensure that all ZRX spent while filling primary orders has been repurchased. + uint256 zrxPurchased = safeSub(feeOrderFillResults.makerAssetFilledAmount, feeOrderFillResults.takerFeePaid); + require( + zrxPurchased >= zrxBuyAmount, + "COMPLETE_FILL_FAILED" + ); + + // Ensure that no extra WETH owned by this contract has been sold. + uint256 wethSold = safeAdd(orderFillResults.takerAssetFilledAmount, feeOrderFillResults.takerAssetFilledAmount); + require( + wethSold <= msg.value, + "OVERSOLD_WETH" + ); + } } -- cgit v1.2.3 From c5029e61e3bd147d8b177b60da43e76a32e57c6f Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Wed, 18 Jul 2018 16:09:13 -0700 Subject: Remove orders length check --- packages/contracts/src/2.0.0/forwarder/LibForwarderErrors.sol | 1 - packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol | 10 ---------- 2 files changed, 11 deletions(-) diff --git a/packages/contracts/src/2.0.0/forwarder/LibForwarderErrors.sol b/packages/contracts/src/2.0.0/forwarder/LibForwarderErrors.sol index af85c75e3..69108738a 100644 --- a/packages/contracts/src/2.0.0/forwarder/LibForwarderErrors.sol +++ b/packages/contracts/src/2.0.0/forwarder/LibForwarderErrors.sol @@ -31,5 +31,4 @@ contract LibForwarderErrors { 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. - string constant INVALID_ORDERS_LENGTH = "INVALID_ORDERS_LENGTH"; // Length of orders must be greater than 1. } diff --git a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol index 7a136b018..2ec2e2638 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol @@ -77,11 +77,6 @@ contract MixinForwarderCore is FillResults memory feeOrderFillResults ) { - require( - orders.length > 0, - "INVALID_ORDERS_LENGTH" - ); - // Convert ETH to WETH. convertEthToWeth(); @@ -175,11 +170,6 @@ contract MixinForwarderCore is FillResults memory feeOrderFillResults ) { - require( - orders.length > 0, - "INVALID_ORDERS_LENGTH" - ); - // Convert ETH to WETH. convertEthToWeth(); -- cgit v1.2.3 From e20f3a0f97d3d2029aea24e761d4689e0382756f Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Fri, 20 Jul 2018 08:01:18 -0700 Subject: Remove redundant external call by reimplementing fillOrderNoThrow --- .../contracts/src/2.0.0/forwarder/Forwarder.sol | 2 + .../src/2.0.0/forwarder/MixinExchangeWrapper.sol | 250 +++++++++++++++++++++ .../src/2.0.0/forwarder/MixinForwarderCore.sol | 134 +---------- .../2.0.0/forwarder/mixins/MExchangeWrapper.sol | 87 +++++++ .../src/2.0.0/forwarder/mixins/MForwarderCore.sol | 46 +--- .../protocol/Exchange/MixinWrapperFunctions.sol | 2 +- 6 files changed, 355 insertions(+), 166 deletions(-) create mode 100644 packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol create mode 100644 packages/contracts/src/2.0.0/forwarder/mixins/MExchangeWrapper.sol diff --git a/packages/contracts/src/2.0.0/forwarder/Forwarder.sol b/packages/contracts/src/2.0.0/forwarder/Forwarder.sol index 4405d5e46..d3721884c 100644 --- a/packages/contracts/src/2.0.0/forwarder/Forwarder.sol +++ b/packages/contracts/src/2.0.0/forwarder/Forwarder.sol @@ -23,6 +23,7 @@ import "./MixinWeth.sol"; import "./MixinForwarderCore.sol"; import "./MixinConstants.sol"; import "./MixinAssets.sol"; +import "./MixinExchangeWrapper.sol"; // solhint-disable no-empty-blocks @@ -30,6 +31,7 @@ contract Forwarder is MixinConstants, MixinWeth, MixinAssets, + MixinExchangeWrapper, MixinForwarderCore { diff --git a/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol b/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol new file mode 100644 index 000000000..5c0a606e6 --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol @@ -0,0 +1,250 @@ +/* + + 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 "./mixins/MConstants.sol"; +import "./mixins/MExchangeWrapper.sol"; +import "../protocol/Exchange/libs/LibAbiEncoder.sol"; +import "../protocol/Exchange/libs/LibOrder.sol"; +import "../protocol/Exchange/libs/LibFillResults.sol"; +import "../protocol/Exchange/libs/LibMath.sol"; + + +contract MixinExchangeWrapper is + LibAbiEncoder, + LibFillResults, + LibMath, + MConstants, + MExchangeWrapper +{ + + /// @dev Fills the input order. + /// Returns false if the transaction would otherwise revert. + /// @param order Order struct containing order specifications. + /// @param takerAssetFillAmount Desired amount of takerAsset to sell. + /// @param signature Proof that order has been created by maker. + /// @return Amounts filled and fees paid by maker and taker. + function fillOrderNoThrow( + LibOrder.Order memory order, + uint256 takerAssetFillAmount, + bytes memory signature + ) + internal + returns (FillResults memory fillResults) + { + // ABI encode calldata for `fillOrder` + bytes memory fillOrderCalldata = abiEncodeFillOrder( + order, + takerAssetFillAmount, + signature + ); + + address exchange = address(EXCHANGE); + + // Call `fillOrder` and handle any exceptions gracefully + assembly { + let success := call( + gas, // forward all gas, TODO: look into gas consumption of assert/throw + exchange, // call address of Exchange contract + 0, // transfer 0 wei + add(fillOrderCalldata, 32), // pointer to start of input (skip array length in first 32 bytes) + mload(fillOrderCalldata), // length of input + fillOrderCalldata, // write output over input + 128 // output size is 128 bytes + ) + switch success + case 0 { + mstore(fillResults, 0) + mstore(add(fillResults, 32), 0) + mstore(add(fillResults, 64), 0) + mstore(add(fillResults, 96), 0) + } + case 1 { + mstore(fillResults, mload(fillOrderCalldata)) + mstore(add(fillResults, 32), mload(add(fillOrderCalldata, 32))) + mstore(add(fillResults, 64), mload(add(fillOrderCalldata, 64))) + mstore(add(fillResults, 96), mload(add(fillOrderCalldata, 96))) + } + } + return fillResults; + } + + /// @dev Synchronously executes multiple calls of fillOrder until total amount of WETH has been sold by taker. + /// Returns false if the transaction would otherwise revert. + /// @param orders Array of order specifications. + /// @param wethSellAmount Desired amount of WETH to sell. + /// @param signatures Proofs that orders have been signed by makers. + /// @return Amounts filled and fees paid by makers and taker. + function marketSellWeth( + LibOrder.Order[] memory orders, + uint256 wethSellAmount, + bytes[] memory signatures + ) + internal + returns (FillResults memory totalFillResults) + { + bytes memory wethAssetData = WETH_ASSET_DATA; + + uint256 ordersLength = orders.length; + for (uint256 i = 0; i < ordersLength; i++) { + + // We assume that asset being sold by taker is WETH for each order. + 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 marketBuyWithWeth( + LibOrder.Order[] memory orders, + uint256 makerAssetFillAmount, + bytes[] memory signatures + ) + internal + returns (FillResults memory totalFillResults) + { + bytes memory makerAssetData = orders[0].makerAssetData; + bytes memory wethAssetData = WETH_ASSET_DATA; + + uint256 ordersLength = orders.length; + for (uint256 i = 0; i < ordersLength; i++) { + + // We assume that asset being bought by taker is the same for each order. + // We assume that asset being sold by taker is WETH for each order. + orders[i].makerAssetData = makerAssetData; + orders[i].takerAssetData = wethAssetData; + + // Calculate the remaining amount of makerAsset to buy + uint256 remainingMakerAssetFillAmount = safeSub(makerAssetFillAmount, totalFillResults.makerAssetFilledAmount); + + // Convert the remaining amount of makerAsset to buy into remaining amount + // of takerAsset to sell, assuming entire amount can be sold in the current order + uint256 remainingTakerAssetFillAmount = getPartialAmount( + orders[i].takerAssetAmount, + orders[i].makerAssetAmount, + remainingMakerAssetFillAmount + ); + + // Attempt to sell the remaining amount of takerAsset + FillResults memory singleFillResults = fillOrderNoThrow( + orders[i], + remainingTakerAssetFillAmount, + signatures[i] + ); + + // Update amounts filled and fees paid by maker and taker + addFillResults(totalFillResults, singleFillResults); + + // Stop execution if the entire amount of makerAsset has been bought + if (totalFillResults.makerAssetFilledAmount == makerAssetFillAmount) { + break; + } + } + 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 marketBuyZrxWithWeth( + LibOrder.Order[] memory orders, + uint256 zrxBuyAmount, + bytes[] memory signatures + ) + internal + returns (FillResults memory totalFillResults) + { + // Do nothing if zrxBuyAmount == 0 + if (zrxBuyAmount == 0) { + return totalFillResults; + } + + bytes memory zrxAssetData = ZRX_ASSET_DATA; + bytes memory wethAssetData = WETH_ASSET_DATA; + uint256 zrxPurchased = 0; + + uint256 ordersLength = orders.length; + for (uint256 i = 0; i < ordersLength; i++) { + + // All of these are ZRX/WETH, so we can drop the respective assetData from calldata. + orders[i].makerAssetData = zrxAssetData; + orders[i].takerAssetData = wethAssetData; + + // Calculate the remaining amount of ZRX to buy. + uint256 remainingZrxBuyAmount = safeSub(zrxBuyAmount, zrxPurchased); + + // Convert the remaining amount of ZRX to buy into remaining amount + // of WETH to sell, assuming entire amount can be sold in the current order. + uint256 remainingWethSellAmount = getPartialAmount( + orders[i].takerAssetAmount, + safeSub(orders[i].makerAssetAmount, orders[i].takerFee), // our exchange rate after fees + remainingZrxBuyAmount + ); + + // Attempt to sell the remaining amount of WETH. + FillResults memory singleFillResult = fillOrderNoThrow( + orders[i], + safeAdd(remainingWethSellAmount, 1), + 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; + } + } + + return totalFillResults; + } +} \ No newline at end of file diff --git a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol index 2ec2e2638..e9acef7a2 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol @@ -22,6 +22,7 @@ pragma experimental ABIEncoderV2; import "./mixins/MWeth.sol"; import "./mixins/MAssets.sol"; import "./mixins/MConstants.sol"; +import "./mixins/MExchangeWrapper.sol"; import "./mixins/MForwarderCore.sol"; import "../utils/LibBytes/LibBytes.sol"; import "../protocol/Exchange/libs/LibOrder.sol"; @@ -35,6 +36,7 @@ contract MixinForwarderCore is MConstants, MWeth, MAssets, + MExchangeWrapper, MForwarderCore { @@ -115,7 +117,7 @@ contract MixinForwarderCore is ); // Buy back all ZRX spent on fees. zrxBuyAmount = orderFillResults.takerFeePaid; - feeOrderFillResults = marketBuyZrx( + feeOrderFillResults = marketBuyZrxWithWeth( feeOrders, zrxBuyAmount, feeSignatures @@ -178,7 +180,7 @@ contract MixinForwarderCore is 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 = marketBuyZrx( + orderFillResults = marketBuyZrxWithWeth( orders, makerAssetFillAmount, signatures @@ -188,14 +190,14 @@ contract MixinForwarderCore is } else { // Attemp to purchase desired amount of makerAsset. // ZRX fees are payed with this contract's balance. - orderFillResults = marketBuyAsset( + orderFillResults = marketBuyWithWeth( orders, makerAssetFillAmount, signatures ); // Buy back all ZRX spent on fees. zrxBuyAmount = orderFillResults.takerFeePaid; - feeOrderFillResults = marketBuyZrx( + feeOrderFillResults = marketBuyZrxWithWeth( feeOrders, zrxBuyAmount, feeSignatures @@ -223,130 +225,6 @@ contract MixinForwarderCore is transferPurchasedAssetToSender(orders[0].makerAssetData, makerAssetAmountPurchased); } - /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. - /// @param wethSellAmount Desired amount of WETH to sell. - /// @param signatures Proofs that orders have been created by makers. - /// @return Amounts filled and fees paid by maker and taker. - function marketSellWeth( - LibOrder.Order[] memory orders, - uint256 wethSellAmount, - bytes[] memory signatures - ) - internal - returns (FillResults memory fillResults) - { - // `marketSellOrders` uses the first order's takerAssetData for all passed in orders. - orders[0].takerAssetData = WETH_ASSET_DATA; - - // All orders are required to have the same makerAssetData. We save on gas by reusing the makerAssetData of the first order. - uint256 ordersLength = orders.length; - for (uint256 i = 0; i < ordersLength; i++) { - orders[i].makerAssetData = orders[0].makerAssetData; - } - - // Sell WETH until entire amount has been sold or all orders have been filled. - fillResults = EXCHANGE.marketSellOrdersNoThrow( - orders, - wethSellAmount, - signatures - ); - - return fillResults; - } - - /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. - /// @param makerAssetFillAmount Desired amount of makerAsset to buy. - /// @param signatures Proofs that orders have been created by makers. - /// @return Amounts filled and fees paid by maker and taker. - function marketBuyAsset( - LibOrder.Order[] memory orders, - uint256 makerAssetFillAmount, - bytes[] memory signatures - ) - internal - returns (FillResults memory fillResults) - { - bytes memory wethAssetData = WETH_ASSET_DATA; - - // All orders are required to have WETH as takerAssetData. We save on gas by populating the orders here, rather than passing in any extra calldata. - uint256 ordersLength = orders.length; - for (uint256 i = 0; i < ordersLength; i++) { - orders[i].takerAssetData = wethAssetData; - } - - // Purchase makerAsset until entire amount has been bought or all orders have been filled. - fillResults = EXCHANGE.marketBuyOrdersNoThrow( - orders, - makerAssetFillAmount, - signatures - ); - - return fillResults; - } - - /// @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. - /// @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 marketBuyZrx( - LibOrder.Order[] memory orders, - uint256 zrxBuyAmount, - bytes[] memory signatures - ) - internal - returns (FillResults memory totalFillResults) - { - // Do nothing if zrxBuyAmount == 0 - if (zrxBuyAmount == 0) { - return totalFillResults; - } - - bytes memory zrxAssetData = ZRX_ASSET_DATA; - bytes memory wethAssetData = WETH_ASSET_DATA; - uint256 zrxPurchased = 0; - - uint256 ordersLength = orders.length; - for (uint256 i = 0; i < ordersLength; i++) { - - // All of these are ZRX/WETH, so we can drop the respective assetData from calldata. - orders[i].makerAssetData = zrxAssetData; - orders[i].takerAssetData = wethAssetData; - - // Calculate the remaining amount of ZRX to buy. - uint256 remainingZrxBuyAmount = safeSub(zrxBuyAmount, zrxPurchased); - - // Convert the remaining amount of ZRX to buy into remaining amount - // of WETH to sell, assuming entire amount can be sold in the current order. - uint256 remainingWethSellAmount = getPartialAmount( - orders[i].takerAssetAmount, - safeSub(orders[i].makerAssetAmount, orders[i].takerFee), // our exchange rate after fees - remainingZrxBuyAmount - ); - - // Attempt to sell the remaining amount of WETH. - FillResults memory singleFillResult = EXCHANGE.fillOrderNoThrow( - orders[i], - safeAdd(remainingWethSellAmount, 1), - 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; - } - } - - return totalFillResults; - } - /// @dev Ensures that all ZRX fees have been repurchased and no extra WETH owned by this contract has been sold. /// @param orderFillResults Amounts filled and fees paid for primary orders. /// @param feeOrderFillResults Amounts filled and fees paid for fee orders. diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MExchangeWrapper.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MExchangeWrapper.sol new file mode 100644 index 000000000..0fa90fd82 --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/mixins/MExchangeWrapper.sol @@ -0,0 +1,87 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; +pragma experimental ABIEncoderV2; + +import "../../protocol/Exchange/libs/LibOrder.sol"; +import "../../protocol/Exchange/libs/LibFillResults.sol"; + + +contract MExchangeWrapper { + + /// @dev Fills the input order. + /// Returns false if the transaction would otherwise revert. + /// @param order Order struct containing order specifications. + /// @param takerAssetFillAmount Desired amount of takerAsset to sell. + /// @param signature Proof that order has been created by maker. + /// @return Amounts filled and fees paid by maker and taker. + function fillOrderNoThrow( + LibOrder.Order memory order, + uint256 takerAssetFillAmount, + bytes memory signature + ) + internal + returns (LibFillResults.FillResults memory fillResults); + + /// @dev Synchronously executes multiple calls of fillOrder until total amount of WETH has been sold by taker. + /// Returns false if the transaction would otherwise revert. + /// @param orders Array of order specifications. + /// @param wethSellAmount Desired amount of WETH to sell. + /// @param signatures Proofs that orders have been signed by makers. + /// @return Amounts filled and fees paid by makers and taker. + function marketSellWeth( + LibOrder.Order[] memory orders, + uint256 wethSellAmount, + bytes[] memory signatures + ) + internal + returns (LibFillResults.FillResults memory totalFillResults); + + /// @dev Synchronously executes multiple fill orders in a single transaction until total amount is bought by taker. + /// Returns false if the transaction would otherwise revert. + /// The asset being sold by taker must always be WETH. + /// @param orders Array of order specifications. + /// @param makerAssetFillAmount Desired amount of makerAsset to buy. + /// @param signatures Proofs that orders have been signed by makers. + /// @return Amounts filled and fees paid by makers and taker. + function marketBuyWithWeth( + 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 marketBuyZrxWithWeth( + LibOrder.Order[] memory orders, + uint256 zrxBuyAmount, + bytes[] memory signatures + ) + internal + returns (LibFillResults.FillResults memory totalFillResults); +} \ No newline at end of file diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MForwarderCore.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MForwarderCore.sol index 9dee15481..0f5cd9c66 100644 --- a/packages/contracts/src/2.0.0/forwarder/mixins/MForwarderCore.sol +++ b/packages/contracts/src/2.0.0/forwarder/mixins/MForwarderCore.sol @@ -28,43 +28,15 @@ contract MForwarderCore is IForwarderCore { - /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. - /// @param wethSellAmount Desired amount of WETH to sell. - /// @param signatures Proofs that orders have been created by makers. - /// @return Amounts filled and fees paid by maker and taker. - function marketSellWeth( - LibOrder.Order[] memory orders, - uint256 wethSellAmount, - bytes[] memory signatures + /// @dev Ensures that all ZRX fees have been repurchased and no extra WETH owned by this contract has been sold. + /// @param orderFillResults Amounts filled and fees paid for primary orders. + /// @param feeOrderFillResults Amounts filled and fees paid for fee orders. + /// @param zrxBuyAmount The amount of ZRX that needed to be repurchased after filling primary orders. + function assertValidFillResults( + LibFillResults.FillResults memory orderFillResults, + LibFillResults.FillResults memory feeOrderFillResults, + uint256 zrxBuyAmount ) internal - returns (LibFillResults.FillResults memory fillResults); - - /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. - /// @param makerAssetFillAmount Desired amount of makerAsset to buy. - /// @param signatures Proofs that orders have been created by makers. - /// @return Amounts filled and fees paid by maker and taker. - function marketBuyAsset( - LibOrder.Order[] memory orders, - uint256 makerAssetFillAmount, - bytes[] memory signatures - ) - internal - returns (LibFillResults.FillResults memory fillResults); - - /// @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. - /// @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 marketBuyZrx( - LibOrder.Order[] memory orders, - uint256 zrxBuyAmount, - bytes[] memory signatures - ) - internal - returns (LibFillResults.FillResults memory totalFillResults); + view; } diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol b/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol index adb56799a..a0882b108 100644 --- a/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol +++ b/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol @@ -57,7 +57,7 @@ contract MixinWrapperFunctions is return fillResults; } - /// @dev Fills an order with specified parameters and ECDSA signature. + /// @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. -- cgit v1.2.3 From 45d68285f14ba328e1bf5a8cc9e59cdb39d7f306 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Sun, 22 Jul 2018 19:35:20 -0500 Subject: Remove MConstants and MixinConstants for LibConstants --- .../contracts/src/2.0.0/forwarder/Forwarder.sol | 6 +-- .../src/2.0.0/forwarder/LibForwarderErrors.sol | 34 ------------- .../contracts/src/2.0.0/forwarder/MixinAssets.sol | 4 +- .../src/2.0.0/forwarder/MixinConstants.sol | 50 ------------------- .../src/2.0.0/forwarder/MixinExchangeWrapper.sol | 4 +- .../src/2.0.0/forwarder/MixinForwarderCore.sol | 4 +- .../contracts/src/2.0.0/forwarder/MixinWeth.sol | 4 +- .../src/2.0.0/forwarder/libs/LibConstants.sol | 58 ++++++++++++++++++++++ .../2.0.0/forwarder/libs/LibForwarderErrors.sol | 34 +++++++++++++ .../src/2.0.0/forwarder/mixins/MConstants.sol | 42 ---------------- 10 files changed, 103 insertions(+), 137 deletions(-) delete mode 100644 packages/contracts/src/2.0.0/forwarder/LibForwarderErrors.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/MixinConstants.sol create mode 100644 packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol create mode 100644 packages/contracts/src/2.0.0/forwarder/libs/LibForwarderErrors.sol delete mode 100644 packages/contracts/src/2.0.0/forwarder/mixins/MConstants.sol diff --git a/packages/contracts/src/2.0.0/forwarder/Forwarder.sol b/packages/contracts/src/2.0.0/forwarder/Forwarder.sol index d3721884c..5b88b05b1 100644 --- a/packages/contracts/src/2.0.0/forwarder/Forwarder.sol +++ b/packages/contracts/src/2.0.0/forwarder/Forwarder.sol @@ -21,14 +21,14 @@ pragma experimental ABIEncoderV2; import "./MixinWeth.sol"; import "./MixinForwarderCore.sol"; -import "./MixinConstants.sol"; +import "./libs/LibConstants.sol"; import "./MixinAssets.sol"; import "./MixinExchangeWrapper.sol"; // solhint-disable no-empty-blocks contract Forwarder is - MixinConstants, + LibConstants, MixinWeth, MixinAssets, MixinExchangeWrapper, @@ -43,7 +43,7 @@ contract Forwarder is bytes memory _wethAssetData ) public - MixinConstants( + LibConstants( _exchange, _etherToken, _zrxToken, diff --git a/packages/contracts/src/2.0.0/forwarder/LibForwarderErrors.sol b/packages/contracts/src/2.0.0/forwarder/LibForwarderErrors.sol deleted file mode 100644 index 69108738a..000000000 --- a/packages/contracts/src/2.0.0/forwarder/LibForwarderErrors.sol +++ /dev/null @@ -1,34 +0,0 @@ -/* - - Copyright 2018 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -// solhint-disable -pragma solidity 0.4.24; - - -/// This contract is intended to serve as a reference, but is not actually used for efficiency reasons. -contract LibForwarderErrors { - string constant FEE_PERCENTAGE_TOO_LARGE = "FEE_PROPORTION_TOO_LARGE"; // Provided fee percentage greater than 5%. - string constant MAX_FEE_EXCEEDED = "MAX_FEE_EXCEEDED"; // Not enough ETH remaining to pay feeRecipient. - string constant OVERSOLD_WETH = "OVERSOLD_WETH"; // More WETH sold than provided with current message call. - string constant COMPLETE_FILL_FAILED = "COMPLETE_FILL_FAILED"; // Desired purchase amount not completely filled (required for ZRX fees only). - string constant TRANSFER_FAILED = "TRANSFER_FAILED"; // Asset transfer failed. - string constant UNSUPPORTED_TOKEN_PROXY = "UNSUPPORTED_TOKEN_PROXY"; // Proxy in assetData not supported. - string constant DEFAULT_FUNCTION_WETH_CONTRACT_ONLY = "DEFAULT_FUNCTION_WETH_CONTRACT_ONLY"; // Fallback function may only be used for WETH withdrawals. - string constant INVALID_MSG_VALUE = "INVALID_MSG_VALUE"; // msg.value must be greater than 0. - string constant INVALID_AMOUNT = "INVALID_AMOUNT"; // Amount must equal 1. -} diff --git a/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol b/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol index 44809ed85..084a28550 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol @@ -22,13 +22,13 @@ import "../utils/LibBytes/LibBytes.sol"; import "../utils/Ownable/Ownable.sol"; import "../tokens/ERC20Token/IERC20Token.sol"; import "../tokens/ERC721Token/IERC721Token.sol"; +import "./libs/LibConstants.sol"; import "./mixins/MAssets.sol"; -import "./mixins/MConstants.sol"; contract MixinAssets is Ownable, - MConstants, + LibConstants, MAssets { diff --git a/packages/contracts/src/2.0.0/forwarder/MixinConstants.sol b/packages/contracts/src/2.0.0/forwarder/MixinConstants.sol deleted file mode 100644 index e430aba41..000000000 --- a/packages/contracts/src/2.0.0/forwarder/MixinConstants.sol +++ /dev/null @@ -1,50 +0,0 @@ -/* - - Copyright 2018 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity 0.4.24; - -import "./mixins/MConstants.sol"; - - -contract MixinConstants is - MConstants -{ - - bytes4 constant internal ERC20_DATA_ID = bytes4(keccak256("ERC20Token(address)")); - bytes4 constant internal ERC721_DATA_ID = bytes4(keccak256("ERC721Token(address,uint256,bytes)")); - 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% - - constructor ( - address _exchange, - address _etherToken, - address _zrxToken, - bytes memory _zrxAssetData, - bytes memory _wethAssetData - ) - public - { - EXCHANGE = IExchange(_exchange); - ETHER_TOKEN = IEtherToken(_etherToken); - ZRX_TOKEN = IERC20Token(_zrxToken); - ZRX_ASSET_DATA = _zrxAssetData; - WETH_ASSET_DATA = _wethAssetData; - } -} diff --git a/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol b/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol index 5c0a606e6..d80e06350 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol @@ -19,7 +19,7 @@ pragma solidity 0.4.24; pragma experimental ABIEncoderV2; -import "./mixins/MConstants.sol"; +import "./libs/LibConstants.sol"; import "./mixins/MExchangeWrapper.sol"; import "../protocol/Exchange/libs/LibAbiEncoder.sol"; import "../protocol/Exchange/libs/LibOrder.sol"; @@ -31,7 +31,7 @@ contract MixinExchangeWrapper is LibAbiEncoder, LibFillResults, LibMath, - MConstants, + LibConstants, MExchangeWrapper { diff --git a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol index e9acef7a2..1164ae919 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol @@ -19,9 +19,9 @@ pragma solidity 0.4.24; pragma experimental ABIEncoderV2; +import "./libs/LibConstants.sol"; import "./mixins/MWeth.sol"; import "./mixins/MAssets.sol"; -import "./mixins/MConstants.sol"; import "./mixins/MExchangeWrapper.sol"; import "./mixins/MForwarderCore.sol"; import "../utils/LibBytes/LibBytes.sol"; @@ -33,7 +33,7 @@ import "../protocol/Exchange/libs/LibMath.sol"; contract MixinForwarderCore is LibFillResults, LibMath, - MConstants, + LibConstants, MWeth, MAssets, MExchangeWrapper, diff --git a/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol b/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol index b566c3ef6..a50863a59 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol @@ -19,13 +19,13 @@ pragma solidity 0.4.24; import "../protocol/Exchange/libs/LibMath.sol"; -import "./mixins/MConstants.sol"; +import "./libs/LibConstants.sol"; import "./mixins/MWeth.sol"; contract MixinWeth is LibMath, - MConstants, + LibConstants, MWeth { diff --git a/packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol b/packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol new file mode 100644 index 000000000..8abe1e42d --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol @@ -0,0 +1,58 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; + +import "../../protocol/Exchange/interfaces/IExchange.sol"; +import "../../tokens/EtherToken/IEtherToken.sol"; +import "../../tokens/ERC20Token/IERC20Token.sol"; + + +contract LibConstants { + + bytes4 constant internal ERC20_DATA_ID = bytes4(keccak256("ERC20Token(address)")); + bytes4 constant internal ERC721_DATA_ID = bytes4(keccak256("ERC721Token(address,uint256,bytes)")); + 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, + address _etherToken, + address _zrxToken, + bytes memory _zrxAssetData, + bytes memory _wethAssetData + ) + public + { + EXCHANGE = IExchange(_exchange); + ETHER_TOKEN = IEtherToken(_etherToken); + ZRX_TOKEN = IERC20Token(_zrxToken); + ZRX_ASSET_DATA = _zrxAssetData; + WETH_ASSET_DATA = _wethAssetData; + } +} diff --git a/packages/contracts/src/2.0.0/forwarder/libs/LibForwarderErrors.sol b/packages/contracts/src/2.0.0/forwarder/libs/LibForwarderErrors.sol new file mode 100644 index 000000000..69108738a --- /dev/null +++ b/packages/contracts/src/2.0.0/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 MAX_FEE_EXCEEDED = "MAX_FEE_EXCEEDED"; // Not enough ETH remaining to pay feeRecipient. + string constant OVERSOLD_WETH = "OVERSOLD_WETH"; // More WETH sold than provided with current message call. + string constant COMPLETE_FILL_FAILED = "COMPLETE_FILL_FAILED"; // Desired purchase amount not completely filled (required for ZRX fees only). + string constant TRANSFER_FAILED = "TRANSFER_FAILED"; // Asset transfer failed. + string constant UNSUPPORTED_TOKEN_PROXY = "UNSUPPORTED_TOKEN_PROXY"; // Proxy in assetData not supported. + string constant DEFAULT_FUNCTION_WETH_CONTRACT_ONLY = "DEFAULT_FUNCTION_WETH_CONTRACT_ONLY"; // Fallback function may only be used for WETH withdrawals. + string constant INVALID_MSG_VALUE = "INVALID_MSG_VALUE"; // msg.value must be greater than 0. + string constant INVALID_AMOUNT = "INVALID_AMOUNT"; // Amount must equal 1. +} diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MConstants.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MConstants.sol deleted file mode 100644 index 712a11c5d..000000000 --- a/packages/contracts/src/2.0.0/forwarder/mixins/MConstants.sol +++ /dev/null @@ -1,42 +0,0 @@ -/* - - Copyright 2018 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity 0.4.24; - -import "../../protocol/Exchange/interfaces/IExchange.sol"; -import "../../tokens/EtherToken/IEtherToken.sol"; -import "../../tokens/ERC20Token/IERC20Token.sol"; - - -contract MConstants { - - bytes4 constant internal ERC20_DATA_ID = bytes4(keccak256("ERC20Token(address)")); - bytes4 constant internal ERC721_DATA_ID = bytes4(keccak256("ERC721Token(address,uint256,bytes)")); - 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 -} -- cgit v1.2.3 From dcc0908617b83de8f45af3e9ca6854b897be3d72 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Sun, 22 Jul 2018 23:04:49 -0500 Subject: Add more tests and fixes --- .../src/2.0.0/forwarder/MixinExchangeWrapper.sol | 5 +- .../contracts/src/2.0.0/forwarder/MixinWeth.sol | 2 +- .../src/2.0.0/forwarder/libs/LibConstants.sol | 2 +- .../2.0.0/forwarder/libs/LibForwarderErrors.sol | 2 +- .../src/2.0.0/utils/SafeMath/SafeMath.sol | 2 +- packages/contracts/test/forwarder/forwarder.ts | 877 +++++++++++---------- packages/contracts/test/utils/forwarder_wrapper.ts | 4 +- packages/types/src/index.ts | 2 +- 8 files changed, 484 insertions(+), 412 deletions(-) diff --git a/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol b/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol index d80e06350..e230767b7 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol @@ -100,12 +100,15 @@ contract MixinExchangeWrapper is 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 @@ -231,7 +234,7 @@ contract MixinExchangeWrapper is // Attempt to sell the remaining amount of WETH. FillResults memory singleFillResult = fillOrderNoThrow( orders[i], - safeAdd(remainingWethSellAmount, 1), + safeAdd(remainingWethSellAmount, 1), // we add 1 wei to the fill amount to make up for rounding errors signatures[i] ); diff --git a/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol b/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol index a50863a59..8ba236e7f 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol @@ -87,7 +87,7 @@ contract MixinWeth is // Ensure fee is less than amount of WETH remaining. require( ethFee <= wethRemaining, - "MAX_FEE_EXCEEDED" + "INSUFFICIENT_ETH_REMAINING" ); // Do nothing if no WETH remaining diff --git a/packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol b/packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol index 8abe1e42d..c26d7902c 100644 --- a/packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol +++ b/packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol @@ -26,7 +26,7 @@ import "../../tokens/ERC20Token/IERC20Token.sol"; contract LibConstants { bytes4 constant internal ERC20_DATA_ID = bytes4(keccak256("ERC20Token(address)")); - bytes4 constant internal ERC721_DATA_ID = bytes4(keccak256("ERC721Token(address,uint256,bytes)")); + 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% diff --git a/packages/contracts/src/2.0.0/forwarder/libs/LibForwarderErrors.sol b/packages/contracts/src/2.0.0/forwarder/libs/LibForwarderErrors.sol index 69108738a..cdfb77a0b 100644 --- a/packages/contracts/src/2.0.0/forwarder/libs/LibForwarderErrors.sol +++ b/packages/contracts/src/2.0.0/forwarder/libs/LibForwarderErrors.sol @@ -23,7 +23,7 @@ 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 MAX_FEE_EXCEEDED = "MAX_FEE_EXCEEDED"; // Not enough ETH remaining to pay feeRecipient. + 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. diff --git a/packages/contracts/src/2.0.0/utils/SafeMath/SafeMath.sol b/packages/contracts/src/2.0.0/utils/SafeMath/SafeMath.sol index 190989181..63a2a085f 100644 --- a/packages/contracts/src/2.0.0/utils/SafeMath/SafeMath.sol +++ b/packages/contracts/src/2.0.0/utils/SafeMath/SafeMath.sol @@ -34,7 +34,7 @@ contract SafeMath { { require( b <= a, - "UINT256_OVERFLOW" + "UINT256_UNDERFLOW" ); return a - b; } diff --git a/packages/contracts/test/forwarder/forwarder.ts b/packages/contracts/test/forwarder/forwarder.ts index f10f22556..b6d81df58 100644 --- a/packages/contracts/test/forwarder/forwarder.ts +++ b/packages/contracts/test/forwarder/forwarder.ts @@ -27,8 +27,9 @@ chaiSetup.configure(); const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); const DECIMALS_DEFAULT = 18; +const MAX_WETH_FILL_PERCENTAGE = 95; -describe(ContractName.Forwarder, () => { +describe.only(ContractName.Forwarder, () => { let makerAddress: string; let owner: string; let takerAddress: string; @@ -45,11 +46,8 @@ describe(ContractName.Forwarder, () => { let exchangeWrapper: ExchangeWrapper; let orderWithoutFee: SignedOrder; - let ordersWithoutFee: SignedOrder[]; let orderWithFee: SignedOrder; - let ordersWithFee: SignedOrder[]; let feeOrder: SignedOrder; - let feeOrders: SignedOrder[]; let orderFactory: OrderFactory; let erc20Wrapper: ERC20Wrapper; let erc20Balances: ERC20BalancesByOwner; @@ -60,8 +58,6 @@ describe(ContractName.Forwarder, () => { let feePercentage: BigNumber; let gasPrice: BigNumber; - const MAX_WETH_FILL_PERCENTAGE = 95; - before(async () => { await blockchainLifecycle.startAsync(); const accounts = await web3Wrapper.getAvailableAddressesAsync(); @@ -168,9 +164,9 @@ describe(ContractName.Forwarder, () => { }); describe('marketSellOrdersWithEth without extra fees', () => { - it('should fill the order', async () => { - ordersWithoutFee = [orderWithoutFee]; - feeOrders = []; + 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, { @@ -206,9 +202,54 @@ describe(ContractName.Forwarder, () => { ); expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); - it('should fill the order and pay ZRX fees from feeOrders', async () => { - ordersWithFee = [orderWithFee]; - feeOrders = [feeOrder]; + 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, { @@ -251,13 +292,71 @@ describe(ContractName.Forwarder, () => { ); 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), }); - ordersWithFee = [orderWithFee]; - feeOrders = []; + const ordersWithFee = [orderWithFee]; + const feeOrders: SignedOrder[] = []; const ethValue = orderWithFee.takerAssetAmount.dividedToIntegerBy(2); tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { @@ -290,7 +389,8 @@ describe(ContractName.Forwarder, () => { expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); it('should refund remaining ETH if amount is greater than takerAssetAmount', async () => { - ordersWithoutFee = [orderWithoutFee]; + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; const ethValue = orderWithoutFee.takerAssetAmount.times(2); tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { @@ -306,12 +406,12 @@ describe(ContractName.Forwarder, () => { orderWithFee = await orderFactory.newSignedOrderAsync({ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), DECIMALS_DEFAULT), }); - ordersWithFee = [orderWithFee]; + const ordersWithFee = [orderWithFee]; feeOrder = await orderFactory.newSignedOrderAsync({ makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), }); - feeOrders = [feeOrder]; + const feeOrders = [feeOrder]; const ethValue = orderWithFee.takerAssetAmount; return expectTransactionFailedAsync( forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { @@ -328,7 +428,8 @@ describe(ContractName.Forwarder, () => { makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), }); const erc20SignedOrder = await orderFactory.newSignedOrderAsync(); - ordersWithoutFee = [erc20SignedOrder, erc721SignedOrder]; + const ordersWithoutFee = [erc20SignedOrder, erc721SignedOrder]; + const feeOrders: SignedOrder[] = []; const ethValue = erc20SignedOrder.takerAssetAmount.plus(erc721SignedOrder.takerAssetAmount); tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { @@ -343,8 +444,8 @@ describe(ContractName.Forwarder, () => { }); describe('marketSellOrdersWithEth with extra fees', () => { it('should fill the order and send fee to feeRecipient', async () => { - ordersWithoutFee = [orderWithoutFee]; - feeOrders = []; + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; const ethValue = orderWithoutFee.takerAssetAmount.div(2); const baseFeePercentage = 2; @@ -392,11 +493,11 @@ describe(ContractName.Forwarder, () => { expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); it('should fail if the fee is set too high', async () => { - const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); const ethValue = orderWithoutFee.takerAssetAmount.div(2); const baseFeePercentage = 6; feePercentage = ForwarderWrapper.getPercentageOfValue(ethValue, baseFeePercentage); - feeOrders = []; + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; await expectTransactionFailedAsync( forwarderWrapper.marketSellOrdersWithEthAsync( ordersWithoutFee, @@ -406,14 +507,28 @@ describe(ContractName.Forwarder, () => { ), RevertReason.FeePercentageTooLarge, ); - const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); - expect(feeRecipientEthBalanceAfter).to.be.bignumber.equal(feeRecipientEthBalanceBefore); + }); + 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 assets', async () => { - ordersWithoutFee = [orderWithoutFee]; - feeOrders = []; + 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); @@ -444,9 +559,47 @@ describe(ContractName.Forwarder, () => { ); expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); - it('should buy the exact amount of assets and return excess ETH', async () => { - ordersWithoutFee = [orderWithoutFee]; - feeOrders = []; + 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; @@ -477,11 +630,11 @@ describe(ContractName.Forwarder, () => { ); expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); - it('should buy the exact amount of assets with fee abstraction', async () => { - ordersWithFee = [orderWithFee]; - feeOrders = [feeOrder]; + 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 = orderWithoutFee.takerAssetAmount; + const ethValue = orderWithFee.takerAssetAmount; tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { value: ethValue, @@ -514,13 +667,13 @@ describe(ContractName.Forwarder, () => { ); expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); - it('should buy the exact amount of assets when buying ZRX with fee abstraction', async () => { + 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), }); - ordersWithFee = [orderWithFee]; - feeOrders = []; + const ordersWithFee = [orderWithFee]; + const feeOrders: SignedOrder[] = []; const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2); const ethValue = orderWithFee.takerAssetAmount; tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { @@ -545,13 +698,20 @@ describe(ContractName.Forwarder, () => { 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(makerAssetFilledAmount).minus(takerFeePaid), + erc20Balances[takerAddress][zrxToken.address].plus(totalZrxPurchased), ); expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), @@ -562,373 +722,282 @@ describe(ContractName.Forwarder, () => { ); expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); - // it('throws if fees are higher than 5% when buying zrx', async () => { - // const highFeeZRXOrder = orderFactory.newSignedOrder({ - // makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - // makerAssetAmount: orderWithoutFee.makerAssetAmount, - // takerFee: orderWithoutFee.makerAssetAmount.times(0.06), - // }); - // ordersWithFee = [highFeeZRXOrder]; - // feeOrders = []; - // const makerAssetAmount = orderWithoutFee.makerAssetAmount.div(2); - // const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - // ordersWithFee, - // feeOrders, - // feePercentage, - // makerAssetAmount, - // ); - // return expectTransactionFailedAsync( - // forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetAmount, { - // from: takerAddress, - // value: fillAmountWei, - // }), - // RevertReason.UnacceptableThreshold, - // ); - // }); - // it('throws if fees are higher than 5% when buying erc20', async () => { - // const highFeeERC20Order = orderFactory.newSignedOrder({ - // takerFee: orderWithoutFee.makerAssetAmount.times(0.06), - // }); - // ordersWithFee = [highFeeERC20Order]; - // feeOrders = [feeOrder]; - // const makerAssetAmount = orderWithoutFee.makerAssetAmount.div(2); - // const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - // ordersWithFee, - // feeOrders, - // feePercentage, - // makerAssetAmount, - // ); - // return expectTransactionFailedAsync( - // forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetAmount, { - // from: takerAddress, - // value: fillAmountWei, - // }), - // RevertReason.UnacceptableThreshold as any, - // ); - // }); - // it('throws if makerAssetAmount is 0', async () => { - // const makerAssetAmount = new BigNumber(0); - // const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - // ordersWithFee, - // feeOrders, - // feePercentage, - // makerAssetAmount, - // ); - // return expectTransactionFailedAsync( - // forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetAmount, { - // from: takerAddress, - // value: fillAmountWei, - // }), - // RevertReason.ValueGreaterThanZero as any, - // ); - // }); - // it('throws if the amount of ETH sent in is less than the takerAssetFilledAmount', async () => { - // const makerAssetAmount = orderWithoutFee.makerAssetAmount; - // const primaryTakerAssetFillAmount = orderWithoutFee.takerAssetAmount.div(2); - // const zero = new BigNumber(0); - // // Deposit enough taker balance to fill the order - // const wethDepositTxHash = await wethContract.deposit.sendTransactionAsync({ - // from: takerAddress, - // value: orderWithoutFee.takerAssetAmount, - // }); - // await web3Wrapper.awaitTransactionSuccessAsync(wethDepositTxHash); - // // Transfer all of this WETH to the forwarding contract - // const wethTransferTxHash = await wethContract.transfer.sendTransactionAsync( - // forwarderContract.address, - // orderWithoutFee.takerAssetAmount, - // { from: takerAddress }, - // ); - // await web3Wrapper.awaitTransactionSuccessAsync(wethTransferTxHash); - // // We use the contract directly to get around wrapper validations and calculations - // const formattedOrders = formatters.createMarketSellOrders(signedOrders, zero); - // const formattedFeeOrders = formatters.createMarketSellOrders(feeOrders, zero); - // return expectTransactionFailedAsync( - // forwarderContract.marketBuyOrdersWithEth.sendTransactionAsync( - // formattedOrders.orders, - // formattedOrders.signatures, - // formattedFeeOrders.orders, - // formattedFeeOrders.signatures, - // makerAssetAmount, - // zero, - // constants.NULL_ADDRESS, - // { value: primaryTakerAssetFillAmount, from: takerAddress }, - // ), - // RevertReason.InvalidMsgValue, - // ); - // }); + it('should not change balances 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); + + 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 totalEthSpent = gasPrice.times(tx.gasUsed); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances).to.deep.equal(erc20Balances); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + 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 buy an ERC721 asset and ignore later orders with 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; + + 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); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress], + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress], + ); + }); + it('should buy an ERC721 asset and pay ZRX fees from a single fee order', async () => { + const makerAssetId = erc721MakerAssetIds[0]; + orderWithFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + const ordersWithFee = [orderWithFee]; + const feeOrders = [feeOrder]; + const makerAssetFillAmount = orderWithFee.makerAssetAmount; + const primaryTakerAssetFillAmount = orderWithFee.takerAssetAmount; + const feeAmount = orderWithFee.takerFee; + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const ethValue = primaryTakerAssetFillAmount.plus(wethSpentOnFeeOrders); + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const totalEthSpent = ethValue.plus(gasPrice.times(tx.gasUsed)); + + expect(newOwner).to.be.bignumber.equal(takerAddress); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should buy an ERC721 asset and pay ZRX fees from multiple fee orders', async () => { + const makerAssetId = erc721MakerAssetIds[0]; + orderWithFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + const ordersWithFee = [orderWithFee]; + const makerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const makerAssetAmount = orderWithFee.takerFee.dividedToIntegerBy(2); + const takerAssetAmount = feeOrder.takerAssetAmount + .times(makerAssetAmount) + .dividedToIntegerBy(feeOrder.makerAssetAmount); + + const firstFeeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + makerAssetAmount, + takerAssetAmount, + }); + const secondFeeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + makerAssetAmount, + takerAssetAmount, + }); + const feeOrders = [firstFeeOrder, secondFeeOrder]; + + const makerAssetFillAmount = orderWithFee.makerAssetAmount; + const primaryTakerAssetFillAmount = orderWithFee.takerAssetAmount; + const feeAmount = orderWithFee.takerFee; + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const ethValue = primaryTakerAssetFillAmount.plus(wethSpentOnFeeOrders); + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const totalEthSpent = ethValue.plus(gasPrice.times(tx.gasUsed)); + + expect(newOwner).to.be.bignumber.equal(takerAddress); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + }); + describe('marketBuyOrdersWithEth with extra fees', () => { + it('should buy an asset and send fee to feeRecipient', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount; + + const baseFeePercentage = 2; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + makerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); + const ethSpentOnFee = ForwarderWrapper.getPercentageOfValue(primaryTakerAssetFillAmount, baseFeePercentage); + const totalEthSpent = primaryTakerAssetFillAmount.plus(ethSpentOnFee).plus(gasPrice.times(tx.gasUsed)); + + expect(feeRecipientEthBalanceAfter).to.be.bignumber.equal(feeRecipientEthBalanceBefore.plus(ethSpentOnFee)); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should fail if the fee is set too high', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount; + + const baseFeePercentage = 6; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + await expectTransactionFailedAsync( + forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + makerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ), + RevertReason.FeePercentageTooLarge, + ); + }); + it('should fail if there is not enough ETH remaining to pay the fee', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); + + const baseFeePercentage = 2; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + await expectTransactionFailedAsync( + forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + makerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ), + RevertReason.InsufficientEthRemaining, + ); + }); }); - // describe('marketBuyOrdersWithEth - ERC721', async () => { - // it('buys ERC721 assets', async () => { - // const makerAssetId = erc721MakerAssetIds[0]; - // orderWithoutFee = orderFactory.newSignedOrder({ - // makerAssetAmount: new BigNumber(1), - // makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), - // }); - // feeOrders = []; - // signedOrders = [orderWithoutFee]; - // const makerAssetAmount = new BigNumber(signedOrders.length); - // const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - // signedOrders, - // feeOrders, - // feePercentage, - // makerAssetAmount, - // ); - // tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { - // from: takerAddress, - // value: fillAmountWei, - // }); - // const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); - // expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress); - // }); - // it('buys ERC721 assets with fee abstraction', async () => { - // const makerAssetId = erc721MakerAssetIds[0]; - // orderWithoutFee = orderFactory.newSignedOrder({ - // makerAssetAmount: new BigNumber(1), - // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), - // makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), - // }); - // signedOrders = [orderWithoutFee]; - // const makerAssetAmount = new BigNumber(signedOrders.length); - // const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - // signedOrders, - // feeOrders, - // feePercentage, - // makerAssetAmount, - // ); - // tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { - // from: takerAddress, - // value: fillAmountWei, - // }); - // const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); - // expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress); - // }); - // it('buys ERC721 assets with fee abstraction and pays fee to fee recipient', async () => { - // const makerAssetId = erc721MakerAssetIds[0]; - // orderWithoutFee = orderFactory.newSignedOrder({ - // makerAssetAmount: new BigNumber(1), - // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), - // makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), - // }); - // signedOrders = [orderWithoutFee]; - // feePercentage = 100; - // const initTakerBalanceWei = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - // const initFeeRecipientBalanceWei = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); - // const makerAssetAmount = new BigNumber(signedOrders.length); - // const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - // signedOrders, - // feeOrders, - // feePercentage, - // makerAssetAmount, - // ); - // tx = await forwarderWrapper.marketBuyOrdersWithEthAsync( - // signedOrders, - // feeOrders, - // makerAssetAmount, - // { - // from: takerAddress, - // value: fillAmountWei, - // gasPrice: gasPrice, - // }, - // { - // feePercentage, - // feeRecipient: feeRecipientAddress, - // }, - // ); - // const afterFeeRecipientEthBalance = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); - // const afterTakerBalanceWei = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - // const takerFilledAmount = initTakerBalanceWei.minus(afterTakerBalanceWei).plus(tx.gasUsed); - // const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); - // expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress); - // const balanceDiff = afterFeeRecipientEthBalance.minus(initFeeRecipientBalanceWei); - // expect(takerFilledAmount.dividedToIntegerBy(balanceDiff)).to.be.bignumber.equal(101); - // expect(takerFilledAmount.minus(balanceDiff).dividedToIntegerBy(balanceDiff)).to.be.bignumber.equal(100); - // }); - // it('buys multiple ERC721 assets with fee abstraction and pays fee to fee recipient', async () => { - // const makerAssetId1 = erc721MakerAssetIds[0]; - // const makerAssetId2 = erc721MakerAssetIds[1]; - // const signedOrder1 = orderFactory.newSignedOrder({ - // makerAssetAmount: new BigNumber(1), - // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), DECIMALS_DEFAULT), - // makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId1), - // }); - // const signedOrder2 = orderFactory.newSignedOrder({ - // makerAssetAmount: new BigNumber(1), - // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(4), DECIMALS_DEFAULT), - // makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId2), - // }); - // signedOrders = [signedOrder1, signedOrder2]; - // feePercentage = 10; - // const makerAssetAmount = new BigNumber(signedOrders.length); - // const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - // signedOrders, - // feeOrders, - // feePercentage, - // makerAssetAmount, - // ); - // tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { - // from: takerAddress, - // value: fillAmountWei, - // }); - // const newOwnerTakerAsset1 = await erc721Token.ownerOf.callAsync(makerAssetId1); - // expect(newOwnerTakerAsset1).to.be.bignumber.equal(takerAddress); - // const newOwnerTakerAsset2 = await erc721Token.ownerOf.callAsync(makerAssetId2); - // expect(newOwnerTakerAsset2).to.be.bignumber.equal(takerAddress); - // }); - // it('buys ERC721 assets with fee abstraction and handles fee orders filled and excess eth', async () => { - // const makerAssetId = erc721MakerAssetIds[0]; - // feePercentage = 0; - // // In this scenario a total of 6 ZRX fees need to be paid. - // // There are two fee orders, but the first fee order is partially filled while - // // the Forwarding contract tx is in the mempool. - // const erc721MakerAssetAmount = new BigNumber(1); - // orderWithoutFee = orderFactory.newSignedOrder({ - // makerAssetAmount: erc721MakerAssetAmount, - // takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT), - // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(6), DECIMALS_DEFAULT), - // makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), - // }); - // signedOrders = [orderWithoutFee]; - // const firstFeeOrder = orderFactory.newSignedOrder({ - // makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(8), DECIMALS_DEFAULT), - // takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.1), DECIMALS_DEFAULT), - // makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), - // }); - // const secondFeeOrder = orderFactory.newSignedOrder({ - // makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(8), DECIMALS_DEFAULT), - // takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.12), DECIMALS_DEFAULT), - // makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), - // }); - // feeOrders = [firstFeeOrder, secondFeeOrder]; - // const makerAssetAmount = new BigNumber(signedOrders.length); - // const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - // signedOrders, - // feeOrders, - // feePercentage, - // erc721MakerAssetAmount, - // ); - // // Simulate another otherAddress user partially filling firstFeeOrder - // const firstFeeOrderFillAmount = firstFeeOrder.makerAssetAmount.div(2); - // tx = await forwarderWrapper.marketBuyOrdersWithEthAsync([firstFeeOrder], [], firstFeeOrderFillAmount, { - // from: otherAddress, - // value: fillAmountWei, - // }); - // // For tests we calculate how much this should've cost given that firstFeeOrder was filled - // const expectedFillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - // signedOrders, - // feeOrders, - // feePercentage, - // erc721MakerAssetAmount, - // ); - // // With 4 ZRX remaining in firstFeeOrder, the secondFeeOrder will need to be filled to make up - // // the total amount of fees required (6) - // // Since the fee orders can be filled while the transaction is pending the user safely sends in - // // extra ether to cover any slippage - // const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - // const slippageFillAmountWei = fillAmountWei.times(2); - // tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { - // from: takerAddress, - // value: slippageFillAmountWei, - // gasPrice: gasPrice, - // }); - // const afterEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - // const expectedEthBalanceAfterGasCosts = initEthBalance.minus(expectedFillAmountWei).minus(tx.gasUsed); - // const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); - // expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress); - // expect(afterEthBalance).to.be.bignumber.equal(expectedEthBalanceAfterGasCosts); - // }); - // it('buys ERC721 assets with fee abstraction and handles fee orders filled', async () => { - // const makerAssetId = erc721MakerAssetIds[0]; - // feePercentage = 0; - // // In this scenario a total of 6 ZRX fees need to be paid. - // // There are two fee orders, but the first fee order is partially filled while - // // the Forwarding contract tx is in the mempool. - // const erc721MakerAssetAmount = new BigNumber(1); - // orderWithoutFee = orderFactory.newSignedOrder({ - // makerAssetAmount: erc721MakerAssetAmount, - // takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT), - // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(6), DECIMALS_DEFAULT), - // makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), - // }); - // const zrxMakerAssetAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(8), DECIMALS_DEFAULT); - // signedOrders = [orderWithoutFee]; - // const firstFeeOrder = orderFactory.newSignedOrder({ - // makerAssetAmount: zrxMakerAssetAmount, - // takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.1), DECIMALS_DEFAULT), - // makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), - // }); - // const secondFeeOrder = orderFactory.newSignedOrder({ - // makerAssetAmount: zrxMakerAssetAmount, - // takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.12), DECIMALS_DEFAULT), - // makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - // takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), - // }); - // feeOrders = [firstFeeOrder, secondFeeOrder]; - // const makerAssetAmount = new BigNumber(signedOrders.length); - // const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - // signedOrders, - // feeOrders, - // feePercentage, - // erc721MakerAssetAmount, - // ); - // // Simulate another otherAddress user partially filling firstFeeOrder - // const firstFeeOrderFillAmount = firstFeeOrder.makerAssetAmount.div(2); - // tx = await forwarderWrapper.marketBuyOrdersWithEthAsync([firstFeeOrder], [], firstFeeOrderFillAmount, { - // from: otherAddress, - // value: fillAmountWei, - // }); - // const expectedFillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - // signedOrders, - // feeOrders, - // feePercentage, - // erc721MakerAssetAmount, - // ); - // tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { - // from: takerAddress, - // value: expectedFillAmountWei, - // }); - // const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); - // expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress); - // }); - // it('throws when mixed ERC721 and ERC20 assets', async () => { - // const makerAssetId = erc721MakerAssetIds[0]; - // const erc721SignedOrder = orderFactory.newSignedOrder({ - // makerAssetAmount: new BigNumber(1), - // makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), - // }); - // const erc20SignedOrder = orderFactory.newSignedOrder(); - // signedOrders = [erc721SignedOrder, erc20SignedOrder]; - // const makerAssetAmount = new BigNumber(signedOrders.length); - // const fillAmountWei = erc20SignedOrder.takerAssetAmount.plus(erc721SignedOrder.takerAssetAmount); - // return expectTransactionFailedAsync( - // forwarderWrapper.marketBuyOrdersWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { - // from: takerAddress, - // value: fillAmountWei, - // }), - // RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, - // ); - // }); - // it('throws when mixed ERC721 and ERC20 assets with ERC20 first', async () => { - // const makerAssetId = erc721MakerAssetIds[0]; - // const erc721SignedOrder = orderFactory.newSignedOrder({ - // makerAssetAmount: new BigNumber(1), - // makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), - // }); - // const erc20SignedOrder = orderFactory.newSignedOrder(); - // signedOrders = [erc20SignedOrder, erc721SignedOrder]; - // const makerAssetAmount = new BigNumber(signedOrders.length); - // const fillAmountWei = erc20SignedOrder.takerAssetAmount.plus(erc721SignedOrder.takerAssetAmount); - // return expectTransactionFailedAsync( - // forwarderWrapper.marketBuyOrdersWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { - // from: takerAddress, - // value: fillAmountWei, - // }), - // RevertReason.InvalidTakerAmount, - // ); - // }); - // }); }); // tslint:disable:max-file-line-count // tslint:enable:no-unnecessary-type-assertion diff --git a/packages/contracts/test/utils/forwarder_wrapper.ts b/packages/contracts/test/utils/forwarder_wrapper.ts index 773ddf897..ef7476e36 100644 --- a/packages/contracts/test/utils/forwarder_wrapper.ts +++ b/packages/contracts/test/utils/forwarder_wrapper.ts @@ -25,13 +25,13 @@ export class ForwarderWrapper { let remainingFeeAmount = feeAmount; _.forEach(feeOrders, feeOrder => { const feeAvailable = feeOrder.makerAssetAmount.minus(feeOrder.takerFee); - if (!remainingFeeAmount.isZero() && feeAvailable.gte(remainingFeeAmount)) { + if (!remainingFeeAmount.isZero() && feeAvailable.gt(remainingFeeAmount)) { wethAmount = wethAmount .plus(feeOrder.takerAssetAmount.times(remainingFeeAmount).dividedToIntegerBy(feeAvailable)) .plus(1); remainingFeeAmount = new BigNumber(0); } else if (!remainingFeeAmount.isZero()) { - wethAmount = wethAmount.plus(feeOrder.takerAssetAmount).plus(1); + wethAmount = wethAmount.plus(feeOrder.takerAssetAmount); remainingFeeAmount = remainingFeeAmount.minus(feeAvailable); } }); diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index b96bdb182..555cd7dec 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -215,10 +215,10 @@ export enum RevertReason { LibBytesGreaterOrEqualToSourceBytesLengthRequired = 'GREATER_OR_EQUAL_TO_SOURCE_BYTES_LENGTH_REQUIRED', Erc20InsufficientBalance = 'ERC20_INSUFFICIENT_BALANCE', Erc20InsufficientAllowance = 'ERC20_INSUFFICIENT_ALLOWANCE', - UnacceptableThreshold = 'UNACCEPTABLE_THRESHOLD', FeePercentageTooLarge = 'FEE_PERCENTAGE_TOO_LARGE', ValueGreaterThanZero = 'VALUE_GREATER_THAN_ZERO', InvalidMsgValue = 'INVALID_MSG_VALUE', + InsufficientEthRemaining = 'INSUFFICIENT_ETH_REMAINING', } export enum StatusCodes { -- cgit v1.2.3 From e5e68de2d7bf8bfc319200b8dc9f27cf8a081b4a Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Sun, 22 Jul 2018 23:16:35 -0500 Subject: Use != instead of > in loops, add sanity checks to market fill functions --- .../src/2.0.0/forwarder/MixinExchangeWrapper.sol | 10 ++++----- .../protocol/Exchange/MixinWrapperFunctions.sol | 26 +++++++++++----------- packages/contracts/test/forwarder/forwarder.ts | 2 +- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol b/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol index e230767b7..e150791da 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol @@ -104,7 +104,7 @@ contract MixinExchangeWrapper is bytes memory wethAssetData = WETH_ASSET_DATA; uint256 ordersLength = orders.length; - for (uint256 i = 0; i < ordersLength; i++) { + 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. @@ -125,7 +125,7 @@ contract MixinExchangeWrapper is addFillResults(totalFillResults, singleFillResults); // Stop execution if the entire amount of takerAsset has been sold - if (totalFillResults.takerAssetFilledAmount == wethSellAmount) { + if (totalFillResults.takerAssetFilledAmount >= wethSellAmount) { break; } } @@ -151,7 +151,7 @@ contract MixinExchangeWrapper is bytes memory wethAssetData = WETH_ASSET_DATA; uint256 ordersLength = orders.length; - for (uint256 i = 0; i < ordersLength; i++) { + 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. @@ -180,7 +180,7 @@ contract MixinExchangeWrapper is addFillResults(totalFillResults, singleFillResults); // Stop execution if the entire amount of makerAsset has been bought - if (totalFillResults.makerAssetFilledAmount == makerAssetFillAmount) { + if (totalFillResults.makerAssetFilledAmount >= makerAssetFillAmount) { break; } } @@ -214,7 +214,7 @@ contract MixinExchangeWrapper is uint256 zrxPurchased = 0; uint256 ordersLength = orders.length; - for (uint256 i = 0; i < ordersLength; i++) { + 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; diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol b/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol index a0882b108..86194f461 100644 --- a/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol +++ b/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol @@ -120,7 +120,7 @@ contract MixinWrapperFunctions is returns (FillResults memory totalFillResults) { uint256 ordersLength = orders.length; - for (uint256 i = 0; i < ordersLength; i++) { + for (uint256 i = 0; i != ordersLength; i++) { FillResults memory singleFillResults = fillOrder( orders[i], takerAssetFillAmounts[i], @@ -146,7 +146,7 @@ contract MixinWrapperFunctions is returns (FillResults memory totalFillResults) { uint256 ordersLength = orders.length; - for (uint256 i = 0; i < ordersLength; i++) { + for (uint256 i = 0; i != ordersLength; i++) { FillResults memory singleFillResults = fillOrKillOrder( orders[i], takerAssetFillAmounts[i], @@ -173,7 +173,7 @@ contract MixinWrapperFunctions is returns (FillResults memory totalFillResults) { uint256 ordersLength = orders.length; - for (uint256 i = 0; i < ordersLength; i++) { + for (uint256 i = 0; i != ordersLength; i++) { FillResults memory singleFillResults = fillOrderNoThrow( orders[i], takerAssetFillAmounts[i], @@ -200,7 +200,7 @@ contract MixinWrapperFunctions is bytes memory takerAssetData = orders[0].takerAssetData; uint256 ordersLength = orders.length; - for (uint256 i = 0; i < ordersLength; i++) { + for (uint256 i = 0; i != ordersLength; i++) { // We assume that asset being sold by taker is the same for each order. // Rather than passing this in as calldata, we use the takerAssetData from the first order in all later orders. @@ -220,7 +220,7 @@ contract MixinWrapperFunctions is addFillResults(totalFillResults, singleFillResults); // Stop execution if the entire amount of takerAsset has been sold - if (totalFillResults.takerAssetFilledAmount == takerAssetFillAmount) { + if (totalFillResults.takerAssetFilledAmount >= takerAssetFillAmount) { break; } } @@ -244,7 +244,7 @@ contract MixinWrapperFunctions is bytes memory takerAssetData = orders[0].takerAssetData; uint256 ordersLength = orders.length; - for (uint256 i = 0; i < ordersLength; i++) { + for (uint256 i = 0; i != ordersLength; i++) { // We assume that asset being sold by taker is the same for each order. // Rather than passing this in as calldata, we use the takerAssetData from the first order in all later orders. @@ -264,7 +264,7 @@ contract MixinWrapperFunctions is addFillResults(totalFillResults, singleFillResults); // Stop execution if the entire amount of takerAsset has been sold - if (totalFillResults.takerAssetFilledAmount == takerAssetFillAmount) { + if (totalFillResults.takerAssetFilledAmount >= takerAssetFillAmount) { break; } } @@ -287,7 +287,7 @@ contract MixinWrapperFunctions is bytes memory makerAssetData = orders[0].makerAssetData; uint256 ordersLength = orders.length; - for (uint256 i = 0; i < ordersLength; i++) { + for (uint256 i = 0; i != ordersLength; i++) { // We assume that asset being bought by taker is the same for each order. // Rather than passing this in as calldata, we copy the makerAssetData from the first order onto all later orders. @@ -315,7 +315,7 @@ contract MixinWrapperFunctions is addFillResults(totalFillResults, singleFillResults); // Stop execution if the entire amount of makerAsset has been bought - if (totalFillResults.makerAssetFilledAmount == makerAssetFillAmount) { + if (totalFillResults.makerAssetFilledAmount >= makerAssetFillAmount) { break; } } @@ -339,7 +339,7 @@ contract MixinWrapperFunctions is bytes memory makerAssetData = orders[0].makerAssetData; uint256 ordersLength = orders.length; - for (uint256 i = 0; i < ordersLength; i++) { + for (uint256 i = 0; i != ordersLength; i++) { // We assume that asset being bought by taker is the same for each order. // Rather than passing this in as calldata, we copy the makerAssetData from the first order onto all later orders. @@ -367,7 +367,7 @@ contract MixinWrapperFunctions is addFillResults(totalFillResults, singleFillResults); // Stop execution if the entire amount of makerAsset has been bought - if (totalFillResults.makerAssetFilledAmount == makerAssetFillAmount) { + if (totalFillResults.makerAssetFilledAmount >= makerAssetFillAmount) { break; } } @@ -380,7 +380,7 @@ contract MixinWrapperFunctions is public { uint256 ordersLength = orders.length; - for (uint256 i = 0; i < ordersLength; i++) { + for (uint256 i = 0; i != ordersLength; i++) { cancelOrder(orders[i]); } } @@ -395,7 +395,7 @@ contract MixinWrapperFunctions is { uint256 ordersLength = orders.length; LibOrder.OrderInfo[] memory ordersInfo = new LibOrder.OrderInfo[](ordersLength); - for (uint256 i = 0; i < ordersLength; i++) { + for (uint256 i = 0; i != ordersLength; i++) { ordersInfo[i] = getOrderInfo(orders[i]); } return ordersInfo; diff --git a/packages/contracts/test/forwarder/forwarder.ts b/packages/contracts/test/forwarder/forwarder.ts index b6d81df58..19639d3aa 100644 --- a/packages/contracts/test/forwarder/forwarder.ts +++ b/packages/contracts/test/forwarder/forwarder.ts @@ -29,7 +29,7 @@ const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); const DECIMALS_DEFAULT = 18; const MAX_WETH_FILL_PERCENTAGE = 95; -describe.only(ContractName.Forwarder, () => { +describe(ContractName.Forwarder, () => { let makerAddress: string; let owner: string; let takerAddress: string; -- cgit v1.2.3 From 06396b8874f2cff1333d3f19c536015502f69d28 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Mon, 23 Jul 2018 09:34:08 -0500 Subject: Remove ERC721 callback functions --- .../contracts/src/2.0.0/forwarder/MixinAssets.sol | 27 ---------------------- .../src/2.0.0/forwarder/MixinExchangeWrapper.sol | 2 +- .../src/2.0.0/forwarder/interfaces/IAssets.sol | 21 +---------------- .../2.0.0/forwarder/mixins/MExchangeWrapper.sol | 2 +- 4 files changed, 3 insertions(+), 49 deletions(-) diff --git a/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol b/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol index 084a28550..5cf5f831b 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol @@ -35,8 +35,6 @@ contract MixinAssets is using LibBytes for bytes; bytes4 constant internal ERC20_TRANSFER_SELECTOR = bytes4(keccak256("transfer(address,uint256)")); - bytes4 constant internal ERC721_RECEIVED = bytes4(keccak256("onERC721Received(address,uint256,bytes)")); - bytes4 constant internal ERC721_RECEIVED_OPERATOR = bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")); /// @dev Withdraws ERC20 tokens 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 @@ -56,31 +54,6 @@ contract MixinAssets is ); } - function onERC721Received( - address, - uint256, - bytes memory - ) - public - pure - returns(bytes4) - { - return ERC721_RECEIVED; - } - - function onERC721Received( - address, - address, - uint256, - bytes memory - ) - public - pure - returns(bytes4) - { - return ERC721_RECEIVED_OPERATOR; - } - /// @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. diff --git a/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol b/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol index e150791da..f3aa483c5 100644 --- a/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol +++ b/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol @@ -250,4 +250,4 @@ contract MixinExchangeWrapper is return totalFillResults; } -} \ No newline at end of file +} diff --git a/packages/contracts/src/2.0.0/forwarder/interfaces/IAssets.sol b/packages/contracts/src/2.0.0/forwarder/interfaces/IAssets.sol index 27adb1221..9b0d995eb 100644 --- a/packages/contracts/src/2.0.0/forwarder/interfaces/IAssets.sol +++ b/packages/contracts/src/2.0.0/forwarder/interfaces/IAssets.sol @@ -31,23 +31,4 @@ contract IAssets { uint256 amount ) external; - - function onERC721Received( - address, - uint256, - bytes memory - ) - public - pure - returns(bytes4); - - function onERC721Received( - address, - address, - uint256, - bytes memory - ) - public - pure - returns(bytes4); -} \ No newline at end of file +} diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MExchangeWrapper.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MExchangeWrapper.sol index 0fa90fd82..5a2def7e5 100644 --- a/packages/contracts/src/2.0.0/forwarder/mixins/MExchangeWrapper.sol +++ b/packages/contracts/src/2.0.0/forwarder/mixins/MExchangeWrapper.sol @@ -84,4 +84,4 @@ contract MExchangeWrapper { ) internal returns (LibFillResults.FillResults memory totalFillResults); -} \ No newline at end of file +} -- cgit v1.2.3