diff options
Diffstat (limited to 'packages/contracts/test/forwarder')
-rw-r--r-- | packages/contracts/test/forwarder/forwarder.ts | 1265 |
1 files changed, 725 insertions, 540 deletions
diff --git a/packages/contracts/test/forwarder/forwarder.ts b/packages/contracts/test/forwarder/forwarder.ts index 0256d7d81..19639d3aa 100644 --- a/packages/contracts/test/forwarder/forwarder.ts +++ b/packages/contracts/test/forwarder/forwarder.ts @@ -18,7 +18,6 @@ import { constants } from '../utils/constants'; import { ERC20Wrapper } from '../utils/erc20_wrapper'; import { ERC721Wrapper } from '../utils/erc721_wrapper'; import { ExchangeWrapper } from '../utils/exchange_wrapper'; -import { formatters } from '../utils/formatters'; import { ForwarderWrapper } from '../utils/forwarder_wrapper'; import { OrderFactory } from '../utils/order_factory'; import { ContractName, ERC20BalancesByOwner } from '../utils/types'; @@ -28,8 +27,7 @@ chaiSetup.configure(); const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); const DECIMALS_DEFAULT = 18; -// Set a gasPrice so when checking balance of msg.sender we can accurately calculate gasPrice*gasUsed -const DEFAULT_GAS_PRICE = new BigNumber(1); +const MAX_WETH_FILL_PERCENTAGE = 95; describe(ContractName.Forwarder, () => { let makerAddress: string; @@ -47,25 +45,28 @@ describe(ContractName.Forwarder, () => { let forwarderWrapper: ForwarderWrapper; let exchangeWrapper: ExchangeWrapper; - let signedOrder: SignedOrder; - let signedOrders: SignedOrder[]; + let orderWithoutFee: SignedOrder; let orderWithFee: SignedOrder; - let signedOrdersWithFee: SignedOrder[]; let feeOrder: SignedOrder; - let feeOrders: SignedOrder[]; let orderFactory: OrderFactory; let erc20Wrapper: ERC20Wrapper; let erc20Balances: ERC20BalancesByOwner; let tx: TransactionReceiptWithDecodedLogs; let erc721MakerAssetIds: BigNumber[]; - let feeProportion: number = 0; + let takerEthBalanceBefore: BigNumber; + let feePercentage: BigNumber; + let gasPrice: BigNumber; before(async () => { await blockchainLifecycle.startAsync(); const accounts = await web3Wrapper.getAvailableAddressesAsync(); const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress, otherAddress] = accounts); + const txHash = await web3Wrapper.sendTransactionAsync({ from: accounts[0], to: accounts[0], value: 0 }); + const transaction = await web3Wrapper.getTransactionByHashAsync(txHash); + gasPrice = new BigNumber(transaction.gasPrice); + const erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); @@ -135,681 +136,865 @@ describe(ContractName.Forwarder, () => { wethAssetData, ); forwarderContract = new ForwarderContract(forwarderInstance.abi, forwarderInstance.address, provider); - forwarderWrapper = new ForwarderWrapper(forwarderContract, provider, zrxToken.address); + forwarderWrapper = new ForwarderWrapper(forwarderContract, provider); + const zrxDepositAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.transfer.sendTransactionAsync(forwarderContract.address, zrxDepositAmount), + ); erc20Wrapper.addTokenOwnerAddress(forwarderInstance.address); - - web3Wrapper.abiDecoder.addABI(forwarderContract.abi); - web3Wrapper.abiDecoder.addABI(exchangeInstance.abi); }); after(async () => { await blockchainLifecycle.revertAsync(); }); beforeEach(async () => { await blockchainLifecycle.startAsync(); - feeProportion = 0; erc20Balances = await erc20Wrapper.getBalancesAsync(); - signedOrder = await orderFactory.newSignedOrderAsync(); - signedOrders = [signedOrder]; + takerEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + orderWithoutFee = await orderFactory.newSignedOrderAsync(); feeOrder = await orderFactory.newSignedOrderAsync({ makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), }); - feeOrders = [feeOrder]; orderWithFee = await orderFactory.newSignedOrderAsync({ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), }); - signedOrdersWithFee = [orderWithFee]; }); afterEach(async () => { await blockchainLifecycle.revertAsync(); }); - describe('calculations', () => { - it('throws if partially filled orders passed in are not enough to satisfy requested amount', async () => { - feeOrders = [feeOrder]; - const makerTokenFillAmount = feeOrder.makerAssetAmount.div(2); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - feeOrders, - [], - feeProportion, - makerTokenFillAmount, - ); - // Fill the feeOrder - tx = await forwarderWrapper.marketBuyTokensWithEthAsync(feeOrders, [], makerTokenFillAmount, { + + describe('marketSellOrdersWithEth without extra fees', () => { + it('should fill a single order', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { + value: ethValue, from: takerAddress, - value: fillAmountWei, }); - return expect( - forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - feeOrders, - [], - feeProportion, - makerTokenFillAmount, - ), - ).to.be.rejectedWith('Unable to satisfy makerAssetFillAmount with provided orders'); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const makerAssetFillAmount = primaryTakerAssetFillAmount + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); - it('throws if orders passed are cancelled', async () => { - tx = await exchangeWrapper.cancelOrderAsync(feeOrder, makerAddress); - // Cancel the feeOrder - return expect( - forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - feeOrders, - [], - feeProportion, - feeOrder.makerAssetAmount.div(2), - ), - ).to.be.rejectedWith('Unable to satisfy makerAssetFillAmount with provided orders'); + it('should fill multiple orders', async () => { + const secondOrderWithoutFee = await orderFactory.newSignedOrderAsync(); + const ordersWithoutFee = [orderWithoutFee, secondOrderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const ethValue = ordersWithoutFee[0].takerAssetAmount.plus( + ordersWithoutFee[1].takerAssetAmount.dividedToIntegerBy(2), + ); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const firstTakerAssetFillAmount = ordersWithoutFee[0].takerAssetAmount; + const secondTakerAssetFillAmount = primaryTakerAssetFillAmount.minus(firstTakerAssetFillAmount); + + const makerAssetFillAmount = ordersWithoutFee[0].makerAssetAmount.plus( + ordersWithoutFee[1].makerAssetAmount + .times(secondTakerAssetFillAmount) + .dividedToIntegerBy(ordersWithoutFee[1].takerAssetAmount), + ); + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); - }); - describe('marketSellEthForERC20 without extra fees', () => { - it('should fill the order', async () => { - const fillAmount = signedOrder.takerAssetAmount.div(2); - const makerBalanceBefore = erc20Balances[makerAddress][defaultMakerAssetAddress]; - const takerBalanceBefore = erc20Balances[takerAddress][defaultMakerAssetAddress]; - feeOrders = []; - tx = await forwarderWrapper.marketSellEthForERC20Async(signedOrders, feeOrders, { - value: fillAmount, + it('should fill the order and pay ZRX fees from a single feeOrder', async () => { + const ordersWithFee = [orderWithFee]; + const feeOrders = [feeOrder]; + const ethValue = orderWithFee.takerAssetAmount.dividedToIntegerBy(2); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { + value: ethValue, from: takerAddress, }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); const newBalances = await erc20Wrapper.getBalancesAsync(); - const makerBalanceAfter = newBalances[makerAddress][defaultMakerAssetAddress]; - const takerBalanceAfter = newBalances[takerAddress][defaultMakerAssetAddress]; - const makerTokenFillAmount = fillAmount - .times(signedOrder.makerAssetAmount) - .dividedToIntegerBy(signedOrder.takerAssetAmount); - - expect(makerBalanceAfter).to.be.bignumber.equal(makerBalanceBefore.minus(makerTokenFillAmount)); - expect(takerBalanceAfter).to.be.bignumber.equal(takerBalanceBefore.plus(makerTokenFillAmount)); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(new BigNumber(0)); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const makerAssetFillAmount = primaryTakerAssetFillAmount + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + const feeAmount = ForwarderWrapper.getPercentageOfValue( + orderWithFee.takerFee.dividedToIntegerBy(2), + MAX_WETH_FILL_PERCENTAGE, + ); + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const totalEthSpent = primaryTakerAssetFillAmount + .plus(wethSpentOnFeeOrders) + .plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); - it('should fill the order and perform fee abstraction', async () => { - const fillAmount = signedOrder.takerAssetAmount.div(4); - const takerBalanceBefore = erc20Balances[takerAddress][defaultMakerAssetAddress]; - tx = await forwarderWrapper.marketSellEthForERC20Async(signedOrdersWithFee, feeOrders, { - value: fillAmount, + it('should fill the orders and pay ZRX from multiple feeOrders', async () => { + const ordersWithFee = [orderWithFee]; + const ethValue = orderWithFee.takerAssetAmount; + const makerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const makerAssetAmount = orderWithFee.takerFee.dividedToIntegerBy(2); + const takerAssetAmount = feeOrder.takerAssetAmount + .times(makerAssetAmount) + .dividedToIntegerBy(feeOrder.makerAssetAmount); + + const firstFeeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + makerAssetAmount, + takerAssetAmount, + }); + const secondFeeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + makerAssetAmount, + takerAssetAmount, + }); + const feeOrders = [firstFeeOrder, secondFeeOrder]; + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { + value: ethValue, from: takerAddress, }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); const newBalances = await erc20Wrapper.getBalancesAsync(); - const takerBalanceAfter = newBalances[takerAddress][defaultMakerAssetAddress]; - const acceptPercentage = 98; - const acceptableThreshold = takerBalanceBefore.plus(fillAmount.times(acceptPercentage).dividedBy(100)); - const isWithinThreshold = takerBalanceAfter.greaterThanOrEqualTo(acceptableThreshold); - expect(isWithinThreshold).to.be.true(); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(new BigNumber(0)); + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const makerAssetFillAmount = primaryTakerAssetFillAmount + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + const feeAmount = ForwarderWrapper.getPercentageOfValue(orderWithFee.takerFee, MAX_WETH_FILL_PERCENTAGE); + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const totalEthSpent = primaryTakerAssetFillAmount + .plus(wethSpentOnFeeOrders) + .plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); it('should fill the order when token is ZRX with fees', async () => { orderWithFee = await orderFactory.newSignedOrderAsync({ makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), }); - signedOrdersWithFee = [orderWithFee]; - feeOrders = []; - const fillAmount = signedOrder.takerAssetAmount.div(4); - const takerBalanceBefore = erc20Balances[takerAddress][zrxToken.address]; - tx = await forwarderWrapper.marketSellEthForERC20Async(signedOrdersWithFee, feeOrders, { - value: fillAmount, + const ordersWithFee = [orderWithFee]; + const feeOrders: SignedOrder[] = []; + const ethValue = orderWithFee.takerAssetAmount.dividedToIntegerBy(2); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { + value: ethValue, from: takerAddress, }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); const newBalances = await erc20Wrapper.getBalancesAsync(); - const takerBalanceAfter = newBalances[takerAddress][zrxToken.address]; - const acceptPercentage = 98; - const acceptableThreshold = takerBalanceBefore.plus(fillAmount.times(acceptPercentage).dividedBy(100)); - const isWithinThreshold = takerBalanceAfter.greaterThanOrEqualTo(acceptableThreshold); - expect(isWithinThreshold).to.be.true(); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(new BigNumber(0)); + const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2); + const totalEthSpent = ethValue.plus(gasPrice.times(tx.gasUsed)); + const takerFeePaid = orderWithFee.takerFee.dividedToIntegerBy(2); + const makerFeePaid = orderWithFee.makerFee.dividedToIntegerBy(2); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFillAmount).minus(makerFeePaid), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].plus(makerAssetFillAmount).minus(takerFeePaid), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(ethValue), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[forwarderContract.address][zrxToken.address], + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); - it('should fail if sent an ETH amount too high', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + it('should refund remaining ETH if amount is greater than takerAssetAmount', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const ethValue = orderWithoutFee.takerAssetAmount.times(2); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { + value: ethValue, + from: takerAddress, }); - const fillAmount = signedOrder.takerAssetAmount.times(2); - return expectTransactionFailedAsync( - forwarderWrapper.marketSellEthForERC20Async(signedOrdersWithFee, feeOrders, { - value: fillAmount, - from: takerAddress, - }), - RevertReason.UnacceptableThreshold, - ); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const totalEthSpent = orderWithoutFee.takerAssetAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); }); - it('should fail if fee abstraction amount is too high', async () => { + it('should revert if ZRX cannot be fully repurchased', async () => { orderWithFee = await orderFactory.newSignedOrderAsync({ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), DECIMALS_DEFAULT), }); - signedOrdersWithFee = [orderWithFee]; + const ordersWithFee = [orderWithFee]; feeOrder = await orderFactory.newSignedOrderAsync({ makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), }); - feeOrders = [feeOrder]; - const fillAmount = signedOrder.takerAssetAmount.div(4); + const feeOrders = [feeOrder]; + const ethValue = orderWithFee.takerAssetAmount; return expectTransactionFailedAsync( - forwarderWrapper.marketSellEthForERC20Async(signedOrdersWithFee, feeOrders, { - value: fillAmount, + forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { + value: ethValue, from: takerAddress, }), - RevertReason.TransferFailed, + RevertReason.CompleteFillFailed, ); }); - it('throws when mixed ERC721 and ERC20 assets with ERC20 first', async () => { + it('should not fill orders with different makerAssetData than the first order', async () => { const makerAssetId = erc721MakerAssetIds[0]; const erc721SignedOrder = await orderFactory.newSignedOrderAsync({ makerAssetAmount: new BigNumber(1), makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), }); const erc20SignedOrder = await orderFactory.newSignedOrderAsync(); - signedOrders = [erc20SignedOrder, erc721SignedOrder]; - const fillAmountWei = erc20SignedOrder.takerAssetAmount.plus(erc721SignedOrder.takerAssetAmount); - return expectTransactionFailedAsync( - forwarderWrapper.marketSellEthForERC20Async(signedOrders, feeOrders, { - from: takerAddress, - value: fillAmountWei, - }), - RevertReason.InvalidOrderSignature, - ); + const ordersWithoutFee = [erc20SignedOrder, erc721SignedOrder]; + const feeOrders: SignedOrder[] = []; + const ethValue = erc20SignedOrder.takerAssetAmount.plus(erc721SignedOrder.takerAssetAmount); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const totalEthSpent = erc20SignedOrder.takerAssetAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); }); }); - describe('marketSellEthForERC20 with extra fees', () => { - it('should fill the order and send fee to fee recipient', async () => { - const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); - const fillAmount = signedOrder.takerAssetAmount.div(2); - feeProportion = 150; // 1.5% - feeOrders = []; - tx = await forwarderWrapper.marketSellEthForERC20Async( - signedOrders, + describe('marketSellOrdersWithEth with extra fees', () => { + it('should fill the order and send fee to feeRecipient', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const ethValue = orderWithoutFee.takerAssetAmount.div(2); + + const baseFeePercentage = 2; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + tx = await forwarderWrapper.marketSellOrdersWithEthAsync( + ordersWithoutFee, feeOrders, { + value: ethValue, from: takerAddress, - value: fillAmount, - gasPrice: DEFAULT_GAS_PRICE, - }, - { - feeProportion, - feeRecipient: feeRecipientAddress, }, + { feePercentage, feeRecipient: feeRecipientAddress }, ); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); const newBalances = await erc20Wrapper.getBalancesAsync(); - const makerBalanceBefore = erc20Balances[makerAddress][defaultMakerAssetAddress]; - const makerBalanceAfter = newBalances[makerAddress][defaultMakerAssetAddress]; - const takerBalanceAfter = newBalances[takerAddress][defaultMakerAssetAddress]; - const afterEthBalance = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); - const takerBoughtAmount = takerBalanceAfter.minus(erc20Balances[takerAddress][defaultMakerAssetAddress]); - expect(makerBalanceAfter).to.be.bignumber.equal(makerBalanceBefore.minus(takerBoughtAmount)); - expect(afterEthBalance).to.be.bignumber.equal( - initEthBalance.plus(fillAmount.times(feeProportion).dividedBy(10000)), + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const makerAssetFillAmount = primaryTakerAssetFillAmount + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + const ethSpentOnFee = ForwarderWrapper.getPercentageOfValue(primaryTakerAssetFillAmount, baseFeePercentage); + const totalEthSpent = primaryTakerAssetFillAmount.plus(ethSpentOnFee).plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), ); - expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(new BigNumber(0)); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(feeRecipientEthBalanceAfter).to.be.bignumber.equal(feeRecipientEthBalanceBefore.plus(ethSpentOnFee)); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); it('should fail if the fee is set too high', async () => { - const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); - const fillAmount = signedOrder.takerAssetAmount.div(2); - feeProportion = 1500; // 15.0% - feeOrders = []; + const ethValue = orderWithoutFee.takerAssetAmount.div(2); + const baseFeePercentage = 6; + feePercentage = ForwarderWrapper.getPercentageOfValue(ethValue, baseFeePercentage); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; await expectTransactionFailedAsync( - forwarderWrapper.marketSellEthForERC20Async( - signedOrders, + forwarderWrapper.marketSellOrdersWithEthAsync( + ordersWithoutFee, feeOrders, - { from: takerAddress, value: fillAmount, gasPrice: DEFAULT_GAS_PRICE }, - { feeProportion, feeRecipient: feeRecipientAddress }, + { from: takerAddress, value: ethValue, gasPrice }, + { feePercentage, feeRecipient: feeRecipientAddress }, ), - RevertReason.FeeProportionTooLarge, + RevertReason.FeePercentageTooLarge, + ); + }); + it('should fail if there is not enough ETH remaining to pay the fee', async () => { + const ethValue = orderWithoutFee.takerAssetAmount.div(2); + const baseFeePercentage = 5; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + const ordersWithFee = [orderWithFee]; + const feeOrders = [feeOrder]; + await expectTransactionFailedAsync( + forwarderWrapper.marketSellOrdersWithEthAsync( + ordersWithFee, + feeOrders, + { from: takerAddress, value: ethValue, gasPrice }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ), + RevertReason.InsufficientEthRemaining, ); - const afterEthBalance = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); - expect(afterEthBalance).to.be.bignumber.equal(initEthBalance); }); }); - describe('marketBuyTokensWithEth', () => { - it('should buy the exact amount of assets', async () => { - const makerAssetAmount = signedOrder.makerAssetAmount.div(2); - const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const balancesBefore = await erc20Wrapper.getBalancesAsync(); - const rate = signedOrder.makerAssetAmount.dividedBy(signedOrder.takerAssetAmount); - const fillAmountWei = makerAssetAmount.dividedToIntegerBy(rate); - feeOrders = []; - tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { + describe('marketBuyOrdersWithEth without extra fees', () => { + it('should buy the exact amount of makerAsset in a single order', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, from: takerAddress, - value: fillAmountWei, - gasPrice: DEFAULT_GAS_PRICE, }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); const newBalances = await erc20Wrapper.getBalancesAsync(); - const takerBalanceBefore = balancesBefore[takerAddress][defaultMakerAssetAddress]; - const takerBalanceAfter = newBalances[takerAddress][defaultMakerAssetAddress]; - const afterEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const expectedEthBalanceAfterGasCosts = initEthBalance.minus(fillAmountWei).minus(tx.gasUsed); - expect(takerBalanceAfter).to.be.bignumber.eq(takerBalanceBefore.plus(makerAssetAmount)); - expect(afterEthBalance).to.be.bignumber.eq(expectedEthBalanceAfterGasCosts); + + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); - it('should buy the exact amount of assets and return excess ETH', async () => { - const makerAssetAmount = signedOrder.makerAssetAmount.div(2); - const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const balancesBefore = await erc20Wrapper.getBalancesAsync(); - const rate = signedOrder.makerAssetAmount.dividedBy(signedOrder.takerAssetAmount); - const fillAmount = makerAssetAmount.dividedToIntegerBy(rate); - const excessFillAmount = fillAmount.times(2); - feeOrders = []; - tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { + it('should buy the exact amount of makerAsset in multiple orders', async () => { + const secondOrderWithoutFee = await orderFactory.newSignedOrderAsync(); + const ordersWithoutFee = [orderWithoutFee, secondOrderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = ordersWithoutFee[0].makerAssetAmount.plus( + ordersWithoutFee[1].makerAssetAmount.dividedToIntegerBy(2), + ); + const ethValue = ordersWithoutFee[0].takerAssetAmount.plus( + ordersWithoutFee[1].takerAssetAmount.dividedToIntegerBy(2), + ); + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, from: takerAddress, - value: excessFillAmount, - gasPrice: DEFAULT_GAS_PRICE, }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); const newBalances = await erc20Wrapper.getBalancesAsync(); - const takerBalanceBefore = balancesBefore[takerAddress][defaultMakerAssetAddress]; - const takerBalanceAfter = newBalances[takerAddress][defaultMakerAssetAddress]; - const afterEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const expectedEthBalanceAfterGasCosts = initEthBalance.minus(fillAmount).minus(tx.gasUsed); - expect(takerBalanceAfter).to.be.bignumber.eq(takerBalanceBefore.plus(makerAssetAmount)); - expect(afterEthBalance).to.be.bignumber.eq(expectedEthBalanceAfterGasCosts); + + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); - it('should buy the exact amount of assets with fee abstraction', async () => { - const makerAssetAmount = signedOrder.makerAssetAmount.div(2); - const balancesBefore = await erc20Wrapper.getBalancesAsync(); - const rate = signedOrder.makerAssetAmount.dividedBy(signedOrder.takerAssetAmount); - const fillAmount = makerAssetAmount.dividedToIntegerBy(rate); - const excessFillAmount = fillAmount.times(2); - tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrdersWithFee, feeOrders, makerAssetAmount, { + it('should buy the exact amount of makerAsset and return excess ETH', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount; + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, from: takerAddress, - value: excessFillAmount, }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); const newBalances = await erc20Wrapper.getBalancesAsync(); - const takerBalanceBefore = balancesBefore[takerAddress][defaultMakerAssetAddress]; - const takerBalanceAfter = newBalances[takerAddress][defaultMakerAssetAddress]; - expect(takerBalanceAfter).to.be.bignumber.eq(takerBalanceBefore.plus(makerAssetAmount)); - }); - it('should buy the exact amount of assets when buying zrx with fee abstraction', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), - }); - signedOrdersWithFee = [signedOrder]; - feeOrders = []; - const makerAssetAmount = signedOrder.makerAssetAmount.div(2); - const takerWeiBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const balancesBefore = await erc20Wrapper.getBalancesAsync(); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrdersWithFee, - feeOrders, - feeProportion, - makerAssetAmount, + + const primaryTakerAssetFillAmount = ethValue.dividedToIntegerBy(2); + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), ); - tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrdersWithFee, feeOrders, makerAssetAmount, { + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should buy the exact amount of makerAsset and pay ZRX from feeOrders', async () => { + const ordersWithFee = [orderWithFee]; + const feeOrders = [feeOrder]; + const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithFee.takerAssetAmount; + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { + value: ethValue, from: takerAddress, - value: fillAmountWei, - gasPrice: DEFAULT_GAS_PRICE, }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); const newBalances = await erc20Wrapper.getBalancesAsync(); - const takerTokenBalanceBefore = balancesBefore[takerAddress][zrxToken.address]; - const takerTokenBalanceAfter = newBalances[takerAddress][zrxToken.address]; - const takerWeiBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const expectedCostAfterGas = fillAmountWei.plus(tx.gasUsed); - expect(takerTokenBalanceAfter).to.be.bignumber.greaterThan(takerTokenBalanceBefore.plus(makerAssetAmount)); - expect(takerWeiBalanceAfter).to.be.bignumber.equal(takerWeiBalanceBefore.minus(expectedCostAfterGas)); - }); - it('throws if fees are higher than 5% when buying zrx', async () => { - const highFeeZRXOrder = await orderFactory.newSignedOrderAsync({ - makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - makerAssetAmount: signedOrder.makerAssetAmount, - takerFee: signedOrder.makerAssetAmount.times(0.06), - }); - signedOrdersWithFee = [highFeeZRXOrder]; - feeOrders = []; - const makerAssetAmount = signedOrder.makerAssetAmount.div(2); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrdersWithFee, - feeOrders, - feeProportion, - makerAssetAmount, + + const primaryTakerAssetFillAmount = orderWithFee.takerAssetAmount.dividedToIntegerBy(2); + const feeAmount = orderWithFee.takerFee.dividedToIntegerBy(2); + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const totalEthSpent = primaryTakerAssetFillAmount + .plus(wethSpentOnFeeOrders) + .plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), ); - return expectTransactionFailedAsync( - forwarderWrapper.marketBuyTokensWithEthAsync(signedOrdersWithFee, feeOrders, makerAssetAmount, { - from: takerAddress, - value: fillAmountWei, - }), - RevertReason.UnacceptableThreshold, + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); - it('throws if fees are higher than 5% when buying erc20', async () => { - const highFeeERC20Order = await orderFactory.newSignedOrderAsync({ - takerFee: signedOrder.makerAssetAmount.times(0.06), + it('should buy slightly greater than makerAssetAmount when buying ZRX', async () => { + orderWithFee = await orderFactory.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), }); - signedOrdersWithFee = [highFeeERC20Order]; - feeOrders = [feeOrder]; - const makerAssetAmount = signedOrder.makerAssetAmount.div(2); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrdersWithFee, - feeOrders, - feeProportion, - makerAssetAmount, + const ordersWithFee = [orderWithFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithFee.takerAssetAmount; + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getWethForFeeOrders( + makerAssetFillAmount, + ordersWithFee, ); - return expectTransactionFailedAsync( - forwarderWrapper.marketBuyTokensWithEthAsync(signedOrdersWithFee, feeOrders, makerAssetAmount, { - from: takerAddress, - value: fillAmountWei, - }), - RevertReason.UnacceptableThreshold as any, + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + const makerAssetFilledAmount = orderWithFee.makerAssetAmount + .times(primaryTakerAssetFillAmount) + .dividedToIntegerBy(orderWithFee.takerAssetAmount); + const takerFeePaid = orderWithFee.takerFee + .times(primaryTakerAssetFillAmount) + .dividedToIntegerBy(orderWithFee.takerAssetAmount); + const makerFeePaid = orderWithFee.makerFee + .times(primaryTakerAssetFillAmount) + .dividedToIntegerBy(orderWithFee.takerAssetAmount); + const totalZrxPurchased = makerAssetFilledAmount.minus(takerFeePaid); + // Up to 1 wei worth of ZRX will be overbought per order + const maxOverboughtZrx = new BigNumber(1) + .times(orderWithFee.makerAssetAmount) + .dividedToIntegerBy(orderWithFee.takerAssetAmount); + + expect(totalZrxPurchased).to.be.bignumber.gte(makerAssetFillAmount); + expect(totalZrxPurchased).to.be.bignumber.lte(makerAssetFillAmount.plus(maxOverboughtZrx)); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFilledAmount).minus(makerFeePaid), ); - }); - it('throws if makerAssetAmount is 0', async () => { - const makerAssetAmount = new BigNumber(0); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrdersWithFee, - feeOrders, - feeProportion, - makerAssetAmount, + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].plus(totalZrxPurchased), ); - return expectTransactionFailedAsync( - forwarderWrapper.marketBuyTokensWithEthAsync(signedOrdersWithFee, feeOrders, makerAssetAmount, { - from: takerAddress, - value: fillAmountWei, - }), - RevertReason.ValueGreaterThanZero as any, + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[forwarderContract.address][zrxToken.address], ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); - it('throws if the amount of ETH sent in is less than the takerAssetFilledAmount', async () => { - const makerAssetAmount = signedOrder.makerAssetAmount; - const fillAmount = signedOrder.takerAssetAmount.div(2); - const zero = new BigNumber(0); - // Deposit enough taker balance to fill the order - const wethDepositTxHash = await wethContract.deposit.sendTransactionAsync({ + it('should not change balances if the amount of ETH sent is too low to fill the makerAssetAmount', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(4); + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, from: takerAddress, - value: signedOrder.takerAssetAmount, }); - await web3Wrapper.awaitTransactionSuccessAsync(wethDepositTxHash); - // Transfer all of this WETH to the forwarding contract - const wethTransferTxHash = await wethContract.transfer.sendTransactionAsync( - forwarderContract.address, - signedOrder.takerAssetAmount, - { from: takerAddress }, - ); - await web3Wrapper.awaitTransactionSuccessAsync(wethTransferTxHash); - // We use the contract directly to get around wrapper validations and calculations - const formattedOrders = formatters.createMarketSellOrders(signedOrders, zero); - const formattedFeeOrders = formatters.createMarketSellOrders(feeOrders, zero); - return expectTransactionFailedAsync( - forwarderContract.marketBuyTokensWithEth.sendTransactionAsync( - formattedOrders.orders, - formattedOrders.signatures, - formattedFeeOrders.orders, - formattedFeeOrders.signatures, - makerAssetAmount, - zero, - constants.NULL_ADDRESS, - { value: fillAmount, from: takerAddress }, - ), - RevertReason.InvalidMsgValue, - ); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const totalEthSpent = gasPrice.times(tx.gasUsed); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances).to.deep.equal(erc20Balances); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); - }); - describe('marketBuyTokensWithEth - ERC721', async () => { - it('buys ERC721 assets', async () => { + it('should buy an ERC721 asset from a single order', async () => { const makerAssetId = erc721MakerAssetIds[0]; - signedOrder = await orderFactory.newSignedOrderAsync({ + orderWithoutFee = await orderFactory.newSignedOrderAsync({ makerAssetAmount: new BigNumber(1), makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), }); - feeOrders = []; - signedOrders = [signedOrder]; - const makerAssetAmount = new BigNumber(signedOrders.length); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrders, - feeOrders, - feeProportion, - makerAssetAmount, - ); - tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = new BigNumber(1); + const ethValue = orderWithFee.takerAssetAmount; + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { from: takerAddress, - value: fillAmountWei, + value: ethValue, }); - const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); - expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + expect(newOwner).to.be.bignumber.equal(takerAddress); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); - it('buys ERC721 assets with fee abstraction', async () => { + it('should buy an ERC721 asset and ignore later orders with different makerAssetData', async () => { const makerAssetId = erc721MakerAssetIds[0]; - signedOrder = await orderFactory.newSignedOrderAsync({ + orderWithoutFee = await orderFactory.newSignedOrderAsync({ makerAssetAmount: new BigNumber(1), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), }); - signedOrders = [signedOrder]; - const makerAssetAmount = new BigNumber(signedOrders.length); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrders, - feeOrders, - feeProportion, - makerAssetAmount, - ); - tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { + const differentMakerAssetDataOrder = await orderFactory.newSignedOrderAsync(); + const ordersWithoutFee = [orderWithoutFee, differentMakerAssetDataOrder]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = new BigNumber(1).plus(differentMakerAssetDataOrder.makerAssetAmount); + const ethValue = orderWithFee.takerAssetAmount; + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { from: takerAddress, - value: fillAmountWei, + value: ethValue, }); - const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); - expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress); - }); - it('buys ERC721 assets with fee abstraction and pays fee to fee recipient', async () => { - const makerAssetId = erc721MakerAssetIds[0]; - signedOrder = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: new BigNumber(1), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), - makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), - }); - signedOrders = [signedOrder]; - feeProportion = 100; - const initTakerBalanceWei = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const initFeeRecipientBalanceWei = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); - const makerAssetAmount = new BigNumber(signedOrders.length); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrders, - feeOrders, - feeProportion, - makerAssetAmount, + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + expect(newOwner).to.be.bignumber.equal(takerAddress); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), ); - tx = await forwarderWrapper.marketBuyTokensWithEthAsync( - signedOrders, - feeOrders, - makerAssetAmount, - { - from: takerAddress, - value: fillAmountWei, - gasPrice: DEFAULT_GAS_PRICE, - }, - { - feeProportion, - feeRecipient: feeRecipientAddress, - }, + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, ); - const afterFeeRecipientEthBalance = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); - const afterTakerBalanceWei = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const takerFilledAmount = initTakerBalanceWei.minus(afterTakerBalanceWei).plus(tx.gasUsed); - const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); - expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress); - const balanceDiff = afterFeeRecipientEthBalance.minus(initFeeRecipientBalanceWei); - expect(takerFilledAmount.dividedToIntegerBy(balanceDiff)).to.be.bignumber.equal(101); - expect(takerFilledAmount.minus(balanceDiff).dividedToIntegerBy(balanceDiff)).to.be.bignumber.equal(100); - }); - it('buys multiple ERC721 assets with fee abstraction and pays fee to fee recipient', async () => { - const makerAssetId1 = erc721MakerAssetIds[0]; - const makerAssetId2 = erc721MakerAssetIds[1]; - const signedOrder1 = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: new BigNumber(1), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), DECIMALS_DEFAULT), - makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId1), - }); - const signedOrder2 = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: new BigNumber(1), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(4), DECIMALS_DEFAULT), - makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId2), - }); - signedOrders = [signedOrder1, signedOrder2]; - feeProportion = 10; - const makerAssetAmount = new BigNumber(signedOrders.length); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrders, - feeOrders, - feeProportion, - makerAssetAmount, + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress], + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress], ); - tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { - from: takerAddress, - value: fillAmountWei, - }); - const newOwnerTakerAsset1 = await erc721Token.ownerOf.callAsync(makerAssetId1); - expect(newOwnerTakerAsset1).to.be.bignumber.equal(takerAddress); - const newOwnerTakerAsset2 = await erc721Token.ownerOf.callAsync(makerAssetId2); - expect(newOwnerTakerAsset2).to.be.bignumber.equal(takerAddress); }); - it('buys ERC721 assets with fee abstraction and handles fee orders filled and excess eth', async () => { + it('should buy an ERC721 asset and pay ZRX fees from a single fee order', async () => { const makerAssetId = erc721MakerAssetIds[0]; - feeProportion = 0; - // In this scenario a total of 6 ZRX fees need to be paid. - // There are two fee orders, but the first fee order is partially filled while - // the Forwarding contract tx is in the mempool. - const erc721MakerAssetAmount = new BigNumber(1); - signedOrder = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: erc721MakerAssetAmount, - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(6), DECIMALS_DEFAULT), + orderWithFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), }); - signedOrders = [signedOrder]; - const firstFeeOrder = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(8), DECIMALS_DEFAULT), - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.1), DECIMALS_DEFAULT), - makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), - }); - const secondFeeOrder = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(8), DECIMALS_DEFAULT), - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.12), DECIMALS_DEFAULT), - makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), - }); - feeOrders = [firstFeeOrder, secondFeeOrder]; - const makerAssetAmount = new BigNumber(signedOrders.length); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrders, - feeOrders, - feeProportion, - erc721MakerAssetAmount, - ); - // Simulate another otherAddress user partially filling firstFeeOrder - const firstFeeOrderFillAmount = firstFeeOrder.makerAssetAmount.div(2); - tx = await forwarderWrapper.marketBuyTokensWithEthAsync([firstFeeOrder], [], firstFeeOrderFillAmount, { - from: otherAddress, - value: fillAmountWei, - }); - // For tests we calculate how much this should've cost given that firstFeeOrder was filled - const expectedFillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrders, - feeOrders, - feeProportion, - erc721MakerAssetAmount, - ); - // With 4 ZRX remaining in firstFeeOrder, the secondFeeOrder will need to be filled to make up - // the total amount of fees required (6) - // Since the fee orders can be filled while the transaction is pending the user safely sends in - // extra ether to cover any slippage - const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const slippageFillAmountWei = fillAmountWei.times(2); - tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { + const ordersWithFee = [orderWithFee]; + const feeOrders = [feeOrder]; + const makerAssetFillAmount = orderWithFee.makerAssetAmount; + const primaryTakerAssetFillAmount = orderWithFee.takerAssetAmount; + const feeAmount = orderWithFee.takerFee; + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const ethValue = primaryTakerAssetFillAmount.plus(wethSpentOnFeeOrders); + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { + value: ethValue, from: takerAddress, - value: slippageFillAmountWei, - gasPrice: DEFAULT_GAS_PRICE, }); - const afterEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - const expectedEthBalanceAfterGasCosts = initEthBalance.minus(expectedFillAmountWei).minus(tx.gasUsed); - const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); - expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress); - expect(afterEthBalance).to.be.bignumber.equal(expectedEthBalanceAfterGasCosts); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const totalEthSpent = ethValue.plus(gasPrice.times(tx.gasUsed)); + + expect(newOwner).to.be.bignumber.equal(takerAddress); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); - it('buys ERC721 assets with fee abstraction and handles fee orders filled', async () => { + it('should buy an ERC721 asset and pay ZRX fees from multiple fee orders', async () => { const makerAssetId = erc721MakerAssetIds[0]; - feeProportion = 0; - // In this scenario a total of 6 ZRX fees need to be paid. - // There are two fee orders, but the first fee order is partially filled while - // the Forwarding contract tx is in the mempool. - const erc721MakerAssetAmount = new BigNumber(1); - signedOrder = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: erc721MakerAssetAmount, - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(6), DECIMALS_DEFAULT), + orderWithFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), }); - const zrxMakerAssetAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(8), DECIMALS_DEFAULT); - signedOrders = [signedOrder]; + const ordersWithFee = [orderWithFee]; + const makerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const makerAssetAmount = orderWithFee.takerFee.dividedToIntegerBy(2); + const takerAssetAmount = feeOrder.takerAssetAmount + .times(makerAssetAmount) + .dividedToIntegerBy(feeOrder.makerAssetAmount); + const firstFeeOrder = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: zrxMakerAssetAmount, - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.1), DECIMALS_DEFAULT), - makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), + makerAssetData, + makerAssetAmount, + takerAssetAmount, }); const secondFeeOrder = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: zrxMakerAssetAmount, - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.12), DECIMALS_DEFAULT), - makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), - }); - feeOrders = [firstFeeOrder, secondFeeOrder]; - const makerAssetAmount = new BigNumber(signedOrders.length); - const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrders, - feeOrders, - feeProportion, - erc721MakerAssetAmount, - ); - // Simulate another otherAddress user partially filling firstFeeOrder - const firstFeeOrderFillAmount = firstFeeOrder.makerAssetAmount.div(2); - tx = await forwarderWrapper.marketBuyTokensWithEthAsync([firstFeeOrder], [], firstFeeOrderFillAmount, { - from: otherAddress, - value: fillAmountWei, + makerAssetData, + makerAssetAmount, + takerAssetAmount, }); - const expectedFillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync( - signedOrders, - feeOrders, - feeProportion, - erc721MakerAssetAmount, - ); - tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { + const feeOrders = [firstFeeOrder, secondFeeOrder]; + + const makerAssetFillAmount = orderWithFee.makerAssetAmount; + const primaryTakerAssetFillAmount = orderWithFee.takerAssetAmount; + const feeAmount = orderWithFee.takerFee; + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const ethValue = primaryTakerAssetFillAmount.plus(wethSpentOnFeeOrders); + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { + value: ethValue, from: takerAddress, - value: expectedFillAmountWei, }); - const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId); - expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const totalEthSpent = ethValue.plus(gasPrice.times(tx.gasUsed)); + + expect(newOwner).to.be.bignumber.equal(takerAddress); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); - it('throws when mixed ERC721 and ERC20 assets', async () => { - const makerAssetId = erc721MakerAssetIds[0]; - const erc721SignedOrder = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: new BigNumber(1), - makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), - }); - const erc20SignedOrder = await orderFactory.newSignedOrderAsync(); - signedOrders = [erc721SignedOrder, erc20SignedOrder]; - const makerAssetAmount = new BigNumber(signedOrders.length); - const fillAmountWei = erc20SignedOrder.takerAssetAmount.plus(erc721SignedOrder.takerAssetAmount); - return expectTransactionFailedAsync( - forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { + }); + describe('marketBuyOrdersWithEth with extra fees', () => { + it('should buy an asset and send fee to feeRecipient', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount; + + const baseFeePercentage = 2; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + makerAssetFillAmount, + { + value: ethValue, from: takerAddress, - value: fillAmountWei, - }), - RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + }, + { feePercentage, feeRecipient: feeRecipientAddress }, ); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); + const ethSpentOnFee = ForwarderWrapper.getPercentageOfValue(primaryTakerAssetFillAmount, baseFeePercentage); + const totalEthSpent = primaryTakerAssetFillAmount.plus(ethSpentOnFee).plus(gasPrice.times(tx.gasUsed)); + + expect(feeRecipientEthBalanceAfter).to.be.bignumber.equal(feeRecipientEthBalanceBefore.plus(ethSpentOnFee)); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); - it('throws when mixed ERC721 and ERC20 assets with ERC20 first', async () => { - const makerAssetId = erc721MakerAssetIds[0]; - const erc721SignedOrder = await orderFactory.newSignedOrderAsync({ - makerAssetAmount: new BigNumber(1), - makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), - }); - const erc20SignedOrder = await orderFactory.newSignedOrderAsync(); - signedOrders = [erc20SignedOrder, erc721SignedOrder]; - const makerAssetAmount = new BigNumber(signedOrders.length); - const fillAmountWei = erc20SignedOrder.takerAssetAmount.plus(erc721SignedOrder.takerAssetAmount); - return expectTransactionFailedAsync( - forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, { - from: takerAddress, - value: fillAmountWei, - }), - RevertReason.InvalidTakerAmount, + it('should fail if the fee is set too high', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount; + + const baseFeePercentage = 6; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + await expectTransactionFailedAsync( + forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + makerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ), + RevertReason.FeePercentageTooLarge, + ); + }); + it('should fail if there is not enough ETH remaining to pay the fee', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); + + const baseFeePercentage = 2; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + await expectTransactionFailedAsync( + forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + makerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ), + RevertReason.InsufficientEthRemaining, ); }); }); |