diff options
author | Greg Hysen <hysz@users.noreply.github.com> | 2018-05-22 05:29:56 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-05-22 05:29:56 +0800 |
commit | d460c0e8b9c6f4081803fff4e2d2347be8cd5ce3 (patch) | |
tree | 596681d82624683e76cdd507e31c46ffad4df73b /packages/contracts/test | |
parent | 0fdf32cf6f0f1def41a1733687e977feb3a652f5 (diff) | |
parent | 11b6d290da18ea4690424a2a518413fb39a4a885 (diff) | |
download | dexon-sol-tools-d460c0e8b9c6f4081803fff4e2d2347be8cd5ce3.tar dexon-sol-tools-d460c0e8b9c6f4081803fff4e2d2347be8cd5ce3.tar.gz dexon-sol-tools-d460c0e8b9c6f4081803fff4e2d2347be8cd5ce3.tar.bz2 dexon-sol-tools-d460c0e8b9c6f4081803fff4e2d2347be8cd5ce3.tar.lz dexon-sol-tools-d460c0e8b9c6f4081803fff4e2d2347be8cd5ce3.tar.xz dexon-sol-tools-d460c0e8b9c6f4081803fff4e2d2347be8cd5ce3.tar.zst dexon-sol-tools-d460c0e8b9c6f4081803fff4e2d2347be8cd5ce3.zip |
Merge pull request #572 from 0xProject/feature/contracts/atomicMatching
Atomic Order Matching
Diffstat (limited to 'packages/contracts/test')
-rw-r--r-- | packages/contracts/test/exchange/core.ts | 28 | ||||
-rw-r--r-- | packages/contracts/test/exchange/match_orders.ts | 831 | ||||
-rw-r--r-- | packages/contracts/test/exchange/transactions.ts | 4 | ||||
-rw-r--r-- | packages/contracts/test/utils/match_order_tester.ts | 353 |
4 files changed, 1200 insertions, 16 deletions
diff --git a/packages/contracts/test/exchange/core.ts b/packages/contracts/test/exchange/core.ts index e8fb87efb..be3252800 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,7 +25,7 @@ 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 { AssetProxyId, ContractName, ERC20BalancesByOwner, ExchangeStatus, SignedOrder } from '../../src/utils/types'; import { provider, txDefaults, web3Wrapper } from '../../src/utils/web3_wrapper'; chaiSetup.configure(); @@ -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<ExchangeErrorContractEventArgs>; - const errCode = log.args.errorId; - expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_EXPIRED); + const log = res.logs[0] as LogWithDecodedArgs<ExchangeStatusContractEventArgs>; + 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<ExchangeErrorContractEventArgs>; - const errCode = log.args.errorId; - expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_FULLY_FILLED); + const log = res.logs[0] as LogWithDecodedArgs<ExchangeStatusContractEventArgs>; + 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<ExchangeErrorContractEventArgs>; - const errCode = log.args.errorId; - expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_CANCELLED); + const log = res.logs[0] as LogWithDecodedArgs<ExchangeStatusContractEventArgs>; + 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<ExchangeErrorContractEventArgs>; - const errCode = log.args.errorId; - expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_EXPIRED); + const log = res.logs[0] as LogWithDecodedArgs<ExchangeStatusContractEventArgs>; + 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..94cdf4598 --- /dev/null +++ b/packages/contracts/test/exchange/match_orders.ts @@ -0,0 +1,831 @@ +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 { artifacts } from '../../src/utils/artifacts'; +import { assetProxyUtils } from '../../src/utils/asset_proxy_utils'; +import { chaiSetup } from '../../src/utils/chai_setup'; +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, + OrderInfo, + SignedOrder, +} from '../../src/utils/types'; +import { provider, txDefaults, web3Wrapper } from '../../src/utils/web3_wrapper'; + +import { MatchOrderTester } from '../utils/match_order_tester'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +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 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(provider, usedAddresses, owner); + erc721Wrapper = new ERC721Wrapper(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 + exchange = await ExchangeContract.deployFrom0xArtifactAsync( + artifacts.Exchange, + provider, + txDefaults, + assetProxyUtils.encodeERC20ProxyData(zrxToken.address), + ); + 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, zrxToken.address); + }); + 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, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus 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, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus 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, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify right order was partially filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus 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, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was partially filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FILLABLE); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus 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, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was partially filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FILLABLE); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus 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, + takerAddress, + newERC20BalancesByOwner, + erc721TokenIdsByOwner, + leftTakerAssetFilledAmount, + rightTakerAssetFilledAmount, + ); + // Verify left order was fully filled + const leftOrderInfo2: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo2.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify second right order was partially filled + const rightOrderInfo2: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight2); + expect(rightOrderInfo2.orderStatus 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, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was partially filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus 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, + takerAddress, + newERC20BalancesByOwner, + erc721TokenIdsByOwner, + leftTakerAssetFilledAmount, + rightTakerAssetFilledAmount, + ); + // Verify second left order was partially filled + const leftOrderInfo2: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft2); + expect(leftOrderInfo2.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FILLABLE); + // Verify right order was fully filled + const rightOrderInfo2: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo2.orderStatus 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, + 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, + 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, + 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, + 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, + 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, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('Should throw 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 + return expect( + exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress), + ).to.be.rejectedWith(constants.REVERT); + }); + + it('Should throw 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 + return expect( + exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress), + ).to.be.rejectedWith(constants.REVERT); + }); + + 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, + 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, + 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, + 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, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus 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, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus 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..14930de08 --- /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 { chaiSetup } from '../../src/utils/chai_setup'; +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 { provider, web3Wrapper } from '../../src/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; + private _feeTokenAddress: string; + + /// @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. + /// @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 validates results. + /// Validation either succeeds or throws. + /// @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 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, + 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 orderTakerAssetFilledAmountLeft = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderUtils.getOrderHashHex(signedOrderLeft), + ); + const expectedOrderFilledAmountLeft = initialTakerAssetFilledAmountLeft + ? initialTakerAssetFilledAmountLeft + : new BigNumber(0); + expect(expectedOrderFilledAmountLeft).to.be.bignumber.equal(orderTakerAssetFilledAmountLeft); + // Verify Right order preconditions + const orderTakerAssetFilledAmountRight = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderUtils.getOrderHashHex(signedOrderRight), + ); + const expectedOrderFilledAmountRight = initialTakerAssetFilledAmountRight + ? initialTakerAssetFilledAmountRight + : new BigNumber(0); + expect(expectedOrderFilledAmountRight).to.be.bignumber.equal(orderTakerAssetFilledAmountRight); + // 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, + orderTakerAssetFilledAmountLeft, + orderTakerAssetFilledAmountRight, + ); + let expectedERC20BalancesByOwner: ERC20BalancesByOwner; + let expectedERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + [expectedERC20BalancesByOwner, expectedERC721TokenIdsByOwner] = this._calculateExpectedBalances( + signedOrderLeft, + signedOrderRight, + 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 orderTakerAssetFilledAmountLeft How much left order has been filled, prior to matching orders. + /// @param orderTakerAssetFilledAmountRight How much 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, + orderTakerAssetFilledAmountLeft: BigNumber, + orderTakerAssetFilledAmountRight: BigNumber, + ): Promise<TransferAmounts> { + let amountBoughtByLeftMaker = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderUtils.getOrderHashHex(signedOrderLeft), + ); + amountBoughtByLeftMaker = amountBoughtByLeftMaker.minus(orderTakerAssetFilledAmountLeft); + 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(orderTakerAssetFilledAmountRight); + 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; + } + /// @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 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 _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 = assetProxyUtils.decodeProxyDataId(signedOrderLeft.makerAssetData); + if (makerAssetProxyIdLeft === AssetProxyId.ERC20) { + // Decode asset data + const erc20ProxyData = assetProxyUtils.decodeERC20ProxyData(signedOrderLeft.makerAssetData); + const makerAssetAddressLeft = erc20ProxyData.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.amountReceivedByRightMaker, + ); + // Taker + expectedNewERC20BalancesByOwner[takerAddress][makerAssetAddressLeft] = expectedNewERC20BalancesByOwner[ + takerAddress + ][makerAssetAddressLeft].add(expectedTransferAmounts.amountReceivedByTaker); + } else if (makerAssetProxyIdLeft === AssetProxyId.ERC721) { + // Decode asset data + const erc721ProxyData = assetProxyUtils.decodeERC721ProxyData(signedOrderLeft.makerAssetData); + const makerAssetAddressLeft = erc721ProxyData.tokenAddress; + const makerAssetIdLeft = erc721ProxyData.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 = assetProxyUtils.decodeProxyDataId(signedOrderLeft.takerAssetData); + if (takerAssetProxyIdLeft === AssetProxyId.ERC20) { + // Decode asset data + const erc20ProxyData = assetProxyUtils.decodeERC20ProxyData(signedOrderLeft.takerAssetData); + const takerAssetAddressLeft = erc20ProxyData.tokenAddress; + 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 + const erc721ProxyData = assetProxyUtils.decodeERC721ProxyData(signedOrderRight.makerAssetData); + const makerAssetAddressRight = erc721ProxyData.tokenAddress; + const makerAssetIdRight = erc721ProxyData.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.totalFeePaidByTaker); + // Left Fee Recipient Fees + expectedNewERC20BalancesByOwner[feeRecipientAddressLeft][ + this._feeTokenAddress + ] = expectedNewERC20BalancesByOwner[feeRecipientAddressLeft][this._feeTokenAddress].add( + expectedTransferAmounts.feeReceivedLeft, + ); + // Right Fee Recipient Fees + expectedNewERC20BalancesByOwner[feeRecipientAddressRight][ + this._feeTokenAddress + ] = expectedNewERC20BalancesByOwner[feeRecipientAddressRight][this._feeTokenAddress].add( + expectedTransferAmounts.feeReceivedRight, + ); + + return [expectedNewERC20BalancesByOwner, expectedNewERC721TokenIdsByOwner]; + } +} |