421 lines
21 KiB
TypeScript
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
|