aboutsummaryrefslogtreecommitdiffstats
path: root/contracts/protocol/test
diff options
context:
space:
mode:
Diffstat (limited to 'contracts/protocol/test')
-rw-r--r--contracts/protocol/test/asset_proxy/authorizable.ts211
-rw-r--r--contracts/protocol/test/asset_proxy/proxies.ts1253
-rw-r--r--contracts/protocol/test/exchange/core.ts1178
-rw-r--r--contracts/protocol/test/exchange/dispatcher.ts284
-rw-r--r--contracts/protocol/test/exchange/fill_order.ts314
-rw-r--r--contracts/protocol/test/exchange/internal.ts472
-rw-r--r--contracts/protocol/test/exchange/match_orders.ts1284
-rw-r--r--contracts/protocol/test/exchange/signature_validator.ts526
-rw-r--r--contracts/protocol/test/exchange/transactions.ts466
-rw-r--r--contracts/protocol/test/exchange/wrapper.ts1461
-rw-r--r--contracts/protocol/test/global_hooks.ts17
-rw-r--r--contracts/protocol/test/multisig/asset_proxy_owner.ts506
-rw-r--r--contracts/protocol/test/utils/asset_proxy_owner_wrapper.ts70
-rw-r--r--contracts/protocol/test/utils/asset_wrapper.ts222
-rw-r--r--contracts/protocol/test/utils/erc20_wrapper.ts179
-rw-r--r--contracts/protocol/test/utils/erc721_wrapper.ts236
-rw-r--r--contracts/protocol/test/utils/exchange_wrapper.ts281
-rw-r--r--contracts/protocol/test/utils/fill_order_combinatorial_utils.ts928
-rw-r--r--contracts/protocol/test/utils/index.ts3
-rw-r--r--contracts/protocol/test/utils/match_order_tester.ts562
-rw-r--r--contracts/protocol/test/utils/order_factory_from_scenario.ts294
-rw-r--r--contracts/protocol/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts19
-rw-r--r--contracts/protocol/test/utils/simple_order_filled_cancelled_fetcher.ts31
23 files changed, 10797 insertions, 0 deletions
diff --git a/contracts/protocol/test/asset_proxy/authorizable.ts b/contracts/protocol/test/asset_proxy/authorizable.ts
new file mode 100644
index 000000000..853d18be0
--- /dev/null
+++ b/contracts/protocol/test/asset_proxy/authorizable.ts
@@ -0,0 +1,211 @@
+import {
+ chaiSetup,
+ constants,
+ expectTransactionFailedAsync,
+ provider,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { RevertReason } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+
+import { MixinAuthorizableContract } from '../../generated-wrappers/mixin_authorizable';
+import { artifacts } from '../../src/artifacts';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+
+describe('Authorizable', () => {
+ let owner: string;
+ let notOwner: string;
+ let address: string;
+ let authorizable: MixinAuthorizableContract;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ [owner, address, notOwner] = _.slice(accounts, 0, 3);
+ authorizable = await MixinAuthorizableContract.deployFrom0xArtifactAsync(
+ artifacts.MixinAuthorizable,
+ provider,
+ txDefaults,
+ );
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('addAuthorizedAddress', () => {
+ it('should throw if not called by owner', async () => {
+ return expectTransactionFailedAsync(
+ authorizable.addAuthorizedAddress.sendTransactionAsync(notOwner, { from: notOwner }),
+ RevertReason.OnlyContractOwner,
+ );
+ });
+ it('should allow owner to add an authorized address', async () => {
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const isAuthorized = await authorizable.authorized.callAsync(address);
+ expect(isAuthorized).to.be.true();
+ });
+ it('should throw if owner attempts to authorize a duplicate address', async () => {
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ return expectTransactionFailedAsync(
+ authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }),
+ RevertReason.TargetAlreadyAuthorized,
+ );
+ });
+ });
+
+ describe('removeAuthorizedAddress', () => {
+ it('should throw if not called by owner', async () => {
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ return expectTransactionFailedAsync(
+ authorizable.removeAuthorizedAddress.sendTransactionAsync(address, {
+ from: notOwner,
+ }),
+ RevertReason.OnlyContractOwner,
+ );
+ });
+
+ it('should allow owner to remove an authorized address', async () => {
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.removeAuthorizedAddress.sendTransactionAsync(address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const isAuthorized = await authorizable.authorized.callAsync(address);
+ expect(isAuthorized).to.be.false();
+ });
+
+ it('should throw if owner attempts to remove an address that is not authorized', async () => {
+ return expectTransactionFailedAsync(
+ authorizable.removeAuthorizedAddress.sendTransactionAsync(address, {
+ from: owner,
+ }),
+ RevertReason.TargetNotAuthorized,
+ );
+ });
+ });
+
+ describe('removeAuthorizedAddressAtIndex', () => {
+ it('should throw if not called by owner', async () => {
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const index = new BigNumber(0);
+ return expectTransactionFailedAsync(
+ authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, {
+ from: notOwner,
+ }),
+ RevertReason.OnlyContractOwner,
+ );
+ });
+ it('should throw if index is >= authorities.length', async () => {
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const index = new BigNumber(1);
+ return expectTransactionFailedAsync(
+ authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, {
+ from: owner,
+ }),
+ RevertReason.IndexOutOfBounds,
+ );
+ });
+ it('should throw if owner attempts to remove an address that is not authorized', async () => {
+ const index = new BigNumber(0);
+ return expectTransactionFailedAsync(
+ authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, {
+ from: owner,
+ }),
+ RevertReason.TargetNotAuthorized,
+ );
+ });
+ it('should throw if address at index does not match target', async () => {
+ const address1 = address;
+ const address2 = notOwner;
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.addAuthorizedAddress.sendTransactionAsync(address1, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.addAuthorizedAddress.sendTransactionAsync(address2, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const address1Index = new BigNumber(0);
+ return expectTransactionFailedAsync(
+ authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address2, address1Index, {
+ from: owner,
+ }),
+ RevertReason.AuthorizedAddressMismatch,
+ );
+ });
+ it('should allow owner to remove an authorized address', async () => {
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const index = new BigNumber(0);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const isAuthorized = await authorizable.authorized.callAsync(address);
+ expect(isAuthorized).to.be.false();
+ });
+ });
+
+ describe('getAuthorizedAddresses', () => {
+ it('should return all authorized addresses', async () => {
+ const initial = await authorizable.getAuthorizedAddresses.callAsync();
+ expect(initial).to.have.length(0);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.addAuthorizedAddress.sendTransactionAsync(address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const afterAdd = await authorizable.getAuthorizedAddresses.callAsync();
+ expect(afterAdd).to.have.length(1);
+ expect(afterAdd).to.include(address);
+
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.removeAuthorizedAddress.sendTransactionAsync(address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const afterRemove = await authorizable.getAuthorizedAddresses.callAsync();
+ expect(afterRemove).to.have.length(0);
+ });
+ });
+});
diff --git a/contracts/protocol/test/asset_proxy/proxies.ts b/contracts/protocol/test/asset_proxy/proxies.ts
new file mode 100644
index 000000000..c4bd95905
--- /dev/null
+++ b/contracts/protocol/test/asset_proxy/proxies.ts
@@ -0,0 +1,1253 @@
+import { artifacts as interfacesArtifacts, IAssetDataContract, IAssetProxyContract } from '@0x/contracts-interfaces';
+import {
+ chaiSetup,
+ constants,
+ expectTransactionFailedAsync,
+ expectTransactionFailedWithoutReasonAsync,
+ LogDecoder,
+ provider,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import {
+ artifacts as tokensArtifacts,
+ DummyERC20TokenContract,
+ DummyERC721ReceiverContract,
+ DummyERC721TokenContract,
+ DummyMultipleReturnERC20TokenContract,
+ DummyNoReturnERC20TokenContract,
+} from '@0x/contracts-tokens';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { assetDataUtils } from '@0x/order-utils';
+import { RevertReason } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+
+import { ERC20ProxyContract } from '../../generated-wrappers/erc20_proxy';
+import { ERC721ProxyContract } from '../../generated-wrappers/erc721_proxy';
+import { MultiAssetProxyContract } from '../../generated-wrappers/multi_asset_proxy';
+import { artifacts } from '../../src/artifacts';
+import { ERC20Wrapper } from '../utils/erc20_wrapper';
+import { ERC721Wrapper } from '../utils/erc721_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+const assetProxyInterface = new IAssetProxyContract(
+ interfacesArtifacts.IAssetProxy.compilerOutput.abi,
+ constants.NULL_ADDRESS,
+ provider,
+);
+const assetDataInterface = new IAssetDataContract(
+ interfacesArtifacts.IAssetData.compilerOutput.abi,
+ constants.NULL_ADDRESS,
+ provider,
+);
+
+// tslint:disable:no-unnecessary-type-assertion
+describe('Asset Transfer Proxies', () => {
+ let owner: string;
+ let notAuthorized: string;
+ let authorized: string;
+ let fromAddress: string;
+ let toAddress: string;
+
+ let erc20TokenA: DummyERC20TokenContract;
+ let erc20TokenB: DummyERC20TokenContract;
+ let erc721TokenA: DummyERC721TokenContract;
+ let erc721TokenB: DummyERC721TokenContract;
+ let erc721Receiver: DummyERC721ReceiverContract;
+ let erc20Proxy: ERC20ProxyContract;
+ let erc721Proxy: ERC721ProxyContract;
+ let noReturnErc20Token: DummyNoReturnERC20TokenContract;
+ let multipleReturnErc20Token: DummyMultipleReturnERC20TokenContract;
+ let multiAssetProxy: MultiAssetProxyContract;
+
+ let erc20Wrapper: ERC20Wrapper;
+ let erc721Wrapper: ERC721Wrapper;
+ let erc721AFromTokenId: BigNumber;
+ let erc721BFromTokenId: BigNumber;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ const usedAddresses = ([owner, notAuthorized, authorized, fromAddress, toAddress] = _.slice(accounts, 0, 5));
+
+ erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
+ erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner);
+
+ // Deploy AssetProxies
+ erc20Proxy = await erc20Wrapper.deployProxyAsync();
+ erc721Proxy = await erc721Wrapper.deployProxyAsync();
+ multiAssetProxy = await MultiAssetProxyContract.deployFrom0xArtifactAsync(
+ artifacts.MultiAssetProxy,
+ provider,
+ txDefaults,
+ );
+
+ // Configure ERC20Proxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(authorized, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(multiAssetProxy.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ // Configure ERC721Proxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(authorized, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(multiAssetProxy.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ // Configure MultiAssetProxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await multiAssetProxy.addAuthorizedAddress.sendTransactionAsync(authorized, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await multiAssetProxy.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await multiAssetProxy.registerAssetProxy.sendTransactionAsync(erc721Proxy.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ // Deploy and configure ERC20 tokens
+ const numDummyErc20ToDeploy = 2;
+ [erc20TokenA, erc20TokenB] = await erc20Wrapper.deployDummyTokensAsync(
+ numDummyErc20ToDeploy,
+ constants.DUMMY_TOKEN_DECIMALS,
+ );
+ noReturnErc20Token = await DummyNoReturnERC20TokenContract.deployFrom0xArtifactAsync(
+ tokensArtifacts.DummyNoReturnERC20Token,
+ provider,
+ txDefaults,
+ constants.DUMMY_TOKEN_NAME,
+ constants.DUMMY_TOKEN_SYMBOL,
+ constants.DUMMY_TOKEN_DECIMALS,
+ constants.DUMMY_TOKEN_TOTAL_SUPPLY,
+ );
+ multipleReturnErc20Token = await DummyMultipleReturnERC20TokenContract.deployFrom0xArtifactAsync(
+ tokensArtifacts.DummyMultipleReturnERC20Token,
+ provider,
+ txDefaults,
+ constants.DUMMY_TOKEN_NAME,
+ constants.DUMMY_TOKEN_SYMBOL,
+ constants.DUMMY_TOKEN_DECIMALS,
+ constants.DUMMY_TOKEN_TOTAL_SUPPLY,
+ );
+
+ await erc20Wrapper.setBalancesAndAllowancesAsync();
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await noReturnErc20Token.setBalance.sendTransactionAsync(fromAddress, constants.INITIAL_ERC20_BALANCE),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await noReturnErc20Token.approve.sendTransactionAsync(
+ erc20Proxy.address,
+ constants.INITIAL_ERC20_ALLOWANCE,
+ { from: fromAddress },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await multipleReturnErc20Token.setBalance.sendTransactionAsync(
+ fromAddress,
+ constants.INITIAL_ERC20_BALANCE,
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await multipleReturnErc20Token.approve.sendTransactionAsync(
+ erc20Proxy.address,
+ constants.INITIAL_ERC20_ALLOWANCE,
+ { from: fromAddress },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ // Deploy and configure ERC721 tokens and receiver
+ [erc721TokenA, erc721TokenB] = await erc721Wrapper.deployDummyTokensAsync();
+ erc721Receiver = await DummyERC721ReceiverContract.deployFrom0xArtifactAsync(
+ tokensArtifacts.DummyERC721Receiver,
+ provider,
+ txDefaults,
+ );
+
+ await erc721Wrapper.setBalancesAndAllowancesAsync();
+ const erc721Balances = await erc721Wrapper.getBalancesAsync();
+ erc721AFromTokenId = erc721Balances[fromAddress][erc721TokenA.address][0];
+ erc721BFromTokenId = erc721Balances[fromAddress][erc721TokenB.address][0];
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+
+ describe('ERC20Proxy', () => {
+ it('should revert if undefined function is called', async () => {
+ const undefinedSelector = '0x01020304';
+ await expectTransactionFailedWithoutReasonAsync(
+ web3Wrapper.sendTransactionAsync({
+ from: owner,
+ to: erc20Proxy.address,
+ value: constants.ZERO_AMOUNT,
+ data: undefinedSelector,
+ }),
+ );
+ });
+ it('should have an id of 0xf47261b0', async () => {
+ const proxyId = await erc20Proxy.getProxyId.callAsync();
+ const expectedProxyId = '0xf47261b0';
+ expect(proxyId).to.equal(expectedProxyId);
+ });
+ describe('transferFrom', () => {
+ it('should successfully transfer tokens', async () => {
+ // Construct ERC20 asset data
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ // Perform a transfer from fromAddress to toAddress
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ const amount = new BigNumber(10);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: erc20Proxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Verify transfer was successful
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenA.address].minus(amount),
+ );
+ expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenA.address].add(amount),
+ );
+ });
+
+ it('should successfully transfer tokens that do not return a value', async () => {
+ // Construct ERC20 asset data
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(noReturnErc20Token.address);
+ // Perform a transfer from fromAddress to toAddress
+ const initialFromBalance = await noReturnErc20Token.balanceOf.callAsync(fromAddress);
+ const initialToBalance = await noReturnErc20Token.balanceOf.callAsync(toAddress);
+ const amount = new BigNumber(10);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: erc20Proxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Verify transfer was successful
+ const newFromBalance = await noReturnErc20Token.balanceOf.callAsync(fromAddress);
+ const newToBalance = await noReturnErc20Token.balanceOf.callAsync(toAddress);
+ expect(newFromBalance).to.be.bignumber.equal(initialFromBalance.minus(amount));
+ expect(newToBalance).to.be.bignumber.equal(initialToBalance.plus(amount));
+ });
+
+ it('should successfully transfer tokens and ignore extra assetData', async () => {
+ // Construct ERC20 asset data
+ const extraData = '0102030405060708';
+ const encodedAssetData = `${assetDataUtils.encodeERC20AssetData(erc20TokenA.address)}${extraData}`;
+ // Perform a transfer from fromAddress to toAddress
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ const amount = new BigNumber(10);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: erc20Proxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Verify transfer was successful
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenA.address].minus(amount),
+ );
+ expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenA.address].add(amount),
+ );
+ });
+
+ it('should do nothing if transferring 0 amount of a token', async () => {
+ // Construct ERC20 asset data
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ // Perform a transfer from fromAddress to toAddress
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ const amount = new BigNumber(0);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: erc20Proxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Verify transfer was successful
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenA.address],
+ );
+ expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenA.address],
+ );
+ });
+
+ it('should revert if allowances are too low', async () => {
+ // Construct ERC20 asset data
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ // Create allowance less than transfer amount. Set allowance on proxy.
+ const allowance = new BigNumber(0);
+ const amount = new BigNumber(10);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20TokenA.approve.sendTransactionAsync(erc20Proxy.address, allowance, {
+ from: fromAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ // Perform a transfer; expect this to fail.
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: erc20Proxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.TransferFailed,
+ );
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.deep.equal(erc20Balances);
+ });
+
+ it('should revert if allowances are too low and token does not return a value', async () => {
+ // Construct ERC20 asset data
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(noReturnErc20Token.address);
+ // Create allowance less than transfer amount. Set allowance on proxy.
+ const allowance = new BigNumber(0);
+ const amount = new BigNumber(10);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await noReturnErc20Token.approve.sendTransactionAsync(erc20Proxy.address, allowance, {
+ from: fromAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const initialFromBalance = await noReturnErc20Token.balanceOf.callAsync(fromAddress);
+ const initialToBalance = await noReturnErc20Token.balanceOf.callAsync(toAddress);
+ // Perform a transfer; expect this to fail.
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: erc20Proxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.TransferFailed,
+ );
+ const newFromBalance = await noReturnErc20Token.balanceOf.callAsync(fromAddress);
+ const newToBalance = await noReturnErc20Token.balanceOf.callAsync(toAddress);
+ expect(newFromBalance).to.be.bignumber.equal(initialFromBalance);
+ expect(newToBalance).to.be.bignumber.equal(initialToBalance);
+ });
+
+ it('should revert if caller is not authorized', async () => {
+ // Construct ERC20 asset data
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ // Perform a transfer from fromAddress to toAddress
+ const amount = new BigNumber(10);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: erc20Proxy.address,
+ data,
+ from: notAuthorized,
+ }),
+ RevertReason.SenderNotAuthorized,
+ );
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.deep.equal(erc20Balances);
+ });
+
+ it('should revert if token returns more than 32 bytes', async () => {
+ // Construct ERC20 asset data
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(multipleReturnErc20Token.address);
+ const amount = new BigNumber(10);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ const initialFromBalance = await multipleReturnErc20Token.balanceOf.callAsync(fromAddress);
+ const initialToBalance = await multipleReturnErc20Token.balanceOf.callAsync(toAddress);
+ // Perform a transfer; expect this to fail.
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: erc20Proxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.TransferFailed,
+ );
+ const newFromBalance = await multipleReturnErc20Token.balanceOf.callAsync(fromAddress);
+ const newToBalance = await multipleReturnErc20Token.balanceOf.callAsync(toAddress);
+ expect(newFromBalance).to.be.bignumber.equal(initialFromBalance);
+ expect(newToBalance).to.be.bignumber.equal(initialToBalance);
+ });
+ });
+ });
+
+ describe('ERC721Proxy', () => {
+ it('should revert if undefined function is called', async () => {
+ const undefinedSelector = '0x01020304';
+ await expectTransactionFailedWithoutReasonAsync(
+ web3Wrapper.sendTransactionAsync({
+ from: owner,
+ to: erc721Proxy.address,
+ value: constants.ZERO_AMOUNT,
+ data: undefinedSelector,
+ }),
+ );
+ });
+ it('should have an id of 0x02571792', async () => {
+ const proxyId = await erc721Proxy.getProxyId.callAsync();
+ const expectedProxyId = '0x02571792';
+ expect(proxyId).to.equal(expectedProxyId);
+ });
+ describe('transferFrom', () => {
+ it('should successfully transfer tokens', async () => {
+ // Construct ERC721 asset data
+ const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ // Verify pre-condition
+ const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset).to.be.equal(fromAddress);
+ // Perform a transfer from fromAddress to toAddress
+ const amount = new BigNumber(1);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: erc721Proxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Verify transfer was successful
+ const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(newOwnerFromAsset).to.be.bignumber.equal(toAddress);
+ });
+
+ it('should successfully transfer tokens and ignore extra assetData', async () => {
+ // Construct ERC721 asset data
+ const extraData = '0102030405060708';
+ const encodedAssetData = `${assetDataUtils.encodeERC721AssetData(
+ erc721TokenA.address,
+ erc721AFromTokenId,
+ )}${extraData}`;
+ // Verify pre-condition
+ const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset).to.be.equal(fromAddress);
+ // Perform a transfer from fromAddress to toAddress
+ const amount = new BigNumber(1);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: erc721Proxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Verify transfer was successful
+ const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(newOwnerFromAsset).to.be.bignumber.equal(toAddress);
+ });
+
+ it('should not call onERC721Received when transferring to a smart contract', async () => {
+ // Construct ERC721 asset data
+ const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ // Verify pre-condition
+ const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset).to.be.equal(fromAddress);
+ // Perform a transfer from fromAddress to toAddress
+ const amount = new BigNumber(1);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ erc721Receiver.address,
+ amount,
+ );
+ const logDecoder = new LogDecoder(web3Wrapper, { ...artifacts, ...tokensArtifacts });
+ const tx = await logDecoder.getTxWithDecodedLogsAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: erc721Proxy.address,
+ data,
+ from: authorized,
+ gas: constants.MAX_TRANSFER_FROM_GAS,
+ }),
+ );
+ // Verify that no log was emitted by erc721 receiver
+ expect(tx.logs.length).to.be.equal(1);
+ // Verify transfer was successful
+ const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(newOwnerFromAsset).to.be.bignumber.equal(erc721Receiver.address);
+ });
+
+ it('should revert if transferring 0 amount of a token', async () => {
+ // Construct ERC721 asset data
+ const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ // Verify pre-condition
+ const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset).to.be.equal(fromAddress);
+ // Perform a transfer from fromAddress to toAddress
+ const amount = new BigNumber(0);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: erc721Proxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.InvalidAmount,
+ );
+ const newOwner = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(newOwner).to.be.equal(ownerFromAsset);
+ });
+
+ it('should revert if transferring > 1 amount of a token', async () => {
+ // Construct ERC721 asset data
+ const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ // Verify pre-condition
+ const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset).to.be.equal(fromAddress);
+ // Perform a transfer from fromAddress to toAddress
+ const amount = new BigNumber(500);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: erc721Proxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.InvalidAmount,
+ );
+ const newOwner = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(newOwner).to.be.equal(ownerFromAsset);
+ });
+
+ it('should revert if allowances are too low', async () => {
+ // Construct ERC721 asset data
+ const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ // Verify pre-condition
+ const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset).to.be.equal(fromAddress);
+ // Remove transfer approval for fromAddress.
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc721TokenA.approve.sendTransactionAsync(constants.NULL_ADDRESS, erc721AFromTokenId, {
+ from: fromAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Perform a transfer; expect this to fail.
+ const amount = new BigNumber(1);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: erc721Proxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.TransferFailed,
+ );
+ const newOwner = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(newOwner).to.be.equal(ownerFromAsset);
+ });
+
+ it('should revert if caller is not authorized', async () => {
+ // Construct ERC721 asset data
+ const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ // Verify pre-condition
+ const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset).to.be.equal(fromAddress);
+ // Perform a transfer from fromAddress to toAddress
+ const amount = new BigNumber(1);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: erc721Proxy.address,
+ data,
+ from: notAuthorized,
+ }),
+ RevertReason.SenderNotAuthorized,
+ );
+ const newOwner = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(newOwner).to.be.equal(ownerFromAsset);
+ });
+ });
+ });
+ describe('MultiAssetProxy', () => {
+ it('should revert if undefined function is called', async () => {
+ const undefinedSelector = '0x01020304';
+ await expectTransactionFailedWithoutReasonAsync(
+ web3Wrapper.sendTransactionAsync({
+ from: owner,
+ to: multiAssetProxy.address,
+ value: constants.ZERO_AMOUNT,
+ data: undefinedSelector,
+ }),
+ );
+ });
+ it('should have an id of 0x94cfcdd7', async () => {
+ const proxyId = await multiAssetProxy.getProxyId.callAsync();
+ // first 4 bytes of `keccak256('MultiAsset(uint256[],bytes[])')`
+ const expectedProxyId = '0x94cfcdd7';
+ expect(proxyId).to.equal(expectedProxyId);
+ });
+ describe('transferFrom', () => {
+ it('should transfer a single ERC20 token', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount = new BigNumber(10);
+ const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const amounts = [erc20Amount];
+ const nestedAssetData = [erc20AssetData];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const totalAmount = inputAmount.times(erc20Amount);
+ expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenA.address].minus(totalAmount),
+ );
+ expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenA.address].add(totalAmount),
+ );
+ });
+ it('should successfully transfer multiple of the same ERC20 token', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount1 = new BigNumber(10);
+ const erc20Amount2 = new BigNumber(20);
+ const erc20AssetData1 = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const erc20AssetData2 = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const amounts = [erc20Amount1, erc20Amount2];
+ const nestedAssetData = [erc20AssetData1, erc20AssetData2];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const totalAmount = inputAmount.times(erc20Amount1).plus(inputAmount.times(erc20Amount2));
+ expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenA.address].minus(totalAmount),
+ );
+ expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenA.address].add(totalAmount),
+ );
+ });
+ it('should successfully transfer multiple different ERC20 tokens', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount1 = new BigNumber(10);
+ const erc20Amount2 = new BigNumber(20);
+ const erc20AssetData1 = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const erc20AssetData2 = assetDataUtils.encodeERC20AssetData(erc20TokenB.address);
+ const amounts = [erc20Amount1, erc20Amount2];
+ const nestedAssetData = [erc20AssetData1, erc20AssetData2];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const totalErc20AAmount = inputAmount.times(erc20Amount1);
+ const totalErc20BAmount = inputAmount.times(erc20Amount2);
+ expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenA.address].minus(totalErc20AAmount),
+ );
+ expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenA.address].add(totalErc20AAmount),
+ );
+ expect(newBalances[fromAddress][erc20TokenB.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenB.address].minus(totalErc20BAmount),
+ );
+ expect(newBalances[toAddress][erc20TokenB.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenB.address].add(totalErc20BAmount),
+ );
+ });
+ it('should transfer a single ERC721 token', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc721Amount = new BigNumber(1);
+ const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ const amounts = [erc721Amount];
+ const nestedAssetData = [erc721AssetData];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset).to.be.equal(fromAddress);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(newOwnerFromAsset).to.be.equal(toAddress);
+ });
+ it('should successfully transfer multiple of the same ERC721 token', async () => {
+ const erc721Balances = await erc721Wrapper.getBalancesAsync();
+ const erc721AFromTokenId2 = erc721Balances[fromAddress][erc721TokenA.address][1];
+ const erc721AssetData1 = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ const erc721AssetData2 = assetDataUtils.encodeERC721AssetData(
+ erc721TokenA.address,
+ erc721AFromTokenId2,
+ );
+ const inputAmount = new BigNumber(1);
+ const erc721Amount = new BigNumber(1);
+ const amounts = [erc721Amount, erc721Amount];
+ const nestedAssetData = [erc721AssetData1, erc721AssetData2];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ const ownerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset1).to.be.equal(fromAddress);
+ const ownerFromAsset2 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId2);
+ expect(ownerFromAsset2).to.be.equal(fromAddress);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ gas: constants.MAX_TRANSFER_FROM_GAS,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const newOwnerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ const newOwnerFromAsset2 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId2);
+ expect(newOwnerFromAsset1).to.be.equal(toAddress);
+ expect(newOwnerFromAsset2).to.be.equal(toAddress);
+ });
+ it('should successfully transfer multiple different ERC721 tokens', async () => {
+ const erc721AssetData1 = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ const erc721AssetData2 = assetDataUtils.encodeERC721AssetData(erc721TokenB.address, erc721BFromTokenId);
+ const inputAmount = new BigNumber(1);
+ const erc721Amount = new BigNumber(1);
+ const amounts = [erc721Amount, erc721Amount];
+ const nestedAssetData = [erc721AssetData1, erc721AssetData2];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ const ownerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset1).to.be.equal(fromAddress);
+ const ownerFromAsset2 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId);
+ expect(ownerFromAsset2).to.be.equal(fromAddress);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ gas: constants.MAX_TRANSFER_FROM_GAS,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const newOwnerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ const newOwnerFromAsset2 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId);
+ expect(newOwnerFromAsset1).to.be.equal(toAddress);
+ expect(newOwnerFromAsset2).to.be.equal(toAddress);
+ });
+ it('should successfully transfer a combination of ERC20 and ERC721 tokens', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount = new BigNumber(10);
+ const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const erc721Amount = new BigNumber(1);
+ const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ const amounts = [erc20Amount, erc721Amount];
+ const nestedAssetData = [erc20AssetData, erc721AssetData];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset).to.be.equal(fromAddress);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const totalAmount = inputAmount.times(erc20Amount);
+ expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenA.address].minus(totalAmount),
+ );
+ expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenA.address].add(totalAmount),
+ );
+ const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(newOwnerFromAsset).to.be.equal(toAddress);
+ });
+ it('should successfully transfer tokens and ignore extra assetData', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount = new BigNumber(10);
+ const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const erc721Amount = new BigNumber(1);
+ const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ const amounts = [erc20Amount, erc721Amount];
+ const nestedAssetData = [erc20AssetData, erc721AssetData];
+ const extraData = '0102030405060708';
+ const assetData = `${assetDataInterface.MultiAsset.getABIEncodedTransactionData(
+ amounts,
+ nestedAssetData,
+ )}${extraData}`;
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset).to.be.equal(fromAddress);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const totalAmount = inputAmount.times(erc20Amount);
+ expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenA.address].minus(totalAmount),
+ );
+ expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenA.address].add(totalAmount),
+ );
+ const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(newOwnerFromAsset).to.be.equal(toAddress);
+ });
+ it('should successfully transfer correct amounts when the `amount` > 1', async () => {
+ const inputAmount = new BigNumber(100);
+ const erc20Amount1 = new BigNumber(10);
+ const erc20Amount2 = new BigNumber(20);
+ const erc20AssetData1 = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const erc20AssetData2 = assetDataUtils.encodeERC20AssetData(erc20TokenB.address);
+ const amounts = [erc20Amount1, erc20Amount2];
+ const nestedAssetData = [erc20AssetData1, erc20AssetData2];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const totalErc20AAmount = inputAmount.times(erc20Amount1);
+ const totalErc20BAmount = inputAmount.times(erc20Amount2);
+ expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenA.address].minus(totalErc20AAmount),
+ );
+ expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenA.address].add(totalErc20AAmount),
+ );
+ expect(newBalances[fromAddress][erc20TokenB.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenB.address].minus(totalErc20BAmount),
+ );
+ expect(newBalances[toAddress][erc20TokenB.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenB.address].add(totalErc20BAmount),
+ );
+ });
+ it('should successfully transfer a large amount of tokens', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount1 = new BigNumber(10);
+ const erc20Amount2 = new BigNumber(20);
+ const erc20AssetData1 = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const erc20AssetData2 = assetDataUtils.encodeERC20AssetData(erc20TokenB.address);
+ const erc721Amount = new BigNumber(1);
+ const erc721Balances = await erc721Wrapper.getBalancesAsync();
+ const erc721AFromTokenId2 = erc721Balances[fromAddress][erc721TokenA.address][1];
+ const erc721BFromTokenId2 = erc721Balances[fromAddress][erc721TokenB.address][1];
+ const erc721AssetData1 = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ const erc721AssetData2 = assetDataUtils.encodeERC721AssetData(
+ erc721TokenA.address,
+ erc721AFromTokenId2,
+ );
+ const erc721AssetData3 = assetDataUtils.encodeERC721AssetData(erc721TokenB.address, erc721BFromTokenId);
+ const erc721AssetData4 = assetDataUtils.encodeERC721AssetData(
+ erc721TokenB.address,
+ erc721BFromTokenId2,
+ );
+ const amounts = [erc721Amount, erc20Amount1, erc721Amount, erc20Amount2, erc721Amount, erc721Amount];
+ const nestedAssetData = [
+ erc721AssetData1,
+ erc20AssetData1,
+ erc721AssetData2,
+ erc20AssetData2,
+ erc721AssetData3,
+ erc721AssetData4,
+ ];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ const ownerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset1).to.be.equal(fromAddress);
+ const ownerFromAsset2 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId2);
+ expect(ownerFromAsset2).to.be.equal(fromAddress);
+ const ownerFromAsset3 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId);
+ expect(ownerFromAsset3).to.be.equal(fromAddress);
+ const ownerFromAsset4 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId2);
+ expect(ownerFromAsset4).to.be.equal(fromAddress);
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ gas: constants.MAX_EXECUTE_TRANSACTION_GAS,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const newOwnerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ const newOwnerFromAsset2 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId2);
+ const newOwnerFromAsset3 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId);
+ const newOwnerFromAsset4 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId2);
+ expect(newOwnerFromAsset1).to.be.equal(toAddress);
+ expect(newOwnerFromAsset2).to.be.equal(toAddress);
+ expect(newOwnerFromAsset3).to.be.equal(toAddress);
+ expect(newOwnerFromAsset4).to.be.equal(toAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const totalErc20AAmount = inputAmount.times(erc20Amount1);
+ const totalErc20BAmount = inputAmount.times(erc20Amount2);
+ expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenA.address].minus(totalErc20AAmount),
+ );
+ expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenA.address].add(totalErc20AAmount),
+ );
+ expect(newBalances[fromAddress][erc20TokenB.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenB.address].minus(totalErc20BAmount),
+ );
+ expect(newBalances[toAddress][erc20TokenB.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenB.address].add(totalErc20BAmount),
+ );
+ });
+ it('should revert if a single transfer fails', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount = new BigNumber(10);
+ const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ // 2 is an invalid erc721 amount
+ const erc721Amount = new BigNumber(2);
+ const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ const amounts = [erc20Amount, erc721Amount];
+ const nestedAssetData = [erc20AssetData, erc721AssetData];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.InvalidAmount,
+ );
+ });
+ it('should revert if an AssetProxy is not registered', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount = new BigNumber(10);
+ const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const erc721Amount = new BigNumber(1);
+ const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ const invalidProxyId = '0x12345678';
+ const invalidErc721AssetData = `${invalidProxyId}${erc721AssetData.slice(10)}`;
+ const amounts = [erc20Amount, erc721Amount];
+ const nestedAssetData = [erc20AssetData, invalidErc721AssetData];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.AssetProxyDoesNotExist,
+ );
+ });
+ it('should revert if the length of `amounts` does not match the length of `nestedAssetData`', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount = new BigNumber(10);
+ const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ const amounts = [erc20Amount];
+ const nestedAssetData = [erc20AssetData, erc721AssetData];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.LengthMismatch,
+ );
+ });
+ it('should revert if amounts multiplication results in an overflow', async () => {
+ const inputAmount = new BigNumber(2).pow(128);
+ const erc20Amount = new BigNumber(2).pow(128);
+ const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const amounts = [erc20Amount];
+ const nestedAssetData = [erc20AssetData];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.Uint256Overflow,
+ );
+ });
+ it('should revert if an element of `nestedAssetData` is < 4 bytes long', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount = new BigNumber(10);
+ const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const erc721Amount = new BigNumber(1);
+ const erc721AssetData = '0x123456';
+ const amounts = [erc20Amount, erc721Amount];
+ const nestedAssetData = [erc20AssetData, erc721AssetData];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.LengthGreaterThan3Required,
+ );
+ });
+ it('should revert if caller is not authorized', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount = new BigNumber(10);
+ const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const erc721Amount = new BigNumber(1);
+ const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ const amounts = [erc20Amount, erc721Amount];
+ const nestedAssetData = [erc20AssetData, erc721AssetData];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: notAuthorized,
+ }),
+ RevertReason.SenderNotAuthorized,
+ );
+ });
+ });
+ });
+});
+// tslint:enable:no-unnecessary-type-assertion
+// tslint:disable:max-file-line-count
diff --git a/contracts/protocol/test/exchange/core.ts b/contracts/protocol/test/exchange/core.ts
new file mode 100644
index 000000000..9b948f991
--- /dev/null
+++ b/contracts/protocol/test/exchange/core.ts
@@ -0,0 +1,1178 @@
+import { artifacts as interfacesArtifacts, IAssetDataContract } from '@0x/contracts-interfaces';
+import {
+ chaiSetup,
+ constants,
+ ERC20BalancesByOwner,
+ expectTransactionFailedAsync,
+ getLatestBlockTimestampAsync,
+ increaseTimeAndMineBlockAsync,
+ OrderFactory,
+ OrderStatus,
+ provider,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import {
+ artifacts as tokensArtifacts,
+ DummyERC20TokenContract,
+ DummyERC20TokenTransferEventArgs,
+ DummyERC721TokenContract,
+ DummyNoReturnERC20TokenContract,
+ ReentrantERC20TokenContract,
+} from '@0x/contracts-tokens';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { assetDataUtils, orderHashUtils } from '@0x/order-utils';
+import { RevertReason, SignatureType, SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import * as chai from 'chai';
+import { LogWithDecodedArgs } from 'ethereum-types';
+import ethUtil = require('ethereumjs-util');
+import * as _ from 'lodash';
+
+import { ERC20ProxyContract } from '../../generated-wrappers/erc20_proxy';
+import { ERC721ProxyContract } from '../../generated-wrappers/erc721_proxy';
+import { ExchangeCancelEventArgs, ExchangeContract } from '../../generated-wrappers/exchange';
+import { MultiAssetProxyContract } from '../../generated-wrappers/multi_asset_proxy';
+import { TestStaticCallReceiverContract } from '../../generated-wrappers/test_static_call_receiver';
+import { artifacts } from '../../src/artifacts';
+import { ERC20Wrapper } from '../utils/erc20_wrapper';
+import { ERC721Wrapper } from '../utils/erc721_wrapper';
+import { ExchangeWrapper } from '../utils/exchange_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+const assetDataInterface = new IAssetDataContract(
+ interfacesArtifacts.IAssetData.compilerOutput.abi,
+ constants.NULL_ADDRESS,
+ provider,
+);
+// tslint:disable:no-unnecessary-type-assertion
+describe('Exchange core', () => {
+ let makerAddress: string;
+ let owner: string;
+ let takerAddress: string;
+ let feeRecipientAddress: string;
+
+ let erc20TokenA: DummyERC20TokenContract;
+ let erc20TokenB: DummyERC20TokenContract;
+ let zrxToken: DummyERC20TokenContract;
+ let erc721Token: DummyERC721TokenContract;
+ let noReturnErc20Token: DummyNoReturnERC20TokenContract;
+ let reentrantErc20Token: ReentrantERC20TokenContract;
+ let exchange: ExchangeContract;
+ let erc20Proxy: ERC20ProxyContract;
+ let erc721Proxy: ERC721ProxyContract;
+ let multiAssetProxy: MultiAssetProxyContract;
+ let maliciousWallet: TestStaticCallReceiverContract;
+ let maliciousValidator: TestStaticCallReceiverContract;
+
+ let signedOrder: SignedOrder;
+ let erc20Balances: ERC20BalancesByOwner;
+ let exchangeWrapper: ExchangeWrapper;
+ let erc20Wrapper: ERC20Wrapper;
+ let erc721Wrapper: ERC721Wrapper;
+ let orderFactory: OrderFactory;
+
+ let erc721MakerAssetIds: BigNumber[];
+ let erc721TakerAssetIds: BigNumber[];
+
+ let defaultMakerAssetAddress: string;
+ let defaultTakerAssetAddress: string;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress] = _.slice(accounts, 0, 4));
+
+ erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
+ erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner);
+
+ // Deploy AssetProxies, Exchange, tokens, and malicious contracts
+ erc20Proxy = await erc20Wrapper.deployProxyAsync();
+ erc721Proxy = await erc721Wrapper.deployProxyAsync();
+ multiAssetProxy = await MultiAssetProxyContract.deployFrom0xArtifactAsync(
+ artifacts.MultiAssetProxy,
+ provider,
+ txDefaults,
+ );
+ const numDummyErc20ToDeploy = 3;
+ [erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync(
+ numDummyErc20ToDeploy,
+ constants.DUMMY_TOKEN_DECIMALS,
+ );
+ [erc721Token] = await erc721Wrapper.deployDummyTokensAsync();
+ exchange = await ExchangeContract.deployFrom0xArtifactAsync(
+ artifacts.Exchange,
+ provider,
+ txDefaults,
+ assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ );
+ maliciousWallet = maliciousValidator = await TestStaticCallReceiverContract.deployFrom0xArtifactAsync(
+ artifacts.TestStaticCallReceiver,
+ provider,
+ txDefaults,
+ );
+ reentrantErc20Token = await ReentrantERC20TokenContract.deployFrom0xArtifactAsync(
+ tokensArtifacts.ReentrantERC20Token,
+ provider,
+ txDefaults,
+ exchange.address,
+ );
+
+ // Configure ERC20Proxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(multiAssetProxy.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ // Configure ERC721Proxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(multiAssetProxy.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ // Configure MultiAssetProxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await multiAssetProxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await multiAssetProxy.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await multiAssetProxy.registerAssetProxy.sendTransactionAsync(erc721Proxy.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ // Configure Exchange
+ exchangeWrapper = new ExchangeWrapper(exchange, provider);
+ await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner);
+ await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner);
+ await exchangeWrapper.registerAssetProxyAsync(multiAssetProxy.address, owner);
+
+ // Configure ERC20 tokens
+ await erc20Wrapper.setBalancesAndAllowancesAsync();
+
+ // Configure ERC721 tokens
+ await erc721Wrapper.setBalancesAndAllowancesAsync();
+ const erc721Balances = await erc721Wrapper.getBalancesAsync();
+ erc721MakerAssetIds = erc721Balances[makerAddress][erc721Token.address];
+ erc721TakerAssetIds = erc721Balances[takerAddress][erc721Token.address];
+
+ // Configure order defaults
+ defaultMakerAssetAddress = erc20TokenA.address;
+ defaultTakerAssetAddress = erc20TokenB.address;
+ const defaultOrderParams = {
+ ...constants.STATIC_ORDER_PARAMS,
+ exchangeAddress: exchange.address,
+ makerAddress,
+ feeRecipientAddress,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress),
+ };
+ const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)];
+ orderFactory = new OrderFactory(privateKey, defaultOrderParams);
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('fillOrder', () => {
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ signedOrder = await orderFactory.newSignedOrderAsync();
+ });
+
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow fillOrder to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress),
+ RevertReason.TransferFailed,
+ );
+ });
+ });
+ };
+ describe('fillOrder reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should throw if signature is invalid', async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+
+ const v = ethUtil.toBuffer(signedOrder.signature.slice(0, 4));
+ const invalidR = ethUtil.sha3('invalidR');
+ const invalidS = ethUtil.sha3('invalidS');
+ const signatureType = ethUtil.toBuffer(`0x${signedOrder.signature.slice(-2)}`);
+ const invalidSigBuff = Buffer.concat([v, invalidR, invalidS, signatureType]);
+ const invalidSigHex = `0x${invalidSigBuff.toString('hex')}`;
+ signedOrder.signature = invalidSigHex;
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress),
+ RevertReason.InvalidOrderSignature,
+ );
+ });
+
+ it('should throw if no value is filled', async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync();
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress);
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress),
+ RevertReason.OrderUnfillable,
+ );
+ });
+
+ it('should revert if `isValidSignature` tries to update state when SignatureType=Wallet', async () => {
+ const maliciousMakerAddress = maliciousWallet.address;
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20TokenA.setBalance.sendTransactionAsync(
+ maliciousMakerAddress,
+ constants.INITIAL_ERC20_BALANCE,
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await maliciousWallet.approveERC20.sendTransactionAsync(
+ erc20TokenA.address,
+ erc20Proxy.address,
+ constants.INITIAL_ERC20_ALLOWANCE,
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAddress: maliciousMakerAddress,
+ makerFee: constants.ZERO_AMOUNT,
+ });
+ signedOrder.signature = `0x0${SignatureType.Wallet}`;
+ await expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress),
+ RevertReason.WalletError,
+ );
+ });
+
+ it('should revert if `isValidSignature` tries to update state when SignatureType=Validator', async () => {
+ const isApproved = true;
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await exchange.setSignatureValidatorApproval.sendTransactionAsync(
+ maliciousValidator.address,
+ isApproved,
+ { from: makerAddress },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ signedOrder.signature = `${maliciousValidator.address}0${SignatureType.Validator}`;
+ await expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress),
+ RevertReason.ValidatorError,
+ );
+ });
+
+ it('should not emit transfer events for transfers where from == to', async () => {
+ const txReceipt = await exchangeWrapper.fillOrderAsync(signedOrder, makerAddress);
+ const logs = txReceipt.logs;
+ const transferLogs = _.filter(
+ logs,
+ log => (log as LogWithDecodedArgs<DummyERC20TokenTransferEventArgs>).event === 'Transfer',
+ );
+ expect(transferLogs.length).to.be.equal(2);
+ expect((transferLogs[0] as LogWithDecodedArgs<DummyERC20TokenTransferEventArgs>).address).to.be.equal(
+ zrxToken.address,
+ );
+ expect((transferLogs[0] as LogWithDecodedArgs<DummyERC20TokenTransferEventArgs>).args._from).to.be.equal(
+ makerAddress,
+ );
+ expect((transferLogs[0] as LogWithDecodedArgs<DummyERC20TokenTransferEventArgs>).args._to).to.be.equal(
+ feeRecipientAddress,
+ );
+ expect(
+ (transferLogs[0] as LogWithDecodedArgs<DummyERC20TokenTransferEventArgs>).args._value,
+ ).to.be.bignumber.equal(signedOrder.makerFee);
+ expect((transferLogs[1] as LogWithDecodedArgs<DummyERC20TokenTransferEventArgs>).address).to.be.equal(
+ zrxToken.address,
+ );
+ expect((transferLogs[1] as LogWithDecodedArgs<DummyERC20TokenTransferEventArgs>).args._from).to.be.equal(
+ makerAddress,
+ );
+ expect((transferLogs[1] as LogWithDecodedArgs<DummyERC20TokenTransferEventArgs>).args._to).to.be.equal(
+ feeRecipientAddress,
+ );
+ expect(
+ (transferLogs[1] as LogWithDecodedArgs<DummyERC20TokenTransferEventArgs>).args._value,
+ ).to.be.bignumber.equal(signedOrder.takerFee);
+ });
+ });
+
+ describe('Testing exchange of ERC20 tokens with no return values', () => {
+ before(async () => {
+ noReturnErc20Token = await DummyNoReturnERC20TokenContract.deployFrom0xArtifactAsync(
+ tokensArtifacts.DummyNoReturnERC20Token,
+ provider,
+ txDefaults,
+ constants.DUMMY_TOKEN_NAME,
+ constants.DUMMY_TOKEN_SYMBOL,
+ constants.DUMMY_TOKEN_DECIMALS,
+ constants.DUMMY_TOKEN_TOTAL_SUPPLY,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await noReturnErc20Token.setBalance.sendTransactionAsync(makerAddress, constants.INITIAL_ERC20_BALANCE),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await noReturnErc20Token.approve.sendTransactionAsync(
+ erc20Proxy.address,
+ constants.INITIAL_ERC20_ALLOWANCE,
+ { from: makerAddress },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ });
+ it('should transfer the correct amounts when makerAssetAmount === takerAssetAmount', async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(noReturnErc20Token.address),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ });
+
+ const initialMakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(makerAddress);
+ const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const initialTakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(takerAddress);
+ const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+ const initialFeeRecipientZrxBalance = await zrxToken.balanceOf.callAsync(feeRecipientAddress);
+
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress);
+
+ const finalMakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(makerAddress);
+ const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const finalTakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(takerAddress);
+ const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+ const finalFeeRecipientZrxBalance = await zrxToken.balanceOf.callAsync(feeRecipientAddress);
+
+ expect(finalMakerBalanceA).to.be.bignumber.equal(initialMakerBalanceA.minus(signedOrder.makerAssetAmount));
+ expect(finalMakerBalanceB).to.be.bignumber.equal(initialMakerBalanceB.plus(signedOrder.takerAssetAmount));
+ expect(finalTakerBalanceA).to.be.bignumber.equal(initialTakerBalanceA.plus(signedOrder.makerAssetAmount));
+ expect(finalTakerBalanceB).to.be.bignumber.equal(initialTakerBalanceB.minus(signedOrder.takerAssetAmount));
+ expect(finalMakerZrxBalance).to.be.bignumber.equal(initialMakerZrxBalance.minus(signedOrder.makerFee));
+ expect(finalTakerZrxBalance).to.be.bignumber.equal(initialTakerZrxBalance.minus(signedOrder.takerFee));
+ expect(finalFeeRecipientZrxBalance).to.be.bignumber.equal(
+ initialFeeRecipientZrxBalance.plus(signedOrder.makerFee.plus(signedOrder.takerFee)),
+ );
+ });
+ it('should transfer the correct amounts when makerAssetAmount > takerAssetAmount', async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(noReturnErc20Token.address),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ });
+
+ const initialMakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(makerAddress);
+ const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const initialTakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(takerAddress);
+ const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+ const initialFeeRecipientZrxBalance = await zrxToken.balanceOf.callAsync(feeRecipientAddress);
+
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress);
+
+ const finalMakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(makerAddress);
+ const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const finalTakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(takerAddress);
+ const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+ const finalFeeRecipientZrxBalance = await zrxToken.balanceOf.callAsync(feeRecipientAddress);
+
+ expect(finalMakerBalanceA).to.be.bignumber.equal(initialMakerBalanceA.minus(signedOrder.makerAssetAmount));
+ expect(finalMakerBalanceB).to.be.bignumber.equal(initialMakerBalanceB.plus(signedOrder.takerAssetAmount));
+ expect(finalTakerBalanceA).to.be.bignumber.equal(initialTakerBalanceA.plus(signedOrder.makerAssetAmount));
+ expect(finalTakerBalanceB).to.be.bignumber.equal(initialTakerBalanceB.minus(signedOrder.takerAssetAmount));
+ expect(finalMakerZrxBalance).to.be.bignumber.equal(initialMakerZrxBalance.minus(signedOrder.makerFee));
+ expect(finalTakerZrxBalance).to.be.bignumber.equal(initialTakerZrxBalance.minus(signedOrder.takerFee));
+ expect(finalFeeRecipientZrxBalance).to.be.bignumber.equal(
+ initialFeeRecipientZrxBalance.plus(signedOrder.makerFee.plus(signedOrder.takerFee)),
+ );
+ });
+ it('should transfer the correct amounts when makerAssetAmount < takerAssetAmount', async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(noReturnErc20Token.address),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18),
+ });
+
+ const initialMakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(makerAddress);
+ const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const initialTakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(takerAddress);
+ const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+ const initialFeeRecipientZrxBalance = await zrxToken.balanceOf.callAsync(feeRecipientAddress);
+
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress);
+
+ const finalMakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(makerAddress);
+ const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const finalTakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(takerAddress);
+ const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+ const finalFeeRecipientZrxBalance = await zrxToken.balanceOf.callAsync(feeRecipientAddress);
+
+ expect(finalMakerBalanceA).to.be.bignumber.equal(initialMakerBalanceA.minus(signedOrder.makerAssetAmount));
+ expect(finalMakerBalanceB).to.be.bignumber.equal(initialMakerBalanceB.plus(signedOrder.takerAssetAmount));
+ expect(finalTakerBalanceA).to.be.bignumber.equal(initialTakerBalanceA.plus(signedOrder.makerAssetAmount));
+ expect(finalTakerBalanceB).to.be.bignumber.equal(initialTakerBalanceB.minus(signedOrder.takerAssetAmount));
+ expect(finalMakerZrxBalance).to.be.bignumber.equal(initialMakerZrxBalance.minus(signedOrder.makerFee));
+ expect(finalTakerZrxBalance).to.be.bignumber.equal(initialTakerZrxBalance.minus(signedOrder.takerFee));
+ expect(finalFeeRecipientZrxBalance).to.be.bignumber.equal(
+ initialFeeRecipientZrxBalance.plus(signedOrder.makerFee.plus(signedOrder.takerFee)),
+ );
+ });
+ });
+
+ describe('cancelOrder', () => {
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ signedOrder = await orderFactory.newSignedOrderAsync();
+ });
+
+ it('should throw if not sent by maker', async () => {
+ return expectTransactionFailedAsync(
+ exchangeWrapper.cancelOrderAsync(signedOrder, takerAddress),
+ RevertReason.InvalidMaker,
+ );
+ });
+
+ it('should throw if makerAssetAmount is 0', async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(0),
+ });
+
+ return expectTransactionFailedAsync(
+ exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress),
+ RevertReason.OrderUnfillable,
+ );
+ });
+
+ it('should throw if takerAssetAmount is 0', async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ takerAssetAmount: new BigNumber(0),
+ });
+
+ return expectTransactionFailedAsync(
+ exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress),
+ RevertReason.OrderUnfillable,
+ );
+ });
+
+ it('should be able to cancel a full order', async () => {
+ await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress);
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, {
+ takerAssetFillAmount: signedOrder.takerAssetAmount.div(2),
+ }),
+ RevertReason.OrderUnfillable,
+ );
+ });
+
+ it('should log 1 event with correct arguments', async () => {
+ const res = await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress);
+ expect(res.logs).to.have.length(1);
+
+ const log = res.logs[0] as LogWithDecodedArgs<ExchangeCancelEventArgs>;
+ const logArgs = log.args;
+
+ expect(signedOrder.makerAddress).to.be.equal(logArgs.makerAddress);
+ expect(signedOrder.makerAddress).to.be.equal(logArgs.senderAddress);
+ expect(signedOrder.feeRecipientAddress).to.be.equal(logArgs.feeRecipientAddress);
+ expect(signedOrder.makerAssetData).to.be.equal(logArgs.makerAssetData);
+ expect(signedOrder.takerAssetData).to.be.equal(logArgs.takerAssetData);
+ expect(orderHashUtils.getOrderHashHex(signedOrder)).to.be.equal(logArgs.orderHash);
+ });
+
+ it('should throw if already cancelled', async () => {
+ await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress);
+ return expectTransactionFailedAsync(
+ exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress),
+ RevertReason.OrderUnfillable,
+ );
+ });
+
+ it('should throw if order is expired', async () => {
+ const currentTimestamp = await getLatestBlockTimestampAsync();
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ expirationTimeSeconds: new BigNumber(currentTimestamp).sub(10),
+ });
+ return expectTransactionFailedAsync(
+ exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress),
+ RevertReason.OrderUnfillable,
+ );
+ });
+
+ it('should throw if rounding error is greater than 0.1%', async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(1001),
+ takerAssetAmount: new BigNumber(3),
+ });
+
+ const fillTakerAssetAmount1 = new BigNumber(2);
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, {
+ takerAssetFillAmount: fillTakerAssetAmount1,
+ });
+
+ const fillTakerAssetAmount2 = new BigNumber(1);
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, {
+ takerAssetFillAmount: fillTakerAssetAmount2,
+ }),
+ RevertReason.RoundingError,
+ );
+ });
+ });
+
+ describe('cancelOrdersUpTo', () => {
+ it('should fail to set orderEpoch less than current orderEpoch', async () => {
+ const orderEpoch = new BigNumber(1);
+ await exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress);
+ const lesserOrderEpoch = new BigNumber(0);
+ return expectTransactionFailedAsync(
+ exchangeWrapper.cancelOrdersUpToAsync(lesserOrderEpoch, makerAddress),
+ RevertReason.InvalidNewOrderEpoch,
+ );
+ });
+
+ it('should fail to set orderEpoch equal to existing orderEpoch', async () => {
+ const orderEpoch = new BigNumber(1);
+ await exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress);
+ return expectTransactionFailedAsync(
+ exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress),
+ RevertReason.InvalidNewOrderEpoch,
+ );
+ });
+
+ it('should cancel only orders with a orderEpoch less than existing orderEpoch', async () => {
+ // Cancel all transactions with a orderEpoch less than 1
+ const orderEpoch = new BigNumber(1);
+ await exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress);
+
+ // Create 3 orders with orderEpoch values: 0,1,2,3
+ // Since we cancelled with orderEpoch=1, orders with orderEpoch<=1 will not be processed
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ const signedOrders = [
+ await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(9), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(9), 18),
+ salt: new BigNumber(0),
+ }),
+ await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(79), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(79), 18),
+ salt: new BigNumber(1),
+ }),
+ await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(979), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(979), 18),
+ salt: new BigNumber(2),
+ }),
+ await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(7979), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(7979), 18),
+ salt: new BigNumber(3),
+ }),
+ ];
+ await exchangeWrapper.batchFillOrdersNoThrowAsync(signedOrders, takerAddress, {
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 600000,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const fillMakerAssetAmount = signedOrders[2].makerAssetAmount.add(signedOrders[3].makerAssetAmount);
+ const fillTakerAssetAmount = signedOrders[2].takerAssetAmount.add(signedOrders[3].takerAssetAmount);
+ const makerFee = signedOrders[2].makerFee.add(signedOrders[3].makerFee);
+ const takerFee = signedOrders[2].takerFee.add(signedOrders[3].takerFee);
+ expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerAssetAddress].minus(fillMakerAssetAmount),
+ );
+ expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultTakerAssetAddress].add(fillTakerAssetAmount),
+ );
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerFee),
+ );
+ expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultTakerAssetAddress].minus(fillTakerAssetAmount),
+ );
+ expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerAssetAddress].add(fillMakerAssetAmount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].minus(takerFee),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)),
+ );
+ });
+ });
+
+ describe('Testing Exchange of ERC721 Tokens', () => {
+ it('should throw when maker does not own the token with id makerAssetId', async () => {
+ // Construct Exchange parameters
+ const makerAssetId = erc721TakerAssetIds[0];
+ const takerAssetId = erc721TakerAssetIds[1];
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(1),
+ takerAssetAmount: new BigNumber(1),
+ makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ takerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, takerAssetId),
+ });
+ // Verify pre-conditions
+ const initialOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
+ expect(initialOwnerMakerAsset).to.be.bignumber.not.equal(makerAddress);
+ const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId);
+ expect(initialOwnerTakerAsset).to.be.bignumber.equal(takerAddress);
+ // Call Exchange
+ const takerAssetFillAmount = signedOrder.takerAssetAmount;
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount }),
+ RevertReason.TransferFailed,
+ );
+ });
+
+ it('should throw when taker does not own the token with id takerAssetId', async () => {
+ // Construct Exchange parameters
+ const makerAssetId = erc721MakerAssetIds[0];
+ const takerAssetId = erc721MakerAssetIds[1];
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(1),
+ takerAssetAmount: new BigNumber(1),
+ makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ takerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, takerAssetId),
+ });
+ // Verify pre-conditions
+ const initialOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
+ expect(initialOwnerMakerAsset).to.be.bignumber.equal(makerAddress);
+ const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId);
+ expect(initialOwnerTakerAsset).to.be.bignumber.not.equal(takerAddress);
+ // Call Exchange
+ const takerAssetFillAmount = signedOrder.takerAssetAmount;
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount }),
+ RevertReason.TransferFailed,
+ );
+ });
+
+ it('should throw when makerAssetAmount is greater than 1', async () => {
+ // Construct Exchange parameters
+ const makerAssetId = erc721MakerAssetIds[0];
+ const takerAssetId = erc721TakerAssetIds[0];
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(2),
+ takerAssetAmount: new BigNumber(1),
+ makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ takerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, takerAssetId),
+ });
+ // Verify pre-conditions
+ const initialOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
+ expect(initialOwnerMakerAsset).to.be.bignumber.equal(makerAddress);
+ const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId);
+ expect(initialOwnerTakerAsset).to.be.bignumber.equal(takerAddress);
+ // Call Exchange
+ const takerAssetFillAmount = signedOrder.takerAssetAmount;
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount }),
+ RevertReason.InvalidAmount,
+ );
+ });
+
+ it('should throw when takerAssetAmount is greater than 1', async () => {
+ // Construct Exchange parameters
+ const makerAssetId = erc721MakerAssetIds[0];
+ const takerAssetId = erc721TakerAssetIds[0];
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(1),
+ takerAssetAmount: new BigNumber(500),
+ makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ takerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, takerAssetId),
+ });
+ // Verify pre-conditions
+ const initialOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
+ expect(initialOwnerMakerAsset).to.be.bignumber.equal(makerAddress);
+ const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId);
+ expect(initialOwnerTakerAsset).to.be.bignumber.equal(takerAddress);
+ // Call Exchange
+ const takerAssetFillAmount = signedOrder.takerAssetAmount;
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount }),
+ RevertReason.InvalidAmount,
+ );
+ });
+
+ it('should throw on partial fill', async () => {
+ // Construct Exchange parameters
+ const makerAssetId = erc721MakerAssetIds[0];
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(1),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress),
+ });
+ // Call Exchange
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount }),
+ RevertReason.RoundingError,
+ );
+ });
+ });
+
+ describe('Testing exchange of multiple assets', () => {
+ it('should allow multiple assets to be exchanged for a single asset', async () => {
+ const makerAmounts = [new BigNumber(10), new BigNumber(20)];
+ const makerNestedAssetData = [
+ assetDataUtils.encodeERC20AssetData(erc20TokenA.address),
+ assetDataUtils.encodeERC20AssetData(erc20TokenB.address),
+ ];
+ const makerAssetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(
+ makerAmounts,
+ makerNestedAssetData,
+ );
+ const makerAssetAmount = new BigNumber(1);
+ const takerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
+ const takerAssetAmount = new BigNumber(10);
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData,
+ takerAssetData,
+ makerAssetAmount,
+ takerAssetAmount,
+ makerFee: constants.ZERO_AMOUNT,
+ takerFee: constants.ZERO_AMOUNT,
+ });
+
+ const initialMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress);
+ const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const initialTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress);
+ const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress);
+
+ const finalMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress);
+ const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const finalTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress);
+ const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+
+ expect(finalMakerBalanceA).to.be.bignumber.equal(
+ initialMakerBalanceA.minus(makerAmounts[0].times(makerAssetAmount)),
+ );
+ expect(finalMakerBalanceB).to.be.bignumber.equal(
+ initialMakerBalanceB.minus(makerAmounts[1].times(makerAssetAmount)),
+ );
+ expect(finalMakerZrxBalance).to.be.bignumber.equal(initialMakerZrxBalance.plus(takerAssetAmount));
+ expect(finalTakerBalanceA).to.be.bignumber.equal(
+ initialTakerBalanceA.plus(makerAmounts[0].times(makerAssetAmount)),
+ );
+ expect(finalTakerBalanceB).to.be.bignumber.equal(
+ initialTakerBalanceB.plus(makerAmounts[1].times(makerAssetAmount)),
+ );
+ expect(finalTakerZrxBalance).to.be.bignumber.equal(initialTakerZrxBalance.minus(takerAssetAmount));
+ });
+ it('should allow multiple assets to be exchanged for multiple assets', async () => {
+ const makerAmounts = [new BigNumber(10), new BigNumber(20)];
+ const makerNestedAssetData = [
+ assetDataUtils.encodeERC20AssetData(erc20TokenA.address),
+ assetDataUtils.encodeERC20AssetData(erc20TokenB.address),
+ ];
+ const makerAssetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(
+ makerAmounts,
+ makerNestedAssetData,
+ );
+ const makerAssetAmount = new BigNumber(1);
+ const takerAmounts = [new BigNumber(10), new BigNumber(1)];
+ const takerAssetId = erc721TakerAssetIds[0];
+ const takerNestedAssetData = [
+ assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ assetDataUtils.encodeERC721AssetData(erc721Token.address, takerAssetId),
+ ];
+ const takerAssetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(
+ takerAmounts,
+ takerNestedAssetData,
+ );
+ const takerAssetAmount = new BigNumber(1);
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData,
+ takerAssetData,
+ makerAssetAmount,
+ takerAssetAmount,
+ makerFee: constants.ZERO_AMOUNT,
+ takerFee: constants.ZERO_AMOUNT,
+ });
+
+ const initialMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress);
+ const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const initialTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress);
+ const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+ const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId);
+ expect(initialOwnerTakerAsset).to.be.bignumber.equal(takerAddress);
+
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress);
+
+ const finalMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress);
+ const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const finalTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress);
+ const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+ const finalOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId);
+
+ expect(finalMakerBalanceA).to.be.bignumber.equal(
+ initialMakerBalanceA.minus(makerAmounts[0].times(makerAssetAmount)),
+ );
+ expect(finalMakerBalanceB).to.be.bignumber.equal(
+ initialMakerBalanceB.minus(makerAmounts[1].times(makerAssetAmount)),
+ );
+ expect(finalMakerZrxBalance).to.be.bignumber.equal(
+ initialMakerZrxBalance.plus(takerAmounts[0].times(takerAssetAmount)),
+ );
+ expect(finalTakerBalanceA).to.be.bignumber.equal(
+ initialTakerBalanceA.plus(makerAmounts[0].times(makerAssetAmount)),
+ );
+ expect(finalTakerBalanceB).to.be.bignumber.equal(
+ initialTakerBalanceB.plus(makerAmounts[1].times(makerAssetAmount)),
+ );
+ expect(finalTakerZrxBalance).to.be.bignumber.equal(
+ initialTakerZrxBalance.minus(takerAmounts[0].times(takerAssetAmount)),
+ );
+ expect(finalOwnerTakerAsset).to.be.equal(makerAddress);
+ });
+ it('should allow an order selling multiple assets to be partially filled', async () => {
+ const makerAmounts = [new BigNumber(10), new BigNumber(20)];
+ const makerNestedAssetData = [
+ assetDataUtils.encodeERC20AssetData(erc20TokenA.address),
+ assetDataUtils.encodeERC20AssetData(erc20TokenB.address),
+ ];
+ const makerAssetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(
+ makerAmounts,
+ makerNestedAssetData,
+ );
+ const makerAssetAmount = new BigNumber(30);
+ const takerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
+ const takerAssetAmount = new BigNumber(10);
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData,
+ takerAssetData,
+ makerAssetAmount,
+ takerAssetAmount,
+ makerFee: constants.ZERO_AMOUNT,
+ takerFee: constants.ZERO_AMOUNT,
+ });
+
+ const initialMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress);
+ const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const initialTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress);
+ const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+
+ const takerAssetFillAmount = takerAssetAmount.dividedToIntegerBy(2);
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, {
+ takerAssetFillAmount,
+ });
+
+ const finalMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress);
+ const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const finalTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress);
+ const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+
+ expect(finalMakerBalanceA).to.be.bignumber.equal(
+ initialMakerBalanceA.minus(
+ makerAmounts[0].times(
+ makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ ),
+ );
+ expect(finalMakerBalanceB).to.be.bignumber.equal(
+ initialMakerBalanceB.minus(
+ makerAmounts[1].times(
+ makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ ),
+ );
+ expect(finalMakerZrxBalance).to.be.bignumber.equal(
+ initialMakerZrxBalance.plus(
+ takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ );
+ expect(finalTakerBalanceA).to.be.bignumber.equal(
+ initialTakerBalanceA.plus(
+ makerAmounts[0].times(
+ makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ ),
+ );
+ expect(finalTakerBalanceB).to.be.bignumber.equal(
+ initialTakerBalanceB.plus(
+ makerAmounts[1].times(
+ makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ ),
+ );
+ expect(finalTakerZrxBalance).to.be.bignumber.equal(
+ initialTakerZrxBalance.minus(
+ takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ );
+ });
+ it('should allow an order buying multiple assets to be partially filled', async () => {
+ const takerAmounts = [new BigNumber(10), new BigNumber(20)];
+ const takerNestedAssetData = [
+ assetDataUtils.encodeERC20AssetData(erc20TokenA.address),
+ assetDataUtils.encodeERC20AssetData(erc20TokenB.address),
+ ];
+ const takerAssetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(
+ takerAmounts,
+ takerNestedAssetData,
+ );
+ const takerAssetAmount = new BigNumber(30);
+ const makerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
+ const makerAssetAmount = new BigNumber(10);
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData,
+ takerAssetData,
+ makerAssetAmount,
+ takerAssetAmount,
+ makerFee: constants.ZERO_AMOUNT,
+ takerFee: constants.ZERO_AMOUNT,
+ });
+
+ const initialMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress);
+ const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const initialTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress);
+ const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+
+ const takerAssetFillAmount = takerAssetAmount.dividedToIntegerBy(2);
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, {
+ takerAssetFillAmount,
+ });
+
+ const finalMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress);
+ const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const finalTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress);
+ const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+
+ expect(finalMakerBalanceA).to.be.bignumber.equal(
+ initialMakerBalanceA.plus(
+ takerAmounts[0].times(
+ takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ ),
+ );
+ expect(finalMakerBalanceB).to.be.bignumber.equal(
+ initialMakerBalanceB.plus(
+ takerAmounts[1].times(
+ takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ ),
+ );
+ expect(finalMakerZrxBalance).to.be.bignumber.equal(
+ initialMakerZrxBalance.minus(
+ makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ );
+ expect(finalTakerBalanceA).to.be.bignumber.equal(
+ initialTakerBalanceA.minus(
+ takerAmounts[0].times(
+ takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ ),
+ );
+ expect(finalTakerBalanceB).to.be.bignumber.equal(
+ initialTakerBalanceB.minus(
+ takerAmounts[1].times(
+ takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ ),
+ );
+ expect(finalTakerZrxBalance).to.be.bignumber.equal(
+ initialTakerZrxBalance.plus(
+ makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ );
+ });
+ });
+
+ describe('getOrderInfo', () => {
+ beforeEach(async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync();
+ });
+ it('should return the correct orderInfo for an unfilled valid order', async () => {
+ const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder);
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = new BigNumber(0);
+ const expectedOrderStatus = OrderStatus.FILLABLE;
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ it('should return the correct orderInfo for a fully filled order', async () => {
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress);
+ const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder);
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = signedOrder.takerAssetAmount;
+ const expectedOrderStatus = OrderStatus.FULLY_FILLED;
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ it('should return the correct orderInfo for a partially filled order', async () => {
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount });
+ const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder);
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = takerAssetFillAmount;
+ const expectedOrderStatus = OrderStatus.FILLABLE;
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ it('should return the correct orderInfo for a cancelled and unfilled order', async () => {
+ await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress);
+ const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder);
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = new BigNumber(0);
+ const expectedOrderStatus = OrderStatus.CANCELLED;
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ it('should return the correct orderInfo for a cancelled and partially filled order', async () => {
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount });
+ await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress);
+ const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder);
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = takerAssetFillAmount;
+ const expectedOrderStatus = OrderStatus.CANCELLED;
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ it('should return the correct orderInfo for an expired and unfilled order', async () => {
+ const currentTimestamp = await getLatestBlockTimestampAsync();
+ const timeUntilExpiration = signedOrder.expirationTimeSeconds.minus(currentTimestamp).toNumber();
+ await increaseTimeAndMineBlockAsync(timeUntilExpiration);
+ const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder);
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = new BigNumber(0);
+ const expectedOrderStatus = OrderStatus.EXPIRED;
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ it('should return the correct orderInfo for an expired and partially filled order', async () => {
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount });
+ const currentTimestamp = await getLatestBlockTimestampAsync();
+ const timeUntilExpiration = signedOrder.expirationTimeSeconds.minus(currentTimestamp).toNumber();
+ await increaseTimeAndMineBlockAsync(timeUntilExpiration);
+ const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder);
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = takerAssetFillAmount;
+ const expectedOrderStatus = OrderStatus.EXPIRED;
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ it('should return the correct orderInfo for an expired and fully filled order', async () => {
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress);
+ const currentTimestamp = await getLatestBlockTimestampAsync();
+ const timeUntilExpiration = signedOrder.expirationTimeSeconds.minus(currentTimestamp).toNumber();
+ await increaseTimeAndMineBlockAsync(timeUntilExpiration);
+ const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder);
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = signedOrder.takerAssetAmount;
+ // FULLY_FILLED takes precedence over EXPIRED
+ const expectedOrderStatus = OrderStatus.FULLY_FILLED;
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ it('should return the correct orderInfo for an order with a makerAssetAmount of 0', async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync({ makerAssetAmount: new BigNumber(0) });
+ const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder);
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = new BigNumber(0);
+ const expectedOrderStatus = OrderStatus.INVALID_MAKER_ASSET_AMOUNT;
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ it('should return the correct orderInfo for an order with a takerAssetAmount of 0', async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync({ takerAssetAmount: new BigNumber(0) });
+ const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder);
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = new BigNumber(0);
+ const expectedOrderStatus = OrderStatus.INVALID_TAKER_ASSET_AMOUNT;
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ });
+});
+// tslint:disable:max-file-line-count
+// tslint:enable:no-unnecessary-type-assertion
diff --git a/contracts/protocol/test/exchange/dispatcher.ts b/contracts/protocol/test/exchange/dispatcher.ts
new file mode 100644
index 000000000..5b1ac1e20
--- /dev/null
+++ b/contracts/protocol/test/exchange/dispatcher.ts
@@ -0,0 +1,284 @@
+import {
+ chaiSetup,
+ constants,
+ expectTransactionFailedAsync,
+ LogDecoder,
+ provider,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import { DummyERC20TokenContract } from '@0x/contracts-tokens';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { assetDataUtils } from '@0x/order-utils';
+import { AssetProxyId, RevertReason } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import * as chai from 'chai';
+import { LogWithDecodedArgs } from 'ethereum-types';
+import * as _ from 'lodash';
+
+import { ERC20ProxyContract } from '../../generated-wrappers/erc20_proxy';
+import { ERC721ProxyContract } from '../../generated-wrappers/erc721_proxy';
+import {
+ TestAssetProxyDispatcherAssetProxyRegisteredEventArgs,
+ TestAssetProxyDispatcherContract,
+} from '../../generated-wrappers/test_asset_proxy_dispatcher';
+import { artifacts } from '../../src/artifacts';
+import { ERC20Wrapper } from '../utils/erc20_wrapper';
+import { ERC721Wrapper } from '../utils/erc721_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+// tslint:disable:no-unnecessary-type-assertion
+describe('AssetProxyDispatcher', () => {
+ let owner: string;
+ let notOwner: string;
+ let makerAddress: string;
+ let takerAddress: string;
+
+ let zrxToken: DummyERC20TokenContract;
+ let erc20Proxy: ERC20ProxyContract;
+ let erc721Proxy: ERC721ProxyContract;
+ let assetProxyDispatcher: TestAssetProxyDispatcherContract;
+
+ let erc20Wrapper: ERC20Wrapper;
+ let erc721Wrapper: ERC721Wrapper;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ // Setup accounts & addresses
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ const usedAddresses = ([owner, notOwner, makerAddress, takerAddress] = _.slice(accounts, 0, 4));
+
+ erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
+ erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner);
+
+ const numDummyErc20ToDeploy = 1;
+ [zrxToken] = await erc20Wrapper.deployDummyTokensAsync(numDummyErc20ToDeploy, constants.DUMMY_TOKEN_DECIMALS);
+ erc20Proxy = await erc20Wrapper.deployProxyAsync();
+ await erc20Wrapper.setBalancesAndAllowancesAsync();
+
+ erc721Proxy = await erc721Wrapper.deployProxyAsync();
+
+ assetProxyDispatcher = await TestAssetProxyDispatcherContract.deployFrom0xArtifactAsync(
+ artifacts.TestAssetProxyDispatcher,
+ provider,
+ txDefaults,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(assetProxyDispatcher.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(assetProxyDispatcher.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('registerAssetProxy', () => {
+ it('should record proxy upon registration', async () => {
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20);
+ expect(proxyAddress).to.be.equal(erc20Proxy.address);
+ });
+
+ it('should be able to record multiple proxies', async () => {
+ // Record first proxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ let proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20);
+ expect(proxyAddress).to.be.equal(erc20Proxy.address);
+ // Record another proxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc721Proxy.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC721);
+ expect(proxyAddress).to.be.equal(erc721Proxy.address);
+ });
+
+ it('should throw if a proxy with the same id is already registered', async () => {
+ // Initial registration
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20);
+ expect(proxyAddress).to.be.equal(erc20Proxy.address);
+ // Deploy a new version of the ERC20 Transfer Proxy contract
+ const newErc20TransferProxy = await ERC20ProxyContract.deployFrom0xArtifactAsync(
+ artifacts.ERC20Proxy,
+ provider,
+ txDefaults,
+ );
+ // Register new ERC20 Transfer Proxy contract
+ return expectTransactionFailedAsync(
+ assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(newErc20TransferProxy.address, {
+ from: owner,
+ }),
+ RevertReason.AssetProxyAlreadyExists,
+ );
+ });
+
+ it('should throw if requesting address is not owner', async () => {
+ return expectTransactionFailedAsync(
+ assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: notOwner }),
+ RevertReason.OnlyContractOwner,
+ );
+ });
+
+ it('should log an event with correct arguments when an asset proxy is registered', async () => {
+ const logDecoder = new LogDecoder(web3Wrapper, artifacts);
+ const txReceipt = await logDecoder.getTxWithDecodedLogsAsync(
+ await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }),
+ );
+ const logs = txReceipt.logs;
+ const log = logs[0] as LogWithDecodedArgs<TestAssetProxyDispatcherAssetProxyRegisteredEventArgs>;
+ expect(log.args.id).to.equal(AssetProxyId.ERC20);
+ expect(log.args.assetProxy).to.equal(erc20Proxy.address);
+ });
+ });
+
+ describe('getAssetProxy', () => {
+ it('should return correct address of registered proxy', async () => {
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20);
+ expect(proxyAddress).to.be.equal(erc20Proxy.address);
+ });
+
+ it('should return NULL address if requesting non-existent proxy', async () => {
+ const proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20);
+ expect(proxyAddress).to.be.equal(constants.NULL_ADDRESS);
+ });
+ });
+
+ describe('dispatchTransferFrom', () => {
+ it('should dispatch transfer to registered proxy', async () => {
+ // Register ERC20 proxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Construct metadata for ERC20 proxy
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
+
+ // Perform a transfer from makerAddress to takerAddress
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ const amount = new BigNumber(10);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.publicDispatchTransferFrom.sendTransactionAsync(
+ encodedAssetData,
+ makerAddress,
+ takerAddress,
+ amount,
+ { from: owner },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Verify transfer was successful
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(amount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].add(amount),
+ );
+ });
+
+ it('should not dispatch a transfer if amount == 0', async () => {
+ // Register ERC20 proxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Construct metadata for ERC20 proxy
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
+
+ // Perform a transfer from makerAddress to takerAddress
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ const amount = constants.ZERO_AMOUNT;
+ const txReceipt = await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.publicDispatchTransferFrom.sendTransactionAsync(
+ encodedAssetData,
+ makerAddress,
+ takerAddress,
+ amount,
+ { from: owner },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ expect(txReceipt.logs.length).to.be.equal(0);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.deep.equal(erc20Balances);
+ });
+
+ it('should not dispatch a transfer if from == to', async () => {
+ // Register ERC20 proxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Construct metadata for ERC20 proxy
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
+
+ // Perform a transfer from makerAddress to takerAddress
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ const amount = new BigNumber(10);
+ const txReceipt = await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.publicDispatchTransferFrom.sendTransactionAsync(
+ encodedAssetData,
+ makerAddress,
+ makerAddress,
+ amount,
+ { from: owner },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ expect(txReceipt.logs.length).to.be.equal(0);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.deep.equal(erc20Balances);
+ });
+
+ it('should throw if dispatching to unregistered proxy', async () => {
+ // Construct metadata for ERC20 proxy
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
+ // Perform a transfer from makerAddress to takerAddress
+ const amount = new BigNumber(10);
+ return expectTransactionFailedAsync(
+ assetProxyDispatcher.publicDispatchTransferFrom.sendTransactionAsync(
+ encodedAssetData,
+ makerAddress,
+ takerAddress,
+ amount,
+ { from: owner },
+ ),
+ RevertReason.AssetProxyDoesNotExist,
+ );
+ });
+ });
+});
+// tslint:enable:no-unnecessary-type-assertion
diff --git a/contracts/protocol/test/exchange/fill_order.ts b/contracts/protocol/test/exchange/fill_order.ts
new file mode 100644
index 000000000..2bdbe4855
--- /dev/null
+++ b/contracts/protocol/test/exchange/fill_order.ts
@@ -0,0 +1,314 @@
+import {
+ AllowanceAmountScenario,
+ AssetDataScenario,
+ BalanceAmountScenario,
+ chaiSetup,
+ ExpirationTimeSecondsScenario,
+ FeeRecipientAddressScenario,
+ FillScenario,
+ OrderAssetAmountScenario,
+ provider,
+ TakerAssetFillAmountScenario,
+ TakerScenario,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import * as _ from 'lodash';
+
+import {
+ FillOrderCombinatorialUtils,
+ fillOrderCombinatorialUtilsFactoryAsync,
+} from '../utils/fill_order_combinatorial_utils';
+
+chaiSetup.configure();
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+
+const defaultFillScenario = {
+ orderScenario: {
+ takerScenario: TakerScenario.Unspecified,
+ feeRecipientScenario: FeeRecipientAddressScenario.EthUserAddress,
+ makerAssetAmountScenario: OrderAssetAmountScenario.Large,
+ takerAssetAmountScenario: OrderAssetAmountScenario.Large,
+ makerFeeScenario: OrderAssetAmountScenario.Large,
+ takerFeeScenario: OrderAssetAmountScenario.Large,
+ expirationTimeSecondsScenario: ExpirationTimeSecondsScenario.InFuture,
+ makerAssetDataScenario: AssetDataScenario.ERC20NonZRXEighteenDecimals,
+ takerAssetDataScenario: AssetDataScenario.ERC20NonZRXEighteenDecimals,
+ },
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount,
+ makerStateScenario: {
+ traderAssetBalance: BalanceAmountScenario.Higher,
+ traderAssetAllowance: AllowanceAmountScenario.Higher,
+ zrxFeeBalance: BalanceAmountScenario.Higher,
+ zrxFeeAllowance: AllowanceAmountScenario.Higher,
+ },
+ takerStateScenario: {
+ traderAssetBalance: BalanceAmountScenario.Higher,
+ traderAssetAllowance: AllowanceAmountScenario.Higher,
+ zrxFeeBalance: BalanceAmountScenario.Higher,
+ zrxFeeAllowance: AllowanceAmountScenario.Higher,
+ },
+};
+
+describe('FillOrder Tests', () => {
+ let fillOrderCombinatorialUtils: FillOrderCombinatorialUtils;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ fillOrderCombinatorialUtils = await fillOrderCombinatorialUtilsFactoryAsync(web3Wrapper, txDefaults);
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('fillOrder', () => {
+ const test = (fillScenarios: FillScenario[]) => {
+ _.forEach(fillScenarios, fillScenario => {
+ const description = `Combinatorial OrderFill: ${JSON.stringify(fillScenario)}`;
+ it(description, async () => {
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+ });
+ };
+
+ const allFillScenarios = FillOrderCombinatorialUtils.generateFillOrderCombinations();
+ describe('Combinatorially generated fills orders', () => test(allFillScenarios));
+
+ it('should transfer the correct amounts when makerAssetAmount === takerAssetAmount', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+ it('should transfer the correct amounts when makerAssetAmount > takerAssetAmount', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ takerAssetAmountScenario: OrderAssetAmountScenario.Small,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+ it('should transfer the correct amounts when makerAssetAmount < takerAssetAmount', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ makerAssetAmountScenario: OrderAssetAmountScenario.Small,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+ it('should transfer the correct amounts when makerAssetAmount < takerAssetAmount with zero decimals', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ makerAssetAmountScenario: OrderAssetAmountScenario.Small,
+ makerAssetDataScenario: AssetDataScenario.ERC20ZeroDecimals,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+ it('should transfer the correct amounts when taker is specified and order is claimed by taker', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ takerScenario: TakerScenario.CorrectlySpecified,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+ it('should fill remaining value if takerAssetFillAmount > remaining takerAssetAmount', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount,
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+ it('should throw when taker is specified and order is claimed by other', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ takerScenario: TakerScenario.IncorrectlySpecified,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should throw if makerAssetAmount is 0', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ makerAssetAmountScenario: OrderAssetAmountScenario.Zero,
+ },
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount,
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should throw if takerAssetAmount is 0', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ takerAssetAmountScenario: OrderAssetAmountScenario.Zero,
+ },
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount,
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should throw if takerAssetFillAmount is 0', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario.Zero,
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should throw if an order is expired', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ expirationTimeSecondsScenario: ExpirationTimeSecondsScenario.InPast,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should throw if maker erc20Balances are too low to fill order', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ makerStateScenario: {
+ ...defaultFillScenario.makerStateScenario,
+ traderAssetBalance: BalanceAmountScenario.TooLow,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should throw if taker erc20Balances are too low to fill order', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ takerStateScenario: {
+ ...defaultFillScenario.makerStateScenario,
+ traderAssetBalance: BalanceAmountScenario.TooLow,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should throw if maker allowances are too low to fill order', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ makerStateScenario: {
+ ...defaultFillScenario.makerStateScenario,
+ traderAssetAllowance: AllowanceAmountScenario.TooLow,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should throw if taker allowances are too low to fill order', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ takerStateScenario: {
+ ...defaultFillScenario.makerStateScenario,
+ traderAssetAllowance: AllowanceAmountScenario.TooLow,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+ });
+
+ describe('Testing exchange of ERC721 Tokens', () => {
+ it('should successfully exchange a single token between the maker and taker (via fillOrder)', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ makerAssetDataScenario: AssetDataScenario.ERC721,
+ takerAssetDataScenario: AssetDataScenario.ERC721,
+ },
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should successfully fill order when makerAsset is ERC721 and takerAsset is ERC20', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ makerAssetDataScenario: AssetDataScenario.ERC721,
+ takerAssetDataScenario: AssetDataScenario.ERC20NonZRXEighteenDecimals,
+ },
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario, true);
+ });
+
+ it('should successfully fill order when makerAsset is ERC20 and takerAsset is ERC721', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ makerAssetDataScenario: AssetDataScenario.ERC20NonZRXEighteenDecimals,
+ takerAssetDataScenario: AssetDataScenario.ERC721,
+ },
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should successfully fill order when makerAsset is ERC721 and approveAll is set for it', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ makerAssetDataScenario: AssetDataScenario.ERC721,
+ takerAssetDataScenario: AssetDataScenario.ERC20NonZRXEighteenDecimals,
+ },
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
+ makerStateScenario: {
+ ...defaultFillScenario.makerStateScenario,
+ traderAssetAllowance: AllowanceAmountScenario.Unlimited,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should successfully fill order when makerAsset and takerAsset are ERC721 and approveAll is set for them', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ makerAssetDataScenario: AssetDataScenario.ERC721,
+ takerAssetDataScenario: AssetDataScenario.ERC721,
+ },
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
+ makerStateScenario: {
+ ...defaultFillScenario.makerStateScenario,
+ traderAssetAllowance: AllowanceAmountScenario.Unlimited,
+ },
+ takerStateScenario: {
+ ...defaultFillScenario.takerStateScenario,
+ traderAssetAllowance: AllowanceAmountScenario.Unlimited,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+ });
+});
diff --git a/contracts/protocol/test/exchange/internal.ts b/contracts/protocol/test/exchange/internal.ts
new file mode 100644
index 000000000..972f5efb6
--- /dev/null
+++ b/contracts/protocol/test/exchange/internal.ts
@@ -0,0 +1,472 @@
+import {
+ bytes32Values,
+ chaiSetup,
+ constants,
+ FillResults,
+ getRevertReasonOrErrorMessageForSendTransactionAsync,
+ provider,
+ testCombinatoriallyWithReferenceFuncAsync,
+ txDefaults,
+ uint256Values,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { Order, RevertReason, SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+
+import { TestExchangeInternalsContract } from '../../generated-wrappers/test_exchange_internals';
+import { artifacts } from '../../src/artifacts';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+
+const MAX_UINT256 = new BigNumber(2).pow(256).minus(1);
+
+const emptyOrder: Order = {
+ senderAddress: constants.NULL_ADDRESS,
+ makerAddress: constants.NULL_ADDRESS,
+ takerAddress: constants.NULL_ADDRESS,
+ makerFee: new BigNumber(0),
+ takerFee: new BigNumber(0),
+ makerAssetAmount: new BigNumber(0),
+ takerAssetAmount: new BigNumber(0),
+ makerAssetData: '0x',
+ takerAssetData: '0x',
+ salt: new BigNumber(0),
+ exchangeAddress: constants.NULL_ADDRESS,
+ feeRecipientAddress: constants.NULL_ADDRESS,
+ expirationTimeSeconds: new BigNumber(0),
+};
+
+const emptySignedOrder: SignedOrder = {
+ ...emptyOrder,
+ signature: '',
+};
+
+const overflowErrorForCall = new Error(RevertReason.Uint256Overflow);
+
+describe('Exchange core internal functions', () => {
+ let testExchange: TestExchangeInternalsContract;
+ let overflowErrorForSendTransaction: Error | undefined;
+ let divisionByZeroErrorForCall: Error | undefined;
+ let roundingErrorForCall: Error | undefined;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ testExchange = await TestExchangeInternalsContract.deployFrom0xArtifactAsync(
+ artifacts.TestExchangeInternals,
+ provider,
+ txDefaults,
+ );
+ overflowErrorForSendTransaction = new Error(
+ await getRevertReasonOrErrorMessageForSendTransactionAsync(RevertReason.Uint256Overflow),
+ );
+ divisionByZeroErrorForCall = new Error(RevertReason.DivisionByZero);
+ roundingErrorForCall = new Error(RevertReason.RoundingError);
+ });
+ // Note(albrow): Don't forget to add beforeEach and afterEach calls to reset
+ // the blockchain state for any tests which modify it!
+
+ async function referenceIsRoundingErrorFloorAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<boolean> {
+ if (denominator.eq(0)) {
+ throw divisionByZeroErrorForCall;
+ }
+ if (numerator.eq(0)) {
+ return false;
+ }
+ if (target.eq(0)) {
+ return false;
+ }
+ const product = numerator.mul(target);
+ const remainder = product.mod(denominator);
+ const remainderTimes1000 = remainder.mul('1000');
+ const isError = remainderTimes1000.gte(product);
+ if (product.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ if (remainderTimes1000.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ return isError;
+ }
+
+ async function referenceIsRoundingErrorCeilAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<boolean> {
+ if (denominator.eq(0)) {
+ throw divisionByZeroErrorForCall;
+ }
+ if (numerator.eq(0)) {
+ return false;
+ }
+ if (target.eq(0)) {
+ return false;
+ }
+ const product = numerator.mul(target);
+ const remainder = product.mod(denominator);
+ const error = denominator.sub(remainder).mod(denominator);
+ const errorTimes1000 = error.mul('1000');
+ const isError = errorTimes1000.gte(product);
+ if (product.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ if (errorTimes1000.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ return isError;
+ }
+
+ async function referenceSafeGetPartialAmountFloorAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<BigNumber> {
+ if (denominator.eq(0)) {
+ throw divisionByZeroErrorForCall;
+ }
+ const isRoundingError = await referenceIsRoundingErrorFloorAsync(numerator, denominator, target);
+ if (isRoundingError) {
+ throw roundingErrorForCall;
+ }
+ const product = numerator.mul(target);
+ if (product.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ return product.dividedToIntegerBy(denominator);
+ }
+
+ describe('addFillResults', async () => {
+ function makeFillResults(value: BigNumber): FillResults {
+ return {
+ makerAssetFilledAmount: value,
+ takerAssetFilledAmount: value,
+ makerFeePaid: value,
+ takerFeePaid: value,
+ };
+ }
+ async function referenceAddFillResultsAsync(
+ totalValue: BigNumber,
+ singleValue: BigNumber,
+ ): Promise<FillResults> {
+ // Note(albrow): Here, each of totalFillResults and
+ // singleFillResults will consist of fields with the same values.
+ // This should be safe because none of the fields in a given
+ // FillResults are ever used together in a mathemetical operation.
+ // They are only used with the corresponding field from *the other*
+ // FillResults, which are different.
+ const totalFillResults = makeFillResults(totalValue);
+ const singleFillResults = makeFillResults(singleValue);
+ // HACK(albrow): _.mergeWith mutates the first argument! To
+ // workaround this we use _.cloneDeep.
+ return _.mergeWith(
+ _.cloneDeep(totalFillResults),
+ singleFillResults,
+ (totalVal: BigNumber, singleVal: BigNumber) => {
+ const newTotal = totalVal.add(singleVal);
+ if (newTotal.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ return newTotal;
+ },
+ );
+ }
+ async function testAddFillResultsAsync(totalValue: BigNumber, singleValue: BigNumber): Promise<FillResults> {
+ const totalFillResults = makeFillResults(totalValue);
+ const singleFillResults = makeFillResults(singleValue);
+ return testExchange.publicAddFillResults.callAsync(totalFillResults, singleFillResults);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'addFillResults',
+ referenceAddFillResultsAsync,
+ testAddFillResultsAsync,
+ [uint256Values, uint256Values],
+ );
+ });
+
+ describe('calculateFillResults', async () => {
+ function makeOrder(
+ makerAssetAmount: BigNumber,
+ takerAssetAmount: BigNumber,
+ makerFee: BigNumber,
+ takerFee: BigNumber,
+ ): Order {
+ return {
+ ...emptyOrder,
+ makerAssetAmount,
+ takerAssetAmount,
+ makerFee,
+ takerFee,
+ };
+ }
+ async function referenceCalculateFillResultsAsync(
+ orderTakerAssetAmount: BigNumber,
+ takerAssetFilledAmount: BigNumber,
+ otherAmount: BigNumber,
+ ): Promise<FillResults> {
+ // Note(albrow): Here we are re-using the same value (otherAmount)
+ // for order.makerAssetAmount, order.makerFee, and order.takerFee.
+ // This should be safe because they are never used with each other
+ // in any mathematical operation in either the reference TypeScript
+ // implementation or the Solidity implementation of
+ // calculateFillResults.
+ const makerAssetFilledAmount = await referenceSafeGetPartialAmountFloorAsync(
+ takerAssetFilledAmount,
+ orderTakerAssetAmount,
+ otherAmount,
+ );
+ const order = makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount);
+ const orderMakerAssetAmount = order.makerAssetAmount;
+ return {
+ makerAssetFilledAmount,
+ takerAssetFilledAmount,
+ makerFeePaid: await referenceSafeGetPartialAmountFloorAsync(
+ makerAssetFilledAmount,
+ orderMakerAssetAmount,
+ otherAmount,
+ ),
+ takerFeePaid: await referenceSafeGetPartialAmountFloorAsync(
+ takerAssetFilledAmount,
+ orderTakerAssetAmount,
+ otherAmount,
+ ),
+ };
+ }
+ async function testCalculateFillResultsAsync(
+ orderTakerAssetAmount: BigNumber,
+ takerAssetFilledAmount: BigNumber,
+ otherAmount: BigNumber,
+ ): Promise<FillResults> {
+ const order = makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount);
+ return testExchange.publicCalculateFillResults.callAsync(order, takerAssetFilledAmount);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'calculateFillResults',
+ referenceCalculateFillResultsAsync,
+ testCalculateFillResultsAsync,
+ [uint256Values, uint256Values, uint256Values],
+ );
+ });
+
+ describe('getPartialAmountFloor', async () => {
+ async function referenceGetPartialAmountFloorAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<BigNumber> {
+ if (denominator.eq(0)) {
+ throw divisionByZeroErrorForCall;
+ }
+ const product = numerator.mul(target);
+ if (product.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ return product.dividedToIntegerBy(denominator);
+ }
+ async function testGetPartialAmountFloorAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<BigNumber> {
+ return testExchange.publicGetPartialAmountFloor.callAsync(numerator, denominator, target);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'getPartialAmountFloor',
+ referenceGetPartialAmountFloorAsync,
+ testGetPartialAmountFloorAsync,
+ [uint256Values, uint256Values, uint256Values],
+ );
+ });
+
+ describe('getPartialAmountCeil', async () => {
+ async function referenceGetPartialAmountCeilAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<BigNumber> {
+ if (denominator.eq(0)) {
+ throw divisionByZeroErrorForCall;
+ }
+ const product = numerator.mul(target);
+ const offset = product.add(denominator.sub(1));
+ if (offset.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ const result = offset.dividedToIntegerBy(denominator);
+ if (product.mod(denominator).eq(0)) {
+ expect(result.mul(denominator)).to.be.bignumber.eq(product);
+ } else {
+ expect(result.mul(denominator)).to.be.bignumber.gt(product);
+ }
+ return result;
+ }
+ async function testGetPartialAmountCeilAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<BigNumber> {
+ return testExchange.publicGetPartialAmountCeil.callAsync(numerator, denominator, target);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'getPartialAmountCeil',
+ referenceGetPartialAmountCeilAsync,
+ testGetPartialAmountCeilAsync,
+ [uint256Values, uint256Values, uint256Values],
+ );
+ });
+
+ describe('safeGetPartialAmountFloor', async () => {
+ async function testSafeGetPartialAmountFloorAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<BigNumber> {
+ return testExchange.publicSafeGetPartialAmountFloor.callAsync(numerator, denominator, target);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'safeGetPartialAmountFloor',
+ referenceSafeGetPartialAmountFloorAsync,
+ testSafeGetPartialAmountFloorAsync,
+ [uint256Values, uint256Values, uint256Values],
+ );
+ });
+
+ describe('safeGetPartialAmountCeil', async () => {
+ async function referenceSafeGetPartialAmountCeilAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<BigNumber> {
+ if (denominator.eq(0)) {
+ throw divisionByZeroErrorForCall;
+ }
+ const isRoundingError = await referenceIsRoundingErrorCeilAsync(numerator, denominator, target);
+ if (isRoundingError) {
+ throw roundingErrorForCall;
+ }
+ const product = numerator.mul(target);
+ const offset = product.add(denominator.sub(1));
+ if (offset.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ const result = offset.dividedToIntegerBy(denominator);
+ if (product.mod(denominator).eq(0)) {
+ expect(result.mul(denominator)).to.be.bignumber.eq(product);
+ } else {
+ expect(result.mul(denominator)).to.be.bignumber.gt(product);
+ }
+ return result;
+ }
+ async function testSafeGetPartialAmountCeilAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<BigNumber> {
+ return testExchange.publicSafeGetPartialAmountCeil.callAsync(numerator, denominator, target);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'safeGetPartialAmountCeil',
+ referenceSafeGetPartialAmountCeilAsync,
+ testSafeGetPartialAmountCeilAsync,
+ [uint256Values, uint256Values, uint256Values],
+ );
+ });
+
+ describe('isRoundingErrorFloor', async () => {
+ async function testIsRoundingErrorFloorAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<boolean> {
+ return testExchange.publicIsRoundingErrorFloor.callAsync(numerator, denominator, target);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'isRoundingErrorFloor',
+ referenceIsRoundingErrorFloorAsync,
+ testIsRoundingErrorFloorAsync,
+ [uint256Values, uint256Values, uint256Values],
+ );
+ });
+
+ describe('isRoundingErrorCeil', async () => {
+ async function testIsRoundingErrorCeilAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<boolean> {
+ return testExchange.publicIsRoundingErrorCeil.callAsync(numerator, denominator, target);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'isRoundingErrorCeil',
+ referenceIsRoundingErrorCeilAsync,
+ testIsRoundingErrorCeilAsync,
+ [uint256Values, uint256Values, uint256Values],
+ );
+ });
+
+ describe('updateFilledState', async () => {
+ // Note(albrow): Since updateFilledState modifies the state by calling
+ // sendTransaction, we must reset the state after each test.
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ async function referenceUpdateFilledStateAsync(
+ takerAssetFilledAmount: BigNumber,
+ orderTakerAssetFilledAmount: BigNumber,
+ // tslint:disable-next-line:no-unused-variable
+ orderHash: string,
+ ): Promise<BigNumber> {
+ const totalFilledAmount = takerAssetFilledAmount.add(orderTakerAssetFilledAmount);
+ if (totalFilledAmount.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForSendTransaction;
+ }
+ return totalFilledAmount;
+ }
+ async function testUpdateFilledStateAsync(
+ takerAssetFilledAmount: BigNumber,
+ orderTakerAssetFilledAmount: BigNumber,
+ orderHash: string,
+ ): Promise<BigNumber> {
+ const fillResults = {
+ makerAssetFilledAmount: new BigNumber(0),
+ takerAssetFilledAmount,
+ makerFeePaid: new BigNumber(0),
+ takerFeePaid: new BigNumber(0),
+ };
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await testExchange.publicUpdateFilledState.sendTransactionAsync(
+ emptySignedOrder,
+ constants.NULL_ADDRESS,
+ orderHash,
+ orderTakerAssetFilledAmount,
+ fillResults,
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ return testExchange.filled.callAsync(orderHash);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'updateFilledState',
+ referenceUpdateFilledStateAsync,
+ testUpdateFilledStateAsync,
+ [uint256Values, uint256Values, bytes32Values],
+ );
+ });
+});
diff --git a/contracts/protocol/test/exchange/match_orders.ts b/contracts/protocol/test/exchange/match_orders.ts
new file mode 100644
index 000000000..8e943c237
--- /dev/null
+++ b/contracts/protocol/test/exchange/match_orders.ts
@@ -0,0 +1,1284 @@
+import {
+ chaiSetup,
+ constants,
+ ERC20BalancesByOwner,
+ ERC721TokenIdsByOwner,
+ expectTransactionFailedAsync,
+ OrderFactory,
+ provider,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import {
+ artifacts as tokensArtifacts,
+ DummyERC20TokenContract,
+ DummyERC721TokenContract,
+ ReentrantERC20TokenContract,
+} from '@0x/contracts-tokens';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { assetDataUtils } from '@0x/order-utils';
+import { RevertReason } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+
+import { ERC20ProxyContract } from '../../generated-wrappers/erc20_proxy';
+import { ERC721ProxyContract } from '../../generated-wrappers/erc721_proxy';
+import { ExchangeContract } from '../../generated-wrappers/exchange';
+import { TestExchangeInternalsContract } from '../../generated-wrappers/test_exchange_internals';
+import { artifacts } from '../../src/artifacts';
+import { ERC20Wrapper } from '../utils/erc20_wrapper';
+import { ERC721Wrapper } from '../utils/erc721_wrapper';
+import { ExchangeWrapper } from '../utils/exchange_wrapper';
+import { MatchOrderTester } from '../utils/match_order_tester';
+
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+chaiSetup.configure();
+const expect = chai.expect;
+
+describe('matchOrders', () => {
+ let makerAddressLeft: string;
+ let makerAddressRight: string;
+ let owner: string;
+ let takerAddress: string;
+ let feeRecipientAddressLeft: string;
+ let feeRecipientAddressRight: string;
+
+ let erc20TokenA: DummyERC20TokenContract;
+ let erc20TokenB: DummyERC20TokenContract;
+ let zrxToken: DummyERC20TokenContract;
+ let erc721Token: DummyERC721TokenContract;
+ let reentrantErc20Token: ReentrantERC20TokenContract;
+ let exchange: ExchangeContract;
+ let erc20Proxy: ERC20ProxyContract;
+ let erc721Proxy: ERC721ProxyContract;
+
+ let erc20BalancesByOwner: ERC20BalancesByOwner;
+ let erc721TokenIdsByOwner: ERC721TokenIdsByOwner;
+ let exchangeWrapper: ExchangeWrapper;
+ let erc20Wrapper: ERC20Wrapper;
+ let erc721Wrapper: ERC721Wrapper;
+ let orderFactoryLeft: OrderFactory;
+ let orderFactoryRight: OrderFactory;
+
+ let erc721LeftMakerAssetIds: BigNumber[];
+ let erc721RightMakerAssetIds: BigNumber[];
+
+ let defaultERC20MakerAssetAddress: string;
+ let defaultERC20TakerAssetAddress: string;
+ let defaultERC721AssetAddress: string;
+
+ let matchOrderTester: MatchOrderTester;
+
+ let testExchange: TestExchangeInternalsContract;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ // Create accounts
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ // Hack(albrow): Both Prettier and TSLint insert a trailing comma below
+ // but that is invalid syntax as of TypeScript version >= 2.8. We don't
+ // have the right fine-grained configuration options in TSLint,
+ // Prettier, or TypeScript, to reconcile this, so we will just have to
+ // wait for them to sort it out. We disable TSLint and Prettier for
+ // this part of the code for now. This occurs several times in this
+ // file. See https://github.com/prettier/prettier/issues/4624.
+ // prettier-ignore
+ const usedAddresses = ([
+ owner,
+ makerAddressLeft,
+ makerAddressRight,
+ takerAddress,
+ feeRecipientAddressLeft,
+ // tslint:disable-next-line:trailing-comma
+ feeRecipientAddressRight
+ ] = _.slice(accounts, 0, 6));
+ // Create wrappers
+ erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
+ erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner);
+ // Deploy ERC20 token & ERC20 proxy
+ const numDummyErc20ToDeploy = 3;
+ [erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync(
+ numDummyErc20ToDeploy,
+ constants.DUMMY_TOKEN_DECIMALS,
+ );
+ erc20Proxy = await erc20Wrapper.deployProxyAsync();
+ await erc20Wrapper.setBalancesAndAllowancesAsync();
+ // Deploy ERC721 token and proxy
+ [erc721Token] = await erc721Wrapper.deployDummyTokensAsync();
+ erc721Proxy = await erc721Wrapper.deployProxyAsync();
+ await erc721Wrapper.setBalancesAndAllowancesAsync();
+ const erc721Balances = await erc721Wrapper.getBalancesAsync();
+ erc721LeftMakerAssetIds = erc721Balances[makerAddressLeft][erc721Token.address];
+ erc721RightMakerAssetIds = erc721Balances[makerAddressRight][erc721Token.address];
+ // Depoy exchange
+ exchange = await ExchangeContract.deployFrom0xArtifactAsync(
+ artifacts.Exchange,
+ provider,
+ txDefaults,
+ assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ );
+ exchangeWrapper = new ExchangeWrapper(exchange, provider);
+ await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner);
+ await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner);
+ // Authorize ERC20 and ERC721 trades by exchange
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ reentrantErc20Token = await ReentrantERC20TokenContract.deployFrom0xArtifactAsync(
+ tokensArtifacts.ReentrantERC20Token,
+ provider,
+ txDefaults,
+ exchange.address,
+ );
+
+ // Set default addresses
+ defaultERC20MakerAssetAddress = erc20TokenA.address;
+ defaultERC20TakerAssetAddress = erc20TokenB.address;
+ defaultERC721AssetAddress = erc721Token.address;
+ // Create default order parameters
+ const defaultOrderParamsLeft = {
+ ...constants.STATIC_ORDER_PARAMS,
+ makerAddress: makerAddressLeft,
+ exchangeAddress: exchange.address,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
+ feeRecipientAddress: feeRecipientAddressLeft,
+ };
+ const defaultOrderParamsRight = {
+ ...constants.STATIC_ORDER_PARAMS,
+ makerAddress: makerAddressRight,
+ exchangeAddress: exchange.address,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
+ feeRecipientAddress: feeRecipientAddressRight,
+ };
+ const privateKeyLeft = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressLeft)];
+ orderFactoryLeft = new OrderFactory(privateKeyLeft, defaultOrderParamsLeft);
+ const privateKeyRight = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressRight)];
+ orderFactoryRight = new OrderFactory(privateKeyRight, defaultOrderParamsRight);
+ // Set match order tester
+ matchOrderTester = new MatchOrderTester(exchangeWrapper, erc20Wrapper, erc721Wrapper, zrxToken.address);
+ testExchange = await TestExchangeInternalsContract.deployFrom0xArtifactAsync(
+ artifacts.TestExchangeInternals,
+ provider,
+ txDefaults,
+ );
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('matchOrders', () => {
+ beforeEach(async () => {
+ erc20BalancesByOwner = await erc20Wrapper.getBalancesAsync();
+ erc721TokenIdsByOwner = await erc721Wrapper.getBalancesAsync();
+ });
+
+ it('Should transfer correct amounts when right order is fully filled and values pass isRoundingErrorFloor but fail isRoundingErrorCeil', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAddress: makerAddressLeft,
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(17), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(98), 0),
+ feeRecipientAddress: feeRecipientAddressLeft,
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAddress: makerAddressRight,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(75), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0),
+ feeRecipientAddress: feeRecipientAddressRight,
+ });
+ // Assert is rounding error ceil & not rounding error floor
+ // These assertions are taken from MixinMatchOrders::calculateMatchedFillResults
+ // The rounding error is derived computating how much the left maker will sell.
+ const numerator = signedOrderLeft.makerAssetAmount;
+ const denominator = signedOrderLeft.takerAssetAmount;
+ const target = signedOrderRight.makerAssetAmount;
+ const isRoundingErrorCeil = await testExchange.publicIsRoundingErrorCeil.callAsync(
+ numerator,
+ denominator,
+ target,
+ );
+ expect(isRoundingErrorCeil).to.be.true();
+ const isRoundingErrorFloor = await testExchange.publicIsRoundingErrorFloor.callAsync(
+ numerator,
+ denominator,
+ target,
+ );
+ expect(isRoundingErrorFloor).to.be.false();
+ // Match signedOrderLeft with signedOrderRight
+ // Note that the left maker received a slightly better sell price.
+ // This is intentional; see note in MixinMatchOrders.calculateMatchedFillResults.
+ // Because the left maker received a slightly more favorable sell price, the fee
+ // paid by the left taker is slightly higher than that paid by the left maker.
+ // Fees can be thought of as a tax paid by the seller, derived from the sale price.
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(75), 0),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber('76.4705882352941176'), 16), // 76.47%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(75), 0),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), 0),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber('76.5306122448979591'), 16), // 76.53%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('Should transfer correct amounts when left order is fully filled and values pass isRoundingErrorCeil but fail isRoundingErrorFloor', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAddress: makerAddressLeft,
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(15), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(90), 0),
+ feeRecipientAddress: feeRecipientAddressLeft,
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAddress: makerAddressRight,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(97), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(14), 0),
+ feeRecipientAddress: feeRecipientAddressRight,
+ });
+ // Assert is rounding error floor & not rounding error ceil
+ // These assertions are taken from MixinMatchOrders::calculateMatchedFillResults
+ // The rounding error is derived computating how much the right maker will buy.
+ const numerator = signedOrderRight.takerAssetAmount;
+ const denominator = signedOrderRight.makerAssetAmount;
+ const target = signedOrderLeft.takerAssetAmount;
+ const isRoundingErrorFloor = await testExchange.publicIsRoundingErrorFloor.callAsync(
+ numerator,
+ denominator,
+ target,
+ );
+ expect(isRoundingErrorFloor).to.be.true();
+ const isRoundingErrorCeil = await testExchange.publicIsRoundingErrorCeil.callAsync(
+ numerator,
+ denominator,
+ target,
+ );
+ expect(isRoundingErrorCeil).to.be.false();
+ // Match signedOrderLeft with signedOrderRight
+ // Note that the right maker received a slightly better purchase price.
+ // This is intentional; see note in MixinMatchOrders.calculateMatchedFillResults.
+ // Because the right maker received a slightly more favorable buy price, the fee
+ // paid by the right taker is slightly higher than that paid by the right maker.
+ // Fees can be thought of as a tax paid by the seller, derived from the sale price.
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(15), 0),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(90), 0),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(90), 0),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber('92.7835051546391752'), 16), // 92.78%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 0),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber('92.8571428571428571'), 16), // 92.85%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('Should give right maker a better buy price when rounding', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAddress: makerAddressLeft,
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(16), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(22), 0),
+ feeRecipientAddress: feeRecipientAddressLeft,
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAddress: makerAddressRight,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(83), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(49), 0),
+ feeRecipientAddress: feeRecipientAddressRight,
+ });
+ // Note:
+ // The correct price buy price for the right maker would yield (49/83) * 22 = 12.988 units
+ // of the left maker asset. This gets rounded up to 13, giving the right maker a better price.
+ // Note:
+ // The maker/taker fee percentage paid on the right order differs because
+ // they received different sale prices. The right maker pays a
+ // fee slightly lower than the right taker.
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(16), 0),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(22), 0),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(22), 0),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber('26.5060240963855421'), 16), // 26.506%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 0),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber('26.5306122448979591'), 16), // 26.531%
+ };
+ // Match signedOrderLeft with signedOrderRight
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('Should give left maker a better sell price when rounding', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAddress: makerAddressLeft,
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(12), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(97), 0),
+ feeRecipientAddress: feeRecipientAddressLeft,
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAddress: makerAddressRight,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(89), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 0),
+ feeRecipientAddress: feeRecipientAddressRight,
+ });
+ // Note:
+ // The maker/taker fee percentage paid on the left order differs because
+ // they received different sale prices. The left maker pays a fee
+ // slightly lower than the left taker.
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(11), 0),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(89), 0),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber('91.6666666666666666'), 16), // 91.6%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(89), 0),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 0),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 0),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber('91.7525773195876288'), 16), // 91.75%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ // Match signedOrderLeft with signedOrderRight
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('Should give right maker and right taker a favorable fee price when rounding', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAddress: makerAddressLeft,
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(16), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(22), 0),
+ feeRecipientAddress: feeRecipientAddressLeft,
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAddress: makerAddressRight,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(83), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(49), 0),
+ feeRecipientAddress: feeRecipientAddressRight,
+ makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 0),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 0),
+ });
+ // Note:
+ // The maker/taker fee percentage paid on the right order differs because
+ // they received different sale prices. The right maker pays a
+ // fee slightly lower than the right taker.
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(16), 0),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(22), 0),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(22), 0),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2650), 0), // 2650.6 rounded down tro 2650
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 0),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(2653), 0), // 2653.1 rounded down to 2653
+ };
+ // Match signedOrderLeft with signedOrderRight
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('Should give left maker and left taker a favorable fee price when rounding', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAddress: makerAddressLeft,
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(12), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(97), 0),
+ feeRecipientAddress: feeRecipientAddressLeft,
+ makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 0),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 0),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAddress: makerAddressRight,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(89), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 0),
+ feeRecipientAddress: feeRecipientAddressRight,
+ });
+ // Note:
+ // The maker/taker fee percentage paid on the left order differs because
+ // they received different sale prices. The left maker pays a
+ // fee slightly lower than the left taker.
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(11), 0),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(89), 0),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(9166), 0), // 9166.6 rounded down to 9166
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(89), 0),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 0),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 0),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(9175), 0), // 9175.2 rounded down to 9175
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ // Match signedOrderLeft with signedOrderRight
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('Should transfer correct amounts when right order fill amount deviates from amount derived by `Exchange.fillOrder`', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAddress: makerAddressLeft,
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1000), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1005), 0),
+ feeRecipientAddress: feeRecipientAddressLeft,
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAddress: makerAddressRight,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2126), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1063), 0),
+ feeRecipientAddress: feeRecipientAddressRight,
+ });
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(1000), 0),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(1005), 0),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ // Notes:
+ // i.
+ // The left order is fully filled by the right order, so the right maker must sell 1005 units of their asset to the left maker.
+ // By selling 1005 units, the right maker should theoretically receive 502.5 units of the left maker's asset.
+ // Since the transfer amount must be an integer, this value must be rounded down to 502 or up to 503.
+ // ii.
+ // If the right order were filled via `Exchange.fillOrder` the respective fill amounts would be [1004, 502] or [1006, 503].
+ // It follows that we cannot trigger a sale of 1005 units of the right maker's asset through `Exchange.fillOrder`.
+ // iii.
+ // For an optimal match, the algorithm must choose either [1005, 502] or [1005, 503] as fill amounts for the right order.
+ // The algorithm favors the right maker when the exchange rate must be rounded, so the final fill for the right order is [1005, 503].
+ // iv.
+ // The right maker fee differs from the right taker fee because their exchange rate differs.
+ // The right maker always receives the better exchange and fee price.
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(1005), 0),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(503), 0),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber('47.2718720602069614'), 16), // 47.27%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(497), 0),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber('47.3189087488240827'), 16), // 47.31%
+ };
+ // Match signedOrderLeft with signedOrderRight
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow matchOrders to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAddress: makerAddressRight,
+ takerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feeRecipientAddress: feeRecipientAddressRight,
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await expectTransactionFailedAsync(
+ exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress),
+ RevertReason.TransferFailed,
+ );
+ });
+ });
+ };
+ describe('matchOrders reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should transfer the correct amounts when orders completely fill each other', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Match signedOrderLeft with signedOrderRight
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('should transfer the correct amounts when orders completely fill each other and taker doesnt take a profit', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ });
+ // Match signedOrderLeft with signedOrderRight
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ // Match signedOrderLeft with signedOrderRight
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('should transfer the correct amounts when left order is completely filled and right order is partially filled', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(20), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(4), 18),
+ });
+ // Match signedOrderLeft with signedOrderRight
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 16), // 50%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 16), // 50%
+ };
+ // Match signedOrderLeft with signedOrderRight
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('should transfer the correct amounts when right order is completely filled and left order is partially filled', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Match signedOrderLeft with signedOrderRight
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 16), // 10%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 16), // 10%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ // Match signedOrderLeft with signedOrderRight
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('should transfer the correct amounts when consecutive calls are used to completely fill the left order', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Match orders
+ let newERC20BalancesByOwner: ERC20BalancesByOwner;
+ let newERC721TokenIdsByOwner: ERC721TokenIdsByOwner;
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 16), // 10%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 16), // 10%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ // prettier-ignore
+ [
+ newERC20BalancesByOwner,
+ // tslint:disable-next-line:trailing-comma
+ newERC721TokenIdsByOwner
+ ] = await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ // Construct second right order
+ // Note: This order needs makerAssetAmount=90/takerAssetAmount=[anything <= 45] to fully fill the right order.
+ // However, we use 100/50 to ensure a partial fill as we want to go down the "left fill"
+ // branch in the contract twice for this test.
+ const signedOrderRight2 = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 18),
+ });
+ // Match signedOrderLeft with signedOrderRight2
+ const leftTakerAssetFilledAmount = signedOrderRight.makerAssetAmount;
+ const rightTakerAssetFilledAmount = new BigNumber(0);
+ const expectedTransferAmounts2 = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(45), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(90), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(90), 16), // 90% (10% paid earlier)
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(90), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(45), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(90), 16), // 90%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(90), 16), // 90% (10% paid earlier)
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(90), 16), // 90%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight2,
+ takerAddress,
+ newERC20BalancesByOwner,
+ newERC721TokenIdsByOwner,
+ expectedTransferAmounts2,
+ leftTakerAssetFilledAmount,
+ rightTakerAssetFilledAmount,
+ );
+ });
+
+ it('should transfer the correct amounts when consecutive calls are used to completely fill the right order', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ });
+ // Match orders
+ let newERC20BalancesByOwner: ERC20BalancesByOwner;
+ let newERC721TokenIdsByOwner: ERC721TokenIdsByOwner;
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(4), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(4), 16), // 4%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(6), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(4), 16), // 4%
+ };
+ // prettier-ignore
+ [
+ newERC20BalancesByOwner,
+ // tslint:disable-next-line:trailing-comma
+ newERC721TokenIdsByOwner
+ ] = await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+
+ // Create second left order
+ // Note: This order needs makerAssetAmount=96/takerAssetAmount=48 to fully fill the right order.
+ // However, we use 100/50 to ensure a partial fill as we want to go down the "right fill"
+ // branch in the contract twice for this test.
+ const signedOrderLeft2 = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 18),
+ });
+ // Match signedOrderLeft2 with signedOrderRight
+ const leftTakerAssetFilledAmount = new BigNumber(0);
+ const takerAmountReceived = newERC20BalancesByOwner[takerAddress][defaultERC20MakerAssetAddress].minus(
+ erc20BalancesByOwner[takerAddress][defaultERC20MakerAssetAddress],
+ );
+ const rightTakerAssetFilledAmount = signedOrderLeft.makerAssetAmount.minus(takerAmountReceived);
+ const expectedTransferAmounts2 = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(96), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(48), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(96), 16), // 96%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(48), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(96), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(96), 16), // 96%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(96), 16), // 96%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(96), 16), // 96%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft2,
+ signedOrderRight,
+ takerAddress,
+ newERC20BalancesByOwner,
+ newERC721TokenIdsByOwner,
+ expectedTransferAmounts2,
+ leftTakerAssetFilledAmount,
+ rightTakerAssetFilledAmount,
+ );
+ });
+
+ it('should transfer the correct amounts if fee recipient is the same across both matched orders', async () => {
+ const feeRecipientAddress = feeRecipientAddressLeft;
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feeRecipientAddress,
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feeRecipientAddress,
+ });
+ // Match orders
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('should transfer the correct amounts if taker is also the left order maker', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Match orders
+ takerAddress = signedOrderLeft.makerAddress;
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('should transfer the correct amounts if taker is also the right order maker', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Match orders
+ takerAddress = signedOrderRight.makerAddress;
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('should transfer the correct amounts if taker is also the left fee recipient', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Match orders
+ takerAddress = feeRecipientAddressLeft;
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('should transfer the correct amounts if taker is also the right fee recipient', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Match orders
+ takerAddress = feeRecipientAddressRight;
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('should transfer the correct amounts if left maker is the left fee recipient and right maker is the right fee recipient', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Match orders
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('Should throw if left order is not fillable', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Cancel left order
+ await exchangeWrapper.cancelOrderAsync(signedOrderLeft, signedOrderLeft.makerAddress);
+ // Match orders
+ return expectTransactionFailedAsync(
+ exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress),
+ RevertReason.OrderUnfillable,
+ );
+ });
+
+ it('Should throw if right order is not fillable', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Cancel right order
+ await exchangeWrapper.cancelOrderAsync(signedOrderRight, signedOrderRight.makerAddress);
+ // Match orders
+ return expectTransactionFailedAsync(
+ exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress),
+ RevertReason.OrderUnfillable,
+ );
+ });
+
+ it('should throw if there is not a positive spread', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18),
+ });
+ // Match orders
+ return expectTransactionFailedAsync(
+ exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress),
+ RevertReason.NegativeSpreadRequired,
+ );
+ });
+
+ it('should throw if the left maker asset is not equal to the right taker asset ', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Match orders
+ return expectTransactionFailedAsync(
+ exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress),
+ // We are assuming assetData fields of the right order are the
+ // reverse of the left order, rather than checking equality. This
+ // saves a bunch of gas, but as a result if the assetData fields are
+ // off then the failure ends up happening at signature validation
+ RevertReason.InvalidOrderSignature,
+ );
+ });
+
+ it('should throw if the right maker asset is not equal to the left taker asset', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Match orders
+ return expectTransactionFailedAsync(
+ exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress),
+ RevertReason.InvalidOrderSignature,
+ );
+ });
+
+ it('should transfer correct amounts when left order maker asset is an ERC721 token', async () => {
+ // Create orders to match
+ const erc721TokenToTransfer = erc721LeftMakerAssetIds[0];
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC721AssetData(defaultERC721AssetAddress, erc721TokenToTransfer),
+ makerAssetAmount: new BigNumber(1),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ takerAssetData: assetDataUtils.encodeERC721AssetData(defaultERC721AssetAddress, erc721TokenToTransfer),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: new BigNumber(1),
+ });
+ // Match orders
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 0),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 0),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 50%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('should transfer correct amounts when right order maker asset is an ERC721 token', async () => {
+ // Create orders to match
+ const erc721TokenToTransfer = erc721RightMakerAssetIds[0];
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ takerAssetData: assetDataUtils.encodeERC721AssetData(defaultERC721AssetAddress, erc721TokenToTransfer),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: new BigNumber(1),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC721AssetData(defaultERC721AssetAddress, erc721TokenToTransfer),
+ makerAssetAmount: new BigNumber(1),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(8), 18),
+ });
+ // Match orders
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 0),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 0),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(8), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+ });
+}); // tslint:disable-line:max-file-line-count
diff --git a/contracts/protocol/test/exchange/signature_validator.ts b/contracts/protocol/test/exchange/signature_validator.ts
new file mode 100644
index 000000000..62579cdc7
--- /dev/null
+++ b/contracts/protocol/test/exchange/signature_validator.ts
@@ -0,0 +1,526 @@
+import { artifacts as examplesArtifacts, ValidatorContract, WalletContract } from '@0x/contracts-examples';
+import {
+ addressUtils,
+ chaiSetup,
+ constants,
+ expectContractCallFailedAsync,
+ LogDecoder,
+ OrderFactory,
+ provider,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import { artifacts as tokensArtifacts } from '@0x/contracts-tokens';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { assetDataUtils, orderHashUtils, signatureUtils } from '@0x/order-utils';
+import { RevertReason, SignatureType, SignedOrder } from '@0x/types';
+import * as chai from 'chai';
+import { LogWithDecodedArgs } from 'ethereum-types';
+import ethUtil = require('ethereumjs-util');
+
+import {
+ artifacts,
+ TestSignatureValidatorContract,
+ TestSignatureValidatorSignatureValidatorApprovalEventArgs,
+ TestStaticCallReceiverContract,
+} from '../../src';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+// tslint:disable:no-unnecessary-type-assertion
+describe('MixinSignatureValidator', () => {
+ let signedOrder: SignedOrder;
+ let orderFactory: OrderFactory;
+ let signatureValidator: TestSignatureValidatorContract;
+ let testWallet: WalletContract;
+ let testValidator: ValidatorContract;
+ let maliciousWallet: TestStaticCallReceiverContract;
+ let maliciousValidator: TestStaticCallReceiverContract;
+ let signerAddress: string;
+ let signerPrivateKey: Buffer;
+ let notSignerAddress: string;
+ let notSignerPrivateKey: Buffer;
+ let signatureValidatorLogDecoder: LogDecoder;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ const makerAddress = accounts[0];
+ signerAddress = makerAddress;
+ notSignerAddress = accounts[1];
+ signatureValidator = await TestSignatureValidatorContract.deployFrom0xArtifactAsync(
+ artifacts.TestSignatureValidator,
+ provider,
+ txDefaults,
+ );
+ testWallet = await WalletContract.deployFrom0xArtifactAsync(
+ examplesArtifacts.Wallet,
+ provider,
+ txDefaults,
+ signerAddress,
+ );
+ testValidator = await ValidatorContract.deployFrom0xArtifactAsync(
+ examplesArtifacts.Validator,
+ provider,
+ txDefaults,
+ signerAddress,
+ );
+ maliciousWallet = maliciousValidator = await TestStaticCallReceiverContract.deployFrom0xArtifactAsync(
+ artifacts.TestStaticCallReceiver,
+ provider,
+ txDefaults,
+ );
+ signatureValidatorLogDecoder = new LogDecoder(web3Wrapper, { ...artifacts, ...tokensArtifacts });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await signatureValidator.setSignatureValidatorApproval.sendTransactionAsync(testValidator.address, true, {
+ from: signerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await signatureValidator.setSignatureValidatorApproval.sendTransactionAsync(
+ maliciousValidator.address,
+ true,
+ {
+ from: signerAddress,
+ },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ const defaultOrderParams = {
+ ...constants.STATIC_ORDER_PARAMS,
+ exchangeAddress: signatureValidator.address,
+ makerAddress,
+ feeRecipientAddress: addressUtils.generatePseudoRandomAddress(),
+ makerAssetData: assetDataUtils.encodeERC20AssetData(addressUtils.generatePseudoRandomAddress()),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(addressUtils.generatePseudoRandomAddress()),
+ };
+ signerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)];
+ notSignerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(notSignerAddress)];
+ orderFactory = new OrderFactory(signerPrivateKey, defaultOrderParams);
+ });
+
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+
+ describe('isValidSignature', () => {
+ beforeEach(async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync();
+ });
+
+ it('should revert when signature is empty', async () => {
+ const emptySignature = '0x';
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ return expectContractCallFailedAsync(
+ signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signedOrder.makerAddress,
+ emptySignature,
+ ),
+ RevertReason.LengthGreaterThan0Required,
+ );
+ });
+
+ it('should revert when signature type is unsupported', async () => {
+ const unsupportedSignatureType = SignatureType.NSignatureTypes;
+ const unsupportedSignatureHex = '0x' + Buffer.from([unsupportedSignatureType]).toString('hex');
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ return expectContractCallFailedAsync(
+ signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signedOrder.makerAddress,
+ unsupportedSignatureHex,
+ ),
+ RevertReason.SignatureUnsupported,
+ );
+ });
+
+ it('should revert when SignatureType=Illegal', async () => {
+ const unsupportedSignatureHex = '0x' + Buffer.from([SignatureType.Illegal]).toString('hex');
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ return expectContractCallFailedAsync(
+ signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signedOrder.makerAddress,
+ unsupportedSignatureHex,
+ ),
+ RevertReason.SignatureIllegal,
+ );
+ });
+
+ it('should return false when SignatureType=Invalid and signature has a length of zero', async () => {
+ const signatureHex = '0x' + Buffer.from([SignatureType.Invalid]).toString('hex');
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signedOrder.makerAddress,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.false();
+ });
+
+ it('should revert when SignatureType=Invalid and signature length is non-zero', async () => {
+ const fillerData = ethUtil.toBuffer('0xdeadbeef');
+ const signatureType = ethUtil.toBuffer(`0x${SignatureType.Invalid}`);
+ const signatureBuffer = Buffer.concat([fillerData, signatureType]);
+ const signatureHex = ethUtil.bufferToHex(signatureBuffer);
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ return expectContractCallFailedAsync(
+ signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signedOrder.makerAddress,
+ signatureHex,
+ ),
+ RevertReason.Length0Required,
+ );
+ });
+
+ it('should return true when SignatureType=EIP712 and signature is valid', async () => {
+ // Create EIP712 signature
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const orderHashBuffer = ethUtil.toBuffer(orderHashHex);
+ const ecSignature = ethUtil.ecsign(orderHashBuffer, signerPrivateKey);
+ // Create 0x signature from EIP712 signature
+ const signature = Buffer.concat([
+ ethUtil.toBuffer(ecSignature.v),
+ ecSignature.r,
+ ecSignature.s,
+ ethUtil.toBuffer(`0x${SignatureType.EIP712}`),
+ ]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ // Validate signature
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signerAddress,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.true();
+ });
+
+ it('should return false when SignatureType=EIP712 and signature is invalid', async () => {
+ // Create EIP712 signature
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const orderHashBuffer = ethUtil.toBuffer(orderHashHex);
+ const ecSignature = ethUtil.ecsign(orderHashBuffer, signerPrivateKey);
+ // Create 0x signature from EIP712 signature
+ const signature = Buffer.concat([
+ ethUtil.toBuffer(ecSignature.v),
+ ecSignature.r,
+ ecSignature.s,
+ ethUtil.toBuffer(`0x${SignatureType.EIP712}`),
+ ]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ // Validate signature.
+ // This will fail because `signerAddress` signed the message, but we're passing in `notSignerAddress`
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ notSignerAddress,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.false();
+ });
+
+ it('should return true when SignatureType=EthSign and signature is valid', async () => {
+ // Create EthSign signature
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const orderHashWithEthSignPrefixHex = signatureUtils.addSignedMessagePrefix(orderHashHex);
+ const orderHashWithEthSignPrefixBuffer = ethUtil.toBuffer(orderHashWithEthSignPrefixHex);
+ const ecSignature = ethUtil.ecsign(orderHashWithEthSignPrefixBuffer, signerPrivateKey);
+ // Create 0x signature from EthSign signature
+ const signature = Buffer.concat([
+ ethUtil.toBuffer(ecSignature.v),
+ ecSignature.r,
+ ecSignature.s,
+ ethUtil.toBuffer(`0x${SignatureType.EthSign}`),
+ ]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ // Validate signature
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signerAddress,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.true();
+ });
+
+ it('should return false when SignatureType=EthSign and signature is invalid', async () => {
+ // Create EthSign signature
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const orderHashWithEthSignPrefixHex = signatureUtils.addSignedMessagePrefix(orderHashHex);
+ const orderHashWithEthSignPrefixBuffer = ethUtil.toBuffer(orderHashWithEthSignPrefixHex);
+ const ecSignature = ethUtil.ecsign(orderHashWithEthSignPrefixBuffer, signerPrivateKey);
+ // Create 0x signature from EthSign signature
+ const signature = Buffer.concat([
+ ethUtil.toBuffer(ecSignature.v),
+ ecSignature.r,
+ ecSignature.s,
+ ethUtil.toBuffer(`0x${SignatureType.EthSign}`),
+ ]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ // Validate signature.
+ // This will fail because `signerAddress` signed the message, but we're passing in `notSignerAddress`
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ notSignerAddress,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.false();
+ });
+
+ it('should return true when SignatureType=Wallet and signature is valid', async () => {
+ // Create EIP712 signature
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const orderHashBuffer = ethUtil.toBuffer(orderHashHex);
+ const ecSignature = ethUtil.ecsign(orderHashBuffer, signerPrivateKey);
+ // Create 0x signature from EIP712 signature
+ const signature = Buffer.concat([
+ ethUtil.toBuffer(ecSignature.v),
+ ecSignature.r,
+ ecSignature.s,
+ ethUtil.toBuffer(`0x${SignatureType.Wallet}`),
+ ]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ // Validate signature
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ testWallet.address,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.true();
+ });
+
+ it('should return false when SignatureType=Wallet and signature is invalid', async () => {
+ // Create EIP712 signature using a private key that does not belong to the wallet owner.
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const orderHashBuffer = ethUtil.toBuffer(orderHashHex);
+ const notWalletOwnerPrivateKey = notSignerPrivateKey;
+ const ecSignature = ethUtil.ecsign(orderHashBuffer, notWalletOwnerPrivateKey);
+ // Create 0x signature from EIP712 signature
+ const signature = Buffer.concat([
+ ethUtil.toBuffer(ecSignature.v),
+ ecSignature.r,
+ ecSignature.s,
+ ethUtil.toBuffer(`0x${SignatureType.Wallet}`),
+ ]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ // Validate signature
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ testWallet.address,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.false();
+ });
+
+ it('should revert when `isValidSignature` attempts to update state and SignatureType=Wallet', async () => {
+ // Create EIP712 signature
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const orderHashBuffer = ethUtil.toBuffer(orderHashHex);
+ const ecSignature = ethUtil.ecsign(orderHashBuffer, signerPrivateKey);
+ // Create 0x signature from EIP712 signature
+ const signature = Buffer.concat([
+ ethUtil.toBuffer(ecSignature.v),
+ ecSignature.r,
+ ecSignature.s,
+ ethUtil.toBuffer(`0x${SignatureType.Wallet}`),
+ ]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ await expectContractCallFailedAsync(
+ signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ maliciousWallet.address,
+ signatureHex,
+ ),
+ RevertReason.WalletError,
+ );
+ });
+
+ it('should return true when SignatureType=Validator, signature is valid and validator is approved', async () => {
+ const validatorAddress = ethUtil.toBuffer(`${testValidator.address}`);
+ const signatureType = ethUtil.toBuffer(`0x${SignatureType.Validator}`);
+ const signature = Buffer.concat([validatorAddress, signatureType]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signerAddress,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.true();
+ });
+
+ it('should return false when SignatureType=Validator, signature is invalid and validator is approved', async () => {
+ const validatorAddress = ethUtil.toBuffer(`${testValidator.address}`);
+ const signatureType = ethUtil.toBuffer(`0x${SignatureType.Validator}`);
+ const signature = Buffer.concat([validatorAddress, signatureType]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ // This will return false because we signed the message with `signerAddress`, but
+ // are validating against `notSignerAddress`
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ notSignerAddress,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.false();
+ });
+
+ it('should revert when `isValidSignature` attempts to update state and SignatureType=Validator', async () => {
+ const validatorAddress = ethUtil.toBuffer(`${maliciousValidator.address}`);
+ const signatureType = ethUtil.toBuffer(`0x${SignatureType.Validator}`);
+ const signature = Buffer.concat([validatorAddress, signatureType]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ await expectContractCallFailedAsync(
+ signatureValidator.publicIsValidSignature.callAsync(orderHashHex, signerAddress, signatureHex),
+ RevertReason.ValidatorError,
+ );
+ });
+ it('should return false when SignatureType=Validator, signature is valid and validator is not approved', async () => {
+ // Set approval of signature validator to false
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await signatureValidator.setSignatureValidatorApproval.sendTransactionAsync(
+ testValidator.address,
+ false,
+ { from: signerAddress },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Validate signature
+ const validatorAddress = ethUtil.toBuffer(`${testValidator.address}`);
+ const signatureType = ethUtil.toBuffer(`0x${SignatureType.Validator}`);
+ const signature = Buffer.concat([validatorAddress, signatureType]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signerAddress,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.false();
+ });
+
+ it('should return true when SignatureType=Presigned and signer has presigned hash', async () => {
+ // Presign hash
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await signatureValidator.preSign.sendTransactionAsync(
+ orderHashHex,
+ signedOrder.makerAddress,
+ signedOrder.signature,
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Validate presigned signature
+ const signature = ethUtil.toBuffer(`0x${SignatureType.PreSigned}`);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signedOrder.makerAddress,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.true();
+ });
+
+ it('should return false when SignatureType=Presigned and signer has not presigned hash', async () => {
+ const signature = ethUtil.toBuffer(`0x${SignatureType.PreSigned}`);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signedOrder.makerAddress,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.false();
+ });
+
+ it('should return true when message was signed by a Trezor One (firmware version 1.6.2)', async () => {
+ // messageHash translates to 0x2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b
+ const messageHash = ethUtil.bufferToHex(ethUtil.toBuffer('++++++++++++++++++++++++++++++++'));
+ const signer = '0xc28b145f10f0bcf0fc000e778615f8fd73490bad';
+ const v = ethUtil.toBuffer('0x1c');
+ const r = ethUtil.toBuffer('0x7b888b596ccf87f0bacab0dcb483124973f7420f169b4824d7a12534ac1e9832');
+ const s = ethUtil.toBuffer('0x0c8e14f7edc01459e13965f1da56e0c23ed11e2cca932571eee1292178f90424');
+ const trezorSignatureType = ethUtil.toBuffer(`0x${SignatureType.EthSign}`);
+ const signature = Buffer.concat([v, r, s, trezorSignatureType]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ messageHash,
+ signer,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.true();
+ });
+
+ it('should return true when message was signed by a Trezor Model T (firmware version 2.0.7)', async () => {
+ // messageHash translates to 0x2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b
+ const messageHash = ethUtil.bufferToHex(ethUtil.toBuffer('++++++++++++++++++++++++++++++++'));
+ const signer = '0x98ce6d9345e8ffa7d99ee0822272fae9d2c0e895';
+ const v = ethUtil.toBuffer('0x1c');
+ const r = ethUtil.toBuffer('0x423b71062c327f0ec4fe199b8da0f34185e59b4c1cb4cc23df86cac4a601fb3f');
+ const s = ethUtil.toBuffer('0x53810d6591b5348b7ee08ee812c874b0fdfb942c9849d59512c90e295221091f');
+ const trezorSignatureType = ethUtil.toBuffer(`0x${SignatureType.EthSign}`);
+ const signature = Buffer.concat([v, r, s, trezorSignatureType]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ messageHash,
+ signer,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.true();
+ });
+ });
+
+ describe('setSignatureValidatorApproval', () => {
+ it('should emit a SignatureValidatorApprovalSet with correct args when a validator is approved', async () => {
+ const approval = true;
+ const res = await signatureValidatorLogDecoder.getTxWithDecodedLogsAsync(
+ await signatureValidator.setSignatureValidatorApproval.sendTransactionAsync(
+ testValidator.address,
+ approval,
+ {
+ from: signerAddress,
+ },
+ ),
+ );
+ expect(res.logs.length).to.equal(1);
+ const log = res.logs[0] as LogWithDecodedArgs<TestSignatureValidatorSignatureValidatorApprovalEventArgs>;
+ const logArgs = log.args;
+ expect(logArgs.signerAddress).to.equal(signerAddress);
+ expect(logArgs.validatorAddress).to.equal(testValidator.address);
+ expect(logArgs.approved).to.equal(approval);
+ });
+ it('should emit a SignatureValidatorApprovalSet with correct args when a validator is disapproved', async () => {
+ const approval = false;
+ const res = await signatureValidatorLogDecoder.getTxWithDecodedLogsAsync(
+ await signatureValidator.setSignatureValidatorApproval.sendTransactionAsync(
+ testValidator.address,
+ approval,
+ {
+ from: signerAddress,
+ },
+ ),
+ );
+ expect(res.logs.length).to.equal(1);
+ const log = res.logs[0] as LogWithDecodedArgs<TestSignatureValidatorSignatureValidatorApprovalEventArgs>;
+ const logArgs = log.args;
+ expect(logArgs.signerAddress).to.equal(signerAddress);
+ expect(logArgs.validatorAddress).to.equal(testValidator.address);
+ expect(logArgs.approved).to.equal(approval);
+ });
+ });
+});
+// tslint:disable:max-file-line-count
+// tslint:enable:no-unnecessary-type-assertion
diff --git a/contracts/protocol/test/exchange/transactions.ts b/contracts/protocol/test/exchange/transactions.ts
new file mode 100644
index 000000000..746f3cb04
--- /dev/null
+++ b/contracts/protocol/test/exchange/transactions.ts
@@ -0,0 +1,466 @@
+import { artifacts as examplesArtifacts, ExchangeWrapperContract, WhitelistContract } from '@0x/contracts-examples';
+import {
+ chaiSetup,
+ constants,
+ ERC20BalancesByOwner,
+ expectTransactionFailedAsync,
+ OrderFactory,
+ orderUtils,
+ provider,
+ SignedTransaction,
+ TransactionFactory,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import { DummyERC20TokenContract } from '@0x/contracts-tokens';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils';
+import { OrderWithoutExchangeAddress, RevertReason, SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+
+import { ERC20ProxyContract } from '../../generated-wrappers/erc20_proxy';
+import { ExchangeContract } from '../../generated-wrappers/exchange';
+import { artifacts } from '../../src/artifacts';
+import { ERC20Wrapper } from '../utils/erc20_wrapper';
+import { ExchangeWrapper } from '../utils/exchange_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+
+describe('Exchange transactions', () => {
+ let senderAddress: string;
+ let owner: string;
+ let makerAddress: string;
+ let takerAddress: string;
+ let feeRecipientAddress: string;
+
+ let erc20TokenA: DummyERC20TokenContract;
+ let erc20TokenB: DummyERC20TokenContract;
+ let zrxToken: DummyERC20TokenContract;
+ let exchange: ExchangeContract;
+ let erc20Proxy: ERC20ProxyContract;
+
+ let erc20Balances: ERC20BalancesByOwner;
+ let signedOrder: SignedOrder;
+ let signedTx: SignedTransaction;
+ let orderWithoutExchangeAddress: OrderWithoutExchangeAddress;
+ let orderFactory: OrderFactory;
+ let makerTransactionFactory: TransactionFactory;
+ let takerTransactionFactory: TransactionFactory;
+ let exchangeWrapper: ExchangeWrapper;
+ let erc20Wrapper: ERC20Wrapper;
+
+ let defaultMakerTokenAddress: string;
+ let defaultTakerTokenAddress: string;
+ let makerPrivateKey: Buffer;
+ let takerPrivateKey: Buffer;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ const usedAddresses = ([owner, senderAddress, makerAddress, takerAddress, feeRecipientAddress] = _.slice(
+ accounts,
+ 0,
+ 5,
+ ));
+
+ erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
+
+ const numDummyErc20ToDeploy = 3;
+ [erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync(
+ numDummyErc20ToDeploy,
+ constants.DUMMY_TOKEN_DECIMALS,
+ );
+ erc20Proxy = await erc20Wrapper.deployProxyAsync();
+ await erc20Wrapper.setBalancesAndAllowancesAsync();
+
+ exchange = await ExchangeContract.deployFrom0xArtifactAsync(
+ artifacts.Exchange,
+ provider,
+ txDefaults,
+ assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ );
+ exchangeWrapper = new ExchangeWrapper(exchange, provider);
+ await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner);
+
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ defaultMakerTokenAddress = erc20TokenA.address;
+ defaultTakerTokenAddress = erc20TokenB.address;
+
+ const defaultOrderParams = {
+ ...constants.STATIC_ORDER_PARAMS,
+ senderAddress,
+ exchangeAddress: exchange.address,
+ makerAddress,
+ feeRecipientAddress,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerTokenAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerTokenAddress),
+ };
+ makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)];
+ takerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(takerAddress)];
+ orderFactory = new OrderFactory(makerPrivateKey, defaultOrderParams);
+ makerTransactionFactory = new TransactionFactory(makerPrivateKey, exchange.address);
+ takerTransactionFactory = new TransactionFactory(takerPrivateKey, exchange.address);
+ });
+ describe('executeTransaction', () => {
+ describe('fillOrder', () => {
+ let takerAssetFillAmount: BigNumber;
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ signedOrder = await orderFactory.newSignedOrderAsync();
+ orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
+
+ takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ const data = exchange.fillOrder.getABIEncodedTransactionData(
+ orderWithoutExchangeAddress,
+ takerAssetFillAmount,
+ signedOrder.signature,
+ );
+ signedTx = takerTransactionFactory.newSignedTransaction(data);
+ });
+
+ it('should throw if not called by specified sender', async () => {
+ return expectTransactionFailedAsync(
+ exchangeWrapper.executeTransactionAsync(signedTx, takerAddress),
+ RevertReason.FailedExecution,
+ );
+ });
+
+ it('should transfer the correct amounts when signed by taker and called by sender', async () => {
+ await exchangeWrapper.executeTransactionAsync(signedTx, senderAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const makerAssetFillAmount = takerAssetFillAmount
+ .times(signedOrder.makerAssetAmount)
+ .dividedToIntegerBy(signedOrder.takerAssetAmount);
+ const makerFeePaid = signedOrder.makerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ const takerFeePaid = signedOrder.takerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ expect(newBalances[makerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerTokenAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultTakerTokenAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerFeePaid),
+ );
+ expect(newBalances[takerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultTakerTokenAddress].minus(takerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerTokenAddress].add(makerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].minus(takerFeePaid),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)),
+ );
+ });
+
+ it('should throw if the a 0x transaction with the same transactionHash has already been executed', async () => {
+ await exchangeWrapper.executeTransactionAsync(signedTx, senderAddress);
+ return expectTransactionFailedAsync(
+ exchangeWrapper.executeTransactionAsync(signedTx, senderAddress),
+ RevertReason.InvalidTxHash,
+ );
+ });
+
+ it('should reset the currentContextAddress', async () => {
+ await exchangeWrapper.executeTransactionAsync(signedTx, senderAddress);
+ const currentContextAddress = await exchange.currentContextAddress.callAsync();
+ expect(currentContextAddress).to.equal(constants.NULL_ADDRESS);
+ });
+ });
+
+ describe('cancelOrder', () => {
+ beforeEach(async () => {
+ const data = exchange.cancelOrder.getABIEncodedTransactionData(orderWithoutExchangeAddress);
+ signedTx = makerTransactionFactory.newSignedTransaction(data);
+ });
+
+ it('should throw if not called by specified sender', async () => {
+ return expectTransactionFailedAsync(
+ exchangeWrapper.executeTransactionAsync(signedTx, makerAddress),
+ RevertReason.FailedExecution,
+ );
+ });
+
+ it('should cancel the order when signed by maker and called by sender', async () => {
+ await exchangeWrapper.executeTransactionAsync(signedTx, senderAddress);
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, senderAddress),
+ RevertReason.OrderUnfillable,
+ );
+ });
+ });
+
+ describe('cancelOrdersUpTo', () => {
+ let exchangeWrapperContract: ExchangeWrapperContract;
+
+ before(async () => {
+ exchangeWrapperContract = await ExchangeWrapperContract.deployFrom0xArtifactAsync(
+ examplesArtifacts.ExchangeWrapper,
+ provider,
+ txDefaults,
+ exchange.address,
+ );
+ });
+
+ it("should cancel an order if called from the order's sender", async () => {
+ const orderSalt = new BigNumber(0);
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ senderAddress: exchangeWrapperContract.address,
+ salt: orderSalt,
+ });
+ const targetOrderEpoch = orderSalt.add(1);
+ const cancelData = exchange.cancelOrdersUpTo.getABIEncodedTransactionData(targetOrderEpoch);
+ const signedCancelTx = makerTransactionFactory.newSignedTransaction(cancelData);
+ await exchangeWrapperContract.cancelOrdersUpTo.sendTransactionAsync(
+ targetOrderEpoch,
+ signedCancelTx.salt,
+ signedCancelTx.signature,
+ {
+ from: makerAddress,
+ },
+ );
+
+ const takerAssetFillAmount = signedOrder.takerAssetAmount;
+ orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
+ const fillData = exchange.fillOrder.getABIEncodedTransactionData(
+ orderWithoutExchangeAddress,
+ takerAssetFillAmount,
+ signedOrder.signature,
+ );
+ const signedFillTx = takerTransactionFactory.newSignedTransaction(fillData);
+ return expectTransactionFailedAsync(
+ exchangeWrapperContract.fillOrder.sendTransactionAsync(
+ orderWithoutExchangeAddress,
+ takerAssetFillAmount,
+ signedFillTx.salt,
+ signedOrder.signature,
+ signedFillTx.signature,
+ { from: takerAddress },
+ ),
+ RevertReason.FailedExecution,
+ );
+ });
+
+ it("should not cancel an order if not called from the order's sender", async () => {
+ const orderSalt = new BigNumber(0);
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ senderAddress: exchangeWrapperContract.address,
+ salt: orderSalt,
+ });
+ const targetOrderEpoch = orderSalt.add(1);
+ await exchangeWrapper.cancelOrdersUpToAsync(targetOrderEpoch, makerAddress);
+
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ const takerAssetFillAmount = signedOrder.takerAssetAmount;
+ orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
+ const data = exchange.fillOrder.getABIEncodedTransactionData(
+ orderWithoutExchangeAddress,
+ takerAssetFillAmount,
+ signedOrder.signature,
+ );
+ signedTx = takerTransactionFactory.newSignedTransaction(data);
+ await exchangeWrapperContract.fillOrder.sendTransactionAsync(
+ orderWithoutExchangeAddress,
+ takerAssetFillAmount,
+ signedTx.salt,
+ signedOrder.signature,
+ signedTx.signature,
+ { from: takerAddress },
+ );
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const makerAssetFillAmount = takerAssetFillAmount
+ .times(signedOrder.makerAssetAmount)
+ .dividedToIntegerBy(signedOrder.takerAssetAmount);
+ const makerFeePaid = signedOrder.makerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ const takerFeePaid = signedOrder.takerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ expect(newBalances[makerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerTokenAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultTakerTokenAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerFeePaid),
+ );
+ expect(newBalances[takerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultTakerTokenAddress].minus(takerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerTokenAddress].add(makerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].minus(takerFeePaid),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)),
+ );
+ });
+ });
+ });
+
+ describe('Whitelist', () => {
+ let whitelist: WhitelistContract;
+ let whitelistOrderFactory: OrderFactory;
+
+ before(async () => {
+ whitelist = await WhitelistContract.deployFrom0xArtifactAsync(
+ examplesArtifacts.Whitelist,
+ provider,
+ txDefaults,
+ exchange.address,
+ );
+ const isApproved = true;
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await exchange.setSignatureValidatorApproval.sendTransactionAsync(whitelist.address, isApproved, {
+ from: takerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const defaultOrderParams = {
+ ...constants.STATIC_ORDER_PARAMS,
+ senderAddress: whitelist.address,
+ exchangeAddress: exchange.address,
+ makerAddress,
+ feeRecipientAddress,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerTokenAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerTokenAddress),
+ };
+ whitelistOrderFactory = new OrderFactory(makerPrivateKey, defaultOrderParams);
+ });
+
+ beforeEach(async () => {
+ signedOrder = await whitelistOrderFactory.newSignedOrderAsync();
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ });
+
+ it('should revert if maker has not been whitelisted', async () => {
+ const isApproved = true;
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await whitelist.updateWhitelistStatus.sendTransactionAsync(takerAddress, isApproved, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
+ const takerAssetFillAmount = signedOrder.takerAssetAmount;
+ const salt = generatePseudoRandomSalt();
+ return expectTransactionFailedAsync(
+ whitelist.fillOrderIfWhitelisted.sendTransactionAsync(
+ orderWithoutExchangeAddress,
+ takerAssetFillAmount,
+ salt,
+ signedOrder.signature,
+ { from: takerAddress },
+ ),
+ RevertReason.MakerNotWhitelisted,
+ );
+ });
+
+ it('should revert if taker has not been whitelisted', async () => {
+ const isApproved = true;
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await whitelist.updateWhitelistStatus.sendTransactionAsync(makerAddress, isApproved, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
+ const takerAssetFillAmount = signedOrder.takerAssetAmount;
+ const salt = generatePseudoRandomSalt();
+ return expectTransactionFailedAsync(
+ whitelist.fillOrderIfWhitelisted.sendTransactionAsync(
+ orderWithoutExchangeAddress,
+ takerAssetFillAmount,
+ salt,
+ signedOrder.signature,
+ { from: takerAddress },
+ ),
+ RevertReason.TakerNotWhitelisted,
+ );
+ });
+
+ it('should fill the order if maker and taker have been whitelisted', async () => {
+ const isApproved = true;
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await whitelist.updateWhitelistStatus.sendTransactionAsync(makerAddress, isApproved, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await whitelist.updateWhitelistStatus.sendTransactionAsync(takerAddress, isApproved, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
+ const takerAssetFillAmount = signedOrder.takerAssetAmount;
+ const salt = generatePseudoRandomSalt();
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await whitelist.fillOrderIfWhitelisted.sendTransactionAsync(
+ orderWithoutExchangeAddress,
+ takerAssetFillAmount,
+ salt,
+ signedOrder.signature,
+ { from: takerAddress },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+
+ const makerAssetFillAmount = signedOrder.makerAssetAmount;
+ const makerFeePaid = signedOrder.makerFee;
+ const takerFeePaid = signedOrder.takerFee;
+
+ expect(newBalances[makerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerTokenAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultTakerTokenAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerFeePaid),
+ );
+ expect(newBalances[takerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultTakerTokenAddress].minus(takerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerTokenAddress].add(makerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].minus(takerFeePaid),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)),
+ );
+ });
+ });
+});
diff --git a/contracts/protocol/test/exchange/wrapper.ts b/contracts/protocol/test/exchange/wrapper.ts
new file mode 100644
index 000000000..c9efb0c9d
--- /dev/null
+++ b/contracts/protocol/test/exchange/wrapper.ts
@@ -0,0 +1,1461 @@
+import {
+ chaiSetup,
+ constants,
+ ERC20BalancesByOwner,
+ expectTransactionFailedAsync,
+ getLatestBlockTimestampAsync,
+ increaseTimeAndMineBlockAsync,
+ OrderFactory,
+ OrderStatus,
+ provider,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import {
+ artifacts as tokensArtifacts,
+ DummyERC20TokenContract,
+ DummyERC721TokenContract,
+ ReentrantERC20TokenContract,
+} from '@0x/contracts-tokens';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { assetDataUtils, orderHashUtils } from '@0x/order-utils';
+import { RevertReason, SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+
+import { ERC20ProxyContract } from '../../generated-wrappers/erc20_proxy';
+import { ERC721ProxyContract } from '../../generated-wrappers/erc721_proxy';
+import { ExchangeContract } from '../../generated-wrappers/exchange';
+import { artifacts } from '../../src/artifacts';
+import { ERC20Wrapper } from '../utils/erc20_wrapper';
+import { ERC721Wrapper } from '../utils/erc721_wrapper';
+import { ExchangeWrapper } from '../utils/exchange_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+
+describe('Exchange wrappers', () => {
+ let makerAddress: string;
+ let owner: string;
+ let takerAddress: string;
+ let feeRecipientAddress: string;
+
+ let erc20TokenA: DummyERC20TokenContract;
+ let erc20TokenB: DummyERC20TokenContract;
+ let zrxToken: DummyERC20TokenContract;
+ let erc721Token: DummyERC721TokenContract;
+ let exchange: ExchangeContract;
+ let erc20Proxy: ERC20ProxyContract;
+ let erc721Proxy: ERC721ProxyContract;
+ let reentrantErc20Token: ReentrantERC20TokenContract;
+
+ let exchangeWrapper: ExchangeWrapper;
+ let erc20Wrapper: ERC20Wrapper;
+ let erc721Wrapper: ERC721Wrapper;
+ let erc20Balances: ERC20BalancesByOwner;
+ let orderFactory: OrderFactory;
+
+ let erc721MakerAssetId: BigNumber;
+ let erc721TakerAssetId: BigNumber;
+
+ let defaultMakerAssetAddress: string;
+ let defaultTakerAssetAddress: string;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress] = _.slice(accounts, 0, 4));
+
+ erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
+ erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner);
+
+ const numDummyErc20ToDeploy = 3;
+ [erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync(
+ numDummyErc20ToDeploy,
+ constants.DUMMY_TOKEN_DECIMALS,
+ );
+ erc20Proxy = await erc20Wrapper.deployProxyAsync();
+ await erc20Wrapper.setBalancesAndAllowancesAsync();
+
+ [erc721Token] = await erc721Wrapper.deployDummyTokensAsync();
+ erc721Proxy = await erc721Wrapper.deployProxyAsync();
+ await erc721Wrapper.setBalancesAndAllowancesAsync();
+ const erc721Balances = await erc721Wrapper.getBalancesAsync();
+ erc721MakerAssetId = erc721Balances[makerAddress][erc721Token.address][0];
+ erc721TakerAssetId = erc721Balances[takerAddress][erc721Token.address][0];
+
+ exchange = await ExchangeContract.deployFrom0xArtifactAsync(
+ artifacts.Exchange,
+ provider,
+ txDefaults,
+ assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ );
+ exchangeWrapper = new ExchangeWrapper(exchange, provider);
+ await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner);
+ await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner);
+
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ reentrantErc20Token = await ReentrantERC20TokenContract.deployFrom0xArtifactAsync(
+ tokensArtifacts.ReentrantERC20Token,
+ provider,
+ txDefaults,
+ exchange.address,
+ );
+
+ defaultMakerAssetAddress = erc20TokenA.address;
+ defaultTakerAssetAddress = erc20TokenB.address;
+
+ const defaultOrderParams = {
+ ...constants.STATIC_ORDER_PARAMS,
+ exchangeAddress: exchange.address,
+ makerAddress,
+ feeRecipientAddress,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress),
+ };
+ const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)];
+ orderFactory = new OrderFactory(privateKey, defaultOrderParams);
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('fillOrKillOrder', () => {
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow fillOrKillOrder to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await expectTransactionFailedAsync(
+ exchangeWrapper.fillOrKillOrderAsync(signedOrder, takerAddress),
+ RevertReason.TransferFailed,
+ );
+ });
+ });
+ };
+ describe('fillOrKillOrder reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should transfer the correct amounts', async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18),
+ });
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ await exchangeWrapper.fillOrKillOrderAsync(signedOrder, takerAddress, {
+ takerAssetFillAmount,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+
+ const makerAssetFilledAmount = takerAssetFillAmount
+ .times(signedOrder.makerAssetAmount)
+ .dividedToIntegerBy(signedOrder.takerAssetAmount);
+ const makerFee = signedOrder.makerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ const takerFee = signedOrder.takerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFilledAmount),
+ );
+ expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerFee),
+ );
+ expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFilledAmount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].minus(takerFee),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)),
+ );
+ });
+
+ it('should throw if a signedOrder is expired', async () => {
+ const currentTimestamp = await getLatestBlockTimestampAsync();
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ expirationTimeSeconds: new BigNumber(currentTimestamp).sub(10),
+ });
+
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrKillOrderAsync(signedOrder, takerAddress),
+ RevertReason.OrderUnfillable,
+ );
+ });
+
+ it('should throw if entire takerAssetFillAmount not filled', async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync();
+
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, {
+ takerAssetFillAmount: signedOrder.takerAssetAmount.div(2),
+ });
+
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrKillOrderAsync(signedOrder, takerAddress),
+ RevertReason.CompleteFillFailed,
+ );
+ });
+ });
+
+ describe('fillOrderNoThrow', () => {
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow fillOrderNoThrow to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(erc20Balances).to.deep.equal(newBalances);
+ });
+ });
+ };
+ describe('fillOrderNoThrow reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should transfer the correct amounts', async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18),
+ });
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress, {
+ takerAssetFillAmount,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 250000,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const makerAssetFilledAmount = takerAssetFillAmount
+ .times(signedOrder.makerAssetAmount)
+ .dividedToIntegerBy(signedOrder.takerAssetAmount);
+ const makerFee = signedOrder.makerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ const takerFee = signedOrder.takerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+
+ expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFilledAmount),
+ );
+ expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerFee),
+ );
+ expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFilledAmount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].minus(takerFee),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)),
+ );
+ });
+
+ it('should not change erc20Balances if maker erc20Balances are too low to fill order', async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18),
+ });
+
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should not change erc20Balances if taker erc20Balances are too low to fill order', async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18),
+ });
+
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should not change erc20Balances if maker allowances are too low to fill order', async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync();
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20TokenA.approve.sendTransactionAsync(erc20Proxy.address, new BigNumber(0), {
+ from: makerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20TokenA.approve.sendTransactionAsync(erc20Proxy.address, constants.INITIAL_ERC20_ALLOWANCE, {
+ from: makerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should not change erc20Balances if taker allowances are too low to fill order', async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync();
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20TokenB.approve.sendTransactionAsync(erc20Proxy.address, new BigNumber(0), {
+ from: takerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20TokenB.approve.sendTransactionAsync(erc20Proxy.address, constants.INITIAL_ERC20_ALLOWANCE, {
+ from: takerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should not change erc20Balances if makerAssetAddress is ZRX, makerAssetAmount + makerFee > maker balance', async () => {
+ const makerZRXBalance = new BigNumber(erc20Balances[makerAddress][zrxToken.address]);
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: makerZRXBalance,
+ makerFee: new BigNumber(1),
+ makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ });
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should not change erc20Balances if makerAssetAddress is ZRX, makerAssetAmount + makerFee > maker allowance', async () => {
+ const makerZRXAllowance = await zrxToken.allowance.callAsync(makerAddress, erc20Proxy.address);
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(makerZRXAllowance),
+ makerFee: new BigNumber(1),
+ makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ });
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should not change erc20Balances if takerAssetAddress is ZRX, takerAssetAmount + takerFee > taker balance', async () => {
+ const takerZRXBalance = new BigNumber(erc20Balances[takerAddress][zrxToken.address]);
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ takerAssetAmount: takerZRXBalance,
+ takerFee: new BigNumber(1),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ });
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should not change erc20Balances if takerAssetAddress is ZRX, takerAssetAmount + takerFee > taker allowance', async () => {
+ const takerZRXAllowance = await zrxToken.allowance.callAsync(takerAddress, erc20Proxy.address);
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ takerAssetAmount: new BigNumber(takerZRXAllowance),
+ takerFee: new BigNumber(1),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ });
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should successfully exchange ERC721 tokens', async () => {
+ // Construct Exchange parameters
+ const makerAssetId = erc721MakerAssetId;
+ const takerAssetId = erc721TakerAssetId;
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(1),
+ takerAssetAmount: new BigNumber(1),
+ makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ takerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, takerAssetId),
+ });
+ // Verify pre-conditions
+ const initialOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
+ expect(initialOwnerMakerAsset).to.be.bignumber.equal(makerAddress);
+ const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId);
+ expect(initialOwnerTakerAsset).to.be.bignumber.equal(takerAddress);
+ // Call Exchange
+ const takerAssetFillAmount = signedOrder.takerAssetAmount;
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress, {
+ takerAssetFillAmount,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 280000,
+ });
+ // Verify post-conditions
+ const newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
+ expect(newOwnerMakerAsset).to.be.bignumber.equal(takerAddress);
+ const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId);
+ expect(newOwnerTakerAsset).to.be.bignumber.equal(makerAddress);
+ });
+ });
+
+ describe('batch functions', () => {
+ let signedOrders: SignedOrder[];
+ beforeEach(async () => {
+ signedOrders = [
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync(),
+ ];
+ });
+
+ describe('batchFillOrders', () => {
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow batchFillOrders to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await expectTransactionFailedAsync(
+ exchangeWrapper.batchFillOrdersAsync([signedOrder], takerAddress),
+ RevertReason.TransferFailed,
+ );
+ });
+ });
+ };
+ describe('batchFillOrders reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should transfer the correct amounts', async () => {
+ const takerAssetFillAmounts: BigNumber[] = [];
+ const makerAssetAddress = erc20TokenA.address;
+ const takerAssetAddress = erc20TokenB.address;
+ _.forEach(signedOrders, signedOrder => {
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ const makerAssetFilledAmount = takerAssetFillAmount
+ .times(signedOrder.makerAssetAmount)
+ .dividedToIntegerBy(signedOrder.takerAssetAmount);
+ const makerFee = signedOrder.makerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ const takerFee = signedOrder.takerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ takerAssetFillAmounts.push(takerAssetFillAmount);
+ erc20Balances[makerAddress][makerAssetAddress] = erc20Balances[makerAddress][
+ makerAssetAddress
+ ].minus(makerAssetFilledAmount);
+ erc20Balances[makerAddress][takerAssetAddress] = erc20Balances[makerAddress][takerAssetAddress].add(
+ takerAssetFillAmount,
+ );
+ erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus(
+ makerFee,
+ );
+ erc20Balances[takerAddress][makerAssetAddress] = erc20Balances[takerAddress][makerAssetAddress].add(
+ makerAssetFilledAmount,
+ );
+ erc20Balances[takerAddress][takerAssetAddress] = erc20Balances[takerAddress][
+ takerAssetAddress
+ ].minus(takerAssetFillAmount);
+ erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus(
+ takerFee,
+ );
+ erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][
+ zrxToken.address
+ ].add(makerFee.add(takerFee));
+ });
+
+ await exchangeWrapper.batchFillOrdersAsync(signedOrders, takerAddress, {
+ takerAssetFillAmounts,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+ });
+
+ describe('batchFillOrKillOrders', () => {
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow batchFillOrKillOrders to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await expectTransactionFailedAsync(
+ exchangeWrapper.batchFillOrKillOrdersAsync([signedOrder], takerAddress),
+ RevertReason.TransferFailed,
+ );
+ });
+ });
+ };
+ describe('batchFillOrKillOrders reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should transfer the correct amounts', async () => {
+ const takerAssetFillAmounts: BigNumber[] = [];
+ const makerAssetAddress = erc20TokenA.address;
+ const takerAssetAddress = erc20TokenB.address;
+ _.forEach(signedOrders, signedOrder => {
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ const makerAssetFilledAmount = takerAssetFillAmount
+ .times(signedOrder.makerAssetAmount)
+ .dividedToIntegerBy(signedOrder.takerAssetAmount);
+ const makerFee = signedOrder.makerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ const takerFee = signedOrder.takerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ takerAssetFillAmounts.push(takerAssetFillAmount);
+ erc20Balances[makerAddress][makerAssetAddress] = erc20Balances[makerAddress][
+ makerAssetAddress
+ ].minus(makerAssetFilledAmount);
+ erc20Balances[makerAddress][takerAssetAddress] = erc20Balances[makerAddress][takerAssetAddress].add(
+ takerAssetFillAmount,
+ );
+ erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus(
+ makerFee,
+ );
+ erc20Balances[takerAddress][makerAssetAddress] = erc20Balances[takerAddress][makerAssetAddress].add(
+ makerAssetFilledAmount,
+ );
+ erc20Balances[takerAddress][takerAssetAddress] = erc20Balances[takerAddress][
+ takerAssetAddress
+ ].minus(takerAssetFillAmount);
+ erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus(
+ takerFee,
+ );
+ erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][
+ zrxToken.address
+ ].add(makerFee.add(takerFee));
+ });
+
+ await exchangeWrapper.batchFillOrKillOrdersAsync(signedOrders, takerAddress, {
+ takerAssetFillAmounts,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should throw if a single signedOrder does not fill the expected amount', async () => {
+ const takerAssetFillAmounts: BigNumber[] = [];
+ _.forEach(signedOrders, signedOrder => {
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ takerAssetFillAmounts.push(takerAssetFillAmount);
+ });
+
+ await exchangeWrapper.fillOrKillOrderAsync(signedOrders[0], takerAddress);
+
+ return expectTransactionFailedAsync(
+ exchangeWrapper.batchFillOrKillOrdersAsync(signedOrders, takerAddress, {
+ takerAssetFillAmounts,
+ }),
+ RevertReason.OrderUnfillable,
+ );
+ });
+ });
+
+ describe('batchFillOrdersNoThrow', async () => {
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow batchFillOrdersNoThrow to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await exchangeWrapper.batchFillOrdersNoThrowAsync([signedOrder], takerAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(erc20Balances).to.deep.equal(newBalances);
+ });
+ });
+ };
+ describe('batchFillOrdersNoThrow reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should transfer the correct amounts', async () => {
+ const takerAssetFillAmounts: BigNumber[] = [];
+ const makerAssetAddress = erc20TokenA.address;
+ const takerAssetAddress = erc20TokenB.address;
+ _.forEach(signedOrders, signedOrder => {
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ const makerAssetFilledAmount = takerAssetFillAmount
+ .times(signedOrder.makerAssetAmount)
+ .dividedToIntegerBy(signedOrder.takerAssetAmount);
+ const makerFee = signedOrder.makerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ const takerFee = signedOrder.takerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ takerAssetFillAmounts.push(takerAssetFillAmount);
+ erc20Balances[makerAddress][makerAssetAddress] = erc20Balances[makerAddress][
+ makerAssetAddress
+ ].minus(makerAssetFilledAmount);
+ erc20Balances[makerAddress][takerAssetAddress] = erc20Balances[makerAddress][takerAssetAddress].add(
+ takerAssetFillAmount,
+ );
+ erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus(
+ makerFee,
+ );
+ erc20Balances[takerAddress][makerAssetAddress] = erc20Balances[takerAddress][makerAssetAddress].add(
+ makerAssetFilledAmount,
+ );
+ erc20Balances[takerAddress][takerAssetAddress] = erc20Balances[takerAddress][
+ takerAssetAddress
+ ].minus(takerAssetFillAmount);
+ erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus(
+ takerFee,
+ );
+ erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][
+ zrxToken.address
+ ].add(makerFee.add(takerFee));
+ });
+
+ await exchangeWrapper.batchFillOrdersNoThrowAsync(signedOrders, takerAddress, {
+ takerAssetFillAmounts,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 600000,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should not throw if an order is invalid and fill the remaining orders', async () => {
+ const takerAssetFillAmounts: BigNumber[] = [];
+ const makerAssetAddress = erc20TokenA.address;
+ const takerAssetAddress = erc20TokenB.address;
+
+ const invalidOrder = {
+ ...signedOrders[0],
+ signature: '0x00',
+ };
+ const validOrders = signedOrders.slice(1);
+
+ takerAssetFillAmounts.push(invalidOrder.takerAssetAmount.div(2));
+ _.forEach(validOrders, signedOrder => {
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ const makerAssetFilledAmount = takerAssetFillAmount
+ .times(signedOrder.makerAssetAmount)
+ .dividedToIntegerBy(signedOrder.takerAssetAmount);
+ const makerFee = signedOrder.makerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ const takerFee = signedOrder.takerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ takerAssetFillAmounts.push(takerAssetFillAmount);
+ erc20Balances[makerAddress][makerAssetAddress] = erc20Balances[makerAddress][
+ makerAssetAddress
+ ].minus(makerAssetFilledAmount);
+ erc20Balances[makerAddress][takerAssetAddress] = erc20Balances[makerAddress][takerAssetAddress].add(
+ takerAssetFillAmount,
+ );
+ erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus(
+ makerFee,
+ );
+ erc20Balances[takerAddress][makerAssetAddress] = erc20Balances[takerAddress][makerAssetAddress].add(
+ makerAssetFilledAmount,
+ );
+ erc20Balances[takerAddress][takerAssetAddress] = erc20Balances[takerAddress][
+ takerAssetAddress
+ ].minus(takerAssetFillAmount);
+ erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus(
+ takerFee,
+ );
+ erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][
+ zrxToken.address
+ ].add(makerFee.add(takerFee));
+ });
+
+ const newOrders = [invalidOrder, ...validOrders];
+ await exchangeWrapper.batchFillOrdersNoThrowAsync(newOrders, takerAddress, {
+ takerAssetFillAmounts,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 450000,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+ });
+
+ describe('marketSellOrders', () => {
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow marketSellOrders to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await expectTransactionFailedAsync(
+ exchangeWrapper.marketSellOrdersAsync([signedOrder], takerAddress, {
+ takerAssetFillAmount: signedOrder.takerAssetAmount,
+ }),
+ RevertReason.TransferFailed,
+ );
+ });
+ });
+ };
+ describe('marketSellOrders reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should stop when the entire takerAssetFillAmount is filled', async () => {
+ const takerAssetFillAmount = signedOrders[0].takerAssetAmount.plus(
+ signedOrders[1].takerAssetAmount.div(2),
+ );
+ await exchangeWrapper.marketSellOrdersAsync(signedOrders, takerAddress, {
+ takerAssetFillAmount,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+
+ const makerAssetFilledAmount = signedOrders[0].makerAssetAmount.add(
+ signedOrders[1].makerAssetAmount.dividedToIntegerBy(2),
+ );
+ const makerFee = signedOrders[0].makerFee.add(signedOrders[1].makerFee.dividedToIntegerBy(2));
+ const takerFee = signedOrders[0].takerFee.add(signedOrders[1].takerFee.dividedToIntegerBy(2));
+ expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFilledAmount),
+ );
+ expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerFee),
+ );
+ expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFilledAmount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].minus(takerFee),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)),
+ );
+ });
+
+ it('should fill all signedOrders if cannot fill entire takerAssetFillAmount', async () => {
+ const takerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18);
+ _.forEach(signedOrders, signedOrder => {
+ erc20Balances[makerAddress][defaultMakerAssetAddress] = erc20Balances[makerAddress][
+ defaultMakerAssetAddress
+ ].minus(signedOrder.makerAssetAmount);
+ erc20Balances[makerAddress][defaultTakerAssetAddress] = erc20Balances[makerAddress][
+ defaultTakerAssetAddress
+ ].add(signedOrder.takerAssetAmount);
+ erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus(
+ signedOrder.makerFee,
+ );
+ erc20Balances[takerAddress][defaultMakerAssetAddress] = erc20Balances[takerAddress][
+ defaultMakerAssetAddress
+ ].add(signedOrder.makerAssetAmount);
+ erc20Balances[takerAddress][defaultTakerAssetAddress] = erc20Balances[takerAddress][
+ defaultTakerAssetAddress
+ ].minus(signedOrder.takerAssetAmount);
+ erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus(
+ signedOrder.takerFee,
+ );
+ erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][
+ zrxToken.address
+ ].add(signedOrder.makerFee.add(signedOrder.takerFee));
+ });
+ await exchangeWrapper.marketSellOrdersAsync(signedOrders, takerAddress, {
+ takerAssetFillAmount,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should throw when a signedOrder does not use the same takerAssetAddress', async () => {
+ signedOrders = [
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync({
+ takerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ }),
+ await orderFactory.newSignedOrderAsync(),
+ ];
+
+ return expectTransactionFailedAsync(
+ exchangeWrapper.marketSellOrdersAsync(signedOrders, takerAddress, {
+ takerAssetFillAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1000), 18),
+ }),
+ // We simply use the takerAssetData from the first order for all orders.
+ // If they are not the same, the contract throws when validating the order signature
+ RevertReason.InvalidOrderSignature,
+ );
+ });
+ });
+
+ describe('marketSellOrdersNoThrow', () => {
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow marketSellOrdersNoThrow to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await exchangeWrapper.marketSellOrdersNoThrowAsync([signedOrder], takerAddress, {
+ takerAssetFillAmount: signedOrder.takerAssetAmount,
+ });
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(erc20Balances).to.deep.equal(newBalances);
+ });
+ });
+ };
+ describe('marketSellOrdersNoThrow reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should stop when the entire takerAssetFillAmount is filled', async () => {
+ const takerAssetFillAmount = signedOrders[0].takerAssetAmount.plus(
+ signedOrders[1].takerAssetAmount.div(2),
+ );
+ await exchangeWrapper.marketSellOrdersNoThrowAsync(signedOrders, takerAddress, {
+ takerAssetFillAmount,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 6000000,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+
+ const makerAssetFilledAmount = signedOrders[0].makerAssetAmount.add(
+ signedOrders[1].makerAssetAmount.dividedToIntegerBy(2),
+ );
+ const makerFee = signedOrders[0].makerFee.add(signedOrders[1].makerFee.dividedToIntegerBy(2));
+ const takerFee = signedOrders[0].takerFee.add(signedOrders[1].takerFee.dividedToIntegerBy(2));
+ expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFilledAmount),
+ );
+ expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerFee),
+ );
+ expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFilledAmount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].minus(takerFee),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)),
+ );
+ });
+
+ it('should fill all signedOrders if cannot fill entire takerAssetFillAmount', async () => {
+ const takerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18);
+ _.forEach(signedOrders, signedOrder => {
+ erc20Balances[makerAddress][defaultMakerAssetAddress] = erc20Balances[makerAddress][
+ defaultMakerAssetAddress
+ ].minus(signedOrder.makerAssetAmount);
+ erc20Balances[makerAddress][defaultTakerAssetAddress] = erc20Balances[makerAddress][
+ defaultTakerAssetAddress
+ ].add(signedOrder.takerAssetAmount);
+ erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus(
+ signedOrder.makerFee,
+ );
+ erc20Balances[takerAddress][defaultMakerAssetAddress] = erc20Balances[takerAddress][
+ defaultMakerAssetAddress
+ ].add(signedOrder.makerAssetAmount);
+ erc20Balances[takerAddress][defaultTakerAssetAddress] = erc20Balances[takerAddress][
+ defaultTakerAssetAddress
+ ].minus(signedOrder.takerAssetAmount);
+ erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus(
+ signedOrder.takerFee,
+ );
+ erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][
+ zrxToken.address
+ ].add(signedOrder.makerFee.add(signedOrder.takerFee));
+ });
+ await exchangeWrapper.marketSellOrdersNoThrowAsync(signedOrders, takerAddress, {
+ takerAssetFillAmount,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 600000,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should not fill a signedOrder that does not use the same takerAssetAddress', async () => {
+ signedOrders = [
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync({
+ takerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ }),
+ ];
+ const takerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18);
+ const filledSignedOrders = signedOrders.slice(0, -1);
+ _.forEach(filledSignedOrders, signedOrder => {
+ erc20Balances[makerAddress][defaultMakerAssetAddress] = erc20Balances[makerAddress][
+ defaultMakerAssetAddress
+ ].minus(signedOrder.makerAssetAmount);
+ erc20Balances[makerAddress][defaultTakerAssetAddress] = erc20Balances[makerAddress][
+ defaultTakerAssetAddress
+ ].add(signedOrder.takerAssetAmount);
+ erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus(
+ signedOrder.makerFee,
+ );
+ erc20Balances[takerAddress][defaultMakerAssetAddress] = erc20Balances[takerAddress][
+ defaultMakerAssetAddress
+ ].add(signedOrder.makerAssetAmount);
+ erc20Balances[takerAddress][defaultTakerAssetAddress] = erc20Balances[takerAddress][
+ defaultTakerAssetAddress
+ ].minus(signedOrder.takerAssetAmount);
+ erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus(
+ signedOrder.takerFee,
+ );
+ erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][
+ zrxToken.address
+ ].add(signedOrder.makerFee.add(signedOrder.takerFee));
+ });
+ await exchangeWrapper.marketSellOrdersNoThrowAsync(signedOrders, takerAddress, {
+ takerAssetFillAmount,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 600000,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+ });
+
+ describe('marketBuyOrders', () => {
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow marketBuyOrders to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await expectTransactionFailedAsync(
+ exchangeWrapper.marketBuyOrdersAsync([signedOrder], takerAddress, {
+ makerAssetFillAmount: signedOrder.makerAssetAmount,
+ }),
+ RevertReason.TransferFailed,
+ );
+ });
+ });
+ };
+ describe('marketBuyOrders reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should stop when the entire makerAssetFillAmount is filled', async () => {
+ const makerAssetFillAmount = signedOrders[0].makerAssetAmount.plus(
+ signedOrders[1].makerAssetAmount.div(2),
+ );
+ await exchangeWrapper.marketBuyOrdersAsync(signedOrders, takerAddress, {
+ makerAssetFillAmount,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+
+ const makerAmountBought = signedOrders[0].takerAssetAmount.add(
+ signedOrders[1].takerAssetAmount.dividedToIntegerBy(2),
+ );
+ const makerFee = signedOrders[0].makerFee.add(signedOrders[1].makerFee.dividedToIntegerBy(2));
+ const takerFee = signedOrders[0].takerFee.add(signedOrders[1].takerFee.dividedToIntegerBy(2));
+ expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultTakerAssetAddress].add(makerAmountBought),
+ );
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerFee),
+ );
+ expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultTakerAssetAddress].minus(makerAmountBought),
+ );
+ expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].minus(takerFee),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)),
+ );
+ });
+
+ it('should fill all signedOrders if cannot fill entire makerAssetFillAmount', async () => {
+ const makerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18);
+ _.forEach(signedOrders, signedOrder => {
+ erc20Balances[makerAddress][defaultMakerAssetAddress] = erc20Balances[makerAddress][
+ defaultMakerAssetAddress
+ ].minus(signedOrder.makerAssetAmount);
+ erc20Balances[makerAddress][defaultTakerAssetAddress] = erc20Balances[makerAddress][
+ defaultTakerAssetAddress
+ ].add(signedOrder.takerAssetAmount);
+ erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus(
+ signedOrder.makerFee,
+ );
+ erc20Balances[takerAddress][defaultMakerAssetAddress] = erc20Balances[takerAddress][
+ defaultMakerAssetAddress
+ ].add(signedOrder.makerAssetAmount);
+ erc20Balances[takerAddress][defaultTakerAssetAddress] = erc20Balances[takerAddress][
+ defaultTakerAssetAddress
+ ].minus(signedOrder.takerAssetAmount);
+ erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus(
+ signedOrder.takerFee,
+ );
+ erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][
+ zrxToken.address
+ ].add(signedOrder.makerFee.add(signedOrder.takerFee));
+ });
+ await exchangeWrapper.marketBuyOrdersAsync(signedOrders, takerAddress, {
+ makerAssetFillAmount,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should throw when a signedOrder does not use the same makerAssetAddress', async () => {
+ signedOrders = [
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ }),
+ await orderFactory.newSignedOrderAsync(),
+ ];
+
+ return expectTransactionFailedAsync(
+ exchangeWrapper.marketBuyOrdersAsync(signedOrders, takerAddress, {
+ makerAssetFillAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1000), 18),
+ }),
+ RevertReason.InvalidOrderSignature,
+ );
+ });
+ });
+
+ describe('marketBuyOrdersNoThrow', () => {
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow marketBuyOrdersNoThrow to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await exchangeWrapper.marketBuyOrdersNoThrowAsync([signedOrder], takerAddress, {
+ makerAssetFillAmount: signedOrder.makerAssetAmount,
+ });
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(erc20Balances).to.deep.equal(newBalances);
+ });
+ });
+ };
+ describe('marketBuyOrdersNoThrow reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should stop when the entire makerAssetFillAmount is filled', async () => {
+ const makerAssetFillAmount = signedOrders[0].makerAssetAmount.plus(
+ signedOrders[1].makerAssetAmount.div(2),
+ );
+ await exchangeWrapper.marketBuyOrdersNoThrowAsync(signedOrders, takerAddress, {
+ makerAssetFillAmount,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 600000,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+
+ const makerAmountBought = signedOrders[0].takerAssetAmount.add(
+ signedOrders[1].takerAssetAmount.dividedToIntegerBy(2),
+ );
+ const makerFee = signedOrders[0].makerFee.add(signedOrders[1].makerFee.dividedToIntegerBy(2));
+ const takerFee = signedOrders[0].takerFee.add(signedOrders[1].takerFee.dividedToIntegerBy(2));
+ expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultTakerAssetAddress].add(makerAmountBought),
+ );
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerFee),
+ );
+ expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultTakerAssetAddress].minus(makerAmountBought),
+ );
+ expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].minus(takerFee),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)),
+ );
+ });
+
+ it('should fill all signedOrders if cannot fill entire makerAssetFillAmount', async () => {
+ const makerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18);
+ _.forEach(signedOrders, signedOrder => {
+ erc20Balances[makerAddress][defaultMakerAssetAddress] = erc20Balances[makerAddress][
+ defaultMakerAssetAddress
+ ].minus(signedOrder.makerAssetAmount);
+ erc20Balances[makerAddress][defaultTakerAssetAddress] = erc20Balances[makerAddress][
+ defaultTakerAssetAddress
+ ].add(signedOrder.takerAssetAmount);
+ erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus(
+ signedOrder.makerFee,
+ );
+ erc20Balances[takerAddress][defaultMakerAssetAddress] = erc20Balances[takerAddress][
+ defaultMakerAssetAddress
+ ].add(signedOrder.makerAssetAmount);
+ erc20Balances[takerAddress][defaultTakerAssetAddress] = erc20Balances[takerAddress][
+ defaultTakerAssetAddress
+ ].minus(signedOrder.takerAssetAmount);
+ erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus(
+ signedOrder.takerFee,
+ );
+ erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][
+ zrxToken.address
+ ].add(signedOrder.makerFee.add(signedOrder.takerFee));
+ });
+ await exchangeWrapper.marketBuyOrdersNoThrowAsync(signedOrders, takerAddress, {
+ makerAssetFillAmount,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 600000,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should not fill a signedOrder that does not use the same makerAssetAddress', async () => {
+ signedOrders = [
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ }),
+ ];
+
+ const makerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18);
+ const filledSignedOrders = signedOrders.slice(0, -1);
+ _.forEach(filledSignedOrders, signedOrder => {
+ erc20Balances[makerAddress][defaultMakerAssetAddress] = erc20Balances[makerAddress][
+ defaultMakerAssetAddress
+ ].minus(signedOrder.makerAssetAmount);
+ erc20Balances[makerAddress][defaultTakerAssetAddress] = erc20Balances[makerAddress][
+ defaultTakerAssetAddress
+ ].add(signedOrder.takerAssetAmount);
+ erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus(
+ signedOrder.makerFee,
+ );
+ erc20Balances[takerAddress][defaultMakerAssetAddress] = erc20Balances[takerAddress][
+ defaultMakerAssetAddress
+ ].add(signedOrder.makerAssetAmount);
+ erc20Balances[takerAddress][defaultTakerAssetAddress] = erc20Balances[takerAddress][
+ defaultTakerAssetAddress
+ ].minus(signedOrder.takerAssetAmount);
+ erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus(
+ signedOrder.takerFee,
+ );
+ erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][
+ zrxToken.address
+ ].add(signedOrder.makerFee.add(signedOrder.takerFee));
+ });
+ await exchangeWrapper.marketBuyOrdersNoThrowAsync(signedOrders, takerAddress, {
+ makerAssetFillAmount,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 600000,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+ });
+
+ describe('batchCancelOrders', () => {
+ it('should be able to cancel multiple signedOrders', async () => {
+ const takerAssetCancelAmounts = _.map(signedOrders, signedOrder => signedOrder.takerAssetAmount);
+ await exchangeWrapper.batchCancelOrdersAsync(signedOrders, makerAddress);
+
+ await exchangeWrapper.batchFillOrdersNoThrowAsync(signedOrders, takerAddress, {
+ takerAssetFillAmounts: takerAssetCancelAmounts,
+ });
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(erc20Balances).to.be.deep.equal(newBalances);
+ });
+ });
+
+ describe('getOrdersInfo', () => {
+ beforeEach(async () => {
+ signedOrders = [
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync(),
+ ];
+ });
+ it('should get the correct information for multiple unfilled orders', async () => {
+ const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders);
+ expect(ordersInfo.length).to.be.equal(3);
+ _.forEach(signedOrders, (signedOrder, index) => {
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = new BigNumber(0);
+ const expectedOrderStatus = OrderStatus.FILLABLE;
+ const orderInfo = ordersInfo[index];
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ });
+ it('should get the correct information for multiple partially filled orders', async () => {
+ const takerAssetFillAmounts = _.map(signedOrders, signedOrder => signedOrder.takerAssetAmount.div(2));
+ await exchangeWrapper.batchFillOrdersAsync(signedOrders, takerAddress, { takerAssetFillAmounts });
+ const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders);
+ expect(ordersInfo.length).to.be.equal(3);
+ _.forEach(signedOrders, (signedOrder, index) => {
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = signedOrder.takerAssetAmount.div(2);
+ const expectedOrderStatus = OrderStatus.FILLABLE;
+ const orderInfo = ordersInfo[index];
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ });
+ it('should get the correct information for multiple fully filled orders', async () => {
+ await exchangeWrapper.batchFillOrdersAsync(signedOrders, takerAddress);
+ const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders);
+ expect(ordersInfo.length).to.be.equal(3);
+ _.forEach(signedOrders, (signedOrder, index) => {
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = signedOrder.takerAssetAmount;
+ const expectedOrderStatus = OrderStatus.FULLY_FILLED;
+ const orderInfo = ordersInfo[index];
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ });
+ it('should get the correct information for multiple cancelled and unfilled orders', async () => {
+ await exchangeWrapper.batchCancelOrdersAsync(signedOrders, makerAddress);
+ const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders);
+ expect(ordersInfo.length).to.be.equal(3);
+ _.forEach(signedOrders, (signedOrder, index) => {
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = new BigNumber(0);
+ const expectedOrderStatus = OrderStatus.CANCELLED;
+ const orderInfo = ordersInfo[index];
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ });
+ it('should get the correct information for multiple cancelled and partially filled orders', async () => {
+ const takerAssetFillAmounts = _.map(signedOrders, signedOrder => signedOrder.takerAssetAmount.div(2));
+ await exchangeWrapper.batchFillOrdersAsync(signedOrders, takerAddress, { takerAssetFillAmounts });
+ await exchangeWrapper.batchCancelOrdersAsync(signedOrders, makerAddress);
+ const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders);
+ expect(ordersInfo.length).to.be.equal(3);
+ _.forEach(signedOrders, (signedOrder, index) => {
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = signedOrder.takerAssetAmount.div(2);
+ const expectedOrderStatus = OrderStatus.CANCELLED;
+ const orderInfo = ordersInfo[index];
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ });
+ it('should get the correct information for multiple expired and unfilled orders', async () => {
+ const currentTimestamp = await getLatestBlockTimestampAsync();
+ const timeUntilExpiration = signedOrders[0].expirationTimeSeconds.minus(currentTimestamp).toNumber();
+ await increaseTimeAndMineBlockAsync(timeUntilExpiration);
+ const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders);
+ expect(ordersInfo.length).to.be.equal(3);
+ _.forEach(signedOrders, (signedOrder, index) => {
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = new BigNumber(0);
+ const expectedOrderStatus = OrderStatus.EXPIRED;
+ const orderInfo = ordersInfo[index];
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ });
+ it('should get the correct information for multiple expired and partially filled orders', async () => {
+ const takerAssetFillAmounts = _.map(signedOrders, signedOrder => signedOrder.takerAssetAmount.div(2));
+ await exchangeWrapper.batchFillOrdersAsync(signedOrders, takerAddress, { takerAssetFillAmounts });
+ const currentTimestamp = await getLatestBlockTimestampAsync();
+ const timeUntilExpiration = signedOrders[0].expirationTimeSeconds.minus(currentTimestamp).toNumber();
+ await increaseTimeAndMineBlockAsync(timeUntilExpiration);
+ const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders);
+ expect(ordersInfo.length).to.be.equal(3);
+ _.forEach(signedOrders, (signedOrder, index) => {
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = signedOrder.takerAssetAmount.div(2);
+ const expectedOrderStatus = OrderStatus.EXPIRED;
+ const orderInfo = ordersInfo[index];
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ });
+ it('should get the correct information for a mix of unfilled, partially filled, fully filled, cancelled, and expired orders', async () => {
+ const unfilledOrder = await orderFactory.newSignedOrderAsync();
+ const partiallyFilledOrder = await orderFactory.newSignedOrderAsync();
+ await exchangeWrapper.fillOrderAsync(partiallyFilledOrder, takerAddress, {
+ takerAssetFillAmount: partiallyFilledOrder.takerAssetAmount.div(2),
+ });
+ const fullyFilledOrder = await orderFactory.newSignedOrderAsync();
+ await exchangeWrapper.fillOrderAsync(fullyFilledOrder, takerAddress);
+ const cancelledOrder = await orderFactory.newSignedOrderAsync();
+ await exchangeWrapper.cancelOrderAsync(cancelledOrder, makerAddress);
+ const currentTimestamp = await getLatestBlockTimestampAsync();
+ const expiredOrder = await orderFactory.newSignedOrderAsync({
+ expirationTimeSeconds: new BigNumber(currentTimestamp),
+ });
+ signedOrders = [unfilledOrder, partiallyFilledOrder, fullyFilledOrder, cancelledOrder, expiredOrder];
+ const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders);
+ expect(ordersInfo.length).to.be.equal(5);
+
+ const expectedUnfilledOrderHash = orderHashUtils.getOrderHashHex(unfilledOrder);
+ const expectedUnfilledTakerAssetFilledAmount = new BigNumber(0);
+ const expectedUnfilledOrderStatus = OrderStatus.FILLABLE;
+ const unfilledOrderInfo = ordersInfo[0];
+ expect(unfilledOrderInfo.orderHash).to.be.equal(expectedUnfilledOrderHash);
+ expect(unfilledOrderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(
+ expectedUnfilledTakerAssetFilledAmount,
+ );
+ expect(unfilledOrderInfo.orderStatus).to.be.equal(expectedUnfilledOrderStatus);
+
+ const expectedPartialOrderHash = orderHashUtils.getOrderHashHex(partiallyFilledOrder);
+ const expectedPartialTakerAssetFilledAmount = partiallyFilledOrder.takerAssetAmount.div(2);
+ const expectedPartialOrderStatus = OrderStatus.FILLABLE;
+ const partialOrderInfo = ordersInfo[1];
+ expect(partialOrderInfo.orderHash).to.be.equal(expectedPartialOrderHash);
+ expect(partialOrderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(
+ expectedPartialTakerAssetFilledAmount,
+ );
+ expect(partialOrderInfo.orderStatus).to.be.equal(expectedPartialOrderStatus);
+
+ const expectedFilledOrderHash = orderHashUtils.getOrderHashHex(fullyFilledOrder);
+ const expectedFilledTakerAssetFilledAmount = fullyFilledOrder.takerAssetAmount;
+ const expectedFilledOrderStatus = OrderStatus.FULLY_FILLED;
+ const filledOrderInfo = ordersInfo[2];
+ expect(filledOrderInfo.orderHash).to.be.equal(expectedFilledOrderHash);
+ expect(filledOrderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(
+ expectedFilledTakerAssetFilledAmount,
+ );
+ expect(filledOrderInfo.orderStatus).to.be.equal(expectedFilledOrderStatus);
+
+ const expectedCancelledOrderHash = orderHashUtils.getOrderHashHex(cancelledOrder);
+ const expectedCancelledTakerAssetFilledAmount = new BigNumber(0);
+ const expectedCancelledOrderStatus = OrderStatus.CANCELLED;
+ const cancelledOrderInfo = ordersInfo[3];
+ expect(cancelledOrderInfo.orderHash).to.be.equal(expectedCancelledOrderHash);
+ expect(cancelledOrderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(
+ expectedCancelledTakerAssetFilledAmount,
+ );
+ expect(cancelledOrderInfo.orderStatus).to.be.equal(expectedCancelledOrderStatus);
+
+ const expectedExpiredOrderHash = orderHashUtils.getOrderHashHex(expiredOrder);
+ const expectedExpiredTakerAssetFilledAmount = new BigNumber(0);
+ const expectedExpiredOrderStatus = OrderStatus.EXPIRED;
+ const expiredOrderInfo = ordersInfo[4];
+ expect(expiredOrderInfo.orderHash).to.be.equal(expectedExpiredOrderHash);
+ expect(expiredOrderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(
+ expectedExpiredTakerAssetFilledAmount,
+ );
+ expect(expiredOrderInfo.orderStatus).to.be.equal(expectedExpiredOrderStatus);
+ });
+ });
+ });
+}); // tslint:disable-line:max-file-line-count
diff --git a/contracts/protocol/test/global_hooks.ts b/contracts/protocol/test/global_hooks.ts
new file mode 100644
index 000000000..f8ace376a
--- /dev/null
+++ b/contracts/protocol/test/global_hooks.ts
@@ -0,0 +1,17 @@
+import { env, EnvVars } from '@0x/dev-utils';
+
+import { coverage, profiler, provider } from '@0x/contracts-test-utils';
+before('start web3 provider', () => {
+ provider.start();
+});
+after('generate coverage report', async () => {
+ if (env.parseBoolean(EnvVars.SolidityCoverage)) {
+ const coverageSubprovider = coverage.getCoverageSubproviderSingleton();
+ await coverageSubprovider.writeCoverageAsync();
+ }
+ if (env.parseBoolean(EnvVars.SolidityProfiler)) {
+ const profilerSubprovider = profiler.getProfilerSubproviderSingleton();
+ await profilerSubprovider.writeProfilerOutputAsync();
+ }
+ provider.stop();
+});
diff --git a/contracts/protocol/test/multisig/asset_proxy_owner.ts b/contracts/protocol/test/multisig/asset_proxy_owner.ts
new file mode 100644
index 000000000..daebfb7fb
--- /dev/null
+++ b/contracts/protocol/test/multisig/asset_proxy_owner.ts
@@ -0,0 +1,506 @@
+import {
+ chaiSetup,
+ constants,
+ expectContractCallFailedAsync,
+ expectContractCreationFailedAsync,
+ expectTransactionFailedAsync,
+ expectTransactionFailedWithoutReasonAsync,
+ increaseTimeAndMineBlockAsync,
+ provider,
+ sendTransactionResult,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { RevertReason } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import * as chai from 'chai';
+import { LogWithDecodedArgs } from 'ethereum-types';
+
+import {
+ AssetProxyOwnerAssetProxyRegistrationEventArgs,
+ AssetProxyOwnerContract,
+ AssetProxyOwnerExecutionEventArgs,
+ AssetProxyOwnerExecutionFailureEventArgs,
+ AssetProxyOwnerSubmissionEventArgs,
+} from '../../generated-wrappers/asset_proxy_owner';
+import { MixinAuthorizableContract } from '../../generated-wrappers/mixin_authorizable';
+import { TestAssetProxyOwnerContract } from '../../generated-wrappers/test_asset_proxy_owner';
+import { artifacts } from '../../src/artifacts';
+import { AssetProxyOwnerWrapper } from '../utils/asset_proxy_owner_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+// tslint:disable:no-unnecessary-type-assertion
+describe('AssetProxyOwner', () => {
+ let owners: string[];
+ let authorized: string;
+ let notOwner: string;
+ const REQUIRED_APPROVALS = new BigNumber(2);
+ const SECONDS_TIME_LOCKED = new BigNumber(1000000);
+
+ let erc20Proxy: MixinAuthorizableContract;
+ let erc721Proxy: MixinAuthorizableContract;
+ let testAssetProxyOwner: TestAssetProxyOwnerContract;
+ let assetProxyOwnerWrapper: AssetProxyOwnerWrapper;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ owners = [accounts[0], accounts[1]];
+ authorized = accounts[2];
+ notOwner = accounts[3];
+ const initialOwner = accounts[0];
+ erc20Proxy = await MixinAuthorizableContract.deployFrom0xArtifactAsync(
+ artifacts.MixinAuthorizable,
+ provider,
+ txDefaults,
+ );
+ erc721Proxy = await MixinAuthorizableContract.deployFrom0xArtifactAsync(
+ artifacts.MixinAuthorizable,
+ provider,
+ txDefaults,
+ );
+ const defaultAssetProxyContractAddresses: string[] = [];
+ testAssetProxyOwner = await TestAssetProxyOwnerContract.deployFrom0xArtifactAsync(
+ artifacts.TestAssetProxyOwner,
+ provider,
+ txDefaults,
+ owners,
+ defaultAssetProxyContractAddresses,
+ REQUIRED_APPROVALS,
+ SECONDS_TIME_LOCKED,
+ );
+ assetProxyOwnerWrapper = new AssetProxyOwnerWrapper(testAssetProxyOwner, provider);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20Proxy.transferOwnership.sendTransactionAsync(testAssetProxyOwner.address, {
+ from: initialOwner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc721Proxy.transferOwnership.sendTransactionAsync(testAssetProxyOwner.address, {
+ from: initialOwner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+
+ describe('constructor', () => {
+ it('should register passed in assetProxyContracts', async () => {
+ const assetProxyContractAddresses = [erc20Proxy.address, erc721Proxy.address];
+ const newMultiSig = await AssetProxyOwnerContract.deployFrom0xArtifactAsync(
+ artifacts.AssetProxyOwner,
+ provider,
+ txDefaults,
+ owners,
+ assetProxyContractAddresses,
+ REQUIRED_APPROVALS,
+ SECONDS_TIME_LOCKED,
+ );
+ const isErc20ProxyRegistered = await newMultiSig.isAssetProxyRegistered.callAsync(erc20Proxy.address);
+ const isErc721ProxyRegistered = await newMultiSig.isAssetProxyRegistered.callAsync(erc721Proxy.address);
+ expect(isErc20ProxyRegistered).to.equal(true);
+ expect(isErc721ProxyRegistered).to.equal(true);
+ });
+ it('should throw if a null address is included in assetProxyContracts', async () => {
+ const assetProxyContractAddresses = [erc20Proxy.address, constants.NULL_ADDRESS];
+ return expectContractCreationFailedAsync(
+ (AssetProxyOwnerContract.deployFrom0xArtifactAsync(
+ artifacts.AssetProxyOwner,
+ provider,
+ txDefaults,
+ owners,
+ assetProxyContractAddresses,
+ REQUIRED_APPROVALS,
+ SECONDS_TIME_LOCKED,
+ ) as any) as sendTransactionResult,
+ RevertReason.InvalidAssetProxy,
+ );
+ });
+ });
+
+ describe('isFunctionRemoveAuthorizedAddressAtIndex', () => {
+ it('should return false if data is not for removeAuthorizedAddressAtIndex', async () => {
+ const notRemoveAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData(
+ owners[0],
+ );
+
+ const isFunctionRemoveAuthorizedAddressAtIndex = await testAssetProxyOwner.isFunctionRemoveAuthorizedAddressAtIndex.callAsync(
+ notRemoveAuthorizedAddressData,
+ );
+ expect(isFunctionRemoveAuthorizedAddressAtIndex).to.be.false();
+ });
+
+ it('should return true if data is for removeAuthorizedAddressAtIndex', async () => {
+ const index = new BigNumber(0);
+ const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
+ owners[0],
+ index,
+ );
+ const isFunctionRemoveAuthorizedAddressAtIndex = await testAssetProxyOwner.isFunctionRemoveAuthorizedAddressAtIndex.callAsync(
+ removeAuthorizedAddressAtIndexData,
+ );
+ expect(isFunctionRemoveAuthorizedAddressAtIndex).to.be.true();
+ });
+ });
+
+ describe('registerAssetProxy', () => {
+ it('should throw if not called by multisig', async () => {
+ const isRegistered = true;
+ return expectTransactionFailedWithoutReasonAsync(
+ testAssetProxyOwner.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, isRegistered, {
+ from: owners[0],
+ }),
+ );
+ });
+
+ it('should register an address if called by multisig after timelock', async () => {
+ const addressToRegister = erc20Proxy.address;
+ const isRegistered = true;
+ const registerAssetProxyData = testAssetProxyOwner.registerAssetProxy.getABIEncodedTransactionData(
+ addressToRegister,
+ isRegistered,
+ );
+ const submitTxRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ testAssetProxyOwner.address,
+ registerAssetProxyData,
+ owners[0],
+ );
+
+ const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = log.args.transactionId;
+
+ await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]);
+ await increaseTimeAndMineBlockAsync(SECONDS_TIME_LOCKED.toNumber());
+
+ const executeTxRes = await assetProxyOwnerWrapper.executeTransactionAsync(txId, owners[0]);
+ const registerLog = executeTxRes.logs[0] as LogWithDecodedArgs<
+ AssetProxyOwnerAssetProxyRegistrationEventArgs
+ >;
+ expect(registerLog.args.assetProxyContract).to.equal(addressToRegister);
+ expect(registerLog.args.isRegistered).to.equal(isRegistered);
+
+ const isAssetProxyRegistered = await testAssetProxyOwner.isAssetProxyRegistered.callAsync(
+ addressToRegister,
+ );
+ expect(isAssetProxyRegistered).to.equal(isRegistered);
+ });
+
+ it('should fail if registering a null address', async () => {
+ const addressToRegister = constants.NULL_ADDRESS;
+ const isRegistered = true;
+ const registerAssetProxyData = testAssetProxyOwner.registerAssetProxy.getABIEncodedTransactionData(
+ addressToRegister,
+ isRegistered,
+ );
+ const submitTxRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ testAssetProxyOwner.address,
+ registerAssetProxyData,
+ owners[0],
+ );
+ const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = log.args.transactionId;
+
+ await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]);
+ await increaseTimeAndMineBlockAsync(SECONDS_TIME_LOCKED.toNumber());
+
+ const executeTxRes = await assetProxyOwnerWrapper.executeTransactionAsync(txId, owners[0]);
+ const failureLog = executeTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerExecutionFailureEventArgs>;
+ expect(failureLog.args.transactionId).to.be.bignumber.equal(txId);
+
+ const isAssetProxyRegistered = await testAssetProxyOwner.isAssetProxyRegistered.callAsync(
+ addressToRegister,
+ );
+ expect(isAssetProxyRegistered).to.equal(false);
+ });
+ });
+
+ describe('Calling removeAuthorizedAddressAtIndex', () => {
+ const erc20Index = new BigNumber(0);
+ const erc721Index = new BigNumber(1);
+ before('authorize both proxies and register erc20 proxy', async () => {
+ // Only register ERC20 proxy
+ const addressToRegister = erc20Proxy.address;
+ const isRegistered = true;
+ const registerAssetProxyData = testAssetProxyOwner.registerAssetProxy.getABIEncodedTransactionData(
+ addressToRegister,
+ isRegistered,
+ );
+ const registerAssetProxySubmitRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ testAssetProxyOwner.address,
+ registerAssetProxyData,
+ owners[0],
+ );
+ const registerAssetProxySubmitLog = registerAssetProxySubmitRes.logs[0] as LogWithDecodedArgs<
+ AssetProxyOwnerSubmissionEventArgs
+ >;
+
+ const addAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData(authorized);
+ const erc20AddAuthorizedAddressSubmitRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc20Proxy.address,
+ addAuthorizedAddressData,
+ owners[0],
+ );
+ const erc721AddAuthorizedAddressSubmitRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc721Proxy.address,
+ addAuthorizedAddressData,
+ owners[0],
+ );
+ const erc20AddAuthorizedAddressSubmitLog = erc20AddAuthorizedAddressSubmitRes.logs[0] as LogWithDecodedArgs<
+ AssetProxyOwnerSubmissionEventArgs
+ >;
+ const erc721AddAuthorizedAddressSubmitLog = erc721AddAuthorizedAddressSubmitRes
+ .logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+
+ const registerAssetProxyTxId = registerAssetProxySubmitLog.args.transactionId;
+ const erc20AddAuthorizedAddressTxId = erc20AddAuthorizedAddressSubmitLog.args.transactionId;
+ const erc721AddAuthorizedAddressTxId = erc721AddAuthorizedAddressSubmitLog.args.transactionId;
+
+ await assetProxyOwnerWrapper.confirmTransactionAsync(registerAssetProxyTxId, owners[1]);
+ await assetProxyOwnerWrapper.confirmTransactionAsync(erc20AddAuthorizedAddressTxId, owners[1]);
+ await assetProxyOwnerWrapper.confirmTransactionAsync(erc721AddAuthorizedAddressTxId, owners[1]);
+ await increaseTimeAndMineBlockAsync(SECONDS_TIME_LOCKED.toNumber());
+ await assetProxyOwnerWrapper.executeTransactionAsync(registerAssetProxyTxId, owners[0]);
+ await assetProxyOwnerWrapper.executeTransactionAsync(erc20AddAuthorizedAddressTxId, owners[0], {
+ gas: constants.MAX_EXECUTE_TRANSACTION_GAS,
+ });
+ await assetProxyOwnerWrapper.executeTransactionAsync(erc721AddAuthorizedAddressTxId, owners[0], {
+ gas: constants.MAX_EXECUTE_TRANSACTION_GAS,
+ });
+ });
+
+ describe('validRemoveAuthorizedAddressAtIndexTx', () => {
+ it('should revert if data is not for removeAuthorizedAddressAtIndex and proxy is registered', async () => {
+ const notRemoveAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData(
+ authorized,
+ );
+ const submitTxRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc20Proxy.address,
+ notRemoveAuthorizedAddressData,
+ owners[0],
+ );
+ const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = log.args.transactionId;
+ return expectContractCallFailedAsync(
+ testAssetProxyOwner.testValidRemoveAuthorizedAddressAtIndexTx.callAsync(txId),
+ RevertReason.InvalidFunctionSelector,
+ );
+ });
+
+ it('should return true if data is for removeAuthorizedAddressAtIndex and proxy is registered', async () => {
+ const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
+ authorized,
+ erc20Index,
+ );
+ const submitTxRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc20Proxy.address,
+ removeAuthorizedAddressAtIndexData,
+ owners[0],
+ );
+ const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = log.args.transactionId;
+ const isValidRemoveAuthorizedAddressAtIndexTx = await testAssetProxyOwner.testValidRemoveAuthorizedAddressAtIndexTx.callAsync(
+ txId,
+ );
+ expect(isValidRemoveAuthorizedAddressAtIndexTx).to.be.true();
+ });
+
+ it('should revert if data is for removeAuthorizedAddressAtIndex and proxy is not registered', async () => {
+ const removeAuthorizedAddressAtIndexData = erc721Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
+ authorized,
+ erc721Index,
+ );
+ const submitTxRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc721Proxy.address,
+ removeAuthorizedAddressAtIndexData,
+ owners[0],
+ );
+ const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = log.args.transactionId;
+ return expectContractCallFailedAsync(
+ testAssetProxyOwner.testValidRemoveAuthorizedAddressAtIndexTx.callAsync(txId),
+ RevertReason.UnregisteredAssetProxy,
+ );
+ });
+ });
+
+ describe('executeRemoveAuthorizedAddressAtIndex', () => {
+ it('should throw without the required confirmations', async () => {
+ const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
+ authorized,
+ erc20Index,
+ );
+ const res = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc20Proxy.address,
+ removeAuthorizedAddressAtIndexData,
+ owners[0],
+ );
+ const log = res.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = log.args.transactionId;
+
+ return expectTransactionFailedAsync(
+ testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, {
+ from: owners[1],
+ }),
+ RevertReason.TxNotFullyConfirmed,
+ );
+ });
+
+ it('should throw if tx destination is not registered', async () => {
+ const removeAuthorizedAddressAtIndexData = erc721Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
+ authorized,
+ erc721Index,
+ );
+ const res = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc721Proxy.address,
+ removeAuthorizedAddressAtIndexData,
+ owners[0],
+ );
+ const log = res.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = log.args.transactionId;
+
+ await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]);
+
+ return expectTransactionFailedAsync(
+ testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, {
+ from: owners[1],
+ }),
+ RevertReason.UnregisteredAssetProxy,
+ );
+ });
+
+ it('should throw if tx data is not for removeAuthorizedAddressAtIndex', async () => {
+ const newAuthorized = owners[1];
+ const addAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData(
+ newAuthorized,
+ );
+ const res = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc20Proxy.address,
+ addAuthorizedAddressData,
+ owners[0],
+ );
+ const log = res.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = log.args.transactionId;
+
+ await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]);
+
+ return expectTransactionFailedAsync(
+ testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, {
+ from: owners[1],
+ }),
+ RevertReason.InvalidFunctionSelector,
+ );
+ });
+
+ it('should execute removeAuthorizedAddressAtIndex for registered address if fully confirmed and called by owner', async () => {
+ const isAuthorizedBefore = await erc20Proxy.authorized.callAsync(authorized);
+ expect(isAuthorizedBefore).to.equal(true);
+
+ const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
+ authorized,
+ erc20Index,
+ );
+ const submitRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc20Proxy.address,
+ removeAuthorizedAddressAtIndexData,
+ owners[0],
+ );
+ const submitLog = submitRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = submitLog.args.transactionId;
+
+ await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]);
+
+ const execRes = await assetProxyOwnerWrapper.executeRemoveAuthorizedAddressAtIndexAsync(
+ txId,
+ owners[0],
+ );
+ const execLog = execRes.logs[1] as LogWithDecodedArgs<AssetProxyOwnerExecutionEventArgs>;
+ expect(execLog.args.transactionId).to.be.bignumber.equal(txId);
+
+ const tx = await testAssetProxyOwner.transactions.callAsync(txId);
+ const isExecuted = tx[3];
+ expect(isExecuted).to.equal(true);
+
+ const isAuthorizedAfter = await erc20Proxy.authorized.callAsync(authorized);
+ expect(isAuthorizedAfter).to.equal(false);
+ });
+
+ it('should execute removeAuthorizedAddressAtIndex for registered address if fully confirmed and called by non-owner', async () => {
+ const isAuthorizedBefore = await erc20Proxy.authorized.callAsync(authorized);
+ expect(isAuthorizedBefore).to.equal(true);
+
+ const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
+ authorized,
+ erc20Index,
+ );
+ const submitRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc20Proxy.address,
+ removeAuthorizedAddressAtIndexData,
+ owners[0],
+ );
+ const submitLog = submitRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = submitLog.args.transactionId;
+
+ await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]);
+
+ const execRes = await assetProxyOwnerWrapper.executeRemoveAuthorizedAddressAtIndexAsync(txId, notOwner);
+ const execLog = execRes.logs[1] as LogWithDecodedArgs<AssetProxyOwnerExecutionEventArgs>;
+ expect(execLog.args.transactionId).to.be.bignumber.equal(txId);
+
+ const tx = await testAssetProxyOwner.transactions.callAsync(txId);
+ const isExecuted = tx[3];
+ expect(isExecuted).to.equal(true);
+
+ const isAuthorizedAfter = await erc20Proxy.authorized.callAsync(authorized);
+ expect(isAuthorizedAfter).to.equal(false);
+ });
+
+ it('should throw if already executed', async () => {
+ const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
+ authorized,
+ erc20Index,
+ );
+ const submitRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc20Proxy.address,
+ removeAuthorizedAddressAtIndexData,
+ owners[0],
+ );
+ const submitLog = submitRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = submitLog.args.transactionId;
+
+ await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]);
+
+ const execRes = await assetProxyOwnerWrapper.executeRemoveAuthorizedAddressAtIndexAsync(
+ txId,
+ owners[0],
+ );
+ const execLog = execRes.logs[1] as LogWithDecodedArgs<AssetProxyOwnerExecutionEventArgs>;
+ expect(execLog.args.transactionId).to.be.bignumber.equal(txId);
+
+ const tx = await testAssetProxyOwner.transactions.callAsync(txId);
+ const isExecuted = tx[3];
+ expect(isExecuted).to.equal(true);
+
+ return expectTransactionFailedWithoutReasonAsync(
+ testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, {
+ from: owners[1],
+ }),
+ );
+ });
+ });
+ });
+});
+// tslint:disable-line max-file-line-count
diff --git a/contracts/protocol/test/utils/asset_proxy_owner_wrapper.ts b/contracts/protocol/test/utils/asset_proxy_owner_wrapper.ts
new file mode 100644
index 000000000..df23658d8
--- /dev/null
+++ b/contracts/protocol/test/utils/asset_proxy_owner_wrapper.ts
@@ -0,0 +1,70 @@
+import { LogDecoder } from '@0x/contracts-test-utils';
+import { artifacts as tokensArtifacts } from '@0x/contracts-tokens';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import { Provider, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
+import * as _ from 'lodash';
+
+import { AssetProxyOwnerContract } from '../../generated-wrappers/asset_proxy_owner';
+import { artifacts } from '../../src/artifacts';
+
+export class AssetProxyOwnerWrapper {
+ private readonly _assetProxyOwner: AssetProxyOwnerContract;
+ private readonly _web3Wrapper: Web3Wrapper;
+ private readonly _logDecoder: LogDecoder;
+ constructor(assetproxyOwnerContract: AssetProxyOwnerContract, provider: Provider) {
+ this._assetProxyOwner = assetproxyOwnerContract;
+ this._web3Wrapper = new Web3Wrapper(provider);
+ this._logDecoder = new LogDecoder(this._web3Wrapper, { ...artifacts, ...tokensArtifacts });
+ }
+ public async submitTransactionAsync(
+ destination: string,
+ data: string,
+ from: string,
+ opts: { value?: BigNumber } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const value = _.isUndefined(opts.value) ? new BigNumber(0) : opts.value;
+ const txHash = await this._assetProxyOwner.submitTransaction.sendTransactionAsync(destination, value, data, {
+ from,
+ });
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async confirmTransactionAsync(txId: BigNumber, from: string): Promise<TransactionReceiptWithDecodedLogs> {
+ const txHash = await this._assetProxyOwner.confirmTransaction.sendTransactionAsync(txId, { from });
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async revokeConfirmationAsync(txId: BigNumber, from: string): Promise<TransactionReceiptWithDecodedLogs> {
+ const txHash = await this._assetProxyOwner.revokeConfirmation.sendTransactionAsync(txId, { from });
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async executeTransactionAsync(
+ txId: BigNumber,
+ from: string,
+ opts: { gas?: number } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const txHash = await this._assetProxyOwner.executeTransaction.sendTransactionAsync(txId, {
+ from,
+ gas: opts.gas,
+ });
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async executeRemoveAuthorizedAddressAtIndexAsync(
+ txId: BigNumber,
+ from: string,
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ // tslint:disable-next-line:no-unnecessary-type-assertion
+ const txHash = await (this
+ ._assetProxyOwner as AssetProxyOwnerContract).executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(
+ txId,
+ {
+ from,
+ },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+}
diff --git a/contracts/protocol/test/utils/asset_wrapper.ts b/contracts/protocol/test/utils/asset_wrapper.ts
new file mode 100644
index 000000000..e4090ad74
--- /dev/null
+++ b/contracts/protocol/test/utils/asset_wrapper.ts
@@ -0,0 +1,222 @@
+import { AbstractAssetWrapper, constants } from '@0x/contracts-test-utils';
+import { assetDataUtils } from '@0x/order-utils';
+import { AssetProxyId } from '@0x/types';
+import { BigNumber, errorUtils } from '@0x/utils';
+import * as _ from 'lodash';
+
+import { ERC20Wrapper } from './erc20_wrapper';
+import { ERC721Wrapper } from './erc721_wrapper';
+
+interface ProxyIdToAssetWrappers {
+ [proxyId: string]: AbstractAssetWrapper;
+}
+
+/**
+ * This class abstracts away the differences between ERC20 and ERC721 tokens so that
+ * the logic that uses it does not need to care what standard a token belongs to.
+ */
+export class AssetWrapper {
+ private readonly _proxyIdToAssetWrappers: ProxyIdToAssetWrappers;
+ constructor(assetWrappers: AbstractAssetWrapper[]) {
+ this._proxyIdToAssetWrappers = {};
+ _.each(assetWrappers, assetWrapper => {
+ const proxyId = assetWrapper.getProxyId();
+ this._proxyIdToAssetWrappers[proxyId] = assetWrapper;
+ });
+ }
+ public async getBalanceAsync(userAddress: string, assetData: string): Promise<BigNumber> {
+ const proxyId = assetDataUtils.decodeAssetProxyId(assetData);
+ switch (proxyId) {
+ case AssetProxyId.ERC20: {
+ const erc20Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC20Wrapper;
+ const balance = await erc20Wrapper.getBalanceAsync(userAddress, assetData);
+ return balance;
+ }
+ case AssetProxyId.ERC721: {
+ const assetWrapper = this._proxyIdToAssetWrappers[proxyId] as ERC721Wrapper;
+ const assetProxyData = assetDataUtils.decodeERC721AssetData(assetData);
+ const isOwner = await assetWrapper.isOwnerAsync(
+ userAddress,
+ assetProxyData.tokenAddress,
+ assetProxyData.tokenId,
+ );
+ const balance = isOwner ? new BigNumber(1) : new BigNumber(0);
+ return balance;
+ }
+ default:
+ throw errorUtils.spawnSwitchErr('proxyId', proxyId);
+ }
+ }
+ public async setBalanceAsync(userAddress: string, assetData: string, desiredBalance: BigNumber): Promise<void> {
+ const proxyId = assetDataUtils.decodeAssetProxyId(assetData);
+ switch (proxyId) {
+ case AssetProxyId.ERC20: {
+ const erc20Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC20Wrapper;
+ await erc20Wrapper.setBalanceAsync(userAddress, assetData, desiredBalance);
+ return;
+ }
+ case AssetProxyId.ERC721: {
+ if (!desiredBalance.eq(0) && !desiredBalance.eq(1)) {
+ throw new Error(`Balance for ERC721 token can only be set to 0 or 1. Got: ${desiredBalance}`);
+ }
+ const erc721Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC721Wrapper;
+ const assetProxyData = assetDataUtils.decodeERC721AssetData(assetData);
+ const doesTokenExist = erc721Wrapper.doesTokenExistAsync(
+ assetProxyData.tokenAddress,
+ assetProxyData.tokenId,
+ );
+ if (!doesTokenExist && desiredBalance.eq(1)) {
+ await erc721Wrapper.mintAsync(assetProxyData.tokenAddress, assetProxyData.tokenId, userAddress);
+ return;
+ } else if (!doesTokenExist && desiredBalance.eq(0)) {
+ return; // noop
+ }
+ const tokenOwner = await erc721Wrapper.ownerOfAsync(
+ assetProxyData.tokenAddress,
+ assetProxyData.tokenId,
+ );
+ if (userAddress !== tokenOwner && desiredBalance.eq(1)) {
+ await erc721Wrapper.transferFromAsync(
+ assetProxyData.tokenAddress,
+ assetProxyData.tokenId,
+ tokenOwner,
+ userAddress,
+ );
+ } else if (tokenOwner === userAddress && desiredBalance.eq(0)) {
+ // Transfer token to someone else
+ const userAddresses = await (erc721Wrapper as any)._web3Wrapper.getAvailableAddressesAsync();
+ const nonOwner = _.find(userAddresses, a => a !== userAddress);
+ await erc721Wrapper.transferFromAsync(
+ assetProxyData.tokenAddress,
+ assetProxyData.tokenId,
+ tokenOwner,
+ nonOwner,
+ );
+ return;
+ } else if (
+ (userAddress !== tokenOwner && desiredBalance.eq(0)) ||
+ (tokenOwner === userAddress && desiredBalance.eq(1))
+ ) {
+ return; // noop
+ }
+ break;
+ }
+ default:
+ throw errorUtils.spawnSwitchErr('proxyId', proxyId);
+ }
+ }
+ public async getProxyAllowanceAsync(userAddress: string, assetData: string): Promise<BigNumber> {
+ const proxyId = assetDataUtils.decodeAssetProxyId(assetData);
+ switch (proxyId) {
+ case AssetProxyId.ERC20: {
+ const erc20Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC20Wrapper;
+ const allowance = await erc20Wrapper.getProxyAllowanceAsync(userAddress, assetData);
+ return allowance;
+ }
+ case AssetProxyId.ERC721: {
+ const assetWrapper = this._proxyIdToAssetWrappers[proxyId] as ERC721Wrapper;
+ const erc721ProxyData = assetDataUtils.decodeERC721AssetData(assetData);
+ const isProxyApprovedForAll = await assetWrapper.isProxyApprovedForAllAsync(
+ userAddress,
+ erc721ProxyData.tokenAddress,
+ );
+ if (isProxyApprovedForAll) {
+ return constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS;
+ }
+
+ const isProxyApproved = await assetWrapper.isProxyApprovedAsync(
+ erc721ProxyData.tokenAddress,
+ erc721ProxyData.tokenId,
+ );
+ const allowance = isProxyApproved ? new BigNumber(1) : new BigNumber(0);
+ return allowance;
+ }
+ default:
+ throw errorUtils.spawnSwitchErr('proxyId', proxyId);
+ }
+ }
+ public async setProxyAllowanceAsync(
+ userAddress: string,
+ assetData: string,
+ desiredAllowance: BigNumber,
+ ): Promise<void> {
+ const proxyId = assetDataUtils.decodeAssetProxyId(assetData);
+ switch (proxyId) {
+ case AssetProxyId.ERC20: {
+ const erc20Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC20Wrapper;
+ await erc20Wrapper.setAllowanceAsync(userAddress, assetData, desiredAllowance);
+ return;
+ }
+ case AssetProxyId.ERC721: {
+ if (
+ !desiredAllowance.eq(0) &&
+ !desiredAllowance.eq(1) &&
+ !desiredAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)
+ ) {
+ throw new Error(
+ `Allowance for ERC721 token can only be set to 0, 1 or 2^256-1. Got: ${desiredAllowance}`,
+ );
+ }
+ const erc721Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC721Wrapper;
+ const assetProxyData = assetDataUtils.decodeERC721AssetData(assetData);
+
+ const doesTokenExist = await erc721Wrapper.doesTokenExistAsync(
+ assetProxyData.tokenAddress,
+ assetProxyData.tokenId,
+ );
+ if (!doesTokenExist) {
+ throw new Error(
+ `Cannot setProxyAllowance on non-existent token: ${assetProxyData.tokenAddress} ${
+ assetProxyData.tokenId
+ }`,
+ );
+ }
+ const isProxyApprovedForAll = await erc721Wrapper.isProxyApprovedForAllAsync(
+ userAddress,
+ assetProxyData.tokenAddress,
+ );
+ if (!isProxyApprovedForAll && desiredAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) {
+ const isApproved = true;
+ await erc721Wrapper.approveProxyForAllAsync(
+ assetProxyData.tokenAddress,
+ assetProxyData.tokenId,
+ isApproved,
+ );
+ } else if (isProxyApprovedForAll && desiredAllowance.eq(0)) {
+ const isApproved = false;
+ await erc721Wrapper.approveProxyForAllAsync(
+ assetProxyData.tokenAddress,
+ assetProxyData.tokenId,
+ isApproved,
+ );
+ } else if (isProxyApprovedForAll && desiredAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) {
+ return; // Noop
+ }
+
+ const isProxyApproved = await erc721Wrapper.isProxyApprovedAsync(
+ assetProxyData.tokenAddress,
+ assetProxyData.tokenId,
+ );
+ if (!isProxyApproved && desiredAllowance.eq(1)) {
+ await erc721Wrapper.approveProxyAsync(assetProxyData.tokenAddress, assetProxyData.tokenId);
+ } else if (isProxyApproved && desiredAllowance.eq(0)) {
+ // Remove approval
+ await erc721Wrapper.approveAsync(
+ constants.NULL_ADDRESS,
+ assetProxyData.tokenAddress,
+ assetProxyData.tokenId,
+ );
+ } else if (
+ (!isProxyApproved && desiredAllowance.eq(0)) ||
+ (isProxyApproved && desiredAllowance.eq(1))
+ ) {
+ return; // noop
+ }
+
+ break;
+ }
+ default:
+ throw errorUtils.spawnSwitchErr('proxyId', proxyId);
+ }
+ }
+}
diff --git a/contracts/protocol/test/utils/erc20_wrapper.ts b/contracts/protocol/test/utils/erc20_wrapper.ts
new file mode 100644
index 000000000..e5ab0e36c
--- /dev/null
+++ b/contracts/protocol/test/utils/erc20_wrapper.ts
@@ -0,0 +1,179 @@
+import { constants, ERC20BalancesByOwner, txDefaults } from '@0x/contracts-test-utils';
+import { artifacts as tokensArtifacts, DummyERC20TokenContract } from '@0x/contracts-tokens';
+import { assetDataUtils } from '@0x/order-utils';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import { Provider } from 'ethereum-types';
+import * as _ from 'lodash';
+
+import { ERC20ProxyContract } from '../../generated-wrappers/erc20_proxy';
+import { artifacts } from '../../src/artifacts';
+
+export class ERC20Wrapper {
+ private readonly _tokenOwnerAddresses: string[];
+ private readonly _contractOwnerAddress: string;
+ private readonly _web3Wrapper: Web3Wrapper;
+ private readonly _provider: Provider;
+ private readonly _dummyTokenContracts: DummyERC20TokenContract[];
+ private _proxyContract?: ERC20ProxyContract;
+ private _proxyIdIfExists?: string;
+ /**
+ * Instanitates an ERC20Wrapper
+ * @param provider Web3 provider to use for all JSON RPC requests
+ * @param tokenOwnerAddresses Addresses that we want to endow as owners for dummy ERC20 tokens
+ * @param contractOwnerAddress Desired owner of the contract
+ * Instance of ERC20Wrapper
+ */
+ constructor(provider: Provider, tokenOwnerAddresses: string[], contractOwnerAddress: string) {
+ this._dummyTokenContracts = [];
+ this._web3Wrapper = new Web3Wrapper(provider);
+ this._provider = provider;
+ this._tokenOwnerAddresses = tokenOwnerAddresses;
+ this._contractOwnerAddress = contractOwnerAddress;
+ }
+ public async deployDummyTokensAsync(
+ numberToDeploy: number,
+ decimals: BigNumber,
+ ): Promise<DummyERC20TokenContract[]> {
+ for (let i = 0; i < numberToDeploy; i++) {
+ this._dummyTokenContracts.push(
+ await DummyERC20TokenContract.deployFrom0xArtifactAsync(
+ tokensArtifacts.DummyERC20Token,
+ this._provider,
+ txDefaults,
+ constants.DUMMY_TOKEN_NAME,
+ constants.DUMMY_TOKEN_SYMBOL,
+ decimals,
+ constants.DUMMY_TOKEN_TOTAL_SUPPLY,
+ ),
+ );
+ }
+ return this._dummyTokenContracts;
+ }
+ public async deployProxyAsync(): Promise<ERC20ProxyContract> {
+ this._proxyContract = await ERC20ProxyContract.deployFrom0xArtifactAsync(
+ artifacts.ERC20Proxy,
+ this._provider,
+ txDefaults,
+ );
+ this._proxyIdIfExists = await this._proxyContract.getProxyId.callAsync();
+ return this._proxyContract;
+ }
+ public getProxyId(): string {
+ this._validateProxyContractExistsOrThrow();
+ return this._proxyIdIfExists as string;
+ }
+ public async setBalancesAndAllowancesAsync(): Promise<void> {
+ this._validateDummyTokenContractsExistOrThrow();
+ this._validateProxyContractExistsOrThrow();
+ for (const dummyTokenContract of this._dummyTokenContracts) {
+ for (const tokenOwnerAddress of this._tokenOwnerAddresses) {
+ await this._web3Wrapper.awaitTransactionSuccessAsync(
+ await dummyTokenContract.setBalance.sendTransactionAsync(
+ tokenOwnerAddress,
+ constants.INITIAL_ERC20_BALANCE,
+ { from: this._contractOwnerAddress },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await this._web3Wrapper.awaitTransactionSuccessAsync(
+ await dummyTokenContract.approve.sendTransactionAsync(
+ (this._proxyContract as ERC20ProxyContract).address,
+ constants.INITIAL_ERC20_ALLOWANCE,
+ { from: tokenOwnerAddress },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ }
+ }
+ }
+ public async getBalanceAsync(userAddress: string, assetData: string): Promise<BigNumber> {
+ const tokenContract = this._getTokenContractFromAssetData(assetData);
+ const balance = new BigNumber(await tokenContract.balanceOf.callAsync(userAddress));
+ return balance;
+ }
+ public async setBalanceAsync(userAddress: string, assetData: string, amount: BigNumber): Promise<void> {
+ const tokenContract = this._getTokenContractFromAssetData(assetData);
+ await this._web3Wrapper.awaitTransactionSuccessAsync(
+ await tokenContract.setBalance.sendTransactionAsync(userAddress, amount, {
+ from: this._contractOwnerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ }
+ public async getProxyAllowanceAsync(userAddress: string, assetData: string): Promise<BigNumber> {
+ const tokenContract = this._getTokenContractFromAssetData(assetData);
+ const proxyAddress = (this._proxyContract as ERC20ProxyContract).address;
+ const allowance = new BigNumber(await tokenContract.allowance.callAsync(userAddress, proxyAddress));
+ return allowance;
+ }
+ public async setAllowanceAsync(userAddress: string, assetData: string, amount: BigNumber): Promise<void> {
+ const tokenContract = this._getTokenContractFromAssetData(assetData);
+ const proxyAddress = (this._proxyContract as ERC20ProxyContract).address;
+ await this._web3Wrapper.awaitTransactionSuccessAsync(
+ await tokenContract.approve.sendTransactionAsync(proxyAddress, amount, {
+ from: userAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ }
+ public async getBalancesAsync(): Promise<ERC20BalancesByOwner> {
+ this._validateDummyTokenContractsExistOrThrow();
+ const balancesByOwner: ERC20BalancesByOwner = {};
+ const balances: BigNumber[] = [];
+ const balanceInfo: Array<{ tokenOwnerAddress: string; tokenAddress: string }> = [];
+ for (const dummyTokenContract of this._dummyTokenContracts) {
+ for (const tokenOwnerAddress of this._tokenOwnerAddresses) {
+ balances.push(await dummyTokenContract.balanceOf.callAsync(tokenOwnerAddress));
+ balanceInfo.push({
+ tokenOwnerAddress,
+ tokenAddress: dummyTokenContract.address,
+ });
+ }
+ }
+ _.forEach(balances, (balance, balanceIndex) => {
+ const tokenAddress = balanceInfo[balanceIndex].tokenAddress;
+ const tokenOwnerAddress = balanceInfo[balanceIndex].tokenOwnerAddress;
+ if (_.isUndefined(balancesByOwner[tokenOwnerAddress])) {
+ balancesByOwner[tokenOwnerAddress] = {};
+ }
+ const wrappedBalance = new BigNumber(balance);
+ balancesByOwner[tokenOwnerAddress][tokenAddress] = wrappedBalance;
+ });
+ return balancesByOwner;
+ }
+ public addDummyTokenContract(dummy: DummyERC20TokenContract): void {
+ if (!_.isUndefined(this._dummyTokenContracts)) {
+ this._dummyTokenContracts.push(dummy);
+ }
+ }
+ public addTokenOwnerAddress(address: string): void {
+ this._tokenOwnerAddresses.push(address);
+ }
+ public getTokenOwnerAddresses(): string[] {
+ return this._tokenOwnerAddresses;
+ }
+ public getTokenAddresses(): string[] {
+ const tokenAddresses = _.map(this._dummyTokenContracts, dummyTokenContract => dummyTokenContract.address);
+ return tokenAddresses;
+ }
+ private _getTokenContractFromAssetData(assetData: string): DummyERC20TokenContract {
+ const erc20ProxyData = assetDataUtils.decodeERC20AssetData(assetData);
+ const tokenAddress = erc20ProxyData.tokenAddress;
+ const tokenContractIfExists = _.find(this._dummyTokenContracts, c => c.address === tokenAddress);
+ if (_.isUndefined(tokenContractIfExists)) {
+ throw new Error(`Token: ${tokenAddress} was not deployed through ERC20Wrapper`);
+ }
+ return tokenContractIfExists;
+ }
+ private _validateDummyTokenContractsExistOrThrow(): void {
+ if (_.isUndefined(this._dummyTokenContracts)) {
+ throw new Error('Dummy ERC20 tokens not yet deployed, please call "deployDummyTokensAsync"');
+ }
+ }
+ private _validateProxyContractExistsOrThrow(): void {
+ if (_.isUndefined(this._proxyContract)) {
+ throw new Error('ERC20 proxy contract not yet deployed, please call "deployProxyAsync"');
+ }
+ }
+}
diff --git a/contracts/protocol/test/utils/erc721_wrapper.ts b/contracts/protocol/test/utils/erc721_wrapper.ts
new file mode 100644
index 000000000..1f658ce86
--- /dev/null
+++ b/contracts/protocol/test/utils/erc721_wrapper.ts
@@ -0,0 +1,236 @@
+import { constants, ERC721TokenIdsByOwner, txDefaults } from '@0x/contracts-test-utils';
+import { artifacts as tokensArtifacts, DummyERC721TokenContract } from '@0x/contracts-tokens';
+import { generatePseudoRandomSalt } from '@0x/order-utils';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import { Provider } from 'ethereum-types';
+import * as _ from 'lodash';
+
+import { ERC721ProxyContract } from '../../generated-wrappers/erc721_proxy';
+import { artifacts } from '../../src/artifacts';
+
+export class ERC721Wrapper {
+ private readonly _tokenOwnerAddresses: string[];
+ private readonly _contractOwnerAddress: string;
+ private readonly _web3Wrapper: Web3Wrapper;
+ private readonly _provider: Provider;
+ private readonly _dummyTokenContracts: DummyERC721TokenContract[];
+ private _proxyContract?: ERC721ProxyContract;
+ private _proxyIdIfExists?: string;
+ private _initialTokenIdsByOwner: ERC721TokenIdsByOwner = {};
+ constructor(provider: Provider, tokenOwnerAddresses: string[], contractOwnerAddress: string) {
+ this._web3Wrapper = new Web3Wrapper(provider);
+ this._provider = provider;
+ this._dummyTokenContracts = [];
+ this._tokenOwnerAddresses = tokenOwnerAddresses;
+ this._contractOwnerAddress = contractOwnerAddress;
+ }
+ public async deployDummyTokensAsync(): Promise<DummyERC721TokenContract[]> {
+ // tslint:disable-next-line:no-unused-variable
+ for (const i of _.times(constants.NUM_DUMMY_ERC721_TO_DEPLOY)) {
+ this._dummyTokenContracts.push(
+ await DummyERC721TokenContract.deployFrom0xArtifactAsync(
+ tokensArtifacts.DummyERC721Token,
+ this._provider,
+ txDefaults,
+ constants.DUMMY_TOKEN_NAME,
+ constants.DUMMY_TOKEN_SYMBOL,
+ ),
+ );
+ }
+ return this._dummyTokenContracts;
+ }
+ public async deployProxyAsync(): Promise<ERC721ProxyContract> {
+ this._proxyContract = await ERC721ProxyContract.deployFrom0xArtifactAsync(
+ artifacts.ERC721Proxy,
+ this._provider,
+ txDefaults,
+ );
+ this._proxyIdIfExists = await this._proxyContract.getProxyId.callAsync();
+ return this._proxyContract;
+ }
+ public getProxyId(): string {
+ this._validateProxyContractExistsOrThrow();
+ return this._proxyIdIfExists as string;
+ }
+ public async setBalancesAndAllowancesAsync(): Promise<void> {
+ this._validateDummyTokenContractsExistOrThrow();
+ this._validateProxyContractExistsOrThrow();
+ this._initialTokenIdsByOwner = {};
+ for (const dummyTokenContract of this._dummyTokenContracts) {
+ for (const tokenOwnerAddress of this._tokenOwnerAddresses) {
+ // tslint:disable-next-line:no-unused-variable
+ for (const i of _.times(constants.NUM_ERC721_TOKENS_TO_MINT)) {
+ const tokenId = generatePseudoRandomSalt();
+ await this.mintAsync(dummyTokenContract.address, tokenId, tokenOwnerAddress);
+ if (_.isUndefined(this._initialTokenIdsByOwner[tokenOwnerAddress])) {
+ this._initialTokenIdsByOwner[tokenOwnerAddress] = {
+ [dummyTokenContract.address]: [],
+ };
+ }
+ if (_.isUndefined(this._initialTokenIdsByOwner[tokenOwnerAddress][dummyTokenContract.address])) {
+ this._initialTokenIdsByOwner[tokenOwnerAddress][dummyTokenContract.address] = [];
+ }
+ this._initialTokenIdsByOwner[tokenOwnerAddress][dummyTokenContract.address].push(tokenId);
+
+ await this.approveProxyAsync(dummyTokenContract.address, tokenId);
+ }
+ }
+ }
+ }
+ public async doesTokenExistAsync(tokenAddress: string, tokenId: BigNumber): Promise<boolean> {
+ const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
+ const owner = await tokenContract.ownerOf.callAsync(tokenId);
+ const doesExist = owner !== constants.NULL_ADDRESS;
+ return doesExist;
+ }
+ public async approveProxyAsync(tokenAddress: string, tokenId: BigNumber): Promise<void> {
+ const proxyAddress = (this._proxyContract as ERC721ProxyContract).address;
+ await this.approveAsync(proxyAddress, tokenAddress, tokenId);
+ }
+ public async approveProxyForAllAsync(tokenAddress: string, tokenId: BigNumber, isApproved: boolean): Promise<void> {
+ const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
+ const tokenOwner = await this.ownerOfAsync(tokenAddress, tokenId);
+ const proxyAddress = (this._proxyContract as ERC721ProxyContract).address;
+ await this._web3Wrapper.awaitTransactionSuccessAsync(
+ await tokenContract.setApprovalForAll.sendTransactionAsync(proxyAddress, isApproved, {
+ from: tokenOwner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ }
+ public async approveAsync(to: string, tokenAddress: string, tokenId: BigNumber): Promise<void> {
+ const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
+ const tokenOwner = await this.ownerOfAsync(tokenAddress, tokenId);
+ await this._web3Wrapper.awaitTransactionSuccessAsync(
+ await tokenContract.approve.sendTransactionAsync(to, tokenId, {
+ from: tokenOwner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ }
+ public async transferFromAsync(
+ tokenAddress: string,
+ tokenId: BigNumber,
+ currentOwner: string,
+ userAddress: string,
+ ): Promise<void> {
+ const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
+ await this._web3Wrapper.awaitTransactionSuccessAsync(
+ await tokenContract.transferFrom.sendTransactionAsync(currentOwner, userAddress, tokenId, {
+ from: currentOwner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ }
+ public async mintAsync(tokenAddress: string, tokenId: BigNumber, userAddress: string): Promise<void> {
+ const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
+ await this._web3Wrapper.awaitTransactionSuccessAsync(
+ await tokenContract.mint.sendTransactionAsync(userAddress, tokenId, {
+ from: this._contractOwnerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ }
+ public async burnAsync(tokenAddress: string, tokenId: BigNumber, owner: string): Promise<void> {
+ const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
+ await this._web3Wrapper.awaitTransactionSuccessAsync(
+ await tokenContract.burn.sendTransactionAsync(owner, tokenId, {
+ from: this._contractOwnerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ }
+ public async ownerOfAsync(tokenAddress: string, tokenId: BigNumber): Promise<string> {
+ const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
+ const owner = await tokenContract.ownerOf.callAsync(tokenId);
+ return owner;
+ }
+ public async isOwnerAsync(userAddress: string, tokenAddress: string, tokenId: BigNumber): Promise<boolean> {
+ const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
+ const tokenOwner = await tokenContract.ownerOf.callAsync(tokenId);
+ const isOwner = tokenOwner === userAddress;
+ return isOwner;
+ }
+ public async isProxyApprovedForAllAsync(userAddress: string, tokenAddress: string): Promise<boolean> {
+ this._validateProxyContractExistsOrThrow();
+ const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
+ const operator = (this._proxyContract as ERC721ProxyContract).address;
+ const didApproveAll = await tokenContract.isApprovedForAll.callAsync(userAddress, operator);
+ return didApproveAll;
+ }
+ public async isProxyApprovedAsync(tokenAddress: string, tokenId: BigNumber): Promise<boolean> {
+ this._validateProxyContractExistsOrThrow();
+ const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
+ const approvedAddress = await tokenContract.getApproved.callAsync(tokenId);
+ const proxyAddress = (this._proxyContract as ERC721ProxyContract).address;
+ const isProxyAnApprovedOperator = approvedAddress === proxyAddress;
+ return isProxyAnApprovedOperator;
+ }
+ public async getBalancesAsync(): Promise<ERC721TokenIdsByOwner> {
+ this._validateDummyTokenContractsExistOrThrow();
+ this._validateBalancesAndAllowancesSetOrThrow();
+ const tokenIdsByOwner: ERC721TokenIdsByOwner = {};
+ const tokenOwnerAddresses: string[] = [];
+ const tokenInfo: Array<{ tokenId: BigNumber; tokenAddress: string }> = [];
+ for (const dummyTokenContract of this._dummyTokenContracts) {
+ for (const tokenOwnerAddress of this._tokenOwnerAddresses) {
+ const initialTokenOwnerIds = this._initialTokenIdsByOwner[tokenOwnerAddress][
+ dummyTokenContract.address
+ ];
+ for (const tokenId of initialTokenOwnerIds) {
+ tokenOwnerAddresses.push(await dummyTokenContract.ownerOf.callAsync(tokenId));
+ tokenInfo.push({
+ tokenId,
+ tokenAddress: dummyTokenContract.address,
+ });
+ }
+ }
+ }
+ _.forEach(tokenOwnerAddresses, (tokenOwnerAddress, ownerIndex) => {
+ const tokenAddress = tokenInfo[ownerIndex].tokenAddress;
+ const tokenId = tokenInfo[ownerIndex].tokenId;
+ if (_.isUndefined(tokenIdsByOwner[tokenOwnerAddress])) {
+ tokenIdsByOwner[tokenOwnerAddress] = {
+ [tokenAddress]: [],
+ };
+ }
+ if (_.isUndefined(tokenIdsByOwner[tokenOwnerAddress][tokenAddress])) {
+ tokenIdsByOwner[tokenOwnerAddress][tokenAddress] = [];
+ }
+ tokenIdsByOwner[tokenOwnerAddress][tokenAddress].push(tokenId);
+ });
+ return tokenIdsByOwner;
+ }
+ public getTokenOwnerAddresses(): string[] {
+ return this._tokenOwnerAddresses;
+ }
+ public getTokenAddresses(): string[] {
+ const tokenAddresses = _.map(this._dummyTokenContracts, dummyTokenContract => dummyTokenContract.address);
+ return tokenAddresses;
+ }
+ private _getTokenContractFromAssetData(tokenAddress: string): DummyERC721TokenContract {
+ const tokenContractIfExists = _.find(this._dummyTokenContracts, c => c.address === tokenAddress);
+ if (_.isUndefined(tokenContractIfExists)) {
+ throw new Error(`Token: ${tokenAddress} was not deployed through ERC20Wrapper`);
+ }
+ return tokenContractIfExists;
+ }
+ private _validateDummyTokenContractsExistOrThrow(): void {
+ if (_.isUndefined(this._dummyTokenContracts)) {
+ throw new Error('Dummy ERC721 tokens not yet deployed, please call "deployDummyTokensAsync"');
+ }
+ }
+ private _validateProxyContractExistsOrThrow(): void {
+ if (_.isUndefined(this._proxyContract)) {
+ throw new Error('ERC721 proxy contract not yet deployed, please call "deployProxyAsync"');
+ }
+ }
+ private _validateBalancesAndAllowancesSetOrThrow(): void {
+ if (_.keys(this._initialTokenIdsByOwner).length === 0) {
+ throw new Error(
+ 'Dummy ERC721 balances and allowances not yet set, please call "setBalancesAndAllowancesAsync"',
+ );
+ }
+ }
+}
diff --git a/contracts/protocol/test/utils/exchange_wrapper.ts b/contracts/protocol/test/utils/exchange_wrapper.ts
new file mode 100644
index 000000000..075d2cb96
--- /dev/null
+++ b/contracts/protocol/test/utils/exchange_wrapper.ts
@@ -0,0 +1,281 @@
+import {
+ FillResults,
+ formatters,
+ LogDecoder,
+ OrderInfo,
+ orderUtils,
+ SignedTransaction,
+} from '@0x/contracts-test-utils';
+import { artifacts as tokensArtifacts } from '@0x/contracts-tokens';
+import { SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import { Provider, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
+
+import { ExchangeContract } from '../../generated-wrappers/exchange';
+import { artifacts } from '../../src/artifacts';
+
+export class ExchangeWrapper {
+ private readonly _exchange: ExchangeContract;
+ private readonly _web3Wrapper: Web3Wrapper;
+ private readonly _logDecoder: LogDecoder;
+ constructor(exchangeContract: ExchangeContract, provider: Provider) {
+ this._exchange = exchangeContract;
+ this._web3Wrapper = new Web3Wrapper(provider);
+ this._logDecoder = new LogDecoder(this._web3Wrapper, { ...artifacts, ...tokensArtifacts });
+ }
+ public async fillOrderAsync(
+ signedOrder: SignedOrder,
+ from: string,
+ opts: { takerAssetFillAmount?: BigNumber } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
+ const txHash = await this._exchange.fillOrder.sendTransactionAsync(
+ params.order,
+ params.takerAssetFillAmount,
+ params.signature,
+ { from },
+ );
+ const txReceipt = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return txReceipt;
+ }
+ public async cancelOrderAsync(signedOrder: SignedOrder, from: string): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = orderUtils.createCancel(signedOrder);
+ const txHash = await this._exchange.cancelOrder.sendTransactionAsync(params.order, { from });
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async fillOrKillOrderAsync(
+ signedOrder: SignedOrder,
+ from: string,
+ opts: { takerAssetFillAmount?: BigNumber } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
+ const txHash = await this._exchange.fillOrKillOrder.sendTransactionAsync(
+ params.order,
+ params.takerAssetFillAmount,
+ params.signature,
+ { from },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async fillOrderNoThrowAsync(
+ signedOrder: SignedOrder,
+ from: string,
+ opts: { takerAssetFillAmount?: BigNumber; gas?: number } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
+ const txHash = await this._exchange.fillOrderNoThrow.sendTransactionAsync(
+ params.order,
+ params.takerAssetFillAmount,
+ params.signature,
+ { from, gas: opts.gas },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async batchFillOrdersAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { takerAssetFillAmounts?: BigNumber[] } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts);
+ const txHash = await this._exchange.batchFillOrders.sendTransactionAsync(
+ params.orders,
+ params.takerAssetFillAmounts,
+ params.signatures,
+ { from },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async batchFillOrKillOrdersAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { takerAssetFillAmounts?: BigNumber[] } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts);
+ const txHash = await this._exchange.batchFillOrKillOrders.sendTransactionAsync(
+ params.orders,
+ params.takerAssetFillAmounts,
+ params.signatures,
+ { from },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async batchFillOrdersNoThrowAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { takerAssetFillAmounts?: BigNumber[]; gas?: number } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts);
+ const txHash = await this._exchange.batchFillOrdersNoThrow.sendTransactionAsync(
+ params.orders,
+ params.takerAssetFillAmounts,
+ params.signatures,
+ { from, gas: opts.gas },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async marketSellOrdersAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { takerAssetFillAmount: BigNumber },
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createMarketSellOrders(orders, opts.takerAssetFillAmount);
+ const txHash = await this._exchange.marketSellOrders.sendTransactionAsync(
+ params.orders,
+ params.takerAssetFillAmount,
+ params.signatures,
+ { from },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async marketSellOrdersNoThrowAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { takerAssetFillAmount: BigNumber; gas?: number },
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createMarketSellOrders(orders, opts.takerAssetFillAmount);
+ const txHash = await this._exchange.marketSellOrdersNoThrow.sendTransactionAsync(
+ params.orders,
+ params.takerAssetFillAmount,
+ params.signatures,
+ { from, gas: opts.gas },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async marketBuyOrdersAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { makerAssetFillAmount: BigNumber },
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createMarketBuyOrders(orders, opts.makerAssetFillAmount);
+ const txHash = await this._exchange.marketBuyOrders.sendTransactionAsync(
+ params.orders,
+ params.makerAssetFillAmount,
+ params.signatures,
+ { from },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async marketBuyOrdersNoThrowAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { makerAssetFillAmount: BigNumber; gas?: number },
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createMarketBuyOrders(orders, opts.makerAssetFillAmount);
+ const txHash = await this._exchange.marketBuyOrdersNoThrow.sendTransactionAsync(
+ params.orders,
+ params.makerAssetFillAmount,
+ params.signatures,
+ { from, gas: opts.gas },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async batchCancelOrdersAsync(
+ orders: SignedOrder[],
+ from: string,
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createBatchCancel(orders);
+ const txHash = await this._exchange.batchCancelOrders.sendTransactionAsync(params.orders, { from });
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async cancelOrdersUpToAsync(salt: BigNumber, from: string): Promise<TransactionReceiptWithDecodedLogs> {
+ const txHash = await this._exchange.cancelOrdersUpTo.sendTransactionAsync(salt, { from });
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async registerAssetProxyAsync(
+ assetProxyAddress: string,
+ from: string,
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const txHash = await this._exchange.registerAssetProxy.sendTransactionAsync(assetProxyAddress, { from });
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async executeTransactionAsync(
+ signedTx: SignedTransaction,
+ from: string,
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const txHash = await this._exchange.executeTransaction.sendTransactionAsync(
+ signedTx.salt,
+ signedTx.signerAddress,
+ signedTx.data,
+ signedTx.signature,
+ { from },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async getTakerAssetFilledAmountAsync(orderHashHex: string): Promise<BigNumber> {
+ const filledAmount = await this._exchange.filled.callAsync(orderHashHex);
+ return filledAmount;
+ }
+ public async isCancelledAsync(orderHashHex: string): Promise<boolean> {
+ const isCancelled = await this._exchange.cancelled.callAsync(orderHashHex);
+ return isCancelled;
+ }
+ public async getOrderEpochAsync(makerAddress: string, senderAddress: string): Promise<BigNumber> {
+ const orderEpoch = await this._exchange.orderEpoch.callAsync(makerAddress, senderAddress);
+ return orderEpoch;
+ }
+ public async getOrderInfoAsync(signedOrder: SignedOrder): Promise<OrderInfo> {
+ const orderInfo = (await this._exchange.getOrderInfo.callAsync(signedOrder)) as OrderInfo;
+ return orderInfo;
+ }
+ public async getOrdersInfoAsync(signedOrders: SignedOrder[]): Promise<OrderInfo[]> {
+ const ordersInfo = (await this._exchange.getOrdersInfo.callAsync(signedOrders)) as OrderInfo[];
+ return ordersInfo;
+ }
+ public async matchOrdersAsync(
+ signedOrderLeft: SignedOrder,
+ signedOrderRight: SignedOrder,
+ from: string,
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = orderUtils.createMatchOrders(signedOrderLeft, signedOrderRight);
+ const txHash = await this._exchange.matchOrders.sendTransactionAsync(
+ params.left,
+ params.right,
+ params.leftSignature,
+ params.rightSignature,
+ { from },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async getFillOrderResultsAsync(
+ signedOrder: SignedOrder,
+ from: string,
+ opts: { takerAssetFillAmount?: BigNumber } = {},
+ ): Promise<FillResults> {
+ const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
+ const fillResults = await this._exchange.fillOrder.callAsync(
+ params.order,
+ params.takerAssetFillAmount,
+ params.signature,
+ { from },
+ );
+ return fillResults;
+ }
+ public abiEncodeFillOrder(signedOrder: SignedOrder, opts: { takerAssetFillAmount?: BigNumber } = {}): string {
+ const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
+ const data = this._exchange.fillOrder.getABIEncodedTransactionData(
+ params.order,
+ params.takerAssetFillAmount,
+ params.signature,
+ );
+ return data;
+ }
+ public getExchangeAddress(): string {
+ return this._exchange.address;
+ }
+}
diff --git a/contracts/protocol/test/utils/fill_order_combinatorial_utils.ts b/contracts/protocol/test/utils/fill_order_combinatorial_utils.ts
new file mode 100644
index 000000000..5d0ea07a8
--- /dev/null
+++ b/contracts/protocol/test/utils/fill_order_combinatorial_utils.ts
@@ -0,0 +1,928 @@
+import { artifacts as libsArtifacts, TestLibsContract } from '@0x/contracts-libs';
+import {
+ AllowanceAmountScenario,
+ AssetDataScenario,
+ BalanceAmountScenario,
+ chaiSetup,
+ constants,
+ expectTransactionFailedAsync,
+ ExpirationTimeSecondsScenario,
+ FeeRecipientAddressScenario,
+ FillScenario,
+ OrderAssetAmountScenario,
+ orderUtils,
+ signingUtils,
+ TakerAssetFillAmountScenario,
+ TakerScenario,
+ TraderStateScenario,
+} from '@0x/contracts-test-utils';
+import {
+ assetDataUtils,
+ BalanceAndProxyAllowanceLazyStore,
+ ExchangeTransferSimulator,
+ orderHashUtils,
+ OrderStateUtils,
+ OrderValidationUtils,
+} from '@0x/order-utils';
+import { AssetProxyId, RevertReason, SignatureType, SignedOrder } from '@0x/types';
+import { BigNumber, errorUtils, logUtils } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import * as chai from 'chai';
+import { LogWithDecodedArgs, Provider, TxData } from 'ethereum-types';
+import * as _ from 'lodash';
+import 'make-promises-safe';
+
+import { ExchangeContract, ExchangeFillEventArgs } from '../../generated-wrappers/exchange';
+import { artifacts } from '../../src/artifacts';
+
+import { AssetWrapper } from './asset_wrapper';
+import { ERC20Wrapper } from './erc20_wrapper';
+import { ERC721Wrapper } from './erc721_wrapper';
+import { ExchangeWrapper } from './exchange_wrapper';
+import { OrderFactoryFromScenario } from './order_factory_from_scenario';
+import { SimpleAssetBalanceAndProxyAllowanceFetcher } from './simple_asset_balance_and_proxy_allowance_fetcher';
+import { SimpleOrderFilledCancelledFetcher } from './simple_order_filled_cancelled_fetcher';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+/**
+ * Instantiates a new instance of FillOrderCombinatorialUtils. Since this method has some
+ * required async setup, a factory method is required.
+ * @param web3Wrapper Web3Wrapper instance
+ * @param txDefaults Default Ethereum tx options
+ * @return FillOrderCombinatorialUtils instance
+ */
+export async function fillOrderCombinatorialUtilsFactoryAsync(
+ web3Wrapper: Web3Wrapper,
+ txDefaults: Partial<TxData>,
+): Promise<FillOrderCombinatorialUtils> {
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ const userAddresses = _.slice(accounts, 0, 5);
+ const [ownerAddress, makerAddress, takerAddress] = userAddresses;
+ const makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
+
+ const provider = web3Wrapper.getProvider();
+ const erc20Wrapper = new ERC20Wrapper(provider, userAddresses, ownerAddress);
+ const erc721Wrapper = new ERC721Wrapper(provider, userAddresses, ownerAddress);
+
+ const erc20EighteenDecimalTokenCount = 3;
+ const eighteenDecimals = new BigNumber(18);
+ const [
+ erc20EighteenDecimalTokenA,
+ erc20EighteenDecimalTokenB,
+ zrxToken,
+ ] = await erc20Wrapper.deployDummyTokensAsync(erc20EighteenDecimalTokenCount, eighteenDecimals);
+ const zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
+
+ const erc20FiveDecimalTokenCount = 2;
+ const fiveDecimals = new BigNumber(5);
+ const [erc20FiveDecimalTokenA, erc20FiveDecimalTokenB] = await erc20Wrapper.deployDummyTokensAsync(
+ erc20FiveDecimalTokenCount,
+ fiveDecimals,
+ );
+ const zeroDecimals = new BigNumber(0);
+ const erc20ZeroDecimalTokenCount = 2;
+ const [erc20ZeroDecimalTokenA, erc20ZeroDecimalTokenB] = await erc20Wrapper.deployDummyTokensAsync(
+ erc20ZeroDecimalTokenCount,
+ zeroDecimals,
+ );
+ const erc20Proxy = await erc20Wrapper.deployProxyAsync();
+ await erc20Wrapper.setBalancesAndAllowancesAsync();
+
+ const [erc721Token] = await erc721Wrapper.deployDummyTokensAsync();
+ const erc721Proxy = await erc721Wrapper.deployProxyAsync();
+ await erc721Wrapper.setBalancesAndAllowancesAsync();
+ const erc721Balances = await erc721Wrapper.getBalancesAsync();
+
+ const assetWrapper = new AssetWrapper([erc20Wrapper, erc721Wrapper]);
+
+ const exchangeContract = await ExchangeContract.deployFrom0xArtifactAsync(
+ artifacts.Exchange,
+ provider,
+ txDefaults,
+ zrxAssetData,
+ );
+ const exchangeWrapper = new ExchangeWrapper(exchangeContract, provider);
+ await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, ownerAddress);
+ await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, ownerAddress);
+
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeContract.address, {
+ from: ownerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeContract.address, {
+ from: ownerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ const orderFactory = new OrderFactoryFromScenario(
+ userAddresses,
+ zrxToken.address,
+ [erc20EighteenDecimalTokenA.address, erc20EighteenDecimalTokenB.address],
+ [erc20FiveDecimalTokenA.address, erc20FiveDecimalTokenB.address],
+ [erc20ZeroDecimalTokenA.address, erc20ZeroDecimalTokenB.address],
+ erc721Token,
+ erc721Balances,
+ exchangeContract.address,
+ );
+
+ const testLibsContract = await TestLibsContract.deployFrom0xArtifactAsync(
+ libsArtifacts.TestLibs,
+ provider,
+ txDefaults,
+ );
+
+ const fillOrderCombinatorialUtils = new FillOrderCombinatorialUtils(
+ orderFactory,
+ ownerAddress,
+ makerAddress,
+ makerPrivateKey,
+ takerAddress,
+ zrxAssetData,
+ exchangeWrapper,
+ assetWrapper,
+ testLibsContract,
+ );
+ return fillOrderCombinatorialUtils;
+}
+
+export class FillOrderCombinatorialUtils {
+ public orderFactory: OrderFactoryFromScenario;
+ public ownerAddress: string;
+ public makerAddress: string;
+ public makerPrivateKey: Buffer;
+ public takerAddress: string;
+ public zrxAssetData: string;
+ public exchangeWrapper: ExchangeWrapper;
+ public assetWrapper: AssetWrapper;
+ public testLibsContract: TestLibsContract;
+ public static generateFillOrderCombinations(): FillScenario[] {
+ const takerScenarios = [
+ TakerScenario.Unspecified,
+ // TakerScenario.CorrectlySpecified,
+ // TakerScenario.IncorrectlySpecified,
+ ];
+ const feeRecipientScenarios = [
+ FeeRecipientAddressScenario.EthUserAddress,
+ // FeeRecipientAddressScenario.BurnAddress,
+ ];
+ const makerAssetAmountScenario = [
+ OrderAssetAmountScenario.Large,
+ // OrderAssetAmountScenario.Zero,
+ // OrderAssetAmountScenario.Small,
+ ];
+ const takerAssetAmountScenario = [
+ OrderAssetAmountScenario.Large,
+ // OrderAssetAmountScenario.Zero,
+ // OrderAssetAmountScenario.Small,
+ ];
+ const makerFeeScenario = [
+ OrderAssetAmountScenario.Large,
+ // OrderAssetAmountScenario.Small,
+ // OrderAssetAmountScenario.Zero,
+ ];
+ const takerFeeScenario = [
+ OrderAssetAmountScenario.Large,
+ // OrderAssetAmountScenario.Small,
+ // OrderAssetAmountScenario.Zero,
+ ];
+ const expirationTimeSecondsScenario = [
+ ExpirationTimeSecondsScenario.InFuture,
+ ExpirationTimeSecondsScenario.InPast,
+ ];
+ const makerAssetDataScenario = [
+ AssetDataScenario.ERC20FiveDecimals,
+ AssetDataScenario.ERC20NonZRXEighteenDecimals,
+ AssetDataScenario.ERC721,
+ AssetDataScenario.ZRXFeeToken,
+ ];
+ const takerAssetDataScenario = [
+ AssetDataScenario.ERC20FiveDecimals,
+ AssetDataScenario.ERC20NonZRXEighteenDecimals,
+ AssetDataScenario.ERC721,
+ AssetDataScenario.ZRXFeeToken,
+ ];
+ const takerAssetFillAmountScenario = [
+ TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
+ // TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount,
+ // TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount,
+ ];
+ const makerAssetBalanceScenario = [
+ BalanceAmountScenario.Higher,
+ // BalanceAmountScenario.Exact,
+ // BalanceAmountScenario.TooLow,
+ ];
+ const makerAssetAllowanceScenario = [
+ AllowanceAmountScenario.Higher,
+ // AllowanceAmountScenario.Exact,
+ // AllowanceAmountScenario.TooLow,
+ // AllowanceAmountScenario.Unlimited,
+ ];
+ const makerZRXBalanceScenario = [
+ BalanceAmountScenario.Higher,
+ // BalanceAmountScenario.Exact,
+ // BalanceAmountScenario.TooLow,
+ ];
+ const makerZRXAllowanceScenario = [
+ AllowanceAmountScenario.Higher,
+ // AllowanceAmountScenario.Exact,
+ // AllowanceAmountScenario.TooLow,
+ // AllowanceAmountScenario.Unlimited,
+ ];
+ const takerAssetBalanceScenario = [
+ BalanceAmountScenario.Higher,
+ // BalanceAmountScenario.Exact,
+ // BalanceAmountScenario.TooLow,
+ ];
+ const takerAssetAllowanceScenario = [
+ AllowanceAmountScenario.Higher,
+ // AllowanceAmountScenario.Exact,
+ // AllowanceAmountScenario.TooLow,
+ // AllowanceAmountScenario.Unlimited,
+ ];
+ const takerZRXBalanceScenario = [
+ BalanceAmountScenario.Higher,
+ // BalanceAmountScenario.Exact,
+ // BalanceAmountScenario.TooLow,
+ ];
+ const takerZRXAllowanceScenario = [
+ AllowanceAmountScenario.Higher,
+ // AllowanceAmountScenario.Exact,
+ // AllowanceAmountScenario.TooLow,
+ // AllowanceAmountScenario.Unlimited,
+ ];
+ const fillScenarioArrays = FillOrderCombinatorialUtils._getAllCombinations([
+ takerScenarios,
+ feeRecipientScenarios,
+ makerAssetAmountScenario,
+ takerAssetAmountScenario,
+ makerFeeScenario,
+ takerFeeScenario,
+ expirationTimeSecondsScenario,
+ makerAssetDataScenario,
+ takerAssetDataScenario,
+ takerAssetFillAmountScenario,
+ makerAssetBalanceScenario,
+ makerAssetAllowanceScenario,
+ makerZRXBalanceScenario,
+ makerZRXAllowanceScenario,
+ takerAssetBalanceScenario,
+ takerAssetAllowanceScenario,
+ takerZRXBalanceScenario,
+ takerZRXAllowanceScenario,
+ ]);
+
+ const fillScenarios = _.map(fillScenarioArrays, fillScenarioArray => {
+ // tslint:disable:custom-no-magic-numbers
+ const fillScenario: FillScenario = {
+ orderScenario: {
+ takerScenario: fillScenarioArray[0] as TakerScenario,
+ feeRecipientScenario: fillScenarioArray[1] as FeeRecipientAddressScenario,
+ makerAssetAmountScenario: fillScenarioArray[2] as OrderAssetAmountScenario,
+ takerAssetAmountScenario: fillScenarioArray[3] as OrderAssetAmountScenario,
+ makerFeeScenario: fillScenarioArray[4] as OrderAssetAmountScenario,
+ takerFeeScenario: fillScenarioArray[5] as OrderAssetAmountScenario,
+ expirationTimeSecondsScenario: fillScenarioArray[6] as ExpirationTimeSecondsScenario,
+ makerAssetDataScenario: fillScenarioArray[7] as AssetDataScenario,
+ takerAssetDataScenario: fillScenarioArray[8] as AssetDataScenario,
+ },
+ takerAssetFillAmountScenario: fillScenarioArray[9] as TakerAssetFillAmountScenario,
+ makerStateScenario: {
+ traderAssetBalance: fillScenarioArray[10] as BalanceAmountScenario,
+ traderAssetAllowance: fillScenarioArray[11] as AllowanceAmountScenario,
+ zrxFeeBalance: fillScenarioArray[12] as BalanceAmountScenario,
+ zrxFeeAllowance: fillScenarioArray[13] as AllowanceAmountScenario,
+ },
+ takerStateScenario: {
+ traderAssetBalance: fillScenarioArray[14] as BalanceAmountScenario,
+ traderAssetAllowance: fillScenarioArray[15] as AllowanceAmountScenario,
+ zrxFeeBalance: fillScenarioArray[16] as BalanceAmountScenario,
+ zrxFeeAllowance: fillScenarioArray[17] as AllowanceAmountScenario,
+ },
+ };
+ // tslint:enable:custom-no-magic-numbers
+ return fillScenario;
+ });
+
+ return fillScenarios;
+ }
+ /**
+ * Recursive implementation of generating all combinations of the supplied
+ * string-containing arrays.
+ */
+ private static _getAllCombinations(arrays: string[][]): string[][] {
+ // Base case
+ if (arrays.length === 1) {
+ const remainingValues = _.map(arrays[0], val => {
+ return [val];
+ });
+ return remainingValues;
+ } else {
+ const result = [];
+ const restOfArrays = arrays.slice(1);
+ const allCombinationsOfRemaining = FillOrderCombinatorialUtils._getAllCombinations(restOfArrays); // recur with the rest of array
+ // tslint:disable:prefer-for-of
+ for (let i = 0; i < allCombinationsOfRemaining.length; i++) {
+ for (let j = 0; j < arrays[0].length; j++) {
+ result.push([arrays[0][j], ...allCombinationsOfRemaining[i]]);
+ }
+ }
+ // tslint:enable:prefer-for-of
+ return result;
+ }
+ }
+ constructor(
+ orderFactory: OrderFactoryFromScenario,
+ ownerAddress: string,
+ makerAddress: string,
+ makerPrivateKey: Buffer,
+ takerAddress: string,
+ zrxAssetData: string,
+ exchangeWrapper: ExchangeWrapper,
+ assetWrapper: AssetWrapper,
+ testLibsContract: TestLibsContract,
+ ) {
+ this.orderFactory = orderFactory;
+ this.ownerAddress = ownerAddress;
+ this.makerAddress = makerAddress;
+ this.makerPrivateKey = makerPrivateKey;
+ this.takerAddress = takerAddress;
+ this.zrxAssetData = zrxAssetData;
+ this.exchangeWrapper = exchangeWrapper;
+ this.assetWrapper = assetWrapper;
+ this.testLibsContract = testLibsContract;
+ }
+ public async testFillOrderScenarioAsync(
+ provider: Provider,
+ fillScenario: FillScenario,
+ isVerbose: boolean = false,
+ ): Promise<void> {
+ // 1. Generate order
+ const order = this.orderFactory.generateOrder(fillScenario.orderScenario);
+
+ // 2. Sign order
+ const orderHashBuff = orderHashUtils.getOrderHashBuffer(order);
+ const signature = signingUtils.signMessage(orderHashBuff, this.makerPrivateKey, SignatureType.EthSign);
+ const signedOrder = {
+ ...order,
+ signature: `0x${signature.toString('hex')}`,
+ };
+
+ const balanceAndProxyAllowanceFetcher = new SimpleAssetBalanceAndProxyAllowanceFetcher(this.assetWrapper);
+ const orderFilledCancelledFetcher = new SimpleOrderFilledCancelledFetcher(
+ this.exchangeWrapper,
+ this.zrxAssetData,
+ );
+
+ // 3. Figure out fill amount
+ const takerAssetFillAmount = await this._getTakerAssetFillAmountAsync(
+ signedOrder,
+ fillScenario.takerAssetFillAmountScenario,
+ balanceAndProxyAllowanceFetcher,
+ orderFilledCancelledFetcher,
+ );
+
+ // 4. Permutate the maker and taker balance/allowance scenarios
+ await this._modifyTraderStateAsync(
+ fillScenario.makerStateScenario,
+ fillScenario.takerStateScenario,
+ signedOrder,
+ takerAssetFillAmount,
+ );
+
+ // 5. If I fill it by X, what are the resulting balances/allowances/filled amounts expected?
+ const orderValidationUtils = new OrderValidationUtils(orderFilledCancelledFetcher, provider);
+ const lazyStore = new BalanceAndProxyAllowanceLazyStore(balanceAndProxyAllowanceFetcher);
+ const exchangeTransferSimulator = new ExchangeTransferSimulator(lazyStore);
+
+ let fillRevertReasonIfExists;
+ try {
+ await orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
+ exchangeTransferSimulator,
+ provider,
+ signedOrder,
+ takerAssetFillAmount,
+ this.takerAddress,
+ this.zrxAssetData,
+ );
+ if (isVerbose) {
+ logUtils.log(`Expecting fillOrder to succeed.`);
+ }
+ } catch (err) {
+ fillRevertReasonIfExists = err.message;
+ if (isVerbose) {
+ logUtils.log(`Expecting fillOrder to fail with:`);
+ logUtils.log(err);
+ }
+ }
+
+ // 6. Fill the order
+ await this._fillOrderAndAssertOutcomeAsync(
+ signedOrder,
+ takerAssetFillAmount,
+ lazyStore,
+ fillRevertReasonIfExists,
+ );
+
+ await this._abiEncodeFillOrderAndAssertOutcomeAsync(signedOrder, takerAssetFillAmount);
+ }
+ private async _fillOrderAndAssertOutcomeAsync(
+ signedOrder: SignedOrder,
+ takerAssetFillAmount: BigNumber,
+ lazyStore: BalanceAndProxyAllowanceLazyStore,
+ fillRevertReasonIfExists: RevertReason | undefined,
+ ): Promise<void> {
+ if (!_.isUndefined(fillRevertReasonIfExists)) {
+ return expectTransactionFailedAsync(
+ this.exchangeWrapper.fillOrderAsync(signedOrder, this.takerAddress, { takerAssetFillAmount }),
+ fillRevertReasonIfExists,
+ );
+ }
+
+ const makerAddress = signedOrder.makerAddress;
+ const makerAssetData = signedOrder.makerAssetData;
+ const takerAssetData = signedOrder.takerAssetData;
+ const feeRecipient = signedOrder.feeRecipientAddress;
+
+ const expMakerAssetBalanceOfMaker = await lazyStore.getBalanceAsync(makerAssetData, makerAddress);
+ const expMakerAssetAllowanceOfMaker = await lazyStore.getProxyAllowanceAsync(makerAssetData, makerAddress);
+ const expTakerAssetBalanceOfMaker = await lazyStore.getBalanceAsync(takerAssetData, makerAddress);
+ const expZRXAssetBalanceOfMaker = await lazyStore.getBalanceAsync(this.zrxAssetData, makerAddress);
+ const expZRXAssetAllowanceOfMaker = await lazyStore.getProxyAllowanceAsync(this.zrxAssetData, makerAddress);
+ const expTakerAssetBalanceOfTaker = await lazyStore.getBalanceAsync(takerAssetData, this.takerAddress);
+ const expTakerAssetAllowanceOfTaker = await lazyStore.getProxyAllowanceAsync(takerAssetData, this.takerAddress);
+ const expMakerAssetBalanceOfTaker = await lazyStore.getBalanceAsync(makerAssetData, this.takerAddress);
+ const expZRXAssetBalanceOfTaker = await lazyStore.getBalanceAsync(this.zrxAssetData, this.takerAddress);
+ const expZRXAssetAllowanceOfTaker = await lazyStore.getProxyAllowanceAsync(
+ this.zrxAssetData,
+ this.takerAddress,
+ );
+ const expZRXAssetBalanceOfFeeRecipient = await lazyStore.getBalanceAsync(this.zrxAssetData, feeRecipient);
+
+ const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const alreadyFilledTakerAmount = await this.exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash);
+ const remainingTakerAmountToFill = signedOrder.takerAssetAmount.minus(alreadyFilledTakerAmount);
+ const expFilledTakerAmount = takerAssetFillAmount.gt(remainingTakerAmountToFill)
+ ? remainingTakerAmountToFill
+ : alreadyFilledTakerAmount.add(takerAssetFillAmount);
+
+ const expFilledMakerAmount = orderUtils.getPartialAmountFloor(
+ expFilledTakerAmount,
+ signedOrder.takerAssetAmount,
+ signedOrder.makerAssetAmount,
+ );
+ const expMakerFeePaid = orderUtils.getPartialAmountFloor(
+ expFilledTakerAmount,
+ signedOrder.takerAssetAmount,
+ signedOrder.makerFee,
+ );
+ const expTakerFeePaid = orderUtils.getPartialAmountFloor(
+ expFilledTakerAmount,
+ signedOrder.takerAssetAmount,
+ signedOrder.takerFee,
+ );
+ const fillResults = await this.exchangeWrapper.getFillOrderResultsAsync(signedOrder, this.takerAddress, {
+ takerAssetFillAmount,
+ });
+ expect(fillResults.takerAssetFilledAmount).to.be.bignumber.equal(
+ expFilledTakerAmount,
+ 'takerAssetFilledAmount',
+ );
+ expect(fillResults.makerAssetFilledAmount).to.be.bignumber.equal(
+ expFilledMakerAmount,
+ 'makerAssetFilledAmount',
+ );
+ expect(fillResults.takerFeePaid).to.be.bignumber.equal(expTakerFeePaid, 'takerFeePaid');
+ expect(fillResults.makerFeePaid).to.be.bignumber.equal(expMakerFeePaid, 'makerFeePaid');
+
+ // - Let's fill the order!
+ const txReceipt = await this.exchangeWrapper.fillOrderAsync(signedOrder, this.takerAddress, {
+ takerAssetFillAmount,
+ });
+
+ const actFilledTakerAmount = await this.exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash);
+ expect(actFilledTakerAmount).to.be.bignumber.equal(expFilledTakerAmount, 'filledTakerAmount');
+
+ const exchangeLogs = _.filter(
+ txReceipt.logs,
+ txLog => txLog.address === this.exchangeWrapper.getExchangeAddress(),
+ );
+ expect(exchangeLogs.length).to.be.equal(1, 'logs length');
+ // tslint:disable-next-line:no-unnecessary-type-assertion
+ const log = txReceipt.logs[0] as LogWithDecodedArgs<ExchangeFillEventArgs>;
+ expect(log.args.makerAddress).to.be.equal(makerAddress, 'log.args.makerAddress');
+ expect(log.args.takerAddress).to.be.equal(this.takerAddress, 'log.args.this.takerAddress');
+ expect(log.args.feeRecipientAddress).to.be.equal(feeRecipient, 'log.args.feeRecipientAddress');
+ expect(log.args.makerAssetFilledAmount).to.be.bignumber.equal(
+ expFilledMakerAmount,
+ 'log.args.makerAssetFilledAmount',
+ );
+ expect(log.args.takerAssetFilledAmount).to.be.bignumber.equal(
+ expFilledTakerAmount,
+ 'log.args.takerAssetFilledAmount',
+ );
+ expect(log.args.makerFeePaid).to.be.bignumber.equal(expMakerFeePaid, 'log.args.makerFeePaid');
+ expect(log.args.takerFeePaid).to.be.bignumber.equal(expTakerFeePaid, 'logs.args.takerFeePaid');
+ expect(log.args.orderHash).to.be.equal(orderHash, 'log.args.orderHash');
+ expect(log.args.makerAssetData).to.be.equal(makerAssetData, 'log.args.makerAssetData');
+ expect(log.args.takerAssetData).to.be.equal(takerAssetData, 'log.args.takerAssetData');
+
+ const actMakerAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, makerAssetData);
+ expect(actMakerAssetBalanceOfMaker).to.be.bignumber.equal(
+ expMakerAssetBalanceOfMaker,
+ 'makerAssetBalanceOfMaker',
+ );
+
+ const actMakerAssetAllowanceOfMaker = await this.assetWrapper.getProxyAllowanceAsync(
+ makerAddress,
+ makerAssetData,
+ );
+ expect(actMakerAssetAllowanceOfMaker).to.be.bignumber.equal(
+ expMakerAssetAllowanceOfMaker,
+ 'makerAssetAllowanceOfMaker',
+ );
+
+ const actTakerAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, takerAssetData);
+ expect(actTakerAssetBalanceOfMaker).to.be.bignumber.equal(
+ expTakerAssetBalanceOfMaker,
+ 'takerAssetBalanceOfMaker',
+ );
+
+ const actZRXAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, this.zrxAssetData);
+ expect(actZRXAssetBalanceOfMaker).to.be.bignumber.equal(expZRXAssetBalanceOfMaker, 'ZRXAssetBalanceOfMaker');
+
+ const actZRXAssetAllowanceOfMaker = await this.assetWrapper.getProxyAllowanceAsync(
+ makerAddress,
+ this.zrxAssetData,
+ );
+ expect(actZRXAssetAllowanceOfMaker).to.be.bignumber.equal(
+ expZRXAssetAllowanceOfMaker,
+ 'ZRXAssetAllowanceOfMaker',
+ );
+
+ const actTakerAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, takerAssetData);
+ expect(actTakerAssetBalanceOfTaker).to.be.bignumber.equal(
+ expTakerAssetBalanceOfTaker,
+ 'TakerAssetBalanceOfTaker',
+ );
+
+ const actTakerAssetAllowanceOfTaker = await this.assetWrapper.getProxyAllowanceAsync(
+ this.takerAddress,
+ takerAssetData,
+ );
+
+ expect(actTakerAssetAllowanceOfTaker).to.be.bignumber.equal(
+ expTakerAssetAllowanceOfTaker,
+ 'TakerAssetAllowanceOfTaker',
+ );
+
+ const actMakerAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, makerAssetData);
+ expect(actMakerAssetBalanceOfTaker).to.be.bignumber.equal(
+ expMakerAssetBalanceOfTaker,
+ 'MakerAssetBalanceOfTaker',
+ );
+
+ const actZRXAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, this.zrxAssetData);
+ expect(actZRXAssetBalanceOfTaker).to.be.bignumber.equal(expZRXAssetBalanceOfTaker, 'ZRXAssetBalanceOfTaker');
+
+ const actZRXAssetAllowanceOfTaker = await this.assetWrapper.getProxyAllowanceAsync(
+ this.takerAddress,
+ this.zrxAssetData,
+ );
+ expect(actZRXAssetAllowanceOfTaker).to.be.bignumber.equal(
+ expZRXAssetAllowanceOfTaker,
+ 'ZRXAssetAllowanceOfTaker',
+ );
+
+ const actZRXAssetBalanceOfFeeRecipient = await this.assetWrapper.getBalanceAsync(
+ feeRecipient,
+ this.zrxAssetData,
+ );
+ expect(actZRXAssetBalanceOfFeeRecipient).to.be.bignumber.equal(
+ expZRXAssetBalanceOfFeeRecipient,
+ 'ZRXAssetBalanceOfFeeRecipient',
+ );
+ }
+ private async _abiEncodeFillOrderAndAssertOutcomeAsync(
+ signedOrder: SignedOrder,
+ takerAssetFillAmount: BigNumber,
+ ): Promise<void> {
+ const params = orderUtils.createFill(signedOrder, takerAssetFillAmount);
+ const expectedAbiEncodedData = this.exchangeWrapper.abiEncodeFillOrder(signedOrder, { takerAssetFillAmount });
+ const libsAbiEncodedData = await this.testLibsContract.publicAbiEncodeFillOrder.callAsync(
+ params.order,
+ params.takerAssetFillAmount,
+ params.signature,
+ );
+ expect(libsAbiEncodedData).to.be.equal(expectedAbiEncodedData, 'ABIEncodedFillOrderData');
+ }
+ private async _getTakerAssetFillAmountAsync(
+ signedOrder: SignedOrder,
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario,
+ balanceAndProxyAllowanceFetcher: SimpleAssetBalanceAndProxyAllowanceFetcher,
+ orderFilledCancelledFetcher: SimpleOrderFilledCancelledFetcher,
+ ): Promise<BigNumber> {
+ const orderStateUtils = new OrderStateUtils(balanceAndProxyAllowanceFetcher, orderFilledCancelledFetcher);
+ const fillableTakerAssetAmount = await orderStateUtils.getMaxFillableTakerAssetAmountAsync(
+ signedOrder,
+ this.takerAddress,
+ );
+
+ let takerAssetFillAmount;
+ switch (takerAssetFillAmountScenario) {
+ case TakerAssetFillAmountScenario.Zero:
+ takerAssetFillAmount = new BigNumber(0);
+ break;
+
+ case TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount:
+ takerAssetFillAmount = fillableTakerAssetAmount;
+ break;
+
+ case TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount:
+ takerAssetFillAmount = fillableTakerAssetAmount.add(1);
+ break;
+
+ case TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount:
+ const takerAssetProxyId = assetDataUtils.decodeAssetProxyId(signedOrder.takerAssetData);
+ const makerAssetProxyId = assetDataUtils.decodeAssetProxyId(signedOrder.makerAssetData);
+ const isEitherAssetERC721 =
+ takerAssetProxyId === AssetProxyId.ERC721 || makerAssetProxyId === AssetProxyId.ERC721;
+ if (isEitherAssetERC721) {
+ throw new Error(
+ 'Cannot test `TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount` together with ERC721 assets since orders involving ERC721 must always be filled exactly.',
+ );
+ }
+ takerAssetFillAmount = fillableTakerAssetAmount.div(2).floor();
+ break;
+
+ default:
+ throw errorUtils.spawnSwitchErr('TakerAssetFillAmountScenario', takerAssetFillAmountScenario);
+ }
+
+ return takerAssetFillAmount;
+ }
+ private async _modifyTraderStateAsync(
+ makerStateScenario: TraderStateScenario,
+ takerStateScenario: TraderStateScenario,
+ signedOrder: SignedOrder,
+ takerAssetFillAmount: BigNumber,
+ ): Promise<void> {
+ const makerAssetFillAmount = orderUtils.getPartialAmountFloor(
+ takerAssetFillAmount,
+ signedOrder.takerAssetAmount,
+ signedOrder.makerAssetAmount,
+ );
+ switch (makerStateScenario.traderAssetBalance) {
+ case BalanceAmountScenario.Higher:
+ break; // Noop since this is already the default
+
+ case BalanceAmountScenario.TooLow:
+ if (makerAssetFillAmount.eq(0)) {
+ throw new Error(`Cannot set makerAssetBalanceOfMaker TooLow if makerAssetFillAmount is 0`);
+ }
+ const tooLowBalance = makerAssetFillAmount.minus(1);
+ await this.assetWrapper.setBalanceAsync(
+ signedOrder.makerAddress,
+ signedOrder.makerAssetData,
+ tooLowBalance,
+ );
+ break;
+
+ case BalanceAmountScenario.Exact:
+ const exactBalance = makerAssetFillAmount;
+ await this.assetWrapper.setBalanceAsync(
+ signedOrder.makerAddress,
+ signedOrder.makerAssetData,
+ exactBalance,
+ );
+ break;
+
+ default:
+ throw errorUtils.spawnSwitchErr(
+ 'makerStateScenario.traderAssetBalance',
+ makerStateScenario.traderAssetBalance,
+ );
+ }
+
+ const makerFee = orderUtils.getPartialAmountFloor(
+ takerAssetFillAmount,
+ signedOrder.takerAssetAmount,
+ signedOrder.makerFee,
+ );
+ switch (makerStateScenario.zrxFeeBalance) {
+ case BalanceAmountScenario.Higher:
+ break; // Noop since this is already the default
+
+ case BalanceAmountScenario.TooLow:
+ if (makerFee.eq(0)) {
+ throw new Error(`Cannot set zrxAsserBalanceOfMaker TooLow if makerFee is 0`);
+ }
+ const tooLowBalance = makerFee.minus(1);
+ await this.assetWrapper.setBalanceAsync(signedOrder.makerAddress, this.zrxAssetData, tooLowBalance);
+ break;
+
+ case BalanceAmountScenario.Exact:
+ const exactBalance = makerFee;
+ await this.assetWrapper.setBalanceAsync(signedOrder.makerAddress, this.zrxAssetData, exactBalance);
+ break;
+
+ default:
+ throw errorUtils.spawnSwitchErr('makerStateScenario.zrxFeeBalance', makerStateScenario.zrxFeeBalance);
+ }
+
+ switch (makerStateScenario.traderAssetAllowance) {
+ case AllowanceAmountScenario.Higher:
+ break; // Noop since this is already the default
+
+ case AllowanceAmountScenario.TooLow:
+ const tooLowAllowance = makerAssetFillAmount.minus(1);
+ await this.assetWrapper.setProxyAllowanceAsync(
+ signedOrder.makerAddress,
+ signedOrder.makerAssetData,
+ tooLowAllowance,
+ );
+ break;
+
+ case AllowanceAmountScenario.Exact:
+ const exactAllowance = makerAssetFillAmount;
+ await this.assetWrapper.setProxyAllowanceAsync(
+ signedOrder.makerAddress,
+ signedOrder.makerAssetData,
+ exactAllowance,
+ );
+ break;
+
+ case AllowanceAmountScenario.Unlimited:
+ await this.assetWrapper.setProxyAllowanceAsync(
+ signedOrder.makerAddress,
+ signedOrder.makerAssetData,
+ constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
+ );
+ break;
+
+ default:
+ throw errorUtils.spawnSwitchErr(
+ 'makerStateScenario.traderAssetAllowance',
+ makerStateScenario.traderAssetAllowance,
+ );
+ }
+
+ switch (makerStateScenario.zrxFeeAllowance) {
+ case AllowanceAmountScenario.Higher:
+ break; // Noop since this is already the default
+
+ case AllowanceAmountScenario.TooLow:
+ const tooLowAllowance = makerFee.minus(1);
+ await this.assetWrapper.setProxyAllowanceAsync(
+ signedOrder.makerAddress,
+ this.zrxAssetData,
+ tooLowAllowance,
+ );
+ break;
+
+ case AllowanceAmountScenario.Exact:
+ const exactAllowance = makerFee;
+ await this.assetWrapper.setProxyAllowanceAsync(
+ signedOrder.makerAddress,
+ this.zrxAssetData,
+ exactAllowance,
+ );
+ break;
+
+ case AllowanceAmountScenario.Unlimited:
+ await this.assetWrapper.setProxyAllowanceAsync(
+ signedOrder.makerAddress,
+ this.zrxAssetData,
+ constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
+ );
+ break;
+
+ default:
+ throw errorUtils.spawnSwitchErr(
+ 'makerStateScenario.zrxFeeAllowance',
+ makerStateScenario.zrxFeeAllowance,
+ );
+ }
+
+ switch (takerStateScenario.traderAssetBalance) {
+ case BalanceAmountScenario.Higher:
+ break; // Noop since this is already the default
+
+ case BalanceAmountScenario.TooLow:
+ if (takerAssetFillAmount.eq(0)) {
+ throw new Error(`Cannot set takerAssetBalanceOfTaker TooLow if takerAssetFillAmount is 0`);
+ }
+ const tooLowBalance = takerAssetFillAmount.minus(1);
+ await this.assetWrapper.setBalanceAsync(this.takerAddress, signedOrder.takerAssetData, tooLowBalance);
+ break;
+
+ case BalanceAmountScenario.Exact:
+ const exactBalance = takerAssetFillAmount;
+ await this.assetWrapper.setBalanceAsync(this.takerAddress, signedOrder.takerAssetData, exactBalance);
+ break;
+
+ default:
+ throw errorUtils.spawnSwitchErr(
+ 'takerStateScenario.traderAssetBalance',
+ takerStateScenario.traderAssetBalance,
+ );
+ }
+
+ const takerFee = orderUtils.getPartialAmountFloor(
+ takerAssetFillAmount,
+ signedOrder.takerAssetAmount,
+ signedOrder.takerFee,
+ );
+ switch (takerStateScenario.zrxFeeBalance) {
+ case BalanceAmountScenario.Higher:
+ break; // Noop since this is already the default
+
+ case BalanceAmountScenario.TooLow:
+ if (takerFee.eq(0)) {
+ throw new Error(`Cannot set zrxAssetBalanceOfTaker TooLow if takerFee is 0`);
+ }
+ const tooLowBalance = takerFee.minus(1);
+ await this.assetWrapper.setBalanceAsync(this.takerAddress, this.zrxAssetData, tooLowBalance);
+ break;
+
+ case BalanceAmountScenario.Exact:
+ const exactBalance = takerFee;
+ await this.assetWrapper.setBalanceAsync(this.takerAddress, this.zrxAssetData, exactBalance);
+ break;
+
+ default:
+ throw errorUtils.spawnSwitchErr('takerStateScenario.zrxFeeBalance', takerStateScenario.zrxFeeBalance);
+ }
+
+ switch (takerStateScenario.traderAssetAllowance) {
+ case AllowanceAmountScenario.Higher:
+ break; // Noop since this is already the default
+
+ case AllowanceAmountScenario.TooLow:
+ const tooLowAllowance = takerAssetFillAmount.minus(1);
+ await this.assetWrapper.setProxyAllowanceAsync(
+ this.takerAddress,
+ signedOrder.takerAssetData,
+ tooLowAllowance,
+ );
+ break;
+
+ case AllowanceAmountScenario.Exact:
+ const exactAllowance = takerAssetFillAmount;
+ await this.assetWrapper.setProxyAllowanceAsync(
+ this.takerAddress,
+ signedOrder.takerAssetData,
+ exactAllowance,
+ );
+ break;
+
+ case AllowanceAmountScenario.Unlimited:
+ await this.assetWrapper.setProxyAllowanceAsync(
+ this.takerAddress,
+ signedOrder.takerAssetData,
+ constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
+ );
+ break;
+
+ default:
+ throw errorUtils.spawnSwitchErr(
+ 'takerStateScenario.traderAssetAllowance',
+ takerStateScenario.traderAssetAllowance,
+ );
+ }
+
+ switch (takerStateScenario.zrxFeeAllowance) {
+ case AllowanceAmountScenario.Higher:
+ break; // Noop since this is already the default
+
+ case AllowanceAmountScenario.TooLow:
+ const tooLowAllowance = takerFee.minus(1);
+ await this.assetWrapper.setProxyAllowanceAsync(this.takerAddress, this.zrxAssetData, tooLowAllowance);
+ break;
+
+ case AllowanceAmountScenario.Exact:
+ const exactAllowance = takerFee;
+ await this.assetWrapper.setProxyAllowanceAsync(this.takerAddress, this.zrxAssetData, exactAllowance);
+ break;
+
+ case AllowanceAmountScenario.Unlimited:
+ await this.assetWrapper.setProxyAllowanceAsync(
+ this.takerAddress,
+ this.zrxAssetData,
+ constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
+ );
+ break;
+
+ default:
+ throw errorUtils.spawnSwitchErr(
+ 'takerStateScenario.zrxFeeAllowance',
+ takerStateScenario.zrxFeeAllowance,
+ );
+ }
+ }
+} // tslint:disable:max-file-line-count
diff --git a/contracts/protocol/test/utils/index.ts b/contracts/protocol/test/utils/index.ts
new file mode 100644
index 000000000..75cd88666
--- /dev/null
+++ b/contracts/protocol/test/utils/index.ts
@@ -0,0 +1,3 @@
+export * from './exchange_wrapper';
+export * from './erc20_wrapper';
+export * from './erc721_wrapper';
diff --git a/contracts/protocol/test/utils/match_order_tester.ts b/contracts/protocol/test/utils/match_order_tester.ts
new file mode 100644
index 000000000..8f574704e
--- /dev/null
+++ b/contracts/protocol/test/utils/match_order_tester.ts
@@ -0,0 +1,562 @@
+import {
+ chaiSetup,
+ ERC20BalancesByOwner,
+ ERC721TokenIdsByOwner,
+ OrderInfo,
+ OrderStatus,
+ TransferAmountsByMatchOrders as TransferAmounts,
+ TransferAmountsLoggedByMatchOrders as LoggedTransferAmounts,
+} from '@0x/contracts-test-utils';
+import { assetDataUtils, orderHashUtils } from '@0x/order-utils';
+import { AssetProxyId, SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+
+import { TransactionReceiptWithDecodedLogs } from '../../../../node_modules/ethereum-types';
+
+import { ERC20Wrapper } from './erc20_wrapper';
+import { ERC721Wrapper } from './erc721_wrapper';
+import { ExchangeWrapper } from './exchange_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+export class MatchOrderTester {
+ private readonly _exchangeWrapper: ExchangeWrapper;
+ private readonly _erc20Wrapper: ERC20Wrapper;
+ private readonly _erc721Wrapper: ERC721Wrapper;
+ private readonly _feeTokenAddress: string;
+ /// @dev Checks values from the logs produced by Exchange.matchOrders against the expected transfer amounts.
+ /// Values include the amounts transferred from the left/right makers and taker, along with
+ /// the fees paid on each matched order. These are also the return values of MatchOrders.
+ /// @param signedOrderLeft First matched order.
+ /// @param signedOrderRight Second matched order.
+ /// @param transactionReceipt Transaction receipt and logs produced by Exchange.matchOrders.
+ /// @param takerAddress Address of taker (account that called Exchange.matchOrders)
+ /// @param expectedTransferAmounts Expected amounts transferred as a result of order matching.
+ private static async _assertLogsAsync(
+ signedOrderLeft: SignedOrder,
+ signedOrderRight: SignedOrder,
+ transactionReceipt: TransactionReceiptWithDecodedLogs,
+ takerAddress: string,
+ expectedTransferAmounts: TransferAmounts,
+ ): Promise<void> {
+ // Should have two fill event logs -- one for each order.
+ const transactionFillLogs = _.filter(transactionReceipt.logs, ['event', 'Fill']);
+ expect(transactionFillLogs.length, 'Checking number of logs').to.be.equal(2);
+ // First log is for left fill
+ const leftLog = (transactionFillLogs[0] as any).args as LoggedTransferAmounts;
+ expect(leftLog.makerAddress, 'Checking logged maker address of left order').to.be.equal(
+ signedOrderLeft.makerAddress,
+ );
+ expect(leftLog.takerAddress, 'Checking logged taker address of right order').to.be.equal(takerAddress);
+ const amountBoughtByLeftMaker = new BigNumber(leftLog.takerAssetFilledAmount);
+ const amountSoldByLeftMaker = new BigNumber(leftLog.makerAssetFilledAmount);
+ const feePaidByLeftMaker = new BigNumber(leftLog.makerFeePaid);
+ const feePaidByTakerLeft = new BigNumber(leftLog.takerFeePaid);
+ // Second log is for right fill
+ const rightLog = (transactionFillLogs[1] as any).args as LoggedTransferAmounts;
+ expect(rightLog.makerAddress, 'Checking logged maker address of right order').to.be.equal(
+ signedOrderRight.makerAddress,
+ );
+ expect(rightLog.takerAddress, 'Checking loggerd taker address of right order').to.be.equal(takerAddress);
+ const amountBoughtByRightMaker = new BigNumber(rightLog.takerAssetFilledAmount);
+ const amountSoldByRightMaker = new BigNumber(rightLog.makerAssetFilledAmount);
+ const feePaidByRightMaker = new BigNumber(rightLog.makerFeePaid);
+ const feePaidByTakerRight = new BigNumber(rightLog.takerFeePaid);
+ // Derive amount received by taker
+ const amountReceivedByTaker = amountSoldByLeftMaker.sub(amountBoughtByRightMaker);
+ // Assert log values - left order
+ expect(amountBoughtByLeftMaker, 'Checking logged amount bought by left maker').to.be.bignumber.equal(
+ expectedTransferAmounts.amountBoughtByLeftMaker,
+ );
+ expect(amountSoldByLeftMaker, 'Checking logged amount sold by left maker').to.be.bignumber.equal(
+ expectedTransferAmounts.amountSoldByLeftMaker,
+ );
+ expect(feePaidByLeftMaker, 'Checking logged fee paid by left maker').to.be.bignumber.equal(
+ expectedTransferAmounts.feePaidByLeftMaker,
+ );
+ expect(feePaidByTakerLeft, 'Checking logged fee paid on left order by taker').to.be.bignumber.equal(
+ expectedTransferAmounts.feePaidByTakerLeft,
+ );
+ // Assert log values - right order
+ expect(amountBoughtByRightMaker, 'Checking logged amount bought by right maker').to.be.bignumber.equal(
+ expectedTransferAmounts.amountBoughtByRightMaker,
+ );
+ expect(amountSoldByRightMaker, 'Checking logged amount sold by right maker').to.be.bignumber.equal(
+ expectedTransferAmounts.amountSoldByRightMaker,
+ );
+ expect(feePaidByRightMaker, 'Checking logged fee paid by right maker').to.be.bignumber.equal(
+ expectedTransferAmounts.feePaidByRightMaker,
+ );
+ expect(feePaidByTakerRight, 'Checking logged fee paid on right order by taker').to.be.bignumber.equal(
+ expectedTransferAmounts.feePaidByTakerRight,
+ );
+ // Assert derived amount received by taker
+ expect(amountReceivedByTaker, 'Checking logged amount received by taker').to.be.bignumber.equal(
+ expectedTransferAmounts.amountReceivedByTaker,
+ );
+ }
+ /// @dev Asserts all expected ERC20 and ERC721 account holdings match the real holdings.
+ /// @param expectedERC20BalancesByOwner Expected ERC20 balances.
+ /// @param realERC20BalancesByOwner Real ERC20 balances.
+ /// @param expectedERC721TokenIdsByOwner Expected ERC721 token owners.
+ /// @param realERC721TokenIdsByOwner Real ERC20 token owners.
+ private static async _assertAllKnownBalancesAsync(
+ expectedERC20BalancesByOwner: ERC20BalancesByOwner,
+ realERC20BalancesByOwner: ERC20BalancesByOwner,
+ expectedERC721TokenIdsByOwner: ERC721TokenIdsByOwner,
+ realERC721TokenIdsByOwner: ERC721TokenIdsByOwner,
+ ): Promise<void> {
+ // ERC20 Balances
+ const areERC20BalancesEqual = _.isEqual(expectedERC20BalancesByOwner, realERC20BalancesByOwner);
+ expect(areERC20BalancesEqual, 'Checking all known ERC20 account balances').to.be.true();
+ // ERC721 Token Ids
+ const sortedExpectedNewERC721TokenIdsByOwner = _.mapValues(expectedERC721TokenIdsByOwner, tokenIdsByOwner => {
+ _.mapValues(tokenIdsByOwner, tokenIds => {
+ _.sortBy(tokenIds);
+ });
+ });
+ const sortedNewERC721TokenIdsByOwner = _.mapValues(realERC721TokenIdsByOwner, tokenIdsByOwner => {
+ _.mapValues(tokenIdsByOwner, tokenIds => {
+ _.sortBy(tokenIds);
+ });
+ });
+ const areERC721TokenIdsEqual = _.isEqual(
+ sortedExpectedNewERC721TokenIdsByOwner,
+ sortedNewERC721TokenIdsByOwner,
+ );
+ expect(areERC721TokenIdsEqual, 'Checking all known ERC721 account balances').to.be.true();
+ }
+ /// @dev Constructs new MatchOrderTester.
+ /// @param exchangeWrapper Used to call to the Exchange.
+ /// @param erc20Wrapper Used to fetch ERC20 balances.
+ /// @param erc721Wrapper Used to fetch ERC721 token owners.
+ /// @param feeTokenAddress Address of ERC20 fee token.
+ constructor(
+ exchangeWrapper: ExchangeWrapper,
+ erc20Wrapper: ERC20Wrapper,
+ erc721Wrapper: ERC721Wrapper,
+ feeTokenAddress: string,
+ ) {
+ this._exchangeWrapper = exchangeWrapper;
+ this._erc20Wrapper = erc20Wrapper;
+ this._erc721Wrapper = erc721Wrapper;
+ this._feeTokenAddress = feeTokenAddress;
+ }
+ /// @dev Matches two complementary orders and asserts results.
+ /// @param signedOrderLeft First matched order.
+ /// @param signedOrderRight Second matched order.
+ /// @param takerAddress Address of taker (the address who matched the two orders)
+ /// @param erc20BalancesByOwner Current ERC20 balances.
+ /// @param erc721TokenIdsByOwner Current ERC721 token owners.
+ /// @param expectedTransferAmounts Expected amounts transferred as a result of order matching.
+ /// @param initialLeftOrderFilledAmount How much left order has been filled, prior to matching orders.
+ /// @param initialRightOrderFilledAmount How much the right order has been filled, prior to matching orders.
+ /// @return New ERC20 balances & ERC721 token owners.
+ public async matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft: SignedOrder,
+ signedOrderRight: SignedOrder,
+ takerAddress: string,
+ erc20BalancesByOwner: ERC20BalancesByOwner,
+ erc721TokenIdsByOwner: ERC721TokenIdsByOwner,
+ expectedTransferAmounts: TransferAmounts,
+ initialLeftOrderFilledAmount: BigNumber = new BigNumber(0),
+ initialRightOrderFilledAmount: BigNumber = new BigNumber(0),
+ ): Promise<[ERC20BalancesByOwner, ERC721TokenIdsByOwner]> {
+ // Assert initial order states
+ await this._assertInitialOrderStatesAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ initialLeftOrderFilledAmount,
+ initialRightOrderFilledAmount,
+ );
+ // Match left & right orders
+ const transactionReceipt = await this._exchangeWrapper.matchOrdersAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ );
+ const newERC20BalancesByOwner = await this._erc20Wrapper.getBalancesAsync();
+ const newERC721TokenIdsByOwner = await this._erc721Wrapper.getBalancesAsync();
+ // Assert logs
+ await MatchOrderTester._assertLogsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ transactionReceipt,
+ takerAddress,
+ expectedTransferAmounts,
+ );
+ // Assert exchange state
+ await this._assertExchangeStateAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ initialLeftOrderFilledAmount,
+ initialRightOrderFilledAmount,
+ expectedTransferAmounts,
+ );
+ // Assert balances of makers, taker, and fee recipients
+ await this._assertBalancesAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ newERC20BalancesByOwner,
+ newERC721TokenIdsByOwner,
+ expectedTransferAmounts,
+ takerAddress,
+ );
+ return [newERC20BalancesByOwner, newERC721TokenIdsByOwner];
+ }
+ /// @dev Asserts initial exchange state for the left and right orders.
+ /// @param signedOrderLeft First matched order.
+ /// @param signedOrderRight Second matched order.
+ /// @param expectedOrderFilledAmountLeft How much left order has been filled, prior to matching orders.
+ /// @param expectedOrderFilledAmountRight How much the right order has been filled, prior to matching orders.
+ private async _assertInitialOrderStatesAsync(
+ signedOrderLeft: SignedOrder,
+ signedOrderRight: SignedOrder,
+ expectedOrderFilledAmountLeft: BigNumber,
+ expectedOrderFilledAmountRight: BigNumber,
+ ): Promise<void> {
+ // Assert left order initial state
+ const orderTakerAssetFilledAmountLeft = await this._exchangeWrapper.getTakerAssetFilledAmountAsync(
+ orderHashUtils.getOrderHashHex(signedOrderLeft),
+ );
+ expect(orderTakerAssetFilledAmountLeft, 'Checking inital state of left order').to.be.bignumber.equal(
+ expectedOrderFilledAmountLeft,
+ );
+ // Assert right order initial state
+ const orderTakerAssetFilledAmountRight = await this._exchangeWrapper.getTakerAssetFilledAmountAsync(
+ orderHashUtils.getOrderHashHex(signedOrderRight),
+ );
+ expect(orderTakerAssetFilledAmountRight, 'Checking inital state of right order').to.be.bignumber.equal(
+ expectedOrderFilledAmountRight,
+ );
+ }
+ /// @dev Asserts the exchange state against the expected amounts transferred by from matching orders.
+ /// @param signedOrderLeft First matched order.
+ /// @param signedOrderRight Second matched order.
+ /// @param initialLeftOrderFilledAmount How much left order has been filled, prior to matching orders.
+ /// @param initialRightOrderFilledAmount How much the right order has been filled, prior to matching orders.
+ /// @return TransferAmounts A struct containing the expected transfer amounts.
+ private async _assertExchangeStateAsync(
+ signedOrderLeft: SignedOrder,
+ signedOrderRight: SignedOrder,
+ initialLeftOrderFilledAmount: BigNumber,
+ initialRightOrderFilledAmount: BigNumber,
+ expectedTransferAmounts: TransferAmounts,
+ ): Promise<void> {
+ // Assert state for left order: amount bought by left maker
+ let amountBoughtByLeftMaker = await this._exchangeWrapper.getTakerAssetFilledAmountAsync(
+ orderHashUtils.getOrderHashHex(signedOrderLeft),
+ );
+ amountBoughtByLeftMaker = amountBoughtByLeftMaker.minus(initialLeftOrderFilledAmount);
+ expect(amountBoughtByLeftMaker, 'Checking exchange state for left order').to.be.bignumber.equal(
+ expectedTransferAmounts.amountBoughtByLeftMaker,
+ );
+ // Assert state for right order: amount bought by right maker
+ let amountBoughtByRightMaker = await this._exchangeWrapper.getTakerAssetFilledAmountAsync(
+ orderHashUtils.getOrderHashHex(signedOrderRight),
+ );
+ amountBoughtByRightMaker = amountBoughtByRightMaker.minus(initialRightOrderFilledAmount);
+ expect(amountBoughtByRightMaker, 'Checking exchange state for right order').to.be.bignumber.equal(
+ expectedTransferAmounts.amountBoughtByRightMaker,
+ );
+ // Assert left order status
+ const maxAmountBoughtByLeftMaker = signedOrderLeft.takerAssetAmount.minus(initialLeftOrderFilledAmount);
+ const leftOrderInfo: OrderInfo = await this._exchangeWrapper.getOrderInfoAsync(signedOrderLeft);
+ const leftExpectedStatus = expectedTransferAmounts.amountBoughtByLeftMaker.equals(maxAmountBoughtByLeftMaker)
+ ? OrderStatus.FULLY_FILLED
+ : OrderStatus.FILLABLE;
+ expect(leftOrderInfo.orderStatus, 'Checking exchange status for left order').to.be.equal(leftExpectedStatus);
+ // Assert right order status
+ const maxAmountBoughtByRightMaker = signedOrderRight.takerAssetAmount.minus(initialRightOrderFilledAmount);
+ const rightOrderInfo: OrderInfo = await this._exchangeWrapper.getOrderInfoAsync(signedOrderRight);
+ const rightExpectedStatus = expectedTransferAmounts.amountBoughtByRightMaker.equals(maxAmountBoughtByRightMaker)
+ ? OrderStatus.FULLY_FILLED
+ : OrderStatus.FILLABLE;
+ expect(rightOrderInfo.orderStatus, 'Checking exchange status for right order').to.be.equal(rightExpectedStatus);
+ }
+ /// @dev Asserts account balances after matching orders.
+ /// @param signedOrderLeft First matched order.
+ /// @param signedOrderRight Second matched order.
+ /// @param initialERC20BalancesByOwner ERC20 balances prior to order matching.
+ /// @param initialERC721TokenIdsByOwner ERC721 token owners prior to order matching.
+ /// @param finalERC20BalancesByOwner ERC20 balances after order matching.
+ /// @param finalERC721TokenIdsByOwner ERC721 token owners after order matching.
+ /// @param expectedTransferAmounts Expected amounts transferred as a result of order matching.
+ /// @param takerAddress Address of taker (account that called Exchange.matchOrders).
+ private async _assertBalancesAsync(
+ signedOrderLeft: SignedOrder,
+ signedOrderRight: SignedOrder,
+ initialERC20BalancesByOwner: ERC20BalancesByOwner,
+ initialERC721TokenIdsByOwner: ERC721TokenIdsByOwner,
+ finalERC20BalancesByOwner: ERC20BalancesByOwner,
+ finalERC721TokenIdsByOwner: ERC721TokenIdsByOwner,
+ expectedTransferAmounts: TransferAmounts,
+ takerAddress: string,
+ ): Promise<void> {
+ let expectedERC20BalancesByOwner: ERC20BalancesByOwner;
+ let expectedERC721TokenIdsByOwner: ERC721TokenIdsByOwner;
+ [expectedERC20BalancesByOwner, expectedERC721TokenIdsByOwner] = this._calculateExpectedBalances(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ initialERC20BalancesByOwner,
+ initialERC721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ // Assert balances of makers, taker, and fee recipients
+ await this._assertMakerTakerAndFeeRecipientBalancesAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ expectedERC20BalancesByOwner,
+ finalERC20BalancesByOwner,
+ expectedERC721TokenIdsByOwner,
+ finalERC721TokenIdsByOwner,
+ takerAddress,
+ );
+ // Assert balances for all known accounts
+ await MatchOrderTester._assertAllKnownBalancesAsync(
+ expectedERC20BalancesByOwner,
+ finalERC20BalancesByOwner,
+ expectedERC721TokenIdsByOwner,
+ finalERC721TokenIdsByOwner,
+ );
+ }
+ /// @dev Calculates the expected balances of order makers, fee recipients, and the taker,
+ /// as a result of matching two orders.
+ /// @param signedOrderRight First matched order.
+ /// @param signedOrderRight Second matched order.
+ /// @param takerAddress Address of taker (the address who matched the two orders)
+ /// @param erc20BalancesByOwner Current ERC20 balances.
+ /// @param erc721TokenIdsByOwner Current ERC721 token owners.
+ /// @param expectedTransferAmounts Expected amounts transferred as a result of order matching.
+ /// @return Expected ERC20 balances & ERC721 token owners after orders have been matched.
+ private _calculateExpectedBalances(
+ signedOrderLeft: SignedOrder,
+ signedOrderRight: SignedOrder,
+ takerAddress: string,
+ erc20BalancesByOwner: ERC20BalancesByOwner,
+ erc721TokenIdsByOwner: ERC721TokenIdsByOwner,
+ expectedTransferAmounts: TransferAmounts,
+ ): [ERC20BalancesByOwner, ERC721TokenIdsByOwner] {
+ const makerAddressLeft = signedOrderLeft.makerAddress;
+ const makerAddressRight = signedOrderRight.makerAddress;
+ const feeRecipientAddressLeft = signedOrderLeft.feeRecipientAddress;
+ const feeRecipientAddressRight = signedOrderRight.feeRecipientAddress;
+ // Operations are performed on copies of the balances
+ const expectedNewERC20BalancesByOwner = _.cloneDeep(erc20BalancesByOwner);
+ const expectedNewERC721TokenIdsByOwner = _.cloneDeep(erc721TokenIdsByOwner);
+ // Left Maker Asset (Right Taker Asset)
+ const makerAssetProxyIdLeft = assetDataUtils.decodeAssetProxyId(signedOrderLeft.makerAssetData);
+ if (makerAssetProxyIdLeft === AssetProxyId.ERC20) {
+ // Decode asset data
+ const erc20AssetData = assetDataUtils.decodeERC20AssetData(signedOrderLeft.makerAssetData);
+ const makerAssetAddressLeft = erc20AssetData.tokenAddress;
+ const takerAssetAddressRight = makerAssetAddressLeft;
+ // Left Maker
+ expectedNewERC20BalancesByOwner[makerAddressLeft][makerAssetAddressLeft] = expectedNewERC20BalancesByOwner[
+ makerAddressLeft
+ ][makerAssetAddressLeft].minus(expectedTransferAmounts.amountSoldByLeftMaker);
+ // Right Maker
+ expectedNewERC20BalancesByOwner[makerAddressRight][
+ takerAssetAddressRight
+ ] = expectedNewERC20BalancesByOwner[makerAddressRight][takerAssetAddressRight].add(
+ expectedTransferAmounts.amountBoughtByRightMaker,
+ );
+ // Taker
+ expectedNewERC20BalancesByOwner[takerAddress][makerAssetAddressLeft] = expectedNewERC20BalancesByOwner[
+ takerAddress
+ ][makerAssetAddressLeft].add(expectedTransferAmounts.amountReceivedByTaker);
+ } else if (makerAssetProxyIdLeft === AssetProxyId.ERC721) {
+ // Decode asset data
+ const erc721AssetData = assetDataUtils.decodeERC721AssetData(signedOrderLeft.makerAssetData);
+ const makerAssetAddressLeft = erc721AssetData.tokenAddress;
+ const makerAssetIdLeft = erc721AssetData.tokenId;
+ const takerAssetAddressRight = makerAssetAddressLeft;
+ const takerAssetIdRight = makerAssetIdLeft;
+ // Left Maker
+ _.remove(expectedNewERC721TokenIdsByOwner[makerAddressLeft][makerAssetAddressLeft], makerAssetIdLeft);
+ // Right Maker
+ expectedNewERC721TokenIdsByOwner[makerAddressRight][takerAssetAddressRight].push(takerAssetIdRight);
+ // Taker: Since there is only 1 asset transferred, the taker does not receive any of the left maker asset.
+ }
+ // Left Taker Asset (Right Maker Asset)
+ // Note: This exchange is only between the order makers: the Taker does not receive any of the left taker asset.
+ const takerAssetProxyIdLeft = assetDataUtils.decodeAssetProxyId(signedOrderLeft.takerAssetData);
+ if (takerAssetProxyIdLeft === AssetProxyId.ERC20) {
+ // Decode asset data
+ const erc20AssetData = assetDataUtils.decodeERC20AssetData(signedOrderLeft.takerAssetData);
+ const takerAssetAddressLeft = erc20AssetData.tokenAddress;
+ const makerAssetAddressRight = takerAssetAddressLeft;
+ // Left Maker
+ expectedNewERC20BalancesByOwner[makerAddressLeft][takerAssetAddressLeft] = expectedNewERC20BalancesByOwner[
+ makerAddressLeft
+ ][takerAssetAddressLeft].add(expectedTransferAmounts.amountBoughtByLeftMaker);
+ // Right Maker
+ expectedNewERC20BalancesByOwner[makerAddressRight][
+ makerAssetAddressRight
+ ] = expectedNewERC20BalancesByOwner[makerAddressRight][makerAssetAddressRight].minus(
+ expectedTransferAmounts.amountSoldByRightMaker,
+ );
+ } else if (takerAssetProxyIdLeft === AssetProxyId.ERC721) {
+ // Decode asset data
+ const erc721AssetData = assetDataUtils.decodeERC721AssetData(signedOrderRight.makerAssetData);
+ const makerAssetAddressRight = erc721AssetData.tokenAddress;
+ const makerAssetIdRight = erc721AssetData.tokenId;
+ const takerAssetAddressLeft = makerAssetAddressRight;
+ const takerAssetIdLeft = makerAssetIdRight;
+ // Right Maker
+ _.remove(expectedNewERC721TokenIdsByOwner[makerAddressRight][makerAssetAddressRight], makerAssetIdRight);
+ // Left Maker
+ expectedNewERC721TokenIdsByOwner[makerAddressLeft][takerAssetAddressLeft].push(takerAssetIdLeft);
+ }
+ // Left Maker Fees
+ expectedNewERC20BalancesByOwner[makerAddressLeft][this._feeTokenAddress] = expectedNewERC20BalancesByOwner[
+ makerAddressLeft
+ ][this._feeTokenAddress].minus(expectedTransferAmounts.feePaidByLeftMaker);
+ // Right Maker Fees
+ expectedNewERC20BalancesByOwner[makerAddressRight][this._feeTokenAddress] = expectedNewERC20BalancesByOwner[
+ makerAddressRight
+ ][this._feeTokenAddress].minus(expectedTransferAmounts.feePaidByRightMaker);
+ // Taker Fees
+ expectedNewERC20BalancesByOwner[takerAddress][this._feeTokenAddress] = expectedNewERC20BalancesByOwner[
+ takerAddress
+ ][this._feeTokenAddress].minus(
+ expectedTransferAmounts.feePaidByTakerLeft.add(expectedTransferAmounts.feePaidByTakerRight),
+ );
+ // Left Fee Recipient Fees
+ expectedNewERC20BalancesByOwner[feeRecipientAddressLeft][
+ this._feeTokenAddress
+ ] = expectedNewERC20BalancesByOwner[feeRecipientAddressLeft][this._feeTokenAddress].add(
+ expectedTransferAmounts.feePaidByLeftMaker.add(expectedTransferAmounts.feePaidByTakerLeft),
+ );
+ // Right Fee Recipient Fees
+ expectedNewERC20BalancesByOwner[feeRecipientAddressRight][
+ this._feeTokenAddress
+ ] = expectedNewERC20BalancesByOwner[feeRecipientAddressRight][this._feeTokenAddress].add(
+ expectedTransferAmounts.feePaidByRightMaker.add(expectedTransferAmounts.feePaidByTakerRight),
+ );
+
+ return [expectedNewERC20BalancesByOwner, expectedNewERC721TokenIdsByOwner];
+ }
+ /// @dev Asserts ERC20 account balances and ERC721 token holdings that result from order matching.
+ /// Specifically checks balances of makers, taker and fee recipients.
+ /// @param signedOrderLeft First matched order.
+ /// @param signedOrderRight Second matched order.
+ /// @param expectedERC20BalancesByOwner Expected ERC20 balances.
+ /// @param realERC20BalancesByOwner Real ERC20 balances.
+ /// @param expectedERC721TokenIdsByOwner Expected ERC721 token owners.
+ /// @param realERC721TokenIdsByOwner Real ERC20 token owners.
+ /// @param takerAddress Address of taker (account that called Exchange.matchOrders).
+ private async _assertMakerTakerAndFeeRecipientBalancesAsync(
+ signedOrderLeft: SignedOrder,
+ signedOrderRight: SignedOrder,
+ expectedERC20BalancesByOwner: ERC20BalancesByOwner,
+ realERC20BalancesByOwner: ERC20BalancesByOwner,
+ expectedERC721TokenIdsByOwner: ERC721TokenIdsByOwner,
+ realERC721TokenIdsByOwner: ERC721TokenIdsByOwner,
+ takerAddress: string,
+ ): Promise<void> {
+ // Individual balance comparisons
+ const makerAssetProxyIdLeft = assetDataUtils.decodeAssetProxyId(signedOrderLeft.makerAssetData);
+ const makerERC20AssetDataLeft =
+ makerAssetProxyIdLeft === AssetProxyId.ERC20
+ ? assetDataUtils.decodeERC20AssetData(signedOrderLeft.makerAssetData)
+ : assetDataUtils.decodeERC721AssetData(signedOrderLeft.makerAssetData);
+ const makerAssetAddressLeft = makerERC20AssetDataLeft.tokenAddress;
+ const makerAssetProxyIdRight = assetDataUtils.decodeAssetProxyId(signedOrderRight.makerAssetData);
+ const makerERC20AssetDataRight =
+ makerAssetProxyIdRight === AssetProxyId.ERC20
+ ? assetDataUtils.decodeERC20AssetData(signedOrderRight.makerAssetData)
+ : assetDataUtils.decodeERC721AssetData(signedOrderRight.makerAssetData);
+ const makerAssetAddressRight = makerERC20AssetDataRight.tokenAddress;
+ if (makerAssetProxyIdLeft === AssetProxyId.ERC20) {
+ expect(
+ realERC20BalancesByOwner[signedOrderLeft.makerAddress][makerAssetAddressLeft],
+ 'Checking left maker egress ERC20 account balance',
+ ).to.be.bignumber.equal(expectedERC20BalancesByOwner[signedOrderLeft.makerAddress][makerAssetAddressLeft]);
+ expect(
+ realERC20BalancesByOwner[signedOrderRight.makerAddress][makerAssetAddressLeft],
+ 'Checking right maker ingress ERC20 account balance',
+ ).to.be.bignumber.equal(expectedERC20BalancesByOwner[signedOrderRight.makerAddress][makerAssetAddressLeft]);
+ expect(
+ realERC20BalancesByOwner[takerAddress][makerAssetAddressLeft],
+ 'Checking taker ingress ERC20 account balance',
+ ).to.be.bignumber.equal(expectedERC20BalancesByOwner[takerAddress][makerAssetAddressLeft]);
+ } else if (makerAssetProxyIdLeft === AssetProxyId.ERC721) {
+ expect(
+ realERC721TokenIdsByOwner[signedOrderLeft.makerAddress][makerAssetAddressLeft].sort(),
+ 'Checking left maker egress ERC721 account holdings',
+ ).to.be.deep.equal(
+ expectedERC721TokenIdsByOwner[signedOrderLeft.makerAddress][makerAssetAddressLeft].sort(),
+ );
+ expect(
+ realERC721TokenIdsByOwner[signedOrderRight.makerAddress][makerAssetAddressLeft].sort(),
+ 'Checking right maker ERC721 account holdings',
+ ).to.be.deep.equal(
+ expectedERC721TokenIdsByOwner[signedOrderRight.makerAddress][makerAssetAddressLeft].sort(),
+ );
+ expect(
+ realERC721TokenIdsByOwner[takerAddress][makerAssetAddressLeft].sort(),
+ 'Checking taker ingress ERC721 account holdings',
+ ).to.be.deep.equal(expectedERC721TokenIdsByOwner[takerAddress][makerAssetAddressLeft].sort());
+ } else {
+ throw new Error(`Unhandled Asset Proxy ID: ${makerAssetProxyIdLeft}`);
+ }
+ if (makerAssetProxyIdRight === AssetProxyId.ERC20) {
+ expect(
+ realERC20BalancesByOwner[signedOrderLeft.makerAddress][makerAssetAddressRight],
+ 'Checking left maker ingress ERC20 account balance',
+ ).to.be.bignumber.equal(expectedERC20BalancesByOwner[signedOrderLeft.makerAddress][makerAssetAddressRight]);
+ expect(
+ realERC20BalancesByOwner[signedOrderRight.makerAddress][makerAssetAddressRight],
+ 'Checking right maker egress ERC20 account balance',
+ ).to.be.bignumber.equal(
+ expectedERC20BalancesByOwner[signedOrderRight.makerAddress][makerAssetAddressRight],
+ );
+ } else if (makerAssetProxyIdRight === AssetProxyId.ERC721) {
+ expect(
+ realERC721TokenIdsByOwner[signedOrderLeft.makerAddress][makerAssetAddressRight].sort(),
+ 'Checking left maker ingress ERC721 account holdings',
+ ).to.be.deep.equal(
+ expectedERC721TokenIdsByOwner[signedOrderLeft.makerAddress][makerAssetAddressRight].sort(),
+ );
+ expect(
+ realERC721TokenIdsByOwner[signedOrderRight.makerAddress][makerAssetAddressRight],
+ 'Checking right maker agress ERC721 account holdings',
+ ).to.be.deep.equal(expectedERC721TokenIdsByOwner[signedOrderRight.makerAddress][makerAssetAddressRight]);
+ } else {
+ throw new Error(`Unhandled Asset Proxy ID: ${makerAssetProxyIdRight}`);
+ }
+ // Paid fees
+ expect(
+ realERC20BalancesByOwner[signedOrderLeft.makerAddress][this._feeTokenAddress],
+ 'Checking left maker egress ERC20 account fees',
+ ).to.be.bignumber.equal(expectedERC20BalancesByOwner[signedOrderLeft.makerAddress][this._feeTokenAddress]);
+ expect(
+ realERC20BalancesByOwner[signedOrderRight.makerAddress][this._feeTokenAddress],
+ 'Checking right maker egress ERC20 account fees',
+ ).to.be.bignumber.equal(expectedERC20BalancesByOwner[signedOrderRight.makerAddress][this._feeTokenAddress]);
+ expect(
+ realERC20BalancesByOwner[takerAddress][this._feeTokenAddress],
+ 'Checking taker egress ERC20 account fees',
+ ).to.be.bignumber.equal(expectedERC20BalancesByOwner[takerAddress][this._feeTokenAddress]);
+ // Received fees
+ expect(
+ realERC20BalancesByOwner[signedOrderLeft.feeRecipientAddress][this._feeTokenAddress],
+ 'Checking left fee recipient ingress ERC20 account fees',
+ ).to.be.bignumber.equal(
+ expectedERC20BalancesByOwner[signedOrderLeft.feeRecipientAddress][this._feeTokenAddress],
+ );
+ expect(
+ realERC20BalancesByOwner[signedOrderRight.feeRecipientAddress][this._feeTokenAddress],
+ 'Checking right fee receipient ingress ERC20 account fees',
+ ).to.be.bignumber.equal(
+ expectedERC20BalancesByOwner[signedOrderRight.feeRecipientAddress][this._feeTokenAddress],
+ );
+ }
+} // tslint:disable-line:max-file-line-count
diff --git a/contracts/protocol/test/utils/order_factory_from_scenario.ts b/contracts/protocol/test/utils/order_factory_from_scenario.ts
new file mode 100644
index 000000000..ecb4b2e28
--- /dev/null
+++ b/contracts/protocol/test/utils/order_factory_from_scenario.ts
@@ -0,0 +1,294 @@
+import {
+ AssetDataScenario,
+ constants,
+ ERC721TokenIdsByOwner,
+ ExpirationTimeSecondsScenario,
+ FeeRecipientAddressScenario,
+ OrderAssetAmountScenario,
+ OrderScenario,
+ TakerScenario,
+} from '@0x/contracts-test-utils';
+import { DummyERC721TokenContract } from '@0x/contracts-tokens';
+import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils';
+import { Order } from '@0x/types';
+import { BigNumber, errorUtils } from '@0x/utils';
+
+const TEN_UNITS_EIGHTEEN_DECIMALS = new BigNumber(10_000_000_000_000_000_000);
+const FIVE_UNITS_EIGHTEEN_DECIMALS = new BigNumber(5_000_000_000_000_000_000);
+const POINT_ONE_UNITS_EIGHTEEN_DECIMALS = new BigNumber(100_000_000_000_000_000);
+const POINT_ZERO_FIVE_UNITS_EIGHTEEN_DECIMALS = new BigNumber(50_000_000_000_000_000);
+const TEN_UNITS_FIVE_DECIMALS = new BigNumber(1_000_000);
+const FIVE_UNITS_FIVE_DECIMALS = new BigNumber(500_000);
+const TEN_UNITS_ZERO_DECIMALS = new BigNumber(10);
+const ONE_THOUSAND_UNITS_ZERO_DECIMALS = new BigNumber(1000);
+const ONE_NFT_UNIT = new BigNumber(1);
+
+export class OrderFactoryFromScenario {
+ private readonly _userAddresses: string[];
+ private readonly _zrxAddress: string;
+ private readonly _nonZrxERC20EighteenDecimalTokenAddresses: string[];
+ private readonly _erc20FiveDecimalTokenAddresses: string[];
+ private readonly _erc20ZeroDecimalTokenAddresses: string[];
+ private readonly _erc721Token: DummyERC721TokenContract;
+ private readonly _erc721Balances: ERC721TokenIdsByOwner;
+ private readonly _exchangeAddress: string;
+ constructor(
+ userAddresses: string[],
+ zrxAddress: string,
+ nonZrxERC20EighteenDecimalTokenAddresses: string[],
+ erc20FiveDecimalTokenAddresses: string[],
+ erc20ZeroDecimalTokenAddresses: string[],
+ erc721Token: DummyERC721TokenContract,
+ erc721Balances: ERC721TokenIdsByOwner,
+ exchangeAddress: string,
+ ) {
+ this._userAddresses = userAddresses;
+ this._zrxAddress = zrxAddress;
+ this._nonZrxERC20EighteenDecimalTokenAddresses = nonZrxERC20EighteenDecimalTokenAddresses;
+ this._erc20FiveDecimalTokenAddresses = erc20FiveDecimalTokenAddresses;
+ this._erc20ZeroDecimalTokenAddresses = erc20ZeroDecimalTokenAddresses;
+ this._erc721Token = erc721Token;
+ this._erc721Balances = erc721Balances;
+ this._exchangeAddress = exchangeAddress;
+ }
+ public generateOrder(orderScenario: OrderScenario): Order {
+ const makerAddress = this._userAddresses[1];
+ let takerAddress = this._userAddresses[2];
+ const erc721MakerAssetIds = this._erc721Balances[makerAddress][this._erc721Token.address];
+ const erc721TakerAssetIds = this._erc721Balances[takerAddress][this._erc721Token.address];
+ let feeRecipientAddress;
+ let makerAssetAmount;
+ let takerAssetAmount;
+ let makerFee;
+ let takerFee;
+ let expirationTimeSeconds;
+ let makerAssetData;
+ let takerAssetData;
+
+ switch (orderScenario.feeRecipientScenario) {
+ case FeeRecipientAddressScenario.BurnAddress:
+ feeRecipientAddress = constants.NULL_ADDRESS;
+ break;
+ case FeeRecipientAddressScenario.EthUserAddress:
+ feeRecipientAddress = this._userAddresses[4];
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('FeeRecipientAddressScenario', orderScenario.feeRecipientScenario);
+ }
+
+ switch (orderScenario.makerAssetDataScenario) {
+ case AssetDataScenario.ZRXFeeToken:
+ makerAssetData = assetDataUtils.encodeERC20AssetData(this._zrxAddress);
+ break;
+ case AssetDataScenario.ERC20NonZRXEighteenDecimals:
+ makerAssetData = assetDataUtils.encodeERC20AssetData(this._nonZrxERC20EighteenDecimalTokenAddresses[0]);
+ break;
+ case AssetDataScenario.ERC20FiveDecimals:
+ makerAssetData = assetDataUtils.encodeERC20AssetData(this._erc20FiveDecimalTokenAddresses[0]);
+ break;
+ case AssetDataScenario.ERC721:
+ makerAssetData = assetDataUtils.encodeERC721AssetData(
+ this._erc721Token.address,
+ erc721MakerAssetIds[0],
+ );
+ break;
+ case AssetDataScenario.ERC20ZeroDecimals:
+ makerAssetData = assetDataUtils.encodeERC20AssetData(this._erc20ZeroDecimalTokenAddresses[0]);
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario);
+ }
+
+ switch (orderScenario.takerAssetDataScenario) {
+ case AssetDataScenario.ZRXFeeToken:
+ takerAssetData = assetDataUtils.encodeERC20AssetData(this._zrxAddress);
+ break;
+ case AssetDataScenario.ERC20NonZRXEighteenDecimals:
+ takerAssetData = assetDataUtils.encodeERC20AssetData(this._nonZrxERC20EighteenDecimalTokenAddresses[1]);
+ break;
+ case AssetDataScenario.ERC20FiveDecimals:
+ takerAssetData = assetDataUtils.encodeERC20AssetData(this._erc20FiveDecimalTokenAddresses[1]);
+ break;
+ case AssetDataScenario.ERC721:
+ takerAssetData = assetDataUtils.encodeERC721AssetData(
+ this._erc721Token.address,
+ erc721TakerAssetIds[0],
+ );
+ break;
+ case AssetDataScenario.ERC20ZeroDecimals:
+ takerAssetData = assetDataUtils.encodeERC20AssetData(this._erc20ZeroDecimalTokenAddresses[1]);
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.takerAssetDataScenario);
+ }
+
+ switch (orderScenario.makerAssetAmountScenario) {
+ case OrderAssetAmountScenario.Large:
+ switch (orderScenario.makerAssetDataScenario) {
+ case AssetDataScenario.ZRXFeeToken:
+ case AssetDataScenario.ERC20NonZRXEighteenDecimals:
+ makerAssetAmount = TEN_UNITS_EIGHTEEN_DECIMALS;
+ break;
+ case AssetDataScenario.ERC20FiveDecimals:
+ makerAssetAmount = TEN_UNITS_FIVE_DECIMALS;
+ break;
+ case AssetDataScenario.ERC721:
+ makerAssetAmount = ONE_NFT_UNIT;
+ break;
+ case AssetDataScenario.ERC20ZeroDecimals:
+ makerAssetAmount = ONE_THOUSAND_UNITS_ZERO_DECIMALS;
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario);
+ }
+ break;
+ case OrderAssetAmountScenario.Small:
+ switch (orderScenario.makerAssetDataScenario) {
+ case AssetDataScenario.ZRXFeeToken:
+ case AssetDataScenario.ERC20NonZRXEighteenDecimals:
+ makerAssetAmount = FIVE_UNITS_EIGHTEEN_DECIMALS;
+ break;
+ case AssetDataScenario.ERC20FiveDecimals:
+ makerAssetAmount = FIVE_UNITS_FIVE_DECIMALS;
+ break;
+ case AssetDataScenario.ERC721:
+ makerAssetAmount = ONE_NFT_UNIT;
+ break;
+ case AssetDataScenario.ERC20ZeroDecimals:
+ makerAssetAmount = TEN_UNITS_ZERO_DECIMALS;
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario);
+ }
+ break;
+ case OrderAssetAmountScenario.Zero:
+ makerAssetAmount = new BigNumber(0);
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('OrderAssetAmountScenario', orderScenario.makerAssetAmountScenario);
+ }
+
+ switch (orderScenario.takerAssetAmountScenario) {
+ case OrderAssetAmountScenario.Large:
+ switch (orderScenario.takerAssetDataScenario) {
+ case AssetDataScenario.ERC20NonZRXEighteenDecimals:
+ case AssetDataScenario.ZRXFeeToken:
+ takerAssetAmount = TEN_UNITS_EIGHTEEN_DECIMALS;
+ break;
+ case AssetDataScenario.ERC20FiveDecimals:
+ takerAssetAmount = TEN_UNITS_FIVE_DECIMALS;
+ break;
+ case AssetDataScenario.ERC721:
+ takerAssetAmount = ONE_NFT_UNIT;
+ break;
+ case AssetDataScenario.ERC20ZeroDecimals:
+ takerAssetAmount = ONE_THOUSAND_UNITS_ZERO_DECIMALS;
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.takerAssetDataScenario);
+ }
+ break;
+ case OrderAssetAmountScenario.Small:
+ switch (orderScenario.takerAssetDataScenario) {
+ case AssetDataScenario.ERC20NonZRXEighteenDecimals:
+ case AssetDataScenario.ZRXFeeToken:
+ takerAssetAmount = FIVE_UNITS_EIGHTEEN_DECIMALS;
+ break;
+ case AssetDataScenario.ERC20FiveDecimals:
+ takerAssetAmount = FIVE_UNITS_FIVE_DECIMALS;
+ break;
+ case AssetDataScenario.ERC721:
+ takerAssetAmount = ONE_NFT_UNIT;
+ break;
+ case AssetDataScenario.ERC20ZeroDecimals:
+ takerAssetAmount = TEN_UNITS_ZERO_DECIMALS;
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.takerAssetDataScenario);
+ }
+ break;
+ case OrderAssetAmountScenario.Zero:
+ takerAssetAmount = new BigNumber(0);
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('OrderAssetAmountScenario', orderScenario.takerAssetAmountScenario);
+ }
+
+ switch (orderScenario.makerFeeScenario) {
+ case OrderAssetAmountScenario.Large:
+ makerFee = POINT_ONE_UNITS_EIGHTEEN_DECIMALS;
+ break;
+ case OrderAssetAmountScenario.Small:
+ makerFee = POINT_ZERO_FIVE_UNITS_EIGHTEEN_DECIMALS;
+ break;
+ case OrderAssetAmountScenario.Zero:
+ makerFee = new BigNumber(0);
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('OrderAssetAmountScenario', orderScenario.makerFeeScenario);
+ }
+
+ switch (orderScenario.takerFeeScenario) {
+ case OrderAssetAmountScenario.Large:
+ takerFee = POINT_ONE_UNITS_EIGHTEEN_DECIMALS;
+ break;
+ case OrderAssetAmountScenario.Small:
+ takerFee = POINT_ZERO_FIVE_UNITS_EIGHTEEN_DECIMALS;
+ break;
+ case OrderAssetAmountScenario.Zero:
+ takerFee = new BigNumber(0);
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('OrderAssetAmountScenario', orderScenario.takerFeeScenario);
+ }
+
+ switch (orderScenario.expirationTimeSecondsScenario) {
+ case ExpirationTimeSecondsScenario.InFuture:
+ expirationTimeSeconds = new BigNumber(2524604400); // Close to infinite
+ break;
+ case ExpirationTimeSecondsScenario.InPast:
+ expirationTimeSeconds = new BigNumber(0); // Jan 1, 1970
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr(
+ 'ExpirationTimeSecondsScenario',
+ orderScenario.expirationTimeSecondsScenario,
+ );
+ }
+
+ switch (orderScenario.takerScenario) {
+ case TakerScenario.CorrectlySpecified:
+ break; // noop since takerAddress is already specified
+
+ case TakerScenario.IncorrectlySpecified:
+ const notTaker = this._userAddresses[3];
+ takerAddress = notTaker;
+ break;
+
+ case TakerScenario.Unspecified:
+ takerAddress = constants.NULL_ADDRESS;
+ break;
+
+ default:
+ throw errorUtils.spawnSwitchErr('TakerScenario', orderScenario.takerScenario);
+ }
+
+ const order = {
+ senderAddress: constants.NULL_ADDRESS,
+ makerAddress,
+ takerAddress,
+ makerFee,
+ takerFee,
+ makerAssetAmount,
+ takerAssetAmount,
+ makerAssetData,
+ takerAssetData,
+ salt: generatePseudoRandomSalt(),
+ exchangeAddress: this._exchangeAddress,
+ feeRecipientAddress,
+ expirationTimeSeconds,
+ };
+
+ return order;
+ }
+}
diff --git a/contracts/protocol/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts b/contracts/protocol/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts
new file mode 100644
index 000000000..64b7dedbe
--- /dev/null
+++ b/contracts/protocol/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts
@@ -0,0 +1,19 @@
+import { AbstractBalanceAndProxyAllowanceFetcher } from '@0x/order-utils';
+import { BigNumber } from '@0x/utils';
+
+import { AssetWrapper } from './asset_wrapper';
+
+export class SimpleAssetBalanceAndProxyAllowanceFetcher implements AbstractBalanceAndProxyAllowanceFetcher {
+ private readonly _assetWrapper: AssetWrapper;
+ constructor(assetWrapper: AssetWrapper) {
+ this._assetWrapper = assetWrapper;
+ }
+ public async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber> {
+ const balance = await this._assetWrapper.getBalanceAsync(userAddress, assetData);
+ return balance;
+ }
+ public async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber> {
+ const proxyAllowance = await this._assetWrapper.getProxyAllowanceAsync(userAddress, assetData);
+ return proxyAllowance;
+ }
+}
diff --git a/contracts/protocol/test/utils/simple_order_filled_cancelled_fetcher.ts b/contracts/protocol/test/utils/simple_order_filled_cancelled_fetcher.ts
new file mode 100644
index 000000000..af959e00e
--- /dev/null
+++ b/contracts/protocol/test/utils/simple_order_filled_cancelled_fetcher.ts
@@ -0,0 +1,31 @@
+import { AbstractOrderFilledCancelledFetcher, orderHashUtils } from '@0x/order-utils';
+import { SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+
+import { ExchangeWrapper } from './exchange_wrapper';
+
+export class SimpleOrderFilledCancelledFetcher implements AbstractOrderFilledCancelledFetcher {
+ private readonly _exchangeWrapper: ExchangeWrapper;
+ private readonly _zrxAssetData: string;
+ constructor(exchange: ExchangeWrapper, zrxAssetData: string) {
+ this._exchangeWrapper = exchange;
+ this._zrxAssetData = zrxAssetData;
+ }
+ public async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber> {
+ const filledTakerAmount = new BigNumber(await this._exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash));
+ return filledTakerAmount;
+ }
+ public async isOrderCancelledAsync(signedOrder: SignedOrder): Promise<boolean> {
+ const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const isCancelled = await this._exchangeWrapper.isCancelledAsync(orderHash);
+ const orderEpoch = await this._exchangeWrapper.getOrderEpochAsync(
+ signedOrder.makerAddress,
+ signedOrder.senderAddress,
+ );
+ const isCancelledByOrderEpoch = orderEpoch > signedOrder.salt;
+ return isCancelled || isCancelledByOrderEpoch;
+ }
+ public getZRXAssetData(): string {
+ return this._zrxAssetData;
+ }
+}