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