aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorAmir Bandeali <abandeali1@gmail.com>2018-07-23 23:50:39 +0800
committerGitHub <noreply@github.com>2018-07-23 23:50:39 +0800
commita05b14e4d9659be1cc495ee33fd8962ce773f87f (patch)
tree0197338c16c6e990613c7cb390ca038286ca10ee /packages
parenta66ccaa1da2af753038bf22a5e7e63bdc307bf2f (diff)
parent06396b8874f2cff1333d3f19c536015502f69d28 (diff)
downloaddexon-sol-tools-a05b14e4d9659be1cc495ee33fd8962ce773f87f.tar
dexon-sol-tools-a05b14e4d9659be1cc495ee33fd8962ce773f87f.tar.gz
dexon-sol-tools-a05b14e4d9659be1cc495ee33fd8962ce773f87f.tar.bz2
dexon-sol-tools-a05b14e4d9659be1cc495ee33fd8962ce773f87f.tar.lz
dexon-sol-tools-a05b14e4d9659be1cc495ee33fd8962ce773f87f.tar.xz
dexon-sol-tools-a05b14e4d9659be1cc495ee33fd8962ce773f87f.tar.zst
dexon-sol-tools-a05b14e4d9659be1cc495ee33fd8962ce773f87f.zip
Merge pull request #847 from 0xProject/refactor/contracts/simplify-forwarder
Refactor forwarding contract architecture, remove batch functions
Diffstat (limited to 'packages')
-rw-r--r--packages/contracts/src/2.0.0/forwarder/Forwarder.sol21
-rw-r--r--packages/contracts/src/2.0.0/forwarder/MixinAssets.sol (renamed from packages/contracts/src/2.0.0/forwarder/MixinTransfer.sol)83
-rw-r--r--packages/contracts/src/2.0.0/forwarder/MixinErrorMessages.sol35
-rw-r--r--packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol253
-rw-r--r--packages/contracts/src/2.0.0/forwarder/MixinExpectedResults.sol161
-rw-r--r--packages/contracts/src/2.0.0/forwarder/MixinFees.sol126
-rw-r--r--packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol499
-rw-r--r--packages/contracts/src/2.0.0/forwarder/MixinMarketBuyZrx.sol83
-rw-r--r--packages/contracts/src/2.0.0/forwarder/MixinWeth.sol110
-rw-r--r--packages/contracts/src/2.0.0/forwarder/interfaces/IAssets.sol (renamed from packages/contracts/src/2.0.0/forwarder/mixins/MTransfer.sol)30
-rw-r--r--packages/contracts/src/2.0.0/forwarder/interfaces/IExpectedResults.sol66
-rw-r--r--packages/contracts/src/2.0.0/forwarder/interfaces/IForwarder.sol4
-rw-r--r--packages/contracts/src/2.0.0/forwarder/interfaces/IForwarderCore.sol69
-rw-r--r--packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol (renamed from packages/contracts/src/2.0.0/forwarder/MixinConstants.sol)27
-rw-r--r--packages/contracts/src/2.0.0/forwarder/libs/LibForwarderErrors.sol34
-rw-r--r--packages/contracts/src/2.0.0/forwarder/mixins/MAssets.sol54
-rw-r--r--packages/contracts/src/2.0.0/forwarder/mixins/MConstants.sol35
-rw-r--r--packages/contracts/src/2.0.0/forwarder/mixins/MExchangeWrapper.sol87
-rw-r--r--packages/contracts/src/2.0.0/forwarder/mixins/MExpectedResults.sol42
-rw-r--r--packages/contracts/src/2.0.0/forwarder/mixins/MFees.sol63
-rw-r--r--packages/contracts/src/2.0.0/forwarder/mixins/MForwarderCore.sol68
-rw-r--r--packages/contracts/src/2.0.0/forwarder/mixins/MMarketBuyZrx.sol42
-rw-r--r--packages/contracts/src/2.0.0/forwarder/mixins/MWeth.sol41
-rw-r--r--packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol41
-rw-r--r--packages/contracts/src/2.0.0/protocol/Exchange/libs/LibMath.sol6
-rw-r--r--packages/contracts/src/2.0.0/protocol/Exchange/libs/LibOrder.sol28
-rw-r--r--packages/contracts/src/2.0.0/utils/SafeMath/SafeMath.sol2
-rw-r--r--packages/contracts/test/forwarder/forwarder.ts1265
-rw-r--r--packages/contracts/test/utils/constants.ts2
-rw-r--r--packages/contracts/test/utils/forwarder_wrapper.ts226
-rw-r--r--packages/types/src/index.ts4
-rw-r--r--packages/web3-wrapper/CHANGELOG.json9
-rw-r--r--packages/web3-wrapper/src/web3_wrapper.ts14
33 files changed, 1740 insertions, 1890 deletions
diff --git a/packages/contracts/src/2.0.0/forwarder/Forwarder.sol b/packages/contracts/src/2.0.0/forwarder/Forwarder.sol
index 546e7f22c..5b88b05b1 100644
--- a/packages/contracts/src/2.0.0/forwarder/Forwarder.sol
+++ b/packages/contracts/src/2.0.0/forwarder/Forwarder.sol
@@ -19,20 +19,19 @@
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 "./libs/LibConstants.sol";
+import "./MixinAssets.sol";
+import "./MixinExchangeWrapper.sol";
+// solhint-disable no-empty-blocks
contract Forwarder is
- MixinConstants,
- MixinExpectedResults,
- MixinFees,
- MixinMarketBuyZrx,
- MixinTransfer,
+ LibConstants,
+ MixinWeth,
+ MixinAssets,
+ MixinExchangeWrapper,
MixinForwarderCore
{
@@ -44,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/MixinTransfer.sol b/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol
index bebfc976b..5cf5f831b 100644
--- a/packages/contracts/src/2.0.0/forwarder/MixinTransfer.sol
+++ b/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol
@@ -19,58 +19,78 @@
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/MTransfer.sol";
+import "./libs/LibConstants.sol";
+import "./mixins/MAssets.sol";
-contract MixinTransfer is
- MTransfer
+contract MixinAssets is
+ Ownable,
+ LibConstants,
+ 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)"));
- function onERC721Received(
- address,
- uint256,
- bytes memory
+ /// @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
)
- public
- pure
- returns(bytes4)
+ external
+ onlyOwner
{
- return ERC721_RECEIVED;
+ require(
+ IERC20Token(token).transfer(msg.sender, amount),
+ "WITHDRAWAL_FAILED"
+ );
}
- function onERC721Received(
- address,
- address,
- uint256,
- bytes memory
+ /// @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
)
- public
- pure
- returns(bytes4)
+ internal
{
- return ERC721_RECEIVED_OPERATOR;
+ bytes4 proxyId = assetData.readBytes4(0);
+
+ if (proxyId == ERC20_DATA_ID) {
+ transferERC20Token(assetData, amount);
+ } else if (proxyId == ERC721_DATA_ID) {
+ transferERC721Token(assetData, amount);
+ } else {
+ revert("UNSUPPORTED_TOKEN_PROXY");
+ }
}
+ /// @dev Decodes ERC20 assetData and transfers given amount to sender.
+ /// @param assetData Byte array encoded for the respective asset proxy.
+ /// @param amount Amount of asset to transfer to sender.
function transferERC20Token(
- address token,
- address to,
+ 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,
- to,
+ msg.sender,
amount
));
require(
@@ -100,18 +120,27 @@ contract MixinTransfer is
);
}
+ /// @dev Decodes ERC721 assetData and transfers given amount to sender.
+ /// @param assetData Byte array encoded for the respective asset proxy.
+ /// @param amount Amount of asset to transfer to sender.
function transferERC721Token(
bytes memory assetData,
- address to
+ uint256 amount
)
internal
{
+ require(
+ amount == 1,
+ "INVALID_AMOUNT"
+ );
// Decode asset data.
address token = assetData.readAddress(16);
uint256 tokenId = assetData.readUint256(36);
+
+ // Perform transfer.
IERC721Token(token).transferFrom(
address(this),
- to,
+ msg.sender,
tokenId
);
}
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 1b3e3f488..000000000
--- a/packages/contracts/src/2.0.0/forwarder/MixinErrorMessages.sol
+++ /dev/null
@@ -1,35 +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 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";
-}
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..f3aa483c5
--- /dev/null
+++ b/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol
@@ -0,0 +1,253 @@
+/*
+
+ Copyright 2018 ZeroEx Intl.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+*/
+
+pragma solidity 0.4.24;
+pragma experimental ABIEncoderV2;
+
+import "./libs/LibConstants.sol";
+import "./mixins/MExchangeWrapper.sol";
+import "../protocol/Exchange/libs/LibAbiEncoder.sol";
+import "../protocol/Exchange/libs/LibOrder.sol";
+import "../protocol/Exchange/libs/LibFillResults.sol";
+import "../protocol/Exchange/libs/LibMath.sol";
+
+
+contract MixinExchangeWrapper is
+ LibAbiEncoder,
+ LibFillResults,
+ LibMath,
+ LibConstants,
+ MExchangeWrapper
+{
+
+ /// @dev Fills the input order.
+ /// Returns false if the transaction would otherwise revert.
+ /// @param order Order struct containing order specifications.
+ /// @param takerAssetFillAmount Desired amount of takerAsset to sell.
+ /// @param signature Proof that order has been created by maker.
+ /// @return Amounts filled and fees paid by maker and taker.
+ function fillOrderNoThrow(
+ LibOrder.Order memory order,
+ uint256 takerAssetFillAmount,
+ bytes memory signature
+ )
+ internal
+ returns (FillResults memory fillResults)
+ {
+ // ABI encode calldata for `fillOrder`
+ bytes memory fillOrderCalldata = abiEncodeFillOrder(
+ order,
+ takerAssetFillAmount,
+ signature
+ );
+
+ address exchange = address(EXCHANGE);
+
+ // Call `fillOrder` and handle any exceptions gracefully
+ assembly {
+ let success := call(
+ gas, // forward all gas, TODO: look into gas consumption of assert/throw
+ exchange, // call address of Exchange contract
+ 0, // transfer 0 wei
+ add(fillOrderCalldata, 32), // pointer to start of input (skip array length in first 32 bytes)
+ mload(fillOrderCalldata), // length of input
+ fillOrderCalldata, // write output over input
+ 128 // output size is 128 bytes
+ )
+ switch success
+ case 0 {
+ mstore(fillResults, 0)
+ mstore(add(fillResults, 32), 0)
+ mstore(add(fillResults, 64), 0)
+ mstore(add(fillResults, 96), 0)
+ }
+ case 1 {
+ mstore(fillResults, mload(fillOrderCalldata))
+ mstore(add(fillResults, 32), mload(add(fillOrderCalldata, 32)))
+ mstore(add(fillResults, 64), mload(add(fillOrderCalldata, 64)))
+ mstore(add(fillResults, 96), mload(add(fillOrderCalldata, 96)))
+ }
+ }
+ return fillResults;
+ }
+
+ /// @dev Synchronously executes multiple calls of fillOrder until total amount of WETH has been sold by taker.
+ /// Returns false if the transaction would otherwise revert.
+ /// @param orders Array of order specifications.
+ /// @param wethSellAmount Desired amount of WETH to sell.
+ /// @param signatures Proofs that orders have been signed by makers.
+ /// @return Amounts filled and fees paid by makers and taker.
+ function marketSellWeth(
+ LibOrder.Order[] memory orders,
+ uint256 wethSellAmount,
+ bytes[] memory signatures
+ )
+ internal
+ returns (FillResults memory totalFillResults)
+ {
+ bytes memory makerAssetData = orders[0].makerAssetData;
+ bytes memory wethAssetData = WETH_ASSET_DATA;
+
+ uint256 ordersLength = orders.length;
+ for (uint256 i = 0; i != ordersLength; i++) {
+
+ // We assume that asset being bought by taker is the same for each order.
+ // We assume that asset being sold by taker is WETH for each order.
+ orders[i].makerAssetData = makerAssetData;
+ orders[i].takerAssetData = wethAssetData;
+
+ // Calculate the remaining amount of WETH to sell
+ uint256 remainingTakerAssetFillAmount = safeSub(wethSellAmount, totalFillResults.takerAssetFilledAmount);
+
+ // Attempt to sell the remaining amount of WETH
+ FillResults memory singleFillResults = fillOrderNoThrow(
+ orders[i],
+ remainingTakerAssetFillAmount,
+ signatures[i]
+ );
+
+ // Update amounts filled and fees paid by maker and taker
+ addFillResults(totalFillResults, singleFillResults);
+
+ // Stop execution if the entire amount of takerAsset has been sold
+ if (totalFillResults.takerAssetFilledAmount >= wethSellAmount) {
+ break;
+ }
+ }
+ return totalFillResults;
+ }
+
+ /// @dev Synchronously executes multiple fill orders in a single transaction until total amount is bought by taker.
+ /// Returns false if the transaction would otherwise revert.
+ /// The asset being sold by taker must always be WETH.
+ /// @param orders Array of order specifications.
+ /// @param makerAssetFillAmount Desired amount of makerAsset to buy.
+ /// @param signatures Proofs that orders have been signed by makers.
+ /// @return Amounts filled and fees paid by makers and taker.
+ function 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), // we add 1 wei to the fill amount to make up for rounding errors
+ signatures[i]
+ );
+
+ // Update amounts filled and fees paid by maker and taker.
+ addFillResults(totalFillResults, singleFillResult);
+ zrxPurchased = safeSub(totalFillResults.makerAssetFilledAmount, totalFillResults.takerFeePaid);
+
+ // Stop execution if the entire amount of ZRX has been bought.
+ if (zrxPurchased >= zrxBuyAmount) {
+ break;
+ }
+ }
+
+ return totalFillResults;
+ }
+}
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..1164ae919 100644
--- a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol
+++ b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol
@@ -19,30 +19,30 @@
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/MConstants.sol";
+import "./libs/LibConstants.sol";
+import "./mixins/MWeth.sol";
+import "./mixins/MAssets.sol";
+import "./mixins/MExchangeWrapper.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";
contract MixinForwarderCore is
LibFillResults,
- MConstants,
- MExpectedResults,
- MFees,
- MMarketBuyZrx,
- MTransfer,
+ LibMath,
+ LibConstants,
+ MWeth,
+ MAssets,
+ MExchangeWrapper,
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;
+ using LibBytes for bytes;
+
+ /// @dev Constructor approves ERC20 proxy to transfer ZRX and WETH on this contract's behalf.
constructor ()
public
{
@@ -53,379 +53,202 @@ 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
- );
- 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(
+ // Convert ETH to WETH.
+ convertEthToWeth();
+
+ uint256 wethSellAmount;
+ uint256 zrxBuyAmount;
+ uint256 makerAssetAmountPurchased;
+ if (orders[0].makerAssetData.equals(ZRX_ASSET_DATA)) {
+ // Calculate amount of WETH that won't be spent on ETH fees.
+ wethSellAmount = getPartialAmount(
+ PERCENTAGE_DENOMINATOR,
+ safeAdd(PERCENTAGE_DENOMINATOR, feePercentage),
+ msg.value
+ );
+ // Market sell available WETH.
+ // ZRX fees are paid with this contract's balance.
+ orderFillResults = marketSellWeth(
orders,
- signatures,
- wethSellAmount
+ wethSellAmount,
+ signatures
);
+ // The fee amount must be deducted from the amount transfered back to sender.
+ makerAssetAmountPurchased = safeSub(orderFillResults.makerAssetFilledAmount, orderFillResults.takerFeePaid);
} else {
- totalFillResults = marketSellEthForERC20Internal(
+ // 5% of WETH is reserved for filling feeOrders and paying feeRecipient.
+ wethSellAmount = getPartialAmount(
+ MAX_WETH_FILL_PERCENTAGE,
+ PERCENTAGE_DENOMINATOR,
+ msg.value
+ );
+ // Market sell 95% of WETH.
+ // ZRX fees are payed with this contract's balance.
+ orderFillResults = marketSellWeth(
orders,
- signatures,
+ wethSellAmount,
+ signatures
+ );
+ // Buy back all ZRX spent on fees.
+ zrxBuyAmount = orderFillResults.takerFeePaid;
+ feeOrderFillResults = marketBuyZrxWithWeth(
feeOrders,
- feeSignatures,
- wethSellAmount
+ zrxBuyAmount,
+ feeSignatures
);
+ makerAssetAmountPurchased = orderFillResults.makerAssetFilledAmount;
}
- // Prevent accidental WETH owned by this contract and it being spent
- require(
- takerEthAmount >= totalFillResults.takerAssetFilledAmount,
- "INVALID_MSG_VALUE"
- );
- // Ensure no WETH is left in this contract
- require(
- wethSellAmount == totalFillResults.takerAssetFilledAmount,
- "UNACCEPTABLE_THRESHOLD"
+
+ // Ensure that all ZRX fees have been repurchased and no extra WETH owned by this contract has been sold.
+ assertValidFillResults(
+ orderFillResults,
+ feeOrderFillResults,
+ zrxBuyAmount
);
- // 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, makerAssetAmountPurchased);
}
- /// @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"
- );
- bytes4 assetDataId = LibBytes.readBytes4(orders[0].makerAssetData, 0);
- require(
- assetDataId == ERC20_DATA_ID || assetDataId == ERC721_DATA_ID,
- "UNSUPPORTED_TOKEN_PROXY"
- );
-
- ETHER_TOKEN.deposit.value(takerEthAmount)();
- if (assetDataId == ERC20_DATA_ID) {
- totalFillResults = marketBuyERC20TokensInternal(
+ // Convert ETH to WETH.
+ convertEthToWeth();
+
+ uint256 zrxBuyAmount;
+ uint256 makerAssetAmountPurchased;
+ if (orders[0].makerAssetData.equals(ZRX_ASSET_DATA)) {
+ // If the makerAsset is ZRX, it is not necessary to pay fees out of this
+ // contracts's ZRX balance because fees are factored into the price of the order.
+ orderFillResults = marketBuyZrxWithWeth(
orders,
- signatures,
- feeOrders,
- feeSignatures,
- makerTokenFillAmount
+ makerAssetFillAmount,
+ signatures
);
- } else if (assetDataId == ERC721_DATA_ID) {
- totalFillResults = batchBuyERC721TokensInternal(
+ // 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 = marketBuyWithWeth(
orders,
- signatures,
+ makerAssetFillAmount,
+ signatures
+ );
+ // Buy back all ZRX spent on fees.
+ zrxBuyAmount = orderFillResults.takerFeePaid;
+ feeOrderFillResults = marketBuyZrxWithWeth(
feeOrders,
+ zrxBuyAmount,
feeSignatures
);
+ makerAssetAmountPurchased = orderFillResults.makerAssetFilledAmount;
}
- // Prevent accidental WETH owned by this contract and it being spent
- require(
- takerEthAmount >= totalFillResults.takerAssetFilledAmount,
- "INVALID_MSG_VALUE"
+
+ // Ensure that all ZRX fees have been repurchased and no extra WETH owned by this contract has been sold.
+ assertValidFillResults(
+ orderFillResults,
+ feeOrderFillResults,
+ zrxBuyAmount
);
- 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;
- }
- /// @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(
- LibOrder.Order[] memory orders,
- bytes[] memory signatures,
- LibOrder.Order[] memory feeOrders,
- bytes[] memory feeSignatures,
- uint256 wethSellAmount
- )
- internal
- returns (FillResults memory totalFillResults)
- {
- 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;
- }
- // Make our market sell to buy the requested tokens with the remaining balance
- FillResults memory requestedTokensResults = EXCHANGE.marketSellOrders(
- orders,
- remainingWethSellAmount,
- signatures
- );
- // Update our return FillResult with the market sell
- addFillResults(totalFillResults, requestedTokensResults);
- return totalFillResults;
+ // Transfer purchased assets to msg.sender.
+ transferPurchasedAssetToSender(orders[0].makerAssetData, makerAssetAmountPurchased);
}
- /// @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(
- LibOrder.Order[] memory orders,
- bytes[] memory signatures,
- uint256 wethSellAmount
+ /// @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
- returns (FillResults memory totalFillResults)
+ view
{
- // Make our market sell to buy the requested tokens with the remaining balance
- totalFillResults = EXCHANGE.marketSellOrders(
- orders,
- wethSellAmount,
- 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);
+ // Ensure that all ZRX spent while filling primary orders has been repurchased.
+ uint256 zrxPurchased = safeSub(feeOrderFillResults.makerAssetFilledAmount, feeOrderFillResults.takerFeePaid);
require(
- isAcceptableThreshold(totalFillResults.makerAssetFilledAmount, zrxAmountBought),
- "UNACCEPTABLE_THRESHOLD"
+ zrxPurchased >= zrxBuyAmount,
+ "COMPLETE_FILL_FAILED"
);
- totalFillResults.makerAssetFilledAmount = zrxAmountBought;
- return 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)
- {
- // 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
- );
- totalFillResults.takerAssetFilledAmount = zrxMarketBuyResults.takerAssetFilledAmount;
- totalFillResults.takerFeePaid = zrxMarketBuyResults.takerFeePaid;
- }
- // 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);
+ // Ensure that no extra WETH owned by this contract has been sold.
+ uint256 wethSold = safeAdd(orderFillResults.takerAssetFilledAmount, feeOrderFillResults.takerAssetFilledAmount);
require(
- isAcceptableThreshold(
- totalFillResults.takerAssetFilledAmount,
- batchFillResults.takerAssetFilledAmount
- ),
- "UNACCEPTABLE_THRESHOLD"
+ wethSold <= msg.value,
+ "OVERSOLD_WETH"
);
- // Transfer all of the tokens filled from the batchFill
- for (i = 0; i < ordersLength; i++) {
- transferERC721Token(
- orders[i].makerAssetData,
- msg.sender
- );
- }
- 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/MixinWeth.sol b/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol
new file mode 100644
index 000000000..8ba236e7f
--- /dev/null
+++ b/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol
@@ -0,0 +1,110 @@
+/*
+
+ Copyright 2018 ZeroEx Intl.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+*/
+
+pragma solidity 0.4.24;
+
+import "../protocol/Exchange/libs/LibMath.sol";
+import "./libs/LibConstants.sol";
+import "./mixins/MWeth.sol";
+
+
+contract MixinWeth is
+ LibMath,
+ LibConstants,
+ MWeth
+{
+
+ /// @dev Default payabale function, this allows us to withdraw WETH
+ function ()
+ public
+ payable
+ {
+ require(
+ msg.sender == address(ETHER_TOKEN),
+ "DEFAULT_FUNCTION_WETH_CONTRACT_ONLY"
+ );
+ }
+
+ /// @dev Converts message call's ETH value into WETH.
+ function convertEthToWeth()
+ internal
+ {
+ require(
+ msg.value > 0,
+ "INVALID_MSG_VALUE"
+ );
+ ETHER_TOKEN.deposit.value(msg.value)();
+ }
+
+ /// @dev Transfers feePercentage of WETH spent on primary orders to feeRecipient.
+ /// Refunds any excess ETH to msg.sender.
+ /// @param wethSoldExcludingFeeOrders Amount of WETH sold when filling primary orders.
+ /// @param wethSoldForZrx Amount of WETH sold when purchasing ZRX required for primary order fees.
+ /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient.
+ /// @param feeRecipient Address that will receive ETH when orders are filled.
+ function transferEthFeeAndRefund(
+ uint256 wethSoldExcludingFeeOrders,
+ uint256 wethSoldForZrx,
+ uint256 feePercentage,
+ address feeRecipient
+ )
+ internal
+ {
+ // Ensure feePercentage is less than 5%.
+ require(
+ feePercentage <= MAX_FEE_PERCENTAGE,
+ "FEE_PERCENTAGE_TOO_LARGE"
+ );
+
+ // Calculate amount of WETH that hasn't been sold.
+ uint256 wethRemaining = safeSub(
+ msg.value,
+ safeAdd(wethSoldExcludingFeeOrders, wethSoldForZrx)
+ );
+
+ // Calculate ETH fee to pay to feeRecipient.
+ uint256 ethFee = getPartialAmount(
+ feePercentage,
+ PERCENTAGE_DENOMINATOR,
+ wethSoldExcludingFeeOrders
+ );
+
+ // Ensure fee is less than amount of WETH remaining.
+ require(
+ ethFee <= wethRemaining,
+ "INSUFFICIENT_ETH_REMAINING"
+ );
+
+ // Do nothing if no WETH remaining
+ if (wethRemaining > 0) {
+ // Convert remaining WETH to ETH
+ ETHER_TOKEN.withdraw(wethRemaining);
+
+ // Pay ETH to feeRecipient
+ if (ethFee > 0) {
+ feeRecipient.transfer(ethFee);
+ }
+
+ // Refund remaining ETH to msg.sender.
+ uint256 ethRefund = safeSub(wethRemaining, ethFee);
+ if (ethRefund > 0) {
+ msg.sender.transfer(ethRefund);
+ }
+ }
+ }
+}
diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MTransfer.sol b/packages/contracts/src/2.0.0/forwarder/interfaces/IAssets.sol
index 418a6288b..9b0d995eb 100644
--- a/packages/contracts/src/2.0.0/forwarder/mixins/MTransfer.sol
+++ b/packages/contracts/src/2.0.0/forwarder/interfaces/IAssets.sol
@@ -19,28 +19,16 @@
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(
+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,
- address to,
uint256 amount
)
- internal;
-
- function transferERC721Token(
- bytes memory assetData,
- address to
- )
- internal;
+ external;
}
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/MixinConstants.sol b/packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol
index 2b064d579..c26d7902c 100644
--- a/packages/contracts/src/2.0.0/forwarder/MixinConstants.sol
+++ b/packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol
@@ -18,12 +18,27 @@
pragma solidity 0.4.24;
-import "./mixins/MConstants.sol";
-
-
-contract MixinConstants is
- MConstants
-{
+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)"));
+ 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,
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..cdfb77a0b
--- /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 INSUFFICIENT_ETH_REMAINING = "INSUFFICIENT_ETH_REMAINING"; // Not enough ETH remaining to pay feeRecipient.
+ string constant OVERSOLD_WETH = "OVERSOLD_WETH"; // More WETH sold than provided with current message call.
+ string constant COMPLETE_FILL_FAILED = "COMPLETE_FILL_FAILED"; // Desired purchase amount not completely filled (required for ZRX fees only).
+ string constant TRANSFER_FAILED = "TRANSFER_FAILED"; // Asset transfer failed.
+ string constant UNSUPPORTED_TOKEN_PROXY = "UNSUPPORTED_TOKEN_PROXY"; // Proxy in assetData not supported.
+ string constant DEFAULT_FUNCTION_WETH_CONTRACT_ONLY = "DEFAULT_FUNCTION_WETH_CONTRACT_ONLY"; // Fallback function may only be used for WETH withdrawals.
+ string constant INVALID_MSG_VALUE = "INVALID_MSG_VALUE"; // msg.value must be greater than 0.
+ string constant INVALID_AMOUNT = "INVALID_AMOUNT"; // Amount must equal 1.
+}
diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MAssets.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MAssets.sol
new file mode 100644
index 000000000..340ee0bcb
--- /dev/null
+++ b/packages/contracts/src/2.0.0/forwarder/mixins/MAssets.sol
@@ -0,0 +1,54 @@
+/*
+
+ Copyright 2018 ZeroEx Intl.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+*/
+
+pragma solidity 0.4.24;
+
+import "../interfaces/IAssets.sol";
+
+
+contract MAssets is
+ IAssets
+{
+
+ /// @dev Transfers given amount of asset to sender.
+ /// @param assetData Byte array encoded for the respective asset proxy.
+ /// @param amount Amount of asset to transfer to sender.
+ function 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.
+ /// @param amount Amount of asset to transfer to sender.
+ function transferERC721Token(
+ bytes memory assetData,
+ uint256 amount
+ )
+ internal;
+}
diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MConstants.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MConstants.sol
deleted file mode 100644
index 348bf169e..000000000
--- a/packages/contracts/src/2.0.0/forwarder/mixins/MConstants.sol
+++ /dev/null
@@ -1,35 +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 {
-
- // 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
-}
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..5a2def7e5
--- /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);
+}
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..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,65 +28,15 @@ 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(
- LibOrder.Order[] memory orders,
- bytes[] memory signatures,
- LibOrder.Order[] memory feeOrders,
- bytes[] memory feeSignatures,
- uint256 wethSellAmount
+ /// @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 totalFillResults);
-
- /// @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(
- LibOrder.Order[] memory orders,
- bytes[] memory signatures,
- uint256 wethSellAmount
- )
- 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);
-
- /// @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);
+ view;
}
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/MWeth.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MWeth.sol
new file mode 100644
index 000000000..88e77be4e
--- /dev/null
+++ b/packages/contracts/src/2.0.0/forwarder/mixins/MWeth.sol
@@ -0,0 +1,41 @@
+/*
+
+ Copyright 2018 ZeroEx Intl.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+*/
+
+pragma solidity 0.4.24;
+
+
+contract MWeth {
+
+ /// @dev Converts message call's ETH value into WETH.
+ function convertEthToWeth()
+ internal;
+
+ /// @dev Transfers feePercentage of WETH spent on primary orders to feeRecipient.
+ /// Refunds any excess ETH to msg.sender.
+ /// @param wethSoldExcludingFeeOrders Amount of WETH sold when filling primary orders.
+ /// @param wethSoldForZrx Amount of WETH sold when purchasing ZRX required for primary order fees.
+ /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient.
+ /// @param feeRecipient Address that will receive ETH when orders are filled.
+ function transferEthFeeAndRefund(
+ uint256 wethSoldExcludingFeeOrders,
+ uint256 wethSoldForZrx,
+ uint256 feePercentage,
+ address feeRecipient
+ )
+ internal;
+}
diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol b/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol
index 567f26c52..86194f461 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.
@@ -56,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.
@@ -118,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],
@@ -143,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],
@@ -169,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],
@@ -195,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.
@@ -215,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;
}
}
@@ -238,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.
@@ -258,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;
}
}
@@ -280,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.
@@ -308,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;
}
}
@@ -331,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.
@@ -359,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;
}
}
@@ -371,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]);
}
}
@@ -384,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;
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)
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
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 0256d7d81..19639d3aa 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';
@@ -28,8 +27,7 @@ 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);
+const MAX_WETH_FILL_PERCENTAGE = 95;
describe(ContractName.Forwarder, () => {
let makerAddress: string;
@@ -47,25 +45,28 @@ describe(ContractName.Forwarder, () => {
let forwarderWrapper: ForwarderWrapper;
let exchangeWrapper: ExchangeWrapper;
- let signedOrder: SignedOrder;
- let signedOrders: SignedOrder[];
+ let orderWithoutFee: SignedOrder;
let orderWithFee: SignedOrder;
- let signedOrdersWithFee: SignedOrder[];
let feeOrder: SignedOrder;
- let feeOrders: SignedOrder[];
let orderFactory: OrderFactory;
let erc20Wrapper: ERC20Wrapper;
let erc20Balances: ERC20BalancesByOwner;
let tx: TransactionReceiptWithDecodedLogs;
let erc721MakerAssetIds: BigNumber[];
- let feeProportion: number = 0;
+ let takerEthBalanceBefore: BigNumber;
+ let feePercentage: BigNumber;
+ let gasPrice: BigNumber;
before(async () => {
await blockchainLifecycle.startAsync();
const accounts = await web3Wrapper.getAvailableAddressesAsync();
const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress, otherAddress] = accounts);
+ const txHash = await web3Wrapper.sendTransactionAsync({ from: accounts[0], to: accounts[0], value: 0 });
+ const transaction = await web3Wrapper.getTransactionByHashAsync(txHash);
+ gasPrice = new BigNumber(transaction.gasPrice);
+
const erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner);
erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
@@ -135,681 +136,865 @@ 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);
- web3Wrapper.abiDecoder.addABI(exchangeInstance.abi);
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
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, {
+
+ describe('marketSellOrdersWithEth without extra fees', () => {
+ it('should fill a single order', async () => {
+ const ordersWithoutFee = [orderWithoutFee];
+ const feeOrders: SignedOrder[] = [];
+ const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2);
+
+ tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, {
+ value: ethValue,
from: takerAddress,
- value: fillAmountWei,
});
- return expect(
- forwarderWrapper.calculateMarketBuyFillAmountWeiAsync(
- feeOrders,
- [],
- feeProportion,
- makerTokenFillAmount,
- ),
- ).to.be.rejectedWith('Unable to satisfy makerAssetFillAmount with provided orders');
+ const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress);
+ const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+
+ const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue(
+ ethValue,
+ MAX_WETH_FILL_PERCENTAGE,
+ );
+ const makerAssetFillAmount = primaryTakerAssetFillAmount
+ .times(orderWithoutFee.makerAssetAmount)
+ .dividedToIntegerBy(orderWithoutFee.takerAssetAmount);
+ const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed));
+
+ expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent));
+ expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount),
+ );
+ expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT);
+ expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ constants.ZERO_AMOUNT,
+ );
+ expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
- it('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');
+ 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);
});
- });
- describe('marketSellEthForERC20 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];
- feeOrders = [];
- tx = await forwarderWrapper.marketSellEthForERC20Async(signedOrders, feeOrders, {
- value: fillAmount,
+ it('should fill the order and pay ZRX fees from a single feeOrder', async () => {
+ const ordersWithFee = [orderWithFee];
+ const feeOrders = [feeOrder];
+ const ethValue = orderWithFee.takerAssetAmount.dividedToIntegerBy(2);
+
+ tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, {
+ value: ethValue,
from: takerAddress,
});
+ const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress);
+ const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address);
const newBalances = await erc20Wrapper.getBalancesAsync();
- const 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 feeAmount = ForwarderWrapper.getPercentageOfValue(
+ orderWithFee.takerFee.dividedToIntegerBy(2),
+ MAX_WETH_FILL_PERCENTAGE,
+ );
+ const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders);
+ const totalEthSpent = primaryTakerAssetFillAmount
+ .plus(wethSpentOnFeeOrders)
+ .plus(gasPrice.times(tx.gasUsed));
+
+ expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent));
+ expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders),
+ );
+ expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT);
+ expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ constants.ZERO_AMOUNT,
+ );
+ expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
- it('should fill the 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 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 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, 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),
});
- signedOrdersWithFee = [orderWithFee];
- feeOrders = [];
- const fillAmount = signedOrder.takerAssetAmount.div(4);
- const takerBalanceBefore = erc20Balances[takerAddress][zrxToken.address];
- tx = await forwarderWrapper.marketSellEthForERC20Async(signedOrdersWithFee, feeOrders, {
- value: fillAmount,
+ const ordersWithFee = [orderWithFee];
+ const feeOrders: SignedOrder[] = [];
+ const ethValue = orderWithFee.takerAssetAmount.dividedToIntegerBy(2);
+
+ tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, {
+ value: ethValue,
from: takerAddress,
});
+ const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress);
+ const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address);
const newBalances = await erc20Wrapper.getBalancesAsync();
- const 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(gasPrice.times(tx.gasUsed));
+ const takerFeePaid = orderWithFee.takerFee.dividedToIntegerBy(2);
+ const makerFeePaid = orderWithFee.makerFee.dividedToIntegerBy(2);
+
+ expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent));
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFillAmount).minus(makerFeePaid),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].plus(makerAssetFillAmount).minus(takerFeePaid),
+ );
+ expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][weth.address].plus(ethValue),
+ );
+ expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT);
+ expect(newBalances[forwarderContract.address][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[forwarderContract.address][zrxToken.address],
+ );
+ expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
- it('should 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 () => {
+ const ordersWithoutFee = [orderWithoutFee];
+ const feeOrders: SignedOrder[] = [];
+ 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(gasPrice.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];
+ const 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 feeOrders = [feeOrder];
+ 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,
- );
+ const ordersWithoutFee = [erc20SignedOrder, erc721SignedOrder];
+ const feeOrders: SignedOrder[] = [];
+ const ethValue = erc20SignedOrder.takerAssetAmount.plus(erc721SignedOrder.takerAssetAmount);
+
+ tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, {
+ value: ethValue,
+ from: takerAddress,
+ });
+ const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress);
+ const totalEthSpent = erc20SignedOrder.takerAssetAmount.plus(gasPrice.times(tx.gasUsed));
+
+ expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent));
});
});
- describe('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%
- feeOrders = [];
- tx = await forwarderWrapper.marketSellEthForERC20Async(
- signedOrders,
+ describe('marketSellOrdersWithEth with extra fees', () => {
+ it('should fill the order and send fee to feeRecipient', async () => {
+ const ordersWithoutFee = [orderWithoutFee];
+ const feeOrders: SignedOrder[] = [];
+ const ethValue = orderWithoutFee.takerAssetAmount.div(2);
+
+ const baseFeePercentage = 2;
+ feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage);
+ const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress);
+ tx = await forwarderWrapper.marketSellOrdersWithEthAsync(
+ ordersWithoutFee,
feeOrders,
{
+ value: ethValue,
from: takerAddress,
- 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(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[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%
- feeOrders = [];
+ const ethValue = orderWithoutFee.takerAssetAmount.div(2);
+ const baseFeePercentage = 6;
+ feePercentage = ForwarderWrapper.getPercentageOfValue(ethValue, baseFeePercentage);
+ const ordersWithoutFee = [orderWithoutFee];
+ const feeOrders: SignedOrder[] = [];
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 },
+ { feePercentage, feeRecipient: feeRecipientAddress },
),
- RevertReason.FeeProportionTooLarge,
+ RevertReason.FeePercentageTooLarge,
+ );
+ });
+ it('should fail if there is not enough ETH remaining to pay the fee', async () => {
+ const ethValue = orderWithoutFee.takerAssetAmount.div(2);
+ const baseFeePercentage = 5;
+ feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage);
+ const ordersWithFee = [orderWithFee];
+ const feeOrders = [feeOrder];
+ await expectTransactionFailedAsync(
+ forwarderWrapper.marketSellOrdersWithEthAsync(
+ ordersWithFee,
+ feeOrders,
+ { from: takerAddress, value: ethValue, gasPrice },
+ { feePercentage, feeRecipient: feeRecipientAddress },
+ ),
+ RevertReason.InsufficientEthRemaining,
);
- const afterEthBalance = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress);
- expect(afterEthBalance).to.be.bignumber.equal(initEthBalance);
});
});
- describe('marketBuyTokensWithEth', () => {
- 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);
- feeOrders = [];
- tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, {
+ describe('marketBuyOrdersWithEth without extra fees', () => {
+ it('should buy the exact amount of makerAsset in a single order', async () => {
+ const ordersWithoutFee = [orderWithoutFee];
+ const feeOrders: SignedOrder[] = [];
+ const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2);
+ const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2);
+
+ tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, {
+ value: ethValue,
from: takerAddress,
- 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(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 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, {
+ 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,
- value: excessFillAmount,
- 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(fillAmount).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(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 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, {
+ it('should buy the exact amount of makerAsset and return excess ETH', async () => {
+ const ordersWithoutFee = [orderWithoutFee];
+ const feeOrders: SignedOrder[] = [];
+ const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2);
+ const ethValue = orderWithoutFee.takerAssetAmount;
+
+ tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, {
+ value: ethValue,
from: takerAddress,
- value: excessFillAmount,
});
+ 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];
- 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];
- 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,
+
+ const primaryTakerAssetFillAmount = ethValue.dividedToIntegerBy(2);
+ const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed));
+
+ expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent));
+ expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount),
);
- tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrdersWithFee, feeOrders, makerAssetAmount, {
+ expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT);
+ expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ constants.ZERO_AMOUNT,
+ );
+ expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT);
+ });
+ it('should buy the exact amount of makerAsset and pay ZRX from feeOrders', async () => {
+ const ordersWithFee = [orderWithFee];
+ const feeOrders = [feeOrder];
+ const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2);
+ const ethValue = orderWithFee.takerAssetAmount;
+
+ tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, {
+ value: ethValue,
from: takerAddress,
- 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 = orderWithFee.takerAssetAmount.dividedToIntegerBy(2);
+ const feeAmount = orderWithFee.takerFee.dividedToIntegerBy(2);
+ const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders);
+ const totalEthSpent = primaryTakerAssetFillAmount
+ .plus(wethSpentOnFeeOrders)
+ .plus(gasPrice.times(tx.gasUsed));
+
+ expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent));
+ expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount),
);
- 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),
);
+ 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('throws if fees are higher than 5% when buying erc20', async () => {
- const highFeeERC20Order = await orderFactory.newSignedOrderAsync({
- takerFee: signedOrder.makerAssetAmount.times(0.06),
+ 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),
});
- signedOrdersWithFee = [highFeeERC20Order];
- feeOrders = [feeOrder];
- const makerAssetAmount = signedOrder.makerAssetAmount.div(2);
- const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync(
- signedOrdersWithFee,
- feeOrders,
- feeProportion,
- makerAssetAmount,
+ const ordersWithFee = [orderWithFee];
+ const feeOrders: SignedOrder[] = [];
+ const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2);
+ const ethValue = orderWithFee.takerAssetAmount;
+ tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, {
+ value: ethValue,
+ from: takerAddress,
+ });
+ const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress);
+ const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+
+ const primaryTakerAssetFillAmount = ForwarderWrapper.getWethForFeeOrders(
+ makerAssetFillAmount,
+ ordersWithFee,
);
- return expectTransactionFailedAsync(
- forwarderWrapper.marketBuyTokensWithEthAsync(signedOrdersWithFee, feeOrders, makerAssetAmount, {
- from: takerAddress,
- value: fillAmountWei,
- }),
- RevertReason.UnacceptableThreshold as any,
+ const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed));
+ const makerAssetFilledAmount = orderWithFee.makerAssetAmount
+ .times(primaryTakerAssetFillAmount)
+ .dividedToIntegerBy(orderWithFee.takerAssetAmount);
+ const takerFeePaid = orderWithFee.takerFee
+ .times(primaryTakerAssetFillAmount)
+ .dividedToIntegerBy(orderWithFee.takerAssetAmount);
+ const makerFeePaid = orderWithFee.makerFee
+ .times(primaryTakerAssetFillAmount)
+ .dividedToIntegerBy(orderWithFee.takerAssetAmount);
+ const totalZrxPurchased = makerAssetFilledAmount.minus(takerFeePaid);
+ // Up to 1 wei worth of ZRX will be overbought per order
+ const maxOverboughtZrx = new BigNumber(1)
+ .times(orderWithFee.makerAssetAmount)
+ .dividedToIntegerBy(orderWithFee.takerAssetAmount);
+
+ expect(totalZrxPurchased).to.be.bignumber.gte(makerAssetFillAmount);
+ expect(totalZrxPurchased).to.be.bignumber.lte(makerAssetFillAmount.plus(maxOverboughtZrx));
+ expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent));
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFilledAmount).minus(makerFeePaid),
);
- });
- it('throws if makerAssetAmount is 0', async () => {
- const makerAssetAmount = new BigNumber(0);
- const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync(
- signedOrdersWithFee,
- feeOrders,
- feeProportion,
- makerAssetAmount,
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].plus(totalZrxPurchased),
);
- return expectTransactionFailedAsync(
- forwarderWrapper.marketBuyTokensWithEthAsync(signedOrdersWithFee, feeOrders, makerAssetAmount, {
- from: takerAddress,
- value: fillAmountWei,
- }),
- RevertReason.ValueGreaterThanZero as any,
+ expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount),
+ );
+ expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT);
+ expect(newBalances[forwarderContract.address][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[forwarderContract.address][zrxToken.address],
);
+ expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT);
});
- it('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 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,
- 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 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);
});
- });
- describe('marketBuyTokensWithEth - ERC721', async () => {
- it('buys ERC721 assets', async () => {
+ it('should buy an ERC721 asset from a single order', async () => {
const makerAssetId = erc721MakerAssetIds[0];
- signedOrder = await orderFactory.newSignedOrderAsync({
+ orderWithoutFee = 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,
- );
- tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, {
+ 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: fillAmountWei,
+ value: ethValue,
});
- const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
- expect(newOwnerTakerAsset).to.be.bignumber.equal(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 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('buys ERC721 assets with fee abstraction', async () => {
+ it('should buy an ERC721 asset and ignore later orders with different makerAssetData', async () => {
const makerAssetId = erc721MakerAssetIds[0];
- signedOrder = await orderFactory.newSignedOrderAsync({
+ orderWithoutFee = await orderFactory.newSignedOrderAsync({
makerAssetAmount: new BigNumber(1),
- 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, {
+ 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: fillAmountWei,
+ value: ethValue,
});
- 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,
+ 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),
);
- tx = await forwarderWrapper.marketBuyTokensWithEthAsync(
- signedOrders,
- feeOrders,
- makerAssetAmount,
- {
- from: takerAddress,
- value: fillAmountWei,
- gasPrice: DEFAULT_GAS_PRICE,
- },
- {
- feeProportion,
- feeRecipient: feeRecipientAddress,
- },
+ 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,
);
- 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,
+ 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],
);
- 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 () => {
+ it('should buy an ERC721 asset and pay ZRX fees from a single fee order', 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),
+ orderWithFee = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(1),
makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT),
});
- 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, {
+ 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,
- 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);
+ 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('buys ERC721 assets with fee abstraction and handles fee orders filled', async () => {
+ it('should buy an ERC721 asset and pay ZRX fees from multiple fee orders', 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),
+ orderWithFee = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(1),
makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT),
});
- const zrxMakerAssetAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(8), DECIMALS_DEFAULT);
- signedOrders = [signedOrder];
+ 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({
- makerAssetAmount: zrxMakerAssetAmount,
- takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.1), DECIMALS_DEFAULT),
- makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address),
- takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT),
+ makerAssetData,
+ makerAssetAmount,
+ takerAssetAmount,
});
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,
+ makerAssetData,
+ makerAssetAmount,
+ takerAssetAmount,
});
- const expectedFillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync(
- signedOrders,
- feeOrders,
- feeProportion,
- erc721MakerAssetAmount,
- );
- tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, {
+ 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,
- value: expectedFillAmountWei,
});
- const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
- expect(newOwnerTakerAsset).to.be.bignumber.equal(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('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, {
+ });
+ 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,
- value: fillAmountWei,
- }),
- RevertReason.LibBytesGreaterOrEqualTo32LengthRequired,
+ },
+ { 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('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,
+ 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,
);
});
});
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..ef7476e36 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.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);
+ 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<TransactionReceiptWithDecodedLogs> {
- 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<TransactionReceiptWithDecodedLogs> {
- 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<BigNumber> {
- 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<BigNumber> {
- 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<BigNumber> {
- // 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<TransactionReceiptWithDecodedLogs> {
+ 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..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',
- FeeProportionTooLarge = 'FEE_PROPORTION_TOO_LARGE',
+ FeePercentageTooLarge = 'FEE_PERCENTAGE_TOO_LARGE',
ValueGreaterThanZero = 'VALUE_GREATER_THAN_ZERO',
InvalidMsgValue = 'INVALID_MSG_VALUE',
+ InsufficientEthRemaining = 'INSUFFICIENT_ETH_REMAINING',
}
export enum StatusCodes {
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,5 +1,14 @@
[
{
+ "version": "1.1.0",
+ "changes": [
+ {
+ "note": "Add `getTransactionByHashAsync` method",
+ "pr": 847
+ }
+ ]
+ },
+ {
"timestamp": 1532357734,
"version": "1.0.1",
"changes": [
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,
@@ -221,6 +222,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<Transaction> {
+ assert.isHexString('txHash', txHash);
+ const transaction = await this._sendRawPayloadAsync<Transaction>({
+ method: 'eth_getTransactionByHash',
+ params: [txHash],
+ });
+ return transaction;
+ }
+ /**
* Retrieves an accounts Ether balance in wei
* @param owner Account whose balance you wish to check
* @returns Balance in wei