MultiplexFeature and BatchFillNativeOrdersFeature (#140)

* WrappedFillFeature

* Address internal feedback

* create features/interfaces/ directory

* Split NativeOrdersFeature into mixins

* Rename mixins to use NativeOrders namespace

* Add BatchFillNativeOrdersFeature

* Rename WrapperFillFeature => MultiplexFeature and add natspec comments

* Emit LiquidityProviderSwap event

* post-rebase fixes

* Multiplex mainnet fork tests

* lint

* Add tests for batch fill functions

* Remove market functions

* Addres PR feedback

* Remove nested _batchFill calls from _multiHopFill

* Add BatchFillIncompleteRevertError type

* Use call{value: amount}() instead of transfer(amount)

* Remove outdated comment

* Update some comments

* Add events

* Address spot-check recommendations

* Remove-top level events, add ExpiredRfqOrder event

* Update changelog

* Change ExpiredRfqOrder event

* Update IZeroEx artifact and contract wrapper
This commit is contained in:
mzhu25
2021-03-08 15:45:49 -08:00
committed by GitHub
parent 22c8e0b6db
commit 3cc639c8d0
60 changed files with 5337 additions and 1612 deletions

View File

@@ -7,6 +7,7 @@ import { ContractArtifact } from 'ethereum-types';
import * as AffiliateFeeTransformer from '../test/generated-artifacts/AffiliateFeeTransformer.json';
import * as AllowanceTarget from '../test/generated-artifacts/AllowanceTarget.json';
import * as BatchFillNativeOrdersFeature from '../test/generated-artifacts/BatchFillNativeOrdersFeature.json';
import * as BootstrapFeature from '../test/generated-artifacts/BootstrapFeature.json';
import * as BridgeAdapter from '../test/generated-artifacts/BridgeAdapter.json';
import * as BridgeSource from '../test/generated-artifacts/BridgeSource.json';
@@ -22,6 +23,7 @@ import * as FixinTokenSpender from '../test/generated-artifacts/FixinTokenSpende
import * as FlashWallet from '../test/generated-artifacts/FlashWallet.json';
import * as FullMigration from '../test/generated-artifacts/FullMigration.json';
import * as IAllowanceTarget from '../test/generated-artifacts/IAllowanceTarget.json';
import * as IBatchFillNativeOrdersFeature from '../test/generated-artifacts/IBatchFillNativeOrdersFeature.json';
import * as IBootstrapFeature from '../test/generated-artifacts/IBootstrapFeature.json';
import * as IBridgeAdapter from '../test/generated-artifacts/IBridgeAdapter.json';
import * as IERC20Bridge from '../test/generated-artifacts/IERC20Bridge.json';
@@ -33,6 +35,8 @@ import * as ILiquidityProviderFeature from '../test/generated-artifacts/ILiquidi
import * as ILiquidityProviderSandbox from '../test/generated-artifacts/ILiquidityProviderSandbox.json';
import * as IMetaTransactionsFeature from '../test/generated-artifacts/IMetaTransactionsFeature.json';
import * as IMooniswapPool from '../test/generated-artifacts/IMooniswapPool.json';
import * as IMultiplexFeature from '../test/generated-artifacts/IMultiplexFeature.json';
import * as INativeOrdersEvents from '../test/generated-artifacts/INativeOrdersEvents.json';
import * as INativeOrdersFeature from '../test/generated-artifacts/INativeOrdersFeature.json';
import * as InitialMigration from '../test/generated-artifacts/InitialMigration.json';
import * as IOwnableFeature from '../test/generated-artifacts/IOwnableFeature.json';
@@ -42,6 +46,7 @@ import * as ITestSimpleFunctionRegistryFeature from '../test/generated-artifacts
import * as ITokenSpenderFeature from '../test/generated-artifacts/ITokenSpenderFeature.json';
import * as ITransformERC20Feature from '../test/generated-artifacts/ITransformERC20Feature.json';
import * as IUniswapFeature from '../test/generated-artifacts/IUniswapFeature.json';
import * as IUniswapV2Pair from '../test/generated-artifacts/IUniswapV2Pair.json';
import * as IZeroEx from '../test/generated-artifacts/IZeroEx.json';
import * as LibBootstrap from '../test/generated-artifacts/LibBootstrap.json';
import * as LibCommonRichErrors from '../test/generated-artifacts/LibCommonRichErrors.json';
@@ -90,7 +95,12 @@ import * as MixinUniswap from '../test/generated-artifacts/MixinUniswap.json';
import * as MixinUniswapV2 from '../test/generated-artifacts/MixinUniswapV2.json';
import * as MixinZeroExBridge from '../test/generated-artifacts/MixinZeroExBridge.json';
import * as MooniswapLiquidityProvider from '../test/generated-artifacts/MooniswapLiquidityProvider.json';
import * as MultiplexFeature from '../test/generated-artifacts/MultiplexFeature.json';
import * as NativeOrdersCancellation from '../test/generated-artifacts/NativeOrdersCancellation.json';
import * as NativeOrdersFeature from '../test/generated-artifacts/NativeOrdersFeature.json';
import * as NativeOrdersInfo from '../test/generated-artifacts/NativeOrdersInfo.json';
import * as NativeOrdersProtocolFees from '../test/generated-artifacts/NativeOrdersProtocolFees.json';
import * as NativeOrdersSettlement from '../test/generated-artifacts/NativeOrdersSettlement.json';
import * as OwnableFeature from '../test/generated-artifacts/OwnableFeature.json';
import * as PayTakerTransformer from '../test/generated-artifacts/PayTakerTransformer.json';
import * as PermissionlessTransformerDeployer from '../test/generated-artifacts/PermissionlessTransformerDeployer.json';
@@ -167,27 +177,36 @@ export const artifacts = {
LiquidityProviderSandbox: LiquidityProviderSandbox as ContractArtifact,
PermissionlessTransformerDeployer: PermissionlessTransformerDeployer as ContractArtifact,
TransformerDeployer: TransformerDeployer as ContractArtifact,
BatchFillNativeOrdersFeature: BatchFillNativeOrdersFeature as ContractArtifact,
BootstrapFeature: BootstrapFeature as ContractArtifact,
IBootstrapFeature: IBootstrapFeature as ContractArtifact,
IFeature: IFeature as ContractArtifact,
ILiquidityProviderFeature: ILiquidityProviderFeature as ContractArtifact,
IMetaTransactionsFeature: IMetaTransactionsFeature as ContractArtifact,
INativeOrdersFeature: INativeOrdersFeature as ContractArtifact,
IOwnableFeature: IOwnableFeature as ContractArtifact,
ISimpleFunctionRegistryFeature: ISimpleFunctionRegistryFeature as ContractArtifact,
ITokenSpenderFeature: ITokenSpenderFeature as ContractArtifact,
ITransformERC20Feature: ITransformERC20Feature as ContractArtifact,
IUniswapFeature: IUniswapFeature as ContractArtifact,
LiquidityProviderFeature: LiquidityProviderFeature as ContractArtifact,
MetaTransactionsFeature: MetaTransactionsFeature as ContractArtifact,
MultiplexFeature: MultiplexFeature as ContractArtifact,
NativeOrdersFeature: NativeOrdersFeature as ContractArtifact,
OwnableFeature: OwnableFeature as ContractArtifact,
SimpleFunctionRegistryFeature: SimpleFunctionRegistryFeature as ContractArtifact,
TokenSpenderFeature: TokenSpenderFeature as ContractArtifact,
TransformERC20Feature: TransformERC20Feature as ContractArtifact,
UniswapFeature: UniswapFeature as ContractArtifact,
IBatchFillNativeOrdersFeature: IBatchFillNativeOrdersFeature as ContractArtifact,
IBootstrapFeature: IBootstrapFeature as ContractArtifact,
IFeature: IFeature as ContractArtifact,
ILiquidityProviderFeature: ILiquidityProviderFeature as ContractArtifact,
IMetaTransactionsFeature: IMetaTransactionsFeature as ContractArtifact,
IMultiplexFeature: IMultiplexFeature as ContractArtifact,
INativeOrdersEvents: INativeOrdersEvents as ContractArtifact,
INativeOrdersFeature: INativeOrdersFeature as ContractArtifact,
IOwnableFeature: IOwnableFeature as ContractArtifact,
ISimpleFunctionRegistryFeature: ISimpleFunctionRegistryFeature as ContractArtifact,
ITokenSpenderFeature: ITokenSpenderFeature as ContractArtifact,
ITransformERC20Feature: ITransformERC20Feature as ContractArtifact,
IUniswapFeature: IUniswapFeature as ContractArtifact,
LibNativeOrder: LibNativeOrder as ContractArtifact,
LibSignature: LibSignature as ContractArtifact,
NativeOrdersCancellation: NativeOrdersCancellation as ContractArtifact,
NativeOrdersInfo: NativeOrdersInfo as ContractArtifact,
NativeOrdersProtocolFees: NativeOrdersProtocolFees as ContractArtifact,
NativeOrdersSettlement: NativeOrdersSettlement as ContractArtifact,
FixinCommon: FixinCommon as ContractArtifact,
FixinEIP712: FixinEIP712 as ContractArtifact,
FixinProtocolFees: FixinProtocolFees as ContractArtifact,
@@ -238,6 +257,7 @@ export const artifacts = {
MixinZeroExBridge: MixinZeroExBridge as ContractArtifact,
ILiquidityProvider: ILiquidityProvider as ContractArtifact,
IMooniswapPool: IMooniswapPool as ContractArtifact,
IUniswapV2Pair: IUniswapV2Pair as ContractArtifact,
IERC20Bridge: IERC20Bridge as ContractArtifact,
IStaking: IStaking as ContractArtifact,
ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact,

View File

@@ -0,0 +1,479 @@
import {
blockchainTests,
constants,
describe,
expect,
getRandomPortion,
verifyEventsFromLogs,
} from '@0x/contracts-test-utils';
import { LimitOrder, LimitOrderFields, OrderStatus, RevertErrors, RfqOrder, RfqOrderFields } from '@0x/protocol-utils';
import { BigNumber } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';
import { BatchFillNativeOrdersFeatureContract, IZeroExContract, IZeroExEvents } from '../../src/wrappers';
import { artifacts } from '../artifacts';
import { abis } from '../utils/abis';
import {
assertOrderInfoEquals,
computeLimitOrderFilledAmounts,
computeRfqOrderFilledAmounts,
createExpiry,
getRandomLimitOrder,
getRandomRfqOrder,
NativeOrdersTestEnvironment,
} from '../utils/orders';
import { TestMintableERC20TokenContract } from '../wrappers';
blockchainTests.resets('BatchFillNativeOrdersFeature', env => {
const { NULL_ADDRESS, ZERO_AMOUNT } = constants;
let maker: string;
let taker: string;
let zeroEx: IZeroExContract;
let feature: BatchFillNativeOrdersFeatureContract;
let verifyingContract: string;
let makerToken: TestMintableERC20TokenContract;
let takerToken: TestMintableERC20TokenContract;
let testUtils: NativeOrdersTestEnvironment;
before(async () => {
testUtils = await NativeOrdersTestEnvironment.createAsync(env);
maker = testUtils.maker;
taker = testUtils.taker;
zeroEx = testUtils.zeroEx;
makerToken = testUtils.makerToken;
takerToken = testUtils.takerToken;
verifyingContract = zeroEx.address;
const featureImpl = await BatchFillNativeOrdersFeatureContract.deployFrom0xArtifactAsync(
artifacts.BatchFillNativeOrdersFeature,
env.provider,
env.txDefaults,
artifacts,
zeroEx.address,
);
const [owner] = await env.getAccountAddressesAsync();
await zeroEx
.migrate(featureImpl.address, featureImpl.migrate().getABIEncodedTransactionData(), owner)
.awaitTransactionSuccessAsync();
feature = new BatchFillNativeOrdersFeatureContract(
zeroEx.address,
env.provider,
{ ...env.txDefaults, gasPrice: testUtils.gasPrice },
abis,
);
});
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('batchFillLimitOrders', () => {
async function assertExpectedFinalBalancesAsync(
orders: LimitOrder[],
takerTokenFillAmounts: BigNumber[] = orders.map(order => order.takerAmount),
takerTokenAlreadyFilledAmounts: BigNumber[] = orders.map(() => ZERO_AMOUNT),
receipt?: TransactionReceiptWithDecodedLogs,
): Promise<void> {
const expectedFeeRecipientBalances: { [feeRecipient: string]: BigNumber } = {};
const { makerTokenFilledAmount, takerTokenFilledAmount } = orders
.map((order, i) =>
computeLimitOrderFilledAmounts(order, takerTokenFillAmounts[i], takerTokenAlreadyFilledAmounts[i]),
)
.reduce(
(previous, current, i) => {
_.update(expectedFeeRecipientBalances, orders[i].feeRecipient, balance =>
(balance || ZERO_AMOUNT).plus(current.takerTokenFeeFilledAmount),
);
return {
makerTokenFilledAmount: previous.makerTokenFilledAmount.plus(
current.makerTokenFilledAmount,
),
takerTokenFilledAmount: previous.takerTokenFilledAmount.plus(
current.takerTokenFilledAmount,
),
};
},
{ makerTokenFilledAmount: ZERO_AMOUNT, takerTokenFilledAmount: ZERO_AMOUNT },
);
const makerBalance = await takerToken.balanceOf(maker).callAsync();
const takerBalance = await makerToken.balanceOf(taker).callAsync();
expect(makerBalance, 'maker token balance').to.bignumber.eq(takerTokenFilledAmount);
expect(takerBalance, 'taker token balance').to.bignumber.eq(makerTokenFilledAmount);
for (const [feeRecipient, expectedFeeRecipientBalance] of Object.entries(expectedFeeRecipientBalances)) {
const feeRecipientBalance = await takerToken.balanceOf(feeRecipient).callAsync();
expect(feeRecipientBalance, `fee recipient balance`).to.bignumber.eq(expectedFeeRecipientBalance);
}
if (receipt) {
const balanceOfTakerNow = await env.web3Wrapper.getBalanceInWeiAsync(taker);
const balanceOfTakerBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker, receipt.blockNumber - 1);
const protocolFees = testUtils.protocolFee.times(orders.length);
const totalCost = testUtils.gasPrice.times(receipt.gasUsed).plus(protocolFees);
expect(balanceOfTakerBefore.minus(totalCost), 'taker ETH balance').to.bignumber.eq(balanceOfTakerNow);
}
}
it('Fully fills multiple orders', async () => {
const orders = [...new Array(3)].map(() => getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT }));
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const value = testUtils.protocolFee.times(orders.length);
const tx = await feature
.batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), false)
.awaitTransactionSuccessAsync({ from: taker, value });
const [orderInfos] = await zeroEx.batchGetLimitOrderRelevantStates(orders, signatures).callAsync();
orderInfos.map((orderInfo, i) =>
assertOrderInfoEquals(orderInfo, {
status: OrderStatus.Filled,
orderHash: orders[i].getHash(),
takerTokenFilledAmount: orders[i].takerAmount,
}),
);
verifyEventsFromLogs(
tx.logs,
orders.map(order => testUtils.createLimitOrderFilledEventArgs(order)),
IZeroExEvents.LimitOrderFilled,
);
return assertExpectedFinalBalancesAsync(orders);
});
it('Partially fills multiple orders', async () => {
const orders = [...new Array(3)].map(getTestLimitOrder);
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const value = testUtils.protocolFee.times(orders.length);
const fillAmounts = orders.map(order => getRandomPortion(order.takerAmount));
const tx = await feature
.batchFillLimitOrders(orders, signatures, fillAmounts, false)
.awaitTransactionSuccessAsync({ from: taker, value });
const [orderInfos] = await zeroEx.batchGetLimitOrderRelevantStates(orders, signatures).callAsync();
orderInfos.map((orderInfo, i) =>
assertOrderInfoEquals(orderInfo, {
status: OrderStatus.Fillable,
orderHash: orders[i].getHash(),
takerTokenFilledAmount: fillAmounts[i],
}),
);
verifyEventsFromLogs(
tx.logs,
orders.map((order, i) => testUtils.createLimitOrderFilledEventArgs(order, fillAmounts[i])),
IZeroExEvents.LimitOrderFilled,
);
return assertExpectedFinalBalancesAsync(orders, fillAmounts);
});
it('Fills multiple orders and refunds excess ETH', async () => {
const orders = [...new Array(3)].map(() => getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT }));
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const value = testUtils.protocolFee.times(orders.length).plus(420);
const tx = await feature
.batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), false)
.awaitTransactionSuccessAsync({ from: taker, value });
const [orderInfos] = await zeroEx.batchGetLimitOrderRelevantStates(orders, signatures).callAsync();
orderInfos.map((orderInfo, i) =>
assertOrderInfoEquals(orderInfo, {
status: OrderStatus.Filled,
orderHash: orders[i].getHash(),
takerTokenFilledAmount: orders[i].takerAmount,
}),
);
verifyEventsFromLogs(
tx.logs,
orders.map(order => testUtils.createLimitOrderFilledEventArgs(order)),
IZeroExEvents.LimitOrderFilled,
);
return assertExpectedFinalBalancesAsync(orders);
});
it('Skips over unfillable orders and refunds excess ETH', async () => {
const fillableOrders = [...new Array(3)].map(() => getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT }));
const expiredOrder = getTestLimitOrder({ expiry: createExpiry(-1), takerTokenFeeAmount: ZERO_AMOUNT });
const orders = [expiredOrder, ...fillableOrders];
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const value = testUtils.protocolFee.times(orders.length);
const tx = await feature
.batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), false)
.awaitTransactionSuccessAsync({ from: taker, value });
const [orderInfos] = await zeroEx.batchGetLimitOrderRelevantStates(orders, signatures).callAsync();
const [expiredOrderInfo, ...filledOrderInfos] = orderInfos;
assertOrderInfoEquals(expiredOrderInfo, {
status: OrderStatus.Expired,
orderHash: expiredOrder.getHash(),
takerTokenFilledAmount: ZERO_AMOUNT,
});
filledOrderInfos.map((orderInfo, i) =>
assertOrderInfoEquals(orderInfo, {
status: OrderStatus.Filled,
orderHash: fillableOrders[i].getHash(),
takerTokenFilledAmount: fillableOrders[i].takerAmount,
}),
);
verifyEventsFromLogs(
tx.logs,
fillableOrders.map(order => testUtils.createLimitOrderFilledEventArgs(order)),
IZeroExEvents.LimitOrderFilled,
);
return assertExpectedFinalBalancesAsync(fillableOrders);
});
it('Fills multiple orders with revertIfIncomplete=true', async () => {
const orders = [...new Array(3)].map(() => getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT }));
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const value = testUtils.protocolFee.times(orders.length);
const tx = await feature
.batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), true)
.awaitTransactionSuccessAsync({ from: taker, value });
const [orderInfos] = await zeroEx.batchGetLimitOrderRelevantStates(orders, signatures).callAsync();
orderInfos.map((orderInfo, i) =>
assertOrderInfoEquals(orderInfo, {
status: OrderStatus.Filled,
orderHash: orders[i].getHash(),
takerTokenFilledAmount: orders[i].takerAmount,
}),
);
verifyEventsFromLogs(
tx.logs,
orders.map(order => testUtils.createLimitOrderFilledEventArgs(order)),
IZeroExEvents.LimitOrderFilled,
);
return assertExpectedFinalBalancesAsync(orders);
});
it('If revertIfIncomplete==true, reverts on an unfillable order', async () => {
const fillableOrders = [...new Array(3)].map(() => getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT }));
const expiredOrder = getTestLimitOrder({ expiry: createExpiry(-1), takerTokenFeeAmount: ZERO_AMOUNT });
const orders = [expiredOrder, ...fillableOrders];
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const value = testUtils.protocolFee.times(orders.length);
const tx = feature
.batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), true)
.awaitTransactionSuccessAsync({ from: taker, value });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.BatchFillIncompleteError(
expiredOrder.getHash(),
ZERO_AMOUNT,
expiredOrder.takerAmount,
),
);
});
it('If revertIfIncomplete==true, reverts on an incomplete fill ', async () => {
const fillableOrders = [...new Array(3)].map(() => getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT }));
const partiallyFilledOrder = getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT });
const partialFillAmount = getRandomPortion(partiallyFilledOrder.takerAmount);
await testUtils.fillLimitOrderAsync(partiallyFilledOrder, { fillAmount: partialFillAmount });
const orders = [partiallyFilledOrder, ...fillableOrders];
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const value = testUtils.protocolFee.times(orders.length);
const tx = feature
.batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), true)
.awaitTransactionSuccessAsync({ from: taker, value });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.BatchFillIncompleteError(
partiallyFilledOrder.getHash(),
partiallyFilledOrder.takerAmount.minus(partialFillAmount),
partiallyFilledOrder.takerAmount,
),
);
});
});
describe('batchFillRfqOrders', () => {
async function assertExpectedFinalBalancesAsync(
orders: RfqOrder[],
takerTokenFillAmounts: BigNumber[] = orders.map(order => order.takerAmount),
takerTokenAlreadyFilledAmounts: BigNumber[] = orders.map(() => ZERO_AMOUNT),
): Promise<void> {
const { makerTokenFilledAmount, takerTokenFilledAmount } = orders
.map((order, i) =>
computeRfqOrderFilledAmounts(order, takerTokenFillAmounts[i], takerTokenAlreadyFilledAmounts[i]),
)
.reduce((previous, current) => ({
makerTokenFilledAmount: previous.makerTokenFilledAmount.plus(current.makerTokenFilledAmount),
takerTokenFilledAmount: previous.takerTokenFilledAmount.plus(current.takerTokenFilledAmount),
}));
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);
}
it('Fully fills multiple orders', async () => {
const orders = [...new Array(3)].map(() => getTestRfqOrder());
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const tx = await feature
.batchFillRfqOrders(orders, signatures, orders.map(order => order.takerAmount), false)
.awaitTransactionSuccessAsync({ from: taker });
const [orderInfos] = await zeroEx.batchGetRfqOrderRelevantStates(orders, signatures).callAsync();
orderInfos.map((orderInfo, i) =>
assertOrderInfoEquals(orderInfo, {
status: OrderStatus.Filled,
orderHash: orders[i].getHash(),
takerTokenFilledAmount: orders[i].takerAmount,
}),
);
verifyEventsFromLogs(
tx.logs,
orders.map(order => testUtils.createRfqOrderFilledEventArgs(order)),
IZeroExEvents.RfqOrderFilled,
);
return assertExpectedFinalBalancesAsync(orders);
});
it('Partially fills multiple orders', async () => {
const orders = [...new Array(3)].map(() => getTestRfqOrder());
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
const fillAmounts = orders.map(order => getRandomPortion(order.takerAmount));
await testUtils.prepareBalancesForOrdersAsync(orders);
const tx = await feature
.batchFillRfqOrders(orders, signatures, fillAmounts, false)
.awaitTransactionSuccessAsync({ from: taker });
const [orderInfos] = await zeroEx.batchGetRfqOrderRelevantStates(orders, signatures).callAsync();
orderInfos.map((orderInfo, i) =>
assertOrderInfoEquals(orderInfo, {
status: OrderStatus.Fillable,
orderHash: orders[i].getHash(),
takerTokenFilledAmount: fillAmounts[i],
}),
);
verifyEventsFromLogs(
tx.logs,
orders.map((order, i) => testUtils.createRfqOrderFilledEventArgs(order, fillAmounts[i])),
IZeroExEvents.RfqOrderFilled,
);
return assertExpectedFinalBalancesAsync(orders, fillAmounts);
});
it('Skips over unfillable orders', async () => {
const fillableOrders = [...new Array(3)].map(() => getTestRfqOrder());
const expiredOrder = getTestRfqOrder({ expiry: createExpiry(-1) });
const orders = [expiredOrder, ...fillableOrders];
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const tx = await feature
.batchFillRfqOrders(orders, signatures, orders.map(order => order.takerAmount), false)
.awaitTransactionSuccessAsync({ from: taker });
const [orderInfos] = await zeroEx.batchGetRfqOrderRelevantStates(orders, signatures).callAsync();
const [expiredOrderInfo, ...filledOrderInfos] = orderInfos;
assertOrderInfoEquals(expiredOrderInfo, {
status: OrderStatus.Expired,
orderHash: expiredOrder.getHash(),
takerTokenFilledAmount: ZERO_AMOUNT,
});
filledOrderInfos.map((orderInfo, i) =>
assertOrderInfoEquals(orderInfo, {
status: OrderStatus.Filled,
orderHash: fillableOrders[i].getHash(),
takerTokenFilledAmount: fillableOrders[i].takerAmount,
}),
);
verifyEventsFromLogs(
tx.logs,
fillableOrders.map(order => testUtils.createRfqOrderFilledEventArgs(order)),
IZeroExEvents.RfqOrderFilled,
);
return assertExpectedFinalBalancesAsync(fillableOrders);
});
it('Fills multiple orders with revertIfIncomplete=true', async () => {
const orders = [...new Array(3)].map(() => getTestRfqOrder());
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const tx = await feature
.batchFillRfqOrders(orders, signatures, orders.map(order => order.takerAmount), true)
.awaitTransactionSuccessAsync({ from: taker });
const [orderInfos] = await zeroEx.batchGetRfqOrderRelevantStates(orders, signatures).callAsync();
orderInfos.map((orderInfo, i) =>
assertOrderInfoEquals(orderInfo, {
status: OrderStatus.Filled,
orderHash: orders[i].getHash(),
takerTokenFilledAmount: orders[i].takerAmount,
}),
);
verifyEventsFromLogs(
tx.logs,
orders.map(order => testUtils.createRfqOrderFilledEventArgs(order)),
IZeroExEvents.RfqOrderFilled,
);
return assertExpectedFinalBalancesAsync(orders);
});
it('If revertIfIncomplete==true, reverts on an unfillable order', async () => {
const fillableOrders = [...new Array(3)].map(() => getTestRfqOrder());
const expiredOrder = getTestRfqOrder({ expiry: createExpiry(-1) });
const orders = [expiredOrder, ...fillableOrders];
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const tx = feature
.batchFillRfqOrders(orders, signatures, orders.map(order => order.takerAmount), true)
.awaitTransactionSuccessAsync({ from: taker });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.BatchFillIncompleteError(
expiredOrder.getHash(),
ZERO_AMOUNT,
expiredOrder.takerAmount,
),
);
});
it('If revertIfIncomplete==true, reverts on an incomplete fill ', async () => {
const fillableOrders = [...new Array(3)].map(() => getTestRfqOrder());
const partiallyFilledOrder = getTestRfqOrder();
const partialFillAmount = getRandomPortion(partiallyFilledOrder.takerAmount);
await testUtils.fillRfqOrderAsync(partiallyFilledOrder, partialFillAmount);
const orders = [partiallyFilledOrder, ...fillableOrders];
const signatures = await Promise.all(
orders.map(order => order.getSignatureWithProviderAsync(env.provider)),
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const tx = feature
.batchFillRfqOrders(orders, signatures, orders.map(order => order.takerAmount), true)
.awaitTransactionSuccessAsync({ from: taker });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.BatchFillIncompleteError(
partiallyFilledOrder.getHash(),
partiallyFilledOrder.takerAmount.minus(partialFillAmount),
partiallyFilledOrder.takerAmount,
),
);
});
});
});

View File

@@ -0,0 +1,764 @@
import {
artifacts as erc20Artifacts,
ERC20TokenContract,
WETH9Contract,
WETH9DepositEventArgs,
WETH9Events,
WETH9WithdrawalEventArgs,
} from '@0x/contracts-erc20';
import { blockchainTests, constants, expect, filterLogsToArguments, toBaseUnitAmount } from '@0x/contracts-test-utils';
import {
BridgeSource,
encodeFillQuoteTransformerData,
encodePayTakerTransformerData,
FillQuoteTransformerOrderType,
FillQuoteTransformerSide,
findTransformerNonce,
RfqOrder,
SIGNATURE_ABI,
} from '@0x/protocol-utils';
import { AbiEncoder, BigNumber, logUtils } from '@0x/utils';
import * as _ from 'lodash';
import { artifacts } from '../artifacts';
import { abis } from '../utils/abis';
import { getRandomRfqOrder } from '../utils/orders';
import {
BridgeAdapterBridgeFillEventArgs,
BridgeAdapterEvents,
IUniswapV2PairEvents,
IUniswapV2PairSwapEventArgs,
IZeroExContract,
IZeroExEvents,
IZeroExRfqOrderFilledEventArgs,
MultiplexFeatureContract,
MultiplexFeatureEvents,
MultiplexFeatureLiquidityProviderSwapEventArgs,
SimpleFunctionRegistryFeatureContract,
} from '../wrappers';
const HIGH_BIT = new BigNumber(2).pow(255);
function encodeFractionalFillAmount(frac: number): BigNumber {
return HIGH_BIT.plus(new BigNumber(frac).times('1e18').integerValue());
}
const EP_GOVERNOR = '0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e';
const DAI_WALLET = '0xbe0eb53f46cd790cd13851d5eff43d12404d33e8';
const WETH_WALLET = '0x1e0447b19bb6ecfdae1e4ae1694b0c3659614e4e';
const USDC_WALLET = '0xbe0eb53f46cd790cd13851d5eff43d12404d33e8';
blockchainTests.configure({
fork: {
unlockedAccounts: [EP_GOVERNOR, DAI_WALLET, WETH_WALLET, USDC_WALLET],
},
});
interface WrappedBatchCall {
selector: string;
sellAmount: BigNumber;
data: string;
}
blockchainTests.fork.skip('Multiplex feature', env => {
const DAI_ADDRESS = '0x6b175474e89094c44da98b954eedeac495271d0f';
const dai = new ERC20TokenContract(DAI_ADDRESS, env.provider, env.txDefaults);
const ETH_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
const WETH_ADDRESS = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
const weth = new WETH9Contract(WETH_ADDRESS, env.provider, env.txDefaults);
const USDC_ADDRESS = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48';
const USDT_ADDRESS = '0xdac17f958d2ee523a2206206994597c13d831ec7';
const usdt = new ERC20TokenContract(USDT_ADDRESS, env.provider, env.txDefaults);
const LON_ADDRESS = '0x0000000000095413afc295d19edeb1ad7b71c952';
const PLP_SANDBOX_ADDRESS = '0x407b4128e9ecad8769b2332312a9f655cb9f5f3a';
const WETH_DAI_PLP_ADDRESS = '0x1db681925786441ba82adefac7bf492089665ca0';
const WETH_USDC_PLP_ADDRESS = '0x8463c03c0c57ff19fa8b431e0d3a34e2df89888e';
const USDC_USDT_PLP_ADDRESS = '0xc340ef96449514cea4dfa11d847a06d7f03d437c';
const GREEDY_TOKENS_BLOOM_FILTER = '0x0000100800000480002c00401000000820000000000000020000001010800001';
const BALANCER_WETH_DAI = '0x8b6e6e7b5b3801fed2cafd4b22b8a16c2f2db21a';
const fqtNonce = findTransformerNonce(
'0xfa6282736af206cb4cfc5cb786d82aecdf1186f9',
'0x39dce47a67ad34344eab877eae3ef1fa2a1d50bb',
);
const payTakerNonce = findTransformerNonce(
'0x4638a7ebe75b911b995d0ec73a81e4f85f41f24e',
'0x39dce47a67ad34344eab877eae3ef1fa2a1d50bb',
);
let zeroEx: IZeroExContract;
let multiplex: MultiplexFeatureContract;
let rfqMaker: string;
let flashWalletAddress: string;
before(async () => {
const erc20Abis = _.mapValues(erc20Artifacts, v => v.compilerOutput.abi);
[rfqMaker] = await env.getAccountAddressesAsync();
zeroEx = new IZeroExContract('0xdef1c0ded9bec7f1a1670819833240f027b25eff', env.provider, env.txDefaults, {
...abis,
...erc20Abis,
});
flashWalletAddress = await zeroEx.getTransformWallet().callAsync();
const registry = new SimpleFunctionRegistryFeatureContract(zeroEx.address, env.provider, env.txDefaults, {
...abis,
...erc20Abis,
});
multiplex = new MultiplexFeatureContract(zeroEx.address, env.provider, env.txDefaults, {
...abis,
...erc20Abis,
});
const multiplexImpl = await MultiplexFeatureContract.deployFrom0xArtifactAsync(
artifacts.MultiplexFeature,
env.provider,
env.txDefaults,
artifacts,
zeroEx.address,
WETH_ADDRESS,
PLP_SANDBOX_ADDRESS,
GREEDY_TOKENS_BLOOM_FILTER,
);
await registry
.extend(multiplex.getSelector('batchFill'), multiplexImpl.address)
.awaitTransactionSuccessAsync({ from: EP_GOVERNOR, gasPrice: 0 }, { shouldValidate: false });
await registry
.extend(multiplex.getSelector('multiHopFill'), multiplexImpl.address)
.awaitTransactionSuccessAsync({ from: EP_GOVERNOR, gasPrice: 0 }, { shouldValidate: false });
await dai
.approve(zeroEx.address, constants.MAX_UINT256)
.awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false });
await weth
.transfer(rfqMaker, toBaseUnitAmount(100))
.awaitTransactionSuccessAsync({ from: WETH_WALLET, gasPrice: 0 }, { shouldValidate: false });
await weth
.approve(zeroEx.address, constants.MAX_UINT256)
.awaitTransactionSuccessAsync({ from: rfqMaker, gasPrice: 0 }, { shouldValidate: false });
});
describe('batchFill', () => {
let rfqDataEncoder: AbiEncoder.DataType;
let uniswapCall: WrappedBatchCall;
let sushiswapCall: WrappedBatchCall;
let plpCall: WrappedBatchCall;
let rfqCall: WrappedBatchCall;
let rfqOrder: RfqOrder;
before(async () => {
rfqDataEncoder = AbiEncoder.create([
{ name: 'order', type: 'tuple', components: RfqOrder.STRUCT_ABI },
{ name: 'signature', type: 'tuple', components: SIGNATURE_ABI },
]);
rfqOrder = getRandomRfqOrder({
maker: rfqMaker,
verifyingContract: zeroEx.address,
chainId: 1,
takerToken: DAI_ADDRESS,
makerToken: WETH_ADDRESS,
makerAmount: toBaseUnitAmount(100),
takerAmount: toBaseUnitAmount(100),
txOrigin: DAI_WALLET,
});
rfqCall = {
selector: zeroEx.getSelector('_fillRfqOrder'),
sellAmount: toBaseUnitAmount(1),
data: rfqDataEncoder.encode({
order: rfqOrder,
signature: await rfqOrder.getSignatureWithProviderAsync(env.provider),
}),
};
const uniswapDataEncoder = AbiEncoder.create([
{ name: 'tokens', type: 'address[]' },
{ name: 'isSushi', type: 'bool' },
]);
const plpDataEncoder = AbiEncoder.create([
{ name: 'provider', type: 'address' },
{ name: 'auxiliaryData', type: 'bytes' },
]);
uniswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
sellAmount: toBaseUnitAmount(1.01),
data: uniswapDataEncoder.encode({ tokens: [DAI_ADDRESS, WETH_ADDRESS], isSushi: false }),
};
sushiswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
sellAmount: toBaseUnitAmount(1.02),
data: uniswapDataEncoder.encode({ tokens: [DAI_ADDRESS, WETH_ADDRESS], isSushi: true }),
};
plpCall = {
selector: multiplex.getSelector('_sellToLiquidityProvider'),
sellAmount: toBaseUnitAmount(1.03),
data: plpDataEncoder.encode({
provider: WETH_DAI_PLP_ADDRESS,
auxiliaryData: constants.NULL_BYTES,
}),
};
});
it('MultiplexFeature.batchFill(RFQ, unused Uniswap fallback)', async () => {
const batchFillData = {
inputToken: DAI_ADDRESS,
outputToken: WETH_ADDRESS,
sellAmount: rfqCall.sellAmount,
calls: [rfqCall, uniswapCall],
};
const tx = await multiplex
.batchFill(batchFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false });
logUtils.log(`${tx.gasUsed} gas used`);
const [rfqEvent] = filterLogsToArguments<IZeroExRfqOrderFilledEventArgs>(
tx.logs,
IZeroExEvents.RfqOrderFilled,
);
expect(rfqEvent.maker).to.equal(rfqMaker);
expect(rfqEvent.taker).to.equal(DAI_WALLET);
expect(rfqEvent.makerToken).to.equal(WETH_ADDRESS);
expect(rfqEvent.takerToken).to.equal(DAI_ADDRESS);
expect(rfqEvent.takerTokenFilledAmount).to.bignumber.equal(rfqCall.sellAmount);
expect(rfqEvent.makerTokenFilledAmount).to.bignumber.equal(rfqCall.sellAmount);
});
it('MultiplexFeature.batchFill(expired RFQ, Uniswap fallback)', async () => {
const expiredRfqOrder = getRandomRfqOrder({
maker: rfqMaker,
verifyingContract: zeroEx.address,
chainId: 1,
takerToken: DAI_ADDRESS,
makerToken: WETH_ADDRESS,
makerAmount: toBaseUnitAmount(100),
takerAmount: toBaseUnitAmount(100),
txOrigin: DAI_WALLET,
expiry: new BigNumber(0),
});
const expiredRfqCall = {
selector: zeroEx.getSelector('_fillRfqOrder'),
sellAmount: toBaseUnitAmount(1.23),
data: rfqDataEncoder.encode({
order: expiredRfqOrder,
signature: await expiredRfqOrder.getSignatureWithProviderAsync(env.provider),
}),
};
const batchFillData = {
inputToken: DAI_ADDRESS,
outputToken: WETH_ADDRESS,
sellAmount: expiredRfqCall.sellAmount,
calls: [expiredRfqCall, uniswapCall],
};
const tx = await multiplex
.batchFill(batchFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false });
logUtils.log(`${tx.gasUsed} gas used`);
const [uniswapEvent] = filterLogsToArguments<IUniswapV2PairSwapEventArgs>(
tx.logs,
IUniswapV2PairEvents.Swap,
);
expect(uniswapEvent.sender, 'Uniswap Swap event sender').to.equal(zeroEx.address);
expect(uniswapEvent.to, 'Uniswap Swap event to').to.equal(DAI_WALLET);
expect(
BigNumber.max(uniswapEvent.amount0In, uniswapEvent.amount1In),
'Uniswap Swap event input amount',
).to.bignumber.equal(uniswapCall.sellAmount);
expect(
BigNumber.max(uniswapEvent.amount0Out, uniswapEvent.amount1Out),
'Uniswap Swap event output amount',
).to.bignumber.gt(0);
});
it('MultiplexFeature.batchFill(expired RFQ, Balancer FQT fallback)', async () => {
const expiredRfqOrder = getRandomRfqOrder({
maker: rfqMaker,
verifyingContract: zeroEx.address,
chainId: 1,
takerToken: DAI_ADDRESS,
makerToken: WETH_ADDRESS,
makerAmount: toBaseUnitAmount(100),
takerAmount: toBaseUnitAmount(100),
txOrigin: DAI_WALLET,
expiry: new BigNumber(0),
});
const expiredRfqCall = {
selector: zeroEx.getSelector('_fillRfqOrder'),
sellAmount: toBaseUnitAmount(1.23),
data: rfqDataEncoder.encode({
order: expiredRfqOrder,
signature: await expiredRfqOrder.getSignatureWithProviderAsync(env.provider),
}),
};
const poolEncoder = AbiEncoder.create([{ name: 'poolAddress', type: 'address' }]);
const fqtData = encodeFillQuoteTransformerData({
side: FillQuoteTransformerSide.Sell,
sellToken: DAI_ADDRESS,
buyToken: WETH_ADDRESS,
bridgeOrders: [
{
source: BridgeSource.Balancer,
takerTokenAmount: expiredRfqCall.sellAmount,
makerTokenAmount: expiredRfqCall.sellAmount,
bridgeData: poolEncoder.encode([BALANCER_WETH_DAI]),
},
],
limitOrders: [],
rfqOrders: [],
fillSequence: [FillQuoteTransformerOrderType.Bridge],
fillAmount: expiredRfqCall.sellAmount,
refundReceiver: constants.NULL_ADDRESS,
});
const payTakerData = encodePayTakerTransformerData({
tokens: [WETH_ADDRESS],
amounts: [constants.MAX_UINT256],
});
const transformERC20Encoder = AbiEncoder.create([
{
name: 'transformations',
type: 'tuple[]',
components: [{ name: 'deploymentNonce', type: 'uint32' }, { name: 'data', type: 'bytes' }],
},
{ name: 'ethValue', type: 'uint256' },
]);
const balancerFqtCall = {
selector: zeroEx.getSelector('_transformERC20'),
sellAmount: expiredRfqCall.sellAmount,
data: transformERC20Encoder.encode({
transformations: [
{
deploymentNonce: fqtNonce,
data: fqtData,
},
{
deploymentNonce: payTakerNonce,
data: payTakerData,
},
],
ethValue: constants.ZERO_AMOUNT,
}),
};
const batchFillData = {
inputToken: DAI_ADDRESS,
outputToken: WETH_ADDRESS,
sellAmount: expiredRfqCall.sellAmount,
calls: [expiredRfqCall, balancerFqtCall],
};
const tx = await multiplex
.batchFill(batchFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false });
logUtils.log(`${tx.gasUsed} gas used`);
const [bridgeFillEvent] = filterLogsToArguments<BridgeAdapterBridgeFillEventArgs>(
tx.logs,
BridgeAdapterEvents.BridgeFill,
);
expect(bridgeFillEvent.source).to.bignumber.equal(BridgeSource.Balancer);
expect(bridgeFillEvent.inputToken).to.equal(DAI_ADDRESS);
expect(bridgeFillEvent.outputToken).to.equal(WETH_ADDRESS);
expect(bridgeFillEvent.inputTokenAmount).to.bignumber.equal(expiredRfqCall.sellAmount);
expect(bridgeFillEvent.outputTokenAmount).to.bignumber.gt(0);
});
it('MultiplexFeature.batchFill(Sushiswap, PLP, Uniswap, RFQ)', async () => {
const batchFillData = {
inputToken: DAI_ADDRESS,
outputToken: WETH_ADDRESS,
sellAmount: BigNumber.sum(
sushiswapCall.sellAmount,
plpCall.sellAmount,
uniswapCall.sellAmount,
rfqCall.sellAmount,
),
calls: [sushiswapCall, plpCall, uniswapCall, rfqCall],
};
const tx = await multiplex
.batchFill(batchFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false });
logUtils.log(`${tx.gasUsed} gas used`);
const [sushiswapEvent, uniswapEvent] = filterLogsToArguments<IUniswapV2PairSwapEventArgs>(
tx.logs,
IUniswapV2PairEvents.Swap,
);
expect(sushiswapEvent.sender, 'Sushiswap Swap event sender').to.equal(zeroEx.address);
expect(sushiswapEvent.to, 'Sushiswap Swap event to').to.equal(DAI_WALLET);
expect(
BigNumber.max(sushiswapEvent.amount0In, sushiswapEvent.amount1In),
'Sushiswap Swap event input amount',
).to.bignumber.equal(sushiswapCall.sellAmount);
expect(
BigNumber.max(sushiswapEvent.amount0Out, sushiswapEvent.amount1Out),
'Sushiswap Swap event output amount',
).to.bignumber.gt(0);
expect(uniswapEvent.sender, 'Uniswap Swap event sender').to.equal(zeroEx.address);
expect(uniswapEvent.to, 'Uniswap Swap event to').to.equal(DAI_WALLET);
expect(
BigNumber.max(uniswapEvent.amount0In, uniswapEvent.amount1In),
'Uniswap Swap event input amount',
).to.bignumber.equal(uniswapCall.sellAmount);
expect(
BigNumber.max(uniswapEvent.amount0Out, uniswapEvent.amount1Out),
'Uniswap Swap event output amount',
).to.bignumber.gt(0);
const [plpEvent] = filterLogsToArguments<MultiplexFeatureLiquidityProviderSwapEventArgs>(
tx.logs,
MultiplexFeatureEvents.LiquidityProviderSwap,
);
expect(plpEvent.inputToken, 'LiquidityProviderSwap event inputToken').to.equal(batchFillData.inputToken);
expect(plpEvent.outputToken, 'LiquidityProviderSwap event outputToken').to.equal(batchFillData.outputToken);
expect(plpEvent.inputTokenAmount, 'LiquidityProviderSwap event inputToken').to.bignumber.equal(
plpCall.sellAmount,
);
expect(plpEvent.outputTokenAmount, 'LiquidityProviderSwap event outputTokenAmount').to.bignumber.gt(0);
expect(plpEvent.provider, 'LiquidityProviderSwap event provider address').to.equal(WETH_DAI_PLP_ADDRESS);
expect(plpEvent.recipient, 'LiquidityProviderSwap event recipient address').to.equal(DAI_WALLET);
const [rfqEvent] = filterLogsToArguments<IZeroExRfqOrderFilledEventArgs>(
tx.logs,
IZeroExEvents.RfqOrderFilled,
);
expect(rfqEvent.maker).to.equal(rfqMaker);
expect(rfqEvent.taker).to.equal(DAI_WALLET);
expect(rfqEvent.makerToken).to.equal(WETH_ADDRESS);
expect(rfqEvent.takerToken).to.equal(DAI_ADDRESS);
expect(rfqEvent.takerTokenFilledAmount).to.bignumber.equal(rfqCall.sellAmount);
expect(rfqEvent.makerTokenFilledAmount).to.bignumber.equal(rfqCall.sellAmount);
});
});
describe('multiHopFill', () => {
let uniswapDataEncoder: AbiEncoder.DataType;
let plpDataEncoder: AbiEncoder.DataType;
let curveEncoder: AbiEncoder.DataType;
let transformERC20Encoder: AbiEncoder.DataType;
let batchFillEncoder: AbiEncoder.DataType;
let multiHopFillEncoder: AbiEncoder.DataType;
before(async () => {
uniswapDataEncoder = AbiEncoder.create([
{ name: 'tokens', type: 'address[]' },
{ name: 'isSushi', type: 'bool' },
]);
plpDataEncoder = AbiEncoder.create([
{ name: 'provider', type: 'address' },
{ name: 'auxiliaryData', type: 'bytes' },
]);
curveEncoder = AbiEncoder.create([
{ name: 'curveAddress', type: 'address' },
{ name: 'exchangeFunctionSelector', type: 'bytes4' },
{ name: 'fromTokenIdx', type: 'int128' },
{ name: 'toTokenIdx', type: 'int128' },
]);
transformERC20Encoder = AbiEncoder.create([
{
name: 'transformations',
type: 'tuple[]',
components: [{ name: 'deploymentNonce', type: 'uint32' }, { name: 'data', type: 'bytes' }],
},
{ name: 'ethValue', type: 'uint256' },
]);
batchFillEncoder = AbiEncoder.create([
{
name: 'calls',
type: 'tuple[]',
components: [
{ name: 'selector', type: 'bytes4' },
{ name: 'sellAmount', type: 'uint256' },
{ name: 'data', type: 'bytes' },
],
},
{ name: 'ethValue', type: 'uint256' },
]);
multiHopFillEncoder = AbiEncoder.create([
{ name: 'tokens', type: 'address[]' },
{
name: 'calls',
type: 'tuple[]',
components: [{ name: 'selector', type: 'bytes4' }, { name: 'data', type: 'bytes' }],
},
{ name: 'ethValue', type: 'uint256' },
]);
});
it('MultiplexFeature.multiHopFill(DAI Curve> USDC Uni> WETH unwrap> ETH)', async () => {
const sellAmount = toBaseUnitAmount(1000000); // 1M DAI
const fqtData = encodeFillQuoteTransformerData({
side: FillQuoteTransformerSide.Sell,
sellToken: DAI_ADDRESS,
buyToken: USDC_ADDRESS,
bridgeOrders: [
{
source: BridgeSource.Curve,
takerTokenAmount: sellAmount,
makerTokenAmount: sellAmount,
bridgeData: curveEncoder.encode([
'0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', // 3-pool
'0x3df02124', // `exchange` selector
0, // DAI
1, // USDC
]),
},
],
limitOrders: [],
rfqOrders: [],
fillSequence: [FillQuoteTransformerOrderType.Bridge],
fillAmount: sellAmount,
refundReceiver: constants.NULL_ADDRESS,
});
const payTakerData = encodePayTakerTransformerData({
tokens: [USDC_ADDRESS],
amounts: [constants.MAX_UINT256],
});
const curveFqtCall = {
selector: zeroEx.getSelector('_transformERC20'),
sellAmount,
data: transformERC20Encoder.encode({
transformations: [
{
deploymentNonce: fqtNonce,
data: fqtData,
},
{
deploymentNonce: payTakerNonce,
data: payTakerData,
},
],
ethValue: constants.ZERO_AMOUNT,
}),
};
const uniswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
data: uniswapDataEncoder.encode({ tokens: [USDC_ADDRESS, WETH_ADDRESS], isSushi: false }),
};
const unwrapEthCall = {
selector: weth.getSelector('withdraw'),
data: constants.NULL_BYTES,
};
const multiHopFillData = {
tokens: [DAI_ADDRESS, USDC_ADDRESS, WETH_ADDRESS, ETH_TOKEN_ADDRESS],
sellAmount,
calls: [curveFqtCall, uniswapCall, unwrapEthCall],
};
const tx = await multiplex
.multiHopFill(multiHopFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false });
logUtils.log(`${tx.gasUsed} gas used`);
const [bridgeFillEvent] = filterLogsToArguments<BridgeAdapterBridgeFillEventArgs>(
tx.logs,
BridgeAdapterEvents.BridgeFill,
);
expect(bridgeFillEvent.source).to.bignumber.equal(BridgeSource.Curve);
expect(bridgeFillEvent.inputToken).to.equal(DAI_ADDRESS);
expect(bridgeFillEvent.outputToken).to.equal(USDC_ADDRESS);
expect(bridgeFillEvent.inputTokenAmount).to.bignumber.equal(sellAmount);
expect(bridgeFillEvent.outputTokenAmount).to.bignumber.gt(0);
const [uniswapEvent] = filterLogsToArguments<IUniswapV2PairSwapEventArgs>(
tx.logs,
IUniswapV2PairEvents.Swap,
);
expect(uniswapEvent.sender, 'Uniswap Swap event sender').to.equal(zeroEx.address);
expect(uniswapEvent.to, 'Uniswap Swap event to').to.equal(zeroEx.address);
const uniswapInputAmount = BigNumber.max(uniswapEvent.amount0In, uniswapEvent.amount1In);
expect(uniswapInputAmount, 'Uniswap Swap event input amount').to.bignumber.equal(
bridgeFillEvent.outputTokenAmount,
);
const uniswapOutputAmount = BigNumber.max(uniswapEvent.amount0Out, uniswapEvent.amount1Out);
expect(uniswapOutputAmount, 'Uniswap Swap event output amount').to.bignumber.gt(0);
const [wethWithdrawalEvent] = filterLogsToArguments<WETH9WithdrawalEventArgs>(
tx.logs,
WETH9Events.Withdrawal,
);
expect(wethWithdrawalEvent._owner, 'WETH Withdrawal event _owner').to.equal(zeroEx.address);
expect(wethWithdrawalEvent._value, 'WETH Withdrawal event _value').to.bignumber.equal(uniswapOutputAmount);
});
it('MultiplexFeature.multiHopFill(ETH wrap-> WETH Uni> USDC Curve> DAI)', async () => {
const sellAmount = toBaseUnitAmount(1); // 1 ETH
const fqtData = encodeFillQuoteTransformerData({
side: FillQuoteTransformerSide.Sell,
sellToken: USDC_ADDRESS,
buyToken: DAI_ADDRESS,
bridgeOrders: [
{
source: BridgeSource.Curve,
takerTokenAmount: constants.MAX_UINT256,
makerTokenAmount: constants.MAX_UINT256,
bridgeData: curveEncoder.encode([
'0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', // 3-pool
'0x3df02124', // `exchange` selector
1, // USDC
0, // DAI
]),
},
],
limitOrders: [],
rfqOrders: [],
fillSequence: [FillQuoteTransformerOrderType.Bridge],
fillAmount: constants.MAX_UINT256,
refundReceiver: constants.NULL_ADDRESS,
});
const payTakerData = encodePayTakerTransformerData({
tokens: [DAI_ADDRESS],
amounts: [constants.MAX_UINT256],
});
const curveFqtCall = {
selector: zeroEx.getSelector('_transformERC20'),
data: transformERC20Encoder.encode({
transformations: [
{
deploymentNonce: fqtNonce,
data: fqtData,
},
{
deploymentNonce: payTakerNonce,
data: payTakerData,
},
],
ethValue: constants.ZERO_AMOUNT,
}),
};
const uniswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
data: uniswapDataEncoder.encode({ tokens: [WETH_ADDRESS, USDC_ADDRESS], isSushi: false }),
};
const wrapEthCall = {
selector: weth.getSelector('deposit'),
data: constants.NULL_BYTES,
};
const multiHopFillData = {
tokens: [ETH_TOKEN_ADDRESS, WETH_ADDRESS, USDC_ADDRESS, DAI_ADDRESS],
sellAmount,
calls: [wrapEthCall, uniswapCall, curveFqtCall],
};
const tx = await multiplex
.multiHopFill(multiHopFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync(
{ from: rfqMaker, gasPrice: 0, value: sellAmount },
{ shouldValidate: false },
);
logUtils.log(`${tx.gasUsed} gas used`);
const [wethDepositEvent] = filterLogsToArguments<WETH9DepositEventArgs>(tx.logs, WETH9Events.Deposit);
expect(wethDepositEvent._owner, 'WETH Deposit event _owner').to.equal(zeroEx.address);
expect(wethDepositEvent._value, 'WETH Deposit event _value').to.bignumber.equal(sellAmount);
const [uniswapEvent] = filterLogsToArguments<IUniswapV2PairSwapEventArgs>(
tx.logs,
IUniswapV2PairEvents.Swap,
);
expect(uniswapEvent.sender, 'Uniswap Swap event sender').to.equal(zeroEx.address);
expect(uniswapEvent.to, 'Uniswap Swap event to').to.equal(flashWalletAddress);
const uniswapInputAmount = BigNumber.max(uniswapEvent.amount0In, uniswapEvent.amount1In);
expect(uniswapInputAmount, 'Uniswap Swap event input amount').to.bignumber.equal(sellAmount);
const uniswapOutputAmount = BigNumber.max(uniswapEvent.amount0Out, uniswapEvent.amount1Out);
expect(uniswapOutputAmount, 'Uniswap Swap event output amount').to.bignumber.gt(0);
const [bridgeFillEvent] = filterLogsToArguments<BridgeAdapterBridgeFillEventArgs>(
tx.logs,
BridgeAdapterEvents.BridgeFill,
);
expect(bridgeFillEvent.source).to.bignumber.equal(BridgeSource.Curve);
expect(bridgeFillEvent.inputToken).to.equal(USDC_ADDRESS);
expect(bridgeFillEvent.outputToken).to.equal(DAI_ADDRESS);
expect(bridgeFillEvent.inputTokenAmount).to.bignumber.equal(uniswapOutputAmount);
expect(bridgeFillEvent.outputTokenAmount).to.bignumber.gt(0);
});
it.skip('MultiplexFeature.multiHopFill() complex scenario', async () => {
/*
/PLP> USDC
/ \
/ PLP
/Uni (via USDC)\
/ V
ETH wrap> WETH Uni/Sushi> USDT Sushi> LON
\ ^
Uni /
*/
// Taker has to have approved the EP for the intermediate tokens :/
await weth
.approve(zeroEx.address, constants.MAX_UINT256)
.awaitTransactionSuccessAsync({ from: rfqMaker, gasPrice: 0 }, { shouldValidate: false });
await usdt
.approve(zeroEx.address, constants.MAX_UINT256)
.awaitTransactionSuccessAsync({ from: rfqMaker, gasPrice: 0 }, { shouldValidate: false });
const sellAmount = toBaseUnitAmount(1); // 1 ETH
const wethUsdcPlpCall = {
selector: multiplex.getSelector('_sellToLiquidityProvider'),
data: plpDataEncoder.encode({
provider: WETH_USDC_PLP_ADDRESS,
auxiliaryData: constants.NULL_BYTES,
}),
};
const usdcUsdtPlpCall = {
selector: multiplex.getSelector('_sellToLiquidityProvider'),
data: plpDataEncoder.encode({
provider: USDC_USDT_PLP_ADDRESS,
auxiliaryData: constants.NULL_BYTES,
}),
};
const wethUsdcUsdtMultiHopCall = {
selector: multiplex.getSelector('_multiHopFill'),
sellAmount: encodeFractionalFillAmount(0.25),
data: multiHopFillEncoder.encode({
tokens: [WETH_ADDRESS, USDC_ADDRESS, USDT_ADDRESS],
calls: [wethUsdcPlpCall, usdcUsdtPlpCall],
ethValue: constants.ZERO_AMOUNT,
}),
};
const wethUsdcUsdtUniswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
sellAmount: encodeFractionalFillAmount(0.25),
data: uniswapDataEncoder.encode({ tokens: [WETH_ADDRESS, USDC_ADDRESS, USDT_ADDRESS], isSushi: false }),
};
const wethUsdtUniswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
sellAmount: encodeFractionalFillAmount(0.25),
data: uniswapDataEncoder.encode({ tokens: [WETH_ADDRESS, USDT_ADDRESS], isSushi: false }),
};
const wethUsdtSushiswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
sellAmount: encodeFractionalFillAmount(0.25),
data: uniswapDataEncoder.encode({ tokens: [WETH_ADDRESS, USDT_ADDRESS], isSushi: true }),
};
const wethUsdtBatchCall = {
selector: multiplex.getSelector('_batchFill'),
data: batchFillEncoder.encode({
calls: [
wethUsdcUsdtMultiHopCall,
wethUsdcUsdtUniswapCall,
wethUsdtUniswapCall,
wethUsdtSushiswapCall,
],
ethValue: constants.ZERO_AMOUNT,
}),
};
const usdtLonSushiCall = {
selector: multiplex.getSelector('_sellToUniswap'),
data: uniswapDataEncoder.encode({ tokens: [USDT_ADDRESS, LON_ADDRESS], isSushi: true }),
};
const wethUsdtLonMultiHopCall = {
selector: multiplex.getSelector('_multiHopFill'),
sellAmount: encodeFractionalFillAmount(0.8),
data: multiHopFillEncoder.encode({
tokens: [WETH_ADDRESS, USDT_ADDRESS],
calls: [wethUsdtBatchCall, usdtLonSushiCall],
ethValue: constants.ZERO_AMOUNT,
}),
};
const wethLonUniswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
sellAmount: encodeFractionalFillAmount(0.2),
data: uniswapDataEncoder.encode({ tokens: [WETH_ADDRESS, LON_ADDRESS], isSushi: false }),
};
const wethLonBatchFillCall = {
selector: multiplex.getSelector('_batchFill'),
data: batchFillEncoder.encode({
calls: [wethUsdtLonMultiHopCall, wethLonUniswapCall],
ethValue: constants.ZERO_AMOUNT,
}),
};
const wrapEthCall = {
selector: weth.getSelector('deposit'),
data: constants.NULL_BYTES,
};
const multiHopFillData = {
tokens: [ETH_TOKEN_ADDRESS, WETH_ADDRESS, LON_ADDRESS],
sellAmount,
calls: [wrapEthCall, wethLonBatchFillCall],
};
const tx = await multiplex
.multiHopFill(multiHopFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync(
{ from: rfqMaker, gasPrice: 0, value: sellAmount },
{ shouldValidate: false },
);
logUtils.log(`${tx.gasUsed} gas used`);
});
});
});

View File

@@ -3,25 +3,28 @@ import {
constants,
describe,
expect,
getRandomPortion,
randomAddress,
verifyEventsFromLogs,
} from '@0x/contracts-test-utils';
import {
LimitOrder,
LimitOrderFields,
OrderInfo,
OrderStatus,
RevertErrors,
RfqOrder,
RfqOrderFields,
} from '@0x/protocol-utils';
import { LimitOrder, LimitOrderFields, OrderStatus, RevertErrors, RfqOrder, RfqOrderFields } 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 { getRandomLimitOrder, getRandomRfqOrder } from '../utils/orders';
import {
assertOrderInfoEquals,
computeLimitOrderFilledAmounts,
computeRfqOrderFilledAmounts,
createExpiry,
getActualFillableTakerTokenAmount,
getFillableMakerTokenAmount,
getRandomLimitOrder,
getRandomRfqOrder,
NativeOrdersTestEnvironment,
} from '../utils/orders';
import { TestMintableERC20TokenContract, TestRfqOriginRegistrationContract } from '../wrappers';
blockchainTests.resets('NativeOrdersFeature', env => {
@@ -39,6 +42,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
let takerToken: TestMintableERC20TokenContract;
let wethToken: TestMintableERC20TokenContract;
let testRfqOriginRegistration: TestRfqOriginRegistrationContract;
let testUtils: NativeOrdersTestEnvironment;
before(async () => {
let owner;
@@ -78,6 +82,16 @@ blockchainTests.resets('NativeOrdersFeature', env => {
env.txDefaults,
artifacts,
);
testUtils = new NativeOrdersTestEnvironment(
maker,
taker,
makerToken,
takerToken,
zeroEx,
GAS_PRICE,
SINGLE_PROTOCOL_FEE,
env,
);
});
function getTestLimitOrder(fields: Partial<LimitOrderFields> = {}): LimitOrder {
@@ -105,27 +119,6 @@ blockchainTests.resets('NativeOrdersFeature', env => {
});
}
async function prepareBalancesForOrderAsync(order: LimitOrder | RfqOrder, _taker: string = taker): Promise<void> {
await makerToken.mint(maker, order.makerAmount).awaitTransactionSuccessAsync();
if ('takerTokenFeeAmount' in order) {
await takerToken
.mint(_taker, order.takerAmount.plus(order.takerTokenFeeAmount))
.awaitTransactionSuccessAsync();
} else {
await takerToken.mint(_taker, order.takerAmount).awaitTransactionSuccessAsync();
}
}
function assertOrderInfoEquals(actual: OrderInfo, expected: OrderInfo): void {
expect(actual.status).to.eq(expected.status);
expect(actual.orderHash).to.eq(expected.orderHash);
expect(actual.takerTokenFilledAmount).to.bignumber.eq(expected.takerTokenFilledAmount);
}
function createExpiry(deltaSeconds: number = 60): BigNumber {
return new BigNumber(Math.floor(Date.now() / 1000) + deltaSeconds);
}
describe('getProtocolFeeMultiplier()', () => {
it('returns the protocol fee multiplier', async () => {
const r = await zeroEx.getProtocolFeeMultiplier().callAsync();
@@ -149,26 +142,6 @@ blockchainTests.resets('NativeOrdersFeature', env => {
});
});
async function fillLimitOrderAsync(
order: LimitOrder,
opts: Partial<{
fillAmount: BigNumber | number;
taker: string;
protocolFee?: BigNumber | number;
}> = {},
): Promise<TransactionReceiptWithDecodedLogs> {
const { fillAmount, taker: _taker, protocolFee } = {
taker,
fillAmount: order.takerAmount,
...opts,
};
await prepareBalancesForOrderAsync(order, _taker);
const _protocolFee = protocolFee === undefined ? SINGLE_PROTOCOL_FEE : protocolFee;
return zeroEx
.fillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), new BigNumber(fillAmount))
.awaitTransactionSuccessAsync({ from: _taker, value: _protocolFee });
}
describe('getLimitOrderInfo()', () => {
it('unfilled order', async () => {
const order = getTestLimitOrder();
@@ -205,7 +178,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
const expiry = createExpiry(60);
const order = getTestLimitOrder({ expiry });
// Fill the order first.
await fillLimitOrderAsync(order);
await testUtils.fillLimitOrderAsync(order);
// Advance time to expire the order.
await env.web3Wrapper.increaseTimeAsync(61);
const info = await zeroEx.getLimitOrderInfo(order).callAsync();
@@ -219,7 +192,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('filled order', async () => {
const order = getTestLimitOrder();
// Fill the order first.
await fillLimitOrderAsync(order);
await testUtils.fillLimitOrderAsync(order);
const info = await zeroEx.getLimitOrderInfo(order).callAsync();
assertOrderInfoEquals(info, {
status: OrderStatus.Filled,
@@ -232,7 +205,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
const order = getTestLimitOrder();
const fillAmount = order.takerAmount.minus(1);
// Fill the order first.
await fillLimitOrderAsync(order, { fillAmount });
await testUtils.fillLimitOrderAsync(order, { fillAmount });
const info = await zeroEx.getLimitOrderInfo(order).callAsync();
assertOrderInfoEquals(info, {
status: OrderStatus.Fillable,
@@ -244,7 +217,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('filled then cancelled order', async () => {
const order = getTestLimitOrder();
// Fill the order first.
await fillLimitOrderAsync(order);
await testUtils.fillLimitOrderAsync(order);
await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
const info = await zeroEx.getLimitOrderInfo(order).callAsync();
assertOrderInfoEquals(info, {
@@ -258,7 +231,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
const order = getTestLimitOrder();
const fillAmount = order.takerAmount.minus(1);
// Fill the order first.
await fillLimitOrderAsync(order, { fillAmount });
await testUtils.fillLimitOrderAsync(order, { fillAmount });
await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
const info = await zeroEx.getLimitOrderInfo(order).callAsync();
assertOrderInfoEquals(info, {
@@ -269,17 +242,6 @@ blockchainTests.resets('NativeOrdersFeature', env => {
});
});
async function fillRfqOrderAsync(
order: RfqOrder,
fillAmount: BigNumber | number = order.takerAmount,
_taker: string = taker,
): Promise<TransactionReceiptWithDecodedLogs> {
await prepareBalancesForOrderAsync(order, _taker);
return zeroEx
.fillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), new BigNumber(fillAmount))
.awaitTransactionSuccessAsync({ from: _taker });
}
describe('getRfqOrderInfo()', () => {
it('unfilled order', async () => {
const order = getTestRfqOrder();
@@ -316,7 +278,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('filled then expired order', async () => {
const expiry = createExpiry(60);
const order = getTestRfqOrder({ expiry });
await prepareBalancesForOrderAsync(order);
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 });
@@ -333,7 +295,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('filled order', async () => {
const order = getTestRfqOrder();
// Fill the order first.
await fillRfqOrderAsync(order, order.takerAmount, taker);
await testUtils.fillRfqOrderAsync(order, order.takerAmount, taker);
const info = await zeroEx.getRfqOrderInfo(order).callAsync();
assertOrderInfoEquals(info, {
status: OrderStatus.Filled,
@@ -346,7 +308,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
const order = getTestRfqOrder();
const fillAmount = order.takerAmount.minus(1);
// Fill the order first.
await fillRfqOrderAsync(order, fillAmount);
await testUtils.fillRfqOrderAsync(order, fillAmount);
const info = await zeroEx.getRfqOrderInfo(order).callAsync();
assertOrderInfoEquals(info, {
status: OrderStatus.Fillable,
@@ -358,7 +320,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('filled then cancelled order', async () => {
const order = getTestRfqOrder();
// Fill the order first.
await fillRfqOrderAsync(order);
await testUtils.fillRfqOrderAsync(order);
await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
const info = await zeroEx.getRfqOrderInfo(order).callAsync();
assertOrderInfoEquals(info, {
@@ -372,7 +334,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
const order = getTestRfqOrder();
const fillAmount = order.takerAmount.minus(1);
// Fill the order first.
await fillRfqOrderAsync(order, fillAmount);
await testUtils.fillRfqOrderAsync(order, fillAmount);
await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
const info = await zeroEx.getRfqOrderInfo(order).callAsync();
assertOrderInfoEquals(info, {
@@ -408,7 +370,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('can cancel a fully filled order', async () => {
const order = getTestLimitOrder();
await fillLimitOrderAsync(order);
await testUtils.fillLimitOrderAsync(order);
const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
verifyEventsFromLogs(
receipt.logs,
@@ -421,7 +383,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('can cancel a partially filled order', async () => {
const order = getTestLimitOrder();
await fillLimitOrderAsync(order, { fillAmount: order.takerAmount.minus(1) });
await testUtils.fillLimitOrderAsync(order, { fillAmount: order.takerAmount.minus(1) });
const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
verifyEventsFromLogs(
receipt.logs,
@@ -482,7 +444,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('can cancel a fully filled order', async () => {
const order = getTestRfqOrder();
await fillRfqOrderAsync(order);
await testUtils.fillRfqOrderAsync(order);
const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
verifyEventsFromLogs(
receipt.logs,
@@ -495,7 +457,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('can cancel a partially filled order', async () => {
const order = getTestRfqOrder();
await fillRfqOrderAsync(order, order.takerAmount.minus(1));
await testUtils.fillRfqOrderAsync(order, order.takerAmount.minus(1));
const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
verifyEventsFromLogs(
receipt.logs,
@@ -747,63 +709,6 @@ blockchainTests.resets('NativeOrdersFeature', env => {
});
});
interface LimitOrderFilledAmounts {
makerTokenFilledAmount: BigNumber;
takerTokenFilledAmount: BigNumber;
takerTokenFeeFilledAmount: BigNumber;
}
function computeLimitOrderFilledAmounts(
order: LimitOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
takerTokenAlreadyFilledAmount: BigNumber = ZERO_AMOUNT,
): LimitOrderFilledAmounts {
const fillAmount = BigNumber.min(
order.takerAmount,
takerTokenFillAmount,
order.takerAmount.minus(takerTokenAlreadyFilledAmount),
);
const makerTokenFilledAmount = fillAmount
.times(order.makerAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
const takerTokenFeeFilledAmount = fillAmount
.times(order.takerTokenFeeAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
return {
makerTokenFilledAmount,
takerTokenFilledAmount: fillAmount,
takerTokenFeeFilledAmount,
};
}
function createLimitOrderFilledEventArgs(
order: LimitOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
takerTokenAlreadyFilledAmount: BigNumber = ZERO_AMOUNT,
): object {
const {
makerTokenFilledAmount,
takerTokenFilledAmount,
takerTokenFeeFilledAmount,
} = computeLimitOrderFilledAmounts(order, takerTokenFillAmount, takerTokenAlreadyFilledAmount);
const protocolFee = order.taker !== NULL_ADDRESS ? ZERO_AMOUNT : SINGLE_PROTOCOL_FEE;
return {
taker,
takerTokenFilledAmount,
makerTokenFilledAmount,
takerTokenFeeFilledAmount,
orderHash: order.getHash(),
maker: order.maker,
feeRecipient: order.feeRecipient,
makerToken: order.makerToken,
takerToken: order.takerToken,
protocolFeePaid: protocolFee,
pool: order.pool,
};
}
async function assertExpectedFinalBalancesFromLimitOrderFillAsync(
order: LimitOrder,
opts: Partial<{
@@ -841,10 +746,10 @@ blockchainTests.resets('NativeOrdersFeature', env => {
describe('fillLimitOrder()', () => {
it('can fully fill an order', async () => {
const order = getTestLimitOrder();
const receipt = await fillLimitOrderAsync(order);
const receipt = await testUtils.fillLimitOrderAsync(order);
verifyEventsFromLogs(
receipt.logs,
[createLimitOrderFilledEventArgs(order)],
[testUtils.createLimitOrderFilledEventArgs(order)],
IZeroExEvents.LimitOrderFilled,
);
assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), {
@@ -858,10 +763,10 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('can partially fill an order', async () => {
const order = getTestLimitOrder();
const fillAmount = order.takerAmount.minus(1);
const receipt = await fillLimitOrderAsync(order, { fillAmount });
const receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount });
verifyEventsFromLogs(
receipt.logs,
[createLimitOrderFilledEventArgs(order, fillAmount)],
[testUtils.createLimitOrderFilledEventArgs(order, fillAmount)],
IZeroExEvents.LimitOrderFilled,
);
assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), {
@@ -869,24 +774,26 @@ blockchainTests.resets('NativeOrdersFeature', env => {
status: OrderStatus.Fillable,
takerTokenFilledAmount: fillAmount,
});
await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { takerTokenFillAmount: 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 fillLimitOrderAsync(order, { fillAmount });
let receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount });
verifyEventsFromLogs(
receipt.logs,
[createLimitOrderFilledEventArgs(order, fillAmount)],
[testUtils.createLimitOrderFilledEventArgs(order, fillAmount)],
IZeroExEvents.LimitOrderFilled,
);
const alreadyFilledAmount = fillAmount;
fillAmount = order.takerAmount.minus(fillAmount);
receipt = await fillLimitOrderAsync(order, { fillAmount });
receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount });
verifyEventsFromLogs(
receipt.logs,
[createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
[testUtils.createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
IZeroExEvents.LimitOrderFilled,
);
assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), {
@@ -899,10 +806,10 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('clamps fill amount to remaining available', async () => {
const order = getTestLimitOrder();
const fillAmount = order.takerAmount.plus(1);
const receipt = await fillLimitOrderAsync(order, { fillAmount });
const receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount });
verifyEventsFromLogs(
receipt.logs,
[createLimitOrderFilledEventArgs(order, fillAmount)],
[testUtils.createLimitOrderFilledEventArgs(order, fillAmount)],
IZeroExEvents.LimitOrderFilled,
);
assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), {
@@ -910,24 +817,26 @@ blockchainTests.resets('NativeOrdersFeature', env => {
status: OrderStatus.Filled,
takerTokenFilledAmount: order.takerAmount,
});
await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { takerTokenFillAmount: fillAmount });
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 fillLimitOrderAsync(order, { fillAmount });
let receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount });
verifyEventsFromLogs(
receipt.logs,
[createLimitOrderFilledEventArgs(order, fillAmount)],
[testUtils.createLimitOrderFilledEventArgs(order, fillAmount)],
IZeroExEvents.LimitOrderFilled,
);
const alreadyFilledAmount = fillAmount;
fillAmount = order.takerAmount.minus(fillAmount).plus(1);
receipt = await fillLimitOrderAsync(order, { fillAmount });
receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount });
verifyEventsFromLogs(
receipt.logs,
[createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
[testUtils.createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
IZeroExEvents.LimitOrderFilled,
);
assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), {
@@ -939,7 +848,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('cannot fill an expired order', async () => {
const order = getTestLimitOrder({ expiry: createExpiry(-60) });
const tx = fillLimitOrderAsync(order);
const tx = testUtils.fillLimitOrderAsync(order);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Expired),
);
@@ -948,7 +857,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('cannot fill a cancelled order', async () => {
const order = getTestLimitOrder();
await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
const tx = fillLimitOrderAsync(order);
const tx = testUtils.fillLimitOrderAsync(order);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled),
);
@@ -959,7 +868,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
await zeroEx
.cancelPairLimitOrders(makerToken.address, takerToken.address, order.salt.plus(1))
.awaitTransactionSuccessAsync({ from: maker });
const tx = fillLimitOrderAsync(order);
const tx = testUtils.fillLimitOrderAsync(order);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled),
);
@@ -967,7 +876,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('non-taker cannot fill order', async () => {
const order = getTestLimitOrder({ taker });
const tx = fillLimitOrderAsync(order, { fillAmount: order.takerAmount, taker: notTaker });
const tx = testUtils.fillLimitOrderAsync(order, { fillAmount: order.takerAmount, taker: notTaker });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableByTakerError(order.getHash(), notTaker, order.taker),
);
@@ -975,7 +884,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('non-sender cannot fill order', async () => {
const order = getTestLimitOrder({ sender: taker });
const tx = fillLimitOrderAsync(order, { fillAmount: order.takerAmount, taker: notTaker });
const tx = testUtils.fillLimitOrderAsync(order, { fillAmount: order.takerAmount, taker: notTaker });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableBySenderError(order.getHash(), notTaker, order.sender),
);
@@ -985,7 +894,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
const order = getTestLimitOrder();
// Overwrite chainId to result in a different hash and therefore different
// signature.
const tx = fillLimitOrderAsync(order.clone({ chainId: 1234 }));
const tx = testUtils.fillLimitOrderAsync(order.clone({ chainId: 1234 }));
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), undefined, order.maker),
);
@@ -993,7 +902,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('fails if no protocol fee attached', async () => {
const order = getTestLimitOrder();
await prepareBalancesForOrderAsync(order);
await testUtils.prepareBalancesForOrdersAsync([order]);
const tx = zeroEx
.fillLimitOrder(
order,
@@ -1008,62 +917,24 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('refunds excess protocol fee', async () => {
const order = getTestLimitOrder();
const receipt = await fillLimitOrderAsync(order, { protocolFee: SINGLE_PROTOCOL_FEE.plus(1) });
const receipt = await testUtils.fillLimitOrderAsync(order, { protocolFee: SINGLE_PROTOCOL_FEE.plus(1) });
verifyEventsFromLogs(
receipt.logs,
[createLimitOrderFilledEventArgs(order)],
[testUtils.createLimitOrderFilledEventArgs(order)],
IZeroExEvents.LimitOrderFilled,
);
await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { receipt });
});
});
interface RfqOrderFilledAmounts {
makerTokenFilledAmount: BigNumber;
takerTokenFilledAmount: BigNumber;
}
function computeRfqOrderFilledAmounts(
order: RfqOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
takerTokenAlreadyFilledAmount: BigNumber = ZERO_AMOUNT,
): RfqOrderFilledAmounts {
const fillAmount = BigNumber.min(
order.takerAmount,
takerTokenFillAmount,
order.takerAmount.minus(takerTokenAlreadyFilledAmount),
);
const makerTokenFilledAmount = fillAmount
.times(order.makerAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
return {
makerTokenFilledAmount,
takerTokenFilledAmount: fillAmount,
};
}
function createRfqOrderFilledEventArgs(
order: RfqOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
takerTokenAlreadyFilledAmount: BigNumber = ZERO_AMOUNT,
): object {
const { makerTokenFilledAmount, takerTokenFilledAmount } = computeRfqOrderFilledAmounts(
order,
takerTokenFillAmount,
takerTokenAlreadyFilledAmount,
);
return {
taker,
takerTokenFilledAmount,
makerTokenFilledAmount,
orderHash: order.getHash(),
maker: order.maker,
makerToken: order.makerToken,
takerToken: order.takerToken,
pool: order.pool,
};
}
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,
@@ -1081,20 +952,15 @@ blockchainTests.resets('NativeOrdersFeature', env => {
expect(takerBalance).to.bignumber.eq(makerTokenFilledAmount);
}
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');
});
});
describe('fillRfqOrder()', () => {
it('can fully fill an order', async () => {
const order = getTestRfqOrder();
const receipt = await fillRfqOrderAsync(order);
verifyEventsFromLogs(receipt.logs, [createRfqOrderFilledEventArgs(order)], IZeroExEvents.RfqOrderFilled);
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,
@@ -1106,10 +972,10 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('can partially fill an order', async () => {
const order = getTestRfqOrder();
const fillAmount = order.takerAmount.minus(1);
const receipt = await fillRfqOrderAsync(order, fillAmount);
const receipt = await testUtils.fillRfqOrderAsync(order, fillAmount);
verifyEventsFromLogs(
receipt.logs,
[createRfqOrderFilledEventArgs(order, fillAmount)],
[testUtils.createRfqOrderFilledEventArgs(order, fillAmount)],
IZeroExEvents.RfqOrderFilled,
);
assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), {
@@ -1123,18 +989,18 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('can fully fill an order in two steps', async () => {
const order = getTestRfqOrder();
let fillAmount = order.takerAmount.dividedToIntegerBy(2);
let receipt = await fillRfqOrderAsync(order, fillAmount);
let receipt = await testUtils.fillRfqOrderAsync(order, fillAmount);
verifyEventsFromLogs(
receipt.logs,
[createRfqOrderFilledEventArgs(order, fillAmount)],
[testUtils.createRfqOrderFilledEventArgs(order, fillAmount)],
IZeroExEvents.RfqOrderFilled,
);
const alreadyFilledAmount = fillAmount;
fillAmount = order.takerAmount.minus(fillAmount);
receipt = await fillRfqOrderAsync(order, fillAmount);
receipt = await testUtils.fillRfqOrderAsync(order, fillAmount);
verifyEventsFromLogs(
receipt.logs,
[createRfqOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
[testUtils.createRfqOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
IZeroExEvents.RfqOrderFilled,
);
assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), {
@@ -1147,10 +1013,10 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('clamps fill amount to remaining available', async () => {
const order = getTestRfqOrder();
const fillAmount = order.takerAmount.plus(1);
const receipt = await fillRfqOrderAsync(order, fillAmount);
const receipt = await testUtils.fillRfqOrderAsync(order, fillAmount);
verifyEventsFromLogs(
receipt.logs,
[createRfqOrderFilledEventArgs(order, fillAmount)],
[testUtils.createRfqOrderFilledEventArgs(order, fillAmount)],
IZeroExEvents.RfqOrderFilled,
);
assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), {
@@ -1164,18 +1030,18 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('clamps fill amount to remaining available in partial filled order', async () => {
const order = getTestRfqOrder();
let fillAmount = order.takerAmount.dividedToIntegerBy(2);
let receipt = await fillRfqOrderAsync(order, fillAmount);
let receipt = await testUtils.fillRfqOrderAsync(order, fillAmount);
verifyEventsFromLogs(
receipt.logs,
[createRfqOrderFilledEventArgs(order, fillAmount)],
[testUtils.createRfqOrderFilledEventArgs(order, fillAmount)],
IZeroExEvents.RfqOrderFilled,
);
const alreadyFilledAmount = fillAmount;
fillAmount = order.takerAmount.minus(fillAmount).plus(1);
receipt = await fillRfqOrderAsync(order, fillAmount);
receipt = await testUtils.fillRfqOrderAsync(order, fillAmount);
verifyEventsFromLogs(
receipt.logs,
[createRfqOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
[testUtils.createRfqOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
IZeroExEvents.RfqOrderFilled,
);
assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), {
@@ -1187,7 +1053,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('cannot fill an order with wrong tx.origin', async () => {
const order = getTestRfqOrder();
const tx = fillRfqOrderAsync(order, order.takerAmount, notTaker);
const tx = testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), notTaker, taker),
);
@@ -1210,7 +1076,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
],
IZeroExEvents.RfqOrderOriginsAllowed,
);
return fillRfqOrderAsync(order, order.takerAmount, notTaker);
return testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker);
});
it('cannot fill an order with registered then unregistered tx.origin', async () => {
@@ -1232,7 +1098,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
IZeroExEvents.RfqOrderOriginsAllowed,
);
const tx = fillRfqOrderAsync(order, order.takerAmount, notTaker);
const tx = testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), notTaker, taker),
);
@@ -1240,7 +1106,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('cannot fill an order with a zero tx.origin', async () => {
const order = getTestRfqOrder({ txOrigin: NULL_ADDRESS });
const tx = fillRfqOrderAsync(order, order.takerAmount, notTaker);
const tx = testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Invalid),
);
@@ -1248,7 +1114,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('non-taker cannot fill order', async () => {
const order = getTestRfqOrder({ taker, txOrigin: notTaker });
const tx = fillRfqOrderAsync(order, order.takerAmount, notTaker);
const tx = testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableByTakerError(order.getHash(), notTaker, order.taker),
);
@@ -1256,7 +1122,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('cannot fill an expired order', async () => {
const order = getTestRfqOrder({ expiry: createExpiry(-60) });
const tx = fillRfqOrderAsync(order);
const tx = testUtils.fillRfqOrderAsync(order);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Expired),
);
@@ -1265,7 +1131,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('cannot fill a cancelled order', async () => {
const order = getTestRfqOrder();
await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker });
const tx = fillRfqOrderAsync(order);
const tx = testUtils.fillRfqOrderAsync(order);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled),
);
@@ -1276,7 +1142,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
await zeroEx
.cancelPairRfqOrders(makerToken.address, takerToken.address, order.salt.plus(1))
.awaitTransactionSuccessAsync({ from: maker });
const tx = fillRfqOrderAsync(order);
const tx = testUtils.fillRfqOrderAsync(order);
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled),
);
@@ -1286,7 +1152,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
const order = getTestRfqOrder();
// Overwrite chainId to result in a different hash and therefore different
// signature.
const tx = fillRfqOrderAsync(order.clone({ chainId: 1234 }));
const tx = testUtils.fillRfqOrderAsync(order.clone({ chainId: 1234 }));
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), undefined, order.maker),
);
@@ -1294,7 +1160,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('fails if ETH is attached', async () => {
const order = getTestRfqOrder();
await prepareBalancesForOrderAsync(order, taker);
await testUtils.prepareBalancesForOrdersAsync([order], taker);
const tx = zeroEx
.fillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount)
.awaitTransactionSuccessAsync({ from: taker, value: 1 });
@@ -1306,20 +1172,20 @@ blockchainTests.resets('NativeOrdersFeature', env => {
describe('fillOrKillLimitOrder()', () => {
it('can fully fill an order', async () => {
const order = getTestLimitOrder();
await prepareBalancesForOrderAsync(order);
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,
[createLimitOrderFilledEventArgs(order)],
[testUtils.createLimitOrderFilledEventArgs(order)],
IZeroExEvents.LimitOrderFilled,
);
});
it('reverts if cannot fill the exact amount', async () => {
const order = getTestLimitOrder();
await prepareBalancesForOrderAsync(order);
await testUtils.prepareBalancesForOrdersAsync([order]);
const fillAmount = order.takerAmount.plus(1);
const tx = zeroEx
.fillOrKillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), fillAmount)
@@ -1331,7 +1197,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('refunds excess protocol fee', async () => {
const order = getTestLimitOrder();
await prepareBalancesForOrderAsync(order);
await testUtils.prepareBalancesForOrdersAsync([order]);
const takerBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker);
const receipt = await zeroEx
.fillOrKillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount)
@@ -1345,16 +1211,20 @@ blockchainTests.resets('NativeOrdersFeature', env => {
describe('fillOrKillRfqOrder()', () => {
it('can fully fill an order', async () => {
const order = getTestRfqOrder();
await prepareBalancesForOrderAsync(order);
await testUtils.prepareBalancesForOrdersAsync([order]);
const receipt = await zeroEx
.fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount)
.awaitTransactionSuccessAsync({ from: taker });
verifyEventsFromLogs(receipt.logs, [createRfqOrderFilledEventArgs(order)], IZeroExEvents.RfqOrderFilled);
verifyEventsFromLogs(
receipt.logs,
[testUtils.createRfqOrderFilledEventArgs(order)],
IZeroExEvents.RfqOrderFilled,
);
});
it('reverts if cannot fill the exact amount', async () => {
const order = getTestRfqOrder();
await prepareBalancesForOrderAsync(order);
await testUtils.prepareBalancesForOrdersAsync([order]);
const fillAmount = order.takerAmount.plus(1);
const tx = zeroEx
.fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), fillAmount)
@@ -1366,7 +1236,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('fails if ETH is attached', async () => {
const order = getTestRfqOrder();
await prepareBalancesForOrderAsync(order);
await testUtils.prepareBalancesForOrdersAsync([order]);
const tx = zeroEx
.fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount)
.awaitTransactionSuccessAsync({ from: taker, value: 1 });
@@ -1385,34 +1255,6 @@ blockchainTests.resets('NativeOrdersFeature', env => {
await makerToken.approve(zeroEx.address, allowance).awaitTransactionSuccessAsync({ from: maker });
}
function getFillableMakerTokenAmount(
order: LimitOrder | RfqOrder,
takerTokenFilledAmount: BigNumber = ZERO_AMOUNT,
): BigNumber {
return order.takerAmount
.minus(takerTokenFilledAmount)
.times(order.makerAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
}
function getActualFillableTakerTokenAmount(
order: LimitOrder | RfqOrder,
makerBalance: BigNumber = order.makerAmount,
makerAllowance: BigNumber = order.makerAmount,
takerTokenFilledAmount: BigNumber = ZERO_AMOUNT,
): BigNumber {
const fillableMakerTokenAmount = getFillableMakerTokenAmount(order, takerTokenFilledAmount);
return BigNumber.min(fillableMakerTokenAmount, makerBalance, makerAllowance)
.times(order.takerAmount)
.div(order.makerAmount)
.integerValue(BigNumber.ROUND_UP);
}
function getRandomFraction(precision: number = 2): string {
return Math.random().toPrecision(precision);
}
describe('getLimitOrderRelevantState()', () => {
it('works with an empty order', async () => {
const order = getTestLimitOrder({
@@ -1487,7 +1329,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
await takerToken
.mint(taker, order.takerAmount.plus(order.takerTokenFeeAmount))
.awaitTransactionSuccessAsync();
await fillLimitOrderAsync(order);
await testUtils.fillLimitOrderAsync(order);
// Partially fill the order.
const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx
.getLimitOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider))
@@ -1509,12 +1351,12 @@ blockchainTests.resets('NativeOrdersFeature', env => {
.mint(taker, order.takerAmount.plus(order.takerTokenFeeAmount))
.awaitTransactionSuccessAsync();
// Partially fill the order.
const fillAmount = order.takerAmount.times(getRandomFraction()).integerValue();
await fillLimitOrderAsync(order, { fillAmount });
const fillAmount = getRandomPortion(order.takerAmount);
await testUtils.fillLimitOrderAsync(order, { fillAmount });
// Reduce maker funds to be < remaining.
const remainingMakerAmount = getFillableMakerTokenAmount(order, fillAmount);
const balance = remainingMakerAmount.times(getRandomFraction()).integerValue();
const allowance = remainingMakerAmount.times(getRandomFraction()).integerValue();
const balance = getRandomPortion(remainingMakerAmount);
const allowance = getRandomPortion(remainingMakerAmount);
await fundOrderMakerAsync(order, balance, allowance);
// Get order state.
const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx
@@ -1604,7 +1446,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
// Fully Fund maker and taker.
await fundOrderMakerAsync(order);
await takerToken.mint(taker, order.takerAmount);
await fillRfqOrderAsync(order);
await testUtils.fillRfqOrderAsync(order);
// Partially fill the order.
const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx
.getRfqOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider))
@@ -1624,12 +1466,12 @@ blockchainTests.resets('NativeOrdersFeature', env => {
await fundOrderMakerAsync(order);
await takerToken.mint(taker, order.takerAmount).awaitTransactionSuccessAsync();
// Partially fill the order.
const fillAmount = order.takerAmount.times(getRandomFraction()).integerValue();
await fillRfqOrderAsync(order, fillAmount);
const fillAmount = getRandomPortion(order.takerAmount);
await testUtils.fillRfqOrderAsync(order, fillAmount);
// Reduce maker funds to be < remaining.
const remainingMakerAmount = getFillableMakerTokenAmount(order, fillAmount);
const balance = remainingMakerAmount.times(getRandomFraction()).integerValue();
const allowance = remainingMakerAmount.times(getRandomFraction()).integerValue();
const balance = getRandomPortion(remainingMakerAmount);
const allowance = getRandomPortion(remainingMakerAmount);
await fundOrderMakerAsync(order, balance, allowance);
// Get order state.
const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx

View File

@@ -1,6 +1,181 @@
import { getRandomInteger, randomAddress } from '@0x/contracts-test-utils';
import { LimitOrder, LimitOrderFields, RfqOrder, RfqOrderFields } from '@0x/protocol-utils';
import {
BlockchainTestsEnvironment,
constants,
expect,
getRandomInteger,
randomAddress,
} from '@0x/contracts-test-utils';
import { LimitOrder, LimitOrderFields, OrderBase, OrderInfo, RfqOrder, RfqOrderFields } from '@0x/protocol-utils';
import { BigNumber, hexUtils } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import { IZeroExContract, IZeroExLimitOrderFilledEventArgs, IZeroExRfqOrderFilledEventArgs } from '../../src/wrappers';
import { artifacts } from '../artifacts';
import { fullMigrateAsync } from '../utils/migration';
import { TestMintableERC20TokenContract } from '../wrappers';
const { ZERO_AMOUNT: ZERO, NULL_ADDRESS } = constants;
interface RfqOrderFilledAmounts {
makerTokenFilledAmount: BigNumber;
takerTokenFilledAmount: BigNumber;
}
interface LimitOrderFilledAmounts {
makerTokenFilledAmount: BigNumber;
takerTokenFilledAmount: BigNumber;
takerTokenFeeFilledAmount: BigNumber;
}
export class NativeOrdersTestEnvironment {
public static async createAsync(
env: BlockchainTestsEnvironment,
gasPrice: BigNumber = new BigNumber('123e9'),
protocolFeeMultiplier: number = 70e3,
): Promise<NativeOrdersTestEnvironment> {
const [owner, maker, taker] = await env.getAccountAddressesAsync();
const [makerToken, takerToken] = await Promise.all(
[...new Array(2)].map(async () =>
TestMintableERC20TokenContract.deployFrom0xArtifactAsync(
artifacts.TestMintableERC20Token,
env.provider,
{ ...env.txDefaults, gasPrice },
artifacts,
),
),
);
const zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, {}, { protocolFeeMultiplier });
await makerToken.approve(zeroEx.address, constants.MAX_UINT256).awaitTransactionSuccessAsync({ from: maker });
await takerToken.approve(zeroEx.address, constants.MAX_UINT256).awaitTransactionSuccessAsync({ from: taker });
return new NativeOrdersTestEnvironment(
maker,
taker,
makerToken,
takerToken,
zeroEx,
gasPrice,
gasPrice.times(protocolFeeMultiplier),
env,
);
}
constructor(
public readonly maker: string,
public readonly taker: string,
public readonly makerToken: TestMintableERC20TokenContract,
public readonly takerToken: TestMintableERC20TokenContract,
public readonly zeroEx: IZeroExContract,
public readonly gasPrice: BigNumber,
public readonly protocolFee: BigNumber,
private readonly _env: BlockchainTestsEnvironment,
) {}
public async prepareBalancesForOrdersAsync(
orders: LimitOrder[] | RfqOrder[],
taker: string = this.taker,
): Promise<void> {
await this.makerToken
.mint(this.maker, BigNumber.sum(...(orders as OrderBase[]).map(order => order.makerAmount)))
.awaitTransactionSuccessAsync();
await this.takerToken
.mint(
taker,
BigNumber.sum(
...(orders as OrderBase[]).map(order =>
order.takerAmount.plus(order instanceof LimitOrder ? order.takerTokenFeeAmount : 0),
),
),
)
.awaitTransactionSuccessAsync();
}
public async fillLimitOrderAsync(
order: LimitOrder,
opts: Partial<{
fillAmount: BigNumber | number;
taker: string;
protocolFee: BigNumber | number;
}> = {},
): Promise<TransactionReceiptWithDecodedLogs> {
const { fillAmount, taker, protocolFee } = {
taker: this.taker,
fillAmount: order.takerAmount,
...opts,
};
await this.prepareBalancesForOrdersAsync([order], taker);
const value = protocolFee === undefined ? this.protocolFee : protocolFee;
return this.zeroEx
.fillLimitOrder(
order,
await order.getSignatureWithProviderAsync(this._env.provider),
new BigNumber(fillAmount),
)
.awaitTransactionSuccessAsync({ from: taker, value });
}
public async fillRfqOrderAsync(
order: RfqOrder,
fillAmount: BigNumber | number = order.takerAmount,
taker: string = this.taker,
): Promise<TransactionReceiptWithDecodedLogs> {
await this.prepareBalancesForOrdersAsync([order], taker);
return this.zeroEx
.fillRfqOrder(
order,
await order.getSignatureWithProviderAsync(this._env.provider),
new BigNumber(fillAmount),
)
.awaitTransactionSuccessAsync({ from: taker });
}
public createLimitOrderFilledEventArgs(
order: LimitOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
takerTokenAlreadyFilledAmount: BigNumber = ZERO,
): IZeroExLimitOrderFilledEventArgs {
const {
makerTokenFilledAmount,
takerTokenFilledAmount,
takerTokenFeeFilledAmount,
} = computeLimitOrderFilledAmounts(order, takerTokenFillAmount, takerTokenAlreadyFilledAmount);
const protocolFee = order.taker !== NULL_ADDRESS ? ZERO : this.protocolFee;
return {
takerTokenFilledAmount,
makerTokenFilledAmount,
takerTokenFeeFilledAmount,
orderHash: order.getHash(),
maker: order.maker,
taker: this.taker,
feeRecipient: order.feeRecipient,
makerToken: order.makerToken,
takerToken: order.takerToken,
protocolFeePaid: protocolFee,
pool: order.pool,
};
}
public createRfqOrderFilledEventArgs(
order: RfqOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
takerTokenAlreadyFilledAmount: BigNumber = ZERO,
): IZeroExRfqOrderFilledEventArgs {
const { makerTokenFilledAmount, takerTokenFilledAmount } = computeRfqOrderFilledAmounts(
order,
takerTokenFillAmount,
takerTokenAlreadyFilledAmount,
);
return {
takerTokenFilledAmount,
makerTokenFilledAmount,
orderHash: order.getHash(),
maker: order.maker,
taker: this.taker,
makerToken: order.makerToken,
takerToken: order.takerToken,
pool: order.pool,
};
}
}
/**
* Generate a random limit order.
@@ -40,3 +215,105 @@ export function getRandomRfqOrder(fields: Partial<RfqOrderFields> = {}): RfqOrde
...fields,
});
}
/**
* Asserts the fields of an OrderInfo object.
*/
export function assertOrderInfoEquals(actual: OrderInfo, expected: OrderInfo): void {
expect(actual.status, 'Order status').to.eq(expected.status);
expect(actual.orderHash, 'Order hash').to.eq(expected.orderHash);
expect(actual.takerTokenFilledAmount, 'Order takerTokenFilledAmount').to.bignumber.eq(
expected.takerTokenFilledAmount,
);
}
/**
* Creates an order expiry field.
*/
export function createExpiry(deltaSeconds: number = 60): BigNumber {
return new BigNumber(Math.floor(Date.now() / 1000) + deltaSeconds);
}
/**
* Computes the maker, taker, and taker token fee amounts filled for
* the given limit order.
*/
export function computeLimitOrderFilledAmounts(
order: LimitOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
takerTokenAlreadyFilledAmount: BigNumber = ZERO,
): LimitOrderFilledAmounts {
const fillAmount = BigNumber.min(
order.takerAmount,
takerTokenFillAmount,
order.takerAmount.minus(takerTokenAlreadyFilledAmount),
);
const makerTokenFilledAmount = fillAmount
.times(order.makerAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
const takerTokenFeeFilledAmount = fillAmount
.times(order.takerTokenFeeAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
return {
makerTokenFilledAmount,
takerTokenFilledAmount: fillAmount,
takerTokenFeeFilledAmount,
};
}
/**
* Computes the maker and taker amounts filled for the given RFQ order.
*/
export function computeRfqOrderFilledAmounts(
order: RfqOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
takerTokenAlreadyFilledAmount: BigNumber = ZERO,
): RfqOrderFilledAmounts {
const fillAmount = BigNumber.min(
order.takerAmount,
takerTokenFillAmount,
order.takerAmount.minus(takerTokenAlreadyFilledAmount),
);
const makerTokenFilledAmount = fillAmount
.times(order.makerAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
return {
makerTokenFilledAmount,
takerTokenFilledAmount: fillAmount,
};
}
/**
* Computes the remaining fillable amount in maker token for
* the given order.
*/
export function getFillableMakerTokenAmount(
order: LimitOrder | RfqOrder,
takerTokenFilledAmount: BigNumber = ZERO,
): BigNumber {
return order.takerAmount
.minus(takerTokenFilledAmount)
.times(order.makerAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
}
/**
* Computes the remaining fillable amnount in taker token, based on
* the amount already filled and the maker's balance/allowance.
*/
export function getActualFillableTakerTokenAmount(
order: LimitOrder | RfqOrder,
makerBalance: BigNumber = order.makerAmount,
makerAllowance: BigNumber = order.makerAmount,
takerTokenFilledAmount: BigNumber = ZERO,
): BigNumber {
const fillableMakerTokenAmount = getFillableMakerTokenAmount(order, takerTokenFilledAmount);
return BigNumber.min(fillableMakerTokenAmount, makerBalance, makerAllowance)
.times(order.takerAmount)
.div(order.makerAmount)
.integerValue(BigNumber.ROUND_UP);
}

View File

@@ -5,6 +5,7 @@
*/
export * from '../test/generated-wrappers/affiliate_fee_transformer';
export * from '../test/generated-wrappers/allowance_target';
export * from '../test/generated-wrappers/batch_fill_native_orders_feature';
export * from '../test/generated-wrappers/bootstrap_feature';
export * from '../test/generated-wrappers/bridge_adapter';
export * from '../test/generated-wrappers/bridge_source';
@@ -20,6 +21,7 @@ export * from '../test/generated-wrappers/fixin_token_spender';
export * from '../test/generated-wrappers/flash_wallet';
export * from '../test/generated-wrappers/full_migration';
export * from '../test/generated-wrappers/i_allowance_target';
export * from '../test/generated-wrappers/i_batch_fill_native_orders_feature';
export * from '../test/generated-wrappers/i_bootstrap_feature';
export * from '../test/generated-wrappers/i_bridge_adapter';
export * from '../test/generated-wrappers/i_erc20_bridge';
@@ -31,6 +33,8 @@ export * from '../test/generated-wrappers/i_liquidity_provider_feature';
export * from '../test/generated-wrappers/i_liquidity_provider_sandbox';
export * from '../test/generated-wrappers/i_meta_transactions_feature';
export * from '../test/generated-wrappers/i_mooniswap_pool';
export * from '../test/generated-wrappers/i_multiplex_feature';
export * from '../test/generated-wrappers/i_native_orders_events';
export * from '../test/generated-wrappers/i_native_orders_feature';
export * from '../test/generated-wrappers/i_ownable_feature';
export * from '../test/generated-wrappers/i_simple_function_registry_feature';
@@ -39,6 +43,7 @@ export * from '../test/generated-wrappers/i_test_simple_function_registry_featur
export * from '../test/generated-wrappers/i_token_spender_feature';
export * from '../test/generated-wrappers/i_transform_erc20_feature';
export * from '../test/generated-wrappers/i_uniswap_feature';
export * from '../test/generated-wrappers/i_uniswap_v2_pair';
export * from '../test/generated-wrappers/i_zero_ex';
export * from '../test/generated-wrappers/initial_migration';
export * from '../test/generated-wrappers/lib_bootstrap';
@@ -88,7 +93,12 @@ export * from '../test/generated-wrappers/mixin_uniswap';
export * from '../test/generated-wrappers/mixin_uniswap_v2';
export * from '../test/generated-wrappers/mixin_zero_ex_bridge';
export * from '../test/generated-wrappers/mooniswap_liquidity_provider';
export * from '../test/generated-wrappers/multiplex_feature';
export * from '../test/generated-wrappers/native_orders_cancellation';
export * from '../test/generated-wrappers/native_orders_feature';
export * from '../test/generated-wrappers/native_orders_info';
export * from '../test/generated-wrappers/native_orders_protocol_fees';
export * from '../test/generated-wrappers/native_orders_settlement';
export * from '../test/generated-wrappers/ownable_feature';
export * from '../test/generated-wrappers/pay_taker_transformer';
export * from '../test/generated-wrappers/permissionless_transformer_deployer';