2019-10-21 11:16:14 -07:00

421 lines
21 KiB
TypeScript

import { ERC20ProxyContract, ERC20Wrapper } from '@0x/contracts-asset-proxy';
import { ApprovalFactory, artifacts, CoordinatorContract } from '@0x/contracts-coordinator';
import { artifacts as erc20Artifacts, DummyERC20TokenContract, WETH9Contract } from '@0x/contracts-erc20';
import {
artifacts as exchangeArtifacts,
constants as exchangeConstants,
ExchangeContract,
exchangeDataEncoder,
ExchangeFunctionName,
TestProtocolFeeCollectorContract,
} from '@0x/contracts-exchange';
import {
blockchainTests,
constants,
hexConcat,
hexSlice,
OrderFactory,
TransactionFactory,
} from '@0x/contracts-test-utils';
import { assetDataUtils, CoordinatorRevertErrors, transactionHashUtils } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import { CoordinatorTestFactory } from './coordinator_test_factory';
// tslint:disable:no-unnecessary-type-assertion
blockchainTests.resets('Coordinator tests', env => {
let chainId: number;
let makerAddress: string;
let owner: string;
let takerAddress: string;
let feeRecipientAddress: string;
let erc20Proxy: ERC20ProxyContract;
let erc20TokenA: DummyERC20TokenContract;
let erc20TokenB: DummyERC20TokenContract;
let makerFeeToken: DummyERC20TokenContract;
let takerFeeToken: DummyERC20TokenContract;
let coordinatorContract: CoordinatorContract;
let exchange: ExchangeContract;
let protocolFeeCollector: TestProtocolFeeCollectorContract;
let wethContract: WETH9Contract;
let erc20Wrapper: ERC20Wrapper;
let orderFactory: OrderFactory;
let takerTransactionFactory: TransactionFactory;
let makerTransactionFactory: TransactionFactory;
let approvalFactory: ApprovalFactory;
let testFactory: CoordinatorTestFactory;
const GAS_PRICE = new BigNumber(env.txDefaults.gasPrice || constants.DEFAULT_GAS_PRICE);
const PROTOCOL_FEE_MULTIPLIER = new BigNumber(150000);
const PROTOCOL_FEE = GAS_PRICE.times(PROTOCOL_FEE_MULTIPLIER);
before(async () => {
chainId = await env.getChainIdAsync();
const accounts = await env.getAccountAddressesAsync();
const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress] = accounts);
// Deploy Exchange
exchange = await ExchangeContract.deployFrom0xArtifactAsync(
exchangeArtifacts.Exchange,
env.provider,
env.txDefaults,
{},
new BigNumber(chainId),
);
// Set up ERC20
erc20Wrapper = new ERC20Wrapper(env.provider, usedAddresses, owner);
erc20Proxy = await erc20Wrapper.deployProxyAsync();
const numDummyErc20ToDeploy = 4;
[erc20TokenA, erc20TokenB, makerFeeToken, takerFeeToken] = await erc20Wrapper.deployDummyTokensAsync(
numDummyErc20ToDeploy,
constants.DUMMY_TOKEN_DECIMALS,
);
await erc20Proxy.addAuthorizedAddress.awaitTransactionSuccessAsync(exchange.address, { from: owner });
await exchange.registerAssetProxy.awaitTransactionSuccessAsync(erc20Proxy.address, { from: owner });
// Set up WETH
wethContract = await WETH9Contract.deployFrom0xArtifactAsync(
erc20Artifacts.WETH9,
env.provider,
env.txDefaults,
{},
);
const weth = new DummyERC20TokenContract(wethContract.address, env.provider);
erc20Wrapper.addDummyTokenContract(weth);
await erc20Wrapper.setBalancesAndAllowancesAsync();
// Set up Protocol Fee Collector
protocolFeeCollector = await TestProtocolFeeCollectorContract.deployFrom0xArtifactAsync(
exchangeArtifacts.TestProtocolFeeCollector,
env.provider,
env.txDefaults,
{},
weth.address,
);
await exchange.setProtocolFeeMultiplier.awaitTransactionSuccessAsync(PROTOCOL_FEE_MULTIPLIER);
await exchange.setProtocolFeeCollectorAddress.awaitTransactionSuccessAsync(protocolFeeCollector.address);
for (const account of usedAddresses) {
await wethContract.deposit.awaitTransactionSuccessAsync({
from: account,
value: constants.ONE_ETHER,
});
await wethContract.approve.awaitTransactionSuccessAsync(
protocolFeeCollector.address,
constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
{
from: account,
},
);
}
erc20Wrapper.addTokenOwnerAddress(protocolFeeCollector.address);
// Deploy Coordinator
coordinatorContract = await CoordinatorContract.deployFrom0xArtifactAsync(
artifacts.Coordinator,
env.provider,
env.txDefaults,
{ ...exchangeArtifacts, ...artifacts },
exchange.address,
new BigNumber(chainId),
);
erc20Wrapper.addTokenOwnerAddress(coordinatorContract.address);
// Configure order defaults
const defaultOrderParams = {
...constants.STATIC_ORDER_PARAMS,
senderAddress: coordinatorContract.address,
makerAddress,
feeRecipientAddress,
makerAssetData: assetDataUtils.encodeERC20AssetData(erc20TokenA.address),
takerAssetData: assetDataUtils.encodeERC20AssetData(erc20TokenB.address),
makerFeeAssetData: assetDataUtils.encodeERC20AssetData(makerFeeToken.address),
takerFeeAssetData: assetDataUtils.encodeERC20AssetData(takerFeeToken.address),
exchangeAddress: exchange.address,
chainId,
};
const makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)];
const takerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(takerAddress)];
const feeRecipientPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(feeRecipientAddress)];
orderFactory = new OrderFactory(makerPrivateKey, defaultOrderParams);
makerTransactionFactory = new TransactionFactory(makerPrivateKey, exchange.address, chainId);
takerTransactionFactory = new TransactionFactory(takerPrivateKey, exchange.address, chainId);
approvalFactory = new ApprovalFactory(feeRecipientPrivateKey, coordinatorContract.address);
testFactory = new CoordinatorTestFactory(
coordinatorContract,
erc20Wrapper,
makerAddress,
takerAddress,
feeRecipientAddress,
protocolFeeCollector.address,
erc20TokenA.address,
erc20TokenB.address,
makerFeeToken.address,
takerFeeToken.address,
weth.address,
GAS_PRICE,
PROTOCOL_FEE_MULTIPLIER,
);
});
describe('single order fills', () => {
for (const fnName of exchangeConstants.SINGLE_FILL_FN_NAMES) {
it(`${fnName} should fill the order with a signed approval`, async () => {
const order = await orderFactory.newSignedOrderAsync();
const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, [order]);
const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data });
const approval = approvalFactory.newSignedApproval(transaction, takerAddress);
const txData = { from: takerAddress, value: PROTOCOL_FEE };
await testFactory.executeFillTransactionTestAsync(
[order],
transaction,
takerAddress,
[approval.signature],
txData,
);
});
it(`${fnName} should fill the order if called by approver (eth protocol fee, no refund)`, async () => {
const order = await orderFactory.newSignedOrderAsync();
const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, [order]);
const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data });
const txData = { from: feeRecipientAddress, value: PROTOCOL_FEE };
await testFactory.executeFillTransactionTestAsync(
[order],
transaction,
feeRecipientAddress,
[],
txData,
);
});
it(`${fnName} should fill the order if called by approver (eth protocol fee, refund)`, async () => {
const order = await orderFactory.newSignedOrderAsync();
const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, [order]);
const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data });
const txData = { from: feeRecipientAddress, value: PROTOCOL_FEE.plus(1) };
await testFactory.executeFillTransactionTestAsync(
[order],
transaction,
feeRecipientAddress,
[],
txData,
);
});
it(`${fnName} should fill the order if called by approver (weth protocol fee, no refund)`, async () => {
const order = await orderFactory.newSignedOrderAsync();
const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, [order]);
const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data });
const txData = { from: feeRecipientAddress };
await testFactory.executeFillTransactionTestAsync(
[order],
transaction,
feeRecipientAddress,
[],
txData,
);
});
it(`${fnName} should fill the order if called by approver (weth protocol fee, refund)`, async () => {
const order = await orderFactory.newSignedOrderAsync();
const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, [order]);
const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data });
const txData = { from: feeRecipientAddress, value: new BigNumber(1) };
await testFactory.executeFillTransactionTestAsync(
[order],
transaction,
feeRecipientAddress,
[],
txData,
);
});
it(`${fnName} should fill the order if called by approver`, async () => {
const order = await orderFactory.newSignedOrderAsync();
const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, [order]);
const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data });
const txData = { from: feeRecipientAddress, value: PROTOCOL_FEE };
await testFactory.executeFillTransactionTestAsync(
[order],
transaction,
feeRecipientAddress,
[],
txData,
);
});
it(`${fnName} should revert with no approval signature`, async () => {
const orders = [await orderFactory.newSignedOrderAsync()];
const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, orders);
const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data });
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
await testFactory.executeFillTransactionTestAsync(
orders,
transaction,
takerAddress,
[],
{
from: takerAddress,
gas: constants.MAX_EXECUTE_TRANSACTION_GAS,
value: PROTOCOL_FEE,
},
new CoordinatorRevertErrors.InvalidApprovalSignatureError(transactionHash, feeRecipientAddress),
);
});
it(`${fnName} should revert with an invalid approval signature`, async () => {
const orders = [await orderFactory.newSignedOrderAsync()];
const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, orders);
const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data });
const approval = approvalFactory.newSignedApproval(transaction, takerAddress);
const signature = hexConcat(
hexSlice(approval.signature, 0, 2),
'0xFFFFFFFF',
hexSlice(approval.signature, 6),
);
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
await testFactory.executeFillTransactionTestAsync(
orders,
transaction,
takerAddress,
[signature],
{ from: takerAddress, value: PROTOCOL_FEE },
new CoordinatorRevertErrors.InvalidApprovalSignatureError(transactionHash, feeRecipientAddress),
);
});
it(`${fnName} should revert if not called by tx signer or approver`, async () => {
const orders = [await orderFactory.newSignedOrderAsync()];
const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, orders);
const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data });
const approval = approvalFactory.newSignedApproval(transaction, takerAddress);
await testFactory.executeFillTransactionTestAsync(
orders,
transaction,
takerAddress,
[approval.signature],
{ from: owner, value: PROTOCOL_FEE },
new CoordinatorRevertErrors.InvalidOriginError(takerAddress),
);
});
}
});
describe('batch order fills', () => {
for (const fnName of [...exchangeConstants.MARKET_FILL_FN_NAMES, ...exchangeConstants.BATCH_FILL_FN_NAMES]) {
it(`${fnName} should fill the orders with a signed approval`, async () => {
const orders = [await orderFactory.newSignedOrderAsync(), await orderFactory.newSignedOrderAsync()];
const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, orders);
const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data });
const approval = approvalFactory.newSignedApproval(transaction, takerAddress);
await testFactory.executeFillTransactionTestAsync(
orders,
transaction,
takerAddress,
[approval.signature],
{
from: takerAddress,
gas: constants.MAX_EXECUTE_TRANSACTION_GAS,
value: PROTOCOL_FEE.times(orders.length),
},
);
});
it(`${fnName} should fill the orders if called by approver (eth fee, no refund)`, async () => {
const orders = [await orderFactory.newSignedOrderAsync(), await orderFactory.newSignedOrderAsync()];
const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, orders);
const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data });
await testFactory.executeFillTransactionTestAsync(orders, transaction, feeRecipientAddress, [], {
from: feeRecipientAddress,
gas: constants.MAX_EXECUTE_TRANSACTION_GAS,
value: PROTOCOL_FEE.times(orders.length),
});
});
it(`${fnName} should fill the orders if called by approver (mixed fees, refund)`, async () => {
const orders = [await orderFactory.newSignedOrderAsync(), await orderFactory.newSignedOrderAsync()];
const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, orders);
const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data });
await testFactory.executeFillTransactionTestAsync(orders, transaction, feeRecipientAddress, [], {
from: feeRecipientAddress,
gas: constants.MAX_EXECUTE_TRANSACTION_GAS,
value: PROTOCOL_FEE.times(orders.length).plus(1),
});
});
it(`${fnName} should revert with an invalid approval signature`, async () => {
const orders = [await orderFactory.newSignedOrderAsync(), await orderFactory.newSignedOrderAsync()];
const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, orders);
const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data });
const approval = approvalFactory.newSignedApproval(transaction, takerAddress);
const signature = hexConcat(
hexSlice(approval.signature, 0, 2),
'0xFFFFFFFF',
hexSlice(approval.signature, 6),
);
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
await testFactory.executeFillTransactionTestAsync(
orders,
transaction,
takerAddress,
[signature],
{ from: takerAddress, value: PROTOCOL_FEE.times(orders.length) },
new CoordinatorRevertErrors.InvalidApprovalSignatureError(transactionHash, feeRecipientAddress),
);
});
it(`${fnName} should revert if not called by tx signer or approver`, async () => {
const orders = [await orderFactory.newSignedOrderAsync(), await orderFactory.newSignedOrderAsync()];
const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, orders);
const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data });
const approval = approvalFactory.newSignedApproval(transaction, takerAddress);
await testFactory.executeFillTransactionTestAsync(
orders,
transaction,
takerAddress,
[approval.signature],
{ from: owner, value: PROTOCOL_FEE.times(orders.length) },
new CoordinatorRevertErrors.InvalidOriginError(takerAddress),
);
});
}
});
describe('cancels', () => {
it('cancelOrder call should be successful without an approval', async () => {
const orders = [await orderFactory.newSignedOrderAsync()];
const data = exchangeDataEncoder.encodeOrdersToExchangeData(ExchangeFunctionName.CancelOrder, orders);
const transaction = await makerTransactionFactory.newSignedTransactionAsync({ data });
await testFactory.executeCancelTransactionTestAsync(
ExchangeFunctionName.CancelOrder,
orders,
transaction,
makerAddress,
[],
{
from: makerAddress,
},
);
});
it('batchCancelOrders call should be successful without an approval', async () => {
const orders = [await orderFactory.newSignedOrderAsync(), await orderFactory.newSignedOrderAsync()];
const data = exchangeDataEncoder.encodeOrdersToExchangeData(ExchangeFunctionName.BatchCancelOrders, orders);
const transaction = await makerTransactionFactory.newSignedTransactionAsync({ data });
await testFactory.executeCancelTransactionTestAsync(
ExchangeFunctionName.BatchCancelOrders,
orders,
transaction,
makerAddress,
[],
{
from: makerAddress,
},
);
});
it('cancelOrdersUpTo call should be successful without an approval', async () => {
const data = exchangeDataEncoder.encodeOrdersToExchangeData(ExchangeFunctionName.CancelOrdersUpTo, []);
const transaction = await makerTransactionFactory.newSignedTransactionAsync({ data });
await testFactory.executeCancelTransactionTestAsync(
ExchangeFunctionName.CancelOrdersUpTo,
[],
transaction,
makerAddress,
[],
{
from: makerAddress,
},
);
});
});
});
// tslint:disable:max-file-line-count