import { blockchainTests, constants, describe, expect, verifyEventsFromLogs } from '@0x/contracts-test-utils'; import { OrderStatus, OtcOrder, RevertErrors, SignatureType } from '@0x/protocol-utils'; import { BigNumber } from '@0x/utils'; import { IOwnableFeatureContract, IZeroExContract, IZeroExEvents } from '../../src/wrappers'; import { artifacts } from '../artifacts'; import { abis } from '../utils/abis'; import { fullMigrateAsync } from '../utils/migration'; import { computeOtcOrderFilledAmounts, createExpiry, getRandomOtcOrder, NativeOrdersTestEnvironment, } from '../utils/orders'; import { OtcOrdersFeatureContract, TestMintableERC20TokenContract, TestOrderSignerRegistryWithContractWalletContract, TestWethContract, } from '../wrappers'; blockchainTests.resets('OtcOrdersFeature', env => { const { NULL_ADDRESS, MAX_UINT256, ZERO_AMOUNT: ZERO } = constants; const ETH_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; let maker: string; let taker: string; let notMaker: string; let notTaker: string; let contractWalletOwner: string; let contractWalletSigner: string; let txOrigin: string; let notTxOrigin: string; let zeroEx: IZeroExContract; let verifyingContract: string; let makerToken: TestMintableERC20TokenContract; let takerToken: TestMintableERC20TokenContract; let wethToken: TestWethContract; let contractWallet: TestOrderSignerRegistryWithContractWalletContract; let testUtils: NativeOrdersTestEnvironment; before(async () => { // Useful for ETH balance accounting const txDefaults = { ...env.txDefaults, gasPrice: 0 }; let owner; [owner, maker, taker, notMaker, notTaker, contractWalletOwner, contractWalletSigner, txOrigin, notTxOrigin] = await env.getAccountAddressesAsync(); [makerToken, takerToken] = await Promise.all( [...new Array(2)].map(async () => TestMintableERC20TokenContract.deployFrom0xArtifactAsync( artifacts.TestMintableERC20Token, env.provider, txDefaults, artifacts, ), ), ); wethToken = await TestWethContract.deployFrom0xArtifactAsync( artifacts.TestWeth, env.provider, txDefaults, artifacts, ); zeroEx = await fullMigrateAsync(owner, env.provider, txDefaults, {}, { wethAddress: wethToken.address }); const otcFeatureImpl = await OtcOrdersFeatureContract.deployFrom0xArtifactAsync( artifacts.OtcOrdersFeature, env.provider, txDefaults, artifacts, zeroEx.address, wethToken.address, ); await new IOwnableFeatureContract(zeroEx.address, env.provider, txDefaults, abis) .migrate(otcFeatureImpl.address, otcFeatureImpl.migrate().getABIEncodedTransactionData(), owner) .awaitTransactionSuccessAsync(); verifyingContract = zeroEx.address; await Promise.all([ makerToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: maker }), makerToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: notMaker }), takerToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: taker }), takerToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: notTaker }), wethToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: maker }), wethToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: notMaker }), wethToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: taker }), wethToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: notTaker }), ]); // 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 }); await contractWallet .approveERC20(takerToken.address, zeroEx.address, MAX_UINT256) .awaitTransactionSuccessAsync({ from: contractWalletOwner }); testUtils = new NativeOrdersTestEnvironment(maker, taker, makerToken, takerToken, zeroEx, ZERO, ZERO, env); }); function getTestOtcOrder(fields: Partial = {}): OtcOrder { return getRandomOtcOrder({ maker, verifyingContract, chainId: 1337, takerToken: takerToken.address, makerToken: makerToken.address, taker: NULL_ADDRESS, txOrigin: taker, ...fields, }); } describe('getOtcOrderHash()', () => { it('returns the correct hash', async () => { const order = getTestOtcOrder(); const hash = await zeroEx.getOtcOrderHash(order).callAsync(); expect(hash).to.eq(order.getHash()); }); }); describe('lastOtcTxOriginNonce()', () => { it('returns 0 if bucket is unused', async () => { const nonce = await zeroEx.lastOtcTxOriginNonce(taker, ZERO).callAsync(); expect(nonce).to.bignumber.eq(0); }); it('returns the last nonce used in a bucket', async () => { const order = getTestOtcOrder(); await testUtils.fillOtcOrderAsync(order); const nonce = await zeroEx.lastOtcTxOriginNonce(taker, order.nonceBucket).callAsync(); expect(nonce).to.bignumber.eq(order.nonce); }); }); describe('getOtcOrderInfo()', () => { it('unfilled order', async () => { const order = getTestOtcOrder(); const info = await zeroEx.getOtcOrderInfo(order).callAsync(); expect(info).to.deep.equal({ status: OrderStatus.Fillable, orderHash: order.getHash(), }); }); it('unfilled expired order', async () => { const expiry = createExpiry(-60); const order = getTestOtcOrder({ expiry }); const info = await zeroEx.getOtcOrderInfo(order).callAsync(); expect(info).to.deep.equal({ status: OrderStatus.Expired, orderHash: order.getHash(), }); }); it('filled then expired order', async () => { const expiry = createExpiry(60); const order = getTestOtcOrder({ expiry }); await testUtils.fillOtcOrderAsync(order); // Advance time to expire the order. await env.web3Wrapper.increaseTimeAsync(61); const info = await zeroEx.getOtcOrderInfo(order).callAsync(); expect(info).to.deep.equal({ status: OrderStatus.Invalid, orderHash: order.getHash(), }); }); it('filled order', async () => { const order = getTestOtcOrder(); // Fill the order first. await testUtils.fillOtcOrderAsync(order); const info = await zeroEx.getOtcOrderInfo(order).callAsync(); expect(info).to.deep.equal({ status: OrderStatus.Invalid, orderHash: order.getHash(), }); }); }); async function assertExpectedFinalBalancesFromOtcOrderFillAsync( order: OtcOrder, takerTokenFillAmount: BigNumber = order.takerAmount, ): Promise { const { makerTokenFilledAmount, takerTokenFilledAmount } = computeOtcOrderFilledAmounts( order, takerTokenFillAmount, ); const makerBalance = await new TestMintableERC20TokenContract(order.takerToken, env.provider) .balanceOf(order.maker) .callAsync(); const takerBalance = await new TestMintableERC20TokenContract(order.makerToken, env.provider) .balanceOf(order.taker !== NULL_ADDRESS ? order.taker : taker) .callAsync(); expect(makerBalance, 'maker balance').to.bignumber.eq(takerTokenFilledAmount); expect(takerBalance, 'taker balance').to.bignumber.eq(makerTokenFilledAmount); } describe('fillOtcOrder()', () => { it('can fully fill an order', async () => { const order = getTestOtcOrder(); const receipt = await testUtils.fillOtcOrderAsync(order); verifyEventsFromLogs( receipt.logs, [testUtils.createOtcOrderFilledEventArgs(order)], IZeroExEvents.OtcOrderFilled, ); await assertExpectedFinalBalancesFromOtcOrderFillAsync(order); }); it('can partially fill an order', async () => { const order = getTestOtcOrder(); const fillAmount = order.takerAmount.minus(1); const receipt = await testUtils.fillOtcOrderAsync(order, fillAmount); verifyEventsFromLogs( receipt.logs, [testUtils.createOtcOrderFilledEventArgs(order, fillAmount)], IZeroExEvents.OtcOrderFilled, ); await assertExpectedFinalBalancesFromOtcOrderFillAsync(order, fillAmount); }); it('clamps fill amount to remaining available', async () => { const order = getTestOtcOrder(); const fillAmount = order.takerAmount.plus(1); const receipt = await testUtils.fillOtcOrderAsync(order, fillAmount); verifyEventsFromLogs( receipt.logs, [testUtils.createOtcOrderFilledEventArgs(order, fillAmount)], IZeroExEvents.OtcOrderFilled, ); await assertExpectedFinalBalancesFromOtcOrderFillAsync(order, fillAmount); }); it('cannot fill an order with wrong tx.origin', async () => { const order = getTestOtcOrder(); const tx = testUtils.fillOtcOrderAsync(order, order.takerAmount, notTaker); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), notTaker, taker), ); }); it('cannot fill an order with wrong taker', async () => { const order = getTestOtcOrder({ taker: notTaker }); const tx = testUtils.fillOtcOrderAsync(order); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableByTakerError(order.getHash(), taker, notTaker), ); }); it('can fill an order from a different tx.origin if registered', async () => { const order = getTestOtcOrder(); await zeroEx.registerAllowedRfqOrigins([notTaker], true).awaitTransactionSuccessAsync({ from: taker }); return testUtils.fillOtcOrderAsync(order, order.takerAmount, notTaker); }); it('cannot fill an order with registered then unregistered tx.origin', async () => { const order = getTestOtcOrder(); await zeroEx.registerAllowedRfqOrigins([notTaker], true).awaitTransactionSuccessAsync({ from: taker }); await zeroEx.registerAllowedRfqOrigins([notTaker], false).awaitTransactionSuccessAsync({ from: taker }); const tx = testUtils.fillOtcOrderAsync(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 = getTestOtcOrder({ txOrigin: NULL_ADDRESS }); const tx = testUtils.fillOtcOrderAsync(order); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), taker, NULL_ADDRESS), ); }); it('cannot fill an expired order', async () => { const order = getTestOtcOrder({ expiry: createExpiry(-60) }); const tx = testUtils.fillOtcOrderAsync(order); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Expired), ); }); it('cannot fill order with bad signature', async () => { const order = getTestOtcOrder(); // Overwrite chainId to result in a different hash and therefore different // signature. const tx = testUtils.fillOtcOrderAsync(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 = getTestOtcOrder(); await testUtils.prepareBalancesForOrdersAsync([order], taker); const tx = zeroEx .fillOtcOrder(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'); }); it('cannot fill the same order twice', async () => { const order = getTestOtcOrder(); await testUtils.fillOtcOrderAsync(order); const tx = testUtils.fillOtcOrderAsync(order); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Invalid), ); }); it('cannot fill two orders with the same nonceBucket and nonce', async () => { const order1 = getTestOtcOrder(); await testUtils.fillOtcOrderAsync(order1); const order2 = getTestOtcOrder({ nonceBucket: order1.nonceBucket, nonce: order1.nonce }); const tx = testUtils.fillOtcOrderAsync(order2); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order2.getHash(), OrderStatus.Invalid), ); }); it('cannot fill an order whose nonce is less than the nonce last used in that bucket', async () => { const order1 = getTestOtcOrder(); await testUtils.fillOtcOrderAsync(order1); const order2 = getTestOtcOrder({ nonceBucket: order1.nonceBucket, nonce: order1.nonce.minus(1) }); const tx = testUtils.fillOtcOrderAsync(order2); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order2.getHash(), OrderStatus.Invalid), ); }); it('can fill two orders that use the same nonce bucket and increasing nonces', async () => { const order1 = getTestOtcOrder(); const tx1 = await testUtils.fillOtcOrderAsync(order1); verifyEventsFromLogs( tx1.logs, [testUtils.createOtcOrderFilledEventArgs(order1)], IZeroExEvents.OtcOrderFilled, ); const order2 = getTestOtcOrder({ nonceBucket: order1.nonceBucket, nonce: order1.nonce.plus(1) }); const tx2 = await testUtils.fillOtcOrderAsync(order2); verifyEventsFromLogs( tx2.logs, [testUtils.createOtcOrderFilledEventArgs(order2)], IZeroExEvents.OtcOrderFilled, ); }); it('can fill two orders that use the same nonce but different nonce buckets', async () => { const order1 = getTestOtcOrder(); const tx1 = await testUtils.fillOtcOrderAsync(order1); verifyEventsFromLogs( tx1.logs, [testUtils.createOtcOrderFilledEventArgs(order1)], IZeroExEvents.OtcOrderFilled, ); const order2 = getTestOtcOrder({ nonce: order1.nonce }); const tx2 = await testUtils.fillOtcOrderAsync(order2); verifyEventsFromLogs( tx2.logs, [testUtils.createOtcOrderFilledEventArgs(order2)], IZeroExEvents.OtcOrderFilled, ); }); it('can fill a WETH buy order and receive ETH', async () => { const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker); const order = getTestOtcOrder({ makerToken: wethToken.address, makerAmount: new BigNumber('1e18') }); await wethToken.deposit().awaitTransactionSuccessAsync({ from: maker, value: order.makerAmount }); const receipt = await testUtils.fillOtcOrderAsync(order, order.takerAmount, taker, true); verifyEventsFromLogs( receipt.logs, [testUtils.createOtcOrderFilledEventArgs(order)], IZeroExEvents.OtcOrderFilled, ); const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker); expect(takerEthBalanceAfter.minus(takerEthBalanceBefore)).to.bignumber.equal(order.makerAmount); }); it('reverts if `unwrapWeth` is true but maker token is not WETH', async () => { const order = getTestOtcOrder(); const tx = testUtils.fillOtcOrderAsync(order, order.takerAmount, taker, true); return expect(tx).to.revertWith('OtcOrdersFeature::fillOtcOrderForEth/MAKER_TOKEN_NOT_WETH'); }); it('allows for fills on orders signed by a approved signer', async () => { const order = getTestOtcOrder({ 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(); // allow signer await contractWallet .registerAllowedOrderSigner(contractWalletSigner, true) .awaitTransactionSuccessAsync({ from: contractWalletOwner }); // fill should succeed const receipt = await zeroEx .fillOtcOrder(order, sig, order.takerAmount) .awaitTransactionSuccessAsync({ from: taker }); verifyEventsFromLogs( receipt.logs, [testUtils.createOtcOrderFilledEventArgs(order)], IZeroExEvents.OtcOrderFilled, ); await assertExpectedFinalBalancesFromOtcOrderFillAsync(order); }); it('disallows fills if the signer is revoked', async () => { const order = getTestOtcOrder({ 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 }); // fill should revert const tx = zeroEx.fillOtcOrder(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 = getTestOtcOrder({ 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(); // fill should revert const tx = zeroEx.fillOtcOrder(order, sig, order.takerAmount).awaitTransactionSuccessAsync({ from: taker }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), maker, order.maker), ); }); }); describe('fillOtcOrderWithEth()', () => { it('Can fill an order with ETH (takerToken=WETH)', async () => { const order = getTestOtcOrder({ takerToken: wethToken.address }); const receipt = await testUtils.fillOtcOrderWithEthAsync(order); verifyEventsFromLogs( receipt.logs, [testUtils.createOtcOrderFilledEventArgs(order)], IZeroExEvents.OtcOrderFilled, ); await assertExpectedFinalBalancesFromOtcOrderFillAsync(order); }); it('Can fill an order with ETH (takerToken=ETH)', async () => { const order = getTestOtcOrder({ takerToken: ETH_TOKEN_ADDRESS }); const makerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(maker); const receipt = await testUtils.fillOtcOrderWithEthAsync(order); verifyEventsFromLogs( receipt.logs, [testUtils.createOtcOrderFilledEventArgs(order)], IZeroExEvents.OtcOrderFilled, ); const takerBalance = await new TestMintableERC20TokenContract(order.makerToken, env.provider) .balanceOf(taker) .callAsync(); expect(takerBalance, 'taker balance').to.bignumber.eq(order.makerAmount); const makerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(maker); expect(makerEthBalanceAfter.minus(makerEthBalanceBefore), 'maker balance').to.bignumber.equal( order.takerAmount, ); }); it('Can partially fill an order with ETH (takerToken=WETH)', async () => { const order = getTestOtcOrder({ takerToken: wethToken.address }); const fillAmount = order.takerAmount.minus(1); const receipt = await testUtils.fillOtcOrderWithEthAsync(order, fillAmount); verifyEventsFromLogs( receipt.logs, [testUtils.createOtcOrderFilledEventArgs(order, fillAmount)], IZeroExEvents.OtcOrderFilled, ); await assertExpectedFinalBalancesFromOtcOrderFillAsync(order, fillAmount); }); it('Can partially fill an order with ETH (takerToken=ETH)', async () => { const order = getTestOtcOrder({ takerToken: ETH_TOKEN_ADDRESS }); const fillAmount = order.takerAmount.minus(1); const makerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(maker); const receipt = await testUtils.fillOtcOrderWithEthAsync(order, fillAmount); verifyEventsFromLogs( receipt.logs, [testUtils.createOtcOrderFilledEventArgs(order, fillAmount)], IZeroExEvents.OtcOrderFilled, ); const { makerTokenFilledAmount, takerTokenFilledAmount } = computeOtcOrderFilledAmounts(order, fillAmount); const takerBalance = await new TestMintableERC20TokenContract(order.makerToken, env.provider) .balanceOf(taker) .callAsync(); expect(takerBalance, 'taker balance').to.bignumber.eq(makerTokenFilledAmount); const makerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(maker); expect(makerEthBalanceAfter.minus(makerEthBalanceBefore), 'maker balance').to.bignumber.equal( takerTokenFilledAmount, ); }); it('Can refund excess ETH is msg.value > order.takerAmount (takerToken=WETH)', async () => { const order = getTestOtcOrder({ takerToken: wethToken.address }); const fillAmount = order.takerAmount.plus(420); const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker); const receipt = await testUtils.fillOtcOrderWithEthAsync(order, fillAmount); verifyEventsFromLogs( receipt.logs, [testUtils.createOtcOrderFilledEventArgs(order)], IZeroExEvents.OtcOrderFilled, ); const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker); expect(takerEthBalanceBefore.minus(takerEthBalanceAfter)).to.bignumber.equal(order.takerAmount); await assertExpectedFinalBalancesFromOtcOrderFillAsync(order); }); it('Can refund excess ETH is msg.value > order.takerAmount (takerToken=ETH)', async () => { const order = getTestOtcOrder({ takerToken: ETH_TOKEN_ADDRESS }); const fillAmount = order.takerAmount.plus(420); const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker); const makerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(maker); const receipt = await testUtils.fillOtcOrderWithEthAsync(order, fillAmount); verifyEventsFromLogs( receipt.logs, [testUtils.createOtcOrderFilledEventArgs(order)], IZeroExEvents.OtcOrderFilled, ); const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker); expect(takerEthBalanceBefore.minus(takerEthBalanceAfter), 'taker eth balance').to.bignumber.equal( order.takerAmount, ); const takerBalance = await new TestMintableERC20TokenContract(order.makerToken, env.provider) .balanceOf(taker) .callAsync(); expect(takerBalance, 'taker balance').to.bignumber.eq(order.makerAmount); const makerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(maker); expect(makerEthBalanceAfter.minus(makerEthBalanceBefore), 'maker balance').to.bignumber.equal( order.takerAmount, ); }); it('Cannot fill an order if taker token is not ETH or WETH', async () => { const order = getTestOtcOrder(); const tx = testUtils.fillOtcOrderWithEthAsync(order); return expect(tx).to.revertWith('OtcOrdersFeature::fillOtcOrderWithEth/INVALID_TAKER_TOKEN'); }); }); describe('fillTakerSignedOtcOrder()', () => { it('can fully fill an order', async () => { const order = getTestOtcOrder({ taker, txOrigin }); const receipt = await testUtils.fillTakerSignedOtcOrderAsync(order); verifyEventsFromLogs( receipt.logs, [testUtils.createOtcOrderFilledEventArgs(order)], IZeroExEvents.OtcOrderFilled, ); await assertExpectedFinalBalancesFromOtcOrderFillAsync(order); }); it('cannot fill an order with wrong tx.origin', async () => { const order = getTestOtcOrder({ taker, txOrigin }); const tx = testUtils.fillTakerSignedOtcOrderAsync(order, notTxOrigin); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), notTxOrigin, txOrigin), ); }); it('can fill an order from a different tx.origin if registered', async () => { const order = getTestOtcOrder({ taker, txOrigin }); await zeroEx .registerAllowedRfqOrigins([notTxOrigin], true) .awaitTransactionSuccessAsync({ from: txOrigin }); return testUtils.fillTakerSignedOtcOrderAsync(order, notTxOrigin); }); it('cannot fill an order with registered then unregistered tx.origin', async () => { const order = getTestOtcOrder({ taker, txOrigin }); await zeroEx .registerAllowedRfqOrigins([notTxOrigin], true) .awaitTransactionSuccessAsync({ from: txOrigin }); await zeroEx .registerAllowedRfqOrigins([notTxOrigin], false) .awaitTransactionSuccessAsync({ from: txOrigin }); const tx = testUtils.fillTakerSignedOtcOrderAsync(order, notTxOrigin); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), notTxOrigin, txOrigin), ); }); it('cannot fill an order with a zero tx.origin', async () => { const order = getTestOtcOrder({ taker, txOrigin: NULL_ADDRESS }); const tx = testUtils.fillTakerSignedOtcOrderAsync(order, txOrigin); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), txOrigin, NULL_ADDRESS), ); }); it('cannot fill an expired order', async () => { const order = getTestOtcOrder({ taker, txOrigin, expiry: createExpiry(-60) }); const tx = testUtils.fillTakerSignedOtcOrderAsync(order); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Expired), ); }); it('cannot fill an order with bad taker signature', async () => { const order = getTestOtcOrder({ taker, txOrigin }); const tx = testUtils.fillTakerSignedOtcOrderAsync(order, txOrigin, notTaker); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableByTakerError(order.getHash(), notTaker, taker), ); }); it('cannot fill order with bad maker signature', async () => { const order = getTestOtcOrder({ taker, txOrigin }); const anotherOrder = getTestOtcOrder({ taker, txOrigin }); await testUtils.prepareBalancesForOrdersAsync([order], taker); const tx = zeroEx .fillTakerSignedOtcOrder( order, await anotherOrder.getSignatureWithProviderAsync(env.provider), await order.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker), ) .awaitTransactionSuccessAsync({ from: txOrigin }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), undefined, order.maker), ); }); it('fails if ETH is attached', async () => { const order = getTestOtcOrder({ taker, txOrigin }); await testUtils.prepareBalancesForOrdersAsync([order], taker); const tx = zeroEx .fillTakerSignedOtcOrder( order, await order.getSignatureWithProviderAsync(env.provider), await order.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker), ) .awaitTransactionSuccessAsync({ from: txOrigin, value: 1 }); // This will revert at the language level because the fill function is not payable. return expect(tx).to.be.rejectedWith('revert'); }); it('cannot fill the same order twice', async () => { const order = getTestOtcOrder({ taker, txOrigin }); await testUtils.fillTakerSignedOtcOrderAsync(order); const tx = testUtils.fillTakerSignedOtcOrderAsync(order); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Invalid), ); }); it('cannot fill two orders with the same nonceBucket and nonce', async () => { const order1 = getTestOtcOrder({ taker, txOrigin }); await testUtils.fillTakerSignedOtcOrderAsync(order1); const order2 = getTestOtcOrder({ taker, txOrigin, nonceBucket: order1.nonceBucket, nonce: order1.nonce }); const tx = testUtils.fillTakerSignedOtcOrderAsync(order2); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order2.getHash(), OrderStatus.Invalid), ); }); it('cannot fill an order whose nonce is less than the nonce last used in that bucket', async () => { const order1 = getTestOtcOrder({ taker, txOrigin }); await testUtils.fillTakerSignedOtcOrderAsync(order1); const order2 = getTestOtcOrder({ taker, txOrigin, nonceBucket: order1.nonceBucket, nonce: order1.nonce.minus(1), }); const tx = testUtils.fillTakerSignedOtcOrderAsync(order2); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order2.getHash(), OrderStatus.Invalid), ); }); it('can fill two orders that use the same nonce bucket and increasing nonces', async () => { const order1 = getTestOtcOrder({ taker, txOrigin }); const tx1 = await testUtils.fillTakerSignedOtcOrderAsync(order1); verifyEventsFromLogs( tx1.logs, [testUtils.createOtcOrderFilledEventArgs(order1)], IZeroExEvents.OtcOrderFilled, ); const order2 = getTestOtcOrder({ taker, txOrigin, nonceBucket: order1.nonceBucket, nonce: order1.nonce.plus(1), }); const tx2 = await testUtils.fillTakerSignedOtcOrderAsync(order2); verifyEventsFromLogs( tx2.logs, [testUtils.createOtcOrderFilledEventArgs(order2)], IZeroExEvents.OtcOrderFilled, ); }); it('can fill two orders that use the same nonce but different nonce buckets', async () => { const order1 = getTestOtcOrder({ taker, txOrigin }); const tx1 = await testUtils.fillTakerSignedOtcOrderAsync(order1); verifyEventsFromLogs( tx1.logs, [testUtils.createOtcOrderFilledEventArgs(order1)], IZeroExEvents.OtcOrderFilled, ); const order2 = getTestOtcOrder({ taker, txOrigin, nonce: order1.nonce }); const tx2 = await testUtils.fillTakerSignedOtcOrderAsync(order2); verifyEventsFromLogs( tx2.logs, [testUtils.createOtcOrderFilledEventArgs(order2)], IZeroExEvents.OtcOrderFilled, ); }); it('can fill a WETH buy order and receive ETH', async () => { const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker); const order = getTestOtcOrder({ taker, txOrigin, makerToken: wethToken.address, makerAmount: new BigNumber('1e18'), }); await wethToken.deposit().awaitTransactionSuccessAsync({ from: maker, value: order.makerAmount }); const receipt = await testUtils.fillTakerSignedOtcOrderAsync(order, txOrigin, taker, true); verifyEventsFromLogs( receipt.logs, [testUtils.createOtcOrderFilledEventArgs(order)], IZeroExEvents.OtcOrderFilled, ); const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker); expect(takerEthBalanceAfter.minus(takerEthBalanceBefore)).to.bignumber.equal(order.makerAmount); }); it('reverts if `unwrapWeth` is true but maker token is not WETH', async () => { const order = getTestOtcOrder({ taker, txOrigin }); const tx = testUtils.fillTakerSignedOtcOrderAsync(order, txOrigin, taker, true); return expect(tx).to.revertWith('OtcOrdersFeature::fillTakerSignedOtcOrder/MAKER_TOKEN_NOT_WETH'); }); }); describe('batchFillTakerSignedOtcOrders()', () => { it('Fills multiple orders', async () => { const order1 = getTestOtcOrder({ taker, txOrigin }); const order2 = getTestOtcOrder({ taker: notTaker, txOrigin, nonceBucket: order1.nonceBucket, nonce: order1.nonce.plus(1), }); await testUtils.prepareBalancesForOrdersAsync([order1], taker); await testUtils.prepareBalancesForOrdersAsync([order2], notTaker); const tx = await zeroEx .batchFillTakerSignedOtcOrders( [order1, order2], [ await order1.getSignatureWithProviderAsync(env.provider), await order2.getSignatureWithProviderAsync(env.provider), ], [ await order1.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker), await order2.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, notTaker), ], [false, false], ) .awaitTransactionSuccessAsync({ from: txOrigin }); verifyEventsFromLogs( tx.logs, [testUtils.createOtcOrderFilledEventArgs(order1), testUtils.createOtcOrderFilledEventArgs(order2)], IZeroExEvents.OtcOrderFilled, ); }); it('Fills multiple orders and unwraps WETH', async () => { const order1 = getTestOtcOrder({ taker, txOrigin }); const order2 = getTestOtcOrder({ taker: notTaker, txOrigin, nonceBucket: order1.nonceBucket, nonce: order1.nonce.plus(1), makerToken: wethToken.address, makerAmount: new BigNumber('1e18'), }); await testUtils.prepareBalancesForOrdersAsync([order1], taker); await testUtils.prepareBalancesForOrdersAsync([order2], notTaker); await wethToken.deposit().awaitTransactionSuccessAsync({ from: maker, value: order2.makerAmount }); const tx = await zeroEx .batchFillTakerSignedOtcOrders( [order1, order2], [ await order1.getSignatureWithProviderAsync(env.provider), await order2.getSignatureWithProviderAsync(env.provider), ], [ await order1.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker), await order2.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, notTaker), ], [false, true], ) .awaitTransactionSuccessAsync({ from: txOrigin }); verifyEventsFromLogs( tx.logs, [testUtils.createOtcOrderFilledEventArgs(order1), testUtils.createOtcOrderFilledEventArgs(order2)], IZeroExEvents.OtcOrderFilled, ); }); it('Skips over unfillable orders', async () => { const order1 = getTestOtcOrder({ taker, txOrigin }); const order2 = getTestOtcOrder({ taker: notTaker, txOrigin, nonceBucket: order1.nonceBucket, nonce: order1.nonce.plus(1), }); await testUtils.prepareBalancesForOrdersAsync([order1], taker); await testUtils.prepareBalancesForOrdersAsync([order2], notTaker); const tx = await zeroEx .batchFillTakerSignedOtcOrders( [order1, order2], [ await order1.getSignatureWithProviderAsync(env.provider), await order2.getSignatureWithProviderAsync(env.provider), ], [ await order1.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker), await order2.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker), // Invalid signature for order2 ], [false, false], ) .awaitTransactionSuccessAsync({ from: txOrigin }); verifyEventsFromLogs( tx.logs, [testUtils.createOtcOrderFilledEventArgs(order1)], IZeroExEvents.OtcOrderFilled, ); }); }); });