refactoring and marketBuyTestAsync

This commit is contained in:
Michael Zhu
2019-07-21 17:37:47 +09:00
parent 116945047b
commit 688209e272
2 changed files with 194 additions and 76 deletions

View File

@@ -1,21 +1,16 @@
import { ERC20Wrapper, ERC721Wrapper } from '@0x/contracts-asset-proxy';
import { chaiSetup, constants, ERC20BalancesByOwner, web3Wrapper } from '@0x/contracts-test-utils';
import { SignedOrder } from '@0x/types';
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';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';
import { ForwarderWrapper } from './forwarder_wrapper';
chaiSetup.configure();
const expect = chai.expect;
// TODO: move these somewhere?
const enum OrderFee {
NoFee,
Percentage,
Weth,
}
interface ForwarderFillState {
takerAssetFillAmount: BigNumber;
makerAssetFillAmount: BigNumber;
@@ -25,14 +20,63 @@ interface ForwarderFillState {
maxOverboughtMakerAsset: BigNumber;
}
function getFeeType(order: SignedOrder): OrderFee {
if (order.takerFee.eq(new BigNumber(0))) {
return OrderFee.NoFee;
} else if (order.takerFeeAssetData === order.makerAssetData) {
return OrderFee.Percentage;
} else {
return OrderFee.Weth;
}
function computeExpectedResults(
orders: SignedOrder[],
fractionalNumberOfOrdersToFill: BigNumber,
): ForwarderFillState {
const currentState = {
takerAssetFillAmount: constants.ZERO_AMOUNT,
makerAssetFillAmount: constants.ZERO_AMOUNT,
wethFees: constants.ZERO_AMOUNT,
percentageFees: constants.ZERO_AMOUNT,
maxOversoldWeth: constants.ZERO_AMOUNT,
maxOverboughtMakerAsset: constants.ZERO_AMOUNT,
};
let remainingOrdersToFill = fractionalNumberOfOrdersToFill;
_.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;
}
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);
}
remainingOrdersToFill = BigNumber.min(remainingOrdersToFill.minus(new BigNumber(1)), constants.ZERO_AMOUNT);
});
return currentState;
}
function expectBalanceWithin(balance: BigNumber, low: BigNumber, high: BigNumber): void {
expect(balance).to.be.bignumber.gte(low);
expect(balance).to.be.bignumber.lte(high);
}
export class ForwarderTestFactory {
@@ -46,6 +90,12 @@ export class ForwarderTestFactory {
private readonly _gasPrice: BigNumber;
private readonly _wethAddress: string;
public static getPercentageOfValue(value: BigNumber, percentage: BigNumber): BigNumber {
const numerator = constants.PERCENTAGE_DENOMINATOR.times(percentage).dividedToIntegerBy(100);
const newValue = value.times(numerator).dividedToIntegerBy(constants.PERCENTAGE_DENOMINATOR);
return newValue;
}
constructor(
forwarderWrapper: ForwarderWrapper,
erc20Wrapper: ERC20Wrapper,
@@ -68,82 +118,155 @@ export class ForwarderTestFactory {
this._wethAddress = wethAddress;
}
// TODO: different assets (ERC721), rounding, forwarder fees
public async generateTestAsync(
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 },
): Promise<void> {
// Simulates filling all but the last order, which will be partially filled
const expectedResults = orders
.reduce(
(prev: ForwarderFillState, order: SignedOrder, currentIndex: number) => {
const current = { ...prev };
const { makerAssetAmount, takerAssetAmount, takerFee } = currentIndex === (orders.length - 1) ?
{
makerAssetAmount: order.makerAssetAmount.dividedToIntegerBy(2),
takerAssetAmount: order.takerAssetAmount.dividedToIntegerBy(2),
takerFee: order.takerFee.dividedToIntegerBy(2),
} : order;
const expectedResults = computeExpectedResults(orders, fractionalNumberOfOrdersToFill);
const ethSpentOnForwarderFee = ForwarderTestFactory.getPercentageOfValue(
expectedResults.takerAssetFillAmount,
forwarderFeeOptions.baseFeePercentage,
);
const ethValue = expectedResults.takerAssetFillAmount
.plus(expectedResults.wethFees)
.plus(ethSpentOnForwarderFee)
.plus(ethValueAdjustment);
const feePercentage = ForwarderTestFactory.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, forwarderFeeOptions.baseFeePercentage);
current.takerAssetFillAmount = current.takerAssetFillAmount.plus(
takerAssetAmount,
);
current.makerAssetFillAmount = current.makerAssetFillAmount.plus(
makerAssetAmount,
);
switch (getFeeType(order)) {
case OrderFee.Percentage:
current.percentageFees = current.percentageFees.plus(takerFee);
break;
case OrderFee.Weth:
current.wethFees = current.wethFees.plus(takerFee);
current.maxOversoldWeth = current.maxOversoldWeth.plus(new BigNumber(1));
current.maxOverboughtMakerAsset = current.maxOversoldWeth
.times(makerAssetAmount)
.dividedToIntegerBy(takerAssetAmount);
break;
default:
}
return current;
},
{
takerAssetFillAmount: new BigNumber(0),
makerAssetFillAmount: new BigNumber(0),
wethFees: new BigNumber(0),
percentageFees: new BigNumber(0),
maxOversoldWeth: new BigNumber(0),
maxOverboughtMakerAsset: new BigNumber(0),
},
if (ethValueAdjustment.isNegative()) {
return expectTransactionFailedAsync(
this._forwarderWrapper.marketBuyOrdersWithEthAsync(
orders,
expectedResults.makerAssetFillAmount, {
value: ethValue,
from: this._takerAddress,
}),
RevertReason.CompleteFillFailed,
);
}
const ethValue = expectedResults.takerAssetFillAmount.plus(expectedResults.wethFees);
const tx = await this._forwarderWrapper.marketSellOrdersWithEthAsync(orders, {
value: ethValue,
from: this._takerAddress,
});
const totalEthSpent = ethValue.plus(this._gasPrice.times(tx.gasUsed));
const tx = await this._forwarderWrapper.marketBuyOrdersWithEthAsync(
orders,
expectedResults.makerAssetFillAmount,
{
value: ethValue,
from: this._takerAddress,
},
{ feePercentage, feeRecipient: this._forwarderFeeRecipientAddress },
);
await this._checkResultsAsync(
tx,
expectedResults,
forwarderFeeOptions.forwarderFeeRecipientEthBalanceBefore,
takerEthBalanceBefore,
erc20Balances,
ethSpentOnForwarderFee,
makerAssetAddress,
);
}
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 },
): Promise<void> {
const expectedResults = computeExpectedResults(orders, fractionalNumberOfOrdersToFill);
const ethSpentOnForwarderFee = ForwarderTestFactory.getPercentageOfValue(
expectedResults.takerAssetFillAmount,
forwarderFeeOptions.baseFeePercentage,
);
const ethValue = expectedResults.takerAssetFillAmount
.plus(expectedResults.wethFees)
.plus(ethSpentOnForwarderFee);
const feePercentage = ForwarderTestFactory.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, forwarderFeeOptions.baseFeePercentage);
const tx = await this._forwarderWrapper.marketSellOrdersWithEthAsync(
orders,
{
value: ethValue,
from: this._takerAddress,
},
{ feePercentage, feeRecipient: this._forwarderFeeRecipientAddress },
);
await this._checkResultsAsync(
tx,
expectedResults,
forwarderFeeOptions.forwarderFeeRecipientEthBalanceBefore,
takerEthBalanceBefore,
erc20Balances,
ethSpentOnForwarderFee,
makerAssetAddress,
);
}
private async _checkResultsAsync(
tx: TransactionReceiptWithDecodedLogs,
expectedResults: ForwarderFillState,
forwarderFeeRecipientEthBalanceBefore: BigNumber,
takerEthBalanceBefore: BigNumber,
erc20Balances: ERC20BalancesByOwner,
ethSpentOnForwarderFee: BigNumber,
makerAssetAddress: string,
): Promise<void> {
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();
expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent));
if (ethSpentOnForwarderFee.gt(0)) {
expect(fowarderFeeRecipientEthBalanceAfter).to.be.bignumber.equal(
forwarderFeeRecipientEthBalanceBefore.plus(ethSpentOnForwarderFee),
);
}
expect(newBalances[this._makerAddress][makerAssetAddress]).to.be.bignumber.equal(
expectBalanceWithin(
newBalances[this._makerAddress][makerAssetAddress],
erc20Balances[this._makerAddress][makerAssetAddress]
.minus(expectedResults.makerAssetFillAmount)
.minus(expectedResults.maxOverboughtMakerAsset),
erc20Balances[this._makerAddress][makerAssetAddress].minus(expectedResults.makerAssetFillAmount),
);
expect(newBalances[this._takerAddress][makerAssetAddress]).to.be.bignumber.equal(
erc20Balances[this._takerAddress][makerAssetAddress].plus(expectedResults.makerAssetFillAmount).minus(expectedResults.percentageFees),
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),
);
expect(newBalances[this._makerAddress][this._wethAddress]).to.be.bignumber.equal(
erc20Balances[this._makerAddress][this._wethAddress].plus(expectedResults.takerAssetFillAmount).minus(expectedResults.wethFees),
expectBalanceWithin(
newBalances[this._makerAddress][this._wethAddress],
erc20Balances[this._makerAddress][this._wethAddress].plus(expectedResults.takerAssetFillAmount),
erc20Balances[this._makerAddress][this._wethAddress]
.plus(expectedResults.takerAssetFillAmount)
.plus(expectedResults.maxOversoldWeth),
);
expect(newBalances[this._orderFeeRecipientAddress][this._wethAddress]).to.be.bignumber.equal(
erc20Balances[this._orderFeeRecipientAddress][this._wethAddress].plus(expectedResults.wethFees),

View File

@@ -15,11 +15,6 @@ export class ForwarderWrapper {
private readonly _web3Wrapper: Web3Wrapper;
private readonly _forwarderContract: ForwarderContract;
private readonly _logDecoder: LogDecoder;
public static getPercentageOfValue(value: BigNumber, percentage: number): BigNumber {
const numerator = constants.PERCENTAGE_DENOMINATOR.times(percentage).dividedToIntegerBy(100);
const newValue = value.times(numerator).dividedToIntegerBy(constants.PERCENTAGE_DENOMINATOR);
return newValue;
}
constructor(contractInstance: ForwarderContract, provider: Web3ProviderEngine) {
this._forwarderContract = contractInstance;
this._web3Wrapper = new Web3Wrapper(provider);