import { artifacts as BrokerArtifacts, BrokerContract, decodeBrokerAssetData, decodePropertyData, encodeBrokerAssetData, encodePropertyData, GodsUnchainedValidatorContract, TestGodsUnchainedContract, } from '@0x/contracts-broker'; import { DummyERC721TokenContract } from '@0x/contracts-erc721'; import { ExchangeFunctionName, ExchangeRevertErrors } from '@0x/contracts-exchange'; import { ReferenceFunctions } from '@0x/contracts-exchange-libs'; import { blockchainTests, constants, expect, getRandomInteger, randomAddress } from '@0x/contracts-test-utils'; import { assetDataUtils } from '@0x/order-utils'; import { SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import { Actor } from '../framework/actors/base'; import { Maker } from '../framework/actors/maker'; import { Taker } from '../framework/actors/taker'; import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store'; import { LocalBalanceStore } from '../framework/balances/local_balance_store'; import { DeploymentManager } from '../framework/deployment_manager'; blockchainTests.resets('Broker <> Gods Unchained integration tests', env => { let deployment: DeploymentManager; let balanceStore: BlockchainBalanceStore; let initialBalances: LocalBalanceStore; let maker: Maker; let taker: Taker; let affiliate: Actor; let broker: BrokerContract; let godsUnchained: TestGodsUnchainedContract; let validator: GodsUnchainedValidatorContract; let godsUnchainedTokenIds: BigNumber[]; const makerSpecifiedProto = new BigNumber(1337); const makerSpecifiedQuality = new BigNumber(25); before(async () => { deployment = await DeploymentManager.deployAsync(env, { numErc20TokensToDeploy: 1, numErc721TokensToDeploy: 0, numErc1155TokensToDeploy: 0, }); const [makerToken] = deployment.tokens.erc20; godsUnchained = await TestGodsUnchainedContract.deployFrom0xArtifactAsync( BrokerArtifacts.TestGodsUnchained, env.provider, env.txDefaults, BrokerArtifacts, 'Gods Unchained Cards', 'GU', ); validator = await GodsUnchainedValidatorContract.deployFrom0xArtifactAsync( BrokerArtifacts.GodsUnchainedValidator, env.provider, env.txDefaults, BrokerArtifacts, godsUnchained.address, ); broker = await BrokerContract.deployFrom0xArtifactAsync( BrokerArtifacts.Broker, env.provider, env.txDefaults, BrokerArtifacts, deployment.exchange.address, deployment.tokens.weth.address, ); const takerAssetData = encodeBrokerAssetData(broker.address, godsUnchained.address, validator.address, { proto: makerSpecifiedProto, quality: makerSpecifiedQuality, }); const orderConfig = { feeRecipientAddress: constants.NULL_ADDRESS, makerAssetData: assetDataUtils.encodeERC20AssetData(makerToken.address), takerAssetData, takerAssetAmount: new BigNumber(2), // buy 2 cards makerFeeAssetData: constants.NULL_BYTES, takerFeeAssetData: constants.NULL_BYTES, makerFee: constants.ZERO_AMOUNT, takerFee: constants.ZERO_AMOUNT, }; maker = new Maker({ name: 'Maker', deployment, orderConfig, }); taker = new Taker({ name: 'Taker', deployment }); affiliate = new Actor({ name: 'Affiliate', deployment }); // Set balances and allowances await maker.configureERC20TokenAsync(makerToken); godsUnchainedTokenIds = await taker.configureERC721TokenAsync( new DummyERC721TokenContract(godsUnchained.address, env.provider), broker.address, 5, ); await maker.configureERC20TokenAsync(deployment.tokens.weth); const tokenOwners = { Maker: maker.address, Taker: taker.address, Affiliate: affiliate.address, Broker: broker.address, StakingProxy: deployment.staking.stakingProxy.address, }; const tokenContracts = { erc20: { makerToken, WETH: deployment.tokens.weth }, erc721: { GodsUnchained: new DummyERC721TokenContract(godsUnchained.address, env.provider) }, }; const tokenIds = { erc721: { [godsUnchained.address]: godsUnchainedTokenIds }, }; balanceStore = new BlockchainBalanceStore(tokenOwners, tokenContracts, tokenIds); await balanceStore.updateBalancesAsync(); initialBalances = LocalBalanceStore.create(balanceStore); }); after(async () => { Actor.reset(); }); function simulateBrokerFills( brokeredAssets: BigNumber[], orders: SignedOrder[], takerAssetFillAmounts: BigNumber[], receipt: TransactionReceiptWithDecodedLogs, ): LocalBalanceStore { const expectedBalances = LocalBalanceStore.create(initialBalances); // Transaction gas cost expectedBalances.burnGas(receipt.from, DeploymentManager.gasPrice.times(receipt.gasUsed)); // Taker -> Maker for (const brokeredAsset of brokeredAssets) { const erc721AssetData = assetDataUtils.encodeERC721AssetData(godsUnchained.address, brokeredAsset); expectedBalances.transferAsset(taker.address, maker.address, new BigNumber(1), erc721AssetData); } // Maker -> Taker for (const [i, order] of orders.entries()) { const amount = ReferenceFunctions.safeGetPartialAmountFloor( takerAssetFillAmounts[i], order.takerAssetAmount, order.makerAssetAmount, ); expectedBalances.transferAsset(maker.address, taker.address, amount, order.makerAssetData); } // Protocol fee expectedBalances.sendEth( receipt.from, deployment.staking.stakingProxy.address, DeploymentManager.protocolFee.times(orders.length), ); expectedBalances.wrapEth( deployment.staking.stakingProxy.address, deployment.tokens.weth.address, DeploymentManager.protocolFee.times(orders.length), ); return expectedBalances; } describe('brokerTrade', () => { let order: SignedOrder; before(async () => { // The first two cards will satisfy the maker-specified proto and quality await godsUnchained .setTokenProperties(godsUnchainedTokenIds[0], makerSpecifiedProto, makerSpecifiedQuality) .awaitTransactionSuccessAsync(); await godsUnchained .setTokenProperties(godsUnchainedTokenIds[1], makerSpecifiedProto, makerSpecifiedQuality) .awaitTransactionSuccessAsync(); order = await maker.signOrderAsync(); }); for (const fnName of constants.SINGLE_FILL_FN_NAMES) { it(`${fnName} with one valid asset`, async () => { const receipt = await broker .brokerTrade( [godsUnchainedTokenIds[0]], order, new BigNumber(1), order.signature, deployment.exchange.getSelector(fnName), [], [], ) .awaitTransactionSuccessAsync({ from: taker.address, value: DeploymentManager.protocolFee, gasPrice: DeploymentManager.gasPrice, }); const expectedBalances = simulateBrokerFills( [godsUnchainedTokenIds[0]], [order], [new BigNumber(1)], receipt, ); await balanceStore.updateBalancesAsync(); balanceStore.assertEquals(expectedBalances); }); it(`${fnName} with two valid assets`, async () => { const receipt = await broker .brokerTrade( [godsUnchainedTokenIds[0], godsUnchainedTokenIds[1]], order, new BigNumber(2), order.signature, deployment.exchange.getSelector(fnName), [], [], ) .awaitTransactionSuccessAsync({ from: taker.address, value: DeploymentManager.protocolFee, gasPrice: DeploymentManager.gasPrice, }); const expectedBalances = simulateBrokerFills( [godsUnchainedTokenIds[0], godsUnchainedTokenIds[1]], [order], [new BigNumber(2)], receipt, ); await balanceStore.updateBalancesAsync(); balanceStore.assertEquals(expectedBalances); }); it(`${fnName} with one invalid asset`, async () => { const tx = broker .brokerTrade( [godsUnchainedTokenIds[2]], order, new BigNumber(1), order.signature, deployment.exchange.getSelector(fnName), [], [], ) .awaitTransactionSuccessAsync({ from: taker.address, value: DeploymentManager.protocolFee, gasPrice: DeploymentManager.gasPrice, }); return expect(tx).to.revertWith(new ExchangeRevertErrors.AssetProxyTransferError()); }); it(`${fnName} with one valid asset, one invalid asset`, async () => { const tx = broker .brokerTrade( [godsUnchainedTokenIds[0], godsUnchainedTokenIds[2]], // valid, invalid order, new BigNumber(2), order.signature, deployment.exchange.getSelector(fnName), [], [], ) .awaitTransactionSuccessAsync({ from: taker.address, value: DeploymentManager.protocolFee, gasPrice: DeploymentManager.gasPrice, }); return expect(tx).to.revertWith(new ExchangeRevertErrors.AssetProxyTransferError()); }); it(`${fnName} with too few assets`, async () => { const tx = broker .brokerTrade( [godsUnchainedTokenIds[0]], // One asset provided order, new BigNumber(2), // But two are required for the fill order.signature, deployment.exchange.getSelector(fnName), [], [], ) .awaitTransactionSuccessAsync({ from: taker.address, value: DeploymentManager.protocolFee, gasPrice: DeploymentManager.gasPrice, }); return expect(tx).to.revertWith(new ExchangeRevertErrors.AssetProxyTransferError()); }); it(`${fnName} with same asset twice`, async () => { const tx = broker .brokerTrade( [godsUnchainedTokenIds[0], godsUnchainedTokenIds[0]], order, new BigNumber(2), order.signature, deployment.exchange.getSelector(fnName), [], [], ) .awaitTransactionSuccessAsync({ from: taker.address, value: DeploymentManager.protocolFee, gasPrice: DeploymentManager.gasPrice, }); return expect(tx).to.revertWith(new ExchangeRevertErrors.AssetProxyTransferError()); }); it(`${fnName} with excess assets`, async () => { const receipt = await broker .brokerTrade( godsUnchainedTokenIds, order, new BigNumber(2), order.signature, deployment.exchange.getSelector(fnName), [], [], ) .awaitTransactionSuccessAsync({ from: taker.address, value: DeploymentManager.protocolFee, gasPrice: DeploymentManager.gasPrice, }); const expectedBalances = simulateBrokerFills( [godsUnchainedTokenIds[0], godsUnchainedTokenIds[1]], // 3rd card isn't transferred [order], [new BigNumber(2)], receipt, ); await balanceStore.updateBalancesAsync(); balanceStore.assertEquals(expectedBalances); }); } describe('ETH/WETH behavior', () => { it(`Reverts if insufficient ETH is provided`, async () => { const tx = broker .brokerTrade( [godsUnchainedTokenIds[0]], order, new BigNumber(1), order.signature, deployment.exchange.getSelector(ExchangeFunctionName.FillOrder), [], [], ) .awaitTransactionSuccessAsync({ from: taker.address, value: DeploymentManager.protocolFee.minus(1), gasPrice: DeploymentManager.gasPrice, }); return expect(tx).to.revertWith(new ExchangeRevertErrors.PayProtocolFeeError()); }); it(`Refunds sender if excess ETH is provided`, async () => { const receipt = await broker .brokerTrade( [godsUnchainedTokenIds[0]], order, new BigNumber(1), order.signature, deployment.exchange.getSelector(ExchangeFunctionName.FillOrder), [], [], ) .awaitTransactionSuccessAsync({ from: taker.address, value: DeploymentManager.protocolFee.plus(1), // 1 wei gets refunded gasPrice: DeploymentManager.gasPrice, }); const expectedBalances = simulateBrokerFills( [godsUnchainedTokenIds[0]], [order], [new BigNumber(1)], receipt, ); await balanceStore.updateBalancesAsync(); balanceStore.assertEquals(expectedBalances); }); it(`Pays a single ETH affiliate fee and refunds excess ETH`, async () => { const affiliateFee = new BigNumber(100); const receipt = await broker .brokerTrade( [godsUnchainedTokenIds[0]], order, new BigNumber(1), order.signature, deployment.exchange.getSelector(ExchangeFunctionName.FillOrder), [affiliateFee], [affiliate.address], ) .awaitTransactionSuccessAsync({ from: taker.address, value: DeploymentManager.protocolFee.plus(affiliateFee).plus(1), gasPrice: DeploymentManager.gasPrice, }); const expectedBalances = simulateBrokerFills( [godsUnchainedTokenIds[0]], [order], [new BigNumber(1)], receipt, ); expectedBalances.sendEth(receipt.from, affiliate.address, affiliateFee); await balanceStore.updateBalancesAsync(); balanceStore.assertEquals(expectedBalances); }); it(`Pays a multiple ETH affiliate fees and refunds excess ETH`, async () => { const affiliateFee = new BigNumber(100); const receipt = await broker .brokerTrade( [godsUnchainedTokenIds[0]], order, new BigNumber(1), order.signature, deployment.exchange.getSelector(ExchangeFunctionName.FillOrder), [affiliateFee, affiliateFee], [affiliate.address, maker.address], ) .awaitTransactionSuccessAsync({ from: taker.address, value: DeploymentManager.protocolFee.plus(affiliateFee.times(2)).plus(1), gasPrice: DeploymentManager.gasPrice, }); const expectedBalances = simulateBrokerFills( [godsUnchainedTokenIds[0]], [order], [new BigNumber(1)], receipt, ); expectedBalances.sendEth(receipt.from, affiliate.address, affiliateFee); expectedBalances.sendEth(receipt.from, maker.address, affiliateFee); await balanceStore.updateBalancesAsync(); balanceStore.assertEquals(expectedBalances); }); it(`Taker can fill an order with a WETH takerFee`, async () => { const wethAssetData = assetDataUtils.encodeERC20AssetData(deployment.tokens.weth.address); const takerFee = new BigNumber(100); const takerFeeOrder = await maker.signOrderAsync({ feeRecipientAddress: affiliate.address, takerFeeAssetData: wethAssetData, takerFee, }); const receipt = await broker .brokerTrade( [godsUnchainedTokenIds[0], godsUnchainedTokenIds[1]], takerFeeOrder, new BigNumber(2), takerFeeOrder.signature, deployment.exchange.getSelector(ExchangeFunctionName.FillOrder), [], [], ) .awaitTransactionSuccessAsync({ from: taker.address, value: takerFee.plus(DeploymentManager.protocolFee), gasPrice: DeploymentManager.gasPrice, }); const expectedBalances = simulateBrokerFills( [godsUnchainedTokenIds[0], godsUnchainedTokenIds[1]], [takerFeeOrder], [new BigNumber(2)], receipt, ); expectedBalances.wrapEth(taker.address, deployment.tokens.weth.address, takerFee); expectedBalances.transferAsset(taker.address, affiliate.address, takerFee, wethAssetData); await balanceStore.updateBalancesAsync(); balanceStore.assertEquals(expectedBalances); }); it(`Taker can fill a vanilla (not property-based) order through the Broker if takerAssetData = WETH`, async () => { const wethAssetData = assetDataUtils.encodeERC20AssetData(deployment.tokens.weth.address); const takerAssetAmount = constants.ONE_ETHER.dividedToIntegerBy(2); const wethOrder = await maker.signOrderAsync({ takerAssetData: wethAssetData, takerAssetAmount, }); const receipt = await broker .brokerTrade( [], wethOrder, takerAssetAmount, wethOrder.signature, deployment.exchange.getSelector(ExchangeFunctionName.FillOrder), [], [], ) .awaitTransactionSuccessAsync({ from: taker.address, value: takerAssetAmount.plus(DeploymentManager.protocolFee), gasPrice: DeploymentManager.gasPrice, }); const expectedBalances = LocalBalanceStore.create(initialBalances); expectedBalances.wrapEth( taker.address, deployment.tokens.weth.address, takerAssetAmount.plus(DeploymentManager.protocolFee), ); expectedBalances.simulateFills([wethOrder], taker.address, receipt, deployment); await balanceStore.updateBalancesAsync(); balanceStore.assertEquals(expectedBalances); }); }); }); describe('batchBrokerTrade', () => { let orders: SignedOrder[]; before(async () => { // Two orders specifying different protos/qualities const firstOrderProto = makerSpecifiedProto; const firstOrderQuality = makerSpecifiedQuality; const secondOrderProto = new BigNumber(42); const secondOrderQuality = new BigNumber(7); // First two cards satisfy the proto/quality of the first order await godsUnchained .setTokenProperties(godsUnchainedTokenIds[0], firstOrderProto, firstOrderQuality) .awaitTransactionSuccessAsync(); await godsUnchained .setTokenProperties(godsUnchainedTokenIds[1], firstOrderProto, firstOrderQuality) .awaitTransactionSuccessAsync(); // Next two cards satisfy the proto/quality of the second order await godsUnchained .setTokenProperties(godsUnchainedTokenIds[2], secondOrderProto, secondOrderQuality) .awaitTransactionSuccessAsync(); await godsUnchained .setTokenProperties(godsUnchainedTokenIds[3], secondOrderProto, secondOrderQuality) .awaitTransactionSuccessAsync(); orders = [ await maker.signOrderAsync({ takerAssetData: encodeBrokerAssetData(broker.address, godsUnchained.address, validator.address, { proto: firstOrderProto, quality: firstOrderQuality, }), }), await maker.signOrderAsync({ takerAssetData: encodeBrokerAssetData(broker.address, godsUnchained.address, validator.address, { proto: secondOrderProto, quality: secondOrderQuality, }), }), ]; }); for (const fnName of constants.BATCH_FILL_FN_NAMES) { it(`${fnName} with one order, one valid asset`, async () => { const receipt = await broker .batchBrokerTrade( [godsUnchainedTokenIds[0]], [orders[0]], [new BigNumber(1)], [orders[0].signature], deployment.exchange.getSelector(fnName), [], [], ) .awaitTransactionSuccessAsync({ from: taker.address, value: DeploymentManager.protocolFee, gasPrice: DeploymentManager.gasPrice, }); const expectedBalances = simulateBrokerFills( [godsUnchainedTokenIds[0]], [orders[0]], [new BigNumber(1)], receipt, ); await balanceStore.updateBalancesAsync(); balanceStore.assertEquals(expectedBalances); }); it(`${fnName} with two orders, one valid asset each`, async () => { const receipt = await broker .batchBrokerTrade( [godsUnchainedTokenIds[0], godsUnchainedTokenIds[2]], // valid for 1st order, valid for 2nd orders, [new BigNumber(1), new BigNumber(1)], [orders[0].signature, orders[1].signature], deployment.exchange.getSelector(fnName), [], [], ) .awaitTransactionSuccessAsync({ from: taker.address, value: DeploymentManager.protocolFee.times(2), gasPrice: DeploymentManager.gasPrice, }); const expectedBalances = simulateBrokerFills( [godsUnchainedTokenIds[0], godsUnchainedTokenIds[2]], orders, [new BigNumber(1), new BigNumber(1)], receipt, ); await balanceStore.updateBalancesAsync(); balanceStore.assertEquals(expectedBalances); }); it(`${fnName} with two orders, two valid assets each`, async () => { const receipt = await broker .batchBrokerTrade( godsUnchainedTokenIds.slice(0, 4), orders, [new BigNumber(2), new BigNumber(2)], [orders[0].signature, orders[1].signature], deployment.exchange.getSelector(fnName), [], [], ) .awaitTransactionSuccessAsync({ from: taker.address, value: DeploymentManager.protocolFee.times(2), gasPrice: DeploymentManager.gasPrice, }); const expectedBalances = simulateBrokerFills( godsUnchainedTokenIds.slice(0, 4), orders, [new BigNumber(2), new BigNumber(2)], receipt, ); await balanceStore.updateBalancesAsync(); balanceStore.assertEquals(expectedBalances); }); it(`${fnName} with two orders, two valid assets each + excess asset`, async () => { const receipt = await broker .batchBrokerTrade( godsUnchainedTokenIds, orders, [new BigNumber(2), new BigNumber(2)], [orders[0].signature, orders[1].signature], deployment.exchange.getSelector(fnName), [], [], ) .awaitTransactionSuccessAsync({ from: taker.address, value: DeploymentManager.protocolFee.times(2), gasPrice: DeploymentManager.gasPrice, }); const expectedBalances = simulateBrokerFills( godsUnchainedTokenIds.slice(0, 4), // 5th card isn't transferred orders, [new BigNumber(2), new BigNumber(2)], receipt, ); await balanceStore.updateBalancesAsync(); balanceStore.assertEquals(expectedBalances); }); } it(`batchFillOrders reverts on invalid asset`, async () => { const tx = broker .batchBrokerTrade( [...godsUnchainedTokenIds.slice(0, 3), godsUnchainedTokenIds[4]], // Last card isn't valid for 2nd order orders, [new BigNumber(2), new BigNumber(2)], [orders[0].signature, orders[1].signature], deployment.exchange.getSelector(ExchangeFunctionName.BatchFillOrders), [], [], ) .awaitTransactionSuccessAsync({ from: taker.address, value: DeploymentManager.protocolFee.times(2), gasPrice: DeploymentManager.gasPrice, }); return expect(tx).to.revertWith(new ExchangeRevertErrors.AssetProxyTransferError()); }); it(`batchFillOrKillOrders reverts on invalid asset`, async () => { const tx = broker .batchBrokerTrade( [...godsUnchainedTokenIds.slice(0, 3), godsUnchainedTokenIds[4]], // Last card isn't valid for 2nd order orders, [new BigNumber(2), new BigNumber(2)], [orders[0].signature, orders[1].signature], deployment.exchange.getSelector(ExchangeFunctionName.BatchFillOrKillOrders), [], [], ) .awaitTransactionSuccessAsync({ from: taker.address, value: DeploymentManager.protocolFee.times(2), gasPrice: DeploymentManager.gasPrice, }); return expect(tx).to.revertWith(new ExchangeRevertErrors.AssetProxyTransferError()); }); it(`batchFillOrdersNoThrow catches revert on invalid asset`, async () => { const receipt = await broker .batchBrokerTrade( [...godsUnchainedTokenIds.slice(0, 3), godsUnchainedTokenIds[4]], // Last card isn't valid for 2nd order orders, [new BigNumber(2), new BigNumber(2)], [orders[0].signature, orders[1].signature], deployment.exchange.getSelector(ExchangeFunctionName.BatchFillOrdersNoThrow), [], [], ) .awaitTransactionSuccessAsync({ from: taker.address, value: DeploymentManager.protocolFee.times(2), gasPrice: DeploymentManager.gasPrice, }); const expectedBalances = simulateBrokerFills( godsUnchainedTokenIds.slice(0, 2), // First order gets filled [orders[0]], [new BigNumber(2)], receipt, ); await balanceStore.updateBalancesAsync(); balanceStore.assertEquals(expectedBalances); }); }); describe('Data encoding/decoding tools', () => { const MAX_UINT8 = 2 ** 8 - 1; const MAX_UINT16 = 2 ** 16 - 1; it('correctly decodes property data', async () => { const properties = { proto: getRandomInteger(0, MAX_UINT16), quality: getRandomInteger(0, MAX_UINT8), }; const encoded = encodePropertyData(properties); const decoded = decodePropertyData(encoded); expect(decoded.proto).to.bignumber.equal(properties.proto); expect(decoded.quality).to.bignumber.equal(properties.quality); }); it('correctly decodes broker asset data', async () => { const properties = { proto: getRandomInteger(0, MAX_UINT16), quality: getRandomInteger(0, MAX_UINT8), }; const encoded = encodeBrokerAssetData(randomAddress(), randomAddress(), randomAddress(), properties); const decoded = decodeBrokerAssetData(encoded); expect(decoded.proto).to.bignumber.equal(properties.proto); expect(decoded.quality).to.bignumber.equal(properties.quality); }); }); }); // tslint:disable-line:max-file-line-count