diff options
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 { |