Compare commits

...

3 Commits

Author SHA1 Message Date
Noah Khamliche
c82ce2bdf9 simulate maker and taker balances of erc20 2022-08-19 15:13:38 -04:00
Noah Khamliche
1170c90141 adding the rest of the changes 2022-08-19 14:27:00 -04:00
Noah Khamliche
cb7916789b otc order implementation added to FQT with failing tests 2022-08-19 14:25:34 -04:00
11 changed files with 314 additions and 43 deletions

View File

@@ -1,4 +1,12 @@
[ [
{
"version": "0.37.0",
"changes": [
{
"note": "Add support for OTC orders in the FillQuoteTransformer"
}
]
},
{ {
"timestamp": 1660093941, "timestamp": 1660093941,
"version": "0.36.3", "version": "0.36.3",

View File

@@ -31,6 +31,7 @@ import "../features/libs/LibNativeOrder.sol";
import "./bridges/IBridgeAdapter.sol"; import "./bridges/IBridgeAdapter.sol";
import "./Transformer.sol"; import "./Transformer.sol";
import "./LibERC20Transformer.sol"; import "./LibERC20Transformer.sol";
import "../IZeroEx.sol";
/// @dev A transformer that fills an ERC20 market sell/buy quote. /// @dev A transformer that fills an ERC20 market sell/buy quote.
/// This transformer shortcuts bridge orders and fills them directly /// This transformer shortcuts bridge orders and fills them directly
@@ -52,7 +53,8 @@ contract FillQuoteTransformer is
enum OrderType { enum OrderType {
Bridge, Bridge,
Limit, Limit,
Rfq Rfq,
Otc
} }
struct LimitOrderInfo { struct LimitOrderInfo {
@@ -62,6 +64,13 @@ contract FillQuoteTransformer is
uint256 maxTakerTokenFillAmount; uint256 maxTakerTokenFillAmount;
} }
struct OtcOrderInfo {
LibNativeOrder.OtcOrder order;
LibSignature.Signature signature;
// Maximum taker token amount of this limit order to fill.
uint256 maxTakerTokenFillAmount;
}
struct RfqOrderInfo { struct RfqOrderInfo {
LibNativeOrder.RfqOrder order; LibNativeOrder.RfqOrder order;
LibSignature.Signature signature; LibSignature.Signature signature;
@@ -84,6 +93,8 @@ contract FillQuoteTransformer is
IBridgeAdapter.BridgeOrder[] bridgeOrders; IBridgeAdapter.BridgeOrder[] bridgeOrders;
// Native limit orders. Sorted by fill sequence. // Native limit orders. Sorted by fill sequence.
LimitOrderInfo[] limitOrders; LimitOrderInfo[] limitOrders;
// Otc orders. Sorted by fill sequence.
OtcOrderInfo[] otcOrders;
// Native RFQ orders. Sorted by fill sequence. // Native RFQ orders. Sorted by fill sequence.
RfqOrderInfo[] rfqOrders; RfqOrderInfo[] rfqOrders;
@@ -123,7 +134,7 @@ contract FillQuoteTransformer is
uint256 soldAmount; uint256 soldAmount;
uint256 protocolFee; uint256 protocolFee;
uint256 takerTokenBalanceRemaining; uint256 takerTokenBalanceRemaining;
uint256[3] currentIndices; uint256[4] currentIndices;
OrderType currentOrderType; OrderType currentOrderType;
} }
@@ -147,12 +158,12 @@ contract FillQuoteTransformer is
IBridgeAdapter public immutable bridgeAdapter; IBridgeAdapter public immutable bridgeAdapter;
/// @dev The exchange proxy contract. /// @dev The exchange proxy contract.
INativeOrdersFeature public immutable zeroEx; IZeroEx public immutable zeroEx;
/// @dev Create this contract. /// @dev Create this contract.
/// @param bridgeAdapter_ The bridge adapter contract. /// @param bridgeAdapter_ The bridge adapter contract.
/// @param zeroEx_ The Exchange Proxy contract. /// @param zeroEx_ The Exchange Proxy contract.
constructor(IBridgeAdapter bridgeAdapter_, INativeOrdersFeature zeroEx_) constructor(IBridgeAdapter bridgeAdapter_, IZeroEx zeroEx_)
public public
Transformer() Transformer()
{ {
@@ -172,7 +183,6 @@ contract FillQuoteTransformer is
{ {
TransformData memory data = abi.decode(context.data, (TransformData)); TransformData memory data = abi.decode(context.data, (TransformData));
FillState memory state; FillState memory state;
// Validate data fields. // Validate data fields.
if (data.sellToken.isTokenETH() || data.buyToken.isTokenETH()) { if (data.sellToken.isTokenETH() || data.buyToken.isTokenETH()) {
LibTransformERC20RichErrors.InvalidTransformDataError( LibTransformERC20RichErrors.InvalidTransformDataError(
@@ -183,6 +193,7 @@ contract FillQuoteTransformer is
if (data.bridgeOrders.length if (data.bridgeOrders.length
+ data.limitOrders.length + data.limitOrders.length
+ data.otcOrders.length
+ data.rfqOrders.length != data.fillSequence.length + data.rfqOrders.length != data.fillSequence.length
) { ) {
LibTransformERC20RichErrors.InvalidTransformDataError( LibTransformERC20RichErrors.InvalidTransformDataError(
@@ -198,7 +209,7 @@ contract FillQuoteTransformer is
// Approve the exchange proxy to spend our sell tokens if native orders // Approve the exchange proxy to spend our sell tokens if native orders
// are present. // are present.
if (data.limitOrders.length + data.rfqOrders.length != 0) { if (data.limitOrders.length + data.rfqOrders.length + data.otcOrders.length != 0) {
data.sellToken.approveIfBelow(address(zeroEx), data.fillAmount); data.sellToken.approveIfBelow(address(zeroEx), data.fillAmount);
// Compute the protocol fee if a limit order is present. // Compute the protocol fee if a limit order is present.
if (data.limitOrders.length != 0) { if (data.limitOrders.length != 0) {
@@ -222,6 +233,7 @@ contract FillQuoteTransformer is
state.currentOrderType = OrderType(data.fillSequence[i]); state.currentOrderType = OrderType(data.fillSequence[i]);
uint256 orderIndex = state.currentIndices[uint256(state.currentOrderType)]; uint256 orderIndex = state.currentIndices[uint256(state.currentOrderType)];
// Fill the order. // Fill the order.
FillOrderResults memory results; FillOrderResults memory results;
if (state.currentOrderType == OrderType.Bridge) { if (state.currentOrderType == OrderType.Bridge) {
@@ -230,6 +242,8 @@ contract FillQuoteTransformer is
results = _fillLimitOrder(data.limitOrders[orderIndex], data, state); results = _fillLimitOrder(data.limitOrders[orderIndex], data, state);
} else if (state.currentOrderType == OrderType.Rfq) { } else if (state.currentOrderType == OrderType.Rfq) {
results = _fillRfqOrder(data.rfqOrders[orderIndex], data, state); results = _fillRfqOrder(data.rfqOrders[orderIndex], data, state);
} else if (state.currentOrderType == OrderType.Otc) {
results = _fillOtcOrder(data.otcOrders[orderIndex], data, state);
} else { } else {
revert("INVALID_ORDER_TYPE"); revert("INVALID_ORDER_TYPE");
} }
@@ -402,6 +416,42 @@ contract FillQuoteTransformer is
} catch {} } catch {}
} }
// Fill a single OTC order.
function _fillOtcOrder(
OtcOrderInfo memory orderInfo,
TransformData memory data,
FillState memory state
)
private
returns (FillOrderResults memory results)
{
uint256 takerTokenFillAmount = LibSafeMathV06.min256(
_computeTakerTokenFillAmount(
data,
state,
orderInfo.order.takerAmount,
orderInfo.order.makerAmount,
0
),
orderInfo.maxTakerTokenFillAmount
);
try
zeroEx.fillOtcOrder
(
orderInfo.order,
orderInfo.signature,
takerTokenFillAmount.safeDowncastToUint128()
)
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
results.takerTokenSoldAmount = takerTokenFilledAmount;
results.makerTokenBoughtAmount = makerTokenFilledAmount;
} catch {
revert("FillQuoteTransformer/OTC_ORDER_FILL_FAILED");
}
}
// Compute the next taker token fill amount of a generic order. // Compute the next taker token fill amount of a generic order.
function _computeTakerTokenFillAmount( function _computeTakerTokenFillAmount(
TransformData memory data, TransformData memory data,

View File

@@ -20,6 +20,7 @@
"test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html", "test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html",
"test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha", "test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha",
"run_mocha": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit", "run_mocha": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit",
"fqt": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' -g 'sell quotes' --timeout 100000 --bail --exit",
"compile": "sol-compiler", "compile": "sol-compiler",
"watch": "sol-compiler -w", "watch": "sol-compiler -w",
"clean": "shx rm -rf lib test/generated-artifacts test/generated-wrappers generated-artifacts generated-wrappers", "clean": "shx rm -rf lib test/generated-artifacts test/generated-wrappers generated-artifacts generated-wrappers",

View File

@@ -12,21 +12,26 @@ import {
FillQuoteTransformerData, FillQuoteTransformerData,
FillQuoteTransformerLimitOrderInfo, FillQuoteTransformerLimitOrderInfo,
FillQuoteTransformerOrderType as OrderType, FillQuoteTransformerOrderType as OrderType,
FillQuoteTransformerOtcOrderInfo,
FillQuoteTransformerRfqOrderInfo, FillQuoteTransformerRfqOrderInfo,
FillQuoteTransformerSide as Side, FillQuoteTransformerSide as Side,
LimitOrder, LimitOrder,
LimitOrderFields, LimitOrderFields,
OtcOrder,
OtcOrderFields,
RfqOrder, RfqOrder,
RfqOrderFields, RfqOrderFields,
Signature, Signature,
} from '@0x/protocol-utils'; } from '@0x/protocol-utils';
import { BigNumber, hexUtils, ZeroExRevertErrors } from '@0x/utils'; import { BigNumber, hexUtils, ZeroExRevertErrors } from '@0x/utils';
import { assert } from 'chai';
import { TransactionReceiptWithDecodedLogs as TxReceipt } from 'ethereum-types'; import { TransactionReceiptWithDecodedLogs as TxReceipt } from 'ethereum-types';
import { ethers } from 'ethers';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { artifacts } from '../artifacts'; import { artifacts } from '../artifacts';
import { TestFillQuoteTransformerBridgeContract } from '../generated-wrappers/test_fill_quote_transformer_bridge'; import { TestFillQuoteTransformerBridgeContract } from '../generated-wrappers/test_fill_quote_transformer_bridge';
import { getRandomLimitOrder, getRandomRfqOrder } from '../utils/orders'; import { getRandomLimitOrder, getRandomOtcOrder, getRandomRfqOrder } from '../utils/orders';
import { import {
EthereumBridgeAdapterContract, EthereumBridgeAdapterContract,
FillQuoteTransformerContract, FillQuoteTransformerContract,
@@ -132,6 +137,18 @@ blockchainTests.resets('FillQuoteTransformer', env => {
}); });
} }
function createOtcOrder(fields: Partial<OtcOrderFields> = {}): OtcOrder {
return getRandomOtcOrder({
makerToken: makerToken.address,
takerToken: takerToken.address,
makerAmount: getRandomInteger('0.1e18', '1e18'),
takerAmount: getRandomInteger('0.1e18', '1e18'),
maker,
taker,
...fields,
});
}
function createBridgeOrder(fillRatio: Numberish = 1.0): BridgeOrder { function createBridgeOrder(fillRatio: Numberish = 1.0): BridgeOrder {
const makerTokenAmount = getRandomInteger('0.1e18', '1e18'); const makerTokenAmount = getRandomInteger('0.1e18', '1e18');
return { return {
@@ -191,7 +208,7 @@ blockchainTests.resets('FillQuoteTransformer', env => {
let soldAmount = ZERO_AMOUNT; let soldAmount = ZERO_AMOUNT;
let boughtAmount = ZERO_AMOUNT; let boughtAmount = ZERO_AMOUNT;
const fillAmount = normalizeFillAmount(data.fillAmount, state.takerTokenBalance); const fillAmount = normalizeFillAmount(data.fillAmount, state.takerTokenBalance);
const orderIndices = [0, 0, 0]; const orderIndices = [0, 0, 0, 0];
function computeTakerTokenFillAmount( function computeTakerTokenFillAmount(
orderTakerTokenAmount: BigNumber, orderTakerTokenAmount: BigNumber,
@@ -272,6 +289,24 @@ blockchainTests.resets('FillQuoteTransformer', env => {
}; };
} }
function fillOtcOrder(oi: FillQuoteTransformerOtcOrderInfo): FillOrderResults {
const preFilledTakerAmount = orderSignatureToPreFilledTakerAmount(oi.signature);
if (preFilledTakerAmount.gte(oi.order.takerAmount) || preFilledTakerAmount.eq(REVERT_AMOUNT)) {
return EMPTY_FILL_ORDER_RESULTS;
}
const takerTokenFillAmount = BigNumber.min(
computeTakerTokenFillAmount(oi.order.takerAmount, oi.order.makerAmount),
oi.order.takerAmount.minus(preFilledTakerAmount),
oi.maxTakerTokenFillAmount,
);
const fillRatio = takerTokenFillAmount.div(oi.order.takerAmount);
return {
...EMPTY_FILL_ORDER_RESULTS,
takerTokenSoldAmount: takerTokenFillAmount,
makerTokenBoughtAmount: fillRatio.times(oi.order.makerAmount).integerValue(BigNumber.ROUND_DOWN),
};
}
// tslint:disable-next-line: prefer-for-of // tslint:disable-next-line: prefer-for-of
for (let i = 0; i < data.fillSequence.length; ++i) { for (let i = 0; i < data.fillSequence.length; ++i) {
const orderType = data.fillSequence[i]; const orderType = data.fillSequence[i];
@@ -301,6 +336,11 @@ blockchainTests.resets('FillQuoteTransformer', env => {
results = fillRfqOrder(data.rfqOrders[orderIndices[orderType]]); results = fillRfqOrder(data.rfqOrders[orderIndices[orderType]]);
} }
break; break;
case OrderType.Otc:
{
results = fillOtcOrder(data.otcOrders[orderIndices[orderType]]);
}
break;
default: default:
throw new Error('Unknown order type'); throw new Error('Unknown order type');
} }
@@ -393,6 +433,7 @@ blockchainTests.resets('FillQuoteTransformer', env => {
buyToken: makerToken.address, buyToken: makerToken.address,
bridgeOrders: [], bridgeOrders: [],
limitOrders: [], limitOrders: [],
otcOrders: [],
rfqOrders: [], rfqOrders: [],
fillSequence: [], fillSequence: [],
fillAmount: MAX_UINT256, fillAmount: MAX_UINT256,
@@ -446,7 +487,7 @@ blockchainTests.resets('FillQuoteTransformer', env => {
await assertCurrentBalancesAsync(exchange.address, { ...ZERO_BALANCES, ethBalance: qfr.protocolFeePaid }); await assertCurrentBalancesAsync(exchange.address, { ...ZERO_BALANCES, ethBalance: qfr.protocolFeePaid });
} }
describe('sell quotes', () => { describe.only('sell quotes', () => {
it('can fully sell to a single bridge order with -1 fillAmount', async () => { it('can fully sell to a single bridge order with -1 fillAmount', async () => {
const bridgeOrders = [createBridgeOrder()]; const bridgeOrders = [createBridgeOrder()];
const data = createTransformData({ const data = createTransformData({
@@ -662,6 +703,44 @@ blockchainTests.resets('FillQuoteTransformer', env => {
return assertFinalBalancesAsync(qfr); return assertFinalBalancesAsync(qfr);
}); });
it.only('can fully buy to a single OTC order', async () => {
const ethersMakerWallet = ethers.Wallet.createRandom();
const _otcOrder = createOtcOrder({maker: ethersMakerWallet.address});
await makerToken.mint(ethersMakerWallet.address, MAX_UINT256).awaitTransactionSuccessAsync();
await takerToken.mint(taker, MAX_UINT256).awaitTransactionSuccessAsync();
let balMaker = await makerToken.balanceOf(ethersMakerWallet.address).callAsync();
let balTaker = await takerToken.balanceOf(taker).callAsync();
const otcOrders = [_otcOrder];
const totalTakerTokens = BigNumber.sum(...otcOrders.map(o => o.takerAmount));
const _otcOrderSignature = await ethersMakerWallet.signMessage(ethers.utils.arrayify(_otcOrder.getHash()));
const { v, r, s } = ethers.utils.splitSignature(_otcOrderSignature);
const _orderSignature : Signature = {
v : v!,
r,
s,
signatureType: 3
};
const data = createTransformData({
side: Side.Buy,
otcOrders: otcOrders.map(o => ({
order: o,
maxTakerTokenFillAmount: MAX_UINT256,
signature: _orderSignature,
})),
fillAmount: BigNumber.sum(...otcOrders.map(o => o.makerAmount)),
fillSequence: otcOrders.map(() => OrderType.Otc),
});
const qfr = getExpectedQuoteFillResults(
data,
createSimulationState({ takerTokenBalance: totalTakerTokens }),
);
await executeTransformAsync({
data,
takerTokenBalance: qfr.takerTokensSpent,
});
return assertFinalBalancesAsync(qfr);
});
it('can fully sell to one of each order type', async () => { it('can fully sell to one of each order type', async () => {
const rfqOrders = [createRfqOrder()]; const rfqOrders = [createRfqOrder()];
const limitOrders = [createLimitOrder()]; const limitOrders = [createLimitOrder()];
@@ -1117,6 +1196,30 @@ blockchainTests.resets('FillQuoteTransformer', env => {
return assertFinalBalancesAsync(qfr); return assertFinalBalancesAsync(qfr);
}); });
it('can fully buy to a single OTC order', async () => {
const otcOrders = [createOtcOrder()];
const totalTakerTokens = BigNumber.sum(...otcOrders.map(o => o.takerAmount));
const data = createTransformData({
side: Side.Buy,
otcOrders: otcOrders.map(o => ({
order: o,
maxTakerTokenFillAmount: MAX_UINT256,
signature: createOrderSignature(),
})),
fillAmount: BigNumber.sum(...otcOrders.map(o => o.makerAmount)),
fillSequence: otcOrders.map(() => OrderType.Rfq),
});
const qfr = getExpectedQuoteFillResults(
data,
createSimulationState({ takerTokenBalance: totalTakerTokens }),
);
await executeTransformAsync({
data,
takerTokenBalance: qfr.takerTokensSpent,
});
return assertFinalBalancesAsync(qfr);
});
it('can fully buy to a single RFQ order', async () => { it('can fully buy to a single RFQ order', async () => {
const rfqOrders = [createRfqOrder()]; const rfqOrders = [createRfqOrder()];
const totalTakerTokens = BigNumber.sum(...rfqOrders.map(o => o.takerAmount)); const totalTakerTokens = BigNumber.sum(...rfqOrders.map(o => o.takerAmount));

View File

@@ -1,4 +1,12 @@
[ [
{
"version": "16.67.0",
"changes": [
{
"note": "Add Otc order support to the fillQuoteTransformer"
}
]
},
{ {
"version": "16.66.4", "version": "16.66.4",
"changes": [ "changes": [

View File

@@ -40,12 +40,14 @@ import {
FinalUniswapV3FillData, FinalUniswapV3FillData,
LiquidityProviderFillData, LiquidityProviderFillData,
NativeRfqOrderFillData, NativeRfqOrderFillData,
NativeOtcOrderFillData,
OptimizedMarketBridgeOrder, OptimizedMarketBridgeOrder,
OptimizedMarketOrder, OptimizedMarketOrder,
UniswapV2FillData, UniswapV2FillData,
} from '../utils/market_operation_utils/types'; } from '../utils/market_operation_utils/types';
import { import {
multiplexOtcOrder,
multiplexPlpEncoder, multiplexPlpEncoder,
multiplexRfqEncoder, multiplexRfqEncoder,
MultiplexSubcall, MultiplexSubcall,
@@ -312,18 +314,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
!requiresTransformERC20(optsWithDefaults) !requiresTransformERC20(optsWithDefaults)
) { ) {
const rfqOrdersData = quote.orders.map(o => o.fillData as NativeRfqOrderFillData); const rfqOrdersData = quote.orders.map(o => o.fillData as NativeRfqOrderFillData);
const fillAmountPerOrder = (() => { const fillAmountPerOrder = generateFillAmounts(sellAmount, quote);
// Don't think order taker amounts are clipped to actual sell amount
// (the last one might be too large) so figure them out manually.
let remaining = sellAmount;
const fillAmounts = [];
for (const o of quote.orders) {
const fillAmount = BigNumber.min(o.takerAmount, remaining);
fillAmounts.push(fillAmount);
remaining = remaining.minus(fillAmount);
}
return fillAmounts;
})();
const callData = const callData =
quote.orders.length === 1 quote.orders.length === 1
? this._exchangeProxy ? this._exchangeProxy
@@ -346,6 +337,46 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
}; };
} }
// OTC orders
if (
[ChainId.Mainnet, ChainId.Ropsten].includes(this.chainId) && // @todo goerli and polygon?
quote.orders.every(o => o.type === FillQuoteTransformerOrderType.Otc) &&
!requiresTransformERC20(optsWithDefaults)
) {
const otcOrdersData = quote.orders.map(o => o.fillData as NativeOtcOrderFillData);
const fillAmountPerOrder = generateFillAmounts(sellAmount, quote);
// grab the amount to fill on each OtcOrder (if more than 1, fallback to multiplexBatchFill)
let callData;
// if we have more than one otc order we want to batch fill them,
if (quote.orders.length === 1) {
// if the otc orders takerToken is the native asset
if (isFromETH) {
callData = this._exchangeProxy
.fillOtcOrderWithEth(otcOrdersData[0].order, otcOrdersData[0].signature)
.getABIEncodedTransactionData();
}
// if the otc orders makerToken is the native asset
if (isToETH) {
callData = this._exchangeProxy
.fillOtcOrderForEth(otcOrdersData[0].order, otcOrdersData[0].signature, fillAmountPerOrder[0])
.getABIEncodedTransactionData();
} else {
// if the otc order contains 2 erc20 tokens
callData = this._exchangeProxy
.fillOtcOrder(otcOrdersData[0].order, otcOrdersData[0].signature, fillAmountPerOrder[0])
.getABIEncodedTransactionData();
}
return {
calldataHexString: callData,
ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT,
toAddress: this._exchangeProxy.address,
allowanceTarget: this._exchangeProxy.address,
gasOverhead: ZERO_AMOUNT,
};
}
}
if (this.chainId === ChainId.Mainnet && isMultiplexBatchFillCompatible(quote, optsWithDefaults)) { if (this.chainId === ChainId.Mainnet && isMultiplexBatchFillCompatible(quote, optsWithDefaults)) {
return { return {
calldataHexString: this._encodeMultiplexBatchFillCalldata( calldataHexString: this._encodeMultiplexBatchFillCalldata(
@@ -539,19 +570,30 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
for_loop: for (const [i, order] of quote.orders.entries()) { for_loop: for (const [i, order] of quote.orders.entries()) {
switch_statement: switch (order.source) { switch_statement: switch (order.source) {
case ERC20BridgeSource.Native: case ERC20BridgeSource.Native:
if (order.type !== FillQuoteTransformerOrderType.Rfq) { if (order.type !== FillQuoteTransformerOrderType.Rfq && order.type !== FillQuoteTransformerOrderType.Otc) {
// Should never happen because we check `isMultiplexBatchFillCompatible` // Should never happen because we check `isMultiplexBatchFillCompatible`
// before calling this function. // before calling this function.
throw new Error('Multiplex batch fill only supported for RFQ native orders'); throw new Error('Multiplex batch fill only supported for RFQ native orders and OTC Orders');
}
if (order.type !== FillQuoteTransformerOrderType.Otc) {
subcalls.push({
id: MultiplexSubcall.Rfq,
sellAmount: order.takerAmount,
data: multiplexRfqEncoder.encode({
order: order.fillData.order,
signature: order.fillData.signature,
}),
});
} else {
subcalls.push({
id: MultiplexSubcall.Otc,
sellAmount: order.takerAmount,
data: multiplexOtcOrder.encode({
order: order.fillData.order,
signature: order.fillData.signature,
}),
});
} }
subcalls.push({
id: MultiplexSubcall.Rfq,
sellAmount: order.takerAmount,
data: multiplexRfqEncoder.encode({
order: order.fillData.order,
signature: order.fillData.signature,
}),
});
break switch_statement; break switch_statement;
case ERC20BridgeSource.UniswapV2: case ERC20BridgeSource.UniswapV2:
case ERC20BridgeSource.SushiSwap: case ERC20BridgeSource.SushiSwap:
@@ -702,6 +744,17 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
} }
} }
function generateFillAmounts(sellAmount: BigNumber, quote: MarketBuySwapQuote | MarketSellSwapQuote): BigNumber[] {
let remaining = sellAmount;
const fillAmounts = [];
for (const o of quote.orders) {
const fillAmount = BigNumber.min(o.takerAmount, remaining);
fillAmounts.push(fillAmount);
remaining = remaining.minus(fillAmount);
}
return fillAmounts;
}
function slipNonNativeOrders(quote: MarketSellSwapQuote | MarketBuySwapQuote): OptimizedMarketOrder[] { function slipNonNativeOrders(quote: MarketSellSwapQuote | MarketBuySwapQuote): OptimizedMarketOrder[] {
const slippage = getMaxQuoteSlippageRate(quote); const slippage = getMaxQuoteSlippageRate(quote);
if (slippage === 0) { if (slippage === 0) {

View File

@@ -1,4 +1,4 @@
import { RfqOrder, SIGNATURE_ABI } from '@0x/protocol-utils'; import { OtcOrder, RfqOrder, SIGNATURE_ABI } from '@0x/protocol-utils';
import { AbiEncoder } from '@0x/utils'; import { AbiEncoder } from '@0x/utils';
export enum MultiplexSubcall { export enum MultiplexSubcall {
@@ -26,6 +26,10 @@ export const multiplexRfqEncoder = AbiEncoder.create([
{ name: 'order', type: 'tuple', components: RfqOrder.STRUCT_ABI }, { name: 'order', type: 'tuple', components: RfqOrder.STRUCT_ABI },
{ name: 'signature', type: 'tuple', components: SIGNATURE_ABI }, { name: 'signature', type: 'tuple', components: SIGNATURE_ABI },
]); ]);
export const multiplexOtcOrder = AbiEncoder.create([
{ name: 'order', type: 'tuple', components: OtcOrder.STRUCT_ABI },
{ name: 'signature', type: 'tuple', components: SIGNATURE_ABI },
]);
export const multiplexUniswapEncoder = AbiEncoder.create([ export const multiplexUniswapEncoder = AbiEncoder.create([
{ name: 'tokens', type: 'address[]' }, { name: 'tokens', type: 'address[]' },
{ name: 'isSushi', type: 'bool' }, { name: 'isSushi', type: 'bool' },

View File

@@ -8,6 +8,7 @@ import {
import { import {
ERC20BridgeSource, ERC20BridgeSource,
NativeLimitOrderFillData, NativeLimitOrderFillData,
NativeOtcOrderFillData,
NativeRfqOrderFillData, NativeRfqOrderFillData,
OptimizedMarketBridgeOrder, OptimizedMarketBridgeOrder,
OptimizedMarketOrder, OptimizedMarketOrder,
@@ -107,19 +108,27 @@ function isOptimizedRfqOrder(x: OptimizedMarketOrder): x is OptimizedMarketOrder
return x.type === FillQuoteTransformerOrderType.Rfq; return x.type === FillQuoteTransformerOrderType.Rfq;
} }
function isOptimizedOtcOrder(x: OptimizedMarketOrder): x is OptimizedMarketOrderBase<NativeOtcOrderFillData> {
return x.type === FillQuoteTransformerOrderType.Otc;
}
/** /**
* Converts the given `OptimizedMarketOrder`s into bridge, limit, and RFQ orders for * Converts the given `OptimizedMarketOrder`s into bridge, limit, and RFQ orders for
* FillQuoteTransformer. * FillQuoteTransformer.
*/ */
export function getFQTTransformerDataFromOptimizedOrders( export function getFQTTransformerDataFromOptimizedOrders(
orders: OptimizedMarketOrder[], orders: OptimizedMarketOrder[],
): Pick<FillQuoteTransformerData, 'bridgeOrders' | 'limitOrders' | 'rfqOrders' | 'fillSequence'> { ): Pick<FillQuoteTransformerData, 'bridgeOrders' | 'limitOrders' | 'rfqOrders' | 'otcOrders' | 'fillSequence'> {
const fqtData: Pick<FillQuoteTransformerData, 'bridgeOrders' | 'limitOrders' | 'rfqOrders' | 'fillSequence'> = { const fqtData: Pick<
bridgeOrders: [], FillQuoteTransformerData,
limitOrders: [], 'bridgeOrders' | 'limitOrders' | 'rfqOrders' | 'otcOrders' | 'fillSequence'
rfqOrders: [], > = {
fillSequence: [], bridgeOrders: [],
}; limitOrders: [],
rfqOrders: [],
otcOrders: [],
fillSequence: [],
};
for (const order of orders) { for (const order of orders) {
if (isOptimizedBridgeOrder(order)) { if (isOptimizedBridgeOrder(order)) {
@@ -141,6 +150,12 @@ export function getFQTTransformerDataFromOptimizedOrders(
signature: order.fillData.signature, signature: order.fillData.signature,
maxTakerTokenFillAmount: order.takerAmount, maxTakerTokenFillAmount: order.takerAmount,
}); });
} else if (isOptimizedOtcOrder(order)) {
fqtData.otcOrders.push({
order: order.fillData.order,
signature: order.fillData.signature,
maxTakerTokenFillAmount: order.takerAmount,
});
} else { } else {
// Should never happen // Should never happen
throw new Error('Unknown Order type'); throw new Error('Unknown Order type');

View File

@@ -2,6 +2,7 @@ import { ChainId } from '@0x/contract-addresses';
import { import {
FillQuoteTransformerLimitOrderInfo, FillQuoteTransformerLimitOrderInfo,
FillQuoteTransformerOrderType, FillQuoteTransformerOrderType,
FillQuoteTransformerOtcOrderInfo,
FillQuoteTransformerRfqOrderInfo, FillQuoteTransformerRfqOrderInfo,
} from '@0x/protocol-utils'; } from '@0x/protocol-utils';
import { MarketOperation } from '@0x/types'; import { MarketOperation } from '@0x/types';
@@ -194,7 +195,8 @@ export interface FillData {}
// `FillData` for native fills. Represents a single native order // `FillData` for native fills. Represents a single native order
export type NativeRfqOrderFillData = FillQuoteTransformerRfqOrderInfo; export type NativeRfqOrderFillData = FillQuoteTransformerRfqOrderInfo;
export type NativeLimitOrderFillData = FillQuoteTransformerLimitOrderInfo; export type NativeLimitOrderFillData = FillQuoteTransformerLimitOrderInfo;
export type NativeFillData = NativeRfqOrderFillData | NativeLimitOrderFillData; export type NativeOtcOrderFillData = FillQuoteTransformerOtcOrderInfo;
export type NativeFillData = NativeRfqOrderFillData | NativeLimitOrderFillData | NativeOtcOrderFillData;
// Represents an individual DEX sample from the sampler contract // Represents an individual DEX sample from the sampler contract
export interface DexSample<TFillData extends FillData = FillData> { export interface DexSample<TFillData extends FillData = FillData> {
@@ -450,7 +452,8 @@ export interface OptimizedRfqOrder extends OptimizedMarketOrderBase<NativeRfqOrd
export type OptimizedMarketOrder = export type OptimizedMarketOrder =
| OptimizedMarketBridgeOrder<FillData> | OptimizedMarketBridgeOrder<FillData>
| OptimizedMarketOrderBase<NativeLimitOrderFillData> | OptimizedMarketOrderBase<NativeLimitOrderFillData>
| OptimizedMarketOrderBase<NativeRfqOrderFillData>; | OptimizedMarketOrderBase<NativeRfqOrderFillData>
| OptimizedMarketOrderBase<NativeOtcOrderFillData>;
export interface GetMarketOrdersRfqOpts extends RfqRequestOpts { export interface GetMarketOrdersRfqOpts extends RfqRequestOpts {
rfqClient?: IRfqClient; rfqClient?: IRfqClient;

View File

@@ -353,7 +353,7 @@ function _isNativeOrderFromCollapsedFill(cf: Fill): cf is Fill<NativeFillData> {
*/ */
export function nativeOrderToReportEntry( export function nativeOrderToReportEntry(
type: FillQuoteTransformerOrderType, type: FillQuoteTransformerOrderType,
fillData: NativeLimitOrderFillData | NativeRfqOrderFillData, fillData: NativeFillData,
fillableAmount: BigNumber, fillableAmount: BigNumber,
comparisonPrice?: BigNumber | undefined, comparisonPrice?: BigNumber | undefined,
quoteRequestor?: QuoteRequestor, quoteRequestor?: QuoteRequestor,

View File

@@ -1,7 +1,7 @@
import { AbiEncoder, BigNumber, hexUtils, NULL_ADDRESS } from '@0x/utils'; import { AbiEncoder, BigNumber, hexUtils, NULL_ADDRESS } from '@0x/utils';
import * as ethjs from 'ethereumjs-util'; import * as ethjs from 'ethereumjs-util';
import { LimitOrder, LimitOrderFields, RfqOrder, RfqOrderFields } from './orders'; import { LimitOrder, LimitOrderFields, RfqOrder, RfqOrderFields, OtcOrder, OtcOrderFields } from './orders';
import { Signature, SIGNATURE_ABI } from './signature_utils'; import { Signature, SIGNATURE_ABI } from './signature_utils';
const BRIDGE_ORDER_ABI_COMPONENTS = [ const BRIDGE_ORDER_ABI_COMPONENTS = [
@@ -39,6 +39,20 @@ const RFQ_ORDER_INFO_ABI_COMPONENTS = [
{ name: 'maxTakerTokenFillAmount', type: 'uint256' }, { name: 'maxTakerTokenFillAmount', type: 'uint256' },
]; ];
const OTC_ORDER_INFO_ABI_COMPONENTS = [
{
name: 'order',
type: 'tuple',
components: OtcOrder.STRUCT_ABI,
},
{
name: 'signature',
type: 'tuple',
components: SIGNATURE_ABI,
},
{ name: 'maxTakerTokenFillAmount', type: 'uint256' },
];
/** /**
* ABI encoder for `FillQuoteTransformer.TransformData` * ABI encoder for `FillQuoteTransformer.TransformData`
*/ */
@@ -60,6 +74,11 @@ export const fillQuoteTransformerDataEncoder = AbiEncoder.create([
type: 'tuple[]', type: 'tuple[]',
components: LIMIT_ORDER_INFO_ABI_COMPONENTS, components: LIMIT_ORDER_INFO_ABI_COMPONENTS,
}, },
{
name: 'otcOrders',
type: 'tuple[]',
components: OTC_ORDER_INFO_ABI_COMPONENTS,
},
{ {
name: 'rfqOrders', name: 'rfqOrders',
type: 'tuple[]', type: 'tuple[]',
@@ -87,6 +106,7 @@ export enum FillQuoteTransformerOrderType {
Bridge, Bridge,
Limit, Limit,
Rfq, Rfq,
Otc
} }
/** /**
@@ -99,6 +119,7 @@ export interface FillQuoteTransformerData {
bridgeOrders: FillQuoteTransformerBridgeOrder[]; bridgeOrders: FillQuoteTransformerBridgeOrder[];
limitOrders: FillQuoteTransformerLimitOrderInfo[]; limitOrders: FillQuoteTransformerLimitOrderInfo[];
rfqOrders: FillQuoteTransformerRfqOrderInfo[]; rfqOrders: FillQuoteTransformerRfqOrderInfo[];
otcOrders: FillQuoteTransformerOtcOrderInfo[];
fillSequence: FillQuoteTransformerOrderType[]; fillSequence: FillQuoteTransformerOrderType[];
fillAmount: BigNumber; fillAmount: BigNumber;
refundReceiver: string; refundReceiver: string;
@@ -178,6 +199,11 @@ export type FillQuoteTransformerLimitOrderInfo = FillQuoteTransformerNativeOrder
*/ */
export type FillQuoteTransformerRfqOrderInfo = FillQuoteTransformerNativeOrderInfo<RfqOrderFields>; export type FillQuoteTransformerRfqOrderInfo = FillQuoteTransformerNativeOrderInfo<RfqOrderFields>;
/**
* `FillQuoteTransformer.OtcOrderInfo`
*/
export type FillQuoteTransformerOtcOrderInfo = FillQuoteTransformerNativeOrderInfo<OtcOrderFields>;
/** /**
* ABI-encode a `FillQuoteTransformer.TransformData` type. * ABI-encode a `FillQuoteTransformer.TransformData` type.
*/ */