aboutsummaryrefslogtreecommitdiffstats
path: root/packages/contracts/test/exchange/internal.ts
diff options
context:
space:
mode:
authorAlex Browne <stephenalexbrowne@gmail.com>2018-07-27 13:09:55 +0800
committerGitHub <noreply@github.com>2018-07-27 13:09:55 +0800
commit554d5f97df55779beed35ef73214e80b386d7927 (patch)
treeeaf73ce6cbd61d96d541c08d7e2d640f02f32083 /packages/contracts/test/exchange/internal.ts
parent95c627f581ec8d724a0c737cbbf5b53b96765f6e (diff)
downloaddexon-sol-tools-554d5f97df55779beed35ef73214e80b386d7927.tar
dexon-sol-tools-554d5f97df55779beed35ef73214e80b386d7927.tar.gz
dexon-sol-tools-554d5f97df55779beed35ef73214e80b386d7927.tar.bz2
dexon-sol-tools-554d5f97df55779beed35ef73214e80b386d7927.tar.lz
dexon-sol-tools-554d5f97df55779beed35ef73214e80b386d7927.tar.xz
dexon-sol-tools-554d5f97df55779beed35ef73214e80b386d7927.tar.zst
dexon-sol-tools-554d5f97df55779beed35ef73214e80b386d7927.zip
Add combinatorial tests for internal Exchange functions (#807)
* WIP add combinatorial tests for internal Exchange functions * Change combinitorial testing strategy based on feedback * Check value of filled[orderHash] in updateFilledState tests * Add combinatorial tests for addFillResults * Add combinatorial tests for getPartialAmount * Implement generic `testWithReferenceFuncAsync` * Implement generic `testCombinatoriallyWithReferenceFuncAsync` * Add combinatorial tests for isRoundingError * Add combinatorial tests for calculateFillResults * Add support for Geth in internal contract tests * Fix contract artifacts * Change DECIMAL_PLACES to 78 and add a note. * Document new functions in utils * Optimize tests by only reseting state when needed * Rename/move some files * Print parameter names on failure in testWithReferenceFuncAsync * Add to changelog for utils package * Appease various linters * Rename some more things related to FillOrderCombinatorialUtils * Remove .only from test/exchange/internal.ts * Remove old test for isRoundingError and getPartialAmount * Appease linters again * Remove old todos * Fix typos, add comments, rename some things * Re-add some LibMath tests * Update contract internal tests to use new SafeMath revert reasons * Apply PR feedback from Amir * Apply PR feedback from Remco * Re-add networks to ZRXToken artifact * Remove duplicate Whitelist in compiler.json
Diffstat (limited to 'packages/contracts/test/exchange/internal.ts')
-rw-r--r--packages/contracts/test/exchange/internal.ts305
1 files changed, 305 insertions, 0 deletions
diff --git a/packages/contracts/test/exchange/internal.ts b/packages/contracts/test/exchange/internal.ts
new file mode 100644
index 000000000..67d1d2d2c
--- /dev/null
+++ b/packages/contracts/test/exchange/internal.ts
@@ -0,0 +1,305 @@
+import { BlockchainLifecycle } from '@0xproject/dev-utils';
+import { Order, RevertReason, SignedOrder } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import * as _ from 'lodash';
+
+import { TestExchangeInternalsContract } from '../../generated_contract_wrappers/test_exchange_internals';
+import { artifacts } from '../utils/artifacts';
+import {
+ getInvalidOpcodeErrorMessageForCallAsync,
+ getRevertReasonOrErrorMessageForSendTransactionAsync,
+} from '../utils/assertions';
+import { chaiSetup } from '../utils/chai_setup';
+import { bytes32Values, testCombinatoriallyWithReferenceFuncAsync, uint256Values } from '../utils/combinatorial_utils';
+import { constants } from '../utils/constants';
+import { FillResults } from '../utils/types';
+import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper';
+
+chaiSetup.configure();
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+
+const MAX_UINT256 = new BigNumber(2).pow(256).minus(1);
+
+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',
+ salt: new BigNumber(0),
+ exchangeAddress: constants.NULL_ADDRESS,
+ feeRecipientAddress: constants.NULL_ADDRESS,
+ expirationTimeSeconds: new BigNumber(0),
+};
+
+const emptySignedOrder: SignedOrder = {
+ ...emptyOrder,
+ signature: '',
+};
+
+const overflowErrorForCall = new Error(RevertReason.Uint256Overflow);
+
+async function referenceGetPartialAmountAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+): Promise<BigNumber> {
+ const invalidOpcodeErrorForCall = new Error(await getInvalidOpcodeErrorMessageForCallAsync());
+ const product = numerator.mul(target);
+ if (product.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ if (denominator.eq(0)) {
+ throw invalidOpcodeErrorForCall;
+ }
+ return product.dividedToIntegerBy(denominator);
+}
+
+describe('Exchange core internal functions', () => {
+ let testExchange: TestExchangeInternalsContract;
+ let invalidOpcodeErrorForCall: Error | undefined;
+ let overflowErrorForSendTransaction: Error | undefined;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ testExchange = await TestExchangeInternalsContract.deployFrom0xArtifactAsync(
+ artifacts.TestExchangeInternals,
+ provider,
+ txDefaults,
+ );
+ overflowErrorForSendTransaction = new Error(
+ await getRevertReasonOrErrorMessageForSendTransactionAsync(RevertReason.Uint256Overflow),
+ );
+ invalidOpcodeErrorForCall = new Error(await getInvalidOpcodeErrorMessageForCallAsync());
+ });
+ // Note(albrow): Don't forget to add beforeEach and afterEach calls to reset
+ // the blockchain state for any tests which modify it!
+
+ 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<FillResults> {
+ // 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.add(singleVal);
+ if (newTotal.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ return newTotal;
+ },
+ );
+ }
+ async function testAddFillResultsAsync(totalValue: BigNumber, singleValue: BigNumber): Promise<FillResults> {
+ const totalFillResults = makeFillResults(totalValue);
+ const singleFillResults = makeFillResults(singleValue);
+ return testExchange.publicAddFillResults.callAsync(totalFillResults, singleFillResults);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'addFillResults',
+ referenceAddFillResultsAsync,
+ testAddFillResultsAsync,
+ [uint256Values, uint256Values],
+ );
+ });
+
+ describe('calculateFillResults', async () => {
+ function makeOrder(
+ makerAssetAmount: BigNumber,
+ takerAssetAmount: BigNumber,
+ makerFee: BigNumber,
+ takerFee: BigNumber,
+ ): Order {
+ return {
+ ...emptyOrder,
+ makerAssetAmount,
+ takerAssetAmount,
+ makerFee,
+ takerFee,
+ };
+ }
+ async function referenceCalculateFillResultsAsync(
+ orderTakerAssetAmount: BigNumber,
+ takerAssetFilledAmount: BigNumber,
+ otherAmount: BigNumber,
+ ): Promise<FillResults> {
+ // 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 {
+ makerAssetFilledAmount: await referenceGetPartialAmountAsync(
+ takerAssetFilledAmount,
+ orderTakerAssetAmount,
+ otherAmount,
+ ),
+ takerAssetFilledAmount,
+ makerFeePaid: await referenceGetPartialAmountAsync(
+ takerAssetFilledAmount,
+ orderTakerAssetAmount,
+ otherAmount,
+ ),
+ takerFeePaid: await referenceGetPartialAmountAsync(
+ takerAssetFilledAmount,
+ orderTakerAssetAmount,
+ otherAmount,
+ ),
+ };
+ }
+ async function testCalculateFillResultsAsync(
+ orderTakerAssetAmount: BigNumber,
+ takerAssetFilledAmount: BigNumber,
+ otherAmount: BigNumber,
+ ): Promise<FillResults> {
+ const order = makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount);
+ return testExchange.publicCalculateFillResults.callAsync(order, takerAssetFilledAmount);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'calculateFillResults',
+ referenceCalculateFillResultsAsync,
+ testCalculateFillResultsAsync,
+ [uint256Values, uint256Values, uint256Values],
+ );
+ });
+
+ describe('getPartialAmount', async () => {
+ async function testGetPartialAmountAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<BigNumber> {
+ return testExchange.publicGetPartialAmount.callAsync(numerator, denominator, target);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'getPartialAmount',
+ referenceGetPartialAmountAsync,
+ testGetPartialAmountAsync,
+ [uint256Values, uint256Values, uint256Values],
+ );
+ });
+
+ describe('isRoundingError', async () => {
+ async function referenceIsRoundingErrorAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<boolean> {
+ const product = numerator.mul(target);
+ if (denominator.eq(0)) {
+ throw invalidOpcodeErrorForCall;
+ }
+ const remainder = product.mod(denominator);
+ if (remainder.eq(0)) {
+ return false;
+ }
+ if (product.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ if (product.eq(0)) {
+ throw invalidOpcodeErrorForCall;
+ }
+ const remainderTimes1000000 = remainder.mul('1000000');
+ if (remainderTimes1000000.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ const errPercentageTimes1000000 = remainderTimes1000000.dividedToIntegerBy(product);
+ return errPercentageTimes1000000.greaterThan('1000');
+ }
+ async function testIsRoundingErrorAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<boolean> {
+ return testExchange.publicIsRoundingError.callAsync(numerator, denominator, target);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'isRoundingError',
+ referenceIsRoundingErrorAsync,
+ testIsRoundingErrorAsync,
+ [uint256Values, uint256Values, uint256Values],
+ );
+ });
+
+ 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();
+ });
+ async function referenceUpdateFilledStateAsync(
+ takerAssetFilledAmount: BigNumber,
+ orderTakerAssetFilledAmount: BigNumber,
+ // tslint:disable-next-line:no-unused-variable
+ orderHash: string,
+ ): Promise<BigNumber> {
+ const totalFilledAmount = takerAssetFilledAmount.add(orderTakerAssetFilledAmount);
+ if (totalFilledAmount.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForSendTransaction;
+ }
+ return totalFilledAmount;
+ }
+ async function testUpdateFilledStateAsync(
+ takerAssetFilledAmount: BigNumber,
+ orderTakerAssetFilledAmount: BigNumber,
+ orderHash: string,
+ ): Promise<BigNumber> {
+ const fillResults = {
+ makerAssetFilledAmount: new BigNumber(0),
+ takerAssetFilledAmount,
+ makerFeePaid: new BigNumber(0),
+ takerFeePaid: new BigNumber(0),
+ };
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await testExchange.publicUpdateFilledState.sendTransactionAsync(
+ emptySignedOrder,
+ constants.NULL_ADDRESS,
+ orderHash,
+ orderTakerAssetFilledAmount,
+ fillResults,
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ return testExchange.filled.callAsync(orderHash);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'updateFilledState',
+ referenceUpdateFilledStateAsync,
+ testUpdateFilledStateAsync,
+ [uint256Values, uint256Values, bytes32Values],
+ );
+ });
+});