Merge pull request #219 from 0xProject/feature/rounding-validation
Rounding validation
This commit is contained in:
@@ -490,6 +490,7 @@ export interface OrderRelevantState {
|
||||
filledTakerTokenAmount: BigNumber;
|
||||
cancelledTakerTokenAmount: BigNumber;
|
||||
remainingFillableMakerTokenAmount: BigNumber;
|
||||
remainingFillableTakerTokenAmount: BigNumber;
|
||||
}
|
||||
|
||||
export interface OrderStateValid {
|
||||
|
@@ -18,6 +18,8 @@ import {constants} from '../utils/constants';
|
||||
import {OrderFilledCancelledLazyStore} from '../stores/order_filled_cancelled_lazy_store';
|
||||
import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store';
|
||||
|
||||
const ACCEPTABLE_RELATIVE_ROUNDING_ERROR = 0.0001;
|
||||
|
||||
export class OrderStateUtils {
|
||||
private balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore;
|
||||
private orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore;
|
||||
@@ -78,6 +80,9 @@ export class OrderStateUtils {
|
||||
.dividedToIntegerBy(totalTakerTokenAmount);
|
||||
const fillableMakerTokenAmount = BigNumber.min([makerProxyAllowance, makerBalance]);
|
||||
const remainingFillableMakerTokenAmount = BigNumber.min(fillableMakerTokenAmount, remainingMakerTokenAmount);
|
||||
const remainingFillableTakerTokenAmount = remainingFillableMakerTokenAmount
|
||||
.times(totalTakerTokenAmount)
|
||||
.dividedToIntegerBy(totalMakerTokenAmount);
|
||||
// TODO: Handle edge case where maker token is ZRX with fee
|
||||
const orderRelevantState = {
|
||||
makerBalance,
|
||||
@@ -87,6 +92,7 @@ export class OrderStateUtils {
|
||||
filledTakerTokenAmount,
|
||||
cancelledTakerTokenAmount,
|
||||
remainingFillableMakerTokenAmount,
|
||||
remainingFillableTakerTokenAmount,
|
||||
};
|
||||
return orderRelevantState;
|
||||
}
|
||||
@@ -113,6 +119,13 @@ export class OrderStateUtils {
|
||||
throw new Error(ExchangeContractErrs.InsufficientMakerFeeAllowance);
|
||||
}
|
||||
}
|
||||
const minFillableTakerTokenAmountWithinNoRoundingErrorRange = signedOrder.takerTokenAmount
|
||||
.dividedBy(ACCEPTABLE_RELATIVE_ROUNDING_ERROR)
|
||||
.dividedBy(signedOrder.makerTokenAmount);
|
||||
if (orderRelevantState.remainingFillableTakerTokenAmount
|
||||
.lessThan(minFillableTakerTokenAmountWithinNoRoundingErrorRange)) {
|
||||
throw new Error(ExchangeContractErrs.OrderFillRoundingError);
|
||||
}
|
||||
// TODO Add linear function solver when maker token is ZRX #badass
|
||||
// Return the max amount that's fillable
|
||||
}
|
||||
|
@@ -47,7 +47,7 @@ describe('OrderStateWatcher', () => {
|
||||
let taker: string;
|
||||
let web3Wrapper: Web3Wrapper;
|
||||
let signedOrder: SignedOrder;
|
||||
const fillableAmount = new BigNumber(5);
|
||||
const fillableAmount = ZeroEx.toBaseUnitAmount(new BigNumber(5), 18);
|
||||
before(async () => {
|
||||
web3 = web3Factory.create();
|
||||
zeroEx = new ZeroEx(web3.currentProvider);
|
||||
@@ -221,6 +221,8 @@ describe('OrderStateWatcher', () => {
|
||||
const remainingFillable = fillableAmount.minus(fillAmountInBaseUnits);
|
||||
expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
|
||||
remainingFillable);
|
||||
expect(orderRelevantState.remainingFillableTakerTokenAmount).to.be.bignumber.equal(
|
||||
remainingFillable);
|
||||
expect(orderRelevantState.makerBalance).to.be.bignumber.equal(remainingMakerBalance);
|
||||
if (eventCount === 2) {
|
||||
done();
|
||||
@@ -233,84 +235,92 @@ describe('OrderStateWatcher', () => {
|
||||
);
|
||||
})().catch(done);
|
||||
});
|
||||
describe('remainingFillableMakerTokenAmount', () => {
|
||||
it('should calculate correct remaining fillable', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
const takerFillableAmount = new BigNumber(10);
|
||||
const makerFillableAmount = new BigNumber(20);
|
||||
signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, makerFillableAmount, takerFillableAmount);
|
||||
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
||||
const takerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, taker);
|
||||
const fillAmountInBaseUnits = new BigNumber(2);
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
zeroEx.orderStateWatcher.addOrder(signedOrder);
|
||||
let eventCount = 0;
|
||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||
eventCount++;
|
||||
expect(orderState.isValid).to.be.true();
|
||||
const validOrderState = orderState as OrderStateValid;
|
||||
expect(validOrderState.orderHash).to.be.equal(orderHash);
|
||||
const orderRelevantState = validOrderState.orderRelevantState;
|
||||
expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
|
||||
new BigNumber(16));
|
||||
if (eventCount === 2) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
zeroEx.orderStateWatcher.subscribe(callback);
|
||||
const shouldThrowOnInsufficientBalanceOrAllowance = true;
|
||||
await zeroEx.exchange.fillOrderAsync(
|
||||
signedOrder, fillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, taker,
|
||||
);
|
||||
})().catch(done);
|
||||
});
|
||||
it('should equal approved amount when approved amount is lowest', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, fillableAmount,
|
||||
);
|
||||
describe('remainingFillable(M|T)akerTokenAmount', () => {
|
||||
it('should calculate correct remaining fillable', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
const takerFillableAmount = ZeroEx.toBaseUnitAmount(new BigNumber(10), 18);
|
||||
const makerFillableAmount = ZeroEx.toBaseUnitAmount(new BigNumber(20), 18);
|
||||
signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, makerFillableAmount,
|
||||
takerFillableAmount,
|
||||
);
|
||||
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
||||
const takerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, taker);
|
||||
const fillAmountInBaseUnits = ZeroEx.toBaseUnitAmount(new BigNumber(2), 18);
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
zeroEx.orderStateWatcher.addOrder(signedOrder);
|
||||
let eventCount = 0;
|
||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||
eventCount++;
|
||||
expect(orderState.isValid).to.be.true();
|
||||
const validOrderState = orderState as OrderStateValid;
|
||||
expect(validOrderState.orderHash).to.be.equal(orderHash);
|
||||
const orderRelevantState = validOrderState.orderRelevantState;
|
||||
expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
|
||||
ZeroEx.toBaseUnitAmount(new BigNumber(16), 18));
|
||||
expect(orderRelevantState.remainingFillableTakerTokenAmount).to.be.bignumber.equal(
|
||||
ZeroEx.toBaseUnitAmount(new BigNumber(8), 18));
|
||||
if (eventCount === 2) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
zeroEx.orderStateWatcher.subscribe(callback);
|
||||
const shouldThrowOnInsufficientBalanceOrAllowance = true;
|
||||
await zeroEx.exchange.fillOrderAsync(
|
||||
signedOrder, fillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, taker,
|
||||
);
|
||||
})().catch(done);
|
||||
});
|
||||
it('should equal approved amount when approved amount is lowest', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, fillableAmount,
|
||||
);
|
||||
|
||||
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
||||
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
||||
|
||||
const changedMakerApprovalAmount = new BigNumber(3);
|
||||
zeroEx.orderStateWatcher.addOrder(signedOrder);
|
||||
const changedMakerApprovalAmount = ZeroEx.toBaseUnitAmount(new BigNumber(3), 18);
|
||||
zeroEx.orderStateWatcher.addOrder(signedOrder);
|
||||
|
||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||
const validOrderState = orderState as OrderStateValid;
|
||||
const orderRelevantState = validOrderState.orderRelevantState;
|
||||
expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
|
||||
changedMakerApprovalAmount);
|
||||
done();
|
||||
});
|
||||
zeroEx.orderStateWatcher.subscribe(callback);
|
||||
await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, changedMakerApprovalAmount);
|
||||
})().catch(done);
|
||||
});
|
||||
it('should equal balance amount when balance amount is lowest', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, fillableAmount,
|
||||
);
|
||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||
const validOrderState = orderState as OrderStateValid;
|
||||
const orderRelevantState = validOrderState.orderRelevantState;
|
||||
expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
|
||||
changedMakerApprovalAmount);
|
||||
expect(orderRelevantState.remainingFillableTakerTokenAmount).to.be.bignumber.equal(
|
||||
changedMakerApprovalAmount);
|
||||
done();
|
||||
});
|
||||
zeroEx.orderStateWatcher.subscribe(callback);
|
||||
await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, changedMakerApprovalAmount);
|
||||
})().catch(done);
|
||||
});
|
||||
it('should equal balance amount when balance amount is lowest', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, fillableAmount,
|
||||
);
|
||||
|
||||
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
||||
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
||||
|
||||
const remainingAmount = new BigNumber(1);
|
||||
const transferAmount = makerBalance.sub(remainingAmount);
|
||||
zeroEx.orderStateWatcher.addOrder(signedOrder);
|
||||
const remainingAmount = ZeroEx.toBaseUnitAmount(new BigNumber(1), 18);
|
||||
const transferAmount = makerBalance.sub(remainingAmount);
|
||||
zeroEx.orderStateWatcher.addOrder(signedOrder);
|
||||
|
||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||
const validOrderState = orderState as OrderStateValid;
|
||||
const orderRelevantState = validOrderState.orderRelevantState;
|
||||
expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
|
||||
remainingAmount);
|
||||
done();
|
||||
});
|
||||
zeroEx.orderStateWatcher.subscribe(callback);
|
||||
await zeroEx.token.transferAsync(
|
||||
makerToken.address, maker, ZeroEx.NULL_ADDRESS, transferAmount);
|
||||
})().catch(done);
|
||||
});
|
||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||
const validOrderState = orderState as OrderStateValid;
|
||||
const orderRelevantState = validOrderState.orderRelevantState;
|
||||
expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
|
||||
remainingAmount);
|
||||
expect(orderRelevantState.remainingFillableTakerTokenAmount).to.be.bignumber.equal(
|
||||
remainingAmount);
|
||||
done();
|
||||
});
|
||||
zeroEx.orderStateWatcher.subscribe(callback);
|
||||
await zeroEx.token.transferAsync(
|
||||
makerToken.address, maker, ZeroEx.NULL_ADDRESS, transferAmount);
|
||||
})().catch(done);
|
||||
});
|
||||
});
|
||||
it('should emit orderStateInvalid when watched order cancelled', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
@@ -333,6 +343,28 @@ describe('OrderStateWatcher', () => {
|
||||
await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount);
|
||||
})().catch(done);
|
||||
});
|
||||
it('should emit orderStateInvalid when within rounding error range', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
const remainingFillableAmountInBaseUnits = new BigNumber(100);
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, fillableAmount,
|
||||
);
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
zeroEx.orderStateWatcher.addOrder(signedOrder);
|
||||
|
||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||
expect(orderState.isValid).to.be.false();
|
||||
const invalidOrderState = orderState as OrderStateInvalid;
|
||||
expect(invalidOrderState.orderHash).to.be.equal(orderHash);
|
||||
expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderFillRoundingError);
|
||||
done();
|
||||
});
|
||||
zeroEx.orderStateWatcher.subscribe(callback);
|
||||
await zeroEx.exchange.cancelOrderAsync(
|
||||
signedOrder, fillableAmount.minus(remainingFillableAmountInBaseUnits),
|
||||
);
|
||||
})().catch(done);
|
||||
});
|
||||
it('should emit orderStateValid when watched order partially cancelled', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
|
Reference in New Issue
Block a user