import { DevUtilsContract } from '@0x/contracts-dev-utils'; import { DummyERC20TokenContract } from '@0x/contracts-erc20'; import { DummyERC721TokenContract } from '@0x/contracts-erc721'; import { artifacts as exchangeArtifacts, ExchangeContract } from '@0x/contracts-exchange'; import { artifacts, ForwarderContract, ForwarderRevertErrors } from '@0x/contracts-exchange-forwarder'; import { blockchainTests, constants, expect, getLatestBlockTimestampAsync, getPercentageOfValue, provider, toBaseUnitAmount, } from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; import { FeeRecipient } from '../framework/actors/fee_recipient'; import { Maker } from '../framework/actors/maker'; import { Taker } from '../framework/actors/taker'; import { actorAddressesByName } from '../framework/actors/utils'; import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store'; import { LocalBalanceStore } from '../framework/balances/local_balance_store'; import { DeploymentManager } from '../framework/deployment_manager'; import { deployForwarderAsync } from './deploy_forwarder'; import { ForwarderTestFactory } from './forwarder_test_factory'; const devUtils = new DevUtilsContract(constants.NULL_ADDRESS, provider); blockchainTests('Forwarder integration tests', env => { let deployment: DeploymentManager; let forwarder: ForwarderContract; let balanceStore: BlockchainBalanceStore; let testFactory: ForwarderTestFactory; let makerToken: DummyERC20TokenContract; let makerFeeToken: DummyERC20TokenContract; let anotherErc20Token: DummyERC20TokenContract; let erc721Token: DummyERC721TokenContract; let nftId: BigNumber; let wethAssetData: string; let makerAssetData: string; let maker: Maker; let taker: Taker; let orderFeeRecipient: FeeRecipient; let forwarderFeeRecipient: FeeRecipient; before(async () => { deployment = await DeploymentManager.deployAsync(env, { numErc20TokensToDeploy: 3, numErc721TokensToDeploy: 1, numErc1155TokensToDeploy: 0, }); forwarder = await deployForwarderAsync(deployment, env); [makerToken, makerFeeToken, anotherErc20Token] = deployment.tokens.erc20; [erc721Token] = deployment.tokens.erc721; wethAssetData = await devUtils.encodeERC20AssetData(deployment.tokens.weth.address).callAsync(); makerAssetData = await devUtils.encodeERC20AssetData(makerToken.address).callAsync(); taker = new Taker({ name: 'Taker', deployment }); orderFeeRecipient = new FeeRecipient({ name: 'Order fee recipient', deployment, }); forwarderFeeRecipient = new FeeRecipient({ name: 'Forwarder fee recipient', deployment, }); maker = new Maker({ name: 'Maker', deployment, orderConfig: { feeRecipientAddress: orderFeeRecipient.address, makerAssetAmount: toBaseUnitAmount(2), takerAssetAmount: toBaseUnitAmount(1), makerAssetData, takerAssetData: wethAssetData, takerFee: constants.ZERO_AMOUNT, makerFeeAssetData: await devUtils.encodeERC20AssetData(makerFeeToken.address).callAsync(), takerFeeAssetData: wethAssetData, }, }); await maker.configureERC20TokenAsync(makerToken); await maker.configureERC20TokenAsync(makerFeeToken); await maker.configureERC20TokenAsync(anotherErc20Token); await forwarder.approveMakerAssetProxy(makerAssetData).awaitTransactionSuccessAsync(); [nftId] = await maker.configureERC721TokenAsync(erc721Token); const tokenOwners = { ...actorAddressesByName([maker, taker, orderFeeRecipient, forwarderFeeRecipient]), Forwarder: forwarder.address, StakingProxy: deployment.staking.stakingProxy.address, }; const tokenContracts = { erc20: { makerToken, makerFeeToken, anotherErc20Token, wETH: deployment.tokens.weth }, erc721: { erc721Token }, }; const tokenIds = { erc721: { [erc721Token.address]: [nftId] } }; balanceStore = new BlockchainBalanceStore(tokenOwners, tokenContracts, tokenIds); testFactory = new ForwarderTestFactory( forwarder, deployment, balanceStore, maker, taker, orderFeeRecipient, forwarderFeeRecipient, devUtils, ); }); blockchainTests.resets('constructor', () => { it('should revert if assetProxy is unregistered', async () => { const chainId = await env.getChainIdAsync(); const exchange = await ExchangeContract.deployFrom0xArtifactAsync( exchangeArtifacts.Exchange, env.provider, env.txDefaults, {}, new BigNumber(chainId), ); const deployForwarder = ForwarderContract.deployFrom0xArtifactAsync( artifacts.Forwarder, env.provider, env.txDefaults, {}, exchange.address, wethAssetData, ); await expect(deployForwarder).to.revertWith(new ForwarderRevertErrors.UnregisteredAssetProxyError()); }); }); blockchainTests.resets('marketSellOrdersWithEth without extra fees', () => { it('should fill a single order without a taker fee', async () => { const orderWithoutFee = await maker.signOrderAsync(); await testFactory.marketSellTestAsync([orderWithoutFee], 0.78); }); it('should fill multiple orders without taker fees', async () => { const firstOrder = await maker.signOrderAsync(); const secondOrder = await maker.signOrderAsync({ makerAssetAmount: toBaseUnitAmount(285), takerAssetAmount: toBaseUnitAmount(21), }); const orders = [firstOrder, secondOrder]; await testFactory.marketSellTestAsync(orders, 1.51); }); it('should fill a single order with a percentage fee', async () => { const orderWithPercentageFee = await maker.signOrderAsync({ takerFee: toBaseUnitAmount(1), takerFeeAssetData: makerAssetData, }); await testFactory.marketSellTestAsync([orderWithPercentageFee], 0.58); }); it('should fill multiple orders with percentage fees', async () => { const firstOrder = await maker.signOrderAsync({ takerFee: toBaseUnitAmount(1), takerFeeAssetData: makerAssetData, }); const secondOrder = await maker.signOrderAsync({ makerAssetAmount: toBaseUnitAmount(190), takerAssetAmount: toBaseUnitAmount(31), takerFee: toBaseUnitAmount(2), takerFeeAssetData: makerAssetData, }); const orders = [firstOrder, secondOrder]; await testFactory.marketSellTestAsync(orders, 1.34); }); it('should fail to fill an order with a percentage fee if the asset proxy is not yet approved', async () => { const unapprovedAsset = await devUtils.encodeERC20AssetData(anotherErc20Token.address).callAsync(); const order = await maker.signOrderAsync({ makerAssetData: unapprovedAsset, takerFee: toBaseUnitAmount(2), takerFeeAssetData: unapprovedAsset, }); await balanceStore.updateBalancesAsync(); // Execute test case const tx = await forwarder .marketSellOrdersWithEth( [order], [order.signature], constants.ZERO_AMOUNT, forwarderFeeRecipient.address, ) .awaitTransactionSuccessAsync({ value: order.takerAssetAmount.plus(DeploymentManager.protocolFee), from: taker.address, }); const expectedBalances = LocalBalanceStore.create(devUtils, balanceStore); expectedBalances.burnGas(tx.from, DeploymentManager.gasPrice.times(tx.gasUsed)); // Verify balances await balanceStore.updateBalancesAsync(); balanceStore.assertEquals(expectedBalances); }); it('should fill a single order with a WETH fee', async () => { const orderWithWethFee = await maker.signOrderAsync({ takerFee: toBaseUnitAmount(1), takerFeeAssetData: wethAssetData, }); await testFactory.marketSellTestAsync([orderWithWethFee], 0.13); }); it('should fill multiple orders with WETH fees', async () => { const firstOrder = await maker.signOrderAsync({ takerFee: toBaseUnitAmount(1), takerFeeAssetData: wethAssetData, }); const secondOrderWithWethFee = await maker.signOrderAsync({ makerAssetAmount: toBaseUnitAmount(97), takerAssetAmount: toBaseUnitAmount(33), takerFee: toBaseUnitAmount(2), takerFeeAssetData: wethAssetData, }); const orders = [firstOrder, secondOrderWithWethFee]; await testFactory.marketSellTestAsync(orders, 1.25); }); it('should refund remaining ETH if amount is greater than takerAssetAmount', async () => { const order = await maker.signOrderAsync(); const ethValue = order.takerAssetAmount.plus(DeploymentManager.protocolFee).plus(2); const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker.address); const tx = await forwarder .marketSellOrdersWithEth( [order], [order.signature], constants.ZERO_AMOUNT, forwarderFeeRecipient.address, ) .awaitTransactionSuccessAsync({ value: ethValue, from: taker.address, }); const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker.address); const totalEthSpent = order.takerAssetAmount .plus(DeploymentManager.protocolFee) .plus(DeploymentManager.gasPrice.times(tx.gasUsed)); expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); }); it('should fill orders with different makerAssetData', async () => { const firstOrder = await maker.signOrderAsync(); const secondOrderMakerAssetData = await devUtils .encodeERC20AssetData(anotherErc20Token.address) .callAsync(); const secondOrder = await maker.signOrderAsync({ makerAssetData: secondOrderMakerAssetData, }); await forwarder.approveMakerAssetProxy(secondOrderMakerAssetData).awaitTransactionSuccessAsync(); const orders = [firstOrder, secondOrder]; await testFactory.marketSellTestAsync(orders, 1.5); }); it('should fail to fill an order with a fee denominated in an asset other than makerAsset or WETH', async () => { const takerFeeAssetData = await devUtils.encodeERC20AssetData(anotherErc20Token.address).callAsync(); const order = await maker.signOrderAsync({ takerFeeAssetData, takerFee: toBaseUnitAmount(1), }); const revertError = new ForwarderRevertErrors.UnsupportedFeeError(takerFeeAssetData); await testFactory.marketSellTestAsync([order], 0.5, { revertError, }); }); it('should fill a partially-filled order without a taker fee', async () => { const order = await maker.signOrderAsync(); await testFactory.marketSellTestAsync([order], 0.3); await testFactory.marketSellTestAsync([order], 0.8); }); it('should skip over an order with an invalid maker asset amount', async () => { const unfillableOrder = await maker.signOrderAsync({ makerAssetAmount: constants.ZERO_AMOUNT, }); const fillableOrder = await maker.signOrderAsync(); await testFactory.marketSellTestAsync([unfillableOrder, fillableOrder], 1.5); }); it('should skip over an order with an invalid taker asset amount', async () => { const unfillableOrder = await maker.signOrderAsync({ takerAssetAmount: constants.ZERO_AMOUNT, }); const fillableOrder = await maker.signOrderAsync(); await testFactory.marketSellTestAsync([unfillableOrder, fillableOrder], 1.5); }); it('should skip over an expired order', async () => { const currentTimestamp = await getLatestBlockTimestampAsync(); const expiredOrder = await maker.signOrderAsync({ expirationTimeSeconds: new BigNumber(currentTimestamp).minus(10), }); const fillableOrder = await maker.signOrderAsync(); await testFactory.marketSellTestAsync([expiredOrder, fillableOrder], 1.5); }); it('should skip over a fully filled order', async () => { const fullyFilledOrder = await maker.signOrderAsync(); await testFactory.marketSellTestAsync([fullyFilledOrder], 1); const fillableOrder = await maker.signOrderAsync(); await testFactory.marketSellTestAsync([fullyFilledOrder, fillableOrder], 1.5); }); it('should skip over a cancelled order', async () => { const cancelledOrder = await maker.signOrderAsync(); await maker.cancelOrderAsync(cancelledOrder); const fillableOrder = await maker.signOrderAsync(); await testFactory.marketSellTestAsync([cancelledOrder, fillableOrder], 1.5); }); }); blockchainTests.resets('marketSellOrdersWithEth with extra fees', () => { it('should fill the order and send fee to feeRecipient', async () => { const order = await maker.signOrderAsync({ makerAssetAmount: toBaseUnitAmount(157), takerAssetAmount: toBaseUnitAmount(36), }); await testFactory.marketSellTestAsync([order], 0.67, { forwarderFeePercentage: new BigNumber(2), }); }); it('should fail if the fee is set too high', async () => { const order = await maker.signOrderAsync(); const forwarderFeePercentage = new BigNumber(6); const revertError = new ForwarderRevertErrors.FeePercentageTooLargeError( getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, forwarderFeePercentage), ); await testFactory.marketSellTestAsync([order], 0.5, { forwarderFeePercentage, revertError, }); }); }); blockchainTests.resets('marketBuyOrdersWithEth without extra fees', () => { it('should buy the exact amount of makerAsset in a single order', async () => { const order = await maker.signOrderAsync({ makerAssetAmount: toBaseUnitAmount(131), takerAssetAmount: toBaseUnitAmount(20), }); await testFactory.marketBuyTestAsync([order], 0.62); }); it('should buy the exact amount of makerAsset in multiple orders', async () => { const firstOrder = await maker.signOrderAsync(); const secondOrder = await maker.signOrderAsync({ makerAssetAmount: toBaseUnitAmount(77), takerAssetAmount: toBaseUnitAmount(11), }); const orders = [firstOrder, secondOrder]; await testFactory.marketBuyTestAsync(orders, 1.96); }); it('should buy exactly makerAssetBuyAmount in orders with different makerAssetData', async () => { const firstOrder = await maker.signOrderAsync(); const secondOrderMakerAssetData = await devUtils .encodeERC20AssetData(anotherErc20Token.address) .callAsync(); const secondOrder = await maker.signOrderAsync({ makerAssetData: secondOrderMakerAssetData, }); await forwarder.approveMakerAssetProxy(secondOrderMakerAssetData).awaitTransactionSuccessAsync(); const orders = [firstOrder, secondOrder]; await testFactory.marketBuyTestAsync(orders, 1.5); }); it('should buy the exact amount of makerAsset and return excess ETH', async () => { const order = await maker.signOrderAsync({ makerAssetAmount: toBaseUnitAmount(80), takerAssetAmount: toBaseUnitAmount(17), }); await testFactory.marketBuyTestAsync([order], 0.57, { ethValueAdjustment: 2, }); }); it('should buy the exact amount of makerAsset from a single order with a WETH fee', async () => { const order = await maker.signOrderAsync({ makerAssetAmount: toBaseUnitAmount(79), takerAssetAmount: toBaseUnitAmount(16), takerFee: toBaseUnitAmount(1), takerFeeAssetData: wethAssetData, }); await testFactory.marketBuyTestAsync([order], 0.38); }); it('should buy the exact amount of makerAsset from a single order with a percentage fee', async () => { const order = await maker.signOrderAsync({ makerAssetAmount: toBaseUnitAmount(80), takerAssetAmount: toBaseUnitAmount(17), takerFee: toBaseUnitAmount(1), takerFeeAssetData: makerAssetData, }); await testFactory.marketBuyTestAsync([order], 0.52); }); it('should revert if the amount of ETH sent is too low to fill the makerAssetAmount', async () => { const order = await maker.signOrderAsync(); const revertError = new ForwarderRevertErrors.CompleteBuyFailedError( order.makerAssetAmount.times(0.5), constants.ZERO_AMOUNT, ); await testFactory.marketBuyTestAsync([order], 0.5, { ethValueAdjustment: -2, revertError, }); }); it('should buy an ERC721 asset from a single order', async () => { const erc721Order = await maker.signOrderAsync({ makerAssetAmount: new BigNumber(1), makerAssetData: await devUtils.encodeERC721AssetData(erc721Token.address, nftId).callAsync(), takerFeeAssetData: wethAssetData, }); await testFactory.marketBuyTestAsync([erc721Order], 1); }); it('should buy an ERC721 asset and pay a WETH fee', async () => { const erc721orderWithWethFee = await maker.signOrderAsync({ makerAssetAmount: new BigNumber(1), makerAssetData: await devUtils.encodeERC721AssetData(erc721Token.address, nftId).callAsync(), takerFee: toBaseUnitAmount(1), takerFeeAssetData: wethAssetData, }); await testFactory.marketBuyTestAsync([erc721orderWithWethFee], 1); }); it('should fail to fill an order with a fee denominated in an asset other than makerAsset or WETH', async () => { const takerFeeAssetData = await devUtils.encodeERC20AssetData(anotherErc20Token.address).callAsync(); const order = await maker.signOrderAsync({ takerFeeAssetData, takerFee: toBaseUnitAmount(1), }); const revertError = new ForwarderRevertErrors.UnsupportedFeeError(takerFeeAssetData); await testFactory.marketBuyTestAsync([order], 0.5, { revertError, }); }); it('should fill a partially-filled order without a taker fee', async () => { const order = await maker.signOrderAsync(); await testFactory.marketBuyTestAsync([order], 0.3); await testFactory.marketBuyTestAsync([order], 0.8); }); it('should skip over an order with an invalid maker asset amount', async () => { const unfillableOrder = await maker.signOrderAsync({ makerAssetAmount: constants.ZERO_AMOUNT, }); const fillableOrder = await maker.signOrderAsync(); await testFactory.marketBuyTestAsync([unfillableOrder, fillableOrder], 1.5); }); it('should skip over an order with an invalid taker asset amount', async () => { const unfillableOrder = await maker.signOrderAsync({ takerAssetAmount: constants.ZERO_AMOUNT, }); const fillableOrder = await maker.signOrderAsync(); await testFactory.marketBuyTestAsync([unfillableOrder, fillableOrder], 1.5); }); it('should skip over an expired order', async () => { const currentTimestamp = await getLatestBlockTimestampAsync(); const expiredOrder = await maker.signOrderAsync({ expirationTimeSeconds: new BigNumber(currentTimestamp).minus(10), }); const fillableOrder = await maker.signOrderAsync(); await testFactory.marketBuyTestAsync([expiredOrder, fillableOrder], 1.5); }); it('should skip over a fully filled order', async () => { const fullyFilledOrder = await maker.signOrderAsync(); await testFactory.marketBuyTestAsync([fullyFilledOrder], 1); const fillableOrder = await maker.signOrderAsync(); await testFactory.marketBuyTestAsync([fullyFilledOrder, fillableOrder], 1.5); }); it('should skip over a cancelled order', async () => { const cancelledOrder = await maker.signOrderAsync(); await maker.cancelOrderAsync(cancelledOrder); const fillableOrder = await maker.signOrderAsync(); await testFactory.marketBuyTestAsync([cancelledOrder, fillableOrder], 1.5); }); it('Should buy slightly greater makerAsset when exchange rate is rounded', async () => { // The 0x Protocol contracts round the exchange rate in favor of the Maker. // In this case, the taker must round up how much they're going to spend, which // in turn increases the amount of MakerAsset being purchased. // Example: // The taker wants to buy 5 units of the MakerAsset at a rate of 3M/2T. // For every 2 units of TakerAsset, the taker will receive 3 units of MakerAsset. // To purchase 5 units, the taker must spend 10/3 = 3.33 units of TakerAssset. // However, the Taker can only spend whole units. // Spending floor(10/3) = 3 units will yield a profit of Floor(3*3/2) = Floor(4.5) = 4 units of MakerAsset. // Spending ceil(10/3) = 4 units will yield a profit of Floor(4*3/2) = 6 units of MakerAsset. // // The forwarding contract will opt for the second option, which overbuys, to ensure the taker // receives at least the amount of MakerAsset they requested. // // Construct test case using values from example above const order = await maker.signOrderAsync({ makerAssetAmount: new BigNumber('30'), takerAssetAmount: new BigNumber('20'), makerFee: new BigNumber(0), takerFee: new BigNumber(0), }); const desiredMakerAssetFillAmount = new BigNumber('5'); const makerAssetFillAmount = new BigNumber('6'); const primaryTakerAssetFillAmount = new BigNumber('4'); const ethValue = primaryTakerAssetFillAmount.plus(DeploymentManager.protocolFee); await balanceStore.updateBalancesAsync(); // Execute test case const tx = await forwarder .marketBuyOrdersWithEth( [order], desiredMakerAssetFillAmount, [order.signature], constants.ZERO_AMOUNT, forwarderFeeRecipient.address, ) .awaitTransactionSuccessAsync({ value: ethValue, from: taker.address, }); // Compute expected balances const expectedBalances = LocalBalanceStore.create(devUtils, balanceStore); await expectedBalances.transferAssetAsync( maker.address, taker.address, makerAssetFillAmount, makerAssetData, ); expectedBalances.wrapEth(taker.address, deployment.tokens.weth.address, ethValue); await expectedBalances.transferAssetAsync( taker.address, maker.address, primaryTakerAssetFillAmount, wethAssetData, ); await expectedBalances.transferAssetAsync( taker.address, deployment.staking.stakingProxy.address, DeploymentManager.protocolFee, wethAssetData, ); expectedBalances.burnGas(tx.from, DeploymentManager.gasPrice.times(tx.gasUsed)); // Verify balances await balanceStore.updateBalancesAsync(); balanceStore.assertEquals(expectedBalances); }); it('Should buy slightly greater MakerAsset when exchange rate is rounded (Regression Test)', async () => { // Order taken from a transaction on mainnet that failed due to a rounding error. const order = await maker.signOrderAsync({ makerAssetAmount: new BigNumber('268166666666666666666'), takerAssetAmount: new BigNumber('219090625878836371'), makerFee: new BigNumber(0), takerFee: new BigNumber(0), }); // The taker will receive more than the desired amount of makerAsset due to rounding const desiredMakerAssetFillAmount = new BigNumber('5000000000000000000'); const takerAssetFillAmount = new BigNumber('4084971271824171'); const makerAssetFillAmount = takerAssetFillAmount .times(order.makerAssetAmount) .dividedToIntegerBy(order.takerAssetAmount); await balanceStore.updateBalancesAsync(); // Execute test case const tx = await forwarder .marketBuyOrdersWithEth( [order], desiredMakerAssetFillAmount, [order.signature], constants.ZERO_AMOUNT, forwarderFeeRecipient.address, ) .awaitTransactionSuccessAsync({ value: takerAssetFillAmount.plus(DeploymentManager.protocolFee), from: taker.address, }); // Compute expected balances const expectedBalances = LocalBalanceStore.create(devUtils, balanceStore); await expectedBalances.transferAssetAsync( maker.address, taker.address, makerAssetFillAmount, makerAssetData, ); expectedBalances.wrapEth( taker.address, deployment.tokens.weth.address, takerAssetFillAmount.plus(DeploymentManager.protocolFee), ); await expectedBalances.transferAssetAsync( taker.address, maker.address, takerAssetFillAmount, wethAssetData, ); await expectedBalances.transferAssetAsync( taker.address, deployment.staking.stakingProxy.address, DeploymentManager.protocolFee, wethAssetData, ); expectedBalances.burnGas(tx.from, DeploymentManager.gasPrice.times(tx.gasUsed)); // Verify balances await balanceStore.updateBalancesAsync(); balanceStore.assertEquals(expectedBalances); }); }); blockchainTests.resets('marketBuyOrdersWithEth with extra fees', () => { it('should buy the asset and send fee to feeRecipient', async () => { const order = await maker.signOrderAsync({ makerAssetAmount: toBaseUnitAmount(125), takerAssetAmount: toBaseUnitAmount(11), }); await testFactory.marketBuyTestAsync([order], 0.33, { forwarderFeePercentage: new BigNumber(2), }); }); it('should fail if the fee is set too high', async () => { const order = await maker.signOrderAsync(); const revertError = new ForwarderRevertErrors.FeePercentageTooLargeError( getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, new BigNumber(6)), ); await testFactory.marketBuyTestAsync([order], 0.5, { forwarderFeePercentage: new BigNumber(6), revertError, }); }); it('should fail if there is not enough ETH remaining to pay the fee', async () => { const order = await maker.signOrderAsync(); const forwarderFeePercentage = new BigNumber(2); const ethFee = getPercentageOfValue( order.takerAssetAmount.times(0.5).plus(DeploymentManager.protocolFee), forwarderFeePercentage, ); const revertError = new ForwarderRevertErrors.InsufficientEthForFeeError(ethFee, ethFee.minus(1)); await testFactory.marketBuyTestAsync([order], 0.5, { ethValueAdjustment: -1, forwarderFeePercentage, revertError, }); }); }); }); // tslint:disable:max-file-line-count