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:
@@ -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,
|
||||
|
||||
479
contracts/zero-ex/test/features/batch_fill_native_orders_test.ts
Normal file
479
contracts/zero-ex/test/features/batch_fill_native_orders_test.ts
Normal 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,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
764
contracts/zero-ex/test/features/multiplex_test.ts
Normal file
764
contracts/zero-ex/test/features/multiplex_test.ts
Normal 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`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user