diff options
Diffstat (limited to 'packages/0x.js/test/order_validation_test.ts')
-rw-r--r-- | packages/0x.js/test/order_validation_test.ts | 327 |
1 files changed, 327 insertions, 0 deletions
diff --git a/packages/0x.js/test/order_validation_test.ts b/packages/0x.js/test/order_validation_test.ts new file mode 100644 index 000000000..4f18742d3 --- /dev/null +++ b/packages/0x.js/test/order_validation_test.ts @@ -0,0 +1,327 @@ +import * as chai from 'chai'; +import * as Web3 from 'web3'; +import BigNumber from 'bignumber.js'; +import * as Sinon from 'sinon'; +import {chaiSetup} from './utils/chai_setup'; +import {web3Factory} from './utils/web3_factory'; +import {ZeroEx, SignedOrder, Token, ExchangeContractErrs, ZeroExError} from '../src'; +import {TradeSide, TransferType} from '../src/types'; +import {TokenUtils} from './utils/token_utils'; +import {BlockchainLifecycle} from './utils/blockchain_lifecycle'; +import {FillScenarios} from './utils/fill_scenarios'; +import {OrderValidationUtils} from '../src/utils/order_validation_utils'; +import {ExchangeTransferSimulator} from '../src/utils/exchange_transfer_simulator'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(); + +describe('OrderValidation', () => { + let web3: Web3; + let zeroEx: ZeroEx; + let userAddresses: string[]; + let tokens: Token[]; + let tokenUtils: TokenUtils; + let exchangeContractAddress: string; + let zrxTokenAddress: string; + let fillScenarios: FillScenarios; + let makerTokenAddress: string; + let takerTokenAddress: string; + let coinbase: string; + let makerAddress: string; + let takerAddress: string; + let feeRecipient: string; + let orderValidationUtils: OrderValidationUtils; + const fillableAmount = new BigNumber(5); + const fillTakerAmount = new BigNumber(5); + before(async () => { + web3 = web3Factory.create(); + zeroEx = new ZeroEx(web3.currentProvider); + exchangeContractAddress = await zeroEx.exchange.getContractAddressAsync(); + userAddresses = await zeroEx.getAvailableAddressesAsync(); + [coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses; + tokens = await zeroEx.tokenRegistry.getTokensAsync(); + tokenUtils = new TokenUtils(tokens); + zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address; + fillScenarios = new FillScenarios(zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress); + const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + orderValidationUtils = new OrderValidationUtils(zeroEx.token, zeroEx.exchange); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('validateOrderFillableOrThrowAsync', () => { + it('should succeed if the order is fillable', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + await zeroEx.exchange.validateOrderFillableOrThrowAsync( + signedOrder, + ); + }); + it('should succeed if the order is asymmetric and fillable', async () => { + const makerFillableAmount = fillableAmount; + const takerFillableAmount = fillableAmount.minus(4); + const signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, + makerFillableAmount, takerFillableAmount, + ); + await zeroEx.exchange.validateOrderFillableOrThrowAsync( + signedOrder, + ); + }); + it('should throw when the order is fully filled or cancelled', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount); + return expect(zeroEx.exchange.validateOrderFillableOrThrowAsync( + signedOrder, + )).to.be.rejectedWith(ExchangeContractErrs.OrderRemainingFillAmountZero); + }); + it('should throw when order is expired', async () => { + const expirationInPast = new BigNumber(1496826058); // 7th Jun 2017 + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, + fillableAmount, expirationInPast, + ); + return expect(zeroEx.exchange.validateOrderFillableOrThrowAsync( + signedOrder, + )).to.be.rejectedWith(ExchangeContractErrs.OrderFillExpired); + }); + }); + describe('validateFillOrderAndThrowIfInvalidAsync', () => { + it('should throw when the fill amount is zero', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + const zeroFillAmount = new BigNumber(0); + return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync( + signedOrder, zeroFillAmount, takerAddress, + )).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero); + }); + it('should throw when the signature is invalid', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + // 27 <--> 28 + signedOrder.ecSignature.v = 27 + (28 - signedOrder.ecSignature.v); + return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync( + signedOrder, fillableAmount, takerAddress, + )).to.be.rejectedWith(ZeroExError.InvalidSignature); + }); + it('should throw when the order is fully filled or cancelled', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount); + return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync( + signedOrder, fillableAmount, takerAddress, + )).to.be.rejectedWith(ExchangeContractErrs.OrderRemainingFillAmountZero); + }); + it('should throw when sender is not a taker', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + const nonTakerAddress = userAddresses[6]; + return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync( + signedOrder, fillTakerAmount, nonTakerAddress, + )).to.be.rejectedWith(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker); + }); + it('should throw when order is expired', async () => { + const expirationInPast = new BigNumber(1496826058); // 7th Jun 2017 + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, + fillableAmount, expirationInPast, + ); + return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync( + signedOrder, fillTakerAmount, takerAddress, + )).to.be.rejectedWith(ExchangeContractErrs.OrderFillExpired); + }); + it('should throw when there a rounding error would have occurred', async () => { + const makerAmount = new BigNumber(3); + const takerAmount = new BigNumber(5); + const signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, + makerAmount, takerAmount, + ); + const fillTakerAmountThatCausesRoundingError = new BigNumber(3); + return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync( + signedOrder, fillTakerAmountThatCausesRoundingError, takerAddress, + )).to.be.rejectedWith(ExchangeContractErrs.OrderFillRoundingError); + }); + }); + describe('#validateFillOrKillOrderAndThrowIfInvalidAsync', () => { + it('should throw if remaining fillAmount is less then the desired fillAmount', async () => { + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + const tooLargeFillAmount = new BigNumber(7); + const fillAmountDifference = tooLargeFillAmount.minus(fillableAmount); + await zeroEx.token.transferAsync(takerTokenAddress, coinbase, takerAddress, fillAmountDifference); + await zeroEx.token.setProxyAllowanceAsync(takerTokenAddress, takerAddress, tooLargeFillAmount); + await zeroEx.token.transferAsync(makerTokenAddress, coinbase, makerAddress, fillAmountDifference); + await zeroEx.token.setProxyAllowanceAsync(makerTokenAddress, makerAddress, tooLargeFillAmount); + + return expect(zeroEx.exchange.validateFillOrKillOrderThrowIfInvalidAsync( + signedOrder, tooLargeFillAmount, takerAddress, + )).to.be.rejectedWith(ExchangeContractErrs.InsufficientRemainingFillAmount); + }); + }); + describe('validateCancelOrderAndThrowIfInvalidAsync', () => { + let signedOrder: SignedOrder; + let orderHashHex: string; + const cancelAmount = new BigNumber(3); + beforeEach(async () => { + [coinbase, makerAddress, takerAddress] = userAddresses; + const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + ); + orderHashHex = ZeroEx.getOrderHashHex(signedOrder); + }); + it('should throw when cancel amount is zero', async () => { + const zeroCancelAmount = new BigNumber(0); + return expect(zeroEx.exchange.validateCancelOrderThrowIfInvalidAsync(signedOrder, zeroCancelAmount)) + .to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero); + }); + it('should throw when order is expired', async () => { + const expirationInPast = new BigNumber(1496826058); // 7th Jun 2017 + const expiredSignedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, + fillableAmount, expirationInPast, + ); + orderHashHex = ZeroEx.getOrderHashHex(expiredSignedOrder); + return expect(zeroEx.exchange.validateCancelOrderThrowIfInvalidAsync(expiredSignedOrder, cancelAmount)) + .to.be.rejectedWith(ExchangeContractErrs.OrderCancelExpired); + }); + it('should throw when order is already cancelled or filled', async () => { + await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount); + return expect(zeroEx.exchange.validateCancelOrderThrowIfInvalidAsync(signedOrder, fillableAmount)) + .to.be.rejectedWith(ExchangeContractErrs.OrderAlreadyCancelledOrFilled); + }); + }); + describe('#validateFillOrderBalancesAllowancesThrowIfInvalidAsync', () => { + let exchangeTransferSimulator: ExchangeTransferSimulator; + let transferFromAsync: Sinon.SinonSpy; + const bigNumberMatch = (expected: BigNumber) => { + return Sinon.match((value: BigNumber) => value.eq(expected)); + }; + beforeEach('create exchangeTransferSimulator', async () => { + exchangeTransferSimulator = new ExchangeTransferSimulator(zeroEx.token); + transferFromAsync = Sinon.spy(); + exchangeTransferSimulator.transferFromAsync = transferFromAsync as any; + }); + it('should call exchangeTransferSimulator.transferFrom in a correct order', async () => { + const makerFee = new BigNumber(2); + const takerFee = new BigNumber(2); + const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( + makerTokenAddress, takerTokenAddress, makerFee, takerFee, + makerAddress, takerAddress, fillableAmount, feeRecipient, + ); + await orderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( + exchangeTransferSimulator, signedOrder, fillableAmount, takerAddress, zrxTokenAddress, + ); + expect(transferFromAsync.callCount).to.be.equal(4); + expect( + transferFromAsync.getCall(0).calledWith( + makerTokenAddress, makerAddress, takerAddress, bigNumberMatch(fillableAmount), + TradeSide.Maker, TransferType.Trade, + ), + ).to.be.true(); + expect( + transferFromAsync.getCall(1).calledWith( + takerTokenAddress, takerAddress, makerAddress, bigNumberMatch(fillableAmount), + TradeSide.Taker, TransferType.Trade, + ), + ).to.be.true(); + expect( + transferFromAsync.getCall(2).calledWith( + zrxTokenAddress, makerAddress, feeRecipient, bigNumberMatch(makerFee), + TradeSide.Maker, TransferType.Fee, + ), + ).to.be.true(); + expect( + transferFromAsync.getCall(3).calledWith( + zrxTokenAddress, takerAddress, feeRecipient, bigNumberMatch(takerFee), + TradeSide.Taker, TransferType.Fee, + ), + ).to.be.true(); + }); + it('should call exchangeTransferSimulator.transferFrom with correct values for an open order', async () => { + const makerFee = new BigNumber(2); + const takerFee = new BigNumber(2); + const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( + makerTokenAddress, takerTokenAddress, makerFee, takerFee, + makerAddress, ZeroEx.NULL_ADDRESS, fillableAmount, feeRecipient, + ); + await orderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( + exchangeTransferSimulator, signedOrder, fillableAmount, takerAddress, zrxTokenAddress, + ); + expect(transferFromAsync.callCount).to.be.equal(4); + expect( + transferFromAsync.getCall(0).calledWith( + makerTokenAddress, makerAddress, takerAddress, bigNumberMatch(fillableAmount), + TradeSide.Maker, TransferType.Trade, + ), + ).to.be.true(); + expect( + transferFromAsync.getCall(1).calledWith( + takerTokenAddress, takerAddress, makerAddress, bigNumberMatch(fillableAmount), + TradeSide.Taker, TransferType.Trade, + ), + ).to.be.true(); + expect( + transferFromAsync.getCall(2).calledWith( + zrxTokenAddress, makerAddress, feeRecipient, bigNumberMatch(makerFee), + TradeSide.Maker, TransferType.Fee, + ), + ).to.be.true(); + expect( + transferFromAsync.getCall(3).calledWith( + zrxTokenAddress, takerAddress, feeRecipient, bigNumberMatch(takerFee), + TradeSide.Taker, TransferType.Fee, + ), + ).to.be.true(); + }); + it('should correctly round the fillMakerTokenAmount', async () => { + const makerTokenAmount = new BigNumber(3); + const takerTokenAmount = new BigNumber(1); + const signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, makerTokenAmount, takerTokenAmount, + ); + await orderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( + exchangeTransferSimulator, signedOrder, takerTokenAmount, takerAddress, zrxTokenAddress, + ); + expect(transferFromAsync.callCount).to.be.equal(4); + const makerFillAmount = transferFromAsync.getCall(0).args[3]; + expect(makerFillAmount).to.be.bignumber.equal(makerTokenAmount); + }); + it('should correctly round the makerFeeAmount', async () => { + const makerFee = new BigNumber(2); + const takerFee = new BigNumber(4); + const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( + makerTokenAddress, takerTokenAddress, makerFee, takerFee, makerAddress, takerAddress, + fillableAmount, ZeroEx.NULL_ADDRESS, + ); + const fillTakerTokenAmount = fillableAmount.div(2).round(0); + await orderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( + exchangeTransferSimulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress, + ); + const makerPartialFee = makerFee.div(2); + const takerPartialFee = takerFee.div(2); + expect(transferFromAsync.callCount).to.be.equal(4); + const partialMakerFee = transferFromAsync.getCall(2).args[3]; + expect(partialMakerFee).to.be.bignumber.equal(makerPartialFee); + const partialTakerFee = transferFromAsync.getCall(3).args[3]; + expect(partialTakerFee).to.be.bignumber.equal(takerPartialFee); + }); + }); +}); |