import { blockchainTests, constants, describe, expect, getRandomPortion, randomAddress, verifyEventsFromLogs, } from '@0x/contracts-test-utils'; import { LimitOrder, LimitOrderFields, OrderStatus, RevertErrors, RfqOrder, RfqOrderFields, SignatureType, } from '@0x/protocol-utils'; import { AnyRevertError, BigNumber } from '@0x/utils'; import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import { IZeroExContract, IZeroExEvents } from '../../src/wrappers'; import { artifacts } from '../artifacts'; import { fullMigrateAsync } from '../utils/migration'; import { assertOrderInfoEquals, computeLimitOrderFilledAmounts, computeRfqOrderFilledAmounts, createExpiry, getActualFillableTakerTokenAmount, getFillableMakerTokenAmount, getRandomLimitOrder, getRandomRfqOrder, NativeOrdersTestEnvironment, } from '../utils/orders'; import { TestMintableERC20TokenContract, TestOrderSignerRegistryWithContractWalletContract, TestRfqOriginRegistrationContract, } from '../wrappers'; blockchainTests.resets('NativeOrdersFeature', env => { const { NULL_ADDRESS, MAX_UINT256, NULL_BYTES32, ZERO_AMOUNT } = constants; const GAS_PRICE = new BigNumber('123e9'); const PROTOCOL_FEE_MULTIPLIER = 1337e3; const SINGLE_PROTOCOL_FEE = GAS_PRICE.times(PROTOCOL_FEE_MULTIPLIER); let maker: string; let taker: string; let notMaker: string; let notTaker: string; let contractWalletOwner: string; let contractWalletSigner: string; let zeroEx: IZeroExContract; let verifyingContract: string; let makerToken: TestMintableERC20TokenContract; let takerToken: TestMintableERC20TokenContract; let wethToken: TestMintableERC20TokenContract; let testRfqOriginRegistration: TestRfqOriginRegistrationContract; let contractWallet: TestOrderSignerRegistryWithContractWalletContract; let testUtils: NativeOrdersTestEnvironment; before(async () => { let owner; [owner, maker, taker, notMaker, notTaker, contractWalletOwner, contractWalletSigner] = await env.getAccountAddressesAsync(); [makerToken, takerToken, wethToken] = await Promise.all( [...new Array(3)].map(async () => TestMintableERC20TokenContract.deployFrom0xArtifactAsync( artifacts.TestMintableERC20Token, env.provider, env.txDefaults, artifacts, ), ), ); zeroEx = await fullMigrateAsync( owner, env.provider, { ...env.txDefaults, gasPrice: GAS_PRICE }, {}, { wethAddress: wethToken.address, protocolFeeMultiplier: PROTOCOL_FEE_MULTIPLIER }, { nativeOrders: artifacts.TestNativeOrdersFeature }, ); verifyingContract = zeroEx.address; await Promise.all( [maker, notMaker].map(a => makerToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: a }), ), ); await Promise.all( [taker, notTaker].map(a => takerToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: a }), ), ); testRfqOriginRegistration = await TestRfqOriginRegistrationContract.deployFrom0xArtifactAsync( artifacts.TestRfqOriginRegistration, env.provider, env.txDefaults, artifacts, ); // contract wallet for signer delegation contractWallet = await TestOrderSignerRegistryWithContractWalletContract.deployFrom0xArtifactAsync( artifacts.TestOrderSignerRegistryWithContractWallet, env.provider, { from: contractWalletOwner, }, artifacts, zeroEx.address, ); await contractWallet .approveERC20(makerToken.address, zeroEx.address, MAX_UINT256) .awaitTransactionSuccessAsync({ from: contractWalletOwner }); testUtils = new NativeOrdersTestEnvironment( maker, taker, makerToken, takerToken, zeroEx, GAS_PRICE, SINGLE_PROTOCOL_FEE, env, ); }); function getTestLimitOrder(fields: Partial = {}): LimitOrder { return getRandomLimitOrder({ maker, verifyingContract, chainId: 1337, takerToken: takerToken.address, makerToken: makerToken.address, taker: NULL_ADDRESS, sender: NULL_ADDRESS, ...fields, }); } function getTestRfqOrder(fields: Partial = {}): RfqOrder { return getRandomRfqOrder({ maker, verifyingContract, chainId: 1337, takerToken: takerToken.address, makerToken: makerToken.address, txOrigin: taker, ...fields, }); } describe('getProtocolFeeMultiplier()', () => { it('returns the protocol fee multiplier', async () => { const r = await zeroEx.getProtocolFeeMultiplier().callAsync(); expect(r).to.bignumber.eq(PROTOCOL_FEE_MULTIPLIER); }); }); describe('getLimitOrderHash()', () => { it('returns the correct hash', async () => { const order = getTestLimitOrder(); const hash = await zeroEx.getLimitOrderHash(order).callAsync(); expect(hash).to.eq(order.getHash()); }); }); describe('getRfqOrderHash()', () => { it('returns the correct hash', async () => { const order = getTestRfqOrder(); const hash = await zeroEx.getRfqOrderHash(order).callAsync(); expect(hash).to.eq(order.getHash()); }); }); describe('getLimitOrderInfo()', () => { it('unfilled order', async () => { const order = getTestLimitOrder(); const info = await zeroEx.getLimitOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Fillable, orderHash: order.getHash(), takerTokenFilledAmount: ZERO_AMOUNT, }); }); it('unfilled cancelled order', async () => { const order = getTestLimitOrder(); await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); const info = await zeroEx.getLimitOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Cancelled, orderHash: order.getHash(), takerTokenFilledAmount: ZERO_AMOUNT, }); }); it('unfilled expired order', async () => { const order = getTestLimitOrder({ expiry: createExpiry(-60) }); const info = await zeroEx.getLimitOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Expired, orderHash: order.getHash(), takerTokenFilledAmount: ZERO_AMOUNT, }); }); it('filled then expired order', async () => { const expiry = createExpiry(60); const order = getTestLimitOrder({ expiry }); // Fill the order first. await testUtils.fillLimitOrderAsync(order); // Advance time to expire the order. await env.web3Wrapper.increaseTimeAsync(61); const info = await zeroEx.getLimitOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Filled, // Still reports filled. orderHash: order.getHash(), takerTokenFilledAmount: order.takerAmount, }); }); it('filled order', async () => { const order = getTestLimitOrder(); // Fill the order first. await testUtils.fillLimitOrderAsync(order); const info = await zeroEx.getLimitOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Filled, orderHash: order.getHash(), takerTokenFilledAmount: order.takerAmount, }); }); it('partially filled order', async () => { const order = getTestLimitOrder(); const fillAmount = order.takerAmount.minus(1); // Fill the order first. await testUtils.fillLimitOrderAsync(order, { fillAmount }); const info = await zeroEx.getLimitOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Fillable, orderHash: order.getHash(), takerTokenFilledAmount: fillAmount, }); }); it('filled then cancelled order', async () => { const order = getTestLimitOrder(); // Fill the order first. await testUtils.fillLimitOrderAsync(order); await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); const info = await zeroEx.getLimitOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Filled, // Still reports filled. orderHash: order.getHash(), takerTokenFilledAmount: order.takerAmount, }); }); it('partially filled then cancelled order', async () => { const order = getTestLimitOrder(); const fillAmount = order.takerAmount.minus(1); // Fill the order first. await testUtils.fillLimitOrderAsync(order, { fillAmount }); await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); const info = await zeroEx.getLimitOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Cancelled, orderHash: order.getHash(), takerTokenFilledAmount: fillAmount, }); }); }); describe('getRfqOrderInfo()', () => { it('unfilled order', async () => { const order = getTestRfqOrder(); const info = await zeroEx.getRfqOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Fillable, orderHash: order.getHash(), takerTokenFilledAmount: ZERO_AMOUNT, }); }); it('unfilled cancelled order', async () => { const order = getTestRfqOrder(); await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); const info = await zeroEx.getRfqOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Cancelled, orderHash: order.getHash(), takerTokenFilledAmount: ZERO_AMOUNT, }); }); it('unfilled expired order', async () => { const expiry = createExpiry(-60); const order = getTestRfqOrder({ expiry }); const info = await zeroEx.getRfqOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Expired, orderHash: order.getHash(), takerTokenFilledAmount: ZERO_AMOUNT, }); }); it('filled then expired order', async () => { const expiry = createExpiry(60); const order = getTestRfqOrder({ expiry }); await testUtils.prepareBalancesForOrdersAsync([order]); const sig = await order.getSignatureWithProviderAsync(env.provider); // Fill the order first. await zeroEx.fillRfqOrder(order, sig, order.takerAmount).awaitTransactionSuccessAsync({ from: taker }); // Advance time to expire the order. await env.web3Wrapper.increaseTimeAsync(61); const info = await zeroEx.getRfqOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Filled, // Still reports filled. orderHash: order.getHash(), takerTokenFilledAmount: order.takerAmount, }); }); it('filled order', async () => { const order = getTestRfqOrder(); // Fill the order first. await testUtils.fillRfqOrderAsync(order, order.takerAmount, taker); const info = await zeroEx.getRfqOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Filled, orderHash: order.getHash(), takerTokenFilledAmount: order.takerAmount, }); }); it('partially filled order', async () => { const order = getTestRfqOrder(); const fillAmount = order.takerAmount.minus(1); // Fill the order first. await testUtils.fillRfqOrderAsync(order, fillAmount); const info = await zeroEx.getRfqOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Fillable, orderHash: order.getHash(), takerTokenFilledAmount: fillAmount, }); }); it('filled then cancelled order', async () => { const order = getTestRfqOrder(); // Fill the order first. await testUtils.fillRfqOrderAsync(order); await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); const info = await zeroEx.getRfqOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Filled, // Still reports filled. orderHash: order.getHash(), takerTokenFilledAmount: order.takerAmount, }); }); it('partially filled then cancelled order', async () => { const order = getTestRfqOrder(); const fillAmount = order.takerAmount.minus(1); // Fill the order first. await testUtils.fillRfqOrderAsync(order, fillAmount); await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); const info = await zeroEx.getRfqOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Cancelled, orderHash: order.getHash(), takerTokenFilledAmount: fillAmount, }); }); it('invalid origin', async () => { const order = getTestRfqOrder({ txOrigin: NULL_ADDRESS }); const info = await zeroEx.getRfqOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Invalid, orderHash: order.getHash(), takerTokenFilledAmount: ZERO_AMOUNT, }); }); }); describe('cancelLimitOrder()', async () => { it('can cancel an unfilled order', async () => { const order = getTestLimitOrder(); const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); verifyEventsFromLogs( receipt.logs, [{ maker: order.maker, orderHash: order.getHash() }], IZeroExEvents.OrderCancelled, ); const { status } = await zeroEx.getLimitOrderInfo(order).callAsync(); expect(status).to.eq(OrderStatus.Cancelled); }); it('can cancel a fully filled order', async () => { const order = getTestLimitOrder(); await testUtils.fillLimitOrderAsync(order); const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); verifyEventsFromLogs( receipt.logs, [{ maker: order.maker, orderHash: order.getHash() }], IZeroExEvents.OrderCancelled, ); const { status } = await zeroEx.getLimitOrderInfo(order).callAsync(); expect(status).to.eq(OrderStatus.Filled); // Still reports filled. }); it('can cancel a partially filled order', async () => { const order = getTestLimitOrder(); await testUtils.fillLimitOrderAsync(order, { fillAmount: order.takerAmount.minus(1) }); const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); verifyEventsFromLogs( receipt.logs, [{ maker: order.maker, orderHash: order.getHash() }], IZeroExEvents.OrderCancelled, ); const { status } = await zeroEx.getLimitOrderInfo(order).callAsync(); expect(status).to.eq(OrderStatus.Cancelled); }); it('can cancel an expired order', async () => { const expiry = createExpiry(-60); const order = getTestLimitOrder({ expiry }); const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); verifyEventsFromLogs( receipt.logs, [{ maker: order.maker, orderHash: order.getHash() }], IZeroExEvents.OrderCancelled, ); const { status } = await zeroEx.getLimitOrderInfo(order).callAsync(); expect(status).to.eq(OrderStatus.Cancelled); }); it('can cancel a cancelled order', async () => { const order = getTestLimitOrder(); await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); verifyEventsFromLogs( receipt.logs, [{ maker: order.maker, orderHash: order.getHash() }], IZeroExEvents.OrderCancelled, ); const { status } = await zeroEx.getLimitOrderInfo(order).callAsync(); expect(status).to.eq(OrderStatus.Cancelled); }); it("cannot cancel someone else's order", async () => { const order = getTestLimitOrder(); const tx = zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: notMaker }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OnlyOrderMakerAllowed(order.getHash(), notMaker, order.maker), ); }); }); describe('cancelRfqOrder()', async () => { it('can cancel an unfilled order', async () => { const order = getTestRfqOrder(); const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); verifyEventsFromLogs( receipt.logs, [{ maker: order.maker, orderHash: order.getHash() }], IZeroExEvents.OrderCancelled, ); const { status } = await zeroEx.getRfqOrderInfo(order).callAsync(); expect(status).to.eq(OrderStatus.Cancelled); }); it('can cancel a fully filled order', async () => { const order = getTestRfqOrder(); await testUtils.fillRfqOrderAsync(order); const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); verifyEventsFromLogs( receipt.logs, [{ maker: order.maker, orderHash: order.getHash() }], IZeroExEvents.OrderCancelled, ); const { status } = await zeroEx.getRfqOrderInfo(order).callAsync(); expect(status).to.eq(OrderStatus.Filled); // Still reports filled. }); it('can cancel a partially filled order', async () => { const order = getTestRfqOrder(); await testUtils.fillRfqOrderAsync(order, order.takerAmount.minus(1)); const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); verifyEventsFromLogs( receipt.logs, [{ maker: order.maker, orderHash: order.getHash() }], IZeroExEvents.OrderCancelled, ); const { status } = await zeroEx.getRfqOrderInfo(order).callAsync(); expect(status).to.eq(OrderStatus.Cancelled); // Still reports filled. }); it('can cancel an expired order', async () => { const expiry = createExpiry(-60); const order = getTestRfqOrder({ expiry }); const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); verifyEventsFromLogs( receipt.logs, [{ maker: order.maker, orderHash: order.getHash() }], IZeroExEvents.OrderCancelled, ); const { status } = await zeroEx.getRfqOrderInfo(order).callAsync(); expect(status).to.eq(OrderStatus.Cancelled); }); it('can cancel a cancelled order', async () => { const order = getTestRfqOrder(); await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); verifyEventsFromLogs( receipt.logs, [{ maker: order.maker, orderHash: order.getHash() }], IZeroExEvents.OrderCancelled, ); const { status } = await zeroEx.getRfqOrderInfo(order).callAsync(); expect(status).to.eq(OrderStatus.Cancelled); }); it("cannot cancel someone else's order", async () => { const order = getTestRfqOrder(); const tx = zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: notMaker }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OnlyOrderMakerAllowed(order.getHash(), notMaker, order.maker), ); }); }); describe('batchCancelLimitOrders()', async () => { it('can cancel multiple orders', async () => { const orders = [...new Array(3)].map(() => getTestLimitOrder()); const receipt = await zeroEx.batchCancelLimitOrders(orders).awaitTransactionSuccessAsync({ from: maker }); verifyEventsFromLogs( receipt.logs, orders.map(o => ({ maker: o.maker, orderHash: o.getHash() })), IZeroExEvents.OrderCancelled, ); const infos = await Promise.all(orders.map(o => zeroEx.getLimitOrderInfo(o).callAsync())); expect(infos.map(i => i.status)).to.deep.eq(infos.map(() => OrderStatus.Cancelled)); }); it("cannot cancel someone else's orders", async () => { const orders = [...new Array(3)].map(() => getTestLimitOrder()); const tx = zeroEx.batchCancelLimitOrders(orders).awaitTransactionSuccessAsync({ from: notMaker }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OnlyOrderMakerAllowed(orders[0].getHash(), notMaker, orders[0].maker), ); }); }); describe('batchCancelRfqOrders()', async () => { it('can cancel multiple orders', async () => { const orders = [...new Array(3)].map(() => getTestRfqOrder()); const receipt = await zeroEx.batchCancelRfqOrders(orders).awaitTransactionSuccessAsync({ from: maker }); verifyEventsFromLogs( receipt.logs, orders.map(o => ({ maker: o.maker, orderHash: o.getHash() })), IZeroExEvents.OrderCancelled, ); const infos = await Promise.all(orders.map(o => zeroEx.getRfqOrderInfo(o).callAsync())); expect(infos.map(i => i.status)).to.deep.eq(infos.map(() => OrderStatus.Cancelled)); }); it("cannot cancel someone else's orders", async () => { const orders = [...new Array(3)].map(() => getTestRfqOrder()); const tx = zeroEx.batchCancelRfqOrders(orders).awaitTransactionSuccessAsync({ from: notMaker }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OnlyOrderMakerAllowed(orders[0].getHash(), notMaker, orders[0].maker), ); }); }); describe('cancelPairOrders()', async () => { it('can cancel multiple limit orders of the same pair with salt < minValidSalt', async () => { const orders = [...new Array(3)].map((_v, i) => getTestLimitOrder().clone({ salt: new BigNumber(i) })); // Cancel the first two orders. const minValidSalt = orders[2].salt; const receipt = await zeroEx .cancelPairLimitOrders(makerToken.address, takerToken.address, minValidSalt) .awaitTransactionSuccessAsync({ from: maker }); verifyEventsFromLogs( receipt.logs, [ { maker, makerToken: makerToken.address, takerToken: takerToken.address, minValidSalt, }, ], IZeroExEvents.PairCancelledLimitOrders, ); const statuses = (await Promise.all(orders.map(o => zeroEx.getLimitOrderInfo(o).callAsync()))).map( oi => oi.status, ); expect(statuses).to.deep.eq([OrderStatus.Cancelled, OrderStatus.Cancelled, OrderStatus.Fillable]); }); it('does not cancel limit orders of a different pair', async () => { const order = getRandomLimitOrder({ salt: new BigNumber(1) }); // Cancel salts <= the order's, but flip the tokens to be a different // pair. const minValidSalt = order.salt.plus(1); await zeroEx .cancelPairLimitOrders(takerToken.address, makerToken.address, minValidSalt) .awaitTransactionSuccessAsync({ from: maker }); const { status } = await zeroEx.getLimitOrderInfo(order).callAsync(); expect(status).to.eq(OrderStatus.Fillable); }); it('can cancel multiple RFQ orders of the same pair with salt < minValidSalt', async () => { const orders = [...new Array(3)].map((_v, i) => getTestRfqOrder().clone({ salt: new BigNumber(i) })); // Cancel the first two orders. const minValidSalt = orders[2].salt; const receipt = await zeroEx .cancelPairRfqOrders(makerToken.address, takerToken.address, minValidSalt) .awaitTransactionSuccessAsync({ from: maker }); verifyEventsFromLogs( receipt.logs, [ { maker, makerToken: makerToken.address, takerToken: takerToken.address, minValidSalt, }, ], IZeroExEvents.PairCancelledRfqOrders, ); const statuses = (await Promise.all(orders.map(o => zeroEx.getRfqOrderInfo(o).callAsync()))).map( oi => oi.status, ); expect(statuses).to.deep.eq([OrderStatus.Cancelled, OrderStatus.Cancelled, OrderStatus.Fillable]); }); it('does not cancel RFQ orders of a different pair', async () => { const order = getRandomRfqOrder({ salt: new BigNumber(1) }); // Cancel salts <= the order's, but flip the tokens to be a different // pair. const minValidSalt = order.salt.plus(1); await zeroEx .cancelPairRfqOrders(takerToken.address, makerToken.address, minValidSalt) .awaitTransactionSuccessAsync({ from: maker }); const { status } = await zeroEx.getRfqOrderInfo(order).callAsync(); expect(status).to.eq(OrderStatus.Fillable); }); }); describe('batchCancelPairOrders()', async () => { it('can cancel multiple limit order pairs', async () => { const orders = [ getTestLimitOrder({ salt: new BigNumber(1) }), // Flip the tokens for the other order. getTestLimitOrder({ makerToken: takerToken.address, takerToken: makerToken.address, salt: new BigNumber(1), }), ]; const minValidSalt = new BigNumber(2); const receipt = await zeroEx .batchCancelPairLimitOrders( [makerToken.address, takerToken.address], [takerToken.address, makerToken.address], [minValidSalt, minValidSalt], ) .awaitTransactionSuccessAsync({ from: maker }); verifyEventsFromLogs( receipt.logs, [ { maker, makerToken: makerToken.address, takerToken: takerToken.address, minValidSalt, }, { maker, makerToken: takerToken.address, takerToken: makerToken.address, minValidSalt, }, ], IZeroExEvents.PairCancelledLimitOrders, ); const statuses = (await Promise.all(orders.map(o => zeroEx.getLimitOrderInfo(o).callAsync()))).map( oi => oi.status, ); expect(statuses).to.deep.eq([OrderStatus.Cancelled, OrderStatus.Cancelled]); }); it('can cancel multiple RFQ order pairs', async () => { const orders = [ getTestRfqOrder({ salt: new BigNumber(1) }), // Flip the tokens for the other order. getTestRfqOrder({ makerToken: takerToken.address, takerToken: makerToken.address, salt: new BigNumber(1), }), ]; const minValidSalt = new BigNumber(2); const receipt = await zeroEx .batchCancelPairRfqOrders( [makerToken.address, takerToken.address], [takerToken.address, makerToken.address], [minValidSalt, minValidSalt], ) .awaitTransactionSuccessAsync({ from: maker }); verifyEventsFromLogs( receipt.logs, [ { maker, makerToken: makerToken.address, takerToken: takerToken.address, minValidSalt, }, { maker, makerToken: takerToken.address, takerToken: makerToken.address, minValidSalt, }, ], IZeroExEvents.PairCancelledRfqOrders, ); const statuses = (await Promise.all(orders.map(o => zeroEx.getRfqOrderInfo(o).callAsync()))).map( oi => oi.status, ); expect(statuses).to.deep.eq([OrderStatus.Cancelled, OrderStatus.Cancelled]); }); }); async function assertExpectedFinalBalancesFromLimitOrderFillAsync( order: LimitOrder, opts: Partial<{ takerTokenFillAmount: BigNumber; takerTokenAlreadyFilledAmount: BigNumber; receipt: TransactionReceiptWithDecodedLogs; }> = {}, ): Promise { const { takerTokenFillAmount, takerTokenAlreadyFilledAmount, receipt } = { takerTokenFillAmount: order.takerAmount, takerTokenAlreadyFilledAmount: ZERO_AMOUNT, receipt: undefined, ...opts, }; const { makerTokenFilledAmount, takerTokenFilledAmount, takerTokenFeeFilledAmount } = computeLimitOrderFilledAmounts(order, takerTokenFillAmount, takerTokenAlreadyFilledAmount); const makerBalance = await takerToken.balanceOf(maker).callAsync(); const takerBalance = await makerToken.balanceOf(taker).callAsync(); const feeRecipientBalance = await takerToken.balanceOf(order.feeRecipient).callAsync(); expect(makerBalance).to.bignumber.eq(takerTokenFilledAmount); expect(takerBalance).to.bignumber.eq(makerTokenFilledAmount); expect(feeRecipientBalance).to.bignumber.eq(takerTokenFeeFilledAmount); if (receipt) { const balanceOfTakerNow = await env.web3Wrapper.getBalanceInWeiAsync(taker); const balanceOfTakerBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker, receipt.blockNumber - 1); const protocolFee = order.taker === NULL_ADDRESS ? SINGLE_PROTOCOL_FEE : 0; const totalCost = GAS_PRICE.times(receipt.gasUsed).plus(protocolFee); expect(balanceOfTakerBefore.minus(totalCost)).to.bignumber.eq(balanceOfTakerNow); } } describe('fillLimitOrder()', () => { it('can fully fill an order', async () => { const order = getTestLimitOrder(); const receipt = await testUtils.fillLimitOrderAsync(order); verifyEventsFromLogs( receipt.logs, [testUtils.createLimitOrderFilledEventArgs(order)], IZeroExEvents.LimitOrderFilled, ); assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), { orderHash: order.getHash(), status: OrderStatus.Filled, takerTokenFilledAmount: order.takerAmount, }); await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { receipt }); }); it('can partially fill an order', async () => { const order = getTestLimitOrder(); const fillAmount = order.takerAmount.minus(1); const receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount }); verifyEventsFromLogs( receipt.logs, [testUtils.createLimitOrderFilledEventArgs(order, fillAmount)], IZeroExEvents.LimitOrderFilled, ); assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), { orderHash: order.getHash(), status: OrderStatus.Fillable, takerTokenFilledAmount: fillAmount, }); await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { takerTokenFillAmount: fillAmount, }); }); it('can fully fill an order in two steps', async () => { const order = getTestLimitOrder(); let fillAmount = order.takerAmount.dividedToIntegerBy(2); let receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount }); verifyEventsFromLogs( receipt.logs, [testUtils.createLimitOrderFilledEventArgs(order, fillAmount)], IZeroExEvents.LimitOrderFilled, ); const alreadyFilledAmount = fillAmount; fillAmount = order.takerAmount.minus(fillAmount); receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount }); verifyEventsFromLogs( receipt.logs, [testUtils.createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)], IZeroExEvents.LimitOrderFilled, ); assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), { orderHash: order.getHash(), status: OrderStatus.Filled, takerTokenFilledAmount: order.takerAmount, }); }); it('clamps fill amount to remaining available', async () => { const order = getTestLimitOrder(); const fillAmount = order.takerAmount.plus(1); const receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount }); verifyEventsFromLogs( receipt.logs, [testUtils.createLimitOrderFilledEventArgs(order, fillAmount)], IZeroExEvents.LimitOrderFilled, ); assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), { orderHash: order.getHash(), status: OrderStatus.Filled, takerTokenFilledAmount: order.takerAmount, }); await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { takerTokenFillAmount: fillAmount, }); }); it('clamps fill amount to remaining available in partial filled order', async () => { const order = getTestLimitOrder(); let fillAmount = order.takerAmount.dividedToIntegerBy(2); let receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount }); verifyEventsFromLogs( receipt.logs, [testUtils.createLimitOrderFilledEventArgs(order, fillAmount)], IZeroExEvents.LimitOrderFilled, ); const alreadyFilledAmount = fillAmount; fillAmount = order.takerAmount.minus(fillAmount).plus(1); receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount }); verifyEventsFromLogs( receipt.logs, [testUtils.createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)], IZeroExEvents.LimitOrderFilled, ); assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), { orderHash: order.getHash(), status: OrderStatus.Filled, takerTokenFilledAmount: order.takerAmount, }); }); it('cannot fill an expired order', async () => { const order = getTestLimitOrder({ expiry: createExpiry(-60) }); const tx = testUtils.fillLimitOrderAsync(order); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Expired), ); }); it('cannot fill a cancelled order', async () => { const order = getTestLimitOrder(); await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); const tx = testUtils.fillLimitOrderAsync(order); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled), ); }); it('cannot fill a salt/pair cancelled order', async () => { const order = getTestLimitOrder(); await zeroEx .cancelPairLimitOrders(makerToken.address, takerToken.address, order.salt.plus(1)) .awaitTransactionSuccessAsync({ from: maker }); const tx = testUtils.fillLimitOrderAsync(order); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled), ); }); it('non-taker cannot fill order', async () => { const order = getTestLimitOrder({ taker }); const tx = testUtils.fillLimitOrderAsync(order, { fillAmount: order.takerAmount, taker: notTaker }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableByTakerError(order.getHash(), notTaker, order.taker), ); }); it('non-sender cannot fill order', async () => { const order = getTestLimitOrder({ sender: taker }); const tx = testUtils.fillLimitOrderAsync(order, { fillAmount: order.takerAmount, taker: notTaker }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableBySenderError(order.getHash(), notTaker, order.sender), ); }); it('cannot fill order with bad signature', async () => { const order = getTestLimitOrder(); // Overwrite chainId to result in a different hash and therefore different // signature. const tx = testUtils.fillLimitOrderAsync(order.clone({ chainId: 1234 })); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), undefined, order.maker), ); }); // TODO: dekz Ganache gasPrice opcode is returning 0, cannot influence it up to test this case it.skip('fails if no protocol fee attached', async () => { const order = getTestLimitOrder(); await testUtils.prepareBalancesForOrdersAsync([order]); const tx = zeroEx .fillLimitOrder( order, await order.getSignatureWithProviderAsync(env.provider), new BigNumber(order.takerAmount), ) .awaitTransactionSuccessAsync({ from: taker, value: ZERO_AMOUNT }); // The exact revert error depends on whether we are still doing a // token spender fallthroigh, so we won't get too specific. return expect(tx).to.revertWith(new AnyRevertError()); }); it('refunds excess protocol fee', async () => { const order = getTestLimitOrder(); const receipt = await testUtils.fillLimitOrderAsync(order, { protocolFee: SINGLE_PROTOCOL_FEE.plus(1) }); verifyEventsFromLogs( receipt.logs, [testUtils.createLimitOrderFilledEventArgs(order)], IZeroExEvents.LimitOrderFilled, ); await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { receipt }); }); }); describe('registerAllowedRfqOrigins()', () => { it('cannot register through a contract', async () => { const tx = testRfqOriginRegistration .registerAllowedRfqOrigins(zeroEx.address, [], true) .awaitTransactionSuccessAsync(); expect(tx).to.revertWith('NativeOrdersFeature/NO_CONTRACT_ORIGINS'); }); }); async function assertExpectedFinalBalancesFromRfqOrderFillAsync( order: RfqOrder, takerTokenFillAmount: BigNumber = order.takerAmount, takerTokenAlreadyFilledAmount: BigNumber = ZERO_AMOUNT, ): Promise { const { makerTokenFilledAmount, takerTokenFilledAmount } = computeRfqOrderFilledAmounts( order, takerTokenFillAmount, takerTokenAlreadyFilledAmount, ); const makerBalance = await takerToken.balanceOf(maker).callAsync(); const takerBalance = await makerToken.balanceOf(taker).callAsync(); expect(makerBalance).to.bignumber.eq(takerTokenFilledAmount); expect(takerBalance).to.bignumber.eq(makerTokenFilledAmount); } describe('fillRfqOrder()', () => { it('can fully fill an order', async () => { const order = getTestRfqOrder(); const receipt = await testUtils.fillRfqOrderAsync(order); verifyEventsFromLogs( receipt.logs, [testUtils.createRfqOrderFilledEventArgs(order)], IZeroExEvents.RfqOrderFilled, ); assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), { orderHash: order.getHash(), status: OrderStatus.Filled, takerTokenFilledAmount: order.takerAmount, }); await assertExpectedFinalBalancesFromRfqOrderFillAsync(order); }); it('can partially fill an order', async () => { const order = getTestRfqOrder(); const fillAmount = order.takerAmount.minus(1); const receipt = await testUtils.fillRfqOrderAsync(order, fillAmount); verifyEventsFromLogs( receipt.logs, [testUtils.createRfqOrderFilledEventArgs(order, fillAmount)], IZeroExEvents.RfqOrderFilled, ); assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), { orderHash: order.getHash(), status: OrderStatus.Fillable, takerTokenFilledAmount: fillAmount, }); await assertExpectedFinalBalancesFromRfqOrderFillAsync(order, fillAmount); }); it('can fully fill an order in two steps', async () => { const order = getTestRfqOrder(); let fillAmount = order.takerAmount.dividedToIntegerBy(2); let receipt = await testUtils.fillRfqOrderAsync(order, fillAmount); verifyEventsFromLogs( receipt.logs, [testUtils.createRfqOrderFilledEventArgs(order, fillAmount)], IZeroExEvents.RfqOrderFilled, ); const alreadyFilledAmount = fillAmount; fillAmount = order.takerAmount.minus(fillAmount); receipt = await testUtils.fillRfqOrderAsync(order, fillAmount); verifyEventsFromLogs( receipt.logs, [testUtils.createRfqOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)], IZeroExEvents.RfqOrderFilled, ); assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), { orderHash: order.getHash(), status: OrderStatus.Filled, takerTokenFilledAmount: order.takerAmount, }); }); it('clamps fill amount to remaining available', async () => { const order = getTestRfqOrder(); const fillAmount = order.takerAmount.plus(1); const receipt = await testUtils.fillRfqOrderAsync(order, fillAmount); verifyEventsFromLogs( receipt.logs, [testUtils.createRfqOrderFilledEventArgs(order, fillAmount)], IZeroExEvents.RfqOrderFilled, ); assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), { orderHash: order.getHash(), status: OrderStatus.Filled, takerTokenFilledAmount: order.takerAmount, }); await assertExpectedFinalBalancesFromRfqOrderFillAsync(order, fillAmount); }); it('clamps fill amount to remaining available in partial filled order', async () => { const order = getTestRfqOrder(); let fillAmount = order.takerAmount.dividedToIntegerBy(2); let receipt = await testUtils.fillRfqOrderAsync(order, fillAmount); verifyEventsFromLogs( receipt.logs, [testUtils.createRfqOrderFilledEventArgs(order, fillAmount)], IZeroExEvents.RfqOrderFilled, ); const alreadyFilledAmount = fillAmount; fillAmount = order.takerAmount.minus(fillAmount).plus(1); receipt = await testUtils.fillRfqOrderAsync(order, fillAmount); verifyEventsFromLogs( receipt.logs, [testUtils.createRfqOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)], IZeroExEvents.RfqOrderFilled, ); assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), { orderHash: order.getHash(), status: OrderStatus.Filled, takerTokenFilledAmount: order.takerAmount, }); }); it('cannot fill an order with wrong tx.origin', async () => { const order = getTestRfqOrder(); const tx = testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), notTaker, taker), ); }); it('can fill an order from a different tx.origin if registered', async () => { const order = getTestRfqOrder(); const receipt = await zeroEx .registerAllowedRfqOrigins([notTaker], true) .awaitTransactionSuccessAsync({ from: taker }); verifyEventsFromLogs( receipt.logs, [ { origin: taker, addrs: [notTaker], allowed: true, }, ], IZeroExEvents.RfqOrderOriginsAllowed, ); return testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker); }); it('cannot fill an order with registered then unregistered tx.origin', async () => { const order = getTestRfqOrder(); await zeroEx.registerAllowedRfqOrigins([notTaker], true).awaitTransactionSuccessAsync({ from: taker }); const receipt = await zeroEx .registerAllowedRfqOrigins([notTaker], false) .awaitTransactionSuccessAsync({ from: taker }); verifyEventsFromLogs( receipt.logs, [ { origin: taker, addrs: [notTaker], allowed: false, }, ], IZeroExEvents.RfqOrderOriginsAllowed, ); const tx = testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), notTaker, taker), ); }); it('cannot fill an order with a zero tx.origin', async () => { const order = getTestRfqOrder({ txOrigin: NULL_ADDRESS }); const tx = testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Invalid), ); }); it('non-taker cannot fill order', async () => { const order = getTestRfqOrder({ taker, txOrigin: notTaker }); const tx = testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableByTakerError(order.getHash(), notTaker, order.taker), ); }); it('cannot fill an expired order', async () => { const order = getTestRfqOrder({ expiry: createExpiry(-60) }); const tx = testUtils.fillRfqOrderAsync(order); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Expired), ); }); it('cannot fill a cancelled order', async () => { const order = getTestRfqOrder(); await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); const tx = testUtils.fillRfqOrderAsync(order); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled), ); }); it('cannot fill a salt/pair cancelled order', async () => { const order = getTestRfqOrder(); await zeroEx .cancelPairRfqOrders(makerToken.address, takerToken.address, order.salt.plus(1)) .awaitTransactionSuccessAsync({ from: maker }); const tx = testUtils.fillRfqOrderAsync(order); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled), ); }); it('cannot fill order with bad signature', async () => { const order = getTestRfqOrder(); // Overwrite chainId to result in a different hash and therefore different // signature. const tx = testUtils.fillRfqOrderAsync(order.clone({ chainId: 1234 })); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), undefined, order.maker), ); }); it('fails if ETH is attached', async () => { const order = getTestRfqOrder(); await testUtils.prepareBalancesForOrdersAsync([order], taker); const tx = zeroEx .fillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount) .awaitTransactionSuccessAsync({ from: taker, value: 1 }); // This will revert at the language level because the fill function is not payable. return expect(tx).to.be.rejectedWith('revert'); }); }); describe('fillOrKillLimitOrder()', () => { it('can fully fill an order', async () => { const order = getTestLimitOrder(); await testUtils.prepareBalancesForOrdersAsync([order]); const receipt = await zeroEx .fillOrKillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount) .awaitTransactionSuccessAsync({ from: taker, value: SINGLE_PROTOCOL_FEE }); verifyEventsFromLogs( receipt.logs, [testUtils.createLimitOrderFilledEventArgs(order)], IZeroExEvents.LimitOrderFilled, ); }); it('reverts if cannot fill the exact amount', async () => { const order = getTestLimitOrder(); await testUtils.prepareBalancesForOrdersAsync([order]); const fillAmount = order.takerAmount.plus(1); const tx = zeroEx .fillOrKillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), fillAmount) .awaitTransactionSuccessAsync({ from: taker, value: SINGLE_PROTOCOL_FEE }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.FillOrKillFailedError(order.getHash(), order.takerAmount, fillAmount), ); }); it('refunds excess protocol fee', async () => { const order = getTestLimitOrder(); await testUtils.prepareBalancesForOrdersAsync([order]); const takerBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker); const receipt = await zeroEx .fillOrKillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount) .awaitTransactionSuccessAsync({ from: taker, value: SINGLE_PROTOCOL_FEE.plus(1) }); const takerBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker); const totalCost = GAS_PRICE.times(receipt.gasUsed).plus(SINGLE_PROTOCOL_FEE); expect(takerBalanceBefore.minus(totalCost)).to.bignumber.eq(takerBalanceAfter); }); }); describe('fillOrKillRfqOrder()', () => { it('can fully fill an order', async () => { const order = getTestRfqOrder(); await testUtils.prepareBalancesForOrdersAsync([order]); const receipt = await zeroEx .fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount) .awaitTransactionSuccessAsync({ from: taker }); verifyEventsFromLogs( receipt.logs, [testUtils.createRfqOrderFilledEventArgs(order)], IZeroExEvents.RfqOrderFilled, ); }); it('reverts if cannot fill the exact amount', async () => { const order = getTestRfqOrder(); await testUtils.prepareBalancesForOrdersAsync([order]); const fillAmount = order.takerAmount.plus(1); const tx = zeroEx .fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), fillAmount) .awaitTransactionSuccessAsync({ from: taker }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.FillOrKillFailedError(order.getHash(), order.takerAmount, fillAmount), ); }); it('fails if ETH is attached', async () => { const order = getTestRfqOrder(); await testUtils.prepareBalancesForOrdersAsync([order]); const tx = zeroEx .fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount) .awaitTransactionSuccessAsync({ from: taker, value: 1 }); // This will revert at the language level because the fill function is not payable. return expect(tx).to.be.rejectedWith('revert'); }); }); async function fundOrderMakerAsync( order: LimitOrder | RfqOrder, balance: BigNumber = order.makerAmount, allowance: BigNumber = order.makerAmount, ): Promise { await makerToken.burn(maker, await makerToken.balanceOf(maker).callAsync()).awaitTransactionSuccessAsync(); await makerToken.mint(maker, balance).awaitTransactionSuccessAsync(); await makerToken.approve(zeroEx.address, allowance).awaitTransactionSuccessAsync({ from: maker }); } describe('getLimitOrderRelevantState()', () => { it('works with an empty order', async () => { const order = getTestLimitOrder({ takerAmount: ZERO_AMOUNT, }); await fundOrderMakerAsync(order); const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx .getLimitOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider)) .callAsync(); expect(orderInfo).to.deep.eq({ orderHash: order.getHash(), status: OrderStatus.Filled, takerTokenFilledAmount: ZERO_AMOUNT, }); expect(fillableTakerAmount).to.bignumber.eq(0); expect(isSignatureValid).to.eq(true); }); it('works with cancelled order', async () => { const order = getTestLimitOrder(); await fundOrderMakerAsync(order); await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx .getLimitOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider)) .callAsync(); expect(orderInfo).to.deep.eq({ orderHash: order.getHash(), status: OrderStatus.Cancelled, takerTokenFilledAmount: ZERO_AMOUNT, }); expect(fillableTakerAmount).to.bignumber.eq(0); expect(isSignatureValid).to.eq(true); }); it('works with a bad signature', async () => { const order = getTestLimitOrder(); await fundOrderMakerAsync(order); const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx .getLimitOrderRelevantState( order, await order.clone({ maker: notMaker }).getSignatureWithProviderAsync(env.provider), ) .callAsync(); expect(orderInfo).to.deep.eq({ orderHash: order.getHash(), status: OrderStatus.Fillable, takerTokenFilledAmount: ZERO_AMOUNT, }); expect(fillableTakerAmount).to.bignumber.eq(order.takerAmount); expect(isSignatureValid).to.eq(false); }); it('works with an unfilled order', async () => { const order = getTestLimitOrder(); await fundOrderMakerAsync(order); const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx .getLimitOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider)) .callAsync(); expect(orderInfo).to.deep.eq({ orderHash: order.getHash(), status: OrderStatus.Fillable, takerTokenFilledAmount: ZERO_AMOUNT, }); expect(fillableTakerAmount).to.bignumber.eq(order.takerAmount); expect(isSignatureValid).to.eq(true); }); it('works with a fully filled order', async () => { const order = getTestLimitOrder(); // Fully Fund maker and taker. await fundOrderMakerAsync(order); await takerToken .mint(taker, order.takerAmount.plus(order.takerTokenFeeAmount)) .awaitTransactionSuccessAsync(); await testUtils.fillLimitOrderAsync(order); // Partially fill the order. const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx .getLimitOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider)) .callAsync(); expect(orderInfo).to.deep.eq({ orderHash: order.getHash(), status: OrderStatus.Filled, takerTokenFilledAmount: order.takerAmount, }); expect(fillableTakerAmount).to.bignumber.eq(0); expect(isSignatureValid).to.eq(true); }); it('works with an under-funded, partially-filled order', async () => { const order = getTestLimitOrder(); // Fully Fund maker and taker. await fundOrderMakerAsync(order); await takerToken .mint(taker, order.takerAmount.plus(order.takerTokenFeeAmount)) .awaitTransactionSuccessAsync(); // Partially fill the order. const fillAmount = getRandomPortion(order.takerAmount); await testUtils.fillLimitOrderAsync(order, { fillAmount }); // Reduce maker funds to be < remaining. const remainingMakerAmount = getFillableMakerTokenAmount(order, fillAmount); const balance = getRandomPortion(remainingMakerAmount); const allowance = getRandomPortion(remainingMakerAmount); await fundOrderMakerAsync(order, balance, allowance); // Get order state. const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx .getLimitOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider)) .callAsync(); expect(orderInfo).to.deep.eq({ orderHash: order.getHash(), status: OrderStatus.Fillable, takerTokenFilledAmount: fillAmount, }); expect(fillableTakerAmount).to.bignumber.eq( getActualFillableTakerTokenAmount(order, balance, allowance, fillAmount), ); expect(isSignatureValid).to.eq(true); }); }); describe('getRfqOrderRelevantState()', () => { it('works with an empty order', async () => { const order = getTestRfqOrder({ takerAmount: ZERO_AMOUNT, }); await fundOrderMakerAsync(order); const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx .getRfqOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider)) .callAsync(); expect(orderInfo).to.deep.eq({ orderHash: order.getHash(), status: OrderStatus.Filled, takerTokenFilledAmount: ZERO_AMOUNT, }); expect(fillableTakerAmount).to.bignumber.eq(0); expect(isSignatureValid).to.eq(true); }); it('works with cancelled order', async () => { const order = getTestRfqOrder(); await fundOrderMakerAsync(order); await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx .getRfqOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider)) .callAsync(); expect(orderInfo).to.deep.eq({ orderHash: order.getHash(), status: OrderStatus.Cancelled, takerTokenFilledAmount: ZERO_AMOUNT, }); expect(fillableTakerAmount).to.bignumber.eq(0); expect(isSignatureValid).to.eq(true); }); it('works with a bad signature', async () => { const order = getTestRfqOrder(); await fundOrderMakerAsync(order); const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx .getRfqOrderRelevantState( order, await order.clone({ maker: notMaker }).getSignatureWithProviderAsync(env.provider), ) .callAsync(); expect(orderInfo).to.deep.eq({ orderHash: order.getHash(), status: OrderStatus.Fillable, takerTokenFilledAmount: ZERO_AMOUNT, }); expect(fillableTakerAmount).to.bignumber.eq(order.takerAmount); expect(isSignatureValid).to.eq(false); }); it('works with an unfilled order', async () => { const order = getTestRfqOrder(); await fundOrderMakerAsync(order); const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx .getRfqOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider)) .callAsync(); expect(orderInfo).to.deep.eq({ orderHash: order.getHash(), status: OrderStatus.Fillable, takerTokenFilledAmount: ZERO_AMOUNT, }); expect(fillableTakerAmount).to.bignumber.eq(order.takerAmount); expect(isSignatureValid).to.eq(true); }); it('works with a fully filled order', async () => { const order = getTestRfqOrder(); // Fully Fund maker and taker. await fundOrderMakerAsync(order); await takerToken.mint(taker, order.takerAmount); await testUtils.fillRfqOrderAsync(order); // Partially fill the order. const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx .getRfqOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider)) .callAsync(); expect(orderInfo).to.deep.eq({ orderHash: order.getHash(), status: OrderStatus.Filled, takerTokenFilledAmount: order.takerAmount, }); expect(fillableTakerAmount).to.bignumber.eq(0); expect(isSignatureValid).to.eq(true); }); it('works with an under-funded, partially-filled order', async () => { const order = getTestRfqOrder(); // Fully Fund maker and taker. await fundOrderMakerAsync(order); await takerToken.mint(taker, order.takerAmount).awaitTransactionSuccessAsync(); // Partially fill the order. const fillAmount = getRandomPortion(order.takerAmount); await testUtils.fillRfqOrderAsync(order, fillAmount); // Reduce maker funds to be < remaining. const remainingMakerAmount = getFillableMakerTokenAmount(order, fillAmount); const balance = getRandomPortion(remainingMakerAmount); const allowance = getRandomPortion(remainingMakerAmount); await fundOrderMakerAsync(order, balance, allowance); // Get order state. const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx .getRfqOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider)) .callAsync(); expect(orderInfo).to.deep.eq({ orderHash: order.getHash(), status: OrderStatus.Fillable, takerTokenFilledAmount: fillAmount, }); expect(fillableTakerAmount).to.bignumber.eq( getActualFillableTakerTokenAmount(order, balance, allowance, fillAmount), ); expect(isSignatureValid).to.eq(true); }); }); async function batchFundOrderMakerAsync(orders: Array): Promise { await makerToken.burn(maker, await makerToken.balanceOf(maker).callAsync()).awaitTransactionSuccessAsync(); const balance = BigNumber.sum(...orders.map(o => o.makerAmount)); await makerToken.mint(maker, balance).awaitTransactionSuccessAsync(); await makerToken.approve(zeroEx.address, balance).awaitTransactionSuccessAsync({ from: maker }); } describe('batchGetLimitOrderRelevantStates()', () => { it('works with multiple orders', async () => { const orders = new Array(3).fill(0).map(() => getTestLimitOrder()); await batchFundOrderMakerAsync(orders); const [orderInfos, fillableTakerAmounts, isSignatureValids] = await zeroEx .batchGetLimitOrderRelevantStates( orders, await Promise.all(orders.map(async o => o.getSignatureWithProviderAsync(env.provider))), ) .callAsync(); expect(orderInfos).to.be.length(orders.length); expect(fillableTakerAmounts).to.be.length(orders.length); expect(isSignatureValids).to.be.length(orders.length); for (let i = 0; i < orders.length; ++i) { expect(orderInfos[i]).to.deep.eq({ orderHash: orders[i].getHash(), status: OrderStatus.Fillable, takerTokenFilledAmount: ZERO_AMOUNT, }); expect(fillableTakerAmounts[i]).to.bignumber.eq(orders[i].takerAmount); expect(isSignatureValids[i]).to.eq(true); } }); it('swallows reverts', async () => { const orders = new Array(3).fill(0).map(() => getTestLimitOrder()); // The second order will revert because its maker token is not valid. orders[1].makerToken = randomAddress(); await batchFundOrderMakerAsync(orders); const [orderInfos, fillableTakerAmounts, isSignatureValids] = await zeroEx .batchGetLimitOrderRelevantStates( orders, await Promise.all(orders.map(async o => o.getSignatureWithProviderAsync(env.provider))), ) .callAsync(); expect(orderInfos).to.be.length(orders.length); expect(fillableTakerAmounts).to.be.length(orders.length); expect(isSignatureValids).to.be.length(orders.length); for (let i = 0; i < orders.length; ++i) { expect(orderInfos[i]).to.deep.eq({ orderHash: i === 1 ? NULL_BYTES32 : orders[i].getHash(), status: i === 1 ? OrderStatus.Invalid : OrderStatus.Fillable, takerTokenFilledAmount: ZERO_AMOUNT, }); expect(fillableTakerAmounts[i]).to.bignumber.eq(i === 1 ? ZERO_AMOUNT : orders[i].takerAmount); expect(isSignatureValids[i]).to.eq(i !== 1); } }); }); describe('batchGetRfqOrderRelevantStates()', () => { it('works with multiple orders', async () => { const orders = new Array(3).fill(0).map(() => getTestRfqOrder()); await batchFundOrderMakerAsync(orders); const [orderInfos, fillableTakerAmounts, isSignatureValids] = await zeroEx .batchGetRfqOrderRelevantStates( orders, await Promise.all(orders.map(async o => o.getSignatureWithProviderAsync(env.provider))), ) .callAsync(); expect(orderInfos).to.be.length(orders.length); expect(fillableTakerAmounts).to.be.length(orders.length); expect(isSignatureValids).to.be.length(orders.length); for (let i = 0; i < orders.length; ++i) { expect(orderInfos[i]).to.deep.eq({ orderHash: orders[i].getHash(), status: OrderStatus.Fillable, takerTokenFilledAmount: ZERO_AMOUNT, }); expect(fillableTakerAmounts[i]).to.bignumber.eq(orders[i].takerAmount); expect(isSignatureValids[i]).to.eq(true); } }); }); describe('registerAllowedSigner()', () => { it('fires appropriate events', async () => { const receiptAllow = await contractWallet .registerAllowedOrderSigner(contractWalletSigner, true) .awaitTransactionSuccessAsync({ from: contractWalletOwner }); verifyEventsFromLogs( receiptAllow.logs, [ { maker: contractWallet.address, signer: contractWalletSigner, allowed: true, }, ], IZeroExEvents.OrderSignerRegistered, ); // then disallow signer const receiptDisallow = await contractWallet .registerAllowedOrderSigner(contractWalletSigner, false) .awaitTransactionSuccessAsync({ from: contractWalletOwner }); verifyEventsFromLogs( receiptDisallow.logs, [ { maker: contractWallet.address, signer: contractWalletSigner, allowed: false, }, ], IZeroExEvents.OrderSignerRegistered, ); }); it('allows for fills on orders signed by a approved signer', async () => { const order = getTestRfqOrder({ maker: contractWallet.address }); const sig = await order.getSignatureWithProviderAsync( env.provider, SignatureType.EthSign, contractWalletSigner, ); // covers taker await testUtils.prepareBalancesForOrdersAsync([order]); // need to provide contract wallet with a balance await makerToken.mint(contractWallet.address, order.makerAmount).awaitTransactionSuccessAsync(); await contractWallet .registerAllowedOrderSigner(contractWalletSigner, true) .awaitTransactionSuccessAsync({ from: contractWalletOwner }); await zeroEx.fillRfqOrder(order, sig, order.takerAmount).awaitTransactionSuccessAsync({ from: taker }); const info = await zeroEx.getRfqOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Filled, orderHash: order.getHash(), takerTokenFilledAmount: order.takerAmount, }); }); it('disallows fills if the signer is revoked', async () => { const order = getTestRfqOrder({ maker: contractWallet.address }); const sig = await order.getSignatureWithProviderAsync( env.provider, SignatureType.EthSign, contractWalletSigner, ); // covers taker await testUtils.prepareBalancesForOrdersAsync([order]); // need to provide contract wallet with a balance await makerToken.mint(contractWallet.address, order.makerAmount).awaitTransactionSuccessAsync(); // first allow signer await contractWallet .registerAllowedOrderSigner(contractWalletSigner, true) .awaitTransactionSuccessAsync({ from: contractWalletOwner }); // then disallow signer await contractWallet .registerAllowedOrderSigner(contractWalletSigner, false) .awaitTransactionSuccessAsync({ from: contractWalletOwner }); const tx = zeroEx.fillRfqOrder(order, sig, order.takerAmount).awaitTransactionSuccessAsync({ from: taker }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotSignedByMakerError( order.getHash(), contractWalletSigner, order.maker, ), ); }); it(`doesn't allow fills with an unapproved signer`, async () => { const order = getTestRfqOrder({ maker: contractWallet.address }); const sig = await order.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, maker); // covers taker await testUtils.prepareBalancesForOrdersAsync([order]); // need to provide contract wallet with a balance await makerToken.mint(contractWallet.address, order.makerAmount).awaitTransactionSuccessAsync(); const tx = zeroEx.fillRfqOrder(order, sig, order.takerAmount).awaitTransactionSuccessAsync({ from: taker }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), maker, order.maker), ); }); it(`allows an approved signer to cancel an RFQ order`, async () => { const order = getTestRfqOrder({ maker: contractWallet.address }); await contractWallet .registerAllowedOrderSigner(contractWalletSigner, true) .awaitTransactionSuccessAsync({ from: contractWalletOwner }); const receipt = await zeroEx .cancelRfqOrder(order) .awaitTransactionSuccessAsync({ from: contractWalletSigner }); verifyEventsFromLogs( receipt.logs, [{ maker: contractWallet.address, orderHash: order.getHash() }], IZeroExEvents.OrderCancelled, ); const info = await zeroEx.getRfqOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Cancelled, orderHash: order.getHash(), takerTokenFilledAmount: new BigNumber(0), }); }); it(`allows an approved signer to cancel a limit order`, async () => { const order = getTestLimitOrder({ maker: contractWallet.address }); await contractWallet .registerAllowedOrderSigner(contractWalletSigner, true) .awaitTransactionSuccessAsync({ from: contractWalletOwner }); const receipt = await zeroEx .cancelLimitOrder(order) .awaitTransactionSuccessAsync({ from: contractWalletSigner }); verifyEventsFromLogs( receipt.logs, [{ maker: contractWallet.address, orderHash: order.getHash() }], IZeroExEvents.OrderCancelled, ); const info = await zeroEx.getLimitOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Cancelled, orderHash: order.getHash(), takerTokenFilledAmount: new BigNumber(0), }); }); it(`doesn't allow an unapproved signer to cancel an RFQ order`, async () => { const order = getTestRfqOrder({ maker: contractWallet.address }); const tx = zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OnlyOrderMakerAllowed(order.getHash(), maker, order.maker), ); }); it(`doesn't allow an unapproved signer to cancel a limit order`, async () => { const order = getTestLimitOrder({ maker: contractWallet.address }); const tx = zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OnlyOrderMakerAllowed(order.getHash(), maker, order.maker), ); }); it(`allows a signer to cancel pair RFQ orders`, async () => { const order = getTestRfqOrder({ maker: contractWallet.address, salt: new BigNumber(1) }); await contractWallet .registerAllowedOrderSigner(contractWalletSigner, true) .awaitTransactionSuccessAsync({ from: contractWalletOwner }); // Cancel salts <= the order's const minValidSalt = order.salt.plus(1); const receipt = await zeroEx .cancelPairRfqOrdersWithSigner( contractWallet.address, makerToken.address, takerToken.address, minValidSalt, ) .awaitTransactionSuccessAsync({ from: contractWalletSigner }); verifyEventsFromLogs( receipt.logs, [ { maker: contractWallet.address, makerToken: makerToken.address, takerToken: takerToken.address, minValidSalt, }, ], IZeroExEvents.PairCancelledRfqOrders, ); const info = await zeroEx.getRfqOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Cancelled, orderHash: order.getHash(), takerTokenFilledAmount: new BigNumber(0), }); }); it(`doesn't allow an unapproved signer to cancel pair RFQ orders`, async () => { const minValidSalt = new BigNumber(2); const tx = zeroEx .cancelPairRfqOrdersWithSigner( contractWallet.address, makerToken.address, takerToken.address, minValidSalt, ) .awaitTransactionSuccessAsync({ from: maker }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.InvalidSignerError(contractWallet.address, maker), ); }); it(`allows a signer to cancel pair limit orders`, async () => { const order = getTestLimitOrder({ maker: contractWallet.address, salt: new BigNumber(1) }); await contractWallet .registerAllowedOrderSigner(contractWalletSigner, true) .awaitTransactionSuccessAsync({ from: contractWalletOwner }); // Cancel salts <= the order's const minValidSalt = order.salt.plus(1); const receipt = await zeroEx .cancelPairLimitOrdersWithSigner( contractWallet.address, makerToken.address, takerToken.address, minValidSalt, ) .awaitTransactionSuccessAsync({ from: contractWalletSigner }); verifyEventsFromLogs( receipt.logs, [ { maker: contractWallet.address, makerToken: makerToken.address, takerToken: takerToken.address, minValidSalt, }, ], IZeroExEvents.PairCancelledLimitOrders, ); const info = await zeroEx.getLimitOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Cancelled, orderHash: order.getHash(), takerTokenFilledAmount: new BigNumber(0), }); }); it(`doesn't allow an unapproved signer to cancel pair limit orders`, async () => { const minValidSalt = new BigNumber(2); const tx = zeroEx .cancelPairLimitOrdersWithSigner( contractWallet.address, makerToken.address, takerToken.address, minValidSalt, ) .awaitTransactionSuccessAsync({ from: maker }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.InvalidSignerError(contractWallet.address, maker), ); }); it(`allows a signer to cancel multiple RFQ order pairs`, async () => { const orders = [ getTestRfqOrder({ maker: contractWallet.address, salt: new BigNumber(1) }), // Flip the tokens for the other order. getTestRfqOrder({ makerToken: takerToken.address, takerToken: makerToken.address, maker: contractWallet.address, salt: new BigNumber(1), }), ]; await contractWallet .registerAllowedOrderSigner(contractWalletSigner, true) .awaitTransactionSuccessAsync({ from: contractWalletOwner }); const minValidSalt = new BigNumber(2); const receipt = await zeroEx .batchCancelPairRfqOrdersWithSigner( contractWallet.address, [makerToken.address, takerToken.address], [takerToken.address, makerToken.address], [minValidSalt, minValidSalt], ) .awaitTransactionSuccessAsync({ from: contractWalletSigner }); verifyEventsFromLogs( receipt.logs, [ { maker: contractWallet.address, makerToken: makerToken.address, takerToken: takerToken.address, minValidSalt, }, { maker: contractWallet.address, makerToken: takerToken.address, takerToken: makerToken.address, minValidSalt, }, ], IZeroExEvents.PairCancelledRfqOrders, ); const statuses = (await Promise.all(orders.map(o => zeroEx.getRfqOrderInfo(o).callAsync()))).map( oi => oi.status, ); expect(statuses).to.deep.eq([OrderStatus.Cancelled, OrderStatus.Cancelled]); }); it(`doesn't allow an unapproved signer to batch cancel pair rfq orders`, async () => { const minValidSalt = new BigNumber(2); const tx = zeroEx .batchCancelPairRfqOrdersWithSigner( contractWallet.address, [makerToken.address, takerToken.address], [takerToken.address, makerToken.address], [minValidSalt, minValidSalt], ) .awaitTransactionSuccessAsync({ from: maker }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.InvalidSignerError(contractWallet.address, maker), ); }); it(`allows a signer to cancel multiple limit order pairs`, async () => { const orders = [ getTestLimitOrder({ maker: contractWallet.address, salt: new BigNumber(1) }), // Flip the tokens for the other order. getTestLimitOrder({ makerToken: takerToken.address, takerToken: makerToken.address, maker: contractWallet.address, salt: new BigNumber(1), }), ]; await contractWallet .registerAllowedOrderSigner(contractWalletSigner, true) .awaitTransactionSuccessAsync({ from: contractWalletOwner }); const minValidSalt = new BigNumber(2); const receipt = await zeroEx .batchCancelPairLimitOrdersWithSigner( contractWallet.address, [makerToken.address, takerToken.address], [takerToken.address, makerToken.address], [minValidSalt, minValidSalt], ) .awaitTransactionSuccessAsync({ from: contractWalletSigner }); verifyEventsFromLogs( receipt.logs, [ { maker: contractWallet.address, makerToken: makerToken.address, takerToken: takerToken.address, minValidSalt, }, { maker: contractWallet.address, makerToken: takerToken.address, takerToken: makerToken.address, minValidSalt, }, ], IZeroExEvents.PairCancelledLimitOrders, ); const statuses = (await Promise.all(orders.map(o => zeroEx.getLimitOrderInfo(o).callAsync()))).map( oi => oi.status, ); expect(statuses).to.deep.eq([OrderStatus.Cancelled, OrderStatus.Cancelled]); }); it(`doesn't allow an unapproved signer to batch cancel pair limit orders`, async () => { const minValidSalt = new BigNumber(2); const tx = zeroEx .batchCancelPairLimitOrdersWithSigner( contractWallet.address, [makerToken.address, takerToken.address], [takerToken.address, makerToken.address], [minValidSalt, minValidSalt], ) .awaitTransactionSuccessAsync({ from: maker }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.InvalidSignerError(contractWallet.address, maker), ); }); }); });