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:ts": "tsc -b",
|
||||||
"build:ci": "yarn build",
|
"build:ci": "yarn build",
|
||||||
"pre_build": "run-s compile contracts:gen generate_contract_wrappers",
|
"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",
|
"rebuild_and_test": "run-s build test",
|
||||||
"test:coverage": "SOLIDITY_COVERAGE=true run-s build run_mocha coverage:report:text coverage:report:lcov",
|
"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:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html",
|
||||||
"test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha",
|
"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",
|
"compile": "sol-compiler",
|
||||||
"watch": "sol-compiler -w",
|
"watch": "sol-compiler -w",
|
||||||
"clean": "shx rm -rf lib generated-artifacts generated-wrappers",
|
"clean": "shx rm -rf lib generated-artifacts generated-wrappers",
|
||||||
|
@ -1,3 +1,2 @@
|
|||||||
export * from './artifacts';
|
export * from './artifacts';
|
||||||
export * from './wrappers';
|
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);
|
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`.
|
* Sends ETH from `fromAddress` to `toAddress`.
|
||||||
* @param fromAddress Sender of ETH.
|
* @param fromAddress Sender of ETH.
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { DummyERC20TokenContract, WETH9Contract } from '@0x/contracts-erc20';
|
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 { SignatureType, SignedZeroExTransaction, ZeroExTransaction } from '@0x/types';
|
||||||
import { BigNumber } from '@0x/utils';
|
import { BigNumber } from '@0x/utils';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import { DeploymentManager } from '../utils/deployment_manager';
|
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.
|
* 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 { Order, SignedOrder } from '@0x/types';
|
||||||
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
|
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
|
||||||
|
|
||||||
@ -41,6 +41,16 @@ export function MakerMixin<TBase extends Constructor>(Base: TBase) {
|
|||||||
return this.orderFactory.newSignedOrderAsync(customOrderParams);
|
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.
|
* Joins the staking pool specified by the given ID.
|
||||||
*/
|
*/
|
||||||
|
@ -21,7 +21,7 @@ import { deployCoordinatorAsync } from './deploy_coordinator';
|
|||||||
import { DeploymentManager } from '../utils/deployment_manager';
|
import { DeploymentManager } from '../utils/deployment_manager';
|
||||||
|
|
||||||
// tslint:disable:no-unnecessary-type-assertion
|
// tslint:disable:no-unnecessary-type-assertion
|
||||||
blockchainTests.resets('Coordinator tests', env => {
|
blockchainTests.resets('Coordinator integration tests', env => {
|
||||||
let deployment: DeploymentManager;
|
let deployment: DeploymentManager;
|
||||||
let coordinator: CoordinatorContract;
|
let coordinator: CoordinatorContract;
|
||||||
let balanceStore: BlockchainBalanceStore;
|
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,
|
getRandomInteger,
|
||||||
getRandomPortion,
|
getRandomPortion,
|
||||||
getNumericalDivergence,
|
getNumericalDivergence,
|
||||||
|
getPercentageOfValue,
|
||||||
toBaseUnitAmount,
|
toBaseUnitAmount,
|
||||||
} from './number_utils';
|
} from './number_utils';
|
||||||
|
@ -3,6 +3,7 @@ import { Web3Wrapper } from '@0x/web3-wrapper';
|
|||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
import { expect } from './chai_setup';
|
import { expect } from './chai_setup';
|
||||||
|
import { constants } from './constants';
|
||||||
import { Numberish } from './types';
|
import { Numberish } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -86,3 +87,12 @@ export function toBaseUnitAmount(amount: Numberish): BigNumber {
|
|||||||
const baseUnitAmount = Web3Wrapper.toBaseUnitAmount(amountAsBigNumber, decimals);
|
const baseUnitAmount = Web3Wrapper.toBaseUnitAmount(amountAsBigNumber, decimals);
|
||||||
return baseUnitAmount;
|
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