diff --git a/contracts/exchange-forwarder/test/utils/forwarder_test_factory.ts b/contracts/exchange-forwarder/test/utils/forwarder_test_factory.ts index bbc2628ae8..7d64e66712 100644 --- a/contracts/exchange-forwarder/test/utils/forwarder_test_factory.ts +++ b/contracts/exchange-forwarder/test/utils/forwarder_test_factory.ts @@ -1,5 +1,13 @@ -import { ERC20Wrapper, ERC721Wrapper } from '@0x/contracts-asset-proxy'; -import { chaiSetup, constants, ERC20BalancesByOwner, expectTransactionFailedAsync, web3Wrapper } from '@0x/contracts-test-utils'; +import { ERC20Wrapper } from '@0x/contracts-asset-proxy'; +import { DummyERC20TokenContract } from '@0x/contracts-erc20'; +import { DummyERC721TokenContract } from '@0x/contracts-erc721'; +import { + chaiSetup, + constants, + ERC20BalancesByOwner, + expectTransactionFailedAsync, + web3Wrapper, +} from '@0x/contracts-test-utils'; import { RevertReason, SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as chai from 'chai'; @@ -20,10 +28,7 @@ interface ForwarderFillState { maxOverboughtMakerAsset: BigNumber; } -function computeExpectedResults( - orders: SignedOrder[], - fractionalNumberOfOrdersToFill: BigNumber, -): ForwarderFillState { +function computeExpectedResults(orders: SignedOrder[], fractionalNumberOfOrdersToFill: BigNumber): ForwarderFillState { const currentState = { takerAssetFillAmount: constants.ZERO_AMOUNT, makerAssetFillAmount: constants.ZERO_AMOUNT, @@ -34,42 +39,49 @@ function computeExpectedResults( }; let remainingOrdersToFill = fractionalNumberOfOrdersToFill; - _.forEach(orders, (order: SignedOrder): void => { - if (remainingOrdersToFill.isEqualTo(constants.ZERO_AMOUNT)) { - return; - } + _.forEach( + orders, + (order: SignedOrder): void => { + if (remainingOrdersToFill.isEqualTo(constants.ZERO_AMOUNT)) { + return; + } - let makerAssetAmount; - let takerAssetAmount; - let takerFee; - if (remainingOrdersToFill.isLessThan(new BigNumber(1))) { - const [partialFillNumerator, partialFillDenominator] = remainingOrdersToFill.toFraction(); - makerAssetAmount = order.makerAssetAmount.times(partialFillNumerator).dividedToIntegerBy(partialFillDenominator); - takerAssetAmount = order.takerAssetAmount.times(partialFillNumerator).dividedToIntegerBy(partialFillDenominator); - takerFee = order.takerFee.times(partialFillNumerator).dividedToIntegerBy(partialFillDenominator); - } else { - makerAssetAmount = order.makerAssetAmount; - takerAssetAmount = order.takerAssetAmount; - takerFee = order.takerFee; - } + let makerAssetAmount; + let takerAssetAmount; + let takerFee; + if (remainingOrdersToFill.isLessThan(new BigNumber(1))) { + const [partialFillNumerator, partialFillDenominator] = remainingOrdersToFill.toFraction(); + makerAssetAmount = order.makerAssetAmount + .times(partialFillNumerator) + .dividedToIntegerBy(partialFillDenominator); + takerAssetAmount = order.takerAssetAmount + .times(partialFillNumerator) + .dividedToIntegerBy(partialFillDenominator); + takerFee = order.takerFee.times(partialFillNumerator).dividedToIntegerBy(partialFillDenominator); + } else { + makerAssetAmount = order.makerAssetAmount; + takerAssetAmount = order.takerAssetAmount; + takerFee = order.takerFee; + } - currentState.takerAssetFillAmount = currentState.takerAssetFillAmount.plus(takerAssetAmount); - currentState.makerAssetFillAmount = currentState.makerAssetFillAmount.plus(makerAssetAmount); + currentState.takerAssetFillAmount = currentState.takerAssetFillAmount.plus(takerAssetAmount); + currentState.makerAssetFillAmount = currentState.makerAssetFillAmount.plus(makerAssetAmount); - if (order.takerFeeAssetData === order.makerAssetData) { - currentState.percentageFees = currentState.percentageFees.plus(takerFee); - } else if (order.takerFeeAssetData === order.takerAssetData) { - currentState.wethFees = currentState.wethFees.plus(takerFee); - // Up to 1 wei worth of WETH will be oversold per order - currentState.maxOversoldWeth = currentState.maxOversoldWeth.plus(new BigNumber(1)); - // Equivalently, up to 1 wei worth of maker asset will be overbought per order - currentState.maxOverboughtMakerAsset = currentState.maxOversoldWeth - .times(makerAssetAmount) - .dividedToIntegerBy(takerAssetAmount); - } + if (order.takerFeeAssetData === order.makerAssetData) { + currentState.percentageFees = currentState.percentageFees.plus(takerFee); + } else if (order.takerFeeAssetData === order.takerAssetData) { + currentState.wethFees = currentState.wethFees.plus(takerFee); + // Up to 1 wei worth of WETH will be oversold per order + currentState.maxOversoldWeth = currentState.maxOversoldWeth.plus(new BigNumber(1)); + // Equivalently, up to 1 wei worth of maker asset will be overbought per order + currentState.maxOverboughtMakerAsset = currentState.maxOversoldWeth + .times(makerAssetAmount) + .dividedToIntegerBy(takerAssetAmount); + } - remainingOrdersToFill = BigNumber.min(remainingOrdersToFill.minus(new BigNumber(1)), constants.ZERO_AMOUNT); - }); + remainingOrdersToFill = BigNumber.max(remainingOrdersToFill.minus(new BigNumber(1)), constants.ZERO_AMOUNT); + }, + ); return currentState; } @@ -121,21 +133,32 @@ export class ForwarderTestFactory { public async marketBuyTestAsync( orders: SignedOrder[], fractionalNumberOfOrdersToFill: BigNumber, - makerAssetAddress: string, - takerEthBalanceBefore: BigNumber, - erc20Balances: ERC20BalancesByOwner, - ethValueAdjustment: BigNumber = constants.ZERO_AMOUNT, - forwarderFeeOptions: { - baseFeePercentage: BigNumber; - forwarderFeeRecipientEthBalanceBefore: BigNumber; - } = { baseFeePercentage: constants.ZERO_AMOUNT, forwarderFeeRecipientEthBalanceBefore: constants.ZERO_AMOUNT }, + makerAssetContract: DummyERC20TokenContract | DummyERC721TokenContract, + options: { + ethValueAdjustment?: BigNumber; + forwarderFeePercentage?: BigNumber; + makerAssetId?: BigNumber; + revertReason?: RevertReason; + } = {}, ): Promise { + const ethValueAdjustment = options.ethValueAdjustment || constants.ZERO_AMOUNT; + const forwarderFeePercentage = options.forwarderFeePercentage || constants.ZERO_AMOUNT; + + const erc20Balances = await this._erc20Wrapper.getBalancesAsync(); + const takerEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(this._takerAddress); + const forwarderFeeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync( + this._forwarderFeeRecipientAddress, + ); + const expectedResults = computeExpectedResults(orders, fractionalNumberOfOrdersToFill); const ethSpentOnForwarderFee = ForwarderTestFactory.getPercentageOfValue( expectedResults.takerAssetFillAmount, - forwarderFeeOptions.baseFeePercentage, + forwarderFeePercentage, + ); + const feePercentage = ForwarderTestFactory.getPercentageOfValue( + constants.PERCENTAGE_DENOMINATOR, + forwarderFeePercentage, ); - const feePercentage = ForwarderTestFactory.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, forwarderFeeOptions.baseFeePercentage); const ethValue = expectedResults.takerAssetFillAmount .plus(expectedResults.wethFees) @@ -143,16 +166,20 @@ export class ForwarderTestFactory { .plus(ethSpentOnForwarderFee) .plus(ethValueAdjustment); - if (ethValueAdjustment.isNegative()) { - return expectTransactionFailedAsync( + if (options.revertReason !== undefined) { + await expectTransactionFailedAsync( this._forwarderWrapper.marketBuyOrdersWithEthAsync( orders, - expectedResults.makerAssetFillAmount, { - value: ethValue, - from: this._takerAddress, - }), - RevertReason.CompleteFillFailed, + expectedResults.makerAssetFillAmount, + { + value: ethValue, + from: this._takerAddress, + }, + { feePercentage, feeRecipient: this._forwarderFeeRecipientAddress }, + ), + options.revertReason, ); + return; } const tx = await this._forwarderWrapper.marketBuyOrdersWithEthAsync( @@ -165,40 +192,60 @@ export class ForwarderTestFactory { { feePercentage, feeRecipient: this._forwarderFeeRecipientAddress }, ); - await this._checkResultsAsync( - tx, - expectedResults, - forwarderFeeOptions.forwarderFeeRecipientEthBalanceBefore, - takerEthBalanceBefore, - erc20Balances, - ethSpentOnForwarderFee, - makerAssetAddress, - ); + await this._checkResultsAsync(tx, expectedResults, takerEthBalanceBefore, erc20Balances, makerAssetContract, { + forwarderFeePercentage, + forwarderFeeRecipientEthBalanceBefore, + makerAssetId: options.makerAssetId, + }); } public async marketSellTestAsync( orders: SignedOrder[], fractionalNumberOfOrdersToFill: BigNumber, - makerAssetAddress: string, - takerEthBalanceBefore: BigNumber, - erc20Balances: ERC20BalancesByOwner, - forwarderFeeOptions: { - baseFeePercentage: BigNumber; - forwarderFeeRecipientEthBalanceBefore: BigNumber; - } = { baseFeePercentage: constants.ZERO_AMOUNT, forwarderFeeRecipientEthBalanceBefore: constants.ZERO_AMOUNT }, + makerAssetContract: DummyERC20TokenContract, + options: { + forwarderFeePercentage?: BigNumber; + revertReason?: RevertReason; + } = {}, ): Promise { + const forwarderFeePercentage = options.forwarderFeePercentage || constants.ZERO_AMOUNT; + + const erc20Balances = await this._erc20Wrapper.getBalancesAsync(); + const takerEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(this._takerAddress); + const forwarderFeeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync( + this._forwarderFeeRecipientAddress, + ); + const expectedResults = computeExpectedResults(orders, fractionalNumberOfOrdersToFill); const ethSpentOnForwarderFee = ForwarderTestFactory.getPercentageOfValue( expectedResults.takerAssetFillAmount, - forwarderFeeOptions.baseFeePercentage, + forwarderFeePercentage, + ); + const feePercentage = ForwarderTestFactory.getPercentageOfValue( + constants.PERCENTAGE_DENOMINATOR, + forwarderFeePercentage, ); - const feePercentage = ForwarderTestFactory.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, forwarderFeeOptions.baseFeePercentage); const ethValue = expectedResults.takerAssetFillAmount .plus(expectedResults.wethFees) .plus(expectedResults.maxOversoldWeth) .plus(ethSpentOnForwarderFee); + if (options.revertReason !== undefined) { + await expectTransactionFailedAsync( + this._forwarderWrapper.marketSellOrdersWithEthAsync( + orders, + { + value: ethValue, + from: this._takerAddress, + }, + { feePercentage, feeRecipient: this._forwarderFeeRecipientAddress }, + ), + options.revertReason, + ); + return; + } + const tx = await this._forwarderWrapper.marketSellOrdersWithEthAsync( orders, { @@ -208,33 +255,65 @@ export class ForwarderTestFactory { { feePercentage, feeRecipient: this._forwarderFeeRecipientAddress }, ); - await this._checkResultsAsync( - tx, - expectedResults, - forwarderFeeOptions.forwarderFeeRecipientEthBalanceBefore, - takerEthBalanceBefore, - erc20Balances, - ethSpentOnForwarderFee, - makerAssetAddress, + await this._checkResultsAsync(tx, expectedResults, takerEthBalanceBefore, erc20Balances, makerAssetContract, { + forwarderFeePercentage, + forwarderFeeRecipientEthBalanceBefore, + }); + } + + private _checkErc20Balances( + oldBalances: ERC20BalancesByOwner, + newBalances: ERC20BalancesByOwner, + expectedResults: ForwarderFillState, + makerAssetContract: DummyERC20TokenContract, + ): void { + const makerAssetAddress = makerAssetContract.address; + expectBalanceWithin( + newBalances[this._makerAddress][makerAssetAddress], + oldBalances[this._makerAddress][makerAssetAddress] + .minus(expectedResults.makerAssetFillAmount) + .minus(expectedResults.maxOverboughtMakerAsset), + oldBalances[this._makerAddress][makerAssetAddress].minus(expectedResults.makerAssetFillAmount), ); + expectBalanceWithin( + newBalances[this._takerAddress][makerAssetAddress], + oldBalances[this._takerAddress][makerAssetAddress] + .plus(expectedResults.makerAssetFillAmount) + .minus(expectedResults.percentageFees), + oldBalances[this._takerAddress][makerAssetAddress] + .plus(expectedResults.makerAssetFillAmount) + .minus(expectedResults.percentageFees) + .plus(expectedResults.maxOverboughtMakerAsset), + ); + expect(newBalances[this._orderFeeRecipientAddress][makerAssetAddress]).to.be.bignumber.equal( + oldBalances[this._orderFeeRecipientAddress][makerAssetAddress].plus(expectedResults.percentageFees), + ); + expect(newBalances[this._forwarderAddress][makerAssetAddress]).to.be.bignumber.equal(constants.ZERO_AMOUNT); } private async _checkResultsAsync( tx: TransactionReceiptWithDecodedLogs, expectedResults: ForwarderFillState, - forwarderFeeRecipientEthBalanceBefore: BigNumber, takerEthBalanceBefore: BigNumber, erc20Balances: ERC20BalancesByOwner, - ethSpentOnForwarderFee: BigNumber, - makerAssetAddress: string, + makerAssetContract: DummyERC20TokenContract | DummyERC721TokenContract, + options: { + forwarderFeePercentage?: BigNumber; + forwarderFeeRecipientEthBalanceBefore?: BigNumber; + makerAssetId?: BigNumber; + } = {}, ): Promise { + const ethSpentOnForwarderFee = ForwarderTestFactory.getPercentageOfValue( + expectedResults.takerAssetFillAmount, + options.forwarderFeePercentage || constants.ZERO_AMOUNT, + ); const totalEthSpent = expectedResults.takerAssetFillAmount .plus(expectedResults.wethFees) .plus(ethSpentOnForwarderFee) .plus(this._gasPrice.times(tx.gasUsed)); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(this._takerAddress); const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(this._forwarderAddress); - const fowarderFeeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(this._forwarderFeeRecipientAddress); const newBalances = await this._erc20Wrapper.getBalancesAsync(); expectBalanceWithin( @@ -242,32 +321,21 @@ export class ForwarderTestFactory { takerEthBalanceBefore.minus(totalEthSpent).minus(expectedResults.maxOversoldWeth), takerEthBalanceBefore.minus(totalEthSpent), ); - if (ethSpentOnForwarderFee.gt(0)) { + if (options.forwarderFeeRecipientEthBalanceBefore !== undefined) { + const fowarderFeeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync( + this._forwarderFeeRecipientAddress, + ); expect(fowarderFeeRecipientEthBalanceAfter).to.be.bignumber.equal( - forwarderFeeRecipientEthBalanceBefore.plus(ethSpentOnForwarderFee), + options.forwarderFeeRecipientEthBalanceBefore.plus(ethSpentOnForwarderFee), ); } - expectBalanceWithin( - newBalances[this._makerAddress][makerAssetAddress], - erc20Balances[this._makerAddress][makerAssetAddress] - .minus(expectedResults.makerAssetFillAmount) - .minus(expectedResults.maxOverboughtMakerAsset), - erc20Balances[this._makerAddress][makerAssetAddress].minus(expectedResults.makerAssetFillAmount), - ); - expectBalanceWithin( - newBalances[this._takerAddress][makerAssetAddress], - erc20Balances[this._takerAddress][makerAssetAddress] - .plus(expectedResults.makerAssetFillAmount) - .minus(expectedResults.percentageFees), - erc20Balances[this._takerAddress][makerAssetAddress] - .plus(expectedResults.makerAssetFillAmount) - .minus(expectedResults.percentageFees) - .plus(expectedResults.maxOverboughtMakerAsset), - ); - expect(newBalances[this._orderFeeRecipientAddress][makerAssetAddress]).to.be.bignumber.equal( - erc20Balances[this._orderFeeRecipientAddress][makerAssetAddress].plus(expectedResults.percentageFees), - ); + if (makerAssetContract instanceof DummyERC20TokenContract) { + await this._checkErc20Balances(erc20Balances, newBalances, expectedResults, makerAssetContract); + } else if (options.makerAssetId !== undefined) { + const newOwner = await makerAssetContract.ownerOf.callAsync(options.makerAssetId); + expect(newOwner).to.be.bignumber.equal(this._takerAddress); + } expectBalanceWithin( newBalances[this._makerAddress][this._wethAddress], @@ -281,7 +349,6 @@ export class ForwarderTestFactory { ); expect(newBalances[this._forwarderAddress][this._wethAddress]).to.be.bignumber.equal(constants.ZERO_AMOUNT); - expect(newBalances[this._forwarderAddress][makerAssetAddress]).to.be.bignumber.equal(constants.ZERO_AMOUNT); expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); } }