import { BlockchainLifecycle } from '@0xproject/dev-utils'; import { Order, RevertReason, SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import * as chai from 'chai'; import * as _ from 'lodash'; import { TestExchangeInternalsContract } from '../../generated_contract_wrappers/test_exchange_internals'; import { artifacts } from '../utils/artifacts'; import { 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 expect = chai.expect; 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); describe('Exchange core internal functions', () => { let testExchange: TestExchangeInternalsContract; let overflowErrorForSendTransaction: Error | undefined; let divisionByZeroErrorForCall: Error | undefined; let roundingErrorForCall: 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), ); divisionByZeroErrorForCall = new Error(RevertReason.DivisionByZero); roundingErrorForCall = new Error(RevertReason.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, 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.mul(target); const remainder = product.mod(denominator); const remainderTimes1000 = remainder.mul('1000'); const isError = remainderTimes1000.gte(product); if (product.greaterThan(MAX_UINT256)) { throw overflowErrorForCall; } if (remainderTimes1000.greaterThan(MAX_UINT256)) { throw overflowErrorForCall; } 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.mul(target); const remainder = product.mod(denominator); const error = denominator.sub(remainder).mod(denominator); const errorTimes1000 = error.mul('1000'); const isError = errorTimes1000.gte(product); if (product.greaterThan(MAX_UINT256)) { throw overflowErrorForCall; } if (errorTimes1000.greaterThan(MAX_UINT256)) { throw overflowErrorForCall; } 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.mul(target); if (product.greaterThan(MAX_UINT256)) { throw overflowErrorForCall; } return product.dividedToIntegerBy(denominator); } 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.add(singleVal); if (newTotal.greaterThan(MAX_UINT256)) { throw overflowErrorForCall; } return newTotal; }, ); } async function testAddFillResultsAsync(totalValue: BigNumber, singleValue: BigNumber): Promise { 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 { // 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. const makerAssetFilledAmount = await referenceSafeGetPartialAmountFloorAsync( 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, otherAmount: BigNumber, ): Promise { const order = makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount); return testExchange.publicCalculateFillResults.callAsync(order, takerAssetFilledAmount); } await testCombinatoriallyWithReferenceFuncAsync( 'calculateFillResults', referenceCalculateFillResultsAsync, testCalculateFillResultsAsync, [uint256Values, uint256Values, uint256Values], ); }); describe('getPartialAmountFloor', async () => { async function referenceGetPartialAmountFloorAsync( numerator: BigNumber, denominator: BigNumber, target: BigNumber, ): Promise { if (denominator.eq(0)) { throw divisionByZeroErrorForCall; } const product = numerator.mul(target); if (product.greaterThan(MAX_UINT256)) { throw overflowErrorForCall; } return product.dividedToIntegerBy(denominator); } async function testGetPartialAmountFloorAsync( numerator: BigNumber, denominator: BigNumber, target: BigNumber, ): Promise { return testExchange.publicGetPartialAmountFloor.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.mul(target); const offset = product.add(denominator.sub(1)); if (offset.greaterThan(MAX_UINT256)) { throw overflowErrorForCall; } const result = offset.dividedToIntegerBy(denominator); if (product.mod(denominator).eq(0)) { expect(result.mul(denominator)).to.be.bignumber.eq(product); } else { expect(result.mul(denominator)).to.be.bignumber.gt(product); } return result; } async function testGetPartialAmountCeilAsync( numerator: BigNumber, denominator: BigNumber, target: BigNumber, ): Promise { return testExchange.publicGetPartialAmountCeil.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.publicSafeGetPartialAmountFloor.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.mul(target); const offset = product.add(denominator.sub(1)); if (offset.greaterThan(MAX_UINT256)) { throw overflowErrorForCall; } const result = offset.dividedToIntegerBy(denominator); if (product.mod(denominator).eq(0)) { expect(result.mul(denominator)).to.be.bignumber.eq(product); } else { expect(result.mul(denominator)).to.be.bignumber.gt(product); } return result; } async function testSafeGetPartialAmountCeilAsync( numerator: BigNumber, denominator: BigNumber, target: BigNumber, ): Promise { return testExchange.publicSafeGetPartialAmountCeil.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.publicIsRoundingErrorFloor.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.publicIsRoundingErrorCeil.callAsync(numerator, denominator, target); } await testCombinatoriallyWithReferenceFuncAsync( 'isRoundingErrorCeil', referenceIsRoundingErrorCeilAsync, testIsRoundingErrorCeilAsync, [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 { const totalFilledAmount = takerAssetFilledAmount.add(orderTakerAssetFilledAmount); if (totalFilledAmount.greaterThan(MAX_UINT256)) { throw overflowErrorForSendTransaction; } return totalFilledAmount; } async function testUpdateFilledStateAsync( takerAssetFilledAmount: BigNumber, 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.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], ); }); });