2030 lines
89 KiB
TypeScript
2030 lines
89 KiB
TypeScript
import {
|
|
blockchainTests,
|
|
constants,
|
|
describe,
|
|
expect,
|
|
getRandomPortion,
|
|
randomAddress,
|
|
verifyEventsFromLogs,
|
|
} from '@0x/contracts-test-utils';
|
|
import {
|
|
LimitOrder,
|
|
LimitOrderFields,
|
|
OrderStatus,
|
|
RevertErrors,
|
|
RfqOrder,
|
|
RfqOrderFields,
|
|
SignatureType,
|
|
} from '@0x/protocol-utils';
|
|
import { AnyRevertError, BigNumber } from '@0x/utils';
|
|
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
|
|
|
|
import { IZeroExContract, IZeroExEvents } from '../../src/wrappers';
|
|
import { artifacts } from '../artifacts';
|
|
import { fullMigrateAsync } from '../utils/migration';
|
|
import {
|
|
assertOrderInfoEquals,
|
|
computeLimitOrderFilledAmounts,
|
|
computeRfqOrderFilledAmounts,
|
|
createExpiry,
|
|
getActualFillableTakerTokenAmount,
|
|
getFillableMakerTokenAmount,
|
|
getRandomLimitOrder,
|
|
getRandomRfqOrder,
|
|
NativeOrdersTestEnvironment,
|
|
} from '../utils/orders';
|
|
import {
|
|
TestMintableERC20TokenContract,
|
|
TestOrderSignerRegistryWithContractWalletContract,
|
|
TestRfqOriginRegistrationContract,
|
|
} from '../wrappers';
|
|
|
|
blockchainTests.resets('NativeOrdersFeature', env => {
|
|
const { NULL_ADDRESS, MAX_UINT256, NULL_BYTES32, ZERO_AMOUNT } = constants;
|
|
const GAS_PRICE = new BigNumber('123e9');
|
|
const PROTOCOL_FEE_MULTIPLIER = 1337e3;
|
|
const SINGLE_PROTOCOL_FEE = GAS_PRICE.times(PROTOCOL_FEE_MULTIPLIER);
|
|
let maker: string;
|
|
let taker: string;
|
|
let notMaker: string;
|
|
let notTaker: string;
|
|
let contractWalletOwner: string;
|
|
let contractWalletSigner: string;
|
|
let zeroEx: IZeroExContract;
|
|
let verifyingContract: string;
|
|
let makerToken: TestMintableERC20TokenContract;
|
|
let takerToken: TestMintableERC20TokenContract;
|
|
let wethToken: TestMintableERC20TokenContract;
|
|
let testRfqOriginRegistration: TestRfqOriginRegistrationContract;
|
|
let contractWallet: TestOrderSignerRegistryWithContractWalletContract;
|
|
let testUtils: NativeOrdersTestEnvironment;
|
|
|
|
before(async () => {
|
|
let owner;
|
|
[owner, maker, taker, notMaker, notTaker, contractWalletOwner, contractWalletSigner] =
|
|
await env.getAccountAddressesAsync();
|
|
[makerToken, takerToken, wethToken] = await Promise.all(
|
|
[...new Array(3)].map(async () =>
|
|
TestMintableERC20TokenContract.deployFrom0xArtifactAsync(
|
|
artifacts.TestMintableERC20Token,
|
|
env.provider,
|
|
env.txDefaults,
|
|
artifacts,
|
|
),
|
|
),
|
|
);
|
|
zeroEx = await fullMigrateAsync(
|
|
owner,
|
|
env.provider,
|
|
{ ...env.txDefaults, gasPrice: GAS_PRICE },
|
|
{},
|
|
{ wethAddress: wethToken.address, protocolFeeMultiplier: PROTOCOL_FEE_MULTIPLIER },
|
|
{ nativeOrders: artifacts.TestNativeOrdersFeature },
|
|
);
|
|
verifyingContract = zeroEx.address;
|
|
await Promise.all(
|
|
[maker, notMaker].map(a =>
|
|
makerToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: a }),
|
|
),
|
|
);
|
|
await Promise.all(
|
|
[taker, notTaker].map(a =>
|
|
takerToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: a }),
|
|
),
|
|
);
|
|
testRfqOriginRegistration = await TestRfqOriginRegistrationContract.deployFrom0xArtifactAsync(
|
|
artifacts.TestRfqOriginRegistration,
|
|
env.provider,
|
|
env.txDefaults,
|
|
artifacts,
|
|
);
|
|
// contract wallet for signer delegation
|
|
contractWallet = await TestOrderSignerRegistryWithContractWalletContract.deployFrom0xArtifactAsync(
|
|
artifacts.TestOrderSignerRegistryWithContractWallet,
|
|
env.provider,
|
|
{
|
|
from: contractWalletOwner,
|
|
},
|
|
artifacts,
|
|
zeroEx.address,
|
|
);
|
|
|
|
await contractWallet
|
|
.approveERC20(makerToken.address, zeroEx.address, MAX_UINT256)
|
|
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
|
|
|
|
testUtils = new NativeOrdersTestEnvironment(
|
|
maker,
|
|
taker,
|
|
makerToken,
|
|
takerToken,
|
|
zeroEx,
|
|
GAS_PRICE,
|
|
SINGLE_PROTOCOL_FEE,
|
|
env,
|
|
);
|
|
});
|
|
|
|
function getTestLimitOrder(fields: Partial<LimitOrderFields> = {}): LimitOrder {
|
|
return getRandomLimitOrder({
|
|
maker,
|
|
verifyingContract,
|
|
chainId: 1337,
|
|
takerToken: takerToken.address,
|
|
makerToken: makerToken.address,
|
|
taker: NULL_ADDRESS,
|
|
sender: NULL_ADDRESS,
|
|
...fields,
|
|
});
|
|
}
|
|
|
|
function getTestRfqOrder(fields: Partial<RfqOrderFields> = {}): RfqOrder {
|
|
return getRandomRfqOrder({
|
|
maker,
|
|
verifyingContract,
|
|
chainId: 1337,
|
|
takerToken: takerToken.address,
|
|
makerToken: makerToken.address,
|
|
txOrigin: taker,
|
|
...fields,
|
|
});
|
|
}
|
|
|
|
describe('getProtocolFeeMultiplier()', () => {
|
|
it('returns the protocol fee multiplier', async () => {
|
|
const r = await zeroEx.getProtocolFeeMultiplier().callAsync();
|
|
expect(r).to.bignumber.eq(PROTOCOL_FEE_MULTIPLIER);
|
|
});
|
|
});
|
|
|
|
describe('getLimitOrderHash()', () => {
|
|
it('returns the correct hash', async () => {
|
|
const order = getTestLimitOrder();
|
|
const hash = await zeroEx.getLimitOrderHash(order).callAsync();
|
|
expect(hash).to.eq(order.getHash());
|
|
});
|
|
});
|
|
|
|
describe('getRfqOrderHash()', () => {
|
|
it('returns the correct hash', async () => {
|
|
const order = getTestRfqOrder();
|
|
const hash = await zeroEx.getRfqOrderHash(order).callAsync();
|
|
expect(hash).to.eq(order.getHash());
|
|
});
|
|
});
|
|
|
|
describe('getLimitOrderInfo()', () => {
|
|
it('unfilled order', async () => {
|
|
const order = getTestLimitOrder();
|
|
const info = await zeroEx.getLimitOrderInfo(order).callAsync();
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Fillable,
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: ZERO_AMOUNT,
|
|
});
|
|
});
|
|
|
|
it('unfilled cancelled order', async () => {
|
|
const order = getTestLimitOrder();
|
|
await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
const info = await zeroEx.getLimitOrderInfo(order).callAsync();
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Cancelled,
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: ZERO_AMOUNT,
|
|
});
|
|
});
|
|
|
|
it('unfilled expired order', async () => {
|
|
const order = getTestLimitOrder({ expiry: createExpiry(-60) });
|
|
const info = await zeroEx.getLimitOrderInfo(order).callAsync();
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Expired,
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: ZERO_AMOUNT,
|
|
});
|
|
});
|
|
|
|
it('filled then expired order', async () => {
|
|
const expiry = createExpiry(60);
|
|
const order = getTestLimitOrder({ expiry });
|
|
// Fill the order first.
|
|
await testUtils.fillLimitOrderAsync(order);
|
|
// Advance time to expire the order.
|
|
await env.web3Wrapper.increaseTimeAsync(61);
|
|
const info = await zeroEx.getLimitOrderInfo(order).callAsync();
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Filled, // Still reports filled.
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: order.takerAmount,
|
|
});
|
|
});
|
|
|
|
it('filled order', async () => {
|
|
const order = getTestLimitOrder();
|
|
// Fill the order first.
|
|
await testUtils.fillLimitOrderAsync(order);
|
|
const info = await zeroEx.getLimitOrderInfo(order).callAsync();
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Filled,
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: order.takerAmount,
|
|
});
|
|
});
|
|
|
|
it('partially filled order', async () => {
|
|
const order = getTestLimitOrder();
|
|
const fillAmount = order.takerAmount.minus(1);
|
|
// Fill the order first.
|
|
await testUtils.fillLimitOrderAsync(order, { fillAmount });
|
|
const info = await zeroEx.getLimitOrderInfo(order).callAsync();
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Fillable,
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: fillAmount,
|
|
});
|
|
});
|
|
|
|
it('filled then cancelled order', async () => {
|
|
const order = getTestLimitOrder();
|
|
// Fill the order first.
|
|
await testUtils.fillLimitOrderAsync(order);
|
|
await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
const info = await zeroEx.getLimitOrderInfo(order).callAsync();
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Filled, // Still reports filled.
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: order.takerAmount,
|
|
});
|
|
});
|
|
|
|
it('partially filled then cancelled order', async () => {
|
|
const order = getTestLimitOrder();
|
|
const fillAmount = order.takerAmount.minus(1);
|
|
// Fill the order first.
|
|
await testUtils.fillLimitOrderAsync(order, { fillAmount });
|
|
await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
const info = await zeroEx.getLimitOrderInfo(order).callAsync();
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Cancelled,
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: fillAmount,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getRfqOrderInfo()', () => {
|
|
it('unfilled order', async () => {
|
|
const order = getTestRfqOrder();
|
|
const info = await zeroEx.getRfqOrderInfo(order).callAsync();
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Fillable,
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: ZERO_AMOUNT,
|
|
});
|
|
});
|
|
|
|
it('unfilled cancelled order', async () => {
|
|
const order = getTestRfqOrder();
|
|
await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
const info = await zeroEx.getRfqOrderInfo(order).callAsync();
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Cancelled,
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: ZERO_AMOUNT,
|
|
});
|
|
});
|
|
|
|
it('unfilled expired order', async () => {
|
|
const expiry = createExpiry(-60);
|
|
const order = getTestRfqOrder({ expiry });
|
|
const info = await zeroEx.getRfqOrderInfo(order).callAsync();
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Expired,
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: ZERO_AMOUNT,
|
|
});
|
|
});
|
|
|
|
it('filled then expired order', async () => {
|
|
const expiry = createExpiry(60);
|
|
const order = getTestRfqOrder({ expiry });
|
|
await testUtils.prepareBalancesForOrdersAsync([order]);
|
|
const sig = await order.getSignatureWithProviderAsync(env.provider);
|
|
// Fill the order first.
|
|
await zeroEx.fillRfqOrder(order, sig, order.takerAmount).awaitTransactionSuccessAsync({ from: taker });
|
|
// Advance time to expire the order.
|
|
await env.web3Wrapper.increaseTimeAsync(61);
|
|
const info = await zeroEx.getRfqOrderInfo(order).callAsync();
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Filled, // Still reports filled.
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: order.takerAmount,
|
|
});
|
|
});
|
|
|
|
it('filled order', async () => {
|
|
const order = getTestRfqOrder();
|
|
// Fill the order first.
|
|
await testUtils.fillRfqOrderAsync(order, order.takerAmount, taker);
|
|
const info = await zeroEx.getRfqOrderInfo(order).callAsync();
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Filled,
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: order.takerAmount,
|
|
});
|
|
});
|
|
|
|
it('partially filled order', async () => {
|
|
const order = getTestRfqOrder();
|
|
const fillAmount = order.takerAmount.minus(1);
|
|
// Fill the order first.
|
|
await testUtils.fillRfqOrderAsync(order, fillAmount);
|
|
const info = await zeroEx.getRfqOrderInfo(order).callAsync();
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Fillable,
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: fillAmount,
|
|
});
|
|
});
|
|
|
|
it('filled then cancelled order', async () => {
|
|
const order = getTestRfqOrder();
|
|
// Fill the order first.
|
|
await testUtils.fillRfqOrderAsync(order);
|
|
await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
const info = await zeroEx.getRfqOrderInfo(order).callAsync();
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Filled, // Still reports filled.
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: order.takerAmount,
|
|
});
|
|
});
|
|
|
|
it('partially filled then cancelled order', async () => {
|
|
const order = getTestRfqOrder();
|
|
const fillAmount = order.takerAmount.minus(1);
|
|
// Fill the order first.
|
|
await testUtils.fillRfqOrderAsync(order, fillAmount);
|
|
await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
const info = await zeroEx.getRfqOrderInfo(order).callAsync();
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Cancelled,
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: fillAmount,
|
|
});
|
|
});
|
|
|
|
it('invalid origin', async () => {
|
|
const order = getTestRfqOrder({ txOrigin: NULL_ADDRESS });
|
|
const info = await zeroEx.getRfqOrderInfo(order).callAsync();
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Invalid,
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: ZERO_AMOUNT,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('cancelLimitOrder()', async () => {
|
|
it('can cancel an unfilled order', async () => {
|
|
const order = getTestLimitOrder();
|
|
const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[{ maker: order.maker, orderHash: order.getHash() }],
|
|
IZeroExEvents.OrderCancelled,
|
|
);
|
|
const { status } = await zeroEx.getLimitOrderInfo(order).callAsync();
|
|
expect(status).to.eq(OrderStatus.Cancelled);
|
|
});
|
|
|
|
it('can cancel a fully filled order', async () => {
|
|
const order = getTestLimitOrder();
|
|
await testUtils.fillLimitOrderAsync(order);
|
|
const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[{ maker: order.maker, orderHash: order.getHash() }],
|
|
IZeroExEvents.OrderCancelled,
|
|
);
|
|
const { status } = await zeroEx.getLimitOrderInfo(order).callAsync();
|
|
expect(status).to.eq(OrderStatus.Filled); // Still reports filled.
|
|
});
|
|
|
|
it('can cancel a partially filled order', async () => {
|
|
const order = getTestLimitOrder();
|
|
await testUtils.fillLimitOrderAsync(order, { fillAmount: order.takerAmount.minus(1) });
|
|
const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[{ maker: order.maker, orderHash: order.getHash() }],
|
|
IZeroExEvents.OrderCancelled,
|
|
);
|
|
const { status } = await zeroEx.getLimitOrderInfo(order).callAsync();
|
|
expect(status).to.eq(OrderStatus.Cancelled);
|
|
});
|
|
|
|
it('can cancel an expired order', async () => {
|
|
const expiry = createExpiry(-60);
|
|
const order = getTestLimitOrder({ expiry });
|
|
const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[{ maker: order.maker, orderHash: order.getHash() }],
|
|
IZeroExEvents.OrderCancelled,
|
|
);
|
|
const { status } = await zeroEx.getLimitOrderInfo(order).callAsync();
|
|
expect(status).to.eq(OrderStatus.Cancelled);
|
|
});
|
|
|
|
it('can cancel a cancelled order', async () => {
|
|
const order = getTestLimitOrder();
|
|
await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[{ maker: order.maker, orderHash: order.getHash() }],
|
|
IZeroExEvents.OrderCancelled,
|
|
);
|
|
const { status } = await zeroEx.getLimitOrderInfo(order).callAsync();
|
|
expect(status).to.eq(OrderStatus.Cancelled);
|
|
});
|
|
|
|
it("cannot cancel someone else's order", async () => {
|
|
const order = getTestLimitOrder();
|
|
const tx = zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: notMaker });
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OnlyOrderMakerAllowed(order.getHash(), notMaker, order.maker),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('cancelRfqOrder()', async () => {
|
|
it('can cancel an unfilled order', async () => {
|
|
const order = getTestRfqOrder();
|
|
const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[{ maker: order.maker, orderHash: order.getHash() }],
|
|
IZeroExEvents.OrderCancelled,
|
|
);
|
|
const { status } = await zeroEx.getRfqOrderInfo(order).callAsync();
|
|
expect(status).to.eq(OrderStatus.Cancelled);
|
|
});
|
|
|
|
it('can cancel a fully filled order', async () => {
|
|
const order = getTestRfqOrder();
|
|
await testUtils.fillRfqOrderAsync(order);
|
|
const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[{ maker: order.maker, orderHash: order.getHash() }],
|
|
IZeroExEvents.OrderCancelled,
|
|
);
|
|
const { status } = await zeroEx.getRfqOrderInfo(order).callAsync();
|
|
expect(status).to.eq(OrderStatus.Filled); // Still reports filled.
|
|
});
|
|
|
|
it('can cancel a partially filled order', async () => {
|
|
const order = getTestRfqOrder();
|
|
await testUtils.fillRfqOrderAsync(order, order.takerAmount.minus(1));
|
|
const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[{ maker: order.maker, orderHash: order.getHash() }],
|
|
IZeroExEvents.OrderCancelled,
|
|
);
|
|
const { status } = await zeroEx.getRfqOrderInfo(order).callAsync();
|
|
expect(status).to.eq(OrderStatus.Cancelled); // Still reports filled.
|
|
});
|
|
|
|
it('can cancel an expired order', async () => {
|
|
const expiry = createExpiry(-60);
|
|
const order = getTestRfqOrder({ expiry });
|
|
const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[{ maker: order.maker, orderHash: order.getHash() }],
|
|
IZeroExEvents.OrderCancelled,
|
|
);
|
|
const { status } = await zeroEx.getRfqOrderInfo(order).callAsync();
|
|
expect(status).to.eq(OrderStatus.Cancelled);
|
|
});
|
|
|
|
it('can cancel a cancelled order', async () => {
|
|
const order = getTestRfqOrder();
|
|
await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[{ maker: order.maker, orderHash: order.getHash() }],
|
|
IZeroExEvents.OrderCancelled,
|
|
);
|
|
const { status } = await zeroEx.getRfqOrderInfo(order).callAsync();
|
|
expect(status).to.eq(OrderStatus.Cancelled);
|
|
});
|
|
|
|
it("cannot cancel someone else's order", async () => {
|
|
const order = getTestRfqOrder();
|
|
const tx = zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: notMaker });
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OnlyOrderMakerAllowed(order.getHash(), notMaker, order.maker),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('batchCancelLimitOrders()', async () => {
|
|
it('can cancel multiple orders', async () => {
|
|
const orders = [...new Array(3)].map(() => getTestLimitOrder());
|
|
const receipt = await zeroEx.batchCancelLimitOrders(orders).awaitTransactionSuccessAsync({ from: maker });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
orders.map(o => ({ maker: o.maker, orderHash: o.getHash() })),
|
|
IZeroExEvents.OrderCancelled,
|
|
);
|
|
const infos = await Promise.all(orders.map(o => zeroEx.getLimitOrderInfo(o).callAsync()));
|
|
expect(infos.map(i => i.status)).to.deep.eq(infos.map(() => OrderStatus.Cancelled));
|
|
});
|
|
|
|
it("cannot cancel someone else's orders", async () => {
|
|
const orders = [...new Array(3)].map(() => getTestLimitOrder());
|
|
const tx = zeroEx.batchCancelLimitOrders(orders).awaitTransactionSuccessAsync({ from: notMaker });
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OnlyOrderMakerAllowed(orders[0].getHash(), notMaker, orders[0].maker),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('batchCancelRfqOrders()', async () => {
|
|
it('can cancel multiple orders', async () => {
|
|
const orders = [...new Array(3)].map(() => getTestRfqOrder());
|
|
const receipt = await zeroEx.batchCancelRfqOrders(orders).awaitTransactionSuccessAsync({ from: maker });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
orders.map(o => ({ maker: o.maker, orderHash: o.getHash() })),
|
|
IZeroExEvents.OrderCancelled,
|
|
);
|
|
const infos = await Promise.all(orders.map(o => zeroEx.getRfqOrderInfo(o).callAsync()));
|
|
expect(infos.map(i => i.status)).to.deep.eq(infos.map(() => OrderStatus.Cancelled));
|
|
});
|
|
|
|
it("cannot cancel someone else's orders", async () => {
|
|
const orders = [...new Array(3)].map(() => getTestRfqOrder());
|
|
const tx = zeroEx.batchCancelRfqOrders(orders).awaitTransactionSuccessAsync({ from: notMaker });
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OnlyOrderMakerAllowed(orders[0].getHash(), notMaker, orders[0].maker),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('cancelPairOrders()', async () => {
|
|
it('can cancel multiple limit orders of the same pair with salt < minValidSalt', async () => {
|
|
const orders = [...new Array(3)].map((_v, i) => getTestLimitOrder().clone({ salt: new BigNumber(i) }));
|
|
// Cancel the first two orders.
|
|
const minValidSalt = orders[2].salt;
|
|
const receipt = await zeroEx
|
|
.cancelPairLimitOrders(makerToken.address, takerToken.address, minValidSalt)
|
|
.awaitTransactionSuccessAsync({ from: maker });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[
|
|
{
|
|
maker,
|
|
makerToken: makerToken.address,
|
|
takerToken: takerToken.address,
|
|
minValidSalt,
|
|
},
|
|
],
|
|
IZeroExEvents.PairCancelledLimitOrders,
|
|
);
|
|
const statuses = (await Promise.all(orders.map(o => zeroEx.getLimitOrderInfo(o).callAsync()))).map(
|
|
oi => oi.status,
|
|
);
|
|
expect(statuses).to.deep.eq([OrderStatus.Cancelled, OrderStatus.Cancelled, OrderStatus.Fillable]);
|
|
});
|
|
|
|
it('does not cancel limit orders of a different pair', async () => {
|
|
const order = getRandomLimitOrder({ salt: new BigNumber(1) });
|
|
// Cancel salts <= the order's, but flip the tokens to be a different
|
|
// pair.
|
|
const minValidSalt = order.salt.plus(1);
|
|
await zeroEx
|
|
.cancelPairLimitOrders(takerToken.address, makerToken.address, minValidSalt)
|
|
.awaitTransactionSuccessAsync({ from: maker });
|
|
const { status } = await zeroEx.getLimitOrderInfo(order).callAsync();
|
|
expect(status).to.eq(OrderStatus.Fillable);
|
|
});
|
|
|
|
it('can cancel multiple RFQ orders of the same pair with salt < minValidSalt', async () => {
|
|
const orders = [...new Array(3)].map((_v, i) => getTestRfqOrder().clone({ salt: new BigNumber(i) }));
|
|
// Cancel the first two orders.
|
|
const minValidSalt = orders[2].salt;
|
|
const receipt = await zeroEx
|
|
.cancelPairRfqOrders(makerToken.address, takerToken.address, minValidSalt)
|
|
.awaitTransactionSuccessAsync({ from: maker });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[
|
|
{
|
|
maker,
|
|
makerToken: makerToken.address,
|
|
takerToken: takerToken.address,
|
|
minValidSalt,
|
|
},
|
|
],
|
|
IZeroExEvents.PairCancelledRfqOrders,
|
|
);
|
|
const statuses = (await Promise.all(orders.map(o => zeroEx.getRfqOrderInfo(o).callAsync()))).map(
|
|
oi => oi.status,
|
|
);
|
|
expect(statuses).to.deep.eq([OrderStatus.Cancelled, OrderStatus.Cancelled, OrderStatus.Fillable]);
|
|
});
|
|
|
|
it('does not cancel RFQ orders of a different pair', async () => {
|
|
const order = getRandomRfqOrder({ salt: new BigNumber(1) });
|
|
// Cancel salts <= the order's, but flip the tokens to be a different
|
|
// pair.
|
|
const minValidSalt = order.salt.plus(1);
|
|
await zeroEx
|
|
.cancelPairRfqOrders(takerToken.address, makerToken.address, minValidSalt)
|
|
.awaitTransactionSuccessAsync({ from: maker });
|
|
const { status } = await zeroEx.getRfqOrderInfo(order).callAsync();
|
|
expect(status).to.eq(OrderStatus.Fillable);
|
|
});
|
|
});
|
|
|
|
describe('batchCancelPairOrders()', async () => {
|
|
it('can cancel multiple limit order pairs', async () => {
|
|
const orders = [
|
|
getTestLimitOrder({ salt: new BigNumber(1) }),
|
|
// Flip the tokens for the other order.
|
|
getTestLimitOrder({
|
|
makerToken: takerToken.address,
|
|
takerToken: makerToken.address,
|
|
salt: new BigNumber(1),
|
|
}),
|
|
];
|
|
const minValidSalt = new BigNumber(2);
|
|
const receipt = await zeroEx
|
|
.batchCancelPairLimitOrders(
|
|
[makerToken.address, takerToken.address],
|
|
[takerToken.address, makerToken.address],
|
|
[minValidSalt, minValidSalt],
|
|
)
|
|
.awaitTransactionSuccessAsync({ from: maker });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[
|
|
{
|
|
maker,
|
|
makerToken: makerToken.address,
|
|
takerToken: takerToken.address,
|
|
minValidSalt,
|
|
},
|
|
{
|
|
maker,
|
|
makerToken: takerToken.address,
|
|
takerToken: makerToken.address,
|
|
minValidSalt,
|
|
},
|
|
],
|
|
IZeroExEvents.PairCancelledLimitOrders,
|
|
);
|
|
const statuses = (await Promise.all(orders.map(o => zeroEx.getLimitOrderInfo(o).callAsync()))).map(
|
|
oi => oi.status,
|
|
);
|
|
expect(statuses).to.deep.eq([OrderStatus.Cancelled, OrderStatus.Cancelled]);
|
|
});
|
|
|
|
it('can cancel multiple RFQ order pairs', async () => {
|
|
const orders = [
|
|
getTestRfqOrder({ salt: new BigNumber(1) }),
|
|
// Flip the tokens for the other order.
|
|
getTestRfqOrder({
|
|
makerToken: takerToken.address,
|
|
takerToken: makerToken.address,
|
|
salt: new BigNumber(1),
|
|
}),
|
|
];
|
|
const minValidSalt = new BigNumber(2);
|
|
const receipt = await zeroEx
|
|
.batchCancelPairRfqOrders(
|
|
[makerToken.address, takerToken.address],
|
|
[takerToken.address, makerToken.address],
|
|
[minValidSalt, minValidSalt],
|
|
)
|
|
.awaitTransactionSuccessAsync({ from: maker });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[
|
|
{
|
|
maker,
|
|
makerToken: makerToken.address,
|
|
takerToken: takerToken.address,
|
|
minValidSalt,
|
|
},
|
|
{
|
|
maker,
|
|
makerToken: takerToken.address,
|
|
takerToken: makerToken.address,
|
|
minValidSalt,
|
|
},
|
|
],
|
|
IZeroExEvents.PairCancelledRfqOrders,
|
|
);
|
|
const statuses = (await Promise.all(orders.map(o => zeroEx.getRfqOrderInfo(o).callAsync()))).map(
|
|
oi => oi.status,
|
|
);
|
|
expect(statuses).to.deep.eq([OrderStatus.Cancelled, OrderStatus.Cancelled]);
|
|
});
|
|
});
|
|
|
|
async function assertExpectedFinalBalancesFromLimitOrderFillAsync(
|
|
order: LimitOrder,
|
|
opts: Partial<{
|
|
takerTokenFillAmount: BigNumber;
|
|
takerTokenAlreadyFilledAmount: BigNumber;
|
|
receipt: TransactionReceiptWithDecodedLogs;
|
|
}> = {},
|
|
): Promise<void> {
|
|
const { takerTokenFillAmount, takerTokenAlreadyFilledAmount, receipt } = {
|
|
takerTokenFillAmount: order.takerAmount,
|
|
takerTokenAlreadyFilledAmount: ZERO_AMOUNT,
|
|
receipt: undefined,
|
|
...opts,
|
|
};
|
|
const { makerTokenFilledAmount, takerTokenFilledAmount, takerTokenFeeFilledAmount } =
|
|
computeLimitOrderFilledAmounts(order, takerTokenFillAmount, takerTokenAlreadyFilledAmount);
|
|
const makerBalance = await takerToken.balanceOf(maker).callAsync();
|
|
const takerBalance = await makerToken.balanceOf(taker).callAsync();
|
|
const feeRecipientBalance = await takerToken.balanceOf(order.feeRecipient).callAsync();
|
|
expect(makerBalance).to.bignumber.eq(takerTokenFilledAmount);
|
|
expect(takerBalance).to.bignumber.eq(makerTokenFilledAmount);
|
|
expect(feeRecipientBalance).to.bignumber.eq(takerTokenFeeFilledAmount);
|
|
if (receipt) {
|
|
const balanceOfTakerNow = await env.web3Wrapper.getBalanceInWeiAsync(taker);
|
|
const balanceOfTakerBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker, receipt.blockNumber - 1);
|
|
const protocolFee = order.taker === NULL_ADDRESS ? SINGLE_PROTOCOL_FEE : 0;
|
|
const totalCost = GAS_PRICE.times(receipt.gasUsed).plus(protocolFee);
|
|
expect(balanceOfTakerBefore.minus(totalCost)).to.bignumber.eq(balanceOfTakerNow);
|
|
}
|
|
}
|
|
|
|
describe('fillLimitOrder()', () => {
|
|
it('can fully fill an order', async () => {
|
|
const order = getTestLimitOrder();
|
|
const receipt = await testUtils.fillLimitOrderAsync(order);
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[testUtils.createLimitOrderFilledEventArgs(order)],
|
|
IZeroExEvents.LimitOrderFilled,
|
|
);
|
|
assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), {
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Filled,
|
|
takerTokenFilledAmount: order.takerAmount,
|
|
});
|
|
await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { receipt });
|
|
});
|
|
|
|
it('can partially fill an order', async () => {
|
|
const order = getTestLimitOrder();
|
|
const fillAmount = order.takerAmount.minus(1);
|
|
const receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[testUtils.createLimitOrderFilledEventArgs(order, fillAmount)],
|
|
IZeroExEvents.LimitOrderFilled,
|
|
);
|
|
assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), {
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Fillable,
|
|
takerTokenFilledAmount: fillAmount,
|
|
});
|
|
await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, {
|
|
takerTokenFillAmount: fillAmount,
|
|
});
|
|
});
|
|
|
|
it('can fully fill an order in two steps', async () => {
|
|
const order = getTestLimitOrder();
|
|
let fillAmount = order.takerAmount.dividedToIntegerBy(2);
|
|
let receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[testUtils.createLimitOrderFilledEventArgs(order, fillAmount)],
|
|
IZeroExEvents.LimitOrderFilled,
|
|
);
|
|
const alreadyFilledAmount = fillAmount;
|
|
fillAmount = order.takerAmount.minus(fillAmount);
|
|
receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[testUtils.createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
|
|
IZeroExEvents.LimitOrderFilled,
|
|
);
|
|
assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), {
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Filled,
|
|
takerTokenFilledAmount: order.takerAmount,
|
|
});
|
|
});
|
|
|
|
it('clamps fill amount to remaining available', async () => {
|
|
const order = getTestLimitOrder();
|
|
const fillAmount = order.takerAmount.plus(1);
|
|
const receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[testUtils.createLimitOrderFilledEventArgs(order, fillAmount)],
|
|
IZeroExEvents.LimitOrderFilled,
|
|
);
|
|
assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), {
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Filled,
|
|
takerTokenFilledAmount: order.takerAmount,
|
|
});
|
|
await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, {
|
|
takerTokenFillAmount: fillAmount,
|
|
});
|
|
});
|
|
|
|
it('clamps fill amount to remaining available in partial filled order', async () => {
|
|
const order = getTestLimitOrder();
|
|
let fillAmount = order.takerAmount.dividedToIntegerBy(2);
|
|
let receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[testUtils.createLimitOrderFilledEventArgs(order, fillAmount)],
|
|
IZeroExEvents.LimitOrderFilled,
|
|
);
|
|
const alreadyFilledAmount = fillAmount;
|
|
fillAmount = order.takerAmount.minus(fillAmount).plus(1);
|
|
receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[testUtils.createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
|
|
IZeroExEvents.LimitOrderFilled,
|
|
);
|
|
assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), {
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Filled,
|
|
takerTokenFilledAmount: order.takerAmount,
|
|
});
|
|
});
|
|
|
|
it('cannot fill an expired order', async () => {
|
|
const order = getTestLimitOrder({ expiry: createExpiry(-60) });
|
|
const tx = testUtils.fillLimitOrderAsync(order);
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Expired),
|
|
);
|
|
});
|
|
|
|
it('cannot fill a cancelled order', async () => {
|
|
const order = getTestLimitOrder();
|
|
await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
const tx = testUtils.fillLimitOrderAsync(order);
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled),
|
|
);
|
|
});
|
|
|
|
it('cannot fill a salt/pair cancelled order', async () => {
|
|
const order = getTestLimitOrder();
|
|
await zeroEx
|
|
.cancelPairLimitOrders(makerToken.address, takerToken.address, order.salt.plus(1))
|
|
.awaitTransactionSuccessAsync({ from: maker });
|
|
const tx = testUtils.fillLimitOrderAsync(order);
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled),
|
|
);
|
|
});
|
|
|
|
it('non-taker cannot fill order', async () => {
|
|
const order = getTestLimitOrder({ taker });
|
|
const tx = testUtils.fillLimitOrderAsync(order, { fillAmount: order.takerAmount, taker: notTaker });
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OrderNotFillableByTakerError(order.getHash(), notTaker, order.taker),
|
|
);
|
|
});
|
|
|
|
it('non-sender cannot fill order', async () => {
|
|
const order = getTestLimitOrder({ sender: taker });
|
|
const tx = testUtils.fillLimitOrderAsync(order, { fillAmount: order.takerAmount, taker: notTaker });
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OrderNotFillableBySenderError(order.getHash(), notTaker, order.sender),
|
|
);
|
|
});
|
|
|
|
it('cannot fill order with bad signature', async () => {
|
|
const order = getTestLimitOrder();
|
|
// Overwrite chainId to result in a different hash and therefore different
|
|
// signature.
|
|
const tx = testUtils.fillLimitOrderAsync(order.clone({ chainId: 1234 }));
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), undefined, order.maker),
|
|
);
|
|
});
|
|
|
|
// TODO: dekz Ganache gasPrice opcode is returning 0, cannot influence it up to test this case
|
|
it.skip('fails if no protocol fee attached', async () => {
|
|
const order = getTestLimitOrder();
|
|
await testUtils.prepareBalancesForOrdersAsync([order]);
|
|
const tx = zeroEx
|
|
.fillLimitOrder(
|
|
order,
|
|
await order.getSignatureWithProviderAsync(env.provider),
|
|
new BigNumber(order.takerAmount),
|
|
)
|
|
.awaitTransactionSuccessAsync({ from: taker, value: ZERO_AMOUNT });
|
|
// The exact revert error depends on whether we are still doing a
|
|
// token spender fallthroigh, so we won't get too specific.
|
|
return expect(tx).to.revertWith(new AnyRevertError());
|
|
});
|
|
|
|
it('refunds excess protocol fee', async () => {
|
|
const order = getTestLimitOrder();
|
|
const receipt = await testUtils.fillLimitOrderAsync(order, { protocolFee: SINGLE_PROTOCOL_FEE.plus(1) });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[testUtils.createLimitOrderFilledEventArgs(order)],
|
|
IZeroExEvents.LimitOrderFilled,
|
|
);
|
|
await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { receipt });
|
|
});
|
|
});
|
|
|
|
describe('registerAllowedRfqOrigins()', () => {
|
|
it('cannot register through a contract', async () => {
|
|
const tx = testRfqOriginRegistration
|
|
.registerAllowedRfqOrigins(zeroEx.address, [], true)
|
|
.awaitTransactionSuccessAsync();
|
|
expect(tx).to.revertWith('NativeOrdersFeature/NO_CONTRACT_ORIGINS');
|
|
});
|
|
});
|
|
|
|
async function assertExpectedFinalBalancesFromRfqOrderFillAsync(
|
|
order: RfqOrder,
|
|
takerTokenFillAmount: BigNumber = order.takerAmount,
|
|
takerTokenAlreadyFilledAmount: BigNumber = ZERO_AMOUNT,
|
|
): Promise<void> {
|
|
const { makerTokenFilledAmount, takerTokenFilledAmount } = computeRfqOrderFilledAmounts(
|
|
order,
|
|
takerTokenFillAmount,
|
|
takerTokenAlreadyFilledAmount,
|
|
);
|
|
const makerBalance = await takerToken.balanceOf(maker).callAsync();
|
|
const takerBalance = await makerToken.balanceOf(taker).callAsync();
|
|
expect(makerBalance).to.bignumber.eq(takerTokenFilledAmount);
|
|
expect(takerBalance).to.bignumber.eq(makerTokenFilledAmount);
|
|
}
|
|
|
|
describe('fillRfqOrder()', () => {
|
|
it('can fully fill an order', async () => {
|
|
const order = getTestRfqOrder();
|
|
const receipt = await testUtils.fillRfqOrderAsync(order);
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[testUtils.createRfqOrderFilledEventArgs(order)],
|
|
IZeroExEvents.RfqOrderFilled,
|
|
);
|
|
assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), {
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Filled,
|
|
takerTokenFilledAmount: order.takerAmount,
|
|
});
|
|
await assertExpectedFinalBalancesFromRfqOrderFillAsync(order);
|
|
});
|
|
|
|
it('can partially fill an order', async () => {
|
|
const order = getTestRfqOrder();
|
|
const fillAmount = order.takerAmount.minus(1);
|
|
const receipt = await testUtils.fillRfqOrderAsync(order, fillAmount);
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[testUtils.createRfqOrderFilledEventArgs(order, fillAmount)],
|
|
IZeroExEvents.RfqOrderFilled,
|
|
);
|
|
assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), {
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Fillable,
|
|
takerTokenFilledAmount: fillAmount,
|
|
});
|
|
await assertExpectedFinalBalancesFromRfqOrderFillAsync(order, fillAmount);
|
|
});
|
|
|
|
it('can fully fill an order in two steps', async () => {
|
|
const order = getTestRfqOrder();
|
|
let fillAmount = order.takerAmount.dividedToIntegerBy(2);
|
|
let receipt = await testUtils.fillRfqOrderAsync(order, fillAmount);
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[testUtils.createRfqOrderFilledEventArgs(order, fillAmount)],
|
|
IZeroExEvents.RfqOrderFilled,
|
|
);
|
|
const alreadyFilledAmount = fillAmount;
|
|
fillAmount = order.takerAmount.minus(fillAmount);
|
|
receipt = await testUtils.fillRfqOrderAsync(order, fillAmount);
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[testUtils.createRfqOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
|
|
IZeroExEvents.RfqOrderFilled,
|
|
);
|
|
assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), {
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Filled,
|
|
takerTokenFilledAmount: order.takerAmount,
|
|
});
|
|
});
|
|
|
|
it('clamps fill amount to remaining available', async () => {
|
|
const order = getTestRfqOrder();
|
|
const fillAmount = order.takerAmount.plus(1);
|
|
const receipt = await testUtils.fillRfqOrderAsync(order, fillAmount);
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[testUtils.createRfqOrderFilledEventArgs(order, fillAmount)],
|
|
IZeroExEvents.RfqOrderFilled,
|
|
);
|
|
assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), {
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Filled,
|
|
takerTokenFilledAmount: order.takerAmount,
|
|
});
|
|
await assertExpectedFinalBalancesFromRfqOrderFillAsync(order, fillAmount);
|
|
});
|
|
|
|
it('clamps fill amount to remaining available in partial filled order', async () => {
|
|
const order = getTestRfqOrder();
|
|
let fillAmount = order.takerAmount.dividedToIntegerBy(2);
|
|
let receipt = await testUtils.fillRfqOrderAsync(order, fillAmount);
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[testUtils.createRfqOrderFilledEventArgs(order, fillAmount)],
|
|
IZeroExEvents.RfqOrderFilled,
|
|
);
|
|
const alreadyFilledAmount = fillAmount;
|
|
fillAmount = order.takerAmount.minus(fillAmount).plus(1);
|
|
receipt = await testUtils.fillRfqOrderAsync(order, fillAmount);
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[testUtils.createRfqOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
|
|
IZeroExEvents.RfqOrderFilled,
|
|
);
|
|
assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), {
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Filled,
|
|
takerTokenFilledAmount: order.takerAmount,
|
|
});
|
|
});
|
|
|
|
it('cannot fill an order with wrong tx.origin', async () => {
|
|
const order = getTestRfqOrder();
|
|
const tx = testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker);
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), notTaker, taker),
|
|
);
|
|
});
|
|
|
|
it('can fill an order from a different tx.origin if registered', async () => {
|
|
const order = getTestRfqOrder();
|
|
|
|
const receipt = await zeroEx
|
|
.registerAllowedRfqOrigins([notTaker], true)
|
|
.awaitTransactionSuccessAsync({ from: taker });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[
|
|
{
|
|
origin: taker,
|
|
addrs: [notTaker],
|
|
allowed: true,
|
|
},
|
|
],
|
|
IZeroExEvents.RfqOrderOriginsAllowed,
|
|
);
|
|
return testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker);
|
|
});
|
|
|
|
it('cannot fill an order with registered then unregistered tx.origin', async () => {
|
|
const order = getTestRfqOrder();
|
|
|
|
await zeroEx.registerAllowedRfqOrigins([notTaker], true).awaitTransactionSuccessAsync({ from: taker });
|
|
const receipt = await zeroEx
|
|
.registerAllowedRfqOrigins([notTaker], false)
|
|
.awaitTransactionSuccessAsync({ from: taker });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[
|
|
{
|
|
origin: taker,
|
|
addrs: [notTaker],
|
|
allowed: false,
|
|
},
|
|
],
|
|
IZeroExEvents.RfqOrderOriginsAllowed,
|
|
);
|
|
|
|
const tx = testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker);
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), notTaker, taker),
|
|
);
|
|
});
|
|
|
|
it('cannot fill an order with a zero tx.origin', async () => {
|
|
const order = getTestRfqOrder({ txOrigin: NULL_ADDRESS });
|
|
const tx = testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker);
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Invalid),
|
|
);
|
|
});
|
|
|
|
it('non-taker cannot fill order', async () => {
|
|
const order = getTestRfqOrder({ taker, txOrigin: notTaker });
|
|
const tx = testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker);
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OrderNotFillableByTakerError(order.getHash(), notTaker, order.taker),
|
|
);
|
|
});
|
|
|
|
it('cannot fill an expired order', async () => {
|
|
const order = getTestRfqOrder({ expiry: createExpiry(-60) });
|
|
const tx = testUtils.fillRfqOrderAsync(order);
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Expired),
|
|
);
|
|
});
|
|
|
|
it('cannot fill a cancelled order', async () => {
|
|
const order = getTestRfqOrder();
|
|
await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
const tx = testUtils.fillRfqOrderAsync(order);
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled),
|
|
);
|
|
});
|
|
|
|
it('cannot fill a salt/pair cancelled order', async () => {
|
|
const order = getTestRfqOrder();
|
|
await zeroEx
|
|
.cancelPairRfqOrders(makerToken.address, takerToken.address, order.salt.plus(1))
|
|
.awaitTransactionSuccessAsync({ from: maker });
|
|
const tx = testUtils.fillRfqOrderAsync(order);
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled),
|
|
);
|
|
});
|
|
|
|
it('cannot fill order with bad signature', async () => {
|
|
const order = getTestRfqOrder();
|
|
// Overwrite chainId to result in a different hash and therefore different
|
|
// signature.
|
|
const tx = testUtils.fillRfqOrderAsync(order.clone({ chainId: 1234 }));
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), undefined, order.maker),
|
|
);
|
|
});
|
|
|
|
it('fails if ETH is attached', async () => {
|
|
const order = getTestRfqOrder();
|
|
await testUtils.prepareBalancesForOrdersAsync([order], taker);
|
|
const tx = zeroEx
|
|
.fillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount)
|
|
.awaitTransactionSuccessAsync({ from: taker, value: 1 });
|
|
// This will revert at the language level because the fill function is not payable.
|
|
return expect(tx).to.be.rejectedWith('revert');
|
|
});
|
|
});
|
|
|
|
describe('fillOrKillLimitOrder()', () => {
|
|
it('can fully fill an order', async () => {
|
|
const order = getTestLimitOrder();
|
|
await testUtils.prepareBalancesForOrdersAsync([order]);
|
|
const receipt = await zeroEx
|
|
.fillOrKillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount)
|
|
.awaitTransactionSuccessAsync({ from: taker, value: SINGLE_PROTOCOL_FEE });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[testUtils.createLimitOrderFilledEventArgs(order)],
|
|
IZeroExEvents.LimitOrderFilled,
|
|
);
|
|
});
|
|
|
|
it('reverts if cannot fill the exact amount', async () => {
|
|
const order = getTestLimitOrder();
|
|
await testUtils.prepareBalancesForOrdersAsync([order]);
|
|
const fillAmount = order.takerAmount.plus(1);
|
|
const tx = zeroEx
|
|
.fillOrKillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), fillAmount)
|
|
.awaitTransactionSuccessAsync({ from: taker, value: SINGLE_PROTOCOL_FEE });
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.FillOrKillFailedError(order.getHash(), order.takerAmount, fillAmount),
|
|
);
|
|
});
|
|
|
|
it('refunds excess protocol fee', async () => {
|
|
const order = getTestLimitOrder();
|
|
await testUtils.prepareBalancesForOrdersAsync([order]);
|
|
const takerBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker);
|
|
const receipt = await zeroEx
|
|
.fillOrKillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount)
|
|
.awaitTransactionSuccessAsync({ from: taker, value: SINGLE_PROTOCOL_FEE.plus(1) });
|
|
const takerBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker);
|
|
const totalCost = GAS_PRICE.times(receipt.gasUsed).plus(SINGLE_PROTOCOL_FEE);
|
|
expect(takerBalanceBefore.minus(totalCost)).to.bignumber.eq(takerBalanceAfter);
|
|
});
|
|
});
|
|
|
|
describe('fillOrKillRfqOrder()', () => {
|
|
it('can fully fill an order', async () => {
|
|
const order = getTestRfqOrder();
|
|
await testUtils.prepareBalancesForOrdersAsync([order]);
|
|
const receipt = await zeroEx
|
|
.fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount)
|
|
.awaitTransactionSuccessAsync({ from: taker });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[testUtils.createRfqOrderFilledEventArgs(order)],
|
|
IZeroExEvents.RfqOrderFilled,
|
|
);
|
|
});
|
|
|
|
it('reverts if cannot fill the exact amount', async () => {
|
|
const order = getTestRfqOrder();
|
|
await testUtils.prepareBalancesForOrdersAsync([order]);
|
|
const fillAmount = order.takerAmount.plus(1);
|
|
const tx = zeroEx
|
|
.fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), fillAmount)
|
|
.awaitTransactionSuccessAsync({ from: taker });
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.FillOrKillFailedError(order.getHash(), order.takerAmount, fillAmount),
|
|
);
|
|
});
|
|
|
|
it('fails if ETH is attached', async () => {
|
|
const order = getTestRfqOrder();
|
|
await testUtils.prepareBalancesForOrdersAsync([order]);
|
|
const tx = zeroEx
|
|
.fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount)
|
|
.awaitTransactionSuccessAsync({ from: taker, value: 1 });
|
|
// This will revert at the language level because the fill function is not payable.
|
|
return expect(tx).to.be.rejectedWith('revert');
|
|
});
|
|
});
|
|
|
|
async function fundOrderMakerAsync(
|
|
order: LimitOrder | RfqOrder,
|
|
balance: BigNumber = order.makerAmount,
|
|
allowance: BigNumber = order.makerAmount,
|
|
): Promise<void> {
|
|
await makerToken.burn(maker, await makerToken.balanceOf(maker).callAsync()).awaitTransactionSuccessAsync();
|
|
await makerToken.mint(maker, balance).awaitTransactionSuccessAsync();
|
|
await makerToken.approve(zeroEx.address, allowance).awaitTransactionSuccessAsync({ from: maker });
|
|
}
|
|
|
|
describe('getLimitOrderRelevantState()', () => {
|
|
it('works with an empty order', async () => {
|
|
const order = getTestLimitOrder({
|
|
takerAmount: ZERO_AMOUNT,
|
|
});
|
|
await fundOrderMakerAsync(order);
|
|
const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx
|
|
.getLimitOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider))
|
|
.callAsync();
|
|
expect(orderInfo).to.deep.eq({
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Filled,
|
|
takerTokenFilledAmount: ZERO_AMOUNT,
|
|
});
|
|
expect(fillableTakerAmount).to.bignumber.eq(0);
|
|
expect(isSignatureValid).to.eq(true);
|
|
});
|
|
|
|
it('works with cancelled order', async () => {
|
|
const order = getTestLimitOrder();
|
|
await fundOrderMakerAsync(order);
|
|
await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx
|
|
.getLimitOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider))
|
|
.callAsync();
|
|
expect(orderInfo).to.deep.eq({
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Cancelled,
|
|
takerTokenFilledAmount: ZERO_AMOUNT,
|
|
});
|
|
expect(fillableTakerAmount).to.bignumber.eq(0);
|
|
expect(isSignatureValid).to.eq(true);
|
|
});
|
|
|
|
it('works with a bad signature', async () => {
|
|
const order = getTestLimitOrder();
|
|
await fundOrderMakerAsync(order);
|
|
const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx
|
|
.getLimitOrderRelevantState(
|
|
order,
|
|
await order.clone({ maker: notMaker }).getSignatureWithProviderAsync(env.provider),
|
|
)
|
|
.callAsync();
|
|
expect(orderInfo).to.deep.eq({
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Fillable,
|
|
takerTokenFilledAmount: ZERO_AMOUNT,
|
|
});
|
|
expect(fillableTakerAmount).to.bignumber.eq(order.takerAmount);
|
|
expect(isSignatureValid).to.eq(false);
|
|
});
|
|
|
|
it('works with an unfilled order', async () => {
|
|
const order = getTestLimitOrder();
|
|
await fundOrderMakerAsync(order);
|
|
const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx
|
|
.getLimitOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider))
|
|
.callAsync();
|
|
expect(orderInfo).to.deep.eq({
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Fillable,
|
|
takerTokenFilledAmount: ZERO_AMOUNT,
|
|
});
|
|
expect(fillableTakerAmount).to.bignumber.eq(order.takerAmount);
|
|
expect(isSignatureValid).to.eq(true);
|
|
});
|
|
|
|
it('works with a fully filled order', async () => {
|
|
const order = getTestLimitOrder();
|
|
// Fully Fund maker and taker.
|
|
await fundOrderMakerAsync(order);
|
|
await takerToken
|
|
.mint(taker, order.takerAmount.plus(order.takerTokenFeeAmount))
|
|
.awaitTransactionSuccessAsync();
|
|
await testUtils.fillLimitOrderAsync(order);
|
|
// Partially fill the order.
|
|
const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx
|
|
.getLimitOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider))
|
|
.callAsync();
|
|
expect(orderInfo).to.deep.eq({
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Filled,
|
|
takerTokenFilledAmount: order.takerAmount,
|
|
});
|
|
expect(fillableTakerAmount).to.bignumber.eq(0);
|
|
expect(isSignatureValid).to.eq(true);
|
|
});
|
|
|
|
it('works with an under-funded, partially-filled order', async () => {
|
|
const order = getTestLimitOrder();
|
|
// Fully Fund maker and taker.
|
|
await fundOrderMakerAsync(order);
|
|
await takerToken
|
|
.mint(taker, order.takerAmount.plus(order.takerTokenFeeAmount))
|
|
.awaitTransactionSuccessAsync();
|
|
// Partially fill the order.
|
|
const fillAmount = getRandomPortion(order.takerAmount);
|
|
await testUtils.fillLimitOrderAsync(order, { fillAmount });
|
|
// Reduce maker funds to be < remaining.
|
|
const remainingMakerAmount = getFillableMakerTokenAmount(order, fillAmount);
|
|
const balance = getRandomPortion(remainingMakerAmount);
|
|
const allowance = getRandomPortion(remainingMakerAmount);
|
|
await fundOrderMakerAsync(order, balance, allowance);
|
|
// Get order state.
|
|
const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx
|
|
.getLimitOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider))
|
|
.callAsync();
|
|
expect(orderInfo).to.deep.eq({
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Fillable,
|
|
takerTokenFilledAmount: fillAmount,
|
|
});
|
|
expect(fillableTakerAmount).to.bignumber.eq(
|
|
getActualFillableTakerTokenAmount(order, balance, allowance, fillAmount),
|
|
);
|
|
expect(isSignatureValid).to.eq(true);
|
|
});
|
|
});
|
|
|
|
describe('getRfqOrderRelevantState()', () => {
|
|
it('works with an empty order', async () => {
|
|
const order = getTestRfqOrder({
|
|
takerAmount: ZERO_AMOUNT,
|
|
});
|
|
await fundOrderMakerAsync(order);
|
|
const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx
|
|
.getRfqOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider))
|
|
.callAsync();
|
|
expect(orderInfo).to.deep.eq({
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Filled,
|
|
takerTokenFilledAmount: ZERO_AMOUNT,
|
|
});
|
|
expect(fillableTakerAmount).to.bignumber.eq(0);
|
|
expect(isSignatureValid).to.eq(true);
|
|
});
|
|
|
|
it('works with cancelled order', async () => {
|
|
const order = getTestRfqOrder();
|
|
await fundOrderMakerAsync(order);
|
|
await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx
|
|
.getRfqOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider))
|
|
.callAsync();
|
|
expect(orderInfo).to.deep.eq({
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Cancelled,
|
|
takerTokenFilledAmount: ZERO_AMOUNT,
|
|
});
|
|
expect(fillableTakerAmount).to.bignumber.eq(0);
|
|
expect(isSignatureValid).to.eq(true);
|
|
});
|
|
|
|
it('works with a bad signature', async () => {
|
|
const order = getTestRfqOrder();
|
|
await fundOrderMakerAsync(order);
|
|
const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx
|
|
.getRfqOrderRelevantState(
|
|
order,
|
|
await order.clone({ maker: notMaker }).getSignatureWithProviderAsync(env.provider),
|
|
)
|
|
.callAsync();
|
|
expect(orderInfo).to.deep.eq({
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Fillable,
|
|
takerTokenFilledAmount: ZERO_AMOUNT,
|
|
});
|
|
expect(fillableTakerAmount).to.bignumber.eq(order.takerAmount);
|
|
expect(isSignatureValid).to.eq(false);
|
|
});
|
|
|
|
it('works with an unfilled order', async () => {
|
|
const order = getTestRfqOrder();
|
|
await fundOrderMakerAsync(order);
|
|
const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx
|
|
.getRfqOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider))
|
|
.callAsync();
|
|
expect(orderInfo).to.deep.eq({
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Fillable,
|
|
takerTokenFilledAmount: ZERO_AMOUNT,
|
|
});
|
|
expect(fillableTakerAmount).to.bignumber.eq(order.takerAmount);
|
|
expect(isSignatureValid).to.eq(true);
|
|
});
|
|
|
|
it('works with a fully filled order', async () => {
|
|
const order = getTestRfqOrder();
|
|
// Fully Fund maker and taker.
|
|
await fundOrderMakerAsync(order);
|
|
await takerToken.mint(taker, order.takerAmount);
|
|
await testUtils.fillRfqOrderAsync(order);
|
|
// Partially fill the order.
|
|
const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx
|
|
.getRfqOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider))
|
|
.callAsync();
|
|
expect(orderInfo).to.deep.eq({
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Filled,
|
|
takerTokenFilledAmount: order.takerAmount,
|
|
});
|
|
expect(fillableTakerAmount).to.bignumber.eq(0);
|
|
expect(isSignatureValid).to.eq(true);
|
|
});
|
|
|
|
it('works with an under-funded, partially-filled order', async () => {
|
|
const order = getTestRfqOrder();
|
|
// Fully Fund maker and taker.
|
|
await fundOrderMakerAsync(order);
|
|
await takerToken.mint(taker, order.takerAmount).awaitTransactionSuccessAsync();
|
|
// Partially fill the order.
|
|
const fillAmount = getRandomPortion(order.takerAmount);
|
|
await testUtils.fillRfqOrderAsync(order, fillAmount);
|
|
// Reduce maker funds to be < remaining.
|
|
const remainingMakerAmount = getFillableMakerTokenAmount(order, fillAmount);
|
|
const balance = getRandomPortion(remainingMakerAmount);
|
|
const allowance = getRandomPortion(remainingMakerAmount);
|
|
await fundOrderMakerAsync(order, balance, allowance);
|
|
// Get order state.
|
|
const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx
|
|
.getRfqOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider))
|
|
.callAsync();
|
|
expect(orderInfo).to.deep.eq({
|
|
orderHash: order.getHash(),
|
|
status: OrderStatus.Fillable,
|
|
takerTokenFilledAmount: fillAmount,
|
|
});
|
|
expect(fillableTakerAmount).to.bignumber.eq(
|
|
getActualFillableTakerTokenAmount(order, balance, allowance, fillAmount),
|
|
);
|
|
expect(isSignatureValid).to.eq(true);
|
|
});
|
|
});
|
|
|
|
async function batchFundOrderMakerAsync(orders: Array<LimitOrder | RfqOrder>): Promise<void> {
|
|
await makerToken.burn(maker, await makerToken.balanceOf(maker).callAsync()).awaitTransactionSuccessAsync();
|
|
const balance = BigNumber.sum(...orders.map(o => o.makerAmount));
|
|
await makerToken.mint(maker, balance).awaitTransactionSuccessAsync();
|
|
await makerToken.approve(zeroEx.address, balance).awaitTransactionSuccessAsync({ from: maker });
|
|
}
|
|
|
|
describe('batchGetLimitOrderRelevantStates()', () => {
|
|
it('works with multiple orders', async () => {
|
|
const orders = new Array(3).fill(0).map(() => getTestLimitOrder());
|
|
await batchFundOrderMakerAsync(orders);
|
|
const [orderInfos, fillableTakerAmounts, isSignatureValids] = await zeroEx
|
|
.batchGetLimitOrderRelevantStates(
|
|
orders,
|
|
await Promise.all(orders.map(async o => o.getSignatureWithProviderAsync(env.provider))),
|
|
)
|
|
.callAsync();
|
|
expect(orderInfos).to.be.length(orders.length);
|
|
expect(fillableTakerAmounts).to.be.length(orders.length);
|
|
expect(isSignatureValids).to.be.length(orders.length);
|
|
for (let i = 0; i < orders.length; ++i) {
|
|
expect(orderInfos[i]).to.deep.eq({
|
|
orderHash: orders[i].getHash(),
|
|
status: OrderStatus.Fillable,
|
|
takerTokenFilledAmount: ZERO_AMOUNT,
|
|
});
|
|
expect(fillableTakerAmounts[i]).to.bignumber.eq(orders[i].takerAmount);
|
|
expect(isSignatureValids[i]).to.eq(true);
|
|
}
|
|
});
|
|
it('swallows reverts', async () => {
|
|
const orders = new Array(3).fill(0).map(() => getTestLimitOrder());
|
|
// The second order will revert because its maker token is not valid.
|
|
orders[1].makerToken = randomAddress();
|
|
await batchFundOrderMakerAsync(orders);
|
|
const [orderInfos, fillableTakerAmounts, isSignatureValids] = await zeroEx
|
|
.batchGetLimitOrderRelevantStates(
|
|
orders,
|
|
await Promise.all(orders.map(async o => o.getSignatureWithProviderAsync(env.provider))),
|
|
)
|
|
.callAsync();
|
|
expect(orderInfos).to.be.length(orders.length);
|
|
expect(fillableTakerAmounts).to.be.length(orders.length);
|
|
expect(isSignatureValids).to.be.length(orders.length);
|
|
for (let i = 0; i < orders.length; ++i) {
|
|
expect(orderInfos[i]).to.deep.eq({
|
|
orderHash: i === 1 ? NULL_BYTES32 : orders[i].getHash(),
|
|
status: i === 1 ? OrderStatus.Invalid : OrderStatus.Fillable,
|
|
takerTokenFilledAmount: ZERO_AMOUNT,
|
|
});
|
|
expect(fillableTakerAmounts[i]).to.bignumber.eq(i === 1 ? ZERO_AMOUNT : orders[i].takerAmount);
|
|
expect(isSignatureValids[i]).to.eq(i !== 1);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('batchGetRfqOrderRelevantStates()', () => {
|
|
it('works with multiple orders', async () => {
|
|
const orders = new Array(3).fill(0).map(() => getTestRfqOrder());
|
|
await batchFundOrderMakerAsync(orders);
|
|
const [orderInfos, fillableTakerAmounts, isSignatureValids] = await zeroEx
|
|
.batchGetRfqOrderRelevantStates(
|
|
orders,
|
|
await Promise.all(orders.map(async o => o.getSignatureWithProviderAsync(env.provider))),
|
|
)
|
|
.callAsync();
|
|
expect(orderInfos).to.be.length(orders.length);
|
|
expect(fillableTakerAmounts).to.be.length(orders.length);
|
|
expect(isSignatureValids).to.be.length(orders.length);
|
|
for (let i = 0; i < orders.length; ++i) {
|
|
expect(orderInfos[i]).to.deep.eq({
|
|
orderHash: orders[i].getHash(),
|
|
status: OrderStatus.Fillable,
|
|
takerTokenFilledAmount: ZERO_AMOUNT,
|
|
});
|
|
expect(fillableTakerAmounts[i]).to.bignumber.eq(orders[i].takerAmount);
|
|
expect(isSignatureValids[i]).to.eq(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('registerAllowedSigner()', () => {
|
|
it('fires appropriate events', async () => {
|
|
const receiptAllow = await contractWallet
|
|
.registerAllowedOrderSigner(contractWalletSigner, true)
|
|
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
|
|
|
|
verifyEventsFromLogs(
|
|
receiptAllow.logs,
|
|
[
|
|
{
|
|
maker: contractWallet.address,
|
|
signer: contractWalletSigner,
|
|
allowed: true,
|
|
},
|
|
],
|
|
IZeroExEvents.OrderSignerRegistered,
|
|
);
|
|
|
|
// then disallow signer
|
|
const receiptDisallow = await contractWallet
|
|
.registerAllowedOrderSigner(contractWalletSigner, false)
|
|
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
|
|
|
|
verifyEventsFromLogs(
|
|
receiptDisallow.logs,
|
|
[
|
|
{
|
|
maker: contractWallet.address,
|
|
signer: contractWalletSigner,
|
|
allowed: false,
|
|
},
|
|
],
|
|
IZeroExEvents.OrderSignerRegistered,
|
|
);
|
|
});
|
|
|
|
it('allows for fills on orders signed by a approved signer', async () => {
|
|
const order = getTestRfqOrder({ maker: contractWallet.address });
|
|
const sig = await order.getSignatureWithProviderAsync(
|
|
env.provider,
|
|
SignatureType.EthSign,
|
|
contractWalletSigner,
|
|
);
|
|
|
|
// covers taker
|
|
await testUtils.prepareBalancesForOrdersAsync([order]);
|
|
// need to provide contract wallet with a balance
|
|
await makerToken.mint(contractWallet.address, order.makerAmount).awaitTransactionSuccessAsync();
|
|
|
|
await contractWallet
|
|
.registerAllowedOrderSigner(contractWalletSigner, true)
|
|
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
|
|
|
|
await zeroEx.fillRfqOrder(order, sig, order.takerAmount).awaitTransactionSuccessAsync({ from: taker });
|
|
|
|
const info = await zeroEx.getRfqOrderInfo(order).callAsync();
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Filled,
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: order.takerAmount,
|
|
});
|
|
});
|
|
|
|
it('disallows fills if the signer is revoked', async () => {
|
|
const order = getTestRfqOrder({ maker: contractWallet.address });
|
|
const sig = await order.getSignatureWithProviderAsync(
|
|
env.provider,
|
|
SignatureType.EthSign,
|
|
contractWalletSigner,
|
|
);
|
|
|
|
// covers taker
|
|
await testUtils.prepareBalancesForOrdersAsync([order]);
|
|
// need to provide contract wallet with a balance
|
|
await makerToken.mint(contractWallet.address, order.makerAmount).awaitTransactionSuccessAsync();
|
|
|
|
// first allow signer
|
|
await contractWallet
|
|
.registerAllowedOrderSigner(contractWalletSigner, true)
|
|
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
|
|
|
|
// then disallow signer
|
|
await contractWallet
|
|
.registerAllowedOrderSigner(contractWalletSigner, false)
|
|
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
|
|
|
|
const tx = zeroEx.fillRfqOrder(order, sig, order.takerAmount).awaitTransactionSuccessAsync({ from: taker });
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OrderNotSignedByMakerError(
|
|
order.getHash(),
|
|
contractWalletSigner,
|
|
order.maker,
|
|
),
|
|
);
|
|
});
|
|
|
|
it(`doesn't allow fills with an unapproved signer`, async () => {
|
|
const order = getTestRfqOrder({ maker: contractWallet.address });
|
|
const sig = await order.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, maker);
|
|
|
|
// covers taker
|
|
await testUtils.prepareBalancesForOrdersAsync([order]);
|
|
// need to provide contract wallet with a balance
|
|
await makerToken.mint(contractWallet.address, order.makerAmount).awaitTransactionSuccessAsync();
|
|
|
|
const tx = zeroEx.fillRfqOrder(order, sig, order.takerAmount).awaitTransactionSuccessAsync({ from: taker });
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), maker, order.maker),
|
|
);
|
|
});
|
|
|
|
it(`allows an approved signer to cancel an RFQ order`, async () => {
|
|
const order = getTestRfqOrder({ maker: contractWallet.address });
|
|
|
|
await contractWallet
|
|
.registerAllowedOrderSigner(contractWalletSigner, true)
|
|
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
|
|
|
|
const receipt = await zeroEx
|
|
.cancelRfqOrder(order)
|
|
.awaitTransactionSuccessAsync({ from: contractWalletSigner });
|
|
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[{ maker: contractWallet.address, orderHash: order.getHash() }],
|
|
IZeroExEvents.OrderCancelled,
|
|
);
|
|
|
|
const info = await zeroEx.getRfqOrderInfo(order).callAsync();
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Cancelled,
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: new BigNumber(0),
|
|
});
|
|
});
|
|
|
|
it(`allows an approved signer to cancel a limit order`, async () => {
|
|
const order = getTestLimitOrder({ maker: contractWallet.address });
|
|
|
|
await contractWallet
|
|
.registerAllowedOrderSigner(contractWalletSigner, true)
|
|
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
|
|
|
|
const receipt = await zeroEx
|
|
.cancelLimitOrder(order)
|
|
.awaitTransactionSuccessAsync({ from: contractWalletSigner });
|
|
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[{ maker: contractWallet.address, orderHash: order.getHash() }],
|
|
IZeroExEvents.OrderCancelled,
|
|
);
|
|
|
|
const info = await zeroEx.getLimitOrderInfo(order).callAsync();
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Cancelled,
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: new BigNumber(0),
|
|
});
|
|
});
|
|
|
|
it(`doesn't allow an unapproved signer to cancel an RFQ order`, async () => {
|
|
const order = getTestRfqOrder({ maker: contractWallet.address });
|
|
|
|
const tx = zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OnlyOrderMakerAllowed(order.getHash(), maker, order.maker),
|
|
);
|
|
});
|
|
|
|
it(`doesn't allow an unapproved signer to cancel a limit order`, async () => {
|
|
const order = getTestLimitOrder({ maker: contractWallet.address });
|
|
|
|
const tx = zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
|
|
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.OnlyOrderMakerAllowed(order.getHash(), maker, order.maker),
|
|
);
|
|
});
|
|
|
|
it(`allows a signer to cancel pair RFQ orders`, async () => {
|
|
const order = getTestRfqOrder({ maker: contractWallet.address, salt: new BigNumber(1) });
|
|
|
|
await contractWallet
|
|
.registerAllowedOrderSigner(contractWalletSigner, true)
|
|
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
|
|
|
|
// Cancel salts <= the order's
|
|
const minValidSalt = order.salt.plus(1);
|
|
|
|
const receipt = await zeroEx
|
|
.cancelPairRfqOrdersWithSigner(
|
|
contractWallet.address,
|
|
makerToken.address,
|
|
takerToken.address,
|
|
minValidSalt,
|
|
)
|
|
.awaitTransactionSuccessAsync({ from: contractWalletSigner });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[
|
|
{
|
|
maker: contractWallet.address,
|
|
makerToken: makerToken.address,
|
|
takerToken: takerToken.address,
|
|
minValidSalt,
|
|
},
|
|
],
|
|
IZeroExEvents.PairCancelledRfqOrders,
|
|
);
|
|
|
|
const info = await zeroEx.getRfqOrderInfo(order).callAsync();
|
|
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Cancelled,
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: new BigNumber(0),
|
|
});
|
|
});
|
|
|
|
it(`doesn't allow an unapproved signer to cancel pair RFQ orders`, async () => {
|
|
const minValidSalt = new BigNumber(2);
|
|
|
|
const tx = zeroEx
|
|
.cancelPairRfqOrdersWithSigner(
|
|
contractWallet.address,
|
|
makerToken.address,
|
|
takerToken.address,
|
|
minValidSalt,
|
|
)
|
|
.awaitTransactionSuccessAsync({ from: maker });
|
|
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.InvalidSignerError(contractWallet.address, maker),
|
|
);
|
|
});
|
|
|
|
it(`allows a signer to cancel pair limit orders`, async () => {
|
|
const order = getTestLimitOrder({ maker: contractWallet.address, salt: new BigNumber(1) });
|
|
|
|
await contractWallet
|
|
.registerAllowedOrderSigner(contractWalletSigner, true)
|
|
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
|
|
|
|
// Cancel salts <= the order's
|
|
const minValidSalt = order.salt.plus(1);
|
|
|
|
const receipt = await zeroEx
|
|
.cancelPairLimitOrdersWithSigner(
|
|
contractWallet.address,
|
|
makerToken.address,
|
|
takerToken.address,
|
|
minValidSalt,
|
|
)
|
|
.awaitTransactionSuccessAsync({ from: contractWalletSigner });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[
|
|
{
|
|
maker: contractWallet.address,
|
|
makerToken: makerToken.address,
|
|
takerToken: takerToken.address,
|
|
minValidSalt,
|
|
},
|
|
],
|
|
IZeroExEvents.PairCancelledLimitOrders,
|
|
);
|
|
|
|
const info = await zeroEx.getLimitOrderInfo(order).callAsync();
|
|
|
|
assertOrderInfoEquals(info, {
|
|
status: OrderStatus.Cancelled,
|
|
orderHash: order.getHash(),
|
|
takerTokenFilledAmount: new BigNumber(0),
|
|
});
|
|
});
|
|
|
|
it(`doesn't allow an unapproved signer to cancel pair limit orders`, async () => {
|
|
const minValidSalt = new BigNumber(2);
|
|
|
|
const tx = zeroEx
|
|
.cancelPairLimitOrdersWithSigner(
|
|
contractWallet.address,
|
|
makerToken.address,
|
|
takerToken.address,
|
|
minValidSalt,
|
|
)
|
|
.awaitTransactionSuccessAsync({ from: maker });
|
|
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.InvalidSignerError(contractWallet.address, maker),
|
|
);
|
|
});
|
|
|
|
it(`allows a signer to cancel multiple RFQ order pairs`, async () => {
|
|
const orders = [
|
|
getTestRfqOrder({ maker: contractWallet.address, salt: new BigNumber(1) }),
|
|
// Flip the tokens for the other order.
|
|
getTestRfqOrder({
|
|
makerToken: takerToken.address,
|
|
takerToken: makerToken.address,
|
|
maker: contractWallet.address,
|
|
salt: new BigNumber(1),
|
|
}),
|
|
];
|
|
|
|
await contractWallet
|
|
.registerAllowedOrderSigner(contractWalletSigner, true)
|
|
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
|
|
|
|
const minValidSalt = new BigNumber(2);
|
|
const receipt = await zeroEx
|
|
.batchCancelPairRfqOrdersWithSigner(
|
|
contractWallet.address,
|
|
[makerToken.address, takerToken.address],
|
|
[takerToken.address, makerToken.address],
|
|
[minValidSalt, minValidSalt],
|
|
)
|
|
.awaitTransactionSuccessAsync({ from: contractWalletSigner });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[
|
|
{
|
|
maker: contractWallet.address,
|
|
makerToken: makerToken.address,
|
|
takerToken: takerToken.address,
|
|
minValidSalt,
|
|
},
|
|
{
|
|
maker: contractWallet.address,
|
|
makerToken: takerToken.address,
|
|
takerToken: makerToken.address,
|
|
minValidSalt,
|
|
},
|
|
],
|
|
IZeroExEvents.PairCancelledRfqOrders,
|
|
);
|
|
const statuses = (await Promise.all(orders.map(o => zeroEx.getRfqOrderInfo(o).callAsync()))).map(
|
|
oi => oi.status,
|
|
);
|
|
expect(statuses).to.deep.eq([OrderStatus.Cancelled, OrderStatus.Cancelled]);
|
|
});
|
|
|
|
it(`doesn't allow an unapproved signer to batch cancel pair rfq orders`, async () => {
|
|
const minValidSalt = new BigNumber(2);
|
|
|
|
const tx = zeroEx
|
|
.batchCancelPairRfqOrdersWithSigner(
|
|
contractWallet.address,
|
|
[makerToken.address, takerToken.address],
|
|
[takerToken.address, makerToken.address],
|
|
[minValidSalt, minValidSalt],
|
|
)
|
|
.awaitTransactionSuccessAsync({ from: maker });
|
|
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.InvalidSignerError(contractWallet.address, maker),
|
|
);
|
|
});
|
|
|
|
it(`allows a signer to cancel multiple limit order pairs`, async () => {
|
|
const orders = [
|
|
getTestLimitOrder({ maker: contractWallet.address, salt: new BigNumber(1) }),
|
|
// Flip the tokens for the other order.
|
|
getTestLimitOrder({
|
|
makerToken: takerToken.address,
|
|
takerToken: makerToken.address,
|
|
maker: contractWallet.address,
|
|
salt: new BigNumber(1),
|
|
}),
|
|
];
|
|
|
|
await contractWallet
|
|
.registerAllowedOrderSigner(contractWalletSigner, true)
|
|
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
|
|
|
|
const minValidSalt = new BigNumber(2);
|
|
const receipt = await zeroEx
|
|
.batchCancelPairLimitOrdersWithSigner(
|
|
contractWallet.address,
|
|
[makerToken.address, takerToken.address],
|
|
[takerToken.address, makerToken.address],
|
|
[minValidSalt, minValidSalt],
|
|
)
|
|
.awaitTransactionSuccessAsync({ from: contractWalletSigner });
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[
|
|
{
|
|
maker: contractWallet.address,
|
|
makerToken: makerToken.address,
|
|
takerToken: takerToken.address,
|
|
minValidSalt,
|
|
},
|
|
{
|
|
maker: contractWallet.address,
|
|
makerToken: takerToken.address,
|
|
takerToken: makerToken.address,
|
|
minValidSalt,
|
|
},
|
|
],
|
|
IZeroExEvents.PairCancelledLimitOrders,
|
|
);
|
|
const statuses = (await Promise.all(orders.map(o => zeroEx.getLimitOrderInfo(o).callAsync()))).map(
|
|
oi => oi.status,
|
|
);
|
|
expect(statuses).to.deep.eq([OrderStatus.Cancelled, OrderStatus.Cancelled]);
|
|
});
|
|
|
|
it(`doesn't allow an unapproved signer to batch cancel pair limit orders`, async () => {
|
|
const minValidSalt = new BigNumber(2);
|
|
|
|
const tx = zeroEx
|
|
.batchCancelPairLimitOrdersWithSigner(
|
|
contractWallet.address,
|
|
[makerToken.address, takerToken.address],
|
|
[takerToken.address, makerToken.address],
|
|
[minValidSalt, minValidSalt],
|
|
)
|
|
.awaitTransactionSuccessAsync({ from: maker });
|
|
|
|
return expect(tx).to.revertWith(
|
|
new RevertErrors.NativeOrders.InvalidSignerError(contractWallet.address, maker),
|
|
);
|
|
});
|
|
});
|
|
});
|