From 9b1015bbce81b3f2a245e3dab6eea7c9028ce93b Mon Sep 17 00:00:00 2001 From: Greg Hysen Date: Thu, 10 May 2018 14:22:49 -0700 Subject: Atomic Order Matching - Tests --- packages/contracts/src/utils/asset_proxy_utils.ts | 74 ++ packages/contracts/src/utils/exchange_wrapper.ts | 22 + packages/contracts/src/utils/order_utils.ts | 9 + packages/contracts/src/utils/types.ts | 48 +- packages/contracts/test/exchange/core.ts | 30 +- packages/contracts/test/exchange/match_orders.ts | 873 +++++++++++++++++++++ packages/contracts/test/exchange/transactions.ts | 4 +- .../contracts/test/utils/match_order_tester.ts | 353 +++++++++ packages/metacoin/test/utils/chai_setup.ts | 8 + yarn.lock | 24 +- 10 files changed, 1412 insertions(+), 33 deletions(-) create mode 100644 packages/contracts/test/exchange/match_orders.ts create mode 100644 packages/contracts/test/utils/match_order_tester.ts diff --git a/packages/contracts/src/utils/asset_proxy_utils.ts b/packages/contracts/src/utils/asset_proxy_utils.ts index dc31c3497..9a26a9ca7 100644 --- a/packages/contracts/src/utils/asset_proxy_utils.ts +++ b/packages/contracts/src/utils/asset_proxy_utils.ts @@ -8,6 +8,9 @@ export const assetProxyUtils = { encodeAssetProxyId(assetProxyId: AssetProxyId): Buffer { return ethUtil.toBuffer(assetProxyId); }, + decodeAssetProxyId(encodedAssetProxyId: Buffer): AssetProxyId { + return ethUtil.bufferToInt(encodedAssetProxyId); + }, encodeAddress(address: string): Buffer { if (!ethUtil.isValidAddress(address)) { throw new Error(`Invalid Address: ${address}`); @@ -15,12 +18,24 @@ export const assetProxyUtils = { const encodedAddress = ethUtil.toBuffer(address); return encodedAddress; }, + decodeAddress(encodedAddress: Buffer): string { + const address = ethUtil.bufferToHex(encodedAddress); + if (!ethUtil.isValidAddress(address)) { + throw new Error(`Invalid Address: ${address}`); + } + return address; + }, encodeUint256(value: BigNumber): Buffer { const formattedValue = new BN(value.toString(10)); const encodedValue = ethUtil.toBuffer(formattedValue); const paddedValue = ethUtil.setLengthLeft(encodedValue, 32); return paddedValue; }, + decodeUint256(encodedValue: Buffer): BigNumber { + const formattedValue = ethUtil.bufferToHex(encodedValue); + const value = new BigNumber(formattedValue, 16); + return value; + }, encodeERC20ProxyData(tokenAddress: string): string { const encodedAssetProxyId = assetProxyUtils.encodeAssetProxyId(AssetProxyId.ERC20); const encodedAddress = assetProxyUtils.encodeAddress(tokenAddress); @@ -28,6 +43,28 @@ export const assetProxyUtils = { const encodedMetadataHex = ethUtil.bufferToHex(encodedMetadata); return encodedMetadataHex; }, + decodeERC20ProxyData(proxyData: string): string /* tokenAddress */ { + const encodedProxyMetadata = ethUtil.toBuffer(proxyData); + if (encodedProxyMetadata.byteLength !== 21) { + throw new Error( + `Could not decode ERC20 Proxy Data. Expected length of encoded data to be 21. Got ${ + encodedProxyMetadata.byteLength + }`, + ); + } + const encodedAssetProxyId = encodedProxyMetadata.slice(0, 1); + const assetProxyId = assetProxyUtils.decodeAssetProxyId(encodedAssetProxyId); + if (assetProxyId !== AssetProxyId.ERC20) { + throw new Error( + `Could not decode ERC20 Proxy Data. Expected Asset Proxy Id to be ERC20 (${ + AssetProxyId.ERC20 + }), but got ${assetProxyId}`, + ); + } + const encodedTokenAddress = encodedProxyMetadata.slice(1, 21); + const tokenAddress = assetProxyUtils.decodeAddress(encodedTokenAddress); + return tokenAddress; + }, encodeERC721ProxyData(tokenAddress: string, tokenId: BigNumber): string { const encodedAssetProxyId = assetProxyUtils.encodeAssetProxyId(AssetProxyId.ERC721); const encodedAddress = assetProxyUtils.encodeAddress(tokenAddress); @@ -36,4 +73,41 @@ export const assetProxyUtils = { const encodedMetadataHex = ethUtil.bufferToHex(encodedMetadata); return encodedMetadataHex; }, + decodeERC721ProxyData(proxyData: string): [string /* tokenAddress */, BigNumber /* tokenId */] { + const encodedProxyMetadata = ethUtil.toBuffer(proxyData); + if (encodedProxyMetadata.byteLength !== 53) { + throw new Error( + `Could not decode ERC20 Proxy Data. Expected length of encoded data to be 53. Got ${ + encodedProxyMetadata.byteLength + }`, + ); + } + const encodedAssetProxyId = encodedProxyMetadata.slice(0, 1); + const assetProxyId = assetProxyUtils.decodeAssetProxyId(encodedAssetProxyId); + if (assetProxyId !== AssetProxyId.ERC721) { + throw new Error( + `Could not decode ERC721 Proxy Data. Expected Asset Proxy Id to be ERC721 (${ + AssetProxyId.ERC721 + }), but got ${assetProxyId}`, + ); + } + const encodedTokenAddress = encodedProxyMetadata.slice(1, 21); + const tokenAddress = assetProxyUtils.decodeAddress(encodedTokenAddress); + const encodedTokenId = encodedProxyMetadata.slice(21, 53); + const tokenId = assetProxyUtils.decodeUint256(encodedTokenId); + return [tokenAddress, tokenId]; + }, + decodeProxyDataId(proxyData: string): AssetProxyId { + const encodedProxyMetadata = ethUtil.toBuffer(proxyData); + if (encodedProxyMetadata.byteLength < 1) { + throw new Error( + `Could not decode Proxy Data. Expected length of encoded data to be at least 1. Got ${ + encodedProxyMetadata.byteLength + }`, + ); + } + const encodedAssetProxyId = encodedProxyMetadata.slice(0, 1); + const assetProxyId = assetProxyUtils.decodeAssetProxyId(encodedAssetProxyId); + return assetProxyId; + }, }; diff --git a/packages/contracts/src/utils/exchange_wrapper.ts b/packages/contracts/src/utils/exchange_wrapper.ts index 27fdd698f..6d36198f2 100644 --- a/packages/contracts/src/utils/exchange_wrapper.ts +++ b/packages/contracts/src/utils/exchange_wrapper.ts @@ -231,4 +231,26 @@ export class ExchangeWrapper { tx.logs = _.map(tx.logs, log => this._logDecoder.decodeLogOrThrow(log)); return tx; } + public async getOrderInfoAsync( + signedOrder: SignedOrder, + ): Promise<[number /* orderStatus */, string /* orderHash */, BigNumber /* orderTakerAssetAmountFilled */]> { + const orderInfo: [number, string, BigNumber] = await this._exchange.getOrderInfo.callAsync(signedOrder); + return orderInfo; + } + public async matchOrdersAsync( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + from: string, + ): Promise { + 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._getTxWithDecodedExchangeLogsAsync(txHash); + return tx; + } } diff --git a/packages/contracts/src/utils/order_utils.ts b/packages/contracts/src/utils/order_utils.ts index 10bbf4f7c..7a482ad9e 100644 --- a/packages/contracts/src/utils/order_utils.ts +++ b/packages/contracts/src/utils/order_utils.ts @@ -80,4 +80,13 @@ export const orderUtils = { const orderHashHex = `0x${orderHashBuff.toString('hex')}`; return orderHashHex; }, + createMatchOrders(signedOrderLeft: SignedOrder, signedOrderRight: SignedOrder) { + const fill = { + left: orderUtils.getOrderStruct(signedOrderLeft), + right: orderUtils.getOrderStruct(signedOrderRight), + leftSignature: signedOrderLeft.signature, + rightSignature: signedOrderRight.signature, + }; + return fill; + }, }; diff --git a/packages/contracts/src/utils/types.ts b/packages/contracts/src/utils/types.ts index 234b14ef9..ce7fbcbe1 100644 --- a/packages/contracts/src/utils/types.ts +++ b/packages/contracts/src/utils/types.ts @@ -74,12 +74,27 @@ export interface Token { swarmHash: string; } -export enum ExchangeContractErrs { - ERROR_ORDER_EXPIRED, - ERROR_ORDER_FULLY_FILLED, - ERROR_ORDER_CANCELLED, - ERROR_ROUNDING_ERROR_TOO_LARGE, - ERROR_INSUFFICIENT_BALANCE_OR_ALLOWANCE, +export enum ExchangeStatus { + /// Default Status /// + INVALID, // General invalid status + + /// General Exchange Statuses /// + SUCCESS, // Indicates a successful operation + ROUNDING_ERROR_TOO_LARGE, // Rounding error too large + INSUFFICIENT_BALANCE_OR_ALLOWANCE, // Insufficient balance or allowance for token transfer + TAKER_ASSET_FILL_AMOUNT_TOO_LOW, // takerAssetFillAmount is <= 0 + INVALID_SIGNATURE, // Invalid signature + INVALID_SENDER, // Invalid sender + INVALID_TAKER, // Invalid taker + INVALID_MAKER, // Invalid maker + + /// Order State Statuses /// + ORDER_INVALID_MAKER_ASSET_AMOUNT, // Order does not have a valid maker asset amount + ORDER_INVALID_TAKER_ASSET_AMOUNT, // Order does not have a valid taker asset amount + ORDER_FILLABLE, // Order is fillable + ORDER_EXPIRED, // Order has already expired + ORDER_FULLY_FILLED, // Order is fully filled + ORDER_CANCELLED, // Order has been cancelled } export enum ContractName { @@ -143,3 +158,24 @@ export interface SignedTransaction { data: string; signature: string; } + +export interface TransferAmountsByMatchOrders { + // Left Maker + amountBoughtByLeftMaker: BigNumber; + amountSoldByLeftMaker: BigNumber; + amountReceivedByLeftMaker: BigNumber; + feePaidByLeftMaker: BigNumber; + // Right Maker + amountBoughtByRightMaker: BigNumber; + amountSoldByRightMaker: BigNumber; + amountReceivedByRightMaker: BigNumber; + feePaidByRightMaker: BigNumber; + // Taker + amountReceivedByTaker: BigNumber; + feePaidByTakerLeft: BigNumber; + feePaidByTakerRight: BigNumber; + totalFeePaidByTaker: BigNumber; + // Fee Recipients + feeReceivedLeft: BigNumber; + feeReceivedRight: BigNumber; +} diff --git a/packages/contracts/test/exchange/core.ts b/packages/contracts/test/exchange/core.ts index 6c190d4da..e9c51ea18 100644 --- a/packages/contracts/test/exchange/core.ts +++ b/packages/contracts/test/exchange/core.ts @@ -12,7 +12,7 @@ import { ERC721ProxyContract } from '../../src/contract_wrappers/generated/e_r_c import { CancelContractEventArgs, ExchangeContract, - ExchangeErrorContractEventArgs, + ExchangeStatusContractEventArgs, FillContractEventArgs, } from '../../src/contract_wrappers/generated/exchange'; import { artifacts } from '../../src/utils/artifacts'; @@ -25,8 +25,8 @@ import { ERC721Wrapper } from '../../src/utils/erc721_wrapper'; import { ExchangeWrapper } from '../../src/utils/exchange_wrapper'; import { OrderFactory } from '../../src/utils/order_factory'; import { orderUtils } from '../../src/utils/order_utils'; -import { AssetProxyId, ERC20BalancesByOwner, ExchangeContractErrs, SignedOrder } from '../../src/utils/types'; -import { provider, txDefaults, web3Wrapper } from '../../src/utils/web3_wrapper'; +import { AssetProxyId, ContractName, ERC20BalancesByOwner, ExchangeStatus, SignedOrder } from '../../src/utils/types'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; chaiSetup.configure(); const expect = chai.expect; @@ -556,9 +556,9 @@ describe('Exchange core', () => { const res = await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress); expect(res.logs).to.have.length(1); - const log = res.logs[0] as LogWithDecodedArgs; - const errCode = log.args.errorId; - expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_EXPIRED); + const log = res.logs[0] as LogWithDecodedArgs; + const statusCode = log.args.statusId; + expect(statusCode).to.be.equal(ExchangeStatus.ORDER_EXPIRED); }); it('should log an error event if no value is filled', async () => { @@ -567,9 +567,9 @@ describe('Exchange core', () => { const res = await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress); expect(res.logs).to.have.length(1); - const log = res.logs[0] as LogWithDecodedArgs; - const errCode = log.args.errorId; - expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_FULLY_FILLED); + const log = res.logs[0] as LogWithDecodedArgs; + const statusCode = log.args.statusId; + expect(statusCode).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); }); }); @@ -635,9 +635,9 @@ describe('Exchange core', () => { const res = await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress); expect(res.logs).to.have.length(1); - const log = res.logs[0] as LogWithDecodedArgs; - const errCode = log.args.errorId; - expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_CANCELLED); + const log = res.logs[0] as LogWithDecodedArgs; + const statusCode = log.args.statusId; + expect(statusCode).to.be.equal(ExchangeStatus.ORDER_CANCELLED); }); it('should log error if order is expired', async () => { @@ -647,9 +647,9 @@ describe('Exchange core', () => { const res = await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress); expect(res.logs).to.have.length(1); - const log = res.logs[0] as LogWithDecodedArgs; - const errCode = log.args.errorId; - expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_EXPIRED); + const log = res.logs[0] as LogWithDecodedArgs; + const statusCode = log.args.statusId; + expect(statusCode).to.be.equal(ExchangeStatus.ORDER_EXPIRED); }); }); diff --git a/packages/contracts/test/exchange/match_orders.ts b/packages/contracts/test/exchange/match_orders.ts new file mode 100644 index 000000000..a114d92ac --- /dev/null +++ b/packages/contracts/test/exchange/match_orders.ts @@ -0,0 +1,873 @@ +import { LogWithDecodedArgs, ZeroEx } from '0x.js'; +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import ethUtil = require('ethereumjs-util'); +import * as _ from 'lodash'; + +import { DummyERC20TokenContract } from '../../src/contract_wrappers/generated/dummy_e_r_c20_token'; +import { DummyERC721TokenContract } from '../../src/contract_wrappers/generated/dummy_e_r_c721_token'; +import { ERC20ProxyContract } from '../../src/contract_wrappers/generated/e_r_c20_proxy'; +import { ERC721ProxyContract } from '../../src/contract_wrappers/generated/e_r_c721_proxy'; +import { + CancelContractEventArgs, + ExchangeContract, + ExchangeStatusContractEventArgs, + FillContractEventArgs, +} from '../../src/contract_wrappers/generated/exchange'; +import { assetProxyUtils } from '../../src/utils/asset_proxy_utils'; +import { constants } from '../../src/utils/constants'; +import { crypto } from '../../src/utils/crypto'; +import { ERC20Wrapper } from '../../src/utils/erc20_wrapper'; +import { ERC721Wrapper } from '../../src/utils/erc721_wrapper'; +import { ExchangeWrapper } from '../../src/utils/exchange_wrapper'; +import { OrderFactory } from '../../src/utils/order_factory'; +import { orderUtils } from '../../src/utils/order_utils'; +import { + AssetProxyId, + ContractName, + ERC20BalancesByOwner, + ERC721TokenIdsByOwner, + ExchangeStatus, + SignedOrder, +} from '../../src/utils/types'; +import { chaiSetup } from '../utils/chai_setup'; +import { deployer } from '../utils/deployer'; +import { provider, web3Wrapper } from '../utils/web3_wrapper'; + +import { MatchOrderTester } from '../utils/match_order_tester'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('matchOrdersAndVerifyBalancesAsync', () => { + 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 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 erc721TakerAssetIds: BigNumber[]; + + let defaultERC20MakerAssetAddress: string; + let defaultERC20TakerAssetAddress: string; + let defaultERC721AssetAddress: string; + + let matchOrderTester: MatchOrderTester; + + let zeroEx: ZeroEx; + + before(async () => { + // Create accounts + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const usedAddresses = ([ + owner, + makerAddressLeft, + makerAddressRight, + takerAddress, + feeRecipientAddressLeft, + feeRecipientAddressRight, + ] = accounts); + // Create wrappers + erc20Wrapper = new ERC20Wrapper(deployer, provider, usedAddresses, owner); + erc721Wrapper = new ERC721Wrapper(deployer, provider, usedAddresses, owner); + // Deploy ERC20 token & ERC20 proxy + [erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync(); + 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]; + erc721TakerAssetIds = erc721Balances[takerAddress][erc721Token.address]; + // Depoy exchange + const exchangeInstance = await deployer.deployAsync(ContractName.Exchange, [ + assetProxyUtils.encodeERC20ProxyData(zrxToken.address), + ]); + exchange = new ExchangeContract(exchangeInstance.abi, exchangeInstance.address, provider); + zeroEx = new ZeroEx(provider, { + exchangeContractAddress: exchange.address, + networkId: constants.TESTRPC_NETWORK_ID, + }); + exchangeWrapper = new ExchangeWrapper(exchange, zeroEx); + await exchangeWrapper.registerAssetProxyAsync(AssetProxyId.ERC20, erc20Proxy.address, owner); + await exchangeWrapper.registerAssetProxyAsync(AssetProxyId.ERC721, erc721Proxy.address, owner); + // Authorize ERC20 and ERC721 trades by exchange + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { + from: owner, + }); + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { + from: owner, + }); + // Set default addresses + defaultERC20MakerAssetAddress = erc20TokenA.address; + defaultERC20TakerAssetAddress = erc20TokenB.address; + defaultERC721AssetAddress = erc721Token.address; + // Create default order parameters + const defaultOrderParams = { + ...constants.STATIC_ORDER_PARAMS, + exchangeAddress: exchange.address, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + }; + const privateKeyLeft = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressLeft)]; + orderFactoryLeft = new OrderFactory(privateKeyLeft, defaultOrderParams); + const privateKeyRight = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressRight)]; + orderFactoryRight = new OrderFactory(privateKeyRight, defaultOrderParams); + // Set match order tester + matchOrderTester = new MatchOrderTester(exchangeWrapper, erc20Wrapper, erc721Wrapper); + }); + 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 the correct amounts when orders completely fill each other', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match signedOrderLeft with signedOrderRight + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + zrxToken.address, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: [number, string, BigNumber] = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo[0] as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: [number, string, BigNumber] = await exchangeWrapper.getOrderInfoAsync( + signedOrderRight, + ); + expect(rightOrderInfo[0] as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + }); + + 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 = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Store original taker balance + const takerInitialBalances = _.cloneDeep(erc20BalancesByOwner[takerAddress][defaultERC20MakerAssetAddress]); + // Match signedOrderLeft with signedOrderRight + let newERC20BalancesByOwner: ERC20BalancesByOwner; + let newERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + [ + newERC20BalancesByOwner, + newERC721TokenIdsByOwner, + ] = await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + zrxToken.address, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: [number, string, BigNumber] = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo[0] as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: [number, string, BigNumber] = await exchangeWrapper.getOrderInfoAsync( + signedOrderRight, + ); + expect(rightOrderInfo[0] as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify taker did not take a profit + expect(takerInitialBalances).to.be.deep.equal( + newERC20BalancesByOwner[takerAddress][defaultERC20MakerAssetAddress], + ); + }); + + 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 = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(20), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(4), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + zrxToken.address, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: [number, string, BigNumber] = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo[0] as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify right order was partially filled + const rightOrderInfo: [number, string, BigNumber] = await exchangeWrapper.getOrderInfoAsync( + signedOrderRight, + ); + expect(rightOrderInfo[0] as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FILLABLE); + }); + + 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 = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(50), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + zrxToken.address, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was partially filled + const leftOrderInfo: [number, string, BigNumber] = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo[0] as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FILLABLE); + // Verify right order was fully filled + const rightOrderInfo: [number, string, BigNumber] = await exchangeWrapper.getOrderInfoAsync( + signedOrderRight, + ); + expect(rightOrderInfo[0] as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + }); + + it('should transfer the correct amounts when consecutive calls are used to completely fill the left order', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(50), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + let newERC20BalancesByOwner: ERC20BalancesByOwner; + let newERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + [ + newERC20BalancesByOwner, + newERC721TokenIdsByOwner, + ] = await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + zrxToken.address, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was partially filled + const leftOrderInfo: [number, string, BigNumber] = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo[0] as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FILLABLE); + // Verify right order was fully filled + const rightOrderInfo: [number, string, BigNumber] = await exchangeWrapper.getOrderInfoAsync( + signedOrderRight, + ); + expect(rightOrderInfo[0] as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // 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 = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(50), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match signedOrderLeft with signedOrderRight2 + const leftTakerAssetFilledAmount = signedOrderRight.makerAssetAmount; + const rightTakerAssetFilledAmount = new BigNumber(0); + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight2, + zrxToken.address, + takerAddress, + newERC20BalancesByOwner, + erc721TokenIdsByOwner, + leftTakerAssetFilledAmount, + rightTakerAssetFilledAmount, + ); + // Verify left order was fully filled + const leftOrderInfo2: [number, string, BigNumber] = await exchangeWrapper.getOrderInfoAsync( + signedOrderLeft, + ); + expect(leftOrderInfo2[0] as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify second right order was partially filled + const rightOrderInfo2: [number, string, BigNumber] = await exchangeWrapper.getOrderInfoAsync( + signedOrderRight2, + ); + expect(rightOrderInfo2[0] as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FILLABLE); + }); + + it('should transfer the correct amounts when consecutive calls are used to completely fill the right order', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(50), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + let newERC20BalancesByOwner: ERC20BalancesByOwner; + let newERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + [ + newERC20BalancesByOwner, + newERC721TokenIdsByOwner, + ] = await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + zrxToken.address, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was partially filled + const leftOrderInfo: [number, string, BigNumber] = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo[0] as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: [number, string, BigNumber] = await exchangeWrapper.getOrderInfoAsync( + signedOrderRight, + ); + expect(rightOrderInfo[0] as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FILLABLE); + // 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 = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(50), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + // Match signedOrderLeft2 with signedOrderRight + const leftTakerAssetFilledAmount = new BigNumber(0); + const takerAmountReceived = newERC20BalancesByOwner[takerAddress][defaultERC20MakerAssetAddress].minus( + erc20BalancesByOwner[takerAddress][defaultERC20MakerAssetAddress], + ); + const rightTakerAssetFilledAmount = signedOrderLeft.makerAssetAmount.minus(takerAmountReceived); + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft2, + signedOrderRight, + zrxToken.address, + takerAddress, + newERC20BalancesByOwner, + erc721TokenIdsByOwner, + leftTakerAssetFilledAmount, + rightTakerAssetFilledAmount, + ); + // Verify second left order was partially filled + const leftOrderInfo2: [number, string, BigNumber] = await exchangeWrapper.getOrderInfoAsync( + signedOrderLeft2, + ); + expect(leftOrderInfo2[0] as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FILLABLE); + // Verify right order was fully filled + const rightOrderInfo2: [number, string, BigNumber] = await exchangeWrapper.getOrderInfoAsync( + signedOrderRight, + ); + expect(rightOrderInfo2[0] as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + }); + + it('should transfer the correct amounts if fee recipient is the same across both matched orders', async () => { + const feeRecipientAddress = feeRecipientAddressLeft; + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + zrxToken.address, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('should transfer the correct amounts if taker is also the left order maker', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + takerAddress = signedOrderLeft.makerAddress; + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + zrxToken.address, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('should transfer the correct amounts if taker is also the right order maker', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + takerAddress = signedOrderRight.makerAddress; + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + zrxToken.address, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('should transfer the correct amounts if taker is also the left fee recipient', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + takerAddress = feeRecipientAddressLeft; + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + zrxToken.address, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('should transfer the correct amounts if taker is also the right fee recipient', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + takerAddress = feeRecipientAddressRight; + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + zrxToken.address, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + 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 = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: makerAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: makerAddressRight, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + zrxToken.address, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('Should not transfer any amounts if left order is not fillable', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Cancel left order + await exchangeWrapper.cancelOrderAsync(signedOrderLeft, signedOrderLeft.makerAddress); + // Match orders + await exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress); + // Verify balances did not change + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20BalancesByOwner); + }); + + it('Should not transfer any amounts if right order is not fillable', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Cancel right order + await exchangeWrapper.cancelOrderAsync(signedOrderRight, signedOrderRight.makerAddress); + // Match orders + await exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress); + // Verify balances did not change + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20BalancesByOwner); + }); + + it('should throw if there is not a positive spread', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + return expect( + matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + zrxToken.address, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ), + ).to.be.rejectedWith(constants.REVERT); + }); + + it('should throw if the left maker asset is not equal to the right taker asset ', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + return expect( + matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + zrxToken.address, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ), + ).to.be.rejectedWith(constants.REVERT); + }); + + it('should throw if the right maker asset is not equal to the left taker asset', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + return expect( + matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + zrxToken.address, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ), + ).to.be.rejectedWith(constants.REVERT); + }); + + 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 = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC721ProxyData(defaultERC721AssetAddress, erc721TokenToTransfer), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: new BigNumber(1), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC721ProxyData(defaultERC721AssetAddress, erc721TokenToTransfer), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: new BigNumber(1), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + zrxToken.address, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: [number, string, BigNumber] = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo[0] as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: [number, string, BigNumber] = await exchangeWrapper.getOrderInfoAsync( + signedOrderRight, + ); + expect(rightOrderInfo[0] as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + }); + + 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 = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC721ProxyData(defaultERC721AssetAddress, erc721TokenToTransfer), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: new BigNumber(1), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC721ProxyData(defaultERC721AssetAddress, erc721TokenToTransfer), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: new BigNumber(1), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + zrxToken.address, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: [number, string, BigNumber] = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo[0] as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: [number, string, BigNumber] = await exchangeWrapper.getOrderInfoAsync( + signedOrderRight, + ); + expect(rightOrderInfo[0] as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + }); + }); +}); // tslint:disable-line:max-file-line-count diff --git a/packages/contracts/test/exchange/transactions.ts b/packages/contracts/test/exchange/transactions.ts index a71b50a61..482475554 100644 --- a/packages/contracts/test/exchange/transactions.ts +++ b/packages/contracts/test/exchange/transactions.ts @@ -21,7 +21,7 @@ import { TransactionFactory } from '../../src/utils/transaction_factory'; import { AssetProxyId, ERC20BalancesByOwner, - ExchangeContractErrs, + ExchangeStatus, OrderStruct, SignatureType, SignedOrder, @@ -197,7 +197,7 @@ describe('Exchange transactions', () => { it('should cancel the order when signed by maker and called by sender', async () => { await exchangeWrapper.executeTransactionAsync(signedTx, senderAddress); - const res = await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress); + const res = await exchangeWrapper.fillOrderAsync(signedOrder, senderAddress); const newBalances = await erc20Wrapper.getBalancesAsync(); expect(newBalances).to.deep.equal(erc20Balances); }); diff --git a/packages/contracts/test/utils/match_order_tester.ts b/packages/contracts/test/utils/match_order_tester.ts new file mode 100644 index 000000000..91c2ab0a3 --- /dev/null +++ b/packages/contracts/test/utils/match_order_tester.ts @@ -0,0 +1,353 @@ +import { LogWithDecodedArgs, ZeroEx } from '0x.js'; +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import ethUtil = require('ethereumjs-util'); +import * as _ from 'lodash'; + +import { DummyERC20TokenContract } from '../../src/contract_wrappers/generated/dummy_e_r_c20_token'; +import { DummyERC721TokenContract } from '../../src/contract_wrappers/generated/dummy_e_r_c721_token'; +import { ERC20ProxyContract } from '../../src/contract_wrappers/generated/e_r_c20_proxy'; +import { ERC721ProxyContract } from '../../src/contract_wrappers/generated/e_r_c721_proxy'; +import { + CancelContractEventArgs, + ExchangeContract, + FillContractEventArgs, +} from '../../src/contract_wrappers/generated/exchange'; +import { assetProxyUtils } from '../../src/utils/asset_proxy_utils'; +import { constants } from '../../src/utils/constants'; +import { crypto } from '../../src/utils/crypto'; +import { ERC20Wrapper } from '../../src/utils/erc20_wrapper'; +import { ERC721Wrapper } from '../../src/utils/erc721_wrapper'; +import { ExchangeWrapper } from '../../src/utils/exchange_wrapper'; +import { OrderFactory } from '../../src/utils/order_factory'; +import { orderUtils } from '../../src/utils/order_utils'; +import { + AssetProxyId, + ContractName, + ERC20BalancesByOwner, + ERC721TokenIdsByOwner, + ExchangeStatus, + SignedOrder, + TransferAmountsByMatchOrders as TransferAmounts, +} from '../../src/utils/types'; +import { chaiSetup } from '../utils/chai_setup'; +import { deployer } from '../utils/deployer'; +import { provider, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +export class MatchOrderTester { + private _exchangeWrapper: ExchangeWrapper; + private _erc20Wrapper: ERC20Wrapper; + private _erc721Wrapper: ERC721Wrapper; + + /// @dev Calculates the expected balances of order makers, fee recipients, and the taker, + /// as a result of matching two orders. + /// @param signedOrderLeft First matched order. + /// @param signedOrderRight Second matched order. + /// @param feeTokenAddress Address of ERC20 fee token. + /// @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 A struct containing the expected transfer amounts. + /// @return Expected ERC20 balances & ERC721 token owners after orders have been matched. + private static _calculateExpectedBalances( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + feeTokenAddress: string, + 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 = assetProxyUtils.decodeProxyDataId(signedOrderLeft.makerAssetData); + if (makerAssetProxyIdLeft === AssetProxyId.ERC20) { + // Decode asset data + const makerAssetAddressLeft = assetProxyUtils.decodeERC20ProxyData(signedOrderLeft.makerAssetData); + const takerAssetAddressRight = makerAssetAddressLeft; + // Left Maker + expectedNewERC20BalancesByOwner[makerAddressLeft][makerAssetAddressLeft] = expectedNewERC20BalancesByOwner[ + makerAddressLeft + ][makerAssetAddressLeft].minus(expectedTransferAmounts.amountSoldByLeftMaker); + // Right Maker + expectedNewERC20BalancesByOwner[makerAddressRight][ + takerAssetAddressRight + ] = expectedNewERC20BalancesByOwner[makerAddressRight][takerAssetAddressRight].add( + expectedTransferAmounts.amountReceivedByRightMaker, + ); + // Taker + expectedNewERC20BalancesByOwner[takerAddress][makerAssetAddressLeft] = expectedNewERC20BalancesByOwner[ + takerAddress + ][makerAssetAddressLeft].add(expectedTransferAmounts.amountReceivedByTaker); + } else if (makerAssetProxyIdLeft === AssetProxyId.ERC721) { + // Decode asset data + let makerAssetAddressLeft; + let makerAssetIdLeft; + [makerAssetAddressLeft, makerAssetIdLeft] = assetProxyUtils.decodeERC721ProxyData( + signedOrderLeft.makerAssetData, + ); + 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 = assetProxyUtils.decodeProxyDataId(signedOrderLeft.takerAssetData); + if (takerAssetProxyIdLeft === AssetProxyId.ERC20) { + // Decode asset data + const takerAssetAddressLeft = assetProxyUtils.decodeERC20ProxyData(signedOrderLeft.takerAssetData); + const makerAssetAddressRight = takerAssetAddressLeft; + // Left Maker + expectedNewERC20BalancesByOwner[makerAddressLeft][takerAssetAddressLeft] = expectedNewERC20BalancesByOwner[ + makerAddressLeft + ][takerAssetAddressLeft].add(expectedTransferAmounts.amountReceivedByLeftMaker); + // Right Maker + expectedNewERC20BalancesByOwner[makerAddressRight][ + makerAssetAddressRight + ] = expectedNewERC20BalancesByOwner[makerAddressRight][makerAssetAddressRight].minus( + expectedTransferAmounts.amountSoldByRightMaker, + ); + } else if (takerAssetProxyIdLeft === AssetProxyId.ERC721) { + // Decode asset data + let makerAssetAddressRight; + let makerAssetIdRight; + [makerAssetAddressRight, makerAssetIdRight] = assetProxyUtils.decodeERC721ProxyData( + signedOrderRight.makerAssetData, + ); + 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][feeTokenAddress] = expectedNewERC20BalancesByOwner[ + makerAddressLeft + ][feeTokenAddress].minus(expectedTransferAmounts.feePaidByLeftMaker); + // Right Maker Fees + expectedNewERC20BalancesByOwner[makerAddressRight][feeTokenAddress] = expectedNewERC20BalancesByOwner[ + makerAddressRight + ][feeTokenAddress].minus(expectedTransferAmounts.feePaidByRightMaker); + // Taker Fees + expectedNewERC20BalancesByOwner[takerAddress][feeTokenAddress] = expectedNewERC20BalancesByOwner[takerAddress][ + feeTokenAddress + ].minus(expectedTransferAmounts.totalFeePaidByTaker); + // Left Fee Recipient Fees + expectedNewERC20BalancesByOwner[feeRecipientAddressLeft][feeTokenAddress] = expectedNewERC20BalancesByOwner[ + feeRecipientAddressLeft + ][feeTokenAddress].add(expectedTransferAmounts.feeReceivedLeft); + // Right Fee Recipient Fees + expectedNewERC20BalancesByOwner[feeRecipientAddressRight][feeTokenAddress] = expectedNewERC20BalancesByOwner[ + feeRecipientAddressRight + ][feeTokenAddress].add(expectedTransferAmounts.feeReceivedRight); + + return [expectedNewERC20BalancesByOwner, expectedNewERC721TokenIdsByOwner]; + } + + /// @dev Compares a pair of ERC20 balances and a pair of ERC721 token owners. + /// @param expectedNewERC20BalancesByOwner Expected ERC20 balances. + /// @param realERC20BalancesByOwner Actual ERC20 balances. + /// @param expectedNewERC721TokenIdsByOwner Expected ERC721 token owners. + /// @param realERC721TokenIdsByOwner Actual ERC20 token owners. + /// @return True only if ERC20 balances match and ERC721 token owners match. + private static _compareExpectedAndRealBalances( + expectedNewERC20BalancesByOwner: ERC20BalancesByOwner, + realERC20BalancesByOwner: ERC20BalancesByOwner, + expectedNewERC721TokenIdsByOwner: ERC721TokenIdsByOwner, + realERC721TokenIdsByOwner: ERC721TokenIdsByOwner, + ) { + // ERC20 Balances + const erc20BalancesMatch = _.isEqual(expectedNewERC20BalancesByOwner, realERC20BalancesByOwner); + if (!erc20BalancesMatch) { + return false; + } + // ERC721 Token Ids + const sortedExpectedNewERC721TokenIdsByOwner = _.mapValues( + expectedNewERC721TokenIdsByOwner, + tokenIdsByOwner => { + _.mapValues(tokenIdsByOwner, tokenIds => { + _.sortBy(tokenIds); + }); + }, + ); + const sortedNewERC721TokenIdsByOwner = _.mapValues(realERC721TokenIdsByOwner, tokenIdsByOwner => { + _.mapValues(tokenIdsByOwner, tokenIds => { + _.sortBy(tokenIds); + }); + }); + const erc721TokenIdsMatch = _.isEqual(sortedExpectedNewERC721TokenIdsByOwner, sortedNewERC721TokenIdsByOwner); + return erc721TokenIdsMatch; + } + + /// @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. + constructor(exchangeWrapper: ExchangeWrapper, erc20Wrapper: ERC20Wrapper, erc721Wrapper: ERC721Wrapper) { + this._exchangeWrapper = exchangeWrapper; + this._erc20Wrapper = erc20Wrapper; + this._erc721Wrapper = erc721Wrapper; + } + + /// @dev Matches two complementary orders and validates results. + /// Validation either succeeds or throws. + /// @param signedOrderLeft First matched order. + /// @param signedOrderRight Second matched order. + /// @param feeTokenAddress Address of ERC20 fee token. + /// @param takerAddress Address of taker (the address who matched the two orders) + /// @param erc20BalancesByOwner Current ERC20 balances. + /// @param erc721TokenIdsByOwner Current ERC721 token owners. + /// @param initialTakerAssetFilledAmountLeft Current amount the left order has been filled. + /// @param initialTakerAssetFilledAmountRight Current amount the right order has been filled. + /// @return New ERC20 balances & ERC721 token owners. + public async matchOrdersAndVerifyBalancesAsync( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + feeTokenAddress: string, + takerAddress: string, + erc20BalancesByOwner: ERC20BalancesByOwner, + erc721TokenIdsByOwner: ERC721TokenIdsByOwner, + initialTakerAssetFilledAmountLeft?: BigNumber, + initialTakerAssetFilledAmountRight?: BigNumber, + ): Promise<[ERC20BalancesByOwner, ERC721TokenIdsByOwner]> { + // Test setup & verify preconditions + const makerAddressLeft = signedOrderLeft.makerAddress; + const makerAddressRight = signedOrderRight.makerAddress; + const feeRecipientAddressLeft = signedOrderLeft.feeRecipientAddress; + const feeRecipientAddressRight = signedOrderRight.feeRecipientAddress; + // Verify Left order preconditions + const takerAssetFilledAmountBeforeLeft = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderUtils.getOrderHashHex(signedOrderLeft), + ); + const expectedLeftOrderFillAmoutBeforeMatch = initialTakerAssetFilledAmountLeft + ? initialTakerAssetFilledAmountLeft + : new BigNumber(0); + expect(takerAssetFilledAmountBeforeLeft).to.be.bignumber.equal(expectedLeftOrderFillAmoutBeforeMatch); + // Verify Right order preconditions + const takerAssetFilledAmountBeforeRight = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderUtils.getOrderHashHex(signedOrderRight), + ); + const expectedRightOrderFillAmoutBeforeMatch = initialTakerAssetFilledAmountRight + ? initialTakerAssetFilledAmountRight + : new BigNumber(0); + expect(takerAssetFilledAmountBeforeRight).to.be.bignumber.equal(expectedRightOrderFillAmoutBeforeMatch); + // Match left & right orders + await this._exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress); + const newERC20BalancesByOwner = await this._erc20Wrapper.getBalancesAsync(); + const newERC721TokenIdsByOwner = await this._erc721Wrapper.getBalancesAsync(); + // Calculate expected balance changes + const expectedTransferAmounts = await this._calculateExpectedTransferAmountsAsync( + signedOrderLeft, + signedOrderRight, + expectedLeftOrderFillAmoutBeforeMatch, + expectedRightOrderFillAmoutBeforeMatch, + ); + let expectedERC20BalancesByOwner: ERC20BalancesByOwner; + let expectedERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + [expectedERC20BalancesByOwner, expectedERC721TokenIdsByOwner] = MatchOrderTester._calculateExpectedBalances( + signedOrderLeft, + signedOrderRight, + feeTokenAddress, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + expectedTransferAmounts, + ); + // Assert our expected balances are equal to the actual balances + const expectedBalancesMatchRealBalances = MatchOrderTester._compareExpectedAndRealBalances( + expectedERC20BalancesByOwner, + newERC20BalancesByOwner, + expectedERC721TokenIdsByOwner, + newERC721TokenIdsByOwner, + ); + expect(expectedBalancesMatchRealBalances).to.be.true(); + return [newERC20BalancesByOwner, newERC721TokenIdsByOwner]; + } + + /// @dev Calculates expected transfer amounts between order makers, fee recipients, and + /// the taker when two orders are matched. + /// @param signedOrderLeft First matched order. + /// @param signedOrderRight Second matched order. + /// @param expectedLeftOrderFillAmoutBeforeMatch How much we expect the left order has been filled, prior to matching orders. + /// @param expectedRightOrderFillAmoutBeforeMatch How much we expect the right order has been filled, prior to matching orders. + /// @return TransferAmounts A struct containing the expected transfer amounts. + private async _calculateExpectedTransferAmountsAsync( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + expectedLeftOrderFillAmoutBeforeMatch: BigNumber, + expectedRightOrderFillAmoutBeforeMatch: BigNumber, + ): Promise { + let amountBoughtByLeftMaker = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderUtils.getOrderHashHex(signedOrderLeft), + ); + amountBoughtByLeftMaker = amountBoughtByLeftMaker.minus(expectedLeftOrderFillAmoutBeforeMatch); + const amountSoldByLeftMaker = amountBoughtByLeftMaker + .times(signedOrderLeft.makerAssetAmount) + .dividedToIntegerBy(signedOrderLeft.takerAssetAmount); + const amountReceivedByRightMaker = amountBoughtByLeftMaker + .times(signedOrderRight.takerAssetAmount) + .dividedToIntegerBy(signedOrderRight.makerAssetAmount); + const amountReceivedByTaker = amountSoldByLeftMaker.minus(amountReceivedByRightMaker); + let amountBoughtByRightMaker = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderUtils.getOrderHashHex(signedOrderRight), + ); + amountBoughtByRightMaker = amountBoughtByRightMaker.minus(expectedRightOrderFillAmoutBeforeMatch); + const amountSoldByRightMaker = amountBoughtByRightMaker + .times(signedOrderRight.makerAssetAmount) + .dividedToIntegerBy(signedOrderRight.takerAssetAmount); + const amountReceivedByLeftMaker = amountSoldByRightMaker; + const feePaidByLeftMaker = signedOrderLeft.makerFee + .times(amountSoldByLeftMaker) + .dividedToIntegerBy(signedOrderLeft.makerAssetAmount); + const feePaidByRightMaker = signedOrderRight.makerFee + .times(amountSoldByRightMaker) + .dividedToIntegerBy(signedOrderRight.makerAssetAmount); + const feePaidByTakerLeft = signedOrderLeft.takerFee + .times(amountSoldByLeftMaker) + .dividedToIntegerBy(signedOrderLeft.makerAssetAmount); + const feePaidByTakerRight = signedOrderRight.takerFee + .times(amountSoldByRightMaker) + .dividedToIntegerBy(signedOrderRight.makerAssetAmount); + const totalFeePaidByTaker = feePaidByTakerLeft.add(feePaidByTakerRight); + const feeReceivedLeft = feePaidByLeftMaker.add(feePaidByTakerLeft); + const feeReceivedRight = feePaidByRightMaker.add(feePaidByTakerRight); + // Return values + const expectedTransferAmounts = { + // Left Maker + amountBoughtByLeftMaker, + amountSoldByLeftMaker, + amountReceivedByLeftMaker, + feePaidByLeftMaker, + // Right Maker + amountBoughtByRightMaker, + amountSoldByRightMaker, + amountReceivedByRightMaker, + feePaidByRightMaker, + // Taker + amountReceivedByTaker, + feePaidByTakerLeft, + feePaidByTakerRight, + totalFeePaidByTaker, + // Fee Recipients + feeReceivedLeft, + feeReceivedRight, + }; + return expectedTransferAmounts; + } +} diff --git a/packages/metacoin/test/utils/chai_setup.ts b/packages/metacoin/test/utils/chai_setup.ts index 1a8733093..49259a368 100644 --- a/packages/metacoin/test/utils/chai_setup.ts +++ b/packages/metacoin/test/utils/chai_setup.ts @@ -1,3 +1,4 @@ +import { BigNumber } from '@0xproject/utils'; import * as chai from 'chai'; import chaiAsPromised = require('chai-as-promised'); import ChaiBigNumber = require('chai-bignumber'); @@ -9,5 +10,12 @@ export const chaiSetup = { chai.use(ChaiBigNumber()); chai.use(dirtyChai); chai.use(chaiAsPromised); + + // Node uses '.inspect()' instead of '.toString()' for log messages + // HACK: Typescript won't allow me to mess with BigNumber.prototype + // directly, so I create an instance and then get the prototype. + Object.getPrototypeOf(new BigNumber(0)).inspect = function() { + return this.toString(); + }; }, }; diff --git a/yarn.lock b/yarn.lock index d0b1276d9..718717580 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1879,10 +1879,6 @@ buffer-from@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-0.1.2.tgz#15f4b9bcef012044df31142c14333caf6e0260d0" -buffer-from@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531" - buffer-indexof@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" @@ -2513,7 +2509,15 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concat-stream@^1.4.10, concat-stream@^1.5.0, concat-stream@^1.5.1: +concat-stream@^1.4.10, concat-stream@^1.5.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.1.tgz#261b8f518301f1d834e36342b9fea095d2620a26" + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +concat-stream@^1.5.1: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" dependencies: @@ -10139,14 +10143,14 @@ semver-sort@0.0.4: semver "^5.0.3" semver-regex "^1.0.0" -"semver@2 || 3 || 4 || 5", semver@5.5.0, semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" - -semver@5.4.1, semver@~5.4.1: +"semver@2 || 3 || 4 || 5", semver@5.4.1, semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@~5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" +semver@5.5.0, semver@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + semver@^4.1.0: version "4.3.6" resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" -- cgit v1.2.3