Move Forwarder test to integrations; update to use new framework
This commit is contained in:
parent
db9be73fec
commit
3d56c06ff3
@ -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",
|
||||
|
@ -1,3 +1,2 @@
|
||||
export * from './artifacts';
|
||||
export * from './wrappers';
|
||||
export * from '../test/utils';
|
||||
|
@ -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
|
@ -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();
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from './forwarder_wrapper';
|
||||
export * from './forwarder_test_factory';
|
@ -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.
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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;
|
20
contracts/integrations/test/forwarder/deploy_forwarder.ts
Normal file
20
contracts/integrations/test/forwarder/deploy_forwarder.ts
Normal 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),
|
||||
);
|
||||
}
|
597
contracts/integrations/test/forwarder/forwarder_test.ts
Normal file
597
contracts/integrations/test/forwarder/forwarder_test.ts
Normal 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
|
269
contracts/integrations/test/forwarder/forwarder_test_factory.ts
Normal file
269
contracts/integrations/test/forwarder/forwarder_test_factory.ts
Normal 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 };
|
||||
}
|
||||
}
|
@ -69,5 +69,6 @@ export {
|
||||
getRandomInteger,
|
||||
getRandomPortion,
|
||||
getNumericalDivergence,
|
||||
getPercentageOfValue,
|
||||
toBaseUnitAmount,
|
||||
} from './number_utils';
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user