Move Forwarder test to integrations; update to use new framework

This commit is contained in:
Michael Zhu 2019-10-22 11:47:16 -07:00
parent db9be73fec
commit 3d56c06ff3
16 changed files with 953 additions and 1238 deletions

View File

@ -14,12 +14,11 @@
"build:ts": "tsc -b",
"build:ci": "yarn build",
"pre_build": "run-s compile contracts:gen generate_contract_wrappers",
"test": "yarn run_mocha",
"test": "mocha --require source-map-support/register --require make-promises-safe '../integrations/lib/test/forwarder/**/*.js' --timeout 100000 --bail --exit",
"rebuild_and_test": "run-s build test",
"test:coverage": "SOLIDITY_COVERAGE=true run-s build run_mocha coverage:report:text coverage:report:lcov",
"test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html",
"test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha",
"run_mocha": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit",
"compile": "sol-compiler",
"watch": "sol-compiler -w",
"clean": "shx rm -rf lib generated-artifacts generated-wrappers",

View File

@ -1,3 +1,2 @@
export * from './artifacts';
export * from './wrappers';
export * from '../test/utils';

View File

@ -1,715 +0,0 @@
import { ERC20Wrapper, ERC721Wrapper } from '@0x/contracts-asset-proxy';
import { artifacts as erc20Artifacts, DummyERC20TokenContract, WETH9Contract } from '@0x/contracts-erc20';
import { DummyERC721TokenContract } from '@0x/contracts-erc721';
import {
artifacts as exchangeArtifacts,
ExchangeContract,
ExchangeWrapper,
TestProtocolFeeCollectorContract,
} from '@0x/contracts-exchange';
import {
blockchainTests,
constants,
ContractName,
expect,
getLatestBlockTimestampAsync,
OrderFactory,
sendTransactionResult,
} from '@0x/contracts-test-utils';
import { assetDataUtils, ForwarderRevertErrors } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { artifacts, ForwarderContract, ForwarderTestFactory, ForwarderWrapper } from '../src';
const DECIMALS_DEFAULT = 18;
blockchainTests(ContractName.Forwarder, env => {
let owner: string;
let makerAddress: string;
let takerAddress: string;
let orderFeeRecipientAddress: string;
let forwarderFeeRecipientAddress: string;
let defaultMakerAssetAddress: string;
let weth: DummyERC20TokenContract;
let erc20Token: DummyERC20TokenContract;
let secondErc20Token: DummyERC20TokenContract;
let erc721Token: DummyERC721TokenContract;
let forwarderContract: ForwarderContract;
let wethContract: WETH9Contract;
let exchangeContract: ExchangeContract;
let protocolFeeCollector: TestProtocolFeeCollectorContract;
let forwarderWrapper: ForwarderWrapper;
let exchangeWrapper: ExchangeWrapper;
let erc20Wrapper: ERC20Wrapper;
let orderFactory: OrderFactory;
let forwarderTestFactory: ForwarderTestFactory;
let chainId: number;
let wethAssetData: string;
let erc721MakerAssetIds: BigNumber[];
const GAS_PRICE = new BigNumber(env.txDefaults.gasPrice || constants.DEFAULT_GAS_PRICE);
const PROTOCOL_FEE_MULTIPLIER = new BigNumber(150);
const PROTOCOL_FEE = GAS_PRICE.times(PROTOCOL_FEE_MULTIPLIER);
before(async () => {
// Set up addresses
const accounts = await env.getAccountAddressesAsync();
const usedAddresses = ([
owner,
makerAddress,
takerAddress,
orderFeeRecipientAddress,
forwarderFeeRecipientAddress,
] = accounts);
// Set up Exchange
chainId = await env.getChainIdAsync();
exchangeContract = await ExchangeContract.deployFrom0xArtifactAsync(
exchangeArtifacts.Exchange,
env.provider,
env.txDefaults,
{},
new BigNumber(chainId),
);
exchangeWrapper = new ExchangeWrapper(exchangeContract);
// Set up ERC20
erc20Wrapper = new ERC20Wrapper(env.provider, usedAddresses, owner);
[erc20Token, secondErc20Token] = await erc20Wrapper.deployDummyTokensAsync(2, constants.DUMMY_TOKEN_DECIMALS);
const erc20Proxy = await erc20Wrapper.deployProxyAsync();
await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner);
await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeContract.address, {
from: owner,
});
// Set up WETH
wethContract = await WETH9Contract.deployFrom0xArtifactAsync(
erc20Artifacts.WETH9,
env.provider,
env.txDefaults,
{},
);
weth = new DummyERC20TokenContract(wethContract.address, env.provider);
wethAssetData = assetDataUtils.encodeERC20AssetData(wethContract.address);
erc20Wrapper.addDummyTokenContract(weth);
await erc20Wrapper.setBalancesAndAllowancesAsync();
// Set up ERC721
const erc721Wrapper = new ERC721Wrapper(env.provider, usedAddresses, owner);
[erc721Token] = await erc721Wrapper.deployDummyTokensAsync();
const erc721Proxy = await erc721Wrapper.deployProxyAsync();
await erc721Wrapper.setBalancesAndAllowancesAsync();
const erc721Balances = await erc721Wrapper.getBalancesAsync();
erc721MakerAssetIds = erc721Balances[makerAddress][erc721Token.address];
await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner);
await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeContract.address, {
from: owner,
});
// Set up Protocol Fee Collector
protocolFeeCollector = await TestProtocolFeeCollectorContract.deployFrom0xArtifactAsync(
exchangeArtifacts.TestProtocolFeeCollector,
env.provider,
env.txDefaults,
{},
wethContract.address,
);
await exchangeContract.setProtocolFeeMultiplier.awaitTransactionSuccessAsync(PROTOCOL_FEE_MULTIPLIER);
await exchangeContract.setProtocolFeeCollectorAddress.awaitTransactionSuccessAsync(
protocolFeeCollector.address,
);
erc20Wrapper.addTokenOwnerAddress(protocolFeeCollector.address);
// Set defaults
defaultMakerAssetAddress = erc20Token.address;
const defaultTakerAssetAddress = wethContract.address;
const defaultOrderParams = {
makerAddress,
feeRecipientAddress: orderFeeRecipientAddress,
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(200, DECIMALS_DEFAULT),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, DECIMALS_DEFAULT),
makerFee: Web3Wrapper.toBaseUnitAmount(0, DECIMALS_DEFAULT),
takerFee: Web3Wrapper.toBaseUnitAmount(0, DECIMALS_DEFAULT),
makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress),
makerFeeAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
takerFeeAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
exchangeAddress: exchangeContract.address,
chainId,
};
// Set up Forwarder
forwarderContract = await ForwarderContract.deployFrom0xArtifactAsync(
artifacts.Forwarder,
env.provider,
env.txDefaults,
{},
exchangeContract.address,
wethAssetData,
);
forwarderWrapper = new ForwarderWrapper(forwarderContract, env.provider);
await forwarderWrapper.approveMakerAssetProxyAsync(defaultOrderParams.makerAssetData, {
from: takerAddress,
});
erc20Wrapper.addTokenOwnerAddress(forwarderContract.address);
// Set up factories
const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)];
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
forwarderTestFactory = new ForwarderTestFactory(
exchangeWrapper,
forwarderWrapper,
erc20Wrapper,
forwarderContract.address,
makerAddress,
takerAddress,
protocolFeeCollector.address,
orderFeeRecipientAddress,
forwarderFeeRecipientAddress,
weth.address,
GAS_PRICE,
PROTOCOL_FEE_MULTIPLIER,
);
});
blockchainTests.resets('constructor', () => {
it('should revert if assetProxy is unregistered', async () => {
const exchange = await ExchangeContract.deployFrom0xArtifactAsync(
exchangeArtifacts.Exchange,
env.provider,
env.txDefaults,
{},
new BigNumber(chainId),
);
const deployForwarder = (ForwarderContract.deployFrom0xArtifactAsync(
artifacts.Forwarder,
env.provider,
env.txDefaults,
{},
exchange.address,
wethAssetData,
) as any) as sendTransactionResult;
await expect(deployForwarder).to.revertWith(new ForwarderRevertErrors.UnregisteredAssetProxyError());
});
});
blockchainTests.resets('marketSellOrdersWithEth without extra fees', () => {
it('should fill a single order without a taker fee', async () => {
const orderWithoutFee = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketSellTestAsync([orderWithoutFee], 0.78, [erc20Token]);
});
it('should fill multiple orders without taker fees', async () => {
const firstOrder = await orderFactory.newSignedOrderAsync();
const secondOrder = await orderFactory.newSignedOrderAsync({
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(285, DECIMALS_DEFAULT),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(21, DECIMALS_DEFAULT),
});
const orders = [firstOrder, secondOrder];
await forwarderTestFactory.marketSellTestAsync(orders, 1.51, [erc20Token]);
});
it('should fill a single order with a percentage fee', async () => {
const orderWithPercentageFee = await orderFactory.newSignedOrderAsync({
takerFee: Web3Wrapper.toBaseUnitAmount(1, DECIMALS_DEFAULT),
takerFeeAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
});
await forwarderTestFactory.marketSellTestAsync([orderWithPercentageFee], 0.58, [erc20Token]);
});
it('should fill multiple orders with percentage fees', async () => {
const firstOrder = await orderFactory.newSignedOrderAsync({
takerFee: Web3Wrapper.toBaseUnitAmount(1, DECIMALS_DEFAULT),
takerFeeAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
});
const secondOrder = await orderFactory.newSignedOrderAsync({
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(190, DECIMALS_DEFAULT),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(31, DECIMALS_DEFAULT),
takerFee: Web3Wrapper.toBaseUnitAmount(2, DECIMALS_DEFAULT),
takerFeeAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
});
const orders = [firstOrder, secondOrder];
await forwarderTestFactory.marketSellTestAsync(orders, 1.34, [erc20Token]);
});
it('should fail to fill an order with a percentage fee if the asset proxy is not yet approved', async () => {
const unapprovedAsset = assetDataUtils.encodeERC20AssetData(secondErc20Token.address);
const order = await orderFactory.newSignedOrderAsync({
makerAssetData: unapprovedAsset,
takerFee: Web3Wrapper.toBaseUnitAmount(2, DECIMALS_DEFAULT),
takerFeeAssetData: unapprovedAsset,
});
const ethValue = order.takerAssetAmount;
const erc20Balances = await erc20Wrapper.getBalancesAsync();
const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(takerAddress);
// Execute test case
const tx = await forwarderWrapper.marketSellOrdersWithEthAsync([order], {
value: ethValue,
from: takerAddress,
});
const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(takerAddress);
const forwarderEthBalance = await env.web3Wrapper.getBalanceInWeiAsync(forwarderContract.address);
const newBalances = await erc20Wrapper.getBalancesAsync();
const totalEthSpent = GAS_PRICE.times(tx.gasUsed);
// Validate test case
expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent));
expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[makerAddress][defaultMakerAssetAddress],
);
expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
erc20Balances[takerAddress][defaultMakerAssetAddress],
);
expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal(
erc20Balances[makerAddress][weth.address],
);
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 a single order with a WETH fee', async () => {
const orderWithWethFee = await orderFactory.newSignedOrderAsync({
takerFee: Web3Wrapper.toBaseUnitAmount(1, DECIMALS_DEFAULT),
takerFeeAssetData: wethAssetData,
});
await forwarderTestFactory.marketSellTestAsync([orderWithWethFee], 0.13, [erc20Token]);
});
it('should fill multiple orders with WETH fees', async () => {
const firstOrder = await orderFactory.newSignedOrderAsync({
takerFee: Web3Wrapper.toBaseUnitAmount(1, DECIMALS_DEFAULT),
takerFeeAssetData: wethAssetData,
});
const secondOrderWithWethFee = await orderFactory.newSignedOrderAsync({
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(97, DECIMALS_DEFAULT),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(33, DECIMALS_DEFAULT),
takerFee: Web3Wrapper.toBaseUnitAmount(2, DECIMALS_DEFAULT),
takerFeeAssetData: wethAssetData,
});
const orders = [firstOrder, secondOrderWithWethFee];
await forwarderTestFactory.marketSellTestAsync(orders, 1.25, [erc20Token]);
});
it('should refund remaining ETH if amount is greater than takerAssetAmount', async () => {
const order = await orderFactory.newSignedOrderAsync();
const ethValue = order.takerAssetAmount.plus(PROTOCOL_FEE).plus(2);
const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(takerAddress);
const tx = await forwarderWrapper.marketSellOrdersWithEthAsync([order], {
value: ethValue,
from: takerAddress,
});
const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(takerAddress);
const totalEthSpent = order.takerAssetAmount.plus(PROTOCOL_FEE).plus(GAS_PRICE.times(tx.gasUsed));
expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent));
});
it('should fill orders with different makerAssetData', async () => {
const firstOrderMakerAssetData = assetDataUtils.encodeERC20AssetData(erc20Token.address);
const firstOrder = await orderFactory.newSignedOrderAsync({
makerAssetData: firstOrderMakerAssetData,
});
const secondOrderMakerAssetData = assetDataUtils.encodeERC20AssetData(secondErc20Token.address);
const secondOrder = await orderFactory.newSignedOrderAsync({
makerAssetData: secondOrderMakerAssetData,
});
await forwarderWrapper.approveMakerAssetProxyAsync(secondOrderMakerAssetData, { from: takerAddress });
const orders = [firstOrder, secondOrder];
await forwarderTestFactory.marketSellTestAsync(orders, 1.5, [erc20Token, secondErc20Token]);
});
it('should fail to fill an order with a fee denominated in an asset other than makerAsset or WETH', async () => {
const makerAssetData = assetDataUtils.encodeERC20AssetData(erc20Token.address);
const takerFeeAssetData = assetDataUtils.encodeERC20AssetData(secondErc20Token.address);
const order = await orderFactory.newSignedOrderAsync({
makerAssetData,
takerFeeAssetData,
takerFee: Web3Wrapper.toBaseUnitAmount(1, DECIMALS_DEFAULT),
});
const revertError = new ForwarderRevertErrors.UnsupportedFeeError(takerFeeAssetData);
await forwarderTestFactory.marketSellTestAsync([order], 0.5, [erc20Token], {
revertError,
});
});
it('should fill a partially-filled order without a taker fee', async () => {
const order = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketSellTestAsync([order], 0.3, [erc20Token]);
await forwarderTestFactory.marketSellTestAsync([order], 0.8, [erc20Token]);
});
it('should skip over an order with an invalid maker asset amount', async () => {
const unfillableOrder = await orderFactory.newSignedOrderAsync({
makerAssetAmount: constants.ZERO_AMOUNT,
});
const fillableOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketSellTestAsync([unfillableOrder, fillableOrder], 1.5, [erc20Token]);
});
it('should skip over an order with an invalid taker asset amount', async () => {
const unfillableOrder = await orderFactory.newSignedOrderAsync({
takerAssetAmount: constants.ZERO_AMOUNT,
});
const fillableOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketSellTestAsync([unfillableOrder, fillableOrder], 1.5, [erc20Token]);
});
it('should skip over an expired order', async () => {
const currentTimestamp = await getLatestBlockTimestampAsync();
const expiredOrder = await orderFactory.newSignedOrderAsync({
expirationTimeSeconds: new BigNumber(currentTimestamp).minus(10),
});
const fillableOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketSellTestAsync([expiredOrder, fillableOrder], 1.5, [erc20Token]);
});
it('should skip over a fully filled order', async () => {
const fullyFilledOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketSellTestAsync([fullyFilledOrder], 1, [erc20Token]);
const fillableOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketSellTestAsync([fullyFilledOrder, fillableOrder], 1.5, [erc20Token]);
});
it('should skip over a cancelled order', async () => {
const cancelledOrder = await orderFactory.newSignedOrderAsync();
await exchangeWrapper.cancelOrderAsync(cancelledOrder, makerAddress);
const fillableOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketSellTestAsync([cancelledOrder, fillableOrder], 1.5, [erc20Token]);
});
});
blockchainTests.resets('marketSellOrdersWithEth with extra fees', () => {
it('should fill the order and send fee to feeRecipient', async () => {
const order = await orderFactory.newSignedOrderAsync({
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(157, DECIMALS_DEFAULT),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(36, DECIMALS_DEFAULT),
});
await forwarderTestFactory.marketSellTestAsync([order], 0.67, [erc20Token], {
forwarderFeePercentage: new BigNumber(2),
});
});
it('should fail if the fee is set too high', async () => {
const order = await orderFactory.newSignedOrderAsync();
const forwarderFeePercentage = new BigNumber(6);
const revertError = new ForwarderRevertErrors.FeePercentageTooLargeError(
ForwarderTestFactory.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, forwarderFeePercentage),
);
await forwarderTestFactory.marketSellTestAsync([order], 0.5, [erc20Token], {
forwarderFeePercentage,
revertError,
});
});
});
blockchainTests.resets('marketBuyOrdersWithEth without extra fees', () => {
it('should buy the exact amount of makerAsset in a single order', async () => {
const order = await orderFactory.newSignedOrderAsync({
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(131, DECIMALS_DEFAULT),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(20, DECIMALS_DEFAULT),
});
await forwarderTestFactory.marketBuyTestAsync([order], 0.62, [erc20Token]);
});
it('should buy the exact amount of makerAsset in multiple orders', async () => {
const firstOrder = await orderFactory.newSignedOrderAsync();
const secondOrder = await orderFactory.newSignedOrderAsync({
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(77, DECIMALS_DEFAULT),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(11, DECIMALS_DEFAULT),
});
const orders = [firstOrder, secondOrder];
await forwarderTestFactory.marketBuyTestAsync(orders, 1.96, [erc20Token]);
});
it('should buy exactly makerAssetBuyAmount in orders with different makerAssetData', async () => {
const firstOrderMakerAssetData = assetDataUtils.encodeERC20AssetData(erc20Token.address);
const firstOrder = await orderFactory.newSignedOrderAsync({
makerAssetData: firstOrderMakerAssetData,
});
const secondOrderMakerAssetData = assetDataUtils.encodeERC20AssetData(secondErc20Token.address);
const secondOrder = await orderFactory.newSignedOrderAsync({
makerAssetData: secondOrderMakerAssetData,
});
await forwarderWrapper.approveMakerAssetProxyAsync(secondOrderMakerAssetData, { from: takerAddress });
const orders = [firstOrder, secondOrder];
await forwarderTestFactory.marketBuyTestAsync(orders, 1.5, [erc20Token, secondErc20Token]);
});
it('should buy the exact amount of makerAsset and return excess ETH', async () => {
const order = await orderFactory.newSignedOrderAsync({
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(80, DECIMALS_DEFAULT),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(17, DECIMALS_DEFAULT),
});
await forwarderTestFactory.marketBuyTestAsync([order], 0.57, [erc20Token], {
ethValueAdjustment: 2,
});
});
it('should buy the exact amount of makerAsset from a single order with a WETH fee', async () => {
const order = await orderFactory.newSignedOrderAsync({
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(79, DECIMALS_DEFAULT),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(16, DECIMALS_DEFAULT),
takerFee: Web3Wrapper.toBaseUnitAmount(1, DECIMALS_DEFAULT),
takerFeeAssetData: wethAssetData,
});
await forwarderTestFactory.marketBuyTestAsync([order], 0.38, [erc20Token]);
});
it('should buy the exact amount of makerAsset from a single order with a percentage fee', async () => {
const order = await orderFactory.newSignedOrderAsync({
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(80, DECIMALS_DEFAULT),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(17, DECIMALS_DEFAULT),
takerFee: Web3Wrapper.toBaseUnitAmount(1, DECIMALS_DEFAULT),
takerFeeAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
});
await forwarderTestFactory.marketBuyTestAsync([order], 0.52, [erc20Token]);
});
it('should revert if the amount of ETH sent is too low to fill the makerAssetAmount', async () => {
const order = await orderFactory.newSignedOrderAsync();
const revertError = new ForwarderRevertErrors.CompleteBuyFailedError(
order.makerAssetAmount.times(0.5),
constants.ZERO_AMOUNT,
);
await forwarderTestFactory.marketBuyTestAsync([order], 0.5, [erc20Token], {
ethValueAdjustment: -2,
revertError,
});
});
it('should buy an ERC721 asset from a single order', async () => {
const makerAssetId = erc721MakerAssetIds[0];
const erc721Order = await orderFactory.newSignedOrderAsync({
makerAssetAmount: new BigNumber(1),
makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
takerFeeAssetData: wethAssetData,
});
await forwarderTestFactory.marketBuyTestAsync([erc721Order], 1, [erc721Token], {
makerAssetId,
});
});
it('should buy an ERC721 asset and pay a WETH fee', async () => {
const makerAssetId = erc721MakerAssetIds[0];
const erc721orderWithWethFee = await orderFactory.newSignedOrderAsync({
makerAssetAmount: new BigNumber(1),
makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
takerFee: Web3Wrapper.toBaseUnitAmount(1, DECIMALS_DEFAULT),
takerFeeAssetData: wethAssetData,
});
await forwarderTestFactory.marketBuyTestAsync([erc721orderWithWethFee], 1, [erc721Token], {
makerAssetId,
});
});
it('should fail to fill an order with a fee denominated in an asset other than makerAsset or WETH', async () => {
const makerAssetData = assetDataUtils.encodeERC20AssetData(erc20Token.address);
const takerFeeAssetData = assetDataUtils.encodeERC20AssetData(secondErc20Token.address);
const order = await orderFactory.newSignedOrderAsync({
makerAssetData,
takerFeeAssetData,
takerFee: Web3Wrapper.toBaseUnitAmount(1, DECIMALS_DEFAULT),
});
const revertError = new ForwarderRevertErrors.UnsupportedFeeError(takerFeeAssetData);
await forwarderTestFactory.marketBuyTestAsync([order], 0.5, [erc20Token], {
revertError,
});
});
it('should fill a partially-filled order without a taker fee', async () => {
const order = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketBuyTestAsync([order], 0.3, [erc20Token]);
await forwarderTestFactory.marketBuyTestAsync([order], 0.8, [erc20Token]);
});
it('should skip over an order with an invalid maker asset amount', async () => {
const unfillableOrder = await orderFactory.newSignedOrderAsync({
makerAssetAmount: constants.ZERO_AMOUNT,
});
const fillableOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketBuyTestAsync([unfillableOrder, fillableOrder], 1.5, [erc20Token]);
});
it('should skip over an order with an invalid taker asset amount', async () => {
const unfillableOrder = await orderFactory.newSignedOrderAsync({
takerAssetAmount: constants.ZERO_AMOUNT,
});
const fillableOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketBuyTestAsync([unfillableOrder, fillableOrder], 1.5, [erc20Token]);
});
it('should skip over an expired order', async () => {
const currentTimestamp = await getLatestBlockTimestampAsync();
const expiredOrder = await orderFactory.newSignedOrderAsync({
expirationTimeSeconds: new BigNumber(currentTimestamp).minus(10),
});
const fillableOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketBuyTestAsync([expiredOrder, fillableOrder], 1.5, [erc20Token]);
});
it('should skip over a fully filled order', async () => {
const fullyFilledOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketBuyTestAsync([fullyFilledOrder], 1, [erc20Token]);
const fillableOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketBuyTestAsync([fullyFilledOrder, fillableOrder], 1.5, [erc20Token]);
});
it('should skip over a cancelled order', async () => {
const cancelledOrder = await orderFactory.newSignedOrderAsync();
await exchangeWrapper.cancelOrderAsync(cancelledOrder, makerAddress);
const fillableOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketBuyTestAsync([cancelledOrder, fillableOrder], 1.5, [erc20Token]);
});
it('Should buy slightly greater makerAsset when exchange rate is rounded', async () => {
// The 0x Protocol contracts round the exchange rate in favor of the Maker.
// In this case, the taker must round up how much they're going to spend, which
// in turn increases the amount of MakerAsset being purchased.
// Example:
// The taker wants to buy 5 units of the MakerAsset at a rate of 3M/2T.
// For every 2 units of TakerAsset, the taker will receive 3 units of MakerAsset.
// To purchase 5 units, the taker must spend 10/3 = 3.33 units of TakerAssset.
// However, the Taker can only spend whole units.
// Spending floor(10/3) = 3 units will yield a profit of Floor(3*3/2) = Floor(4.5) = 4 units of MakerAsset.
// Spending ceil(10/3) = 4 units will yield a profit of Floor(4*3/2) = 6 units of MakerAsset.
//
// The forwarding contract will opt for the second option, which overbuys, to ensure the taker
// receives at least the amount of MakerAsset they requested.
//
// Construct test case using values from example above
const order = await orderFactory.newSignedOrderAsync({
makerAssetAmount: new BigNumber('30'),
takerAssetAmount: new BigNumber('20'),
makerAssetData: assetDataUtils.encodeERC20AssetData(erc20Token.address),
takerAssetData: assetDataUtils.encodeERC20AssetData(weth.address),
makerFee: new BigNumber(0),
takerFee: new BigNumber(0),
});
const desiredMakerAssetFillAmount = new BigNumber('5');
const makerAssetFillAmount = new BigNumber('6');
const primaryTakerAssetFillAmount = new BigNumber('4');
const ethValue = primaryTakerAssetFillAmount.plus(PROTOCOL_FEE);
const erc20Balances = await erc20Wrapper.getBalancesAsync();
const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(takerAddress);
// Execute test case
const tx = await forwarderWrapper.marketBuyOrdersWithEthAsync([order], desiredMakerAssetFillAmount, {
value: ethValue,
from: takerAddress,
});
// Fetch end balances and construct expected outputs
const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(takerAddress);
const forwarderEthBalance = await env.web3Wrapper.getBalanceInWeiAsync(forwarderContract.address);
const newBalances = await erc20Wrapper.getBalancesAsync();
const totalEthSpent = ethValue.plus(GAS_PRICE.times(tx.gasUsed));
// Validate test case
expect(makerAssetFillAmount).to.be.bignumber.greaterThan(desiredMakerAssetFillAmount);
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 slightly greater MakerAsset when exchange rate is rounded (Regression Test)', async () => {
// Disable protocol fees for regression test
await exchangeContract.setProtocolFeeCollectorAddress.awaitTransactionSuccessAsync(constants.NULL_ADDRESS);
// Order taken from a transaction on mainnet that failed due to a rounding error.
const order = await orderFactory.newSignedOrderAsync({
makerAssetAmount: new BigNumber('268166666666666666666'),
takerAssetAmount: new BigNumber('219090625878836371'),
makerAssetData: assetDataUtils.encodeERC20AssetData(erc20Token.address),
takerAssetData: assetDataUtils.encodeERC20AssetData(weth.address),
makerFee: new BigNumber(0),
takerFee: new BigNumber(0),
});
const erc20Balances = await erc20Wrapper.getBalancesAsync();
const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(takerAddress);
// The taker will receive more than the desired amount of makerAsset due to rounding
const desiredMakerAssetFillAmount = new BigNumber('5000000000000000000');
const ethValue = new BigNumber('4084971271824171');
const makerAssetFillAmount = ethValue
.times(order.makerAssetAmount)
.dividedToIntegerBy(order.takerAssetAmount);
// Execute test case
const tx = await forwarderWrapper.marketBuyOrdersWithEthAsync([order], desiredMakerAssetFillAmount, {
value: ethValue,
from: takerAddress,
});
// Fetch end balances and construct expected outputs
const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(takerAddress);
const forwarderEthBalance = await env.web3Wrapper.getBalanceInWeiAsync(forwarderContract.address);
const newBalances = await erc20Wrapper.getBalancesAsync();
const primaryTakerAssetFillAmount = ethValue;
const totalEthSpent = primaryTakerAssetFillAmount.plus(GAS_PRICE.times(tx.gasUsed));
// Validate test case
expect(makerAssetFillAmount).to.be.bignumber.greaterThan(desiredMakerAssetFillAmount);
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);
});
});
blockchainTests.resets('marketBuyOrdersWithEth with extra fees', () => {
it('should buy the asset and send fee to feeRecipient', async () => {
const order = await orderFactory.newSignedOrderAsync({
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(125, DECIMALS_DEFAULT),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(11, DECIMALS_DEFAULT),
});
await forwarderTestFactory.marketBuyTestAsync([order], 0.33, [erc20Token], {
forwarderFeePercentage: new BigNumber(2),
});
});
it('should fail if the fee is set too high', async () => {
const order = await orderFactory.newSignedOrderAsync();
const revertError = new ForwarderRevertErrors.FeePercentageTooLargeError(
ForwarderTestFactory.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, new BigNumber(6)),
);
await forwarderTestFactory.marketBuyTestAsync([order], 0.5, [erc20Token], {
forwarderFeePercentage: new BigNumber(6),
revertError,
});
});
it('should fail if there is not enough ETH remaining to pay the fee', async () => {
const order = await orderFactory.newSignedOrderAsync();
const forwarderFeePercentage = new BigNumber(2);
const ethFee = ForwarderTestFactory.getPercentageOfValue(
order.takerAssetAmount.times(0.5).plus(PROTOCOL_FEE),
forwarderFeePercentage,
);
const revertError = new ForwarderRevertErrors.InsufficientEthForFeeError(ethFee, ethFee.minus(1));
// -2 to compensate for the extra 1 wei added in ForwarderTestFactory to account for rounding
await forwarderTestFactory.marketBuyTestAsync([order], 0.5, [erc20Token], {
ethValueAdjustment: -2,
forwarderFeePercentage,
revertError,
});
});
});
});
// tslint:disable:max-file-line-count
// tslint:enable:no-unnecessary-type-assertion

View File

@ -1,19 +0,0 @@
import { env, EnvVars } from '@0x/dev-utils';
import { coverage, profiler, provider } from '@0x/contracts-test-utils';
import { providerUtils } from '@0x/utils';
before('start web3 provider', () => {
providerUtils.startProviderEngine(provider);
});
after('generate coverage report', async () => {
if (env.parseBoolean(EnvVars.SolidityCoverage)) {
const coverageSubprovider = coverage.getCoverageSubproviderSingleton();
await coverageSubprovider.writeCoverageAsync();
}
if (env.parseBoolean(EnvVars.SolidityProfiler)) {
const profilerSubprovider = profiler.getProfilerSubproviderSingleton();
await profilerSubprovider.writeProfilerOutputAsync();
}
provider.stop();
});

View File

@ -1,415 +0,0 @@
import { ERC20Wrapper } from '@0x/contracts-asset-proxy';
import { DummyERC20TokenContract } from '@0x/contracts-erc20';
import { DummyERC721TokenContract } from '@0x/contracts-erc721';
import { ExchangeWrapper } from '@0x/contracts-exchange';
import { constants, ERC20BalancesByOwner, expect, OrderStatus, web3Wrapper } from '@0x/contracts-test-utils';
import { assetDataUtils } from '@0x/order-utils';
import { OrderInfo, SignedOrder } from '@0x/types';
import { BigNumber, RevertError } from '@0x/utils';
import * as _ from 'lodash';
import { ForwarderWrapper } from './forwarder_wrapper';
// Necessary bookkeeping to validate Forwarder results
interface ForwarderFillState {
takerAssetFillAmount: BigNumber;
makerAssetFillAmount: {
[makerAssetData: string]: BigNumber;
};
protocolFees: BigNumber;
wethFees: BigNumber;
percentageFees: {
[makerAssetData: string]: BigNumber;
};
maxOversoldWeth: BigNumber;
maxOverboughtMakerAsset: BigNumber;
}
// Since bignumber is not compatible with chai's within
function expectBalanceWithin(balance: BigNumber, low: BigNumber, high: BigNumber, message?: string): void {
expect(balance, message).to.be.bignumber.gte(low);
expect(balance, message).to.be.bignumber.lte(high);
}
export class ForwarderTestFactory {
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(
private readonly _exchangeWrapper: ExchangeWrapper,
private readonly _forwarderWrapper: ForwarderWrapper,
private readonly _erc20Wrapper: ERC20Wrapper,
private readonly _forwarderAddress: string,
private readonly _makerAddress: string,
private readonly _takerAddress: string,
private readonly _protocolFeeCollectorAddress: string,
private readonly _orderFeeRecipientAddress: string,
private readonly _forwarderFeeRecipientAddress: string,
private readonly _wethAddress: string,
private readonly _gasPrice: BigNumber,
private readonly _protocolFeeMultiplier: BigNumber,
) {}
public async marketBuyTestAsync(
orders: SignedOrder[],
fractionalNumberOfOrdersToFill: number,
makerAssetContracts: Array<DummyERC20TokenContract | DummyERC721TokenContract>,
options: {
ethValueAdjustment?: number; // Used to provided insufficient/excess ETH
forwarderFeePercentage?: BigNumber;
makerAssetId?: BigNumber;
revertError?: RevertError;
} = {},
): Promise<void> {
const ethValueAdjustment = options.ethValueAdjustment || 0;
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 ordersInfoBefore = await Promise.all(orders.map(order => this._exchangeWrapper.getOrderInfoAsync(order)));
const orderStatusesBefore = ordersInfoBefore.map(orderInfo => orderInfo.orderStatus);
const expectedResults = this._computeExpectedResults(orders, ordersInfoBefore, fractionalNumberOfOrdersToFill);
const wethSpent = expectedResults.takerAssetFillAmount
.plus(expectedResults.protocolFees)
.plus(expectedResults.wethFees)
.plus(expectedResults.maxOversoldWeth);
const ethSpentOnForwarderFee = ForwarderTestFactory.getPercentageOfValue(wethSpent, forwarderFeePercentage);
const ethValue = wethSpent.plus(ethSpentOnForwarderFee).plus(ethValueAdjustment);
const feePercentage = ForwarderTestFactory.getPercentageOfValue(
constants.PERCENTAGE_DENOMINATOR,
forwarderFeePercentage,
);
const totalMakerAssetFillAmount = Object.values(expectedResults.makerAssetFillAmount).reduce((prev, current) =>
prev.plus(current),
);
const totalPercentageFees = Object.values(expectedResults.percentageFees).reduce((prev, current) =>
prev.plus(current),
);
const tx = this._forwarderWrapper.marketBuyOrdersWithEthAsync(
orders,
totalMakerAssetFillAmount.minus(totalPercentageFees),
{
value: ethValue,
from: this._takerAddress,
},
{ feePercentage, feeRecipient: this._forwarderFeeRecipientAddress },
);
if (options.revertError !== undefined) {
await expect(tx).to.revertWith(options.revertError);
} else {
const gasUsed = (await tx).gasUsed;
const ordersInfoAfter = await Promise.all(
orders.map(order => this._exchangeWrapper.getOrderInfoAsync(order)),
);
const orderStatusesAfter = ordersInfoAfter.map(orderInfo => orderInfo.orderStatus);
await this._checkResultsAsync(
fractionalNumberOfOrdersToFill,
orderStatusesBefore,
orderStatusesAfter,
gasUsed,
expectedResults,
takerEthBalanceBefore,
erc20Balances,
makerAssetContracts,
{
forwarderFeePercentage,
forwarderFeeRecipientEthBalanceBefore,
makerAssetId: options.makerAssetId,
},
);
}
}
public async marketSellTestAsync(
orders: SignedOrder[],
fractionalNumberOfOrdersToFill: number,
makerAssetContracts: DummyERC20TokenContract[],
options: {
forwarderFeePercentage?: BigNumber;
revertError?: RevertError;
} = {},
): Promise<void> {
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 ordersInfoBefore = await Promise.all(orders.map(order => this._exchangeWrapper.getOrderInfoAsync(order)));
const orderStatusesBefore = ordersInfoBefore.map(orderInfo => orderInfo.orderStatus);
const expectedResults = this._computeExpectedResults(orders, ordersInfoBefore, fractionalNumberOfOrdersToFill);
const wethSpent = expectedResults.takerAssetFillAmount
.plus(expectedResults.protocolFees)
.plus(expectedResults.wethFees)
.plus(expectedResults.maxOversoldWeth);
const ethSpentOnForwarderFee = ForwarderTestFactory.getPercentageOfValue(wethSpent, forwarderFeePercentage);
const ethValue = wethSpent.plus(ethSpentOnForwarderFee);
const feePercentage = ForwarderTestFactory.getPercentageOfValue(
constants.PERCENTAGE_DENOMINATOR,
forwarderFeePercentage,
);
const tx = this._forwarderWrapper.marketSellOrdersWithEthAsync(
orders,
{
value: ethValue,
from: this._takerAddress,
},
{ feePercentage, feeRecipient: this._forwarderFeeRecipientAddress },
);
if (options.revertError !== undefined) {
await expect(tx).to.revertWith(options.revertError);
} else {
const gasUsed = (await tx).gasUsed;
const orderStatusesAfter = await Promise.all(
orders.map(async order => (await this._exchangeWrapper.getOrderInfoAsync(order)).orderStatus),
);
await this._checkResultsAsync(
fractionalNumberOfOrdersToFill,
orderStatusesBefore,
orderStatusesAfter,
gasUsed,
expectedResults,
takerEthBalanceBefore,
erc20Balances,
makerAssetContracts,
{
forwarderFeePercentage,
forwarderFeeRecipientEthBalanceBefore,
},
);
}
}
private _checkErc20Balances(
oldBalances: ERC20BalancesByOwner,
newBalances: ERC20BalancesByOwner,
expectedResults: ForwarderFillState,
makerAssetContract: DummyERC20TokenContract,
): void {
const makerAssetAddress = makerAssetContract.address;
const makerAssetData = assetDataUtils.encodeERC20AssetData(makerAssetAddress);
const {
maxOverboughtMakerAsset,
makerAssetFillAmount: { [makerAssetData]: makerAssetFillAmount },
percentageFees: { [makerAssetData]: percentageFees },
} = expectedResults;
expectBalanceWithin(
newBalances[this._makerAddress][makerAssetAddress],
oldBalances[this._makerAddress][makerAssetAddress]
.minus(makerAssetFillAmount)
.minus(maxOverboughtMakerAsset),
oldBalances[this._makerAddress][makerAssetAddress].minus(makerAssetFillAmount),
'Maker makerAsset balance',
);
expectBalanceWithin(
newBalances[this._takerAddress][makerAssetAddress],
oldBalances[this._takerAddress][makerAssetAddress].plus(makerAssetFillAmount).minus(percentageFees),
oldBalances[this._takerAddress][makerAssetAddress]
.plus(makerAssetFillAmount)
.minus(percentageFees)
.plus(maxOverboughtMakerAsset),
'Taker makerAsset balance',
);
expect(
newBalances[this._orderFeeRecipientAddress][makerAssetAddress],
'Order fee recipient makerAsset balance',
).to.be.bignumber.equal(oldBalances[this._orderFeeRecipientAddress][makerAssetAddress].plus(percentageFees));
expect(
newBalances[this._forwarderAddress][makerAssetAddress],
'Forwarder contract makerAsset balance',
).to.be.bignumber.equal(constants.ZERO_AMOUNT);
}
private async _checkResultsAsync(
fractionalNumberOfOrdersToFill: number,
orderStatusesBefore: OrderStatus[],
orderStatusesAfter: OrderStatus[],
gasUsed: number,
expectedResults: ForwarderFillState,
takerEthBalanceBefore: BigNumber,
erc20Balances: ERC20BalancesByOwner,
makerAssetContracts: Array<DummyERC20TokenContract | DummyERC721TokenContract>,
options: {
forwarderFeePercentage?: BigNumber;
forwarderFeeRecipientEthBalanceBefore?: BigNumber;
makerAssetId?: BigNumber;
} = {},
): Promise<void> {
for (const [i, orderStatus] of orderStatusesAfter.entries()) {
let expectedOrderStatus = orderStatusesBefore[i];
if (fractionalNumberOfOrdersToFill >= i + 1 && orderStatusesBefore[i] === OrderStatus.Fillable) {
expectedOrderStatus = OrderStatus.FullyFilled;
}
expect(orderStatus, ` Order ${i} status`).to.equal(expectedOrderStatus);
}
const wethSpent = expectedResults.takerAssetFillAmount
.plus(expectedResults.protocolFees)
.plus(expectedResults.wethFees);
const ethSpentOnForwarderFee = ForwarderTestFactory.getPercentageOfValue(
wethSpent,
options.forwarderFeePercentage || constants.ZERO_AMOUNT,
);
const totalEthSpent = wethSpent.plus(ethSpentOnForwarderFee).plus(this._gasPrice.times(gasUsed));
const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(this._takerAddress);
const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(this._forwarderAddress);
const newBalances = await this._erc20Wrapper.getBalancesAsync();
expectBalanceWithin(
takerEthBalanceAfter,
takerEthBalanceBefore.minus(totalEthSpent).minus(expectedResults.maxOversoldWeth),
takerEthBalanceBefore.minus(totalEthSpent),
'Taker ETH balance',
);
if (options.forwarderFeeRecipientEthBalanceBefore !== undefined) {
const fowarderFeeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(
this._forwarderFeeRecipientAddress,
);
expect(fowarderFeeRecipientEthBalanceAfter, 'Forwarder fee recipient ETH balance').to.be.bignumber.equal(
options.forwarderFeeRecipientEthBalanceBefore.plus(ethSpentOnForwarderFee),
);
}
for (const makerAssetContract of makerAssetContracts) {
if (makerAssetContract instanceof DummyERC20TokenContract) {
this._checkErc20Balances(erc20Balances, newBalances, expectedResults, makerAssetContract);
} else if (options.makerAssetId !== undefined) {
const newOwner = await makerAssetContract.ownerOf.callAsync(options.makerAssetId);
expect(newOwner, 'New ERC721 owner').to.be.bignumber.equal(this._takerAddress);
}
}
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),
'Maker WETH balance',
);
expect(
newBalances[this._orderFeeRecipientAddress][this._wethAddress],
'Order fee recipient WETH balance',
).to.be.bignumber.equal(
erc20Balances[this._orderFeeRecipientAddress][this._wethAddress].plus(expectedResults.wethFees),
);
expect(
newBalances[this._forwarderAddress][this._wethAddress],
'Forwarder contract WETH balance',
).to.be.bignumber.equal(constants.ZERO_AMOUNT);
expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT);
}
// Simulates filling some orders via the Forwarder contract. For example, if
// orders = [A, B, C, D] and fractionalNumberOfOrdersToFill = 2.3, then
// we simulate A and B being completely filled, and 0.3 * C being filled.
private _computeExpectedResults(
orders: SignedOrder[],
ordersInfoBefore: OrderInfo[],
fractionalNumberOfOrdersToFill: number,
): ForwarderFillState {
const currentState: ForwarderFillState = {
takerAssetFillAmount: constants.ZERO_AMOUNT,
makerAssetFillAmount: {},
protocolFees: constants.ZERO_AMOUNT,
wethFees: constants.ZERO_AMOUNT,
percentageFees: {},
maxOversoldWeth: constants.ZERO_AMOUNT,
maxOverboughtMakerAsset: constants.ZERO_AMOUNT,
};
let remainingOrdersToFill = fractionalNumberOfOrdersToFill;
for (const [i, order] of orders.entries()) {
if (currentState.makerAssetFillAmount[order.makerAssetData] === undefined) {
currentState.makerAssetFillAmount[order.makerAssetData] = new BigNumber(0);
}
if (currentState.percentageFees[order.makerAssetData] === undefined) {
currentState.percentageFees[order.makerAssetData] = new BigNumber(0);
}
if (remainingOrdersToFill === 0) {
break;
}
if (ordersInfoBefore[i].orderStatus !== OrderStatus.Fillable) {
// If the order is not fillable, skip over it but still count it towards fractionalNumberOfOrdersToFill
remainingOrdersToFill = Math.max(remainingOrdersToFill - 1, 0);
continue;
}
let makerAssetAmount;
let takerAssetAmount;
let takerFee;
if (remainingOrdersToFill < 1) {
makerAssetAmount = order.makerAssetAmount.times(remainingOrdersToFill).integerValue();
takerAssetAmount = order.takerAssetAmount.times(remainingOrdersToFill).integerValue();
takerFee = order.takerFee.times(remainingOrdersToFill).integerValue();
// Up to 1 wei worth of WETH will be oversold on the last order due to rounding
currentState.maxOversoldWeth = new BigNumber(1);
// Equivalently, up to 1 wei worth of maker asset will be overbought
currentState.maxOverboughtMakerAsset = currentState.maxOversoldWeth
.times(order.makerAssetAmount)
.dividedToIntegerBy(order.takerAssetAmount);
} else {
makerAssetAmount = order.makerAssetAmount;
takerAssetAmount = order.takerAssetAmount;
takerFee = order.takerFee;
}
// Accounting for partially filled orders
// As with unfillable orders, these still count as 1 towards fractionalNumberOfOrdersToFill
const takerAssetFilled = ordersInfoBefore[i].orderTakerAssetFilledAmount;
const makerAssetFilled = takerAssetFilled
.times(order.makerAssetAmount)
.dividedToIntegerBy(order.takerAssetAmount);
takerAssetAmount = BigNumber.max(takerAssetAmount.minus(takerAssetFilled), constants.ZERO_AMOUNT);
makerAssetAmount = BigNumber.max(makerAssetAmount.minus(makerAssetFilled), constants.ZERO_AMOUNT);
currentState.takerAssetFillAmount = currentState.takerAssetFillAmount.plus(takerAssetAmount);
currentState.makerAssetFillAmount[order.makerAssetData] = currentState.makerAssetFillAmount[
order.makerAssetData
].plus(makerAssetAmount);
if (this._protocolFeeCollectorAddress !== constants.NULL_ADDRESS) {
currentState.protocolFees = currentState.protocolFees.plus(
this._gasPrice.times(this._protocolFeeMultiplier),
);
}
if (order.takerFeeAssetData === order.makerAssetData) {
currentState.percentageFees[order.makerAssetData] = currentState.percentageFees[
order.makerAssetData
].plus(takerFee);
} else if (order.takerFeeAssetData === order.takerAssetData) {
currentState.wethFees = currentState.wethFees.plus(takerFee);
}
remainingOrdersToFill = Math.max(remainingOrdersToFill - 1, 0);
}
return currentState;
}
}

View File

@ -1,81 +0,0 @@
import { artifacts as erc20Artifacts } from '@0x/contracts-erc20';
import { artifacts as erc721Artifacts } from '@0x/contracts-erc721';
import { artifacts as exchangeArtifacts } from '@0x/contracts-exchange';
import { constants, LogDecoder, Web3ProviderEngine } from '@0x/contracts-test-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { TransactionReceiptWithDecodedLogs, TxDataPayable } from 'ethereum-types';
import * as _ from 'lodash';
import { ForwarderContract } from '../../generated-wrappers/forwarder';
import { artifacts } from '../../src/artifacts';
export class ForwarderWrapper {
private readonly _web3Wrapper: Web3Wrapper;
private readonly _forwarderContract: ForwarderContract;
private readonly _logDecoder: LogDecoder;
constructor(contractInstance: ForwarderContract, provider: Web3ProviderEngine) {
this._forwarderContract = contractInstance;
this._web3Wrapper = new Web3Wrapper(provider);
this._logDecoder = new LogDecoder(this._web3Wrapper, {
...artifacts,
...exchangeArtifacts,
...erc20Artifacts,
...erc721Artifacts,
});
}
public async marketSellOrdersWithEthAsync(
orders: SignedOrder[],
txData: TxDataPayable,
opts: { feePercentage?: BigNumber; feeRecipient?: string } = {},
): Promise<TransactionReceiptWithDecodedLogs> {
const feePercentage = opts.feePercentage === undefined ? constants.ZERO_AMOUNT : opts.feePercentage;
const feeRecipient = opts.feeRecipient === undefined ? constants.NULL_ADDRESS : opts.feeRecipient;
const txHash = await this._forwarderContract.marketSellOrdersWithEth.sendTransactionAsync(
orders,
orders.map(signedOrder => signedOrder.signature),
feePercentage,
feeRecipient,
txData,
);
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
public async marketBuyOrdersWithEthAsync(
orders: SignedOrder[],
makerAssetFillAmount: BigNumber,
txData: TxDataPayable,
opts: { feePercentage?: BigNumber; feeRecipient?: string } = {},
): Promise<TransactionReceiptWithDecodedLogs> {
const feePercentage = opts.feePercentage === undefined ? constants.ZERO_AMOUNT : opts.feePercentage;
const feeRecipient = opts.feeRecipient === undefined ? constants.NULL_ADDRESS : opts.feeRecipient;
const txHash = await this._forwarderContract.marketBuyOrdersWithEth.sendTransactionAsync(
orders,
makerAssetFillAmount,
orders.map(signedOrder => signedOrder.signature),
feePercentage,
feeRecipient,
txData,
);
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
public async withdrawAssetAsync(
assetData: string,
amount: BigNumber,
txData: TxDataPayable,
): Promise<TransactionReceiptWithDecodedLogs> {
const txHash = await this._forwarderContract.withdrawAsset.sendTransactionAsync(assetData, amount, txData);
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
public async approveMakerAssetProxyAsync(
assetData: string,
txData: TxDataPayable,
): Promise<TransactionReceiptWithDecodedLogs> {
const txHash = await this._forwarderContract.approveMakerAssetProxy.sendTransactionAsync(assetData, txData);
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
}

View File

@ -1,2 +0,0 @@
export * from './forwarder_wrapper';
export * from './forwarder_test_factory';

View File

@ -41,6 +41,18 @@ export class LocalBalanceStore extends BalanceStore {
this._balances.eth[senderAddress] = this._balances.eth[senderAddress].minus(amount);
}
/**
* Converts some amount of the ETH balance of an address to WETH balance to simulate wrapping ETH.
* @param senderAddress Address whose ETH to wrap.
* @param amount Amount to wrap.
*/
public wrapEth(senderAddress: string, wethAddress: string, amount: Numberish): void {
this._balances.eth[senderAddress] = this._balances.eth[senderAddress].minus(amount);
_.update(this._balances.erc20, [senderAddress, wethAddress], balance =>
(balance || constants.ZERO_AMOUNT).plus(amount),
);
}
/**
* Sends ETH from `fromAddress` to `toAddress`.
* @param fromAddress Sender of ETH.

View File

@ -1,7 +1,9 @@
import { DummyERC20TokenContract, WETH9Contract } from '@0x/contracts-erc20';
import { constants, TransactionFactory } from '@0x/contracts-test-utils';
import { DummyERC721TokenContract } from '@0x/contracts-erc721';
import { constants, getRandomInteger, TransactionFactory } from '@0x/contracts-test-utils';
import { SignatureType, SignedZeroExTransaction, ZeroExTransaction } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { DeploymentManager } from '../utils/deployment_manager';
@ -62,6 +64,34 @@ export class Actor {
);
}
/**
* Mints some number of ERC721 NFTs and approves a spender (defaults to the ERC721 asset proxy)
* to transfer the token.
*/
public async configureERC721TokenAsync(
token: DummyERC721TokenContract,
spender?: string,
numToMint?: number,
): Promise<BigNumber[]> {
const tokenIds: BigNumber[] = [];
_.times(numToMint || 1, async () => {
const tokenId = getRandomInteger(constants.ZERO_AMOUNT, constants.MAX_UINT256);
await token.mint.awaitTransactionSuccessAsync(this.address, tokenId, {
from: this.address,
});
tokenIds.push(tokenId);
});
await token.setApprovalForAll.awaitTransactionSuccessAsync(
spender || this.deployment.assetProxies.erc721Proxy.address,
true,
{
from: this.address,
},
);
return tokenIds;
}
/**
* Signs a transaction.
*/

View File

@ -1,4 +1,4 @@
import { constants, OrderFactory } from '@0x/contracts-test-utils';
import { constants, OrderFactory, orderUtils } from '@0x/contracts-test-utils';
import { Order, SignedOrder } from '@0x/types';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
@ -41,6 +41,16 @@ export function MakerMixin<TBase extends Constructor>(Base: TBase) {
return this.orderFactory.newSignedOrderAsync(customOrderParams);
}
/**
* Cancels one of the maker's orders.
*/
public async cancelOrderAsync(order: SignedOrder): Promise<TransactionReceiptWithDecodedLogs> {
const params = orderUtils.createCancel(order);
return this.actor.deployment.exchange.cancelOrder.awaitTransactionSuccessAsync(params.order, {
from: this.actor.address,
});
}
/**
* Joins the staking pool specified by the given ID.
*/

View File

@ -21,7 +21,7 @@ import { deployCoordinatorAsync } from './deploy_coordinator';
import { DeploymentManager } from '../utils/deployment_manager';
// tslint:disable:no-unnecessary-type-assertion
blockchainTests.resets('Coordinator tests', env => {
blockchainTests.resets('Coordinator integration tests', env => {
let deployment: DeploymentManager;
let coordinator: CoordinatorContract;
let balanceStore: BlockchainBalanceStore;

View File

@ -0,0 +1,20 @@
import { artifacts as exchangeArtifacts } from '@0x/contracts-exchange';
import { artifacts, ForwarderContract } from '@0x/contracts-exchange-forwarder';
import { BlockchainTestsEnvironment } from '@0x/contracts-test-utils';
import { assetDataUtils } from '@0x/order-utils';
import { DeploymentManager } from '../utils/deployment_manager';
export async function deployForwarderAsync(
deployment: DeploymentManager,
environment: BlockchainTestsEnvironment,
): Promise<ForwarderContract> {
return await ForwarderContract.deployFrom0xArtifactAsync(
artifacts.Forwarder,
environment.provider,
deployment.txDefaults,
{ ...exchangeArtifacts, ...artifacts },
deployment.exchange.address,
assetDataUtils.encodeERC20AssetData(deployment.tokens.weth.address),
);
}

View File

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

View File

@ -0,0 +1,269 @@
import { BlockchainBalanceStore, LocalBalanceStore } from '@0x/contracts-exchange';
import { ForwarderContract } from '@0x/contracts-exchange-forwarder';
import { constants, expect, getPercentageOfValue, OrderStatus } from '@0x/contracts-test-utils';
import { OrderInfo, SignedOrder } from '@0x/types';
import { BigNumber, RevertError } from '@0x/utils';
import * as _ from 'lodash';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import { Actor, FeeRecipient, Maker } from '../actors';
import { DeploymentManager } from '../utils/deployment_manager';
// Necessary bookkeeping to validate Forwarder results
interface ForwarderFillState {
balances: LocalBalanceStore;
wethSpentAmount: BigNumber;
makerAssetAcquiredAmount: BigNumber;
}
interface MarketSellOptions {
forwarderFeePercentage: BigNumber;
revertError: RevertError;
}
interface MarketBuyOptions extends MarketSellOptions {
ethValueAdjustment: number; // Used to provided insufficient/excess ETH
}
export class ForwarderTestFactory {
constructor(
private readonly _forwarder: ForwarderContract,
private readonly _deployment: DeploymentManager,
private readonly _balanceStore: BlockchainBalanceStore,
private readonly _maker: Maker,
private readonly _taker: Actor,
private readonly _orderFeeRecipient: FeeRecipient,
private readonly _forwarderFeeRecipient: FeeRecipient,
) {}
public async marketBuyTestAsync(
orders: SignedOrder[],
fractionalNumberOfOrdersToFill: number,
options: Partial<MarketBuyOptions> = {},
): Promise<void> {
const ethValueAdjustment = options.ethValueAdjustment || 0;
const forwarderFeePercentage = options.forwarderFeePercentage || 0;
const orderInfoBefore = await Promise.all(
orders.map(order => this._deployment.exchange.getOrderInfo.callAsync(order)),
);
const expectedOrderStatuses = orderInfoBefore.map((orderInfo, i) =>
fractionalNumberOfOrdersToFill >= i + 1 && orderInfo.orderStatus === OrderStatus.Fillable
? OrderStatus.FullyFilled
: orderInfo.orderStatus,
);
const {
balances: expectedBalances,
wethSpentAmount,
makerAssetAcquiredAmount,
} = await this._simulateForwarderFillAsync(orders, orderInfoBefore, fractionalNumberOfOrdersToFill, options);
const ethSpentOnForwarderFee = getPercentageOfValue(wethSpentAmount, forwarderFeePercentage);
const feePercentage = getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, forwarderFeePercentage);
const tx = this._forwarder.marketBuyOrdersWithEth.awaitTransactionSuccessAsync(
orders,
makerAssetAcquiredAmount,
orders.map(signedOrder => signedOrder.signature),
feePercentage,
this._forwarderFeeRecipient.address,
{
value: wethSpentAmount.plus(ethSpentOnForwarderFee).plus(ethValueAdjustment),
from: this._taker.address,
},
);
if (options.revertError !== undefined) {
await expect(tx).to.revertWith(options.revertError);
} else {
const txReceipt = await tx;
await this._checkResultsAsync(txReceipt, orders, expectedOrderStatuses, expectedBalances);
}
}
public async marketSellTestAsync(
orders: SignedOrder[],
fractionalNumberOfOrdersToFill: number,
options: Partial<MarketSellOptions> = {},
): Promise<void> {
const orderInfoBefore = await Promise.all(
orders.map(order => this._deployment.exchange.getOrderInfo.callAsync(order)),
);
const expectedOrderStatuses = orderInfoBefore.map((orderInfo, i) =>
fractionalNumberOfOrdersToFill >= i + 1 && orderInfo.orderStatus === OrderStatus.Fillable
? OrderStatus.FullyFilled
: orderInfo.orderStatus,
);
const { balances: expectedBalances, wethSpentAmount } = await this._simulateForwarderFillAsync(
orders,
orderInfoBefore,
fractionalNumberOfOrdersToFill,
options,
);
const forwarderFeePercentage = options.forwarderFeePercentage || 0;
const ethSpentOnForwarderFee = getPercentageOfValue(wethSpentAmount, forwarderFeePercentage);
const feePercentage = getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, forwarderFeePercentage);
const tx = this._forwarder.marketSellOrdersWithEth.awaitTransactionSuccessAsync(
orders,
orders.map(signedOrder => signedOrder.signature),
feePercentage,
this._forwarderFeeRecipient.address,
{
value: wethSpentAmount.plus(ethSpentOnForwarderFee),
from: this._taker.address,
},
);
if (options.revertError !== undefined) {
await expect(tx).to.revertWith(options.revertError);
} else {
const txReceipt = await tx;
await this._checkResultsAsync(txReceipt, orders, expectedOrderStatuses, expectedBalances);
}
}
private async _checkResultsAsync(
txReceipt: TransactionReceiptWithDecodedLogs,
orders: SignedOrder[],
expectedOrderStatuses: OrderStatus[],
expectedBalances: LocalBalanceStore,
): Promise<void> {
// Transaction gas cost
expectedBalances.burnGas(txReceipt.from, DeploymentManager.gasPrice.times(txReceipt.gasUsed));
await this._balanceStore.updateBalancesAsync();
// Check balances
this._balanceStore.assertEquals(expectedBalances);
// Get updated order info
const orderInfoAfter = await Promise.all(
orders.map(order => this._deployment.exchange.getOrderInfo.callAsync(order)),
);
// Check order statuses
for (const [i, orderInfo] of orderInfoAfter.entries()) {
expect(orderInfo.orderStatus, ` Order ${i} status`).to.equal(expectedOrderStatuses[i]);
}
}
// Simulates filling some orders via the Forwarder contract. For example, if
// orders = [A, B, C, D] and fractionalNumberOfOrdersToFill = 2.3, then
// we simulate A and B being completely filled, and 0.3 * C being filled.
private async _simulateForwarderFillAsync(
orders: SignedOrder[],
ordersInfoBefore: OrderInfo[],
fractionalNumberOfOrdersToFill: number,
options: Partial<MarketBuyOptions>,
): Promise<ForwarderFillState> {
await this._balanceStore.updateBalancesAsync();
const balances = LocalBalanceStore.create(this._balanceStore);
const currentTotal = {
wethSpentAmount: constants.ZERO_AMOUNT,
makerAssetAcquiredAmount: constants.ZERO_AMOUNT,
};
let remainingOrdersToFill = fractionalNumberOfOrdersToFill;
for (const [i, order] of orders.entries()) {
if (remainingOrdersToFill === 0) {
break;
} else if (ordersInfoBefore[i].orderStatus !== OrderStatus.Fillable) {
// If the order is not fillable, skip over it but still count it towards fractionalNumberOfOrdersToFill
remainingOrdersToFill = Math.max(remainingOrdersToFill - 1, 0);
continue;
}
const { wethSpentAmount, makerAssetAcquiredAmount } = this._simulateSingleFill(
balances,
order,
ordersInfoBefore[i].orderTakerAssetFilledAmount,
Math.min(remainingOrdersToFill, 1),
);
remainingOrdersToFill = Math.max(remainingOrdersToFill - 1, 0);
currentTotal.wethSpentAmount = currentTotal.wethSpentAmount.plus(wethSpentAmount);
currentTotal.makerAssetAcquiredAmount = currentTotal.makerAssetAcquiredAmount.plus(
makerAssetAcquiredAmount,
);
}
const ethSpentOnForwarderFee = getPercentageOfValue(
currentTotal.wethSpentAmount,
options.forwarderFeePercentage || 0,
);
// In reality the Forwarder is a middleman in this transaction and the ETH gets wrapped and unwrapped.
balances.sendEth(this._taker.address, this._forwarderFeeRecipient.address, ethSpentOnForwarderFee);
return { ...currentTotal, balances };
}
private _simulateSingleFill(
balances: LocalBalanceStore,
order: SignedOrder,
takerAssetFilled: BigNumber,
fillFraction: number,
): ForwarderFillState {
let { makerAssetAmount, takerAssetAmount, makerFee, takerFee } = order;
makerAssetAmount = makerAssetAmount.times(fillFraction).integerValue(BigNumber.ROUND_CEIL);
takerAssetAmount = takerAssetAmount.times(fillFraction).integerValue(BigNumber.ROUND_CEIL);
makerFee = makerFee.times(fillFraction).integerValue(BigNumber.ROUND_CEIL);
takerFee = takerFee.times(fillFraction).integerValue(BigNumber.ROUND_CEIL);
// Accounting for partially filled orders
// As with unfillable orders, these still count as 1 towards fractionalNumberOfOrdersToFill
takerAssetAmount = BigNumber.max(takerAssetAmount.minus(takerAssetFilled), 0);
const makerAssetFilled = takerAssetFilled
.times(order.makerAssetAmount)
.dividedToIntegerBy(order.takerAssetAmount);
makerAssetAmount = BigNumber.max(makerAssetAmount.minus(makerAssetFilled), 0);
const makerFeeFilled = takerAssetFilled.times(order.makerFee).dividedToIntegerBy(order.takerAssetAmount);
makerFee = BigNumber.max(makerFee.minus(makerFeeFilled), 0);
const takerFeeFilled = takerAssetFilled.times(order.takerFee).dividedToIntegerBy(order.takerAssetAmount);
takerFee = BigNumber.max(takerFee.minus(takerFeeFilled), 0);
let wethSpentAmount = takerAssetAmount.plus(DeploymentManager.protocolFee);
let makerAssetAcquiredAmount = makerAssetAmount;
if (order.takerFeeAssetData === order.makerAssetData) {
makerAssetAcquiredAmount = makerAssetAcquiredAmount.minus(takerFee);
} else if (order.takerFeeAssetData === order.takerAssetData) {
wethSpentAmount = wethSpentAmount.plus(takerFee);
}
// Taker sends ETH to Forwarder
balances.sendEth(this._taker.address, this._forwarder.address, wethSpentAmount);
// Forwarder wraps the ETH
balances.wrapEth(this._forwarder.address, this._deployment.tokens.weth.address, wethSpentAmount);
// (In reality this is done all at once, but we simulate it order by order)
// Maker -> Forwarder
balances.transferAsset(this._maker.address, this._forwarder.address, makerAssetAmount, order.makerAssetData);
// Maker -> Order fee recipient
balances.transferAsset(this._maker.address, this._orderFeeRecipient.address, makerFee, order.makerFeeAssetData);
// Forwarder -> Maker
balances.transferAsset(this._forwarder.address, this._maker.address, takerAssetAmount, order.takerAssetData);
// Forwarder -> Order fee recipient
balances.transferAsset(
this._forwarder.address,
this._orderFeeRecipient.address,
takerFee,
order.takerFeeAssetData,
);
// Forwarder pays the protocol fee in WETH
balances.transferAsset(
this._forwarder.address,
this._deployment.staking.stakingProxy.address,
DeploymentManager.protocolFee,
order.takerAssetData,
);
// Forwarder gives acquired maker asset to taker
balances.transferAsset(
this._forwarder.address,
this._taker.address,
makerAssetAcquiredAmount,
order.makerAssetData,
);
return { wethSpentAmount, makerAssetAcquiredAmount, balances };
}
}

View File

@ -69,5 +69,6 @@ export {
getRandomInteger,
getRandomPortion,
getNumericalDivergence,
getPercentageOfValue,
toBaseUnitAmount,
} from './number_utils';

View File

@ -3,6 +3,7 @@ import { Web3Wrapper } from '@0x/web3-wrapper';
import * as crypto from 'crypto';
import { expect } from './chai_setup';
import { constants } from './constants';
import { Numberish } from './types';
/**
@ -86,3 +87,12 @@ export function toBaseUnitAmount(amount: Numberish): BigNumber {
const baseUnitAmount = Web3Wrapper.toBaseUnitAmount(amountAsBigNumber, decimals);
return baseUnitAmount;
}
/**
* Computes a percentage of `value`, first converting `percentage` to be expressed in 18 digits.
*/
export function getPercentageOfValue(value: Numberish, percentage: Numberish): BigNumber {
const numerator = constants.PERCENTAGE_DENOMINATOR.times(percentage).dividedToIntegerBy(100);
const newValue = numerator.times(value).dividedToIntegerBy(constants.PERCENTAGE_DENOMINATOR);
return newValue;
}