import * as chai from 'chai'; import * as Web3 from 'web3'; import * as BigNumber from 'bignumber.js'; import promisify = require('es6-promisify'); import {chaiSetup} from './utils/chai_setup'; import {web3Factory} from './utils/web3_factory'; import {ZeroEx, SignedOrder, Token, ExchangeContractErrs, ZeroExError} from '../src'; 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'; 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); const shouldThrowOnInsufficientBalanceOrAllowance = false; 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('#validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync', () => { describe('should throw when not enough balance or allowance to fulfill the order', () => { const balanceToSubtractFromMaker = new BigNumber(3); const balanceToSubtractFromTaker = new BigNumber(3); const lackingAllowance = new BigNumber(3); let signedOrder: SignedOrder; beforeEach('create fillable signed order', async () => { signedOrder = await fillScenarios.createFillableSignedOrderAsync( makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, ); }); it('should throw when maker balance is less than maker fill amount', async () => { await zeroEx.token.transferAsync( makerTokenAddress, makerAddress, coinbase, balanceToSubtractFromMaker, ); return expect((orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync( signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerBalance); }); it('should throw when maker allowance is less than maker fill amount', async () => { const newAllowanceWhichIsLessThanFillAmount = fillTakerAmount.minus(lackingAllowance); await zeroEx.token.setProxyAllowanceAsync(makerTokenAddress, makerAddress, newAllowanceWhichIsLessThanFillAmount); return expect((orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync( signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerAllowance); }); }); describe('should throw when not enough balance or allowance to pay fees', () => { const makerFee = new BigNumber(2); const takerFee = new BigNumber(2); let signedOrder: SignedOrder; beforeEach('setup', async () => { signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( makerTokenAddress, takerTokenAddress, makerFee, takerFee, makerAddress, takerAddress, fillableAmount, feeRecipient, ); }); it('should throw when maker doesn\'t have enough balance to pay fees', async () => { const balanceToSubtractFromMaker = new BigNumber(1); await zeroEx.token.transferAsync( zrxTokenAddress, makerAddress, coinbase, balanceToSubtractFromMaker, ); return expect((orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync( signedOrder, fillTakerAmount, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerFeeBalance); }); it('should throw when maker doesn\'t have enough allowance to pay fees', async () => { const newAllowanceWhichIsLessThanFees = makerFee.minus(1); await zeroEx.token.setProxyAllowanceAsync(zrxTokenAddress, makerAddress, newAllowanceWhichIsLessThanFees); return expect((orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync( signedOrder, fillTakerAmount, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerFeeAllowance); }); }); describe('should throw on insufficient balance or allowance when makerToken is ZRX', () => { const makerFee = new BigNumber(2); const takerFee = new BigNumber(2); let signedOrder: SignedOrder; beforeEach(async () => { signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( zrxTokenAddress, takerTokenAddress, makerFee, takerFee, makerAddress, takerAddress, fillableAmount, feeRecipient, ); }); it('should throw on insufficient balance when makerToken is ZRX', async () => { const balanceToSubtractFromMaker = new BigNumber(1); await zeroEx.token.transferAsync( zrxTokenAddress, makerAddress, coinbase, balanceToSubtractFromMaker, ); return expect((orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync( signedOrder, fillTakerAmount, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerBalance); }); it('should throw on insufficient allowance when makerToken is ZRX', async () => { const oldAllowance = await zeroEx.token.getProxyAllowanceAsync(zrxTokenAddress, makerAddress); const newAllowanceWhichIsInsufficient = oldAllowance.minus(1); await zeroEx.token.setProxyAllowanceAsync( zrxTokenAddress, makerAddress, newAllowanceWhichIsInsufficient); return expect((orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync( signedOrder, fillTakerAmount, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerAllowance); }); }); describe('should correctly validate fees amounts if taker token is ZRX', () => { let signedOrder: SignedOrder; let txHash: string; it('should not throw if maker will have enough ZRX to pay fees after the transfer', async () => { const makerFee = new BigNumber(2); const takerFee = new BigNumber(2); signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( makerTokenAddress, zrxTokenAddress, makerFee, takerFee, makerAddress, takerAddress, fillableAmount, feeRecipient, ); txHash = await zeroEx.token.transferAsync(zrxTokenAddress, makerAddress, coinbase, makerFee); await zeroEx.awaitTransactionMinedAsync(txHash); await (orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync( signedOrder, fillTakerAmount, zrxTokenAddress, ); }); it('should throw if maker will not have enough ZRX to pay fees even after the transfer', async () => { const makerFee = fillableAmount.plus(1); const takerFee = fillableAmount.plus(1); signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( makerTokenAddress, zrxTokenAddress, makerFee, takerFee, makerAddress, takerAddress, fillableAmount, feeRecipient, ); txHash = await zeroEx.token.transferAsync(zrxTokenAddress, makerAddress, coinbase, makerFee); await zeroEx.awaitTransactionMinedAsync(txHash); return expect( (orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync( signedOrder, fillTakerAmount, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerFeeBalance); }); }); }); describe('#validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync', () => { describe('should throw when not enough balance or allowance to fulfill the order', () => { const balanceToSubtractFromMaker = new BigNumber(3); const balanceToSubtractFromTaker = new BigNumber(3); const lackingAllowance = new BigNumber(3); let signedOrder: SignedOrder; beforeEach('create fillable signed order', async () => { signedOrder = await fillScenarios.createFillableSignedOrderAsync( makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, ); }); it('should throw when taker balance is less than fill amount', async () => { await zeroEx.token.transferAsync( takerTokenAddress, takerAddress, coinbase, balanceToSubtractFromTaker, ); return expect((orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync( signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerBalance); }); it('should throw when taker allowance is less than fill amount', async () => { const newAllowanceWhichIsLessThanFillAmount = fillTakerAmount.minus(lackingAllowance); await zeroEx.token.setProxyAllowanceAsync(takerTokenAddress, takerAddress, newAllowanceWhichIsLessThanFillAmount); return expect((orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync( signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerAllowance); }); }); describe('should throw when not enough balance or allowance to pay fees', () => { const makerFee = new BigNumber(2); const takerFee = new BigNumber(2); let signedOrder: SignedOrder; beforeEach('setup', async () => { signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( makerTokenAddress, takerTokenAddress, makerFee, takerFee, makerAddress, takerAddress, fillableAmount, feeRecipient, ); }); it('should throw when taker doesn\'t have enough balance to pay fees', async () => { const balanceToSubtractFromTaker = new BigNumber(1); await zeroEx.token.transferAsync( zrxTokenAddress, takerAddress, coinbase, balanceToSubtractFromTaker, ); return expect((orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync( signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerFeeBalance); }); it('should throw when taker doesn\'t have enough allowance to pay fees', async () => { const newAllowanceWhichIsLessThanFees = makerFee.minus(1); await zeroEx.token.setProxyAllowanceAsync(zrxTokenAddress, takerAddress, newAllowanceWhichIsLessThanFees); return expect((orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync( signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerFeeAllowance); }); }); describe('should throw on insufficient balance or allowance when takerToken is ZRX', () => { const makerFee = new BigNumber(2); const takerFee = new BigNumber(2); let signedOrder: SignedOrder; beforeEach(async () => { signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( makerTokenAddress, zrxTokenAddress, makerFee, takerFee, makerAddress, takerAddress, fillableAmount, feeRecipient, ); }); it('should throw on insufficient balance when takerToken is ZRX', async () => { const balanceToSubtractFromTaker = new BigNumber(1); await zeroEx.token.transferAsync( zrxTokenAddress, takerAddress, coinbase, balanceToSubtractFromTaker, ); return expect((orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync( signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerBalance); }); it('should throw on insufficient allowance when takerToken is ZRX', async () => { const oldAllowance = await zeroEx.token.getProxyAllowanceAsync(zrxTokenAddress, takerAddress); const newAllowanceWhichIsInsufficient = oldAllowance.minus(1); await zeroEx.token.setProxyAllowanceAsync( zrxTokenAddress, takerAddress, newAllowanceWhichIsInsufficient); return expect((orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync( signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerAllowance); }); }); describe('should correctly validate fees amounts if maker token is ZRX', () => { let signedOrder: SignedOrder; let txHash: string; it('should not throw if taker will have enough ZRX to pay fees after the transfer', async () => { const makerFee = new BigNumber(2); const takerFee = new BigNumber(2); signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( zrxTokenAddress, takerTokenAddress, makerFee, takerFee, makerAddress, takerAddress, fillableAmount, feeRecipient, ); txHash = await zeroEx.token.transferAsync(zrxTokenAddress, takerAddress, coinbase, takerFee); await zeroEx.awaitTransactionMinedAsync(txHash); await (orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync( signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress, ); }); it('should throw if maker will not have enough ZRX to pay fees even after the transfer', async () => { const makerFee = fillableAmount.plus(1); const takerFee = fillableAmount.plus(1); signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( zrxTokenAddress, takerTokenAddress, makerFee, takerFee, makerAddress, takerAddress, fillableAmount, feeRecipient, ); txHash = await zeroEx.token.transferAsync(zrxTokenAddress, takerAddress, coinbase, takerFee); await zeroEx.awaitTransactionMinedAsync(txHash); return expect( (orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync( signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress, )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerFeeBalance); }); }); }); });