import { LibMathRevertErrors, ReferenceFunctions as LibReferenceFunctions } from '@0x/contracts-exchange-libs'; import { blockchainTests, constants, expect } from '@0x/contracts-test-utils'; import { SafeMathRevertErrors } from '@0x/contracts-utils'; import { FillResults, OrderInfo, OrderStatus, SignatureType } from '@0x/types'; import { BigNumber, ExchangeRevertErrors, hexUtils } from '@0x/utils'; import * as _ from 'lodash'; import { AssetBalances, createBadAssetData, createBadSignature, createGoodAssetData, createGoodSignature, IsolatedExchangeWrapper, Order, } from './utils/isolated_exchange_wrapper'; blockchainTests('Isolated fillOrder() tests', env => { const randomAddress = () => hexUtils.random(constants.ADDRESS_LENGTH); const getCurrentTime = () => Math.floor(_.now() / 1000); const { ZERO_AMOUNT, ONE_ETHER, MAX_UINT256_ROOT } = constants; const ONE_DAY = 60 * 60 * 24; const TOMORROW = getCurrentTime() + ONE_DAY; const DEFAULT_ORDER: Order = { senderAddress: constants.NULL_ADDRESS, makerAddress: randomAddress(), takerAddress: constants.NULL_ADDRESS, feeRecipientAddress: randomAddress(), makerAssetAmount: ONE_ETHER, takerAssetAmount: ONE_ETHER.times(2), makerFee: ONE_ETHER.times(0.0015), takerFee: ONE_ETHER.times(0.0025), salt: ZERO_AMOUNT, expirationTimeSeconds: new BigNumber(TOMORROW), makerAssetData: createGoodAssetData(), takerAssetData: createGoodAssetData(), makerFeeAssetData: createGoodAssetData(), takerFeeAssetData: createGoodAssetData(), exchangeAddress: constants.NULL_ADDRESS, chainId: 1, }; const DEFAULT_GAS_PRICE = new BigNumber(20000); const DEFAULT_PROTOCOL_FEE_MULTIPLIER = new BigNumber(15000); let takerAddress: string; let notTakerAddress: string; let exchange: IsolatedExchangeWrapper; let nextSaltValue = 1; before(async () => { [takerAddress, notTakerAddress] = await env.getAccountAddressesAsync(); exchange = await IsolatedExchangeWrapper.deployAsync(env.web3Wrapper, { ...env.txDefaults, from: takerAddress, }); }); function createOrder(details: Partial = {}): Order { return { ...DEFAULT_ORDER, salt: new BigNumber(nextSaltValue++), ...details, }; } interface FillOrderAndAssertResultsResults { fillResults: FillResults; orderInfo: OrderInfo; } async function fillOrderAndAssertResultsAsync( order: Order, takerAssetFillAmount: BigNumber, signature?: string, ): Promise { const orderInfo = await exchange.getOrderInfoAsync(order); const efr = calculateExpectedFillResults( order, orderInfo, takerAssetFillAmount, DEFAULT_PROTOCOL_FEE_MULTIPLIER, DEFAULT_GAS_PRICE, ); const eoi = calculateExpectedOrderInfo(order, orderInfo, efr); const efb = calculateExpectedFillBalances(order, efr); // Fill the order. const fillResults = await exchange.fillOrderAsync(order, takerAssetFillAmount, signature); const newOrderInfo = await exchange.getOrderInfoAsync(order); // Check returned fillResults. expect(fillResults.makerAssetFilledAmount).to.bignumber.eq(efr.makerAssetFilledAmount); expect(fillResults.takerAssetFilledAmount).to.bignumber.eq(efr.takerAssetFilledAmount); expect(fillResults.makerFeePaid).to.bignumber.eq(efr.makerFeePaid); expect(fillResults.takerFeePaid).to.bignumber.eq(efr.takerFeePaid); // Check balances. for (const assetData of Object.keys(efb)) { for (const address of Object.keys(efb[assetData])) { expect( exchange.getBalanceChange(assetData, address), `checking balance of assetData: ${assetData}, address: ${address}`, ).to.bignumber.eq(efb[assetData][address]); } } // Check order info. expect(newOrderInfo.orderStatus).to.eq(eoi.orderStatus); expect(newOrderInfo.orderTakerAssetFilledAmount).to.bignumber.eq(eoi.orderTakerAssetFilledAmount); // Check that there wasn't an overfill. expect(newOrderInfo.orderTakerAssetFilledAmount.lte(order.takerAssetAmount), 'checking for overfill').to.be.ok( '', ); return { fillResults, orderInfo: newOrderInfo, }; } function calculateExpectedFillResults( order: Order, orderInfo: OrderInfo, takerAssetFillAmount: BigNumber, protocolFeeMultiplier: BigNumber, gasPrice: BigNumber, ): FillResults { const remainingTakerAssetAmount = order.takerAssetAmount.minus(orderInfo.orderTakerAssetFilledAmount); return LibReferenceFunctions.calculateFillResults( order, BigNumber.min(takerAssetFillAmount, remainingTakerAssetAmount), protocolFeeMultiplier, gasPrice, ); } function calculateExpectedOrderInfo(order: Order, orderInfo: OrderInfo, fillResults: FillResults): OrderInfo { const orderTakerAssetFilledAmount = orderInfo.orderTakerAssetFilledAmount.plus( fillResults.takerAssetFilledAmount, ); const orderStatus = orderTakerAssetFilledAmount.gte(order.takerAssetAmount) ? OrderStatus.FullyFilled : OrderStatus.Fillable; return { orderHash: exchange.getOrderHash(order), orderStatus, orderTakerAssetFilledAmount, }; } function calculateExpectedFillBalances(order: Order, fillResults: FillResults): AssetBalances { const balances: AssetBalances = {}; const addBalance = (assetData: string, address: string, amount: BigNumber) => { balances[assetData] = balances[assetData] || {}; const balance = balances[assetData][address] || ZERO_AMOUNT; balances[assetData][address] = balance.plus(amount); }; addBalance(order.makerAssetData, order.makerAddress, fillResults.makerAssetFilledAmount.negated()); addBalance(order.makerAssetData, takerAddress, fillResults.makerAssetFilledAmount); addBalance(order.takerAssetData, order.makerAddress, fillResults.takerAssetFilledAmount); addBalance(order.takerAssetData, takerAddress, fillResults.takerAssetFilledAmount.negated()); addBalance(order.makerFeeAssetData, order.makerAddress, fillResults.makerFeePaid.negated()); addBalance(order.makerFeeAssetData, order.feeRecipientAddress, fillResults.makerFeePaid); addBalance(order.takerFeeAssetData, takerAddress, fillResults.takerFeePaid.negated()); addBalance(order.takerFeeAssetData, order.feeRecipientAddress, fillResults.takerFeePaid); return balances; } function splitAmount(total: BigNumber, n: number = 2): BigNumber[] { const splitSize = total.dividedToIntegerBy(n); const splits = _.times(n - 1, () => splitSize); splits.push(total.minus(splitSize.times(n - 1))); return splits; } describe('full fills', () => { it('can fully fill an order', async () => { const order = createOrder(); const { orderInfo } = await fillOrderAndAssertResultsAsync(order, order.takerAssetAmount); expect(orderInfo.orderStatus).to.eq(OrderStatus.FullyFilled); }); it("can't overfill an order", async () => { const order = createOrder(); const { orderInfo } = await fillOrderAndAssertResultsAsync(order, order.takerAssetAmount.times(1.01)); expect(orderInfo.orderStatus).to.eq(OrderStatus.FullyFilled); }); it('no fees', async () => { const order = createOrder({ makerFee: ZERO_AMOUNT, takerFee: ZERO_AMOUNT, }); const { orderInfo } = await fillOrderAndAssertResultsAsync(order, order.takerAssetAmount); expect(orderInfo.orderStatus).to.eq(OrderStatus.FullyFilled); }); it('only maker fees', async () => { const order = createOrder({ takerFee: ZERO_AMOUNT, }); const { orderInfo } = await fillOrderAndAssertResultsAsync(order, order.takerAssetAmount); expect(orderInfo.orderStatus).to.eq(OrderStatus.FullyFilled); }); it('only taker fees', async () => { const order = createOrder({ makerFee: ZERO_AMOUNT, }); const { orderInfo } = await fillOrderAndAssertResultsAsync(order, order.takerAssetAmount); expect(orderInfo.orderStatus).to.eq(OrderStatus.FullyFilled); }); }); describe('partial fills', () => { const takerAssetFillAmount = DEFAULT_ORDER.takerAssetAmount.dividedToIntegerBy(2); it('can partially fill an order', async () => { const order = createOrder(); const { orderInfo } = await fillOrderAndAssertResultsAsync(order, takerAssetFillAmount); expect(orderInfo.orderStatus).to.eq(OrderStatus.Fillable); }); it('no fees', async () => { const order = createOrder({ makerFee: ZERO_AMOUNT, takerFee: ZERO_AMOUNT, }); const { orderInfo } = await fillOrderAndAssertResultsAsync(order, takerAssetFillAmount); expect(orderInfo.orderStatus).to.eq(OrderStatus.Fillable); }); it('only maker fees', async () => { const order = createOrder({ takerFee: ZERO_AMOUNT, }); const { orderInfo } = await fillOrderAndAssertResultsAsync(order, takerAssetFillAmount); expect(orderInfo.orderStatus).to.eq(OrderStatus.Fillable); }); it('only taker fees', async () => { const order = createOrder({ makerFee: ZERO_AMOUNT, }); const { orderInfo } = await fillOrderAndAssertResultsAsync(order, takerAssetFillAmount); expect(orderInfo.orderStatus).to.eq(OrderStatus.Fillable); }); }); describe('multiple fills', () => { it('can fully fill an order in two fills', async () => { const order = createOrder(); const fillAmounts = splitAmount(order.takerAssetAmount); const orderInfos = [ (await fillOrderAndAssertResultsAsync(order, fillAmounts[0])).orderInfo, (await fillOrderAndAssertResultsAsync(order, fillAmounts[1])).orderInfo, ]; expect(orderInfos[0].orderStatus).to.eq(OrderStatus.Fillable); expect(orderInfos[1].orderStatus).to.eq(OrderStatus.FullyFilled); }); it('can partially fill an order in two fills', async () => { const order = createOrder(); const fillAmounts = splitAmount(order.takerAssetAmount.dividedToIntegerBy(2)); const orderInfos = [ (await fillOrderAndAssertResultsAsync(order, fillAmounts[0])).orderInfo, (await fillOrderAndAssertResultsAsync(order, fillAmounts[1])).orderInfo, ]; expect(orderInfos[0].orderStatus).to.eq(OrderStatus.Fillable); expect(orderInfos[1].orderStatus).to.eq(OrderStatus.Fillable); }); it("can't overfill an order in two fills", async () => { const order = createOrder(); const fillAmounts = splitAmount(order.takerAssetAmount); fillAmounts[0] = fillAmounts[0].times(1.01); const orderInfos = [ (await fillOrderAndAssertResultsAsync(order, fillAmounts[0])).orderInfo, (await fillOrderAndAssertResultsAsync(order, fillAmounts[1])).orderInfo, ]; expect(orderInfos[0].orderStatus).to.eq(OrderStatus.Fillable); expect(orderInfos[1].orderStatus).to.eq(OrderStatus.FullyFilled); }); it('can fully fill an order with no fees in two fills', async () => { const order = createOrder({ makerFee: ZERO_AMOUNT, takerFee: ZERO_AMOUNT, }); const fillAmounts = splitAmount(order.takerAssetAmount); const orderInfos = [ (await fillOrderAndAssertResultsAsync(order, fillAmounts[0])).orderInfo, (await fillOrderAndAssertResultsAsync(order, fillAmounts[1])).orderInfo, ]; expect(orderInfos[0].orderStatus).to.eq(OrderStatus.Fillable); expect(orderInfos[1].orderStatus).to.eq(OrderStatus.FullyFilled); }); it('can fully fill an order with only maker fees in two fills', async () => { const order = createOrder({ takerFee: ZERO_AMOUNT, }); const fillAmounts = splitAmount(order.takerAssetAmount); const orderInfos = [ (await fillOrderAndAssertResultsAsync(order, fillAmounts[0])).orderInfo, (await fillOrderAndAssertResultsAsync(order, fillAmounts[1])).orderInfo, ]; expect(orderInfos[0].orderStatus).to.eq(OrderStatus.Fillable); expect(orderInfos[1].orderStatus).to.eq(OrderStatus.FullyFilled); }); it('can fully fill an order with only taker fees in two fills', async () => { const order = createOrder({ makerFee: ZERO_AMOUNT, }); const fillAmounts = splitAmount(order.takerAssetAmount); const orderInfos = [ (await fillOrderAndAssertResultsAsync(order, fillAmounts[0])).orderInfo, (await fillOrderAndAssertResultsAsync(order, fillAmounts[1])).orderInfo, ]; expect(orderInfos[0].orderStatus).to.eq(OrderStatus.Fillable); expect(orderInfos[1].orderStatus).to.eq(OrderStatus.FullyFilled); }); }); describe('bad fills', () => { it("can't fill an order with zero takerAssetAmount", async () => { const order = createOrder({ takerAssetAmount: ZERO_AMOUNT, }); const expectedError = new ExchangeRevertErrors.OrderStatusError( exchange.getOrderHash(order), OrderStatus.InvalidTakerAssetAmount, ); return expect(exchange.fillOrderAsync(order, ONE_ETHER)).to.revertWith(expectedError); }); it("can't fill an order with zero makerAssetAmount", async () => { const order = createOrder({ makerAssetAmount: ZERO_AMOUNT, }); const expectedError = new ExchangeRevertErrors.OrderStatusError( exchange.getOrderHash(order), OrderStatus.InvalidMakerAssetAmount, ); return expect(exchange.fillOrderAsync(order, ONE_ETHER)).to.revertWith(expectedError); }); it("can't fill an order that is fully filled", async () => { const order = createOrder(); const expectedError = new ExchangeRevertErrors.OrderStatusError( exchange.getOrderHash(order), OrderStatus.FullyFilled, ); await exchange.fillOrderAsync(order, order.takerAssetAmount); return expect(exchange.fillOrderAsync(order, 1)).to.revertWith(expectedError); }); it("can't fill an order that is expired", async () => { const order = createOrder({ expirationTimeSeconds: new BigNumber(getCurrentTime() - 60), }); const expectedError = new ExchangeRevertErrors.OrderStatusError( exchange.getOrderHash(order), OrderStatus.Expired, ); return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)).to.revertWith(expectedError); }); it("can't fill an order that is cancelled by `cancelOrder()`", async () => { const order = createOrder({ makerAddress: notTakerAddress, }); const expectedError = new ExchangeRevertErrors.OrderStatusError( exchange.getOrderHash(order), OrderStatus.Cancelled, ); await exchange.cancelOrderAsync(order, { from: notTakerAddress }); return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)).to.revertWith(expectedError); }); it("can't fill an order that is cancelled by `cancelOrdersUpTo()`", async () => { const order = createOrder({ makerAddress: notTakerAddress, }); const expectedError = new ExchangeRevertErrors.OrderStatusError( exchange.getOrderHash(order), OrderStatus.Cancelled, ); await exchange.cancelOrdersUpToAsync(order.salt, { from: notTakerAddress }); return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)).to.revertWith(expectedError); }); it("can't fill an order if taker is not `takerAddress`", async () => { const order = createOrder({ takerAddress: randomAddress(), }); const expectedError = new ExchangeRevertErrors.ExchangeInvalidContextError( ExchangeRevertErrors.ExchangeContextErrorCodes.InvalidTaker, exchange.getOrderHash(order), takerAddress, ); return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)).to.revertWith(expectedError); }); it("can't fill an order if sender is not `senderAddress`", async () => { const order = createOrder({ senderAddress: randomAddress(), }); const expectedError = new ExchangeRevertErrors.ExchangeInvalidContextError( ExchangeRevertErrors.ExchangeContextErrorCodes.InvalidSender, exchange.getOrderHash(order), takerAddress, ); return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)).to.revertWith(expectedError); }); it("can't fill an order with a taker amount that results in a maker asset rounding error", async () => { const order = createOrder({ makerAssetAmount: new BigNumber(100), takerAssetAmount: ONE_ETHER, }); const takerAssetFillAmount = order.takerAssetAmount.dividedToIntegerBy(3); const expectedError = new LibMathRevertErrors.RoundingError( takerAssetFillAmount, order.takerAssetAmount, order.makerAssetAmount, ); return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)).to.revertWith(expectedError); }); it("can't fill an order with a taker amount that results in a maker fee rounding error", async () => { const order = createOrder({ makerAssetAmount: ONE_ETHER.times(2), takerAssetAmount: ONE_ETHER, makerFee: new BigNumber(100), }); const takerAssetFillAmount = order.takerAssetAmount.dividedToIntegerBy(3); const expectedError = new LibMathRevertErrors.RoundingError( takerAssetFillAmount, order.takerAssetAmount, order.makerFee, ); return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)).to.revertWith(expectedError); }); it("can't fill an order with a taker amount that results in a taker fee rounding error", async () => { const order = createOrder({ makerAssetAmount: ONE_ETHER.times(2), takerAssetAmount: ONE_ETHER, takerFee: new BigNumber(100), }); const takerAssetFillAmount = order.takerAssetAmount.dividedToIntegerBy(3); const expectedError = new LibMathRevertErrors.RoundingError( takerAssetFillAmount, order.takerAssetAmount, order.takerFee, ); return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)).to.revertWith(expectedError); }); it("can't fill an order that results in a `makerAssetFilledAmount` overflow.", async () => { // All values need to be large to ensure we don't trigger a Rounding. const order = createOrder({ makerAssetAmount: MAX_UINT256_ROOT.times(2), takerAssetAmount: MAX_UINT256_ROOT, }); const takerAssetFillAmount = MAX_UINT256_ROOT; const expectedError = new SafeMathRevertErrors.Uint256BinOpError( SafeMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow, takerAssetFillAmount, order.makerAssetAmount, ); return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)).to.revertWith(expectedError); }); it("can't fill an order that results in a `makerFeePaid` overflow.", async () => { // All values need to be large to ensure we don't trigger a Rounding. const order = createOrder({ makerAssetAmount: MAX_UINT256_ROOT, takerAssetAmount: MAX_UINT256_ROOT, makerFee: MAX_UINT256_ROOT.times(11), }); const takerAssetFillAmount = MAX_UINT256_ROOT.dividedToIntegerBy(10); const makerAssetFilledAmount = LibReferenceFunctions.getPartialAmountFloor( takerAssetFillAmount, order.takerAssetAmount, order.makerAssetAmount, ); const expectedError = new SafeMathRevertErrors.Uint256BinOpError( SafeMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow, makerAssetFilledAmount, order.makerFee, ); return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)).to.revertWith(expectedError); }); it("can't fill an order that results in a `takerFeePaid` overflow.", async () => { // All values need to be large to ensure we don't trigger a Rounding. const order = createOrder({ makerAssetAmount: MAX_UINT256_ROOT, takerAssetAmount: MAX_UINT256_ROOT, takerFee: MAX_UINT256_ROOT.times(11), }); const takerAssetFillAmount = MAX_UINT256_ROOT.dividedToIntegerBy(10); const expectedError = new SafeMathRevertErrors.Uint256BinOpError( SafeMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow, takerAssetFillAmount, order.takerFee, ); return expect(exchange.fillOrderAsync(order, takerAssetFillAmount)).to.revertWith(expectedError); }); it("can't fill an order with a bad signature", async () => { const order = createOrder(); const signature = createBadSignature(); const expectedError = new ExchangeRevertErrors.SignatureError( ExchangeRevertErrors.SignatureErrorCode.BadOrderSignature, exchange.getOrderHash(order), order.makerAddress, signature, ); return expect(exchange.fillOrderAsync(order, order.takerAssetAmount, signature)).to.revertWith( expectedError, ); }); it("can't complementary fill an order with a bad signature that is always checked", async () => { const order = createOrder(); const takerAssetFillAmounts = splitAmount(order.takerAssetAmount); const goodSignature = createGoodSignature(SignatureType.Wallet); const badSignature = createBadSignature(SignatureType.Wallet); const expectedError = new ExchangeRevertErrors.SignatureError( ExchangeRevertErrors.SignatureErrorCode.BadOrderSignature, exchange.getOrderHash(order), order.makerAddress, badSignature, ); await exchange.fillOrderAsync(order, takerAssetFillAmounts[0], goodSignature); return expect(exchange.fillOrderAsync(order, takerAssetFillAmounts[1], badSignature)).to.revertWith( expectedError, ); }); const TRANSFER_ERROR = 'TRANSFER_FAILED'; it("can't fill an order with a maker asset that fails to transfer", async () => { const order = createOrder({ makerAssetData: createBadAssetData(), }); return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)).to.revertWith(TRANSFER_ERROR); }); it("can't fill an order with a taker asset that fails to transfer", async () => { const order = createOrder({ takerAssetData: createBadAssetData(), }); return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)).to.revertWith(TRANSFER_ERROR); }); it("can't fill an order with a maker fee asset that fails to transfer", async () => { const order = createOrder({ makerFeeAssetData: createBadAssetData(), }); return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)).to.revertWith(TRANSFER_ERROR); }); it("can't fill an order with a taker fee asset that fails to transfer", async () => { const order = createOrder({ takerFeeAssetData: createBadAssetData(), }); return expect(exchange.fillOrderAsync(order, order.takerAssetAmount)).to.revertWith(TRANSFER_ERROR); }); }); describe('permitted fills', () => { it('should allow takerAssetFillAmount to be zero', async () => { const order = createOrder(); return fillOrderAndAssertResultsAsync(order, constants.ZERO_AMOUNT); }); it('can fill an order if taker is `takerAddress`', async () => { const order = createOrder({ takerAddress, }); return fillOrderAndAssertResultsAsync(order, order.takerAssetAmount); }); it('can fill an order if sender is `senderAddress`', async () => { const order = createOrder({ senderAddress: takerAddress, }); return fillOrderAndAssertResultsAsync(order, order.takerAssetAmount); }); it('cannot fill an order with a bad signature that has already been partially filled', async () => { const order = createOrder(); const takerAssetFillAmounts = splitAmount(order.takerAssetAmount); const goodSignature = createGoodSignature(SignatureType.EthSign); const badSignature = createBadSignature(SignatureType.EthSign); await fillOrderAndAssertResultsAsync(order, takerAssetFillAmounts[0], goodSignature); const expectedError = new ExchangeRevertErrors.SignatureError( ExchangeRevertErrors.SignatureErrorCode.BadOrderSignature, exchange.getOrderHash(order), order.makerAddress, badSignature, ); return expect(fillOrderAndAssertResultsAsync(order, takerAssetFillAmounts[1], badSignature)).to.revertWith( expectedError, ); }); }); }); // tslint:disable: max-file-line-count