aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/stale.yml2
-rw-r--r--contracts/extensions/CHANGELOG.json9
-rw-r--r--contracts/extensions/compiler.json2
-rw-r--r--contracts/extensions/contracts/BalanceThresholdFilter/BalanceThresholdFilter.sol45
-rw-r--r--contracts/extensions/contracts/BalanceThresholdFilter/MixinBalanceThresholdFilterCore.sol135
-rw-r--r--contracts/extensions/contracts/BalanceThresholdFilter/MixinExchangeCalldata.sol103
-rw-r--r--contracts/extensions/contracts/BalanceThresholdFilter/interfaces/IBalanceThresholdFilterCore.sol55
-rw-r--r--contracts/extensions/contracts/BalanceThresholdFilter/interfaces/IThresholdAsset.sol31
-rw-r--r--contracts/extensions/contracts/BalanceThresholdFilter/mixins/MBalanceThresholdFilterCore.sol54
-rw-r--r--contracts/extensions/contracts/BalanceThresholdFilter/mixins/MExchangeCalldata.sol56
-rw-r--r--contracts/extensions/package.json2
-rw-r--r--contracts/extensions/src/artifacts/index.ts2
-rw-r--r--contracts/extensions/src/wrappers/index.ts1
-rw-r--r--contracts/extensions/test/extensions/balance_threshold_filter.ts1644
-rw-r--r--contracts/extensions/test/utils/balance_threshold_wrapper.ts283
-rw-r--r--contracts/extensions/tsconfig.json6
-rw-r--r--contracts/libs/contracts/libs/LibAddressArray.sol84
-rw-r--r--contracts/libs/contracts/libs/LibExchangeSelectors.sol152
-rw-r--r--contracts/multisig/src/index.ts2
-rw-r--r--contracts/protocol/CHANGELOG.json9
-rw-r--r--contracts/protocol/package.json2
-rw-r--r--contracts/test-utils/src/types.ts1
-rw-r--r--packages/types/src/index.ts4
23 files changed, 2679 insertions, 5 deletions
diff --git a/.github/stale.yml b/.github/stale.yml
index af12c62d5..09eb40a77 100644
--- a/.github/stale.yml
+++ b/.github/stale.yml
@@ -1,7 +1,7 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 30
# Number of days of inactivity before a stale issue is closed
-daysUntilClose: 7
+daysUntilClose: 30
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
diff --git a/contracts/extensions/CHANGELOG.json b/contracts/extensions/CHANGELOG.json
index 19ac770af..da4d9c2ba 100644
--- a/contracts/extensions/CHANGELOG.json
+++ b/contracts/extensions/CHANGELOG.json
@@ -1,5 +1,14 @@
[
{
+ "version": "1.1.0",
+ "changes": [
+ {
+ "note": "Added Balance Threshold Filter",
+ "pr": 1383
+ }
+ ]
+ },
+ {
"timestamp": 1544741676,
"version": "1.0.2",
"changes": [
diff --git a/contracts/extensions/compiler.json b/contracts/extensions/compiler.json
index 69d607b3e..e6ed0c215 100644
--- a/contracts/extensions/compiler.json
+++ b/contracts/extensions/compiler.json
@@ -18,5 +18,5 @@
}
}
},
- "contracts": ["DutchAuction", "Forwarder"]
+ "contracts": ["BalanceThresholdFilter", "DutchAuction", "Forwarder"]
}
diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/BalanceThresholdFilter.sol b/contracts/extensions/contracts/BalanceThresholdFilter/BalanceThresholdFilter.sol
new file mode 100644
index 000000000..16cacd461
--- /dev/null
+++ b/contracts/extensions/contracts/BalanceThresholdFilter/BalanceThresholdFilter.sol
@@ -0,0 +1,45 @@
+/*
+
+ Copyright 2018 ZeroEx Intl.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+*/
+
+pragma solidity 0.4.24;
+
+import "@0x/contracts-interfaces/contracts/protocol/Exchange/IExchange.sol";
+import "./interfaces/IThresholdAsset.sol";
+import "./MixinBalanceThresholdFilterCore.sol";
+
+
+contract BalanceThresholdFilter is
+ MixinBalanceThresholdFilterCore
+{
+
+ /// @dev Constructs BalanceThresholdFilter.
+ /// @param exchange Address of 0x exchange.
+ /// @param thresholdAsset The asset that must be held by makers/takers.
+ /// @param balanceThreshold The minimum balance of `thresholdAsset` that must be held by makers/takers.
+ constructor(
+ address exchange,
+ address thresholdAsset,
+ uint256 balanceThreshold
+ )
+ public
+ {
+ EXCHANGE = IExchange(exchange);
+ THRESHOLD_ASSET = IThresholdAsset(thresholdAsset);
+ BALANCE_THRESHOLD = balanceThreshold;
+ }
+}
diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/MixinBalanceThresholdFilterCore.sol b/contracts/extensions/contracts/BalanceThresholdFilter/MixinBalanceThresholdFilterCore.sol
new file mode 100644
index 000000000..df830f36e
--- /dev/null
+++ b/contracts/extensions/contracts/BalanceThresholdFilter/MixinBalanceThresholdFilterCore.sol
@@ -0,0 +1,135 @@
+/*
+
+ Copyright 2018 ZeroEx Intl.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+*/
+
+pragma solidity 0.4.24;
+
+import "@0x/contracts-libs/contracts/libs/LibExchangeSelectors.sol";
+import "@0x/contracts-libs/contracts/libs/LibOrder.sol";
+import "./mixins/MBalanceThresholdFilterCore.sol";
+import "./MixinExchangeCalldata.sol";
+
+
+contract MixinBalanceThresholdFilterCore is
+ MBalanceThresholdFilterCore,
+ MixinExchangeCalldata,
+ LibOrder,
+ LibExchangeSelectors
+{
+
+ /// @dev Executes an Exchange transaction iff the maker and taker meet
+ /// the hold at least `BALANCE_THRESHOLD` of the asset `THRESHOLD_ASSET` OR
+ /// the exchange function is a cancellation.
+ /// Supported Exchange functions:
+ /// batchFillOrders
+ /// batchFillOrdersNoThrow
+ /// batchFillOrKillOrders
+ /// fillOrder
+ /// fillOrderNoThrow
+ /// fillOrKillOrder
+ /// marketBuyOrders
+ /// marketBuyOrdersNoThrow
+ /// marketSellOrders
+ /// marketSellOrdersNoThrow
+ /// matchOrders
+ /// cancelOrder
+ /// batchCancelOrders
+ /// cancelOrdersUpTo
+ /// Trying to call any other exchange function will throw.
+ /// @param salt Arbitrary number to ensure uniqueness of transaction hash.
+ /// @param signerAddress Address of transaction signer.
+ /// @param signedExchangeTransaction AbiV2 encoded calldata.
+ /// @param signature Proof of signer transaction by signer.
+ function executeTransaction(
+ uint256 salt,
+ address signerAddress,
+ bytes signedExchangeTransaction,
+ bytes signature
+ )
+ external
+ {
+ // Get accounts whose balances must be validated
+ address[] memory addressesToValidate = getAddressesToValidate(signerAddress);
+
+ // Validate account balances
+ uint256 balanceThreshold = BALANCE_THRESHOLD;
+ IThresholdAsset thresholdAsset = THRESHOLD_ASSET;
+ for (uint256 i = 0; i < addressesToValidate.length; ++i) {
+ uint256 addressBalance = thresholdAsset.balanceOf(addressesToValidate[i]);
+ require(
+ addressBalance >= balanceThreshold,
+ "AT_LEAST_ONE_ADDRESS_DOES_NOT_MEET_BALANCE_THRESHOLD"
+ );
+ }
+ emit ValidatedAddresses(addressesToValidate);
+
+ // All addresses are valid. Execute exchange function.
+ EXCHANGE.executeTransaction(
+ salt,
+ signerAddress,
+ signedExchangeTransaction,
+ signature
+ );
+ }
+
+ /// @dev Constructs an array of addresses to be validated.
+ /// Addresses depend on which Exchange function is to be called
+ /// (defined by `signedExchangeTransaction` above).
+ /// @param signerAddress Address of transaction signer.
+ /// @return addressesToValidate Array of addresses to validate.
+ function getAddressesToValidate(address signerAddress)
+ internal pure
+ returns (address[] memory addressesToValidate)
+ {
+ bytes4 exchangeFunctionSelector = bytes4(exchangeCalldataload(0));
+ // solhint-disable expression-indent
+ if (
+ exchangeFunctionSelector == BATCH_FILL_ORDERS_SELECTOR ||
+ exchangeFunctionSelector == BATCH_FILL_ORDERS_NO_THROW_SELECTOR ||
+ exchangeFunctionSelector == BATCH_FILL_OR_KILL_ORDERS_SELECTOR ||
+ exchangeFunctionSelector == MARKET_BUY_ORDERS_SELECTOR ||
+ exchangeFunctionSelector == MARKET_BUY_ORDERS_NO_THROW_SELECTOR ||
+ exchangeFunctionSelector == MARKET_SELL_ORDERS_SELECTOR ||
+ exchangeFunctionSelector == MARKET_SELL_ORDERS_NO_THROW_SELECTOR
+ ) {
+ addressesToValidate = loadMakerAddressesFromOrderArray(0);
+ addressesToValidate = addressesToValidate.append(signerAddress);
+ } else if (
+ exchangeFunctionSelector == FILL_ORDER_SELECTOR ||
+ exchangeFunctionSelector == FILL_ORDER_NO_THROW_SELECTOR ||
+ exchangeFunctionSelector == FILL_OR_KILL_ORDER_SELECTOR
+ ) {
+ address makerAddress = loadMakerAddressFromOrder(0);
+ addressesToValidate = addressesToValidate.append(makerAddress);
+ addressesToValidate = addressesToValidate.append(signerAddress);
+ } else if (exchangeFunctionSelector == MATCH_ORDERS_SELECTOR) {
+ address leftMakerAddress = loadMakerAddressFromOrder(0);
+ addressesToValidate = addressesToValidate.append(leftMakerAddress);
+ address rightMakerAddress = loadMakerAddressFromOrder(1);
+ addressesToValidate = addressesToValidate.append(rightMakerAddress);
+ addressesToValidate = addressesToValidate.append(signerAddress);
+ } else if (
+ exchangeFunctionSelector != CANCEL_ORDER_SELECTOR &&
+ exchangeFunctionSelector != BATCH_CANCEL_ORDERS_SELECTOR &&
+ exchangeFunctionSelector != CANCEL_ORDERS_UP_TO_SELECTOR
+ ) {
+ revert("INVALID_OR_BLOCKED_EXCHANGE_SELECTOR");
+ }
+ // solhint-enable expression-indent
+ return addressesToValidate;
+ }
+}
diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/MixinExchangeCalldata.sol b/contracts/extensions/contracts/BalanceThresholdFilter/MixinExchangeCalldata.sol
new file mode 100644
index 000000000..bd26a468f
--- /dev/null
+++ b/contracts/extensions/contracts/BalanceThresholdFilter/MixinExchangeCalldata.sol
@@ -0,0 +1,103 @@
+
+ /*
+
+ Copyright 2018 ZeroEx Intl.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+*/
+
+pragma solidity 0.4.24;
+
+import "./mixins/MExchangeCalldata.sol";
+import "@0x/contracts-libs/contracts/libs/LibAddressArray.sol";
+
+
+contract MixinExchangeCalldata is
+ MExchangeCalldata
+{
+
+ using LibAddressArray for address[];
+
+ /// @dev Emulates the `calldataload` opcode on the embedded Exchange calldata,
+ /// which is accessed through `signedExchangeTransaction`.
+ /// @param offset Offset into the Exchange calldata.
+ /// @return value Corresponding 32 byte value stored at `offset`.
+ function exchangeCalldataload(uint256 offset)
+ internal pure
+ returns (bytes32 value)
+ {
+ assembly {
+ // Pointer to exchange transaction
+ // 0x04 for calldata selector
+ // 0x40 to access `signedExchangeTransaction`, which is the third parameter
+ let exchangeTxPtr := calldataload(0x44)
+
+ // Offset into Exchange calldata
+ // We compute this by adding 0x24 to the `exchangeTxPtr` computed above.
+ // 0x04 for calldata selector
+ // 0x20 for length field of `signedExchangeTransaction`
+ let exchangeCalldataOffset := add(exchangeTxPtr, add(0x24, offset))
+ value := calldataload(exchangeCalldataOffset)
+ }
+ return value;
+ }
+
+ /// @dev Convenience function that skips the 4 byte selector when loading
+ /// from the embedded Exchange calldata.
+ /// @param offset Offset into the Exchange calldata (minus the 4 byte selector)
+ /// @return value Corresponding 32 byte value stored at `offset` + 4.
+ function loadExchangeData(uint256 offset)
+ internal pure
+ returns (bytes32 value)
+ {
+ value = exchangeCalldataload(offset + 4);
+ return value;
+ }
+
+ /// @dev Extracts the maker address from an order stored in the Exchange calldata
+ /// (which is embedded in `signedExchangeTransaction`).
+ /// @param orderParamIndex Index of the order in the Exchange function's signature.
+ /// @return makerAddress The extracted maker address.
+ function loadMakerAddressFromOrder(uint256 orderParamIndex)
+ internal pure
+ returns (address makerAddress)
+ {
+ uint256 orderOffsetInBytes = orderParamIndex * 32;
+ uint256 orderPtr = uint256(loadExchangeData(orderOffsetInBytes));
+ makerAddress = address(loadExchangeData(orderPtr));
+ return makerAddress;
+ }
+
+ /// @dev Extracts the maker addresses from an array of orders stored in the Exchange calldata
+ /// (which is embedded in `signedExchangeTransaction`).
+ /// @param orderArrayParamIndex Index of the order array in the Exchange function's signature
+ /// @return makerAddresses The extracted maker addresses.
+ function loadMakerAddressesFromOrderArray(uint256 orderArrayParamIndex)
+ internal pure
+ returns (address[] makerAddresses)
+ {
+ uint256 orderArrayOffsetInBytes = orderArrayParamIndex * 32;
+ uint256 orderArrayPtr = uint256(loadExchangeData(orderArrayOffsetInBytes));
+ uint256 orderArrayLength = uint256(loadExchangeData(orderArrayPtr));
+ uint256 orderArrayLengthInBytes = orderArrayLength * 32;
+ uint256 orderArrayElementPtr = orderArrayPtr + 32;
+ uint256 orderArrayElementEndPtr = orderArrayElementPtr + orderArrayLengthInBytes;
+ for (uint orderPtrOffset = orderArrayElementPtr; orderPtrOffset < orderArrayElementEndPtr; orderPtrOffset += 32) {
+ uint256 orderPtr = uint256(loadExchangeData(orderPtrOffset));
+ address makerAddress = address(loadExchangeData(orderPtr + orderArrayElementPtr));
+ makerAddresses = makerAddresses.append(makerAddress);
+ }
+ return makerAddresses;
+ }
+}
diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/interfaces/IBalanceThresholdFilterCore.sol b/contracts/extensions/contracts/BalanceThresholdFilter/interfaces/IBalanceThresholdFilterCore.sol
new file mode 100644
index 000000000..3d8e2bbd1
--- /dev/null
+++ b/contracts/extensions/contracts/BalanceThresholdFilter/interfaces/IBalanceThresholdFilterCore.sol
@@ -0,0 +1,55 @@
+
+/*
+
+ Copyright 2018 ZeroEx Intl.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+*/
+
+pragma solidity 0.4.24;
+
+
+contract IBalanceThresholdFilterCore {
+
+ /// @dev Executes an Exchange transaction iff the maker and taker meet
+ /// the hold at least `BALANCE_THRESHOLD` of the asset `THRESHOLD_ASSET` OR
+ /// the exchange function is a cancellation.
+ /// Supported Exchange functions:
+ /// - batchFillOrders
+ /// - batchFillOrdersNoThrow
+ /// - batchFillOrKillOrders
+ /// - fillOrder
+ /// - fillOrderNoThrow
+ /// - fillOrKillOrder
+ /// - marketBuyOrders
+ /// - marketBuyOrdersNoThrow
+ /// - marketSellOrders
+ /// - marketSellOrdersNoThrow
+ /// - matchOrders
+ /// - cancelOrder
+ /// - batchCancelOrders
+ /// - cancelOrdersUpTo
+ /// Trying to call any other exchange function will throw.
+ /// @param salt Arbitrary number to ensure uniqueness of transaction hash.
+ /// @param signerAddress Address of transaction signer.
+ /// @param signedExchangeTransaction AbiV2 encoded calldata.
+ /// @param signature Proof of signer transaction by signer.
+ function executeTransaction(
+ uint256 salt,
+ address signerAddress,
+ bytes signedExchangeTransaction,
+ bytes signature
+ )
+ external;
+}
diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/interfaces/IThresholdAsset.sol b/contracts/extensions/contracts/BalanceThresholdFilter/interfaces/IThresholdAsset.sol
new file mode 100644
index 000000000..3e424b9f4
--- /dev/null
+++ b/contracts/extensions/contracts/BalanceThresholdFilter/interfaces/IThresholdAsset.sol
@@ -0,0 +1,31 @@
+/*
+
+ Copyright 2018 ZeroEx Intl.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+*/
+
+pragma solidity 0.4.24;
+
+
+contract IThresholdAsset {
+
+ /// @param _owner The address from which the balance will be retrieved
+ /// @return Balance of owner
+ function balanceOf(address _owner)
+ external
+ view
+ returns (uint256);
+
+}
diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/mixins/MBalanceThresholdFilterCore.sol b/contracts/extensions/contracts/BalanceThresholdFilter/mixins/MBalanceThresholdFilterCore.sol
new file mode 100644
index 000000000..b8b67e6ee
--- /dev/null
+++ b/contracts/extensions/contracts/BalanceThresholdFilter/mixins/MBalanceThresholdFilterCore.sol
@@ -0,0 +1,54 @@
+/*
+
+ Copyright 2018 ZeroEx Intl.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+*/
+
+pragma solidity 0.4.24;
+
+import "@0x/contracts-interfaces/contracts/protocol/Exchange/IExchange.sol";
+import "../interfaces/IThresholdAsset.sol";
+import "../interfaces/IBalanceThresholdFilterCore.sol";
+
+
+contract MBalanceThresholdFilterCore is
+ IBalanceThresholdFilterCore
+{
+
+ // Points to 0x exchange contract
+ // solhint-disable var-name-mixedcase
+ IExchange internal EXCHANGE;
+
+ // The asset that must be held by makers/takers
+ IThresholdAsset internal THRESHOLD_ASSET;
+
+ // The minimum balance of `THRESHOLD_ASSET` that must be held by makers/takers
+ uint256 internal BALANCE_THRESHOLD;
+ // solhint-enable var-name-mixedcase
+
+ // Addresses that hold at least `BALANCE_THRESHOLD` of `THRESHOLD_ASSET`
+ event ValidatedAddresses (
+ address[] addresses
+ );
+
+ /// @dev Constructs an array of addresses to be validated.
+ /// Addresses depend on which Exchange function is to be called
+ /// (defined by `signedExchangeTransaction` above).
+ /// @param signerAddress Address of transaction signer.
+ /// @return addressesToValidate Array of addresses to validate.
+ function getAddressesToValidate(address signerAddress)
+ internal pure
+ returns (address[] memory addressesToValidate);
+}
diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/mixins/MExchangeCalldata.sol b/contracts/extensions/contracts/BalanceThresholdFilter/mixins/MExchangeCalldata.sol
new file mode 100644
index 000000000..bf2940fe1
--- /dev/null
+++ b/contracts/extensions/contracts/BalanceThresholdFilter/mixins/MExchangeCalldata.sol
@@ -0,0 +1,56 @@
+
+ /*
+
+ Copyright 2018 ZeroEx Intl.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+*/
+
+pragma solidity 0.4.24;
+
+
+contract MExchangeCalldata {
+
+ /// @dev Emulates the `calldataload` opcode on the embedded Exchange calldata,
+ /// which is accessed through `signedExchangeTransaction`.
+ /// @param offset Offset into the Exchange calldata.
+ /// @return value Corresponding 32 byte value stored at `offset`.
+ function exchangeCalldataload(uint256 offset)
+ internal pure
+ returns (bytes32 value);
+
+ /// @dev Convenience function that skips the 4 byte selector when loading
+ /// from the embedded Exchange calldata.
+ /// @param offset Offset into the Exchange calldata (minus the 4 byte selector)
+ /// @return value Corresponding 32 byte value stored at `offset` + 4.
+ function loadExchangeData(uint256 offset)
+ internal pure
+ returns (bytes32 value);
+
+ /// @dev Extracts the maker address from an order stored in the Exchange calldata
+ /// (which is embedded in `signedExchangeTransaction`).
+ /// @param orderParamIndex Index of the order in the Exchange function's signature.
+ /// @return makerAddress The extracted maker address.
+ function loadMakerAddressFromOrder(uint256 orderParamIndex)
+ internal pure
+ returns (address makerAddress);
+
+ /// @dev Extracts the maker addresses from an array of orders stored in the Exchange calldata
+ /// (which is embedded in `signedExchangeTransaction`).
+ /// @param orderArrayParamIndex Index of the order array in the Exchange function's signature
+ /// @return makerAddresses The extracted maker addresses.
+ function loadMakerAddressesFromOrderArray(uint256 orderArrayParamIndex)
+ internal pure
+ returns (address[] makerAddresses);
+}
diff --git a/contracts/extensions/package.json b/contracts/extensions/package.json
index 938e1138c..2d9ed4dcd 100644
--- a/contracts/extensions/package.json
+++ b/contracts/extensions/package.json
@@ -31,7 +31,7 @@
"lint-contracts": "solhint -c ../.solhint.json contracts/**/**/**/**/*.sol"
},
"config": {
- "abis": "generated-artifacts/@(DutchAuction|Forwarder).json"
+ "abis": "generated-artifacts/@(BalanceThresholdFilter|DutchAuction|Forwarder).json"
},
"repository": {
"type": "git",
diff --git a/contracts/extensions/src/artifacts/index.ts b/contracts/extensions/src/artifacts/index.ts
index 7588178f0..ebf0b8050 100644
--- a/contracts/extensions/src/artifacts/index.ts
+++ b/contracts/extensions/src/artifacts/index.ts
@@ -1,9 +1,11 @@
import { ContractArtifact } from 'ethereum-types';
+import * as BalanceThresholdFilter from '../../generated-artifacts/BalanceThresholdFilter.json';
import * as DutchAuction from '../../generated-artifacts/DutchAuction.json';
import * as Forwarder from '../../generated-artifacts/Forwarder.json';
export const artifacts = {
+ BalanceThresholdFilter: BalanceThresholdFilter as ContractArtifact,
DutchAuction: DutchAuction as ContractArtifact,
Forwarder: Forwarder as ContractArtifact,
};
diff --git a/contracts/extensions/src/wrappers/index.ts b/contracts/extensions/src/wrappers/index.ts
index 90880e37f..8a8122caa 100644
--- a/contracts/extensions/src/wrappers/index.ts
+++ b/contracts/extensions/src/wrappers/index.ts
@@ -1,2 +1,3 @@
+export * from '../../generated-wrappers/balance_threshold_filter';
export * from '../../generated-wrappers/dutch_auction';
export * from '../../generated-wrappers/forwarder';
diff --git a/contracts/extensions/test/extensions/balance_threshold_filter.ts b/contracts/extensions/test/extensions/balance_threshold_filter.ts
new file mode 100644
index 000000000..07199d60b
--- /dev/null
+++ b/contracts/extensions/test/extensions/balance_threshold_filter.ts
@@ -0,0 +1,1644 @@
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { assetDataUtils } from '@0x/order-utils';
+import { Order, RevertReason, SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import * as chai from 'chai';
+import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
+import * as _ from 'lodash';
+
+import {
+ artifacts as protocolArtifacts,
+ ERC20Wrapper,
+ ERC721Wrapper,
+ ExchangeContract,
+ ExchangeWrapper,
+} from '@0x/contracts-protocol';
+import {
+ chaiSetup,
+ constants,
+ ContractName,
+ ERC20BalancesByOwner,
+ expectTransactionFailedAsync,
+ OrderFactory,
+ OrderStatus,
+ provider,
+ TransactionFactory,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import { DummyERC20TokenContract } from '@0x/contracts-tokens';
+
+import { BalanceThresholdFilterContract } from '../../generated-wrappers/balance_threshold_filter';
+import { artifacts } from '../../src/artifacts';
+import { BalanceThresholdWrapper } from '../utils/balance_threshold_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+const DECIMALS_DEFAULT = 18;
+
+interface ValidatedAddressesLog {
+ args: { addresses: string[] };
+}
+
+describe(ContractName.BalanceThresholdFilter, () => {
+ const takerAssetAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(500), DECIMALS_DEFAULT);
+ const makerAssetAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(1000), DECIMALS_DEFAULT);
+ const takerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(250), DECIMALS_DEFAULT);
+
+ let validMakerAddress: string;
+ let validMakerAddress2: string;
+ let owner: string;
+ let validTakerAddress: string;
+ let feeRecipientAddress: string;
+ let invalidAddress: string;
+ let defaultMakerAssetAddress: string;
+ let defaultTakerAssetAddress: string;
+ let zrxAssetData: string;
+ let zrxToken: DummyERC20TokenContract;
+ let exchangeInstance: ExchangeContract;
+ let exchangeWrapper: ExchangeWrapper;
+
+ let orderFactory: OrderFactory;
+ let orderFactory2: OrderFactory;
+ let invalidOrderFactory: OrderFactory;
+ let erc20Wrapper: ERC20Wrapper;
+ let erc20Balances: ERC20BalancesByOwner;
+ let erc20TakerBalanceThresholdWrapper: BalanceThresholdWrapper;
+ let erc721TakerBalanceThresholdWrapper: BalanceThresholdWrapper;
+ let erc721MakerBalanceThresholdWrapper: BalanceThresholdWrapper;
+ let erc721NonValidBalanceThresholdWrapper: BalanceThresholdWrapper;
+
+ let defaultOrderParams: Partial<Order>;
+ let validSignedOrder: SignedOrder;
+ let validSignedOrder2: SignedOrder;
+
+ let erc721BalanceThresholdFilterInstance: BalanceThresholdFilterContract;
+ let erc20BalanceThresholdFilterInstance: BalanceThresholdFilterContract;
+
+ const assertValidatedAddressesLog = async (
+ txReceipt: TransactionReceiptWithDecodedLogs,
+ expectedValidatedAddresses: string[],
+ ) => {
+ expect(txReceipt.logs.length).to.be.gte(1);
+ const validatedAddressesLog = (txReceipt.logs[0] as any) as ValidatedAddressesLog;
+ const validatedAddresses = validatedAddressesLog.args.addresses;
+ // @HACK-hysz: Nested addresses are not translated to lower-case but this will change once
+ // the new ABI Encoder/Decoder is used by the contract templates.
+ const validatedAddressesNormalized: string[] = [];
+ _.each(validatedAddresses, address => {
+ const normalizedAddress = _.toLower(address);
+ validatedAddressesNormalized.push(normalizedAddress);
+ });
+ expect(validatedAddressesNormalized).to.be.deep.equal(expectedValidatedAddresses);
+ };
+
+ before(async () => {
+ // Create accounts
+ await blockchainLifecycle.startAsync();
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ const usedAddresses = ([
+ owner,
+ validMakerAddress,
+ validMakerAddress2,
+ validTakerAddress,
+ feeRecipientAddress,
+ invalidAddress,
+ ] = accounts);
+ const takerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(validTakerAddress)];
+ const makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(validMakerAddress)];
+ const secondMakerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(validMakerAddress2)];
+ const invalidAddressPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(invalidAddress)];
+ // Create wrappers
+ erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
+ const validAddresses = _.cloneDeepWith(usedAddresses);
+ _.remove(validAddresses, (address: string) => {
+ return address === invalidAddress;
+ });
+ const erc721Wrapper = new ERC721Wrapper(provider, validAddresses, owner);
+ // Deploy ERC20 tokens
+ const numDummyErc20ToDeploy = 4;
+ let erc20TokenA: DummyERC20TokenContract;
+ let erc20TokenB: DummyERC20TokenContract;
+ let erc20BalanceThresholdAsset: DummyERC20TokenContract;
+ [erc20TokenA, erc20TokenB, zrxToken, erc20BalanceThresholdAsset] = await erc20Wrapper.deployDummyTokensAsync(
+ numDummyErc20ToDeploy,
+ constants.DUMMY_TOKEN_DECIMALS,
+ );
+ defaultMakerAssetAddress = erc20TokenA.address;
+ defaultTakerAssetAddress = erc20TokenB.address;
+ zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
+ // Create proxies
+ const erc20Proxy = await erc20Wrapper.deployProxyAsync();
+ await erc20Wrapper.setBalancesAndAllowancesAsync();
+ // Deploy Exchange contract
+ exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync(
+ protocolArtifacts.Exchange,
+ provider,
+ txDefaults,
+ zrxAssetData,
+ );
+ exchangeWrapper = new ExchangeWrapper(exchangeInstance, provider);
+ // Register proxies
+ await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner);
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, {
+ from: owner,
+ });
+ // Deploy Balance Threshold Filters
+ // One uses an ERC721 token as its balance threshold asset; the other uses an ERC20
+ const erc721alanceThreshold = new BigNumber(1);
+ await erc721Wrapper.deployProxyAsync();
+ const [erc721BalanceThresholdAsset] = await erc721Wrapper.deployDummyTokensAsync();
+ await erc721Wrapper.setBalancesAndAllowancesAsync();
+ erc721BalanceThresholdFilterInstance = await BalanceThresholdFilterContract.deployFrom0xArtifactAsync(
+ artifacts.BalanceThresholdFilter,
+ provider,
+ txDefaults,
+ exchangeInstance.address,
+ erc721BalanceThresholdAsset.address,
+ erc721alanceThreshold,
+ );
+ const erc20BalanceThreshold = Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 10);
+ erc20BalanceThresholdFilterInstance = await BalanceThresholdFilterContract.deployFrom0xArtifactAsync(
+ artifacts.BalanceThresholdFilter,
+ provider,
+ txDefaults,
+ exchangeInstance.address,
+ erc20BalanceThresholdAsset.address,
+ erc20BalanceThreshold,
+ );
+ // Default order parameters
+ defaultOrderParams = {
+ exchangeAddress: exchangeInstance.address,
+ feeRecipientAddress,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress),
+ makerAssetAmount,
+ takerAssetAmount,
+ makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), DECIMALS_DEFAULT),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(150), DECIMALS_DEFAULT),
+ senderAddress: erc721BalanceThresholdFilterInstance.address,
+ };
+ // Create two order factories with valid makers (who meet the threshold balance), and
+ // one factory for an invalid address (that does not meet the threshold balance)
+ // Valid order factory #1
+ const defaultOrderParams1 = {
+ makerAddress: validMakerAddress,
+ ...defaultOrderParams,
+ };
+ orderFactory = new OrderFactory(makerPrivateKey, defaultOrderParams1);
+ // Valid order factory #2
+ const defaultOrderParams2 = {
+ makerAddress: validMakerAddress2,
+ ...defaultOrderParams,
+ };
+ orderFactory2 = new OrderFactory(secondMakerPrivateKey, defaultOrderParams2);
+ // Invalid order factory
+ const defaultNonValidOrderParams = {
+ makerAddress: invalidAddress,
+ ...defaultOrderParams,
+ };
+ invalidOrderFactory = new OrderFactory(invalidAddressPrivateKey, defaultNonValidOrderParams);
+ // Create Balance Thresold Wrappers
+ erc20TakerBalanceThresholdWrapper = new BalanceThresholdWrapper(
+ erc20BalanceThresholdFilterInstance,
+ exchangeInstance,
+ new TransactionFactory(takerPrivateKey, exchangeInstance.address),
+ provider,
+ );
+ erc721TakerBalanceThresholdWrapper = new BalanceThresholdWrapper(
+ erc721BalanceThresholdFilterInstance,
+ exchangeInstance,
+ new TransactionFactory(takerPrivateKey, exchangeInstance.address),
+ provider,
+ );
+ erc721MakerBalanceThresholdWrapper = new BalanceThresholdWrapper(
+ erc721BalanceThresholdFilterInstance,
+ exchangeInstance,
+ new TransactionFactory(makerPrivateKey, exchangeInstance.address),
+ provider,
+ );
+ erc721NonValidBalanceThresholdWrapper = new BalanceThresholdWrapper(
+ erc721BalanceThresholdFilterInstance,
+ exchangeInstance,
+ new TransactionFactory(invalidAddressPrivateKey, exchangeInstance.address),
+ provider,
+ );
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+
+ describe('General Sanity Checks', () => {
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ validSignedOrder = await orderFactory.newSignedOrderAsync();
+ validSignedOrder2 = await orderFactory2.newSignedOrderAsync();
+ });
+ it('should transfer the correct amounts and validate both maker/taker when both maker and taker exceed the balance threshold of an ERC20 token', async () => {
+ const validSignedOrderERC20Sender = await orderFactory.newSignedOrderAsync({
+ ...defaultOrderParams,
+ makerAddress: validMakerAddress,
+ senderAddress: erc20TakerBalanceThresholdWrapper.getBalanceThresholdAddress(),
+ });
+ // Execute a valid fill
+ const txReceipt = await erc20TakerBalanceThresholdWrapper.fillOrderAsync(
+ validSignedOrderERC20Sender,
+ validTakerAddress,
+ { takerAssetFillAmount },
+ );
+ // Assert validated addresses
+ const expectedValidatedAddresseses = [validSignedOrder.makerAddress, validTakerAddress];
+ await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses);
+ // Check balances
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const makerAssetFillAmount = takerAssetFillAmount
+ .times(validSignedOrder.makerAssetAmount)
+ .dividedToIntegerBy(validSignedOrder.takerAssetAmount);
+ const makerFeePaid = validSignedOrder.makerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(validSignedOrder.makerAssetAmount);
+ const takerFeePaid = validSignedOrder.takerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(validSignedOrder.makerAssetAmount);
+ expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid),
+ );
+ expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount),
+ );
+ expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount),
+ );
+ expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)),
+ );
+ });
+ it('should revert if the Exchange transaction function is not supported', async () => {
+ // Create signed order without the fillOrder function selector
+ const salt = new BigNumber(0);
+ const badSelectorHex = '0x00000000';
+ const signatureHex = '0x';
+ // Call valid forwarder
+ return expectTransactionFailedAsync(
+ erc721BalanceThresholdFilterInstance.executeTransaction.sendTransactionAsync(
+ salt,
+ validTakerAddress,
+ badSelectorHex,
+ signatureHex,
+ ),
+ RevertReason.InvalidOrBlockedExchangeSelector,
+ );
+ });
+ it('should revert if senderAddress is not set to the valid forwarding contract', async () => {
+ // Create signed order with incorrect senderAddress
+ const notBalanceThresholdFilterAddress = zrxToken.address;
+ const signedOrderWithBadSenderAddress = await orderFactory.newSignedOrderAsync({
+ senderAddress: notBalanceThresholdFilterAddress,
+ });
+ // Call valid forwarder
+ return expectTransactionFailedAsync(
+ erc721TakerBalanceThresholdWrapper.fillOrderAsync(signedOrderWithBadSenderAddress, validTakerAddress, {
+ takerAssetFillAmount,
+ }),
+ RevertReason.FailedExecution,
+ );
+ });
+ });
+
+ describe('batchFillOrders', () => {
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ validSignedOrder = await orderFactory.newSignedOrderAsync();
+ validSignedOrder2 = await orderFactory2.newSignedOrderAsync();
+ });
+ it('should transfer the correct amounts and validate both makers/taker when both maker and taker meet the balance threshold', async () => {
+ // Execute a valid fill
+ const orders = [validSignedOrder, validSignedOrder2];
+ const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount];
+ const txReceipt = await erc721TakerBalanceThresholdWrapper.batchFillOrdersAsync(orders, validTakerAddress, {
+ takerAssetFillAmounts,
+ });
+ // Assert validated addresses
+ const expectedValidatedAddresseses = [
+ validSignedOrder.makerAddress,
+ validSignedOrder2.makerAddress,
+ validTakerAddress,
+ ];
+ await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses);
+ // Check balances
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const cumulativeTakerAssetFillAmount = takerAssetFillAmount.times(2);
+ const makerAssetFillAmount = takerAssetFillAmount
+ .times(validSignedOrder.makerAssetAmount)
+ .dividedToIntegerBy(validSignedOrder.takerAssetAmount);
+ const makerFeePaid = validSignedOrder.makerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(validSignedOrder.makerAssetAmount);
+ const takerFeePaid = validSignedOrder.takerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(validSignedOrder.makerAssetAmount)
+ .times(2);
+ // Maker #1
+ expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid),
+ );
+ // Maker #2
+ expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid),
+ );
+ // Taker
+ expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount),
+ );
+
+ expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount.times(2)),
+ );
+ expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid),
+ );
+ // Fee recipient
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.times(2).add(takerFeePaid)),
+ );
+ });
+ it('should revert if one maker does not meet the balance threshold', async () => {
+ // Create order set with one non-valid maker address
+ const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount];
+ const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({
+ makerAddress: invalidAddress,
+ });
+ const orders = [validSignedOrder, signedOrderWithBadMakerAddress];
+ // Execute transaction
+ return expectTransactionFailedAsync(
+ erc721TakerBalanceThresholdWrapper.batchFillOrdersAsync(orders, validTakerAddress, {
+ takerAssetFillAmounts,
+ }),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ it('should revert if taker does not meet the balance threshold', async () => {
+ const orders = [validSignedOrder, validSignedOrder2];
+ const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount];
+ return expectTransactionFailedAsync(
+ erc721NonValidBalanceThresholdWrapper.batchFillOrdersAsync(orders, invalidAddress, {
+ takerAssetFillAmounts,
+ }),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ });
+
+ describe('batchFillOrdersNoThrow', () => {
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ validSignedOrder = await orderFactory.newSignedOrderAsync();
+ validSignedOrder2 = await orderFactory2.newSignedOrderAsync();
+ });
+ it('should transfer the correct amounts and validate both makers/taker when both maker and taker meet the balance threshold', async () => {
+ // Execute a valid fill
+ const orders = [validSignedOrder, validSignedOrder2];
+ const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount];
+ const txReceipt = await erc721TakerBalanceThresholdWrapper.batchFillOrdersNoThrowAsync(
+ orders,
+ validTakerAddress,
+ {
+ takerAssetFillAmounts,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 600000,
+ },
+ );
+ // Assert validated addresses
+ const expectedValidatedAddresseses = [
+ validSignedOrder.makerAddress,
+ validSignedOrder2.makerAddress,
+ validTakerAddress,
+ ];
+ await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses);
+ // Check balances
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const cumulativeTakerAssetFillAmount = takerAssetFillAmount.times(2);
+ const makerAssetFillAmount = takerAssetFillAmount
+ .times(validSignedOrder.makerAssetAmount)
+ .dividedToIntegerBy(validSignedOrder.takerAssetAmount);
+ const makerFeePaid = validSignedOrder.makerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(validSignedOrder.makerAssetAmount);
+ const takerFeePaid = validSignedOrder.takerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(validSignedOrder.makerAssetAmount)
+ .times(2);
+ // Maker #1
+ expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid),
+ );
+ // Maker #2
+ expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid),
+ );
+ // Taker
+ expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount),
+ );
+
+ expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount.times(2)),
+ );
+ expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid),
+ );
+ // Fee recipient
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.times(2).add(takerFeePaid)),
+ );
+ });
+ it('should revert if one maker does not meet the balance threshold', async () => {
+ // Create order set with one non-valid maker address
+ const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount];
+ const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({
+ makerAddress: invalidAddress,
+ });
+ const orders = [validSignedOrder, signedOrderWithBadMakerAddress];
+ // Execute transaction
+ return expectTransactionFailedAsync(
+ erc721TakerBalanceThresholdWrapper.batchFillOrdersNoThrowAsync(orders, validTakerAddress, {
+ takerAssetFillAmounts,
+ }),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ it('should revert if taker does not meet the balance threshold', async () => {
+ const orders = [validSignedOrder, validSignedOrder2];
+ const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount];
+ return expectTransactionFailedAsync(
+ erc721NonValidBalanceThresholdWrapper.batchFillOrdersNoThrowAsync(orders, invalidAddress, {
+ takerAssetFillAmounts,
+ }),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ });
+
+ describe('batchFillOrKillOrders', () => {
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ validSignedOrder = await orderFactory.newSignedOrderAsync();
+ validSignedOrder2 = await orderFactory2.newSignedOrderAsync();
+ });
+ it('should transfer the correct amounts and validate both makers/taker when both makers and taker meet the balance threshold', async () => {
+ // Execute a valid fill
+ const orders = [validSignedOrder, validSignedOrder2];
+ const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount];
+ const txReceipt = await erc721TakerBalanceThresholdWrapper.batchFillOrKillOrdersAsync(
+ orders,
+ validTakerAddress,
+ { takerAssetFillAmounts },
+ );
+ // Assert validated addresses
+ const expectedValidatedAddresseses = [
+ validSignedOrder.makerAddress,
+ validSignedOrder2.makerAddress,
+ validTakerAddress,
+ ];
+ await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses);
+ // Check balances
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const cumulativeTakerAssetFillAmount = takerAssetFillAmount.times(2);
+ const makerAssetFillAmount = takerAssetFillAmount
+ .times(validSignedOrder.makerAssetAmount)
+ .dividedToIntegerBy(validSignedOrder.takerAssetAmount);
+ const makerFeePaid = validSignedOrder.makerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(validSignedOrder.makerAssetAmount);
+ const takerFeePaid = validSignedOrder.takerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(validSignedOrder.makerAssetAmount)
+ .times(2);
+ // Maker #1
+ expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid),
+ );
+ // Maker #2
+ expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid),
+ );
+ // Taker
+ expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount),
+ );
+
+ expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount.times(2)),
+ );
+ expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid),
+ );
+ // Fee recipient
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.times(2).add(takerFeePaid)),
+ );
+ });
+ it('should revert if one maker does not meet the balance threshold', async () => {
+ // Create order set with one non-valid maker address
+ const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount];
+ const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({
+ makerAddress: invalidAddress,
+ });
+ const orders = [validSignedOrder, signedOrderWithBadMakerAddress];
+ // Execute transaction
+ return expectTransactionFailedAsync(
+ erc721TakerBalanceThresholdWrapper.batchFillOrKillOrdersAsync(orders, validTakerAddress, {
+ takerAssetFillAmounts,
+ }),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ it('should revert if taker does not meet the balance threshold', async () => {
+ const orders = [validSignedOrder, validSignedOrder2];
+ const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount];
+ return expectTransactionFailedAsync(
+ erc721NonValidBalanceThresholdWrapper.batchFillOrKillOrdersAsync(orders, invalidAddress, {
+ takerAssetFillAmounts,
+ }),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ it('should revert if one takerAssetFillAmount is not fully filled', async () => {
+ const tooBigTakerAssetFillAmount = validSignedOrder.takerAssetAmount.times(2);
+ const orders = [validSignedOrder, validSignedOrder2];
+ const takerAssetFillAmounts = [takerAssetFillAmount, tooBigTakerAssetFillAmount];
+ return expectTransactionFailedAsync(
+ erc721TakerBalanceThresholdWrapper.batchFillOrKillOrdersAsync(orders, validTakerAddress, {
+ takerAssetFillAmounts,
+ }),
+ RevertReason.FailedExecution,
+ );
+ });
+ });
+
+ describe('fillOrder', () => {
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ validSignedOrder = await orderFactory.newSignedOrderAsync();
+ });
+ it('should transfer the correct amounts and validate both maker/taker when both maker and taker meet the balance threshold', async () => {
+ // Execute a valid fill
+ const txReceipt = await erc721TakerBalanceThresholdWrapper.fillOrderAsync(
+ validSignedOrder,
+ validTakerAddress,
+ { takerAssetFillAmount },
+ );
+ // Assert validated addresses
+ const expectedValidatedAddresseses = [validSignedOrder.makerAddress, validTakerAddress];
+ await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses);
+ // Check balances
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const makerAssetFillAmount = takerAssetFillAmount
+ .times(validSignedOrder.makerAssetAmount)
+ .dividedToIntegerBy(validSignedOrder.takerAssetAmount);
+ const makerFeePaid = validSignedOrder.makerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(validSignedOrder.makerAssetAmount);
+ const takerFeePaid = validSignedOrder.takerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(validSignedOrder.makerAssetAmount);
+ expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid),
+ );
+ expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount),
+ );
+ expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount),
+ );
+ expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)),
+ );
+ });
+ it('should revert if maker does not meet the balance threshold', async () => {
+ // Create signed order with non-valid maker address
+ const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({
+ senderAddress: erc721BalanceThresholdFilterInstance.address,
+ makerAddress: invalidAddress,
+ });
+ // Execute transaction
+ return expectTransactionFailedAsync(
+ erc721TakerBalanceThresholdWrapper.fillOrderAsync(signedOrderWithBadMakerAddress, validTakerAddress, {
+ takerAssetFillAmount,
+ }),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ it('should revert if taker does not meet the balance threshold', async () => {
+ return expectTransactionFailedAsync(
+ erc721NonValidBalanceThresholdWrapper.fillOrderAsync(validSignedOrder, invalidAddress, {
+ takerAssetFillAmount,
+ }),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ });
+
+ describe('fillOrderNoThrow', () => {
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ validSignedOrder = await orderFactory.newSignedOrderAsync();
+ });
+ it('should transfer the correct amounts and validate both maker/taker when both maker and taker meet the balance threshold', async () => {
+ // Execute a valid fill
+ const txReceipt = await erc721TakerBalanceThresholdWrapper.fillOrderNoThrowAsync(
+ validSignedOrder,
+ validTakerAddress,
+ {
+ takerAssetFillAmount,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 600000,
+ },
+ );
+ // Assert validated addresses
+ const expectedValidatedAddresseses = [validSignedOrder.makerAddress, validTakerAddress];
+ await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses);
+ // Check balances
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const makerAssetFillAmount = takerAssetFillAmount
+ .times(validSignedOrder.makerAssetAmount)
+ .dividedToIntegerBy(validSignedOrder.takerAssetAmount);
+ const makerFeePaid = validSignedOrder.makerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(validSignedOrder.makerAssetAmount);
+ const takerFeePaid = validSignedOrder.takerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(validSignedOrder.makerAssetAmount);
+ expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid),
+ );
+ expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount),
+ );
+ expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount),
+ );
+ expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)),
+ );
+ });
+ it('should revert if maker does not meet the balance threshold', async () => {
+ // Create signed order with non-valid maker address
+ const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({
+ senderAddress: erc721BalanceThresholdFilterInstance.address,
+ makerAddress: invalidAddress,
+ });
+ // Execute transaction
+ return expectTransactionFailedAsync(
+ erc721TakerBalanceThresholdWrapper.fillOrderNoThrowAsync(
+ signedOrderWithBadMakerAddress,
+ validTakerAddress,
+ { takerAssetFillAmount },
+ ),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ it('should revert if taker does not meet the balance threshold', async () => {
+ return expectTransactionFailedAsync(
+ erc721NonValidBalanceThresholdWrapper.fillOrderNoThrowAsync(validSignedOrder, invalidAddress, {
+ takerAssetFillAmount,
+ }),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ });
+
+ describe('fillOrKillOrder', () => {
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ validSignedOrder = await orderFactory.newSignedOrderAsync();
+ });
+ it('should transfer the correct amounts and validate both maker/taker when both maker and taker meet the balance threshold', async () => {
+ // Execute a valid fill
+ const takerAssetFillAmount_ = validSignedOrder.takerAssetAmount;
+ const txReceipt = await erc721TakerBalanceThresholdWrapper.fillOrKillOrderAsync(
+ validSignedOrder,
+ validTakerAddress,
+ { takerAssetFillAmount: takerAssetFillAmount_ },
+ );
+ // Assert validated addresses
+ const expectedValidatedAddresseses = [validSignedOrder.makerAddress, validTakerAddress];
+ await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses);
+ // Check balances
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const makerAssetFillAmount = takerAssetFillAmount_
+ .times(validSignedOrder.makerAssetAmount)
+ .dividedToIntegerBy(validSignedOrder.takerAssetAmount);
+ const makerFeePaid = validSignedOrder.makerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(validSignedOrder.makerAssetAmount);
+ const takerFeePaid = validSignedOrder.takerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(validSignedOrder.makerAssetAmount);
+ expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount_),
+ );
+ expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid),
+ );
+ expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount_),
+ );
+ expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount),
+ );
+ expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)),
+ );
+ });
+ it('should revert if maker does not meet the balance threshold', async () => {
+ // Create signed order with non-valid maker address
+ const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({
+ senderAddress: erc721BalanceThresholdFilterInstance.address,
+ makerAddress: invalidAddress,
+ });
+ // Execute transaction
+ return expectTransactionFailedAsync(
+ erc721TakerBalanceThresholdWrapper.fillOrKillOrderAsync(
+ signedOrderWithBadMakerAddress,
+ validTakerAddress,
+ { takerAssetFillAmount },
+ ),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ it('should revert if taker does not meet the balance threshold', async () => {
+ return expectTransactionFailedAsync(
+ erc721NonValidBalanceThresholdWrapper.fillOrKillOrderAsync(validSignedOrder, invalidAddress, {
+ takerAssetFillAmount,
+ }),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ it('should revert if takerAssetFillAmount is not fully filled', async () => {
+ const tooBigTakerAssetFillAmount = validSignedOrder.takerAssetAmount.times(2);
+ return expectTransactionFailedAsync(
+ erc721TakerBalanceThresholdWrapper.fillOrKillOrderAsync(validSignedOrder, validTakerAddress, {
+ takerAssetFillAmount: tooBigTakerAssetFillAmount,
+ }),
+ RevertReason.FailedExecution,
+ );
+ });
+ });
+
+ describe('marketSellOrders', () => {
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ validSignedOrder = await orderFactory.newSignedOrderAsync();
+ validSignedOrder2 = await orderFactory2.newSignedOrderAsync();
+ });
+ it('should transfer the correct amounts and validate both makers/taker when both makers and taker meet the balance threshold', async () => {
+ // Execute a valid fill
+ const orders = [validSignedOrder, validSignedOrder2];
+ const cumulativeTakerAssetFillAmount = validSignedOrder.takerAssetAmount.plus(takerAssetFillAmount);
+ const txReceipt = await erc721TakerBalanceThresholdWrapper.marketSellOrdersAsync(
+ orders,
+ validTakerAddress,
+ { takerAssetFillAmount: cumulativeTakerAssetFillAmount },
+ );
+ // Assert validated addresses
+ const expectedValidatedAddresseses = [
+ validSignedOrder.makerAddress,
+ validSignedOrder2.makerAddress,
+ validTakerAddress,
+ ];
+ await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses);
+ // Check balances
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const makerAssetFillAmount2 = takerAssetFillAmount
+ .times(validSignedOrder.makerAssetAmount)
+ .dividedToIntegerBy(validSignedOrder.takerAssetAmount);
+ const makerFeePaid2 = validSignedOrder2.makerFee
+ .times(makerAssetFillAmount2)
+ .dividedToIntegerBy(validSignedOrder2.makerAssetAmount);
+ const takerFeePaid2 = validSignedOrder2.takerFee
+ .times(makerAssetFillAmount2)
+ .dividedToIntegerBy(validSignedOrder2.makerAssetAmount);
+ const takerFeePaid = validSignedOrder.takerFee.plus(takerFeePaid2);
+ const cumulativeMakerAssetFillAmount = validSignedOrder.makerAssetAmount.plus(makerAssetFillAmount2);
+ // Maker #1
+ expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(validSignedOrder.makerAssetAmount),
+ );
+ expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(validSignedOrder.takerAssetAmount),
+ );
+ expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][zrxToken.address].minus(validSignedOrder.makerFee),
+ );
+ // Maker #2
+ expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount2),
+ );
+ expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid2),
+ );
+ // Taker
+ expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount),
+ );
+ expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(cumulativeMakerAssetFillAmount),
+ );
+ expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid),
+ );
+ // Fee recipient
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address]
+ .add(validSignedOrder.makerFee)
+ .add(makerFeePaid2)
+ .add(takerFeePaid),
+ );
+ });
+ it('should revert if one maker does not meet the balance threshold', async () => {
+ // Create order set with one non-valid maker address
+ const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({
+ makerAddress: invalidAddress,
+ });
+ const orders = [validSignedOrder, signedOrderWithBadMakerAddress];
+ // Execute transaction
+ return expectTransactionFailedAsync(
+ erc721TakerBalanceThresholdWrapper.marketSellOrdersAsync(orders, validTakerAddress, {
+ takerAssetFillAmount,
+ }),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ it('should revert if taker does not meet the balance threshold', async () => {
+ const orders = [validSignedOrder, validSignedOrder2];
+ return expectTransactionFailedAsync(
+ erc721NonValidBalanceThresholdWrapper.marketSellOrdersAsync(orders, invalidAddress, {
+ takerAssetFillAmount,
+ }),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ });
+
+ describe('marketSellOrdersNoThrow', () => {
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ validSignedOrder = await orderFactory.newSignedOrderAsync();
+ validSignedOrder2 = await orderFactory2.newSignedOrderAsync();
+ });
+ it('should transfer the correct amounts and validate both makers/taker when both makers and taker meet the balance threshold', async () => {
+ // Execute a valid fill
+ const orders = [validSignedOrder, validSignedOrder2];
+ const cumulativeTakerAssetFillAmount = validSignedOrder.takerAssetAmount.plus(takerAssetFillAmount);
+ const txReceipt = await erc721TakerBalanceThresholdWrapper.marketSellOrdersNoThrowAsync(
+ orders,
+ validTakerAddress,
+ {
+ takerAssetFillAmount: cumulativeTakerAssetFillAmount,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 600000,
+ },
+ );
+ // Assert validated addresses
+ const expectedValidatedAddresseses = [
+ validSignedOrder.makerAddress,
+ validSignedOrder2.makerAddress,
+ validTakerAddress,
+ ];
+ await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses);
+ // Check balances
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const makerAssetFillAmount2 = takerAssetFillAmount
+ .times(validSignedOrder.makerAssetAmount)
+ .dividedToIntegerBy(validSignedOrder.takerAssetAmount);
+ const makerFeePaid2 = validSignedOrder2.makerFee
+ .times(makerAssetFillAmount2)
+ .dividedToIntegerBy(validSignedOrder2.makerAssetAmount);
+ const takerFeePaid2 = validSignedOrder2.takerFee
+ .times(makerAssetFillAmount2)
+ .dividedToIntegerBy(validSignedOrder2.makerAssetAmount);
+ const takerFeePaid = validSignedOrder.takerFee.plus(takerFeePaid2);
+ const cumulativeMakerAssetFillAmount = validSignedOrder.makerAssetAmount.plus(makerAssetFillAmount2);
+ // Maker #1
+ expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(validSignedOrder.makerAssetAmount),
+ );
+ expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(validSignedOrder.takerAssetAmount),
+ );
+ expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][zrxToken.address].minus(validSignedOrder.makerFee),
+ );
+ // Maker #2
+ expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount2),
+ );
+ expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid2),
+ );
+ // Taker
+ expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount),
+ );
+ expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(cumulativeMakerAssetFillAmount),
+ );
+ expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid),
+ );
+ // Fee recipient
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address]
+ .add(validSignedOrder.makerFee)
+ .add(makerFeePaid2)
+ .add(takerFeePaid),
+ );
+ });
+ it('should revert if one maker does not meet the balance threshold', async () => {
+ // Create order set with one non-valid maker address
+ const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({
+ makerAddress: invalidAddress,
+ });
+ const orders = [validSignedOrder, signedOrderWithBadMakerAddress];
+ // Execute transaction
+ return expectTransactionFailedAsync(
+ erc721TakerBalanceThresholdWrapper.marketSellOrdersNoThrowAsync(orders, validTakerAddress, {
+ takerAssetFillAmount,
+ }),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ it('should revert if taker does not meet the balance threshold', async () => {
+ const orders = [validSignedOrder, validSignedOrder2];
+ return expectTransactionFailedAsync(
+ erc721NonValidBalanceThresholdWrapper.marketSellOrdersNoThrowAsync(orders, invalidAddress, {
+ takerAssetFillAmount,
+ }),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ });
+
+ describe('marketBuyOrders', () => {
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ validSignedOrder = await orderFactory.newSignedOrderAsync();
+ validSignedOrder2 = await orderFactory2.newSignedOrderAsync();
+ });
+ it('should transfer the correct amounts and validate both makers/taker when both makers and taker meet the balance threshold', async () => {
+ // Execute a valid fill
+ const orders = [validSignedOrder, validSignedOrder2];
+ const cumulativeTakerAssetFillAmount = validSignedOrder.takerAssetAmount.plus(takerAssetFillAmount);
+ const makerAssetFillAmount2 = takerAssetFillAmount
+ .times(validSignedOrder.makerAssetAmount)
+ .dividedToIntegerBy(validSignedOrder.takerAssetAmount);
+ const cumulativeMakerAssetFillAmount = validSignedOrder.makerAssetAmount.plus(makerAssetFillAmount2);
+ const txReceipt = await erc721TakerBalanceThresholdWrapper.marketBuyOrdersAsync(orders, validTakerAddress, {
+ makerAssetFillAmount: cumulativeMakerAssetFillAmount,
+ });
+ // Assert validated addresses
+ const expectedValidatedAddresseses = [
+ validSignedOrder.makerAddress,
+ validSignedOrder2.makerAddress,
+ validTakerAddress,
+ ];
+ await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses);
+ // Check balances
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const makerFeePaid2 = validSignedOrder2.makerFee
+ .times(makerAssetFillAmount2)
+ .dividedToIntegerBy(validSignedOrder2.makerAssetAmount);
+ const takerFeePaid2 = validSignedOrder2.takerFee
+ .times(makerAssetFillAmount2)
+ .dividedToIntegerBy(validSignedOrder2.makerAssetAmount);
+ const takerFeePaid = validSignedOrder.takerFee.plus(takerFeePaid2);
+ // Maker #1
+ expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(validSignedOrder.makerAssetAmount),
+ );
+ expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(validSignedOrder.takerAssetAmount),
+ );
+ expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][zrxToken.address].minus(validSignedOrder.makerFee),
+ );
+ // Maker #2
+ expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount2),
+ );
+ expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid2),
+ );
+ // Taker
+ expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount),
+ );
+ expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(cumulativeMakerAssetFillAmount),
+ );
+ expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid),
+ );
+ // Fee recipient
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address]
+ .add(validSignedOrder.makerFee)
+ .add(makerFeePaid2)
+ .add(takerFeePaid),
+ );
+ });
+ it('should revert if one maker does not meet the balance threshold', async () => {
+ // Create order set with one non-valid maker address
+ const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({
+ makerAddress: invalidAddress,
+ });
+ const orders = [validSignedOrder, signedOrderWithBadMakerAddress];
+ // Execute transaction
+ const dummyMakerAssetFillAmount = new BigNumber(0);
+ return expectTransactionFailedAsync(
+ erc721TakerBalanceThresholdWrapper.marketBuyOrdersAsync(orders, validTakerAddress, {
+ makerAssetFillAmount: dummyMakerAssetFillAmount,
+ }),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ it('should revert if taker does not meet the balance threshold', async () => {
+ const orders = [validSignedOrder, validSignedOrder2];
+ const dummyMakerAssetFillAmount = new BigNumber(0);
+ return expectTransactionFailedAsync(
+ erc721NonValidBalanceThresholdWrapper.marketBuyOrdersAsync(orders, invalidAddress, {
+ makerAssetFillAmount: dummyMakerAssetFillAmount,
+ }),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ });
+
+ describe('marketBuyOrdersNoThrowAsync', () => {
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ validSignedOrder = await orderFactory.newSignedOrderAsync();
+ validSignedOrder2 = await orderFactory2.newSignedOrderAsync();
+ });
+ it('should transfer the correct amounts and validate both makers/taker when both makers and taker meet the balance threshold', async () => {
+ // Execute a valid fill
+ const orders = [validSignedOrder, validSignedOrder2];
+ const cumulativeTakerAssetFillAmount = validSignedOrder.takerAssetAmount.plus(takerAssetFillAmount);
+ const makerAssetFillAmount2 = takerAssetFillAmount
+ .times(validSignedOrder.makerAssetAmount)
+ .dividedToIntegerBy(validSignedOrder.takerAssetAmount);
+ const cumulativeMakerAssetFillAmount = validSignedOrder.makerAssetAmount.plus(makerAssetFillAmount2);
+ const txReceipt = await erc721TakerBalanceThresholdWrapper.marketBuyOrdersNoThrowAsync(
+ orders,
+ validTakerAddress,
+ {
+ makerAssetFillAmount: cumulativeMakerAssetFillAmount,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 600000,
+ },
+ );
+ // Assert validated addresses
+ const expectedValidatedAddresseses = [
+ validSignedOrder.makerAddress,
+ validSignedOrder2.makerAddress,
+ validTakerAddress,
+ ];
+ await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses);
+ // Check balances
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const makerFeePaid2 = validSignedOrder2.makerFee
+ .times(makerAssetFillAmount2)
+ .dividedToIntegerBy(validSignedOrder2.makerAssetAmount);
+ const takerFeePaid2 = validSignedOrder2.takerFee
+ .times(makerAssetFillAmount2)
+ .dividedToIntegerBy(validSignedOrder2.makerAssetAmount);
+ const takerFeePaid = validSignedOrder.takerFee.plus(takerFeePaid2);
+ // Maker #1
+ expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(validSignedOrder.makerAssetAmount),
+ );
+ expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(validSignedOrder.takerAssetAmount),
+ );
+ expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress][zrxToken.address].minus(validSignedOrder.makerFee),
+ );
+ // Maker #2
+ expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount2),
+ );
+ expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid2),
+ );
+ // Taker
+ expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount),
+ );
+ expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(cumulativeMakerAssetFillAmount),
+ );
+ expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid),
+ );
+ // Fee recipient
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address]
+ .add(validSignedOrder.makerFee)
+ .add(makerFeePaid2)
+ .add(takerFeePaid),
+ );
+ });
+ it('should revert if one maker does not meet the balance threshold', async () => {
+ // Create order set with one non-valid maker address
+ const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({
+ makerAddress: invalidAddress,
+ });
+ const orders = [validSignedOrder, signedOrderWithBadMakerAddress];
+ // Execute transaction
+ const dummyMakerAssetFillAmount = new BigNumber(0);
+ return expectTransactionFailedAsync(
+ erc721TakerBalanceThresholdWrapper.marketBuyOrdersNoThrowAsync(orders, validTakerAddress, {
+ makerAssetFillAmount: dummyMakerAssetFillAmount,
+ }),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ it('should revert if taker does not meet the balance threshold', async () => {
+ const orders = [validSignedOrder, validSignedOrder2];
+ const dummyMakerAssetFillAmount = new BigNumber(0);
+ return expectTransactionFailedAsync(
+ erc721NonValidBalanceThresholdWrapper.marketBuyOrdersNoThrowAsync(orders, invalidAddress, {
+ makerAssetFillAmount: dummyMakerAssetFillAmount,
+ }),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ });
+
+ describe('matchOrders', () => {
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ validSignedOrder = await orderFactory.newSignedOrderAsync();
+ validSignedOrder2 = await orderFactory2.newSignedOrderAsync();
+ });
+ it('Should transfer correct amounts when both makers and taker meet the balance threshold', async () => {
+ // Test values/results taken from Match Orders test:
+ // 'Should transfer correct amounts when right order is fully filled and values pass isRoundingErrorFloor but fail isRoundingErrorCeil'
+ // Create orders to match
+ const signedOrderLeft = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(17), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(98), 0),
+ makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18),
+ feeRecipientAddress,
+ });
+ const signedOrderRight = await orderFactory2.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(75), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0),
+ makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18),
+ feeRecipientAddress,
+ });
+ // Compute expected transfer amounts
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(75), 0),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber('76.4705882352941176'), 16), // 76.47%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(75), 0),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), 0),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber('76.5306122448979591'), 16), // 76.53%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ const txReceipt = await erc721TakerBalanceThresholdWrapper.matchOrdersAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ validTakerAddress,
+ );
+ // Assert validated addresses
+ const expectedValidatedAddresseses = [
+ signedOrderLeft.makerAddress,
+ signedOrderRight.makerAddress,
+ validTakerAddress,
+ ];
+ await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses);
+ // Check balances
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(
+ newBalances[signedOrderLeft.makerAddress][defaultMakerAssetAddress],
+ 'Checking left maker egress ERC20 account balance',
+ ).to.be.bignumber.equal(
+ erc20Balances[signedOrderLeft.makerAddress][defaultMakerAssetAddress].sub(
+ expectedTransferAmounts.amountSoldByLeftMaker,
+ ),
+ );
+ expect(
+ newBalances[signedOrderRight.makerAddress][defaultTakerAssetAddress],
+ 'Checking right maker ingress ERC20 account balance',
+ ).to.be.bignumber.equal(
+ erc20Balances[signedOrderRight.makerAddress][defaultTakerAssetAddress].sub(
+ expectedTransferAmounts.amountSoldByRightMaker,
+ ),
+ );
+ expect(
+ newBalances[validTakerAddress][defaultMakerAssetAddress],
+ 'Checking taker ingress ERC20 account balance',
+ ).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(
+ expectedTransferAmounts.amountReceivedByTaker,
+ ),
+ );
+ expect(
+ newBalances[signedOrderLeft.makerAddress][defaultTakerAssetAddress],
+ 'Checking left maker ingress ERC20 account balance',
+ ).to.be.bignumber.equal(
+ erc20Balances[signedOrderLeft.makerAddress][defaultTakerAssetAddress].add(
+ expectedTransferAmounts.amountBoughtByLeftMaker,
+ ),
+ );
+ expect(
+ newBalances[signedOrderRight.makerAddress][defaultMakerAssetAddress],
+ 'Checking right maker egress ERC20 account balance',
+ ).to.be.bignumber.equal(
+ erc20Balances[signedOrderRight.makerAddress][defaultMakerAssetAddress].add(
+ expectedTransferAmounts.amountBoughtByRightMaker,
+ ),
+ );
+ // Paid fees
+ expect(
+ newBalances[signedOrderLeft.makerAddress][zrxToken.address],
+ 'Checking left maker egress ERC20 account fees',
+ ).to.be.bignumber.equal(
+ erc20Balances[signedOrderLeft.makerAddress][zrxToken.address].minus(
+ expectedTransferAmounts.feePaidByLeftMaker,
+ ),
+ );
+ expect(
+ newBalances[signedOrderRight.makerAddress][zrxToken.address],
+ 'Checking right maker egress ERC20 account fees',
+ ).to.be.bignumber.equal(
+ erc20Balances[signedOrderRight.makerAddress][zrxToken.address].minus(
+ expectedTransferAmounts.feePaidByRightMaker,
+ ),
+ );
+ expect(
+ newBalances[validTakerAddress][zrxToken.address],
+ 'Checking taker egress ERC20 account fees',
+ ).to.be.bignumber.equal(
+ erc20Balances[validTakerAddress][zrxToken.address]
+ .minus(expectedTransferAmounts.feePaidByTakerLeft)
+ .sub(expectedTransferAmounts.feePaidByTakerRight),
+ );
+ // Received fees
+ expect(
+ newBalances[signedOrderLeft.feeRecipientAddress][zrxToken.address],
+ 'Checking left fee recipient ingress ERC20 account fees',
+ ).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address]
+ .add(expectedTransferAmounts.feePaidByLeftMaker)
+ .add(expectedTransferAmounts.feePaidByRightMaker)
+ .add(expectedTransferAmounts.feePaidByTakerLeft)
+ .add(expectedTransferAmounts.feePaidByTakerRight),
+ );
+ });
+ it('should revert if left maker does not meet the balance threshold', async () => {
+ // Create signed order with non-valid maker address
+ const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({
+ senderAddress: erc721BalanceThresholdFilterInstance.address,
+ makerAddress: invalidAddress,
+ });
+ // Execute transaction
+ return expectTransactionFailedAsync(
+ erc721TakerBalanceThresholdWrapper.matchOrdersAsync(
+ validSignedOrder,
+ signedOrderWithBadMakerAddress,
+ validTakerAddress,
+ ),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ it('should revert if right maker does not meet the balance threshold', async () => {
+ // Create signed order with non-valid maker address
+ const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({
+ senderAddress: erc721BalanceThresholdFilterInstance.address,
+ makerAddress: invalidAddress,
+ });
+ // Execute transaction
+ return expectTransactionFailedAsync(
+ erc721TakerBalanceThresholdWrapper.matchOrdersAsync(
+ signedOrderWithBadMakerAddress,
+ validSignedOrder,
+ validTakerAddress,
+ ),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ it('should revert if taker does not meet the balance threshold', async () => {
+ return expectTransactionFailedAsync(
+ erc721NonValidBalanceThresholdWrapper.matchOrdersAsync(
+ validSignedOrder,
+ validSignedOrder,
+ invalidAddress,
+ ),
+ RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold,
+ );
+ });
+ });
+
+ describe('cancelOrder', () => {
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ validSignedOrder = await orderFactory.newSignedOrderAsync();
+ validSignedOrder2 = await orderFactory2.newSignedOrderAsync();
+ });
+ it('Should successfully cancel order if maker meets balance threshold', async () => {
+ // Verify order is not cancelled
+ const orderInfoBeforeCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync(
+ validSignedOrder,
+ );
+ expect(orderInfoBeforeCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE);
+ // Cancel
+ const txReceipt = await erc721MakerBalanceThresholdWrapper.cancelOrderAsync(
+ validSignedOrder,
+ validSignedOrder.makerAddress,
+ );
+ // Assert validated addresses
+ const expectedValidatedAddresseses: string[] = [];
+ await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses);
+ // Check that order was cancelled
+ const orderInfoAfterCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync(
+ validSignedOrder,
+ );
+ expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.CANCELLED);
+ });
+ it('Should successfully cancel order if maker does not meet balance threshold', async () => {
+ // Create order where maker does not meet balance threshold
+ const signedOrderWithBadMakerAddress = await invalidOrderFactory.newSignedOrderAsync({});
+ // Verify order is not cancelled
+ const orderInfoBeforeCancelling = await erc721NonValidBalanceThresholdWrapper.getOrderInfoAsync(
+ signedOrderWithBadMakerAddress,
+ );
+ expect(orderInfoBeforeCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE);
+ // Cancel
+ const txReceipt = await erc721NonValidBalanceThresholdWrapper.cancelOrderAsync(
+ signedOrderWithBadMakerAddress,
+ signedOrderWithBadMakerAddress.makerAddress,
+ );
+ // Assert validated addresses
+ const expectedValidatedAddresseses: string[] = [];
+ await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses);
+ // Check that order was cancelled
+ const orderInfoAfterCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync(
+ signedOrderWithBadMakerAddress,
+ );
+ expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.CANCELLED);
+ });
+ });
+
+ describe('batchCancelOrders', () => {
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ });
+ it('Should successfully batch cancel orders if maker meets balance threshold', async () => {
+ // Create orders to cancel
+ const validSignedOrders = [
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync(),
+ ];
+ // Verify orders are not cancelled
+ _.each(validSignedOrders, async signedOrder => {
+ const orderInfoBeforeCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync(
+ signedOrder,
+ );
+ return expect(orderInfoBeforeCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE);
+ });
+ // Cancel
+ const txReceipt = await erc721MakerBalanceThresholdWrapper.batchCancelOrdersAsync(
+ validSignedOrders,
+ validSignedOrders[0].makerAddress,
+ );
+ // Assert validated addresses
+ const expectedValidatedAddresseses: string[] = [];
+ await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses);
+ // Check that order was cancelled
+ _.each(validSignedOrders, async signedOrder => {
+ const orderInfoAfterCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync(
+ signedOrder,
+ );
+ return expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.CANCELLED);
+ });
+ });
+ it('Should successfully batch cancel order if maker does not meet balance threshold', async () => {
+ // Create orders to cancel
+ const invalidSignedOrders = [
+ await invalidOrderFactory.newSignedOrderAsync(),
+ await invalidOrderFactory.newSignedOrderAsync(),
+ await invalidOrderFactory.newSignedOrderAsync(),
+ ];
+ // Verify orders are not cancelled
+ _.each(invalidSignedOrders, async signedOrder => {
+ const orderInfoBeforeCancelling = await erc721NonValidBalanceThresholdWrapper.getOrderInfoAsync(
+ signedOrder,
+ );
+ return expect(orderInfoBeforeCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE);
+ });
+ // Cancel
+ const txReceipt = await erc721NonValidBalanceThresholdWrapper.batchCancelOrdersAsync(
+ invalidSignedOrders,
+ invalidAddress,
+ );
+ // Assert validated addresses
+ const expectedValidatedAddresseses: string[] = [];
+ await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses);
+ // Check that order was cancelled
+ _.each(invalidSignedOrders, async signedOrder => {
+ const orderInfoAfterCancelling = await erc721NonValidBalanceThresholdWrapper.getOrderInfoAsync(
+ signedOrder,
+ );
+ return expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.CANCELLED);
+ });
+ });
+ });
+
+ describe('cancelOrdersUpTo', () => {
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ });
+ it('Should successfully batch cancel orders if maker meets balance threshold', async () => {
+ // Create orders to cancel
+ const validSignedOrders = [
+ await orderFactory.newSignedOrderAsync({ salt: new BigNumber(0) }),
+ await orderFactory.newSignedOrderAsync({ salt: new BigNumber(1) }),
+ await orderFactory.newSignedOrderAsync({ salt: new BigNumber(2) }),
+ ];
+ // Verify orders are not cancelled
+ _.each(validSignedOrders, async signedOrder => {
+ const orderInfoBeforeCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync(
+ signedOrder,
+ );
+ return expect(orderInfoBeforeCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE);
+ });
+ // Cancel
+ const cancelOrdersUpToThisSalt = new BigNumber(1);
+ const txReceipt = await erc721MakerBalanceThresholdWrapper.cancelOrdersUpToAsync(
+ cancelOrdersUpToThisSalt,
+ validSignedOrders[0].makerAddress,
+ );
+ // Assert validated addresses
+ const expectedValidatedAddresseses: string[] = [];
+ await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses);
+ // Check that order was cancelled
+ _.each(validSignedOrders, async (signedOrder, salt: number) => {
+ const orderInfoAfterCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync(
+ signedOrder,
+ );
+ const saltAsBigNumber = new BigNumber(salt);
+ if (saltAsBigNumber.lessThanOrEqualTo(cancelOrdersUpToThisSalt)) {
+ return expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.CANCELLED);
+ } else {
+ return expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE);
+ }
+ });
+ });
+ it('Should successfully batch cancel order if maker does not meet balance threshold', async () => {
+ // Create orders to cancel
+ const invalidSignedOrders = [
+ await invalidOrderFactory.newSignedOrderAsync({ salt: new BigNumber(0) }),
+ await invalidOrderFactory.newSignedOrderAsync({ salt: new BigNumber(1) }),
+ await invalidOrderFactory.newSignedOrderAsync({ salt: new BigNumber(2) }),
+ ];
+ // Verify orders are not cancelled
+ _.each(invalidSignedOrders, async signedOrder => {
+ const orderInfoBeforeCancelling = await erc721NonValidBalanceThresholdWrapper.getOrderInfoAsync(
+ signedOrder,
+ );
+ return expect(orderInfoBeforeCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE);
+ });
+ // Cancel
+ const cancelOrdersUpToThisSalt = new BigNumber(1);
+ const txReceipt = await erc721NonValidBalanceThresholdWrapper.cancelOrdersUpToAsync(
+ cancelOrdersUpToThisSalt,
+ invalidAddress,
+ );
+ // Assert validated addresses
+ const expectedValidatedAddresseses: string[] = [];
+ await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses);
+ // Check that order was cancelled
+ _.each(invalidSignedOrders, async (signedOrder, salt: number) => {
+ const orderInfoAfterCancelling = await erc721NonValidBalanceThresholdWrapper.getOrderInfoAsync(
+ signedOrder,
+ );
+ const saltAsBigNumber = new BigNumber(salt);
+ if (saltAsBigNumber.lessThanOrEqualTo(cancelOrdersUpToThisSalt)) {
+ return expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.CANCELLED);
+ } else {
+ return expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE);
+ }
+ });
+ });
+ });
+});
+// tslint:disable:max-file-line-count
+// tslint:enable:no-unnecessary-type-assertion
diff --git a/contracts/extensions/test/utils/balance_threshold_wrapper.ts b/contracts/extensions/test/utils/balance_threshold_wrapper.ts
new file mode 100644
index 000000000..28a4ef011
--- /dev/null
+++ b/contracts/extensions/test/utils/balance_threshold_wrapper.ts
@@ -0,0 +1,283 @@
+import { artifacts as protocolArtifacts, ExchangeContract } from '@0x/contracts-protocol';
+import {
+ FillResults,
+ formatters,
+ LogDecoder,
+ OrderInfo,
+ orderUtils,
+ TransactionFactory,
+} from '@0x/contracts-test-utils';
+import { artifacts as tokensArtifacts } from '@0x/contracts-tokens';
+import { SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import { Provider, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
+import * as _ from 'lodash';
+
+import { BalanceThresholdFilterContract } from '../../generated-wrappers/balance_threshold_filter';
+import { artifacts } from '../../src/artifacts';
+
+export class BalanceThresholdWrapper {
+ private readonly _balanceThresholdFilter: BalanceThresholdFilterContract;
+ private readonly _signerTransactionFactory: TransactionFactory;
+ private readonly _exchange: ExchangeContract;
+ private readonly _web3Wrapper: Web3Wrapper;
+ private readonly _logDecoder: LogDecoder;
+ constructor(
+ balanceThresholdFilter: BalanceThresholdFilterContract,
+ exchangeContract: ExchangeContract,
+ signerTransactionFactory: TransactionFactory,
+ provider: Provider,
+ ) {
+ this._balanceThresholdFilter = balanceThresholdFilter;
+ this._exchange = exchangeContract;
+ this._signerTransactionFactory = signerTransactionFactory;
+ this._web3Wrapper = new Web3Wrapper(provider);
+ this._logDecoder = new LogDecoder(this._web3Wrapper, {
+ ...artifacts,
+ ...tokensArtifacts,
+ ...protocolArtifacts,
+ });
+ }
+ public async fillOrderAsync(
+ signedOrder: SignedOrder,
+ from: string,
+ opts: { takerAssetFillAmount?: BigNumber } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
+ const data = this._exchange.fillOrder.getABIEncodedTransactionData(
+ params.order,
+ params.takerAssetFillAmount,
+ params.signature,
+ );
+ const txReceipt = this._executeTransactionAsync(data, from);
+ return txReceipt;
+ }
+ public async fillOrKillOrderAsync(
+ signedOrder: SignedOrder,
+ from: string,
+ opts: { takerAssetFillAmount?: BigNumber } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
+ const data = this._exchange.fillOrKillOrder.getABIEncodedTransactionData(
+ params.order,
+ params.takerAssetFillAmount,
+ params.signature,
+ );
+ const txReceipt = this._executeTransactionAsync(data, from);
+ return txReceipt;
+ }
+ public async fillOrderNoThrowAsync(
+ signedOrder: SignedOrder,
+ from: string,
+ opts: { takerAssetFillAmount?: BigNumber; gas?: number } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
+ const data = this._exchange.fillOrderNoThrow.getABIEncodedTransactionData(
+ params.order,
+ params.takerAssetFillAmount,
+ params.signature,
+ );
+ const txReceipt = this._executeTransactionAsync(data, from, opts.gas);
+ return txReceipt;
+ }
+ public async batchFillOrdersAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { takerAssetFillAmounts?: BigNumber[] } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts);
+ const data = this._exchange.batchFillOrders.getABIEncodedTransactionData(
+ params.orders,
+ params.takerAssetFillAmounts,
+ params.signatures,
+ );
+ const txReceipt = this._executeTransactionAsync(data, from);
+ return txReceipt;
+ }
+ public async batchFillOrKillOrdersAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { takerAssetFillAmounts?: BigNumber[] } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts);
+ const data = this._exchange.batchFillOrKillOrders.getABIEncodedTransactionData(
+ params.orders,
+ params.takerAssetFillAmounts,
+ params.signatures,
+ );
+ const txReceipt = this._executeTransactionAsync(data, from);
+ return txReceipt;
+ }
+ public async batchFillOrdersNoThrowAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { takerAssetFillAmounts?: BigNumber[]; gas?: number } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts);
+ const data = this._exchange.batchFillOrKillOrders.getABIEncodedTransactionData(
+ params.orders,
+ params.takerAssetFillAmounts,
+ params.signatures,
+ );
+ const txReceipt = this._executeTransactionAsync(data, from, opts.gas);
+ return txReceipt;
+ }
+ public async marketSellOrdersAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { takerAssetFillAmount: BigNumber },
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createMarketSellOrders(orders, opts.takerAssetFillAmount);
+ const data = this._exchange.marketSellOrders.getABIEncodedTransactionData(
+ params.orders,
+ params.takerAssetFillAmount,
+ params.signatures,
+ );
+ const txReceipt = this._executeTransactionAsync(data, from);
+ return txReceipt;
+ }
+ public async marketSellOrdersNoThrowAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { takerAssetFillAmount: BigNumber; gas?: number },
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createMarketSellOrders(orders, opts.takerAssetFillAmount);
+ const data = this._exchange.marketSellOrdersNoThrow.getABIEncodedTransactionData(
+ params.orders,
+ params.takerAssetFillAmount,
+ params.signatures,
+ );
+ const txReceipt = this._executeTransactionAsync(data, from, opts.gas);
+ return txReceipt;
+ }
+ public async marketBuyOrdersAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { makerAssetFillAmount: BigNumber },
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createMarketBuyOrders(orders, opts.makerAssetFillAmount);
+ const data = this._exchange.marketBuyOrders.getABIEncodedTransactionData(
+ params.orders,
+ params.makerAssetFillAmount,
+ params.signatures,
+ );
+ const txReceipt = this._executeTransactionAsync(data, from);
+ return txReceipt;
+ }
+ public async marketBuyOrdersNoThrowAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { makerAssetFillAmount: BigNumber; gas?: number },
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createMarketBuyOrders(orders, opts.makerAssetFillAmount);
+ const data = this._exchange.marketBuyOrdersNoThrow.getABIEncodedTransactionData(
+ params.orders,
+ params.makerAssetFillAmount,
+ params.signatures,
+ );
+ const txReceipt = this._executeTransactionAsync(data, from, opts.gas);
+ return txReceipt;
+ }
+ public async cancelOrderAsync(signedOrder: SignedOrder, from: string): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = orderUtils.createCancel(signedOrder);
+ const data = this._exchange.cancelOrder.getABIEncodedTransactionData(params.order);
+ const txReceipt = this._executeTransactionAsync(data, from);
+ return txReceipt;
+ }
+ public async batchCancelOrdersAsync(
+ orders: SignedOrder[],
+ from: string,
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createBatchCancel(orders);
+ const data = this._exchange.batchCancelOrders.getABIEncodedTransactionData(params.orders);
+ const txReceipt = this._executeTransactionAsync(data, from);
+ return txReceipt;
+ }
+ public async cancelOrdersUpToAsync(salt: BigNumber, from: string): Promise<TransactionReceiptWithDecodedLogs> {
+ const data = this._exchange.cancelOrdersUpTo.getABIEncodedTransactionData(salt);
+ const txReceipt = this._executeTransactionAsync(data, from);
+ return txReceipt;
+ }
+ public async getTakerAssetFilledAmountAsync(orderHashHex: string): Promise<BigNumber> {
+ const filledAmount = await this._exchange.filled.callAsync(orderHashHex);
+ return filledAmount;
+ }
+ public async isCancelledAsync(orderHashHex: string): Promise<boolean> {
+ const isCancelled = await this._exchange.cancelled.callAsync(orderHashHex);
+ return isCancelled;
+ }
+ public async getOrderEpochAsync(makerAddress: string, senderAddress: string): Promise<BigNumber> {
+ const orderEpoch = await this._exchange.orderEpoch.callAsync(makerAddress, senderAddress);
+ return orderEpoch;
+ }
+ public async getOrderInfoAsync(signedOrder: SignedOrder): Promise<OrderInfo> {
+ const orderInfo = await this._exchange.getOrderInfo.callAsync(signedOrder);
+ return orderInfo;
+ }
+ public async getOrdersInfoAsync(signedOrders: SignedOrder[]): Promise<OrderInfo[]> {
+ const ordersInfo = (await this._exchange.getOrdersInfo.callAsync(signedOrders)) as OrderInfo[];
+ return ordersInfo;
+ }
+ public async matchOrdersAsync(
+ signedOrderLeft: SignedOrder,
+ signedOrderRight: SignedOrder,
+ from: string,
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = orderUtils.createMatchOrders(signedOrderLeft, signedOrderRight);
+ const data = await this._exchange.matchOrders.getABIEncodedTransactionData(
+ params.left,
+ params.right,
+ params.leftSignature,
+ params.rightSignature,
+ );
+ const txReceipt = this._executeTransactionAsync(data, from);
+ return txReceipt;
+ }
+ public async getFillOrderResultsAsync(
+ signedOrder: SignedOrder,
+ from: string,
+ opts: { takerAssetFillAmount?: BigNumber } = {},
+ ): Promise<FillResults> {
+ const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
+ const fillResults = await this._exchange.fillOrder.callAsync(
+ params.order,
+ params.takerAssetFillAmount,
+ params.signature,
+ { from },
+ );
+ return fillResults;
+ }
+ public abiEncodeFillOrder(signedOrder: SignedOrder, opts: { takerAssetFillAmount?: BigNumber } = {}): string {
+ const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
+ const data = this._exchange.fillOrder.getABIEncodedTransactionData(
+ params.order,
+ params.takerAssetFillAmount,
+ params.signature,
+ );
+ return data;
+ }
+ public getBalanceThresholdAddress(): string {
+ return this._balanceThresholdFilter.address;
+ }
+ public getExchangeAddress(): string {
+ return this._exchange.address;
+ }
+ private async _executeTransactionAsync(
+ abiEncodedExchangeTxData: string,
+ from: string,
+ gas?: number,
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const signedExchangeTx = this._signerTransactionFactory.newSignedTransaction(abiEncodedExchangeTxData);
+ const txOpts = _.isUndefined(gas) ? { from } : { from, gas };
+ const txHash = await this._balanceThresholdFilter.executeTransaction.sendTransactionAsync(
+ signedExchangeTx.salt,
+ signedExchangeTx.signerAddress,
+ signedExchangeTx.data,
+ signedExchangeTx.signature,
+ txOpts,
+ );
+ const txReceipt = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return txReceipt;
+ }
+}
diff --git a/contracts/extensions/tsconfig.json b/contracts/extensions/tsconfig.json
index a4ce1e002..a303e3f5c 100644
--- a/contracts/extensions/tsconfig.json
+++ b/contracts/extensions/tsconfig.json
@@ -6,6 +6,10 @@
"resolveJsonModule": true
},
"include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"],
- "files": ["./generated-artifacts/DutchAuction.json", "./generated-artifacts/Forwarder.json"],
+ "files": [
+ "./generated-artifacts/BalanceThresholdFilter.json",
+ "./generated-artifacts/DutchAuction.json",
+ "./generated-artifacts/Forwarder.json"
+ ],
"exclude": ["./deploy/solc/solc_bin"]
}
diff --git a/contracts/libs/contracts/libs/LibAddressArray.sol b/contracts/libs/contracts/libs/LibAddressArray.sol
new file mode 100644
index 000000000..ccae2ac5f
--- /dev/null
+++ b/contracts/libs/contracts/libs/LibAddressArray.sol
@@ -0,0 +1,84 @@
+/*
+
+ Copyright 2018 ZeroEx Intl.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+*/
+
+pragma solidity 0.4.24;
+
+import "@0x/contracts-utils/contracts/utils/LibBytes/LibBytes.sol";
+
+
+library LibAddressArray {
+
+ /// @dev Append a new address to an array of addresses.
+ /// The `addressArray` may need to be reallocated to make space
+ /// for the new address. Because of this we return the resulting
+ /// memory location of `addressArray`.
+ /// @param addressToAppend Address to append.
+ /// @return Array of addresses: [... addressArray, addressToAppend]
+ function append(address[] memory addressArray, address addressToAppend)
+ internal pure
+ returns (address[])
+ {
+ // Get stats on address array and free memory
+ uint256 freeMemPtr = 0;
+ uint256 addressArrayBeginPtr = 0;
+ uint256 addressArrayEndPtr = 0;
+ uint256 addressArrayLength = addressArray.length;
+ uint256 addressArrayMemSizeInBytes = 32 + (32 * addressArrayLength);
+ assembly {
+ freeMemPtr := mload(0x40)
+ addressArrayBeginPtr := addressArray
+ addressArrayEndPtr := add(addressArray, addressArrayMemSizeInBytes)
+ }
+
+ // Cases for `freeMemPtr`:
+ // `freeMemPtr` == `addressArrayEndPtr`: Nothing occupies memory after `addressArray`
+ // `freeMemPtr` > `addressArrayEndPtr`: Some value occupies memory after `addressArray`
+ // `freeMemPtr` < `addressArrayEndPtr`: Memory has not been managed properly.
+ require(
+ freeMemPtr >= addressArrayEndPtr,
+ "INVALID_FREE_MEMORY_PTR"
+ );
+
+ // If free memory begins at the end of `addressArray`
+ // then we can append `addressToAppend` directly.
+ // Otherwise, we must copy the array to free memory
+ // before appending new values to it.
+ if (freeMemPtr > addressArrayEndPtr) {
+ LibBytes.memCopy(freeMemPtr, addressArrayBeginPtr, addressArrayMemSizeInBytes);
+ assembly {
+ addressArray := freeMemPtr
+ addressArrayBeginPtr := addressArray
+ }
+ }
+
+ // Append `addressToAppend`
+ addressArrayLength += 1;
+ addressArrayMemSizeInBytes += 32;
+ addressArrayEndPtr = addressArrayBeginPtr + addressArrayMemSizeInBytes;
+ freeMemPtr = addressArrayEndPtr;
+ assembly {
+ // Store new array length
+ mstore(addressArray, addressArrayLength)
+
+ // Update `freeMemPtr`
+ mstore(0x40, freeMemPtr)
+ }
+ addressArray[addressArrayLength - 1] = addressToAppend;
+ return addressArray;
+ }
+}
diff --git a/contracts/libs/contracts/libs/LibExchangeSelectors.sol b/contracts/libs/contracts/libs/LibExchangeSelectors.sol
new file mode 100644
index 000000000..edb4f9cbd
--- /dev/null
+++ b/contracts/libs/contracts/libs/LibExchangeSelectors.sol
@@ -0,0 +1,152 @@
+/*
+
+ 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 LibExchangeSelectors {
+
+ // solhint-disable max-line-length
+ // allowedValidators
+ bytes4 constant public ALLOWED_VALIDATORS_SELECTOR = 0x7b8e3514;
+ bytes4 constant public ALLOWED_VALIDATORS_SELECTOR_GENERATOR = bytes4(keccak256("allowedValidators(address,address)"));
+
+ // assetProxies
+ bytes4 constant public ASSET_PROXIES_SELECTOR = 0x3fd3c997;
+ bytes4 constant public ASSET_PROXIES_SELECTOR_GENERATOR = bytes4(keccak256("assetProxies(bytes4)"));
+
+ // batchCancelOrders
+ bytes4 constant public BATCH_CANCEL_ORDERS_SELECTOR = 0x4ac14782;
+ bytes4 constant public BATCH_CANCEL_ORDERS_SELECTOR_GENERATOR = bytes4(keccak256("batchCancelOrders((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[])"));
+
+ // batchFillOrKillOrders
+ bytes4 constant public BATCH_FILL_OR_KILL_ORDERS_SELECTOR = 0x4d0ae546;
+ bytes4 constant public BATCH_FILL_OR_KILL_ORDERS_SELECTOR_GENERATOR = bytes4(keccak256("batchFillOrKillOrders((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[],uint256[],bytes[])"));
+
+ // batchFillOrders
+ bytes4 constant public BATCH_FILL_ORDERS_SELECTOR = 0x297bb70b;
+ bytes4 constant public BATCH_FILL_ORDERS_SELECTOR_GENERATOR = bytes4(keccak256("batchFillOrders((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[],uint256[],bytes[])"));
+
+ // batchFillOrdersNoThrow
+ bytes4 constant public BATCH_FILL_ORDERS_NO_THROW_SELECTOR = 0x50dde190;
+ bytes4 constant public BATCH_FILL_ORDERS_NO_THROW_SELECTOR_GENERATOR = bytes4(keccak256("batchFillOrdersNoThrow((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[],uint256[],bytes[])"));
+
+ // cancelOrder
+ bytes4 constant public CANCEL_ORDER_SELECTOR = 0xd46b02c3;
+ bytes4 constant public CANCEL_ORDER_SELECTOR_GENERATOR = bytes4(keccak256("cancelOrder((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes))"));
+
+ // cancelOrdersUpTo
+ bytes4 constant public CANCEL_ORDERS_UP_TO_SELECTOR = 0x4f9559b1;
+ bytes4 constant public CANCEL_ORDERS_UP_TO_SELECTOR_GENERATOR = bytes4(keccak256("cancelOrdersUpTo(uint256)"));
+
+ // cancelled
+ bytes4 constant public CANCELLED_SELECTOR = 0x2ac12622;
+ bytes4 constant public CANCELLED_SELECTOR_GENERATOR = bytes4(keccak256("cancelled(bytes32)"));
+
+ // currentContextAddress
+ bytes4 constant public CURRENT_CONTEXT_ADDRESS_SELECTOR = 0xeea086ba;
+ bytes4 constant public CURRENT_CONTEXT_ADDRESS_SELECTOR_GENERATOR = bytes4(keccak256("currentContextAddress()"));
+
+ // executeTransaction
+ bytes4 constant public EXECUTE_TRANSACTION_SELECTOR = 0xbfc8bfce;
+ bytes4 constant public EXECUTE_TRANSACTION_SELECTOR_GENERATOR = bytes4(keccak256("executeTransaction(uint256,address,bytes,bytes)"));
+
+ // fillOrKillOrder
+ bytes4 constant public FILL_OR_KILL_ORDER_SELECTOR = 0x64a3bc15;
+ bytes4 constant public FILL_OR_KILL_ORDER_SELECTOR_GENERATOR = bytes4(keccak256("fillOrKillOrder((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes),uint256,bytes)"));
+
+ // fillOrder
+ bytes4 constant public FILL_ORDER_SELECTOR = 0xb4be83d5;
+ bytes4 constant public FILL_ORDER_SELECTOR_GENERATOR = bytes4(keccak256("fillOrder((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes),uint256,bytes)"));
+
+ // fillOrderNoThrow
+ bytes4 constant public FILL_ORDER_NO_THROW_SELECTOR = 0x3e228bae;
+ bytes4 constant public FILL_ORDER_NO_THROW_SELECTOR_GENERATOR = bytes4(keccak256("fillOrderNoThrow((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes),uint256,bytes)"));
+
+ // filled
+ bytes4 constant public FILLED_SELECTOR = 0x288cdc91;
+ bytes4 constant public FILLED_SELECTOR_GENERATOR = bytes4(keccak256("filled(bytes32)"));
+
+ // getAssetProxy
+ bytes4 constant public GET_ASSET_PROXY_SELECTOR = 0x60704108;
+ bytes4 constant public GET_ASSET_PROXY_SELECTOR_GENERATOR = bytes4(keccak256("getAssetProxy(bytes4)"));
+
+ // getOrderInfo
+ bytes4 constant public GET_ORDER_INFO_SELECTOR = 0xc75e0a81;
+ bytes4 constant public GET_ORDER_INFO_SELECTOR_GENERATOR = bytes4(keccak256("getOrderInfo((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes))"));
+
+ // getOrdersInfo
+ bytes4 constant public GET_ORDERS_INFO_SELECTOR = 0x7e9d74dc;
+ bytes4 constant public GET_ORDERS_INFO_SELECTOR_GENERATOR = bytes4(keccak256("getOrdersInfo((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[])"));
+
+ // isValidSignature
+ bytes4 constant public IS_VALID_SIGNATURE_SELECTOR = 0x93634702;
+ bytes4 constant public IS_VALID_SIGNATURE_SELECTOR_GENERATOR = bytes4(keccak256("isValidSignature(bytes32,address,bytes)"));
+
+ // marketBuyOrders
+ bytes4 constant public MARKET_BUY_ORDERS_SELECTOR = 0xe5fa431b;
+ bytes4 constant public MARKET_BUY_ORDERS_SELECTOR_GENERATOR = bytes4(keccak256("marketBuyOrders((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[],uint256,bytes[])"));
+
+ // marketBuyOrdersNoThrow
+ bytes4 constant public MARKET_BUY_ORDERS_NO_THROW_SELECTOR = 0xa3e20380;
+ bytes4 constant public MARKET_BUY_ORDERS_NO_THROW_SELECTOR_GENERATOR = bytes4(keccak256("marketBuyOrdersNoThrow((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[],uint256,bytes[])"));
+
+ // marketSellOrders
+ bytes4 constant public MARKET_SELL_ORDERS_SELECTOR = 0x7e1d9808;
+ bytes4 constant public MARKET_SELL_ORDERS_SELECTOR_GENERATOR = bytes4(keccak256("marketSellOrders((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[],uint256,bytes[])"));
+
+ // marketSellOrdersNoThrow
+ bytes4 constant public MARKET_SELL_ORDERS_NO_THROW_SELECTOR = 0xdd1c7d18;
+ bytes4 constant public MARKET_SELL_ORDERS_NO_THROW_SELECTOR_GENERATOR = bytes4(keccak256("marketSellOrdersNoThrow((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[],uint256,bytes[])"));
+
+ // matchOrders
+ bytes4 constant public MATCH_ORDERS_SELECTOR = 0x3c28d861;
+ bytes4 constant public MATCH_ORDERS_SELECTOR_GENERATOR = bytes4(keccak256("matchOrders((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes),(address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes),bytes,bytes)"));
+
+ // orderEpoch
+ bytes4 constant public ORDER_EPOCH_SELECTOR = 0xd9bfa73e;
+ bytes4 constant public ORDER_EPOCH_SELECTOR_GENERATOR = bytes4(keccak256("orderEpoch(address,address)"));
+
+ // owner
+ bytes4 constant public OWNER_SELECTOR = 0x8da5cb5b;
+ bytes4 constant public OWNER_SELECTOR_GENERATOR = bytes4(keccak256("owner()"));
+
+ // preSign
+ bytes4 constant public PRE_SIGN_SELECTOR = 0x3683ef8e;
+ bytes4 constant public PRE_SIGN_SELECTOR_GENERATOR = bytes4(keccak256("preSign(bytes32,address,bytes)"));
+
+ // preSigned
+ bytes4 constant public PRE_SIGNED_SELECTOR = 0x82c174d0;
+ bytes4 constant public PRE_SIGNED_SELECTOR_GENERATOR = bytes4(keccak256("preSigned(bytes32,address)"));
+
+ // registerAssetProxy
+ bytes4 constant public REGISTER_ASSET_PROXY_SELECTOR = 0xc585bb93;
+ bytes4 constant public REGISTER_ASSET_PROXY_SELECTOR_GENERATOR = bytes4(keccak256("registerAssetProxy(address)"));
+
+ // setSignatureValidatorApproval
+ bytes4 constant public SET_SIGNATURE_VALIDATOR_APPROVAL_SELECTOR = 0x77fcce68;
+ bytes4 constant public SET_SIGNATURE_VALIDATOR_APPROVAL_SELECTOR_GENERATOR = bytes4(keccak256("setSignatureValidatorApproval(address,bool)"));
+
+ // transactions
+ bytes4 constant public TRANSACTIONS_SELECTOR = 0x642f2eaf;
+ bytes4 constant public TRANSACTIONS_SELECTOR_GENERATOR = bytes4(keccak256("transactions(bytes32)"));
+
+ // transferOwnership
+ bytes4 constant public TRANSFER_OWNERSHIP_SELECTOR = 0xf2fde38b;
+ bytes4 constant public TRANSFER_OWNERSHIP_SELECTOR_GENERATOR = bytes4(keccak256("transferOwnership(address)"));
+} \ No newline at end of file
diff --git a/contracts/multisig/src/index.ts b/contracts/multisig/src/index.ts
new file mode 100644
index 000000000..d55f08ea2
--- /dev/null
+++ b/contracts/multisig/src/index.ts
@@ -0,0 +1,2 @@
+export * from './artifacts';
+export * from './wrappers';
diff --git a/contracts/protocol/CHANGELOG.json b/contracts/protocol/CHANGELOG.json
index 5c3798a69..2dca9794a 100644
--- a/contracts/protocol/CHANGELOG.json
+++ b/contracts/protocol/CHANGELOG.json
@@ -1,5 +1,14 @@
[
{
+ "version": "2.2.0",
+ "changes": [
+ {
+ "note": "Added LibAddressArray",
+ "pr": 1383
+ }
+ ]
+ },
+ {
"timestamp": 1544741676,
"version": "2.1.59",
"changes": [
diff --git a/contracts/protocol/package.json b/contracts/protocol/package.json
index 838189371..122ce07c3 100644
--- a/contracts/protocol/package.json
+++ b/contracts/protocol/package.json
@@ -44,7 +44,6 @@
"homepage": "https://github.com/0xProject/0x-monorepo/contracts/protocol/README.md",
"devDependencies": {
"@0x/abi-gen": "^1.0.19",
- "@0x/contracts-test-utils": "^1.0.2",
"@0x/dev-utils": "^1.0.21",
"@0x/sol-compiler": "^1.1.16",
"@0x/sol-cov": "^2.1.16",
@@ -75,6 +74,7 @@
"@0x/contracts-interfaces": "^1.0.2",
"@0x/contracts-libs": "^1.0.2",
"@0x/contracts-multisig": "^1.0.2",
+ "@0x/contracts-test-utils": "^1.0.2",
"@0x/contracts-tokens": "^1.0.2",
"@0x/contracts-utils": "^1.0.2",
"@0x/order-utils": "^3.0.7",
diff --git a/contracts/test-utils/src/types.ts b/contracts/test-utils/src/types.ts
index d738fcd4e..1630eab0d 100644
--- a/contracts/test-utils/src/types.ts
+++ b/contracts/test-utils/src/types.ts
@@ -104,6 +104,7 @@ export enum ContractName {
Authorizable = 'Authorizable',
Whitelist = 'Whitelist',
Forwarder = 'Forwarder',
+ BalanceThresholdFilter = 'BalanceThresholdFilter',
}
export interface SignedTransaction {
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index 6b728af71..4470dd501 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -243,6 +243,10 @@ export enum RevertReason {
AuctionNotStarted = 'AUCTION_NOT_STARTED',
AuctionInvalidBeginTime = 'INVALID_BEGIN_TIME',
InvalidAssetData = 'INVALID_ASSET_DATA',
+ // Balance Threshold Filter
+ InvalidOrBlockedExchangeSelector = 'INVALID_OR_BLOCKED_EXCHANGE_SELECTOR',
+ BalanceQueryFailed = 'BALANCE_QUERY_FAILED',
+ AtLeastOneAddressDoesNotMeetBalanceThreshold = 'AT_LEAST_ONE_ADDRESS_DOES_NOT_MEET_BALANCE_THRESHOLD',
}
export enum StatusCodes {