From 72c7157138f74bf3aff50f375327ee3a8c3dc33f Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Sat, 27 Jul 2019 22:47:48 -0400 Subject: [PATCH 01/48] `@0x/contracts-exchange`: Start work on isolated fill order tests. --- contracts/exchange/compiler.json | 1 + .../contracts/src/MixinExchangeCore.sol | 2 +- .../test/TestExchangeIsolatedFillOrder.sol | 112 ++++++++++++++++++ contracts/exchange/package.json | 2 +- contracts/exchange/src/artifacts.ts | 2 + contracts/exchange/src/wrappers.ts | 1 + .../exchange/test/isolated_fill_order.ts | 49 ++++++++ .../utils/fill_order_combinatorial_utils.ts | 6 +- contracts/exchange/tsconfig.json | 1 + 9 files changed, 169 insertions(+), 7 deletions(-) create mode 100644 contracts/exchange/contracts/test/TestExchangeIsolatedFillOrder.sol create mode 100644 contracts/exchange/test/isolated_fill_order.ts diff --git a/contracts/exchange/compiler.json b/contracts/exchange/compiler.json index 167f1f515a..da36d1f73d 100644 --- a/contracts/exchange/compiler.json +++ b/contracts/exchange/compiler.json @@ -39,6 +39,7 @@ "test/ReentrantERC20Token.sol", "test/TestAssetProxyDispatcher.sol", "test/TestExchangeInternals.sol", + "test/TestExchangeIsolatedFillOrder.sol", "test/TestExchangeMath.sol", "test/TestLibExchangeRichErrorDecoder.sol", "test/TestSignatureValidator.sol", diff --git a/contracts/exchange/contracts/src/MixinExchangeCore.sol b/contracts/exchange/contracts/src/MixinExchangeCore.sol index 656b31806f..d0b082e213 100644 --- a/contracts/exchange/contracts/src/MixinExchangeCore.sol +++ b/contracts/exchange/contracts/src/MixinExchangeCore.sol @@ -529,7 +529,7 @@ contract MixinExchangeCore is address takerAddress, LibFillResults.FillResults memory fillResults ) - private + internal { // Transfer taker -> maker _dispatchTransferFrom( diff --git a/contracts/exchange/contracts/test/TestExchangeIsolatedFillOrder.sol b/contracts/exchange/contracts/test/TestExchangeIsolatedFillOrder.sol new file mode 100644 index 0000000000..5cf4b0d472 --- /dev/null +++ b/contracts/exchange/contracts/test/TestExchangeIsolatedFillOrder.sol @@ -0,0 +1,112 @@ +/* + + Copyright 2019 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.5.5; +pragma experimental ABIEncoderV2; + +import "../src/Exchange.sol"; + + +/// @dev A version of the Exchange contract where we override dependent +/// functions called by `_fillOrder()` to test what goes on inside of it. +contract TestExchangeIsolatedFillOrder is + Exchange +{ + // solhint-disable no-unused-vars + event UpdateFilledStateCalled( + Order order, + address takerAddress, + bytes32 orderHash, + uint256 orderTakerAssetAmount, + FillResults fillResults + ); + + event SettleOrderCalled( + bytes32 orderHash, + LibOrder.Order order, + address takerAddress, + LibFillResults.FillResults fillResults + ); + + // solhint-disable no-empty-blocks + constructor () + public + Exchange(1337) + {} + + /// @dev Allow setting of `filled` state for an order hash. + function setOrderTakerAssetFilledAmount( + bytes32 orderHash, + uint256 takerAssetFilledAmount + ) + external + { + filled[orderHash] = takerAssetFilledAmount; + } + + /// @dev Override that just logs arguments. + function _updateFilledState( + Order memory order, + address takerAddress, + bytes32 orderHash, + uint256 orderTakerAssetFilledAmount, + FillResults memory fillResults + ) + internal + { + emit UpdateFilledStateCalled( + order, + takerAddress, + orderHash, + orderTakerAssetFilledAmount, + fillResults + ); + } + + /// @dev Override that just logs arguments. + function _settleOrder( + bytes32 orderHash, + LibOrder.Order memory order, + address takerAddress, + LibFillResults.FillResults memory fillResults + ) + internal + { + emit SettleOrderCalled( + orderHash, + order, + takerAddress, + fillResults + ); + } + + /// @dev Override to log arguments and pass all empty signatures. + function _isValidOrderWithHashSignature( + Order memory order, + bytes32 orderHash, + address signerAddress, + bytes memory signature + ) + internal + view + returns (bool isValid) + { + // Pass if the signature is empty. + return signature.length == 0; + } +} diff --git a/contracts/exchange/package.json b/contracts/exchange/package.json index 83dc128500..500ecfb9c9 100644 --- a/contracts/exchange/package.json +++ b/contracts/exchange/package.json @@ -34,7 +34,7 @@ "lint-contracts": "solhint -c ../.solhint.json contracts/**/**/**/**/*.sol" }, "config": { - "abis": "./generated-artifacts/@(Exchange|ExchangeWrapper|IAssetProxyDispatcher|IEIP1271Wallet|IExchange|IExchangeCore|IMatchOrders|ISignatureValidator|ITransactions|IWallet|IWrapperFunctions|ReentrantERC20Token|TestAssetProxyDispatcher|TestExchangeInternals|TestExchangeMath|TestLibExchangeRichErrorDecoder|TestSignatureValidator|TestValidatorWallet|Whitelist).json", + "abis": "./generated-artifacts/@(Exchange|ExchangeWrapper|IAssetProxyDispatcher|IEIP1271Wallet|IExchange|IExchangeCore|IMatchOrders|ISignatureValidator|ITransactions|IWallet|IWrapperFunctions|ReentrantERC20Token|TestAssetProxyDispatcher|TestExchangeInternals|TestExchangeIsolatedFillOrder|TestExchangeMath|TestLibExchangeRichErrorDecoder|TestSignatureValidator|TestValidatorWallet|Whitelist).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/exchange/src/artifacts.ts b/contracts/exchange/src/artifacts.ts index b0aa72ec35..32ff463527 100644 --- a/contracts/exchange/src/artifacts.ts +++ b/contracts/exchange/src/artifacts.ts @@ -19,6 +19,7 @@ import * as IWrapperFunctions from '../generated-artifacts/IWrapperFunctions.jso import * as ReentrantERC20Token from '../generated-artifacts/ReentrantERC20Token.json'; import * as TestAssetProxyDispatcher from '../generated-artifacts/TestAssetProxyDispatcher.json'; import * as TestExchangeInternals from '../generated-artifacts/TestExchangeInternals.json'; +import * as TestExchangeIsolatedFillOrder from '../generated-artifacts/TestExchangeIsolatedFillOrder.json'; import * as TestExchangeMath from '../generated-artifacts/TestExchangeMath.json'; import * as TestLibExchangeRichErrorDecoder from '../generated-artifacts/TestLibExchangeRichErrorDecoder.json'; import * as TestSignatureValidator from '../generated-artifacts/TestSignatureValidator.json'; @@ -39,6 +40,7 @@ export const artifacts = { IWrapperFunctions: IWrapperFunctions as ContractArtifact, ReentrantERC20Token: ReentrantERC20Token as ContractArtifact, TestAssetProxyDispatcher: TestAssetProxyDispatcher as ContractArtifact, + TestExchangeIsolatedFillOrder: TestExchangeIsolatedFillOrder as ContractArtifact, TestExchangeInternals: TestExchangeInternals as ContractArtifact, TestExchangeMath: TestExchangeMath as ContractArtifact, TestLibExchangeRichErrorDecoder: TestLibExchangeRichErrorDecoder as ContractArtifact, diff --git a/contracts/exchange/src/wrappers.ts b/contracts/exchange/src/wrappers.ts index 719df58807..cac8d768e0 100644 --- a/contracts/exchange/src/wrappers.ts +++ b/contracts/exchange/src/wrappers.ts @@ -17,6 +17,7 @@ export * from '../generated-wrappers/i_wrapper_functions'; export * from '../generated-wrappers/reentrant_erc20_token'; export * from '../generated-wrappers/test_asset_proxy_dispatcher'; export * from '../generated-wrappers/test_exchange_internals'; +export * from '../generated-wrappers/test_exchange_isolated_fill_order'; export * from '../generated-wrappers/test_exchange_math'; export * from '../generated-wrappers/test_lib_exchange_rich_error_decoder'; export * from '../generated-wrappers/test_signature_validator'; diff --git a/contracts/exchange/test/isolated_fill_order.ts b/contracts/exchange/test/isolated_fill_order.ts new file mode 100644 index 0000000000..b81684d9ba --- /dev/null +++ b/contracts/exchange/test/isolated_fill_order.ts @@ -0,0 +1,49 @@ +import { + blockchainTests, + constants, + expect, + FillResults, + txDefaults, +} from '@0x/contracts-test-utils'; +import { Order, SignedOrder } from '@0x/types'; +import { BigNumber } from '@0x/utils'; + +import { artifacts, TestExchangeInternalsContract, TestExchangeMathContract } from '../src'; + +const emptyOrder: Order = { + senderAddress: constants.NULL_ADDRESS, + makerAddress: constants.NULL_ADDRESS, + takerAddress: constants.NULL_ADDRESS, + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + makerAssetAmount: new BigNumber(0), + takerAssetAmount: new BigNumber(0), + makerAssetData: '0x', + takerAssetData: '0x', + makerFeeAssetData: '0x', + takerFeeAssetData: '0x', + salt: new BigNumber(0), + feeRecipientAddress: constants.NULL_ADDRESS, + expirationTimeSeconds: new BigNumber(0), + domain: { + verifyingContractAddress: constants.NULL_ADDRESS, + chainId: 0, // To be filled in later. + }, +}; + +const emptySignedOrder: SignedOrder = { + ...emptyOrder, + signature: '', +}; + +blockchainTests.only('Isolated fillOrder()', () => { + it('foo', async () => { + expect('foo').to.equal('foo'); + }); + blockchainTests.resets('inner', () => { + it('bar', async () => { + return expect(Promise.resolve(true)).to.eventually.be.ok; + }); + }); + require('./nested'); +}); diff --git a/contracts/exchange/test/utils/fill_order_combinatorial_utils.ts b/contracts/exchange/test/utils/fill_order_combinatorial_utils.ts index f14aa630d0..9cd360d360 100644 --- a/contracts/exchange/test/utils/fill_order_combinatorial_utils.ts +++ b/contracts/exchange/test/utils/fill_order_combinatorial_utils.ts @@ -5,12 +5,11 @@ import { ERC721Wrapper, MultiAssetProxyContract, } from '@0x/contracts-asset-proxy'; -import { chaiSetup, constants, FillResults, orderUtils, signingUtils } from '@0x/contracts-test-utils'; +import { constants, expect, FillResults, orderUtils, signingUtils } from '@0x/contracts-test-utils'; import { BalanceAndProxyAllowanceLazyStore, ExchangeRevertErrors, orderHashUtils } from '@0x/order-utils'; import { Order, SignatureType, SignedOrder } from '@0x/types'; import { BigNumber, errorUtils, providerUtils, RevertError, StringRevertError } from '@0x/utils'; import { SupportedProvider, Web3Wrapper } from '@0x/web3-wrapper'; -import * as chai from 'chai'; import { LogWithDecodedArgs, TxData } from 'ethereum-types'; import * as _ from 'lodash'; import 'make-promises-safe'; @@ -36,9 +35,6 @@ import { FillOrderError, FillOrderSimulator } from './fill_order_simulator'; import { OrderFactoryFromScenario } from './order_factory_from_scenario'; import { SimpleAssetBalanceAndProxyAllowanceFetcher } from './simple_asset_balance_and_proxy_allowance_fetcher'; -chaiSetup.configure(); -const expect = chai.expect; - const EMPTY_FILL_RESULTS = { takerAssetFilledAmount: constants.ZERO_AMOUNT, makerAssetFilledAmount: constants.ZERO_AMOUNT, diff --git a/contracts/exchange/tsconfig.json b/contracts/exchange/tsconfig.json index 6d8ad9c993..19b7903d4d 100644 --- a/contracts/exchange/tsconfig.json +++ b/contracts/exchange/tsconfig.json @@ -17,6 +17,7 @@ "generated-artifacts/ReentrantERC20Token.json", "generated-artifacts/TestAssetProxyDispatcher.json", "generated-artifacts/TestExchangeInternals.json", + "generated-artifacts/TestExchangeIsolatedFillOrder.json", "generated-artifacts/TestExchangeMath.json", "generated-artifacts/TestLibExchangeRichErrorDecoder.json", "generated-artifacts/TestSignatureValidator.json", From cfa362321dd48fd851bfa0598b1b4390546fc207 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 30 Jul 2019 12:35:04 -0400 Subject: [PATCH 02/48] `@0x/contracts-exchange`: Switch to `TestExchangeIsolated` contract for `isolated_fill_order` tests. --- contracts/exchange/compiler.json | 2 +- .../contracts/test/TestExchangeIsolated.sol | 88 +++++++++++ .../test/TestExchangeIsolatedFillOrder.sol | 112 -------------- contracts/exchange/package.json | 2 +- contracts/exchange/src/artifacts.ts | 4 +- contracts/exchange/src/wrappers.ts | 2 +- .../exchange/test/isolated_fill_order.ts | 144 +++++++++++++----- contracts/exchange/tsconfig.json | 2 +- 8 files changed, 201 insertions(+), 155 deletions(-) create mode 100644 contracts/exchange/contracts/test/TestExchangeIsolated.sol delete mode 100644 contracts/exchange/contracts/test/TestExchangeIsolatedFillOrder.sol diff --git a/contracts/exchange/compiler.json b/contracts/exchange/compiler.json index da36d1f73d..7b036accec 100644 --- a/contracts/exchange/compiler.json +++ b/contracts/exchange/compiler.json @@ -39,7 +39,7 @@ "test/ReentrantERC20Token.sol", "test/TestAssetProxyDispatcher.sol", "test/TestExchangeInternals.sol", - "test/TestExchangeIsolatedFillOrder.sol", + "test/TestExchangeIsolated.sol", "test/TestExchangeMath.sol", "test/TestLibExchangeRichErrorDecoder.sol", "test/TestSignatureValidator.sol", diff --git a/contracts/exchange/contracts/test/TestExchangeIsolated.sol b/contracts/exchange/contracts/test/TestExchangeIsolated.sol new file mode 100644 index 0000000000..57896b7235 --- /dev/null +++ b/contracts/exchange/contracts/test/TestExchangeIsolated.sol @@ -0,0 +1,88 @@ +/* + + Copyright 2019 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.5.5; +pragma experimental ABIEncoderV2; + +import "../src/Exchange.sol"; + + +/// @dev A version of the Exchange contract with simplified signature validation +/// and a `_dispatchTransferFrom()` that only logs arguments. +contract TestExchangeIsolated is + Exchange +{ + // solhint-disable no-unused-vars + event DispatchTransferFromCalled( + bytes32 orderHash, + bytes assetData, + address from, + address to, + uint256 amount + ); + + /// @dev Raw asset balance changes of addresses based by asset data hash. + /// Updated by `_dispatchTransferFrom()`. + mapping(bytes32 => mapping(address => int256)) public rawAssetBalanceChanges; + + // solhint-disable no-empty-blocks + constructor () + public + Exchange(1337) + {} + + /// @dev Overriden to only log arguments and track raw asset balances. + function _dispatchTransferFrom( + bytes32 orderHash, + bytes memory assetData, + address from, + address to, + uint256 amount + ) + internal + { + emit DispatchTransferFromCalled( + orderHash, + assetData, + from, + to, + amount + ); + + mapping(address => int256) storage balances = + rawAssetBalanceChanges[keccak256(assetData)]; + balances[from] -= int256(amount); + balances[to] += int256(amount); + } + + /// @dev Overriden to simplify signature validation. + /// Unfortunately, this is `view`, so it can't log arguments. + function _isValidOrderWithHashSignature( + Order memory order, + bytes32 orderHash, + address signerAddress, + bytes memory signature + ) + internal + view + returns (bool isValid) + { + // '0x01' in the first byte is valid. + return signature.length == 2 && signature[0] == 0x01; + } +} diff --git a/contracts/exchange/contracts/test/TestExchangeIsolatedFillOrder.sol b/contracts/exchange/contracts/test/TestExchangeIsolatedFillOrder.sol deleted file mode 100644 index 5cf4b0d472..0000000000 --- a/contracts/exchange/contracts/test/TestExchangeIsolatedFillOrder.sol +++ /dev/null @@ -1,112 +0,0 @@ -/* - - Copyright 2019 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.5.5; -pragma experimental ABIEncoderV2; - -import "../src/Exchange.sol"; - - -/// @dev A version of the Exchange contract where we override dependent -/// functions called by `_fillOrder()` to test what goes on inside of it. -contract TestExchangeIsolatedFillOrder is - Exchange -{ - // solhint-disable no-unused-vars - event UpdateFilledStateCalled( - Order order, - address takerAddress, - bytes32 orderHash, - uint256 orderTakerAssetAmount, - FillResults fillResults - ); - - event SettleOrderCalled( - bytes32 orderHash, - LibOrder.Order order, - address takerAddress, - LibFillResults.FillResults fillResults - ); - - // solhint-disable no-empty-blocks - constructor () - public - Exchange(1337) - {} - - /// @dev Allow setting of `filled` state for an order hash. - function setOrderTakerAssetFilledAmount( - bytes32 orderHash, - uint256 takerAssetFilledAmount - ) - external - { - filled[orderHash] = takerAssetFilledAmount; - } - - /// @dev Override that just logs arguments. - function _updateFilledState( - Order memory order, - address takerAddress, - bytes32 orderHash, - uint256 orderTakerAssetFilledAmount, - FillResults memory fillResults - ) - internal - { - emit UpdateFilledStateCalled( - order, - takerAddress, - orderHash, - orderTakerAssetFilledAmount, - fillResults - ); - } - - /// @dev Override that just logs arguments. - function _settleOrder( - bytes32 orderHash, - LibOrder.Order memory order, - address takerAddress, - LibFillResults.FillResults memory fillResults - ) - internal - { - emit SettleOrderCalled( - orderHash, - order, - takerAddress, - fillResults - ); - } - - /// @dev Override to log arguments and pass all empty signatures. - function _isValidOrderWithHashSignature( - Order memory order, - bytes32 orderHash, - address signerAddress, - bytes memory signature - ) - internal - view - returns (bool isValid) - { - // Pass if the signature is empty. - return signature.length == 0; - } -} diff --git a/contracts/exchange/package.json b/contracts/exchange/package.json index 500ecfb9c9..7e40e2687e 100644 --- a/contracts/exchange/package.json +++ b/contracts/exchange/package.json @@ -34,7 +34,7 @@ "lint-contracts": "solhint -c ../.solhint.json contracts/**/**/**/**/*.sol" }, "config": { - "abis": "./generated-artifacts/@(Exchange|ExchangeWrapper|IAssetProxyDispatcher|IEIP1271Wallet|IExchange|IExchangeCore|IMatchOrders|ISignatureValidator|ITransactions|IWallet|IWrapperFunctions|ReentrantERC20Token|TestAssetProxyDispatcher|TestExchangeInternals|TestExchangeIsolatedFillOrder|TestExchangeMath|TestLibExchangeRichErrorDecoder|TestSignatureValidator|TestValidatorWallet|Whitelist).json", + "abis": "./generated-artifacts/@(Exchange|ExchangeWrapper|IAssetProxyDispatcher|IEIP1271Wallet|IExchange|IExchangeCore|IMatchOrders|ISignatureValidator|ITransactions|IWallet|IWrapperFunctions|ReentrantERC20Token|TestAssetProxyDispatcher|TestExchangeInternals|TestExchangeIsolated|TestExchangeMath|TestLibExchangeRichErrorDecoder|TestSignatureValidator|TestValidatorWallet|Whitelist).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/exchange/src/artifacts.ts b/contracts/exchange/src/artifacts.ts index 32ff463527..2d9a08e626 100644 --- a/contracts/exchange/src/artifacts.ts +++ b/contracts/exchange/src/artifacts.ts @@ -19,7 +19,7 @@ import * as IWrapperFunctions from '../generated-artifacts/IWrapperFunctions.jso import * as ReentrantERC20Token from '../generated-artifacts/ReentrantERC20Token.json'; import * as TestAssetProxyDispatcher from '../generated-artifacts/TestAssetProxyDispatcher.json'; import * as TestExchangeInternals from '../generated-artifacts/TestExchangeInternals.json'; -import * as TestExchangeIsolatedFillOrder from '../generated-artifacts/TestExchangeIsolatedFillOrder.json'; +import * as TestExchangeIsolated from '../generated-artifacts/TestExchangeIsolated.json'; import * as TestExchangeMath from '../generated-artifacts/TestExchangeMath.json'; import * as TestLibExchangeRichErrorDecoder from '../generated-artifacts/TestLibExchangeRichErrorDecoder.json'; import * as TestSignatureValidator from '../generated-artifacts/TestSignatureValidator.json'; @@ -40,8 +40,8 @@ export const artifacts = { IWrapperFunctions: IWrapperFunctions as ContractArtifact, ReentrantERC20Token: ReentrantERC20Token as ContractArtifact, TestAssetProxyDispatcher: TestAssetProxyDispatcher as ContractArtifact, - TestExchangeIsolatedFillOrder: TestExchangeIsolatedFillOrder as ContractArtifact, TestExchangeInternals: TestExchangeInternals as ContractArtifact, + TestExchangeIsolated: TestExchangeIsolated as ContractArtifact, TestExchangeMath: TestExchangeMath as ContractArtifact, TestLibExchangeRichErrorDecoder: TestLibExchangeRichErrorDecoder as ContractArtifact, TestSignatureValidator: TestSignatureValidator as ContractArtifact, diff --git a/contracts/exchange/src/wrappers.ts b/contracts/exchange/src/wrappers.ts index cac8d768e0..cb5b39e00e 100644 --- a/contracts/exchange/src/wrappers.ts +++ b/contracts/exchange/src/wrappers.ts @@ -17,7 +17,7 @@ export * from '../generated-wrappers/i_wrapper_functions'; export * from '../generated-wrappers/reentrant_erc20_token'; export * from '../generated-wrappers/test_asset_proxy_dispatcher'; export * from '../generated-wrappers/test_exchange_internals'; -export * from '../generated-wrappers/test_exchange_isolated_fill_order'; +export * from '../generated-wrappers/test_exchange_isolated'; export * from '../generated-wrappers/test_exchange_math'; export * from '../generated-wrappers/test_lib_exchange_rich_error_decoder'; export * from '../generated-wrappers/test_signature_validator'; diff --git a/contracts/exchange/test/isolated_fill_order.ts b/contracts/exchange/test/isolated_fill_order.ts index b81684d9ba..1bddd445e8 100644 --- a/contracts/exchange/test/isolated_fill_order.ts +++ b/contracts/exchange/test/isolated_fill_order.ts @@ -1,49 +1,119 @@ import { + addressUtils, blockchainTests, constants, expect, FillResults, - txDefaults, + LogDecoder, } from '@0x/contracts-test-utils'; -import { Order, SignedOrder } from '@0x/types'; +import { Order } from '@0x/types'; import { BigNumber } from '@0x/utils'; +import { LogWithDecodedArgs } from 'ethereum-types'; +import * as _ from 'lodash'; -import { artifacts, TestExchangeInternalsContract, TestExchangeMathContract } from '../src'; - -const emptyOrder: Order = { - senderAddress: constants.NULL_ADDRESS, - makerAddress: constants.NULL_ADDRESS, - takerAddress: constants.NULL_ADDRESS, - makerFee: new BigNumber(0), - takerFee: new BigNumber(0), - makerAssetAmount: new BigNumber(0), - takerAssetAmount: new BigNumber(0), - makerAssetData: '0x', - takerAssetData: '0x', - makerFeeAssetData: '0x', - takerFeeAssetData: '0x', - salt: new BigNumber(0), - feeRecipientAddress: constants.NULL_ADDRESS, - expirationTimeSeconds: new BigNumber(0), - domain: { - verifyingContractAddress: constants.NULL_ADDRESS, - chainId: 0, // To be filled in later. - }, -}; - -const emptySignedOrder: SignedOrder = { - ...emptyOrder, - signature: '', -}; - -blockchainTests.only('Isolated fillOrder()', () => { - it('foo', async () => { - expect('foo').to.equal('foo'); +import { + artifacts, + TestExchangeIsolatedContract, + TestExchangeIsolatedDispatchTransferFromCalledEventArgs as DispatchTransferFromCallArgs, + TestExchangeIsolatedFillEventArgs as FillEventArgs, +} from '../src'; + +blockchainTests.resets.only('Isolated fillOrder() tests', env => { + const GOOD_SIGNATURE = '0x0101'; + const BAD_SIGNATURE = '0x0001'; + const TOMORROW = Math.floor(_.now() / 1000) + 60 * 60 * 24; + const DEFAULT_ORDER: Order = { + senderAddress: constants.NULL_ADDRESS, + makerAddress: addressUtils.generatePseudoRandomAddress(), + takerAddress: constants.NULL_ADDRESS, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + makerAssetAmount: constants.ZERO_AMOUNT, + takerAssetAmount: constants.ZERO_AMOUNT, + makerAssetData: constants.NULL_BYTES, + takerAssetData: constants.NULL_BYTES, + makerFeeAssetData: constants.NULL_BYTES, + takerFeeAssetData: constants.NULL_BYTES, + salt: constants.ZERO_AMOUNT, + feeRecipientAddress: constants.NULL_ADDRESS, + expirationTimeSeconds: toBN(TOMORROW), + domain: { + verifyingContractAddress: constants.NULL_ADDRESS, + chainId: 1337, + }, + }; + let takerAddress: string; + let testExchange: TestExchangeIsolatedContract; + let logDecoder: LogDecoder; + let nextSaltValue = 1; + + before(async () => { + [ takerAddress ] = await env.getAccountAddressesAsync(); + testExchange = await TestExchangeIsolatedContract.deployFrom0xArtifactAsync( + artifacts.TestExchangeIsolated, + env.provider, + env.txDefaults, + ); + logDecoder = new LogDecoder(env.web3Wrapper, artifacts); }); - blockchainTests.resets('inner', () => { - it('bar', async () => { - return expect(Promise.resolve(true)).to.eventually.be.ok; + + interface IsolatedFillOrderAsyncResults { + fillResults: FillResults; + fillEventArgs: FillEventArgs; + transferFromCallArgs: DispatchTransferFromCallArgs[]; + } + + async function isolatedFillOrderAsync( + order: Order, + takerAssetFillAmount: BigNumber | number, + signature: string = GOOD_SIGNATURE, + ): Promise { + const _takerAssetFillAmount = toBN(takerAssetFillAmount); + // Call to get the return value. + const fillResults = await testExchange.fillOrder.callAsync( + order, + _takerAssetFillAmount, + signature, + ); + // Transact to execute it. + const receipt = await logDecoder.getTxWithDecodedLogsAsync( + await testExchange.fillOrder.sendTransactionAsync( + order, + _takerAssetFillAmount, + signature, + ), + ); + const fillEventArgs = (receipt.logs[0] as LogWithDecodedArgs).args; + const transferFromCallArgs = + (receipt.logs.slice(1) as Array>).map( + log => log.args, + ); + return { + fillResults, + fillEventArgs, + transferFromCallArgs, + }; + } + + function createOrder(details: Partial = {}): Order { + return _.assign( + {}, + DEFAULT_ORDER, + { salt: toBN(nextSaltValue++) }, + details, + ); + } + + it('works', async () => { + const order = createOrder({ + makerAssetAmount: toBN(1), + takerAssetAmount: toBN(1), }); + const results = await isolatedFillOrderAsync(order, 1); + console.log(results); }); - require('./nested'); }); + +function toBN(num: BigNumber | string | number): BigNumber { + return new BigNumber(num); +} diff --git a/contracts/exchange/tsconfig.json b/contracts/exchange/tsconfig.json index 19b7903d4d..32e6499b2f 100644 --- a/contracts/exchange/tsconfig.json +++ b/contracts/exchange/tsconfig.json @@ -17,7 +17,7 @@ "generated-artifacts/ReentrantERC20Token.json", "generated-artifacts/TestAssetProxyDispatcher.json", "generated-artifacts/TestExchangeInternals.json", - "generated-artifacts/TestExchangeIsolatedFillOrder.json", + "generated-artifacts/TestExchangeIsolated.json", "generated-artifacts/TestExchangeMath.json", "generated-artifacts/TestLibExchangeRichErrorDecoder.json", "generated-artifacts/TestSignatureValidator.json", From fc5963fa3d588318d93ada0adec2b321dfdf216f Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 30 Jul 2019 13:59:41 -0400 Subject: [PATCH 03/48] `@0x/contracts-test-utils`: Add `hexRandom()` to `hex_utils.ts`. --- contracts/test-utils/src/hex_utils.ts | 7 +++++++ contracts/test-utils/src/index.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/contracts/test-utils/src/hex_utils.ts b/contracts/test-utils/src/hex_utils.ts index 95c2435b1f..d1a70442c2 100644 --- a/contracts/test-utils/src/hex_utils.ts +++ b/contracts/test-utils/src/hex_utils.ts @@ -1,3 +1,4 @@ +import * as crypto from 'crypto'; import * as ethUtil from 'ethereumjs-util'; /** @@ -6,3 +7,9 @@ import * as ethUtil from 'ethereumjs-util'; export function hexConcat(...args: Array): string { return ethUtil.bufferToHex(Buffer.concat(args.map(h => ethUtil.toBuffer(h)))); } +/** + * Generate a random hex string. + */ +export function hexRandom(size: number = 32): string { + return ethUtil.bufferToHex(crypto.randomBytes(size)); +} diff --git a/contracts/test-utils/src/index.ts b/contracts/test-utils/src/index.ts index 3abd3f6070..20b0aa47bc 100644 --- a/contracts/test-utils/src/index.ts +++ b/contracts/test-utils/src/index.ts @@ -26,7 +26,7 @@ export { OrderFactory } from './order_factory'; export { bytes32Values, testCombinatoriallyWithReferenceFuncAsync, uint256Values } from './combinatorial_utils'; export { TransactionFactory } from './transaction_factory'; export { testWithReferenceFuncAsync } from './test_with_reference'; -export { hexConcat } from './hex_utils'; +export { hexConcat, hexRandom } from './hex_utils'; export { BatchMatchedFillResults, BatchMatchOrder, From 7fb87d40392d53b432d2a2048d042e6a9748cdf1 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 30 Jul 2019 14:00:36 -0400 Subject: [PATCH 04/48] `@0x/contracts-exchange`: Update signature and rich error decoder tests to use `hexRandom()` and `blockchainTests()`. --- .../test/lib_exchange_rich_error_decoder.ts | 41 +++++----------- .../exchange/test/signature_validator.ts | 48 ++++++------------- 2 files changed, 25 insertions(+), 64 deletions(-) diff --git a/contracts/exchange/test/lib_exchange_rich_error_decoder.ts b/contracts/exchange/test/lib_exchange_rich_error_decoder.ts index 09aa40501a..7dab8e6742 100644 --- a/contracts/exchange/test/lib_exchange_rich_error_decoder.ts +++ b/contracts/exchange/test/lib_exchange_rich_error_decoder.ts @@ -1,37 +1,23 @@ import { addressUtils, - chaiSetup, + blockchainTests, + expect, + hexRandom, OrderStatus, orderUtils, - provider, - txDefaults, - web3Wrapper, } from '@0x/contracts-test-utils'; -import { BlockchainLifecycle } from '@0x/dev-utils'; import { ExchangeRevertErrors, generatePseudoRandomSalt } from '@0x/order-utils'; import { RevertError } from '@0x/utils'; -import * as chai from 'chai'; -import * as crypto from 'crypto'; import * as _ from 'lodash'; import { artifacts, TestLibExchangeRichErrorDecoderContract } from '../src'; -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -describe('LibExchangeRichErrorDecoder', () => { +blockchainTests.resets('LibExchangeRichErrorDecoder', ({ provider, txDefaults }) => { const SIGNATURE_LENGTH = 66; const ASSET_DATA_LENGTH = 36; const ERROR_DATA_LENGTH = 100; let decoder: TestLibExchangeRichErrorDecoderContract; - before(async () => { - await blockchainLifecycle.startAsync(); - }); - after(async () => { - await blockchainLifecycle.revertAsync(); - }); before(async () => { decoder = await TestLibExchangeRichErrorDecoderContract.deployFrom0xArtifactAsync( artifacts.TestLibExchangeRichErrorDecoder, @@ -40,11 +26,6 @@ describe('LibExchangeRichErrorDecoder', () => { ); }); - function generateRandomBytes(length: number): string { - const bytes = crypto.randomBytes(length).toString('hex'); - return `0x${bytes}`; - } - function createDecodeTest(revertType: new (...args: any[]) => RevertError, parameters: any[]): void { const revert = new revertType(...parameters); const encoded = revert.encode(); @@ -76,8 +57,8 @@ describe('LibExchangeRichErrorDecoder', () => { const orderHash = orderUtils.generatePseudoRandomOrderHash(); const signer = addressUtils.generatePseudoRandomAddress(); const validator = addressUtils.generatePseudoRandomAddress(); - const signature = generateRandomBytes(SIGNATURE_LENGTH); - const errorData = generateRandomBytes(ERROR_DATA_LENGTH); + const signature = hexRandom(SIGNATURE_LENGTH); + const errorData = hexRandom(ERROR_DATA_LENGTH); createDecodeTest(ExchangeRevertErrors.SignatureError, [errorCode, orderHash, signer, signature]); createDecodeTest(ExchangeRevertErrors.SignatureValidatorNotApprovedError, [signer, validator]); createDecodeTest(ExchangeRevertErrors.SignatureValidatorError, [ @@ -125,14 +106,14 @@ describe('LibExchangeRichErrorDecoder', () => { (() => { const errorCode = ExchangeRevertErrors.AssetProxyDispatchErrorCode.UnknownAssetProxy; const orderHash = orderUtils.generatePseudoRandomOrderHash(); - const assetData = generateRandomBytes(ASSET_DATA_LENGTH); + const assetData = hexRandom(ASSET_DATA_LENGTH); createDecodeTest(ExchangeRevertErrors.AssetProxyDispatchError, [errorCode, orderHash, assetData]); })(); (() => { const orderHash = orderUtils.generatePseudoRandomOrderHash(); - const assetData = generateRandomBytes(ASSET_DATA_LENGTH); - const errorData = generateRandomBytes(ERROR_DATA_LENGTH); + const assetData = hexRandom(ASSET_DATA_LENGTH); + const errorData = hexRandom(ERROR_DATA_LENGTH); createDecodeTest(ExchangeRevertErrors.AssetProxyTransferError, [orderHash, assetData, errorData]); })(); @@ -151,13 +132,13 @@ describe('LibExchangeRichErrorDecoder', () => { (() => { const transactionHash = orderUtils.generatePseudoRandomOrderHash(); const signer = addressUtils.generatePseudoRandomAddress(); - const signature = generateRandomBytes(SIGNATURE_LENGTH); + const signature = hexRandom(SIGNATURE_LENGTH); createDecodeTest(ExchangeRevertErrors.TransactionSignatureError, [transactionHash, signer, signature]); })(); (() => { const transactionHash = orderUtils.generatePseudoRandomOrderHash(); - const errorData = generateRandomBytes(ERROR_DATA_LENGTH); + const errorData = hexRandom(ERROR_DATA_LENGTH); createDecodeTest(ExchangeRevertErrors.TransactionExecutionError, [transactionHash, errorData]); })(); diff --git a/contracts/exchange/test/signature_validator.ts b/contracts/exchange/test/signature_validator.ts index ba5b84822d..80c2140569 100644 --- a/contracts/exchange/test/signature_validator.ts +++ b/contracts/exchange/test/signature_validator.ts @@ -1,15 +1,14 @@ import { addressUtils, - chaiSetup, + blockchainTests, constants, + expect, hexConcat, + hexRandom, LogDecoder, OrderFactory, orderUtils, - provider, TransactionFactory, - txDefaults, - web3Wrapper, } from '@0x/contracts-test-utils'; import { BlockchainLifecycle } from '@0x/dev-utils'; import { @@ -20,9 +19,7 @@ import { transactionHashUtils, } from '@0x/order-utils'; import { SignatureType, SignedOrder, SignedZeroExTransaction } from '@0x/types'; -import { BigNumber, providerUtils, StringRevertError } from '@0x/utils'; -import * as chai from 'chai'; -import * as crypto from 'crypto'; +import { BigNumber, StringRevertError } from '@0x/utils'; import { LogWithDecodedArgs } from 'ethereum-types'; import ethUtil = require('ethereumjs-util'); @@ -35,11 +32,8 @@ import { import { ValidatorWalletAction, ValidatorWalletDataType } from './utils'; -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); // tslint:disable:no-unnecessary-type-assertion -describe('MixinSignatureValidator', () => { +blockchainTests.resets('MixinSignatureValidator', env => { let chainId: number; let signatureValidator: TestSignatureValidatorContract; let validatorWallet: TestValidatorWalletContract; @@ -49,26 +43,20 @@ describe('MixinSignatureValidator', () => { let notSignerAddress: string; before(async () => { - await blockchainLifecycle.startAsync(); - }); - after(async () => { - await blockchainLifecycle.revertAsync(); - }); - before(async () => { - chainId = await providerUtils.getChainIdAsync(provider); - const accounts = await web3Wrapper.getAvailableAddressesAsync(); + chainId = await env.getChainIdAsync(); + const accounts = await env.getAccountAddressesAsync(); signerAddress = accounts[0]; notSignerAddress = accounts[1]; signatureValidator = await TestSignatureValidatorContract.deployFrom0xArtifactAsync( artifacts.TestSignatureValidator, - provider, - txDefaults, + env.provider, + env.txDefaults, new BigNumber(chainId), ); validatorWallet = await TestValidatorWalletContract.deployFrom0xArtifactAsync( artifacts.TestValidatorWallet, - provider, - txDefaults, + env.provider, + env.txDefaults, signatureValidator.address, ); validatorWalletRevertReason = await validatorWallet.REVERT_REASON.callAsync(); @@ -87,16 +75,8 @@ describe('MixinSignatureValidator', () => { signerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(signerAddress)]; }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - const SIGNATURE_LENGTH = 65; - const generateRandomBytes = (count: number): string => ethUtil.bufferToHex(crypto.randomBytes(count)); - const generateRandomSignature = (): string => generateRandomBytes(SIGNATURE_LENGTH); + const generateRandomSignature = (): string => hexRandom(SIGNATURE_LENGTH); const hashBytes = (bytesHex: string): string => ethUtil.bufferToHex(ethUtil.sha3(ethUtil.toBuffer(bytesHex))); const signDataHex = (dataHex: string, privateKey: Buffer): string => { const ecSignature = ethUtil.ecsign(ethUtil.toBuffer(dataHex), signerPrivateKey); @@ -609,7 +589,7 @@ describe('MixinSignatureValidator', () => { // We don't actually do anything with the transaction so we can just // fill it with random data. signedTransaction = await transactionFactory.newSignedTransactionAsync({ - data: generateRandomBytes(TRANSACTION_DATA_LENGTH), + data: hexRandom(TRANSACTION_DATA_LENGTH), }); }); @@ -807,7 +787,7 @@ describe('MixinSignatureValidator', () => { let signatureValidatorLogDecoder: LogDecoder; before(async () => { - signatureValidatorLogDecoder = new LogDecoder(web3Wrapper, artifacts); + signatureValidatorLogDecoder = new LogDecoder(env.web3Wrapper, artifacts); }); it('should emit a SignatureValidatorApprovalSet with correct args when a validator is approved', async () => { From d974ee169aef7afda058083e360f4bb4aec5eec6 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 30 Jul 2019 14:03:58 -0400 Subject: [PATCH 05/48] `@0x/contracts-exchange`: Rename `TestExchangeIsolated.sol` -> `TestIsolatedExchange.sol`. `@0x/contracts-exchange`: Automatically track raw asset balances in `TestIsolatedExchange` contract. --- contracts/exchange/compiler.json | 2 +- ...eIsolated.sol => TestIsolatedExchange.sol} | 42 ++++++++++++++--- contracts/exchange/package.json | 2 +- contracts/exchange/src/artifacts.ts | 4 +- contracts/exchange/src/wrappers.ts | 2 +- .../exchange/test/isolated_fill_order.ts | 47 ++++++++++++++----- contracts/exchange/tsconfig.json | 2 +- 7 files changed, 77 insertions(+), 24 deletions(-) rename contracts/exchange/contracts/test/{TestExchangeIsolated.sol => TestIsolatedExchange.sol} (61%) diff --git a/contracts/exchange/compiler.json b/contracts/exchange/compiler.json index 7b036accec..be13b3e3b9 100644 --- a/contracts/exchange/compiler.json +++ b/contracts/exchange/compiler.json @@ -39,8 +39,8 @@ "test/ReentrantERC20Token.sol", "test/TestAssetProxyDispatcher.sol", "test/TestExchangeInternals.sol", - "test/TestExchangeIsolated.sol", "test/TestExchangeMath.sol", + "test/TestIsolatedExchange.sol", "test/TestLibExchangeRichErrorDecoder.sol", "test/TestSignatureValidator.sol", "test/TestValidatorWallet.sol" diff --git a/contracts/exchange/contracts/test/TestExchangeIsolated.sol b/contracts/exchange/contracts/test/TestIsolatedExchange.sol similarity index 61% rename from contracts/exchange/contracts/test/TestExchangeIsolated.sol rename to contracts/exchange/contracts/test/TestIsolatedExchange.sol index 57896b7235..7a6a4ea1f3 100644 --- a/contracts/exchange/contracts/test/TestExchangeIsolated.sol +++ b/contracts/exchange/contracts/test/TestIsolatedExchange.sol @@ -24,7 +24,7 @@ import "../src/Exchange.sol"; /// @dev A version of the Exchange contract with simplified signature validation /// and a `_dispatchTransferFrom()` that only logs arguments. -contract TestExchangeIsolated is +contract TestIsolatedExchange is Exchange { // solhint-disable no-unused-vars @@ -36,9 +36,10 @@ contract TestExchangeIsolated is uint256 amount ); - /// @dev Raw asset balance changes of addresses based by asset data hash. + /// @dev Raw asset balances of addresses based on asset data hash. + /// These start at 0 and are allowed to be negative. /// Updated by `_dispatchTransferFrom()`. - mapping(bytes32 => mapping(address => int256)) public rawAssetBalanceChanges; + mapping(bytes32 => mapping(address => int256)) public rawAssetBalances; // solhint-disable no-empty-blocks constructor () @@ -46,6 +47,23 @@ contract TestExchangeIsolated is Exchange(1337) {} + /// @dev Convenience function to get the `rawAssetBalances` for + /// multiple addresses. + function getMultipleRawAssetBalanceChanges( + bytes calldata assetData, + address[] calldata addresses + ) + external + returns (int256[] memory balances) + { + balances = new int256[](addresses.length); + mapping(address => int256) storage assetBalances = + rawAssetBalances[keccak256(assetData)]; + for (uint i = 0; i < addresses.length; i++) { + balances[i] = assetBalances[addresses[i]]; + } + } + /// @dev Overriden to only log arguments and track raw asset balances. function _dispatchTransferFrom( bytes32 orderHash, @@ -64,10 +82,10 @@ contract TestExchangeIsolated is amount ); - mapping(address => int256) storage balances = - rawAssetBalanceChanges[keccak256(assetData)]; - balances[from] -= int256(amount); - balances[to] += int256(amount); + mapping(address => int256) storage assetBalances = + rawAssetBalances[keccak256(assetData)]; + assetBalances[from] = _subAssetAmount(assetBalances[from], amount); + assetBalances[to] = _addAssetAmount(assetBalances[to], amount); } /// @dev Overriden to simplify signature validation. @@ -85,4 +103,14 @@ contract TestExchangeIsolated is // '0x01' in the first byte is valid. return signature.length == 2 && signature[0] == 0x01; } + + function _subAssetAmount(int256 a, uint256 b) private pure returns (int256 r) { + r = a - int256(b); + require(r <= a, "ASSET_AMOUNT_UNDERFLOW"); + } + + function _addAssetAmount(int256 a, uint256 b) private pure returns (int256 r) { + r = a + int256(b); + require(r >= a, "ASSET_AMOUNT_OVERFLOW"); + } } diff --git a/contracts/exchange/package.json b/contracts/exchange/package.json index 7e40e2687e..930c64852b 100644 --- a/contracts/exchange/package.json +++ b/contracts/exchange/package.json @@ -34,7 +34,7 @@ "lint-contracts": "solhint -c ../.solhint.json contracts/**/**/**/**/*.sol" }, "config": { - "abis": "./generated-artifacts/@(Exchange|ExchangeWrapper|IAssetProxyDispatcher|IEIP1271Wallet|IExchange|IExchangeCore|IMatchOrders|ISignatureValidator|ITransactions|IWallet|IWrapperFunctions|ReentrantERC20Token|TestAssetProxyDispatcher|TestExchangeInternals|TestExchangeIsolated|TestExchangeMath|TestLibExchangeRichErrorDecoder|TestSignatureValidator|TestValidatorWallet|Whitelist).json", + "abis": "./generated-artifacts/@(Exchange|ExchangeWrapper|IAssetProxyDispatcher|IEIP1271Wallet|IExchange|IExchangeCore|IMatchOrders|ISignatureValidator|ITransactions|IWallet|IWrapperFunctions|ReentrantERC20Token|TestAssetProxyDispatcher|TestExchangeInternals|TestExchangeMath|TestIsolatedExchange|TestLibExchangeRichErrorDecoder|TestSignatureValidator|TestValidatorWallet|Whitelist).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/exchange/src/artifacts.ts b/contracts/exchange/src/artifacts.ts index 2d9a08e626..169fb06c75 100644 --- a/contracts/exchange/src/artifacts.ts +++ b/contracts/exchange/src/artifacts.ts @@ -19,8 +19,8 @@ import * as IWrapperFunctions from '../generated-artifacts/IWrapperFunctions.jso import * as ReentrantERC20Token from '../generated-artifacts/ReentrantERC20Token.json'; import * as TestAssetProxyDispatcher from '../generated-artifacts/TestAssetProxyDispatcher.json'; import * as TestExchangeInternals from '../generated-artifacts/TestExchangeInternals.json'; -import * as TestExchangeIsolated from '../generated-artifacts/TestExchangeIsolated.json'; import * as TestExchangeMath from '../generated-artifacts/TestExchangeMath.json'; +import * as TestIsolatedExchange from '../generated-artifacts/TestIsolatedExchange.json'; import * as TestLibExchangeRichErrorDecoder from '../generated-artifacts/TestLibExchangeRichErrorDecoder.json'; import * as TestSignatureValidator from '../generated-artifacts/TestSignatureValidator.json'; import * as TestValidatorWallet from '../generated-artifacts/TestValidatorWallet.json'; @@ -41,8 +41,8 @@ export const artifacts = { ReentrantERC20Token: ReentrantERC20Token as ContractArtifact, TestAssetProxyDispatcher: TestAssetProxyDispatcher as ContractArtifact, TestExchangeInternals: TestExchangeInternals as ContractArtifact, - TestExchangeIsolated: TestExchangeIsolated as ContractArtifact, TestExchangeMath: TestExchangeMath as ContractArtifact, + TestIsolatedExchange: TestIsolatedExchange as ContractArtifact, TestLibExchangeRichErrorDecoder: TestLibExchangeRichErrorDecoder as ContractArtifact, TestSignatureValidator: TestSignatureValidator as ContractArtifact, TestValidatorWallet: TestValidatorWallet as ContractArtifact, diff --git a/contracts/exchange/src/wrappers.ts b/contracts/exchange/src/wrappers.ts index cb5b39e00e..dae2960063 100644 --- a/contracts/exchange/src/wrappers.ts +++ b/contracts/exchange/src/wrappers.ts @@ -17,8 +17,8 @@ export * from '../generated-wrappers/i_wrapper_functions'; export * from '../generated-wrappers/reentrant_erc20_token'; export * from '../generated-wrappers/test_asset_proxy_dispatcher'; export * from '../generated-wrappers/test_exchange_internals'; -export * from '../generated-wrappers/test_exchange_isolated'; export * from '../generated-wrappers/test_exchange_math'; +export * from '../generated-wrappers/test_isolated_exchange'; export * from '../generated-wrappers/test_lib_exchange_rich_error_decoder'; export * from '../generated-wrappers/test_signature_validator'; export * from '../generated-wrappers/test_validator_wallet'; diff --git a/contracts/exchange/test/isolated_fill_order.ts b/contracts/exchange/test/isolated_fill_order.ts index 1bddd445e8..ccf25534bc 100644 --- a/contracts/exchange/test/isolated_fill_order.ts +++ b/contracts/exchange/test/isolated_fill_order.ts @@ -13,9 +13,9 @@ import * as _ from 'lodash'; import { artifacts, - TestExchangeIsolatedContract, - TestExchangeIsolatedDispatchTransferFromCalledEventArgs as DispatchTransferFromCallArgs, - TestExchangeIsolatedFillEventArgs as FillEventArgs, + TestIsolatedExchangeContract, + TestIsolatedExchangeDispatchTransferFromCalledEventArgs as DispatchTransferFromCallArgs, + TestIsolatedExchangeFillEventArgs as FillEventArgs, } from '../src'; blockchainTests.resets.only('Isolated fillOrder() tests', env => { @@ -43,24 +43,29 @@ blockchainTests.resets.only('Isolated fillOrder() tests', env => { }, }; let takerAddress: string; - let testExchange: TestExchangeIsolatedContract; + let testExchange: TestIsolatedExchangeContract; let logDecoder: LogDecoder; let nextSaltValue = 1; before(async () => { [ takerAddress ] = await env.getAccountAddressesAsync(); - testExchange = await TestExchangeIsolatedContract.deployFrom0xArtifactAsync( - artifacts.TestExchangeIsolated, + testExchange = await TestIsolatedExchangeContract.deployFrom0xArtifactAsync( + artifacts.TestIsolatedExchange, env.provider, env.txDefaults, ); logDecoder = new LogDecoder(env.web3Wrapper, artifacts); }); + interface IsolatedExchangeAssetBalances { + [assetData: string]: {[address: string]: BigNumber}; + } + interface IsolatedFillOrderAsyncResults { fillResults: FillResults; fillEventArgs: FillEventArgs; - transferFromCallArgs: DispatchTransferFromCallArgs[]; + transferFromCalls: DispatchTransferFromCallArgs[]; + balances: IsolatedExchangeAssetBalances; } async function isolatedFillOrderAsync( @@ -83,15 +88,35 @@ blockchainTests.resets.only('Isolated fillOrder() tests', env => { signature, ), ); + // Parse logs. const fillEventArgs = (receipt.logs[0] as LogWithDecodedArgs).args; - const transferFromCallArgs = + const transferFromCalls = (receipt.logs.slice(1) as Array>).map( log => log.args, ); + // Extract addresses involved in transfers. + const addresses = _.uniq(_.flatten(transferFromCalls.map(c => [c.from, c.to]))); + // Extract assets involved in transfers. + const assets = _.uniq(transferFromCalls.map(c => c.assetData)); + // Query balances of addresses and assets involved in transfers. + const balances = await (async () => { + const result: IsolatedExchangeAssetBalances = {}; + for (const assetData of assets) { + result[assetData] = _.zipObject( + addresses, + await testExchange.getMultipleRawAssetBalanceChanges.callAsync( + assetData, + addresses, + ), + ); + } + return result; + })(); return { fillResults, fillEventArgs, - transferFromCallArgs, + transferFromCalls, + balances, }; } @@ -107,9 +132,9 @@ blockchainTests.resets.only('Isolated fillOrder() tests', env => { it('works', async () => { const order = createOrder({ makerAssetAmount: toBN(1), - takerAssetAmount: toBN(1), + takerAssetAmount: toBN(2), }); - const results = await isolatedFillOrderAsync(order, 1); + const results = await isolatedFillOrderAsync(order, 2); console.log(results); }); }); diff --git a/contracts/exchange/tsconfig.json b/contracts/exchange/tsconfig.json index 32e6499b2f..925c1a198d 100644 --- a/contracts/exchange/tsconfig.json +++ b/contracts/exchange/tsconfig.json @@ -17,8 +17,8 @@ "generated-artifacts/ReentrantERC20Token.json", "generated-artifacts/TestAssetProxyDispatcher.json", "generated-artifacts/TestExchangeInternals.json", - "generated-artifacts/TestExchangeIsolated.json", "generated-artifacts/TestExchangeMath.json", + "generated-artifacts/TestIsolatedExchange.json", "generated-artifacts/TestLibExchangeRichErrorDecoder.json", "generated-artifacts/TestSignatureValidator.json", "generated-artifacts/TestValidatorWallet.json", From 1e462f5cc07d71c3702aef459ce0e00245557c2f Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 30 Jul 2019 15:50:15 -0400 Subject: [PATCH 06/48] `@0x/contracts-test-utils`: Add `ADDRESS_LENGTH` constant. --- contracts/test-utils/src/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/test-utils/src/constants.ts b/contracts/test-utils/src/constants.ts index 3ec8ff987d..2b3308a2ee 100644 --- a/contracts/test-utils/src/constants.ts +++ b/contracts/test-utils/src/constants.ts @@ -56,6 +56,7 @@ export const constants = { takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18), }, WORD_LENGTH: 32, + ADDRESS_LENGTH: 20, ZERO_AMOUNT: new BigNumber(0), PERCENTAGE_DENOMINATOR: new BigNumber(10).pow(18), TIME_BUFFER: new BigNumber(1000), From 92d112083ee4369033ef96ccaa78efde9d7932c1 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 30 Jul 2019 16:55:37 -0400 Subject: [PATCH 07/48] `@0x/contracts-test-utils`: Add `filterLogs()` and `filterLogsToArguments()` helpers. --- contracts/test-utils/src/index.ts | 1 + contracts/test-utils/src/log_utils.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 contracts/test-utils/src/log_utils.ts diff --git a/contracts/test-utils/src/index.ts b/contracts/test-utils/src/index.ts index 20b0aa47bc..c626fbb77a 100644 --- a/contracts/test-utils/src/index.ts +++ b/contracts/test-utils/src/index.ts @@ -15,6 +15,7 @@ export { export { getLatestBlockTimestampAsync, increaseTimeAndMineBlockAsync } from './block_timestamp'; export { provider, txDefaults, web3Wrapper } from './web3_wrapper'; export { LogDecoder } from './log_decoder'; +export { filterLogs, filterLogsToArguments } from './log_utils'; export { signingUtils } from './signing_utils'; export { orderUtils } from './order_utils'; export { typeEncodingUtils } from './type_encoding_utils'; diff --git a/contracts/test-utils/src/log_utils.ts b/contracts/test-utils/src/log_utils.ts new file mode 100644 index 0000000000..ae95ace2f8 --- /dev/null +++ b/contracts/test-utils/src/log_utils.ts @@ -0,0 +1,17 @@ +import { LogEntry, LogWithDecodedArgs } from 'ethereum-types'; + +// tslint:disable no-unnecessary-type-assertion + +/** + * Filter logs by event name/type. + */ +export function filterLogs(logs: LogEntry[], event: string): Array> { + return (logs as Array>).filter(log => log.event === event); +} + +/** + * Filter logs by event name/type and convert to arguments. + */ +export function filterLogsToArguments(logs: LogEntry[], event: string): TEventArgs[] { + return filterLogs(logs, event).map(log => log.args); +} From 0851c5ac8efef71d22ec513dbdc227cbf31396ef Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 30 Jul 2019 16:57:12 -0400 Subject: [PATCH 08/48] `@0x/contracts-exchange`: Run prettier. --- .../exchange/test/lib_exchange_rich_error_decoder.ts | 9 +-------- contracts/exchange/test/signature_validator.ts | 1 - 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/contracts/exchange/test/lib_exchange_rich_error_decoder.ts b/contracts/exchange/test/lib_exchange_rich_error_decoder.ts index 7dab8e6742..8c3ce7e660 100644 --- a/contracts/exchange/test/lib_exchange_rich_error_decoder.ts +++ b/contracts/exchange/test/lib_exchange_rich_error_decoder.ts @@ -1,11 +1,4 @@ -import { - addressUtils, - blockchainTests, - expect, - hexRandom, - OrderStatus, - orderUtils, -} from '@0x/contracts-test-utils'; +import { addressUtils, blockchainTests, expect, hexRandom, OrderStatus, orderUtils } from '@0x/contracts-test-utils'; import { ExchangeRevertErrors, generatePseudoRandomSalt } from '@0x/order-utils'; import { RevertError } from '@0x/utils'; import * as _ from 'lodash'; diff --git a/contracts/exchange/test/signature_validator.ts b/contracts/exchange/test/signature_validator.ts index 80c2140569..3a19671a2d 100644 --- a/contracts/exchange/test/signature_validator.ts +++ b/contracts/exchange/test/signature_validator.ts @@ -10,7 +10,6 @@ import { orderUtils, TransactionFactory, } from '@0x/contracts-test-utils'; -import { BlockchainLifecycle } from '@0x/dev-utils'; import { assetDataUtils, ExchangeRevertErrors, From 1030c96eec7f951a53eef09ffa7bbdf61d72f7cd Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 30 Jul 2019 16:57:29 -0400 Subject: [PATCH 09/48] `@0x/contracts-exchange`: Create `IsolatedExchangeWrapper` class. --- .../contracts/test/TestIsolatedExchange.sol | 2 +- .../exchange/test/isolated_fill_order.ts | 117 ++-------------- .../test/utils/isolated_exchange_wrapper.ts | 129 ++++++++++++++++++ 3 files changed, 145 insertions(+), 103 deletions(-) create mode 100644 contracts/exchange/test/utils/isolated_exchange_wrapper.ts diff --git a/contracts/exchange/contracts/test/TestIsolatedExchange.sol b/contracts/exchange/contracts/test/TestIsolatedExchange.sol index 7a6a4ea1f3..bf5e8adff8 100644 --- a/contracts/exchange/contracts/test/TestIsolatedExchange.sol +++ b/contracts/exchange/contracts/test/TestIsolatedExchange.sol @@ -49,7 +49,7 @@ contract TestIsolatedExchange is /// @dev Convenience function to get the `rawAssetBalances` for /// multiple addresses. - function getMultipleRawAssetBalanceChanges( + function getRawAssetBalances( bytes calldata assetData, address[] calldata addresses ) diff --git a/contracts/exchange/test/isolated_fill_order.ts b/contracts/exchange/test/isolated_fill_order.ts index ccf25534bc..a775370f79 100644 --- a/contracts/exchange/test/isolated_fill_order.ts +++ b/contracts/exchange/test/isolated_fill_order.ts @@ -1,30 +1,14 @@ -import { - addressUtils, - blockchainTests, - constants, - expect, - FillResults, - LogDecoder, -} from '@0x/contracts-test-utils'; -import { Order } from '@0x/types'; +import { blockchainTests, constants, expect, hexRandom } from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; -import { LogWithDecodedArgs } from 'ethereum-types'; import * as _ from 'lodash'; -import { - artifacts, - TestIsolatedExchangeContract, - TestIsolatedExchangeDispatchTransferFromCalledEventArgs as DispatchTransferFromCallArgs, - TestIsolatedExchangeFillEventArgs as FillEventArgs, -} from '../src'; +import { IsolatedExchangeWrapper, Order } from './utils/isolated_exchange_wrapper'; blockchainTests.resets.only('Isolated fillOrder() tests', env => { - const GOOD_SIGNATURE = '0x0101'; - const BAD_SIGNATURE = '0x0001'; const TOMORROW = Math.floor(_.now() / 1000) + 60 * 60 * 24; const DEFAULT_ORDER: Order = { senderAddress: constants.NULL_ADDRESS, - makerAddress: addressUtils.generatePseudoRandomAddress(), + makerAddress: randomAddress(), takerAddress: constants.NULL_ADDRESS, makerFee: constants.ZERO_AMOUNT, takerFee: constants.ZERO_AMOUNT, @@ -37,96 +21,21 @@ blockchainTests.resets.only('Isolated fillOrder() tests', env => { salt: constants.ZERO_AMOUNT, feeRecipientAddress: constants.NULL_ADDRESS, expirationTimeSeconds: toBN(TOMORROW), - domain: { - verifyingContractAddress: constants.NULL_ADDRESS, - chainId: 1337, - }, }; let takerAddress: string; - let testExchange: TestIsolatedExchangeContract; - let logDecoder: LogDecoder; + let testExchange: IsolatedExchangeWrapper; let nextSaltValue = 1; before(async () => { - [ takerAddress ] = await env.getAccountAddressesAsync(); - testExchange = await TestIsolatedExchangeContract.deployFrom0xArtifactAsync( - artifacts.TestIsolatedExchange, - env.provider, - env.txDefaults, + [takerAddress] = await env.getAccountAddressesAsync(); + testExchange = await IsolatedExchangeWrapper.deployAsync( + env.web3Wrapper, + _.assign(env.txDefaults, { from: takerAddress }), ); - logDecoder = new LogDecoder(env.web3Wrapper, artifacts); }); - interface IsolatedExchangeAssetBalances { - [assetData: string]: {[address: string]: BigNumber}; - } - - interface IsolatedFillOrderAsyncResults { - fillResults: FillResults; - fillEventArgs: FillEventArgs; - transferFromCalls: DispatchTransferFromCallArgs[]; - balances: IsolatedExchangeAssetBalances; - } - - async function isolatedFillOrderAsync( - order: Order, - takerAssetFillAmount: BigNumber | number, - signature: string = GOOD_SIGNATURE, - ): Promise { - const _takerAssetFillAmount = toBN(takerAssetFillAmount); - // Call to get the return value. - const fillResults = await testExchange.fillOrder.callAsync( - order, - _takerAssetFillAmount, - signature, - ); - // Transact to execute it. - const receipt = await logDecoder.getTxWithDecodedLogsAsync( - await testExchange.fillOrder.sendTransactionAsync( - order, - _takerAssetFillAmount, - signature, - ), - ); - // Parse logs. - const fillEventArgs = (receipt.logs[0] as LogWithDecodedArgs).args; - const transferFromCalls = - (receipt.logs.slice(1) as Array>).map( - log => log.args, - ); - // Extract addresses involved in transfers. - const addresses = _.uniq(_.flatten(transferFromCalls.map(c => [c.from, c.to]))); - // Extract assets involved in transfers. - const assets = _.uniq(transferFromCalls.map(c => c.assetData)); - // Query balances of addresses and assets involved in transfers. - const balances = await (async () => { - const result: IsolatedExchangeAssetBalances = {}; - for (const assetData of assets) { - result[assetData] = _.zipObject( - addresses, - await testExchange.getMultipleRawAssetBalanceChanges.callAsync( - assetData, - addresses, - ), - ); - } - return result; - })(); - return { - fillResults, - fillEventArgs, - transferFromCalls, - balances, - }; - } - function createOrder(details: Partial = {}): Order { - return _.assign( - {}, - DEFAULT_ORDER, - { salt: toBN(nextSaltValue++) }, - details, - ); + return _.assign({}, DEFAULT_ORDER, { salt: toBN(nextSaltValue++) }, details); } it('works', async () => { @@ -134,11 +43,15 @@ blockchainTests.resets.only('Isolated fillOrder() tests', env => { makerAssetAmount: toBN(1), takerAssetAmount: toBN(2), }); - const results = await isolatedFillOrderAsync(order, 2); - console.log(results); + const results = await testExchange.fillOrderAsync(order, 2); + console.log(results, testExchange.getOrderHash(order)); }); }); function toBN(num: BigNumber | string | number): BigNumber { return new BigNumber(num); } + +function randomAddress(): string { + return hexRandom(constants.ADDRESS_LENGTH); +} diff --git a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts new file mode 100644 index 0000000000..c20002e693 --- /dev/null +++ b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts @@ -0,0 +1,129 @@ +import { FillResults, filterLogsToArguments, LogDecoder, txDefaults as testTxDefaults } from '@0x/contracts-test-utils'; +import { orderHashUtils } from '@0x/order-utils'; +import { OrderWithoutDomain, SignatureType } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import { TxData, Web3Wrapper } from '@0x/web3-wrapper'; +import * as _ from 'lodash'; + +import { + artifacts, + TestIsolatedExchangeContract, + TestIsolatedExchangeDispatchTransferFromCalledEventArgs as DispatchTransferFromCallArgs, + TestIsolatedExchangeFillEventArgs as FillEventArgs, +} from '../../src'; + +/** + * @dev Create a signature for the `TestIsolatedExchange` contract that will pass. + */ +export function createGoodSignature(type: SignatureType = SignatureType.EIP712): string { + return `0x01${Buffer.from([type]).toString('hex')}`; +} + +/** + * @dev Create a signature for the `TestIsolatedExchange` contract that will fail. + */ +export function createBadSignature(type: SignatureType = SignatureType.EIP712): string { + return `0x00${Buffer.from([type]).toString('hex')}`; +} + +export interface IsolatedAssetBalances { + [assetData: string]: { [address: string]: BigNumber }; +} + +export interface IsolatedFillOrderResults { + fillResults: FillResults; + fillEventArgs: FillEventArgs; + transferFromCalls: DispatchTransferFromCallArgs[]; + balances: IsolatedAssetBalances; +} + +export type Order = OrderWithoutDomain; + +export const DEFAULT_GOOD_SIGNATURE = createGoodSignature(); +export const DEFAULT_BAD_SIGNATURE = createBadSignature(); + +/** + * @dev Convenience wrapper for the `TestIsolatedExchange` contract. + */ +export class IsolatedExchangeWrapper { + public instance: TestIsolatedExchangeContract; + public logDecoder: LogDecoder; + + public static async deployAsync( + web3Wrapper: Web3Wrapper, + txDefaults: Partial = testTxDefaults, + ): Promise { + const provider = web3Wrapper.getProvider(); + const instance = await TestIsolatedExchangeContract.deployFrom0xArtifactAsync( + artifacts.TestIsolatedExchange, + provider, + txDefaults, + ); + return new IsolatedExchangeWrapper(web3Wrapper, instance); + } + + public static fromAddress( + address: string, + web3Wrapper: Web3Wrapper, + txDefaults: Partial = testTxDefaults, + ): IsolatedExchangeWrapper { + const provider = web3Wrapper.getProvider(); + const instance = new TestIsolatedExchangeContract(address, provider, txDefaults); + return new IsolatedExchangeWrapper(web3Wrapper, instance); + } + + public constructor(web3Wrapper: Web3Wrapper, instance: TestIsolatedExchangeContract) { + this.instance = instance; + this.logDecoder = new LogDecoder(web3Wrapper, artifacts); + } + + public async fillOrderAsync( + order: Order, + takerAssetFillAmount: BigNumber | number, + signature: string = DEFAULT_GOOD_SIGNATURE, + txOpts?: TxData, + ): Promise { + const _takerAssetFillAmount = new BigNumber(takerAssetFillAmount); + // Call to get the return value. + const fillResults = await this.instance.fillOrder.callAsync(order, _takerAssetFillAmount, signature, txOpts); + // Transact to execute it. + const receipt = await this.logDecoder.getTxWithDecodedLogsAsync( + await this.instance.fillOrder.sendTransactionAsync(order, _takerAssetFillAmount, signature, txOpts), + ); + // Parse logs. + const fillEventArgs = filterLogsToArguments(receipt.logs, 'Fill')[0]; + const transferFromCalls = filterLogsToArguments( + receipt.logs, + 'DispatchTransferFromCalled', + ); + // Extract addresses involved in transfers. + const addresses = _.uniq(_.flatten(transferFromCalls.map(c => [c.from, c.to]))); + // Extract assets involved in transfers. + const assets = _.uniq(transferFromCalls.map(c => c.assetData)); + // Query balances of addresses and assets involved in transfers. + const balances = await (async () => { + const result: IsolatedAssetBalances = {}; + for (const assetData of assets) { + result[assetData] = _.zipObject( + addresses, + await this.instance.getRawAssetBalances.callAsync(assetData, addresses), + ); + } + return result; + })(); + return { + fillResults, + fillEventArgs, + transferFromCalls, + balances, + }; + } + + public getOrderHash(order: Order): string { + const domain = { + verifyingContractAddress: this.instance.address, + chainId: 1337, + }; + return orderHashUtils.getOrderHashHex(_.assign(order, { domain })); + } +} From 039cc6e28b009b1e83edadb9b707e12e1fa81d22 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 30 Jul 2019 18:58:33 -0400 Subject: [PATCH 10/48] `@0x/contracts-exchange`: Allow fetching of balance of multiple assets in `TestIsolatedExchange` contract. `@0x/contracts-exchange`: Refactor `IsolatedExchangeWrapper` to be more extensible. --- .../contracts/test/TestIsolatedExchange.sol | 19 +-- .../exchange/test/isolated_fill_order.ts | 26 ++-- .../test/utils/isolated_exchange_wrapper.ts | 133 +++++++++++++----- 3 files changed, 123 insertions(+), 55 deletions(-) diff --git a/contracts/exchange/contracts/test/TestIsolatedExchange.sol b/contracts/exchange/contracts/test/TestIsolatedExchange.sol index bf5e8adff8..4d53567334 100644 --- a/contracts/exchange/contracts/test/TestIsolatedExchange.sol +++ b/contracts/exchange/contracts/test/TestIsolatedExchange.sol @@ -48,19 +48,22 @@ contract TestIsolatedExchange is {} /// @dev Convenience function to get the `rawAssetBalances` for - /// multiple addresses. + /// multiple assets and addresses. function getRawAssetBalances( - bytes calldata assetData, + bytes[] calldata assets, address[] calldata addresses ) external - returns (int256[] memory balances) + returns (int256[][] memory balances) { - balances = new int256[](addresses.length); - mapping(address => int256) storage assetBalances = - rawAssetBalances[keccak256(assetData)]; - for (uint i = 0; i < addresses.length; i++) { - balances[i] = assetBalances[addresses[i]]; + balances = new int256[][](assets.length); + for (uint assetIdx = 0; assetIdx < assets.length; ++assetIdx) { + balances[assetIdx] = new int256[](addresses.length); + mapping(address => int256) storage assetBalances = + rawAssetBalances[keccak256(assets[assetIdx])]; + for (uint addrIdx = 0; addrIdx < addresses.length; ++addrIdx) { + balances[assetIdx][addrIdx] = assetBalances[addresses[addrIdx]]; + } } } diff --git a/contracts/exchange/test/isolated_fill_order.ts b/contracts/exchange/test/isolated_fill_order.ts index a775370f79..b1f1cf53f7 100644 --- a/contracts/exchange/test/isolated_fill_order.ts +++ b/contracts/exchange/test/isolated_fill_order.ts @@ -4,8 +4,10 @@ import * as _ from 'lodash'; import { IsolatedExchangeWrapper, Order } from './utils/isolated_exchange_wrapper'; + blockchainTests.resets.only('Isolated fillOrder() tests', env => { const TOMORROW = Math.floor(_.now() / 1000) + 60 * 60 * 24; + const ERC20_ASSET_DATA_LENGTH = 24; const DEFAULT_ORDER: Order = { senderAddress: constants.NULL_ADDRESS, makerAddress: randomAddress(), @@ -14,13 +16,13 @@ blockchainTests.resets.only('Isolated fillOrder() tests', env => { takerFee: constants.ZERO_AMOUNT, makerAssetAmount: constants.ZERO_AMOUNT, takerAssetAmount: constants.ZERO_AMOUNT, - makerAssetData: constants.NULL_BYTES, - takerAssetData: constants.NULL_BYTES, - makerFeeAssetData: constants.NULL_BYTES, - takerFeeAssetData: constants.NULL_BYTES, salt: constants.ZERO_AMOUNT, feeRecipientAddress: constants.NULL_ADDRESS, expirationTimeSeconds: toBN(TOMORROW), + makerAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH), + takerAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH), + makerFeeAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH), + takerFeeAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH), }; let takerAddress: string; let testExchange: IsolatedExchangeWrapper; @@ -38,14 +40,16 @@ blockchainTests.resets.only('Isolated fillOrder() tests', env => { return _.assign({}, DEFAULT_ORDER, { salt: toBN(nextSaltValue++) }, details); } - it('works', async () => { - const order = createOrder({ - makerAssetAmount: toBN(1), - takerAssetAmount: toBN(2), + for (const i of _.times(100)) { + it('works', async () => { + const order = createOrder({ + makerAssetAmount: toBN(1), + takerAssetAmount: toBN(2), + }); + const results = await testExchange.fillOrderAsync(order, 2); + // console.log(results, testExchange.getOrderHash(order)); }); - const results = await testExchange.fillOrderAsync(order, 2); - console.log(results, testExchange.getOrderHash(order)); - }); + } }); function toBN(num: BigNumber | string | number): BigNumber { diff --git a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts index c20002e693..d6c6af1da2 100644 --- a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts +++ b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts @@ -3,6 +3,7 @@ import { orderHashUtils } from '@0x/order-utils'; import { OrderWithoutDomain, SignatureType } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { TxData, Web3Wrapper } from '@0x/web3-wrapper'; +import { LogEntry } from 'ethereum-types'; import * as _ from 'lodash'; import { @@ -26,15 +27,22 @@ export function createBadSignature(type: SignatureType = SignatureType.EIP712): return `0x00${Buffer.from([type]).toString('hex')}`; } -export interface IsolatedAssetBalances { +export interface AssetBalances { [assetData: string]: { [address: string]: BigNumber }; } -export interface IsolatedFillOrderResults { - fillResults: FillResults; - fillEventArgs: FillEventArgs; +export interface IsolatedExchangeEvents { + fillEvents: FillEventArgs[]; transferFromCalls: DispatchTransferFromCallArgs[]; - balances: IsolatedAssetBalances; +} + +export interface EventsAndBalances { + events: IsolatedExchangeEvents; + balances: AssetBalances; +} + +export interface IsolatedFillOrderResults extends EventsAndBalances { + fillResults: FillResults; } export type Order = OrderWithoutDomain; @@ -42,10 +50,20 @@ export type Order = OrderWithoutDomain; export const DEFAULT_GOOD_SIGNATURE = createGoodSignature(); export const DEFAULT_BAD_SIGNATURE = createBadSignature(); +interface CallAndSendResult extends EventsAndBalances { + result: TResult; +} + +interface TransactionContractFunction { + callAsync: (...args: any[]) => Promise; + sendTransactionAsync: (...args: any[]) => Promise; +} + /** * @dev Convenience wrapper for the `TestIsolatedExchange` contract. */ export class IsolatedExchangeWrapper { + public static readonly CHAIN_ID = 1337; public instance: TestIsolatedExchangeContract; public logDecoder: LogDecoder; @@ -84,46 +102,89 @@ export class IsolatedExchangeWrapper { txOpts?: TxData, ): Promise { const _takerAssetFillAmount = new BigNumber(takerAssetFillAmount); - // Call to get the return value. - const fillResults = await this.instance.fillOrder.callAsync(order, _takerAssetFillAmount, signature, txOpts); - // Transact to execute it. - const receipt = await this.logDecoder.getTxWithDecodedLogsAsync( - await this.instance.fillOrder.sendTransactionAsync(order, _takerAssetFillAmount, signature, txOpts), - ); - // Parse logs. - const fillEventArgs = filterLogsToArguments(receipt.logs, 'Fill')[0]; - const transferFromCalls = filterLogsToArguments( - receipt.logs, - 'DispatchTransferFromCalled', + const results = await this._callAndSendExchangeFunctionAsync( + this.instance.fillOrder, + order, + _takerAssetFillAmount, + signature, + txOpts, ); - // Extract addresses involved in transfers. - const addresses = _.uniq(_.flatten(transferFromCalls.map(c => [c.from, c.to]))); - // Extract assets involved in transfers. - const assets = _.uniq(transferFromCalls.map(c => c.assetData)); - // Query balances of addresses and assets involved in transfers. - const balances = await (async () => { - const result: IsolatedAssetBalances = {}; - for (const assetData of assets) { - result[assetData] = _.zipObject( - addresses, - await this.instance.getRawAssetBalances.callAsync(assetData, addresses), - ); - } - return result; - })(); return { - fillResults, - fillEventArgs, - transferFromCalls, - balances, + fillResults: results.result, + events: results.events, + balances: results.balances, }; } public getOrderHash(order: Order): string { const domain = { verifyingContractAddress: this.instance.address, - chainId: 1337, + chainId: IsolatedExchangeWrapper.CHAIN_ID, }; return orderHashUtils.getOrderHashHex(_.assign(order, { domain })); } + + public async getAssetBalanceAsync(assetData: string, address: string): Promise { + return this.getAssetBalancesAsync(assetData, [ address ]); + } + + public async getAssetBalancesAsync(assetData: string, addresses: string[]): Promise { + return (await this.instance.getRawAssetBalances.callAsync([ assetData ], addresses))[0]; + } + + public async getBalancesAsync(assets: string[], addresses: string[]): + Promise { + const callResults = await this.instance.getRawAssetBalances.callAsync(assets, addresses); + const result: AssetBalances = {}; + for (const i of _.times(assets.length)) { + const assetData = assets[i]; + result[assetData] = {}; + for (const j of _.times(addresses.length)) { + const address = addresses[j]; + result[assetData][address] = callResults[i][j]; + } + } + return result; + } + + protected async _getBalancesFromTransferFromCallsAsync( + calls: DispatchTransferFromCallArgs[], + ): Promise { + // Extract addresses involved in transfers. + const addresses = _.uniq(_.flatten(calls.map(c => [c.from, c.to]))); + // Extract assets involved in transfers. + const assets = _.uniq(calls.map(c => c.assetData)); + // Query balances of addresses and assets involved in transfers. + return this.getBalancesAsync(assets, addresses); + } + + protected async _callAndSendExchangeFunctionAsync( + instanceMethod: TransactionContractFunction, + // tslint:disable-next-line: trailing-comma + ...args: any[] + ): Promise> { + // Call to get the return value. + const result = await instanceMethod.callAsync.call(this.instance, ...args); + // Transact to execute it. + const receipt = await this.logDecoder.getTxWithDecodedLogsAsync( + await this.instance.fillOrder.sendTransactionAsync.call(this.instance, ...args), + ); + const events = extractEvents(receipt.logs); + const balances = await this._getBalancesFromTransferFromCallsAsync(events.transferFromCalls); + return { + result, + events, + balances, + }; + } +} + +function extractEvents(logs: LogEntry[]): IsolatedExchangeEvents { + return { + fillEvents: filterLogsToArguments(logs, 'Fill'), + transferFromCalls: filterLogsToArguments( + logs, + 'DispatchTransferFromCalled', + ), + }; } From e2bd80253b871a949d4c730082abb5c3b7fbf6ba Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 31 Jul 2019 10:24:00 -0400 Subject: [PATCH 11/48] `@0x/contracts-exchange`: More `TestIsolatedExchange` rework. --- .../contracts/test/TestIsolatedExchange.sol | 42 +----- .../exchange/test/isolated_fill_order.ts | 2 - .../test/utils/isolated_exchange_wrapper.ts | 141 ++++++++---------- 3 files changed, 61 insertions(+), 124 deletions(-) diff --git a/contracts/exchange/contracts/test/TestIsolatedExchange.sol b/contracts/exchange/contracts/test/TestIsolatedExchange.sol index 4d53567334..04c531a57b 100644 --- a/contracts/exchange/contracts/test/TestIsolatedExchange.sol +++ b/contracts/exchange/contracts/test/TestIsolatedExchange.sol @@ -36,38 +36,13 @@ contract TestIsolatedExchange is uint256 amount ); - /// @dev Raw asset balances of addresses based on asset data hash. - /// These start at 0 and are allowed to be negative. - /// Updated by `_dispatchTransferFrom()`. - mapping(bytes32 => mapping(address => int256)) public rawAssetBalances; - // solhint-disable no-empty-blocks constructor () public Exchange(1337) {} - /// @dev Convenience function to get the `rawAssetBalances` for - /// multiple assets and addresses. - function getRawAssetBalances( - bytes[] calldata assets, - address[] calldata addresses - ) - external - returns (int256[][] memory balances) - { - balances = new int256[][](assets.length); - for (uint assetIdx = 0; assetIdx < assets.length; ++assetIdx) { - balances[assetIdx] = new int256[](addresses.length); - mapping(address => int256) storage assetBalances = - rawAssetBalances[keccak256(assets[assetIdx])]; - for (uint addrIdx = 0; addrIdx < addresses.length; ++addrIdx) { - balances[assetIdx][addrIdx] = assetBalances[addresses[addrIdx]]; - } - } - } - - /// @dev Overriden to only log arguments and track raw asset balances. + /// @dev Overriden to only log arguments. function _dispatchTransferFrom( bytes32 orderHash, bytes memory assetData, @@ -84,11 +59,6 @@ contract TestIsolatedExchange is to, amount ); - - mapping(address => int256) storage assetBalances = - rawAssetBalances[keccak256(assetData)]; - assetBalances[from] = _subAssetAmount(assetBalances[from], amount); - assetBalances[to] = _addAssetAmount(assetBalances[to], amount); } /// @dev Overriden to simplify signature validation. @@ -106,14 +76,4 @@ contract TestIsolatedExchange is // '0x01' in the first byte is valid. return signature.length == 2 && signature[0] == 0x01; } - - function _subAssetAmount(int256 a, uint256 b) private pure returns (int256 r) { - r = a - int256(b); - require(r <= a, "ASSET_AMOUNT_UNDERFLOW"); - } - - function _addAssetAmount(int256 a, uint256 b) private pure returns (int256 r) { - r = a + int256(b); - require(r >= a, "ASSET_AMOUNT_OVERFLOW"); - } } diff --git a/contracts/exchange/test/isolated_fill_order.ts b/contracts/exchange/test/isolated_fill_order.ts index b1f1cf53f7..9c0079d2b5 100644 --- a/contracts/exchange/test/isolated_fill_order.ts +++ b/contracts/exchange/test/isolated_fill_order.ts @@ -4,7 +4,6 @@ import * as _ from 'lodash'; import { IsolatedExchangeWrapper, Order } from './utils/isolated_exchange_wrapper'; - blockchainTests.resets.only('Isolated fillOrder() tests', env => { const TOMORROW = Math.floor(_.now() / 1000) + 60 * 60 * 24; const ERC20_ASSET_DATA_LENGTH = 24; @@ -47,7 +46,6 @@ blockchainTests.resets.only('Isolated fillOrder() tests', env => { takerAssetAmount: toBN(2), }); const results = await testExchange.fillOrderAsync(order, 2); - // console.log(results, testExchange.getOrderHash(order)); }); } }); diff --git a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts index d6c6af1da2..1572dcb807 100644 --- a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts +++ b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts @@ -1,4 +1,4 @@ -import { FillResults, filterLogsToArguments, LogDecoder, txDefaults as testTxDefaults } from '@0x/contracts-test-utils'; +import { constants, FillResults, filterLogsToArguments, LogDecoder, txDefaults as testTxDefaults } from '@0x/contracts-test-utils'; import { orderHashUtils } from '@0x/order-utils'; import { OrderWithoutDomain, SignatureType } from '@0x/types'; import { BigNumber } from '@0x/utils'; @@ -13,20 +13,6 @@ import { TestIsolatedExchangeFillEventArgs as FillEventArgs, } from '../../src'; -/** - * @dev Create a signature for the `TestIsolatedExchange` contract that will pass. - */ -export function createGoodSignature(type: SignatureType = SignatureType.EIP712): string { - return `0x01${Buffer.from([type]).toString('hex')}`; -} - -/** - * @dev Create a signature for the `TestIsolatedExchange` contract that will fail. - */ -export function createBadSignature(type: SignatureType = SignatureType.EIP712): string { - return `0x00${Buffer.from([type]).toString('hex')}`; -} - export interface AssetBalances { [assetData: string]: { [address: string]: BigNumber }; } @@ -36,29 +22,11 @@ export interface IsolatedExchangeEvents { transferFromCalls: DispatchTransferFromCallArgs[]; } -export interface EventsAndBalances { - events: IsolatedExchangeEvents; - balances: AssetBalances; -} - -export interface IsolatedFillOrderResults extends EventsAndBalances { - fillResults: FillResults; -} - export type Order = OrderWithoutDomain; export const DEFAULT_GOOD_SIGNATURE = createGoodSignature(); export const DEFAULT_BAD_SIGNATURE = createBadSignature(); -interface CallAndSendResult extends EventsAndBalances { - result: TResult; -} - -interface TransactionContractFunction { - callAsync: (...args: any[]) => Promise; - sendTransactionAsync: (...args: any[]) => Promise; -} - /** * @dev Convenience wrapper for the `TestIsolatedExchange` contract. */ @@ -66,6 +34,8 @@ export class IsolatedExchangeWrapper { public static readonly CHAIN_ID = 1337; public instance: TestIsolatedExchangeContract; public logDecoder: LogDecoder; + public lastTxEvents: IsolatedExchangeEvents = createEmptyEvents(); + public lastTxBalanceChanges: AssetBalances = {}; public static async deployAsync( web3Wrapper: Web3Wrapper, @@ -100,20 +70,14 @@ export class IsolatedExchangeWrapper { takerAssetFillAmount: BigNumber | number, signature: string = DEFAULT_GOOD_SIGNATURE, txOpts?: TxData, - ): Promise { - const _takerAssetFillAmount = new BigNumber(takerAssetFillAmount); - const results = await this._callAndSendExchangeFunctionAsync( + ): Promise { + return this._callAndSendExchangeFunctionAsync( this.instance.fillOrder, order, - _takerAssetFillAmount, + new BigNumber(takerAssetFillAmount), signature, txOpts, ); - return { - fillResults: results.result, - events: results.events, - balances: results.balances, - }; } public getOrderHash(order: Order): string { @@ -124,61 +88,60 @@ export class IsolatedExchangeWrapper { return orderHashUtils.getOrderHashHex(_.assign(order, { domain })); } - public async getAssetBalanceAsync(assetData: string, address: string): Promise { - return this.getAssetBalancesAsync(assetData, [ address ]); - } - - public async getAssetBalancesAsync(assetData: string, addresses: string[]): Promise { - return (await this.instance.getRawAssetBalances.callAsync([ assetData ], addresses))[0]; - } - - public async getBalancesAsync(assets: string[], addresses: string[]): - Promise { - const callResults = await this.instance.getRawAssetBalances.callAsync(assets, addresses); - const result: AssetBalances = {}; - for (const i of _.times(assets.length)) { - const assetData = assets[i]; - result[assetData] = {}; - for (const j of _.times(addresses.length)) { - const address = addresses[j]; - result[assetData][address] = callResults[i][j]; + public getBalanceChange(assetData: string, address: string): BigNumber { + if (assetData in this.lastTxBalanceChanges) { + const balances = this.lastTxBalanceChanges[assetData]; + if (address in balances) { + return balances[address]; } } - return result; - } - - protected async _getBalancesFromTransferFromCallsAsync( - calls: DispatchTransferFromCallArgs[], - ): Promise { - // Extract addresses involved in transfers. - const addresses = _.uniq(_.flatten(calls.map(c => [c.from, c.to]))); - // Extract assets involved in transfers. - const assets = _.uniq(calls.map(c => c.assetData)); - // Query balances of addresses and assets involved in transfers. - return this.getBalancesAsync(assets, addresses); + return constants.ZERO_AMOUNT; } protected async _callAndSendExchangeFunctionAsync( instanceMethod: TransactionContractFunction, // tslint:disable-next-line: trailing-comma ...args: any[] - ): Promise> { + ): Promise { + this.lastTxEvents = createEmptyEvents(); + this.lastTxBalanceChanges = {}; // Call to get the return value. - const result = await instanceMethod.callAsync.call(this.instance, ...args); + const result = await instanceMethod.callAsync(...args); // Transact to execute it. const receipt = await this.logDecoder.getTxWithDecodedLogsAsync( await this.instance.fillOrder.sendTransactionAsync.call(this.instance, ...args), ); - const events = extractEvents(receipt.logs); - const balances = await this._getBalancesFromTransferFromCallsAsync(events.transferFromCalls); - return { - result, - events, - balances, - }; + this.lastTxEvents = extractEvents(receipt.logs); + this.lastTxBalanceChanges = getBalanceChangesFromTransferFromCalls( + this.lastTxEvents.transferFromCalls, + ); + return result; } } +interface TransactionContractFunction { + callAsync: (...args: any[]) => Promise; + sendTransactionAsync: (...args: any[]) => Promise; +} + +/** + * @dev Create a signature for the `TestIsolatedExchange` contract that will pass. + */ +export function createGoodSignature(type: SignatureType = SignatureType.EIP712): string { + return `0x01${Buffer.from([type]).toString('hex')}`; +} + +/** + * @dev Create a signature for the `TestIsolatedExchange` contract that will fail. + */ +export function createBadSignature(type: SignatureType = SignatureType.EIP712): string { + return `0x00${Buffer.from([type]).toString('hex')}`; +} + +function createEmptyEvents(): IsolatedExchangeEvents { + return { fillEvents: [], transferFromCalls: [] }; +} + function extractEvents(logs: LogEntry[]): IsolatedExchangeEvents { return { fillEvents: filterLogsToArguments(logs, 'Fill'), @@ -188,3 +151,19 @@ function extractEvents(logs: LogEntry[]): IsolatedExchangeEvents { ), }; } + +// Executes transferFrom calls to compute relative balances for addresses. +function getBalanceChangesFromTransferFromCalls( + calls: DispatchTransferFromCallArgs[], +): AssetBalances { + const changes: AssetBalances = {}; + for (const call of calls) { + const { assetData, from, to, amount } = call; + const balances = changes[assetData] = changes[assetData ] || {}; + const fromBalance = balances[from] || constants.ZERO_AMOUNT; + const toBalance = balances[to] || constants.ZERO_AMOUNT; + balances[from] = fromBalance.minus(amount); + balances[to] = toBalance.plus(amount); + } + return changes; +} From 38a1f08413439e819d7460c1635f84facf6295f3 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 31 Jul 2019 13:14:36 -0400 Subject: [PATCH 12/48] `@0x/contracts-test-utils`: Add MAX_UINT256 constant. --- contracts/test-utils/src/constants.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/test-utils/src/constants.ts b/contracts/test-utils/src/constants.ts index 2b3308a2ee..7519018744 100644 --- a/contracts/test-utils/src/constants.ts +++ b/contracts/test-utils/src/constants.ts @@ -1,4 +1,4 @@ -import { BigNumber } from '@0x/utils'; +import { BigNumber } from '@0x/util'; import { Web3Wrapper } from '@0x/web3-wrapper'; import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; @@ -16,6 +16,8 @@ const TESTRPC_PRIVATE_KEYS_STRINGS = [ '0x23cb7121166b9a2f93ae0b7c05bde02eae50d64449b2cbb42bc84e9d38d6cc89', ]; +const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); + export const constants = { BASE_16: 16, INVALID_OPCODE: 'invalid opcode', @@ -43,7 +45,8 @@ export const constants = { NUM_ERC1155_NONFUNGIBLE_TOKENS_MINT: 4, NULL_ADDRESS: '0x0000000000000000000000000000000000000000', NULL_BYTES32: '0x0000000000000000000000000000000000000000000000000000000000000000', - UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1), + UNLIMITED_ALLOWANCE_IN_BASE_UNITS: MAX_UINT256, + MAX_UINT256, TESTRPC_PRIVATE_KEYS: _.map(TESTRPC_PRIVATE_KEYS_STRINGS, privateKeyString => ethUtil.toBuffer(privateKeyString)), INITIAL_ERC20_BALANCE: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18), INITIAL_ERC20_ALLOWANCE: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18), From c54d69e5aee0da4870fd50e62f3da69177e19431 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 31 Jul 2019 13:15:09 -0400 Subject: [PATCH 13/48] `@0x/contracts-exchange`: Create reference functions test util. `@0x/contracts-exchange`: Use reference functions to assert fill results in `isolated_fill_order` tests. --- contracts/exchange/test/internal.ts | 2 +- .../exchange/test/isolated_fill_order.ts | 116 +++++++++++++----- .../test/utils/isolated_exchange_wrapper.ts | 17 ++- .../test/utils/reference_functions.ts | 107 ++++++++++++++++ 4 files changed, 206 insertions(+), 36 deletions(-) create mode 100644 contracts/exchange/test/utils/reference_functions.ts diff --git a/contracts/exchange/test/internal.ts b/contracts/exchange/test/internal.ts index 48aec23621..6a544a2caa 100644 --- a/contracts/exchange/test/internal.ts +++ b/contracts/exchange/test/internal.ts @@ -23,7 +23,7 @@ const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); -const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); +const { MAX_UINT256 } = constants; const emptyOrder: Order = { senderAddress: constants.NULL_ADDRESS, diff --git a/contracts/exchange/test/isolated_fill_order.ts b/contracts/exchange/test/isolated_fill_order.ts index 9c0079d2b5..9ee94f613c 100644 --- a/contracts/exchange/test/isolated_fill_order.ts +++ b/contracts/exchange/test/isolated_fill_order.ts @@ -1,59 +1,117 @@ -import { blockchainTests, constants, expect, hexRandom } from '@0x/contracts-test-utils'; +import { + blockchainTests, + constants, + expect, + FillResults, + hexRandom, +} from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; -import { IsolatedExchangeWrapper, Order } from './utils/isolated_exchange_wrapper'; +import { AssetBalances, IsolatedExchangeWrapper, Orderish } from './utils/isolated_exchange_wrapper'; +import { calculateFillResults } from './utils/reference_functions'; blockchainTests.resets.only('Isolated fillOrder() tests', env => { + const { ZERO_AMOUNT } = constants; const TOMORROW = Math.floor(_.now() / 1000) + 60 * 60 * 24; const ERC20_ASSET_DATA_LENGTH = 24; - const DEFAULT_ORDER: Order = { + const randomAddress = () => hexRandom(constants.ADDRESS_LENGTH); + const DEFAULT_ORDER: Orderish = { senderAddress: constants.NULL_ADDRESS, makerAddress: randomAddress(), takerAddress: constants.NULL_ADDRESS, - makerFee: constants.ZERO_AMOUNT, - takerFee: constants.ZERO_AMOUNT, - makerAssetAmount: constants.ZERO_AMOUNT, - takerAssetAmount: constants.ZERO_AMOUNT, - salt: constants.ZERO_AMOUNT, + makerFee: ZERO_AMOUNT, + takerFee: ZERO_AMOUNT, + makerAssetAmount: ZERO_AMOUNT, + takerAssetAmount: ZERO_AMOUNT, + salt: ZERO_AMOUNT, feeRecipientAddress: constants.NULL_ADDRESS, - expirationTimeSeconds: toBN(TOMORROW), + expirationTimeSeconds: new BigNumber(TOMORROW), makerAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH), takerAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH), makerFeeAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH), takerFeeAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH), }; let takerAddress: string; - let testExchange: IsolatedExchangeWrapper; + let exchange: IsolatedExchangeWrapper; let nextSaltValue = 1; before(async () => { - [takerAddress] = await env.getAccountAddressesAsync(); - testExchange = await IsolatedExchangeWrapper.deployAsync( + [ takerAddress ] = await env.getAccountAddressesAsync(); + exchange = await IsolatedExchangeWrapper.deployAsync( env.web3Wrapper, _.assign(env.txDefaults, { from: takerAddress }), ); }); - function createOrder(details: Partial = {}): Order { - return _.assign({}, DEFAULT_ORDER, { salt: toBN(nextSaltValue++) }, details); + function createOrder(details: Partial = {}): Orderish { + return _.assign({}, DEFAULT_ORDER, { salt: new BigNumber(nextSaltValue++) }, details); } - for (const i of _.times(100)) { - it('works', async () => { - const order = createOrder({ - makerAssetAmount: toBN(1), - takerAssetAmount: toBN(2), - }); - const results = await testExchange.fillOrderAsync(order, 2); - }); + async function fillOrderAndAssertResultsAsync( + order: Orderish, + takerAssetFillAmount: BigNumber, + ): Promise { + const efr = await calculateExpectedFillResultsAsync(order, takerAssetFillAmount); + const efb = calculateExpectedFillBalances(order, efr); + const fillResults = await exchange.fillOrderAsync(order, takerAssetFillAmount); + // Check returned fillResults. + expect(fillResults.makerAssetFilledAmount) + .to.bignumber.eq(efr.makerAssetFilledAmount); + expect(fillResults.takerAssetFilledAmount) + .to.bignumber.eq(efr.takerAssetFilledAmount); + expect(fillResults.makerFeePaid) + .to.bignumber.eq(efr.makerFeePaid); + expect(fillResults.takerFeePaid) + .to.bignumber.eq(efr.takerFeePaid); + // Check balances. + for (const assetData of Object.keys(efb)) { + for (const address of Object.keys(efb[assetData])) { + expect(exchange.getBalanceChange(assetData, address)) + .to.bignumber.eq(efb[assetData][address], `assetData: ${assetData}, address: ${address}`); + } + } + return fillResults; } -}); -function toBN(num: BigNumber | string | number): BigNumber { - return new BigNumber(num); -} + async function calculateExpectedFillResultsAsync( + order: Orderish, + takerAssetFillAmount: BigNumber, + ): Promise { + const takerAssetFilledAmount = await exchange.getTakerAssetFilledAmountAsync(order); + const remainingTakerAssetAmount = order.takerAssetAmount.minus(takerAssetFilledAmount); + return calculateFillResults( + order, + BigNumber.min(takerAssetFillAmount, remainingTakerAssetAmount), + ); + } -function randomAddress(): string { - return hexRandom(constants.ADDRESS_LENGTH); -} + function calculateExpectedFillBalances( + order: Orderish, + fillResults: FillResults, + ): AssetBalances { + const balances: AssetBalances = {}; + const addBalance = (assetData: string, address: string, amount: BigNumber) => { + balances[assetData] = balances[assetData] || {}; + const balance = balances[assetData][address] || ZERO_AMOUNT; + balances[assetData][address] = balance.plus(amount); + }; + addBalance(order.makerAssetData, order.makerAddress, fillResults.makerAssetFilledAmount.negated()); + addBalance(order.makerAssetData, takerAddress, fillResults.makerAssetFilledAmount); + addBalance(order.takerAssetData, order.makerAddress, fillResults.takerAssetFilledAmount); + addBalance(order.takerAssetData, takerAddress, fillResults.takerAssetFilledAmount.negated()); + addBalance(order.makerFeeAssetData, order.makerAddress, fillResults.makerFeePaid.negated()); + addBalance(order.makerFeeAssetData, order.feeRecipientAddress, fillResults.makerFeePaid); + addBalance(order.takerFeeAssetData, takerAddress, fillResults.takerFeePaid.negated()); + addBalance(order.takerFeeAssetData, order.feeRecipientAddress, fillResults.takerFeePaid); + return balances; + } + + it('can fully fill an order', async () => { + const order = createOrder({ + makerAssetAmount: new BigNumber(1), + takerAssetAmount: new BigNumber(2), + }); + return fillOrderAndAssertResultsAsync(order, order.takerAssetAmount); + }); +}); diff --git a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts index 1572dcb807..64580a6e27 100644 --- a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts +++ b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts @@ -22,7 +22,8 @@ export interface IsolatedExchangeEvents { transferFromCalls: DispatchTransferFromCallArgs[]; } -export type Order = OrderWithoutDomain; +export type Orderish = OrderWithoutDomain; +export type Numberish = string | number | BigNumber; export const DEFAULT_GOOD_SIGNATURE = createGoodSignature(); export const DEFAULT_BAD_SIGNATURE = createBadSignature(); @@ -65,9 +66,13 @@ export class IsolatedExchangeWrapper { this.logDecoder = new LogDecoder(web3Wrapper, artifacts); } + public async getTakerAssetFilledAmountAsync(order: Orderish): Promise { + return this.instance.filled.callAsync(this.getOrderHash(order)); + } + public async fillOrderAsync( - order: Order, - takerAssetFillAmount: BigNumber | number, + order: Orderish, + takerAssetFillAmount: Numberish, signature: string = DEFAULT_GOOD_SIGNATURE, txOpts?: TxData, ): Promise { @@ -80,7 +85,7 @@ export class IsolatedExchangeWrapper { ); } - public getOrderHash(order: Order): string { + public getOrderHash(order: Orderish): string { const domain = { verifyingContractAddress: this.instance.address, chainId: IsolatedExchangeWrapper.CHAIN_ID, @@ -125,14 +130,14 @@ interface TransactionContractFunction { } /** - * @dev Create a signature for the `TestIsolatedExchange` contract that will pass. + * Create a signature for the `TestIsolatedExchange` contract that will pass. */ export function createGoodSignature(type: SignatureType = SignatureType.EIP712): string { return `0x01${Buffer.from([type]).toString('hex')}`; } /** - * @dev Create a signature for the `TestIsolatedExchange` contract that will fail. + * Create a signature for the `TestIsolatedExchange` contract that will fail. */ export function createBadSignature(type: SignatureType = SignatureType.EIP712): string { return `0x00${Buffer.from([type]).toString('hex')}`; diff --git a/contracts/exchange/test/utils/reference_functions.ts b/contracts/exchange/test/utils/reference_functions.ts new file mode 100644 index 0000000000..5092215113 --- /dev/null +++ b/contracts/exchange/test/utils/reference_functions.ts @@ -0,0 +1,107 @@ +import { constants, FillResults } from '@0x/contracts-test-utils'; +import { LibMathRevertErrors } from '@0x/order-utils'; +import { OrderWithoutDomain } from '@0x/types'; +import { AnyRevertError, BigNumber, SafeMathRevertErrors } from '@0x/utils'; + +const { MAX_UINT256 } = constants; + +export function isRoundingErrorFloor( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, +): boolean { + if (denominator.eq(0)) { + throw new LibMathRevertErrors.DivisionByZeroError(); + } + if (numerator.eq(0)) { + return false; + } + if (target.eq(0)) { + return false; + } + const product = numerator.multipliedBy(target); + const remainder = product.mod(denominator); + const remainderTimes1000 = remainder.multipliedBy('1000'); + const isError = remainderTimes1000.gte(product); + if (remainderTimes1000.isGreaterThan(MAX_UINT256)) { + // Solidity implementation won't actually throw. + throw new AnyRevertError(); + } + return isError; +} + +export function IsRoundingErrorCeil( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, +): boolean { + if (denominator.eq(0)) { + throw new LibMathRevertErrors.DivisionByZeroError(); + } + if (numerator.eq(0)) { + return false; + } + if (target.eq(0)) { + return false; + } + const product = numerator.multipliedBy(target); + const remainder = product.mod(denominator); + const error = denominator.minus(remainder).mod(denominator); + const errorTimes1000 = error.multipliedBy('1000'); + const isError = errorTimes1000.gte(product); + if (errorTimes1000.isGreaterThan(MAX_UINT256)) { + // Solidity implementation won't actually throw. + throw new AnyRevertError(); + } + return isError; +} + +export function safeGetPartialAmountFloor( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, +): BigNumber { + if (denominator.eq(0)) { + throw new LibMathRevertErrors.DivisionByZeroError(); + } + const isRoundingError = isRoundingErrorFloor(numerator, denominator, target); + if (isRoundingError) { + throw new LibMathRevertErrors.RoundingError(numerator, denominator, target); + } + const product = numerator.multipliedBy(target); + if (product.isGreaterThan(MAX_UINT256)) { + throw new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + numerator, + denominator, + ); + } + return product.dividedToIntegerBy(denominator); +} + +export function calculateFillResults( + order: OrderWithoutDomain, + takerAssetFilledAmount: BigNumber, +): FillResults { + const makerAssetFilledAmount = safeGetPartialAmountFloor( + takerAssetFilledAmount, + order.takerAssetAmount, + order.makerAssetAmount, + ); + const makerFeePaid = safeGetPartialAmountFloor( + makerAssetFilledAmount, + order.makerAssetAmount, + order.makerFee, + ); + const takerFeePaid = safeGetPartialAmountFloor( + takerAssetFilledAmount, + order.takerAssetAmount, + order.takerFee, + ); + return { + makerAssetFilledAmount, + takerAssetFilledAmount, + makerFeePaid, + takerFeePaid, + }; +} From c30d59d5d3eebd3da902733e0d6edcf35839ed95 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 31 Jul 2019 15:56:36 -0400 Subject: [PATCH 14/48] `@0x/types`: Add `FillResults`, `MatchedFillResults`, and `BatchMatchedFillResults` types. `@0x/contracts-utils`: Add reference functions for `SafeMath`. `@0x/contracts-exchange-libs`: Add reference functions for `LibMath` and `LibFillResults`. `@0x/contracts-test-utils`: Move `*FillResults` types to `@0x/types`. `@0x/contracts-test-utils`: Add `log_utils.ts`. `@0x/contracts-test-utils`: Add `hexRandom()` to `hex_utils.ts`. `@0x/contracts-test-utils`: Add the contstants: `MAX_UINT256`, `ADDRESS_LENGTH`. --- contracts/exchange-libs/CHANGELOG.json | 55 ++++---- contracts/exchange-libs/src/index.ts | 3 + .../exchange-libs/src/reference_functions.ts | 122 ++++++++++++++++++ contracts/test-utils/CHANGELOG.json | 16 +++ contracts/test-utils/src/index.ts | 3 - contracts/test-utils/src/types.ts | 21 --- contracts/utils/CHANGELOG.json | 4 + contracts/utils/src/index.ts | 3 + contracts/utils/src/reference_functions.ts | 44 +++++++ packages/types/CHANGELOG.json | 4 + packages/types/src/index.ts | 21 +++ 11 files changed, 249 insertions(+), 47 deletions(-) create mode 100644 contracts/exchange-libs/src/reference_functions.ts create mode 100644 contracts/utils/src/reference_functions.ts diff --git a/contracts/exchange-libs/CHANGELOG.json b/contracts/exchange-libs/CHANGELOG.json index f5f95a1b70..f8ce90f4df 100644 --- a/contracts/exchange-libs/CHANGELOG.json +++ b/contracts/exchange-libs/CHANGELOG.json @@ -1,29 +1,7 @@ [ { - "timestamp": 1563193019, - "version": "3.0.2", - "changes": [ - { - "note": "Dependencies updated" - } - ] - }, - { - "timestamp": 1563047529, - "version": "3.0.1", - "changes": [ - { - "note": "Dependencies updated" - } - ] - }, - { - "version": "3.0.0", + "version": "3.1.0", "changes": [ - { - "note": "Move `LibTransactionDecoder` to contracts/dev-utils package", - "pr": 1848 - }, { "note": "Break up `LibEIP712` into reusable components", "pr": 1742 @@ -75,6 +53,37 @@ { "note": "Add `expirationTimeSeconds` to `ZeroExTransaction` struct", "pr": 1823 + }, + { + "note": "Add reference functions for `LibMath` and `LibFillResults`", + "pr": "TODO" + } + ] + }, + { + "timestamp": 1563193019, + "version": "3.0.2", + "changes": [ + { + "note": "Dependencies updated" + } + ] + }, + { + "timestamp": 1563047529, + "version": "3.0.1", + "changes": [ + { + "note": "Dependencies updated" + } + ] + }, + { + "version": "3.0.0", + "changes": [ + { + "note": "Move `LibTransactionDecoder` to contracts/dev-utils package", + "pr": 1848 } ], "timestamp": 1563006338 diff --git a/contracts/exchange-libs/src/index.ts b/contracts/exchange-libs/src/index.ts index d55f08ea2d..7e9c4805a6 100644 --- a/contracts/exchange-libs/src/index.ts +++ b/contracts/exchange-libs/src/index.ts @@ -1,2 +1,5 @@ export * from './artifacts'; export * from './wrappers'; + +import * as reference_functions from './reference_functions'; +export import ReferenceFunctions = reference_functions; diff --git a/contracts/exchange-libs/src/reference_functions.ts b/contracts/exchange-libs/src/reference_functions.ts new file mode 100644 index 0000000000..6a41601166 --- /dev/null +++ b/contracts/exchange-libs/src/reference_functions.ts @@ -0,0 +1,122 @@ +import { ReferenceFunctions } from '@0x/contracts-utils'; +import { LibMathRevertErrors } from '@0x/order-utils'; +import { FillResults } from '@0x/types'; +import { BigNumber } from '@0x/utils'; + +const { + safeAdd, + safeSub, + safeMul, + safeDiv, +} = ReferenceFunctions; + +export function isRoundingErrorFloor( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, +): boolean { + if (denominator.eq(0)) { + throw new LibMathRevertErrors.DivisionByZeroError(); + } + if (numerator.eq(0) || target.eq(0)) { + return false; + } + const remainder = numerator.multipliedBy(target).mod(denominator); + return safeMul(new BigNumber(1000), remainder).gte(safeMul(numerator, target)); +} + +export function isRoundingErrorCeil( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, +): boolean { + if (denominator.eq(0)) { + throw new LibMathRevertErrors.DivisionByZeroError(); + } + if (numerator.eq(0) || target.eq(0)) { + return false; + } + let remainder = numerator.multipliedBy(target).mod(denominator); + remainder = safeSub(denominator, remainder).mod(denominator); + return safeMul(new BigNumber(1000), remainder).gte(safeMul(numerator, target)); +} + +export function safeGetPartialAmountFloor( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, +): BigNumber { + if (denominator.eq(0)) { + throw new LibMathRevertErrors.DivisionByZeroError(); + } + if (isRoundingErrorFloor(numerator, denominator, target)) { + throw new LibMathRevertErrors.RoundingError(numerator, denominator, target); + } + return safeDiv( + safeMul(numerator, target), + denominator, + ); +} + +export function safeGetPartialAmountCeil( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, +): BigNumber { + if (denominator.eq(0)) { + throw new LibMathRevertErrors.DivisionByZeroError(); + } + if (isRoundingErrorCeil(numerator, denominator, target)) { + throw new LibMathRevertErrors.RoundingError(numerator, denominator, target); + } + return safeDiv( + safeAdd( + safeMul(numerator, target), + safeSub(denominator, new BigNumber(1)), + ), + denominator, + ); +} + +export function getPartialAmountFloor( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, +): BigNumber { + if (denominator.eq(0)) { + throw new LibMathRevertErrors.DivisionByZeroError(); + } + return safeDiv( + safeMul(numerator, target), + denominator, + ); +} + +export function getPartialAmountCeil( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, +): BigNumber { + if (denominator.eq(0)) { + throw new LibMathRevertErrors.DivisionByZeroError(); + } + return safeDiv( + safeAdd( + safeMul(numerator, target), + safeSub(denominator, new BigNumber(1)), + ), + denominator, + ); +} + +export function addFillResults( + a: FillResults, + b: FillResults, +): FillResults { + return { + makerAssetFilledAmount: safeAdd(a.makerAssetFilledAmount, b.makerAssetFilledAmount), + takerAssetFilledAmount: safeAdd(a.takerAssetFilledAmount, b.takerAssetFilledAmount), + makerFeePaid: safeAdd(a.makerFeePaid, b.makerFeePaid), + takerFeePaid: safeAdd(a.takerFeePaid, b.takerFeePaid), + }; +} diff --git a/contracts/test-utils/CHANGELOG.json b/contracts/test-utils/CHANGELOG.json index 21eb15b419..3b921bea73 100644 --- a/contracts/test-utils/CHANGELOG.json +++ b/contracts/test-utils/CHANGELOG.json @@ -37,6 +37,22 @@ { "note": "Introduce Mocha blockchain extensions", "pr": 2007 + }, + { + "note": "Move `*FillResults` types to `@0x/types`", + "pr": "TODO" + }, + { + "note": "Add `log_utils.ts`", + "pr": "TODO" + }, + { + "note": "Add `haxRandom()` to `hex_utils.ts`", + "pr": "TODO" + }, + { + "note": "Add the constants: `MAX_UINT256`, `ADDRESS_LENGTH`", + "pr": "TODO" } ] }, diff --git a/contracts/test-utils/src/index.ts b/contracts/test-utils/src/index.ts index c626fbb77a..040ba809d7 100644 --- a/contracts/test-utils/src/index.ts +++ b/contracts/test-utils/src/index.ts @@ -29,7 +29,6 @@ export { TransactionFactory } from './transaction_factory'; export { testWithReferenceFuncAsync } from './test_with_reference'; export { hexConcat, hexRandom } from './hex_utils'; export { - BatchMatchedFillResults, BatchMatchOrder, ContractName, ERC20BalancesByOwner, @@ -37,10 +36,8 @@ export { ERC1155HoldingsByOwner, ERC1155NonFungibleHoldingsByOwner, ERC721TokenIdsByOwner, - FillResults, MarketBuyOrders, MarketSellOrders, - MatchedFillResults, OrderInfo, OrderStatus, Token, diff --git a/contracts/test-utils/src/types.ts b/contracts/test-utils/src/types.ts index b085a34d0c..5c534b43f6 100644 --- a/contracts/test-utils/src/types.ts +++ b/contracts/test-utils/src/types.ts @@ -143,24 +143,3 @@ export interface MatchOrder { leftSignature: string; rightSignature: string; } - -export interface FillResults { - makerAssetFilledAmount: BigNumber; - takerAssetFilledAmount: BigNumber; - makerFeePaid: BigNumber; - takerFeePaid: BigNumber; -} - -export interface MatchedFillResults { - left: FillResults; - right: FillResults; - profitInLeftMakerAsset: BigNumber; - profitInRightMakerAsset: BigNumber; -} - -export interface BatchMatchedFillResults { - left: FillResults[]; - right: FillResults[]; - profitInLeftMakerAsset: BigNumber; - profitInRightMakerAsset: BigNumber; -} diff --git a/contracts/utils/CHANGELOG.json b/contracts/utils/CHANGELOG.json index 8cf9dbdc51..19a0c19fd0 100644 --- a/contracts/utils/CHANGELOG.json +++ b/contracts/utils/CHANGELOG.json @@ -33,6 +33,10 @@ { "note": "Updated Ownable to revert when the owner attempts to transfer ownership to the zero address", "pr": 2019 + }, + { + "note": "Add reference functions for `SafeMath` functions.", + "pr": "2031" } ] }, diff --git a/contracts/utils/src/index.ts b/contracts/utils/src/index.ts index d55f08ea2d..7e9c4805a6 100644 --- a/contracts/utils/src/index.ts +++ b/contracts/utils/src/index.ts @@ -1,2 +1,5 @@ export * from './artifacts'; export * from './wrappers'; + +import * as reference_functions from './reference_functions'; +export import ReferenceFunctions = reference_functions; diff --git a/contracts/utils/src/reference_functions.ts b/contracts/utils/src/reference_functions.ts new file mode 100644 index 0000000000..717baafc09 --- /dev/null +++ b/contracts/utils/src/reference_functions.ts @@ -0,0 +1,44 @@ +import { AnyRevertError, BigNumber, SafeMathRevertErrors } from '@0x/utils'; + +const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); + +export function safeAdd(a: BigNumber, b: BigNumber): BigNumber { + const r = a.plus(b); + if (r.isGreaterThan(MAX_UINT256)) { + throw new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256AdditionOverflow, + a, + b, + ); + } + return r; +} + +export function safeSub(a: BigNumber, b: BigNumber): BigNumber { + const r = a.minus(b); + if (r.isLessThan(0)) { + throw new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256SubtractionUnderflow, + a, + b, + ); + } + return r; +} + +export function safeMul(a: BigNumber, b: BigNumber): BigNumber { + const r = a.times(b); + if (r.isGreaterThan(MAX_UINT256)) { + // Solidity implementation does not throw a reason. + throw new AnyRevertError(); + } + return r; +} + +export function safeDiv(a: BigNumber, b: BigNumber): BigNumber { + if (b.isEqualTo(0)) { + // Solidity implementation does not throw a reason. + throw new AnyRevertError(); + } + return a.dividedToIntegerBy(b); +} diff --git a/packages/types/CHANGELOG.json b/packages/types/CHANGELOG.json index f47eea8aec..a7e3f894fb 100644 --- a/packages/types/CHANGELOG.json +++ b/packages/types/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Add `OrderStatus` type", "pr": 1761 + }, + { + "note": "Add `FillResults`, `MatchedFillResults`, `BatchMatchedFillResults` types", + "pr": "TODO" } ] }, diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 57ce884d27..b5bc97b63a 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -810,3 +810,24 @@ export enum OrderTransferResults { MakerFeeAssetDataFailed, TransfersSuccessful, } + +export interface FillResults { + makerAssetFilledAmount: BigNumber; + takerAssetFilledAmount: BigNumber; + makerFeePaid: BigNumber; + takerFeePaid: BigNumber; +} + +export interface MatchedFillResults { + left: FillResults; + right: FillResults; + profitInLeftMakerAsset: BigNumber; + profitInRightMakerAsset: BigNumber; +} + +export interface BatchMatchedFillResults { + left: FillResults[]; + right: FillResults[]; + profitInLeftMakerAsset: BigNumber; + profitInRightMakerAsset: BigNumber; +} From 898213bb855de51e8762d2481eedc5c328df4761 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 31 Jul 2019 16:47:41 -0400 Subject: [PATCH 15/48] `@0x/contracts-exchange`: Update tests for moved types. --- contracts/exchange/CHANGELOG.json | 8 ++ .../contracts/test/TestIsolatedExchange.sol | 7 +- contracts/exchange/test/internal.ts | 59 ++++--------- .../exchange/test/isolated_fill_order.ts | 62 ++++++++++---- contracts/exchange/test/transactions.ts | 3 +- .../exchange/test/utils/exchange_wrapper.ts | 11 ++- .../utils/fill_order_combinatorial_utils.ts | 4 +- .../test/utils/fill_order_scenarios.ts | 7 -- .../test/utils/fill_order_simulator.ts | 3 +- .../test/utils/isolated_exchange_wrapper.ts | 21 ++++- .../exchange/test/utils/match_order_tester.ts | 17 ++-- .../test/utils/reference_functions.ts | 83 +------------------ contracts/exchange/test/wrapper.ts | 3 +- 13 files changed, 121 insertions(+), 167 deletions(-) diff --git a/contracts/exchange/CHANGELOG.json b/contracts/exchange/CHANGELOG.json index fd52350d72..cf51699254 100644 --- a/contracts/exchange/CHANGELOG.json +++ b/contracts/exchange/CHANGELOG.json @@ -117,6 +117,14 @@ { "note": "Rewrote _dispatchTransferFrom in Solidity", "pr": 2020 + }, + { + "note": "Add `TestIsolatedExchange` contract and `IsolatedExchangeWrapper` test class", + "pr": "TODO" + }, + { + "note": "Add `test/utils/reference_functions.ts` for `calculateFillResults`", + "pr": "TODO" } ] }, diff --git a/contracts/exchange/contracts/test/TestIsolatedExchange.sol b/contracts/exchange/contracts/test/TestIsolatedExchange.sol index 04c531a57b..9ebcee373a 100644 --- a/contracts/exchange/contracts/test/TestIsolatedExchange.sol +++ b/contracts/exchange/contracts/test/TestIsolatedExchange.sol @@ -42,7 +42,7 @@ contract TestIsolatedExchange is Exchange(1337) {} - /// @dev Overriden to only log arguments. + /// @dev Overriden to only log arguments and revert on certain assetDatas. function _dispatchTransferFrom( bytes32 orderHash, bytes memory assetData, @@ -59,6 +59,11 @@ contract TestIsolatedExchange is to, amount ); + + // Fail if the first byte is 0. + if (assetData.length > 0 && assetData[0] == 0x00) { + revert('TRANSFER_FAILED'); + } } /// @dev Overriden to simplify signature validation. diff --git a/contracts/exchange/test/internal.ts b/contracts/exchange/test/internal.ts index 6a544a2caa..4a55ee5cb1 100644 --- a/contracts/exchange/test/internal.ts +++ b/contracts/exchange/test/internal.ts @@ -1,28 +1,18 @@ import { + blockchainTests, bytes32Values, - chaiSetup, constants, - FillResults, - provider, + expect, testCombinatoriallyWithReferenceFuncAsync, - txDefaults, uint256Values, - web3Wrapper, } from '@0x/contracts-test-utils'; -import { BlockchainLifecycle } from '@0x/dev-utils'; import { LibMathRevertErrors } from '@0x/order-utils'; -import { Order, RevertReason, SignedOrder } from '@0x/types'; +import { FillResults, Order, RevertReason, SignedOrder } from '@0x/types'; import { BigNumber, providerUtils, SafeMathRevertErrors } from '@0x/utils'; -import * as chai from 'chai'; import * as _ from 'lodash'; import { artifacts, TestExchangeInternalsContract, TestExchangeMathContract } from '../src'; -chaiSetup.configure(); -const expect = chai.expect; - -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - const { MAX_UINT256 } = constants; const emptyOrder: Order = { @@ -53,35 +43,28 @@ const emptySignedOrder: SignedOrder = { const safeMathErrorForCall = new SafeMathRevertErrors.SafeMathError(); -describe('Exchange math internal functions', () => { +// TODO(dorothy-zbornak): Move this to `exchange-libs` and `utils`. +blockchainTests.resets('Exchange math internal functions', env => { let chainId: number; let testExchange: TestExchangeMathContract; let divisionByZeroErrorForCall: Error | undefined; let roundingErrorForCall: Error | undefined; before(async () => { - await blockchainLifecycle.startAsync(); - }); - after(async () => { - await blockchainLifecycle.revertAsync(); - }); - before(async () => { - chainId = await providerUtils.getChainIdAsync(provider); + chainId = await env.getChainIdAsync(); emptyOrder.domain.chainId = chainId; emptySignedOrder.domain.chainId = chainId; testExchange = await TestExchangeMathContract.deployFrom0xArtifactAsync( artifacts.TestExchangeMath, - provider, - txDefaults, + env.provider, + env.txDefaults, ); divisionByZeroErrorForCall = new Error(RevertReason.DivisionByZero); roundingErrorForCall = new Error(RevertReason.RoundingError); divisionByZeroErrorForCall = new LibMathRevertErrors.DivisionByZeroError(); roundingErrorForCall = new LibMathRevertErrors.RoundingError(); }); - // Note(albrow): Don't forget to add beforeEach and afterEach calls to reset - // the blockchain state for any tests which modify it! async function referenceIsRoundingErrorFloorAsync( numerator: BigNumber, @@ -314,7 +297,8 @@ describe('Exchange math internal functions', () => { }); }); -describe('Exchange core internal functions', () => { +// TODO(dorothy-zbornak): Add _settleOrder, _dispatchTransferFrom +blockchainTests.resets('Exchange core internal functions', env => { let chainId: number; let testExchange: TestExchangeInternalsContract; let safeMathErrorForSendTransaction: Error | undefined; @@ -322,20 +306,14 @@ describe('Exchange core internal functions', () => { let roundingErrorForCall: Error | undefined; before(async () => { - await blockchainLifecycle.startAsync(); - }); - after(async () => { - await blockchainLifecycle.revertAsync(); - }); - before(async () => { - chainId = await providerUtils.getChainIdAsync(provider); + chainId = await providerUtils.getChainIdAsync(env.provider); emptyOrder.domain.chainId = chainId; emptySignedOrder.domain.chainId = chainId; testExchange = await TestExchangeInternalsContract.deployFrom0xArtifactAsync( artifacts.TestExchangeInternals, - provider, - txDefaults, + env.provider, + env.txDefaults, new BigNumber(chainId), ); divisionByZeroErrorForCall = new Error(RevertReason.DivisionByZero); @@ -393,6 +371,7 @@ describe('Exchange core internal functions', () => { return product.dividedToIntegerBy(denominator); } + // TODO(dorothy-zbornak): Move this to `exchange-libs`. describe('addFillResults', async () => { function makeFillResults(value: BigNumber): FillResults { return { @@ -505,15 +484,7 @@ describe('Exchange core internal functions', () => { ); }); - describe('updateFilledState', async () => { - // Note(albrow): Since updateFilledState modifies the state by calling - // sendTransaction, we must reset the state after each test. - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); + blockchainTests.resets('updateFilledState', async ({ web3Wrapper }) => { async function referenceUpdateFilledStateAsync( takerAssetFilledAmount: BigNumber, orderTakerAssetFilledAmount: BigNumber, diff --git a/contracts/exchange/test/isolated_fill_order.ts b/contracts/exchange/test/isolated_fill_order.ts index 9ee94f613c..8baf1baaa8 100644 --- a/contracts/exchange/test/isolated_fill_order.ts +++ b/contracts/exchange/test/isolated_fill_order.ts @@ -2,19 +2,24 @@ import { blockchainTests, constants, expect, - FillResults, hexRandom, } from '@0x/contracts-test-utils'; +import { FillResults } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; -import { AssetBalances, IsolatedExchangeWrapper, Orderish } from './utils/isolated_exchange_wrapper'; +import { + AssetBalances, + createGoodAssetData, + IsolatedExchangeWrapper, + Orderish, +} from './utils/isolated_exchange_wrapper'; import { calculateFillResults } from './utils/reference_functions'; blockchainTests.resets.only('Isolated fillOrder() tests', env => { const { ZERO_AMOUNT } = constants; const TOMORROW = Math.floor(_.now() / 1000) + 60 * 60 * 24; - const ERC20_ASSET_DATA_LENGTH = 24; + const ONE_ETHER = new BigNumber(10).pow(18); const randomAddress = () => hexRandom(constants.ADDRESS_LENGTH); const DEFAULT_ORDER: Orderish = { senderAddress: constants.NULL_ADDRESS, @@ -22,15 +27,15 @@ blockchainTests.resets.only('Isolated fillOrder() tests', env => { takerAddress: constants.NULL_ADDRESS, makerFee: ZERO_AMOUNT, takerFee: ZERO_AMOUNT, - makerAssetAmount: ZERO_AMOUNT, - takerAssetAmount: ZERO_AMOUNT, + makerAssetAmount: ONE_ETHER, + takerAssetAmount: ONE_ETHER.times(2), salt: ZERO_AMOUNT, feeRecipientAddress: constants.NULL_ADDRESS, expirationTimeSeconds: new BigNumber(TOMORROW), - makerAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH), - takerAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH), - makerFeeAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH), - takerFeeAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH), + makerAssetData: createGoodAssetData(), + takerAssetData: createGoodAssetData(), + makerFeeAssetData: createGoodAssetData(), + takerFeeAssetData: createGoodAssetData(), }; let takerAddress: string; let exchange: IsolatedExchangeWrapper; @@ -107,11 +112,40 @@ blockchainTests.resets.only('Isolated fillOrder() tests', env => { return balances; } - it('can fully fill an order', async () => { - const order = createOrder({ - makerAssetAmount: new BigNumber(1), - takerAssetAmount: new BigNumber(2), + describe('full fills', () => { + it('can fully fill an order', async () => { + const order = createOrder(); + return fillOrderAndAssertResultsAsync(order, order.takerAssetAmount); + }); + + it('can\'t overfill an order', async () => { + const order = createOrder(); + return fillOrderAndAssertResultsAsync(order, order.takerAssetAmount.times(1.01)); + }); + + it('pays maker and taker fees', async () => { + const order = createOrder({ + makerFee: ONE_ETHER.times(0.025), + takerFee: ONE_ETHER.times(0.035), + }); + return fillOrderAndAssertResultsAsync(order, order.takerAssetAmount); + }); + }); + + describe('partial fills', () => { + const takerAssetFillAmount = DEFAULT_ORDER.takerAssetAmount.dividedToIntegerBy(2); + + it('can partially fill an order', async () => { + const order = createOrder(); + return fillOrderAndAssertResultsAsync(order, takerAssetFillAmount); + }); + + it('pays maker and taker fees', async () => { + const order = createOrder({ + makerFee: ONE_ETHER.times(0.025), + takerFee: ONE_ETHER.times(0.035), + }); + return fillOrderAndAssertResultsAsync(order, takerAssetFillAmount); }); - return fillOrderAndAssertResultsAsync(order, order.takerAssetAmount); }); }); diff --git a/contracts/exchange/test/transactions.ts b/contracts/exchange/test/transactions.ts index d9073f71e8..78ca9540e8 100644 --- a/contracts/exchange/test/transactions.ts +++ b/contracts/exchange/test/transactions.ts @@ -4,7 +4,6 @@ import { DummyERC20TokenContract } from '@0x/contracts-erc20'; import { chaiSetup, constants, - FillResults, getLatestBlockTimestampAsync, LogDecoder, OrderFactory, @@ -21,7 +20,7 @@ import { orderHashUtils, transactionHashUtils, } from '@0x/order-utils'; -import { EIP712DomainWithDefaultSchema, OrderStatus, RevertReason } from '@0x/types'; +import { EIP712DomainWithDefaultSchema, FillResults, OrderStatus, RevertReason } from '@0x/types'; import { AbiEncoder, BigNumber, providerUtils } from '@0x/utils'; import * as chai from 'chai'; import { LogWithDecodedArgs, MethodAbi } from 'ethereum-types'; diff --git a/contracts/exchange/test/utils/exchange_wrapper.ts b/contracts/exchange/test/utils/exchange_wrapper.ts index fc07fc1534..3152cd2fda 100644 --- a/contracts/exchange/test/utils/exchange_wrapper.ts +++ b/contracts/exchange/test/utils/exchange_wrapper.ts @@ -2,16 +2,19 @@ import { artifacts as erc1155Artifacts } from '@0x/contracts-erc1155'; import { artifacts as erc20Artifacts } from '@0x/contracts-erc20'; import { artifacts as erc721Artifacts } from '@0x/contracts-erc721'; import { - BatchMatchedFillResults, BatchMatchOrder, - FillResults, LogDecoder, - MatchedFillResults, OrderInfo, orderUtils, Web3ProviderEngine, } from '@0x/contracts-test-utils'; -import { SignedOrder, SignedZeroExTransaction } from '@0x/types'; +import { + BatchMatchedFillResults, + FillResults, + MatchedFillResults, + SignedOrder, + SignedZeroExTransaction, +} from '@0x/types'; import { AbiEncoder, BigNumber } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import { MethodAbi, TransactionReceiptWithDecodedLogs, ZeroExProvider } from 'ethereum-types'; diff --git a/contracts/exchange/test/utils/fill_order_combinatorial_utils.ts b/contracts/exchange/test/utils/fill_order_combinatorial_utils.ts index 9cd360d360..de2b4adaba 100644 --- a/contracts/exchange/test/utils/fill_order_combinatorial_utils.ts +++ b/contracts/exchange/test/utils/fill_order_combinatorial_utils.ts @@ -5,9 +5,9 @@ import { ERC721Wrapper, MultiAssetProxyContract, } from '@0x/contracts-asset-proxy'; -import { constants, expect, FillResults, orderUtils, signingUtils } from '@0x/contracts-test-utils'; +import { constants, expect, orderUtils, signingUtils } from '@0x/contracts-test-utils'; import { BalanceAndProxyAllowanceLazyStore, ExchangeRevertErrors, orderHashUtils } from '@0x/order-utils'; -import { Order, SignatureType, SignedOrder } from '@0x/types'; +import { FillResults, Order, SignatureType, SignedOrder } from '@0x/types'; import { BigNumber, errorUtils, providerUtils, RevertError, StringRevertError } from '@0x/utils'; import { SupportedProvider, Web3Wrapper } from '@0x/web3-wrapper'; import { LogWithDecodedArgs, TxData } from 'ethereum-types'; diff --git a/contracts/exchange/test/utils/fill_order_scenarios.ts b/contracts/exchange/test/utils/fill_order_scenarios.ts index 111068d5a2..d120560c17 100644 --- a/contracts/exchange/test/utils/fill_order_scenarios.ts +++ b/contracts/exchange/test/utils/fill_order_scenarios.ts @@ -82,13 +82,6 @@ export interface FillScenario { takerStateScenario: TraderStateScenario; } -export interface FillResults { - makerAssetFilledAmount: BigNumber; - takerAssetFilledAmount: BigNumber; - makerFeePaid: BigNumber; - takerFeePaid: BigNumber; -} - export interface OrderScenario { takerScenario: TakerScenario; feeRecipientScenario: FeeRecipientAddressScenario; diff --git a/contracts/exchange/test/utils/fill_order_simulator.ts b/contracts/exchange/test/utils/fill_order_simulator.ts index 887595c6e1..c2b4224656 100644 --- a/contracts/exchange/test/utils/fill_order_simulator.ts +++ b/contracts/exchange/test/utils/fill_order_simulator.ts @@ -1,4 +1,4 @@ -import { constants, FillResults, orderUtils } from '@0x/contracts-test-utils'; +import { constants, orderUtils } from '@0x/contracts-test-utils'; import { AbstractBalanceAndProxyAllowanceLazyStore as LazyStore, ExchangeTransferSimulator, @@ -6,6 +6,7 @@ import { TradeSide, TransferType, } from '@0x/order-utils'; +import { FillResults } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; diff --git a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts index 64580a6e27..e887839a67 100644 --- a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts +++ b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts @@ -1,8 +1,9 @@ -import { constants, FillResults, filterLogsToArguments, LogDecoder, txDefaults as testTxDefaults } from '@0x/contracts-test-utils'; +import { constants, filterLogsToArguments, LogDecoder, txDefaults as testTxDefaults } from '@0x/contracts-test-utils'; import { orderHashUtils } from '@0x/order-utils'; -import { OrderWithoutDomain, SignatureType } from '@0x/types'; +import { FillResults, OrderWithoutDomain, SignatureType } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { TxData, Web3Wrapper } from '@0x/web3-wrapper'; +import * as crypto from 'crypto'; import { LogEntry } from 'ethereum-types'; import * as _ from 'lodash'; @@ -143,6 +144,22 @@ export function createBadSignature(type: SignatureType = SignatureType.EIP712): return `0x00${Buffer.from([type]).toString('hex')}`; } +const ERC20_ASSET_DATA_LENGTH = 24; + +/** + * Create asset data for the `TestIsolatedExchange` contract that will pass. + */ +export function createGoodAssetData(length: number = ERC20_ASSET_DATA_LENGTH): string { + return `0x01${crypto.randomBytes(length - 1).toString('hex')}`; +} + +/** + * Create asset data for the `TestIsolatedExchange` contract that will fail. + */ +export function createBadAssetData(length: number = ERC20_ASSET_DATA_LENGTH): string { + return `0x00${crypto.randomBytes(length - 1).toString('hex')}`; +} + function createEmptyEvents(): IsolatedExchangeEvents { return { fillEvents: [], transferFromCalls: [] }; } diff --git a/contracts/exchange/test/utils/match_order_tester.ts b/contracts/exchange/test/utils/match_order_tester.ts index 9d8572e6b0..16f21f4cc9 100644 --- a/contracts/exchange/test/utils/match_order_tester.ts +++ b/contracts/exchange/test/utils/match_order_tester.ts @@ -1,16 +1,18 @@ import { ERC1155ProxyWrapper, ERC20Wrapper, ERC721Wrapper } from '@0x/contracts-asset-proxy'; import { - BatchMatchedFillResults, - chaiSetup, ERC1155HoldingsByOwner, - FillResults, - MatchedFillResults, + expect, OrderStatus, } from '@0x/contracts-test-utils'; import { assetDataUtils, orderHashUtils } from '@0x/order-utils'; -import { AssetProxyId, SignedOrder } from '@0x/types'; +import { + AssetProxyId, + BatchMatchedFillResults, + FillResults, + MatchedFillResults, + SignedOrder, +} from '@0x/types'; import { BigNumber } from '@0x/utils'; -import * as chai from 'chai'; import { LogWithDecodedArgs, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import * as _ from 'lodash'; @@ -18,9 +20,6 @@ import { ExchangeWrapper } from './exchange_wrapper'; const ZERO = new BigNumber(0); -chaiSetup.configure(); -const expect = chai.expect; - export interface IndividualERC1155Holdings { fungible: { [tokenId: string]: BigNumber; diff --git a/contracts/exchange/test/utils/reference_functions.ts b/contracts/exchange/test/utils/reference_functions.ts index 5092215113..8fe0b53bdd 100644 --- a/contracts/exchange/test/utils/reference_functions.ts +++ b/contracts/exchange/test/utils/reference_functions.ts @@ -1,83 +1,8 @@ -import { constants, FillResults } from '@0x/contracts-test-utils'; -import { LibMathRevertErrors } from '@0x/order-utils'; -import { OrderWithoutDomain } from '@0x/types'; -import { AnyRevertError, BigNumber, SafeMathRevertErrors } from '@0x/utils'; +import { ReferenceFunctions as ExchangeLibsReferenceFunctions } from '@0x/contracts-exchange-libs'; +import { FillResults, OrderWithoutDomain } from '@0x/types'; +import { BigNumber } from '@0x/utils'; -const { MAX_UINT256 } = constants; - -export function isRoundingErrorFloor( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, -): boolean { - if (denominator.eq(0)) { - throw new LibMathRevertErrors.DivisionByZeroError(); - } - if (numerator.eq(0)) { - return false; - } - if (target.eq(0)) { - return false; - } - const product = numerator.multipliedBy(target); - const remainder = product.mod(denominator); - const remainderTimes1000 = remainder.multipliedBy('1000'); - const isError = remainderTimes1000.gte(product); - if (remainderTimes1000.isGreaterThan(MAX_UINT256)) { - // Solidity implementation won't actually throw. - throw new AnyRevertError(); - } - return isError; -} - -export function IsRoundingErrorCeil( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, -): boolean { - if (denominator.eq(0)) { - throw new LibMathRevertErrors.DivisionByZeroError(); - } - if (numerator.eq(0)) { - return false; - } - if (target.eq(0)) { - return false; - } - const product = numerator.multipliedBy(target); - const remainder = product.mod(denominator); - const error = denominator.minus(remainder).mod(denominator); - const errorTimes1000 = error.multipliedBy('1000'); - const isError = errorTimes1000.gte(product); - if (errorTimes1000.isGreaterThan(MAX_UINT256)) { - // Solidity implementation won't actually throw. - throw new AnyRevertError(); - } - return isError; -} - -export function safeGetPartialAmountFloor( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, -): BigNumber { - if (denominator.eq(0)) { - throw new LibMathRevertErrors.DivisionByZeroError(); - } - const isRoundingError = isRoundingErrorFloor(numerator, denominator, target); - if (isRoundingError) { - throw new LibMathRevertErrors.RoundingError(numerator, denominator, target); - } - const product = numerator.multipliedBy(target); - if (product.isGreaterThan(MAX_UINT256)) { - throw new SafeMathRevertErrors.SafeMathError( - SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, - numerator, - denominator, - ); - } - return product.dividedToIntegerBy(denominator); -} +const { safeGetPartialAmountFloor } = ExchangeLibsReferenceFunctions; export function calculateFillResults( order: OrderWithoutDomain, diff --git a/contracts/exchange/test/wrapper.ts b/contracts/exchange/test/wrapper.ts index 3b07144d00..9dcd4e0501 100644 --- a/contracts/exchange/test/wrapper.ts +++ b/contracts/exchange/test/wrapper.ts @@ -5,7 +5,6 @@ import { chaiSetup, constants, ERC20BalancesByOwner, - FillResults, getLatestBlockTimestampAsync, increaseTimeAndMineBlockAsync, OrderFactory, @@ -15,7 +14,7 @@ import { } from '@0x/contracts-test-utils'; import { BlockchainLifecycle } from '@0x/dev-utils'; import { assetDataUtils, ExchangeRevertErrors, orderHashUtils } from '@0x/order-utils'; -import { OrderStatus, SignedOrder } from '@0x/types'; +import { FillResults, OrderStatus, SignedOrder } from '@0x/types'; import { BigNumber, providerUtils, ReentrancyGuardRevertErrors } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import * as chai from 'chai'; From 8670fbe2aee158f33c706a5780f8eb34c7cd6c76 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 31 Jul 2019 22:19:56 -0400 Subject: [PATCH 16/48] `@0x/types`: Add `OrderInfo` type. --- packages/types/CHANGELOG.json | 2 +- packages/types/src/index.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/types/CHANGELOG.json b/packages/types/CHANGELOG.json index a7e3f894fb..89d56b3594 100644 --- a/packages/types/CHANGELOG.json +++ b/packages/types/CHANGELOG.json @@ -7,7 +7,7 @@ "pr": 1761 }, { - "note": "Add `FillResults`, `MatchedFillResults`, `BatchMatchedFillResults` types", + "note": "Add `OrderInfo`, `FillResults`, `MatchedFillResults`, `BatchMatchedFillResults` types", "pr": "TODO" } ] diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index b5bc97b63a..f9c2205d83 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -831,3 +831,9 @@ export interface BatchMatchedFillResults { profitInLeftMakerAsset: BigNumber; profitInRightMakerAsset: BigNumber; } + +export interface OrderInfo { + orderStatus: number; + orderHash: string; + orderTakerAssetFilledAmount: BigNumber; +} From abaa0cf3d02954e8a5efbb118065c11198305554 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 1 Aug 2019 01:30:51 -0400 Subject: [PATCH 17/48] `@0x/contracts-test-utils`: Move `OrderInfo` to `@0x/types`. --- contracts/test-utils/CHANGELOG.json | 2 +- contracts/test-utils/src/index.ts | 1 - contracts/test-utils/src/types.ts | 6 ------ 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/contracts/test-utils/CHANGELOG.json b/contracts/test-utils/CHANGELOG.json index 3b921bea73..ce10c25494 100644 --- a/contracts/test-utils/CHANGELOG.json +++ b/contracts/test-utils/CHANGELOG.json @@ -39,7 +39,7 @@ "pr": 2007 }, { - "note": "Move `*FillResults` types to `@0x/types`", + "note": "Move `*FillResults`, `OrderInfo` types to `@0x/types`", "pr": "TODO" }, { diff --git a/contracts/test-utils/src/index.ts b/contracts/test-utils/src/index.ts index 040ba809d7..8ac90bad3a 100644 --- a/contracts/test-utils/src/index.ts +++ b/contracts/test-utils/src/index.ts @@ -38,7 +38,6 @@ export { ERC721TokenIdsByOwner, MarketBuyOrders, MarketSellOrders, - OrderInfo, OrderStatus, Token, TransactionDataParams, diff --git a/contracts/test-utils/src/types.ts b/contracts/test-utils/src/types.ts index 5c534b43f6..84fdd18b52 100644 --- a/contracts/test-utils/src/types.ts +++ b/contracts/test-utils/src/types.ts @@ -119,12 +119,6 @@ export enum ContractName { BalanceThresholdFilter = 'BalanceThresholdFilter', } -export interface OrderInfo { - orderStatus: number; - orderHash: string; - orderTakerAssetFilledAmount: BigNumber; -} - export interface CancelOrder { order: OrderWithoutDomain; takerAssetCancelAmount: BigNumber; From 41e04c0178f9fd9c1c1f0a5e657b857665aec645 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 1 Aug 2019 01:31:28 -0400 Subject: [PATCH 18/48] `@0x/contracts-exchange`: Add a buttload of tests to `isolated_fill_order.ts`. --- .../exchange/test/isolated_fill_order.ts | 470 ++++++++++++++++-- .../exchange/test/utils/exchange_wrapper.ts | 2 +- .../test/utils/isolated_exchange_wrapper.ts | 22 +- 3 files changed, 454 insertions(+), 40 deletions(-) diff --git a/contracts/exchange/test/isolated_fill_order.ts b/contracts/exchange/test/isolated_fill_order.ts index 8baf1baaa8..709d8dff3d 100644 --- a/contracts/exchange/test/isolated_fill_order.ts +++ b/contracts/exchange/test/isolated_fill_order.ts @@ -4,33 +4,39 @@ import { expect, hexRandom, } from '@0x/contracts-test-utils'; -import { FillResults } from '@0x/types'; +import { ExchangeRevertErrors, LibMathRevertErrors } from '@0x/order-utils'; +import { FillResults, OrderInfo, OrderStatus, SignatureType } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import { AssetBalances, + createBadAssetData, + createBadSignature, createGoodAssetData, + createGoodSignature, IsolatedExchangeWrapper, - Orderish, + Order, } from './utils/isolated_exchange_wrapper'; import { calculateFillResults } from './utils/reference_functions'; -blockchainTests.resets.only('Isolated fillOrder() tests', env => { +blockchainTests.only('Isolated fillOrder() tests', env => { + const randomAddress = () => hexRandom(constants.ADDRESS_LENGTH); + const getCurrentTime = () => Math.floor(_.now() / 1000); const { ZERO_AMOUNT } = constants; - const TOMORROW = Math.floor(_.now() / 1000) + 60 * 60 * 24; const ONE_ETHER = new BigNumber(10).pow(18); - const randomAddress = () => hexRandom(constants.ADDRESS_LENGTH); - const DEFAULT_ORDER: Orderish = { + const ONE_DAY = 60 * 60 * 24; + const TOMORROW = getCurrentTime() + ONE_DAY; + const DEFAULT_ORDER: Order = { senderAddress: constants.NULL_ADDRESS, makerAddress: randomAddress(), takerAddress: constants.NULL_ADDRESS, - makerFee: ZERO_AMOUNT, - takerFee: ZERO_AMOUNT, + feeRecipientAddress: randomAddress(), makerAssetAmount: ONE_ETHER, takerAssetAmount: ONE_ETHER.times(2), + makerFee: ONE_ETHER.times(0.015), + takerFee: ONE_ETHER.times(0.025), salt: ZERO_AMOUNT, - feeRecipientAddress: constants.NULL_ADDRESS, expirationTimeSeconds: new BigNumber(TOMORROW), makerAssetData: createGoodAssetData(), takerAssetData: createGoodAssetData(), @@ -38,28 +44,37 @@ blockchainTests.resets.only('Isolated fillOrder() tests', env => { takerFeeAssetData: createGoodAssetData(), }; let takerAddress: string; + let notTakerAddress: string; let exchange: IsolatedExchangeWrapper; let nextSaltValue = 1; before(async () => { - [ takerAddress ] = await env.getAccountAddressesAsync(); + [ takerAddress, notTakerAddress ] = await env.getAccountAddressesAsync(); exchange = await IsolatedExchangeWrapper.deployAsync( env.web3Wrapper, _.assign(env.txDefaults, { from: takerAddress }), ); }); - function createOrder(details: Partial = {}): Orderish { + function createOrder(details: Partial = {}): Order { return _.assign({}, DEFAULT_ORDER, { salt: new BigNumber(nextSaltValue++) }, details); } + interface FillOrderAndAssertResultsResults { + fillResults: FillResults; + orderInfo: OrderInfo; + } + async function fillOrderAndAssertResultsAsync( - order: Orderish, + order: Order, takerAssetFillAmount: BigNumber, - ): Promise { - const efr = await calculateExpectedFillResultsAsync(order, takerAssetFillAmount); + ): Promise { + const orderInfo = await exchange.getOrderInfoAsync(order); + const efr = calculateExpectedFillResults(order, orderInfo, takerAssetFillAmount); + const eoi = calculateExpectedOrderInfo(order, orderInfo, efr); const efb = calculateExpectedFillBalances(order, efr); const fillResults = await exchange.fillOrderAsync(order, takerAssetFillAmount); + const newOrderInfo = await exchange.getOrderInfoAsync(order); // Check returned fillResults. expect(fillResults.makerAssetFilledAmount) .to.bignumber.eq(efr.makerAssetFilledAmount); @@ -72,27 +87,59 @@ blockchainTests.resets.only('Isolated fillOrder() tests', env => { // Check balances. for (const assetData of Object.keys(efb)) { for (const address of Object.keys(efb[assetData])) { - expect(exchange.getBalanceChange(assetData, address)) - .to.bignumber.eq(efb[assetData][address], `assetData: ${assetData}, address: ${address}`); + expect( + exchange.getBalanceChange(assetData, address), + `checking balance of assetData: ${assetData}, address: ${address}`, + ).to.bignumber.eq(efb[assetData][address]); } } - return fillResults; + // Check order info. + expect(newOrderInfo.orderStatus).to.eq(eoi.orderStatus); + expect(newOrderInfo.orderTakerAssetFilledAmount) + .to.bignumber.eq(eoi.orderTakerAssetFilledAmount); + // Check that there wasn't an overfill. + expect( + newOrderInfo.orderTakerAssetFilledAmount.lte(order.takerAssetAmount), + 'checking for overfill', + ).to.be.ok(''); + return { + fillResults, + orderInfo: newOrderInfo, + }; } - async function calculateExpectedFillResultsAsync( - order: Orderish, + function calculateExpectedFillResults( + order: Order, + orderInfo: OrderInfo, takerAssetFillAmount: BigNumber, - ): Promise { - const takerAssetFilledAmount = await exchange.getTakerAssetFilledAmountAsync(order); - const remainingTakerAssetAmount = order.takerAssetAmount.minus(takerAssetFilledAmount); + ): FillResults { + const remainingTakerAssetAmount = order.takerAssetAmount.minus( + orderInfo.orderTakerAssetFilledAmount, + ); return calculateFillResults( order, BigNumber.min(takerAssetFillAmount, remainingTakerAssetAmount), ); } + function calculateExpectedOrderInfo( + order: Order, + orderInfo: OrderInfo, + fillResults: FillResults, + ): OrderInfo { + const orderTakerAssetFilledAmount = + orderInfo.orderTakerAssetFilledAmount.plus(fillResults.takerAssetFilledAmount); + const orderStatus = orderTakerAssetFilledAmount.gte(order.takerAssetAmount) ? + OrderStatus.FullyFilled : OrderStatus.Fillable; + return { + orderHash: exchange.getOrderHash(order), + orderStatus, + orderTakerAssetFilledAmount, + }; + } + function calculateExpectedFillBalances( - order: Orderish, + order: Order, fillResults: FillResults, ): AssetBalances { const balances: AssetBalances = {}; @@ -112,23 +159,49 @@ blockchainTests.resets.only('Isolated fillOrder() tests', env => { return balances; } + function splitAmount(total: BigNumber, n: number = 2): BigNumber[] { + const splitSize = total.dividedToIntegerBy(n); + const splits = _.times(n - 1, () => splitSize); + splits.push(total.minus(splitSize.times(n - 1))); + return splits; + } + describe('full fills', () => { it('can fully fill an order', async () => { const order = createOrder(); - return fillOrderAndAssertResultsAsync(order, order.takerAssetAmount); + const { orderInfo } = await fillOrderAndAssertResultsAsync(order, order.takerAssetAmount); + expect(orderInfo.orderStatus).to.eq(OrderStatus.FullyFilled); }); it('can\'t overfill an order', async () => { const order = createOrder(); - return fillOrderAndAssertResultsAsync(order, order.takerAssetAmount.times(1.01)); + const { orderInfo } = await fillOrderAndAssertResultsAsync(order, order.takerAssetAmount.times(1.01)); + expect(orderInfo.orderStatus).to.eq(OrderStatus.FullyFilled); }); - it('pays maker and taker fees', async () => { + it('no fees', async () => { const order = createOrder({ - makerFee: ONE_ETHER.times(0.025), - takerFee: ONE_ETHER.times(0.035), + makerFee: ZERO_AMOUNT, + takerFee: ZERO_AMOUNT, }); - return fillOrderAndAssertResultsAsync(order, order.takerAssetAmount); + const { orderInfo } = await fillOrderAndAssertResultsAsync(order, order.takerAssetAmount); + expect(orderInfo.orderStatus).to.eq(OrderStatus.FullyFilled); + }); + + it('only maker fees', async () => { + const order = createOrder({ + takerFee: ZERO_AMOUNT, + }); + const { orderInfo } = await fillOrderAndAssertResultsAsync(order, order.takerAssetAmount); + expect(orderInfo.orderStatus).to.eq(OrderStatus.FullyFilled); + }); + + it('only taker fees', async () => { + const order = createOrder({ + makerFee: ZERO_AMOUNT, + }); + const { orderInfo } = await fillOrderAndAssertResultsAsync(order, order.takerAssetAmount); + expect(orderInfo.orderStatus).to.eq(OrderStatus.FullyFilled); }); }); @@ -137,15 +210,344 @@ blockchainTests.resets.only('Isolated fillOrder() tests', env => { it('can partially fill an order', async () => { const order = createOrder(); - return fillOrderAndAssertResultsAsync(order, takerAssetFillAmount); + const { orderInfo } = await fillOrderAndAssertResultsAsync(order, takerAssetFillAmount); + expect(orderInfo.orderStatus).to.eq(OrderStatus.Fillable); + }); + + it('no fees', async () => { + const order = createOrder({ + makerFee: ZERO_AMOUNT, + takerFee: ZERO_AMOUNT, + }); + const { orderInfo } = await fillOrderAndAssertResultsAsync(order, takerAssetFillAmount); + expect(orderInfo.orderStatus).to.eq(OrderStatus.Fillable); }); - it('pays maker and taker fees', async () => { + it('only maker fees', async () => { const order = createOrder({ - makerFee: ONE_ETHER.times(0.025), - takerFee: ONE_ETHER.times(0.035), + takerFee: ZERO_AMOUNT, }); - return fillOrderAndAssertResultsAsync(order, takerAssetFillAmount); + const { orderInfo } = await fillOrderAndAssertResultsAsync(order, takerAssetFillAmount); + expect(orderInfo.orderStatus).to.eq(OrderStatus.Fillable); + }); + + it('only taker fees', async () => { + const order = createOrder({ + makerFee: ZERO_AMOUNT, + }); + const { orderInfo } = await fillOrderAndAssertResultsAsync(order, takerAssetFillAmount); + expect(orderInfo.orderStatus).to.eq(OrderStatus.Fillable); + }); + }); + + describe('multiple fills', () => { + it('can fully fill an order in two fills', async () => { + const order = createOrder(); + const fillAmounts = splitAmount(order.takerAssetAmount); + const orderInfos = [ + (await fillOrderAndAssertResultsAsync(order, fillAmounts[0])).orderInfo, + (await fillOrderAndAssertResultsAsync(order, fillAmounts[1])).orderInfo, + ]; + expect(orderInfos[0].orderStatus).to.eq(OrderStatus.Fillable); + expect(orderInfos[1].orderStatus).to.eq(OrderStatus.FullyFilled); + }); + + it('can partially fill an order in two fills', async () => { + const order = createOrder(); + const fillAmounts = splitAmount(order.takerAssetAmount.dividedToIntegerBy(2)); + const orderInfos = [ + (await fillOrderAndAssertResultsAsync(order, fillAmounts[0])).orderInfo, + (await fillOrderAndAssertResultsAsync(order, fillAmounts[1])).orderInfo, + ]; + expect(orderInfos[0].orderStatus).to.eq(OrderStatus.Fillable); + expect(orderInfos[1].orderStatus).to.eq(OrderStatus.Fillable); + }); + + it('can\'t overfill an order in two fills', async () => { + const order = createOrder(); + const fillAmounts = splitAmount(order.takerAssetAmount); + fillAmounts[0] = fillAmounts[0].times(1.01); + const orderInfos = [ + (await fillOrderAndAssertResultsAsync(order, fillAmounts[0])).orderInfo, + (await fillOrderAndAssertResultsAsync(order, fillAmounts[1])).orderInfo, + ]; + expect(orderInfos[0].orderStatus).to.eq(OrderStatus.Fillable); + expect(orderInfos[1].orderStatus).to.eq(OrderStatus.FullyFilled); + }); + + it('can fully fill an order with no fees in two fills', async () => { + const order = createOrder({ + makerFee: ZERO_AMOUNT, + takerFee: ZERO_AMOUNT, + }); + const fillAmounts = splitAmount(order.takerAssetAmount); + const orderInfos = [ + (await fillOrderAndAssertResultsAsync(order, fillAmounts[0])).orderInfo, + (await fillOrderAndAssertResultsAsync(order, fillAmounts[1])).orderInfo, + ]; + expect(orderInfos[0].orderStatus).to.eq(OrderStatus.Fillable); + expect(orderInfos[1].orderStatus).to.eq(OrderStatus.FullyFilled); + }); + + it('can fully fill an order with only maker fees in two fills', async () => { + const order = createOrder({ + takerFee: ZERO_AMOUNT, + }); + const fillAmounts = splitAmount(order.takerAssetAmount); + const orderInfos = [ + (await fillOrderAndAssertResultsAsync(order, fillAmounts[0])).orderInfo, + (await fillOrderAndAssertResultsAsync(order, fillAmounts[1])).orderInfo, + ]; + expect(orderInfos[0].orderStatus).to.eq(OrderStatus.Fillable); + expect(orderInfos[1].orderStatus).to.eq(OrderStatus.FullyFilled); + }); + + it('can fully fill an order with only taker fees in two fills', async () => { + const order = createOrder({ + makerFee: ZERO_AMOUNT, + }); + const fillAmounts = splitAmount(order.takerAssetAmount); + const orderInfos = [ + (await fillOrderAndAssertResultsAsync(order, fillAmounts[0])).orderInfo, + (await fillOrderAndAssertResultsAsync(order, fillAmounts[1])).orderInfo, + ]; + expect(orderInfos[0].orderStatus).to.eq(OrderStatus.Fillable); + expect(orderInfos[1].orderStatus).to.eq(OrderStatus.FullyFilled); + }); + }); + + describe('bad fills', () => { + it('can\'t fill an order with zero takerAssetAmount', async () => { + const order = createOrder({ + takerAssetAmount: ZERO_AMOUNT, + }); + const expectedError = new ExchangeRevertErrors.OrderStatusError( + exchange.getOrderHash(order), + OrderStatus.InvalidTakerAssetAmount, + ); + return expect(exchange.fillOrderAsync(order, ONE_ETHER)) + .to.revertWith(expectedError); + }); + + it('can\'t fill an order with zero makerAssetAmount', async () => { + const order = createOrder({ + makerAssetAmount: ZERO_AMOUNT, + }); + const expectedError = new ExchangeRevertErrors.OrderStatusError( + exchange.getOrderHash(order), + OrderStatus.InvalidMakerAssetAmount, + ); + return expect(exchange.fillOrderAsync(order, ONE_ETHER)) + .to.revertWith(expectedError); + }); + + it('can\'t fill an order that is fully filled', async () => { + const order = createOrder(); + const expectedError = new ExchangeRevertErrors.OrderStatusError( + exchange.getOrderHash(order), + OrderStatus.FullyFilled, + ); + await exchange.fillOrderAsync(order, order.takerAssetAmount); + return expect(exchange.fillOrderAsync(order, 1)) + .to.revertWith(expectedError); + }); + + it('can\'t fill an order that is expired', async () => { + const order = createOrder({ + expirationTimeSeconds: new BigNumber(getCurrentTime() - 60), + }); + const expectedError = new ExchangeRevertErrors.OrderStatusError( + exchange.getOrderHash(order), + OrderStatus.Expired, + ); + return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)) + .to.revertWith(expectedError); + }); + + it('can\'t fill an order that is cancelled by `cancelOrder()`', async () => { + const order = createOrder({ + makerAddress: notTakerAddress, + }); + const expectedError = new ExchangeRevertErrors.OrderStatusError( + exchange.getOrderHash(order), + OrderStatus.Cancelled, + ); + await exchange.cancelOrderAsync(order, { from: notTakerAddress }); + return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)) + .to.revertWith(expectedError); + }); + + it('can\'t fill an order that is cancelled by `cancelOrdersUpTo()`', async () => { + const order = createOrder({ + makerAddress: notTakerAddress, + }); + const expectedError = new ExchangeRevertErrors.OrderStatusError( + exchange.getOrderHash(order), + OrderStatus.Cancelled, + ); + await exchange.cancelOrdersUpToAsync(order.salt, { from: notTakerAddress }); + return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)) + .to.revertWith(expectedError); + }); + + it('can\'t fill an order if taker is not `takerAddress`', async () => { + const order = createOrder({ + takerAddress: randomAddress(), + }); + const expectedError = new ExchangeRevertErrors.InvalidTakerError( + exchange.getOrderHash(order), + takerAddress, + ); + return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)) + .to.revertWith(expectedError); + }); + + it('can\'t fill an order if sender is not `senderAddress`', async () => { + const order = createOrder({ + senderAddress: randomAddress(), + }); + const expectedError = new ExchangeRevertErrors.InvalidSenderError( + exchange.getOrderHash(order), + takerAddress, + ); + return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)) + .to.revertWith(expectedError); + }); + + it('can\'t fill an order with a taker amount that results in a maker asset rounding error', async () => { + const order = createOrder({ + makerAssetAmount: new BigNumber(100), + takerAssetAmount: ONE_ETHER, + }); + const takerAssetFillAmount = order.takerAssetAmount.dividedToIntegerBy(3); + const expectedError = new LibMathRevertErrors.RoundingError( + takerAssetFillAmount, + order.takerAssetAmount, + order.makerAssetAmount, + ); + return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)) + .to.revertWith(expectedError); + }); + + it('can\'t fill an order with a taker amount that results in a maker fee rounding error', async () => { + const order = createOrder({ + makerAssetAmount: ONE_ETHER.times(2), + takerAssetAmount: ONE_ETHER, + makerFee: new BigNumber(100), + }); + const takerAssetFillAmount = order.takerAssetAmount.dividedToIntegerBy(3); + const expectedError = new LibMathRevertErrors.RoundingError( + takerAssetFillAmount.times(2), + order.makerAssetAmount, + order.makerFee, + ); + return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)) + .to.revertWith(expectedError); + }); + + it('can\'t fill an order with a taker amount that results in a taker fee rounding error', async () => { + const order = createOrder({ + makerAssetAmount: ONE_ETHER.times(2), + takerAssetAmount: ONE_ETHER, + takerFee: new BigNumber(100), + }); + const takerAssetFillAmount = order.takerAssetAmount.dividedToIntegerBy(3); + const expectedError = new LibMathRevertErrors.RoundingError( + takerAssetFillAmount, + order.takerAssetAmount, + order.takerFee, + ); + return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)) + .to.revertWith(expectedError); + }); + + it('can\'t fill an order with a bad signature', async () => { + const order = createOrder(); + const signature = createBadSignature(); + const expectedError = new ExchangeRevertErrors.SignatureError( + ExchangeRevertErrors.SignatureErrorCode.BadSignature, + exchange.getOrderHash(order), + order.makerAddress, + signature, + ); + return expect(exchange.fillOrderAsync(order, order.takerAssetAmount, signature)) + .to.revertWith(expectedError); + }); + + it('can\'t complementary fill an order with a bad signature that is always checked', async () => { + const order = createOrder(); + const takerAssetFillAmounts = splitAmount(order.takerAssetAmount); + const goodSignature = createGoodSignature(SignatureType.Wallet); + const badSignature = createBadSignature(SignatureType.Wallet); + const expectedError = new ExchangeRevertErrors.SignatureError( + ExchangeRevertErrors.SignatureErrorCode.BadSignature, + exchange.getOrderHash(order), + order.makerAddress, + badSignature, + ); + await exchange.fillOrderAsync(order, takerAssetFillAmounts[0], goodSignature); + return expect(exchange.fillOrderAsync(order, takerAssetFillAmounts[1], badSignature)) + .to.revertWith(expectedError); + }); + + const TRANSFER_ERROR = 'TRANSFER_FAILED'; + + it('can\'t fill an order with a maker asset that fails to transfer', async () => { + const order = createOrder({ + makerAssetData: createBadAssetData(), + }); + return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)) + .to.revertWith(TRANSFER_ERROR); + }); + + it('can\'t fill an order with a taker asset that fails to transfer', async () => { + const order = createOrder({ + takerAssetData: createBadAssetData(), + }); + return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)) + .to.revertWith(TRANSFER_ERROR); + }); + + it('can\'t fill an order with a maker fee asset that fails to transfer', async () => { + const order = createOrder({ + makerFeeAssetData: createBadAssetData(), + }); + return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)) + .to.revertWith(TRANSFER_ERROR); + }); + + it('can\'t fill an order with a taker fee asset that fails to transfer', async () => { + const order = createOrder({ + takerFeeAssetData: createBadAssetData(), + }); + return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)) + .to.revertWith(TRANSFER_ERROR); + }); + }); + + describe('permitted fills', () => { + it('can fill an order if taker is `takerAddress`', async () => { + const order = createOrder({ + takerAddress, + }); + return fillOrderAndAssertResultsAsync(order, order.takerAssetAmount); + }); + + it('can fill an order if sender is `senderAddress`', async () => { + const order = createOrder({ + senderAddress: takerAddress, + }); + return fillOrderAndAssertResultsAsync(order, order.takerAssetAmount); + }); + + it('can complementary fill an order with a bad signature that is checked only once', async () => { + const order = createOrder(); + const takerAssetFillAmounts = splitAmount(order.takerAssetAmount); + const goodSignature = createGoodSignature(SignatureType.EthSign); + const badSignature = createBadSignature(SignatureType.EthSign); + await exchange.fillOrderAsync(order, takerAssetFillAmounts[0], goodSignature); + await exchange.fillOrderAsync(order, takerAssetFillAmounts[1], badSignature); }); }); }); +// tslint:disable: max-file-line-count diff --git a/contracts/exchange/test/utils/exchange_wrapper.ts b/contracts/exchange/test/utils/exchange_wrapper.ts index 3152cd2fda..ddf62f79ac 100644 --- a/contracts/exchange/test/utils/exchange_wrapper.ts +++ b/contracts/exchange/test/utils/exchange_wrapper.ts @@ -4,7 +4,6 @@ import { artifacts as erc721Artifacts } from '@0x/contracts-erc721'; import { BatchMatchOrder, LogDecoder, - OrderInfo, orderUtils, Web3ProviderEngine, } from '@0x/contracts-test-utils'; @@ -12,6 +11,7 @@ import { BatchMatchedFillResults, FillResults, MatchedFillResults, + OrderInfo, SignedOrder, SignedZeroExTransaction, } from '@0x/types'; diff --git a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts index e887839a67..27d65486d9 100644 --- a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts +++ b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts @@ -1,6 +1,6 @@ import { constants, filterLogsToArguments, LogDecoder, txDefaults as testTxDefaults } from '@0x/contracts-test-utils'; import { orderHashUtils } from '@0x/order-utils'; -import { FillResults, OrderWithoutDomain, SignatureType } from '@0x/types'; +import { FillResults, OrderInfo, OrderWithoutDomain, SignatureType } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { TxData, Web3Wrapper } from '@0x/web3-wrapper'; import * as crypto from 'crypto'; @@ -23,7 +23,7 @@ export interface IsolatedExchangeEvents { transferFromCalls: DispatchTransferFromCallArgs[]; } -export type Orderish = OrderWithoutDomain; +export type Order = OrderWithoutDomain; export type Numberish = string | number | BigNumber; export const DEFAULT_GOOD_SIGNATURE = createGoodSignature(); @@ -67,12 +67,20 @@ export class IsolatedExchangeWrapper { this.logDecoder = new LogDecoder(web3Wrapper, artifacts); } - public async getTakerAssetFilledAmountAsync(order: Orderish): Promise { + public async getTakerAssetFilledAmountAsync(order: Order): Promise { return this.instance.filled.callAsync(this.getOrderHash(order)); } + public async cancelOrderAsync(order: Order, txOpts?: TxData): Promise { + await this.instance.cancelOrder.awaitTransactionSuccessAsync(order, txOpts); + } + + public async cancelOrdersUpToAsync(epoch: BigNumber, txOpts?: TxData): Promise { + await this.instance.cancelOrdersUpTo.awaitTransactionSuccessAsync(epoch, txOpts); + } + public async fillOrderAsync( - order: Orderish, + order: Order, takerAssetFillAmount: Numberish, signature: string = DEFAULT_GOOD_SIGNATURE, txOpts?: TxData, @@ -86,7 +94,7 @@ export class IsolatedExchangeWrapper { ); } - public getOrderHash(order: Orderish): string { + public getOrderHash(order: Order): string { const domain = { verifyingContractAddress: this.instance.address, chainId: IsolatedExchangeWrapper.CHAIN_ID, @@ -94,6 +102,10 @@ export class IsolatedExchangeWrapper { return orderHashUtils.getOrderHashHex(_.assign(order, { domain })); } + public async getOrderInfoAsync(order: Order): Promise { + return this.instance.getOrderInfo.callAsync(order); + } + public getBalanceChange(assetData: string, address: string): BigNumber { if (assetData in this.lastTxBalanceChanges) { const balances = this.lastTxBalanceChanges[assetData]; From 9d5b23acd39b9445befc131f0148c28d4f8dc7ba Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 1 Aug 2019 01:37:01 -0400 Subject: [PATCH 19/48] `@0x/contracts-exchange`: Update tests in `isolated_fill_order.ts`. --- contracts/exchange/test/isolated_fill_order.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/contracts/exchange/test/isolated_fill_order.ts b/contracts/exchange/test/isolated_fill_order.ts index 709d8dff3d..fad7d368c9 100644 --- a/contracts/exchange/test/isolated_fill_order.ts +++ b/contracts/exchange/test/isolated_fill_order.ts @@ -34,8 +34,8 @@ blockchainTests.only('Isolated fillOrder() tests', env => { feeRecipientAddress: randomAddress(), makerAssetAmount: ONE_ETHER, takerAssetAmount: ONE_ETHER.times(2), - makerFee: ONE_ETHER.times(0.015), - takerFee: ONE_ETHER.times(0.025), + makerFee: ONE_ETHER.times(0.0015), + takerFee: ONE_ETHER.times(0.0025), salt: ZERO_AMOUNT, expirationTimeSeconds: new BigNumber(TOMORROW), makerAssetData: createGoodAssetData(), @@ -68,12 +68,14 @@ blockchainTests.only('Isolated fillOrder() tests', env => { async function fillOrderAndAssertResultsAsync( order: Order, takerAssetFillAmount: BigNumber, + signature?: string, ): Promise { const orderInfo = await exchange.getOrderInfoAsync(order); const efr = calculateExpectedFillResults(order, orderInfo, takerAssetFillAmount); const eoi = calculateExpectedOrderInfo(order, orderInfo, efr); const efb = calculateExpectedFillBalances(order, efr); - const fillResults = await exchange.fillOrderAsync(order, takerAssetFillAmount); + // Fill the order. + const fillResults = await exchange.fillOrderAsync(order, takerAssetFillAmount, signature); const newOrderInfo = await exchange.getOrderInfoAsync(order); // Check returned fillResults. expect(fillResults.makerAssetFilledAmount) @@ -545,8 +547,8 @@ blockchainTests.only('Isolated fillOrder() tests', env => { const takerAssetFillAmounts = splitAmount(order.takerAssetAmount); const goodSignature = createGoodSignature(SignatureType.EthSign); const badSignature = createBadSignature(SignatureType.EthSign); - await exchange.fillOrderAsync(order, takerAssetFillAmounts[0], goodSignature); - await exchange.fillOrderAsync(order, takerAssetFillAmounts[1], badSignature); + await fillOrderAndAssertResultsAsync(order, takerAssetFillAmounts[0], goodSignature); + await fillOrderAndAssertResultsAsync(order, takerAssetFillAmounts[1], badSignature); }); }); }); From a3cdb63ae1e5f080091661f52d30f311f00391a7 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 1 Aug 2019 11:31:57 -0400 Subject: [PATCH 20/48] `@0x/contracts-exchange`: Rename `TestIsolatedExchange` to just `IsolatedExchange`. --- contracts/exchange/compiler.json | 2 +- ...latedExchange.sol => IsolatedExchange.sol} | 4 ++- contracts/exchange/package.json | 2 +- contracts/exchange/src/artifacts.ts | 4 +-- contracts/exchange/src/wrappers.ts | 2 +- .../test/utils/isolated_exchange_wrapper.ts | 26 +++++++++---------- contracts/exchange/tsconfig.json | 2 +- 7 files changed, 22 insertions(+), 20 deletions(-) rename contracts/exchange/contracts/test/{TestIsolatedExchange.sol => IsolatedExchange.sol} (94%) diff --git a/contracts/exchange/compiler.json b/contracts/exchange/compiler.json index be13b3e3b9..341e06c30a 100644 --- a/contracts/exchange/compiler.json +++ b/contracts/exchange/compiler.json @@ -36,11 +36,11 @@ "src/interfaces/ITransactions.sol", "src/interfaces/IWallet.sol", "src/interfaces/IWrapperFunctions.sol", + "test/IsolatedExchange.sol", "test/ReentrantERC20Token.sol", "test/TestAssetProxyDispatcher.sol", "test/TestExchangeInternals.sol", "test/TestExchangeMath.sol", - "test/TestIsolatedExchange.sol", "test/TestLibExchangeRichErrorDecoder.sol", "test/TestSignatureValidator.sol", "test/TestValidatorWallet.sol" diff --git a/contracts/exchange/contracts/test/TestIsolatedExchange.sol b/contracts/exchange/contracts/test/IsolatedExchange.sol similarity index 94% rename from contracts/exchange/contracts/test/TestIsolatedExchange.sol rename to contracts/exchange/contracts/test/IsolatedExchange.sol index 9ebcee373a..e670550f9d 100644 --- a/contracts/exchange/contracts/test/TestIsolatedExchange.sol +++ b/contracts/exchange/contracts/test/IsolatedExchange.sol @@ -24,7 +24,9 @@ import "../src/Exchange.sol"; /// @dev A version of the Exchange contract with simplified signature validation /// and a `_dispatchTransferFrom()` that only logs arguments. -contract TestIsolatedExchange is +/// See the `IsolatedExchangeWrapper` and `isolated_fill_order` tests +/// for example usage. +contract IsolatedExchange is Exchange { // solhint-disable no-unused-vars diff --git a/contracts/exchange/package.json b/contracts/exchange/package.json index 930c64852b..0f7ce901b5 100644 --- a/contracts/exchange/package.json +++ b/contracts/exchange/package.json @@ -34,7 +34,7 @@ "lint-contracts": "solhint -c ../.solhint.json contracts/**/**/**/**/*.sol" }, "config": { - "abis": "./generated-artifacts/@(Exchange|ExchangeWrapper|IAssetProxyDispatcher|IEIP1271Wallet|IExchange|IExchangeCore|IMatchOrders|ISignatureValidator|ITransactions|IWallet|IWrapperFunctions|ReentrantERC20Token|TestAssetProxyDispatcher|TestExchangeInternals|TestExchangeMath|TestIsolatedExchange|TestLibExchangeRichErrorDecoder|TestSignatureValidator|TestValidatorWallet|Whitelist).json", + "abis": "./generated-artifacts/@(Exchange|ExchangeWrapper|IAssetProxyDispatcher|IEIP1271Wallet|IExchange|IExchangeCore|IMatchOrders|ISignatureValidator|ITransactions|IWallet|IWrapperFunctions|IsolatedExchange|ReentrantERC20Token|TestAssetProxyDispatcher|TestExchangeInternals|TestExchangeMath|TestLibExchangeRichErrorDecoder|TestSignatureValidator|TestValidatorWallet|Whitelist).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/exchange/src/artifacts.ts b/contracts/exchange/src/artifacts.ts index 169fb06c75..7ac7a00b37 100644 --- a/contracts/exchange/src/artifacts.ts +++ b/contracts/exchange/src/artifacts.ts @@ -16,11 +16,11 @@ import * as ISignatureValidator from '../generated-artifacts/ISignatureValidator import * as ITransactions from '../generated-artifacts/ITransactions.json'; import * as IWallet from '../generated-artifacts/IWallet.json'; import * as IWrapperFunctions from '../generated-artifacts/IWrapperFunctions.json'; +import * as IsolatedExchange from '../generated-artifacts/IsolatedExchange.json'; import * as ReentrantERC20Token from '../generated-artifacts/ReentrantERC20Token.json'; import * as TestAssetProxyDispatcher from '../generated-artifacts/TestAssetProxyDispatcher.json'; import * as TestExchangeInternals from '../generated-artifacts/TestExchangeInternals.json'; import * as TestExchangeMath from '../generated-artifacts/TestExchangeMath.json'; -import * as TestIsolatedExchange from '../generated-artifacts/TestIsolatedExchange.json'; import * as TestLibExchangeRichErrorDecoder from '../generated-artifacts/TestLibExchangeRichErrorDecoder.json'; import * as TestSignatureValidator from '../generated-artifacts/TestSignatureValidator.json'; import * as TestValidatorWallet from '../generated-artifacts/TestValidatorWallet.json'; @@ -42,7 +42,7 @@ export const artifacts = { TestAssetProxyDispatcher: TestAssetProxyDispatcher as ContractArtifact, TestExchangeInternals: TestExchangeInternals as ContractArtifact, TestExchangeMath: TestExchangeMath as ContractArtifact, - TestIsolatedExchange: TestIsolatedExchange as ContractArtifact, + IsolatedExchange: IsolatedExchange as ContractArtifact, TestLibExchangeRichErrorDecoder: TestLibExchangeRichErrorDecoder as ContractArtifact, TestSignatureValidator: TestSignatureValidator as ContractArtifact, TestValidatorWallet: TestValidatorWallet as ContractArtifact, diff --git a/contracts/exchange/src/wrappers.ts b/contracts/exchange/src/wrappers.ts index dae2960063..d96c39afcb 100644 --- a/contracts/exchange/src/wrappers.ts +++ b/contracts/exchange/src/wrappers.ts @@ -14,11 +14,11 @@ export * from '../generated-wrappers/i_signature_validator'; export * from '../generated-wrappers/i_transactions'; export * from '../generated-wrappers/i_wallet'; export * from '../generated-wrappers/i_wrapper_functions'; +export * from '../generated-wrappers/isolated_exchange'; export * from '../generated-wrappers/reentrant_erc20_token'; export * from '../generated-wrappers/test_asset_proxy_dispatcher'; export * from '../generated-wrappers/test_exchange_internals'; export * from '../generated-wrappers/test_exchange_math'; -export * from '../generated-wrappers/test_isolated_exchange'; export * from '../generated-wrappers/test_lib_exchange_rich_error_decoder'; export * from '../generated-wrappers/test_signature_validator'; export * from '../generated-wrappers/test_validator_wallet'; diff --git a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts index 27d65486d9..7f92c80479 100644 --- a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts +++ b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts @@ -9,9 +9,9 @@ import * as _ from 'lodash'; import { artifacts, - TestIsolatedExchangeContract, - TestIsolatedExchangeDispatchTransferFromCalledEventArgs as DispatchTransferFromCallArgs, - TestIsolatedExchangeFillEventArgs as FillEventArgs, + IsolatedExchangeContract, + IsolatedExchangeDispatchTransferFromCalledEventArgs as DispatchTransferFromCallArgs, + IsolatedExchangeFillEventArgs as FillEventArgs, } from '../../src'; export interface AssetBalances { @@ -30,11 +30,11 @@ export const DEFAULT_GOOD_SIGNATURE = createGoodSignature(); export const DEFAULT_BAD_SIGNATURE = createBadSignature(); /** - * @dev Convenience wrapper for the `TestIsolatedExchange` contract. + * @dev Convenience wrapper for the `IsolatedExchange` contract. */ export class IsolatedExchangeWrapper { public static readonly CHAIN_ID = 1337; - public instance: TestIsolatedExchangeContract; + public instance: IsolatedExchangeContract; public logDecoder: LogDecoder; public lastTxEvents: IsolatedExchangeEvents = createEmptyEvents(); public lastTxBalanceChanges: AssetBalances = {}; @@ -44,8 +44,8 @@ export class IsolatedExchangeWrapper { txDefaults: Partial = testTxDefaults, ): Promise { const provider = web3Wrapper.getProvider(); - const instance = await TestIsolatedExchangeContract.deployFrom0xArtifactAsync( - artifacts.TestIsolatedExchange, + const instance = await IsolatedExchangeContract.deployFrom0xArtifactAsync( + artifacts.IsolatedExchange, provider, txDefaults, ); @@ -58,11 +58,11 @@ export class IsolatedExchangeWrapper { txDefaults: Partial = testTxDefaults, ): IsolatedExchangeWrapper { const provider = web3Wrapper.getProvider(); - const instance = new TestIsolatedExchangeContract(address, provider, txDefaults); + const instance = new IsolatedExchangeContract(address, provider, txDefaults); return new IsolatedExchangeWrapper(web3Wrapper, instance); } - public constructor(web3Wrapper: Web3Wrapper, instance: TestIsolatedExchangeContract) { + public constructor(web3Wrapper: Web3Wrapper, instance: IsolatedExchangeContract) { this.instance = instance; this.logDecoder = new LogDecoder(web3Wrapper, artifacts); } @@ -143,14 +143,14 @@ interface TransactionContractFunction { } /** - * Create a signature for the `TestIsolatedExchange` contract that will pass. + * Create a signature for the `IsolatedExchange` contract that will pass. */ export function createGoodSignature(type: SignatureType = SignatureType.EIP712): string { return `0x01${Buffer.from([type]).toString('hex')}`; } /** - * Create a signature for the `TestIsolatedExchange` contract that will fail. + * Create a signature for the `IsolatedExchange` contract that will fail. */ export function createBadSignature(type: SignatureType = SignatureType.EIP712): string { return `0x00${Buffer.from([type]).toString('hex')}`; @@ -159,14 +159,14 @@ export function createBadSignature(type: SignatureType = SignatureType.EIP712): const ERC20_ASSET_DATA_LENGTH = 24; /** - * Create asset data for the `TestIsolatedExchange` contract that will pass. + * Create asset data for the `IsolatedExchange` contract that will pass. */ export function createGoodAssetData(length: number = ERC20_ASSET_DATA_LENGTH): string { return `0x01${crypto.randomBytes(length - 1).toString('hex')}`; } /** - * Create asset data for the `TestIsolatedExchange` contract that will fail. + * Create asset data for the `IsolatedExchange` contract that will fail. */ export function createBadAssetData(length: number = ERC20_ASSET_DATA_LENGTH): string { return `0x00${crypto.randomBytes(length - 1).toString('hex')}`; diff --git a/contracts/exchange/tsconfig.json b/contracts/exchange/tsconfig.json index 925c1a198d..3d2713ff02 100644 --- a/contracts/exchange/tsconfig.json +++ b/contracts/exchange/tsconfig.json @@ -14,11 +14,11 @@ "generated-artifacts/ITransactions.json", "generated-artifacts/IWallet.json", "generated-artifacts/IWrapperFunctions.json", + "generated-artifacts/IsolatedExchange.json", "generated-artifacts/ReentrantERC20Token.json", "generated-artifacts/TestAssetProxyDispatcher.json", "generated-artifacts/TestExchangeInternals.json", "generated-artifacts/TestExchangeMath.json", - "generated-artifacts/TestIsolatedExchange.json", "generated-artifacts/TestLibExchangeRichErrorDecoder.json", "generated-artifacts/TestSignatureValidator.json", "generated-artifacts/TestValidatorWallet.json", From 8d26f58dfa0ddd6ce16280362d07021c9e5a7ad9 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 1 Aug 2019 12:56:08 -0400 Subject: [PATCH 21/48] `@0x/contracts-test-utils`: Make `testCombinatoriallyWithReferenceFuncAsync` not async. --- .../test-utils/src/combinatorial_utils.ts | 20 +++++++++---------- contracts/test-utils/src/index.ts | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/test-utils/src/combinatorial_utils.ts b/contracts/test-utils/src/combinatorial_utils.ts index bb1b55b4dd..a50190a6eb 100644 --- a/contracts/test-utils/src/combinatorial_utils.ts +++ b/contracts/test-utils/src/combinatorial_utils.ts @@ -36,30 +36,30 @@ export const bytes32Values = [ '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', ]; -export async function testCombinatoriallyWithReferenceFuncAsync( +export function testCombinatoriallyWithReferenceFunc( name: string, referenceFunc: (p0: P0, p1: P1) => Promise, testFunc: (p0: P0, p1: P1) => Promise, allValues: [P0[], P1[]], -): Promise; -export async function testCombinatoriallyWithReferenceFuncAsync( +): void; +export function testCombinatoriallyWithReferenceFunc( name: string, referenceFunc: (p0: P0, p1: P1, p2: P2) => Promise, testFunc: (p0: P0, p1: P1, p2: P2) => Promise, allValues: [P0[], P1[], P2[]], -): Promise; -export async function testCombinatoriallyWithReferenceFuncAsync( +): void; +export function testCombinatoriallyWithReferenceFunc( name: string, referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise, testFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise, allValues: [P0[], P1[], P2[], P3[]], -): Promise; -export async function testCombinatoriallyWithReferenceFuncAsync( +): void; +export function testCombinatoriallyWithReferenceFunc( name: string, referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise, testFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise, allValues: [P0[], P1[], P2[], P3[], P4[]], -): Promise; +): void; /** * Uses combinatorics to test the behavior of a test function by comparing it to @@ -96,12 +96,12 @@ export async function testCombinatoriallyWithReferenceFuncAsync Promise, testFuncAsync: (...args: any[]) => Promise, allValues: any[], -): Promise { +): void { const testCases = combinatorics.cartesianProduct(...allValues); let counter = 0; testCases.forEach(async testCase => { diff --git a/contracts/test-utils/src/index.ts b/contracts/test-utils/src/index.ts index 8ac90bad3a..710c7b5c9e 100644 --- a/contracts/test-utils/src/index.ts +++ b/contracts/test-utils/src/index.ts @@ -24,7 +24,7 @@ export { coverage } from './coverage'; export { Web3ProviderEngine } from '@0x/subproviders'; export { addressUtils } from './address_utils'; export { OrderFactory } from './order_factory'; -export { bytes32Values, testCombinatoriallyWithReferenceFuncAsync, uint256Values } from './combinatorial_utils'; +export { bytes32Values, testCombinatoriallyWithReferenceFunc, uint256Values } from './combinatorial_utils'; export { TransactionFactory } from './transaction_factory'; export { testWithReferenceFuncAsync } from './test_with_reference'; export { hexConcat, hexRandom } from './hex_utils'; From 5a088690b2c2bb794a371870ebbfee265109aa80 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 1 Aug 2019 13:08:50 -0400 Subject: [PATCH 22/48] `@0x/utils`: Add `SafeMathRevertErrors.SafeMathErrorCodes.Uint256DivisionByZero`. --- packages/utils/src/safe_math_revert_errors.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/utils/src/safe_math_revert_errors.ts b/packages/utils/src/safe_math_revert_errors.ts index 42cbf1e355..023e85e4b8 100644 --- a/packages/utils/src/safe_math_revert_errors.ts +++ b/packages/utils/src/safe_math_revert_errors.ts @@ -5,6 +5,7 @@ export enum SafeMathErrorCodes { Uint256AdditionOverflow, Uint256MultiplicationOverflow, Uint256SubtractionUnderflow, + Uint256DivisionByZero, } export class SafeMathError extends RevertError { From d03f13a72929d680ea3ec3123edb9795ccf14049 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 1 Aug 2019 13:10:48 -0400 Subject: [PATCH 23/48] `@0x/contracts-utils`: `LibMath._safeDiv()` now throws a rich revert when dividing by zero. --- .../contracts/src/LibSafeMathRichErrors.sol | 3 ++- contracts/utils/contracts/src/SafeMath.sol | 7 +++++++ contracts/utils/src/reference_functions.ts | 16 +++++++++++----- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/contracts/utils/contracts/src/LibSafeMathRichErrors.sol b/contracts/utils/contracts/src/LibSafeMathRichErrors.sol index a1f39cc41d..0b13af3706 100644 --- a/contracts/utils/contracts/src/LibSafeMathRichErrors.sol +++ b/contracts/utils/contracts/src/LibSafeMathRichErrors.sol @@ -10,7 +10,8 @@ library LibSafeMathRichErrors { enum SafeMathErrorCodes { UINT256_ADDITION_OVERFLOW, UINT256_MULTIPLICATION_OVERFLOW, - UINT256_SUBTRACTION_UNDERFLOW + UINT256_SUBTRACTION_UNDERFLOW, + UINT256_DIVISION_BY_ZERO } // solhint-disable func-name-mixedcase diff --git a/contracts/utils/contracts/src/SafeMath.sol b/contracts/utils/contracts/src/SafeMath.sol index ed22105a9a..1edc1bac93 100644 --- a/contracts/utils/contracts/src/SafeMath.sol +++ b/contracts/utils/contracts/src/SafeMath.sol @@ -30,6 +30,13 @@ contract SafeMath { pure returns (uint256) { + if (b == 0) { + LibRichErrors._rrevert(LibSafeMathRichErrors.SafeMathError( + LibSafeMathRichErrors.SafeMathErrorCodes.UINT256_DIVISION_BY_ZERO, + a, + b + )); + } uint256 c = a / b; return c; } diff --git a/contracts/utils/src/reference_functions.ts b/contracts/utils/src/reference_functions.ts index 717baafc09..e623ef639f 100644 --- a/contracts/utils/src/reference_functions.ts +++ b/contracts/utils/src/reference_functions.ts @@ -1,4 +1,4 @@ -import { AnyRevertError, BigNumber, SafeMathRevertErrors } from '@0x/utils'; +import { BigNumber, SafeMathRevertErrors } from '@0x/utils'; const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); @@ -29,16 +29,22 @@ export function safeSub(a: BigNumber, b: BigNumber): BigNumber { export function safeMul(a: BigNumber, b: BigNumber): BigNumber { const r = a.times(b); if (r.isGreaterThan(MAX_UINT256)) { - // Solidity implementation does not throw a reason. - throw new AnyRevertError(); + throw new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + a, + b, + ); } return r; } export function safeDiv(a: BigNumber, b: BigNumber): BigNumber { if (b.isEqualTo(0)) { - // Solidity implementation does not throw a reason. - throw new AnyRevertError(); + throw new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256DivisionByZero, + a, + b, + ); } return a.dividedToIntegerBy(b); } From 4600a656d119dada722190bd070ac7b4d7974a31 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 1 Aug 2019 14:44:19 -0400 Subject: [PATCH 24/48] `@0x/contracts-test-utils`: Update `testWithReferenceFunctionAsync()` to support `RevertError`s. --- .../test-utils/src/test_with_reference.ts | 121 +++++++++++------- .../test-utils/test/test_with_reference.ts | 82 ++++++++---- 2 files changed, 127 insertions(+), 76 deletions(-) diff --git a/contracts/test-utils/src/test_with_reference.ts b/contracts/test-utils/src/test_with_reference.ts index 7585f8bd4e..7d0dc23521 100644 --- a/contracts/test-utils/src/test_with_reference.ts +++ b/contracts/test-utils/src/test_with_reference.ts @@ -1,35 +1,8 @@ +import { BigNumber, RevertError } from '@0x/utils'; import * as _ from 'lodash'; import { expect } from './chai_setup'; -class Value { - public value: T; - constructor(value: T) { - this.value = value; - } -} - -// tslint:disable-next-line: max-classes-per-file -class ErrorMessage { - public error: string; - constructor(message: string) { - this.error = message; - } -} - -type PromiseResult = Value | ErrorMessage; - -// TODO(albrow): This seems like a generic utility function that could exist in -// lodash. We should replace it by a library implementation, or move it to our -// own. -async function evaluatePromiseAsync(promise: Promise): Promise> { - try { - return new Value(await promise); - } catch (e) { - return new ErrorMessage(e.message); - } -} - export async function testWithReferenceFuncAsync( referenceFunc: (p0: P0) => Promise, testFunc: (p0: P0) => Promise, @@ -88,36 +61,86 @@ export async function testWithReferenceFuncAsync( testFuncAsync: (...args: any[]) => Promise, values: any[], ): Promise { - // Measure correct behaviour - const expected = await evaluatePromiseAsync(referenceFuncAsync(...values)); - - // Measure actual behaviour - const actual = await evaluatePromiseAsync(testFuncAsync(...values)); + // Measure correct behavior + let expected: any; + let expectedError: Error | undefined; + try { + expected = await referenceFuncAsync(...values); + } catch (err) { + expectedError = err; + } + // Measure actual behavior + let actual: any; + let actualError: Error | undefined; + try { + actual = await testFuncAsync(...values); + } catch (err) { + actualError = err; + } - // Compare behaviour - if (expected instanceof ErrorMessage) { - // If we expected an error, check if the actual error message contains the - // expected error message. - if (!(actual instanceof ErrorMessage)) { - throw new Error( - `Expected error containing ${expected.error} but got no error\n\tTest case: ${_getTestCaseString( - referenceFuncAsync, - values, - )}`, + const testCaseString = _getTestCaseString(referenceFuncAsync, values); + // Compare behavior + if (expectedError !== undefined) { + // Expecting an error. + if (actualError === undefined) { + return expect.fail( + actualError, + expectedError, + `${testCaseString}: expected failure but instead succeeded`, ); + } else { + if (expectedError instanceof RevertError) { + // Expecting a RevertError. + if (actualError instanceof RevertError) { + if (!actualError.equals(expectedError)) { + return expect.fail( + actualError, + expectedError, + `${testCaseString}: expected error ${actualError.toString()} to equal ${expectedError.toString()}`, + ); + } + } else { + return expect.fail( + actualError, + expectedError, + `${testCaseString}: expected a RevertError but received an Error`, + ); + } + } else { + // Expecing any Error type. + if (actualError.message !== expectedError.message) { + return expect.fail( + actualError, + expectedError, + `${testCaseString}: expected error message '${actualError.message}' to equal '${expectedError.message}'`, + ); + } + } } - expect(actual.error).to.contain( - expected.error, - `${actual.error}\n\tTest case: ${_getTestCaseString(referenceFuncAsync, values)}`, - ); } else { - // If we do not expect an error, compare actual and expected directly. - expect(actual).to.deep.equal(expected, `Test case ${_getTestCaseString(referenceFuncAsync, values)}`); + // Not expecting an error. + if (actualError !== undefined) { + return expect.fail( + actualError, + expectedError, + `${testCaseString}: expected success but instead failed`, + ); + } + if (expected instanceof BigNumber) { + // Technically we can do this with `deep.eq`, but this prints prettier + // error messages for BigNumbers. + expect(actual).to.bignumber.eq(expected, testCaseString); + } else { + expect(actual).to.deep.eq(expected, testCaseString); + } } } function _getTestCaseString(referenceFuncAsync: (...args: any[]) => Promise, values: any[]): string { const paramNames = _getParameterNames(referenceFuncAsync); + while (paramNames.length < values.length) { + paramNames.push(`${paramNames.length}`); + } return JSON.stringify(_.zipObject(paramNames, values)); } diff --git a/contracts/test-utils/test/test_with_reference.ts b/contracts/test-utils/test/test_with_reference.ts index 1c1211003e..b3fbef3599 100644 --- a/contracts/test-utils/test/test_with_reference.ts +++ b/contracts/test-utils/test/test_with_reference.ts @@ -1,11 +1,8 @@ -import * as chai from 'chai'; +import { AnyRevertError, StringRevertError } from '@0x/utils'; -import { chaiSetup } from '../src/chai_setup'; +import { expect } from '../src'; import { testWithReferenceFuncAsync } from '../src/test_with_reference'; -chaiSetup.configure(); -const expect = chai.expect; - async function divAsync(x: number, y: number): Promise { if (y === 0) { throw new Error('MathError: divide by zero'); @@ -18,46 +15,77 @@ function alwaysValueFunc(value: number): (x: number, y: number) => Promise value; } -// returns an async function which always throws/rejects with the given error -// message. -function alwaysFailFunc(errMessage: string): (x: number, y: number) => Promise { +// returns an async function which always throws/rejects with the given error. +function alwaysFailFunc(error: Error): (x: number, y: number) => Promise { return async (x: number, y: number) => { - throw new Error(errMessage); + throw error; }; } describe('testWithReferenceFuncAsync', () => { - it('passes when both succeed and actual === expected', async () => { - await testWithReferenceFuncAsync(alwaysValueFunc(0.5), divAsync, [1, 2]); + it('passes when both succeed and actual == expected', async () => { + return testWithReferenceFuncAsync(alwaysValueFunc(0.5), divAsync, [1, 2]); }); - it('passes when both fail and actual error contains expected error', async () => { - await testWithReferenceFuncAsync(alwaysFailFunc('divide by zero'), divAsync, [1, 0]); + it('fails when both succeed and actual != expected', async () => { + return expect(testWithReferenceFuncAsync(alwaysValueFunc(3), divAsync, [1, 2])) + .to.be.rejectedWith('{"x":1,"y":2}: expected 0.5 to deeply equal 3'); }); - it('fails when both succeed and actual !== expected', async () => { - expect(testWithReferenceFuncAsync(alwaysValueFunc(3), divAsync, [1, 2])).to.be.rejectedWith( - 'Test case {"x":1,"y":2}: expected { value: 0.5 } to deeply equal { value: 3 }', - ); + it('passes when both fail and error messages are the same', async () => { + const err = new Error('whoopsie'); + return testWithReferenceFuncAsync(alwaysFailFunc(err), alwaysFailFunc(err), [1, 1]); }); - it('fails when both fail and actual error does not contain expected error', async () => { - expect( - testWithReferenceFuncAsync(alwaysFailFunc('Unexpected math error'), divAsync, [1, 0]), + it('fails when both fail and error messages are not identical', async () => { + const errorMessage = 'whoopsie'; + const notErrorMessage = 'not whoopsie'; + const error = new Error(errorMessage); + const notError = new Error(notErrorMessage); + return expect( + testWithReferenceFuncAsync(alwaysFailFunc(notError), alwaysFailFunc(error), [1, 2]), ).to.be.rejectedWith( - 'MathError: divide by zero\n\tTest case: {"x":1,"y":0}: expected \'MathError: divide by zero\' to include \'Unexpected math error\'', + `{"x":1,"y":2}: expected error message '${errorMessage}' to equal '${notErrorMessage}'`, ); }); + it('passes when both fail with compatible RevertErrors', async () => { + const error1 = new StringRevertError('whoopsie'); + const error2 = new AnyRevertError(); + return testWithReferenceFuncAsync(alwaysFailFunc(error1), alwaysFailFunc(error2), [1, 1]); + }); + + it('fails when both fail with incompatible RevertErrors', async () => { + const error1 = new StringRevertError('whoopsie'); + const error2 = new StringRevertError('not whoopsie'); + return expect(testWithReferenceFuncAsync(alwaysFailFunc(error1), alwaysFailFunc(error2), [1, 1])) + .to.be.rejectedWith( + `{"x":1,"y":1}: expected error StringRevertError({ message: 'not whoopsie' }) to equal StringRevertError({ message: 'whoopsie' })`, + ); + }); + + it('fails when reference function fails with a RevertError but test function fails with a regular Error', async () => { + const error1 = new StringRevertError('whoopsie'); + const error2 = new Error('whoopsie'); + return expect(testWithReferenceFuncAsync(alwaysFailFunc(error1), alwaysFailFunc(error2), [1, 1])) + .to.be.rejectedWith( + `{"x":1,"y":1}: expected a RevertError but received an Error`, + ); + }); + it('fails when referenceFunc succeeds and testFunc fails', async () => { - expect(testWithReferenceFuncAsync(alwaysValueFunc(0), divAsync, [1, 0])).to.be.rejectedWith( - 'Test case {"x":1,"y":0}: expected { error: \'MathError: divide by zero\' } to deeply equal { value: 0 }', - ); + const error = new Error('whoopsie'); + return expect(testWithReferenceFuncAsync(alwaysValueFunc(0), alwaysFailFunc(error), [1, 2])) + .to.be.rejectedWith( + `{"x":1,"y":2}: expected success but instead failed`, + ); }); it('fails when referenceFunc fails and testFunc succeeds', async () => { - expect(testWithReferenceFuncAsync(alwaysFailFunc('divide by zero'), divAsync, [1, 2])).to.be.rejectedWith( - 'Expected error containing divide by zero but got no error\n\tTest case: {"x":1,"y":2}', - ); + const error = new Error('whoopsie'); + return expect(testWithReferenceFuncAsync(alwaysFailFunc(error), divAsync, [1, 2])) + .to.be.rejectedWith( + '{"x":1,"y":2}: expected failure but instead succeeded', + ); }); }); From f791cd3a377fd31c2068e7c205e12db6e5fd46f6 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 1 Aug 2019 15:13:40 -0400 Subject: [PATCH 25/48] `@0x/contracts-exchange-libs`: Remove unecessary checks for zero denominator. `@0x/contracts-exchange-libs`: `LibMath` tests from `@0x/contracts-exchange` into this package. `@0x/contracts-exchange-libs`: Adjust logic in reference functions to be closer to solidity implementation. --- contracts/exchange-libs/CHANGELOG.json | 8 ++ .../exchange-libs/contracts/src/LibMath.sol | 16 --- .../exchange-libs/contracts/test/TestLibs.sol | 34 ++++++ .../exchange-libs/src/reference_functions.ts | 26 ++--- contracts/exchange-libs/test/lib_math.ts | 109 ++++++++++++++++++ 5 files changed, 161 insertions(+), 32 deletions(-) create mode 100644 contracts/exchange-libs/test/lib_math.ts diff --git a/contracts/exchange-libs/CHANGELOG.json b/contracts/exchange-libs/CHANGELOG.json index f8ce90f4df..3fdc4345ba 100644 --- a/contracts/exchange-libs/CHANGELOG.json +++ b/contracts/exchange-libs/CHANGELOG.json @@ -57,6 +57,14 @@ { "note": "Add reference functions for `LibMath` and `LibFillResults`", "pr": "TODO" + }, + { + "note": "Bring in revamped `LibMath` tests from the `contracts-exchange` package.", + "pr": "TODO" + }, + { + "note": "Remove unecessary zero-denominator checks in `LibMath`.", + "pr": "TODO" } ] }, diff --git a/contracts/exchange-libs/contracts/src/LibMath.sol b/contracts/exchange-libs/contracts/src/LibMath.sol index 6cc082b052..bc145499af 100644 --- a/contracts/exchange-libs/contracts/src/LibMath.sol +++ b/contracts/exchange-libs/contracts/src/LibMath.sol @@ -41,10 +41,6 @@ contract LibMath is pure returns (uint256 partialAmount) { - if (denominator == 0) { - LibRichErrors._rrevert(LibMathRichErrors.DivisionByZeroError()); - } - if (_isRoundingErrorFloor( numerator, denominator, @@ -79,10 +75,6 @@ contract LibMath is pure returns (uint256 partialAmount) { - if (denominator == 0) { - LibRichErrors._rrevert(LibMathRichErrors.DivisionByZeroError()); - } - if (_isRoundingErrorCeil( numerator, denominator, @@ -122,10 +114,6 @@ contract LibMath is pure returns (uint256 partialAmount) { - if (denominator == 0) { - LibRichErrors._rrevert(LibMathRichErrors.DivisionByZeroError()); - } - partialAmount = _safeDiv( _safeMul(numerator, target), denominator @@ -147,10 +135,6 @@ contract LibMath is pure returns (uint256 partialAmount) { - if (denominator == 0) { - LibRichErrors._rrevert(LibMathRichErrors.DivisionByZeroError()); - } - // _safeDiv computes `floor(a / b)`. We use the identity (a, b integer): // ceil(a / b) = floor((a + b - 1) / b) // To implement `ceil(a / b)` using _safeDiv. diff --git a/contracts/exchange-libs/contracts/test/TestLibs.sol b/contracts/exchange-libs/contracts/test/TestLibs.sol index 8f2925af83..43beae3161 100644 --- a/contracts/exchange-libs/contracts/test/TestLibs.sol +++ b/contracts/exchange-libs/contracts/test/TestLibs.sol @@ -73,6 +73,40 @@ contract TestLibs is return partialAmount; } + function safeGetPartialAmountFloor( + uint256 numerator, + uint256 denominator, + uint256 target + ) + public + pure + returns (uint256 partialAmount) + { + partialAmount = _safeGetPartialAmountFloor( + numerator, + denominator, + target + ); + return partialAmount; + } + + function safeGetPartialAmountCeil( + uint256 numerator, + uint256 denominator, + uint256 target + ) + public + pure + returns (uint256 partialAmount) + { + partialAmount = _safeGetPartialAmountCeil( + numerator, + denominator, + target + ); + return partialAmount; + } + function isRoundingErrorFloor( uint256 numerator, uint256 denominator, diff --git a/contracts/exchange-libs/src/reference_functions.ts b/contracts/exchange-libs/src/reference_functions.ts index 6a41601166..8585f0a64c 100644 --- a/contracts/exchange-libs/src/reference_functions.ts +++ b/contracts/exchange-libs/src/reference_functions.ts @@ -21,8 +21,11 @@ export function isRoundingErrorFloor( if (numerator.eq(0) || target.eq(0)) { return false; } - const remainder = numerator.multipliedBy(target).mod(denominator); - return safeMul(new BigNumber(1000), remainder).gte(safeMul(numerator, target)); + const remainder = numerator.times(target).mod(denominator); + // Need to do this separately because solidity evaluates RHS of the comparison expression first. + const rhs = safeMul(numerator, target); + const lhs = safeMul(new BigNumber(1000), remainder); + return lhs.gte(rhs); } export function isRoundingErrorCeil( @@ -36,9 +39,12 @@ export function isRoundingErrorCeil( if (numerator.eq(0) || target.eq(0)) { return false; } - let remainder = numerator.multipliedBy(target).mod(denominator); + let remainder = numerator.times(target).mod(denominator); remainder = safeSub(denominator, remainder).mod(denominator); - return safeMul(new BigNumber(1000), remainder).gte(safeMul(numerator, target)); + // Need to do this separately because solidity evaluates RHS of the comparison expression first. + const rhs = safeMul(numerator, target); + const lhs = safeMul(new BigNumber(1000), remainder); + return lhs.gte(rhs); } export function safeGetPartialAmountFloor( @@ -46,9 +52,6 @@ export function safeGetPartialAmountFloor( denominator: BigNumber, target: BigNumber, ): BigNumber { - if (denominator.eq(0)) { - throw new LibMathRevertErrors.DivisionByZeroError(); - } if (isRoundingErrorFloor(numerator, denominator, target)) { throw new LibMathRevertErrors.RoundingError(numerator, denominator, target); } @@ -63,9 +66,6 @@ export function safeGetPartialAmountCeil( denominator: BigNumber, target: BigNumber, ): BigNumber { - if (denominator.eq(0)) { - throw new LibMathRevertErrors.DivisionByZeroError(); - } if (isRoundingErrorCeil(numerator, denominator, target)) { throw new LibMathRevertErrors.RoundingError(numerator, denominator, target); } @@ -83,9 +83,6 @@ export function getPartialAmountFloor( denominator: BigNumber, target: BigNumber, ): BigNumber { - if (denominator.eq(0)) { - throw new LibMathRevertErrors.DivisionByZeroError(); - } return safeDiv( safeMul(numerator, target), denominator, @@ -97,9 +94,6 @@ export function getPartialAmountCeil( denominator: BigNumber, target: BigNumber, ): BigNumber { - if (denominator.eq(0)) { - throw new LibMathRevertErrors.DivisionByZeroError(); - } return safeDiv( safeAdd( safeMul(numerator, target), diff --git a/contracts/exchange-libs/test/lib_math.ts b/contracts/exchange-libs/test/lib_math.ts new file mode 100644 index 0000000000..92da9baecc --- /dev/null +++ b/contracts/exchange-libs/test/lib_math.ts @@ -0,0 +1,109 @@ +import { + blockchainTests, + describe, + testCombinatoriallyWithReferenceFunc, + uint256Values, +} from '@0x/contracts-test-utils'; +import { BigNumber } from '@0x/utils'; +import * as _ from 'lodash'; + +import { artifacts, ReferenceFunctions, TestLibsContract } from '../src'; + +const CHAIN_ID = 1337; + +blockchainTests('LibMath', env => { + let libsContract: TestLibsContract; + + before(async () => { + libsContract = await TestLibsContract.deployFrom0xArtifactAsync( + artifacts.TestLibs, + env.provider, + env.txDefaults, + new BigNumber(CHAIN_ID), + ); + }); + + // Wrap a reference function with identical arguments in a promise. + function createAsyncReferenceFunction( + ref: (...args: any[]) => T, + ): (...args: any[]) => Promise { + return async (...args: any[]): Promise => { + return ref(...args); + }; + } + + function createContractTestFunction( + name: string, + ): (...args: any[]) => Promise { + return async (...args: any[]): Promise => { + const method = (libsContract as any)[name] as { callAsync: (...args: any[]) => Promise }; + return method.callAsync(...args); + }; + } + + describe('getPartialAmountFloor', () => { + describe.optional('combinatorial tests', () => { + testCombinatoriallyWithReferenceFunc( + 'getPartialAmountFloor', + createAsyncReferenceFunction(ReferenceFunctions.getPartialAmountFloor), + createContractTestFunction('getPartialAmountFloor'), + [uint256Values, uint256Values, uint256Values], + ); + }); + }); + + describe('getPartialAmountCeil', () => { + describe.optional('combinatorial tests', () => { + testCombinatoriallyWithReferenceFunc( + 'getPartialAmountCeil', + createAsyncReferenceFunction(ReferenceFunctions.getPartialAmountCeil), + createContractTestFunction('getPartialAmountCeil'), + [uint256Values, uint256Values, uint256Values], + ); + }); + }); + + describe('safeGetPartialAmountFloor', () => { + describe.optional('combinatorial tests', () => { + testCombinatoriallyWithReferenceFunc( + 'safeGetPartialAmountFloor', + createAsyncReferenceFunction(ReferenceFunctions.safeGetPartialAmountFloor), + createContractTestFunction('safeGetPartialAmountFloor'), + [uint256Values, uint256Values, uint256Values], + ); + }); + }); + + describe('safeGetPartialAmountCeil', () => { + describe.optional('combinatorial tests', () => { + testCombinatoriallyWithReferenceFunc( + 'safeGetPartialAmountCeil', + createAsyncReferenceFunction(ReferenceFunctions.safeGetPartialAmountCeil), + createContractTestFunction('safeGetPartialAmountCeil'), + [uint256Values, uint256Values, uint256Values], + ); + }); + }); + + describe('isRoundingErrorFloor', () => { + describe.optional('combinatorial tests', () => { + testCombinatoriallyWithReferenceFunc( + 'isRoundingErrorFloor', + createAsyncReferenceFunction(ReferenceFunctions.isRoundingErrorFloor), + createContractTestFunction('isRoundingErrorFloor'), + [uint256Values, uint256Values, uint256Values], + ); + }); + }); + + describe('isRoundingErrorCeil', () => { + describe.optional('combinatorial tests', () => { + testCombinatoriallyWithReferenceFunc( + 'isRoundingErrorCeil', + createAsyncReferenceFunction(ReferenceFunctions.isRoundingErrorCeil), + createContractTestFunction('isRoundingErrorCeil'), + [uint256Values, uint256Values, uint256Values], + ); + }); + }); +}); From 8c05a92a1eb1ffe21bb2a6acc8057d8020211439 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 1 Aug 2019 15:23:07 -0400 Subject: [PATCH 26/48] Update changelogs --- contracts/test-utils/CHANGELOG.json | 10 +++++++++- contracts/utils/CHANGELOG.json | 4 ++++ packages/utils/CHANGELOG.json | 9 +++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/contracts/test-utils/CHANGELOG.json b/contracts/test-utils/CHANGELOG.json index ce10c25494..aff50f2677 100644 --- a/contracts/test-utils/CHANGELOG.json +++ b/contracts/test-utils/CHANGELOG.json @@ -53,7 +53,15 @@ { "note": "Add the constants: `MAX_UINT256`, `ADDRESS_LENGTH`", "pr": "TODO" - } + }, + { + "note": "Make `testCombinatoriallyWithReferenceFuncAsync` non-async", + "pr": "TODO" + }, + { + "note": "Update `testWithReferenceFuncAsync` to work with `RevertErrors`", + "pr": "TODO" + }, ] }, { diff --git a/contracts/utils/CHANGELOG.json b/contracts/utils/CHANGELOG.json index 19a0c19fd0..8a8ec3f74e 100644 --- a/contracts/utils/CHANGELOG.json +++ b/contracts/utils/CHANGELOG.json @@ -37,6 +37,10 @@ { "note": "Add reference functions for `SafeMath` functions.", "pr": "2031" + }, + { + "note": "Throw a `SafeMathError` in `SafeMath._safeDiv()` when denominator is zero.", + "pr": "2031" } ] }, diff --git a/packages/utils/CHANGELOG.json b/packages/utils/CHANGELOG.json index 9ac05c4b9e..a6ab09776a 100644 --- a/packages/utils/CHANGELOG.json +++ b/packages/utils/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "4.5.0", + "changes": [ + { + "note": "Add `SafeMathRevertErrors.SafeMathErrorCodes.Uint256DivisionByZero`", + "pr": "TODO" + } + ] + }, { "version": "4.4.0", "changes": [ From 884b1add8e8f5d3ff7e554beb244dd82e209a14b Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 1 Aug 2019 15:48:44 -0400 Subject: [PATCH 27/48] `@0x/contracts-exchange-libs`: Move in revamped `LibFillResults` tests from `@0x/contracts-exchange`. --- contracts/exchange-libs/CHANGELOG.json | 6 +- .../exchange-libs/test/lib_fill_results.ts | 66 +++++++++++++++++++ contracts/exchange-libs/test/lib_math.ts | 4 +- 3 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 contracts/exchange-libs/test/lib_fill_results.ts diff --git a/contracts/exchange-libs/CHANGELOG.json b/contracts/exchange-libs/CHANGELOG.json index 3fdc4345ba..4586bef220 100644 --- a/contracts/exchange-libs/CHANGELOG.json +++ b/contracts/exchange-libs/CHANGELOG.json @@ -59,7 +59,11 @@ "pr": "TODO" }, { - "note": "Bring in revamped `LibMath` tests from the `contracts-exchange` package.", + "note": "Move in revamped `LibMath` tests from the `contracts-exchange` package.", + "pr": "TODO" + }, + { + "note": "Move in revamped `LibFillResults` tests from the `contracts-exchange` package.", "pr": "TODO" }, { diff --git a/contracts/exchange-libs/test/lib_fill_results.ts b/contracts/exchange-libs/test/lib_fill_results.ts new file mode 100644 index 0000000000..0b9224d25f --- /dev/null +++ b/contracts/exchange-libs/test/lib_fill_results.ts @@ -0,0 +1,66 @@ +import { + blockchainTests, + describe, + testCombinatoriallyWithReferenceFunc, + uint256Values, +} from '@0x/contracts-test-utils'; +import { FillResults } from '@0x/types'; +import { BigNumber } from '@0x/utils'; + +import { artifacts, ReferenceFunctions, TestLibsContract } from '../src'; + +blockchainTests('LibFillResults', env => { + const CHAIN_ID = 1337; + let libsContract: TestLibsContract; + + before(async () => { + libsContract = await TestLibsContract.deployFrom0xArtifactAsync( + artifacts.TestLibs, + env.provider, + env.txDefaults, + new BigNumber(CHAIN_ID), + ); + }); + + describe('addFillResults', () => { + function makeFillResults(value: BigNumber): FillResults { + // We reuse values across fields, but this is fine because + // `addFillResults()` never does any math between them. + return { + makerAssetFilledAmount: value, + takerAssetFilledAmount: value, + makerFeePaid: value, + takerFeePaid: value, + }; + } + + async function referenceAddFillResultsAsync( + totalValue: BigNumber, + singleValue: BigNumber, + ): Promise { + return ReferenceFunctions.addFillResults( + makeFillResults(totalValue), + makeFillResults(singleValue), + ); + } + + async function testAddFillResultsAsync( + totalValue: BigNumber, + singleValue: BigNumber, + ): Promise { + return libsContract.addFillResults.callAsync( + makeFillResults(totalValue), + makeFillResults(singleValue), + ); + } + + describe.optional('combinatorial tests', () => { + testCombinatoriallyWithReferenceFunc( + 'addFillResults', + referenceAddFillResultsAsync, + testAddFillResultsAsync, + [uint256Values, uint256Values], + ); + }); + }); +}); diff --git a/contracts/exchange-libs/test/lib_math.ts b/contracts/exchange-libs/test/lib_math.ts index 92da9baecc..b6db862909 100644 --- a/contracts/exchange-libs/test/lib_math.ts +++ b/contracts/exchange-libs/test/lib_math.ts @@ -5,13 +5,11 @@ import { uint256Values, } from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; -import * as _ from 'lodash'; import { artifacts, ReferenceFunctions, TestLibsContract } from '../src'; -const CHAIN_ID = 1337; - blockchainTests('LibMath', env => { + const CHAIN_ID = 1337; let libsContract: TestLibsContract; before(async () => { From 264b1d69d95bc0c65c650b6ed406259cc15368d4 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 1 Aug 2019 18:15:29 -0400 Subject: [PATCH 28/48] `@0x/dev-utils`: `revertWith` mocha extensions now accept Promise-like objects instead of just Promises. --- packages/dev-utils/CHANGELOG.json | 9 +++++++++ packages/dev-utils/src/chai_revert_error.ts | 11 +++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/dev-utils/CHANGELOG.json b/packages/dev-utils/CHANGELOG.json index 81f7fe017e..64799c5bd6 100644 --- a/packages/dev-utils/CHANGELOG.json +++ b/packages/dev-utils/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "2.3.1", + "changes": [ + { + "note": "`revertWith` mocha extensions now accept Promise-like objects instead of just Promises", + "pr": "TODO" + } + ] + }, { "version": "2.3.0", "changes": [ diff --git a/packages/dev-utils/src/chai_revert_error.ts b/packages/dev-utils/src/chai_revert_error.ts index a0fcd31103..47123875bb 100644 --- a/packages/dev-utils/src/chai_revert_error.ts +++ b/packages/dev-utils/src/chai_revert_error.ts @@ -34,7 +34,7 @@ export function revertErrorHelper(_chai: Chai): void { return async function(this: ChaiAssertionInstance, expected: any, ...rest: any[]): Promise { const maybePromise = this._obj; // Make sure we're working with a promise. - chaiAssert(_chai, maybePromise instanceof Promise, `Expected ${maybePromise} to be a promise`); + assertIsPromiseLike(_chai, maybePromise); // Wait for the promise to reject. let resolveValue; let rejectValue: any; @@ -58,7 +58,7 @@ export function revertErrorHelper(_chai: Chai): void { return async function(this: ChaiAssertionInstance, expected: any, ...rest: any[]): Promise { const maybePromise = this._obj; // Make sure we're working with a promise. - chaiAssert(_chai, maybePromise instanceof Promise, `Expected ${maybePromise} to be a promise`); + assertIsPromiseLike(_chai, maybePromise); // Wait for the promise to resolve. if (!compareRevertErrors.call(this, _chai, await maybePromise, expected)) { // Wasn't handled by the comparison function so call the previous handler. @@ -133,3 +133,10 @@ function chaiFail(_chai: Chai, failMessage?: string, expected?: any, actual?: an const assert = new _chai.Assertion(); assert.assert(false, failMessage, undefined, expected, actual); } + +function assertIsPromiseLike(_chai: Chai, maybePromise: any): void { + if (maybePromise.then instanceof Function && maybePromise.catch instanceof Function) { + return; + } + chaiFail(_chai, `Expected ${maybePromise} to be a promise`, new Promise(() => 1), maybePromise); +} From 51391b7f0e5473bf4945704c1f02eb8aa7d4e883 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 1 Aug 2019 18:17:46 -0400 Subject: [PATCH 29/48] `@0x/contracts-exchange-libs`: Correct internal variable naming in `src/index.ts`. `@0x/contracts-utils`: Correct internal variable naming in `src/index.ts`. `@0x/contracts-exchange`: Remove functions from `TestExchangeInternals.sol` that are now in other packages. `@0x/contracts-exchange`: Remove `TestExchangeMath.sol`. Exchange math functions are now tested in `@0x/contracts-exchange-libs`. `@0x/contracts-exchange`: Move `ReferenceFunctions` to default package export. `@0x/contracts-exchange`: Update `match_order.ts` tests to use reference math functions instead of `TestExchangeMath`. `@0x/contracts-exchange`: Remove `_updateFilledState()` combinatorial tests in favor of normal unit testing. Combinatorial testing was overkill. `@0x/contracts-exchange`: Update/refactor `calculateFillResults()` combinatorial tests to use the reference functions and hide them behind `TEST_ALL`. --- contracts/exchange-libs/src/index.ts | 4 +- .../exchange-libs/test/lib_fill_results.ts | 7 +- contracts/exchange/compiler.json | 1 - .../contracts/test/TestExchangeInternals.sol | 30 +- .../contracts/test/TestExchangeMath.sol | 131 ---- contracts/exchange/package.json | 2 +- contracts/exchange/src/artifacts.ts | 4 +- contracts/exchange/src/index.ts | 3 + .../utils => src}/reference_functions.ts | 0 contracts/exchange/src/wrappers.ts | 1 - contracts/exchange/test/internal.ts | 620 +++++------------- .../exchange/test/isolated_fill_order.ts | 5 +- contracts/exchange/test/match_orders.ts | 45 +- .../test/utils/isolated_exchange_wrapper.ts | 2 +- contracts/exchange/tsconfig.json | 1 - contracts/utils/src/index.ts | 4 +- 16 files changed, 195 insertions(+), 665 deletions(-) delete mode 100644 contracts/exchange/contracts/test/TestExchangeMath.sol rename contracts/exchange/{test/utils => src}/reference_functions.ts (100%) diff --git a/contracts/exchange-libs/src/index.ts b/contracts/exchange-libs/src/index.ts index 7e9c4805a6..0233c608d1 100644 --- a/contracts/exchange-libs/src/index.ts +++ b/contracts/exchange-libs/src/index.ts @@ -1,5 +1,5 @@ export * from './artifacts'; export * from './wrappers'; -import * as reference_functions from './reference_functions'; -export import ReferenceFunctions = reference_functions; +import * as ReferenceFunctionsToExport from './reference_functions'; +export import ReferenceFunctions = ReferenceFunctionsToExport; diff --git a/contracts/exchange-libs/test/lib_fill_results.ts b/contracts/exchange-libs/test/lib_fill_results.ts index 0b9224d25f..d4c7f4b94c 100644 --- a/contracts/exchange-libs/test/lib_fill_results.ts +++ b/contracts/exchange-libs/test/lib_fill_results.ts @@ -24,8 +24,9 @@ blockchainTests('LibFillResults', env => { describe('addFillResults', () => { function makeFillResults(value: BigNumber): FillResults { - // We reuse values across fields, but this is fine because - // `addFillResults()` never does any math between them. + // HACK(dorothy-zbornak): We reuse values across fields, + // but this is fine because `addFillResults()` never does + // any math between them. return { makerAssetFilledAmount: value, takerAssetFilledAmount: value, @@ -54,6 +55,8 @@ blockchainTests('LibFillResults', env => { ); } + // TODO(dorothy-zbornak): Do we really need these? + // Just a couple edge cases would likely suffice. describe.optional('combinatorial tests', () => { testCombinatoriallyWithReferenceFunc( 'addFillResults', diff --git a/contracts/exchange/compiler.json b/contracts/exchange/compiler.json index 341e06c30a..e00f266e0f 100644 --- a/contracts/exchange/compiler.json +++ b/contracts/exchange/compiler.json @@ -40,7 +40,6 @@ "test/ReentrantERC20Token.sol", "test/TestAssetProxyDispatcher.sol", "test/TestExchangeInternals.sol", - "test/TestExchangeMath.sol", "test/TestLibExchangeRichErrorDecoder.sol", "test/TestSignatureValidator.sol", "test/TestValidatorWallet.sol" diff --git a/contracts/exchange/contracts/test/TestExchangeInternals.sol b/contracts/exchange/contracts/test/TestExchangeInternals.sol index f40ca21797..2ae6467300 100644 --- a/contracts/exchange/contracts/test/TestExchangeInternals.sol +++ b/contracts/exchange/contracts/test/TestExchangeInternals.sol @@ -31,26 +31,6 @@ contract TestExchangeInternals is Exchange(chainId) {} - /// @dev Adds properties of both FillResults instances. - /// Modifies the first FillResults instance specified. - /// Note that this function has been modified from the original - // internal version to return the FillResults. - /// @param totalFillResults Fill results instance that will be added onto. - /// @param singleFillResults Fill results instance that will be added to totalFillResults. - /// @return newTotalFillResults The result of adding singleFillResults to totalFilResults. - function addFillResults(FillResults memory totalFillResults, FillResults memory singleFillResults) - public - pure - returns (FillResults memory) - { - _addFillResults(totalFillResults, singleFillResults); - return totalFillResults; - } - - /// @dev Calculates amounts filled and fees paid by maker and taker. - /// @param order to be filled. - /// @param takerAssetFilledAmount Amount of takerAsset that will be filled. - /// @return fillResults Amounts filled and fees paid by maker and taker. function calculateFillResults( Order memory order, uint256 takerAssetFilledAmount @@ -62,12 +42,9 @@ contract TestExchangeInternals is return _calculateFillResults(order, takerAssetFilledAmount); } - /// @dev Updates state with results of a fill order. - /// @param order that was filled. - /// @param takerAddress Address of taker who filled the order. - /// @param orderTakerAssetFilledAmount Amount of order already filled. - /// @return fillResults Amounts filled and fees paid by maker and taker. - function updateFilledState( + /// @dev Call `_updateFilledState()` but first set `filled[order]` to + /// `orderTakerAssetFilledAmount`. + function testUpdateFilledState( Order memory order, address takerAddress, bytes32 orderHash, @@ -76,6 +53,7 @@ contract TestExchangeInternals is ) public { + filled[getOrderHash(order)] = orderTakerAssetFilledAmount; _updateFilledState( order, takerAddress, diff --git a/contracts/exchange/contracts/test/TestExchangeMath.sol b/contracts/exchange/contracts/test/TestExchangeMath.sol deleted file mode 100644 index 832e4c5db7..0000000000 --- a/contracts/exchange/contracts/test/TestExchangeMath.sol +++ /dev/null @@ -1,131 +0,0 @@ -/* - - Copyright 2018 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity ^0.5.9; -pragma experimental ABIEncoderV2; - -import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol"; - - -contract TestExchangeMath is - LibMath -{ - /// @dev Calculates partial value given a numerator and denominator. - /// Reverts if rounding error is >= 0.1% - /// @param numerator Numerator. - /// @param denominator Denominator. - /// @param target Value to calculate partial of. - /// @return Partial value of target. - function safeGetPartialAmountFloor( - uint256 numerator, - uint256 denominator, - uint256 target - ) - public - pure - returns (uint256 partialAmount) - { - return _safeGetPartialAmountFloor(numerator, denominator, target); - } - - /// @dev Calculates partial value given a numerator and denominator. - /// Reverts if rounding error is >= 0.1% - /// @param numerator Numerator. - /// @param denominator Denominator. - /// @param target Value to calculate partial of. - /// @return Partial value of target. - function safeGetPartialAmountCeil( - uint256 numerator, - uint256 denominator, - uint256 target - ) - public - pure - returns (uint256 partialAmount) - { - return _safeGetPartialAmountCeil(numerator, denominator, target); - } - - /// @dev Calculates partial value given a numerator and denominator. - /// @param numerator Numerator. - /// @param denominator Denominator. - /// @param target Value to calculate partial of. - /// @return Partial value of target. - function getPartialAmountFloor( - uint256 numerator, - uint256 denominator, - uint256 target - ) - public - pure - returns (uint256 partialAmount) - { - return _getPartialAmountFloor(numerator, denominator, target); - } - - /// @dev Calculates partial value given a numerator and denominator. - /// @param numerator Numerator. - /// @param denominator Denominator. - /// @param target Value to calculate partial of. - /// @return Partial value of target. - function getPartialAmountCeil( - uint256 numerator, - uint256 denominator, - uint256 target - ) - public - pure - returns (uint256 partialAmount) - { - return _getPartialAmountCeil(numerator, denominator, target); - } - - /// @dev Checks if rounding error >= 0.1%. - /// @param numerator Numerator. - /// @param denominator Denominator. - /// @param target Value to multiply with numerator/denominator. - /// @return Rounding error is present. - function isRoundingErrorFloor( - uint256 numerator, - uint256 denominator, - uint256 target - ) - public - pure - returns (bool isError) - { - return _isRoundingErrorFloor(numerator, denominator, target); - } - - /// @dev Checks if rounding error >= 0.1%. - /// @param numerator Numerator. - /// @param denominator Denominator. - /// @param target Value to multiply with numerator/denominator. - /// @return Rounding error is present. - function isRoundingErrorCeil( - uint256 numerator, - uint256 denominator, - uint256 target - ) - public - pure - returns (bool isError) - { - return _isRoundingErrorCeil(numerator, denominator, target); - } -} diff --git a/contracts/exchange/package.json b/contracts/exchange/package.json index 0f7ce901b5..a73ab1b0ac 100644 --- a/contracts/exchange/package.json +++ b/contracts/exchange/package.json @@ -34,7 +34,7 @@ "lint-contracts": "solhint -c ../.solhint.json contracts/**/**/**/**/*.sol" }, "config": { - "abis": "./generated-artifacts/@(Exchange|ExchangeWrapper|IAssetProxyDispatcher|IEIP1271Wallet|IExchange|IExchangeCore|IMatchOrders|ISignatureValidator|ITransactions|IWallet|IWrapperFunctions|IsolatedExchange|ReentrantERC20Token|TestAssetProxyDispatcher|TestExchangeInternals|TestExchangeMath|TestLibExchangeRichErrorDecoder|TestSignatureValidator|TestValidatorWallet|Whitelist).json", + "abis": "./generated-artifacts/@(Exchange|ExchangeWrapper|IAssetProxyDispatcher|IEIP1271Wallet|IExchange|IExchangeCore|IMatchOrders|ISignatureValidator|ITransactions|IWallet|IWrapperFunctions|IsolatedExchange|ReentrantERC20Token|TestAssetProxyDispatcher|TestExchangeInternals|TestLibExchangeRichErrorDecoder|TestSignatureValidator|TestValidatorWallet|Whitelist).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/exchange/src/artifacts.ts b/contracts/exchange/src/artifacts.ts index 7ac7a00b37..e25db9a76d 100644 --- a/contracts/exchange/src/artifacts.ts +++ b/contracts/exchange/src/artifacts.ts @@ -20,7 +20,6 @@ import * as IsolatedExchange from '../generated-artifacts/IsolatedExchange.json' import * as ReentrantERC20Token from '../generated-artifacts/ReentrantERC20Token.json'; import * as TestAssetProxyDispatcher from '../generated-artifacts/TestAssetProxyDispatcher.json'; import * as TestExchangeInternals from '../generated-artifacts/TestExchangeInternals.json'; -import * as TestExchangeMath from '../generated-artifacts/TestExchangeMath.json'; import * as TestLibExchangeRichErrorDecoder from '../generated-artifacts/TestLibExchangeRichErrorDecoder.json'; import * as TestSignatureValidator from '../generated-artifacts/TestSignatureValidator.json'; import * as TestValidatorWallet from '../generated-artifacts/TestValidatorWallet.json'; @@ -38,11 +37,10 @@ export const artifacts = { ITransactions: ITransactions as ContractArtifact, IWallet: IWallet as ContractArtifact, IWrapperFunctions: IWrapperFunctions as ContractArtifact, + IsolatedExchange: IsolatedExchange as ContractArtifact, ReentrantERC20Token: ReentrantERC20Token as ContractArtifact, TestAssetProxyDispatcher: TestAssetProxyDispatcher as ContractArtifact, TestExchangeInternals: TestExchangeInternals as ContractArtifact, - TestExchangeMath: TestExchangeMath as ContractArtifact, - IsolatedExchange: IsolatedExchange as ContractArtifact, TestLibExchangeRichErrorDecoder: TestLibExchangeRichErrorDecoder as ContractArtifact, TestSignatureValidator: TestSignatureValidator as ContractArtifact, TestValidatorWallet: TestValidatorWallet as ContractArtifact, diff --git a/contracts/exchange/src/index.ts b/contracts/exchange/src/index.ts index ba813e7caf..28c617a1a2 100644 --- a/contracts/exchange/src/index.ts +++ b/contracts/exchange/src/index.ts @@ -1,3 +1,6 @@ export * from './artifacts'; export * from './wrappers'; export * from '../test/utils'; + +import * as ReferenceFunctionsToExport from './reference_functions'; +export import ReferenceFunctions = ReferenceFunctionsToExport; diff --git a/contracts/exchange/test/utils/reference_functions.ts b/contracts/exchange/src/reference_functions.ts similarity index 100% rename from contracts/exchange/test/utils/reference_functions.ts rename to contracts/exchange/src/reference_functions.ts diff --git a/contracts/exchange/src/wrappers.ts b/contracts/exchange/src/wrappers.ts index d96c39afcb..4b5500fd68 100644 --- a/contracts/exchange/src/wrappers.ts +++ b/contracts/exchange/src/wrappers.ts @@ -18,7 +18,6 @@ export * from '../generated-wrappers/isolated_exchange'; export * from '../generated-wrappers/reentrant_erc20_token'; export * from '../generated-wrappers/test_asset_proxy_dispatcher'; export * from '../generated-wrappers/test_exchange_internals'; -export * from '../generated-wrappers/test_exchange_math'; export * from '../generated-wrappers/test_lib_exchange_rich_error_decoder'; export * from '../generated-wrappers/test_signature_validator'; export * from '../generated-wrappers/test_validator_wallet'; diff --git a/contracts/exchange/test/internal.ts b/contracts/exchange/test/internal.ts index 4a55ee5cb1..a39a73f62e 100644 --- a/contracts/exchange/test/internal.ts +++ b/contracts/exchange/test/internal.ts @@ -1,426 +1,60 @@ import { blockchainTests, - bytes32Values, constants, + describe, expect, - testCombinatoriallyWithReferenceFuncAsync, + hexRandom, + LogDecoder, + testCombinatoriallyWithReferenceFunc, uint256Values, } from '@0x/contracts-test-utils'; -import { LibMathRevertErrors } from '@0x/order-utils'; -import { FillResults, Order, RevertReason, SignedOrder } from '@0x/types'; -import { BigNumber, providerUtils, SafeMathRevertErrors } from '@0x/utils'; +import { FillResults, OrderWithoutDomain as Order } from '@0x/types'; +import { BigNumber, SafeMathRevertErrors } from '@0x/utils'; +import { LogWithDecodedArgs } from 'ethereum-types'; import * as _ from 'lodash'; -import { artifacts, TestExchangeInternalsContract, TestExchangeMathContract } from '../src'; - -const { MAX_UINT256 } = constants; - -const emptyOrder: Order = { - senderAddress: constants.NULL_ADDRESS, - makerAddress: constants.NULL_ADDRESS, - takerAddress: constants.NULL_ADDRESS, - makerFee: new BigNumber(0), - takerFee: new BigNumber(0), - makerAssetAmount: new BigNumber(0), - takerAssetAmount: new BigNumber(0), - makerAssetData: '0x', - takerAssetData: '0x', - makerFeeAssetData: '0x', - takerFeeAssetData: '0x', - salt: new BigNumber(0), - feeRecipientAddress: constants.NULL_ADDRESS, - expirationTimeSeconds: new BigNumber(0), - domain: { - verifyingContractAddress: constants.NULL_ADDRESS, - chainId: 0, // To be filled in later. - }, -}; - -const emptySignedOrder: SignedOrder = { - ...emptyOrder, - signature: '', -}; - -const safeMathErrorForCall = new SafeMathRevertErrors.SafeMathError(); - -// TODO(dorothy-zbornak): Move this to `exchange-libs` and `utils`. -blockchainTests.resets('Exchange math internal functions', env => { - let chainId: number; - let testExchange: TestExchangeMathContract; - let divisionByZeroErrorForCall: Error | undefined; - let roundingErrorForCall: Error | undefined; - - before(async () => { - chainId = await env.getChainIdAsync(); - emptyOrder.domain.chainId = chainId; - emptySignedOrder.domain.chainId = chainId; - - testExchange = await TestExchangeMathContract.deployFrom0xArtifactAsync( - artifacts.TestExchangeMath, - env.provider, - env.txDefaults, - ); - divisionByZeroErrorForCall = new Error(RevertReason.DivisionByZero); - roundingErrorForCall = new Error(RevertReason.RoundingError); - divisionByZeroErrorForCall = new LibMathRevertErrors.DivisionByZeroError(); - roundingErrorForCall = new LibMathRevertErrors.RoundingError(); - }); - - async function referenceIsRoundingErrorFloorAsync( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, - ): Promise { - if (denominator.eq(0)) { - throw divisionByZeroErrorForCall; - } - if (numerator.eq(0)) { - return false; - } - if (target.eq(0)) { - return false; - } - const product = numerator.multipliedBy(target); - const remainder = product.mod(denominator); - const remainderTimes1000 = remainder.multipliedBy('1000'); - const isError = remainderTimes1000.gte(product); - if (product.isGreaterThan(MAX_UINT256)) { - throw safeMathErrorForCall; - } - if (remainderTimes1000.isGreaterThan(MAX_UINT256)) { - throw safeMathErrorForCall; - } - return isError; - } - - async function referenceIsRoundingErrorCeilAsync( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, - ): Promise { - if (denominator.eq(0)) { - throw divisionByZeroErrorForCall; - } - if (numerator.eq(0)) { - return false; - } - if (target.eq(0)) { - return false; - } - const product = numerator.multipliedBy(target); - const remainder = product.mod(denominator); - const error = denominator.minus(remainder).mod(denominator); - const errorTimes1000 = error.multipliedBy('1000'); - const isError = errorTimes1000.gte(product); - if (product.isGreaterThan(MAX_UINT256)) { - throw safeMathErrorForCall; - } - if (errorTimes1000.isGreaterThan(MAX_UINT256)) { - throw safeMathErrorForCall; - } - return isError; - } - - async function referenceSafeGetPartialAmountFloorAsync( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, - ): Promise { - if (denominator.eq(0)) { - throw divisionByZeroErrorForCall; - } - const isRoundingError = await referenceIsRoundingErrorFloorAsync(numerator, denominator, target); - if (isRoundingError) { - throw roundingErrorForCall; - } - const product = numerator.multipliedBy(target); - if (product.isGreaterThan(MAX_UINT256)) { - throw safeMathErrorForCall; - } - return product.dividedToIntegerBy(denominator); - } - - describe('getPartialAmountFloor', async () => { - async function referenceGetPartialAmountFloorAsync( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, - ): Promise { - if (denominator.eq(0)) { - throw divisionByZeroErrorForCall; - } - const product = numerator.multipliedBy(target); - if (product.isGreaterThan(MAX_UINT256)) { - throw safeMathErrorForCall; - } - return product.dividedToIntegerBy(denominator); - } - async function testGetPartialAmountFloorAsync( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, - ): Promise { - return testExchange.getPartialAmountFloor.callAsync(numerator, denominator, target); - } - await testCombinatoriallyWithReferenceFuncAsync( - 'getPartialAmountFloor', - referenceGetPartialAmountFloorAsync, - testGetPartialAmountFloorAsync, - [uint256Values, uint256Values, uint256Values], - ); - }); - - describe('getPartialAmountCeil', async () => { - async function referenceGetPartialAmountCeilAsync( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, - ): Promise { - if (denominator.eq(0)) { - throw divisionByZeroErrorForCall; - } - const product = numerator.multipliedBy(target); - const offset = product.plus(denominator.minus(1)); - if (offset.isGreaterThan(MAX_UINT256)) { - throw safeMathErrorForCall; - } - const result = offset.dividedToIntegerBy(denominator); - if (product.mod(denominator).eq(0)) { - expect(result.multipliedBy(denominator)).to.be.bignumber.eq(product); - } else { - expect(result.multipliedBy(denominator)).to.be.bignumber.gt(product); - } - return result; - } - async function testGetPartialAmountCeilAsync( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, - ): Promise { - return testExchange.getPartialAmountCeil.callAsync(numerator, denominator, target); - } - await testCombinatoriallyWithReferenceFuncAsync( - 'getPartialAmountCeil', - referenceGetPartialAmountCeilAsync, - testGetPartialAmountCeilAsync, - [uint256Values, uint256Values, uint256Values], - ); - }); - - describe('safeGetPartialAmountFloor', async () => { - async function testSafeGetPartialAmountFloorAsync( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, - ): Promise { - return testExchange.safeGetPartialAmountFloor.callAsync(numerator, denominator, target); - } - await testCombinatoriallyWithReferenceFuncAsync( - 'safeGetPartialAmountFloor', - referenceSafeGetPartialAmountFloorAsync, - testSafeGetPartialAmountFloorAsync, - [uint256Values, uint256Values, uint256Values], - ); - }); - - describe('safeGetPartialAmountCeil', async () => { - async function referenceSafeGetPartialAmountCeilAsync( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, - ): Promise { - if (denominator.eq(0)) { - throw divisionByZeroErrorForCall; - } - const isRoundingError = await referenceIsRoundingErrorCeilAsync(numerator, denominator, target); - if (isRoundingError) { - throw roundingErrorForCall; - } - const product = numerator.multipliedBy(target); - const offset = product.plus(denominator.minus(1)); - if (offset.isGreaterThan(MAX_UINT256)) { - throw safeMathErrorForCall; - } - const result = offset.dividedToIntegerBy(denominator); - if (product.mod(denominator).eq(0)) { - expect(result.multipliedBy(denominator)).to.be.bignumber.eq(product); - } else { - expect(result.multipliedBy(denominator)).to.be.bignumber.gt(product); - } - return result; - } - async function testSafeGetPartialAmountCeilAsync( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, - ): Promise { - return testExchange.safeGetPartialAmountCeil.callAsync(numerator, denominator, target); - } - await testCombinatoriallyWithReferenceFuncAsync( - 'safeGetPartialAmountCeil', - referenceSafeGetPartialAmountCeilAsync, - testSafeGetPartialAmountCeilAsync, - [uint256Values, uint256Values, uint256Values], - ); - }); - - describe('isRoundingErrorFloor', async () => { - async function testIsRoundingErrorFloorAsync( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, - ): Promise { - return testExchange.isRoundingErrorFloor.callAsync(numerator, denominator, target); - } - await testCombinatoriallyWithReferenceFuncAsync( - 'isRoundingErrorFloor', - referenceIsRoundingErrorFloorAsync, - testIsRoundingErrorFloorAsync, - [uint256Values, uint256Values, uint256Values], - ); - }); - - describe('isRoundingErrorCeil', async () => { - async function testIsRoundingErrorCeilAsync( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, - ): Promise { - return testExchange.isRoundingErrorCeil.callAsync(numerator, denominator, target); - } - await testCombinatoriallyWithReferenceFuncAsync( - 'isRoundingErrorCeil', - referenceIsRoundingErrorCeilAsync, - testIsRoundingErrorCeilAsync, - [uint256Values, uint256Values, uint256Values], - ); - }); -}); - -// TODO(dorothy-zbornak): Add _settleOrder, _dispatchTransferFrom -blockchainTests.resets('Exchange core internal functions', env => { - let chainId: number; +import { + artifacts, + ReferenceFunctions, + TestExchangeInternalsContract, + TestExchangeInternalsFillEventArgs, +} from '../src'; + +// TODO(dorothy-zbornak): Add _settleOrder +blockchainTests.only('Exchange core internal functions', env => { + const CHAIN_ID = 1337; + const EMPTY_ORDER: Order = { + senderAddress: constants.NULL_ADDRESS, + makerAddress: constants.NULL_ADDRESS, + takerAddress: constants.NULL_ADDRESS, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + makerAssetAmount: constants.ZERO_AMOUNT, + takerAssetAmount: constants.ZERO_AMOUNT, + makerAssetData: constants.NULL_BYTES, + takerAssetData: constants.NULL_BYTES, + makerFeeAssetData: constants.NULL_BYTES, + takerFeeAssetData: constants.NULL_BYTES, + salt: constants.ZERO_AMOUNT, + feeRecipientAddress: constants.NULL_ADDRESS, + expirationTimeSeconds: constants.ZERO_AMOUNT, + }; let testExchange: TestExchangeInternalsContract; - let safeMathErrorForSendTransaction: Error | undefined; - let divisionByZeroErrorForCall: Error | undefined; - let roundingErrorForCall: Error | undefined; + let logDecoder: LogDecoder; + let senderAddress: string; before(async () => { - chainId = await providerUtils.getChainIdAsync(env.provider); - emptyOrder.domain.chainId = chainId; - emptySignedOrder.domain.chainId = chainId; - + [ senderAddress ] = await env.getAccountAddressesAsync(); testExchange = await TestExchangeInternalsContract.deployFrom0xArtifactAsync( artifacts.TestExchangeInternals, env.provider, env.txDefaults, - new BigNumber(chainId), + new BigNumber(CHAIN_ID), ); - divisionByZeroErrorForCall = new Error(RevertReason.DivisionByZero); - roundingErrorForCall = new Error(RevertReason.RoundingError); - safeMathErrorForSendTransaction = safeMathErrorForCall; - divisionByZeroErrorForCall = new LibMathRevertErrors.DivisionByZeroError(); - roundingErrorForCall = new LibMathRevertErrors.RoundingError(); + logDecoder = new LogDecoder(env.web3Wrapper, artifacts); }); - // Note(albrow): Don't forget to add beforeEach and afterEach calls to reset - // the blockchain state for any tests which modify it! - - async function referenceIsRoundingErrorFloorAsync( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, - ): Promise { - if (denominator.eq(0)) { - throw divisionByZeroErrorForCall; - } - if (numerator.eq(0)) { - return false; - } - if (target.eq(0)) { - return false; - } - const product = numerator.multipliedBy(target); - const remainder = product.mod(denominator); - const remainderTimes1000 = remainder.multipliedBy('1000'); - const isError = remainderTimes1000.gte(product); - if (product.isGreaterThan(MAX_UINT256)) { - throw safeMathErrorForCall; - } - if (remainderTimes1000.isGreaterThan(MAX_UINT256)) { - throw safeMathErrorForCall; - } - return isError; - } - async function referenceSafeGetPartialAmountFloorAsync( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, - ): Promise { - if (denominator.eq(0)) { - throw divisionByZeroErrorForCall; - } - const isRoundingError = await referenceIsRoundingErrorFloorAsync(numerator, denominator, target); - if (isRoundingError) { - throw roundingErrorForCall; - } - const product = numerator.multipliedBy(target); - if (product.isGreaterThan(MAX_UINT256)) { - throw safeMathErrorForCall; - } - return product.dividedToIntegerBy(denominator); - } - - // TODO(dorothy-zbornak): Move this to `exchange-libs`. - describe('addFillResults', async () => { - function makeFillResults(value: BigNumber): FillResults { - return { - makerAssetFilledAmount: value, - takerAssetFilledAmount: value, - makerFeePaid: value, - takerFeePaid: value, - }; - } - async function referenceAddFillResultsAsync( - totalValue: BigNumber, - singleValue: BigNumber, - ): Promise { - // Note(albrow): Here, each of totalFillResults and - // singleFillResults will consist of fields with the same values. - // This should be safe because none of the fields in a given - // FillResults are ever used together in a mathemetical operation. - // They are only used with the corresponding field from *the other* - // FillResults, which are different. - const totalFillResults = makeFillResults(totalValue); - const singleFillResults = makeFillResults(singleValue); - // HACK(albrow): _.mergeWith mutates the first argument! To - // workaround this we use _.cloneDeep. - return _.mergeWith( - _.cloneDeep(totalFillResults), - singleFillResults, - (totalVal: BigNumber, singleVal: BigNumber) => { - const newTotal = totalVal.plus(singleVal); - if (newTotal.isGreaterThan(MAX_UINT256)) { - throw safeMathErrorForCall; - } - return newTotal; - }, - ); - } - async function testAddFillResultsAsync(totalValue: BigNumber, singleValue: BigNumber): Promise { - const totalFillResults = makeFillResults(totalValue); - const singleFillResults = makeFillResults(singleValue); - return testExchange.addFillResults.callAsync(totalFillResults, singleFillResults); - } - await testCombinatoriallyWithReferenceFuncAsync( - 'addFillResults', - referenceAddFillResultsAsync, - testAddFillResultsAsync, - [uint256Values, uint256Values], - ); - }); - - describe('calculateFillResults', async () => { + blockchainTests('calculateFillResults', async () => { function makeOrder( makerAssetAmount: BigNumber, takerAssetAmount: BigNumber, @@ -428,13 +62,14 @@ blockchainTests.resets('Exchange core internal functions', env => { takerFee: BigNumber, ): Order { return { - ...emptyOrder, + ...EMPTY_ORDER, makerAssetAmount, takerAssetAmount, makerFee, takerFee, }; } + async function referenceCalculateFillResultsAsync( orderTakerAssetAmount: BigNumber, takerAssetFilledAmount: BigNumber, @@ -446,28 +81,12 @@ blockchainTests.resets('Exchange core internal functions', env => { // in any mathematical operation in either the reference TypeScript // implementation or the Solidity implementation of // calculateFillResults. - const makerAssetFilledAmount = await referenceSafeGetPartialAmountFloorAsync( + return ReferenceFunctions.calculateFillResults( + makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount), takerAssetFilledAmount, - orderTakerAssetAmount, - otherAmount, ); - const order = makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount); - const orderMakerAssetAmount = order.makerAssetAmount; - return { - makerAssetFilledAmount, - takerAssetFilledAmount, - makerFeePaid: await referenceSafeGetPartialAmountFloorAsync( - makerAssetFilledAmount, - orderMakerAssetAmount, - otherAmount, - ), - takerFeePaid: await referenceSafeGetPartialAmountFloorAsync( - takerAssetFilledAmount, - orderTakerAssetAmount, - otherAmount, - ), - }; } + async function testCalculateFillResultsAsync( orderTakerAssetAmount: BigNumber, takerAssetFilledAmount: BigNumber, @@ -476,57 +95,126 @@ blockchainTests.resets('Exchange core internal functions', env => { const order = makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount); return testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount); } - await testCombinatoriallyWithReferenceFuncAsync( - 'calculateFillResults', - referenceCalculateFillResultsAsync, - testCalculateFillResultsAsync, - [uint256Values, uint256Values, uint256Values], - ); + + describe.optional('combinatorial tests', () => { + testCombinatoriallyWithReferenceFunc( + 'calculateFillResults', + referenceCalculateFillResultsAsync, + testCalculateFillResultsAsync, + [uint256Values, uint256Values, uint256Values], + ); + }); }); - blockchainTests.resets('updateFilledState', async ({ web3Wrapper }) => { - async function referenceUpdateFilledStateAsync( - takerAssetFilledAmount: BigNumber, - orderTakerAssetFilledAmount: BigNumber, - // tslint:disable-next-line:no-unused-variable - orderHash: string, - ): Promise { - const totalFilledAmount = takerAssetFilledAmount.plus(orderTakerAssetFilledAmount); - if (totalFilledAmount.isGreaterThan(MAX_UINT256)) { - // FIXME throw safeMathErrorForSendTransaction(takerAssetFilledAmount, orderTakerAssetFilledAmount); - throw safeMathErrorForSendTransaction; - } - return totalFilledAmount; + blockchainTests.resets('updateFilledState', async () => { + const ONE_ETHER = new BigNumber(1e18); + const randomAddress = () => hexRandom(constants.ADDRESS_LENGTH); + const randomHash = () => hexRandom(constants.WORD_LENGTH); + const randomAssetData = () => hexRandom(36); + const ORDER_DEFAULTS = { + senderAddress: randomAddress(), + makerAddress: randomAddress(), + takerAddress: randomAddress(), + makerFee: ONE_ETHER.times(0.001), + takerFee: ONE_ETHER.times(0.003), + makerAssetAmount: ONE_ETHER, + takerAssetAmount: ONE_ETHER.times(0.5), + makerAssetData: randomAssetData(), + takerAssetData: randomAssetData(), + makerFeeAssetData: randomAssetData(), + takerFeeAssetData: randomAssetData(), + salt: new BigNumber(_.random(0, 1e8)), + feeRecipientAddress: randomAddress(), + expirationTimeSeconds: new BigNumber(_.random(0, 1e8)), + }; + + function makeOrder(details?: Partial): Order { + return _.assign( + {}, + ORDER_DEFAULTS, + details, + ); } + async function testUpdateFilledStateAsync( - takerAssetFilledAmount: BigNumber, + order: Order, orderTakerAssetFilledAmount: BigNumber, - orderHash: string, - ): Promise { - const fillResults = { - makerAssetFilledAmount: new BigNumber(0), - takerAssetFilledAmount, - makerFeePaid: new BigNumber(0), - takerFeePaid: new BigNumber(0), - }; - await web3Wrapper.awaitTransactionSuccessAsync( - await testExchange.updateFilledState.sendTransactionAsync( - emptySignedOrder, - constants.NULL_ADDRESS, + takerAddress: string, + takerAssetFillAmount: BigNumber, + ): Promise { + const orderHash = randomHash(); + const fillResults = ReferenceFunctions.calculateFillResults( + order, + takerAssetFillAmount, + ); + const expectedFilledState = orderTakerAssetFilledAmount.plus(takerAssetFillAmount); + // CAll `testUpdateFilledState()`, which will set the `filled` + // state for this order to `orderTakerAssetFilledAmount` before + // calling `_updateFilledState()`. + const receipt = await logDecoder.getTxWithDecodedLogsAsync( + await testExchange.testUpdateFilledState.sendTransactionAsync( + order, + takerAddress, orderHash, orderTakerAssetFilledAmount, fillResults, ), - constants.AWAIT_TRANSACTION_MINED_MS, ); - return testExchange.filled.callAsync(orderHash); - } - await testCombinatoriallyWithReferenceFuncAsync( - 'updateFilledState', - referenceUpdateFilledStateAsync, - testUpdateFilledStateAsync, - [uint256Values, uint256Values, bytes32Values], - ); + // Grab the new `filled` state for this order. + const actualFilledState = await testExchange.filled.callAsync(orderHash); + // Assert the `filled` state for this order. + expect(actualFilledState).to.bignumber.eq(expectedFilledState); + // Assert the logs. + const fillEvent = receipt.logs[0] as LogWithDecodedArgs; + expect(fillEvent.event).to.eq('Fill'); + expect(fillEvent.args.makerAddress).to.eq(order.makerAddress); + expect(fillEvent.args.feeRecipientAddress).to.eq(order.feeRecipientAddress); + expect(fillEvent.args.makerAssetData).to.eq(order.makerAssetData); + expect(fillEvent.args.takerAssetData).to.eq(order.takerAssetData); + expect(fillEvent.args.makerFeeAssetData).to.eq(order.makerFeeAssetData); + expect(fillEvent.args.takerFeeAssetData).to.eq(order.takerFeeAssetData); + expect(fillEvent.args.makerAssetFilledAmount).to.bignumber.eq(fillResults.makerAssetFilledAmount); + expect(fillEvent.args.takerAssetFilledAmount).to.bignumber.eq(fillResults.takerAssetFilledAmount); + expect(fillEvent.args.makerFeePaid).to.bignumber.eq(fillResults.makerFeePaid); + expect(fillEvent.args.takerFeePaid).to.bignumber.eq(fillResults.takerFeePaid); + expect(fillEvent.args.takerAddress).to.eq(takerAddress); + expect(fillEvent.args.senderAddress).to.eq(senderAddress); + expect(fillEvent.args.orderHash).to.eq(orderHash); + } + + it('emits a `Fill` event and updates `filled` state correctly', async () => { + const order = makeOrder(); + return testUpdateFilledStateAsync( + order, + order.takerAssetAmount.times(0.1), + randomAddress(), + order.takerAssetAmount.times(0.25), + ); + }); + + it('throws if `leftOrderTakerAssetFilledAmount + fillResults.takerAssetFilledAmount` overflows', async () => { + const order = makeOrder(); + const orderTakerAssetFilledAmount = constants.MAX_UINT256.dividedToIntegerBy(2); + const takerAssetFillAmount = constants.MAX_UINT256.dividedToIntegerBy(2).plus(2); + const fillResults = { + makerAssetFilledAmount: constants.ZERO_AMOUNT, + takerAssetFilledAmount: takerAssetFillAmount, + makerFeePaid: constants.ZERO_AMOUNT, + takerFeePaid: constants.ZERO_AMOUNT, + }; + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256AdditionOverflow, + orderTakerAssetFilledAmount, + takerAssetFillAmount, + ); + return expect(testExchange.testUpdateFilledState.awaitTransactionSuccessAsync( + order, + randomAddress(), + randomHash(), + orderTakerAssetFilledAmount, + fillResults, + )).to.revertWith(expectedError); + }); }); }); // tslint:disable-line:max-file-line-count diff --git a/contracts/exchange/test/isolated_fill_order.ts b/contracts/exchange/test/isolated_fill_order.ts index fad7d368c9..71b28343f3 100644 --- a/contracts/exchange/test/isolated_fill_order.ts +++ b/contracts/exchange/test/isolated_fill_order.ts @@ -9,6 +9,8 @@ import { FillResults, OrderInfo, OrderStatus, SignatureType } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; +import { calculateFillResults } from '../src/reference_functions'; + import { AssetBalances, createBadAssetData, @@ -18,9 +20,8 @@ import { IsolatedExchangeWrapper, Order, } from './utils/isolated_exchange_wrapper'; -import { calculateFillResults } from './utils/reference_functions'; -blockchainTests.only('Isolated fillOrder() tests', env => { +blockchainTests('Isolated fillOrder() tests', env => { const randomAddress = () => hexRandom(constants.ADDRESS_LENGTH); const getCurrentTime = () => Math.floor(_.now() / 1000); const { ZERO_AMOUNT } = constants; diff --git a/contracts/exchange/test/match_orders.ts b/contracts/exchange/test/match_orders.ts index 74edbd9acd..6547813428 100644 --- a/contracts/exchange/test/match_orders.ts +++ b/contracts/exchange/test/match_orders.ts @@ -11,6 +11,7 @@ import { import { ERC1155Contract as ERC1155TokenContract, Erc1155Wrapper as ERC1155Wrapper } from '@0x/contracts-erc1155'; import { DummyERC20TokenContract } from '@0x/contracts-erc20'; import { DummyERC721TokenContract } from '@0x/contracts-erc721'; +import { ReferenceFunctions as LibReferenceFunctions } from '@0x/contracts-exchange-libs'; import { chaiSetup, constants, @@ -34,7 +35,6 @@ import { ExchangeContract, ExchangeWrapper, ReentrantERC20TokenContract, - TestExchangeMathContract, } from '../src'; import { MatchOrderTester, TokenBalances } from './utils/match_order_tester'; @@ -42,6 +42,7 @@ import { MatchOrderTester, TokenBalances } from './utils/match_order_tester'; const ZERO = new BigNumber(0); const ONE = new BigNumber(1); const TWO = new BigNumber(2); +const { isRoundingErrorCeil, isRoundingErrorFloor } = LibReferenceFunctions; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); chaiSetup.configure(); @@ -88,8 +89,6 @@ describe('matchOrders', () => { let matchOrderTester: MatchOrderTester; - let testExchangeMath: TestExchangeMathContract; - before(async () => { await blockchainLifecycle.startAsync(); }); @@ -240,11 +239,6 @@ describe('matchOrders', () => { orderFactoryLeft = new OrderFactory(privateKeyLeft, defaultOrderParamsLeft); const privateKeyRight = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressRight)]; orderFactoryRight = new OrderFactory(privateKeyRight, defaultOrderParamsRight); - testExchangeMath = await TestExchangeMathContract.deployFrom0xArtifactAsync( - artifacts.TestExchangeMath, - provider, - txDefaults, - ); // Create match order tester matchOrderTester = new MatchOrderTester(exchangeWrapper, erc20Wrapper, erc721Wrapper, erc1155ProxyWrapper); tokenBalances = await matchOrderTester.getBalancesAsync(); @@ -276,18 +270,18 @@ describe('matchOrders', () => { const numerator = signedOrderLeft.makerAssetAmount; const denominator = signedOrderLeft.takerAssetAmount; const target = signedOrderRight.makerAssetAmount; - const isRoundingErrorCeil = await testExchangeMath.isRoundingErrorCeil.callAsync( + const _isRoundingErrorCeil = isRoundingErrorCeil( numerator, denominator, target, ); - expect(isRoundingErrorCeil).to.be.true(); - const isRoundingErrorFloor = await testExchangeMath.isRoundingErrorFloor.callAsync( + expect(_isRoundingErrorCeil).to.be.true(); + const _isRoundingErrorFloor = isRoundingErrorFloor( numerator, denominator, target, ); - expect(isRoundingErrorFloor).to.be.false(); + expect(_isRoundingErrorFloor).to.be.false(); // Match signedOrderLeft with signedOrderRight // Note that the left maker received a slightly better sell price. // This is intentional; see note in MixinMatchOrders.calculateMatchedFillResults. @@ -342,20 +336,19 @@ describe('matchOrders', () => { const numerator = signedOrderRight.takerAssetAmount; const denominator = signedOrderRight.makerAssetAmount; const target = signedOrderLeft.takerAssetAmount; - const isRoundingErrorFloor = await testExchangeMath.isRoundingErrorFloor.callAsync( + const _isRoundingErrorFloor = isRoundingErrorFloor( numerator, denominator, target, ); - expect(isRoundingErrorFloor).to.be.true(); - const isRoundingErrorCeil = await testExchangeMath.isRoundingErrorCeil.callAsync( + expect(_isRoundingErrorFloor).to.be.true(); + const _isRoundingErrorCeil = isRoundingErrorCeil( numerator, denominator, target, ); - expect(isRoundingErrorCeil).to.be.false(); - // Match signedOrderLeft with signedOrderRight - // Note that the right maker received a slightly better purchase price. + expect(_isRoundingErrorCeil).to.be.false(); + // Match signedOrderLeft isRoundingErrorFloor right maker received a slightly better purchase price. // This is intentional; see note in MixinMatchOrders.calculateMatchedFillResults. // Because the right maker received a slightly more favorable buy price, the fee // paid by the right taker is slightly higher than that paid by the right maker. @@ -1421,18 +1414,18 @@ describe('matchOrders', () => { const numerator = signedOrderLeft.makerAssetAmount; const denominator = signedOrderLeft.takerAssetAmount; const target = signedOrderRight.makerAssetAmount; - const isRoundingErrorCeil = await testExchangeMath.isRoundingErrorCeil.callAsync( + const _isRoundingErrorCeil = isRoundingErrorCeil( numerator, denominator, target, ); - expect(isRoundingErrorCeil).to.be.true(); - const isRoundingErrorFloor = await testExchangeMath.isRoundingErrorFloor.callAsync( + expect(_isRoundingErrorCeil).to.be.true(); + const _isRoundingErrorFloor = isRoundingErrorFloor( numerator, denominator, target, ); - expect(isRoundingErrorFloor).to.be.false(); + expect(_isRoundingErrorFloor).to.be.false(); // Match signedOrderLeft with signedOrderRight // Note that the left maker received a slightly better sell price. // This is intentional; see note in MixinMatchOrders.calculateMatchedFillResults. @@ -1487,18 +1480,18 @@ describe('matchOrders', () => { const numerator = signedOrderRight.makerAssetAmount; const denominator = signedOrderRight.takerAssetAmount; const target = signedOrderLeft.makerAssetAmount; - const isRoundingErrorCeil = await testExchangeMath.isRoundingErrorCeil.callAsync( + const _isRoundingErrorCeil = isRoundingErrorCeil( numerator, denominator, target, ); - expect(isRoundingErrorCeil).to.be.false(); - const isRoundingErrorFloor = await testExchangeMath.isRoundingErrorFloor.callAsync( + expect(_isRoundingErrorCeil).to.be.false(); + const _isRoundingErrorFloor = isRoundingErrorFloor( numerator, denominator, target, ); - expect(isRoundingErrorFloor).to.be.false(); + expect(_isRoundingErrorFloor).to.be.false(); // Match signedOrderLeft with signedOrderRight // Note that the right maker received a slightly better purchase price. // This is intentional; see note in MixinMatchOrders.calculateMatchedFillResults. diff --git a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts index 7f92c80479..43f483d782 100644 --- a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts +++ b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts @@ -156,7 +156,7 @@ export function createBadSignature(type: SignatureType = SignatureType.EIP712): return `0x00${Buffer.from([type]).toString('hex')}`; } -const ERC20_ASSET_DATA_LENGTH = 24; +const ERC20_ASSET_DATA_LENGTH = 36; /** * Create asset data for the `IsolatedExchange` contract that will pass. diff --git a/contracts/exchange/tsconfig.json b/contracts/exchange/tsconfig.json index 3d2713ff02..319109caae 100644 --- a/contracts/exchange/tsconfig.json +++ b/contracts/exchange/tsconfig.json @@ -18,7 +18,6 @@ "generated-artifacts/ReentrantERC20Token.json", "generated-artifacts/TestAssetProxyDispatcher.json", "generated-artifacts/TestExchangeInternals.json", - "generated-artifacts/TestExchangeMath.json", "generated-artifacts/TestLibExchangeRichErrorDecoder.json", "generated-artifacts/TestSignatureValidator.json", "generated-artifacts/TestValidatorWallet.json", diff --git a/contracts/utils/src/index.ts b/contracts/utils/src/index.ts index 7e9c4805a6..0233c608d1 100644 --- a/contracts/utils/src/index.ts +++ b/contracts/utils/src/index.ts @@ -1,5 +1,5 @@ export * from './artifacts'; export * from './wrappers'; -import * as reference_functions from './reference_functions'; -export import ReferenceFunctions = reference_functions; +import * as ReferenceFunctionsToExport from './reference_functions'; +export import ReferenceFunctions = ReferenceFunctionsToExport; From afb310e90aa48cd101ca7cc7e96cb319e8c9eb5d Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 1 Aug 2019 18:27:24 -0400 Subject: [PATCH 30/48] `@0x/contracts-exchange`: Update changelog. --- contracts/exchange/CHANGELOG.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/contracts/exchange/CHANGELOG.json b/contracts/exchange/CHANGELOG.json index cf51699254..abdb2c003a 100644 --- a/contracts/exchange/CHANGELOG.json +++ b/contracts/exchange/CHANGELOG.json @@ -123,7 +123,15 @@ "pr": "TODO" }, { - "note": "Add `test/utils/reference_functions.ts` for `calculateFillResults`", + "note": "Add `ReferenceFunctions` as package export.", + "pr": "TODO" + }, + { + "note": "Remove `TestExchangeMath.sol`. Exchange math functions are now tested in the `exchange-libs` package and reference implementations are available there as well.", + "pr": "TODO" + }, + { + "note": "Remove functions from `TestExchangeInternals.sol` that are no longer tested in this package.", "pr": "TODO" } ] From a179a6892cf8a567fa45c50957f6fe2931ec258e Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 2 Aug 2019 11:10:13 -0400 Subject: [PATCH 31/48] `@0x/contracts-test-utils`: Add `ONE_ETHER` and `MAX_UINT256_ROOT` constants. --- contracts/test-utils/CHANGELOG.json | 2 +- contracts/test-utils/src/constants.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/test-utils/CHANGELOG.json b/contracts/test-utils/CHANGELOG.json index aff50f2677..c69893c22c 100644 --- a/contracts/test-utils/CHANGELOG.json +++ b/contracts/test-utils/CHANGELOG.json @@ -51,7 +51,7 @@ "pr": "TODO" }, { - "note": "Add the constants: `MAX_UINT256`, `ADDRESS_LENGTH`", + "note": "Add the constants: `MAX_UINT256`, `ADDRESS_LENGTH`, `MAX_UINT256_ROOT`, `ONE_ETHER`", "pr": "TODO" }, { diff --git a/contracts/test-utils/src/constants.ts b/contracts/test-utils/src/constants.ts index 7519018744..380f08929b 100644 --- a/contracts/test-utils/src/constants.ts +++ b/contracts/test-utils/src/constants.ts @@ -64,4 +64,6 @@ export const constants = { PERCENTAGE_DENOMINATOR: new BigNumber(10).pow(18), TIME_BUFFER: new BigNumber(1000), KECCAK256_NULL: ethUtil.addHexPrefix(ethUtil.bufferToHex(ethUtil.SHA3_NULL)), + MAX_UINT256_ROOT: new BigNumber('340282366920938463463374607431768211456'), + ONE_ETHER: new BigNumber(1e18), }; From 293510c087cc0642a908ce16ec3d526ea2c3a6a3 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 2 Aug 2019 13:02:16 -0400 Subject: [PATCH 32/48] `@0x/contracts-exchange-libs`: Add explicit tests for `LibMath` and `LibFillResults` functions. `@0x/contracts-exchange-libs`: Add tests for `ReferenceFunctions`. --- .../exchange-libs/test/lib_fill_results.ts | 114 ++++--- contracts/exchange-libs/test/lib_math.ts | 271 ++++++++++++++- .../exchange-libs/test/reference_functions.ts | 310 ++++++++++++++++++ 3 files changed, 652 insertions(+), 43 deletions(-) create mode 100644 contracts/exchange-libs/test/reference_functions.ts diff --git a/contracts/exchange-libs/test/lib_fill_results.ts b/contracts/exchange-libs/test/lib_fill_results.ts index d4c7f4b94c..ebfd3a4957 100644 --- a/contracts/exchange-libs/test/lib_fill_results.ts +++ b/contracts/exchange-libs/test/lib_fill_results.ts @@ -1,16 +1,17 @@ import { blockchainTests, + constants, describe, - testCombinatoriallyWithReferenceFunc, - uint256Values, + expect, } from '@0x/contracts-test-utils'; -import { FillResults } from '@0x/types'; -import { BigNumber } from '@0x/utils'; +import { BigNumber, SafeMathRevertErrors } from '@0x/utils'; +import * as _ from 'lodash'; import { artifacts, ReferenceFunctions, TestLibsContract } from '../src'; blockchainTests('LibFillResults', env => { const CHAIN_ID = 1337; + const { ONE_ETHER, MAX_UINT256 } = constants; let libsContract: TestLibsContract; before(async () => { @@ -23,47 +24,76 @@ blockchainTests('LibFillResults', env => { }); describe('addFillResults', () => { - function makeFillResults(value: BigNumber): FillResults { - // HACK(dorothy-zbornak): We reuse values across fields, - // but this is fine because `addFillResults()` never does - // any math between them. - return { - makerAssetFilledAmount: value, - takerAssetFilledAmount: value, - makerFeePaid: value, - takerFeePaid: value, - }; - } + describe('explicit tests', () => { + const DEFAULT_FILL_RESULTS = [ + { + makerAssetFilledAmount: ONE_ETHER, + takerAssetFilledAmount: ONE_ETHER.times(2), + makerFeePaid: ONE_ETHER.times(0.001), + takerFeePaid: ONE_ETHER.times(0.002), + }, + { + makerAssetFilledAmount: ONE_ETHER.times(0.01), + takerAssetFilledAmount: ONE_ETHER.times(2).times(0.01), + makerFeePaid: ONE_ETHER.times(0.001).times(0.01), + takerFeePaid: ONE_ETHER.times(0.002).times(0.01), + }, + ]; - async function referenceAddFillResultsAsync( - totalValue: BigNumber, - singleValue: BigNumber, - ): Promise { - return ReferenceFunctions.addFillResults( - makeFillResults(totalValue), - makeFillResults(singleValue), - ); - } + it('matches the output of the reference function', async () => { + const [ a, b ] = DEFAULT_FILL_RESULTS; + const expected = ReferenceFunctions.addFillResults(a, b); + const actual = await libsContract.addFillResults.callAsync(a, b); + expect(actual).to.deep.equal(expected); + }); - async function testAddFillResultsAsync( - totalValue: BigNumber, - singleValue: BigNumber, - ): Promise { - return libsContract.addFillResults.callAsync( - makeFillResults(totalValue), - makeFillResults(singleValue), - ); - } + it('reverts if computing `makerAssetFilledAmount` overflows', async () => { + const [ a, b ] = _.cloneDeep(DEFAULT_FILL_RESULTS); + b.makerAssetFilledAmount = MAX_UINT256; + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256AdditionOverflow, + a.makerAssetFilledAmount, + b.makerAssetFilledAmount, + ); + return expect(libsContract.addFillResults.callAsync(a, b)) + .to.revertWith(expectedError); + }); - // TODO(dorothy-zbornak): Do we really need these? - // Just a couple edge cases would likely suffice. - describe.optional('combinatorial tests', () => { - testCombinatoriallyWithReferenceFunc( - 'addFillResults', - referenceAddFillResultsAsync, - testAddFillResultsAsync, - [uint256Values, uint256Values], - ); + it('reverts if computing `takerAssetFilledAmount` overflows', async () => { + const [ a, b ] = _.cloneDeep(DEFAULT_FILL_RESULTS); + b.takerAssetFilledAmount = MAX_UINT256; + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256AdditionOverflow, + a.takerAssetFilledAmount, + b.takerAssetFilledAmount, + ); + return expect(libsContract.addFillResults.callAsync(a, b)) + .to.revertWith(expectedError); + }); + + it('reverts if computing `makerFeePaid` overflows', async () => { + const [ a, b ] = _.cloneDeep(DEFAULT_FILL_RESULTS); + b.makerFeePaid = MAX_UINT256; + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256AdditionOverflow, + a.makerFeePaid, + b.makerFeePaid, + ); + return expect(libsContract.addFillResults.callAsync(a, b)) + .to.revertWith(expectedError); + }); + + it('reverts if computing `takerFeePaid` overflows', async () => { + const [ a, b ] = _.cloneDeep(DEFAULT_FILL_RESULTS); + b.takerFeePaid = MAX_UINT256; + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256AdditionOverflow, + a.takerFeePaid, + b.takerFeePaid, + ); + return expect(libsContract.addFillResults.callAsync(a, b)) + .to.revertWith(expectedError); + }); }); }); }); diff --git a/contracts/exchange-libs/test/lib_math.ts b/contracts/exchange-libs/test/lib_math.ts index b6db862909..9215a1e747 100644 --- a/contracts/exchange-libs/test/lib_math.ts +++ b/contracts/exchange-libs/test/lib_math.ts @@ -1,15 +1,19 @@ import { blockchainTests, + constants, describe, + expect, testCombinatoriallyWithReferenceFunc, uint256Values, } from '@0x/contracts-test-utils'; -import { BigNumber } from '@0x/utils'; +import { LibMathRevertErrors } from '@0x/order-utils'; +import { BigNumber, SafeMathRevertErrors } from '@0x/utils'; import { artifacts, ReferenceFunctions, TestLibsContract } from '../src'; blockchainTests('LibMath', env => { const CHAIN_ID = 1337; + const { ONE_ETHER, MAX_UINT256, MAX_UINT256_ROOT, ZERO_AMOUNT } = constants; let libsContract: TestLibsContract; before(async () => { @@ -48,6 +52,43 @@ blockchainTests('LibMath', env => { [uint256Values, uint256Values, uint256Values], ); }); + + describe('explicit tests', () => { + it('matches the reference function output', async () => { + const numerator = ONE_ETHER; + const denominator = ONE_ETHER.dividedToIntegerBy(2); + const target = ONE_ETHER.times(0.01); + const expected = ReferenceFunctions.getPartialAmountFloor(numerator, denominator, target); + const actual = await libsContract.getPartialAmountFloor.callAsync(numerator, denominator, target); + expect(actual).to.bignumber.eq(expected); + }); + + it('reverts if `denominator` is zero', async () => { + const numerator = ONE_ETHER; + const denominator = ZERO_AMOUNT; + const target = ONE_ETHER.times(0.01); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256DivisionByZero, + numerator.times(target), + denominator, + ); + return expect(libsContract.getPartialAmountFloor.callAsync(numerator, denominator, target)) + .to.revertWith(expectedError); + }); + + it('reverts if `numerator * target` overflows', async () => { + const numerator = MAX_UINT256; + const denominator = ONE_ETHER.dividedToIntegerBy(2); + const target = MAX_UINT256_ROOT.times(2); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + numerator, + target, + ); + return expect(libsContract.getPartialAmountFloor.callAsync(numerator, denominator, target)) + .to.revertWith(expectedError); + }); + }); }); describe('getPartialAmountCeil', () => { @@ -59,6 +100,44 @@ blockchainTests('LibMath', env => { [uint256Values, uint256Values, uint256Values], ); }); + + describe('explicit tests', () => { + it('matches the reference function output', async () => { + const numerator = ONE_ETHER; + const denominator = ONE_ETHER.dividedToIntegerBy(2); + const target = ONE_ETHER.times(0.01); + const expected = ReferenceFunctions.getPartialAmountCeil(numerator, denominator, target); + const actual = await libsContract.getPartialAmountCeil.callAsync(numerator, denominator, target); + expect(actual).to.bignumber.eq(expected); + }); + + it('reverts if `denominator` is zero', async () => { + const numerator = ONE_ETHER; + const denominator = ZERO_AMOUNT; + const target = ONE_ETHER.times(0.01); + // This will actually manifest as a subtraction underflow. + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256SubtractionUnderflow, + denominator, + new BigNumber(1), + ); + return expect(libsContract.getPartialAmountCeil.callAsync(numerator, denominator, target)) + .to.revertWith(expectedError); + }); + + it('reverts if `numerator * target` overflows', async () => { + const numerator = MAX_UINT256; + const denominator = ONE_ETHER.dividedToIntegerBy(2); + const target = MAX_UINT256_ROOT.times(2); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + numerator, + target, + ); + return expect(libsContract.getPartialAmountCeil.callAsync(numerator, denominator, target)) + .to.revertWith(expectedError); + }); + }); }); describe('safeGetPartialAmountFloor', () => { @@ -70,6 +149,52 @@ blockchainTests('LibMath', env => { [uint256Values, uint256Values, uint256Values], ); }); + + describe('explicit tests', () => { + it('matches the reference function output', async () => { + const numerator = ONE_ETHER; + const denominator = ONE_ETHER.dividedToIntegerBy(2); + const target = ONE_ETHER.times(0.01); + const expected = ReferenceFunctions.safeGetPartialAmountFloor(numerator, denominator, target); + const actual = await libsContract.safeGetPartialAmountFloor.callAsync(numerator, denominator, target); + expect(actual).to.bignumber.eq(expected); + }); + + it('reverts for a rounding error', async () => { + const numerator = new BigNumber(1e3); + const denominator = new BigNumber(1e4); + const target = new BigNumber(333); + const expectedError = new LibMathRevertErrors.RoundingError( + numerator, + denominator, + target, + ); + return expect(libsContract.safeGetPartialAmountFloor.callAsync(numerator, denominator, target)) + .to.revertWith(expectedError); + }); + + it('reverts if `denominator` is zero', async () => { + const numerator = ONE_ETHER; + const denominator = ZERO_AMOUNT; + const target = ONE_ETHER.times(0.01); + const expectedError = new LibMathRevertErrors.DivisionByZeroError(); + return expect(libsContract.safeGetPartialAmountFloor.callAsync(numerator, denominator, target)) + .to.revertWith(expectedError); + }); + + it('reverts if `numerator * target` overflows', async () => { + const numerator = MAX_UINT256; + const denominator = ONE_ETHER.dividedToIntegerBy(2); + const target = MAX_UINT256_ROOT.times(2); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + numerator, + target, + ); + return expect(libsContract.safeGetPartialAmountFloor.callAsync(numerator, denominator, target)) + .to.revertWith(expectedError); + }); + }); }); describe('safeGetPartialAmountCeil', () => { @@ -81,6 +206,52 @@ blockchainTests('LibMath', env => { [uint256Values, uint256Values, uint256Values], ); }); + + describe('explicit tests', () => { + it('matches the reference function output', async () => { + const numerator = ONE_ETHER; + const denominator = ONE_ETHER.dividedToIntegerBy(2); + const target = ONE_ETHER.times(0.01); + const expected = ReferenceFunctions.safeGetPartialAmountCeil(numerator, denominator, target); + const actual = await libsContract.safeGetPartialAmountCeil.callAsync(numerator, denominator, target); + expect(actual).to.bignumber.eq(expected); + }); + + it('reverts for a rounding error', async () => { + const numerator = new BigNumber(1e3); + const denominator = new BigNumber(1e4); + const target = new BigNumber(333); + const expectedError = new LibMathRevertErrors.RoundingError( + numerator, + denominator, + target, + ); + return expect(libsContract.safeGetPartialAmountCeil.callAsync(numerator, denominator, target)) + .to.revertWith(expectedError); + }); + + it('reverts if `denominator` is zero', async () => { + const numerator = ONE_ETHER; + const denominator = ZERO_AMOUNT; + const target = ONE_ETHER.times(0.01); + const expectedError = new LibMathRevertErrors.DivisionByZeroError(); + return expect(libsContract.safeGetPartialAmountCeil.callAsync(numerator, denominator, target)) + .to.revertWith(expectedError); + }); + + it('reverts if `numerator * target` overflows', async () => { + const numerator = MAX_UINT256; + const denominator = ONE_ETHER.dividedToIntegerBy(2); + const target = MAX_UINT256_ROOT.times(2); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + numerator, + target, + ); + return expect(libsContract.safeGetPartialAmountCeil.callAsync(numerator, denominator, target)) + .to.revertWith(expectedError); + }); + }); }); describe('isRoundingErrorFloor', () => { @@ -92,6 +263,55 @@ blockchainTests('LibMath', env => { [uint256Values, uint256Values, uint256Values], ); }); + + describe('explicit tests', () => { + it('returns true for a rounding error', async () => { + const numerator = new BigNumber(1e3); + const denominator = new BigNumber(1e4); + const target = new BigNumber(333); + const actual = await libsContract.isRoundingErrorFloor.callAsync(numerator, denominator, target); + expect(actual).to.eq(true); + }); + + it('returns false for not a rounding error', async () => { + const numerator = new BigNumber(1e3); + const denominator = new BigNumber(1e4); + const target = new BigNumber(5e2); + const actual = await libsContract.isRoundingErrorFloor.callAsync(numerator, denominator, target); + expect(actual).to.eq(false); + }); + + it('matches the reference function output', async () => { + const numerator = ONE_ETHER; + const denominator = ONE_ETHER.dividedToIntegerBy(2); + const target = ONE_ETHER.times(0.01); + const expected = ReferenceFunctions.isRoundingErrorFloor(numerator, denominator, target); + const actual = await libsContract.isRoundingErrorFloor.callAsync(numerator, denominator, target); + expect(actual).to.eq(expected); + }); + + it('reverts if `denominator` is zero', async () => { + const numerator = ONE_ETHER; + const denominator = ZERO_AMOUNT; + const target = ONE_ETHER.times(0.01); + const expectedError = new LibMathRevertErrors.DivisionByZeroError(); + return expect(libsContract.isRoundingErrorFloor.callAsync(numerator, denominator, target)) + .to.revertWith(expectedError); + }); + + it('reverts if `numerator * target` overflows', async () => { + const numerator = MAX_UINT256; + const denominator = ONE_ETHER.dividedToIntegerBy(2); + const target = MAX_UINT256_ROOT.times(2); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + numerator, + target, + ); + return expect(libsContract.isRoundingErrorFloor.callAsync(numerator, denominator, target)) + .to.revertWith(expectedError); + }); + }); }); describe('isRoundingErrorCeil', () => { @@ -103,5 +323,54 @@ blockchainTests('LibMath', env => { [uint256Values, uint256Values, uint256Values], ); }); + + describe('explicit tests', () => { + it('returns true for a rounding error', async () => { + const numerator = new BigNumber(1e3); + const denominator = new BigNumber(1e4); + const target = new BigNumber(333); + const actual = await libsContract.isRoundingErrorFloor.callAsync(numerator, denominator, target); + expect(actual).to.eq(true); + }); + + it('returns false for not a rounding error', async () => { + const numerator = new BigNumber(1e3); + const denominator = new BigNumber(1e4); + const target = new BigNumber(5e2); + const actual = await libsContract.isRoundingErrorFloor.callAsync(numerator, denominator, target); + expect(actual).to.eq(false); + }); + + it('matches the reference function output', async () => { + const numerator = ONE_ETHER; + const denominator = ONE_ETHER.dividedToIntegerBy(2); + const target = ONE_ETHER.times(0.01); + const expected = ReferenceFunctions.isRoundingErrorCeil(numerator, denominator, target); + const actual = await libsContract.isRoundingErrorCeil.callAsync(numerator, denominator, target); + expect(actual).to.eq(expected); + }); + + it('reverts if `denominator` is zero', async () => { + const numerator = ONE_ETHER; + const denominator = ZERO_AMOUNT; + const target = ONE_ETHER.times(0.01); + const expectedError = new LibMathRevertErrors.DivisionByZeroError(); + return expect(libsContract.isRoundingErrorCeil.callAsync(numerator, denominator, target)) + .to.revertWith(expectedError); + }); + + it('reverts if `numerator * target` overflows', async () => { + const numerator = MAX_UINT256; + const denominator = ONE_ETHER.dividedToIntegerBy(2); + const target = MAX_UINT256_ROOT.times(2); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + numerator, + target, + ); + return expect(libsContract.isRoundingErrorCeil.callAsync(numerator, denominator, target)) + .to.revertWith(expectedError); + }); + }); }); }); diff --git a/contracts/exchange-libs/test/reference_functions.ts b/contracts/exchange-libs/test/reference_functions.ts new file mode 100644 index 0000000000..c7ea2536e2 --- /dev/null +++ b/contracts/exchange-libs/test/reference_functions.ts @@ -0,0 +1,310 @@ +import { + constants, + describe, + expect, +} from '@0x/contracts-test-utils'; +import { LibMathRevertErrors } from '@0x/order-utils'; +import { BigNumber, SafeMathRevertErrors } from '@0x/utils'; +import * as _ from 'lodash'; + +import { + addFillResults, + getPartialAmountCeil, + getPartialAmountFloor, + isRoundingErrorCeil, + isRoundingErrorFloor, + safeGetPartialAmountCeil, + safeGetPartialAmountFloor, +} from '../src/reference_functions'; + +describe('Reference Functions', () => { + const { ONE_ETHER, MAX_UINT256, MAX_UINT256_ROOT, ZERO_AMOUNT } = constants; + describe('LibFillResults', () => { + + describe('addFillResults', () => { + const DEFAULT_FILL_RESULTS = [ + { + makerAssetFilledAmount: ONE_ETHER, + takerAssetFilledAmount: ONE_ETHER.times(2), + makerFeePaid: ONE_ETHER.times(0.001), + takerFeePaid: ONE_ETHER.times(0.002), + }, + { + makerAssetFilledAmount: ONE_ETHER.times(0.01), + takerAssetFilledAmount: ONE_ETHER.times(2).times(0.01), + makerFeePaid: ONE_ETHER.times(0.001).times(0.01), + takerFeePaid: ONE_ETHER.times(0.002).times(0.01), + }, + ]; + + it('reverts if computing `makerAssetFilledAmount` overflows', () => { + const [ a, b ] = _.cloneDeep(DEFAULT_FILL_RESULTS); + b.makerAssetFilledAmount = MAX_UINT256; + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256AdditionOverflow, + a.makerAssetFilledAmount, + b.makerAssetFilledAmount, + ); + expect(() => addFillResults(a, b)).to.throw(expectedError.message); + }); + + it('reverts if computing `takerAssetFilledAmount` overflows', () => { + const [ a, b ] = _.cloneDeep(DEFAULT_FILL_RESULTS); + b.takerAssetFilledAmount = MAX_UINT256; + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256AdditionOverflow, + a.takerAssetFilledAmount, + b.takerAssetFilledAmount, + ); + expect(() => addFillResults(a, b)).to.throw(expectedError.message); + }); + + it('reverts if computing `makerFeePaid` overflows', () => { + const [ a, b ] = _.cloneDeep(DEFAULT_FILL_RESULTS); + b.makerFeePaid = MAX_UINT256; + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256AdditionOverflow, + a.makerFeePaid, + b.makerFeePaid, + ); + expect(() => addFillResults(a, b)).to.throw(expectedError.message); + }); + + it('reverts if computing `takerFeePaid` overflows', () => { + const [ a, b ] = _.cloneDeep(DEFAULT_FILL_RESULTS); + b.takerFeePaid = MAX_UINT256; + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256AdditionOverflow, + a.takerFeePaid, + b.takerFeePaid, + ); + expect(() => addFillResults(a, b)).to.throw(expectedError.message); + }); + }); + }); + + describe('LibMath', () => { + describe('getPartialAmountFloor', () => { + describe('explicit tests', () => { + it('reverts if `denominator` is zero', () => { + const numerator = ONE_ETHER; + const denominator = ZERO_AMOUNT; + const target = ONE_ETHER.times(0.01); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256DivisionByZero, + numerator.times(target), + denominator, + ); + return expect(() => getPartialAmountFloor(numerator, denominator, target)) + .to.throw(expectedError.message); + }); + + it('reverts if `numerator * target` overflows', () => { + const numerator = MAX_UINT256; + const denominator = ONE_ETHER.dividedToIntegerBy(2); + const target = MAX_UINT256_ROOT.times(2); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + numerator, + target, + ); + return expect(() => getPartialAmountFloor(numerator, denominator, target)) + .to.throw(expectedError.message); + }); + }); + }); + + describe('getPartialAmountCeil', () => { + describe('explicit tests', () => { + it('reverts if `denominator` is zero', () => { + const numerator = ONE_ETHER; + const denominator = ZERO_AMOUNT; + const target = ONE_ETHER.times(0.01); + // This will actually manifest as a subtraction underflow. + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256SubtractionUnderflow, + denominator, + new BigNumber(1), + ); + return expect(() => getPartialAmountCeil(numerator, denominator, target)) + .to.throw(expectedError.message); + }); + + it('reverts if `numerator * target` overflows', () => { + const numerator = MAX_UINT256; + const denominator = ONE_ETHER.dividedToIntegerBy(2); + const target = MAX_UINT256_ROOT.times(2); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + numerator, + target, + ); + return expect(() => getPartialAmountCeil(numerator, denominator, target)) + .to.throw(expectedError.message); + }); + }); + }); + + describe('safeGetPartialAmountFloor', () => { + describe('explicit tests', () => { + it('reverts for a rounding error', () => { + const numerator = new BigNumber(1e3); + const denominator = new BigNumber(1e4); + const target = new BigNumber(333); + const expectedError = new LibMathRevertErrors.RoundingError( + numerator, + denominator, + target, + ); + return expect(() => safeGetPartialAmountFloor(numerator, denominator, target)) + .to.throw(expectedError.message); + }); + + it('reverts if `denominator` is zero', () => { + const numerator = ONE_ETHER; + const denominator = ZERO_AMOUNT; + const target = ONE_ETHER.times(0.01); + const expectedError = new LibMathRevertErrors.DivisionByZeroError(); + return expect(() => safeGetPartialAmountFloor(numerator, denominator, target)) + .to.throw(expectedError.message); + }); + + it('reverts if `numerator * target` overflows', () => { + const numerator = MAX_UINT256; + const denominator = ONE_ETHER.dividedToIntegerBy(2); + const target = MAX_UINT256_ROOT.times(2); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + numerator, + target, + ); + return expect(() => safeGetPartialAmountFloor(numerator, denominator, target)) + .to.throw(expectedError.message); + }); + }); + }); + + describe('safeGetPartialAmountCeil', () => { + describe('explicit tests', () => { + it('reverts for a rounding error', () => { + const numerator = new BigNumber(1e3); + const denominator = new BigNumber(1e4); + const target = new BigNumber(333); + const expectedError = new LibMathRevertErrors.RoundingError( + numerator, + denominator, + target, + ); + return expect(() => safeGetPartialAmountCeil(numerator, denominator, target)) + .to.throw(expectedError.message); + }); + + it('reverts if `denominator` is zero', () => { + const numerator = ONE_ETHER; + const denominator = ZERO_AMOUNT; + const target = ONE_ETHER.times(0.01); + const expectedError = new LibMathRevertErrors.DivisionByZeroError(); + return expect(() => safeGetPartialAmountCeil(numerator, denominator, target)) + .to.throw(expectedError.message); + }); + + it('reverts if `numerator * target` overflows', () => { + const numerator = MAX_UINT256; + const denominator = ONE_ETHER.dividedToIntegerBy(2); + const target = MAX_UINT256_ROOT.times(2); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + numerator, + target, + ); + return expect(() => safeGetPartialAmountCeil(numerator, denominator, target)) + .to.throw(expectedError.message); + }); + }); + }); + + describe('isRoundingErrorFloor', () => { + describe('explicit tests', () => { + it('returns true for a rounding error', () => { + const numerator = new BigNumber(1e3); + const denominator = new BigNumber(1e4); + const target = new BigNumber(333); + const actual = isRoundingErrorFloor(numerator, denominator, target); + expect(actual).to.eq(true); + }); + + it('returns false for not a rounding error', () => { + const numerator = new BigNumber(1e3); + const denominator = new BigNumber(1e4); + const target = new BigNumber(5e2); + const actual = isRoundingErrorFloor(numerator, denominator, target); + expect(actual).to.eq(false); + }); + + it('reverts if `denominator` is zero', () => { + const numerator = ONE_ETHER; + const denominator = ZERO_AMOUNT; + const target = ONE_ETHER.times(0.01); + const expectedError = new LibMathRevertErrors.DivisionByZeroError(); + return expect(() => isRoundingErrorFloor(numerator, denominator, target)) + .to.throw(expectedError.message); + }); + + it('reverts if `numerator * target` overflows', () => { + const numerator = MAX_UINT256; + const denominator = ONE_ETHER.dividedToIntegerBy(2); + const target = MAX_UINT256_ROOT.times(2); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + numerator, + target, + ); + return expect(() => isRoundingErrorFloor(numerator, denominator, target)) + .to.throw(expectedError.message); + }); + }); + }); + + describe('isRoundingErrorCeil', () => { + describe('explicit tests', () => { + it('returns true for a rounding error', () => { + const numerator = new BigNumber(1e3); + const denominator = new BigNumber(1e4); + const target = new BigNumber(333); + const actual = isRoundingErrorFloor(numerator, denominator, target); + expect(actual).to.eq(true); + }); + + it('returns false for not a rounding error', () => { + const numerator = new BigNumber(1e3); + const denominator = new BigNumber(1e4); + const target = new BigNumber(5e2); + const actual = isRoundingErrorFloor(numerator, denominator, target); + expect(actual).to.eq(false); + }); + + it('reverts if `denominator` is zero', () => { + const numerator = ONE_ETHER; + const denominator = ZERO_AMOUNT; + const target = ONE_ETHER.times(0.01); + const expectedError = new LibMathRevertErrors.DivisionByZeroError(); + return expect(() => isRoundingErrorCeil(numerator, denominator, target)) + .to.throw(expectedError.message); + }); + + it('reverts if `numerator * target` overflows', () => { + const numerator = MAX_UINT256; + const denominator = ONE_ETHER.dividedToIntegerBy(2); + const target = MAX_UINT256_ROOT.times(2); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + numerator, + target, + ); + return expect(() => isRoundingErrorCeil(numerator, denominator, target)) + .to.throw(expectedError.message); + }); + }); + }); + }); +}); From 4711ce553276b2acf449ac8c4d8874d9863f4532 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 2 Aug 2019 13:13:59 -0400 Subject: [PATCH 33/48] `@0x/contracts-exchange`: Remove `_assertValidFill()`. `@0x/contracts-exchange`: Add `_settleOrder()` unit tests. `@0x/contracts-exchange`: Add explicit tests for `_calculateFillResults()`. `@0x/contracts-exchange`: Add overflow tests to `isolated_fill_order` tests. `@0x/contracts-exchange`: Add explicit `takerAssetFillAmount = 0` test to `isolated_fill_order` tests. --- contracts/exchange/CHANGELOG.json | 4 + .../contracts/src/MixinExchangeCore.sol | 81 ----- .../contracts/src/MixinMatchOrders.sol | 16 - .../contracts/test/TestExchangeInternals.sol | 38 ++ contracts/exchange/test/fill_order.ts | 10 +- contracts/exchange/test/internal.ts | 326 +++++++++++++++--- .../exchange/test/isolated_fill_order.ts | 66 +++- .../exchange/test/reference_functions.ts | 178 ++++++++++ 8 files changed, 558 insertions(+), 161 deletions(-) create mode 100644 contracts/exchange/test/reference_functions.ts diff --git a/contracts/exchange/CHANGELOG.json b/contracts/exchange/CHANGELOG.json index abdb2c003a..741bd115f7 100644 --- a/contracts/exchange/CHANGELOG.json +++ b/contracts/exchange/CHANGELOG.json @@ -133,6 +133,10 @@ { "note": "Remove functions from `TestExchangeInternals.sol` that are no longer tested in this package.", "pr": "TODO" + }, + { + "note": "Remove `_assertValidFill()`", + "pr": "TODO" } ] }, diff --git a/contracts/exchange/contracts/src/MixinExchangeCore.sol b/contracts/exchange/contracts/src/MixinExchangeCore.sol index d0b082e213..844419ec70 100644 --- a/contracts/exchange/contracts/src/MixinExchangeCore.sol +++ b/contracts/exchange/contracts/src/MixinExchangeCore.sol @@ -210,15 +210,6 @@ contract MixinExchangeCore is uint256 remainingTakerAssetAmount = _safeSub(order.takerAssetAmount, orderInfo.orderTakerAssetFilledAmount); uint256 takerAssetFilledAmount = _min256(takerAssetFillAmount, remainingTakerAssetAmount); - // Validate context - _assertValidFill( - order, - orderInfo, - takerAssetFillAmount, - takerAssetFilledAmount, - fillResults.makerAssetFilledAmount - ); - // Compute proportional fill amounts fillResults = _calculateFillResults(order, takerAssetFilledAmount); @@ -389,78 +380,6 @@ contract MixinExchangeCore is } } - /// @dev Validates context for fillOrder. Succeeds or throws. - /// @param order to be filled. - /// @param orderInfo OrderStatus, orderHash, and amount already filled of order. - /// @param takerAssetFillAmount Desired amount of order to fill by taker. - /// @param takerAssetFilledAmount Amount of takerAsset that will be filled. - /// @param makerAssetFilledAmount Amount of makerAsset that will be transfered. - function _assertValidFill( - Order memory order, - OrderInfo memory orderInfo, - uint256 takerAssetFillAmount, // TODO: use FillResults - uint256 takerAssetFilledAmount, - uint256 makerAssetFilledAmount - ) - internal - pure - { - // Revert if fill amount is invalid - // TODO: reconsider necessity for v2.1 - if (takerAssetFillAmount == 0) { - LibRichErrors._rrevert(LibExchangeRichErrors.FillError( - FillErrorCodes.INVALID_TAKER_AMOUNT, - orderInfo.orderHash - )); - } - - // Make sure taker does not pay more than desired amount - // NOTE: This assertion should never fail, it is here - // as an extra defence against potential bugs. - if (takerAssetFilledAmount > takerAssetFillAmount) { - LibRichErrors._rrevert(LibExchangeRichErrors.FillError( - FillErrorCodes.TAKER_OVERPAY, - orderInfo.orderHash - )); - } - - // Make sure order is not overfilled - // NOTE: This assertion should never fail, it is here - // as an extra defence against potential bugs. - if (_safeAdd(orderInfo.orderTakerAssetFilledAmount, takerAssetFilledAmount) - > order.takerAssetAmount) { - LibRichErrors._rrevert(LibExchangeRichErrors.FillError( - FillErrorCodes.OVERFILL, - orderInfo.orderHash - )); - } - - // Make sure order is filled at acceptable price. - // The order has an implied price from the makers perspective: - // order price = order.makerAssetAmount / order.takerAssetAmount - // i.e. the number of makerAsset maker is paying per takerAsset. The - // maker is guaranteed to get this price or a better (lower) one. The - // actual price maker is getting in this fill is: - // fill price = makerAssetFilledAmount / takerAssetFilledAmount - // We need `fill price <= order price` for the fill to be fair to maker. - // This amounts to: - // makerAssetFilledAmount order.makerAssetAmount - // ------------------------ <= ----------------------- - // takerAssetFilledAmount order.takerAssetAmount - // or, equivalently: - // makerAssetFilledAmount * order.takerAssetAmount <= - // order.makerAssetAmount * takerAssetFilledAmount - // NOTE: This assertion should never fail, it is here - // as an extra defence against potential bugs. - if (_safeMul(makerAssetFilledAmount, order.takerAssetAmount) - > _safeMul(order.makerAssetAmount, takerAssetFilledAmount)) { - LibRichErrors._rrevert(LibExchangeRichErrors.FillError( - FillErrorCodes.INVALID_FILL_PRICE, - orderInfo.orderHash - )); - } - } - /// @dev Validates context for cancelOrder. Succeeds or throws. /// @param order to be cancelled. /// @param orderInfo OrderStatus, orderHash, and amount already filled of order. diff --git a/contracts/exchange/contracts/src/MixinMatchOrders.sol b/contracts/exchange/contracts/src/MixinMatchOrders.sol index 04ddb08a9b..17eeeaf453 100644 --- a/contracts/exchange/contracts/src/MixinMatchOrders.sol +++ b/contracts/exchange/contracts/src/MixinMatchOrders.sol @@ -680,22 +680,6 @@ contract MixinMatchOrders is shouldMaximallyFillOrders ); - // Validate fill contexts - _assertValidFill( - leftOrder, - leftOrderInfo, - matchedFillResults.left.takerAssetFilledAmount, - matchedFillResults.left.takerAssetFilledAmount, - matchedFillResults.left.makerAssetFilledAmount - ); - _assertValidFill( - rightOrder, - rightOrderInfo, - matchedFillResults.right.takerAssetFilledAmount, - matchedFillResults.right.takerAssetFilledAmount, - matchedFillResults.right.makerAssetFilledAmount - ); - // Update exchange state _updateFilledState( leftOrder, diff --git a/contracts/exchange/contracts/test/TestExchangeInternals.sol b/contracts/exchange/contracts/test/TestExchangeInternals.sol index 2ae6467300..151a2858de 100644 --- a/contracts/exchange/contracts/test/TestExchangeInternals.sol +++ b/contracts/exchange/contracts/test/TestExchangeInternals.sol @@ -26,6 +26,14 @@ import "../src/Exchange.sol"; contract TestExchangeInternals is Exchange { + event DispatchTransferFromCalled( + bytes32 orderHash, + bytes assetData, + address from, + address to, + uint256 amount + ); + constructor (uint256 chainId) public Exchange(chainId) @@ -62,4 +70,34 @@ contract TestExchangeInternals is fillResults ); } + + function settleOrder( + bytes32 orderHash, + LibOrder.Order memory order, + address takerAddress, + LibFillResults.FillResults memory fillResults + ) + public + { + _settleOrder(orderHash, order, takerAddress, fillResults); + } + + /// @dev Overidden to only log arguments so we can test `_settleOrder()`. + function _dispatchTransferFrom( + bytes32 orderHash, + bytes memory assetData, + address from, + address to, + uint256 amount + ) + internal + { + emit DispatchTransferFromCalled( + orderHash, + assetData, + from, + to, + amount + ); + } } diff --git a/contracts/exchange/test/fill_order.ts b/contracts/exchange/test/fill_order.ts index 34ddafdff2..342b4014f9 100644 --- a/contracts/exchange/test/fill_order.ts +++ b/contracts/exchange/test/fill_order.ts @@ -189,15 +189,7 @@ blockchainTests.resets('FillOrder Tests', ({ web3Wrapper, txDefaults }) => { await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario); }); - it('should revert if takerAssetFillAmount is 0', async () => { - const fillScenario = { - ...defaultFillScenario, - takerAssetFillAmountScenario: TakerAssetFillAmountScenario.Zero, - }; - await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario); - }); - - it('should revert if an order is expired', async () => { + it('should throw if an order is expired', async () => { const fillScenario = { ...defaultFillScenario, orderScenario: { diff --git a/contracts/exchange/test/internal.ts b/contracts/exchange/test/internal.ts index a39a73f62e..9798ea53e2 100644 --- a/contracts/exchange/test/internal.ts +++ b/contracts/exchange/test/internal.ts @@ -1,3 +1,4 @@ +import { ReferenceFunctions as LibReferenceFunctions } from '@0x/contracts-exchange-libs'; import { blockchainTests, constants, @@ -8,6 +9,7 @@ import { testCombinatoriallyWithReferenceFunc, uint256Values, } from '@0x/contracts-test-utils'; +import { LibMathRevertErrors } from '@0x/order-utils'; import { FillResults, OrderWithoutDomain as Order } from '@0x/types'; import { BigNumber, SafeMathRevertErrors } from '@0x/utils'; import { LogWithDecodedArgs } from 'ethereum-types'; @@ -18,11 +20,12 @@ import { ReferenceFunctions, TestExchangeInternalsContract, TestExchangeInternalsFillEventArgs, + TestExchangeInternalsDispatchTransferFromCalledEventArgs, } from '../src'; -// TODO(dorothy-zbornak): Add _settleOrder -blockchainTests.only('Exchange core internal functions', env => { +blockchainTests('Exchange core internal functions', env => { const CHAIN_ID = 1337; + const ONE_ETHER = constants.ONE_ETHER; const EMPTY_ORDER: Order = { senderAddress: constants.NULL_ADDRESS, makerAddress: constants.NULL_ADDRESS, @@ -39,6 +42,9 @@ blockchainTests.only('Exchange core internal functions', env => { feeRecipientAddress: constants.NULL_ADDRESS, expirationTimeSeconds: constants.ZERO_AMOUNT, }; + const randomAddress = () => hexRandom(constants.ADDRESS_LENGTH); + const randomHash = () => hexRandom(constants.WORD_LENGTH); + const randomAssetData = () => hexRandom(36); let testExchange: TestExchangeInternalsContract; let logDecoder: LogDecoder; let senderAddress: string; @@ -54,49 +60,49 @@ blockchainTests.only('Exchange core internal functions', env => { logDecoder = new LogDecoder(env.web3Wrapper, artifacts); }); - blockchainTests('calculateFillResults', async () => { - function makeOrder( - makerAssetAmount: BigNumber, - takerAssetAmount: BigNumber, - makerFee: BigNumber, - takerFee: BigNumber, - ): Order { - return { - ...EMPTY_ORDER, - makerAssetAmount, - takerAssetAmount, - makerFee, - takerFee, - }; - } + blockchainTests('calculateFillResults', () => { + describe.optional('combinatorial tests', () => { + function makeOrder( + makerAssetAmount: BigNumber, + takerAssetAmount: BigNumber, + makerFee: BigNumber, + takerFee: BigNumber, + ): Order { + return { + ...EMPTY_ORDER, + makerAssetAmount, + takerAssetAmount, + makerFee, + takerFee, + }; + } - async function referenceCalculateFillResultsAsync( - orderTakerAssetAmount: BigNumber, - takerAssetFilledAmount: BigNumber, - otherAmount: BigNumber, - ): Promise { - // Note(albrow): Here we are re-using the same value (otherAmount) - // for order.makerAssetAmount, order.makerFee, and order.takerFee. - // This should be safe because they are never used with each other - // in any mathematical operation in either the reference TypeScript - // implementation or the Solidity implementation of - // calculateFillResults. - return ReferenceFunctions.calculateFillResults( - makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount), - takerAssetFilledAmount, - ); - } + async function referenceCalculateFillResultsAsync( + orderTakerAssetAmount: BigNumber, + takerAssetFilledAmount: BigNumber, + otherAmount: BigNumber, + ): Promise { + // Note(albrow): Here we are re-using the same value (otherAmount) + // for order.makerAssetAmount, order.makerFee, and order.takerFee. + // This should be safe because they are never used with each other + // in any mathematical operation in either the reference TypeScript + // implementation or the Solidity implementation of + // calculateFillResults. + return ReferenceFunctions.calculateFillResults( + makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount), + takerAssetFilledAmount, + ); + } - async function testCalculateFillResultsAsync( - orderTakerAssetAmount: BigNumber, - takerAssetFilledAmount: BigNumber, - otherAmount: BigNumber, - ): Promise { - const order = makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount); - return testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount); - } + async function testCalculateFillResultsAsync( + orderTakerAssetAmount: BigNumber, + takerAssetFilledAmount: BigNumber, + otherAmount: BigNumber, + ): Promise { + const order = makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount); + return testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount); + } - describe.optional('combinatorial tests', () => { testCombinatoriallyWithReferenceFunc( 'calculateFillResults', referenceCalculateFillResultsAsync, @@ -104,13 +110,167 @@ blockchainTests.only('Exchange core internal functions', env => { [uint256Values, uint256Values, uint256Values], ); }); + + describe('explicit tests', () => { + const MAX_UINT256_ROOT = constants.MAX_UINT256_ROOT; + function makeOrder(details?: Partial): Order { + return _.assign( + {}, + EMPTY_ORDER, + details, + ); + } + + it('matches the output of the reference function', async () => { + const order = makeOrder({ + makerAssetAmount: ONE_ETHER, + takerAssetAmount: ONE_ETHER.times(2), + makerFee: ONE_ETHER.times(0.0023), + takerFee: ONE_ETHER.times(0.0025), + }); + const takerAssetFilledAmount = ONE_ETHER.dividedToIntegerBy(3); + const expected = ReferenceFunctions.calculateFillResults(order, takerAssetFilledAmount); + const actual = await testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount); + expect(actual).to.deep.eq(expected); + }); + + it('reverts if computing `fillResults.makerAssetFilledAmount` overflows', async () => { + // All values need to be large to ensure we don't trigger a RoundingError. + const order = makeOrder({ + makerAssetAmount: MAX_UINT256_ROOT.times(2), + takerAssetAmount: MAX_UINT256_ROOT, + }); + const takerAssetFilledAmount = MAX_UINT256_ROOT; + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + takerAssetFilledAmount, + order.makerAssetAmount, + ); + return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)) + .to.revertWith(expectedError); + }); + + it('reverts if computing `fillResults.makerFeePaid` overflows', async () => { + // All values need to be large to ensure we don't trigger a RoundingError. + const order = makeOrder({ + makerAssetAmount: MAX_UINT256_ROOT, + takerAssetAmount: MAX_UINT256_ROOT, + makerFee: MAX_UINT256_ROOT.times(11), + }); + const takerAssetFilledAmount = MAX_UINT256_ROOT.dividedToIntegerBy(10); + const makerAssetFilledAmount = LibReferenceFunctions.getPartialAmountFloor( + takerAssetFilledAmount, + order.takerAssetAmount, + order.makerAssetAmount, + ); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + makerAssetFilledAmount, + order.makerFee, + ); + return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)) + .to.revertWith(expectedError); + }); + + it('reverts if computing `fillResults.takerFeePaid` overflows', async () => { + // All values need to be large to ensure we don't trigger a RoundingError. + const order = makeOrder({ + makerAssetAmount: MAX_UINT256_ROOT, + takerAssetAmount: MAX_UINT256_ROOT, + takerFee: MAX_UINT256_ROOT.times(11), + }); + const takerAssetFilledAmount = MAX_UINT256_ROOT.dividedToIntegerBy(10); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + takerAssetFilledAmount, + order.takerFee, + ); + return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)) + .to.revertWith(expectedError); + }); + + it('reverts if `order.makerAssetAmount` is 0', async () => { + const order = makeOrder({ + makerAssetAmount: constants.ZERO_AMOUNT, + takerAssetAmount: ONE_ETHER, + }); + const takerAssetFilledAmount = ONE_ETHER; + const expectedError = new LibMathRevertErrors.DivisionByZeroError(); + return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)) + .to.revertWith(expectedError); + }); + + it('reverts if `order.takerAssetAmount` is 0', async () => { + const order = makeOrder({ + makerAssetAmount: ONE_ETHER, + takerAssetAmount: constants.ZERO_AMOUNT, + }); + const takerAssetFilledAmount = ONE_ETHER; + const expectedError = new LibMathRevertErrors.DivisionByZeroError(); + return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)) + .to.revertWith(expectedError); + }); + + it('reverts if there is a rounding error computing `makerAsssetFilledAmount`', async () => { + const order = makeOrder({ + makerAssetAmount: new BigNumber(100), + takerAssetAmount: ONE_ETHER, + }); + const takerAssetFilledAmount = order.takerAssetAmount.dividedToIntegerBy(3); + const expectedError = new LibMathRevertErrors.RoundingError( + takerAssetFilledAmount, + order.takerAssetAmount, + order.makerAssetAmount, + ); + return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)) + .to.revertWith(expectedError); + }); + + it('reverts if there is a rounding error computing `makerFeePaid`', async () => { + const order = makeOrder({ + makerAssetAmount: ONE_ETHER, + takerAssetAmount: ONE_ETHER, + makerFee: new BigNumber(100), + }); + const takerAssetFilledAmount = order.takerAssetAmount.dividedToIntegerBy(3); + const makerAssetFilledAmount = LibReferenceFunctions.getPartialAmountFloor( + takerAssetFilledAmount, + order.takerAssetAmount, + order.makerAssetAmount, + ); + const expectedError = new LibMathRevertErrors.RoundingError( + makerAssetFilledAmount, + order.makerAssetAmount, + order.makerFee, + ); + return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)) + .to.revertWith(expectedError); + }); + + it('reverts if there is a rounding error computing `takerFeePaid`', async () => { + const order = makeOrder({ + makerAssetAmount: ONE_ETHER, + takerAssetAmount: ONE_ETHER, + takerFee: new BigNumber(100), + }); + const takerAssetFilledAmount = order.takerAssetAmount.dividedToIntegerBy(3); + const makerAssetFilledAmount = LibReferenceFunctions.getPartialAmountFloor( + takerAssetFilledAmount, + order.takerAssetAmount, + order.makerAssetAmount, + ); + const expectedError = new LibMathRevertErrors.RoundingError( + makerAssetFilledAmount, + order.makerAssetAmount, + order.takerFee, + ); + return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)) + .to.revertWith(expectedError); + }); + }); }); blockchainTests.resets('updateFilledState', async () => { - const ONE_ETHER = new BigNumber(1e18); - const randomAddress = () => hexRandom(constants.ADDRESS_LENGTH); - const randomHash = () => hexRandom(constants.WORD_LENGTH); - const randomAssetData = () => hexRandom(36); const ORDER_DEFAULTS = { senderAddress: randomAddress(), makerAddress: randomAddress(), @@ -129,11 +289,7 @@ blockchainTests.only('Exchange core internal functions', env => { }; function makeOrder(details?: Partial): Order { - return _.assign( - {}, - ORDER_DEFAULTS, - details, - ); + return _.assign({}, ORDER_DEFAULTS, details); } async function testUpdateFilledStateAsync( @@ -192,7 +348,7 @@ blockchainTests.only('Exchange core internal functions', env => { ); }); - it('throws if `leftOrderTakerAssetFilledAmount + fillResults.takerAssetFilledAmount` overflows', async () => { + it('reverts if `leftOrderTakerAssetFilledAmount + fillResults.takerAssetFilledAmount` overflows', async () => { const order = makeOrder(); const orderTakerAssetFilledAmount = constants.MAX_UINT256.dividedToIntegerBy(2); const takerAssetFillAmount = constants.MAX_UINT256.dividedToIntegerBy(2).plus(2); @@ -216,5 +372,71 @@ blockchainTests.only('Exchange core internal functions', env => { )).to.revertWith(expectedError); }); }); + + blockchainTests('settleOrder', () => { + const DEFAULT_ORDER = { + senderAddress: randomAddress(), + makerAddress: randomAddress(), + takerAddress: randomAddress(), + makerFee: ONE_ETHER.times(0.001), + takerFee: ONE_ETHER.times(0.003), + makerAssetAmount: ONE_ETHER, + takerAssetAmount: ONE_ETHER.times(0.5), + makerAssetData: randomAssetData(), + takerAssetData: randomAssetData(), + makerFeeAssetData: randomAssetData(), + takerFeeAssetData: randomAssetData(), + salt: new BigNumber(_.random(0, 1e8)), + feeRecipientAddress: randomAddress(), + expirationTimeSeconds: new BigNumber(_.random(0, 1e8)), + }; + + it('calls `_dispatchTransferFrom()` in the right order with the correct arguments', async () => { + const order = DEFAULT_ORDER; + const orderHash = randomHash(); + const takerAddress = randomAddress(); + const fillResults = { + makerAssetFilledAmount: ONE_ETHER.times(2), + takerAssetFilledAmount: ONE_ETHER.times(10), + makerFeePaid: ONE_ETHER.times(0.01), + takerFeePaid: ONE_ETHER.times(0.025), + }; + const receipt = await logDecoder.getTxWithDecodedLogsAsync( + await testExchange.settleOrder.sendTransactionAsync( + orderHash, + order, + takerAddress, + fillResults, + ), + ); + const logs = receipt.logs as Array>; + expect(logs.length === 4); + expect(_.every(logs, log => log.event === 'DispatchTransferFromCalled')).to.be.true(); + // taker -> maker + expect(logs[0].args.orderHash).to.eq(orderHash); + expect(logs[0].args.assetData).to.eq(order.takerAssetData); + expect(logs[0].args.from).to.eq(takerAddress); + expect(logs[0].args.to).to.eq(order.makerAddress); + expect(logs[0].args.amount).to.bignumber.eq(fillResults.takerAssetFilledAmount); + // maker -> taker + expect(logs[1].args.orderHash).to.eq(orderHash); + expect(logs[1].args.assetData).to.eq(order.makerAssetData); + expect(logs[1].args.from).to.eq(order.makerAddress); + expect(logs[1].args.to).to.eq(takerAddress); + expect(logs[1].args.amount).to.bignumber.eq(fillResults.makerAssetFilledAmount); + // taker fee -> feeRecipient + expect(logs[2].args.orderHash).to.eq(orderHash); + expect(logs[2].args.assetData).to.eq(order.takerFeeAssetData); + expect(logs[2].args.from).to.eq(takerAddress); + expect(logs[2].args.to).to.eq(order.feeRecipientAddress); + expect(logs[2].args.amount).to.bignumber.eq(fillResults.takerFeePaid); + // maker fee -> feeRecipient + expect(logs[3].args.orderHash).to.eq(orderHash); + expect(logs[3].args.assetData).to.eq(order.makerFeeAssetData); + expect(logs[3].args.from).to.eq(order.makerAddress); + expect(logs[3].args.to).to.eq(order.feeRecipientAddress); + expect(logs[3].args.amount).to.bignumber.eq(fillResults.makerFeePaid); + }); + }); }); // tslint:disable-line:max-file-line-count diff --git a/contracts/exchange/test/isolated_fill_order.ts b/contracts/exchange/test/isolated_fill_order.ts index 71b28343f3..3513373155 100644 --- a/contracts/exchange/test/isolated_fill_order.ts +++ b/contracts/exchange/test/isolated_fill_order.ts @@ -1,3 +1,4 @@ +import { ReferenceFunctions as LibReferenceFunctions } from '@0x/contracts-exchange-libs'; import { blockchainTests, constants, @@ -6,7 +7,7 @@ import { } from '@0x/contracts-test-utils'; import { ExchangeRevertErrors, LibMathRevertErrors } from '@0x/order-utils'; import { FillResults, OrderInfo, OrderStatus, SignatureType } from '@0x/types'; -import { BigNumber } from '@0x/utils'; +import { BigNumber, SafeMathRevertErrors } from '@0x/utils'; import * as _ from 'lodash'; import { calculateFillResults } from '../src/reference_functions'; @@ -24,8 +25,7 @@ import { blockchainTests('Isolated fillOrder() tests', env => { const randomAddress = () => hexRandom(constants.ADDRESS_LENGTH); const getCurrentTime = () => Math.floor(_.now() / 1000); - const { ZERO_AMOUNT } = constants; - const ONE_ETHER = new BigNumber(10).pow(18); + const { ZERO_AMOUNT, ONE_ETHER, MAX_UINT256_ROOT } = constants; const ONE_DAY = 60 * 60 * 24; const TOMORROW = getCurrentTime() + ONE_DAY; const DEFAULT_ORDER: Order = { @@ -464,6 +464,61 @@ blockchainTests('Isolated fillOrder() tests', env => { .to.revertWith(expectedError); }); + it('can\'t fill an order that results in a `makerAssetFilledAmount` overflow.', async () => { + // All values need to be large to ensure we don't trigger a Rounding. + const order = createOrder({ + makerAssetAmount: MAX_UINT256_ROOT.times(2), + takerAssetAmount: MAX_UINT256_ROOT, + }); + const takerAssetFillAmount = MAX_UINT256_ROOT; + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + takerAssetFillAmount, + order.makerAssetAmount, + ); + return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)) + .to.revertWith(expectedError); + }); + + it('can\'t fill an order that results in a `makerFeePaid` overflow.', async () => { + // All values need to be large to ensure we don't trigger a Rounding. + const order = createOrder({ + makerAssetAmount: MAX_UINT256_ROOT, + takerAssetAmount: MAX_UINT256_ROOT, + makerFee: MAX_UINT256_ROOT.times(11), + }); + const takerAssetFillAmount = MAX_UINT256_ROOT.dividedToIntegerBy(10); + const makerAssetFilledAmount = LibReferenceFunctions.getPartialAmountFloor( + takerAssetFillAmount, + order.takerAssetAmount, + order.makerAssetAmount, + ); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + makerAssetFilledAmount, + order.makerFee, + ); + return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)) + .to.revertWith(expectedError); + }); + + it('can\'t fill an order that results in a `takerFeePaid` overflow.', async () => { + // All values need to be large to ensure we don't trigger a Rounding. + const order = createOrder({ + makerAssetAmount: MAX_UINT256_ROOT, + takerAssetAmount: MAX_UINT256_ROOT, + takerFee: MAX_UINT256_ROOT.times(11), + }); + const takerAssetFillAmount = MAX_UINT256_ROOT.dividedToIntegerBy(10); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + takerAssetFillAmount, + order.takerFee, + ); + return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)) + .to.revertWith(expectedError); + }); + it('can\'t fill an order with a bad signature', async () => { const order = createOrder(); const signature = createBadSignature(); @@ -529,6 +584,11 @@ blockchainTests('Isolated fillOrder() tests', env => { }); describe('permitted fills', () => { + it('should allow takerAssetFillAmount to be zero', async () => { + const order = createOrder(); + return fillOrderAndAssertResultsAsync(order, constants.ZERO_AMOUNT); + }); + it('can fill an order if taker is `takerAddress`', async () => { const order = createOrder({ takerAddress, diff --git a/contracts/exchange/test/reference_functions.ts b/contracts/exchange/test/reference_functions.ts new file mode 100644 index 0000000000..b2dd6dae23 --- /dev/null +++ b/contracts/exchange/test/reference_functions.ts @@ -0,0 +1,178 @@ +import { ReferenceFunctions as LibReferenceFunctions } from '@0x/contracts-exchange-libs'; +import { + constants, + describe, + expect, +} from '@0x/contracts-test-utils'; +import { LibMathRevertErrors } from '@0x/order-utils'; +import { OrderWithoutDomain as Order } from '@0x/types'; +import { BigNumber, SafeMathRevertErrors } from '@0x/utils'; +import * as _ from 'lodash'; + +import { calculateFillResults } from '../src/reference_functions'; + +describe('Reference functions', () => { + const ONE_ETHER = constants.ONE_ETHER; + const EMPTY_ORDER: Order = { + senderAddress: constants.NULL_ADDRESS, + makerAddress: constants.NULL_ADDRESS, + takerAddress: constants.NULL_ADDRESS, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + makerAssetAmount: constants.ZERO_AMOUNT, + takerAssetAmount: constants.ZERO_AMOUNT, + makerAssetData: constants.NULL_BYTES, + takerAssetData: constants.NULL_BYTES, + makerFeeAssetData: constants.NULL_BYTES, + takerFeeAssetData: constants.NULL_BYTES, + salt: constants.ZERO_AMOUNT, + feeRecipientAddress: constants.NULL_ADDRESS, + expirationTimeSeconds: constants.ZERO_AMOUNT, + }; + + describe('calculateFillResults', () => { + const MAX_UINT256_ROOT = constants.MAX_UINT256_ROOT; + function makeOrder(details?: Partial): Order { + return _.assign( + {}, + EMPTY_ORDER, + details, + ); + } + + it('reverts if computing `fillResults.makerAssetFilledAmount` overflows', () => { + // All values need to be large to ensure we don't trigger a RondingError. + const order = makeOrder({ + makerAssetAmount: MAX_UINT256_ROOT.times(2), + takerAssetAmount: MAX_UINT256_ROOT, + }); + const takerAssetFilledAmount = MAX_UINT256_ROOT; + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + takerAssetFilledAmount, + order.makerAssetAmount, + ); + return expect(() => calculateFillResults(order, takerAssetFilledAmount)) + .to.throw(expectedError.message); + }); + + it('reverts if computing `fillResults.makerFeePaid` overflows', () => { + // All values need to be large to ensure we don't trigger a RondingError. + const order = makeOrder({ + makerAssetAmount: MAX_UINT256_ROOT, + takerAssetAmount: MAX_UINT256_ROOT, + makerFee: MAX_UINT256_ROOT.times(11), + }); + const takerAssetFilledAmount = MAX_UINT256_ROOT.dividedToIntegerBy(10); + const makerAssetFilledAmount = LibReferenceFunctions.getPartialAmountFloor( + takerAssetFilledAmount, + order.takerAssetAmount, + order.makerAssetAmount, + ); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + makerAssetFilledAmount, + order.makerFee, + ); + return expect(() => calculateFillResults(order, takerAssetFilledAmount)) + .to.throw(expectedError.message); + }); + + it('reverts if computing `fillResults.takerFeePaid` overflows', () => { + // All values need to be large to ensure we don't trigger a RondingError. + const order = makeOrder({ + makerAssetAmount: MAX_UINT256_ROOT, + takerAssetAmount: MAX_UINT256_ROOT, + takerFee: MAX_UINT256_ROOT.times(11), + }); + const takerAssetFilledAmount = MAX_UINT256_ROOT.dividedToIntegerBy(10); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + takerAssetFilledAmount, + order.takerFee, + ); + return expect(() => calculateFillResults(order, takerAssetFilledAmount)) + .to.throw(expectedError.message); + }); + + it('reverts if `order.makerAssetAmount` is 0', () => { + const order = makeOrder({ + makerAssetAmount: constants.ZERO_AMOUNT, + takerAssetAmount: ONE_ETHER, + }); + const takerAssetFilledAmount = ONE_ETHER; + const expectedError = new LibMathRevertErrors.DivisionByZeroError(); + return expect(() => calculateFillResults(order, takerAssetFilledAmount)) + .to.throw(expectedError.message); + }); + + it('reverts if `order.takerAssetAmount` is 0', () => { + const order = makeOrder({ + makerAssetAmount: ONE_ETHER, + takerAssetAmount: constants.ZERO_AMOUNT, + }); + const takerAssetFilledAmount = ONE_ETHER; + const expectedError = new LibMathRevertErrors.DivisionByZeroError(); + return expect(() => calculateFillResults(order, takerAssetFilledAmount)) + .to.throw(expectedError.message); + }); + + it('reverts if there is a rounding error computing `makerAsssetFilledAmount`', () => { + const order = makeOrder({ + makerAssetAmount: new BigNumber(100), + takerAssetAmount: ONE_ETHER, + }); + const takerAssetFilledAmount = order.takerAssetAmount.dividedToIntegerBy(3); + const expectedError = new LibMathRevertErrors.RoundingError( + takerAssetFilledAmount, + order.takerAssetAmount, + order.makerAssetAmount, + ); + return expect(() => calculateFillResults(order, takerAssetFilledAmount)) + .to.throw(expectedError.message); + }); + + it('reverts if there is a rounding error computing `makerFeePaid`', () => { + const order = makeOrder({ + makerAssetAmount: ONE_ETHER, + takerAssetAmount: ONE_ETHER, + makerFee: new BigNumber(100), + }); + const takerAssetFilledAmount = order.takerAssetAmount.dividedToIntegerBy(3); + const makerAssetFilledAmount = LibReferenceFunctions.getPartialAmountFloor( + takerAssetFilledAmount, + order.takerAssetAmount, + order.makerAssetAmount, + ); + const expectedError = new LibMathRevertErrors.RoundingError( + makerAssetFilledAmount, + order.makerAssetAmount, + order.makerFee, + ); + return expect(() => calculateFillResults(order, takerAssetFilledAmount)) + .to.throw(expectedError.message); + }); + + it('reverts if there is a rounding error computing `takerFeePaid`', () => { + const order = makeOrder({ + makerAssetAmount: ONE_ETHER, + takerAssetAmount: ONE_ETHER, + takerFee: new BigNumber(100), + }); + const takerAssetFilledAmount = order.takerAssetAmount.dividedToIntegerBy(3); + const makerAssetFilledAmount = LibReferenceFunctions.getPartialAmountFloor( + takerAssetFilledAmount, + order.takerAssetAmount, + order.makerAssetAmount, + ); + const expectedError = new LibMathRevertErrors.RoundingError( + makerAssetFilledAmount, + order.makerAssetAmount, + order.takerFee, + ); + return expect(() => calculateFillResults(order, takerAssetFilledAmount)) + .to.throw(expectedError.message); + }); + }); +}); +// tslint:disable-line:max-file-line-count From 6345faa4a9af4f60f87fc2071a3cf8421412b30c Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 2 Aug 2019 13:30:48 -0400 Subject: [PATCH 34/48] `@0x/contracts-exchange-libs: Appease the linter and prettier gods. --- .../exchange-libs/src/reference_functions.ts | 97 ++++------ .../exchange-libs/test/lib_fill_results.ts | 29 ++- contracts/exchange-libs/test/lib_math.ts | 102 ++++++----- .../exchange-libs/test/reference_functions.ts | 101 ++++++----- contracts/exchange/compiler.json | 2 +- .../contracts/test/IsolatedExchange.sol | 2 +- contracts/exchange/src/reference_functions.ts | 20 +-- contracts/exchange/test/internal.ts | 83 ++++----- .../exchange/test/isolated_fill_order.ts | 169 +++++++----------- contracts/exchange/test/match_orders.ts | 48 +---- .../exchange/test/reference_functions.ts | 36 ++-- .../exchange/test/utils/exchange_wrapper.ts | 7 +- .../test/utils/fill_order_scenarios.ts | 2 - .../test/utils/isolated_exchange_wrapper.ts | 15 +- .../exchange/test/utils/match_order_tester.ts | 14 +- contracts/exchange/tslint.json | 3 + contracts/test-utils/CHANGELOG.json | 2 +- .../test-utils/src/test_with_reference.ts | 16 +- .../test-utils/test/test_with_reference.ts | 39 ++-- contracts/utils/src/reference_functions.ts | 12 ++ 20 files changed, 334 insertions(+), 465 deletions(-) diff --git a/contracts/exchange-libs/src/reference_functions.ts b/contracts/exchange-libs/src/reference_functions.ts index 8585f0a64c..e3894adf13 100644 --- a/contracts/exchange-libs/src/reference_functions.ts +++ b/contracts/exchange-libs/src/reference_functions.ts @@ -3,18 +3,12 @@ import { LibMathRevertErrors } from '@0x/order-utils'; import { FillResults } from '@0x/types'; import { BigNumber } from '@0x/utils'; -const { - safeAdd, - safeSub, - safeMul, - safeDiv, -} = ReferenceFunctions; +const { safeAdd, safeSub, safeMul, safeDiv } = ReferenceFunctions; -export function isRoundingErrorFloor( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, -): boolean { +/** + * Checks if rounding error >= 0.1% when rounding down. + */ +export function isRoundingErrorFloor(numerator: BigNumber, denominator: BigNumber, target: BigNumber): boolean { if (denominator.eq(0)) { throw new LibMathRevertErrors.DivisionByZeroError(); } @@ -28,11 +22,10 @@ export function isRoundingErrorFloor( return lhs.gte(rhs); } -export function isRoundingErrorCeil( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, -): boolean { +/** + * Checks if rounding error >= 0.1% when rounding up. + */ +export function isRoundingErrorCeil(numerator: BigNumber, denominator: BigNumber, target: BigNumber): boolean { if (denominator.eq(0)) { throw new LibMathRevertErrors.DivisionByZeroError(); } @@ -47,66 +40,46 @@ export function isRoundingErrorCeil( return lhs.gte(rhs); } -export function safeGetPartialAmountFloor( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, -): BigNumber { +/** + * Calculates partial value given a numerator and denominator rounded down. + * Reverts if rounding error is >= 0.1% + */ +export function safeGetPartialAmountFloor(numerator: BigNumber, denominator: BigNumber, target: BigNumber): BigNumber { if (isRoundingErrorFloor(numerator, denominator, target)) { throw new LibMathRevertErrors.RoundingError(numerator, denominator, target); } - return safeDiv( - safeMul(numerator, target), - denominator, - ); + return safeDiv(safeMul(numerator, target), denominator); } -export function safeGetPartialAmountCeil( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, -): BigNumber { +/** + * Calculates partial value given a numerator and denominator rounded down. + * Reverts if rounding error is >= 0.1% + */ +export function safeGetPartialAmountCeil(numerator: BigNumber, denominator: BigNumber, target: BigNumber): BigNumber { if (isRoundingErrorCeil(numerator, denominator, target)) { throw new LibMathRevertErrors.RoundingError(numerator, denominator, target); } - return safeDiv( - safeAdd( - safeMul(numerator, target), - safeSub(denominator, new BigNumber(1)), - ), - denominator, - ); + return safeDiv(safeAdd(safeMul(numerator, target), safeSub(denominator, new BigNumber(1))), denominator); } -export function getPartialAmountFloor( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, -): BigNumber { - return safeDiv( - safeMul(numerator, target), - denominator, - ); +/** + * Calculates partial value given a numerator and denominator rounded down. + */ +export function getPartialAmountFloor(numerator: BigNumber, denominator: BigNumber, target: BigNumber): BigNumber { + return safeDiv(safeMul(numerator, target), denominator); } -export function getPartialAmountCeil( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, -): BigNumber { - return safeDiv( - safeAdd( - safeMul(numerator, target), - safeSub(denominator, new BigNumber(1)), - ), - denominator, - ); +/** + * Calculates partial value given a numerator and denominator rounded down. + */ +export function getPartialAmountCeil(numerator: BigNumber, denominator: BigNumber, target: BigNumber): BigNumber { + return safeDiv(safeAdd(safeMul(numerator, target), safeSub(denominator, new BigNumber(1))), denominator); } -export function addFillResults( - a: FillResults, - b: FillResults, -): FillResults { +/** + * Adds properties of two `FillResults`. + */ +export function addFillResults(a: FillResults, b: FillResults): FillResults { return { makerAssetFilledAmount: safeAdd(a.makerAssetFilledAmount, b.makerAssetFilledAmount), takerAssetFilledAmount: safeAdd(a.takerAssetFilledAmount, b.takerAssetFilledAmount), diff --git a/contracts/exchange-libs/test/lib_fill_results.ts b/contracts/exchange-libs/test/lib_fill_results.ts index ebfd3a4957..a57db9bb38 100644 --- a/contracts/exchange-libs/test/lib_fill_results.ts +++ b/contracts/exchange-libs/test/lib_fill_results.ts @@ -1,9 +1,4 @@ -import { - blockchainTests, - constants, - describe, - expect, -} from '@0x/contracts-test-utils'; +import { blockchainTests, constants, describe, expect } from '@0x/contracts-test-utils'; import { BigNumber, SafeMathRevertErrors } from '@0x/utils'; import * as _ from 'lodash'; @@ -41,58 +36,54 @@ blockchainTests('LibFillResults', env => { ]; it('matches the output of the reference function', async () => { - const [ a, b ] = DEFAULT_FILL_RESULTS; + const [a, b] = DEFAULT_FILL_RESULTS; const expected = ReferenceFunctions.addFillResults(a, b); const actual = await libsContract.addFillResults.callAsync(a, b); expect(actual).to.deep.equal(expected); }); it('reverts if computing `makerAssetFilledAmount` overflows', async () => { - const [ a, b ] = _.cloneDeep(DEFAULT_FILL_RESULTS); + const [a, b] = _.cloneDeep(DEFAULT_FILL_RESULTS); b.makerAssetFilledAmount = MAX_UINT256; const expectedError = new SafeMathRevertErrors.SafeMathError( SafeMathRevertErrors.SafeMathErrorCodes.Uint256AdditionOverflow, a.makerAssetFilledAmount, b.makerAssetFilledAmount, ); - return expect(libsContract.addFillResults.callAsync(a, b)) - .to.revertWith(expectedError); + return expect(libsContract.addFillResults.callAsync(a, b)).to.revertWith(expectedError); }); it('reverts if computing `takerAssetFilledAmount` overflows', async () => { - const [ a, b ] = _.cloneDeep(DEFAULT_FILL_RESULTS); + const [a, b] = _.cloneDeep(DEFAULT_FILL_RESULTS); b.takerAssetFilledAmount = MAX_UINT256; const expectedError = new SafeMathRevertErrors.SafeMathError( SafeMathRevertErrors.SafeMathErrorCodes.Uint256AdditionOverflow, a.takerAssetFilledAmount, b.takerAssetFilledAmount, ); - return expect(libsContract.addFillResults.callAsync(a, b)) - .to.revertWith(expectedError); + return expect(libsContract.addFillResults.callAsync(a, b)).to.revertWith(expectedError); }); it('reverts if computing `makerFeePaid` overflows', async () => { - const [ a, b ] = _.cloneDeep(DEFAULT_FILL_RESULTS); + const [a, b] = _.cloneDeep(DEFAULT_FILL_RESULTS); b.makerFeePaid = MAX_UINT256; const expectedError = new SafeMathRevertErrors.SafeMathError( SafeMathRevertErrors.SafeMathErrorCodes.Uint256AdditionOverflow, a.makerFeePaid, b.makerFeePaid, ); - return expect(libsContract.addFillResults.callAsync(a, b)) - .to.revertWith(expectedError); + return expect(libsContract.addFillResults.callAsync(a, b)).to.revertWith(expectedError); }); it('reverts if computing `takerFeePaid` overflows', async () => { - const [ a, b ] = _.cloneDeep(DEFAULT_FILL_RESULTS); + const [a, b] = _.cloneDeep(DEFAULT_FILL_RESULTS); b.takerFeePaid = MAX_UINT256; const expectedError = new SafeMathRevertErrors.SafeMathError( SafeMathRevertErrors.SafeMathErrorCodes.Uint256AdditionOverflow, a.takerFeePaid, b.takerFeePaid, ); - return expect(libsContract.addFillResults.callAsync(a, b)) - .to.revertWith(expectedError); + return expect(libsContract.addFillResults.callAsync(a, b)).to.revertWith(expectedError); }); }); }); diff --git a/contracts/exchange-libs/test/lib_math.ts b/contracts/exchange-libs/test/lib_math.ts index 9215a1e747..42060dec13 100644 --- a/contracts/exchange-libs/test/lib_math.ts +++ b/contracts/exchange-libs/test/lib_math.ts @@ -26,17 +26,13 @@ blockchainTests('LibMath', env => { }); // Wrap a reference function with identical arguments in a promise. - function createAsyncReferenceFunction( - ref: (...args: any[]) => T, - ): (...args: any[]) => Promise { + function createAsyncReferenceFunction(ref: (...args: any[]) => T): (...args: any[]) => Promise { return async (...args: any[]): Promise => { return ref(...args); }; } - function createContractTestFunction( - name: string, - ): (...args: any[]) => Promise { + function createContractTestFunction(name: string): (...args: any[]) => Promise { return async (...args: any[]): Promise => { const method = (libsContract as any)[name] as { callAsync: (...args: any[]) => Promise }; return method.callAsync(...args); @@ -72,8 +68,9 @@ blockchainTests('LibMath', env => { numerator.times(target), denominator, ); - return expect(libsContract.getPartialAmountFloor.callAsync(numerator, denominator, target)) - .to.revertWith(expectedError); + return expect( + libsContract.getPartialAmountFloor.callAsync(numerator, denominator, target), + ).to.revertWith(expectedError); }); it('reverts if `numerator * target` overflows', async () => { @@ -85,8 +82,9 @@ blockchainTests('LibMath', env => { numerator, target, ); - return expect(libsContract.getPartialAmountFloor.callAsync(numerator, denominator, target)) - .to.revertWith(expectedError); + return expect( + libsContract.getPartialAmountFloor.callAsync(numerator, denominator, target), + ).to.revertWith(expectedError); }); }); }); @@ -121,8 +119,9 @@ blockchainTests('LibMath', env => { denominator, new BigNumber(1), ); - return expect(libsContract.getPartialAmountCeil.callAsync(numerator, denominator, target)) - .to.revertWith(expectedError); + return expect( + libsContract.getPartialAmountCeil.callAsync(numerator, denominator, target), + ).to.revertWith(expectedError); }); it('reverts if `numerator * target` overflows', async () => { @@ -134,8 +133,9 @@ blockchainTests('LibMath', env => { numerator, target, ); - return expect(libsContract.getPartialAmountCeil.callAsync(numerator, denominator, target)) - .to.revertWith(expectedError); + return expect( + libsContract.getPartialAmountCeil.callAsync(numerator, denominator, target), + ).to.revertWith(expectedError); }); }); }); @@ -164,13 +164,10 @@ blockchainTests('LibMath', env => { const numerator = new BigNumber(1e3); const denominator = new BigNumber(1e4); const target = new BigNumber(333); - const expectedError = new LibMathRevertErrors.RoundingError( - numerator, - denominator, - target, - ); - return expect(libsContract.safeGetPartialAmountFloor.callAsync(numerator, denominator, target)) - .to.revertWith(expectedError); + const expectedError = new LibMathRevertErrors.RoundingError(numerator, denominator, target); + return expect( + libsContract.safeGetPartialAmountFloor.callAsync(numerator, denominator, target), + ).to.revertWith(expectedError); }); it('reverts if `denominator` is zero', async () => { @@ -178,8 +175,9 @@ blockchainTests('LibMath', env => { const denominator = ZERO_AMOUNT; const target = ONE_ETHER.times(0.01); const expectedError = new LibMathRevertErrors.DivisionByZeroError(); - return expect(libsContract.safeGetPartialAmountFloor.callAsync(numerator, denominator, target)) - .to.revertWith(expectedError); + return expect( + libsContract.safeGetPartialAmountFloor.callAsync(numerator, denominator, target), + ).to.revertWith(expectedError); }); it('reverts if `numerator * target` overflows', async () => { @@ -191,8 +189,9 @@ blockchainTests('LibMath', env => { numerator, target, ); - return expect(libsContract.safeGetPartialAmountFloor.callAsync(numerator, denominator, target)) - .to.revertWith(expectedError); + return expect( + libsContract.safeGetPartialAmountFloor.callAsync(numerator, denominator, target), + ).to.revertWith(expectedError); }); }); }); @@ -221,13 +220,10 @@ blockchainTests('LibMath', env => { const numerator = new BigNumber(1e3); const denominator = new BigNumber(1e4); const target = new BigNumber(333); - const expectedError = new LibMathRevertErrors.RoundingError( - numerator, - denominator, - target, - ); - return expect(libsContract.safeGetPartialAmountCeil.callAsync(numerator, denominator, target)) - .to.revertWith(expectedError); + const expectedError = new LibMathRevertErrors.RoundingError(numerator, denominator, target); + return expect( + libsContract.safeGetPartialAmountCeil.callAsync(numerator, denominator, target), + ).to.revertWith(expectedError); }); it('reverts if `denominator` is zero', async () => { @@ -235,8 +231,9 @@ blockchainTests('LibMath', env => { const denominator = ZERO_AMOUNT; const target = ONE_ETHER.times(0.01); const expectedError = new LibMathRevertErrors.DivisionByZeroError(); - return expect(libsContract.safeGetPartialAmountCeil.callAsync(numerator, denominator, target)) - .to.revertWith(expectedError); + return expect( + libsContract.safeGetPartialAmountCeil.callAsync(numerator, denominator, target), + ).to.revertWith(expectedError); }); it('reverts if `numerator * target` overflows', async () => { @@ -248,8 +245,9 @@ blockchainTests('LibMath', env => { numerator, target, ); - return expect(libsContract.safeGetPartialAmountCeil.callAsync(numerator, denominator, target)) - .to.revertWith(expectedError); + return expect( + libsContract.safeGetPartialAmountCeil.callAsync(numerator, denominator, target), + ).to.revertWith(expectedError); }); }); }); @@ -269,6 +267,7 @@ blockchainTests('LibMath', env => { const numerator = new BigNumber(1e3); const denominator = new BigNumber(1e4); const target = new BigNumber(333); + // tslint:disable-next-line: boolean-naming const actual = await libsContract.isRoundingErrorFloor.callAsync(numerator, denominator, target); expect(actual).to.eq(true); }); @@ -277,6 +276,7 @@ blockchainTests('LibMath', env => { const numerator = new BigNumber(1e3); const denominator = new BigNumber(1e4); const target = new BigNumber(5e2); + // tslint:disable-next-line: boolean-naming const actual = await libsContract.isRoundingErrorFloor.callAsync(numerator, denominator, target); expect(actual).to.eq(false); }); @@ -285,7 +285,9 @@ blockchainTests('LibMath', env => { const numerator = ONE_ETHER; const denominator = ONE_ETHER.dividedToIntegerBy(2); const target = ONE_ETHER.times(0.01); + // tslint:disable-next-line: boolean-naming const expected = ReferenceFunctions.isRoundingErrorFloor(numerator, denominator, target); + // tslint:disable-next-line: boolean-naming const actual = await libsContract.isRoundingErrorFloor.callAsync(numerator, denominator, target); expect(actual).to.eq(expected); }); @@ -295,8 +297,9 @@ blockchainTests('LibMath', env => { const denominator = ZERO_AMOUNT; const target = ONE_ETHER.times(0.01); const expectedError = new LibMathRevertErrors.DivisionByZeroError(); - return expect(libsContract.isRoundingErrorFloor.callAsync(numerator, denominator, target)) - .to.revertWith(expectedError); + return expect( + libsContract.isRoundingErrorFloor.callAsync(numerator, denominator, target), + ).to.revertWith(expectedError); }); it('reverts if `numerator * target` overflows', async () => { @@ -308,8 +311,9 @@ blockchainTests('LibMath', env => { numerator, target, ); - return expect(libsContract.isRoundingErrorFloor.callAsync(numerator, denominator, target)) - .to.revertWith(expectedError); + return expect( + libsContract.isRoundingErrorFloor.callAsync(numerator, denominator, target), + ).to.revertWith(expectedError); }); }); }); @@ -329,7 +333,8 @@ blockchainTests('LibMath', env => { const numerator = new BigNumber(1e3); const denominator = new BigNumber(1e4); const target = new BigNumber(333); - const actual = await libsContract.isRoundingErrorFloor.callAsync(numerator, denominator, target); + // tslint:disable-next-line: boolean-naming + const actual = await libsContract.isRoundingErrorCeil.callAsync(numerator, denominator, target); expect(actual).to.eq(true); }); @@ -337,7 +342,8 @@ blockchainTests('LibMath', env => { const numerator = new BigNumber(1e3); const denominator = new BigNumber(1e4); const target = new BigNumber(5e2); - const actual = await libsContract.isRoundingErrorFloor.callAsync(numerator, denominator, target); + // tslint:disable-next-line: boolean-naming + const actual = await libsContract.isRoundingErrorCeil.callAsync(numerator, denominator, target); expect(actual).to.eq(false); }); @@ -345,7 +351,9 @@ blockchainTests('LibMath', env => { const numerator = ONE_ETHER; const denominator = ONE_ETHER.dividedToIntegerBy(2); const target = ONE_ETHER.times(0.01); + // tslint:disable-next-line: boolean-naming const expected = ReferenceFunctions.isRoundingErrorCeil(numerator, denominator, target); + // tslint:disable-next-line: boolean-naming const actual = await libsContract.isRoundingErrorCeil.callAsync(numerator, denominator, target); expect(actual).to.eq(expected); }); @@ -355,8 +363,9 @@ blockchainTests('LibMath', env => { const denominator = ZERO_AMOUNT; const target = ONE_ETHER.times(0.01); const expectedError = new LibMathRevertErrors.DivisionByZeroError(); - return expect(libsContract.isRoundingErrorCeil.callAsync(numerator, denominator, target)) - .to.revertWith(expectedError); + return expect(libsContract.isRoundingErrorCeil.callAsync(numerator, denominator, target)).to.revertWith( + expectedError, + ); }); it('reverts if `numerator * target` overflows', async () => { @@ -368,8 +377,9 @@ blockchainTests('LibMath', env => { numerator, target, ); - return expect(libsContract.isRoundingErrorCeil.callAsync(numerator, denominator, target)) - .to.revertWith(expectedError); + return expect(libsContract.isRoundingErrorCeil.callAsync(numerator, denominator, target)).to.revertWith( + expectedError, + ); }); }); }); diff --git a/contracts/exchange-libs/test/reference_functions.ts b/contracts/exchange-libs/test/reference_functions.ts index c7ea2536e2..d6c519b9d8 100644 --- a/contracts/exchange-libs/test/reference_functions.ts +++ b/contracts/exchange-libs/test/reference_functions.ts @@ -1,8 +1,4 @@ -import { - constants, - describe, - expect, -} from '@0x/contracts-test-utils'; +import { constants, describe, expect } from '@0x/contracts-test-utils'; import { LibMathRevertErrors } from '@0x/order-utils'; import { BigNumber, SafeMathRevertErrors } from '@0x/utils'; import * as _ from 'lodash'; @@ -20,7 +16,6 @@ import { describe('Reference Functions', () => { const { ONE_ETHER, MAX_UINT256, MAX_UINT256_ROOT, ZERO_AMOUNT } = constants; describe('LibFillResults', () => { - describe('addFillResults', () => { const DEFAULT_FILL_RESULTS = [ { @@ -38,7 +33,7 @@ describe('Reference Functions', () => { ]; it('reverts if computing `makerAssetFilledAmount` overflows', () => { - const [ a, b ] = _.cloneDeep(DEFAULT_FILL_RESULTS); + const [a, b] = _.cloneDeep(DEFAULT_FILL_RESULTS); b.makerAssetFilledAmount = MAX_UINT256; const expectedError = new SafeMathRevertErrors.SafeMathError( SafeMathRevertErrors.SafeMathErrorCodes.Uint256AdditionOverflow, @@ -49,7 +44,7 @@ describe('Reference Functions', () => { }); it('reverts if computing `takerAssetFilledAmount` overflows', () => { - const [ a, b ] = _.cloneDeep(DEFAULT_FILL_RESULTS); + const [a, b] = _.cloneDeep(DEFAULT_FILL_RESULTS); b.takerAssetFilledAmount = MAX_UINT256; const expectedError = new SafeMathRevertErrors.SafeMathError( SafeMathRevertErrors.SafeMathErrorCodes.Uint256AdditionOverflow, @@ -60,7 +55,7 @@ describe('Reference Functions', () => { }); it('reverts if computing `makerFeePaid` overflows', () => { - const [ a, b ] = _.cloneDeep(DEFAULT_FILL_RESULTS); + const [a, b] = _.cloneDeep(DEFAULT_FILL_RESULTS); b.makerFeePaid = MAX_UINT256; const expectedError = new SafeMathRevertErrors.SafeMathError( SafeMathRevertErrors.SafeMathErrorCodes.Uint256AdditionOverflow, @@ -71,7 +66,7 @@ describe('Reference Functions', () => { }); it('reverts if computing `takerFeePaid` overflows', () => { - const [ a, b ] = _.cloneDeep(DEFAULT_FILL_RESULTS); + const [a, b] = _.cloneDeep(DEFAULT_FILL_RESULTS); b.takerFeePaid = MAX_UINT256; const expectedError = new SafeMathRevertErrors.SafeMathError( SafeMathRevertErrors.SafeMathErrorCodes.Uint256AdditionOverflow, @@ -95,8 +90,9 @@ describe('Reference Functions', () => { numerator.times(target), denominator, ); - return expect(() => getPartialAmountFloor(numerator, denominator, target)) - .to.throw(expectedError.message); + return expect(() => getPartialAmountFloor(numerator, denominator, target)).to.throw( + expectedError.message, + ); }); it('reverts if `numerator * target` overflows', () => { @@ -108,8 +104,9 @@ describe('Reference Functions', () => { numerator, target, ); - return expect(() => getPartialAmountFloor(numerator, denominator, target)) - .to.throw(expectedError.message); + return expect(() => getPartialAmountFloor(numerator, denominator, target)).to.throw( + expectedError.message, + ); }); }); }); @@ -126,8 +123,9 @@ describe('Reference Functions', () => { denominator, new BigNumber(1), ); - return expect(() => getPartialAmountCeil(numerator, denominator, target)) - .to.throw(expectedError.message); + return expect(() => getPartialAmountCeil(numerator, denominator, target)).to.throw( + expectedError.message, + ); }); it('reverts if `numerator * target` overflows', () => { @@ -139,8 +137,9 @@ describe('Reference Functions', () => { numerator, target, ); - return expect(() => getPartialAmountCeil(numerator, denominator, target)) - .to.throw(expectedError.message); + return expect(() => getPartialAmountCeil(numerator, denominator, target)).to.throw( + expectedError.message, + ); }); }); }); @@ -151,13 +150,10 @@ describe('Reference Functions', () => { const numerator = new BigNumber(1e3); const denominator = new BigNumber(1e4); const target = new BigNumber(333); - const expectedError = new LibMathRevertErrors.RoundingError( - numerator, - denominator, - target, + const expectedError = new LibMathRevertErrors.RoundingError(numerator, denominator, target); + return expect(() => safeGetPartialAmountFloor(numerator, denominator, target)).to.throw( + expectedError.message, ); - return expect(() => safeGetPartialAmountFloor(numerator, denominator, target)) - .to.throw(expectedError.message); }); it('reverts if `denominator` is zero', () => { @@ -165,8 +161,9 @@ describe('Reference Functions', () => { const denominator = ZERO_AMOUNT; const target = ONE_ETHER.times(0.01); const expectedError = new LibMathRevertErrors.DivisionByZeroError(); - return expect(() => safeGetPartialAmountFloor(numerator, denominator, target)) - .to.throw(expectedError.message); + return expect(() => safeGetPartialAmountFloor(numerator, denominator, target)).to.throw( + expectedError.message, + ); }); it('reverts if `numerator * target` overflows', () => { @@ -178,8 +175,9 @@ describe('Reference Functions', () => { numerator, target, ); - return expect(() => safeGetPartialAmountFloor(numerator, denominator, target)) - .to.throw(expectedError.message); + return expect(() => safeGetPartialAmountFloor(numerator, denominator, target)).to.throw( + expectedError.message, + ); }); }); }); @@ -190,13 +188,10 @@ describe('Reference Functions', () => { const numerator = new BigNumber(1e3); const denominator = new BigNumber(1e4); const target = new BigNumber(333); - const expectedError = new LibMathRevertErrors.RoundingError( - numerator, - denominator, - target, + const expectedError = new LibMathRevertErrors.RoundingError(numerator, denominator, target); + return expect(() => safeGetPartialAmountCeil(numerator, denominator, target)).to.throw( + expectedError.message, ); - return expect(() => safeGetPartialAmountCeil(numerator, denominator, target)) - .to.throw(expectedError.message); }); it('reverts if `denominator` is zero', () => { @@ -204,8 +199,9 @@ describe('Reference Functions', () => { const denominator = ZERO_AMOUNT; const target = ONE_ETHER.times(0.01); const expectedError = new LibMathRevertErrors.DivisionByZeroError(); - return expect(() => safeGetPartialAmountCeil(numerator, denominator, target)) - .to.throw(expectedError.message); + return expect(() => safeGetPartialAmountCeil(numerator, denominator, target)).to.throw( + expectedError.message, + ); }); it('reverts if `numerator * target` overflows', () => { @@ -217,8 +213,9 @@ describe('Reference Functions', () => { numerator, target, ); - return expect(() => safeGetPartialAmountCeil(numerator, denominator, target)) - .to.throw(expectedError.message); + return expect(() => safeGetPartialAmountCeil(numerator, denominator, target)).to.throw( + expectedError.message, + ); }); }); }); @@ -229,6 +226,7 @@ describe('Reference Functions', () => { const numerator = new BigNumber(1e3); const denominator = new BigNumber(1e4); const target = new BigNumber(333); + // tslint:disable-next-line: boolean-naming const actual = isRoundingErrorFloor(numerator, denominator, target); expect(actual).to.eq(true); }); @@ -237,6 +235,7 @@ describe('Reference Functions', () => { const numerator = new BigNumber(1e3); const denominator = new BigNumber(1e4); const target = new BigNumber(5e2); + // tslint:disable-next-line: boolean-naming const actual = isRoundingErrorFloor(numerator, denominator, target); expect(actual).to.eq(false); }); @@ -246,8 +245,9 @@ describe('Reference Functions', () => { const denominator = ZERO_AMOUNT; const target = ONE_ETHER.times(0.01); const expectedError = new LibMathRevertErrors.DivisionByZeroError(); - return expect(() => isRoundingErrorFloor(numerator, denominator, target)) - .to.throw(expectedError.message); + return expect(() => isRoundingErrorFloor(numerator, denominator, target)).to.throw( + expectedError.message, + ); }); it('reverts if `numerator * target` overflows', () => { @@ -259,8 +259,9 @@ describe('Reference Functions', () => { numerator, target, ); - return expect(() => isRoundingErrorFloor(numerator, denominator, target)) - .to.throw(expectedError.message); + return expect(() => isRoundingErrorFloor(numerator, denominator, target)).to.throw( + expectedError.message, + ); }); }); }); @@ -271,7 +272,8 @@ describe('Reference Functions', () => { const numerator = new BigNumber(1e3); const denominator = new BigNumber(1e4); const target = new BigNumber(333); - const actual = isRoundingErrorFloor(numerator, denominator, target); + // tslint:disable-next-line: boolean-naming + const actual = isRoundingErrorCeil(numerator, denominator, target); expect(actual).to.eq(true); }); @@ -279,7 +281,8 @@ describe('Reference Functions', () => { const numerator = new BigNumber(1e3); const denominator = new BigNumber(1e4); const target = new BigNumber(5e2); - const actual = isRoundingErrorFloor(numerator, denominator, target); + // tslint:disable-next-line: boolean-naming + const actual = isRoundingErrorCeil(numerator, denominator, target); expect(actual).to.eq(false); }); @@ -288,8 +291,9 @@ describe('Reference Functions', () => { const denominator = ZERO_AMOUNT; const target = ONE_ETHER.times(0.01); const expectedError = new LibMathRevertErrors.DivisionByZeroError(); - return expect(() => isRoundingErrorCeil(numerator, denominator, target)) - .to.throw(expectedError.message); + return expect(() => isRoundingErrorCeil(numerator, denominator, target)).to.throw( + expectedError.message, + ); }); it('reverts if `numerator * target` overflows', () => { @@ -301,8 +305,9 @@ describe('Reference Functions', () => { numerator, target, ); - return expect(() => isRoundingErrorCeil(numerator, denominator, target)) - .to.throw(expectedError.message); + return expect(() => isRoundingErrorCeil(numerator, denominator, target)).to.throw( + expectedError.message, + ); }); }); }); diff --git a/contracts/exchange/compiler.json b/contracts/exchange/compiler.json index e00f266e0f..2f7baf36f1 100644 --- a/contracts/exchange/compiler.json +++ b/contracts/exchange/compiler.json @@ -33,10 +33,10 @@ "src/interfaces/IExchangeCore.sol", "src/interfaces/IMatchOrders.sol", "src/interfaces/ISignatureValidator.sol", + "test/IsolatedExchange.sol", "src/interfaces/ITransactions.sol", "src/interfaces/IWallet.sol", "src/interfaces/IWrapperFunctions.sol", - "test/IsolatedExchange.sol", "test/ReentrantERC20Token.sol", "test/TestAssetProxyDispatcher.sol", "test/TestExchangeInternals.sol", diff --git a/contracts/exchange/contracts/test/IsolatedExchange.sol b/contracts/exchange/contracts/test/IsolatedExchange.sol index e670550f9d..00ca515957 100644 --- a/contracts/exchange/contracts/test/IsolatedExchange.sol +++ b/contracts/exchange/contracts/test/IsolatedExchange.sol @@ -64,7 +64,7 @@ contract IsolatedExchange is // Fail if the first byte is 0. if (assetData.length > 0 && assetData[0] == 0x00) { - revert('TRANSFER_FAILED'); + revert("TRANSFER_FAILED"); } } diff --git a/contracts/exchange/src/reference_functions.ts b/contracts/exchange/src/reference_functions.ts index 8fe0b53bdd..91a96999ad 100644 --- a/contracts/exchange/src/reference_functions.ts +++ b/contracts/exchange/src/reference_functions.ts @@ -4,25 +4,17 @@ import { BigNumber } from '@0x/utils'; const { safeGetPartialAmountFloor } = ExchangeLibsReferenceFunctions; -export function calculateFillResults( - order: OrderWithoutDomain, - takerAssetFilledAmount: BigNumber, -): FillResults { +/** + * Calculates amounts filled and fees paid by maker and taker. + */ +export function calculateFillResults(order: OrderWithoutDomain, takerAssetFilledAmount: BigNumber): FillResults { const makerAssetFilledAmount = safeGetPartialAmountFloor( takerAssetFilledAmount, order.takerAssetAmount, order.makerAssetAmount, ); - const makerFeePaid = safeGetPartialAmountFloor( - makerAssetFilledAmount, - order.makerAssetAmount, - order.makerFee, - ); - const takerFeePaid = safeGetPartialAmountFloor( - takerAssetFilledAmount, - order.takerAssetAmount, - order.takerFee, - ); + const makerFeePaid = safeGetPartialAmountFloor(makerAssetFilledAmount, order.makerAssetAmount, order.makerFee); + const takerFeePaid = safeGetPartialAmountFloor(takerAssetFilledAmount, order.takerAssetAmount, order.takerFee); return { makerAssetFilledAmount, takerAssetFilledAmount, diff --git a/contracts/exchange/test/internal.ts b/contracts/exchange/test/internal.ts index 9798ea53e2..4467beaf2b 100644 --- a/contracts/exchange/test/internal.ts +++ b/contracts/exchange/test/internal.ts @@ -19,8 +19,8 @@ import { artifacts, ReferenceFunctions, TestExchangeInternalsContract, - TestExchangeInternalsFillEventArgs, TestExchangeInternalsDispatchTransferFromCalledEventArgs, + TestExchangeInternalsFillEventArgs, } from '../src'; blockchainTests('Exchange core internal functions', env => { @@ -50,7 +50,7 @@ blockchainTests('Exchange core internal functions', env => { let senderAddress: string; before(async () => { - [ senderAddress ] = await env.getAccountAddressesAsync(); + [senderAddress] = await env.getAccountAddressesAsync(); testExchange = await TestExchangeInternalsContract.deployFrom0xArtifactAsync( artifacts.TestExchangeInternals, env.provider, @@ -114,11 +114,7 @@ blockchainTests('Exchange core internal functions', env => { describe('explicit tests', () => { const MAX_UINT256_ROOT = constants.MAX_UINT256_ROOT; function makeOrder(details?: Partial): Order { - return _.assign( - {}, - EMPTY_ORDER, - details, - ); + return _.assign({}, EMPTY_ORDER, details); } it('matches the output of the reference function', async () => { @@ -146,8 +142,9 @@ blockchainTests('Exchange core internal functions', env => { takerAssetFilledAmount, order.makerAssetAmount, ); - return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)) - .to.revertWith(expectedError); + return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)).to.revertWith( + expectedError, + ); }); it('reverts if computing `fillResults.makerFeePaid` overflows', async () => { @@ -168,8 +165,9 @@ blockchainTests('Exchange core internal functions', env => { makerAssetFilledAmount, order.makerFee, ); - return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)) - .to.revertWith(expectedError); + return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)).to.revertWith( + expectedError, + ); }); it('reverts if computing `fillResults.takerFeePaid` overflows', async () => { @@ -185,8 +183,9 @@ blockchainTests('Exchange core internal functions', env => { takerAssetFilledAmount, order.takerFee, ); - return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)) - .to.revertWith(expectedError); + return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)).to.revertWith( + expectedError, + ); }); it('reverts if `order.makerAssetAmount` is 0', async () => { @@ -196,8 +195,9 @@ blockchainTests('Exchange core internal functions', env => { }); const takerAssetFilledAmount = ONE_ETHER; const expectedError = new LibMathRevertErrors.DivisionByZeroError(); - return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)) - .to.revertWith(expectedError); + return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)).to.revertWith( + expectedError, + ); }); it('reverts if `order.takerAssetAmount` is 0', async () => { @@ -207,8 +207,9 @@ blockchainTests('Exchange core internal functions', env => { }); const takerAssetFilledAmount = ONE_ETHER; const expectedError = new LibMathRevertErrors.DivisionByZeroError(); - return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)) - .to.revertWith(expectedError); + return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)).to.revertWith( + expectedError, + ); }); it('reverts if there is a rounding error computing `makerAsssetFilledAmount`', async () => { @@ -222,8 +223,9 @@ blockchainTests('Exchange core internal functions', env => { order.takerAssetAmount, order.makerAssetAmount, ); - return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)) - .to.revertWith(expectedError); + return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)).to.revertWith( + expectedError, + ); }); it('reverts if there is a rounding error computing `makerFeePaid`', async () => { @@ -243,8 +245,9 @@ blockchainTests('Exchange core internal functions', env => { order.makerAssetAmount, order.makerFee, ); - return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)) - .to.revertWith(expectedError); + return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)).to.revertWith( + expectedError, + ); }); it('reverts if there is a rounding error computing `takerFeePaid`', async () => { @@ -264,8 +267,9 @@ blockchainTests('Exchange core internal functions', env => { order.makerAssetAmount, order.takerFee, ); - return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)) - .to.revertWith(expectedError); + return expect(testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount)).to.revertWith( + expectedError, + ); }); }); }); @@ -299,10 +303,7 @@ blockchainTests('Exchange core internal functions', env => { takerAssetFillAmount: BigNumber, ): Promise { const orderHash = randomHash(); - const fillResults = ReferenceFunctions.calculateFillResults( - order, - takerAssetFillAmount, - ); + const fillResults = ReferenceFunctions.calculateFillResults(order, takerAssetFillAmount); const expectedFilledState = orderTakerAssetFilledAmount.plus(takerAssetFillAmount); // CAll `testUpdateFilledState()`, which will set the `filled` // state for this order to `orderTakerAssetFilledAmount` before @@ -321,6 +322,7 @@ blockchainTests('Exchange core internal functions', env => { // Assert the `filled` state for this order. expect(actualFilledState).to.bignumber.eq(expectedFilledState); // Assert the logs. + // tslint:disable-next-line: no-unnecessary-type-assertion const fillEvent = receipt.logs[0] as LogWithDecodedArgs; expect(fillEvent.event).to.eq('Fill'); expect(fillEvent.args.makerAddress).to.eq(order.makerAddress); @@ -363,13 +365,15 @@ blockchainTests('Exchange core internal functions', env => { orderTakerAssetFilledAmount, takerAssetFillAmount, ); - return expect(testExchange.testUpdateFilledState.awaitTransactionSuccessAsync( - order, - randomAddress(), - randomHash(), - orderTakerAssetFilledAmount, - fillResults, - )).to.revertWith(expectedError); + return expect( + testExchange.testUpdateFilledState.awaitTransactionSuccessAsync( + order, + randomAddress(), + randomHash(), + orderTakerAssetFilledAmount, + fillResults, + ), + ).to.revertWith(expectedError); }); }); @@ -402,14 +406,11 @@ blockchainTests('Exchange core internal functions', env => { takerFeePaid: ONE_ETHER.times(0.025), }; const receipt = await logDecoder.getTxWithDecodedLogsAsync( - await testExchange.settleOrder.sendTransactionAsync( - orderHash, - order, - takerAddress, - fillResults, - ), + await testExchange.settleOrder.sendTransactionAsync(orderHash, order, takerAddress, fillResults), ); - const logs = receipt.logs as Array>; + const logs = receipt.logs as Array< + LogWithDecodedArgs + >; expect(logs.length === 4); expect(_.every(logs, log => log.event === 'DispatchTransferFromCalled')).to.be.true(); // taker -> maker diff --git a/contracts/exchange/test/isolated_fill_order.ts b/contracts/exchange/test/isolated_fill_order.ts index 3513373155..dde64c2c7f 100644 --- a/contracts/exchange/test/isolated_fill_order.ts +++ b/contracts/exchange/test/isolated_fill_order.ts @@ -1,10 +1,5 @@ import { ReferenceFunctions as LibReferenceFunctions } from '@0x/contracts-exchange-libs'; -import { - blockchainTests, - constants, - expect, - hexRandom, -} from '@0x/contracts-test-utils'; +import { blockchainTests, constants, expect, hexRandom } from '@0x/contracts-test-utils'; import { ExchangeRevertErrors, LibMathRevertErrors } from '@0x/order-utils'; import { FillResults, OrderInfo, OrderStatus, SignatureType } from '@0x/types'; import { BigNumber, SafeMathRevertErrors } from '@0x/utils'; @@ -50,7 +45,7 @@ blockchainTests('Isolated fillOrder() tests', env => { let nextSaltValue = 1; before(async () => { - [ takerAddress, notTakerAddress ] = await env.getAccountAddressesAsync(); + [takerAddress, notTakerAddress] = await env.getAccountAddressesAsync(); exchange = await IsolatedExchangeWrapper.deployAsync( env.web3Wrapper, _.assign(env.txDefaults, { from: takerAddress }), @@ -79,14 +74,10 @@ blockchainTests('Isolated fillOrder() tests', env => { const fillResults = await exchange.fillOrderAsync(order, takerAssetFillAmount, signature); const newOrderInfo = await exchange.getOrderInfoAsync(order); // Check returned fillResults. - expect(fillResults.makerAssetFilledAmount) - .to.bignumber.eq(efr.makerAssetFilledAmount); - expect(fillResults.takerAssetFilledAmount) - .to.bignumber.eq(efr.takerAssetFilledAmount); - expect(fillResults.makerFeePaid) - .to.bignumber.eq(efr.makerFeePaid); - expect(fillResults.takerFeePaid) - .to.bignumber.eq(efr.takerFeePaid); + expect(fillResults.makerAssetFilledAmount).to.bignumber.eq(efr.makerAssetFilledAmount); + expect(fillResults.takerAssetFilledAmount).to.bignumber.eq(efr.takerAssetFilledAmount); + expect(fillResults.makerFeePaid).to.bignumber.eq(efr.makerFeePaid); + expect(fillResults.takerFeePaid).to.bignumber.eq(efr.takerFeePaid); // Check balances. for (const assetData of Object.keys(efb)) { for (const address of Object.keys(efb[assetData])) { @@ -98,13 +89,11 @@ blockchainTests('Isolated fillOrder() tests', env => { } // Check order info. expect(newOrderInfo.orderStatus).to.eq(eoi.orderStatus); - expect(newOrderInfo.orderTakerAssetFilledAmount) - .to.bignumber.eq(eoi.orderTakerAssetFilledAmount); + expect(newOrderInfo.orderTakerAssetFilledAmount).to.bignumber.eq(eoi.orderTakerAssetFilledAmount); // Check that there wasn't an overfill. - expect( - newOrderInfo.orderTakerAssetFilledAmount.lte(order.takerAssetAmount), - 'checking for overfill', - ).to.be.ok(''); + expect(newOrderInfo.orderTakerAssetFilledAmount.lte(order.takerAssetAmount), 'checking for overfill').to.be.ok( + '', + ); return { fillResults, orderInfo: newOrderInfo, @@ -116,24 +105,17 @@ blockchainTests('Isolated fillOrder() tests', env => { orderInfo: OrderInfo, takerAssetFillAmount: BigNumber, ): FillResults { - const remainingTakerAssetAmount = order.takerAssetAmount.minus( - orderInfo.orderTakerAssetFilledAmount, - ); - return calculateFillResults( - order, - BigNumber.min(takerAssetFillAmount, remainingTakerAssetAmount), - ); + const remainingTakerAssetAmount = order.takerAssetAmount.minus(orderInfo.orderTakerAssetFilledAmount); + return calculateFillResults(order, BigNumber.min(takerAssetFillAmount, remainingTakerAssetAmount)); } - function calculateExpectedOrderInfo( - order: Order, - orderInfo: OrderInfo, - fillResults: FillResults, - ): OrderInfo { - const orderTakerAssetFilledAmount = - orderInfo.orderTakerAssetFilledAmount.plus(fillResults.takerAssetFilledAmount); - const orderStatus = orderTakerAssetFilledAmount.gte(order.takerAssetAmount) ? - OrderStatus.FullyFilled : OrderStatus.Fillable; + function calculateExpectedOrderInfo(order: Order, orderInfo: OrderInfo, fillResults: FillResults): OrderInfo { + const orderTakerAssetFilledAmount = orderInfo.orderTakerAssetFilledAmount.plus( + fillResults.takerAssetFilledAmount, + ); + const orderStatus = orderTakerAssetFilledAmount.gte(order.takerAssetAmount) + ? OrderStatus.FullyFilled + : OrderStatus.Fillable; return { orderHash: exchange.getOrderHash(order), orderStatus, @@ -141,10 +123,7 @@ blockchainTests('Isolated fillOrder() tests', env => { }; } - function calculateExpectedFillBalances( - order: Order, - fillResults: FillResults, - ): AssetBalances { + function calculateExpectedFillBalances(order: Order, fillResults: FillResults): AssetBalances { const balances: AssetBalances = {}; const addBalance = (assetData: string, address: string, amount: BigNumber) => { balances[assetData] = balances[assetData] || {}; @@ -176,7 +155,7 @@ blockchainTests('Isolated fillOrder() tests', env => { expect(orderInfo.orderStatus).to.eq(OrderStatus.FullyFilled); }); - it('can\'t overfill an order', async () => { + it("can't overfill an order", async () => { const order = createOrder(); const { orderInfo } = await fillOrderAndAssertResultsAsync(order, order.takerAssetAmount.times(1.01)); expect(orderInfo.orderStatus).to.eq(OrderStatus.FullyFilled); @@ -266,7 +245,7 @@ blockchainTests('Isolated fillOrder() tests', env => { expect(orderInfos[1].orderStatus).to.eq(OrderStatus.Fillable); }); - it('can\'t overfill an order in two fills', async () => { + it("can't overfill an order in two fills", async () => { const order = createOrder(); const fillAmounts = splitAmount(order.takerAssetAmount); fillAmounts[0] = fillAmounts[0].times(1.01); @@ -320,7 +299,7 @@ blockchainTests('Isolated fillOrder() tests', env => { }); describe('bad fills', () => { - it('can\'t fill an order with zero takerAssetAmount', async () => { + it("can't fill an order with zero takerAssetAmount", async () => { const order = createOrder({ takerAssetAmount: ZERO_AMOUNT, }); @@ -328,11 +307,10 @@ blockchainTests('Isolated fillOrder() tests', env => { exchange.getOrderHash(order), OrderStatus.InvalidTakerAssetAmount, ); - return expect(exchange.fillOrderAsync(order, ONE_ETHER)) - .to.revertWith(expectedError); + return expect(exchange.fillOrderAsync(order, ONE_ETHER)).to.revertWith(expectedError); }); - it('can\'t fill an order with zero makerAssetAmount', async () => { + it("can't fill an order with zero makerAssetAmount", async () => { const order = createOrder({ makerAssetAmount: ZERO_AMOUNT, }); @@ -340,22 +318,20 @@ blockchainTests('Isolated fillOrder() tests', env => { exchange.getOrderHash(order), OrderStatus.InvalidMakerAssetAmount, ); - return expect(exchange.fillOrderAsync(order, ONE_ETHER)) - .to.revertWith(expectedError); + return expect(exchange.fillOrderAsync(order, ONE_ETHER)).to.revertWith(expectedError); }); - it('can\'t fill an order that is fully filled', async () => { + it("can't fill an order that is fully filled", async () => { const order = createOrder(); const expectedError = new ExchangeRevertErrors.OrderStatusError( exchange.getOrderHash(order), OrderStatus.FullyFilled, ); await exchange.fillOrderAsync(order, order.takerAssetAmount); - return expect(exchange.fillOrderAsync(order, 1)) - .to.revertWith(expectedError); + return expect(exchange.fillOrderAsync(order, 1)).to.revertWith(expectedError); }); - it('can\'t fill an order that is expired', async () => { + it("can't fill an order that is expired", async () => { const order = createOrder({ expirationTimeSeconds: new BigNumber(getCurrentTime() - 60), }); @@ -363,11 +339,10 @@ blockchainTests('Isolated fillOrder() tests', env => { exchange.getOrderHash(order), OrderStatus.Expired, ); - return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)) - .to.revertWith(expectedError); + return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)).to.revertWith(expectedError); }); - it('can\'t fill an order that is cancelled by `cancelOrder()`', async () => { + it("can't fill an order that is cancelled by `cancelOrder()`", async () => { const order = createOrder({ makerAddress: notTakerAddress, }); @@ -376,11 +351,10 @@ blockchainTests('Isolated fillOrder() tests', env => { OrderStatus.Cancelled, ); await exchange.cancelOrderAsync(order, { from: notTakerAddress }); - return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)) - .to.revertWith(expectedError); + return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)).to.revertWith(expectedError); }); - it('can\'t fill an order that is cancelled by `cancelOrdersUpTo()`', async () => { + it("can't fill an order that is cancelled by `cancelOrdersUpTo()`", async () => { const order = createOrder({ makerAddress: notTakerAddress, }); @@ -389,11 +363,10 @@ blockchainTests('Isolated fillOrder() tests', env => { OrderStatus.Cancelled, ); await exchange.cancelOrdersUpToAsync(order.salt, { from: notTakerAddress }); - return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)) - .to.revertWith(expectedError); + return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)).to.revertWith(expectedError); }); - it('can\'t fill an order if taker is not `takerAddress`', async () => { + it("can't fill an order if taker is not `takerAddress`", async () => { const order = createOrder({ takerAddress: randomAddress(), }); @@ -401,11 +374,10 @@ blockchainTests('Isolated fillOrder() tests', env => { exchange.getOrderHash(order), takerAddress, ); - return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)) - .to.revertWith(expectedError); + return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)).to.revertWith(expectedError); }); - it('can\'t fill an order if sender is not `senderAddress`', async () => { + it("can't fill an order if sender is not `senderAddress`", async () => { const order = createOrder({ senderAddress: randomAddress(), }); @@ -413,11 +385,10 @@ blockchainTests('Isolated fillOrder() tests', env => { exchange.getOrderHash(order), takerAddress, ); - return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)) - .to.revertWith(expectedError); + return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)).to.revertWith(expectedError); }); - it('can\'t fill an order with a taker amount that results in a maker asset rounding error', async () => { + it("can't fill an order with a taker amount that results in a maker asset rounding error", async () => { const order = createOrder({ makerAssetAmount: new BigNumber(100), takerAssetAmount: ONE_ETHER, @@ -428,11 +399,10 @@ blockchainTests('Isolated fillOrder() tests', env => { order.takerAssetAmount, order.makerAssetAmount, ); - return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)) - .to.revertWith(expectedError); + return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)).to.revertWith(expectedError); }); - it('can\'t fill an order with a taker amount that results in a maker fee rounding error', async () => { + it("can't fill an order with a taker amount that results in a maker fee rounding error", async () => { const order = createOrder({ makerAssetAmount: ONE_ETHER.times(2), takerAssetAmount: ONE_ETHER, @@ -444,11 +414,10 @@ blockchainTests('Isolated fillOrder() tests', env => { order.makerAssetAmount, order.makerFee, ); - return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)) - .to.revertWith(expectedError); + return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)).to.revertWith(expectedError); }); - it('can\'t fill an order with a taker amount that results in a taker fee rounding error', async () => { + it("can't fill an order with a taker amount that results in a taker fee rounding error", async () => { const order = createOrder({ makerAssetAmount: ONE_ETHER.times(2), takerAssetAmount: ONE_ETHER, @@ -460,11 +429,10 @@ blockchainTests('Isolated fillOrder() tests', env => { order.takerAssetAmount, order.takerFee, ); - return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)) - .to.revertWith(expectedError); + return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)).to.revertWith(expectedError); }); - it('can\'t fill an order that results in a `makerAssetFilledAmount` overflow.', async () => { + it("can't fill an order that results in a `makerAssetFilledAmount` overflow.", async () => { // All values need to be large to ensure we don't trigger a Rounding. const order = createOrder({ makerAssetAmount: MAX_UINT256_ROOT.times(2), @@ -476,11 +444,10 @@ blockchainTests('Isolated fillOrder() tests', env => { takerAssetFillAmount, order.makerAssetAmount, ); - return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)) - .to.revertWith(expectedError); + return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)).to.revertWith(expectedError); }); - it('can\'t fill an order that results in a `makerFeePaid` overflow.', async () => { + it("can't fill an order that results in a `makerFeePaid` overflow.", async () => { // All values need to be large to ensure we don't trigger a Rounding. const order = createOrder({ makerAssetAmount: MAX_UINT256_ROOT, @@ -498,11 +465,10 @@ blockchainTests('Isolated fillOrder() tests', env => { makerAssetFilledAmount, order.makerFee, ); - return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)) - .to.revertWith(expectedError); + return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)).to.revertWith(expectedError); }); - it('can\'t fill an order that results in a `takerFeePaid` overflow.', async () => { + it("can't fill an order that results in a `takerFeePaid` overflow.", async () => { // All values need to be large to ensure we don't trigger a Rounding. const order = createOrder({ makerAssetAmount: MAX_UINT256_ROOT, @@ -515,11 +481,10 @@ blockchainTests('Isolated fillOrder() tests', env => { takerAssetFillAmount, order.takerFee, ); - return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)) - .to.revertWith(expectedError); + return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)).to.revertWith(expectedError); }); - it('can\'t fill an order with a bad signature', async () => { + it("can't fill an order with a bad signature", async () => { const order = createOrder(); const signature = createBadSignature(); const expectedError = new ExchangeRevertErrors.SignatureError( @@ -528,11 +493,12 @@ blockchainTests('Isolated fillOrder() tests', env => { order.makerAddress, signature, ); - return expect(exchange.fillOrderAsync(order, order.takerAssetAmount, signature)) - .to.revertWith(expectedError); + return expect(exchange.fillOrderAsync(order, order.takerAssetAmount, signature)).to.revertWith( + expectedError, + ); }); - it('can\'t complementary fill an order with a bad signature that is always checked', async () => { + it("can't complementary fill an order with a bad signature that is always checked", async () => { const order = createOrder(); const takerAssetFillAmounts = splitAmount(order.takerAssetAmount); const goodSignature = createGoodSignature(SignatureType.Wallet); @@ -544,42 +510,39 @@ blockchainTests('Isolated fillOrder() tests', env => { badSignature, ); await exchange.fillOrderAsync(order, takerAssetFillAmounts[0], goodSignature); - return expect(exchange.fillOrderAsync(order, takerAssetFillAmounts[1], badSignature)) - .to.revertWith(expectedError); + return expect(exchange.fillOrderAsync(order, takerAssetFillAmounts[1], badSignature)).to.revertWith( + expectedError, + ); }); const TRANSFER_ERROR = 'TRANSFER_FAILED'; - it('can\'t fill an order with a maker asset that fails to transfer', async () => { + it("can't fill an order with a maker asset that fails to transfer", async () => { const order = createOrder({ makerAssetData: createBadAssetData(), }); - return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)) - .to.revertWith(TRANSFER_ERROR); + return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)).to.revertWith(TRANSFER_ERROR); }); - it('can\'t fill an order with a taker asset that fails to transfer', async () => { + it("can't fill an order with a taker asset that fails to transfer", async () => { const order = createOrder({ takerAssetData: createBadAssetData(), }); - return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)) - .to.revertWith(TRANSFER_ERROR); + return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)).to.revertWith(TRANSFER_ERROR); }); - it('can\'t fill an order with a maker fee asset that fails to transfer', async () => { + it("can't fill an order with a maker fee asset that fails to transfer", async () => { const order = createOrder({ makerFeeAssetData: createBadAssetData(), }); - return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)) - .to.revertWith(TRANSFER_ERROR); + return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)).to.revertWith(TRANSFER_ERROR); }); - it('can\'t fill an order with a taker fee asset that fails to transfer', async () => { + it("can't fill an order with a taker fee asset that fails to transfer", async () => { const order = createOrder({ takerFeeAssetData: createBadAssetData(), }); - return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)) - .to.revertWith(TRANSFER_ERROR); + return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)).to.revertWith(TRANSFER_ERROR); }); }); diff --git a/contracts/exchange/test/match_orders.ts b/contracts/exchange/test/match_orders.ts index 6547813428..4a908aa045 100644 --- a/contracts/exchange/test/match_orders.ts +++ b/contracts/exchange/test/match_orders.ts @@ -270,17 +270,9 @@ describe('matchOrders', () => { const numerator = signedOrderLeft.makerAssetAmount; const denominator = signedOrderLeft.takerAssetAmount; const target = signedOrderRight.makerAssetAmount; - const _isRoundingErrorCeil = isRoundingErrorCeil( - numerator, - denominator, - target, - ); + const _isRoundingErrorCeil = isRoundingErrorCeil(numerator, denominator, target); expect(_isRoundingErrorCeil).to.be.true(); - const _isRoundingErrorFloor = isRoundingErrorFloor( - numerator, - denominator, - target, - ); + const _isRoundingErrorFloor = isRoundingErrorFloor(numerator, denominator, target); expect(_isRoundingErrorFloor).to.be.false(); // Match signedOrderLeft with signedOrderRight // Note that the left maker received a slightly better sell price. @@ -336,17 +328,9 @@ describe('matchOrders', () => { const numerator = signedOrderRight.takerAssetAmount; const denominator = signedOrderRight.makerAssetAmount; const target = signedOrderLeft.takerAssetAmount; - const _isRoundingErrorFloor = isRoundingErrorFloor( - numerator, - denominator, - target, - ); + const _isRoundingErrorFloor = isRoundingErrorFloor(numerator, denominator, target); expect(_isRoundingErrorFloor).to.be.true(); - const _isRoundingErrorCeil = isRoundingErrorCeil( - numerator, - denominator, - target, - ); + const _isRoundingErrorCeil = isRoundingErrorCeil(numerator, denominator, target); expect(_isRoundingErrorCeil).to.be.false(); // Match signedOrderLeft isRoundingErrorFloor right maker received a slightly better purchase price. // This is intentional; see note in MixinMatchOrders.calculateMatchedFillResults. @@ -1414,17 +1398,9 @@ describe('matchOrders', () => { const numerator = signedOrderLeft.makerAssetAmount; const denominator = signedOrderLeft.takerAssetAmount; const target = signedOrderRight.makerAssetAmount; - const _isRoundingErrorCeil = isRoundingErrorCeil( - numerator, - denominator, - target, - ); + const _isRoundingErrorCeil = isRoundingErrorCeil(numerator, denominator, target); expect(_isRoundingErrorCeil).to.be.true(); - const _isRoundingErrorFloor = isRoundingErrorFloor( - numerator, - denominator, - target, - ); + const _isRoundingErrorFloor = isRoundingErrorFloor(numerator, denominator, target); expect(_isRoundingErrorFloor).to.be.false(); // Match signedOrderLeft with signedOrderRight // Note that the left maker received a slightly better sell price. @@ -1480,17 +1456,9 @@ describe('matchOrders', () => { const numerator = signedOrderRight.makerAssetAmount; const denominator = signedOrderRight.takerAssetAmount; const target = signedOrderLeft.makerAssetAmount; - const _isRoundingErrorCeil = isRoundingErrorCeil( - numerator, - denominator, - target, - ); + const _isRoundingErrorCeil = isRoundingErrorCeil(numerator, denominator, target); expect(_isRoundingErrorCeil).to.be.false(); - const _isRoundingErrorFloor = isRoundingErrorFloor( - numerator, - denominator, - target, - ); + const _isRoundingErrorFloor = isRoundingErrorFloor(numerator, denominator, target); expect(_isRoundingErrorFloor).to.be.false(); // Match signedOrderLeft with signedOrderRight // Note that the right maker received a slightly better purchase price. diff --git a/contracts/exchange/test/reference_functions.ts b/contracts/exchange/test/reference_functions.ts index b2dd6dae23..2ac0c5ea23 100644 --- a/contracts/exchange/test/reference_functions.ts +++ b/contracts/exchange/test/reference_functions.ts @@ -1,9 +1,5 @@ import { ReferenceFunctions as LibReferenceFunctions } from '@0x/contracts-exchange-libs'; -import { - constants, - describe, - expect, -} from '@0x/contracts-test-utils'; +import { constants, describe, expect } from '@0x/contracts-test-utils'; import { LibMathRevertErrors } from '@0x/order-utils'; import { OrderWithoutDomain as Order } from '@0x/types'; import { BigNumber, SafeMathRevertErrors } from '@0x/utils'; @@ -33,11 +29,7 @@ describe('Reference functions', () => { describe('calculateFillResults', () => { const MAX_UINT256_ROOT = constants.MAX_UINT256_ROOT; function makeOrder(details?: Partial): Order { - return _.assign( - {}, - EMPTY_ORDER, - details, - ); + return _.assign({}, EMPTY_ORDER, details); } it('reverts if computing `fillResults.makerAssetFilledAmount` overflows', () => { @@ -52,8 +44,7 @@ describe('Reference functions', () => { takerAssetFilledAmount, order.makerAssetAmount, ); - return expect(() => calculateFillResults(order, takerAssetFilledAmount)) - .to.throw(expectedError.message); + return expect(() => calculateFillResults(order, takerAssetFilledAmount)).to.throw(expectedError.message); }); it('reverts if computing `fillResults.makerFeePaid` overflows', () => { @@ -74,8 +65,7 @@ describe('Reference functions', () => { makerAssetFilledAmount, order.makerFee, ); - return expect(() => calculateFillResults(order, takerAssetFilledAmount)) - .to.throw(expectedError.message); + return expect(() => calculateFillResults(order, takerAssetFilledAmount)).to.throw(expectedError.message); }); it('reverts if computing `fillResults.takerFeePaid` overflows', () => { @@ -91,8 +81,7 @@ describe('Reference functions', () => { takerAssetFilledAmount, order.takerFee, ); - return expect(() => calculateFillResults(order, takerAssetFilledAmount)) - .to.throw(expectedError.message); + return expect(() => calculateFillResults(order, takerAssetFilledAmount)).to.throw(expectedError.message); }); it('reverts if `order.makerAssetAmount` is 0', () => { @@ -102,8 +91,7 @@ describe('Reference functions', () => { }); const takerAssetFilledAmount = ONE_ETHER; const expectedError = new LibMathRevertErrors.DivisionByZeroError(); - return expect(() => calculateFillResults(order, takerAssetFilledAmount)) - .to.throw(expectedError.message); + return expect(() => calculateFillResults(order, takerAssetFilledAmount)).to.throw(expectedError.message); }); it('reverts if `order.takerAssetAmount` is 0', () => { @@ -113,8 +101,7 @@ describe('Reference functions', () => { }); const takerAssetFilledAmount = ONE_ETHER; const expectedError = new LibMathRevertErrors.DivisionByZeroError(); - return expect(() => calculateFillResults(order, takerAssetFilledAmount)) - .to.throw(expectedError.message); + return expect(() => calculateFillResults(order, takerAssetFilledAmount)).to.throw(expectedError.message); }); it('reverts if there is a rounding error computing `makerAsssetFilledAmount`', () => { @@ -128,8 +115,7 @@ describe('Reference functions', () => { order.takerAssetAmount, order.makerAssetAmount, ); - return expect(() => calculateFillResults(order, takerAssetFilledAmount)) - .to.throw(expectedError.message); + return expect(() => calculateFillResults(order, takerAssetFilledAmount)).to.throw(expectedError.message); }); it('reverts if there is a rounding error computing `makerFeePaid`', () => { @@ -149,8 +135,7 @@ describe('Reference functions', () => { order.makerAssetAmount, order.makerFee, ); - return expect(() => calculateFillResults(order, takerAssetFilledAmount)) - .to.throw(expectedError.message); + return expect(() => calculateFillResults(order, takerAssetFilledAmount)).to.throw(expectedError.message); }); it('reverts if there is a rounding error computing `takerFeePaid`', () => { @@ -170,8 +155,7 @@ describe('Reference functions', () => { order.makerAssetAmount, order.takerFee, ); - return expect(() => calculateFillResults(order, takerAssetFilledAmount)) - .to.throw(expectedError.message); + return expect(() => calculateFillResults(order, takerAssetFilledAmount)).to.throw(expectedError.message); }); }); }); diff --git a/contracts/exchange/test/utils/exchange_wrapper.ts b/contracts/exchange/test/utils/exchange_wrapper.ts index ddf62f79ac..da244e35e3 100644 --- a/contracts/exchange/test/utils/exchange_wrapper.ts +++ b/contracts/exchange/test/utils/exchange_wrapper.ts @@ -1,12 +1,7 @@ import { artifacts as erc1155Artifacts } from '@0x/contracts-erc1155'; import { artifacts as erc20Artifacts } from '@0x/contracts-erc20'; import { artifacts as erc721Artifacts } from '@0x/contracts-erc721'; -import { - BatchMatchOrder, - LogDecoder, - orderUtils, - Web3ProviderEngine, -} from '@0x/contracts-test-utils'; +import { BatchMatchOrder, LogDecoder, orderUtils, Web3ProviderEngine } from '@0x/contracts-test-utils'; import { BatchMatchedFillResults, FillResults, diff --git a/contracts/exchange/test/utils/fill_order_scenarios.ts b/contracts/exchange/test/utils/fill_order_scenarios.ts index d120560c17..e80015d6e8 100644 --- a/contracts/exchange/test/utils/fill_order_scenarios.ts +++ b/contracts/exchange/test/utils/fill_order_scenarios.ts @@ -1,5 +1,3 @@ -import { BigNumber } from '@0x/utils'; - export enum FeeRecipientAddressScenario { BurnAddress = 'BURN_ADDRESS', EthUserAddress = 'ETH_USER_ADDRESS', diff --git a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts index 43f483d782..89a6a5b182 100644 --- a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts +++ b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts @@ -130,9 +130,7 @@ export class IsolatedExchangeWrapper { await this.instance.fillOrder.sendTransactionAsync.call(this.instance, ...args), ); this.lastTxEvents = extractEvents(receipt.logs); - this.lastTxBalanceChanges = getBalanceChangesFromTransferFromCalls( - this.lastTxEvents.transferFromCalls, - ); + this.lastTxBalanceChanges = getBalanceChangesFromTransferFromCalls(this.lastTxEvents.transferFromCalls); return result; } } @@ -179,21 +177,16 @@ function createEmptyEvents(): IsolatedExchangeEvents { function extractEvents(logs: LogEntry[]): IsolatedExchangeEvents { return { fillEvents: filterLogsToArguments(logs, 'Fill'), - transferFromCalls: filterLogsToArguments( - logs, - 'DispatchTransferFromCalled', - ), + transferFromCalls: filterLogsToArguments(logs, 'DispatchTransferFromCalled'), }; } // Executes transferFrom calls to compute relative balances for addresses. -function getBalanceChangesFromTransferFromCalls( - calls: DispatchTransferFromCallArgs[], -): AssetBalances { +function getBalanceChangesFromTransferFromCalls(calls: DispatchTransferFromCallArgs[]): AssetBalances { const changes: AssetBalances = {}; for (const call of calls) { const { assetData, from, to, amount } = call; - const balances = changes[assetData] = changes[assetData ] || {}; + const balances = (changes[assetData] = changes[assetData] || {}); const fromBalance = balances[from] || constants.ZERO_AMOUNT; const toBalance = balances[to] || constants.ZERO_AMOUNT; balances[from] = fromBalance.minus(amount); diff --git a/contracts/exchange/test/utils/match_order_tester.ts b/contracts/exchange/test/utils/match_order_tester.ts index 16f21f4cc9..d9e997d601 100644 --- a/contracts/exchange/test/utils/match_order_tester.ts +++ b/contracts/exchange/test/utils/match_order_tester.ts @@ -1,17 +1,7 @@ import { ERC1155ProxyWrapper, ERC20Wrapper, ERC721Wrapper } from '@0x/contracts-asset-proxy'; -import { - ERC1155HoldingsByOwner, - expect, - OrderStatus, -} from '@0x/contracts-test-utils'; +import { ERC1155HoldingsByOwner, expect, OrderStatus } from '@0x/contracts-test-utils'; import { assetDataUtils, orderHashUtils } from '@0x/order-utils'; -import { - AssetProxyId, - BatchMatchedFillResults, - FillResults, - MatchedFillResults, - SignedOrder, -} from '@0x/types'; +import { AssetProxyId, BatchMatchedFillResults, FillResults, MatchedFillResults, SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { LogWithDecodedArgs, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import * as _ from 'lodash'; diff --git a/contracts/exchange/tslint.json b/contracts/exchange/tslint.json index 1bb3ac2a22..2a304eb78b 100644 --- a/contracts/exchange/tslint.json +++ b/contracts/exchange/tslint.json @@ -2,5 +2,8 @@ "extends": ["@0x/tslint-config"], "rules": { "custom-no-magic-numbers": false + }, + "linterOptions": { + "exclude": ["src/artifacts.ts"] } } diff --git a/contracts/test-utils/CHANGELOG.json b/contracts/test-utils/CHANGELOG.json index c69893c22c..3922291aec 100644 --- a/contracts/test-utils/CHANGELOG.json +++ b/contracts/test-utils/CHANGELOG.json @@ -61,7 +61,7 @@ { "note": "Update `testWithReferenceFuncAsync` to work with `RevertErrors`", "pr": "TODO" - }, + } ] }, { diff --git a/contracts/test-utils/src/test_with_reference.ts b/contracts/test-utils/src/test_with_reference.ts index 7d0dc23521..fde39852e6 100644 --- a/contracts/test-utils/src/test_with_reference.ts +++ b/contracts/test-utils/src/test_with_reference.ts @@ -83,11 +83,7 @@ export async function testWithReferenceFuncAsync( if (expectedError !== undefined) { // Expecting an error. if (actualError === undefined) { - return expect.fail( - actualError, - expectedError, - `${testCaseString}: expected failure but instead succeeded`, - ); + return expect.fail(actualError, expectedError, `${testCaseString}: expected failure but instead succeeded`); } else { if (expectedError instanceof RevertError) { // Expecting a RevertError. @@ -112,7 +108,9 @@ export async function testWithReferenceFuncAsync( return expect.fail( actualError, expectedError, - `${testCaseString}: expected error message '${actualError.message}' to equal '${expectedError.message}'`, + `${testCaseString}: expected error message '${actualError.message}' to equal '${ + expectedError.message + }'`, ); } } @@ -120,11 +118,7 @@ export async function testWithReferenceFuncAsync( } else { // Not expecting an error. if (actualError !== undefined) { - return expect.fail( - actualError, - expectedError, - `${testCaseString}: expected success but instead failed`, - ); + return expect.fail(actualError, expectedError, `${testCaseString}: expected success but instead failed`); } if (expected instanceof BigNumber) { // Technically we can do this with `deep.eq`, but this prints prettier diff --git a/contracts/test-utils/test/test_with_reference.ts b/contracts/test-utils/test/test_with_reference.ts index b3fbef3599..7407bf0c93 100644 --- a/contracts/test-utils/test/test_with_reference.ts +++ b/contracts/test-utils/test/test_with_reference.ts @@ -28,8 +28,9 @@ describe('testWithReferenceFuncAsync', () => { }); it('fails when both succeed and actual != expected', async () => { - return expect(testWithReferenceFuncAsync(alwaysValueFunc(3), divAsync, [1, 2])) - .to.be.rejectedWith('{"x":1,"y":2}: expected 0.5 to deeply equal 3'); + return expect(testWithReferenceFuncAsync(alwaysValueFunc(3), divAsync, [1, 2])).to.be.rejectedWith( + '{"x":1,"y":2}: expected 0.5 to deeply equal 3', + ); }); it('passes when both fail and error messages are the same', async () => { @@ -44,9 +45,7 @@ describe('testWithReferenceFuncAsync', () => { const notError = new Error(notErrorMessage); return expect( testWithReferenceFuncAsync(alwaysFailFunc(notError), alwaysFailFunc(error), [1, 2]), - ).to.be.rejectedWith( - `{"x":1,"y":2}: expected error message '${errorMessage}' to equal '${notErrorMessage}'`, - ); + ).to.be.rejectedWith(`{"x":1,"y":2}: expected error message '${errorMessage}' to equal '${notErrorMessage}'`); }); it('passes when both fail with compatible RevertErrors', async () => { @@ -58,34 +57,32 @@ describe('testWithReferenceFuncAsync', () => { it('fails when both fail with incompatible RevertErrors', async () => { const error1 = new StringRevertError('whoopsie'); const error2 = new StringRevertError('not whoopsie'); - return expect(testWithReferenceFuncAsync(alwaysFailFunc(error1), alwaysFailFunc(error2), [1, 1])) - .to.be.rejectedWith( - `{"x":1,"y":1}: expected error StringRevertError({ message: 'not whoopsie' }) to equal StringRevertError({ message: 'whoopsie' })`, - ); + return expect( + testWithReferenceFuncAsync(alwaysFailFunc(error1), alwaysFailFunc(error2), [1, 1]), + ).to.be.rejectedWith( + `{"x":1,"y":1}: expected error StringRevertError({ message: 'not whoopsie' }) to equal StringRevertError({ message: 'whoopsie' })`, + ); }); it('fails when reference function fails with a RevertError but test function fails with a regular Error', async () => { const error1 = new StringRevertError('whoopsie'); const error2 = new Error('whoopsie'); - return expect(testWithReferenceFuncAsync(alwaysFailFunc(error1), alwaysFailFunc(error2), [1, 1])) - .to.be.rejectedWith( - `{"x":1,"y":1}: expected a RevertError but received an Error`, - ); + return expect( + testWithReferenceFuncAsync(alwaysFailFunc(error1), alwaysFailFunc(error2), [1, 1]), + ).to.be.rejectedWith(`{"x":1,"y":1}: expected a RevertError but received an Error`); }); it('fails when referenceFunc succeeds and testFunc fails', async () => { const error = new Error('whoopsie'); - return expect(testWithReferenceFuncAsync(alwaysValueFunc(0), alwaysFailFunc(error), [1, 2])) - .to.be.rejectedWith( - `{"x":1,"y":2}: expected success but instead failed`, - ); + return expect(testWithReferenceFuncAsync(alwaysValueFunc(0), alwaysFailFunc(error), [1, 2])).to.be.rejectedWith( + `{"x":1,"y":2}: expected success but instead failed`, + ); }); it('fails when referenceFunc fails and testFunc succeeds', async () => { const error = new Error('whoopsie'); - return expect(testWithReferenceFuncAsync(alwaysFailFunc(error), divAsync, [1, 2])) - .to.be.rejectedWith( - '{"x":1,"y":2}: expected failure but instead succeeded', - ); + return expect(testWithReferenceFuncAsync(alwaysFailFunc(error), divAsync, [1, 2])).to.be.rejectedWith( + '{"x":1,"y":2}: expected failure but instead succeeded', + ); }); }); diff --git a/contracts/utils/src/reference_functions.ts b/contracts/utils/src/reference_functions.ts index e623ef639f..0049d661bb 100644 --- a/contracts/utils/src/reference_functions.ts +++ b/contracts/utils/src/reference_functions.ts @@ -2,6 +2,9 @@ import { BigNumber, SafeMathRevertErrors } from '@0x/utils'; const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); +/** + * Add two `uint256` values. Reverts on overflow. + */ export function safeAdd(a: BigNumber, b: BigNumber): BigNumber { const r = a.plus(b); if (r.isGreaterThan(MAX_UINT256)) { @@ -14,6 +17,9 @@ export function safeAdd(a: BigNumber, b: BigNumber): BigNumber { return r; } +/** + * Subract two `uint256` values. Reverts on overflow. + */ export function safeSub(a: BigNumber, b: BigNumber): BigNumber { const r = a.minus(b); if (r.isLessThan(0)) { @@ -26,6 +32,9 @@ export function safeSub(a: BigNumber, b: BigNumber): BigNumber { return r; } +/** + * Multiplies two `uint256` values. Reverts on overflow. + */ export function safeMul(a: BigNumber, b: BigNumber): BigNumber { const r = a.times(b); if (r.isGreaterThan(MAX_UINT256)) { @@ -38,6 +47,9 @@ export function safeMul(a: BigNumber, b: BigNumber): BigNumber { return r; } +/** + * Divides two `uint256` values. Reverts on division by zero. + */ export function safeDiv(a: BigNumber, b: BigNumber): BigNumber { if (b.isEqualTo(0)) { throw new SafeMathRevertErrors.SafeMathError( From 8c9bdadf666a3d40c3dafd7df62b3bd4b15cfb19 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 2 Aug 2019 16:18:05 -0400 Subject: [PATCH 35/48] `@0x/contracts-utils`: Add unit tests for `ReferenceFunctions`. --- contracts/utils/test/reference_functions.ts | 93 +++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 contracts/utils/test/reference_functions.ts diff --git a/contracts/utils/test/reference_functions.ts b/contracts/utils/test/reference_functions.ts new file mode 100644 index 0000000000..047ecfd185 --- /dev/null +++ b/contracts/utils/test/reference_functions.ts @@ -0,0 +1,93 @@ +import { constants, describe, expect } from '@0x/contracts-test-utils'; +import { SafeMathRevertErrors } from '@0x/utils'; + +import { safeAdd, safeDiv, safeMul, safeSub } from '../src/reference_functions'; + +describe('Reference Functions', () => { + const { ONE_ETHER, MAX_UINT256, ZERO_AMOUNT } = constants; + const DEFAULT_VALUES = { + a: ONE_ETHER.times(2), + b: ONE_ETHER, + }; + describe('SafeMath', () => { + describe('safeAdd', () => { + it('adds two numbers', () => { + const { a, b } = DEFAULT_VALUES; + const expected = a.plus(b); + const actual = safeAdd(a, b); + expect(actual).to.bignumber.eq(expected); + }); + + it('reverts on overflow', () => { + const a = MAX_UINT256.dividedToIntegerBy(2); + const b = MAX_UINT256.dividedToIntegerBy(2).plus(2); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256AdditionOverflow, + a, + b, + ); + expect(() => safeAdd(a, b)).to.throw(expectedError.message); + }); + }); + + describe('safeSub', () => { + it('subracts two numbers', () => { + const { a, b } = DEFAULT_VALUES; + const expected = a.minus(b); + const actual = safeSub(a, b); + expect(actual).to.bignumber.eq(expected); + }); + + it('reverts on underflow', () => { + const a = MAX_UINT256.dividedToIntegerBy(2); + const b = MAX_UINT256.dividedToIntegerBy(2).plus(2); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256SubtractionUnderflow, + a, + b, + ); + expect(() => safeSub(a, b)).to.throw(expectedError.message); + }); + }); + + describe('safeMul', () => { + it('multiplies two numbers', () => { + const { a, b } = DEFAULT_VALUES; + const expected = a.times(b); + const actual = safeMul(a, b); + expect(actual).to.bignumber.eq(expected); + }); + + it('reverts on overflow', () => { + const a = MAX_UINT256.dividedToIntegerBy(2); + const b = MAX_UINT256.dividedToIntegerBy(2).plus(2); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow, + a, + b, + ); + expect(() => safeMul(a, b)).to.throw(expectedError.message); + }); + }); + + describe('safeDiv', () => { + it('multiplies two numbers', () => { + const { a, b } = DEFAULT_VALUES; + const expected = a.times(b); + const actual = safeMul(a, b); + expect(actual).to.bignumber.eq(expected); + }); + + it('reverts if denominator is zero', () => { + const a = MAX_UINT256.dividedToIntegerBy(2); + const b = ZERO_AMOUNT; + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256DivisionByZero, + a, + b, + ); + expect(() => safeDiv(a, b)).to.throw(expectedError.message); + }); + }); + }); +}); From fddbfc2d32f14d7462b3de5ac0c0af7d60922bad Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 2 Aug 2019 16:30:51 -0400 Subject: [PATCH 36/48] Update CHANGELOGs --- contracts/exchange-libs/CHANGELOG.json | 8 ++++---- contracts/exchange/CHANGELOG.json | 10 +++++----- contracts/test-utils/CHANGELOG.json | 12 ++++++------ contracts/utils/CHANGELOG.json | 4 ++-- packages/dev-utils/CHANGELOG.json | 2 +- packages/types/CHANGELOG.json | 2 +- packages/utils/CHANGELOG.json | 2 +- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/contracts/exchange-libs/CHANGELOG.json b/contracts/exchange-libs/CHANGELOG.json index 4586bef220..28cde2a4a1 100644 --- a/contracts/exchange-libs/CHANGELOG.json +++ b/contracts/exchange-libs/CHANGELOG.json @@ -56,19 +56,19 @@ }, { "note": "Add reference functions for `LibMath` and `LibFillResults`", - "pr": "TODO" + "pr": 2031 }, { "note": "Move in revamped `LibMath` tests from the `contracts-exchange` package.", - "pr": "TODO" + "pr": 2031 }, { "note": "Move in revamped `LibFillResults` tests from the `contracts-exchange` package.", - "pr": "TODO" + "pr": 2031 }, { "note": "Remove unecessary zero-denominator checks in `LibMath`.", - "pr": "TODO" + "pr": 2031 } ] }, diff --git a/contracts/exchange/CHANGELOG.json b/contracts/exchange/CHANGELOG.json index 741bd115f7..8ec36624f4 100644 --- a/contracts/exchange/CHANGELOG.json +++ b/contracts/exchange/CHANGELOG.json @@ -120,23 +120,23 @@ }, { "note": "Add `TestIsolatedExchange` contract and `IsolatedExchangeWrapper` test class", - "pr": "TODO" + "pr": 2031 }, { "note": "Add `ReferenceFunctions` as package export.", - "pr": "TODO" + "pr": 2031 }, { "note": "Remove `TestExchangeMath.sol`. Exchange math functions are now tested in the `exchange-libs` package and reference implementations are available there as well.", - "pr": "TODO" + "pr": 2031 }, { "note": "Remove functions from `TestExchangeInternals.sol` that are no longer tested in this package.", - "pr": "TODO" + "pr": 2031 }, { "note": "Remove `_assertValidFill()`", - "pr": "TODO" + "pr": 2031 } ] }, diff --git a/contracts/test-utils/CHANGELOG.json b/contracts/test-utils/CHANGELOG.json index 3922291aec..e16993f703 100644 --- a/contracts/test-utils/CHANGELOG.json +++ b/contracts/test-utils/CHANGELOG.json @@ -40,27 +40,27 @@ }, { "note": "Move `*FillResults`, `OrderInfo` types to `@0x/types`", - "pr": "TODO" + "pr": 2031 }, { "note": "Add `log_utils.ts`", - "pr": "TODO" + "pr": 2031 }, { "note": "Add `haxRandom()` to `hex_utils.ts`", - "pr": "TODO" + "pr": 2031 }, { "note": "Add the constants: `MAX_UINT256`, `ADDRESS_LENGTH`, `MAX_UINT256_ROOT`, `ONE_ETHER`", - "pr": "TODO" + "pr": 2031 }, { "note": "Make `testCombinatoriallyWithReferenceFuncAsync` non-async", - "pr": "TODO" + "pr": 2031 }, { "note": "Update `testWithReferenceFuncAsync` to work with `RevertErrors`", - "pr": "TODO" + "pr": 2031 } ] }, diff --git a/contracts/utils/CHANGELOG.json b/contracts/utils/CHANGELOG.json index 8a8ec3f74e..ff9ced6e96 100644 --- a/contracts/utils/CHANGELOG.json +++ b/contracts/utils/CHANGELOG.json @@ -36,11 +36,11 @@ }, { "note": "Add reference functions for `SafeMath` functions.", - "pr": "2031" + "pr": 2031 }, { "note": "Throw a `SafeMathError` in `SafeMath._safeDiv()` when denominator is zero.", - "pr": "2031" + "pr": 2031 } ] }, diff --git a/packages/dev-utils/CHANGELOG.json b/packages/dev-utils/CHANGELOG.json index 64799c5bd6..c420baa714 100644 --- a/packages/dev-utils/CHANGELOG.json +++ b/packages/dev-utils/CHANGELOG.json @@ -4,7 +4,7 @@ "changes": [ { "note": "`revertWith` mocha extensions now accept Promise-like objects instead of just Promises", - "pr": "TODO" + "pr": 2031 } ] }, diff --git a/packages/types/CHANGELOG.json b/packages/types/CHANGELOG.json index 89d56b3594..2dda41a7a3 100644 --- a/packages/types/CHANGELOG.json +++ b/packages/types/CHANGELOG.json @@ -8,7 +8,7 @@ }, { "note": "Add `OrderInfo`, `FillResults`, `MatchedFillResults`, `BatchMatchedFillResults` types", - "pr": "TODO" + "pr": 2031 } ] }, diff --git a/packages/utils/CHANGELOG.json b/packages/utils/CHANGELOG.json index a6ab09776a..7ed47a6ffc 100644 --- a/packages/utils/CHANGELOG.json +++ b/packages/utils/CHANGELOG.json @@ -4,7 +4,7 @@ "changes": [ { "note": "Add `SafeMathRevertErrors.SafeMathErrorCodes.Uint256DivisionByZero`", - "pr": "TODO" + "pr": 2031 } ] }, From ad25942731182d245452c01f4234c1b74b23248a Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 2 Aug 2019 16:42:36 -0400 Subject: [PATCH 37/48] `@0x/dev-utils`: Appease the linter gods. --- packages/dev-utils/src/chai_revert_error.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev-utils/src/chai_revert_error.ts b/packages/dev-utils/src/chai_revert_error.ts index 47123875bb..a470ae3970 100644 --- a/packages/dev-utils/src/chai_revert_error.ts +++ b/packages/dev-utils/src/chai_revert_error.ts @@ -138,5 +138,5 @@ function assertIsPromiseLike(_chai: Chai, maybePromise: any): void { if (maybePromise.then instanceof Function && maybePromise.catch instanceof Function) { return; } - chaiFail(_chai, `Expected ${maybePromise} to be a promise`, new Promise(() => 1), maybePromise); + chaiFail(_chai, `Expected ${maybePromise} to be a promise`, Promise.resolve(), maybePromise); } From 3156f602dd606d4c9bdd332ab5432dc231011eaa Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 2 Aug 2019 17:14:10 -0400 Subject: [PATCH 38/48] Fix rebase errors --- contracts/test-utils/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/test-utils/src/constants.ts b/contracts/test-utils/src/constants.ts index 380f08929b..7a2740da2a 100644 --- a/contracts/test-utils/src/constants.ts +++ b/contracts/test-utils/src/constants.ts @@ -1,4 +1,4 @@ -import { BigNumber } from '@0x/util'; +import { BigNumber } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; From ac38390241ff03d26959f1e8b984aef2c63e0af8 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 2 Aug 2019 17:23:58 -0400 Subject: [PATCH 39/48] `@0x/contracts-utils`: Fix failing test due to rebase. --- contracts/utils/test/safe_math.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/contracts/utils/test/safe_math.ts b/contracts/utils/test/safe_math.ts index 51ac43662b..be712eaa37 100644 --- a/contracts/utils/test/safe_math.ts +++ b/contracts/utils/test/safe_math.ts @@ -77,10 +77,14 @@ describe('SafeMath', () => { }); it('should revert if second argument is zero', async () => { - const errMessage = 'VM Exception while processing transaction: invalid opcode'; - return expect(safeMath.externalSafeDiv.callAsync(toBigNumber(1), constants.ZERO_AMOUNT)).to.be.rejectedWith( - errMessage, + const a = toBigNumber(1); + const b = toBigNumber(0); + const expectedError = new SafeMathRevertErrors.SafeMathError( + SafeMathRevertErrors.SafeMathErrorCodes.Uint256DivisionByZero, + a, + b, ); + return expect(safeMath.externalSafeDiv.callAsync(a, b)).to.revertWith(expectedError); }); }); From 7eedfc201a2db97d2d2523160198da0d66e0f457 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 2 Aug 2019 17:34:27 -0400 Subject: [PATCH 40/48] `@0x/contracts-utils`: Add testing against reference functions in `SafeMath` unit tests. --- contracts/utils/test/safe_math.ts | 55 +++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/contracts/utils/test/safe_math.ts b/contracts/utils/test/safe_math.ts index be712eaa37..f8c4b7466d 100644 --- a/contracts/utils/test/safe_math.ts +++ b/contracts/utils/test/safe_math.ts @@ -1,33 +1,36 @@ -import { chaiSetup, constants, provider, txDefaults, web3Wrapper } from '@0x/contracts-test-utils'; -import { BlockchainLifecycle } from '@0x/dev-utils'; +import { blockchainTests, constants, describe, expect } from '@0x/contracts-test-utils'; import { BigNumber, SafeMathRevertErrors } from '@0x/utils'; -import * as chai from 'chai'; import * as _ from 'lodash'; import { artifacts, TestSafeMathContract } from '../src'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +import * as ReferenceFunctions from '../src/reference_functions'; function toBigNumber(a: number | string): BigNumber { return new BigNumber(a); } -describe('SafeMath', () => { +blockchainTests('SafeMath', env => { + const { ONE_ETHER } = constants; let safeMath: TestSafeMathContract; before(async () => { - await blockchainLifecycle.startAsync(); // Deploy SafeMath - safeMath = await TestSafeMathContract.deployFrom0xArtifactAsync(artifacts.TestSafeMath, provider, txDefaults); - }); - - after(async () => { - await blockchainLifecycle.revertAsync(); + safeMath = await TestSafeMathContract.deployFrom0xArtifactAsync( + artifacts.TestSafeMath, + env.provider, + env.txDefaults, + ); }); describe('_safeMul', () => { + it('should match the output of the reference function', async () => { + const a = ONE_ETHER; + const b = ONE_ETHER.times(2); + const expected = ReferenceFunctions.safeMul(a, b); + const actual = await safeMath.externalSafeMul.callAsync(a, b); + expect(actual).bignumber.to.be.eq(expected); + }); + it('should return zero if first argument is zero', async () => { const result = await safeMath.externalSafeMul.callAsync(constants.ZERO_AMOUNT, toBigNumber(1)); expect(result).bignumber.to.be.eq(constants.ZERO_AMOUNT); @@ -56,6 +59,14 @@ describe('SafeMath', () => { }); describe('_safeDiv', () => { + it('should match the output of the reference function', async () => { + const a = ONE_ETHER; + const b = ONE_ETHER.times(2); + const expected = ReferenceFunctions.safeDiv(a, b); + const actual = await safeMath.externalSafeDiv.callAsync(a, b); + expect(actual).bignumber.to.be.eq(expected); + }); + it('should return the correct value if both values are the same', async () => { const result = await safeMath.externalSafeDiv.callAsync(toBigNumber(1), toBigNumber(1)); expect(result).bignumber.to.be.eq(toBigNumber(1)); @@ -89,6 +100,14 @@ describe('SafeMath', () => { }); describe('_safeSub', () => { + it('should match the output of the reference function', async () => { + const a = ONE_ETHER; + const b = ONE_ETHER.dividedToIntegerBy(2); + const expected = ReferenceFunctions.safeSub(a, b); + const actual = await safeMath.externalSafeSub.callAsync(a, b); + expect(actual).bignumber.to.be.eq(expected); + }); + it('should revert if the subtraction underflows', async () => { const a = toBigNumber(0); const b = toBigNumber(1); @@ -112,6 +131,14 @@ describe('SafeMath', () => { }); describe('_safeAdd', () => { + it('should match the output of the reference function', async () => { + const a = ONE_ETHER; + const b = ONE_ETHER.dividedToIntegerBy(2); + const expected = ReferenceFunctions.safeAdd(a, b); + const actual = await safeMath.externalSafeAdd.callAsync(a, b); + expect(actual).bignumber.to.be.eq(expected); + }); + it('should revert if the addition overflows', async () => { const a = toBigNumber('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'); // The largest uint256 number const b = toBigNumber(1); From 7002dc63bd2e222326c004ef5603926dcb44e8b2 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 2 Aug 2019 21:22:15 -0400 Subject: [PATCH 41/48] `@0x/contracts-exchange`: Fix typos in comments. --- contracts/exchange/test/reference_functions.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/exchange/test/reference_functions.ts b/contracts/exchange/test/reference_functions.ts index 2ac0c5ea23..5e961d51c6 100644 --- a/contracts/exchange/test/reference_functions.ts +++ b/contracts/exchange/test/reference_functions.ts @@ -33,7 +33,7 @@ describe('Reference functions', () => { } it('reverts if computing `fillResults.makerAssetFilledAmount` overflows', () => { - // All values need to be large to ensure we don't trigger a RondingError. + // All values need to be large to ensure we don't trigger a RoundingError. const order = makeOrder({ makerAssetAmount: MAX_UINT256_ROOT.times(2), takerAssetAmount: MAX_UINT256_ROOT, @@ -48,7 +48,7 @@ describe('Reference functions', () => { }); it('reverts if computing `fillResults.makerFeePaid` overflows', () => { - // All values need to be large to ensure we don't trigger a RondingError. + // All values need to be large to ensure we don't trigger a RoundingError. const order = makeOrder({ makerAssetAmount: MAX_UINT256_ROOT, takerAssetAmount: MAX_UINT256_ROOT, @@ -69,7 +69,7 @@ describe('Reference functions', () => { }); it('reverts if computing `fillResults.takerFeePaid` overflows', () => { - // All values need to be large to ensure we don't trigger a RondingError. + // All values need to be large to ensure we don't trigger a RoundingError. const order = makeOrder({ makerAssetAmount: MAX_UINT256_ROOT, takerAssetAmount: MAX_UINT256_ROOT, From b6dfc791d482e06798284b9f8e7079be032a0990 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 2 Aug 2019 22:18:05 -0400 Subject: [PATCH 42/48] `@0x/contracts-exchange`: Correct test case name. --- contracts/exchange/test/fill_order.ts | 2 +- yarn.lock | 33 ++------------------------- 2 files changed, 3 insertions(+), 32 deletions(-) diff --git a/contracts/exchange/test/fill_order.ts b/contracts/exchange/test/fill_order.ts index 342b4014f9..dee68e6a57 100644 --- a/contracts/exchange/test/fill_order.ts +++ b/contracts/exchange/test/fill_order.ts @@ -189,7 +189,7 @@ blockchainTests.resets('FillOrder Tests', ({ web3Wrapper, txDefaults }) => { await fillOrderCombinatorialUtils.testFillOrderScenarioFailureAsync(fillScenario); }); - it('should throw if an order is expired', async () => { + it('should revert if an order is expired', async () => { const fillScenario = { ...defaultFillScenario, orderScenario: { diff --git a/yarn.lock b/yarn.lock index 69342ef56c..bc11be3614 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8653,7 +8653,8 @@ got@^6.7.1: graceful-fs@4.1.15, graceful-fs@^3.0.0, graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@~1.2.0: version "4.1.15" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" + integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== "graceful-readlink@>= 1.0.0": version "1.0.1" @@ -14531,16 +14532,6 @@ react-dom@^16.3.2: object-assign "^4.1.1" prop-types "^15.6.0" -react-dom@^16.4.2: - version "16.8.6" - resolved "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f" - integrity sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.13.6" - react-dom@^16.5.2: version "16.5.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7" @@ -14598,8 +14589,6 @@ react-highlight@0xproject/react-highlight#react-peer-deps: dependencies: highlight.js "^9.11.0" highlightjs-solidity "^1.0.5" - react "^16.4.2" - react-dom "^16.4.2" react-hot-loader@^4.3.3: version "4.3.4" @@ -14844,16 +14833,6 @@ react@^16.3.2: object-assign "^4.1.1" prop-types "^15.6.0" -react@^16.4.2: - version "16.8.6" - resolved "https://registry.npmjs.org/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe" - integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.13.6" - react@^16.5.2: version "16.5.2" resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42" @@ -15751,14 +15730,6 @@ schedule@^0.5.0: dependencies: object-assign "^4.1.1" -scheduler@^0.13.6: - version "0.13.6" - resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889" - integrity sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - schema-utils@^0.4.4: version "0.4.7" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187" From 0eff19f0ff6b7725b29a489150895febb5bcb594 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 5 Aug 2019 22:22:37 -0400 Subject: [PATCH 43/48] `@0x/contracts-test-utils`: Add `TransactionHelper` class. `@0x/contracts-test-utils`: Add `decodeReceiptLogs()` to `LogDecoder` class. --- contracts/test-utils/src/index.ts | 1 + contracts/test-utils/src/log_decoder.ts | 13 ++++- .../test-utils/src/transaction_helper.ts | 56 +++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 contracts/test-utils/src/transaction_helper.ts diff --git a/contracts/test-utils/src/index.ts b/contracts/test-utils/src/index.ts index 710c7b5c9e..ea1ff0839c 100644 --- a/contracts/test-utils/src/index.ts +++ b/contracts/test-utils/src/index.ts @@ -26,6 +26,7 @@ export { addressUtils } from './address_utils'; export { OrderFactory } from './order_factory'; export { bytes32Values, testCombinatoriallyWithReferenceFunc, uint256Values } from './combinatorial_utils'; export { TransactionFactory } from './transaction_factory'; +export { MutatorContractFunction, TransactionHelper } from './transaction_helper'; export { testWithReferenceFuncAsync } from './test_with_reference'; export { hexConcat, hexRandom } from './hex_utils'; export { diff --git a/contracts/test-utils/src/log_decoder.ts b/contracts/test-utils/src/log_decoder.ts index f380f5cef2..a49f29d26b 100644 --- a/contracts/test-utils/src/log_decoder.ts +++ b/contracts/test-utils/src/log_decoder.ts @@ -7,6 +7,7 @@ import { LogEntry, LogWithDecodedArgs, RawLog, + TransactionReceipt, TransactionReceiptWithDecodedLogs, } from 'ethereum-types'; import * as _ from 'lodash'; @@ -44,8 +45,14 @@ export class LogDecoder { return logWithDecodedArgsOrLog; } public async getTxWithDecodedLogsAsync(txHash: string): Promise { - const tx = await this._web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); - tx.logs = _.map(tx.logs, log => this.decodeLogOrThrow(log)); - return tx; + const receipt = await this._web3Wrapper.awaitTransactionSuccessAsync( + txHash, + constants.AWAIT_TRANSACTION_MINED_MS, + ); + return this.decodeReceiptLogs(receipt); + } + public decodeReceiptLogs(receipt: TransactionReceipt): TransactionReceiptWithDecodedLogs { + const decodedLogs = receipt.logs.map(log => this.decodeLogOrThrow(log)); + return _.merge({}, receipt, { logs: decodedLogs }); } } diff --git a/contracts/test-utils/src/transaction_helper.ts b/contracts/test-utils/src/transaction_helper.ts new file mode 100644 index 0000000000..e861e389f7 --- /dev/null +++ b/contracts/test-utils/src/transaction_helper.ts @@ -0,0 +1,56 @@ +import { Web3Wrapper } from '@0x/web3-wrapper'; +import { ContractArtifact, TransactionReceipt } from 'ethereum-types'; + +import { LogDecoder } from './log_decoder'; + +type AsyncFunction = (...args: TArgs) => Promise; + +interface ContractArtifacts { + [contractName: string]: ContractArtifact; +} + +export interface MutatorContractFunction< + TCallAsyncArgs extends any[], + TAwaitTransactionSuccessAsyncArgs extends any[], + TCallAsyncResult +> { + callAsync: AsyncFunction; + sendTransactionAsync: AsyncFunction; +} + +/** + * Helper class for performing non-constant contract functions calls. + */ +export class TransactionHelper { + public readonly logDecoder: LogDecoder; + + constructor(web3Wrapper: Web3Wrapper, artifacts: ContractArtifacts) { + this.logDecoder = new LogDecoder(web3Wrapper, artifacts); + } + + /** + * Call a non-constant contract function `contractFunction`, passing `args`. + * This will first perform an 'eth_call' (read-only) call in order to + * retrieve the return value, then a 'sendTransaction' to actually modify + * the state. Returns a tuple of the return value amd receipt, with decoded + * logs. + */ + public async getResultAndReceiptAsync< + TCallAsyncArgs extends any[], + TAwaitTransactionSuccessAsyncArgs extends any[], + TCallAsyncResult + >( + contractFunction: MutatorContractFunction, + // tslint:disable-next-line: trailing-comma + ...args: TAwaitTransactionSuccessAsyncArgs + ): Promise<[TCallAsyncResult, TransactionReceipt]> { + // HACK(dorothy-zbornak): We take advantage of the general rule that + // the parameters for `callAsync()` are a subset of the + // parameters for `sendTransactionAsync()`. + const result = await contractFunction.callAsync(...((args as any) as TCallAsyncArgs)); + const receipt = await this.logDecoder.getTxWithDecodedLogsAsync( + await contractFunction.sendTransactionAsync(...args), + ); + return [result, receipt]; + } +} From 6d502b6898cc6c1a63b9e74bcd65e2f635938406 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 5 Aug 2019 22:23:35 -0400 Subject: [PATCH 44/48] `@0x/contracts-exchange`: Use `TransactionHelper` to call and execute contract functions. --- .../test/utils/isolated_exchange_wrapper.ts | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts index 89a6a5b182..ce0069852b 100644 --- a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts +++ b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts @@ -1,4 +1,10 @@ -import { constants, filterLogsToArguments, LogDecoder, txDefaults as testTxDefaults } from '@0x/contracts-test-utils'; +import { + constants, + filterLogsToArguments, + MutatorContractFunction, + TransactionHelper, + txDefaults as testTxDefaults, +} from '@0x/contracts-test-utils'; import { orderHashUtils } from '@0x/order-utils'; import { FillResults, OrderInfo, OrderWithoutDomain, SignatureType } from '@0x/types'; import { BigNumber } from '@0x/utils'; @@ -34,8 +40,8 @@ export const DEFAULT_BAD_SIGNATURE = createBadSignature(); */ export class IsolatedExchangeWrapper { public static readonly CHAIN_ID = 1337; - public instance: IsolatedExchangeContract; - public logDecoder: LogDecoder; + public readonly instance: IsolatedExchangeContract; + public readonly txHelper: TransactionHelper; public lastTxEvents: IsolatedExchangeEvents = createEmptyEvents(); public lastTxBalanceChanges: AssetBalances = {}; @@ -64,7 +70,7 @@ export class IsolatedExchangeWrapper { public constructor(web3Wrapper: Web3Wrapper, instance: IsolatedExchangeContract) { this.instance = instance; - this.logDecoder = new LogDecoder(web3Wrapper, artifacts); + this.txHelper = new TransactionHelper(web3Wrapper, artifacts); } public async getTakerAssetFilledAmountAsync(order: Order): Promise { @@ -85,7 +91,7 @@ export class IsolatedExchangeWrapper { signature: string = DEFAULT_GOOD_SIGNATURE, txOpts?: TxData, ): Promise { - return this._callAndSendExchangeFunctionAsync( + return this._runFillContractFunctionAsync( this.instance.fillOrder, order, new BigNumber(takerAssetFillAmount), @@ -116,30 +122,24 @@ export class IsolatedExchangeWrapper { return constants.ZERO_AMOUNT; } - protected async _callAndSendExchangeFunctionAsync( - instanceMethod: TransactionContractFunction, + protected async _runFillContractFunctionAsync< + TCallAsyncArgs extends any[], + TAwaitTransactionSuccessAsyncArgs extends any[], + TResult + >( + contractFunction: MutatorContractFunction, // tslint:disable-next-line: trailing-comma - ...args: any[] + ...args: TAwaitTransactionSuccessAsyncArgs ): Promise { this.lastTxEvents = createEmptyEvents(); this.lastTxBalanceChanges = {}; - // Call to get the return value. - const result = await instanceMethod.callAsync(...args); - // Transact to execute it. - const receipt = await this.logDecoder.getTxWithDecodedLogsAsync( - await this.instance.fillOrder.sendTransactionAsync.call(this.instance, ...args), - ); + const [result, receipt] = await this.txHelper.getResultAndReceiptAsync(contractFunction, ...args); this.lastTxEvents = extractEvents(receipt.logs); this.lastTxBalanceChanges = getBalanceChangesFromTransferFromCalls(this.lastTxEvents.transferFromCalls); return result; } } -interface TransactionContractFunction { - callAsync: (...args: any[]) => Promise; - sendTransactionAsync: (...args: any[]) => Promise; -} - /** * Create a signature for the `IsolatedExchange` contract that will pass. */ From 08118ec36f08028da0743cdabce60784f4e3e899 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 8 Aug 2019 11:41:51 -0400 Subject: [PATCH 45/48] `@0x/contracts-exchange-libs`: Fix coverage hooks. `@0x/contracts-exchange-libs`: Add explicit tests for `*getPartialAmount*()` for rounding modes. --- contracts/exchange-libs/CHANGELOG.json | 4 +++ contracts/exchange-libs/package.json | 1 + contracts/exchange-libs/test/global_hooks.ts | 18 +++++++--- contracts/exchange-libs/test/lib_math.ts | 36 ++++++++++++++++++++ 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/contracts/exchange-libs/CHANGELOG.json b/contracts/exchange-libs/CHANGELOG.json index 28cde2a4a1..9e0b18542c 100644 --- a/contracts/exchange-libs/CHANGELOG.json +++ b/contracts/exchange-libs/CHANGELOG.json @@ -69,6 +69,10 @@ { "note": "Remove unecessary zero-denominator checks in `LibMath`.", "pr": 2031 + }, + { + "note": "Fix coverage hooks.", + "pr": 2031 } ] }, diff --git a/contracts/exchange-libs/package.json b/contracts/exchange-libs/package.json index 5e9b69a81f..cd8d1ec81c 100644 --- a/contracts/exchange-libs/package.json +++ b/contracts/exchange-libs/package.json @@ -53,6 +53,7 @@ "@0x/contracts-test-utils": "^3.1.10", "@0x/dev-utils": "^2.2.4", "@0x/sol-compiler": "^3.1.9", + "@0x/subproviders": "^4.1.1", "@0x/tslint-config": "^3.0.1", "@types/lodash": "4.14.104", "@types/node": "*", diff --git a/contracts/exchange-libs/test/global_hooks.ts b/contracts/exchange-libs/test/global_hooks.ts index 2ca47d433b..91d8693e15 100644 --- a/contracts/exchange-libs/test/global_hooks.ts +++ b/contracts/exchange-libs/test/global_hooks.ts @@ -1,18 +1,28 @@ -import { env, EnvVars } from '@0x/dev-utils'; - import { coverage, profiler, provider } from '@0x/contracts-test-utils'; +import { env, EnvVars } from '@0x/dev-utils'; +import { prependSubprovider } from '@0x/subproviders'; import { providerUtils } from '@0x/utils'; +const coverageSubprovider = coverage.getCoverageSubproviderSingleton(); +const profilerSubprovider = profiler.getProfilerSubproviderSingleton(); + +if (env.parseBoolean(EnvVars.SolidityCoverage)) { + prependSubprovider(provider, coverageSubprovider); + provider.stop(); +} +if (env.parseBoolean(EnvVars.SolidityProfiler)) { + prependSubprovider(provider, profilerSubprovider); + provider.stop(); +} + before('start web3 provider', () => { providerUtils.startProviderEngine(provider); }); after('generate coverage report', async () => { if (env.parseBoolean(EnvVars.SolidityCoverage)) { - const coverageSubprovider = coverage.getCoverageSubproviderSingleton(); await coverageSubprovider.writeCoverageAsync(); } if (env.parseBoolean(EnvVars.SolidityProfiler)) { - const profilerSubprovider = profiler.getProfilerSubproviderSingleton(); await profilerSubprovider.writeProfilerOutputAsync(); } provider.stop(); diff --git a/contracts/exchange-libs/test/lib_math.ts b/contracts/exchange-libs/test/lib_math.ts index 42060dec13..01204e0676 100644 --- a/contracts/exchange-libs/test/lib_math.ts +++ b/contracts/exchange-libs/test/lib_math.ts @@ -59,6 +59,15 @@ blockchainTests('LibMath', env => { expect(actual).to.bignumber.eq(expected); }); + it('rounds down when computing the partial amount', async () => { + const numerator = ONE_ETHER.times(0.6); + const denominator = ONE_ETHER.times(1.8); + const target = ONE_ETHER; + const expected = ONE_ETHER.dividedToIntegerBy(3); + const actual = await libsContract.getPartialAmountFloor.callAsync(numerator, denominator, target); + expect(actual).to.bignumber.eq(expected); + }); + it('reverts if `denominator` is zero', async () => { const numerator = ONE_ETHER; const denominator = ZERO_AMOUNT; @@ -109,6 +118,15 @@ blockchainTests('LibMath', env => { expect(actual).to.bignumber.eq(expected); }); + it('rounds up when computing the partial amount', async () => { + const numerator = ONE_ETHER.times(0.6); + const denominator = ONE_ETHER.times(1.8); + const target = ONE_ETHER; + const expected = ONE_ETHER.dividedToIntegerBy(3).plus(1); + const actual = await libsContract.getPartialAmountCeil.callAsync(numerator, denominator, target); + expect(actual).to.bignumber.eq(expected); + }); + it('reverts if `denominator` is zero', async () => { const numerator = ONE_ETHER; const denominator = ZERO_AMOUNT; @@ -160,6 +178,15 @@ blockchainTests('LibMath', env => { expect(actual).to.bignumber.eq(expected); }); + it('rounds down when computing the partial amount', async () => { + const numerator = ONE_ETHER.times(0.6); + const denominator = ONE_ETHER.times(1.8); + const target = ONE_ETHER; + const expected = ONE_ETHER.dividedToIntegerBy(3); + const actual = await libsContract.safeGetPartialAmountFloor.callAsync(numerator, denominator, target); + expect(actual).to.bignumber.eq(expected); + }); + it('reverts for a rounding error', async () => { const numerator = new BigNumber(1e3); const denominator = new BigNumber(1e4); @@ -216,6 +243,15 @@ blockchainTests('LibMath', env => { expect(actual).to.bignumber.eq(expected); }); + it('rounds up when computing the partial amount', async () => { + const numerator = ONE_ETHER.times(0.6); + const denominator = ONE_ETHER.times(1.8); + const target = ONE_ETHER; + const expected = ONE_ETHER.dividedToIntegerBy(3).plus(1); + const actual = await libsContract.safeGetPartialAmountCeil.callAsync(numerator, denominator, target); + expect(actual).to.bignumber.eq(expected); + }); + it('reverts for a rounding error', async () => { const numerator = new BigNumber(1e3); const denominator = new BigNumber(1e4); From de897d2ebfdcab699301c61affd8fc5e997332e8 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 8 Aug 2019 11:43:20 -0400 Subject: [PATCH 46/48] `@0x/contracts-test-utils`: Refactor `LogDecoder` slightly. --- contracts/test-utils/src/log_decoder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/test-utils/src/log_decoder.ts b/contracts/test-utils/src/log_decoder.ts index a49f29d26b..215e942381 100644 --- a/contracts/test-utils/src/log_decoder.ts +++ b/contracts/test-utils/src/log_decoder.ts @@ -52,7 +52,7 @@ export class LogDecoder { return this.decodeReceiptLogs(receipt); } public decodeReceiptLogs(receipt: TransactionReceipt): TransactionReceiptWithDecodedLogs { - const decodedLogs = receipt.logs.map(log => this.decodeLogOrThrow(log)); + const decodedLogs = (receipt.logs as LogEntry[]).map(log => this.decodeLogOrThrow(log)); return _.merge({}, receipt, { logs: decodedLogs }); } } From e3aa76cd09a1ce7c616560b2420392f2dc7323d1 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 8 Aug 2019 11:43:55 -0400 Subject: [PATCH 47/48] `@0x/contracts-exchange`: Light refactoring in `isolated_fill_order.ts` and `isolated_exchange_wrapper.ts`. --- contracts/exchange/test/isolated_fill_order.ts | 14 +++++++++----- .../test/utils/isolated_exchange_wrapper.ts | 3 +-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/contracts/exchange/test/isolated_fill_order.ts b/contracts/exchange/test/isolated_fill_order.ts index dde64c2c7f..31300fcac1 100644 --- a/contracts/exchange/test/isolated_fill_order.ts +++ b/contracts/exchange/test/isolated_fill_order.ts @@ -46,14 +46,18 @@ blockchainTests('Isolated fillOrder() tests', env => { before(async () => { [takerAddress, notTakerAddress] = await env.getAccountAddressesAsync(); - exchange = await IsolatedExchangeWrapper.deployAsync( - env.web3Wrapper, - _.assign(env.txDefaults, { from: takerAddress }), - ); + exchange = await IsolatedExchangeWrapper.deployAsync(env.web3Wrapper, { + ...env.txDefaults, + from: takerAddress, + }); }); function createOrder(details: Partial = {}): Order { - return _.assign({}, DEFAULT_ORDER, { salt: new BigNumber(nextSaltValue++) }, details); + return { + ...DEFAULT_ORDER, + salt: new BigNumber(nextSaltValue++), + ...details, + }; } interface FillOrderAndAssertResultsResults { diff --git a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts index ce0069852b..d1dfb22078 100644 --- a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts +++ b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts @@ -11,7 +11,6 @@ import { BigNumber } from '@0x/utils'; import { TxData, Web3Wrapper } from '@0x/web3-wrapper'; import * as crypto from 'crypto'; import { LogEntry } from 'ethereum-types'; -import * as _ from 'lodash'; import { artifacts, @@ -105,7 +104,7 @@ export class IsolatedExchangeWrapper { verifyingContractAddress: this.instance.address, chainId: IsolatedExchangeWrapper.CHAIN_ID, }; - return orderHashUtils.getOrderHashHex(_.assign(order, { domain })); + return orderHashUtils.getOrderHashHex({ ...order, domain }); } public async getOrderInfoAsync(order: Order): Promise { From 18485dd45670bdaa044768152a76a3eec725fc7c Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 9 Aug 2019 21:07:23 -0400 Subject: [PATCH 48/48] `@0x/contracts-exchange-libs`: Add complementary tests when rounding up and down behavior with `isRoundingErrorFloor()` and `isRoundingerrorCeil()`. --- contracts/exchange-libs/test/lib_math.ts | 32 +++++++++---------- .../exchange-libs/test/reference_functions.ts | 32 +++++++++---------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/contracts/exchange-libs/test/lib_math.ts b/contracts/exchange-libs/test/lib_math.ts index 01204e0676..a25d23fa6f 100644 --- a/contracts/exchange-libs/test/lib_math.ts +++ b/contracts/exchange-libs/test/lib_math.ts @@ -299,19 +299,19 @@ blockchainTests('LibMath', env => { }); describe('explicit tests', () => { - it('returns true for a rounding error', async () => { - const numerator = new BigNumber(1e3); - const denominator = new BigNumber(1e4); - const target = new BigNumber(333); + it('returns true when `numerator * target / denominator` produces an error >= 0.1%', async () => { + const numerator = new BigNumber(100); + const denominator = new BigNumber(102); + const target = new BigNumber(52); // tslint:disable-next-line: boolean-naming const actual = await libsContract.isRoundingErrorFloor.callAsync(numerator, denominator, target); expect(actual).to.eq(true); }); - it('returns false for not a rounding error', async () => { - const numerator = new BigNumber(1e3); - const denominator = new BigNumber(1e4); - const target = new BigNumber(5e2); + it('returns false when `numerator * target / denominator` produces an error < 0.1%', async () => { + const numerator = new BigNumber(100); + const denominator = new BigNumber(101); + const target = new BigNumber(92); // tslint:disable-next-line: boolean-naming const actual = await libsContract.isRoundingErrorFloor.callAsync(numerator, denominator, target); expect(actual).to.eq(false); @@ -365,19 +365,19 @@ blockchainTests('LibMath', env => { }); describe('explicit tests', () => { - it('returns true for a rounding error', async () => { - const numerator = new BigNumber(1e3); - const denominator = new BigNumber(1e4); - const target = new BigNumber(333); + it('returns true when `numerator * target / (denominator - 1)` produces an error >= 0.1%', async () => { + const numerator = new BigNumber(100); + const denominator = new BigNumber(101); + const target = new BigNumber(92); // tslint:disable-next-line: boolean-naming const actual = await libsContract.isRoundingErrorCeil.callAsync(numerator, denominator, target); expect(actual).to.eq(true); }); - it('returns false for not a rounding error', async () => { - const numerator = new BigNumber(1e3); - const denominator = new BigNumber(1e4); - const target = new BigNumber(5e2); + it('returns false when `numerator * target / (denominator - 1)` produces an error < 0.1%', async () => { + const numerator = new BigNumber(100); + const denominator = new BigNumber(102); + const target = new BigNumber(52); // tslint:disable-next-line: boolean-naming const actual = await libsContract.isRoundingErrorCeil.callAsync(numerator, denominator, target); expect(actual).to.eq(false); diff --git a/contracts/exchange-libs/test/reference_functions.ts b/contracts/exchange-libs/test/reference_functions.ts index d6c519b9d8..623582f884 100644 --- a/contracts/exchange-libs/test/reference_functions.ts +++ b/contracts/exchange-libs/test/reference_functions.ts @@ -222,19 +222,19 @@ describe('Reference Functions', () => { describe('isRoundingErrorFloor', () => { describe('explicit tests', () => { - it('returns true for a rounding error', () => { - const numerator = new BigNumber(1e3); - const denominator = new BigNumber(1e4); - const target = new BigNumber(333); + it('returns true when `numerator * target / denominator` produces an error >= 0.1%', async () => { + const numerator = new BigNumber(100); + const denominator = new BigNumber(102); + const target = new BigNumber(52); // tslint:disable-next-line: boolean-naming const actual = isRoundingErrorFloor(numerator, denominator, target); expect(actual).to.eq(true); }); - it('returns false for not a rounding error', () => { - const numerator = new BigNumber(1e3); - const denominator = new BigNumber(1e4); - const target = new BigNumber(5e2); + it('returns false when `numerator * target / denominator` produces an error < 0.1%', async () => { + const numerator = new BigNumber(100); + const denominator = new BigNumber(101); + const target = new BigNumber(92); // tslint:disable-next-line: boolean-naming const actual = isRoundingErrorFloor(numerator, denominator, target); expect(actual).to.eq(false); @@ -268,19 +268,19 @@ describe('Reference Functions', () => { describe('isRoundingErrorCeil', () => { describe('explicit tests', () => { - it('returns true for a rounding error', () => { - const numerator = new BigNumber(1e3); - const denominator = new BigNumber(1e4); - const target = new BigNumber(333); + it('returns true when `numerator * target / (denominator - 1)` produces an error >= 0.1%', async () => { + const numerator = new BigNumber(100); + const denominator = new BigNumber(101); + const target = new BigNumber(92); // tslint:disable-next-line: boolean-naming const actual = isRoundingErrorCeil(numerator, denominator, target); expect(actual).to.eq(true); }); - it('returns false for not a rounding error', () => { - const numerator = new BigNumber(1e3); - const denominator = new BigNumber(1e4); - const target = new BigNumber(5e2); + it('returns false when `numerator * target / (denominator - 1)` produces an error < 0.1%', async () => { + const numerator = new BigNumber(100); + const denominator = new BigNumber(102); + const target = new BigNumber(52); // tslint:disable-next-line: boolean-naming const actual = isRoundingErrorCeil(numerator, denominator, target); expect(actual).to.eq(false);