459 lines
21 KiB
TypeScript

import { CoordinatorContract, SignedCoordinatorApproval } from '@0x/contracts-coordinator';
import {
BlockchainBalanceStore,
LocalBalanceStore,
constants as exchangeConstants,
ExchangeCancelEventArgs,
ExchangeCancelUpToEventArgs,
exchangeDataEncoder,
ExchangeEvents,
ExchangeFillEventArgs,
ExchangeFunctionName,
} from '@0x/contracts-exchange';
import { blockchainTests, expect, hexConcat, hexSlice, verifyEvents } from '@0x/contracts-test-utils';
import { assetDataUtils, CoordinatorRevertErrors, orderHashUtils, transactionHashUtils } from '@0x/order-utils';
import { SignedOrder, SignedZeroExTransaction } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import { Actor, actorAddressesByName, FeeRecipient, Maker } from '../actors';
import { deployCoordinatorAsync } from './deploy_coordinator';
import { DeploymentManager } from '../deployment/deployment_mananger';
// tslint:disable:no-unnecessary-type-assertion
blockchainTests.resets('Coordinator tests', env => {
let deployment: DeploymentManager;
let coordinator: CoordinatorContract;
let balanceStore: BlockchainBalanceStore;
let maker: Maker;
let taker: Actor;
let feeRecipient: FeeRecipient;
before(async () => {
deployment = await DeploymentManager.deployAsync(env, {
numErc20TokensToDeploy: 4,
numErc721TokensToDeploy: 0,
numErc1155TokensToDeploy: 0,
});
coordinator = await deployCoordinatorAsync(deployment, env);
const [makerToken, takerToken, makerFeeToken, takerFeeToken] = deployment.tokens.erc20;
taker = new Actor({ name: 'Taker', deployment });
feeRecipient = new FeeRecipient({
name: 'Fee recipient',
deployment,
verifyingContract: coordinator,
});
maker = new Maker({
name: 'Maker',
deployment,
orderConfig: {
senderAddress: coordinator.address,
feeRecipientAddress: feeRecipient.address,
makerAssetData: assetDataUtils.encodeERC20AssetData(makerToken.address),
takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken.address),
makerFeeAssetData: assetDataUtils.encodeERC20AssetData(makerFeeToken.address),
takerFeeAssetData: assetDataUtils.encodeERC20AssetData(takerFeeToken.address),
},
});
taker.configureERC20TokenAsync(takerToken);
taker.configureERC20TokenAsync(takerFeeToken);
taker.configureERC20TokenAsync(deployment.tokens.weth, deployment.staking.stakingProxy.address);
maker.configureERC20TokenAsync(makerToken);
maker.configureERC20TokenAsync(makerFeeToken);
balanceStore = new BlockchainBalanceStore(
{
...actorAddressesByName([maker, taker, feeRecipient]),
Coordinator: coordinator.address,
StakingProxy: deployment.staking.stakingProxy.address,
},
{ erc20: { makerToken, takerToken, makerFeeToken, takerFeeToken, wETH: deployment.tokens.weth } },
{},
);
});
function simulateFills(
orders: SignedOrder[],
txReceipt: TransactionReceiptWithDecodedLogs,
msgValue: BigNumber = new BigNumber(0),
): LocalBalanceStore {
const localBalanceStore = LocalBalanceStore.create(balanceStore);
// Transaction gas cost
localBalanceStore.burnGas(txReceipt.from, DeploymentManager.gasPrice.times(txReceipt.gasUsed));
for (const order of orders) {
// Taker -> Maker
localBalanceStore.transferAsset(taker.address, maker.address, order.takerAssetAmount, order.takerAssetData);
// Maker -> Taker
localBalanceStore.transferAsset(maker.address, taker.address, order.makerAssetAmount, order.makerAssetData);
// Taker -> Fee Recipient
localBalanceStore.transferAsset(
taker.address,
feeRecipient.address,
order.takerFee,
order.takerFeeAssetData,
);
// Maker -> Fee Recipient
localBalanceStore.transferAsset(
maker.address,
feeRecipient.address,
order.makerFee,
order.makerFeeAssetData,
);
// Protocol fee
if (msgValue.isGreaterThanOrEqualTo(DeploymentManager.protocolFee)) {
localBalanceStore.sendEth(
txReceipt.from,
deployment.staking.stakingProxy.address,
DeploymentManager.protocolFee,
);
msgValue = msgValue.minus(DeploymentManager.protocolFee);
} else {
localBalanceStore.transferAsset(
taker.address,
deployment.staking.stakingProxy.address,
DeploymentManager.protocolFee,
assetDataUtils.encodeERC20AssetData(deployment.tokens.weth.address),
);
}
}
return localBalanceStore;
}
function expectedFillEvent(order: SignedOrder): ExchangeFillEventArgs {
return {
makerAddress: order.makerAddress,
takerAddress: taker.address,
senderAddress: order.senderAddress,
feeRecipientAddress: order.feeRecipientAddress,
makerAssetData: order.makerAssetData,
takerAssetData: order.takerAssetData,
makerFeeAssetData: order.makerFeeAssetData,
takerFeeAssetData: order.takerFeeAssetData,
makerAssetFilledAmount: order.makerAssetAmount,
takerAssetFilledAmount: order.takerAssetAmount,
makerFeePaid: order.makerFee,
takerFeePaid: order.takerFee,
protocolFeePaid: DeploymentManager.protocolFee,
orderHash: orderHashUtils.getOrderHashHex(order),
};
}
describe('single order fills', () => {
let order: SignedOrder;
let data: string;
let transaction: SignedZeroExTransaction;
let approval: SignedCoordinatorApproval;
for (const fnName of exchangeConstants.SINGLE_FILL_FN_NAMES) {
before(async () => {
order = await maker.signOrderAsync();
data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, [order]);
transaction = await taker.signTransactionAsync({
data,
gasPrice: DeploymentManager.gasPrice,
});
approval = feeRecipient.signCoordinatorApproval(transaction, taker.address);
});
it(`${fnName} should fill the order with a signed approval`, async () => {
await balanceStore.updateBalancesAsync();
const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync(
transaction,
taker.address,
transaction.signature,
[approval.signature],
{ from: taker.address, value: DeploymentManager.protocolFee },
);
const expectedBalances = simulateFills([order], txReceipt, DeploymentManager.protocolFee);
await balanceStore.updateBalancesAsync();
balanceStore.assertEquals(expectedBalances);
verifyEvents(txReceipt, [expectedFillEvent(order)], ExchangeEvents.Fill);
});
it(`${fnName} should fill the order if called by approver (eth protocol fee, no refund)`, async () => {
await balanceStore.updateBalancesAsync();
const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync(
transaction,
feeRecipient.address,
transaction.signature,
[],
{ from: feeRecipient.address, value: DeploymentManager.protocolFee },
);
const expectedBalances = simulateFills([order], txReceipt, DeploymentManager.protocolFee);
await balanceStore.updateBalancesAsync();
balanceStore.assertEquals(expectedBalances);
verifyEvents(txReceipt, [expectedFillEvent(order)], ExchangeEvents.Fill);
});
it(`${fnName} should fill the order if called by approver (eth protocol fee, refund)`, async () => {
await balanceStore.updateBalancesAsync();
const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync(
transaction,
feeRecipient.address,
transaction.signature,
[],
{ from: feeRecipient.address, value: DeploymentManager.protocolFee.plus(1) },
);
const expectedBalances = simulateFills([order], txReceipt, DeploymentManager.protocolFee.plus(1));
await balanceStore.updateBalancesAsync();
balanceStore.assertEquals(expectedBalances);
verifyEvents(txReceipt, [expectedFillEvent(order)], ExchangeEvents.Fill);
});
it(`${fnName} should fill the order if called by approver (weth protocol fee, no refund)`, async () => {
await balanceStore.updateBalancesAsync();
const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync(
transaction,
feeRecipient.address,
transaction.signature,
[],
{ from: feeRecipient.address },
);
const expectedBalances = simulateFills([order], txReceipt);
await balanceStore.updateBalancesAsync();
balanceStore.assertEquals(expectedBalances);
verifyEvents(txReceipt, [expectedFillEvent(order)], ExchangeEvents.Fill);
});
it(`${fnName} should fill the order if called by approver (weth protocol fee, refund)`, async () => {
await balanceStore.updateBalancesAsync();
const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync(
transaction,
feeRecipient.address,
transaction.signature,
[],
{ from: feeRecipient.address, value: new BigNumber(1) },
);
const expectedBalances = simulateFills([order], txReceipt, new BigNumber(1));
await balanceStore.updateBalancesAsync();
balanceStore.assertEquals(expectedBalances);
verifyEvents(txReceipt, [expectedFillEvent(order)], ExchangeEvents.Fill);
});
it(`${fnName} should revert with no approval signature`, async () => {
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
const tx = coordinator.executeTransaction.awaitTransactionSuccessAsync(
transaction,
taker.address,
transaction.signature,
[],
{ from: taker.address, value: DeploymentManager.protocolFee },
);
const expectedError = new CoordinatorRevertErrors.InvalidApprovalSignatureError(
transactionHash,
feeRecipient.address,
);
expect(tx).to.revertWith(expectedError);
});
it(`${fnName} should revert with an invalid approval signature`, async () => {
const approvalSignature = hexConcat(
hexSlice(approval.signature, 0, 2),
'0xFFFFFFFF',
hexSlice(approval.signature, 6),
);
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
const tx = coordinator.executeTransaction.awaitTransactionSuccessAsync(
transaction,
taker.address,
transaction.signature,
[approvalSignature],
{ from: taker.address, value: DeploymentManager.protocolFee },
);
const expectedError = new CoordinatorRevertErrors.InvalidApprovalSignatureError(
transactionHash,
feeRecipient.address,
);
expect(tx).to.revertWith(expectedError);
});
it(`${fnName} should revert if not called by tx signer or approver`, async () => {
const tx = coordinator.executeTransaction.awaitTransactionSuccessAsync(
transaction,
taker.address,
transaction.signature,
[approval.signature],
{ from: maker.address, value: DeploymentManager.protocolFee },
);
const expectedError = new CoordinatorRevertErrors.InvalidOriginError(taker.address);
expect(tx).to.revertWith(expectedError);
});
}
});
describe('batch order fills', () => {
let orders: SignedOrder[];
let data: string;
let transaction: SignedZeroExTransaction;
let approval: SignedCoordinatorApproval;
for (const fnName of [...exchangeConstants.MARKET_FILL_FN_NAMES, ...exchangeConstants.BATCH_FILL_FN_NAMES]) {
before(async () => {
orders = [await maker.signOrderAsync(), await maker.signOrderAsync()];
data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, orders);
transaction = await taker.signTransactionAsync({
data,
gasPrice: DeploymentManager.gasPrice,
});
approval = feeRecipient.signCoordinatorApproval(transaction, taker.address);
});
it(`${fnName} should fill the orders with a signed approval`, async () => {
await balanceStore.updateBalancesAsync();
const value = DeploymentManager.protocolFee.times(orders.length);
const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync(
transaction,
taker.address,
transaction.signature,
[approval.signature],
{ from: taker.address, value },
);
const expectedBalances = simulateFills(orders, txReceipt, value);
await balanceStore.updateBalancesAsync();
balanceStore.assertEquals(expectedBalances);
verifyEvents(txReceipt, orders.map(order => expectedFillEvent(order)), ExchangeEvents.Fill);
});
it(`${fnName} should fill the orders if called by approver (eth fee, no refund)`, async () => {
await balanceStore.updateBalancesAsync();
const value = DeploymentManager.protocolFee.times(orders.length);
const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync(
transaction,
feeRecipient.address,
transaction.signature,
[],
{ from: feeRecipient.address, value },
);
const expectedBalances = simulateFills(orders, txReceipt, value);
await balanceStore.updateBalancesAsync();
balanceStore.assertEquals(expectedBalances);
verifyEvents(txReceipt, orders.map(order => expectedFillEvent(order)), ExchangeEvents.Fill);
});
it(`${fnName} should fill the orders if called by approver (mixed fees, refund)`, async () => {
await balanceStore.updateBalancesAsync();
const value = DeploymentManager.protocolFee.plus(1);
const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync(
transaction,
feeRecipient.address,
transaction.signature,
[],
{ from: feeRecipient.address, value },
);
const expectedBalances = simulateFills(orders, txReceipt, value);
await balanceStore.updateBalancesAsync();
balanceStore.assertEquals(expectedBalances);
verifyEvents(txReceipt, orders.map(order => expectedFillEvent(order)), ExchangeEvents.Fill);
});
it(`${fnName} should revert with an invalid approval signature`, async () => {
const approvalSignature = hexConcat(
hexSlice(approval.signature, 0, 2),
'0xFFFFFFFF',
hexSlice(approval.signature, 6),
);
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
const tx = coordinator.executeTransaction.awaitTransactionSuccessAsync(
transaction,
taker.address,
transaction.signature,
[approvalSignature],
{ from: taker.address, value: DeploymentManager.protocolFee.times(orders.length) },
);
const expectedError = new CoordinatorRevertErrors.InvalidApprovalSignatureError(
transactionHash,
feeRecipient.address,
);
expect(tx).to.revertWith(expectedError);
});
it(`${fnName} should revert if not called by tx signer or approver`, async () => {
const tx = coordinator.executeTransaction.awaitTransactionSuccessAsync(
transaction,
taker.address,
transaction.signature,
[approval.signature],
{ from: maker.address, value: DeploymentManager.protocolFee.times(orders.length) },
);
const expectedError = new CoordinatorRevertErrors.InvalidOriginError(taker.address);
expect(tx).to.revertWith(expectedError);
});
}
});
describe('cancels', () => {
function expectedCancelEvent(order: SignedOrder): ExchangeCancelEventArgs {
return {
makerAddress: order.makerAddress,
senderAddress: order.senderAddress,
feeRecipientAddress: order.feeRecipientAddress,
makerAssetData: order.makerAssetData,
takerAssetData: order.takerAssetData,
orderHash: orderHashUtils.getOrderHashHex(order),
};
}
it('cancelOrder call should be successful without an approval', async () => {
const order = await maker.signOrderAsync();
const data = exchangeDataEncoder.encodeOrdersToExchangeData(ExchangeFunctionName.CancelOrder, [order]);
const transaction = await maker.signTransactionAsync({
data,
gasPrice: DeploymentManager.gasPrice,
});
const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync(
transaction,
maker.address,
transaction.signature,
[],
{ from: maker.address },
);
verifyEvents(txReceipt, [expectedCancelEvent(order)], ExchangeEvents.Cancel);
});
it('batchCancelOrders call should be successful without an approval', async () => {
const orders = [await maker.signOrderAsync(), await maker.signOrderAsync()];
const data = exchangeDataEncoder.encodeOrdersToExchangeData(ExchangeFunctionName.BatchCancelOrders, orders);
const transaction = await maker.signTransactionAsync({
data,
gasPrice: DeploymentManager.gasPrice,
});
const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync(
transaction,
maker.address,
transaction.signature,
[],
{ from: maker.address },
);
verifyEvents(txReceipt, orders.map(order => expectedCancelEvent(order)), ExchangeEvents.Cancel);
});
it('cancelOrdersUpTo call should be successful without an approval', async () => {
const data = exchangeDataEncoder.encodeOrdersToExchangeData(ExchangeFunctionName.CancelOrdersUpTo, []);
const transaction = await maker.signTransactionAsync({
data,
gasPrice: DeploymentManager.gasPrice,
});
const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync(
transaction,
maker.address,
transaction.signature,
[],
{ from: maker.address },
);
const expectedEvent: ExchangeCancelUpToEventArgs = {
makerAddress: maker.address,
orderSenderAddress: coordinator.address,
orderEpoch: new BigNumber(1),
};
verifyEvents(txReceipt, [expectedEvent], ExchangeEvents.CancelUpTo);
});
});
});
// tslint:disable:max-file-line-count