diff --git a/contracts/exchange/CHANGELOG.json b/contracts/exchange/CHANGELOG.json index ca7469e6b5..1e066b61b9 100644 --- a/contracts/exchange/CHANGELOG.json +++ b/contracts/exchange/CHANGELOG.json @@ -29,6 +29,14 @@ { "note": "Add support for `SignatureType.WalletOrderValidator` for orders", "pr": 1774 + }, + { + "note": "Remove ZRX fees in lieu of arbitrary maker and taker fee tokens.", + "pr": 1819 + }, + { + "note": "Incorporate Multi-asset and ERC1155 tests into `fillOrder` and `matchOrders` tests", + "pr": 1819 } ] }, diff --git a/contracts/exchange/test/fill_order.ts b/contracts/exchange/test/fill_order.ts index d8ebdfb617..8ae5c21e0a 100644 --- a/contracts/exchange/test/fill_order.ts +++ b/contracts/exchange/test/fill_order.ts @@ -177,6 +177,24 @@ describe('FillOrder Tests', () => { }); describe('ERC20', () => { + const assetCombinations = getAllPossiblePairs([ + AssetDataScenario.ERC20ZeroDecimals, + AssetDataScenario.ERC20FiveDecimals, + AssetDataScenario.ERC20EighteenDecimals, + ]); + for (const [makerAsset, takerAsset] of assetCombinations) { + it(`should transfer correct amounts between ${makerAsset} and ${takerAsset}`, async () => { + const fillScenario = { + ...defaultFillScenario, + orderScenario: { + ...defaultFillScenario.orderScenario, + makerAssetDataScenario: makerAsset, + takerAssetDataScenario: takerAsset, + }, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioSuccessAsync(fillScenario); + }); + } it('should be able to pay maker fee with taker asset', async () => { const fillScenario = { ...defaultFillScenario, @@ -573,14 +591,14 @@ describe('FillOrder Tests', () => { AssetDataScenario.ERC1155NonFungible, AssetDataScenario.MultiAssetERC20, ]; - for (const [makerAssetData, takerAssetData] of getAllPossiblePairs(assetDataScenarios)) { - it(`should successfully exchange ${makerAssetData} for ${takerAssetData}`, async () => { + for (const [makerAsset, takerAsset] of getAllPossiblePairs(assetDataScenarios)) { + it(`should successfully exchange ${makerAsset} for ${takerAsset}`, async () => { const fillScenario = { ...defaultFillScenario, orderScenario: { ...defaultFillScenario.orderScenario, - makerAssetDataScenario: makerAssetData, - takerAssetDataScenario: takerAssetData, + makerAssetDataScenario: makerAsset, + takerAssetDataScenario: takerAsset, }, takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount, }; @@ -590,21 +608,21 @@ describe('FillOrder Tests', () => { }); describe('Maker/taker fee asset combinations', () => { - const feeAssetDataScenarios = [ + const feeAssetPairs = getAllPossiblePairs([ FeeAssetDataScenario.ERC20EighteenDecimals, FeeAssetDataScenario.ERC721, FeeAssetDataScenario.ERC1155Fungible, FeeAssetDataScenario.ERC1155NonFungible, FeeAssetDataScenario.MultiAssetERC20, - ]; - for (const [makerFeeAssetData, takerFeeAssetData] of getAllPossiblePairs(feeAssetDataScenarios)) { - it(`should successfully pay maker fee ${makerFeeAssetData} and taker fee ${takerFeeAssetData}`, async () => { + ]); + for (const [makerFeeAsset, takerFeeAsset] of feeAssetPairs) { + it(`should successfully pay maker fee ${makerFeeAsset} and taker fee ${takerFeeAsset}`, async () => { const fillScenario = { ...defaultFillScenario, orderScenario: { ...defaultFillScenario.orderScenario, - makerFeeAssetDataScenario: makerFeeAssetData, - takerFeeAssetDataScenario: takerFeeAssetData, + makerFeeAssetDataScenario: makerFeeAsset, + takerFeeAssetDataScenario: takerFeeAsset, }, takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyTakerAssetAmount, }; diff --git a/contracts/exchange/test/match_orders.ts b/contracts/exchange/test/match_orders.ts index 0fdbca69c8..b857f55633 100644 --- a/contracts/exchange/test/match_orders.ts +++ b/contracts/exchange/test/match_orders.ts @@ -14,8 +14,6 @@ import { DummyERC721TokenContract } from '@0x/contracts-erc721'; import { chaiSetup, constants, - ERC1155HoldingsByOwner, - ERC721TokenIdsByOwner, OrderFactory, provider, txDefaults, @@ -34,17 +32,14 @@ import { constants as exchangeConstants, ExchangeContract, ExchangeWrapper, - MatchOrderTester, ReentrantERC20TokenContract, TestExchangeInternalsContract, } from '../src'; -interface IndividualERC1155Holdings { - fungible: { - [tokenId: string]: BigNumber; - }; - nonFungible: BigNumber[]; -} +import { MatchOrderTester, TokenBalances } from './utils/match_order_tester'; + +const ONE = new BigNumber(1); +const TWO = new BigNumber(2); const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); chaiSetup.configure(); @@ -76,14 +71,12 @@ describe('matchOrders', () => { let orderFactoryLeft: OrderFactory; let orderFactoryRight: OrderFactory; - let erc721LeftMakerAssetIds: BigNumber[]; - let erc721RightMakerAssetIds: BigNumber[]; - let erc1155LeftMakerHoldings: IndividualERC1155Holdings; - let erc1155RightMakerHoldings: IndividualERC1155Holdings; + let tokenBalances: TokenBalances; let defaultERC20MakerAssetAddress: string; let defaultERC20TakerAssetAddress: string; let defaultERC721AssetAddress: string; + let defaultERC1155AssetAddress: string; let defaultFeeTokenAddress: string; let matchOrderTester: MatchOrderTester; @@ -109,41 +102,25 @@ describe('matchOrders', () => { feeRecipientAddressLeft, feeRecipientAddressRight, ] = accounts); + const addressesWithBalances = usedAddresses.slice(1); // Create wrappers - erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); - erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); - erc1155ProxyWrapper = new ERC1155ProxyWrapper(provider, usedAddresses, owner); + erc20Wrapper = new ERC20Wrapper(provider, addressesWithBalances, owner); + erc721Wrapper = new ERC721Wrapper(provider, addressesWithBalances, owner); + erc1155ProxyWrapper = new ERC1155ProxyWrapper(provider, addressesWithBalances, owner); // Deploy ERC20 token & ERC20 proxy const numDummyErc20ToDeploy = 4; - erc20Tokens = await erc20Wrapper.deployDummyTokensAsync( - numDummyErc20ToDeploy, - constants.DUMMY_TOKEN_DECIMALS, - ); + erc20Tokens = await erc20Wrapper.deployDummyTokensAsync(numDummyErc20ToDeploy, constants.DUMMY_TOKEN_DECIMALS); erc20Proxy = await erc20Wrapper.deployProxyAsync(); await erc20Wrapper.setBalancesAndAllowancesAsync(); // Deploy ERC721 token and proxy [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); erc721Proxy = await erc721Wrapper.deployProxyAsync(); await erc721Wrapper.setBalancesAndAllowancesAsync(); - const erc721Balances = await erc721Wrapper.getBalancesAsync(); - erc721LeftMakerAssetIds = erc721Balances[makerAddressLeft][erc721Token.address]; - erc721RightMakerAssetIds = erc721Balances[makerAddressRight][erc721Token.address]; // Deploy ERC1155 token and proxy [erc1155Wrapper] = await erc1155ProxyWrapper.deployDummyContractsAsync(); erc1155Token = erc1155Wrapper.getContract(); erc1155Proxy = await erc1155ProxyWrapper.deployProxyAsync(); await erc1155ProxyWrapper.setBalancesAndAllowancesAsync(); - const erc1155Holdings = await erc1155ProxyWrapper.getBalancesAsync(); - erc1155LeftMakerHoldings = getIndividualERC1155Holdings( - erc1155Holdings, - erc1155Token.address, - makerAddressLeft, - ); - erc1155RightMakerHoldings = getIndividualERC1155Holdings( - erc1155Holdings, - erc1155Token.address, - makerAddressRight, - ); // Deploy MultiAssetProxy. const multiAssetProxyContract = await MultiAssetProxyContract.deployFrom0xArtifactAsync( assetProxyArtifacts.MultiAssetProxy, @@ -178,6 +155,11 @@ describe('matchOrders', () => { { from: owner }, constants.AWAIT_TRANSACTION_MINED_MS, ); + await multiAssetProxyContract.addAuthorizedAddress.awaitTransactionSuccessAsync( + exchange.address, + { from: owner }, + constants.AWAIT_TRANSACTION_MINED_MS, + ); await erc20Proxy.addAuthorizedAddress.awaitTransactionSuccessAsync( multiAssetProxyContract.address, { from: owner }, @@ -193,8 +175,18 @@ describe('matchOrders', () => { { from: owner }, constants.AWAIT_TRANSACTION_MINED_MS, ); - await multiAssetProxyContract.addAuthorizedAddress.awaitTransactionSuccessAsync( - exchange.address, + await multiAssetProxyContract.registerAssetProxy.awaitTransactionSuccessAsync( + erc20Proxy.address, + { from: owner }, + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await multiAssetProxyContract.registerAssetProxy.awaitTransactionSuccessAsync( + erc721Proxy.address, + { from: owner }, + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await multiAssetProxyContract.registerAssetProxy.awaitTransactionSuccessAsync( + erc1155Proxy.address, { from: owner }, constants.AWAIT_TRANSACTION_MINED_MS, ); @@ -211,6 +203,7 @@ describe('matchOrders', () => { defaultERC20TakerAssetAddress = erc20Tokens[1].address; defaultFeeTokenAddress = erc20Tokens[2].address; defaultERC721AssetAddress = erc721Token.address; + defaultERC1155AssetAddress = erc1155Token.address; const domain = { verifyingContractAddress: exchange.address, chainId, @@ -240,17 +233,15 @@ describe('matchOrders', () => { orderFactoryLeft = new OrderFactory(privateKeyLeft, defaultOrderParamsLeft); const privateKeyRight = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressRight)]; orderFactoryRight = new OrderFactory(privateKeyRight, defaultOrderParamsRight); - // Set match order tester - matchOrderTester = new MatchOrderTester(exchangeWrapper, erc20Wrapper, erc721Wrapper); testExchange = await TestExchangeInternalsContract.deployFrom0xArtifactAsync( artifacts.TestExchangeInternals, provider, txDefaults, new BigNumber(chainId), ); - - console.log(erc1155LeftMakerHoldings); - console.log(erc1155RightMakerHoldings); + // Create match order tester + matchOrderTester = new MatchOrderTester(exchangeWrapper, erc20Wrapper, erc721Wrapper, erc1155ProxyWrapper); + tokenBalances = await matchOrderTester.getBalancesAsync(); }); beforeEach(async () => { await blockchainLifecycle.startAsync(); @@ -1266,267 +1257,452 @@ describe('matchOrders', () => { return expect(tx).to.revertWith(expectedError); }); - it('should transfer correct amounts when left order maker asset is an ERC721 token', async () => { - // Create orders to match - const erc721TokenToTransfer = erc721LeftMakerAssetIds[0]; - const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ - makerAssetData: assetDataUtils.encodeERC721AssetData(defaultERC721AssetAddress, erc721TokenToTransfer), - makerAssetAmount: new BigNumber(1), - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), - }); - const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ - takerAssetData: assetDataUtils.encodeERC721AssetData(defaultERC721AssetAddress, erc721TokenToTransfer), - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), - takerAssetAmount: new BigNumber(1), - }); - // Match orders - const expectedTransferAmounts = { - // Left Maker - leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(1, 0), - leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% - // Right Maker - rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(10, 18), - rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% - // Taker - leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% - rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 50% - }; - await matchOrderTester.matchOrdersAndAssertEffectsAsync( - { - leftOrder: signedOrderLeft, - rightOrder: signedOrderRight, - }, - takerAddress, - expectedTransferAmounts, - ); - }); - - it('should transfer correct amounts when right order maker asset is an ERC721 token', async () => { - // Create orders to match - const erc721TokenToTransfer = erc721RightMakerAssetIds[0]; - const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ - takerAssetData: assetDataUtils.encodeERC721AssetData(defaultERC721AssetAddress, erc721TokenToTransfer), - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), - takerAssetAmount: new BigNumber(1), - }); - const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ - makerAssetData: assetDataUtils.encodeERC721AssetData(defaultERC721AssetAddress, erc721TokenToTransfer), - makerAssetAmount: new BigNumber(1), - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(8, 18), - }); - // Match orders - const expectedTransferAmounts = { - // Left Maker - leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(10, 18), - leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% - // Right Maker - rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(1, 0), - leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(8, 18), - rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% - // Taker - leftMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(2, 18), - leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% - rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% - }; - await matchOrderTester.matchOrdersAndAssertEffectsAsync( - { - leftOrder: signedOrderLeft, - rightOrder: signedOrderRight, - }, - takerAddress, - expectedTransferAmounts, - ); - }); - - describe('fee tokens', () => { + describe('combinations', () => { // tslint:disable: enum-naming - enum TokenType { - ERC20A = 'ERC20A', - ERC20B = 'ERC20B', - ERC20C = 'ERC20C', - ERC20D = 'ERC20D', - ERC721A = 'ERC721A', - ERC721B = 'ERC721B', - ERC721C = 'ERC721C', - ERC721D = 'ERC721D', + enum AssetType { + ERC20A = 'ERC20_A', + ERC20B = 'ERC20_B', + ERC20C = 'ERC20_C', + ERC20D = 'ERC20_D', + ERC721LeftMaker = 'ERC721_LEFT_MAKER', + ERC721RightMaker = 'ERC721_RIGHT_MAKER', + ERC721Taker = 'ERC721_TAKER', + ERC1155FungibleA = 'ERC1155_FUNGIBLE_A', + ERC1155FungibleB = 'ERC1155_FUNGIBLE_B', + ERC1155FungibleC = 'ERC1155_FUNGIBLE_C', + ERC1155FungibleD = 'ERC1155_FUNGIBLE_D', + ERC1155NonFungibleLeftMaker = 'ERC1155_NON_FUNGIBLE_LEFT_MAKER', + ERC1155NonFungibleRightMaker = 'ERC1155_NON_FUNGIBLE_RIGHT_MAKER', + ERC1155NonFungibleTaker = 'ERC1155_NON_FUNGIBLE_TAKER', + MultiAssetA = 'MULTI_ASSET_A', + MultiAssetB = 'MULTI_ASSET_B', + MultiAssetC = 'MULTI_ASSET_C', + MultiAssetD = 'MULTI_ASSET_D', } + const fungibleTypes = [ + AssetType.ERC20A, + AssetType.ERC20B, + AssetType.ERC20C, + AssetType.ERC20D, + AssetType.ERC1155FungibleA, + AssetType.ERC1155FungibleB, + AssetType.ERC1155FungibleC, + AssetType.ERC1155FungibleD, + AssetType.MultiAssetA, + AssetType.MultiAssetB, + AssetType.MultiAssetC, + AssetType.MultiAssetD, + ]; interface AssetCombination { - leftMaker: TokenType; - rightMaker: TokenType; - leftMakerFee: TokenType; - rightMakerFee: TokenType; - leftTakerFee: TokenType; - rightTakerFee: TokenType; + leftMaker: AssetType; + rightMaker: AssetType; + leftMakerFee: AssetType; + rightMakerFee: AssetType; + leftTakerFee: AssetType; + rightTakerFee: AssetType; description?: string; shouldFail?: boolean; } - const feeAssetCombinations: AssetCombination[] = [ + const assetCombinations: AssetCombination[] = [ { - description: 'Swapping tokens then using them to pay maker fees.', - leftMaker: TokenType.ERC20A, - rightMaker: TokenType.ERC20B, - leftMakerFee: TokenType.ERC20B, - rightMakerFee: TokenType.ERC20A, - leftTakerFee: TokenType.ERC20C, - rightTakerFee: TokenType.ERC20C, + leftMaker: AssetType.ERC20A, + rightMaker: AssetType.ERC20B, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, }, { - description: 'Swapping tokens then using them to pay taker fees.', - leftMaker: TokenType.ERC20A, - rightMaker: TokenType.ERC20B, - leftMakerFee: TokenType.ERC20C, - rightMakerFee: TokenType.ERC20C, - leftTakerFee: TokenType.ERC20B, - rightTakerFee: TokenType.ERC20A, + leftMaker: AssetType.ERC721LeftMaker, + rightMaker: AssetType.ERC721RightMaker, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, }, { - description: 'Swapping tokens then using them to pay maker and taker fees.', - leftMaker: TokenType.ERC20A, - rightMaker: TokenType.ERC20B, - leftMakerFee: TokenType.ERC20B, - rightMakerFee: TokenType.ERC20A, - leftTakerFee: TokenType.ERC20C, - rightTakerFee: TokenType.ERC20C, + leftMaker: AssetType.ERC721LeftMaker, + rightMaker: AssetType.ERC20A, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, }, { - description: 'Paying maker and taker fees with same tokens being sold.', - leftMaker: TokenType.ERC20A, - rightMaker: TokenType.ERC20B, - leftMakerFee: TokenType.ERC20A, - rightMakerFee: TokenType.ERC20B, - leftTakerFee: TokenType.ERC20A, - rightTakerFee: TokenType.ERC20B, + leftMaker: AssetType.ERC20A, + rightMaker: AssetType.ERC721RightMaker, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, }, { - description: 'Paying maker and taker fees with same tokens being bought.', - leftMaker: TokenType.ERC20A, - rightMaker: TokenType.ERC20B, - leftMakerFee: TokenType.ERC20B, - rightMakerFee: TokenType.ERC20A, - leftTakerFee: TokenType.ERC20B, - rightTakerFee: TokenType.ERC20A, + leftMaker: AssetType.ERC1155FungibleA, + rightMaker: AssetType.ERC20A, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, }, { - description: 'Buy an ERC721 then use it to pay maker fees.', - leftMaker: TokenType.ERC20A, - rightMaker: TokenType.ERC721A, - leftMakerFee: TokenType.ERC721A, - rightMakerFee: TokenType.ERC20B, - leftTakerFee: TokenType.ERC20C, - rightTakerFee: TokenType.ERC20C, + leftMaker: AssetType.ERC20A, + rightMaker: AssetType.ERC1155FungibleB, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, }, { - description: 'Buy an ERC721 then use it to pay maker fee (the other way).', - leftMaker: TokenType.ERC721A, - rightMaker: TokenType.ERC20A, - leftMakerFee: TokenType.ERC20B, - rightMakerFee: TokenType.ERC721A, - leftTakerFee: TokenType.ERC20C, - rightTakerFee: TokenType.ERC20C, + leftMaker: AssetType.ERC1155FungibleA, + rightMaker: AssetType.ERC1155FungibleA, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.ERC1155NonFungibleLeftMaker, + rightMaker: AssetType.ERC20A, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.ERC20A, + rightMaker: AssetType.ERC1155NonFungibleRightMaker, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.ERC1155NonFungibleLeftMaker, + rightMaker: AssetType.ERC1155NonFungibleRightMaker, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.ERC1155FungibleA, + rightMaker: AssetType.ERC20A, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.ERC20A, + rightMaker: AssetType.ERC1155FungibleB, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.ERC1155FungibleB, + rightMaker: AssetType.ERC1155FungibleB, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.MultiAssetA, + rightMaker: AssetType.ERC20A, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.ERC20A, + rightMaker: AssetType.MultiAssetB, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.MultiAssetA, + rightMaker: AssetType.MultiAssetB, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.MultiAssetA, + rightMaker: AssetType.ERC1155FungibleA, + leftMakerFee: AssetType.ERC1155FungibleA, + rightMakerFee: AssetType.MultiAssetA, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + description: 'Paying maker fees with the same ERC20 tokens being bought.', + leftMaker: AssetType.ERC20A, + rightMaker: AssetType.ERC20B, + leftMakerFee: AssetType.ERC20B, + rightMakerFee: AssetType.ERC20A, + leftTakerFee: AssetType.ERC20B, + rightTakerFee: AssetType.ERC20A, + }, + { + description: 'Paying maker fees with the same ERC20 tokens being sold.', + leftMaker: AssetType.ERC20A, + rightMaker: AssetType.ERC20B, + leftMakerFee: AssetType.ERC20A, + rightMakerFee: AssetType.ERC20B, + leftTakerFee: AssetType.ERC20A, + rightTakerFee: AssetType.ERC20B, + }, + { + description: 'Using all the same ERC20 asset.', + leftMaker: AssetType.ERC20A, + rightMaker: AssetType.ERC20A, + leftMakerFee: AssetType.ERC20A, + rightMakerFee: AssetType.ERC20A, + leftTakerFee: AssetType.ERC20A, + rightTakerFee: AssetType.ERC20A, + }, + { + description: 'Paying fees with the same MAP assets being sold.', + leftMaker: AssetType.MultiAssetA, + rightMaker: AssetType.MultiAssetB, + leftMakerFee: AssetType.MultiAssetA, + rightMakerFee: AssetType.MultiAssetB, + leftTakerFee: AssetType.MultiAssetA, + rightTakerFee: AssetType.MultiAssetB, + }, + { + description: 'Paying fees with the same MAP assets being bought.', + leftMaker: AssetType.MultiAssetA, + rightMaker: AssetType.MultiAssetB, + leftMakerFee: AssetType.MultiAssetB, + rightMakerFee: AssetType.MultiAssetA, + leftTakerFee: AssetType.MultiAssetB, + rightTakerFee: AssetType.MultiAssetA, + }, + { + description: 'Using all the same MAP assets.', + leftMaker: AssetType.MultiAssetA, + rightMaker: AssetType.MultiAssetA, + leftMakerFee: AssetType.MultiAssetA, + rightMakerFee: AssetType.MultiAssetA, + leftTakerFee: AssetType.MultiAssetA, + rightTakerFee: AssetType.MultiAssetA, }, { description: 'Swapping ERC721s then using them to pay maker fees.', - leftMaker: TokenType.ERC721A, - rightMaker: TokenType.ERC721B, - leftMakerFee: TokenType.ERC721B, - rightMakerFee: TokenType.ERC721A, - leftTakerFee: TokenType.ERC20A, - rightTakerFee: TokenType.ERC20A, + leftMaker: AssetType.ERC721LeftMaker, + rightMaker: AssetType.ERC721RightMaker, + leftMakerFee: AssetType.ERC721RightMaker, + rightMakerFee: AssetType.ERC721LeftMaker, + leftTakerFee: AssetType.ERC20A, + rightTakerFee: AssetType.ERC20A, + }, + { + description: 'Swapping ERC1155 NFTs then using them to pay maker fees.', + leftMaker: AssetType.ERC1155NonFungibleLeftMaker, + rightMaker: AssetType.ERC1155NonFungibleRightMaker, + leftMakerFee: AssetType.ERC1155NonFungibleRightMaker, + rightMakerFee: AssetType.ERC1155NonFungibleLeftMaker, + leftTakerFee: AssetType.ERC20A, + rightTakerFee: AssetType.ERC20A, }, { description: 'Double-spend by trying to pay maker fees with sold ERC721 token (fail).', - leftMaker: TokenType.ERC721A, - rightMaker: TokenType.ERC721B, - leftMakerFee: TokenType.ERC721A, - rightMakerFee: TokenType.ERC721A, - leftTakerFee: TokenType.ERC20A, - rightTakerFee: TokenType.ERC20A, + leftMaker: AssetType.ERC721LeftMaker, + rightMaker: AssetType.ERC721RightMaker, + leftMakerFee: AssetType.ERC721LeftMaker, + rightMakerFee: AssetType.ERC721LeftMaker, + leftTakerFee: AssetType.ERC20A, + rightTakerFee: AssetType.ERC20A, + shouldFail: true, + }, + { + description: 'Double-spend by trying to pay maker fees with sold ERC1155 NFT (fail).', + leftMaker: AssetType.ERC20A, + rightMaker: AssetType.ERC1155NonFungibleLeftMaker, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC1155NonFungibleLeftMaker, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, shouldFail: true, }, ]; - let erc721TokenIdsByOwner: ERC721TokenIdsByOwner; - let nameToERC20Tokens: { [name: string]: string }; - let nameToERC721Tokens: { [name: string]: string }; + let nameToERC20Asset: { [name: string]: string }; + let nameToERC721Asset: { [name: string]: [string, BigNumber] }; + let nameToERC1155FungibleAsset: { [name: string]: [string, BigNumber] }; + let nameToERC1155NonFungibleAsset: { [name: string]: [string, BigNumber] }; + let nameToMultiAssetAsset: { [name: string]: [BigNumber[], string[]] }; - function getAssetData(tokenType: TokenType, ownerAddress: string): string { + function getAssetData(assetType: AssetType): string { const encodeERC20AssetData = assetDataUtils.encodeERC20AssetData; const encodeERC721AssetData = assetDataUtils.encodeERC721AssetData; - if (nameToERC20Tokens[tokenType] !== undefined) { - const tokenAddress = nameToERC20Tokens[tokenType]; + const encodeERC1155AssetData = assetDataUtils.encodeERC1155AssetData; + const encodeMultiAssetData = assetDataUtils.encodeMultiAssetData; + if (nameToERC20Asset[assetType] !== undefined) { + const tokenAddress = nameToERC20Asset[assetType]; return encodeERC20AssetData(tokenAddress); } - if (nameToERC721Tokens[tokenType] !== undefined) { - const tokenAddress = nameToERC721Tokens[tokenType]; - const tokenIdx = tokenType.charCodeAt(tokenType.length - 1) - 'A'.charCodeAt(0); - const tokenId = erc721TokenIdsByOwner[ownerAddress][tokenAddress][tokenIdx]; - return encodeERC721AssetData(nameToERC721Tokens[tokenType], tokenId); + if (nameToERC721Asset[assetType] !== undefined) { + const [tokenAddress, tokenId] = nameToERC721Asset[assetType]; + return encodeERC721AssetData(tokenAddress, tokenId); } - return '0x'; + if (nameToERC1155FungibleAsset[assetType] !== undefined) { + const [tokenAddress, tokenId] = nameToERC1155FungibleAsset[assetType]; + return encodeERC1155AssetData(tokenAddress, [tokenId], [ONE], constants.NULL_BYTES); + } + if (nameToERC1155NonFungibleAsset[assetType] !== undefined) { + const [tokenAddress, tokenId] = nameToERC1155NonFungibleAsset[assetType]; + return encodeERC1155AssetData(tokenAddress, [tokenId], [ONE], constants.NULL_BYTES); + } + if (nameToMultiAssetAsset[assetType] !== undefined) { + const [amounts, nestedAssetData] = nameToMultiAssetAsset[assetType]; + return encodeMultiAssetData(amounts, nestedAssetData); + } + throw new Error(`Unknown asset type: ${assetType}`); } before(async () => { - erc721TokenIdsByOwner = await erc721Wrapper.getBalancesAsync(); - nameToERC20Tokens = { - ERC20A: erc20Tokens[0].address, - ERC20B: erc20Tokens[1].address, - ERC20C: erc20Tokens[0].address, - ERC20D: erc20Tokens[1].address, + nameToERC20Asset = { + ERC20_A: erc20Tokens[0].address, + ERC20_B: erc20Tokens[1].address, + ERC20_C: erc20Tokens[2].address, + ERC20_D: erc20Tokens[3].address, }; - nameToERC721Tokens = { - ERC721A: erc721Token.address, - ERC721B: erc721Token.address, - ERC721C: erc721Token.address, - ERC721D: erc721Token.address, + const erc721TokenIds = _.mapValues(tokenBalances.erc721, v => v[defaultERC721AssetAddress.address][0]); + nameToERC721Asset = { + ERC721_LEFT_MAKER: [defaultERC721AssetAddress.address, erc721TokenIds[makerAddressLeft]], + ERC721_RIGHT_MAKER: [defaultERC721AssetAddress.address, erc721TokenIds[makerAddressRight]], + ERC721_TAKER: [defaultERC721AssetAddress.address, erc721TokenIds[takerAddress]], + }; + const erc1155FungibleTokens = _.keys( + _.values(tokenBalances.erc1155)[0][defaultERC1155AssetAddress].fungible, + ).map(k => new BigNumber(k)); + nameToERC1155FungibleAsset = { + ERC1155_FUNGIBLE_A: [defaultERC1155AssetAddress, erc1155FungibleTokens[0]], + ERC1155_FUNGIBLE_B: [defaultERC1155AssetAddress, erc1155FungibleTokens[1]], + ERC1155_FUNGIBLE_C: [defaultERC1155AssetAddress, erc1155FungibleTokens[2]], + ERC1155_FUNGIBLE_D: [defaultERC1155AssetAddress, erc1155FungibleTokens[3]], + }; + const erc1155NonFungibleTokenIds = _.mapValues( + tokenBalances.erc1155, + v => v[defaultERC1155AssetAddress].nonFungible[0], + ); + nameToERC1155NonFungibleAsset = { + ERC1155_NON_FUNGIBLE_LEFT_MAKER: [ + defaultERC1155AssetAddress, + erc1155NonFungibleTokenIds[makerAddressLeft], + ], + ERC1155_NON_FUNGIBLE_RIGHT_MAKER: [ + defaultERC1155AssetAddress, + erc1155NonFungibleTokenIds[makerAddressRight], + ], + ERC1155_NON_FUNGIBLE_TAKER: [defaultERC1155AssetAddress, erc1155NonFungibleTokenIds[takerAddress]], + }; + nameToMultiAssetAsset = { + MULTI_ASSET_A: [ + [ONE, TWO], + [ + assetDataUtils.encodeERC20AssetData(erc20Tokens[0].address), + assetDataUtils.encodeERC1155AssetData( + defaultERC1155AssetAddress, + [erc1155FungibleTokens[0]], + [ONE], + constants.NULL_BYTES, + ), + ], + ], + MULTI_ASSET_B: [ + [ONE, TWO], + [ + assetDataUtils.encodeERC20AssetData(erc20Tokens[1].address), + assetDataUtils.encodeERC1155AssetData( + defaultERC1155AssetAddress, + [erc1155FungibleTokens[1]], + [ONE], + constants.NULL_BYTES, + ), + ], + ], + MULTI_ASSET_C: [ + [ONE, TWO], + [ + assetDataUtils.encodeERC20AssetData(erc20Tokens[2].address), + assetDataUtils.encodeERC1155AssetData( + defaultERC1155AssetAddress, + [erc1155FungibleTokens[2]], + [ONE], + constants.NULL_BYTES, + ), + ], + ], + MULTI_ASSET_D: [ + [ONE, TWO], + [ + assetDataUtils.encodeERC20AssetData(erc20Tokens[3].address), + assetDataUtils.encodeERC1155AssetData( + erc1155Token.address, + [erc1155FungibleTokens[3]], + [ONE], + constants.NULL_BYTES, + ), + ], + ], }; }); - for (const combo of feeAssetCombinations) { + for (const combo of assetCombinations) { const description = combo.description || JSON.stringify(combo); it(description, async () => { // Create orders to match. For ERC20s, there will be a spread. - const leftMakerAssetAmount = combo.leftMaker.startsWith('ERC20') + const leftMakerAssetAmount = _.includes(fungibleTypes, combo.leftMaker) ? Web3Wrapper.toBaseUnitAmount(15, 18) : Web3Wrapper.toBaseUnitAmount(1, 0); - const leftTakerAssetAmount = combo.rightMaker.startsWith('ERC20') + const leftTakerAssetAmount = _.includes(fungibleTypes, combo.rightMaker) ? Web3Wrapper.toBaseUnitAmount(30, 18) : Web3Wrapper.toBaseUnitAmount(1, 0); - const rightMakerAssetAmount = combo.rightMaker.startsWith('ERC20') + const rightMakerAssetAmount = _.includes(fungibleTypes, combo.rightMaker) ? Web3Wrapper.toBaseUnitAmount(30, 18) : Web3Wrapper.toBaseUnitAmount(1, 0); - const rightTakerAssetAmount = combo.leftMaker.startsWith('ERC20') + const rightTakerAssetAmount = _.includes(fungibleTypes, combo.leftMaker) ? Web3Wrapper.toBaseUnitAmount(14, 18) : Web3Wrapper.toBaseUnitAmount(1, 0); - const leftMakerFeeAssetAmount = combo.leftMakerFee.startsWith('ERC20') + const leftMakerFeeAssetAmount = _.includes(fungibleTypes, combo.leftMakerFee) ? Web3Wrapper.toBaseUnitAmount(8, 12) : Web3Wrapper.toBaseUnitAmount(1, 0); - const rightMakerFeeAssetAmount = combo.rightMakerFee.startsWith('ERC20') + const rightMakerFeeAssetAmount = _.includes(fungibleTypes, combo.rightMakerFee) ? Web3Wrapper.toBaseUnitAmount(7, 12) : Web3Wrapper.toBaseUnitAmount(1, 0); - const leftTakerFeeAssetAmount = combo.leftTakerFee.startsWith('ERC20') + const leftTakerFeeAssetAmount = _.includes(fungibleTypes, combo.leftTakerFee) ? Web3Wrapper.toBaseUnitAmount(6, 12) : Web3Wrapper.toBaseUnitAmount(1, 0); - const rightTakerFeeAssetAmount = combo.rightTakerFee.startsWith('ERC20') + const rightTakerFeeAssetAmount = _.includes(fungibleTypes, combo.rightTakerFee) ? Web3Wrapper.toBaseUnitAmount(5, 12) : Web3Wrapper.toBaseUnitAmount(1, 0); - const leftMakerAssetReceivedByTakerAmount = combo.leftMaker.startsWith('ERC20') + const leftMakerAssetReceivedByTakerAmount = _.includes(fungibleTypes, combo.leftMaker) ? leftMakerAssetAmount.minus(rightTakerAssetAmount) : Web3Wrapper.toBaseUnitAmount(0, 0); const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ - makerAssetData: getAssetData(combo.leftMaker, makerAddressLeft), - takerAssetData: getAssetData(combo.rightMaker, makerAddressRight), - makerFeeAssetData: getAssetData(combo.leftMakerFee, makerAddressLeft), - takerFeeAssetData: getAssetData(combo.leftTakerFee, takerAddress), + makerAssetData: getAssetData(combo.leftMaker), + takerAssetData: getAssetData(combo.rightMaker), + makerFeeAssetData: getAssetData(combo.leftMakerFee), + takerFeeAssetData: getAssetData(combo.leftTakerFee), makerAssetAmount: leftMakerAssetAmount, takerAssetAmount: leftTakerAssetAmount, makerFee: leftMakerFeeAssetAmount, takerFee: leftTakerFeeAssetAmount, }); const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ - makerAssetData: getAssetData(combo.rightMaker, makerAddressRight), - takerAssetData: getAssetData(combo.leftMaker, makerAddressLeft), - makerFeeAssetData: getAssetData(combo.rightMakerFee, makerAddressRight), - takerFeeAssetData: getAssetData(combo.rightTakerFee, takerAddress), + makerAssetData: getAssetData(combo.rightMaker), + takerAssetData: getAssetData(combo.leftMaker), + makerFeeAssetData: getAssetData(combo.rightMakerFee), + takerFeeAssetData: getAssetData(combo.rightTakerFee), makerAssetAmount: rightMakerAssetAmount, takerAssetAmount: rightTakerAssetAmount, makerFee: rightMakerFeeAssetAmount, @@ -1536,15 +1712,15 @@ describe('matchOrders', () => { const expectedTransferAmounts = { // Left Maker leftMakerAssetSoldByLeftMakerAmount: leftMakerAssetAmount, - leftMakerFeeAssetPaidByLeftMakerAmount: leftMakerFeeAssetAmount, // 100% + leftMakerFeeAssetPaidByLeftMakerAmount: leftMakerFeeAssetAmount, // Right Maker rightMakerAssetSoldByRightMakerAmount: rightMakerAssetAmount, leftMakerAssetBoughtByRightMakerAmount: rightTakerAssetAmount, - rightMakerFeeAssetPaidByRightMakerAmount: rightMakerFeeAssetAmount, // 100% + rightMakerFeeAssetPaidByRightMakerAmount: rightMakerFeeAssetAmount, // Taker leftMakerAssetReceivedByTakerAmount, - leftTakerFeeAssetPaidByTakerAmount: leftTakerFeeAssetAmount, // 100% - rightTakerFeeAssetPaidByTakerAmount: rightTakerFeeAssetAmount, // 100% + leftTakerFeeAssetPaidByTakerAmount: leftTakerFeeAssetAmount, + rightTakerFeeAssetPaidByTakerAmount: rightTakerFeeAssetAmount, }; if (!combo.shouldFail) { await matchOrderTester.matchOrdersAndAssertEffectsAsync( @@ -1564,18 +1740,4 @@ describe('matchOrders', () => { }); }); }); - -function getIndividualERC1155Holdings( - erc1155HoldingsByOwner: ERC1155HoldingsByOwner, - tokenAddress: string, - ownerAddress: string, -): IndividualERC1155Holdings { - return { - fungible: erc1155HoldingsByOwner.fungible[ownerAddress][tokenAddress], - nonFungible: _.uniqBy(_.flatten(_.values( - erc1155HoldingsByOwner.nonFungible[ownerAddress][tokenAddress])), - v => v.toString(10), - ), - }; -} // tslint:disable-line:max-file-line-count diff --git a/contracts/exchange/test/utils/asset_wrapper.ts b/contracts/exchange/test/utils/asset_wrapper.ts index c3f85c3c3b..9c560dc6c7 100644 --- a/contracts/exchange/test/utils/asset_wrapper.ts +++ b/contracts/exchange/test/utils/asset_wrapper.ts @@ -96,24 +96,24 @@ export class AssetWrapper { assetProxyData.tokenAddress, assetProxyData.tokenId, ); - if (!doesTokenExist && desiredBalance.gt(0)) { + if (!doesTokenExist && desiredBalance.gte(1)) { await erc721Wrapper.mintAsync(assetProxyData.tokenAddress, assetProxyData.tokenId, userAddress); return; - } else if (!doesTokenExist && desiredBalance.lte(0)) { + } else if (!doesTokenExist && desiredBalance.lt(1)) { return; // noop } const tokenOwner = await erc721Wrapper.ownerOfAsync( assetProxyData.tokenAddress, assetProxyData.tokenId, ); - if (userAddress !== tokenOwner && desiredBalance.gt(0)) { + if (userAddress !== tokenOwner && desiredBalance.gte(1)) { await erc721Wrapper.transferFromAsync( assetProxyData.tokenAddress, assetProxyData.tokenId, tokenOwner, userAddress, ); - } else if (tokenOwner === userAddress && desiredBalance.lte(0)) { + } else if (tokenOwner === userAddress && desiredBalance.lt(1)) { // Burn token await erc721Wrapper.transferFromAsync( assetProxyData.tokenAddress, @@ -123,8 +123,8 @@ export class AssetWrapper { ); return; } else if ( - (userAddress !== tokenOwner && desiredBalance.lte(0)) || - (tokenOwner === userAddress && desiredBalance.gt(0)) + (userAddress !== tokenOwner && desiredBalance.lt(1)) || + (tokenOwner === userAddress && desiredBalance.gte(1)) ) { return; // noop } @@ -168,7 +168,7 @@ export class AssetWrapper { } } else { const nftOwner = await assetWrapper.getOwnerOfAsync(tokenId); - if (scaledDesiredBalance.gt(0)) { + if (scaledDesiredBalance.gte(1)) { if (nftOwner === userAddress) { // Nothing to do. } else if (nftOwner !== constants.NULL_ADDRESS) { diff --git a/contracts/exchange/test/utils/fill_order_combinatorial_utils.ts b/contracts/exchange/test/utils/fill_order_combinatorial_utils.ts index a36914e7db..11def47b43 100644 --- a/contracts/exchange/test/utils/fill_order_combinatorial_utils.ts +++ b/contracts/exchange/test/utils/fill_order_combinatorial_utils.ts @@ -254,7 +254,8 @@ export class FillOrderCombinatorialUtils { // ExpirationTimeSecondsScenario.InPast, ]; const makerAssetDataScenario = [ - AssetDataScenario.ERC20FiveDecimals, + // FeeAssetDataScenario.ERC20ZeroDecimals, + // AssetDataScenario.ERC20FiveDecimals, AssetDataScenario.ERC20EighteenDecimals, AssetDataScenario.ERC721, AssetDataScenario.ERC1155Fungible, @@ -262,7 +263,8 @@ export class FillOrderCombinatorialUtils { AssetDataScenario.MultiAssetERC20, ]; const takerAssetDataScenario = [ - AssetDataScenario.ERC20FiveDecimals, + // FeeAssetDataScenario.ERC20ZeroDecimals, + // AssetDataScenario.ERC20FiveDecimals, AssetDataScenario.ERC20EighteenDecimals, AssetDataScenario.ERC721, AssetDataScenario.ERC1155Fungible, @@ -270,7 +272,8 @@ export class FillOrderCombinatorialUtils { AssetDataScenario.MultiAssetERC20, ]; const makerFeeAssetDataScenario = [ - FeeAssetDataScenario.ERC20FiveDecimals, + // FeeAssetDataScenario.ERC20ZeroDecimals, + // FeeAssetDataScenario.ERC20FiveDecimals, FeeAssetDataScenario.ERC20EighteenDecimals, FeeAssetDataScenario.ERC721, FeeAssetDataScenario.ERC1155Fungible, @@ -280,7 +283,8 @@ export class FillOrderCombinatorialUtils { FeeAssetDataScenario.TakerToken, ]; const takerFeeAssetDataScenario = [ - FeeAssetDataScenario.ERC20FiveDecimals, + // FeeAssetDataScenario.ERC20ZeroDecimals, + // FeeAssetDataScenario.ERC20FiveDecimals, FeeAssetDataScenario.ERC20EighteenDecimals, FeeAssetDataScenario.ERC721, FeeAssetDataScenario.ERC1155Fungible, diff --git a/contracts/exchange/test/utils/index.ts b/contracts/exchange/test/utils/index.ts index 4d5785a54a..3fe740f591 100644 --- a/contracts/exchange/test/utils/index.ts +++ b/contracts/exchange/test/utils/index.ts @@ -1,5 +1,4 @@ export * from './exchange_wrapper'; -export * from './match_order_tester'; export * from './exchange_data_encoder'; export * from './types'; export * from './constants'; diff --git a/contracts/exchange/test/utils/match_order_tester.ts b/contracts/exchange/test/utils/match_order_tester.ts index 8962bc5798..1f5c9b24bc 100644 --- a/contracts/exchange/test/utils/match_order_tester.ts +++ b/contracts/exchange/test/utils/match_order_tester.ts @@ -1,5 +1,5 @@ -import { ERC20Wrapper, ERC721Wrapper } from '@0x/contracts-asset-proxy'; -import { chaiSetup, OrderStatus, TokenBalancesByOwner } from '@0x/contracts-test-utils'; +import { ERC1155ProxyWrapper, ERC20Wrapper, ERC721Wrapper } from '@0x/contracts-asset-proxy'; +import { chaiSetup, ERC1155HoldingsByOwner, OrderStatus } from '@0x/contracts-test-utils'; import { assetDataUtils, orderHashUtils } from '@0x/order-utils'; import { AssetProxyId, SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; @@ -14,6 +14,13 @@ const ZERO = new BigNumber(0); chaiSetup.configure(); const expect = chai.expect; +export interface IndividualERC1155Holdings { + fungible: { + [tokenId: string]: BigNumber; + }; + nonFungible: BigNumber[]; +} + export interface FillEventArgs { orderHash: string; makerAddress: string; @@ -46,7 +53,32 @@ export interface MatchTransferAmounts { export interface MatchResults { orders: MatchedOrders; fills: FillEventArgs[]; - balances: TokenBalancesByOwner; + balances: TokenBalances; +} + +export interface ERC1155Holdings { + [owner: string]: { + [contract: string]: { + fungible: { + [tokenId: string]: BigNumber; + }; + nonFungible: BigNumber[]; + }; + }; +} + +export interface TokenBalances { + erc20: { + [owner: string]: { + [contract: string]: BigNumber; + }; + }; + erc721: { + [owner: string]: { + [contract: string]: BigNumber[]; + }; + }; + erc1155: ERC1155Holdings; } export interface MatchedOrders { @@ -66,14 +98,16 @@ export class MatchOrderTester { public exchangeWrapper: ExchangeWrapper; public erc20Wrapper: ERC20Wrapper; public erc721Wrapper: ERC721Wrapper; + public erc1155ProxyWrapper: ERC1155ProxyWrapper; public matchOrdersCallAsync?: MatchOrdersAsyncCall; - private readonly _initialTokenBalancesPromise: Promise; + private readonly _initialTokenBalancesPromise: Promise; /** * 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 erc1155Wrapper Used to fetch ERC1155 token owners. * @param matchOrdersCallAsync Optional, custom caller for * `ExchangeWrapper.matchOrdersAsync()`. */ @@ -81,11 +115,13 @@ export class MatchOrderTester { exchangeWrapper: ExchangeWrapper, erc20Wrapper: ERC20Wrapper, erc721Wrapper: ERC721Wrapper, + erc1155ProxyWrapper: ERC1155ProxyWrapper, matchOrdersCallAsync?: MatchOrdersAsyncCall, ) { this.exchangeWrapper = exchangeWrapper; this.erc20Wrapper = erc20Wrapper; this.erc721Wrapper = erc721Wrapper; + this.erc1155ProxyWrapper = erc1155ProxyWrapper; this.matchOrdersCallAsync = matchOrdersCallAsync; this._initialTokenBalancesPromise = this.getBalancesAsync(); } @@ -103,7 +139,7 @@ export class MatchOrderTester { orders: MatchedOrders, takerAddress: string, expectedTransferAmounts: Partial, - initialTokenBalances?: TokenBalancesByOwner, + initialTokenBalances?: TokenBalances, ): Promise { await assertInitialOrderStatesAsync(orders, this.exchangeWrapper); // Get the token balances before executing `matchOrders()`. @@ -136,8 +172,8 @@ export class MatchOrderTester { /** * Fetch the current token balances of all known accounts. */ - public async getBalancesAsync(): Promise { - return getTokenBalancesAsync(this.erc20Wrapper, this.erc721Wrapper); + public async getBalancesAsync(): Promise { + return getTokenBalancesAsync(this.erc20Wrapper, this.erc721Wrapper, this.erc1155ProxyWrapper); } private async _executeMatchOrdersAsync( @@ -203,7 +239,7 @@ function toFullMatchTransferAmounts(partial: Partial): Mat function simulateMatchOrders( orders: MatchedOrders, takerAddress: string, - tokenBalances: TokenBalancesByOwner, + tokenBalances: TokenBalances, transferAmounts: MatchTransferAmounts, ): MatchResults { // prettier-ignore @@ -336,8 +372,47 @@ function transferAsset( const tokenId = erc721AssetData.tokenId; const fromTokens = matchResults.balances.erc721[fromAddress][assetAddress]; const toTokens = matchResults.balances.erc721[toAddress][assetAddress]; - _.remove(fromTokens, tokenId); - toTokens.push(tokenId); + if (amount.gte(1)) { + const tokenIndex = _.findIndex(fromTokens, t => t.eq(tokenId)); + if (tokenIndex !== -1) { + fromTokens.splice(tokenIndex, 1); + toTokens.push(tokenId); + } + } + break; + } + case AssetProxyId.ERC1155: { + const erc1155AssetData = assetDataUtils.decodeERC1155AssetData(assetData); + const assetAddress = erc1155AssetData.tokenAddress; + const fromBalances = matchResults.balances.erc1155[fromAddress][assetAddress]; + const toBalances = matchResults.balances.erc1155[toAddress][assetAddress]; + for (const i of _.times(erc1155AssetData.tokenIds.length)) { + const tokenId = erc1155AssetData.tokenIds[i]; + const tokenValue = erc1155AssetData.tokenValues[i]; + const tokenAmount = amount.times(tokenValue); + if (tokenAmount.gt(0)) { + const tokenIndex = _.findIndex(fromBalances.nonFungible, t => t.eq(tokenId)); + if (tokenIndex !== -1) { + // Transfer a non-fungible. + fromBalances.nonFungible.splice(tokenIndex, 1); + toBalances.nonFungible.push(tokenId); + } else { + // Transfer a fungible. + const _tokenId = tokenId.toString(10); + fromBalances.fungible[_tokenId] = fromBalances.fungible[_tokenId].minus(tokenAmount); + toBalances.fungible[_tokenId] = toBalances.fungible[_tokenId].plus(tokenAmount); + } + } + } + break; + } + case AssetProxyId.MultiAsset: { + const multiAssetData = assetDataUtils.decodeMultiAssetData(assetData); + for (const i of _.times(multiAssetData.amounts.length)) { + const nestedAmount = amount.times(multiAssetData.amounts[i]); + const nestedAssetData = multiAssetData.nestedAssetData[i]; + transferAsset(fromAddress, toAddress, nestedAmount, nestedAssetData, matchResults); + } break; } default: @@ -355,7 +430,7 @@ function transferAsset( async function assertMatchResultsAsync( matchResults: MatchResults, transactionReceipt: TransactionReceiptWithDecodedLogs, - actualTokenBalances: TokenBalancesByOwner, + actualTokenBalances: TokenBalances, exchangeWrapper: ExchangeWrapper, ): Promise { // Check the fill events. @@ -462,21 +537,8 @@ function extractFillEventsfromReceipt(receipt: TransactionReceiptWithDecodedLogs * @param expectedBalances Expected balances. * @param actualBalances Actual balances. */ -function assertBalances(expectedBalances: TokenBalancesByOwner, actualBalances: TokenBalancesByOwner): void { - // ERC20 Balances - expect(actualBalances.erc20, 'ERC20 balances').to.deep.equal(expectedBalances.erc20); - // ERC721 Token Ids - const sortedExpectedERC721Balances = _.mapValues(expectedBalances.erc721, tokenIdsByOwner => { - _.mapValues(tokenIdsByOwner, tokenIds => { - _.sortBy(tokenIds); - }); - }); - const sortedActualERC721Balances = _.mapValues(actualBalances.erc721, tokenIdsByOwner => { - _.mapValues(tokenIdsByOwner, tokenIds => { - _.sortBy(tokenIds); - }); - }); - expect(sortedExpectedERC721Balances, 'ERC721 balances').to.deep.equal(sortedActualERC721Balances); +function assertBalances(expectedBalances: TokenBalances, actualBalances: TokenBalances): void { + expect(encodeTokenBalances(expectedBalances)).to.deep.equal(encodeTokenBalances(actualBalances)); } /** @@ -533,17 +595,57 @@ async function assertPostExchangeStateAsync( * Retrive the current token balances of all known addresses. * @param erc20Wrapper The ERC20Wrapper instance. * @param erc721Wrapper The ERC721Wrapper instance. - * @return A promise that resolves to a `TokenBalancesByOwner`. + * @param erc1155Wrapper The ERC1155ProxyWrapper instance. + * @return A promise that resolves to a `TokenBalances`. */ export async function getTokenBalancesAsync( erc20Wrapper: ERC20Wrapper, erc721Wrapper: ERC721Wrapper, -): Promise { - const [erc20, erc721] = await Promise.all([erc20Wrapper.getBalancesAsync(), erc721Wrapper.getBalancesAsync()]); + erc1155ProxyWrapper: ERC1155ProxyWrapper, +): Promise { + const [erc20, erc721, erc1155] = await Promise.all([ + erc20Wrapper.getBalancesAsync(), + erc721Wrapper.getBalancesAsync(), + erc1155ProxyWrapper.getBalancesAsync(), + ]); return { erc20, erc721, + erc1155: transformERC1155Holdings(erc1155), }; } +/** + * Restructures `ERC1155HoldingsByOwner` to be compatible with `TokenBalances.erc1155`. + * @param erc1155HoldingsByOwner Holdings returned by `ERC1155ProxyWrapper.getBalancesAsync()`. + */ +function transformERC1155Holdings(erc1155HoldingsByOwner: ERC1155HoldingsByOwner): ERC1155Holdings { + const result = {}; + for (const owner of _.keys(erc1155HoldingsByOwner.fungible)) { + for (const contract of _.keys(erc1155HoldingsByOwner.fungible[owner])) { + _.set(result as any, [owner, contract, 'fungible'], erc1155HoldingsByOwner.fungible[owner][contract]); + } + } + for (const owner of _.keys(erc1155HoldingsByOwner.nonFungible)) { + for (const contract of _.keys(erc1155HoldingsByOwner.nonFungible[owner])) { + const tokenIds = _.flatten(_.values(erc1155HoldingsByOwner.nonFungible[owner][contract])); + _.set(result as any, [owner, contract, 'nonFungible'], _.uniqBy(tokenIds, v => v.toString(10))); + } + } + return result; +} + +function encodeTokenBalances(obj: any): any { + if (!_.isPlainObject(obj)) { + if (BigNumber.isBigNumber(obj)) { + return obj.toString(10); + } + if (_.isArray(obj)) { + return _.sortBy(obj, v => encodeTokenBalances(v)); + } + return obj; + } + const keys = _.keys(obj).sort(); + return _.zip(keys, keys.map(k => encodeTokenBalances(obj[k]))); +} // tslint:disable-line:max-file-line-count