472 lines
18 KiB
TypeScript
472 lines
18 KiB
TypeScript
import {
|
|
bytes32Values,
|
|
chaiSetup,
|
|
constants,
|
|
FillResults,
|
|
getRevertReasonOrErrorMessageForSendTransactionAsync,
|
|
provider,
|
|
testCombinatoriallyWithReferenceFuncAsync,
|
|
txDefaults,
|
|
uint256Values,
|
|
web3Wrapper,
|
|
} from '@0x/contracts-test-utils';
|
|
import { BlockchainLifecycle } from '@0x/dev-utils';
|
|
import { Order, RevertReason, SignedOrder } from '@0x/types';
|
|
import { BigNumber } from '@0x/utils';
|
|
import * as chai from 'chai';
|
|
import * as _ from 'lodash';
|
|
|
|
import { artifacts, TestExchangeInternalsContract } from '../src';
|
|
|
|
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<boolean> {
|
|
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 overflowErrorForCall;
|
|
}
|
|
if (remainderTimes1000.isGreaterThan(MAX_UINT256)) {
|
|
throw overflowErrorForCall;
|
|
}
|
|
return isError;
|
|
}
|
|
|
|
async function referenceIsRoundingErrorCeilAsync(
|
|
numerator: BigNumber,
|
|
denominator: BigNumber,
|
|
target: BigNumber,
|
|
): Promise<boolean> {
|
|
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 overflowErrorForCall;
|
|
}
|
|
if (errorTimes1000.isGreaterThan(MAX_UINT256)) {
|
|
throw overflowErrorForCall;
|
|
}
|
|
return isError;
|
|
}
|
|
|
|
async function referenceSafeGetPartialAmountFloorAsync(
|
|
numerator: BigNumber,
|
|
denominator: BigNumber,
|
|
target: BigNumber,
|
|
): Promise<BigNumber> {
|
|
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 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<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.plus(singleVal);
|
|
if (newTotal.isGreaterThan(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.
|
|
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<FillResults> {
|
|
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<BigNumber> {
|
|
if (denominator.eq(0)) {
|
|
throw divisionByZeroErrorForCall;
|
|
}
|
|
const product = numerator.multipliedBy(target);
|
|
if (product.isGreaterThan(MAX_UINT256)) {
|
|
throw overflowErrorForCall;
|
|
}
|
|
return product.dividedToIntegerBy(denominator);
|
|
}
|
|
async function testGetPartialAmountFloorAsync(
|
|
numerator: BigNumber,
|
|
denominator: BigNumber,
|
|
target: BigNumber,
|
|
): Promise<BigNumber> {
|
|
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<BigNumber> {
|
|
if (denominator.eq(0)) {
|
|
throw divisionByZeroErrorForCall;
|
|
}
|
|
const product = numerator.multipliedBy(target);
|
|
const offset = product.plus(denominator.minus(1));
|
|
if (offset.isGreaterThan(MAX_UINT256)) {
|
|
throw overflowErrorForCall;
|
|
}
|
|
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<BigNumber> {
|
|
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<BigNumber> {
|
|
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<BigNumber> {
|
|
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 overflowErrorForCall;
|
|
}
|
|
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<BigNumber> {
|
|
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<boolean> {
|
|
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<boolean> {
|
|
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<BigNumber> {
|
|
const totalFilledAmount = takerAssetFilledAmount.plus(orderTakerAssetFilledAmount);
|
|
if (totalFilledAmount.isGreaterThan(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],
|
|
);
|
|
});
|
|
});
|