feat: v4 final (#136)

* v4 FillQuoteTransformer (#104)

* Update FQT to support v4 orders

* `@0x/contracts-zero-ex`: Tweak FQT
`@0x/contracts-zero-ex`: Drop `ERC20BridgeTransfer` event and add `PartialQuoteFill` event.

* `@0x/contracts-utils`: Add `LibSafeMathV06.downcastToUint128()`

* `@0x/protocol-utils`: Update transformer utils for V4 FQT

* `@0x/contracts-zero-ex`: Fixing FQT tests...

* `@0x/contracts-zero-ex`: rename FQT bridge event

* `@0x/contracts-zero-ex`: Un-`only` tests

* `@0x/migrations`: Update `BridgeAdapter` deployment

* `@0x/contracts-integrations`: Delete `mtx_tests`

* `@0x/protocol-utils`: Address review comments

* `@0x/contracts-zero-ex`: Address review comments

* `@0x/migrations`: Update migrations

Co-authored-by: Michael Zhu <mchl.zhu.96@gmail.com>
Co-authored-by: Lawrence Forman <me@merklejerk.com>

* v4: Asset-swapper (main branch) (#113)

* refactor quote_requestor

* WIP v4/asset-swapper: Clean up SwapQuoter and remove @0x/orderbook

* Start replacing SignedOrder everywhere

* wip: new order type

* wip

* remove order-utils from most places

* hack: Play around with VerboseX types (#119)

* hack: Play around with VerboseX types

* More hacks

* Fix up the bridgeData encodings

* Rework Orderbook return type

* feat: Don't charge a protocol fee for RFQ orders WIP (#121)

* fix simple build errors

* simplify types a little

* remove SwapQuoteCalculator: unnecessary abstraction

* Fix all ./src build errors; make types consistent

* export more types for use in 0x API; modify Orderbook interface

* stop overriding APIOrder

* feat: RFQ v4 + consolidated bridge encoders (#125)

* feat: check if taker address is contract

* Rework bridge data

* Worst case adjustments

* RFQT v4

* Future/v4 validate orders (#126)

* RFQT v4

* v4 validate native orders

* use default invalid signature

* refactor rfqt validations in swap quoter

* fix types

* fix RFQT unlisted api key

* remove priceAwareRFQFlag

* adjust maker/taker amounts

* update JSON schemas

* filter zero fillable orders

Co-authored-by: xianny <xianny@gmail.com>

* fix type export

Co-authored-by: xianny <xianny@gmail.com>

* remove order-utils as much as possible

* work on tests compile

* Comment out quote reporter test

* updated tests

* restore order-utils accidental changes

* some lints

* Remove old fill_test

* ts lint disable for now

* update quote report

* Re-enable quote report tests

* make fill data required field

* fix lint

* type guards

* force fillData as required

* fix lint

* fix naming

* exports

* adjust MultiBridge by slippage

* cleanups (checkpoint 1)

* cleanup types (checkpoint #2)

* remove unused deps

* `@0x/contract-addresses`: Deploy new FQT (#129)

Co-authored-by: Lawrence Forman <me@merklejerk.com>

* commit bump to republish

* DRY up the rfqt mocker

* fix: Balancer load top pools (#131)

* fix: Balancer load top 250 pools

* refetch top pools on an interval

Co-authored-by: Jacob Evans <jacob@dekz.net>
Co-authored-by: Kim Persson <kimpers@users.noreply.github.com>
Co-authored-by: Lawrence Forman <lawrence@0xproject.com>
Co-authored-by: Lawrence Forman <me@merklejerk.com>

* Update post rebase

* prettier

* Remove test helpers exported in asset-swapper

* Clean up from review comments

* prettier

* lint

* recreate rfqt mocker

* change merge and INVALID_SIGNATURE

Co-authored-by: Lawrence Forman <lawrence@0xproject.com>
Co-authored-by: Michael Zhu <mchl.zhu.96@gmail.com>
Co-authored-by: Lawrence Forman <me@merklejerk.com>
Co-authored-by: Xianny <8582774+xianny@users.noreply.github.com>
Co-authored-by: Kim Persson <kimpers@users.noreply.github.com>
This commit is contained in:
Jacob Evans 2021-02-10 19:20:15 +10:00 committed by GitHub
parent 501b7b9b65
commit 3f4bb933d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
115 changed files with 5654 additions and 9332 deletions

View File

@ -1,239 +0,0 @@
import { MarketBuySwapQuote, MarketSellSwapQuote, Orderbook, SwapQuoter } from '@0x/asset-swapper';
import { blockchainTests, expect, Numberish } from '@0x/contracts-test-utils';
import { assetDataUtils } from '@0x/order-utils';
import { FillResults, SignedOrder } from '@0x/types';
import { BigNumber, logUtils } from '@0x/utils';
import * as _ from 'lodash';
import { TestMainnetAggregatorFillsContract } from '../wrappers';
import { tokens } from './tokens';
blockchainTests.live('Aggregator Mainnet Tests', env => {
// Mainnet address of the `TestMainnetAggregatorFills` contract.
const TEST_CONTRACT_ADDRESS = '0x37Ca306F42748b7fe105F89FCBb2CD03D27c8146';
const TAKER_ADDRESS = '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B'; // Vitalik
const ORDERBOOK_POLLING_MS = 1000;
const GAS_PRICE = new BigNumber(1);
const TAKER_ASSET_ETH_VALUE = 500e18;
const MIN_BALANCE = 500.1e18;
const SYMBOLS = ['ETH', 'DAI', 'USDC', 'FOAM'];
const TEST_PAIRS = _.flatten(SYMBOLS.map(m => SYMBOLS.filter(t => t !== m).map(t => [m, t])));
const FILL_VALUES = [1, 10, 1e2, 1e3, 1e4, 2.5e4, 5e4];
let testContract: TestMainnetAggregatorFillsContract;
let swapQuoter: SwapQuoter;
let takerEthBalance: BigNumber;
const orderbooks: { [name: string]: Orderbook } = {};
async function getTakerOrdersAsync(takerAssetSymbol: string): Promise<SignedOrder[]> {
if (takerAssetSymbol === 'ETH') {
return [];
}
return getOrdersAsync(takerAssetSymbol, 'ETH');
}
// Fetches ETH -> taker asset orders for the forwarder contract.
async function getOrdersAsync(makerAssetSymbol: string, takerAssetSymbol: string): Promise<SignedOrder[]> {
const takerTokenAddress = tokens[takerAssetSymbol].address;
const makerTokenAddress = tokens[makerAssetSymbol].address;
const makerAssetData = assetDataUtils.encodeERC20AssetData(makerTokenAddress);
const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress);
const orders = _.flatten(
await Promise.all(
Object.keys(orderbooks).map(async name =>
getOrdersFromOrderBookAsync(name, makerAssetData, takerAssetData),
),
),
);
const uniqueOrders: SignedOrder[] = [];
for (const order of orders) {
if (!order.makerFee.eq(0) || !order.takerFee.eq(0)) {
continue;
}
if (uniqueOrders.findIndex(o => isSameOrder(order, o)) === -1) {
uniqueOrders.push(order);
}
}
return uniqueOrders;
}
async function getOrdersFromOrderBookAsync(
name: string,
makerAssetData: string,
takerAssetData: string,
): Promise<SignedOrder[]> {
try {
return (await orderbooks[name].getOrdersAsync(makerAssetData, takerAssetData)).map(r => r.order);
} catch (err) {
logUtils.warn(`Failed to retrieve orders from orderbook "${name}".`);
}
return [];
}
function isSameOrder(a: SignedOrder, b: SignedOrder): boolean {
for (const [k, v] of Object.entries(a)) {
if (k in (b as any)) {
if (BigNumber.isBigNumber(v) && !v.eq((b as any)[k])) {
return false;
}
if (v !== (b as any)[k]) {
return false;
}
}
}
return true;
}
function toTokenUnits(symbol: string, weis: Numberish): BigNumber {
return new BigNumber(weis).div(new BigNumber(10).pow(tokens[symbol].decimals));
}
function fromTokenUnits(symbol: string, units: Numberish): BigNumber {
return new BigNumber(units)
.times(new BigNumber(10).pow(tokens[symbol].decimals))
.integerValue(BigNumber.ROUND_DOWN);
}
interface MarketOperationResult {
makerAssetBalanceBefore: BigNumber;
takerAssetBalanceBefore: BigNumber;
makerAssetBalanceAfter: BigNumber;
takerAssetBalanceAfter: BigNumber;
fillResults: FillResults;
}
// Liquidity is low right now so it's possible we didn't have
// enough taker assets to cover the orders, so occasionally we'll get incomplete
// fills. This function will catch those cases.
// TODO(dorothy-zbornak): Remove this special case when liquidity is up.
function checkHadEnoughTakerAsset(
quote: MarketBuySwapQuote | MarketSellSwapQuote,
result: MarketOperationResult,
): boolean {
if (result.takerAssetBalanceBefore.gte(quote.worstCaseQuoteInfo.takerAssetAmount)) {
return true;
}
const takerAssetPct = result.takerAssetBalanceBefore
.div(quote.worstCaseQuoteInfo.takerAssetAmount)
.times(100)
.toNumber()
.toFixed(1);
logUtils.warn(`Could not acquire enough taker asset to complete the fill: ${takerAssetPct}%`);
expect(result.fillResults.makerAssetFilledAmount).to.bignumber.lt(quote.worstCaseQuoteInfo.makerAssetAmount);
return false;
}
before(async () => {
testContract = new TestMainnetAggregatorFillsContract(TEST_CONTRACT_ADDRESS, env.provider, {
...env.txDefaults,
gasPrice: GAS_PRICE,
gas: 10e6,
});
swapQuoter = SwapQuoter.getSwapQuoterForStandardRelayerAPIUrl(env.provider, 'https://api.0x.org/sra');
// Pool orderbooks because we're desperate for liquidity.
orderbooks.swapQuoter = swapQuoter.orderbook;
orderbooks.bamboo = Orderbook.getOrderbookForPollingProvider({
httpEndpoint: 'https://sra.bamboorelay.com/0x/v3',
pollingIntervalMs: ORDERBOOK_POLLING_MS,
});
// TODO(dorothy-zbornak): Uncomment when radar's SRA is up.
// orderbooks.radar = Orderbook.getOrderbookForPollingProvider({
// httpEndpoint: 'https://api-v3.radarrelay.com/v3',
// pollingIntervalMs: ORDERBOOK_POLLING_MS,
// });
takerEthBalance = await env.web3Wrapper.getBalanceInWeiAsync(TAKER_ADDRESS);
});
it('taker has minimum ETH', async () => {
expect(takerEthBalance).to.bignumber.gte(MIN_BALANCE);
});
describe('market sells', () => {
for (const [makerSymbol, takerSymbol] of TEST_PAIRS) {
for (const fillValue of FILL_VALUES) {
const fillAmount = fromTokenUnits(takerSymbol, new BigNumber(fillValue).div(tokens[takerSymbol].price));
it(`sell ${toTokenUnits(takerSymbol, fillAmount)} ${takerSymbol} for ${makerSymbol}`, async () => {
const [quote, takerOrders] = await Promise.all([
swapQuoter.getMarketSellSwapQuoteAsync(
tokens[makerSymbol].address,
tokens[takerSymbol].address,
fillAmount,
{ gasPrice: GAS_PRICE },
),
getTakerOrdersAsync(takerSymbol),
]);
// Buy taker assets from `takerOrders` and and perform a
// market sell on the bridge orders.
const fill = await testContract
.marketSell(
tokens[makerSymbol].address,
tokens[takerSymbol].address,
quote.orders,
takerOrders,
quote.orders.map(o => o.signature),
takerOrders.map(o => o.signature),
quote.takerAssetFillAmount,
)
.callAsync({
value: quote.worstCaseQuoteInfo.protocolFeeInWeiAmount.plus(TAKER_ASSET_ETH_VALUE),
from: TAKER_ADDRESS,
gasPrice: quote.gasPrice,
});
if (checkHadEnoughTakerAsset(quote, fill)) {
expect(fill.fillResults.makerAssetFilledAmount, 'makerAssetFilledAmount').to.bignumber.gte(
quote.worstCaseQuoteInfo.makerAssetAmount,
);
expect(fill.fillResults.takerAssetFilledAmount, 'takerAssetFilledAmount').to.bignumber.lte(
quote.takerAssetFillAmount,
);
}
});
}
}
});
describe('market buys', () => {
for (const [makerSymbol, takerSymbol] of TEST_PAIRS) {
for (const fillValue of FILL_VALUES) {
const fillAmount = fromTokenUnits(makerSymbol, new BigNumber(fillValue).div(tokens[makerSymbol].price));
it(`buy ${toTokenUnits(makerSymbol, fillAmount)} ${makerSymbol} with ${takerSymbol}`, async () => {
const [quote, takerOrders] = await Promise.all([
swapQuoter.getMarketBuySwapQuoteAsync(
tokens[makerSymbol].address,
tokens[takerSymbol].address,
fillAmount,
{ gasPrice: GAS_PRICE },
),
getTakerOrdersAsync(takerSymbol),
]);
// Buy taker assets from `takerOrders` and and perform a
// market buy on the bridge orders.
const fill = await testContract
.marketBuy(
tokens[makerSymbol].address,
tokens[takerSymbol].address,
quote.orders,
takerOrders,
quote.orders.map(o => o.signature),
takerOrders.map(o => o.signature),
quote.makerAssetFillAmount,
)
.callAsync({
value: quote.worstCaseQuoteInfo.protocolFeeInWeiAmount.plus(TAKER_ASSET_ETH_VALUE),
from: TAKER_ADDRESS,
gasPrice: quote.gasPrice,
});
if (checkHadEnoughTakerAsset(quote, fill)) {
expect(fill.fillResults.takerAssetFilledAmount, 'takerAssetFilledAmount').to.bignumber.lte(
quote.worstCaseQuoteInfo.takerAssetAmount,
);
expect(fill.fillResults.makerAssetFilledAmount, 'makerAssetFilledAmount').to.bignumber.gte(
quote.makerAssetFillAmount,
);
}
});
}
}
});
});

View File

@ -1,77 +0,0 @@
export const tokens: { [symbol: string]: { address: string; decimals: number; price: number } } = {
ETH: {
address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
decimals: 18,
price: 133,
},
SAI: {
address: '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359',
decimals: 18,
price: 1,
},
DAI: {
address: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
decimals: 18,
price: 1,
},
USDC: {
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
decimals: 6,
price: 1,
},
WBTC: {
address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599',
decimals: 8,
price: 6900,
},
MKR: {
address: '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2',
decimals: 18,
price: 454,
},
BAT: {
address: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF',
decimals: 18,
price: 0.17,
},
OMG: {
address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07',
decimals: 18,
price: 0.65,
},
ZRX: {
address: '0xE41d2489571d322189246DaFA5ebDe1F4699F498',
decimals: 18,
price: 0.19,
},
ZIL: {
address: '0x05f4a42e251f2d52b8ed15E9FEdAacFcEF1FAD27',
decimals: 12,
price: 0.004,
},
FOAM: {
address: '0x4946Fcea7C692606e8908002e55A582af44AC121',
decimals: 18,
price: 0.004,
},
USDT: {
address: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
decimals: 6,
price: 0.019,
},
REP: {
address: '0x1985365e9f78359a9B6AD760e32412f4a445E862',
decimals: 18,
price: 8.9,
},
MANA: {
address: '0x0F5D2fB29fb7d3CFeE444a200298f468908cC942',
decimals: 18,
price: 0.025,
},
LINK: {
address: '0x514910771AF9Ca656af840dff83E8264EcF986CA',
decimals: 18,
price: 1.8,
},
};

View File

@ -1,360 +0,0 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { artifacts as erc20Artifacts, DummyERC20TokenContract } from '@0x/contracts-erc20';
import { IExchangeContract } from '@0x/contracts-exchange';
import { blockchainTests, constants, expect, getRandomPortion, verifyEventsFromLogs } from '@0x/contracts-test-utils';
import {
artifacts as exchangeProxyArtifacts,
IZeroExContract,
LogMetadataTransformerContract,
} from '@0x/contracts-zero-ex';
import { migrateOnceAsync } from '@0x/migrations';
import { assetDataUtils, signatureUtils, SignedExchangeProxyMetaTransaction } from '@0x/order-utils';
import {
encodeFillQuoteTransformerData,
encodePayTakerTransformerData,
ETH_TOKEN_ADDRESS,
FillQuoteTransformerSide,
findTransformerNonce,
Signature,
} from '@0x/protocol-utils';
import { AssetProxyId, Order, SignedOrder } from '@0x/types';
import { BigNumber, hexUtils } from '@0x/utils';
import * as ethjs from 'ethereumjs-util';
const { MAX_UINT256, NULL_ADDRESS, NULL_BYTES, ZERO_AMOUNT } = constants;
function sigstruct(signature: string): Signature {
return {
v: parseInt(hexUtils.slice(signature, 0, 1), 16),
signatureType: parseInt(hexUtils.slice(signature, 65, 66), 16),
r: hexUtils.slice(signature, 1, 33),
s: hexUtils.slice(signature, 33, 65),
};
}
blockchainTests.resets('exchange proxy - meta-transactions', env => {
const quoteSignerKey = hexUtils.random();
const quoteSigner = hexUtils.toHex(ethjs.privateToAddress(ethjs.toBuffer(quoteSignerKey)));
let owner: string;
let relayer: string;
let maker: string;
let taker: string;
let flashWalletAddress: string;
let zeroEx: IZeroExContract;
let exchange: IExchangeContract;
let inputToken: DummyERC20TokenContract;
let outputToken: DummyERC20TokenContract;
let feeToken: DummyERC20TokenContract;
let addresses: ContractAddresses;
let protocolFee: BigNumber;
let metadataTransformer: LogMetadataTransformerContract;
const GAS_PRICE = new BigNumber('1e9');
const MAKER_BALANCE = new BigNumber('100e18');
const TAKER_BALANCE = new BigNumber('100e18');
const TAKER_FEE_BALANCE = new BigNumber('100e18');
before(async () => {
[, relayer, maker, taker] = await env.getAccountAddressesAsync();
addresses = await migrateOnceAsync(env.provider);
zeroEx = new IZeroExContract(addresses.exchangeProxy, env.provider, env.txDefaults, {
LogMetadataTransformer: LogMetadataTransformerContract.ABI(),
DummyERC20Token: DummyERC20TokenContract.ABI(),
});
exchange = new IExchangeContract(addresses.exchange, env.provider, env.txDefaults);
[inputToken, outputToken, feeToken] = await Promise.all(
[...new Array(3)].map(i =>
DummyERC20TokenContract.deployFrom0xArtifactAsync(
erc20Artifacts.DummyERC20Token,
env.provider,
env.txDefaults,
{},
`DummyToken-${i}`,
`TOK${i}`,
new BigNumber(18),
BigNumber.max(MAKER_BALANCE, TAKER_BALANCE),
),
),
);
// LogMetadataTransformer is not deployed in migrations.
metadataTransformer = await LogMetadataTransformerContract.deployFrom0xArtifactAsync(
exchangeProxyArtifacts.LogMetadataTransformer,
env.provider,
{
...env.txDefaults,
from: addresses.exchangeProxyTransformerDeployer,
},
{},
);
owner = await zeroEx.owner().callAsync();
protocolFee = await exchange.protocolFeeMultiplier().callAsync();
flashWalletAddress = await zeroEx.getTransformWallet().callAsync();
const erc20Proxy = await exchange.getAssetProxy(AssetProxyId.ERC20).callAsync();
const allowanceTarget = await zeroEx.getAllowanceTarget().callAsync();
await outputToken.mint(MAKER_BALANCE).awaitTransactionSuccessAsync({ from: maker });
await inputToken.mint(TAKER_BALANCE).awaitTransactionSuccessAsync({ from: taker });
await feeToken.mint(TAKER_FEE_BALANCE).awaitTransactionSuccessAsync({ from: taker });
await outputToken.approve(erc20Proxy, MAX_UINT256).awaitTransactionSuccessAsync({ from: maker });
await inputToken.approve(allowanceTarget, MAX_UINT256).awaitTransactionSuccessAsync({ from: taker });
await feeToken.approve(allowanceTarget, MAX_UINT256).awaitTransactionSuccessAsync({ from: taker });
await zeroEx.setQuoteSigner(quoteSigner).awaitTransactionSuccessAsync({ from: owner });
});
interface Transformation {
deploymentNonce: number;
data: string;
}
interface SwapInfo {
inputTokenAddress: string;
outputTokenAddress: string;
inputTokenAmount: BigNumber;
minOutputTokenAmount: BigNumber;
transformations: Transformation[];
orders: SignedOrder[];
}
async function generateSwapAsync(orderFields: Partial<Order> = {}, isRfqt: boolean = false): Promise<SwapInfo> {
const order = await signatureUtils.ecSignTypedDataOrderAsync(
env.provider,
{
chainId: 1337,
exchangeAddress: exchange.address,
expirationTimeSeconds: new BigNumber(Date.now()),
salt: new BigNumber(hexUtils.random()),
feeRecipientAddress: NULL_ADDRESS,
senderAddress: NULL_ADDRESS,
takerAddress: isRfqt ? flashWalletAddress : NULL_ADDRESS,
makerAddress: maker,
makerAssetData: assetDataUtils.encodeERC20AssetData(outputToken.address),
takerAssetData: assetDataUtils.encodeERC20AssetData(inputToken.address),
makerFeeAssetData: NULL_BYTES,
takerFeeAssetData: NULL_BYTES,
takerAssetAmount: getRandomPortion(TAKER_BALANCE),
makerAssetAmount: getRandomPortion(MAKER_BALANCE),
makerFee: ZERO_AMOUNT,
takerFee: ZERO_AMOUNT,
...orderFields,
},
maker,
);
const transformations = [
{
deploymentNonce: findTransformerNonce(
addresses.transformers.fillQuoteTransformer,
addresses.exchangeProxyTransformerDeployer,
),
data: encodeFillQuoteTransformerData({
orders: [order],
signatures: [order.signature],
buyToken: outputToken.address,
sellToken: inputToken.address,
fillAmount: order.takerAssetAmount,
maxOrderFillAmounts: [],
refundReceiver: hexUtils.leftPad(2, 20), // Send refund to sender.
rfqtTakerAddress: isRfqt ? taker : NULL_ADDRESS,
side: FillQuoteTransformerSide.Sell,
}),
},
{
deploymentNonce: findTransformerNonce(
addresses.transformers.payTakerTransformer,
addresses.exchangeProxyTransformerDeployer,
),
data: encodePayTakerTransformerData({
tokens: [inputToken.address, outputToken.address, ETH_TOKEN_ADDRESS],
amounts: [MAX_UINT256, MAX_UINT256, MAX_UINT256],
}),
},
{
deploymentNonce: findTransformerNonce(
metadataTransformer.address,
addresses.exchangeProxyTransformerDeployer,
),
data: NULL_BYTES,
},
];
return {
transformations,
orders: [order],
inputTokenAddress: inputToken.address,
outputTokenAddress: outputToken.address,
inputTokenAmount: order.takerAssetAmount,
minOutputTokenAmount: order.makerAssetAmount,
};
}
function getSwapData(swap: SwapInfo): string {
return zeroEx
.transformERC20(
swap.inputTokenAddress,
swap.outputTokenAddress,
swap.inputTokenAmount,
swap.minOutputTokenAmount,
swap.transformations,
)
.getABIEncodedTransactionData();
}
async function createMetaTransactionAsync(
data: string,
value: BigNumber,
fee?: BigNumber | number,
): Promise<SignedExchangeProxyMetaTransaction> {
return signatureUtils.ecSignTypedDataExchangeProxyMetaTransactionAsync(
env.provider,
{
value,
signer: taker,
sender: relayer,
minGasPrice: GAS_PRICE,
maxGasPrice: GAS_PRICE,
expirationTimeSeconds: new BigNumber(Math.floor(Date.now() / 1000) + 60),
salt: new BigNumber(hexUtils.random()),
callData: data,
feeToken: feeToken.address,
feeAmount: fee !== undefined ? new BigNumber(fee) : getRandomPortion(TAKER_FEE_BALANCE),
domain: {
chainId: 1,
name: 'ZeroEx',
version: '1.0.0',
verifyingContract: zeroEx.address,
},
},
taker,
);
}
it('can call `transformERC20()` with calldata and no relayer fee', async () => {
const swap = await generateSwapAsync();
const _protocolFee = protocolFee.times(GAS_PRICE).times(swap.orders.length + 1); // Pay a little more fee than needed.
const mtx = await createMetaTransactionAsync(getSwapData(swap), _protocolFee, 0);
const relayerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(relayer);
const receipt = await zeroEx
.executeMetaTransaction(mtx, sigstruct(mtx.signature))
.awaitTransactionSuccessAsync({ from: relayer, value: mtx.value, gasPrice: GAS_PRICE });
const relayerEthRefund = relayerEthBalanceBefore
.minus(await env.web3Wrapper.getBalanceInWeiAsync(relayer))
.minus(GAS_PRICE.times(receipt.gasUsed));
// Ensure the relayer got back the unused protocol fees.
expect(relayerEthRefund).to.bignumber.eq(protocolFee.times(GAS_PRICE));
// Ensure the relayer got paid no mtx fees.
expect(await feeToken.balanceOf(relayer).callAsync()).to.bignumber.eq(0);
// Ensure the taker got output tokens.
expect(await outputToken.balanceOf(taker).callAsync()).to.bignumber.eq(swap.minOutputTokenAmount);
// Ensure the maker got input tokens.
expect(await inputToken.balanceOf(maker).callAsync()).to.bignumber.eq(swap.inputTokenAmount);
// Check events.
verifyEventsFromLogs(
receipt.logs,
[
{
taker,
sender: zeroEx.address,
data: NULL_BYTES,
},
],
'TransformerMetadata',
);
});
it('can call `transformERC20()` with calldata and a relayer fee', async () => {
const swap = await generateSwapAsync();
const _protocolFee = protocolFee.times(GAS_PRICE).times(swap.orders.length + 1); // Pay a little more fee than needed.
const mtx = await createMetaTransactionAsync(getSwapData(swap), _protocolFee);
const relayerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(relayer);
const receipt = await zeroEx
.executeMetaTransaction(mtx, sigstruct(mtx.signature))
.awaitTransactionSuccessAsync({ from: relayer, value: mtx.value, gasPrice: GAS_PRICE });
const relayerEthRefund = relayerEthBalanceBefore
.minus(await env.web3Wrapper.getBalanceInWeiAsync(relayer))
.minus(GAS_PRICE.times(receipt.gasUsed));
// Ensure the relayer got back the unused protocol fees.
expect(relayerEthRefund).to.bignumber.eq(protocolFee.times(GAS_PRICE));
// Ensure the relayer got paid mtx fees.
expect(await feeToken.balanceOf(relayer).callAsync()).to.bignumber.eq(mtx.feeAmount);
// Ensure the taker got output tokens.
expect(await outputToken.balanceOf(taker).callAsync()).to.bignumber.eq(swap.minOutputTokenAmount);
// Ensure the maker got input tokens.
expect(await inputToken.balanceOf(maker).callAsync()).to.bignumber.eq(swap.inputTokenAmount);
// Check events.
verifyEventsFromLogs(
receipt.logs,
[
{
taker,
sender: zeroEx.address,
data: NULL_BYTES,
},
],
'TransformerMetadata',
);
});
it('`transformERC20()` can fill RFQT order', async () => {
const swap = await generateSwapAsync({}, true);
const _protocolFee = protocolFee.times(GAS_PRICE).times(swap.orders.length + 1); // Pay a little more fee than needed.
const mtx = await createMetaTransactionAsync(getSwapData(swap), _protocolFee, 0);
const relayerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(relayer);
const receipt = await zeroEx
.executeMetaTransaction(mtx, sigstruct(mtx.signature))
.awaitTransactionSuccessAsync({ from: relayer, value: mtx.value, gasPrice: GAS_PRICE });
const relayerEthRefund = relayerEthBalanceBefore
.minus(await env.web3Wrapper.getBalanceInWeiAsync(relayer))
.minus(GAS_PRICE.times(receipt.gasUsed));
// Ensure the relayer got back the unused protocol fees.
expect(relayerEthRefund).to.bignumber.eq(protocolFee.times(GAS_PRICE));
// Ensure the relayer got paid no mtx fees.
expect(await feeToken.balanceOf(relayer).callAsync()).to.bignumber.eq(0);
// Ensure the taker got output tokens.
expect(await outputToken.balanceOf(taker).callAsync()).to.bignumber.eq(swap.minOutputTokenAmount);
// Ensure the maker got input tokens.
expect(await inputToken.balanceOf(maker).callAsync()).to.bignumber.eq(swap.inputTokenAmount);
// Check events.
verifyEventsFromLogs(
receipt.logs,
[
{
taker,
sender: zeroEx.address,
data: NULL_BYTES,
},
],
'TransformerMetadata',
);
});
it('`transformERC20()` can fill RFQT order if quote signer configured', async () => {
const swap = await generateSwapAsync({}, true);
const callData = getSwapData(swap);
const _protocolFee = protocolFee.times(GAS_PRICE).times(swap.orders.length + 1); // Pay a little more fee than needed.
const mtx = await createMetaTransactionAsync(callData, _protocolFee, 0);
const relayerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(relayer);
await zeroEx.setQuoteSigner(NULL_ADDRESS).awaitTransactionSuccessAsync({ from: owner });
const receipt = await zeroEx
.executeMetaTransaction(mtx, sigstruct(mtx.signature))
.awaitTransactionSuccessAsync({ from: relayer, value: mtx.value, gasPrice: GAS_PRICE });
const relayerEthRefund = relayerEthBalanceBefore
.minus(await env.web3Wrapper.getBalanceInWeiAsync(relayer))
.minus(GAS_PRICE.times(receipt.gasUsed));
// Ensure the relayer got back the unused protocol fees.
expect(relayerEthRefund).to.bignumber.eq(protocolFee.times(GAS_PRICE));
// Ensure the relayer got paid no mtx fees.
expect(await feeToken.balanceOf(relayer).callAsync()).to.bignumber.eq(0);
// Ensure the taker got output tokens.
expect(await outputToken.balanceOf(taker).callAsync()).to.bignumber.eq(swap.minOutputTokenAmount);
// Ensure the maker got input tokens.
expect(await inputToken.balanceOf(maker).callAsync()).to.bignumber.eq(swap.inputTokenAmount);
// Check events.
verifyEventsFromLogs(
receipt.logs,
[
{
taker,
sender: zeroEx.address,
data: NULL_BYTES,
},
],
'TransformerMetadata',
);
});
});

View File

@ -1,4 +1,13 @@
[
{
"version": "0.18.2",
"changes": [
{
"note": "Update FQT for v4 native orders",
"pr": 104
}
]
},
{
"version": "0.18.1",
"changes": [

View File

@ -22,13 +22,12 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibBytesV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibMathV06.sol";
import "../errors/LibTransformERC20RichErrors.sol";
import "../vendor/v3/IExchange.sol";
import "../vendor/v3/LibOrderHash.sol";
import "../features/INativeOrdersFeature.sol";
import "../features/libs/LibNativeOrder.sol";
import "./bridges/IBridgeAdapter.sol";
import "./Transformer.sol";
import "./LibERC20Transformer.sol";
@ -41,8 +40,8 @@ contract FillQuoteTransformer is
using LibERC20TokenV06 for IERC20TokenV06;
using LibERC20Transformer for IERC20TokenV06;
using LibSafeMathV06 for uint256;
using LibSafeMathV06 for uint128;
using LibRichErrorsV06 for bytes;
using LibBytesV06 for bytes;
/// @dev Whether we are performing a market sell or buy.
enum Side {
@ -50,6 +49,26 @@ contract FillQuoteTransformer is
Buy
}
enum OrderType {
Bridge,
Limit,
Rfq
}
struct LimitOrderInfo {
LibNativeOrder.LimitOrder order;
LibSignature.Signature signature;
// Maximum taker token amount of this limit order to fill.
uint256 maxTakerTokenFillAmount;
}
struct RfqOrderInfo {
LibNativeOrder.RfqOrder order;
LibSignature.Signature signature;
// Maximum taker token amount of this limit order to fill.
uint256 maxTakerTokenFillAmount;
}
/// @dev Transform data to ABI-encode and pass into `transform()`.
struct TransformData {
// Whether we are performing a market sell or buy.
@ -60,31 +79,34 @@ contract FillQuoteTransformer is
// The token being bought.
// This should be an actual token, not the ETH pseudo-token.
IERC20TokenV06 buyToken;
// The orders to fill.
IExchange.Order[] orders;
// Signatures for each respective order in `orders`.
bytes[] signatures;
// Maximum fill amount for each order. This may be shorter than the
// number of orders, where missing entries will be treated as `uint256(-1)`.
// For sells, this will be the maximum sell amount (taker asset).
// For buys, this will be the maximum buy amount (maker asset).
uint256[] maxOrderFillAmounts;
// External liquidity bridge orders. Sorted by fill sequence.
IBridgeAdapter.BridgeOrder[] bridgeOrders;
// Native limit orders. Sorted by fill sequence.
LimitOrderInfo[] limitOrders;
// Native RFQ orders. Sorted by fill sequence.
RfqOrderInfo[] rfqOrders;
// The sequence to fill the orders in. Each item will fill the next
// order of that type in either `bridgeOrders`, `limitOrders`,
// or `rfqOrders.`
OrderType[] fillSequence;
// Amount of `sellToken` to sell or `buyToken` to buy.
// For sells, this may be `uint256(-1)` to sell the entire balance of
// `sellToken`.
// For sells, setting the high-bit indicates that
// `sellAmount & LOW_BITS` should be treated as a `1e18` fraction of
// the current balance of `sellToken`, where
// `1e18+ == 100%` and `0.5e18 == 50%`, etc.
uint256 fillAmount;
// Who to transfer unused protocol fees to.
// May be a valid address or one of:
// `address(0)`: Stay in flash wallet.
// `address(1)`: Send to the taker.
// `address(2)`: Send to the sender (caller of `transformERC20()`).
address payable refundReceiver;
// Required taker address for RFQT orders.
// Null means any taker can fill it.
address rfqtTakerAddress;
}
/// @dev Results of a call to `_fillOrder()`.
struct FillOrderResults {
// The amount of taker tokens sold, according to balance checks.
uint256 takerTokenSoldAmount;
@ -101,7 +123,8 @@ contract FillQuoteTransformer is
uint256 soldAmount;
uint256 protocolFee;
uint256 takerTokenBalanceRemaining;
bool isRfqtAllowed;
uint256[3] currentIndices;
OrderType currentOrderType;
}
/// @dev Emitted when a trade is skipped due to a lack of funds
@ -109,12 +132,12 @@ contract FillQuoteTransformer is
/// @param orderHash The hash of the order that was skipped.
event ProtocolFeeUnfunded(bytes32 orderHash);
/// @dev The Exchange ERC20Proxy ID.
bytes4 private constant ERC20_ASSET_PROXY_ID = 0xf47261b0;
/// @dev The Exchange ERC20BridgeProxy ID.
bytes4 private constant ERC20_BRIDGE_PROXY_ID = 0xdc1600f3;
/// @dev Maximum uint256 value.
uint256 private constant MAX_UINT256 = uint256(-1);
/// @dev The highest bit of a uint256 value.
uint256 private constant HIGH_BIT = 2 ** 255;
/// @dev Mask of the lower 255 bits of a uint256 value.
uint256 private constant LOWER_255_BITS = HIGH_BIT - 1;
/// @dev If `refundReceiver` is set to this address, unpsent
/// protocol fees will be sent to the taker.
address private constant REFUND_RECEIVER_TAKER = address(1);
@ -122,33 +145,32 @@ contract FillQuoteTransformer is
/// protocol fees will be sent to the sender.
address private constant REFUND_RECEIVER_SENDER = address(2);
/// @dev The Exchange contract.
IExchange public immutable exchange;
/// @dev The ERC20Proxy address.
address public immutable erc20Proxy;
/// @dev The BridgeAdapter address
IBridgeAdapter public immutable bridgeAdapter;
/// @dev The exchange proxy contract.
INativeOrdersFeature public immutable zeroEx;
/// @dev Create this contract.
/// @param exchange_ The Exchange V3 instance.
constructor(IExchange exchange_, IBridgeAdapter bridgeAdapter_)
/// @param bridgeAdapter_ The bridge adapter contract.
/// @param zeroEx_ The Exchange Proxy contract.
constructor(IBridgeAdapter bridgeAdapter_, INativeOrdersFeature zeroEx_)
public
Transformer()
{
exchange = exchange_;
erc20Proxy = exchange_.getAssetProxy(ERC20_ASSET_PROXY_ID);
bridgeAdapter = bridgeAdapter_;
zeroEx = zeroEx_;
}
/// @dev Sell this contract's entire balance of of `sellToken` in exchange
/// for `buyToken` by filling `orders`. Protocol fees should be attached
/// to this call. `buyToken` and excess ETH will be transferred back to the caller.
/// @param context Context information.
/// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`).
/// @return magicBytes The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`).
function transform(TransformContext calldata context)
external
override
returns (bytes4 success)
returns (bytes4 magicBytes)
{
TransformData memory data = abi.decode(context.data, (TransformData));
FillState memory state;
@ -160,7 +182,11 @@ contract FillQuoteTransformer is
context.data
).rrevert();
}
if (data.orders.length != data.signatures.length) {
if (data.bridgeOrders.length
+ data.limitOrders.length
+ data.rfqOrders.length != data.fillSequence.length
) {
LibTransformERC20RichErrors.InvalidTransformDataError(
LibTransformERC20RichErrors.InvalidTransformDataErrorCode.INVALID_ARRAY_LENGTH,
context.data
@ -168,76 +194,58 @@ contract FillQuoteTransformer is
}
state.takerTokenBalanceRemaining = data.sellToken.getTokenBalanceOf(address(this));
if (data.side == Side.Sell && data.fillAmount == MAX_UINT256) {
// If `sellAmount == -1 then we are selling
// the entire balance of `sellToken`. This is useful in cases where
// the exact sell amount is not exactly known in advance, like when
// unwrapping Chai/cUSDC/cDAI.
data.fillAmount = state.takerTokenBalanceRemaining;
if (data.side == Side.Sell) {
data.fillAmount = _normalizeFillAmount(data.fillAmount, state.takerTokenBalanceRemaining);
}
// Approve the ERC20 proxy to spend `sellToken`.
data.sellToken.approveIfBelow(erc20Proxy, data.fillAmount);
// Approve the exchange proxy to spend our sell tokens if native orders
// are present.
if (data.limitOrders.length + data.rfqOrders.length != 0) {
data.sellToken.approveIfBelow(address(zeroEx), data.fillAmount);
// Compute the protocol fee if a limit order is present.
if (data.limitOrders.length != 0) {
state.protocolFee = uint256(zeroEx.getProtocolFeeMultiplier())
.safeMul(tx.gasprice);
}
}
state.protocolFee = exchange.protocolFeeMultiplier().safeMul(tx.gasprice);
state.ethRemaining = address(this).balance;
// RFQT orders can only be filled if the actual taker matches the RFQT
// taker (if set).
state.isRfqtAllowed = data.rfqtTakerAddress == address(0)
|| context.taker == data.rfqtTakerAddress;
// Fill the orders.
for (uint256 i = 0; i < data.orders.length; ++i) {
for (uint256 i = 0; i < data.fillSequence.length; ++i) {
// Check if we've hit our targets.
if (data.side == Side.Sell) {
// Market sell check.
if (state.soldAmount >= data.fillAmount) {
break;
}
if (state.soldAmount >= data.fillAmount) { break; }
} else {
// Market buy check.
if (state.boughtAmount >= data.fillAmount) {
break;
}
if (state.boughtAmount >= data.fillAmount) { break; }
}
state.currentOrderType = OrderType(data.fillSequence[i]);
uint256 orderIndex = state.currentIndices[uint256(state.currentOrderType)];
// Fill the order.
FillOrderResults memory results;
if (data.side == Side.Sell) {
// Market sell.
results = _sellToOrder(
data.buyToken,
data.sellToken,
data.orders[i],
data.signatures[i],
data.fillAmount.safeSub(state.soldAmount).min256(
data.maxOrderFillAmounts.length > i
? data.maxOrderFillAmounts[i]
: MAX_UINT256
),
state
);
if (state.currentOrderType == OrderType.Bridge) {
results = _fillBridgeOrder(data.bridgeOrders[orderIndex], data, state);
} else if (state.currentOrderType == OrderType.Limit) {
results = _fillLimitOrder(data.limitOrders[orderIndex], data, state);
} else if (state.currentOrderType == OrderType.Rfq) {
results = _fillRfqOrder(data.rfqOrders[orderIndex], data, state);
} else {
// Market buy.
results = _buyFromOrder(
data.buyToken,
data.sellToken,
data.orders[i],
data.signatures[i],
data.fillAmount.safeSub(state.boughtAmount).min256(
data.maxOrderFillAmounts.length > i
? data.maxOrderFillAmounts[i]
: MAX_UINT256
),
state
);
revert("INVALID_ORDER_TYPE");
}
// Accumulate totals.
state.soldAmount = state.soldAmount.safeAdd(results.takerTokenSoldAmount);
state.boughtAmount = state.boughtAmount.safeAdd(results.makerTokenBoughtAmount);
state.ethRemaining = state.ethRemaining.safeSub(results.protocolFeePaid);
state.takerTokenBalanceRemaining = state.takerTokenBalanceRemaining.safeSub(results.takerTokenSoldAmount);
state.soldAmount = state.soldAmount
.safeAdd(results.takerTokenSoldAmount);
state.boughtAmount = state.boughtAmount
.safeAdd(results.makerTokenBoughtAmount);
state.ethRemaining = state.ethRemaining
.safeSub(results.protocolFeePaid);
state.takerTokenBalanceRemaining = state.takerTokenBalanceRemaining
.safeSub(results.takerTokenSoldAmount);
state.currentIndices[uint256(state.currentOrderType)]++;
}
// Ensure we hit our targets.
@ -276,223 +284,174 @@ contract FillQuoteTransformer is
return LibERC20Transformer.TRANSFORMER_SUCCESS;
}
/// @dev Try to sell up to `sellAmount` from an order.
/// @param makerToken The maker/buy token.
/// @param takerToken The taker/sell token.
/// @param order The order to fill.
/// @param signature The signature for `order`.
/// @param sellAmount Amount of taker token to sell.
/// @param state Intermediate state variables to get around stack limits.
function _sellToOrder(
IERC20TokenV06 makerToken,
IERC20TokenV06 takerToken,
IExchange.Order memory order,
bytes memory signature,
uint256 sellAmount,
// Fill a single bridge order.
function _fillBridgeOrder(
IBridgeAdapter.BridgeOrder memory order,
TransformData memory data,
FillState memory state
)
private
returns (FillOrderResults memory results)
{
IERC20TokenV06 takerFeeToken =
_getTokenFromERC20AssetData(order.takerFeeAssetData);
uint256 takerTokenFillAmount = sellAmount;
if (order.takerFee != 0) {
if (takerFeeToken == makerToken) {
// Taker fee is payable in the maker token, so we need to
// approve the proxy to spend the maker token.
// It isn't worth computing the actual taker fee
// since `approveIfBelow()` will set the allowance to infinite. We
// just need a reasonable upper bound to avoid unnecessarily re-approving.
takerFeeToken.approveIfBelow(erc20Proxy, order.takerFee);
} else if (takerFeeToken == takerToken){
// Taker fee is payable in the taker token, so we need to
// reduce the fill amount to cover the fee.
// takerTokenFillAmount' =
// (takerTokenFillAmount * order.takerAssetAmount) /
// (order.takerAssetAmount + order.takerFee)
takerTokenFillAmount = LibMathV06.getPartialAmountCeil(
order.takerAssetAmount,
order.takerAssetAmount.safeAdd(order.takerFee),
sellAmount
);
} else {
// Only support taker or maker asset denominated taker fees.
LibTransformERC20RichErrors.InvalidTakerFeeTokenError(
address(takerFeeToken)
).rrevert();
}
}
// Perform the fill.
return _fillOrder(
order,
signature,
takerTokenFillAmount,
uint256 takerTokenFillAmount = _computeTakerTokenFillAmount(
data,
state,
takerFeeToken == takerToken
order.takerTokenAmount,
order.makerTokenAmount,
0
);
(bool success, bytes memory resultData) = address(bridgeAdapter).delegatecall(
abi.encodeWithSelector(
IBridgeAdapter.trade.selector,
order,
data.sellToken,
data.buyToken,
takerTokenFillAmount
)
);
if (success) {
results.makerTokenBoughtAmount = abi.decode(resultData, (uint256));
results.takerTokenSoldAmount = takerTokenFillAmount;
}
}
/// @dev Try to buy up to `buyAmount` from an order.
/// @param makerToken The maker/buy token.
/// @param takerToken The taker/sell token.
/// @param order The order to fill.
/// @param signature The signature for `order`.
/// @param buyAmount Amount of maker token to buy.
/// @param state Intermediate state variables to get around stack limits.
function _buyFromOrder(
IERC20TokenV06 makerToken,
IERC20TokenV06 takerToken,
IExchange.Order memory order,
bytes memory signature,
uint256 buyAmount,
// Fill a single limit order.
function _fillLimitOrder(
LimitOrderInfo memory orderInfo,
TransformData memory data,
FillState memory state
)
private
returns (FillOrderResults memory results)
{
IERC20TokenV06 takerFeeToken =
_getTokenFromERC20AssetData(order.takerFeeAssetData);
// Compute the default taker token fill amount.
uint256 takerTokenFillAmount = LibMathV06.getPartialAmountCeil(
buyAmount,
order.makerAssetAmount,
order.takerAssetAmount
uint256 takerTokenFillAmount = LibSafeMathV06.min256(
_computeTakerTokenFillAmount(
data,
state,
orderInfo.order.takerAmount,
orderInfo.order.makerAmount,
orderInfo.order.takerTokenFeeAmount
),
orderInfo.maxTakerTokenFillAmount
);
if (order.takerFee != 0) {
if (takerFeeToken == makerToken) {
// Taker fee is payable in the maker token.
// Adjust the taker token fill amount to account for maker
// tokens being lost to the taker fee.
// takerTokenFillAmount' =
// (order.takerAssetAmount * buyAmount) /
// (order.makerAssetAmount - order.takerFee)
takerTokenFillAmount = LibMathV06.getPartialAmountCeil(
buyAmount,
order.makerAssetAmount.safeSub(order.takerFee),
order.takerAssetAmount
);
// Approve the proxy to spend the maker token.
// It isn't worth computing the actual taker fee
// since `approveIfBelow()` will set the allowance to infinite. We
// just need a reasonable upper bound to avoid unnecessarily re-approving.
takerFeeToken.approveIfBelow(erc20Proxy, order.takerFee);
} else if (takerFeeToken != takerToken) {
// Only support taker or maker asset denominated taker fees.
LibTransformERC20RichErrors.InvalidTakerFeeTokenError(
address(takerFeeToken)
).rrevert();
}
// Emit an event if we do not have sufficient ETH to cover the protocol fee.
if (state.ethRemaining < state.protocolFee) {
bytes32 orderHash = zeroEx.getLimitOrderHash(orderInfo.order);
emit ProtocolFeeUnfunded(orderHash);
return results; // Empty results.
}
// Perform the fill.
return _fillOrder(
order,
signature,
takerTokenFillAmount,
state,
takerFeeToken == takerToken
);
}
/// @dev Attempt to fill an order. If the fill reverts, the revert will be
/// swallowed and `results` will be zeroed out.
/// @param order The order to fill.
/// @param signature The order signature.
/// @param takerAssetFillAmount How much taker asset to fill.
/// @param state Intermediate state variables to get around stack limits.
/// @param isTakerFeeInTakerToken Whether the taker fee token is the same as the
/// taker token.
function _fillOrder(
IExchange.Order memory order,
bytes memory signature,
uint256 takerAssetFillAmount,
FillState memory state,
bool isTakerFeeInTakerToken
)
private
returns (FillOrderResults memory results)
{
// Clamp to remaining taker asset amount or order size.
uint256 availableTakerAssetFillAmount =
takerAssetFillAmount.min256(order.takerAssetAmount);
availableTakerAssetFillAmount =
availableTakerAssetFillAmount.min256(state.takerTokenBalanceRemaining);
// If it is a Bridge order we fill this directly through the BridgeAdapter
if (order.makerAssetData.readBytes4(0) == ERC20_BRIDGE_PROXY_ID) {
(bool success, bytes memory resultData) = address(bridgeAdapter).delegatecall(
abi.encodeWithSelector(
IBridgeAdapter.trade.selector,
order.makerAssetData,
address(_getTokenFromERC20AssetData(order.takerAssetData)),
availableTakerAssetFillAmount
try
zeroEx.fillLimitOrder
{value: state.protocolFee}
(
orderInfo.order,
orderInfo.signature,
takerTokenFillAmount.safeDowncastToUint128()
)
);
if (success) {
results.makerTokenBoughtAmount = abi.decode(resultData, (uint256));
results.takerTokenSoldAmount = availableTakerAssetFillAmount;
// protocol fee paid remains 0
}
return results;
} else {
// If the order taker address is set to this contract's address then
// this is an RFQT order, and we will only fill it if allowed to.
if (order.takerAddress == address(this) && !state.isRfqtAllowed) {
return results; // Empty results.
}
// Emit an event if we do not have sufficient ETH to cover the protocol fee.
if (state.ethRemaining < state.protocolFee) {
bytes32 orderHash = LibOrderHash.getTypedDataHash(
order,
exchange.EIP712_EXCHANGE_DOMAIN_HASH()
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
if (orderInfo.order.takerTokenFeeAmount > 0) {
takerTokenFilledAmount = takerTokenFilledAmount.safeAdd128(
LibMathV06.getPartialAmountFloor(
takerTokenFilledAmount,
orderInfo.order.takerAmount,
orderInfo.order.takerTokenFeeAmount
).safeDowncastToUint128()
);
emit ProtocolFeeUnfunded(orderHash);
return results;
}
try
exchange.fillOrder
{value: state.protocolFee}
(order, availableTakerAssetFillAmount, signature)
returns (IExchange.FillResults memory fillResults)
{
results.makerTokenBoughtAmount = fillResults.makerAssetFilledAmount;
results.takerTokenSoldAmount = fillResults.takerAssetFilledAmount;
results.protocolFeePaid = fillResults.protocolFeePaid;
// If the taker fee is payable in the taker asset, include the
// taker fee in the total amount sold.
if (isTakerFeeInTakerToken) {
results.takerTokenSoldAmount =
results.takerTokenSoldAmount.safeAdd(fillResults.takerFeePaid);
}
} catch (bytes memory) {
// Swallow failures, leaving all results as zero.
}
}
results.takerTokenSoldAmount = takerTokenFilledAmount;
results.makerTokenBoughtAmount = makerTokenFilledAmount;
results.protocolFeePaid = state.protocolFee;
} catch {}
}
/// @dev Extract the token from plain ERC20 asset data.
/// If the asset-data is empty, a zero token address will be returned.
/// @param assetData The order asset data.
function _getTokenFromERC20AssetData(bytes memory assetData)
// Fill a single RFQ order.
function _fillRfqOrder(
RfqOrderInfo 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.fillRfqOrder
(
orderInfo.order,
orderInfo.signature,
takerTokenFillAmount.safeDowncastToUint128()
)
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
results.takerTokenSoldAmount = takerTokenFilledAmount;
results.makerTokenBoughtAmount = makerTokenFilledAmount;
} catch {}
}
// Compute the next taker token fill amount of a generic order.
function _computeTakerTokenFillAmount(
TransformData memory data,
FillState memory state,
uint256 orderTakerAmount,
uint256 orderMakerAmount,
uint256 orderTakerTokenFeeAmount
)
private
pure
returns (IERC20TokenV06 token)
returns (uint256 takerTokenFillAmount)
{
if (assetData.length == 0) {
return IERC20TokenV06(address(0));
if (data.side == Side.Sell) {
takerTokenFillAmount = data.fillAmount.safeSub(state.soldAmount);
if (orderTakerTokenFeeAmount != 0) {
takerTokenFillAmount = LibMathV06.getPartialAmountCeil(
takerTokenFillAmount,
orderTakerAmount.safeAdd(orderTakerTokenFeeAmount),
orderTakerAmount
);
}
} else { // Buy
takerTokenFillAmount = LibMathV06.getPartialAmountCeil(
data.fillAmount.safeSub(state.boughtAmount),
orderMakerAmount,
orderTakerAmount
);
}
if (assetData.length != 36 ||
LibBytesV06.readBytes4(assetData, 0) != ERC20_ASSET_PROXY_ID)
{
LibTransformERC20RichErrors
.InvalidERC20AssetDataError(assetData)
.rrevert();
return LibSafeMathV06.min256(
LibSafeMathV06.min256(takerTokenFillAmount, orderTakerAmount),
state.takerTokenBalanceRemaining
);
}
// Convert possible proportional values to absolute quantities.
function _normalizeFillAmount(uint256 rawAmount, uint256 balance)
private
pure
returns (uint256 normalized)
{
if ((rawAmount & HIGH_BIT) == HIGH_BIT) {
// If the high bit of `rawAmount` is set then the lower 255 bits
// specify a fraction of `balance`.
return LibSafeMathV06.min256(
balance
* LibSafeMathV06.min256(rawAmount & LOWER_255_BITS, 1e18)
/ 1e18,
balance
);
}
return IERC20TokenV06(LibBytesV06.readAddress(assetData, 16));
return rawAmount;
}
}

View File

@ -20,7 +20,8 @@
pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "./mixins/MixinAdapterAddresses.sol";
import "./IBridgeAdapter.sol";
import "./BridgeSource.sol";
import "./mixins/MixinBalancer.sol";
import "./mixins/MixinBancor.sol";
import "./mixins/MixinCoFiX.sol";
@ -38,7 +39,7 @@ import "./mixins/MixinUniswapV2.sol";
import "./mixins/MixinZeroExBridge.sol";
contract BridgeAdapter is
MixinAdapterAddresses,
IBridgeAdapter,
MixinBalancer,
MixinBancor,
MixinCoFiX,
@ -55,203 +56,146 @@ contract BridgeAdapter is
MixinUniswapV2,
MixinZeroExBridge
{
/// @dev Emitted when a trade occurs.
/// @param inputToken The token the bridge is converting from.
/// @param outputToken The token the bridge is converting to.
/// @param inputTokenAmount Amount of input token.
/// @param outputTokenAmount Amount of output token.
/// @param from The bridge address, indicating the underlying source of the fill.
/// @param to The `to` address, currrently `address(this)`
event ERC20BridgeTransfer(
IERC20TokenV06 inputToken,
IERC20TokenV06 outputToken,
uint256 inputTokenAmount,
uint256 outputTokenAmount,
address from,
address to
);
address private immutable BALANCER_BRIDGE_ADDRESS;
address private immutable BANCOR_BRIDGE_ADDRESS;
address private immutable COFIX_BRIDGE_ADDRESS;
address private immutable CREAM_BRIDGE_ADDRESS;
address private immutable CURVE_BRIDGE_ADDRESS;
address private immutable CRYPTO_COM_BRIDGE_ADDRESS;
address private immutable DODO_BRIDGE_ADDRESS;
address private immutable KYBER_BRIDGE_ADDRESS;
address private immutable MOONISWAP_BRIDGE_ADDRESS;
address private immutable MSTABLE_BRIDGE_ADDRESS;
address private immutable OASIS_BRIDGE_ADDRESS;
address private immutable SHELL_BRIDGE_ADDRESS;
address private immutable SNOW_SWAP_BRIDGE_ADDRESS;
address private immutable SUSHISWAP_BRIDGE_ADDRESS;
address private immutable SWERVE_BRIDGE_ADDRESS;
address private immutable UNISWAP_BRIDGE_ADDRESS;
address private immutable UNISWAP_V2_BRIDGE_ADDRESS;
constructor(AdapterAddresses memory addresses)
constructor(IEtherTokenV06 weth)
public
MixinBalancer()
MixinBancor(addresses)
MixinBancor(weth)
MixinCoFiX()
MixinCurve()
MixinCryptoCom()
MixinDodo(addresses)
MixinKyber(addresses)
MixinMooniswap(addresses)
MixinMStable(addresses)
MixinOasis(addresses)
MixinDodo()
MixinKyber(weth)
MixinMooniswap(weth)
MixinMStable()
MixinOasis()
MixinShell()
MixinSushiswap(addresses)
MixinUniswap(addresses)
MixinUniswapV2(addresses)
MixinSushiswap()
MixinUniswap(weth)
MixinUniswapV2()
MixinZeroExBridge()
{
BALANCER_BRIDGE_ADDRESS = addresses.balancerBridge;
BANCOR_BRIDGE_ADDRESS = addresses.bancorBridge;
COFIX_BRIDGE_ADDRESS = addresses.cofixBridge;
CURVE_BRIDGE_ADDRESS = addresses.curveBridge;
CRYPTO_COM_BRIDGE_ADDRESS = addresses.cryptoComBridge;
KYBER_BRIDGE_ADDRESS = addresses.kyberBridge;
MOONISWAP_BRIDGE_ADDRESS = addresses.mooniswapBridge;
MSTABLE_BRIDGE_ADDRESS = addresses.mStableBridge;
OASIS_BRIDGE_ADDRESS = addresses.oasisBridge;
SHELL_BRIDGE_ADDRESS = addresses.shellBridge;
SUSHISWAP_BRIDGE_ADDRESS = addresses.sushiswapBridge;
SWERVE_BRIDGE_ADDRESS = addresses.swerveBridge;
UNISWAP_BRIDGE_ADDRESS = addresses.uniswapBridge;
UNISWAP_V2_BRIDGE_ADDRESS = addresses.uniswapV2Bridge;
CREAM_BRIDGE_ADDRESS = addresses.creamBridge;
SNOW_SWAP_BRIDGE_ADDRESS = addresses.snowSwapBridge;
DODO_BRIDGE_ADDRESS = addresses.dodoBridge;
}
{}
function trade(
bytes calldata makerAssetData,
BridgeOrder memory order,
IERC20TokenV06 sellToken,
IERC20TokenV06 buyToken,
uint256 sellAmount
)
external
public
override
returns (uint256 boughtAmount)
{
(
IERC20TokenV06 buyToken,
address bridgeAddress,
bytes memory bridgeData
) = abi.decode(
makerAssetData[4:],
(IERC20TokenV06, address, bytes)
);
require(
bridgeAddress != address(this) && bridgeAddress != address(0),
"BridgeAdapter/INVALID_BRIDGE_ADDRESS"
);
if (bridgeAddress == CURVE_BRIDGE_ADDRESS ||
bridgeAddress == SWERVE_BRIDGE_ADDRESS ||
bridgeAddress == SNOW_SWAP_BRIDGE_ADDRESS) {
if (order.source == BridgeSource.CURVE ||
order.source == BridgeSource.SWERVE ||
order.source == BridgeSource.SNOWSWAP) {
boughtAmount = _tradeCurve(
buyToken,
sellAmount,
bridgeData
);
} else if (bridgeAddress == SUSHISWAP_BRIDGE_ADDRESS) {
boughtAmount = _tradeSushiswap(
buyToken,
sellAmount,
bridgeData
);
} else if (bridgeAddress == UNISWAP_V2_BRIDGE_ADDRESS) {
boughtAmount = _tradeUniswapV2(
buyToken,
sellAmount,
bridgeData
);
} else if (bridgeAddress == UNISWAP_BRIDGE_ADDRESS) {
boughtAmount = _tradeUniswap(
buyToken,
sellAmount,
bridgeData
);
} else if (bridgeAddress == BALANCER_BRIDGE_ADDRESS ||
bridgeAddress == CREAM_BRIDGE_ADDRESS) {
boughtAmount = _tradeBalancer(
buyToken,
sellAmount,
bridgeData
);
} else if (bridgeAddress == KYBER_BRIDGE_ADDRESS) {
boughtAmount = _tradeKyber(
buyToken,
sellAmount,
bridgeData
);
} else if (bridgeAddress == MOONISWAP_BRIDGE_ADDRESS) {
boughtAmount = _tradeMooniswap(
buyToken,
sellAmount,
bridgeData
);
} else if (bridgeAddress == MSTABLE_BRIDGE_ADDRESS) {
boughtAmount = _tradeMStable(
buyToken,
sellAmount,
bridgeData
);
} else if (bridgeAddress == OASIS_BRIDGE_ADDRESS) {
boughtAmount = _tradeOasis(
buyToken,
sellAmount,
bridgeData
);
} else if (bridgeAddress == SHELL_BRIDGE_ADDRESS) {
boughtAmount = _tradeShell(
buyToken,
sellAmount,
bridgeData
);
} else if (bridgeAddress == DODO_BRIDGE_ADDRESS) {
boughtAmount = _tradeDodo(
buyToken,
sellAmount,
bridgeData
);
} else if (bridgeAddress == CRYPTO_COM_BRIDGE_ADDRESS) {
boughtAmount = _tradeCryptoCom(
buyToken,
sellAmount,
bridgeData
);
} else if (bridgeAddress == BANCOR_BRIDGE_ADDRESS) {
boughtAmount = _tradeBancor(
buyToken,
sellAmount,
bridgeData
);
} else if (bridgeAddress == COFIX_BRIDGE_ADDRESS) {
boughtAmount = _tradeCoFiX(
buyToken,
sellAmount,
bridgeData
);
} else {
boughtAmount = _tradeZeroExBridge(
bridgeAddress,
sellToken,
buyToken,
sellAmount,
bridgeData
order.bridgeData
);
} else if (order.source == BridgeSource.SUSHISWAP) {
boughtAmount = _tradeSushiswap(
buyToken,
sellAmount,
order.bridgeData
);
} else if (order.source == BridgeSource.UNISWAPV2) {
boughtAmount = _tradeUniswapV2(
buyToken,
sellAmount,
order.bridgeData
);
} else if (order.source == BridgeSource.UNISWAP) {
boughtAmount = _tradeUniswap(
sellToken,
buyToken,
sellAmount,
order.bridgeData
);
} else if (order.source == BridgeSource.BALANCER ||
order.source == BridgeSource.CREAM) {
boughtAmount = _tradeBalancer(
sellToken,
buyToken,
sellAmount,
order.bridgeData
);
} else if (order.source == BridgeSource.KYBER) {
boughtAmount = _tradeKyber(
sellToken,
buyToken,
sellAmount,
order.bridgeData
);
} else if (order.source == BridgeSource.MOONISWAP) {
boughtAmount = _tradeMooniswap(
sellToken,
buyToken,
sellAmount,
order.bridgeData
);
} else if (order.source == BridgeSource.MSTABLE) {
boughtAmount = _tradeMStable(
sellToken,
buyToken,
sellAmount,
order.bridgeData
);
} else if (order.source == BridgeSource.OASIS) {
boughtAmount = _tradeOasis(
sellToken,
buyToken,
sellAmount,
order.bridgeData
);
} else if (order.source == BridgeSource.SHELL) {
boughtAmount = _tradeShell(
sellToken,
buyToken,
sellAmount,
order.bridgeData
);
} else if (order.source == BridgeSource.DODO) {
boughtAmount = _tradeDodo(
sellToken,
sellAmount,
order.bridgeData
);
} else if (order.source == BridgeSource.CRYPTOCOM) {
boughtAmount = _tradeCryptoCom(
buyToken,
sellAmount,
order.bridgeData
);
} else if (order.source == BridgeSource.BANCOR) {
boughtAmount = _tradeBancor(
buyToken,
sellAmount,
order.bridgeData
);
} else if (order.source == BridgeSource.COFIX) {
boughtAmount = _tradeCoFiX(
sellToken,
buyToken,
sellAmount,
order.bridgeData
);
} else {
boughtAmount = _tradeZeroExBridge(
sellToken,
buyToken,
sellAmount,
order.bridgeData
);
}
emit ERC20BridgeTransfer(
emit BridgeFill(
order.source,
sellToken,
buyToken,
sellAmount,
boughtAmount,
bridgeAddress,
address(this)
boughtAmount
);
}
}

View File

@ -0,0 +1,45 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
library BridgeSource {
uint256 constant internal BALANCER = 0;
uint256 constant internal BANCOR = 1;
uint256 constant internal COFIX = 2;
uint256 constant internal CURVE = 3;
uint256 constant internal CREAM = 4;
uint256 constant internal CRYPTOCOM = 5;
uint256 constant internal DODO = 6;
uint256 constant internal KYBER = 7;
uint256 constant internal LIQUIDITYPROVIDER = 8;
uint256 constant internal MOONISWAP = 9;
uint256 constant internal MSTABLE = 10;
uint256 constant internal OASIS = 11;
uint256 constant internal SHELL = 12;
uint256 constant internal SNOWSWAP = 13;
uint256 constant internal SUSHISWAP = 14;
uint256 constant internal SWERVE = 15;
uint256 constant internal UNISWAP = 16;
uint256 constant internal UNISWAPV2 = 17;
// New sources should be APPENDED to this list, taking the next highest
// integer value.
}

View File

@ -18,12 +18,38 @@
*/
pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
interface IBridgeAdapter {
struct BridgeOrder {
uint256 source;
uint256 takerTokenAmount;
uint256 makerTokenAmount;
bytes bridgeData;
}
/// @dev Emitted when tokens are swapped with an external source.
/// @param source The unique ID for the source. See `BridgeSource.sol`
/// @param inputToken The token the bridge is converting from.
/// @param outputToken The token the bridge is converting to.
/// @param inputTokenAmount Amount of input token sold.
/// @param outputTokenAmount Amount of output token bought.
event BridgeFill(
uint256 source,
IERC20TokenV06 inputToken,
IERC20TokenV06 outputToken,
uint256 inputTokenAmount,
uint256 outputTokenAmount
);
function trade(
bytes calldata makerAssetData,
address fromTokenAddress,
BridgeOrder calldata order,
IERC20TokenV06 sellToken,
IERC20TokenV06 buyToken,
uint256 sellAmount
)
external

View File

@ -1,55 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.6.5;
contract MixinAdapterAddresses
{
struct AdapterAddresses {
// Bridges
address balancerBridge;
address bancorBridge;
address cofixBridge;
address creamBridge;
address curveBridge;
address cryptoComBridge;
address dodoBridge;
address kyberBridge;
address mooniswapBridge;
address mStableBridge;
address oasisBridge;
address shellBridge;
address snowSwapBridge;
address swerveBridge;
address sushiswapBridge;
address uniswapBridge;
address uniswapV2Bridge;
// Exchanges
address kyberNetworkProxy;
address oasis;
address sushiswapRouter;
address uniswapV2Router;
address uniswapExchangeFactory;
address mStable;
address dodoHelper;
// Other
address weth;
}
}

View File

@ -46,6 +46,7 @@ contract MixinBalancer {
using LibERC20TokenV06 for IERC20TokenV06;
function _tradeBalancer(
IERC20TokenV06 sellToken,
IERC20TokenV06 buyToken,
uint256 sellAmount,
bytes memory bridgeData
@ -54,9 +55,9 @@ contract MixinBalancer {
returns (uint256 boughtAmount)
{
// Decode the bridge data.
(IERC20TokenV06 sellToken, IBalancerPool pool) = abi.decode(
(IBalancerPool pool) = abi.decode(
bridgeData,
(IERC20TokenV06, IBalancerPool)
(IBalancerPool)
);
sellToken.approveIfBelow(
address(pool),

View File

@ -24,12 +24,12 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol";
import "./MixinAdapterAddresses.sol";
import "../IBridgeAdapter.sol";
interface IBancorNetwork {
function convertByPath(
address[] calldata _path,
IERC20TokenV06[] calldata _path,
uint256 _amount,
uint256 _minReturn,
address _beneficiary,
@ -42,17 +42,17 @@ interface IBancorNetwork {
}
contract MixinBancor is
MixinAdapterAddresses
{
contract MixinBancor {
/// @dev Bancor ETH pseudo-address.
address constant public BANCOR_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
IERC20TokenV06 constant public BANCOR_ETH_ADDRESS =
IERC20TokenV06(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE);
IEtherTokenV06 private immutable WETH;
constructor(AdapterAddresses memory addresses)
constructor(IEtherTokenV06 weth)
public
{
WETH = IEtherTokenV06(addresses.weth);
WETH = weth;
}
function _tradeBancor(
@ -64,17 +64,22 @@ contract MixinBancor is
returns (uint256 boughtAmount)
{
// Decode the bridge data.
(
address[] memory path,
address bancorNetworkAddress
// solhint-disable indent
) = abi.decode(bridgeData, (address[], address));
// solhint-enable indent
IBancorNetwork bancorNetworkAddress;
IERC20TokenV06[] memory path;
{
address[] memory _path;
(
bancorNetworkAddress,
_path
) = abi.decode(bridgeData, (IBancorNetwork, address[]));
// To get around `abi.decode()` not supporting interface array types.
assembly { path := _path }
}
require(path.length >= 2, "MixinBancor/PATH_LENGTH_MUST_BE_AT_LEAST_TWO");
require(
path[path.length - 1] == address(buyToken) ||
(path[path.length - 1] == BANCOR_ETH_ADDRESS && address(buyToken) == address(WETH)),
path[path.length - 1] == buyToken ||
(path[path.length - 1] == BANCOR_ETH_ADDRESS && buyToken == WETH),
"MixinBancor/LAST_ELEMENT_OF_PATH_MUST_MATCH_OUTPUT_TOKEN"
);
@ -88,14 +93,14 @@ contract MixinBancor is
} else {
// Grant an allowance to the Bancor Network.
LibERC20TokenV06.approveIfBelow(
IERC20TokenV06(path[0]),
bancorNetworkAddress,
path[0],
address(bancorNetworkAddress),
sellAmount
);
}
// Convert the tokens
boughtAmount = IBancorNetwork(bancorNetworkAddress).convertByPath{value: payableAmount}(
boughtAmount = bancorNetworkAddress.convertByPath{value: payableAmount}(
path, // path originating with source token and terminating in destination token
sellAmount, // amount of source token to trade
1, // minimum amount of destination token expected to receive

View File

@ -23,7 +23,6 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol";
import "./MixinAdapterAddresses.sol";
interface ICoFiXRouter {
@ -53,15 +52,20 @@ interface ICoFiXPair {
function swapWithExact(address outToken, address to)
external
payable
returns (uint amountIn, uint amountOut, uint oracleFeeChange, uint256[4] memory tradeInfo);
returns (
uint amountIn,
uint amountOut,
uint oracleFeeChange,
uint256[4] memory tradeInfo
);
}
contract MixinCoFiX is
MixinAdapterAddresses
{
contract MixinCoFiX {
using LibERC20TokenV06 for IERC20TokenV06;
function _tradeCoFiX(
IERC20TokenV06 sellToken,
IERC20TokenV06 buyToken,
uint256 sellAmount,
bytes memory bridgeData
@ -69,15 +73,16 @@ contract MixinCoFiX is
internal
returns (uint256 boughtAmount)
{
(address fromTokenAddress, uint256 fee, address pool) = abi.decode(bridgeData, (address, uint256, address));
(uint256 fee, ICoFiXPair pool) = abi.decode(bridgeData, (uint256, ICoFiXPair));
// Transfer tokens into the pool
LibERC20TokenV06.compatTransfer(
IERC20TokenV06(fromTokenAddress),
pool,
sellAmount);
sellToken,
address(pool),
sellAmount
);
// Call the swap exact with the tokens now in the pool
// pay the NEST Oracle fee with ETH
(/* In */, boughtAmount, , ) = ICoFiXPair(pool).swapWithExact{value: fee}(
(/* In */, boughtAmount, , ) = pool.swapWithExact{value: fee}(
address(buyToken),
address(this)
);

View File

@ -37,21 +37,24 @@ contract MixinCryptoCom
internal
returns (uint256 boughtAmount)
{
// solhint-disable indent
address[] memory path;
address router;
(path, router) = abi.decode(bridgeData, (address[], address));
// solhint-enable indent
IUniswapV2Router02 router;
IERC20TokenV06[] memory path;
{
address[] memory _path;
(router, _path) = abi.decode(bridgeData, (IUniswapV2Router02, address[]));
// To get around `abi.decode()` not supporting interface array types.
assembly { path := _path }
}
require(path.length >= 2, "CryptoComBridge/PATH_LENGTH_MUST_BE_AT_LEAST_TWO");
require(path.length >= 2, "MixinCryptoCom/PATH_LENGTH_MUST_BE_AT_LEAST_TWO");
require(
path[path.length - 1] == address(buyToken),
"CryptoComBridge/LAST_ELEMENT_OF_PATH_MUST_MATCH_OUTPUT_TOKEN"
path[path.length - 1] == buyToken,
"MixinCryptoCom/LAST_ELEMENT_OF_PATH_MUST_MATCH_OUTPUT_TOKEN"
);
// Grant the CryptoCom router an allowance to sell the first token.
IERC20TokenV06(path[0]).approveIfBelow(router, sellAmount);
path[0].approveIfBelow(address(router), sellAmount);
uint[] memory amounts = IUniswapV2Router02(router).swapExactTokensForTokens(
uint[] memory amounts = router.swapExactTokensForTokens(
// Sell all tokens we hold.
sellAmount,
// Minimum buy amount.

View File

@ -35,12 +35,12 @@ contract MixinCurve {
struct CurveBridgeData {
address curveAddress;
bytes4 exchangeFunctionSelector;
IERC20TokenV06 sellToken;
int128 fromCoinIdx;
int128 toCoinIdx;
}
function _tradeCurve(
IERC20TokenV06 sellToken,
IERC20TokenV06 buyToken,
uint256 sellAmount,
bytes memory bridgeData
@ -50,7 +50,7 @@ contract MixinCurve {
{
// Decode the bridge data to get the Curve metadata.
CurveBridgeData memory data = abi.decode(bridgeData, (CurveBridgeData));
data.sellToken.approveIfBelow(data.curveAddress, sellAmount);
sellToken.approveIfBelow(data.curveAddress, sellAmount);
uint256 beforeBalance = buyToken.balanceOf(address(this));
(bool success, bytes memory resultData) =
data.curveAddress.call(abi.encodeWithSelector(

View File

@ -23,55 +23,60 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "./MixinAdapterAddresses.sol";
interface IDODOHelper {
function querySellQuoteToken(address dodo, uint256 amount) external view returns (uint256);
}
import "../IBridgeAdapter.sol";
interface IDODO {
function sellBaseToken(
uint256 amount,
uint256 minReceiveQuote,
bytes calldata data
)
external
returns (uint256);
function sellBaseToken(uint256 amount, uint256 minReceiveQuote, bytes calldata data) external returns (uint256);
function buyBaseToken(uint256 amount, uint256 maxPayQuote, bytes calldata data) external returns (uint256);
function buyBaseToken(
uint256 amount,
uint256 maxPayQuote,
bytes calldata data
)
external
returns (uint256);
}
contract MixinDodo is
MixinAdapterAddresses
{
interface IDODOHelper {
function querySellQuoteToken(
IDODO dodo,
uint256 amount
)
external
view
returns (uint256);
}
contract MixinDodo {
using LibERC20TokenV06 for IERC20TokenV06;
/// @dev Mainnet address of the `DOODO Helper` contract.
IDODOHelper private immutable DODO_HELPER;
constructor(AdapterAddresses memory addresses)
public
{
DODO_HELPER = IDODOHelper(addresses.dodoHelper);
}
function _tradeDodo(
IERC20TokenV06 /* buyToken */,
IERC20TokenV06 sellToken,
uint256 sellAmount,
bytes memory bridgeData
)
internal
returns (uint256 boughtAmount)
{
(address fromTokenAddress,
address pool,
bool isSellBase) = abi.decode(bridgeData, (address, address, bool));
(IDODOHelper helper, IDODO pool, bool isSellBase) =
abi.decode(bridgeData, (IDODOHelper, IDODO, bool));
// Grant the Dodo pool contract an allowance to sell the first token.
IERC20TokenV06(fromTokenAddress).approveIfBelow(pool, sellAmount);
sellToken.approveIfBelow(address(pool), sellAmount);
if (isSellBase) {
// Sell the Base token directly against the contract
boughtAmount = IDODO(pool).sellBaseToken(
boughtAmount = pool.sellBaseToken(
// amount to sell
sellAmount,
// min receive amount
@ -80,11 +85,11 @@ contract MixinDodo is
);
} else {
// Need to re-calculate the sell quote amount into buyBase
boughtAmount = DODO_HELPER.querySellQuoteToken(
boughtAmount = helper.querySellQuoteToken(
pool,
sellAmount
);
IDODO(pool).buyBaseToken(
pool.buyBaseToken(
// amount to buy
boughtAmount,
// max pay amount

View File

@ -23,7 +23,7 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol";
import "./MixinAdapterAddresses.sol";
import "../IBridgeAdapter.sol";
interface IKyberNetworkProxy {
@ -54,26 +54,24 @@ interface IKyberNetworkProxy {
returns (uint256 boughtAmount);
}
contract MixinKyber is
MixinAdapterAddresses
{
contract MixinKyber {
using LibERC20TokenV06 for IERC20TokenV06;
/// @dev Address indicating the trade is using ETH
address private immutable KYBER_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
IERC20TokenV06 private immutable KYBER_ETH_ADDRESS =
IERC20TokenV06(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE);
/// @dev Mainnet address of the WETH contract.
IEtherTokenV06 private immutable WETH;
/// @dev Mainnet address of the KyberNetworkProxy contract.
IKyberNetworkProxy private immutable KYBER_NETWORK_PROXY;
constructor(AdapterAddresses memory addresses)
constructor(IEtherTokenV06 weth)
public
{
WETH = IEtherTokenV06(addresses.weth);
KYBER_NETWORK_PROXY = IKyberNetworkProxy(addresses.kyberNetworkProxy);
WETH = weth;
}
function _tradeKyber(
IERC20TokenV06 sellToken,
IERC20TokenV06 buyToken,
uint256 sellAmount,
bytes memory bridgeData
@ -81,15 +79,15 @@ contract MixinKyber is
internal
returns (uint256 boughtAmount)
{
(IERC20TokenV06 sellToken, bytes memory hint) =
abi.decode(bridgeData, (IERC20TokenV06, bytes));
(IKyberNetworkProxy kyber, bytes memory hint) =
abi.decode(bridgeData, (IKyberNetworkProxy, bytes));
uint256 payableAmount = 0;
if (sellToken != WETH) {
// If the input token is not WETH, grant an allowance to the exchange
// to spend them.
sellToken.approveIfBelow(
address(KYBER_NETWORK_PROXY),
address(kyber),
sellAmount
);
} else {
@ -100,13 +98,13 @@ contract MixinKyber is
// Try to sell all of this contract's input token balance through
// `KyberNetworkProxy.trade()`.
boughtAmount = KYBER_NETWORK_PROXY.tradeWithHint{ value: payableAmount }(
boughtAmount = kyber.tradeWithHint{ value: payableAmount }(
// Input token.
sellToken == WETH ? IERC20TokenV06(KYBER_ETH_ADDRESS) : sellToken,
sellToken == WETH ? KYBER_ETH_ADDRESS : sellToken,
// Sell amount.
sellAmount,
// Output token.
buyToken == WETH ? IERC20TokenV06(KYBER_ETH_ADDRESS) : buyToken,
buyToken == WETH ? KYBER_ETH_ADDRESS : buyToken,
// Transfer to this contract
address(uint160(address(this))),
// Buy as much as possible.

View File

@ -22,7 +22,7 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "./MixinAdapterAddresses.sol";
import "../IBridgeAdapter.sol";
interface IMStable {
@ -37,21 +37,12 @@ interface IMStable {
returns (uint256 boughtAmount);
}
contract MixinMStable is
MixinAdapterAddresses
{
contract MixinMStable {
using LibERC20TokenV06 for IERC20TokenV06;
/// @dev Mainnet address of the mStable mUSD contract.
IMStable private immutable MSTABLE;
constructor(AdapterAddresses memory addresses)
public
{
MSTABLE = IMStable(addresses.mStable);
}
function _tradeMStable(
IERC20TokenV06 sellToken,
IERC20TokenV06 buyToken,
uint256 sellAmount,
bytes memory bridgeData
@ -59,12 +50,12 @@ contract MixinMStable is
internal
returns (uint256 boughtAmount)
{
// Decode the bridge data to get the `sellToken`.
(IERC20TokenV06 sellToken) = abi.decode(bridgeData, (IERC20TokenV06));
// Grant an allowance to the exchange to spend `sellToken` token.
sellToken.approveIfBelow(address(MSTABLE), sellAmount);
(IMStable mstable) = abi.decode(bridgeData, (IMStable));
boughtAmount = MSTABLE.swap(
// Grant an allowance to the exchange to spend `sellToken` token.
sellToken.approveIfBelow(address(mstable), sellAmount);
boughtAmount = mstable.swap(
sellToken,
buyToken,
sellAmount,

View File

@ -24,7 +24,7 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol";
import "./MixinAdapterAddresses.sol";
import "../IBridgeAdapter.sol";
/// @dev Moooniswap pool interface.
@ -43,22 +43,22 @@ interface IMooniswapPool {
}
/// @dev BridgeAdapter mixin for mooniswap.
contract MixinMooniswap is
MixinAdapterAddresses
{
contract MixinMooniswap {
using LibERC20TokenV06 for IERC20TokenV06;
using LibERC20TokenV06 for IEtherTokenV06;
/// @dev WETH token.
IEtherTokenV06 private immutable WETH;
constructor(AdapterAddresses memory addresses)
constructor(IEtherTokenV06 weth)
public
{
WETH = IEtherTokenV06(addresses.weth);
WETH = weth;
}
function _tradeMooniswap(
IERC20TokenV06 sellToken,
IERC20TokenV06 buyToken,
uint256 sellAmount,
bytes memory bridgeData
@ -66,8 +66,7 @@ contract MixinMooniswap is
internal
returns (uint256 boughtAmount)
{
(IERC20TokenV06 sellToken, IMooniswapPool pool) =
abi.decode(bridgeData, (IERC20TokenV06, IMooniswapPool));
(IMooniswapPool pool) = abi.decode(bridgeData, (IMooniswapPool));
// Convert WETH to ETH.
uint256 ethValue = 0;

View File

@ -22,7 +22,7 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "./MixinAdapterAddresses.sol";
import "../IBridgeAdapter.sol";
interface IOasis {
@ -42,21 +42,12 @@ interface IOasis {
returns (uint256 boughtAmount);
}
contract MixinOasis is
MixinAdapterAddresses
{
contract MixinOasis {
using LibERC20TokenV06 for IERC20TokenV06;
/// @dev Mainnet address of the Oasis `MatchingMarket` contract.
IOasis private immutable OASIS;
constructor(AdapterAddresses memory addresses)
public
{
OASIS = IOasis(addresses.oasis);
}
function _tradeOasis(
IERC20TokenV06 sellToken,
IERC20TokenV06 buyToken,
uint256 sellAmount,
bytes memory bridgeData
@ -64,15 +55,16 @@ contract MixinOasis is
internal
returns (uint256 boughtAmount)
{
// Decode the bridge data to get the `sellToken`.
(IERC20TokenV06 sellToken) = abi.decode(bridgeData, (IERC20TokenV06));
(IOasis oasis) = abi.decode(bridgeData, (IOasis));
// Grant an allowance to the exchange to spend `sellToken` token.
sellToken.approveIfBelow(
address(OASIS),
address(oasis),
sellAmount
);
// Try to sell all of this contract's `sellToken` token balance.
boughtAmount = OASIS.sellAllAmount(
boughtAmount = oasis.sellAllAmount(
sellToken,
sellAmount,
buyToken,

View File

@ -23,13 +23,12 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "./MixinAdapterAddresses.sol";
interface IShell {
function originSwap(
address from,
address to,
IERC20TokenV06 from,
IERC20TokenV06 to,
uint256 fromAmount,
uint256 minTargetAmount,
uint256 deadline
@ -38,12 +37,12 @@ interface IShell {
returns (uint256 toAmount);
}
contract MixinShell is
MixinAdapterAddresses
{
contract MixinShell {
using LibERC20TokenV06 for IERC20TokenV06;
function _tradeShell(
IERC20TokenV06 sellToken,
IERC20TokenV06 buyToken,
uint256 sellAmount,
bytes memory bridgeData
@ -51,17 +50,17 @@ contract MixinShell is
internal
returns (uint256 boughtAmount)
{
(address fromTokenAddress, address pool) = abi.decode(bridgeData, (address, address));
IShell pool = abi.decode(bridgeData, (IShell));
// Grant the Shell contract an allowance to sell the first token.
IERC20TokenV06(fromTokenAddress).approveIfBelow(
pool,
IERC20TokenV06(sellToken).approveIfBelow(
address(pool),
sellAmount
);
boughtAmount = IShell(pool).originSwap(
fromTokenAddress,
address(buyToken),
boughtAmount = pool.originSwap(
sellToken,
buyToken,
// Sell all tokens we hold.
sellAmount,
// Minimum buy amount.

View File

@ -23,23 +23,12 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "./MixinAdapterAddresses.sol";
import "./MixinUniswapV2.sol";
contract MixinSushiswap is
MixinAdapterAddresses
{
contract MixinSushiswap {
using LibERC20TokenV06 for IERC20TokenV06;
/// @dev Mainnet address of the `SushiswapRouter` contract.
IUniswapV2Router02 private immutable SUSHISWAP_ROUTER;
constructor(AdapterAddresses memory addresses)
public
{
SUSHISWAP_ROUTER = IUniswapV2Router02(addresses.sushiswapRouter);
}
function _tradeSushiswap(
IERC20TokenV06 buyToken,
uint256 sellAmount,
@ -48,22 +37,28 @@ contract MixinSushiswap is
internal
returns (uint256 boughtAmount)
{
// solhint-disable indent
address[] memory path = abi.decode(bridgeData, (address[]));
// solhint-enable indent
IERC20TokenV06[] memory path;
IUniswapV2Router02 router;
{
address[] memory _path;
(router, _path) =
abi.decode(bridgeData, (IUniswapV2Router02, address[]));
// To get around `abi.decode()` not supporting interface array types.
assembly { path := _path }
}
require(path.length >= 2, "SushiswapBridge/PATH_LENGTH_MUST_BE_AT_LEAST_TWO");
require(path.length >= 2, "MixinSushiswap/PATH_LENGTH_MUST_BE_AT_LEAST_TWO");
require(
path[path.length - 1] == address(buyToken),
"SushiswapBridge/LAST_ELEMENT_OF_PATH_MUST_MATCH_OUTPUT_TOKEN"
path[path.length - 1] == buyToken,
"MixinSushiswap/LAST_ELEMENT_OF_PATH_MUST_MATCH_OUTPUT_TOKEN"
);
// Grant the Uniswap router an allowance to sell the first token.
IERC20TokenV06(path[0]).approveIfBelow(
address(SUSHISWAP_ROUTER),
path[0].approveIfBelow(
address(router),
sellAmount
);
uint[] memory amounts = SUSHISWAP_ROUTER.swapExactTokensForTokens(
uint[] memory amounts = router.swapExactTokensForTokens(
// Sell all tokens we hold.
sellAmount,
// Minimum buy amount.

View File

@ -23,7 +23,7 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol";
import "./MixinAdapterAddresses.sol";
import "../IBridgeAdapter.sol";
interface IUniswapExchangeFactory {
@ -103,24 +103,21 @@ interface IUniswapExchange {
returns (uint256 tokensBought);
}
contract MixinUniswap is
MixinAdapterAddresses
{
contract MixinUniswap {
using LibERC20TokenV06 for IERC20TokenV06;
/// @dev Mainnet address of the WETH contract.
IEtherTokenV06 private immutable WETH;
/// @dev Mainnet address of the `UniswapExchangeFactory` contract.
IUniswapExchangeFactory private immutable UNISWAP_EXCHANGE_FACTORY;
constructor(AdapterAddresses memory addresses)
constructor(IEtherTokenV06 weth)
public
{
WETH = IEtherTokenV06(addresses.weth);
UNISWAP_EXCHANGE_FACTORY = IUniswapExchangeFactory(addresses.uniswapExchangeFactory);
WETH = weth;
}
function _tradeUniswap(
IERC20TokenV06 sellToken,
IERC20TokenV06 buyToken,
uint256 sellAmount,
bytes memory bridgeData
@ -128,11 +125,12 @@ contract MixinUniswap is
internal
returns (uint256 boughtAmount)
{
// Decode the bridge data to get the `sellToken`.
(IERC20TokenV06 sellToken) = abi.decode(bridgeData, (IERC20TokenV06));
IUniswapExchangeFactory exchangeFactory =
abi.decode(bridgeData, (IUniswapExchangeFactory));
// Get the exchange for the token pair.
IUniswapExchange exchange = _getUniswapExchangeForTokenPair(
exchangeFactory,
sellToken,
buyToken
);
@ -197,10 +195,12 @@ contract MixinUniswap is
/// @dev Retrieves the uniswap exchange for a given token pair.
/// In the case of a WETH-token exchange, this will be the non-WETH token.
/// In th ecase of a token-token exchange, this will be the first token.
/// @param exchangeFactory The exchange factory.
/// @param sellToken The address of the token we are converting from.
/// @param buyToken The address of the token we are converting to.
/// @return exchange The uniswap exchange.
function _getUniswapExchangeForTokenPair(
IUniswapExchangeFactory exchangeFactory,
IERC20TokenV06 sellToken,
IERC20TokenV06 buyToken
)
@ -210,8 +210,8 @@ contract MixinUniswap is
{
// Whichever isn't WETH is the exchange token.
exchange = sellToken == WETH
? UNISWAP_EXCHANGE_FACTORY.getExchange(buyToken)
: UNISWAP_EXCHANGE_FACTORY.getExchange(sellToken);
require(address(exchange) != address(0), "NO_UNISWAP_EXCHANGE_FOR_TOKEN");
? exchangeFactory.getExchange(buyToken)
: exchangeFactory.getExchange(sellToken);
require(address(exchange) != address(0), "MixinUniswap/NO_EXCHANGE");
}
}

View File

@ -23,7 +23,7 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "./MixinAdapterAddresses.sol";
import "../IBridgeAdapter.sol";
/*
UniswapV2
@ -42,26 +42,16 @@ interface IUniswapV2Router02 {
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
IERC20TokenV06[] calldata path,
address to,
uint deadline
) external returns (uint[] memory amounts);
}
contract MixinUniswapV2 is
MixinAdapterAddresses
{
contract MixinUniswapV2 {
using LibERC20TokenV06 for IERC20TokenV06;
/// @dev Mainnet address of the `UniswapV2Router02` contract.
IUniswapV2Router02 private immutable UNISWAP_V2_ROUTER;
constructor(AdapterAddresses memory addresses)
public
{
UNISWAP_V2_ROUTER = IUniswapV2Router02(addresses.uniswapV2Router);
}
function _tradeUniswapV2(
IERC20TokenV06 buyToken,
uint256 sellAmount,
@ -70,22 +60,24 @@ contract MixinUniswapV2 is
internal
returns (uint256 boughtAmount)
{
// solhint-disable indent
address[] memory path = abi.decode(bridgeData, (address[]));
// solhint-enable indent
IUniswapV2Router02 router;
IERC20TokenV06[] memory path;
{
address[] memory _path;
(router, _path) = abi.decode(bridgeData, (IUniswapV2Router02, address[]));
// To get around `abi.decode()` not supporting interface array types.
assembly { path := _path }
}
require(path.length >= 2, "UniswapV2Bridge/PATH_LENGTH_MUST_BE_AT_LEAST_TWO");
require(path.length >= 2, "MixinUniswapV3/PATH_LENGTH_MUST_BE_AT_LEAST_TWO");
require(
path[path.length - 1] == address(buyToken),
"UniswapV2Bridge/LAST_ELEMENT_OF_PATH_MUST_MATCH_OUTPUT_TOKEN"
path[path.length - 1] == buyToken,
"MixinUniswapV2/LAST_ELEMENT_OF_PATH_MUST_MATCH_OUTPUT_TOKEN"
);
// Grant the Uniswap router an allowance to sell the first token.
IERC20TokenV06(path[0]).approveIfBelow(
address(UNISWAP_V2_ROUTER),
sellAmount
);
path[0].approveIfBelow(address(router), sellAmount);
uint[] memory amounts = UNISWAP_V2_ROUTER.swapExactTokensForTokens(
uint[] memory amounts = router.swapExactTokensForTokens(
// Sell all tokens we hold.
sellAmount,
// Minimum buy amount.

View File

@ -31,7 +31,6 @@ contract MixinZeroExBridge {
using LibSafeMathV06 for uint256;
function _tradeZeroExBridge(
address bridgeAddress,
IERC20TokenV06 sellToken,
IERC20TokenV06 buyToken,
uint256 sellAmount,
@ -40,17 +39,19 @@ contract MixinZeroExBridge {
internal
returns (uint256 boughtAmount)
{
(ILiquidityProvider provider, bytes memory lpData) =
abi.decode(bridgeData, (ILiquidityProvider, bytes));
// Trade the good old fashioned way
sellToken.compatTransfer(
bridgeAddress,
address(provider),
sellAmount
);
boughtAmount = ILiquidityProvider(bridgeAddress).sellTokenForToken(
boughtAmount = provider.sellTokenForToken(
address(sellToken),
address(buyToken),
address(this), // recipient
1, // minBuyAmount
bridgeData
lpData
);
}
}

View File

@ -1,113 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
/// @dev Interface to the V3 Exchange.
interface IExchange {
/// @dev V3 Order structure.
struct Order {
// Address that created the order.
address makerAddress;
// Address that is allowed to fill the order.
// If set to 0, any address is allowed to fill the order.
address takerAddress;
// Address that will recieve fees when order is filled.
address feeRecipientAddress;
// Address that is allowed to call Exchange contract methods that affect this order.
// If set to 0, any address is allowed to call these methods.
address senderAddress;
// Amount of makerAsset being offered by maker. Must be greater than 0.
uint256 makerAssetAmount;
// Amount of takerAsset being bid on by maker. Must be greater than 0.
uint256 takerAssetAmount;
// Fee paid to feeRecipient by maker when order is filled.
uint256 makerFee;
// Fee paid to feeRecipient by taker when order is filled.
uint256 takerFee;
// Timestamp in seconds at which order expires.
uint256 expirationTimeSeconds;
// Arbitrary number to facilitate uniqueness of the order's hash.
uint256 salt;
// Encoded data that can be decoded by a specified proxy contract when transferring makerAsset.
// The leading bytes4 references the id of the asset proxy.
bytes makerAssetData;
// Encoded data that can be decoded by a specified proxy contract when transferring takerAsset.
// The leading bytes4 references the id of the asset proxy.
bytes takerAssetData;
// Encoded data that can be decoded by a specified proxy contract when transferring makerFeeAsset.
// The leading bytes4 references the id of the asset proxy.
bytes makerFeeAssetData;
// Encoded data that can be decoded by a specified proxy contract when transferring takerFeeAsset.
// The leading bytes4 references the id of the asset proxy.
bytes takerFeeAssetData;
}
/// @dev V3 `fillOrder()` results.`
struct FillResults {
// Total amount of makerAsset(s) filled.
uint256 makerAssetFilledAmount;
// Total amount of takerAsset(s) filled.
uint256 takerAssetFilledAmount;
// Total amount of fees paid by maker(s) to feeRecipient(s).
uint256 makerFeePaid;
// Total amount of fees paid by taker to feeRecipients(s).
uint256 takerFeePaid;
// Total amount of fees paid by taker to the staking contract.
uint256 protocolFeePaid;
}
/// @dev Fills the input order.
/// @param order Order struct containing order specifications.
/// @param takerAssetFillAmount Desired amount of takerAsset to sell.
/// @param signature Proof that order has been created by maker.
/// @return fillResults Amounts filled and fees paid by maker and taker.
function fillOrder(
Order calldata order,
uint256 takerAssetFillAmount,
bytes calldata signature
)
external
payable
returns (FillResults memory fillResults);
/// @dev Returns the protocolFeeMultiplier
/// @return multiplier The multiplier for protocol fees.
function protocolFeeMultiplier()
external
view
returns (uint256 multiplier);
/// @dev Gets an asset proxy.
/// @param assetProxyId Id of the asset proxy.
/// @return proxyAddress The asset proxy registered to assetProxyId.
/// Returns 0x0 if no proxy is registered.
function getAssetProxy(bytes4 assetProxyId)
external
view
returns (address proxyAddress);
function EIP712_EXCHANGE_DOMAIN_HASH()
external
view
returns (bytes32 domainHash);
}

View File

@ -1,38 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.6.5;
interface IGasToken {
/// @dev Frees up to `value` sub-tokens
/// @param value The amount of tokens to free
/// @return freed How many tokens were freed
function freeUpTo(uint256 value) external returns (uint256 freed);
/// @dev Frees up to `value` sub-tokens owned by `from`
/// @param from The owner of tokens to spend
/// @param value The amount of tokens to free
/// @return freed How many tokens were freed
function freeFromUpTo(address from, uint256 value) external returns (uint256 freed);
/// @dev Mints `value` amount of tokens
/// @param value The amount of tokens to mint
function mint(uint256 value) external;
}

View File

@ -1,168 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.6.5;
import "./IExchange.sol";
library LibOrderHash {
using LibOrderHash for IExchange.Order;
// Hash for the EIP712 Order Schema:
// keccak256(abi.encodePacked(
// "Order(",
// "address makerAddress,",
// "address takerAddress,",
// "address feeRecipientAddress,",
// "address senderAddress,",
// "uint256 makerAssetAmount,",
// "uint256 takerAssetAmount,",
// "uint256 makerFee,",
// "uint256 takerFee,",
// "uint256 expirationTimeSeconds,",
// "uint256 salt,",
// "bytes makerAssetData,",
// "bytes takerAssetData,",
// "bytes makerFeeAssetData,",
// "bytes takerFeeAssetData",
// ")"
// ))
bytes32 constant internal _EIP712_ORDER_SCHEMA_HASH =
0xf80322eb8376aafb64eadf8f0d7623f22130fd9491a221e902b713cb984a7534;
/// @dev Calculates the EIP712 typed data hash of an order with a given domain separator.
/// @param order The order structure.
/// @param eip712ExchangeDomainHash Domain hash for the Exchange.
/// @return orderHash EIP712 typed data hash of the order.
function getTypedDataHash(IExchange.Order memory order, bytes32 eip712ExchangeDomainHash)
internal
pure
returns (bytes32 orderHash)
{
orderHash = _hashEIP712Message(
eip712ExchangeDomainHash,
order.getStructHash()
);
return orderHash;
}
/// @dev Calculates EIP712 hash of the order struct.
/// @param order The order structure.
/// @return result EIP712 hash of the order struct.
function getStructHash(IExchange.Order memory order)
internal
pure
returns (bytes32 result)
{
bytes32 schemaHash = _EIP712_ORDER_SCHEMA_HASH;
bytes memory makerAssetData = order.makerAssetData;
bytes memory takerAssetData = order.takerAssetData;
bytes memory makerFeeAssetData = order.makerFeeAssetData;
bytes memory takerFeeAssetData = order.takerFeeAssetData;
// Assembly for more efficiently computing:
// keccak256(abi.encodePacked(
// EIP712_ORDER_SCHEMA_HASH,
// uint256(order.makerAddress),
// uint256(order.takerAddress),
// uint256(order.feeRecipientAddress),
// uint256(order.senderAddress),
// order.makerAssetAmount,
// order.takerAssetAmount,
// order.makerFee,
// order.takerFee,
// order.expirationTimeSeconds,
// order.salt,
// keccak256(order.makerAssetData),
// keccak256(order.takerAssetData),
// keccak256(order.makerFeeAssetData),
// keccak256(order.takerFeeAssetData)
// ));
assembly {
// Assert order offset (this is an internal error that should never be triggered)
if lt(order, 32) {
invalid()
}
// Calculate memory addresses that will be swapped out before hashing
let pos1 := sub(order, 32)
let pos2 := add(order, 320)
let pos3 := add(order, 352)
let pos4 := add(order, 384)
let pos5 := add(order, 416)
// Backup
let temp1 := mload(pos1)
let temp2 := mload(pos2)
let temp3 := mload(pos3)
let temp4 := mload(pos4)
let temp5 := mload(pos5)
// Hash in place
mstore(pos1, schemaHash)
mstore(pos2, keccak256(add(makerAssetData, 32), mload(makerAssetData))) // store hash of makerAssetData
mstore(pos3, keccak256(add(takerAssetData, 32), mload(takerAssetData))) // store hash of takerAssetData
mstore(pos4, keccak256(add(makerFeeAssetData, 32), mload(makerFeeAssetData))) // store hash of makerFeeAssetData
mstore(pos5, keccak256(add(takerFeeAssetData, 32), mload(takerFeeAssetData))) // store hash of takerFeeAssetData
result := keccak256(pos1, 480)
// Restore
mstore(pos1, temp1)
mstore(pos2, temp2)
mstore(pos3, temp3)
mstore(pos4, temp4)
mstore(pos5, temp5)
}
return result;
}
/// @dev Calculates EIP712 encoding for a hash struct with a given domain hash.
/// @param eip712DomainHash Hash of the domain domain separator data, computed
/// with getDomainHash().
/// @param hashStruct The EIP712 hash struct.
/// @return result EIP712 hash applied to the given EIP712 Domain.
function _hashEIP712Message(bytes32 eip712DomainHash, bytes32 hashStruct)
internal
pure
returns (bytes32 result)
{
// Assembly for more efficient computing:
// keccak256(abi.encodePacked(
// EIP191_HEADER,
// EIP712_DOMAIN_HASH,
// hashStruct
// ));
assembly {
// Load free memory pointer
let memPtr := mload(64)
mstore(memPtr, 0x1901000000000000000000000000000000000000000000000000000000000000) // EIP191 header
mstore(add(memPtr, 2), eip712DomainHash) // EIP712 domain hash
mstore(add(memPtr, 34), hashStruct) // Hash of struct
// Compute hash
result := keccak256(memPtr, 66)
}
return result;
}
}

View File

@ -27,39 +27,25 @@ import "./TestMintableERC20Token.sol";
contract TestFillQuoteTransformerBridge {
struct FillBehavior {
// Scaling for maker assets minted, in 1e18.
uint256 makerAssetMintRatio;
uint256 amount;
}
uint256 private constant REVERT_AMOUNT = 0xdeadbeef;
function sellTokenForToken(
address takerToken,
address /* takerToken */,
address makerToken,
address recipient,
uint256 minBuyAmount,
uint256 /* minBuyAmount */,
bytes calldata auxiliaryData
)
external
returns (uint256 boughtAmount)
{
FillBehavior memory behavior = abi.decode(auxiliaryData, (FillBehavior));
boughtAmount = LibMathV06.getPartialAmountFloor(
behavior.makerAssetMintRatio,
1e18,
behavior.amount
);
boughtAmount = abi.decode(auxiliaryData, (uint256));
if (REVERT_AMOUNT == boughtAmount) {
revert("REVERT_AMOUNT");
}
TestMintableERC20Token(makerToken).mint(
recipient,
boughtAmount
);
}
function encodeBehaviorData(FillBehavior calldata behavior)
external
pure
returns (bytes memory encoded)
{
return abi.encode(behavior);
}
}

View File

@ -23,106 +23,109 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/v06/LibBytesV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibMathV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol";
import "../src/vendor/v3/IExchange.sol";
import "./TestMintableERC20Token.sol";
import "../src/features/libs/LibNativeOrder.sol";
import "../src/features/libs/LibSignature.sol";
contract TestFillQuoteTransformerExchange {
struct FillBehavior {
// How much of the order is filled, in taker asset amount.
uint256 filledTakerAssetAmount;
// Scaling for maker assets minted, in 1e18.
uint256 makerAssetMintRatio;
}
bytes32 public constant EIP712_EXCHANGE_DOMAIN_HASH = 0xaa81d881b1adbbf115e15b849cb9cdc643cad3c6a90f30eb505954af943247e6;
uint256 private constant REVERT_AMOUNT = 0xdeadbeef;
uint256 private constant PROTOCOL_FEE_MULTIPLIER = 1337;
using LibSafeMathV06 for uint256;
function fillOrder(
IExchange.Order calldata order,
uint256 takerAssetFillAmount,
bytes calldata signature
function fillLimitOrder(
LibNativeOrder.LimitOrder calldata order,
LibSignature.Signature calldata signature,
uint128 takerTokenFillAmount
)
external
payable
returns (IExchange.FillResults memory fillResults)
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
require(
signature.length != 0,
"TestFillQuoteTransformerExchange/INVALID_SIGNATURE"
);
// The signature is the ABI-encoded FillBehavior data.
FillBehavior memory behavior = abi.decode(signature, (FillBehavior));
// The r field of the signature is the pre-filled amount.
uint128 takerTokenPreFilledAmount = uint128(uint256(signature.r));
if (REVERT_AMOUNT == takerTokenPreFilledAmount) {
revert("REVERT_AMOUNT");
}
if (takerTokenPreFilledAmount >= order.takerAmount) {
revert('FILLED');
}
uint256 protocolFee = PROTOCOL_FEE_MULTIPLIER * tx.gasprice;
require(
msg.value == protocolFee,
"TestFillQuoteTransformerExchange/INSUFFICIENT_PROTOCOL_FEE"
);
// Return excess protocol fee.
msg.sender.transfer(msg.value - protocolFee);
takerTokenFilledAmount = LibSafeMathV06.min128(
order.takerAmount - takerTokenPreFilledAmount,
takerTokenFillAmount
);
// Take taker tokens.
TestMintableERC20Token takerToken = _getTokenFromAssetData(order.takerAssetData);
takerAssetFillAmount = LibSafeMathV06.min256(
order.takerAssetAmount.safeSub(behavior.filledTakerAssetAmount),
takerAssetFillAmount
order.takerToken.transferFrom(
msg.sender,
order.maker,
takerTokenFilledAmount
);
require(
takerToken.getSpendableAmount(msg.sender, address(this)) >= takerAssetFillAmount,
"TestFillQuoteTransformerExchange/INSUFFICIENT_TAKER_FUNDS"
);
takerToken.transferFrom(msg.sender, order.makerAddress, takerAssetFillAmount);
// Mint maker tokens.
uint256 makerAssetFilledAmount = LibMathV06.getPartialAmountFloor(
takerAssetFillAmount,
order.takerAssetAmount,
order.makerAssetAmount
);
TestMintableERC20Token makerToken = _getTokenFromAssetData(order.makerAssetData);
makerToken.mint(
msg.sender,
LibMathV06.getPartialAmountFloor(
behavior.makerAssetMintRatio,
1e18,
makerAssetFilledAmount
)
makerTokenFilledAmount = LibSafeMathV06.safeDowncastToUint128(
uint256(takerTokenFilledAmount)
* uint256(order.makerAmount)
/ uint256(order.takerAmount)
);
TestMintableERC20Token(address(order.makerToken))
.mint(msg.sender, makerTokenFilledAmount);
// Take taker fee.
TestMintableERC20Token takerFeeToken = _getTokenFromAssetData(order.takerFeeAssetData);
uint256 takerFee = LibMathV06.getPartialAmountFloor(
takerAssetFillAmount,
order.takerAssetAmount,
order.takerFee
// Take taker token fee.
uint128 takerFee = LibSafeMathV06.safeDowncastToUint128(
uint256(takerTokenFilledAmount)
* uint256(order.takerTokenFeeAmount)
/ uint256(order.takerAmount)
);
require(
takerFeeToken.getSpendableAmount(msg.sender, address(this)) >= takerFee,
"TestFillQuoteTransformerExchange/INSUFFICIENT_TAKER_FEE_FUNDS"
);
takerFeeToken.transferFrom(msg.sender, order.feeRecipientAddress, takerFee);
fillResults.makerAssetFilledAmount = makerAssetFilledAmount;
fillResults.takerAssetFilledAmount = takerAssetFillAmount;
fillResults.makerFeePaid = uint256(-1);
fillResults.takerFeePaid = takerFee;
fillResults.protocolFeePaid = protocolFee;
order.takerToken.transferFrom(msg.sender, order.feeRecipient, takerFee);
}
function encodeBehaviorData(FillBehavior calldata behavior)
function fillRfqOrder(
LibNativeOrder.RfqOrder calldata order,
LibSignature.Signature calldata signature,
uint128 takerTokenFillAmount
)
external
pure
returns (bytes memory encoded)
payable
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{
return abi.encode(behavior);
// The r field of the signature is the pre-filled amount.
uint128 takerTokenPreFilledAmount = uint128(uint256(signature.r));
if (REVERT_AMOUNT == takerTokenPreFilledAmount) {
revert("REVERT_AMOUNT");
}
if (takerTokenPreFilledAmount >= order.takerAmount) {
revert('FILLED');
}
takerTokenFilledAmount = LibSafeMathV06.min128(
order.takerAmount - takerTokenPreFilledAmount,
takerTokenFillAmount
);
// Take taker tokens.
order.takerToken.transferFrom(
msg.sender,
order.maker,
takerTokenFilledAmount
);
// Mint maker tokens.
makerTokenFilledAmount = LibSafeMathV06.safeDowncastToUint128(
uint256(takerTokenFilledAmount)
* uint256(order.makerAmount)
/ uint256(order.takerAmount)
);
TestMintableERC20Token(address(order.makerToken))
.mint(msg.sender, makerTokenFilledAmount);
}
function protocolFeeMultiplier()
function getProtocolFeeMultiplier()
external
pure
returns (uint256)
@ -130,19 +133,11 @@ contract TestFillQuoteTransformerExchange {
return PROTOCOL_FEE_MULTIPLIER;
}
function getAssetProxy(bytes4)
function getLimitOrderHash(LibNativeOrder.LimitOrder calldata order)
external
view
returns (address)
{
return address(this);
}
function _getTokenFromAssetData(bytes memory assetData)
private
pure
returns (TestMintableERC20Token token)
returns (bytes32)
{
return TestMintableERC20Token(LibBytesV06.readAddress(assetData, 16));
return bytes32(order.salt);
}
}

View File

@ -65,14 +65,13 @@ contract TestLiquidityProvider {
/// @param outputToken The token being bought.
/// @param recipient The recipient of the bought tokens.
/// @param minBuyAmount The minimum acceptable amount of `outputToken` to buy.
/// @param auxiliaryData Arbitrary auxiliary data supplied to the contract.
/// @return boughtAmount The amount of `outputToken` bought.
function sellTokenForToken(
address inputToken,
address outputToken,
address recipient,
uint256 minBuyAmount,
bytes calldata auxiliaryData
bytes calldata // auxiliaryData
)
external
returns (uint256 boughtAmount)
@ -91,13 +90,12 @@ contract TestLiquidityProvider {
/// @param outputToken The token being bought.
/// @param recipient The recipient of the bought tokens.
/// @param minBuyAmount The minimum acceptable amount of `outputToken` to buy.
/// @param auxiliaryData Arbitrary auxiliary data supplied to the contract.
/// @return boughtAmount The amount of `outputToken` bought.
function sellEthForToken(
address outputToken,
address recipient,
uint256 minBuyAmount,
bytes calldata auxiliaryData
bytes calldata // auxiliaryData
)
external
returns (uint256 boughtAmount)
@ -115,13 +113,12 @@ contract TestLiquidityProvider {
/// @param inputToken The token being sold.
/// @param recipient The recipient of the bought tokens.
/// @param minBuyAmount The minimum acceptable amount of ETH to buy.
/// @param auxiliaryData Arbitrary auxiliary data supplied to the contract.
/// @return boughtAmount The amount of ETH bought.
function sellTokenForEth(
address inputToken,
address payable recipient,
uint256 minBuyAmount,
bytes calldata auxiliaryData
bytes calldata // auxiliaryData
)
external
returns (uint256 boughtAmount)

View File

@ -43,7 +43,7 @@
"config": {
"publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature,ILiquidityProviderFeature,NativeOrdersFeature,INativeOrdersFeature,FeeCollectorController,FeeCollector",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|INativeOrdersFeature|IOwnableFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOrderHash|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinBancor|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinDodo|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinShell|MixinSushiswap|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|NativeOrdersFeature|OwnableFeature|PayTakerTransformer|PermissionlessTransformerDeployer|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestDelegateCaller|TestFeeCollectorController|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestNativeOrdersFeature|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx|ZeroExOptimized).json"
"abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|BridgeSource|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|INativeOrdersFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBancor|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinDodo|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinShell|MixinSushiswap|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|NativeOrdersFeature|OwnableFeature|PayTakerTransformer|PermissionlessTransformerDeployer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestDelegateCaller|TestFeeCollectorController|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestNativeOrdersFeature|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx|ZeroExOptimized).json"
},
"repository": {
"type": "git",

View File

@ -9,6 +9,7 @@ import * as AffiliateFeeTransformer from '../test/generated-artifacts/AffiliateF
import * as AllowanceTarget from '../test/generated-artifacts/AllowanceTarget.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';
import * as FeeCollector from '../test/generated-artifacts/FeeCollector.json';
import * as FeeCollectorController from '../test/generated-artifacts/FeeCollectorController.json';
import * as FillQuoteTransformer from '../test/generated-artifacts/FillQuoteTransformer.json';
@ -24,10 +25,8 @@ import * as IBootstrapFeature from '../test/generated-artifacts/IBootstrapFeatur
import * as IBridgeAdapter from '../test/generated-artifacts/IBridgeAdapter.json';
import * as IERC20Bridge from '../test/generated-artifacts/IERC20Bridge.json';
import * as IERC20Transformer from '../test/generated-artifacts/IERC20Transformer.json';
import * as IExchange from '../test/generated-artifacts/IExchange.json';
import * as IFeature from '../test/generated-artifacts/IFeature.json';
import * as IFlashWallet from '../test/generated-artifacts/IFlashWallet.json';
import * as IGasToken from '../test/generated-artifacts/IGasToken.json';
import * as ILiquidityProvider from '../test/generated-artifacts/ILiquidityProvider.json';
import * as ILiquidityProviderFeature from '../test/generated-artifacts/ILiquidityProviderFeature.json';
import * as ILiquidityProviderSandbox from '../test/generated-artifacts/ILiquidityProviderSandbox.json';
@ -53,7 +52,6 @@ import * as LibMigrate from '../test/generated-artifacts/LibMigrate.json';
import * as LibNativeOrder from '../test/generated-artifacts/LibNativeOrder.json';
import * as LibNativeOrdersRichErrors from '../test/generated-artifacts/LibNativeOrdersRichErrors.json';
import * as LibNativeOrdersStorage from '../test/generated-artifacts/LibNativeOrdersStorage.json';
import * as LibOrderHash from '../test/generated-artifacts/LibOrderHash.json';
import * as LibOwnableRichErrors from '../test/generated-artifacts/LibOwnableRichErrors.json';
import * as LibOwnableStorage from '../test/generated-artifacts/LibOwnableStorage.json';
import * as LibProxyRichErrors from '../test/generated-artifacts/LibProxyRichErrors.json';
@ -73,7 +71,6 @@ import * as LiquidityProviderFeature from '../test/generated-artifacts/Liquidity
import * as LiquidityProviderSandbox from '../test/generated-artifacts/LiquidityProviderSandbox.json';
import * as LogMetadataTransformer from '../test/generated-artifacts/LogMetadataTransformer.json';
import * as MetaTransactionsFeature from '../test/generated-artifacts/MetaTransactionsFeature.json';
import * as MixinAdapterAddresses from '../test/generated-artifacts/MixinAdapterAddresses.json';
import * as MixinBalancer from '../test/generated-artifacts/MixinBalancer.json';
import * as MixinBancor from '../test/generated-artifacts/MixinBancor.json';
import * as MixinCoFiX from '../test/generated-artifacts/MixinCoFiX.json';
@ -211,8 +208,8 @@ export const artifacts = {
Transformer: Transformer as ContractArtifact,
WethTransformer: WethTransformer as ContractArtifact,
BridgeAdapter: BridgeAdapter as ContractArtifact,
BridgeSource: BridgeSource as ContractArtifact,
IBridgeAdapter: IBridgeAdapter as ContractArtifact,
MixinAdapterAddresses: MixinAdapterAddresses as ContractArtifact,
MixinBalancer: MixinBalancer as ContractArtifact,
MixinBancor: MixinBancor as ContractArtifact,
MixinCoFiX: MixinCoFiX as ContractArtifact,
@ -230,10 +227,7 @@ export const artifacts = {
MixinZeroExBridge: MixinZeroExBridge as ContractArtifact,
ILiquidityProvider: ILiquidityProvider as ContractArtifact,
IERC20Bridge: IERC20Bridge as ContractArtifact,
IExchange: IExchange as ContractArtifact,
IGasToken: IGasToken as ContractArtifact,
IStaking: IStaking as ContractArtifact,
LibOrderHash: LibOrderHash as ContractArtifact,
ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact,
TestBridge: TestBridge as ContractArtifact,
TestCallTarget: TestCallTarget as ContractArtifact,

View File

@ -7,6 +7,7 @@ export * from '../test/generated-wrappers/affiliate_fee_transformer';
export * from '../test/generated-wrappers/allowance_target';
export * from '../test/generated-wrappers/bootstrap_feature';
export * from '../test/generated-wrappers/bridge_adapter';
export * from '../test/generated-wrappers/bridge_source';
export * from '../test/generated-wrappers/fee_collector';
export * from '../test/generated-wrappers/fee_collector_controller';
export * from '../test/generated-wrappers/fill_quote_transformer';
@ -22,10 +23,8 @@ export * from '../test/generated-wrappers/i_bootstrap_feature';
export * from '../test/generated-wrappers/i_bridge_adapter';
export * from '../test/generated-wrappers/i_erc20_bridge';
export * from '../test/generated-wrappers/i_erc20_transformer';
export * from '../test/generated-wrappers/i_exchange';
export * from '../test/generated-wrappers/i_feature';
export * from '../test/generated-wrappers/i_flash_wallet';
export * from '../test/generated-wrappers/i_gas_token';
export * from '../test/generated-wrappers/i_liquidity_provider';
export * from '../test/generated-wrappers/i_liquidity_provider_feature';
export * from '../test/generated-wrappers/i_liquidity_provider_sandbox';
@ -51,7 +50,6 @@ export * from '../test/generated-wrappers/lib_migrate';
export * from '../test/generated-wrappers/lib_native_order';
export * from '../test/generated-wrappers/lib_native_orders_rich_errors';
export * from '../test/generated-wrappers/lib_native_orders_storage';
export * from '../test/generated-wrappers/lib_order_hash';
export * from '../test/generated-wrappers/lib_ownable_rich_errors';
export * from '../test/generated-wrappers/lib_ownable_storage';
export * from '../test/generated-wrappers/lib_proxy_rich_errors';
@ -71,7 +69,6 @@ export * from '../test/generated-wrappers/liquidity_provider_feature';
export * from '../test/generated-wrappers/liquidity_provider_sandbox';
export * from '../test/generated-wrappers/log_metadata_transformer';
export * from '../test/generated-wrappers/meta_transactions_feature';
export * from '../test/generated-wrappers/mixin_adapter_addresses';
export * from '../test/generated-wrappers/mixin_balancer';
export * from '../test/generated-wrappers/mixin_bancor';
export * from '../test/generated-wrappers/mixin_co_fi_x';

View File

@ -35,6 +35,7 @@
"test/generated-artifacts/AllowanceTarget.json",
"test/generated-artifacts/BootstrapFeature.json",
"test/generated-artifacts/BridgeAdapter.json",
"test/generated-artifacts/BridgeSource.json",
"test/generated-artifacts/FeeCollector.json",
"test/generated-artifacts/FeeCollectorController.json",
"test/generated-artifacts/FillQuoteTransformer.json",
@ -50,10 +51,8 @@
"test/generated-artifacts/IBridgeAdapter.json",
"test/generated-artifacts/IERC20Bridge.json",
"test/generated-artifacts/IERC20Transformer.json",
"test/generated-artifacts/IExchange.json",
"test/generated-artifacts/IFeature.json",
"test/generated-artifacts/IFlashWallet.json",
"test/generated-artifacts/IGasToken.json",
"test/generated-artifacts/ILiquidityProvider.json",
"test/generated-artifacts/ILiquidityProviderFeature.json",
"test/generated-artifacts/ILiquidityProviderSandbox.json",
@ -79,7 +78,6 @@
"test/generated-artifacts/LibNativeOrder.json",
"test/generated-artifacts/LibNativeOrdersRichErrors.json",
"test/generated-artifacts/LibNativeOrdersStorage.json",
"test/generated-artifacts/LibOrderHash.json",
"test/generated-artifacts/LibOwnableRichErrors.json",
"test/generated-artifacts/LibOwnableStorage.json",
"test/generated-artifacts/LibProxyRichErrors.json",
@ -99,7 +97,6 @@
"test/generated-artifacts/LiquidityProviderSandbox.json",
"test/generated-artifacts/LogMetadataTransformer.json",
"test/generated-artifacts/MetaTransactionsFeature.json",
"test/generated-artifacts/MixinAdapterAddresses.json",
"test/generated-artifacts/MixinBalancer.json",
"test/generated-artifacts/MixinBancor.json",
"test/generated-artifacts/MixinCoFiX.json",

View File

@ -1,4 +1,29 @@
[
{
"version": "6.0.0",
"changes": [
{
"note": "Pull top 250 Balancer pairs on initialization",
"pr": 113
},
{
"note": "Support v4 `RFQ` and `Limit` orders",
"pr": 113
},
{
"note": "Refactor to consume latest `FillQuoteTransformer`",
"pr": 113
},
{
"note": "Enable `fillData` for all sources, no longer optional",
"pr": 113
},
{
"note": "Support `tx.origin` in RFQT quote requestor",
"pr": 113
}
]
},
{
"version": "5.8.2",
"changes": [

View File

@ -58,6 +58,39 @@ contract BalanceChecker {
return addrBalances;
}
/*
Check the token balances of wallet-token pairs with a spender contract for an allowance check.
Pass 0xeee... as a "token" address to get ETH balance.
Possible error throws:
- extremely large arrays for user and or tokens (gas cost too high)
Returns a one-dimensional that's user.length long. It is the lesser of balance and allowance
*/
function getMinOfBalancesOrAllowances(address[] calldata users, address[] calldata tokens, address spender) external view returns (uint256[] memory) {
// make sure the users array and tokens array are of equal length
require(users.length == tokens.length, "users array is a different length than the tokens array");
uint256[] memory addrBalances = new uint256[](users.length);
for(uint i = 0; i < users.length; i++) {
if (tokens[i] != address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) {
uint256 balance;
uint256 allowance;
balance = IToken(tokens[i]).balanceOf(users[i]);
allowance = IToken(tokens[i]).allowance(users[i], spender);
if (allowance < balance) {
addrBalances[i] = allowance;
} else {
addrBalances[i] = balance;
}
} else {
addrBalances[i] = users[i].balance; // ETH balance
}
}
return addrBalances;
}
/*
Check the allowances of an array of owner-spender-tokens

View File

@ -36,6 +36,7 @@ import "./SushiSwapSampler.sol";
import "./TwoHopSampler.sol";
import "./UniswapSampler.sol";
import "./UniswapV2Sampler.sol";
import "./UtilitySampler.sol";
contract ERC20BridgeSampler is
@ -54,7 +55,8 @@ contract ERC20BridgeSampler is
SushiSwapSampler,
TwoHopSampler,
UniswapSampler,
UniswapV2Sampler
UniswapV2Sampler,
UtilitySampler
{
struct CallResults {

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2020 ZeroEx Intl.
Copyright 2021 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -28,126 +28,118 @@ import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol";
interface IExchange {
/// @dev V3 Order structure.
struct Order {
// Address that created the order.
address makerAddress;
// Address that is allowed to fill the order.
// If set to 0, any address is allowed to fill the order.
address takerAddress;
// Address that will recieve fees when order is filled.
address feeRecipientAddress;
// Address that is allowed to call Exchange contract methods that affect this order.
// If set to 0, any address is allowed to call these methods.
address senderAddress;
// Amount of makerAsset being offered by maker. Must be greater than 0.
uint256 makerAssetAmount;
// Amount of takerAsset being bid on by maker. Must be greater than 0.
uint256 takerAssetAmount;
// Fee paid to feeRecipient by maker when order is filled.
uint256 makerFee;
// Fee paid to feeRecipient by taker when order is filled.
uint256 takerFee;
// Timestamp in seconds at which order expires.
uint256 expirationTimeSeconds;
// Arbitrary number to facilitate uniqueness of the order's hash.
uint256 salt;
// Encoded data that can be decoded by a specified proxy contract when transferring makerAsset.
// The leading bytes4 references the id of the asset proxy.
bytes makerAssetData;
// Encoded data that can be decoded by a specified proxy contract when transferring takerAsset.
// The leading bytes4 references the id of the asset proxy.
bytes takerAssetData;
// Encoded data that can be decoded by a specified proxy contract when transferring makerFeeAsset.
// The leading bytes4 references the id of the asset proxy.
bytes makerFeeAssetData;
// Encoded data that can be decoded by a specified proxy contract when transferring takerFeeAsset.
// The leading bytes4 references the id of the asset proxy.
bytes takerFeeAssetData;
}
// A valid order remains fillable until it is expired, fully filled, or cancelled.
// An order's status is unaffected by external factors, like account balances.
enum OrderStatus {
INVALID, // Default value
INVALID_MAKER_ASSET_AMOUNT, // Order does not have a valid maker asset amount
INVALID_TAKER_ASSET_AMOUNT, // Order does not have a valid taker asset amount
FILLABLE, // Order is fillable
EXPIRED, // Order has already expired
FULLY_FILLED, // Order is fully filled
CANCELLED // Order has been cancelled
INVALID,
FILLABLE,
FILLED,
CANCELLED,
EXPIRED
}
/// @dev Order information returned by `getOrderInfo()`.
/// @dev A standard OTC or OO limit order.
struct LimitOrder {
IERC20TokenV06 makerToken;
IERC20TokenV06 takerToken;
uint128 makerAmount;
uint128 takerAmount;
uint128 takerTokenFeeAmount;
address maker;
address taker;
address sender;
address feeRecipient;
bytes32 pool;
uint64 expiry;
uint256 salt;
}
/// @dev An RFQ limit order.
struct RfqOrder {
IERC20TokenV06 makerToken;
IERC20TokenV06 takerToken;
uint128 makerAmount;
uint128 takerAmount;
address maker;
address taker;
address txOrigin;
bytes32 pool;
uint64 expiry;
uint256 salt;
}
/// @dev Info on a limit or RFQ order.
struct OrderInfo {
OrderStatus orderStatus; // Status that describes order's validity and fillability.
bytes32 orderHash; // EIP712 typed data hash of the order (see LibOrder.getTypedDataHash).
uint256 orderTakerAssetFilledAmount; // Amount of order that has already been filled.
bytes32 orderHash;
OrderStatus status;
uint128 takerTokenFilledAmount;
}
/// @dev Gets information about an order: status, hash, and amount filled.
/// @param order Order to gather information on.
/// @return orderInfo Information about the order and its state.
function getOrderInfo(IExchange.Order calldata order)
/// @dev Allowed signature types.
enum SignatureType {
ILLEGAL,
INVALID,
EIP712,
ETHSIGN
}
/// @dev Encoded EC signature.
struct Signature {
// How to validate the signature.
SignatureType signatureType;
// EC Signature data.
uint8 v;
// EC Signature data.
bytes32 r;
// EC Signature data.
bytes32 s;
}
/// @dev Get the order info for a limit order.
/// @param order The limit order.
/// @return orderInfo Info about the order.
function getLimitOrderInfo(LimitOrder memory order)
external
view
returns (IExchange.OrderInfo memory orderInfo);
returns (OrderInfo memory orderInfo);
/// @dev Verifies that a hash has been signed by the given signer.
/// @param hash Any 32-byte hash.
/// @param signature Proof that the hash has been signed by signer.
/// @return isValid `true` if the signature is valid for the given hash and signer.
function isValidHashSignature(
bytes32 hash,
address signerAddress,
bytes calldata signature
/// @dev Get order info, fillable amount, and signature validity for a limit order.
/// Fillable amount is determined using balances and allowances of the maker.
/// @param order The limit order.
/// @param signature The order signature.
/// @return orderInfo Info about the order.
/// @return actualFillableTakerTokenAmount How much of the order is fillable
/// based on maker funds, in taker tokens.
/// @return isSignatureValid Whether the signature is valid.
function getLimitOrderRelevantState(
LimitOrder memory order,
Signature calldata signature
)
external
view
returns (bool isValid);
/// @dev Gets an asset proxy.
/// @param assetProxyId Id of the asset proxy.
/// @return The asset proxy registered to assetProxyId. Returns 0x0 if no proxy is registered.
function getAssetProxy(bytes4 assetProxyId)
external
view
returns (address);
returns (
OrderInfo memory orderInfo,
uint128 actualFillableTakerTokenAmount,
bool isSignatureValid
);
}
contract NativeOrderSampler {
using LibSafeMathV06 for uint256;
using LibBytesV06 for bytes;
/// @dev The Exchange ERC20Proxy ID.
bytes4 private constant ERC20_ASSET_PROXY_ID = 0xf47261b0;
/// @dev Gas limit for calls to `getOrderFillableTakerAmount()`.
uint256 constant internal DEFAULT_CALL_GAS = 200e3; // 200k
function getTokenDecimals(
address makerTokenAddress,
address takerTokenAddress
)
public
view
returns (uint256, uint256)
{
uint256 fromTokenDecimals = LibERC20TokenV06.compatDecimals(IERC20TokenV06(makerTokenAddress));
uint256 toTokenDecimals = LibERC20TokenV06.compatDecimals(IERC20TokenV06(takerTokenAddress));
return (fromTokenDecimals, toTokenDecimals);
}
/// @dev Queries the fillable taker asset amounts of native orders.
/// Effectively ignores orders that have empty signatures or
/// maker/taker asset amounts (returning 0).
/// @param orders Native orders to query.
/// @param orders Native limit orders to query.
/// @param orderSignatures Signatures for each respective order in `orders`.
/// @param exchange The V3 exchange.
/// @param exchange The V4 exchange.
/// @return orderFillableTakerAssetAmounts How much taker asset can be filled
/// by each order in `orders`.
function getOrderFillableTakerAssetAmounts(
IExchange.Order[] memory orders,
bytes[] memory orderSignatures,
function getLimitOrderFillableTakerAssetAmounts(
IExchange.LimitOrder[] memory orders,
IExchange.Signature[] memory orderSignatures,
IExchange exchange
)
public
@ -157,7 +149,7 @@ contract NativeOrderSampler {
orderFillableTakerAssetAmounts = new uint256[](orders.length);
for (uint256 i = 0; i != orders.length; i++) {
try
this.getOrderFillableTakerAmount
this.getLimitOrderFillableTakerAmount
{gas: DEFAULT_CALL_GAS}
(
orders[i],
@ -178,19 +170,19 @@ contract NativeOrderSampler {
/// Effectively ignores orders that have empty signatures or
/// @param orders Native orders to query.
/// @param orderSignatures Signatures for each respective order in `orders`.
/// @param exchange The V3 exchange.
/// @param exchange The V4 exchange.
/// @return orderFillableMakerAssetAmounts How much maker asset can be filled
/// by each order in `orders`.
function getOrderFillableMakerAssetAmounts(
IExchange.Order[] memory orders,
bytes[] memory orderSignatures,
function getLimitOrderFillableMakerAssetAmounts(
IExchange.LimitOrder[] memory orders,
IExchange.Signature[] memory orderSignatures,
IExchange exchange
)
public
view
returns (uint256[] memory orderFillableMakerAssetAmounts)
{
orderFillableMakerAssetAmounts = getOrderFillableTakerAssetAmounts(
orderFillableMakerAssetAmounts = getLimitOrderFillableTakerAssetAmounts(
orders,
orderSignatures,
exchange
@ -201,8 +193,8 @@ contract NativeOrderSampler {
if (orderFillableMakerAssetAmounts[i] != 0) {
orderFillableMakerAssetAmounts[i] = LibMathV06.getPartialAmountCeil(
orderFillableMakerAssetAmounts[i],
orders[i].takerAssetAmount,
orders[i].makerAssetAmount
orders[i].takerAmount,
orders[i].makerAmount
);
}
}
@ -210,9 +202,9 @@ contract NativeOrderSampler {
/// @dev Get the fillable taker amount of an order, taking into account
/// order state, maker fees, and maker balances.
function getOrderFillableTakerAmount(
IExchange.Order memory order,
bytes memory signature,
function getLimitOrderFillableTakerAmount(
IExchange.LimitOrder memory order,
IExchange.Signature memory signature,
IExchange exchange
)
virtual
@ -220,88 +212,28 @@ contract NativeOrderSampler {
view
returns (uint256 fillableTakerAmount)
{
if (signature.length == 0 ||
order.makerAssetAmount == 0 ||
order.takerAssetAmount == 0)
if (signature.signatureType == IExchange.SignatureType.ILLEGAL ||
signature.signatureType == IExchange.SignatureType.INVALID ||
order.makerAmount == 0 ||
order.takerAmount == 0)
{
return 0;
}
IExchange.OrderInfo memory orderInfo = exchange.getOrderInfo(order);
if (orderInfo.orderStatus != IExchange.OrderStatus.FILLABLE) {
return 0;
}
if (!exchange.isValidHashSignature(orderInfo.orderHash, order.makerAddress, signature)) {
return 0;
}
address spender = exchange.getAssetProxy(ERC20_ASSET_PROXY_ID);
IERC20TokenV06 makerToken = _getTokenFromERC20AssetData(order.makerAssetData);
if (makerToken == IERC20TokenV06(0)) {
return 0;
}
IERC20TokenV06 makerFeeToken = order.makerFee > 0
? _getTokenFromERC20AssetData(order.makerFeeAssetData)
: IERC20TokenV06(0);
uint256 remainingTakerAmount = order.takerAssetAmount
.safeSub(orderInfo.orderTakerAssetFilledAmount);
fillableTakerAmount = remainingTakerAmount;
// The total fillable maker amount is the remaining fillable maker amount
// PLUS maker fees, if maker fees are denominated in the maker token.
uint256 totalFillableMakerAmount = LibMathV06.safeGetPartialAmountFloor(
remainingTakerAmount,
order.takerAssetAmount,
makerFeeToken == makerToken
? order.makerAssetAmount.safeAdd(order.makerFee)
: order.makerAssetAmount
);
// The spendable amount of maker tokens (by the maker) is the lesser of
// the maker's balance and the allowance they've granted to the ERC20Proxy.
uint256 spendableMakerAmount = LibSafeMathV06.min256(
makerToken.balanceOf(order.makerAddress),
makerToken.allowance(order.makerAddress, spender)
);
// Scale the fillable taker amount by the ratio of the maker's
// spendable maker amount over the total fillable maker amount.
if (spendableMakerAmount < totalFillableMakerAmount) {
fillableTakerAmount = LibMathV06.getPartialAmountCeil(
spendableMakerAmount,
totalFillableMakerAmount,
remainingTakerAmount
);
}
// If the maker fee is denominated in another token, constrain
// the fillable taker amount by how much the maker can pay of that token.
if (makerFeeToken != makerToken && makerFeeToken != IERC20TokenV06(0)) {
uint256 spendableExtraMakerFeeAmount = LibSafeMathV06.min256(
makerFeeToken.balanceOf(order.makerAddress),
makerFeeToken.allowance(order.makerAddress, spender)
);
if (spendableExtraMakerFeeAmount < order.makerFee) {
fillableTakerAmount = LibSafeMathV06.min256(
fillableTakerAmount,
LibMathV06.getPartialAmountCeil(
spendableExtraMakerFeeAmount,
order.makerFee,
remainingTakerAmount
)
);
}
}
}
(
IExchange.OrderInfo memory orderInfo,
uint128 remainingFillableTakerAmount,
bool isSignatureValid
) = exchange.getLimitOrderRelevantState(order, signature);
function _getTokenFromERC20AssetData(bytes memory assetData)
private
pure
returns (IERC20TokenV06 token)
{
if (assetData.length == 0) {
return IERC20TokenV06(address(0));
if (
orderInfo.status != IExchange.OrderStatus.FILLABLE ||
!isSignatureValid ||
order.makerToken == IERC20TokenV06(0)
) {
return 0;
}
if (assetData.length != 36 ||
assetData.readBytes4(0) != ERC20_ASSET_PROXY_ID)
{
return IERC20TokenV06(address(0));
}
return IERC20TokenV06(assetData.readAddress(16));
fillableTakerAmount = uint256(remainingFillableTakerAmount);
}
}

View File

@ -31,11 +31,13 @@ contract UniswapV2Sampler is
uint256 constant private UNISWAPV2_CALL_GAS = 150e3; // 150k
/// @dev Sample sell quotes from UniswapV2.
/// @param router Router to look up tokens and amounts
/// @param path Token route. Should be takerToken -> makerToken
/// @param takerTokenAmounts Taker token sell amount for each sample.
/// @return makerTokenAmounts Maker amounts bought at each taker token
/// amount.
function sampleSellsFromUniswapV2(
address router,
address[] memory path,
uint256[] memory takerTokenAmounts
)
@ -47,7 +49,7 @@ contract UniswapV2Sampler is
makerTokenAmounts = new uint256[](numSamples);
for (uint256 i = 0; i < numSamples; i++) {
try
IUniswapV2Router01(_getUniswapV2Router01Address()).getAmountsOut
IUniswapV2Router01(router).getAmountsOut
{gas: UNISWAPV2_CALL_GAS}
(takerTokenAmounts[i], path)
returns (uint256[] memory amounts)
@ -61,11 +63,13 @@ contract UniswapV2Sampler is
}
/// @dev Sample buy quotes from UniswapV2.
/// @param router Router to look up tokens and amounts
/// @param path Token route. Should be takerToken -> makerToken.
/// @param makerTokenAmounts Maker token buy amount for each sample.
/// @return takerTokenAmounts Taker amounts sold at each maker token
/// amount.
function sampleBuysFromUniswapV2(
address router,
address[] memory path,
uint256[] memory makerTokenAmounts
)
@ -77,7 +81,7 @@ contract UniswapV2Sampler is
takerTokenAmounts = new uint256[](numSamples);
for (uint256 i = 0; i < numSamples; i++) {
try
IUniswapV2Router01(_getUniswapV2Router01Address()).getAmountsIn
IUniswapV2Router01(router).getAmountsIn
{gas: UNISWAPV2_CALL_GAS}
(makerTokenAmounts[i], path)
returns (uint256[] memory amounts)

View File

@ -0,0 +1,80 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2021 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.6;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
contract UtilitySampler {
using LibERC20TokenV06 for IERC20TokenV06;
IERC20TokenV06 private immutable UTILITY_ETH_ADDRESS = IERC20TokenV06(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE);
function getTokenDecimals(IERC20TokenV06[] memory tokens)
public
view
returns (uint256[] memory decimals)
{
decimals = new uint256[](tokens.length);
for (uint256 i = 0; i != tokens.length; i++) {
decimals[i] = tokens[i] == UTILITY_ETH_ADDRESS
? 18
: tokens[i].compatDecimals();
}
}
function getBalanceOf(IERC20TokenV06[] memory tokens, address account)
public
view
returns (uint256[] memory balances)
{
balances = new uint256[](tokens.length);
for (uint256 i = 0; i != tokens.length; i++) {
balances[i] = tokens[i] == UTILITY_ETH_ADDRESS
? account.balance
: tokens[i].compatBalanceOf(account);
}
}
function getAllowanceOf(IERC20TokenV06[] memory tokens, address account, address spender)
public
view
returns (uint256[] memory allowances)
{
allowances = new uint256[](tokens.length);
for (uint256 i = 0; i != tokens.length; i++) {
allowances[i] = tokens[i] == UTILITY_ETH_ADDRESS
? 0
: tokens[i].compatAllowance(account, spender);
}
}
function isContract(address account)
public
view
returns (bool)
{
uint256 size;
assembly { size := extcodesize(account) }
return size > 0;
}
}

View File

@ -480,9 +480,9 @@ contract TestERC20BridgeSampler is
}
// Overridden to return deterministic states.
function getOrderFillableTakerAmount(
IExchange.Order memory order,
bytes memory,
function getLimitOrderFillableTakerAmount(
IExchange.LimitOrder memory order,
IExchange.Signature memory,
IExchange
)
override
@ -490,7 +490,7 @@ contract TestERC20BridgeSampler is
view
returns (uint256 fillableTakerAmount)
{
return uint256(keccak256(abi.encode(order.salt))) % order.takerAssetAmount;
return uint256(keccak256(abi.encode(order.salt))) % order.takerAmount;
}
// Overriden to return deterministic decimals.

View File

@ -20,6 +20,7 @@ pragma solidity ^0.6;
pragma experimental ABIEncoderV2;
import "../src/NativeOrderSampler.sol";
import "../src/UtilitySampler.sol";
contract TestNativeOrderSamplerToken {
@ -40,7 +41,8 @@ contract TestNativeOrderSamplerToken {
}
contract TestNativeOrderSampler is
NativeOrderSampler
NativeOrderSampler,
UtilitySampler
{
uint8 private constant MAX_ORDER_STATUS = uint8(IExchange.OrderStatus.CANCELLED) + 1;
bytes32 private constant VALID_SIGNATURE_HASH = keccak256(hex"01");
@ -67,42 +69,67 @@ contract TestNativeOrderSampler is
token.setBalanceAndAllowance(owner, spender, balance, allowance);
}
// IExchange.getAssetProxy()
function getAssetProxy(bytes4 proxyId)
public
pure
returns (address)
{
return address(uint160(uint256(keccak256(abi.encode(proxyId)))));
}
// IExchange.getOrderInfo()
function getOrderInfo(IExchange.Order calldata order)
// IExchange.getLimitOrderRelevantState()
function getLimitOrderRelevantState(
IExchange.LimitOrder memory order,
IExchange.Signature calldata signature
)
external
pure
returns (IExchange.OrderInfo memory orderInfo)
view
returns (
IExchange.OrderInfo memory orderInfo,
uint128 actualFillableTakerTokenAmount,
bool isSignatureValid
)
{
// The order salt determines everything.
orderInfo.orderHash = keccak256(abi.encode(order.salt));
if (uint8(order.salt) == 0xFF) {
orderInfo.orderStatus = IExchange.OrderStatus.FULLY_FILLED;
orderInfo.status = IExchange.OrderStatus.FILLED;
} else {
orderInfo.orderStatus = IExchange.OrderStatus.FILLABLE;
orderInfo.status = IExchange.OrderStatus.FILLABLE;
}
isSignatureValid = signature.r == VALID_SIGNATURE_HASH;
// The expiration time is the filled taker asset amount.
orderInfo.orderTakerAssetFilledAmount = order.expirationTimeSeconds;
orderInfo.takerTokenFilledAmount = uint128(order.expiry);
// Calculate how much is fillable in maker terms given the filled taker amount
uint256 fillableMakerTokenAmount = LibMathV06.getPartialAmountFloor(
uint256(
order.takerAmount
- orderInfo.takerTokenFilledAmount
),
uint256(order.takerAmount),
uint256(order.makerAmount)
);
// Take the min of the balance/allowance and the fillable maker amount
fillableMakerTokenAmount = LibSafeMathV06.min256(
fillableMakerTokenAmount,
_getSpendableERC20BalanceOf(order.makerToken, order.maker)
);
// Convert to taker terms
actualFillableTakerTokenAmount = LibMathV06.getPartialAmountCeil(
fillableMakerTokenAmount,
uint256(order.makerAmount),
uint256(order.takerAmount)
).safeDowncastToUint128();
}
// IExchange.isValidSignature()
function isValidHashSignature(
bytes32,
address,
bytes calldata signature
function _getSpendableERC20BalanceOf(
IERC20TokenV06 token,
address owner
)
external
pure
returns (bool isValid)
internal
view
returns (uint256)
{
return keccak256(signature) == VALID_SIGNATURE_HASH;
return LibSafeMathV06.min256(
token.allowance(owner, address(this)),
token.balanceOf(owner)
);
}
}

View File

@ -38,7 +38,7 @@
"config": {
"publicInterfaceContracts": "ERC20BridgeSampler,BalanceChecker",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./test/generated-artifacts/@(ApproximateBuys|BalanceChecker|BalancerSampler|BancorSampler|CurveSampler|DODOSampler|DeploymentConstants|DummyLiquidityProvider|ERC20BridgeSampler|Eth2DaiSampler|IBalancer|IBancor|ICurve|IEth2Dai|IKyberNetwork|IMStable|IMooniswap|IMultiBridge|IShell|IUniswapExchangeQuotes|IUniswapV2Router01|KyberSampler|LiquidityProviderSampler|MStableSampler|MooniswapSampler|MultiBridgeSampler|NativeOrderSampler|SamplerUtils|ShellSampler|SushiSwapSampler|TestERC20BridgeSampler|TestNativeOrderSampler|TwoHopSampler|UniswapSampler|UniswapV2Sampler).json",
"abis": "./test/generated-artifacts/@(ApproximateBuys|BalanceChecker|BalancerSampler|BancorSampler|CurveSampler|DODOSampler|DeploymentConstants|DummyLiquidityProvider|ERC20BridgeSampler|Eth2DaiSampler|IBalancer|IBancor|ICurve|IEth2Dai|IKyberNetwork|IMStable|IMooniswap|IMultiBridge|IShell|IUniswapExchangeQuotes|IUniswapV2Router01|KyberSampler|LiquidityProviderSampler|MStableSampler|MooniswapSampler|MultiBridgeSampler|NativeOrderSampler|SamplerUtils|ShellSampler|SushiSwapSampler|TestERC20BridgeSampler|TestNativeOrderSampler|TwoHopSampler|UniswapSampler|UniswapV2Sampler|UtilitySampler).json",
"postpublish": {
"assets": []
}
@ -63,8 +63,7 @@
"@0x/contract-wrappers": "^13.12.2",
"@0x/dev-utils": "^4.2.1",
"@0x/json-schemas": "^5.4.1",
"@0x/order-utils": "^10.4.15",
"@0x/orderbook": "0xProject/gitpkg-registry#0x-orderbook-v2.2.7-e10a81023",
"@0x/protocol-utils": "^1.1.3",
"@0x/quote-server": "^4.0.1",
"@0x/types": "^3.3.1",
"@0x/typescript-typings": "^5.1.6",

View File

@ -1,15 +1,13 @@
import { ChainId } from '@0x/contract-addresses';
import { SignatureType } from '@0x/protocol-utils';
import { BigNumber, logUtils } from '@0x/utils';
import {
ExchangeProxyContractOpts,
ExtensionContractType,
ForwarderExtensionContractOpts,
LogFunction,
OrderPrunerOpts,
OrderPrunerPermittedFeeTypes,
RfqtRequestOpts,
SwapQuoteExecutionOpts,
SwapQuoteGetOutputOpts,
SwapQuoteRequestOpts,
SwapQuoterOpts,
@ -29,10 +27,7 @@ const ZERO_AMOUNT = new BigNumber(0);
const DEFAULT_ORDER_PRUNER_OPTS: OrderPrunerOpts = {
expiryBufferMs: 120000, // 2 minutes
permittedOrderFeeTypes: new Set<OrderPrunerPermittedFeeTypes>([
OrderPrunerPermittedFeeTypes.NoFees,
OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee,
]), // Default asset-swapper for CFL oriented fee types
permittedOrderFeeTypes: new Set<OrderPrunerPermittedFeeTypes>([OrderPrunerPermittedFeeTypes.NoFees]), // Default asset-swapper for CFL oriented fee types
};
// 6 seconds polling interval
@ -55,16 +50,6 @@ const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = {
},
};
const DEFAULT_FORWARDER_EXTENSION_CONTRACT_OPTS: ForwarderExtensionContractOpts = {
feePercentage: 0,
feeRecipient: NULL_ADDRESS,
};
const DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS: SwapQuoteGetOutputOpts = {
useExtensionContract: ExtensionContractType.Forwarder,
extensionContractOpts: DEFAULT_FORWARDER_EXTENSION_CONTRACT_OPTS,
};
const DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS: ExchangeProxyContractOpts = {
isFromETH: false,
isToETH: false,
@ -78,10 +63,7 @@ const DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS: ExchangeProxyContractOpts
shouldSellEntireBalance: false,
};
const DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS: SwapQuoteExecutionOpts = DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS;
const DEFAULT_EXCHANGE_PROXY_SWAP_QUOTE_GET_OPTS: SwapQuoteGetOutputOpts = {
useExtensionContract: ExtensionContractType.ExchangeProxy,
extensionContractOpts: DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS,
};
@ -91,10 +73,6 @@ const DEFAULT_SWAP_QUOTE_REQUEST_OPTS: SwapQuoteRequestOpts = {
const DEFAULT_RFQT_REQUEST_OPTS: Partial<RfqtRequestOpts> = {
makerEndpointMaxResponseTimeMs: 1000,
priceAwareRFQFlag: {
isFirmPriceAwareEnabled: false,
isIndicativePriceAwareEnabled: false,
},
};
export const DEFAULT_INFO_LOGGER: LogFunction = (obj, msg) =>
@ -102,14 +80,8 @@ export const DEFAULT_INFO_LOGGER: LogFunction = (obj, msg) =>
export const DEFAULT_WARNING_LOGGER: LogFunction = (obj, msg) =>
logUtils.warn(`${msg ? `${msg}: ` : ''}${JSON.stringify(obj)}`);
// This feature flag allows us to merge the price-aware RFQ pricing
// project while still controlling when to activate the feature. We plan to do some
// data analysis work and address some of the issues with maker fillable amounts
// in later milestones. Once the feature is fully rolled out and is providing value
// and we have assessed that there is no user impact, we will proceed in cleaning up
// the feature flag. When that time comes, follow this PR to "undo" the feature flag:
// https://github.com/0xProject/0x-monorepo/pull/2735
export const IS_PRICE_AWARE_RFQ_ENABLED: boolean = false;
const EMPTY_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000';
export const INVALID_SIGNATURE = { signatureType: SignatureType.Invalid, v: 1, r: EMPTY_BYTES32, s: EMPTY_BYTES32 };
export {
BRIDGE_ADDRESSES_BY_CHAIN,
@ -131,8 +103,6 @@ export const constants = {
ONE_MINUTE_MS,
DEFAULT_SWAP_QUOTER_OPTS,
DEFAULT_INTERMEDIATE_TOKENS,
DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS,
DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS,
DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
DEFAULT_EXCHANGE_PROXY_SWAP_QUOTE_GET_OPTS,
DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS,
@ -144,4 +114,5 @@ export const constants = {
BRIDGE_ASSET_DATA_PREFIX: '0xdc1600f3',
DEFAULT_INFO_LOGGER,
DEFAULT_WARNING_LOGGER,
EMPTY_BYTES32,
};

View File

@ -5,30 +5,18 @@ export {
SendTransactionOpts,
} from '@0x/base-contract';
export { ContractAddresses } from '@0x/contract-addresses';
export { WSOpts } from '@0x/mesh-rpc-client';
export {
AcceptedRejectedOrders,
AddedRemovedOrders,
BaseOrderProvider,
MeshOrderProviderOpts,
Orderbook,
OrderSet,
OrderStore,
RejectedOrder,
SRAPollingOrderProviderOpts,
SRAWebsocketOrderProviderOpts,
} from '@0x/orderbook';
export { V3RFQFirmQuote, V3RFQIndicativeQuote, TakerRequestQueryParams } from '@0x/quote-server';
export {
APIOrder,
Asset,
AssetPairsItem,
DecodedLogEvent,
EventCallback,
IndexedFilterValues,
SignedOrder,
} from '@0x/types';
export { V4RFQFirmQuote, V4RFQIndicativeQuote, V4SignedRfqOrder, TakerRequestQueryParams } from '@0x/quote-server';
export { Asset, AssetPairsItem, DecodedLogEvent, EventCallback, IndexedFilterValues } from '@0x/types';
export { BigNumber } from '@0x/utils';
export {
RfqOrderFields,
LimitOrderFields,
FillQuoteTransformerOrderType,
RfqOrder,
LimitOrder,
Signature,
SignatureType,
} from '@0x/protocol-utils';
export { AxiosInstance } from 'axios';
export {
AbiDefinition,
@ -84,29 +72,26 @@ export {
export { artifacts } from './artifacts';
export { InsufficientAssetLiquidityError } from './errors';
export { SwapQuoteConsumer } from './quote_consumers/swap_quote_consumer';
export { getSwapMinBuyAmount, getQuoteInfoMinBuyAmount } from './quote_consumers/utils';
export { SwapQuoter } from './swap_quoter';
export { SwapQuoter, Orderbook } from './swap_quoter';
export {
AffiliateFee,
AssetSwapperContractAddresses,
CalldataInfo,
ExchangeProxyContractOpts,
ExchangeProxyRefundReceiver,
ExtensionContractType,
ForwarderExtensionContractOpts,
GetExtensionContractTypeOpts,
LiquidityForTakerMakerAssetDataPair,
LogFunction,
MarketBuySwapQuote,
MarketOperation,
MarketSellSwapQuote,
MockedRfqtFirmQuoteResponse,
MockedRfqtQuoteResponse,
OrderPrunerPermittedFeeTypes,
RfqtMakerAssetOfferings,
RfqtFirmQuoteValidator,
RfqtRequestOpts,
SamplerOverrides,
SignedOrderWithFillableAmounts,
SignedNativeOrder,
SignedOrder,
SwapQuote,
SwapQuoteConsumerBase,
SwapQuoteConsumerError,
@ -154,11 +139,12 @@ export {
MooniswapFillData,
MultiHopFillData,
NativeCollapsedFill,
NativeRfqOrderFillData,
NativeLimitOrderFillData,
NativeFillData,
OptimizedMarketOrder,
SnowSwapFillData,
SnowSwapInfo,
SourceInfo,
SourceQuoteOperation,
SushiSwapFillData,
SwerveFillData,
@ -168,16 +154,17 @@ export {
} from './utils/market_operation_utils/types';
export { ProtocolFeeUtils } from './utils/protocol_fee_utils';
export {
BridgeReportSource,
MultiHopReportSource,
NativeOrderbookReportSource,
NativeRFQTReportSource,
BridgeQuoteReportEntry,
MultiHopQuoteReportEntry,
NativeLimitOrderQuoteReportEntry,
NativeRfqOrderQuoteReportEntry,
QuoteReport,
QuoteReportSource,
QuoteReportEntry,
} from './utils/quote_report_generator';
export { QuoteRequestor } from './utils/quote_requestor';
export { rfqtMocker } from './utils/rfqt_mocker';
export { ERC20BridgeSamplerContract, BalanceCheckerContract } from './wrappers';
import { ERC20BridgeSource } from './utils/market_operation_utils/types';
export type Native = ERC20BridgeSource.Native;
export type MultiHop = ERC20BridgeSource.MultiHop;
export { rfqtMocker, RfqtQuoteEndpoint } from './utils/rfqt_mocker';

View File

@ -6,9 +6,11 @@ import {
encodePayTakerTransformerData,
encodeWethTransformerData,
ETH_TOKEN_ADDRESS,
FillQuoteTransformerData,
FillQuoteTransformerOrderType,
FillQuoteTransformerSide,
findTransformerNonce,
} from '@0x/order-utils';
} from '@0x/protocol-utils';
import { BigNumber, providerUtils } from '@0x/utils';
import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
import * as _ from 'lodash';
@ -27,10 +29,20 @@ import {
SwapQuoteGetOutputOpts,
} from '../types';
import { assert } from '../utils/assert';
import { ERC20BridgeSource, UniswapV2FillData } from '../utils/market_operation_utils/types';
import { getTokenFromAssetData } from '../utils/utils';
import { getSwapMinBuyAmount } from './utils';
import {
createBridgeDataForBridgeOrder,
getERC20BridgeSourceToBridgeSource,
} from '../utils/market_operation_utils/orders';
import {
ERC20BridgeSource,
LiquidityProviderFillData,
NativeLimitOrderFillData,
NativeRfqOrderFillData,
OptimizedMarketBridgeOrder,
OptimizedMarketOrder,
OptimizedMarketOrderBase,
UniswapV2FillData,
} from '../utils/market_operation_utils/types';
// tslint:disable-next-line:custom-no-magic-numbers
const MAX_UINT256 = new BigNumber(2).pow(256).minus(1);
@ -84,7 +96,6 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
quote: MarketBuySwapQuote | MarketSellSwapQuote,
opts: Partial<SwapQuoteGetOutputOpts> = {},
): Promise<CalldataInfo> {
assert.isValidSwapQuote('quote', quote);
const optsWithDefaults: ExchangeProxyContractOpts = {
...constants.DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS,
...opts.extensionContractOpts,
@ -92,11 +103,14 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
// tslint:disable-next-line:no-object-literal-type-assertion
const { refundReceiver, affiliateFee, isFromETH, isToETH, shouldSellEntireBalance } = optsWithDefaults;
const sellToken = getTokenFromAssetData(quote.takerAssetData);
const buyToken = getTokenFromAssetData(quote.makerAssetData);
const sellAmount = quote.worstCaseQuoteInfo.totalTakerAssetAmount;
let minBuyAmount = getSwapMinBuyAmount(quote);
const sellToken = quote.takerToken;
const buyToken = quote.makerToken;
// Take the bounds from the worst case
const sellAmount = quote.worstCaseQuoteInfo.totalTakerAmount;
let minBuyAmount = quote.worstCaseQuoteInfo.makerAmount;
let ethAmount = quote.worstCaseQuoteInfo.protocolFeeInWeiAmount;
if (isFromETH) {
ethAmount = ethAmount.plus(sellAmount);
}
@ -106,8 +120,8 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
if (
isDirectSwapCompatible(quote, optsWithDefaults, [ERC20BridgeSource.UniswapV2, ERC20BridgeSource.SushiSwap])
) {
const source = quote.orders[0].fills[0].source;
const fillData = quote.orders[0].fills[0].fillData as UniswapV2FillData;
const source = quote.orders[0].source;
const fillData = (quote.orders[0] as OptimizedMarketBridgeOrder<UniswapV2FillData>).fillData;
return {
calldataHexString: this._exchangeProxy
.sellToUniswap(
@ -132,7 +146,8 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
}
if (isDirectSwapCompatible(quote, optsWithDefaults, [ERC20BridgeSource.LiquidityProvider])) {
const target = quote.orders[0].makerAddress;
const fillData = (quote.orders[0] as OptimizedMarketBridgeOrder<LiquidityProviderFillData>).fillData;
const target = fillData.poolAddress;
return {
calldataHexString: this._exchangeProxy
.sellToLiquidityProvider(
@ -164,52 +179,46 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
});
}
const intermediateToken = quote.isTwoHop ? getTokenFromAssetData(quote.orders[0].makerAssetData) : NULL_ADDRESS;
// If it's two hop we have an intermediate token this is needed to encode the individual FQT
// and we also want to ensure no dust amount is left in the flash wallet
const intermediateToken = quote.isTwoHop ? quote.orders[0].makerToken : NULL_ADDRESS;
// This transformer will fill the quote.
if (quote.isTwoHop) {
const [firstHopOrder, secondHopOrder] = quote.orders;
transforms.push({
deploymentNonce: this.transformerNonces.fillQuoteTransformer,
data: encodeFillQuoteTransformerData({
side: FillQuoteTransformerSide.Sell,
sellToken,
buyToken: intermediateToken,
side: FillQuoteTransformerSide.Sell,
...getFQTTransformerDataFromOptimizedOrders([firstHopOrder]),
refundReceiver: refundReceiver || NULL_ADDRESS,
fillAmount: shouldSellEntireBalance ? MAX_UINT256 : firstHopOrder.takerAssetAmount,
maxOrderFillAmounts: [],
rfqtTakerAddress: NULL_ADDRESS,
orders: [firstHopOrder],
signatures: [firstHopOrder.signature],
fillAmount: shouldSellEntireBalance ? MAX_UINT256 : firstHopOrder.takerAmount,
}),
});
transforms.push({
deploymentNonce: this.transformerNonces.fillQuoteTransformer,
data: encodeFillQuoteTransformerData({
side: FillQuoteTransformerSide.Sell,
buyToken,
sellToken: intermediateToken,
...getFQTTransformerDataFromOptimizedOrders([secondHopOrder]),
refundReceiver: refundReceiver || NULL_ADDRESS,
side: FillQuoteTransformerSide.Sell,
fillAmount: MAX_UINT256,
maxOrderFillAmounts: [],
rfqtTakerAddress: NULL_ADDRESS,
orders: [secondHopOrder],
signatures: [secondHopOrder.signature],
}),
});
} else {
const fillAmount = isBuyQuote(quote) ? quote.makerAssetFillAmount : quote.takerAssetFillAmount;
const fillAmount = isBuyQuote(quote) ? quote.makerTokenFillAmount : quote.takerTokenFillAmount;
transforms.push({
deploymentNonce: this.transformerNonces.fillQuoteTransformer,
data: encodeFillQuoteTransformerData({
side: isBuyQuote(quote) ? FillQuoteTransformerSide.Buy : FillQuoteTransformerSide.Sell,
sellToken,
buyToken,
...getFQTTransformerDataFromOptimizedOrders(quote.orders),
refundReceiver: refundReceiver || NULL_ADDRESS,
side: isBuyQuote(quote) ? FillQuoteTransformerSide.Buy : FillQuoteTransformerSide.Sell,
fillAmount: !isBuyQuote(quote) && shouldSellEntireBalance ? MAX_UINT256 : fillAmount,
maxOrderFillAmounts: [],
rfqtTakerAddress: NULL_ADDRESS,
orders: quote.orders,
signatures: quote.orders.map(o => o.signature),
}),
});
}
@ -282,10 +291,6 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
}
}
function isBuyQuote(quote: SwapQuote): quote is MarketBuySwapQuote {
return quote.type === MarketOperation.Buy;
}
function isDirectSwapCompatible(
quote: SwapQuote,
opts: ExchangeProxyContractOpts,
@ -304,12 +309,7 @@ function isDirectSwapCompatible(
return false;
}
const order = quote.orders[0];
// With a single underlying fill/source.
if (order.fills.length !== 1) {
return false;
}
const fill = order.fills[0];
if (!directSources.includes(fill.source)) {
if (!directSources.includes(order.source)) {
return false;
}
// VIP does not support selling the entire balance
@ -318,3 +318,58 @@ function isDirectSwapCompatible(
}
return true;
}
function isBuyQuote(quote: SwapQuote): quote is MarketBuySwapQuote {
return quote.type === MarketOperation.Buy;
}
function isOptimizedBridgeOrder(x: OptimizedMarketOrder): x is OptimizedMarketBridgeOrder {
return x.type === FillQuoteTransformerOrderType.Bridge;
}
function isOptimizedLimitOrder(x: OptimizedMarketOrder): x is OptimizedMarketOrderBase<NativeLimitOrderFillData> {
return x.type === FillQuoteTransformerOrderType.Limit;
}
function isOptimizedRfqOrder(x: OptimizedMarketOrder): x is OptimizedMarketOrderBase<NativeRfqOrderFillData> {
return x.type === FillQuoteTransformerOrderType.Rfq;
}
function getFQTTransformerDataFromOptimizedOrders(
orders: OptimizedMarketOrder[],
): Pick<FillQuoteTransformerData, 'bridgeOrders' | 'limitOrders' | 'rfqOrders' | 'fillSequence'> {
const fqtData: Pick<FillQuoteTransformerData, 'bridgeOrders' | 'limitOrders' | 'rfqOrders' | 'fillSequence'> = {
bridgeOrders: [],
limitOrders: [],
rfqOrders: [],
fillSequence: [],
};
for (const order of orders) {
if (isOptimizedBridgeOrder(order)) {
fqtData.bridgeOrders.push({
bridgeData: createBridgeDataForBridgeOrder(order),
makerTokenAmount: order.makerAmount,
takerTokenAmount: order.takerAmount,
source: getERC20BridgeSourceToBridgeSource(order.source),
});
} else if (isOptimizedLimitOrder(order)) {
fqtData.limitOrders.push({
order: order.fillData.order,
signature: order.fillData.signature,
maxTakerTokenFillAmount: order.takerAmount,
});
} else if (isOptimizedRfqOrder(order)) {
fqtData.rfqOrders.push({
order: order.fillData.order,
signature: order.fillData.signature,
maxTakerTokenFillAmount: order.takerAmount,
});
} else {
// Should never happen
throw new Error('Unknown Order type');
}
fqtData.fillSequence.push(order.type);
}
return fqtData;
}

View File

@ -6,8 +6,6 @@ import * as _ from 'lodash';
import { constants } from '../constants';
import {
CalldataInfo,
ExtensionContractType,
GetExtensionContractTypeOpts,
SwapQuote,
SwapQuoteConsumerBase,
SwapQuoteConsumerOpts,
@ -15,7 +13,6 @@ import {
SwapQuoteGetOutputOpts,
} from '../types';
import { assert } from '../utils/assert';
import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils';
import { ExchangeProxySwapQuoteConsumer } from './exchange_proxy_swap_quote_consumer';
@ -57,7 +54,6 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
quote: SwapQuote,
opts: Partial<SwapQuoteGetOutputOpts> = {},
): Promise<CalldataInfo> {
assert.isValidSwapQuote('quote', quote);
const consumer = await this._getConsumerForSwapQuoteAsync(opts);
return consumer.getCalldataOrThrowAsync(quote, opts);
}
@ -71,35 +67,11 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
quote: SwapQuote,
opts: Partial<SwapQuoteExecutionOpts> = {},
): Promise<string> {
assert.isValidSwapQuote('quote', quote);
const consumer = await this._getConsumerForSwapQuoteAsync(opts);
return consumer.executeSwapQuoteOrThrowAsync(quote, opts);
}
/**
* Given a SwapQuote, returns optimal 0x protocol interface (extension or no extension) to perform the swap.
* @param quote An object that conforms to SwapQuote. See type definition for more information.
* @param opts Options for getting optimal exteion contract to fill quote. See type definition for more information.
*/
public async getOptimalExtensionContractTypeAsync(
quote: SwapQuote,
opts: Partial<GetExtensionContractTypeOpts> = {},
): Promise<ExtensionContractType> {
return swapQuoteConsumerUtils.getExtensionContractTypeForSwapQuoteAsync(
quote,
this._contractAddresses,
this.provider,
opts,
);
}
private async _getConsumerForSwapQuoteAsync(opts: Partial<SwapQuoteGetOutputOpts>): Promise<SwapQuoteConsumerBase> {
// ( akroeger)leaving this switch to use different contracts in the future
switch (opts.useExtensionContract) {
case ExtensionContractType.ExchangeProxy:
return this._exchangeProxyConsumer;
default:
return this._exchangeProxyConsumer;
}
return this._exchangeProxyConsumer;
}
}

View File

@ -1,64 +0,0 @@
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { MarketOperation, SwapQuote, SwapQuoteInfo } from '../types';
import { ERC20BridgeSource, OptimizedMarketOrder } from '../utils/market_operation_utils/types';
/**
* Compute the minimum buy token amount for market operations by inferring
* the slippage from the orders in a quote. We cannot rely on
* `worstCaseQuoteInfo.makerAssetAmount` because that does not stop at
* maximum slippage.
*/
export function getSwapMinBuyAmount(quote: SwapQuote): BigNumber {
if (quote.type === MarketOperation.Buy || quote.isTwoHop) {
return quote.worstCaseQuoteInfo.makerAssetAmount;
}
let slipRatio = new BigNumber(1);
// Infer the allowed maker asset slippage from any non-native order.
for (const o of quote.orders) {
if (o.fills.length === 0 || o.fills[0].source === ERC20BridgeSource.Native) {
// No slippage on native orders.
continue;
}
const totalFillMakerAssetAmount = BigNumber.sum(...o.fills.map(f => f.output));
slipRatio = o.fillableMakerAssetAmount.div(totalFillMakerAssetAmount);
break;
}
if (slipRatio.gte(1)) {
// No slippage allowed across all orders.
return quote.bestCaseQuoteInfo.makerAssetAmount;
}
return quote.bestCaseQuoteInfo.makerAssetAmount.times(slipRatio).integerValue(BigNumber.ROUND_DOWN);
}
/**
* Same as `getSwapMinBuyAmount` but operates
* on a single quote info instead of using best and worst case
* Orders must be derived from the same path as the quote info
*/
export function getQuoteInfoMinBuyAmount(
quoteInfo: SwapQuoteInfo,
orders: OptimizedMarketOrder[],
marketOperation: MarketOperation,
): BigNumber {
if (marketOperation === MarketOperation.Buy) {
return quoteInfo.makerAssetAmount;
}
let slipRatio = new BigNumber(1);
// Infer the allowed maker asset slippage from any non-native order.
for (const o of orders) {
if (o.fills.length === 0 || o.fills[0].source === ERC20BridgeSource.Native) {
// No slippage on native orders.
continue;
}
const totalFillMakerAssetAmount = BigNumber.sum(...o.fills.map(f => f.output));
slipRatio = o.fillableMakerAssetAmount.div(totalFillMakerAssetAmount);
break;
}
if (slipRatio.gte(1)) {
// No slippage allowed across all orders.
return quoteInfo.makerAssetAmount;
}
return quoteInfo.makerAssetAmount.times(slipRatio).integerValue(BigNumber.ROUND_DOWN);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,13 @@
import { ChainId } from '@0x/contract-addresses';
import { BlockParam, ContractAddresses, GethCallOverrides } from '@0x/contract-wrappers';
import {
FillQuoteTransformerOrderType,
LimitOrderFields,
RfqOrder,
RfqOrderFields,
Signature,
} from '@0x/protocol-utils';
import { TakerRequestQueryParams } from '@0x/quote-server';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import {
@ -22,34 +28,23 @@ export interface OrderPrunerOpts {
permittedOrderFeeTypes: Set<OrderPrunerPermittedFeeTypes>;
}
/**
* Represents the on-chain metadata of a signed order
*/
export interface OrderPrunerOnChainMetadata {
orderStatus: number;
orderHash: string;
orderTakerAssetFilledAmount: BigNumber;
fillableTakerAssetAmount: BigNumber;
isValidSignature: boolean;
export interface SignedOrder<T> {
order: T;
type: FillQuoteTransformerOrderType.Limit | FillQuoteTransformerOrderType.Rfq;
signature: Signature;
}
/**
* makerAssetData: The assetData representing the desired makerAsset.
* takerAssetData: The assetData representing the desired takerAsset.
*/
export interface OrderProviderRequest {
makerAssetData: string;
takerAssetData: string;
}
export type SignedNativeOrder = SignedOrder<LimitOrderFields> | SignedOrder<RfqOrderFields>;
export type NativeOrderWithFillableAmounts = SignedNativeOrder & NativeOrderFillableAmountFields;
/**
* fillableMakerAssetAmount: Amount of makerAsset that is fillable
* fillableTakerAssetAmount: Amount of takerAsset that is fillable
* fillableTakerFeeAmount: Amount of takerFee paid to fill fillableTakerAssetAmount
* fillableMakerAmount: Amount of makerAsset that is fillable
* fillableTakerAmount: Amount of takerAsset that is fillable
* fillableTakerFeeAmount: Amount of takerFee paid to fill fillableTakerAmount
*/
export interface SignedOrderWithFillableAmounts extends SignedOrder {
fillableMakerAssetAmount: BigNumber;
fillableTakerAssetAmount: BigNumber;
export interface NativeOrderFillableAmountFields {
fillableMakerAmount: BigNumber;
fillableTakerAmount: BigNumber;
fillableTakerFeeAmount: BigNumber;
}
@ -67,24 +62,6 @@ export interface CalldataInfo {
allowanceTarget: string;
}
/**
* Represents the varying smart contracts that can consume a valid swap quote
*/
export enum ExtensionContractType {
None = 'NONE',
Forwarder = 'FORWARDER',
ExchangeProxy = 'EXCHANGE_PROXY',
}
/**
* feePercentage: Optional affiliate fee percentage used to calculate the eth amount paid to fee recipient.
* feeRecipient: The address where affiliate fees are sent. Defaults to null address (0x000...000).
*/
export interface ForwarderSmartContractParamsBase {
feePercentage: BigNumber;
feeRecipient: string;
}
/**
* Interface that varying SwapQuoteConsumers adhere to (exchange consumer, router consumer, forwarder consumer, coordinator consumer)
* getCalldataOrThrow: Get CalldataInfo to swap for tokens with provided SwapQuote. Throws if invalid SwapQuote is provided.
@ -107,8 +84,7 @@ export interface SwapQuoteConsumerOpts {
* Represents the options provided to a generic SwapQuoteConsumer
*/
export interface SwapQuoteGetOutputOpts {
useExtensionContract: ExtensionContractType;
extensionContractOpts?: ForwarderExtensionContractOpts | ExchangeProxyContractOpts | any;
extensionContractOpts?: ExchangeProxyContractOpts | any;
}
/**
@ -122,15 +98,6 @@ export interface SwapQuoteExecutionOpts extends SwapQuoteGetOutputOpts {
gasLimit?: number;
}
/**
* feePercentage: percentage (up to 5%) of the taker asset paid to feeRecipient
* feeRecipient: address of the receiver of the feePercentage of taker asset
*/
export interface ForwarderExtensionContractOpts {
feePercentage: number;
feeRecipient: string;
}
export interface AffiliateFee {
recipient: string;
buyTokenFeeAmount: BigNumber;
@ -175,31 +142,27 @@ export interface GetExtensionContractTypeOpts {
}
/**
* takerAssetData: String that represents a specific taker asset (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* makerAssetData: String that represents a specific maker asset (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
* takerToken: Address of the taker asset.
* makerToken: Address of the maker asset.
* gasPrice: gas price used to determine protocolFee amount, default to ethGasStation fast amount.
* orders: An array of objects conforming to OptimizedMarketOrder. These orders can be used to cover the requested assetBuyAmount plus slippage.
* bestCaseQuoteInfo: Info about the best case price for the asset.
* worstCaseQuoteInfo: Info about the worst case price for the asset.
* unoptimizedQuoteInfo: Info about the unoptimized (best single source) price for the swap
* unoptimizedOrders: Orders used in the unoptimized quote info
*/
export interface SwapQuoteBase {
takerAssetData: string;
makerAssetData: string;
takerToken: string;
makerToken: string;
gasPrice: BigNumber;
orders: OptimizedMarketOrder[];
bestCaseQuoteInfo: SwapQuoteInfo;
worstCaseQuoteInfo: SwapQuoteInfo;
sourceBreakdown: SwapQuoteOrdersBreakdown;
quoteReport?: QuoteReport;
unoptimizedQuoteInfo: SwapQuoteInfo;
unoptimizedOrders: OptimizedMarketOrder[];
isTwoHop: boolean;
makerTokenDecimals: number;
takerTokenDecimals: number;
takerAssetToEthRate: BigNumber;
makerAssetToEthRate: BigNumber;
takerTokenToEthRate: BigNumber;
makerTokenToEthRate: BigNumber;
}
/**
@ -207,7 +170,7 @@ export interface SwapQuoteBase {
* type: Specified MarketOperation the SwapQuote is provided for
*/
export interface MarketSellSwapQuote extends SwapQuoteBase {
takerAssetFillAmount: BigNumber;
takerTokenFillAmount: BigNumber;
type: MarketOperation.Sell;
}
@ -216,25 +179,25 @@ export interface MarketSellSwapQuote extends SwapQuoteBase {
* type: Specified MarketOperation the SwapQuote is provided for
*/
export interface MarketBuySwapQuote extends SwapQuoteBase {
makerAssetFillAmount: BigNumber;
makerTokenFillAmount: BigNumber;
type: MarketOperation.Buy;
}
export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote;
/**
* feeTakerAssetAmount: The amount of takerAsset reserved for paying takerFees when swapping for desired assets.
* takerAssetAmount: The amount of takerAsset swapped for desired makerAsset.
* totalTakerAssetAmount: The total amount of takerAsset required to complete the swap (filling orders, and paying takerFees).
* makerAssetAmount: The amount of makerAsset that will be acquired through the swap.
* feeTakerTokenAmount: The amount of takerAsset reserved for paying takerFees when swapping for desired assets.
* takerTokenAmount: The amount of takerAsset swapped for desired makerAsset.
* totalTakerTokenAmount: The total amount of takerAsset required to complete the swap (filling orders, and paying takerFees).
* makerTokenAmount: The amount of makerAsset that will be acquired through the swap.
* protocolFeeInWeiAmount: The amount of ETH to pay (in WEI) as protocol fee to perform the swap for desired asset.
* gas: Amount of estimated gas needed to fill the quote.
*/
export interface SwapQuoteInfo {
feeTakerAssetAmount: BigNumber;
takerAssetAmount: BigNumber;
totalTakerAssetAmount: BigNumber;
makerAssetAmount: BigNumber;
feeTakerTokenAmount: BigNumber;
takerAmount: BigNumber;
totalTakerAmount: BigNumber;
makerAmount: BigNumber;
protocolFeeInWeiAmount: BigNumber;
gas: number;
}
@ -252,11 +215,6 @@ export type SwapQuoteOrdersBreakdown = Partial<
}
>;
export interface PriceAwareRFQFlags {
isIndicativePriceAwareEnabled: boolean;
isFirmPriceAwareEnabled: boolean;
}
/**
* nativeExclusivelyRFQT: if set to `true`, Swap quote will exclude Open Orderbook liquidity.
* If set to `true` and `ERC20BridgeSource.Native` is part of the `excludedSources`
@ -264,37 +222,22 @@ export interface PriceAwareRFQFlags {
*/
export interface RfqtRequestOpts {
takerAddress: string;
txOrigin: string;
apiKey: string;
intentOnFilling: boolean;
isIndicative?: boolean;
makerEndpointMaxResponseTimeMs?: number;
nativeExclusivelyRFQT?: boolean;
/**
* This feature flag allows us to merge the price-aware RFQ pricing
* project while still controlling when to activate the feature. We plan to do some
* data analysis work and address some of the issues with maker fillable amounts
* in later milestones. Once the feature is fully rolled out and is providing value
* and we have assessed that there is no user impact, we will proceed in cleaning up
* the feature flag. When that time comes, follow this PR to "undo" the feature flag:
* https://github.com/0xProject/0x-monorepo/pull/2735
*/
priceAwareRFQFlag?: PriceAwareRFQFlags;
}
/**
* gasPrice: gas price to determine protocolFee amount, default to ethGasStation fast amount
*/
export interface SwapQuoteRequestOpts extends CalculateSwapQuoteOpts {
export interface SwapQuoteRequestOpts extends GetMarketOrdersOpts {
gasPrice?: BigNumber;
rfqt?: RfqtRequestOpts;
}
/**
* Opts required to generate a SwapQuote with SwapQuoteCalculator
*/
export interface CalculateSwapQuoteOpts extends GetMarketOrdersOpts {}
/**
* A mapping from RFQ-T quote provider URLs to the trading pairs they support.
* The value type represents an array of supported asset pairs, with each array element encoded as a 2-element array of token addresses.
@ -306,7 +249,7 @@ export interface RfqtMakerAssetOfferings {
export type LogFunction = (obj: object, msg?: string, ...args: any[]) => void;
export interface RfqtFirmQuoteValidator {
getRfqtTakerFillableAmountsAsync(quotes: SignedOrder[]): Promise<BigNumber[]>;
getRfqtTakerFillableAmountsAsync(quotes: RfqOrder[]): Promise<BigNumber[]>;
}
export interface SwapQuoterRfqtOpts {
@ -363,14 +306,6 @@ export enum SwapQuoterError {
AssetDataUnsupported = 'ASSET_DATA_UNSUPPORTED',
}
/**
* Represents available liquidity for a given assetData.
*/
export interface LiquidityForTakerMakerAssetDataPair {
makerAssetAvailableInBaseUnits: BigNumber;
takerAssetAvailableInBaseUnits: BigNumber;
}
/**
* Represents two main market operations supported by asset-swapper.
*/
@ -384,25 +319,13 @@ export enum MarketOperation {
*/
export enum OrderPrunerPermittedFeeTypes {
NoFees = 'NO_FEES',
MakerDenominatedTakerFee = 'MAKER_DENOMINATED_TAKER_FEE',
TakerDenominatedTakerFee = 'TAKER_DENOMINATED_TAKER_FEE',
}
/**
* Represents a mocked RFQT maker responses.
*/
export interface MockedRfqtFirmQuoteResponse {
endpoint: string;
requestApiKey: string;
requestParams: TakerRequestQueryParams;
responseData: any;
responseCode: number;
}
/**
* Represents a mocked RFQT maker responses.
*/
export interface MockedRfqtIndicativeQuoteResponse {
export interface MockedRfqtQuoteResponse {
endpoint: string;
requestApiKey: string;
requestParams: TakerRequestQueryParams;

View File

@ -11,7 +11,7 @@ export const affiliateFeeUtils = {
* @param feePercentage Percentage of additive fees to apply to totalTakerAssetAmount + protocol fee.
*/
getTotalEthAmountWithAffiliateFee(swapQuoteInfo: SwapQuoteInfo, feePercentage: number): BigNumber {
const ethAmount = swapQuoteInfo.protocolFeeInWeiAmount.plus(swapQuoteInfo.totalTakerAssetAmount);
const ethAmount = swapQuoteInfo.protocolFeeInWeiAmount.plus(swapQuoteInfo.totalTakerAmount);
const ethAmountWithFees = ethAmount.plus(affiliateFeeUtils.getFeeAmount(swapQuoteInfo, feePercentage));
return ethAmountWithFees;
},
@ -22,7 +22,7 @@ export const affiliateFeeUtils = {
*/
getFeeAmount(swapQuoteInfo: SwapQuoteInfo, feePercentage: number): BigNumber {
assert.assert(feePercentage >= 0, 'feePercentage must be >= 0');
const ethAmount = swapQuoteInfo.protocolFeeInWeiAmount.plus(swapQuoteInfo.totalTakerAssetAmount);
const ethAmount = swapQuoteInfo.protocolFeeInWeiAmount.plus(swapQuoteInfo.totalTakerAmount);
// HACK(dekz): This is actually in WEI amount not ETH
return ethAmount.times(feePercentage).integerValue(BigNumber.ROUND_UP);
},

View File

@ -1,130 +1,16 @@
import { assert as sharedAssert } from '@0x/assert';
import { schemas } from '@0x/json-schemas';
import { Orderbook } from '@0x/orderbook';
import { Order, SignedOrder } from '@0x/types';
import * as _ from 'lodash';
import { MarketOperation, OrderProviderRequest, SwapQuote, SwapQuoteInfo } from '../types';
import {
isAssetDataEquivalent,
isExactAssetData,
isOrderTakerFeePayableWithMakerAsset,
isOrderTakerFeePayableWithTakerAsset,
} from './utils';
import { Orderbook } from '../swap_quoter';
export const assert = {
...sharedAssert,
isValidSwapQuote(variableName: string, swapQuote: SwapQuote): void {
sharedAssert.isHexString(`${variableName}.takerAssetData`, swapQuote.takerAssetData);
sharedAssert.isHexString(`${variableName}.makerAssetData`, swapQuote.makerAssetData);
sharedAssert.doesConformToSchema(`${variableName}.orders`, swapQuote.orders, schemas.signedOrdersSchema);
if (swapQuote.isTwoHop) {
assert.isValidTwoHopSwapQuoteOrders(
`${variableName}.orders`,
swapQuote.orders,
swapQuote.makerAssetData,
swapQuote.takerAssetData,
);
} else {
assert.isValidSwapQuoteOrders(
`${variableName}.orders`,
swapQuote.orders,
swapQuote.makerAssetData,
swapQuote.takerAssetData,
);
}
assert.isValidSwapQuoteInfo(`${variableName}.bestCaseQuoteInfo`, swapQuote.bestCaseQuoteInfo);
assert.isValidSwapQuoteInfo(`${variableName}.worstCaseQuoteInfo`, swapQuote.worstCaseQuoteInfo);
if (swapQuote.type === MarketOperation.Buy) {
sharedAssert.isBigNumber(`${variableName}.makerAssetFillAmount`, swapQuote.makerAssetFillAmount);
} else {
sharedAssert.isBigNumber(`${variableName}.takerAssetFillAmount`, swapQuote.takerAssetFillAmount);
}
},
isValidSwapQuoteOrders(
variableName: string,
orders: SignedOrder[],
makerAssetData: string,
takerAssetData: string,
): void {
_.every(orders, (order: SignedOrder, index: number) => {
assert.assert(
isAssetDataEquivalent(takerAssetData, order.takerAssetData),
`Expected ${variableName}[${index}].takerAssetData to be ${takerAssetData} but found ${
order.takerAssetData
}`,
);
assert.assert(
isAssetDataEquivalent(makerAssetData, order.makerAssetData),
`Expected ${variableName}[${index}].makerAssetData to be ${makerAssetData} but found ${
order.makerAssetData
}`,
);
});
},
isValidTwoHopSwapQuoteOrders(
variableName: string,
orders: SignedOrder[],
makerAssetData: string,
takerAssetData: string,
): void {
assert.assert(orders.length === 2, `Expected ${variableName}.length to be 2 for a two-hop quote`);
assert.assert(
isAssetDataEquivalent(takerAssetData, orders[0].takerAssetData),
`Expected ${variableName}[0].takerAssetData to be ${takerAssetData} but found ${orders[0].takerAssetData}`,
);
assert.assert(
isAssetDataEquivalent(makerAssetData, orders[1].makerAssetData),
`Expected ${variableName}[1].makerAssetData to be ${makerAssetData} but found ${orders[1].makerAssetData}`,
);
assert.assert(
isAssetDataEquivalent(orders[0].makerAssetData, orders[1].takerAssetData),
`Expected ${variableName}[0].makerAssetData (${
orders[0].makerAssetData
}) to equal ${variableName}[1].takerAssetData (${orders[1].takerAssetData})`,
);
},
isValidOrdersForSwapQuoter<T extends Order>(variableName: string, orders: T[]): void {
_.every(orders, (order: T, index: number) => {
assert.assert(
order.takerFee.isZero() ||
isOrderTakerFeePayableWithTakerAsset(order) ||
isOrderTakerFeePayableWithMakerAsset(order),
`Expected ${variableName}[${index}].takerFeeAssetData to be ${order.makerAssetData} or ${
order.takerAssetData
} but found ${order.takerFeeAssetData}`,
);
});
},
isValidForwarderSwapQuote(variableName: string, swapQuote: SwapQuote, wethAssetData: string): void {
assert.isValidSwapQuote(variableName, swapQuote);
assert.isValidForwarderSignedOrders(`${variableName}.orders`, swapQuote.orders, wethAssetData);
},
isValidForwarderSignedOrders(variableName: string, orders: SignedOrder[], wethAssetData: string): void {
_.forEach(orders, (o: SignedOrder, i: number) => {
assert.isValidForwarderSignedOrder(`${variableName}[${i}]`, o, wethAssetData);
});
},
isValidForwarderSignedOrder(variableName: string, order: SignedOrder, wethAssetData: string): void {
assert.assert(
isExactAssetData(order.takerAssetData, wethAssetData),
`Expected ${variableName} to have takerAssetData set as ${wethAssetData}, but is ${order.takerAssetData}`,
);
},
isValidSwapQuoteInfo(variableName: string, swapQuoteInfo: SwapQuoteInfo): void {
sharedAssert.isNumber(`${variableName}.gas`, swapQuoteInfo.gas);
sharedAssert.isBigNumber(`${variableName}.feeTakerAssetAmount`, swapQuoteInfo.feeTakerAssetAmount);
sharedAssert.isBigNumber(`${variableName}.totalTakerAssetAmount`, swapQuoteInfo.totalTakerAssetAmount);
sharedAssert.isBigNumber(`${variableName}.takerAssetAmount`, swapQuoteInfo.takerAssetAmount);
sharedAssert.isBigNumber(`${variableName}.makerAssetAmount`, swapQuoteInfo.makerAssetAmount);
},
isValidOrderbook(variableName: string, orderFetcher: Orderbook): void {
sharedAssert.isFunction(`${variableName}.getOrdersAsync`, orderFetcher.getOrdersAsync);
},
isValidOrderProviderRequest(variableName: string, orderFetcherRequest: OrderProviderRequest): void {
sharedAssert.isHexString(`${variableName}.makerAssetData`, orderFetcherRequest.makerAssetData);
sharedAssert.isHexString(`${variableName}.takerAssetData`, orderFetcherRequest.takerAssetData);
sharedAssert.isFunction(`${variableName}.getOrdersAsync`, orderFetcher.getOrdersAsync.bind(orderFetcher));
sharedAssert.isFunction(
`${variableName}.getBatchOrdersAsync`,
orderFetcher.getBatchOrdersAsync.bind(orderFetcher),
);
},
isValidPercentage(variableName: string, percentage: number): void {
assert.isNumber(variableName, percentage);
@ -133,8 +19,4 @@ export const assert = {
`Expected ${variableName} to be between 0 and 1, but is ${percentage}`,
);
},
isValidForwarderExtensionContractOpts(variableName: string, opts: any): void {
assert.isValidPercentage(`${variableName}.feePercentage`, opts.feePercentage);
assert.isETHAddressHex(`${variableName}.feeRecipient`, opts.feeRecipient);
},
};

View File

@ -1,29 +0,0 @@
import { BigNumber } from '@0x/utils';
import { LiquidityForTakerMakerAssetDataPair, SignedOrderWithFillableAmounts } from '../types';
import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from './utils';
export const calculateLiquidity = (
prunedOrders: SignedOrderWithFillableAmounts[],
): LiquidityForTakerMakerAssetDataPair => {
const liquidityInBigNumbers = prunedOrders.reduce(
(acc, order) => {
const fillableMakerAssetAmount = isOrderTakerFeePayableWithMakerAsset(order)
? order.fillableMakerAssetAmount.minus(order.fillableTakerFeeAmount)
: order.fillableMakerAssetAmount;
const fillableTakerAssetAmount = isOrderTakerFeePayableWithTakerAsset(order)
? order.fillableTakerAssetAmount.plus(order.fillableTakerFeeAmount)
: order.fillableTakerAssetAmount;
return {
makerAssetAvailableInBaseUnits: acc.makerAssetAvailableInBaseUnits.plus(fillableMakerAssetAmount),
takerAssetAvailableInBaseUnits: acc.takerAssetAvailableInBaseUnits.plus(fillableTakerAssetAmount),
};
},
{
makerAssetAvailableInBaseUnits: new BigNumber(0),
takerAssetAvailableInBaseUnits: new BigNumber(0),
},
);
return liquidityInBigNumbers;
};

View File

@ -1,23 +0,0 @@
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { SignedOrderWithFillableAmounts } from '../types';
import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from './utils';
export const fillableAmountsUtils = {
getTakerAssetAmountSwappedAfterOrderFees(order: SignedOrderWithFillableAmounts): BigNumber {
if (isOrderTakerFeePayableWithTakerAsset(order)) {
return order.fillableTakerAssetAmount.plus(order.fillableTakerFeeAmount);
} else {
return order.fillableTakerAssetAmount;
}
},
getMakerAssetAmountSwappedAfterOrderFees(order: SignedOrderWithFillableAmounts): BigNumber {
if (isOrderTakerFeePayableWithMakerAsset(order)) {
return order.fillableMakerAssetAmount.minus(order.fillableTakerFeeAmount);
} else {
return order.fillableMakerAssetAmount;
}
},
};

View File

@ -1,48 +1,47 @@
import { BigNumber } from '@0x/utils';
import { bmath, getPoolsWithTokens, parsePoolData } from '@balancer-labs/sor';
import { Decimal } from 'decimal.js';
import { getPoolsWithTokens, parsePoolData } from '@balancer-labs/sor';
import { Pool } from '@balancer-labs/sor/dist/types';
import { ZERO_AMOUNT } from './constants';
import { BALANCER_MAX_POOLS_FETCHED, BALANCER_SUBGRAPH_URL, BALANCER_TOP_POOLS_FETCHED } from './constants';
// tslint:disable:boolean-naming
export interface BalancerPool {
id: string;
balanceIn: BigNumber;
balanceOut: BigNumber;
weightIn: BigNumber;
weightOut: BigNumber;
swapFee: BigNumber;
spotPrice?: BigNumber;
slippage?: BigNumber;
limitAmount?: BigNumber;
}
interface CacheValue {
timestamp: number;
pools: BalancerPool[];
pools: Pool[];
}
// tslint:disable:custom-no-magic-numbers
const FIVE_SECONDS_MS = 5 * 1000;
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
const DEFAULT_TIMEOUT_MS = 1000;
const MAX_POOLS_FETCHED = 3;
const Decimal20 = Decimal.clone({ precision: 20 });
// tslint:enable:custom-no-magic-numbers
interface BalancerPoolResponse {
id: string;
swapFee: string;
tokens: Array<{ address: string; decimals: number; balance: string }>;
tokensList: string[];
totalWeight: string;
}
export class BalancerPoolsCache {
constructor(
private readonly _cache: { [key: string]: CacheValue } = {},
private readonly maxPoolsFetched: number = MAX_POOLS_FETCHED,
) {}
private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED,
private readonly subgraphUrl: string = BALANCER_SUBGRAPH_URL,
private readonly topPoolsFetched: number = BALANCER_TOP_POOLS_FETCHED,
) {
void this._loadTopPoolsAsync();
// Reload the top pools every 12 hours
setInterval(async () => void this._loadTopPoolsAsync(), ONE_DAY_MS / 2);
}
public async getPoolsForPairAsync(
takerToken: string,
makerToken: string,
timeoutMs: number = DEFAULT_TIMEOUT_MS,
): Promise<BalancerPool[]> {
const timeout = new Promise<BalancerPool[]>(resolve => setTimeout(resolve, timeoutMs, []));
): Promise<Pool[]> {
const timeout = new Promise<Pool[]>(resolve => setTimeout(resolve, timeoutMs, []));
return Promise.race([this._getPoolsForPairAsync(takerToken, makerToken), timeout]);
}
@ -88,23 +87,26 @@ export class BalancerPoolsCache {
takerToken: string,
makerToken: string,
cacheExpiryMs: number = FIVE_SECONDS_MS,
): Promise<BalancerPool[]> {
): Promise<Pool[]> {
const key = JSON.stringify([takerToken, makerToken]);
const value = this._cache[key];
const minTimestamp = Date.now() - cacheExpiryMs;
if (value === undefined || value.timestamp < minTimestamp) {
const pools = await this._fetchPoolsForPairAsync(takerToken, makerToken);
const timestamp = Date.now();
this._cache[key] = {
pools,
timestamp,
};
this._cachePoolsForPair(takerToken, makerToken, pools);
}
return this._cache[key].pools;
}
// tslint:disable-next-line:prefer-function-over-method
protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<BalancerPool[]> {
protected _cachePoolsForPair(takerToken: string, makerToken: string, pools: Pool[]): void {
const key = JSON.stringify([takerToken, makerToken]);
this._cache[key] = {
pools,
timestamp: Date.now(),
};
}
protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]> {
try {
const poolData = (await getPoolsWithTokens(takerToken, makerToken)).pools;
// Sort by maker token balance (descending)
@ -116,36 +118,75 @@ export class BalancerPoolsCache {
return [];
}
}
}
// tslint:disable completed-docs
export function computeBalancerSellQuote(pool: BalancerPool, takerFillAmount: BigNumber): BigNumber {
if (takerFillAmount.isGreaterThan(bmath.bmul(pool.balanceIn, bmath.MAX_IN_RATIO))) {
return ZERO_AMOUNT;
}
const weightRatio = pool.weightIn.dividedBy(pool.weightOut);
const adjustedIn = bmath.BONE.minus(pool.swapFee)
.dividedBy(bmath.BONE)
.times(takerFillAmount);
const y = pool.balanceIn.dividedBy(pool.balanceIn.plus(adjustedIn));
const foo = Math.pow(y.toNumber(), weightRatio.toNumber());
const bar = new BigNumber(1).minus(foo);
const tokenAmountOut = pool.balanceOut.times(bar);
return tokenAmountOut.integerValue();
}
protected async _loadTopPoolsAsync(): Promise<void> {
const fromToPools: {
[from: string]: { [to: string]: Pool[] };
} = {};
export function computeBalancerBuyQuote(pool: BalancerPool, makerFillAmount: BigNumber): BigNumber {
if (makerFillAmount.isGreaterThan(bmath.bmul(pool.balanceOut, bmath.MAX_OUT_RATIO))) {
return ZERO_AMOUNT;
const pools = await this._fetchTopPoolsAsync();
pools.forEach(pool => {
const { tokensList } = pool;
for (const from of tokensList) {
for (const to of tokensList.filter(t => t.toLowerCase() !== from.toLowerCase())) {
if (!fromToPools[from]) {
fromToPools[from] = {};
}
if (!fromToPools[from][to]) {
fromToPools[from][to] = [];
}
try {
// The list of pools must be relevant to `from` and `to` for `parsePoolData`
const poolData = parsePoolData([pool], from, to);
fromToPools[from][to].push(poolData[0]);
// Cache this as we progress through
this._cachePoolsForPair(from, to, fromToPools[from][to]);
} catch {
// soldier on
}
}
}
});
}
const weightRatio = pool.weightOut.dividedBy(pool.weightIn);
const diff = pool.balanceOut.minus(makerFillAmount);
const y = pool.balanceOut.dividedBy(diff);
let foo: number | Decimal = Math.pow(y.toNumber(), weightRatio.toNumber()) - 1;
if (!Number.isFinite(foo)) {
foo = new Decimal20(y.toString()).pow(weightRatio.toString()).minus(1);
protected async _fetchTopPoolsAsync(): Promise<BalancerPoolResponse[]> {
const query = `
query {
pools (first: ${
this.topPoolsFetched
}, where: {publicSwap: true, liquidity_gt: 0}, orderBy: swapsCount, orderDirection: desc) {
id
publicSwap
swapFee
totalWeight
tokensList
tokens {
id
address
balance
decimals
symbol
denormWeight
}
}
}
`;
try {
const response = await fetch(this.subgraphUrl, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
}),
});
const { data } = await response.json();
return data.pools;
} catch (err) {
return [];
}
}
let tokenAmountIn = bmath.BONE.minus(pool.swapFee).dividedBy(bmath.BONE);
tokenAmountIn = pool.balanceIn.times(foo.toString()).dividedBy(tokenAmountIn);
return tokenAmountIn.integerValue();
}

View File

@ -1,4 +1,5 @@
import { Web3Wrapper } from '@0x/dev-utils';
import { FillQuoteTransformerOrderType } from '@0x/protocol-utils';
import { BigNumber, logUtils } from '@0x/utils';
import * as _ from 'lodash';
@ -38,7 +39,9 @@ export function getComparisonPrices(
return { wholeOrder };
} else {
try {
feeInEth = new BigNumber((feeSchedule[ERC20BridgeSource.Native] as FeeEstimate)(undefined));
feeInEth = new BigNumber(
(feeSchedule[ERC20BridgeSource.Native] as FeeEstimate)({ type: FillQuoteTransformerOrderType.Rfq }),
);
} catch {
logUtils.warn('Native order fee schedule requires fill data');

View File

@ -405,16 +405,23 @@ export const MAINNET_SNOWSWAP_INFOS: { [name: string]: CurveInfo } = {
*/
export const KYBER_BRIDGED_LIQUIDITY_PREFIX = '0xbb';
export const MAX_KYBER_RESERVES_QUERIED = 5;
export const MAINNET_KYBER_NETWORK_PROXY = '0x9aab3f75489902f3a48495025729a0af77d4b11e';
export const LIQUIDITY_PROVIDER_REGISTRY: LiquidityProviderRegistry = {};
export const MAINNET_SUSHI_SWAP_ROUTER = '0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F';
export const MAINNET_CRYPTO_COM_ROUTER = '0xCeB90E4C17d626BE0fACd78b79c9c87d7ca181b3';
export const MAINNET_UNISWAP_V1_ROUTER = '0xc0a47dfe034b400b47bdad5fecda2621de6c4d95';
export const MAINNET_UNISWAP_V2_ROUTER = '0xf164fc0ec4e93095b804a4795bbe1e041497b92a';
export const MAINNET_SUSHI_SWAP_ROUTER = '0xd9e1ce17f2641f24ae83637ab66a2cca9c378b9f';
export const MAINNET_CRYPTO_COM_ROUTER = '0xceb90e4c17d626be0facd78b79c9c87d7ca181b3';
export const MAINNET_MSTABLE_ROUTER = '0xe2f2a5c287993345a840db3b0845fbc70f5935a5';
export const MAINNET_OASIS_ROUTER = '0x794e6e91555438afc3ccf1c5076a74f42133d08d';
export const MAINNET_MOONISWAP_REGISTRY = '0x71CD6666064C3A1354a3B4dca5fA1E2D3ee7D303';
export const MAINNET_MOONISWAP_V2_REGISTRY = '0xc4a8b7e29e3c8ec560cd4945c1cf3461a85a148d';
export const MAINNET_MOONISWAP_V2_1_REGISTRY = '0xbaf9a5d4b0052359326a6cdab54babaa3a3a9643';
export const MAINNET_DODO_HELPER = '0x533da777aedce766ceae696bf90f8541a4ba80eb';
export const MAINNET_SHELL_POOLS = {
StableCoins: {
poolAddress: '0x8f26d7bab7a73309141a291525c965ecdea7bf42',
@ -426,6 +433,10 @@ export const MAINNET_SHELL_POOLS = {
},
};
export const BALANCER_SUBGRAPH_URL = 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer';
export const BALANCER_TOP_POOLS_FETCHED = 250;
export const BALANCER_MAX_POOLS_FETCHED = 3;
export const ERC20_PROXY_ID = '0xf47261b0';
export const WALLET_SIGNATURE = '0x04';
export const ONE_ETHER = new BigNumber(1e18);
@ -502,7 +513,10 @@ export const BRIDGE_ADDRESSES_BY_CHAIN: { [chainId in ChainId]: BridgeContractAd
*/
// tslint:disable:custom-no-magic-numbers
export const DEFAULT_GAS_SCHEDULE: Required<FeeSchedule> = {
[ERC20BridgeSource.Native]: () => 150e3,
[ERC20BridgeSource.Native]: _fillData => {
// const nativeFillData = (_fillData as NativeRfqOrderFillData|NativeLimitOrderFillData)
return 100e3;
},
[ERC20BridgeSource.Uniswap]: () => 90e3,
[ERC20BridgeSource.LiquidityProvider]: fillData => {
return (fillData as LiquidityProviderFillData).gasCost;

View File

@ -1,33 +1,33 @@
import { Pool } from '@balancer-labs/sor/dist/types';
import { getPoolsWithTokens, parsePoolData } from 'cream-sor';
import { BalancerPool } from './balancer_utils';
import { BALANCER_MAX_POOLS_FETCHED } from './constants';
// tslint:disable:boolean-naming
interface CacheValue {
timestamp: number;
pools: BalancerPool[];
pools: Pool[];
}
// tslint:disable:custom-no-magic-numbers
const FIVE_SECONDS_MS = 5 * 1000;
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
const DEFAULT_TIMEOUT_MS = 1000;
const MAX_POOLS_FETCHED = 3;
// tslint:enable:custom-no-magic-numbers
export class CreamPoolsCache {
constructor(
private readonly _cache: { [key: string]: CacheValue } = {},
private readonly maxPoolsFetched: number = MAX_POOLS_FETCHED,
private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED,
) {}
public async getPoolsForPairAsync(
takerToken: string,
makerToken: string,
timeoutMs: number = DEFAULT_TIMEOUT_MS,
): Promise<BalancerPool[]> {
const timeout = new Promise<BalancerPool[]>(resolve => setTimeout(resolve, timeoutMs, []));
): Promise<Pool[]> {
const timeout = new Promise<Pool[]>(resolve => setTimeout(resolve, timeoutMs, []));
return Promise.race([this._getPoolsForPairAsync(takerToken, makerToken), timeout]);
}
@ -73,7 +73,7 @@ export class CreamPoolsCache {
takerToken: string,
makerToken: string,
cacheExpiryMs: number = FIVE_SECONDS_MS,
): Promise<BalancerPool[]> {
): Promise<Pool[]> {
const key = JSON.stringify([takerToken, makerToken]);
const value = this._cache[key];
const minTimestamp = Date.now() - cacheExpiryMs;
@ -88,8 +88,13 @@ export class CreamPoolsCache {
return this._cache[key].pools;
}
// tslint:disable-next-line: prefer-function-over-method
protected async _loadTopPoolsAsync(): Promise<void> {
// Do nothing
}
// tslint:disable-next-line:prefer-function-over-method
protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<BalancerPool[]> {
protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]> {
try {
const poolData = (await getPoolsWithTokens(takerToken, makerToken)).pools;
// Sort by maker token balance (descending)

View File

@ -1,7 +1,7 @@
import { FillQuoteTransformerOrderType } from '@0x/protocol-utils';
import { BigNumber, hexUtils } from '@0x/utils';
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
import { fillableAmountsUtils } from '../../utils/fillable_amounts_utils';
import { MarketOperation, NativeOrderWithFillableAmounts } from '../../types';
import { POSITIVE_INF, SOURCE_FLAGS, ZERO_AMOUNT } from './constants';
import { DexSample, ERC20BridgeSource, FeeSchedule, Fill } from './types';
@ -13,7 +13,7 @@ import { DexSample, ERC20BridgeSource, FeeSchedule, Fill } from './types';
*/
export function createFills(opts: {
side: MarketOperation;
orders?: SignedOrderWithFillableAmounts[];
orders?: NativeOrderWithFillableAmounts[];
dexQuotes?: DexSample[][];
targetInput?: BigNumber;
ethToOutputRate?: BigNumber;
@ -31,7 +31,7 @@ export function createFills(opts: {
// Create native fills.
const nativeFills = nativeOrdersToFills(
side,
orders,
orders.filter(o => o.fillableTakerAmount.isGreaterThan(0)),
opts.targetInput,
ethToOutputRate,
ethToInputRate,
@ -73,7 +73,7 @@ function hasLiquidity(fills: Fill[]): boolean {
function nativeOrdersToFills(
side: MarketOperation,
orders: SignedOrderWithFillableAmounts[],
orders: NativeOrderWithFillableAmounts[],
targetInput: BigNumber = POSITIVE_INF,
ethToOutputRate: BigNumber,
ethToInputRate: BigNumber,
@ -82,12 +82,13 @@ function nativeOrdersToFills(
const sourcePathId = hexUtils.random();
// Create a single path from all orders.
let fills: Array<Fill & { adjustedRate: BigNumber }> = [];
for (const order of orders) {
const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(order);
const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(order);
for (const o of orders) {
const { fillableTakerAmount, fillableTakerFeeAmount, fillableMakerAmount } = o;
const makerAmount = fillableMakerAmount;
const takerAmount = fillableTakerAmount.plus(fillableTakerFeeAmount);
const input = side === MarketOperation.Sell ? takerAmount : makerAmount;
const output = side === MarketOperation.Sell ? makerAmount : takerAmount;
const fee = fees[ERC20BridgeSource.Native] === undefined ? 0 : fees[ERC20BridgeSource.Native]!();
const fee = fees[ERC20BridgeSource.Native] === undefined ? 0 : fees[ERC20BridgeSource.Native]!(o);
const outputPenalty = !ethToOutputRate.isZero()
? ethToOutputRate.times(fee)
: ethToInputRate.times(fee).times(output.dividedToIntegerBy(input));
@ -117,7 +118,8 @@ function nativeOrdersToFills(
index: 0, // TBD
parent: undefined, // TBD
source: ERC20BridgeSource.Native,
fillData: { order },
type: o.type,
fillData: { ...o },
});
}
// Sort by descending adjusted rate.
@ -167,6 +169,7 @@ function dexSamplesToFills(
adjustedOutput,
source,
fillData,
type: FillQuoteTransformerOrderType.Bridge,
index: i,
parent: i !== 0 ? fills[fills.length - 1] : undefined,
flags: SOURCE_FLAGS[source],

View File

@ -1,12 +1,20 @@
import { V3RFQIndicativeQuote } from '@0x/quote-server';
import { SignedOrder } from '@0x/types';
import { FillQuoteTransformerOrderType, RfqOrder } from '@0x/protocol-utils';
import { BigNumber, NULL_ADDRESS } from '@0x/utils';
import * as _ from 'lodash';
import { DEFAULT_INFO_LOGGER } from '../../constants';
import { AssetSwapperContractAddresses, MarketOperation } from '../../types';
import { DEFAULT_INFO_LOGGER, INVALID_SIGNATURE } from '../../constants';
import {
AssetSwapperContractAddresses,
MarketOperation,
NativeOrderWithFillableAmounts,
SignedNativeOrder,
} from '../../types';
import { QuoteRequestor } from '../quote_requestor';
import { getPriceAwareRFQRolloutFlags } from '../utils';
import {
getNativeAdjustedFillableAmountsFromMakerAmount,
getNativeAdjustedFillableAmountsFromTakerAmount,
getNativeAdjustedMakerFillAmount,
} from '../utils';
import { generateQuoteReport, QuoteReport } from './../quote_report_generator';
import { getComparisonPrices } from './comparison_price';
@ -21,13 +29,8 @@ import {
} from './constants';
import { createFills } from './fills';
import { getBestTwoHopQuote } from './multihop_utils';
import {
createOrdersFromTwoHopSample,
createSignedOrdersFromRfqtIndicativeQuotes,
createSignedOrdersWithFillableAmounts,
getNativeOrderTokens,
} from './orders';
import { fillsToSortedPaths, findOptimalPathAsync } from './path_optimizer';
import { createOrdersFromTwoHopSample } from './orders';
import { findOptimalPathAsync } from './path_optimizer';
import { DexOrderSampler, getSampleAmounts } from './sampler';
import { SourceFilters } from './source_filters';
import {
@ -45,36 +48,6 @@ import {
// tslint:disable:boolean-naming
/**
* Returns a indicative quotes or an empty array if RFQT is not enabled or requested
* @param makerAssetData the maker asset data
* @param takerAssetData the taker asset data
* @param marketOperation Buy or Sell
* @param assetFillAmount the amount to fill, in base units
* @param opts market request options
*/
export async function getRfqtIndicativeQuotesAsync(
makerAssetData: string,
takerAssetData: string,
marketOperation: MarketOperation,
assetFillAmount: BigNumber,
comparisonPrice: BigNumber | undefined,
opts: Partial<GetMarketOrdersOpts>,
): Promise<V3RFQIndicativeQuote[]> {
if (opts.rfqt && opts.rfqt.isIndicative === true && opts.rfqt.quoteRequestor) {
return opts.rfqt.quoteRequestor.requestRfqtIndicativeQuotesAsync(
makerAssetData,
takerAssetData,
assetFillAmount,
marketOperation,
comparisonPrice,
opts.rfqt,
);
} else {
return Promise.resolve<V3RFQIndicativeQuote[]>([]);
}
}
export class MarketOperationUtils {
private readonly _wethAddress: string;
private readonly _sellSources: SourceFilters;
@ -87,14 +60,14 @@ export class MarketOperationUtils {
optimizerResult: OptimizerResult,
comparisonPrice?: BigNumber | undefined,
): QuoteReport {
const { side, dexQuotes, twoHopQuotes, orderFillableAmounts, nativeOrders } = marketSideLiquidity;
const { side, quotes } = marketSideLiquidity;
const { dexQuotes, twoHopQuotes, nativeOrders } = quotes;
const { liquidityDelivered } = optimizerResult;
return generateQuoteReport(
side,
_.flatten(dexQuotes),
twoHopQuotes,
nativeOrders,
orderFillableAmounts,
liquidityDelivered,
comparisonPrice,
quoteRequestor,
@ -113,26 +86,22 @@ export class MarketOperationUtils {
/**
* Gets the liquidity available for a market sell operation
* @param nativeOrders Native orders.
* @param nativeOrders Native orders. Assumes LimitOrders not RfqOrders
* @param takerAmount Amount of taker asset to sell.
* @param opts Options object.
* @return MarketSideLiquidity.
*/
public async getMarketSellLiquidityAsync(
nativeOrders: SignedOrder[],
nativeOrders: SignedNativeOrder[],
takerAmount: BigNumber,
opts?: Partial<GetMarketOrdersOpts>,
): Promise<MarketSideLiquidity> {
if (nativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders);
}
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]);
const { makerToken, takerToken } = nativeOrders[0].order;
const sampleAmounts = getSampleAmounts(takerAmount, _opts.numSamples, _opts.sampleDistributionBase);
const requestFilters = new SourceFilters().exclude(_opts.excludedSources).include(_opts.includedSources);
const quoteSourceFilters = this._sellSources.merge(requestFilters);
const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources);
const {
@ -158,11 +127,14 @@ export class MarketOperationUtils {
...(!sampleBalancerOnChain ? [ERC20BridgeSource.Balancer] : []),
];
// Used to determine whether the tx origin is an EOA or a contract
const txOrigin = (_opts.rfqt && _opts.rfqt.txOrigin) || NULL_ADDRESS;
// Call the sampler contract.
const samplerPromise = this._sampler.executeAsync(
this._sampler.getTokenDecimals(makerToken, takerToken),
this._sampler.getTokenDecimals([makerToken, takerToken]),
// Get native order fillable amounts.
this._sampler.getOrderFillableTakerAmounts(nativeOrders, this.contractAddresses.exchange),
this._sampler.getLimitOrderFillableTakerAmounts(nativeOrders, this.contractAddresses.exchangeProxy),
// Get ETH -> maker token price.
this._sampler.getMedianSellRate(feeSourceFilters.sources, makerToken, this._wethAddress, ONE_ETHER),
// Get ETH -> taker token price.
@ -180,22 +152,9 @@ export class MarketOperationUtils {
takerToken,
takerAmount,
),
this._sampler.isAddressContract(txOrigin),
);
const isPriceAwareRfqEnabled =
_opts.rfqt && getPriceAwareRFQRolloutFlags(_opts.rfqt.priceAwareRFQFlag).isIndicativePriceAwareEnabled;
const rfqtPromise =
!isPriceAwareRfqEnabled && quoteSourceFilters.isAllowed(ERC20BridgeSource.Native)
? getRfqtIndicativeQuotesAsync(
nativeOrders[0].makerAssetData,
nativeOrders[0].takerAssetData,
MarketOperation.Sell,
takerAmount,
undefined,
_opts,
)
: Promise.resolve([]);
const offChainBalancerPromise = sampleBalancerOffChain
? this._sampler.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, sampleAmounts)
: Promise.resolve([]);
@ -205,56 +164,68 @@ export class MarketOperationUtils {
: Promise.resolve([]);
const [
[tokenDecimals, orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, rawTwoHopQuotes],
rfqtIndicativeQuotes,
[
tokenDecimals,
orderFillableTakerAmounts,
ethToMakerAssetRate,
ethToTakerAssetRate,
dexQuotes,
rawTwoHopQuotes,
isTxOriginContract,
],
offChainBalancerQuotes,
offChainCreamQuotes,
] = await Promise.all([samplerPromise, rfqtPromise, offChainBalancerPromise, offChainCreamPromise]);
] = await Promise.all([samplerPromise, offChainBalancerPromise, offChainCreamPromise]);
// Filter out any invalid two hop quotes where we couldn't find a route
const twoHopQuotes = rawTwoHopQuotes.filter(q => q && q.fillData && q.fillData.firstHopSource);
const [makerTokenDecimals, takerTokenDecimals] = tokenDecimals;
const isRfqSupported = !!(_opts.rfqt && !isTxOriginContract);
const limitOrdersWithFillableAmounts = nativeOrders.map((order, i) => ({
...order,
...getNativeAdjustedFillableAmountsFromTakerAmount(order, orderFillableTakerAmounts[i]),
}));
return {
side: MarketOperation.Sell,
inputAmount: takerAmount,
inputToken: takerToken,
outputToken: makerToken,
dexQuotes: dexQuotes.concat([...offChainBalancerQuotes, ...offChainCreamQuotes]),
nativeOrders,
orderFillableAmounts,
ethToOutputRate: ethToMakerAssetRate,
ethToInputRate: ethToTakerAssetRate,
rfqtIndicativeQuotes,
twoHopQuotes,
quoteSourceFilters,
makerTokenDecimals: makerTokenDecimals.toNumber(),
takerTokenDecimals: takerTokenDecimals.toNumber(),
quotes: {
nativeOrders: limitOrdersWithFillableAmounts,
rfqtIndicativeQuotes: [],
twoHopQuotes,
dexQuotes: dexQuotes.concat([...offChainBalancerQuotes, ...offChainCreamQuotes]),
},
isRfqSupported,
};
}
/**
* Gets the liquidity available for a market buy operation
* @param nativeOrders Native orders.
* @param nativeOrders Native orders. Assumes LimitOrders not RfqOrders
* @param makerAmount Amount of maker asset to buy.
* @param opts Options object.
* @return MarketSideLiquidity.
*/
public async getMarketBuyLiquidityAsync(
nativeOrders: SignedOrder[],
nativeOrders: SignedNativeOrder[],
makerAmount: BigNumber,
opts?: Partial<GetMarketOrdersOpts>,
): Promise<MarketSideLiquidity> {
if (nativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders);
}
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]);
const { makerToken, takerToken } = nativeOrders[0].order;
const sampleAmounts = getSampleAmounts(makerAmount, _opts.numSamples, _opts.sampleDistributionBase);
const requestFilters = new SourceFilters().exclude(_opts.excludedSources).include(_opts.includedSources);
const quoteSourceFilters = this._buySources.merge(requestFilters);
const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources);
const {
@ -280,11 +251,14 @@ export class MarketOperationUtils {
...(!sampleBalancerOnChain ? [ERC20BridgeSource.Balancer] : []),
];
// Used to determine whether the tx origin is an EOA or a contract
const txOrigin = (_opts.rfqt && _opts.rfqt.txOrigin) || NULL_ADDRESS;
// Call the sampler contract.
const samplerPromise = this._sampler.executeAsync(
this._sampler.getTokenDecimals(makerToken, takerToken),
this._sampler.getTokenDecimals([makerToken, takerToken]),
// Get native order fillable amounts.
this._sampler.getOrderFillableMakerAmounts(nativeOrders, this.contractAddresses.exchange),
this._sampler.getLimitOrderFillableMakerAmounts(nativeOrders, this.contractAddresses.exchangeProxy),
// Get ETH -> makerToken token price.
this._sampler.getMedianSellRate(feeSourceFilters.sources, makerToken, this._wethAddress, ONE_ETHER),
// Get ETH -> taker token price.
@ -302,20 +276,9 @@ export class MarketOperationUtils {
takerToken,
makerAmount,
),
this._sampler.isAddressContract(txOrigin),
);
const isPriceAwareRfqEnabled =
_opts.rfqt && getPriceAwareRFQRolloutFlags(_opts.rfqt.priceAwareRFQFlag).isIndicativePriceAwareEnabled;
const rfqtPromise =
!isPriceAwareRfqEnabled && quoteSourceFilters.isAllowed(ERC20BridgeSource.Native)
? getRfqtIndicativeQuotesAsync(
nativeOrders[0].makerAssetData,
nativeOrders[0].takerAssetData,
MarketOperation.Buy,
makerAmount,
undefined,
_opts,
)
: Promise.resolve([]);
const offChainBalancerPromise = sampleBalancerOffChain
? this._sampler.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, sampleAmounts)
: Promise.resolve([]);
@ -325,79 +288,63 @@ export class MarketOperationUtils {
: Promise.resolve([]);
const [
[tokenDecimals, orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, rawTwoHopQuotes],
rfqtIndicativeQuotes,
[
tokenDecimals,
orderFillableMakerAmounts,
ethToMakerAssetRate,
ethToTakerAssetRate,
dexQuotes,
rawTwoHopQuotes,
isTxOriginContract,
],
offChainBalancerQuotes,
offChainCreamQuotes,
] = await Promise.all([samplerPromise, rfqtPromise, offChainBalancerPromise, offChainCreamPromise]);
] = await Promise.all([samplerPromise, offChainBalancerPromise, offChainCreamPromise]);
// Filter out any invalid two hop quotes where we couldn't find a route
const twoHopQuotes = rawTwoHopQuotes.filter(q => q && q.fillData && q.fillData.firstHopSource);
const [makerTokenDecimals, takerTokenDecimals] = tokenDecimals;
const isRfqSupported = !isTxOriginContract;
const limitOrdersWithFillableAmounts = nativeOrders.map((order, i) => ({
...order,
...getNativeAdjustedFillableAmountsFromMakerAmount(order, orderFillableMakerAmounts[i]),
}));
return {
side: MarketOperation.Buy,
inputAmount: makerAmount,
inputToken: makerToken,
outputToken: takerToken,
dexQuotes: dexQuotes.concat(offChainBalancerQuotes, offChainCreamQuotes),
nativeOrders,
orderFillableAmounts,
ethToOutputRate: ethToTakerAssetRate,
ethToInputRate: ethToMakerAssetRate,
rfqtIndicativeQuotes,
twoHopQuotes,
quoteSourceFilters,
makerTokenDecimals: makerTokenDecimals.toNumber(),
takerTokenDecimals: takerTokenDecimals.toNumber(),
quotes: {
nativeOrders: limitOrdersWithFillableAmounts,
rfqtIndicativeQuotes: [],
twoHopQuotes,
dexQuotes: dexQuotes.concat(offChainBalancerQuotes, offChainCreamQuotes),
},
isRfqSupported,
};
}
/**
* gets the orders required for a market sell operation by (potentially) merging native orders with
* generated bridge orders.
* @param nativeOrders Native orders.
* @param takerAmount Amount of taker asset to sell.
* @param opts Options object.
* @return object with optimized orders and a QuoteReport
*/
public async getMarketSellOrdersAsync(
nativeOrders: SignedOrder[],
takerAmount: BigNumber,
opts?: Partial<GetMarketOrdersOpts>,
): Promise<OptimizerResultWithReport> {
return this._getMarketSideOrdersAsync(nativeOrders, takerAmount, MarketOperation.Sell, opts);
}
/**
* gets the orders required for a market buy operation by (potentially) merging native orders with
* generated bridge orders.
* @param nativeOrders Native orders.
* @param makerAmount Amount of maker asset to buy.
* @param opts Options object.
* @return object with optimized orders and a QuoteReport
*/
public async getMarketBuyOrdersAsync(
nativeOrders: SignedOrder[],
makerAmount: BigNumber,
opts?: Partial<GetMarketOrdersOpts>,
): Promise<OptimizerResultWithReport> {
return this._getMarketSideOrdersAsync(nativeOrders, makerAmount, MarketOperation.Buy, opts);
}
/**
* gets the orders required for a batch of market buy operations by (potentially) merging native orders with
* generated bridge orders.
*
* NOTE: Currently `getBatchMarketBuyOrdersAsync()` does not support external liquidity providers.
*
* @param batchNativeOrders Batch of Native orders.
* @param batchNativeOrders Batch of Native orders. Assumes LimitOrders not RfqOrders
* @param makerAmounts Array amount of maker asset to buy for each batch.
* @param opts Options object.
* @return orders.
*/
public async getBatchMarketBuyOrdersAsync(
batchNativeOrders: SignedOrder[][],
batchNativeOrders: SignedNativeOrder[][],
makerAmounts: BigNumber[],
opts?: Partial<GetMarketOrdersOpts>,
): Promise<Array<OptimizerResult | undefined>> {
@ -413,12 +360,12 @@ export class MarketOperationUtils {
const ops = [
...batchNativeOrders.map(orders =>
this._sampler.getOrderFillableMakerAmounts(orders, this.contractAddresses.exchange),
this._sampler.getLimitOrderFillableMakerAmounts(orders, this.contractAddresses.exchangeProxy),
),
...batchNativeOrders.map(orders =>
this._sampler.getMedianSellRate(
feeSourceFilters.sources,
getNativeOrderTokens(orders[0])[1],
orders[0].order.takerToken,
this._wethAddress,
ONE_ETHER,
),
@ -426,18 +373,18 @@ export class MarketOperationUtils {
...batchNativeOrders.map((orders, i) =>
this._sampler.getBuyQuotes(
quoteSourceFilters.sources,
getNativeOrderTokens(orders[0])[0],
getNativeOrderTokens(orders[0])[1],
orders[0].order.makerToken,
orders[0].order.takerToken,
[makerAmounts[i]],
),
),
...batchNativeOrders.map(orders =>
this._sampler.getTokenDecimals(getNativeOrderTokens(orders[0])[0], getNativeOrderTokens(orders[0])[1]),
this._sampler.getTokenDecimals([orders[0].order.makerToken, orders[0].order.takerToken]),
),
];
const executeResults = await this._sampler.executeBatchAsync(ops);
const batchOrderFillableAmounts = executeResults.splice(0, batchNativeOrders.length) as BigNumber[][];
const batchOrderFillableMakerAmounts = executeResults.splice(0, batchNativeOrders.length) as BigNumber[][];
const batchEthToTakerAssetRate = executeResults.splice(0, batchNativeOrders.length) as BigNumber[];
const batchDexQuotes = executeResults.splice(0, batchNativeOrders.length) as DexSample[][][];
const batchTokenDecimals = executeResults.splice(0, batchNativeOrders.length) as number[][];
@ -448,8 +395,8 @@ export class MarketOperationUtils {
if (nativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders);
}
const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]);
const orderFillableAmounts = batchOrderFillableAmounts[i];
const { makerToken, takerToken } = nativeOrders[0].order;
const orderFillableMakerAmounts = batchOrderFillableMakerAmounts[i];
const ethToTakerAssetRate = batchEthToTakerAssetRate[i];
const dexQuotes = batchDexQuotes[i];
const makerAmount = makerAmounts[i];
@ -457,19 +404,24 @@ export class MarketOperationUtils {
const optimizerResult = await this._generateOptimizedOrdersAsync(
{
side: MarketOperation.Buy,
nativeOrders,
orderFillableAmounts,
dexQuotes,
inputToken: makerToken,
outputToken: takerToken,
inputAmount: makerAmount,
ethToOutputRate: ethToTakerAssetRate,
ethToInputRate,
rfqtIndicativeQuotes: [],
inputToken: makerToken,
outputToken: takerToken,
twoHopQuotes: [],
quoteSourceFilters,
makerTokenDecimals: batchTokenDecimals[i][0],
takerTokenDecimals: batchTokenDecimals[i][1],
quotes: {
nativeOrders: nativeOrders.map((o, k) => ({
...o,
...getNativeAdjustedFillableAmountsFromMakerAmount(o, orderFillableMakerAmounts[k]),
})),
dexQuotes,
rfqtIndicativeQuotes: [],
twoHopQuotes: [],
},
isRfqSupported: false,
},
{
bridgeSlippage: _opts.bridgeSlippage,
@ -498,13 +450,11 @@ export class MarketOperationUtils {
outputToken,
side,
inputAmount,
nativeOrders,
orderFillableAmounts,
rfqtIndicativeQuotes,
dexQuotes,
quotes,
ethToOutputRate,
ethToInputRate,
} = marketSideLiquidity;
const { nativeOrders, rfqtIndicativeQuotes, dexQuotes } = quotes;
const maxFallbackSlippage = opts.maxFallbackSlippage || 0;
const orderOpts = {
@ -516,14 +466,23 @@ export class MarketOperationUtils {
bridgeSlippage: opts.bridgeSlippage || 0,
};
const augmentedRfqtIndicativeQuotes: NativeOrderWithFillableAmounts[] = rfqtIndicativeQuotes.map(
q =>
// tslint:disable-next-line: no-object-literal-type-assertion
({
order: { ...new RfqOrder({ ...q }) },
signature: INVALID_SIGNATURE,
fillableMakerAmount: new BigNumber(q.makerAmount),
fillableTakerAmount: new BigNumber(q.takerAmount),
fillableTakerFeeAmount: ZERO_AMOUNT,
type: FillQuoteTransformerOrderType.Rfq,
} as NativeOrderWithFillableAmounts),
);
// Convert native orders and dex quotes into `Fill` objects.
const fills = createFills({
side,
// Augment native orders with their fillable amounts.
orders: [
...createSignedOrdersWithFillableAmounts(side, nativeOrders, orderFillableAmounts),
...createSignedOrdersFromRfqtIndicativeQuotes(rfqtIndicativeQuotes),
],
orders: [...nativeOrders, ...augmentedRfqtIndicativeQuotes],
dexQuotes,
targetInput: inputAmount,
ethToOutputRate,
@ -540,12 +499,8 @@ export class MarketOperationUtils {
};
// NOTE: For sell quotes input is the taker asset and for buy quotes input is the maker asset
const takerAssetToEthRate = side === MarketOperation.Sell ? ethToInputRate : ethToOutputRate;
const makerAssetToEthRate = side === MarketOperation.Sell ? ethToOutputRate : ethToInputRate;
// Find the unoptimized best rate to calculate savings from optimizer
const _unoptimizedPath = fillsToSortedPaths(fills, side, inputAmount, optimizerOpts)[0];
const unoptimizedPath = _unoptimizedPath ? _unoptimizedPath.collapse(orderOpts) : undefined;
const takerTokenToEthRate = side === MarketOperation.Sell ? ethToInputRate : ethToOutputRate;
const makerTokenToEthRate = side === MarketOperation.Sell ? ethToOutputRate : ethToInputRate;
// Find the optimal path
const optimalPath = await findOptimalPathAsync(side, fills, inputAmount, opts.runLimit, optimizerOpts);
@ -564,9 +519,8 @@ export class MarketOperationUtils {
sourceFlags: SOURCE_FLAGS[ERC20BridgeSource.MultiHop],
marketSideLiquidity,
adjustedRate: bestTwoHopRate,
unoptimizedPath,
takerAssetToEthRate,
makerAssetToEthRate,
takerTokenToEthRate,
makerTokenToEthRate,
};
}
@ -599,14 +553,16 @@ export class MarketOperationUtils {
sourceFlags: collapsedPath.sourceFlags,
marketSideLiquidity,
adjustedRate: optimalPathRate,
unoptimizedPath,
takerAssetToEthRate,
makerAssetToEthRate,
takerTokenToEthRate,
makerTokenToEthRate,
};
}
private async _getMarketSideOrdersAsync(
nativeOrders: SignedOrder[],
/**
* @param nativeOrders: Assumes LimitOrders not RfqOrders
*/
public async getOptimizerResultAsync(
nativeOrders: SignedNativeOrder[],
amount: BigNumber,
side: MarketOperation,
opts?: Partial<GetMarketOrdersOpts>,
@ -621,12 +577,16 @@ export class MarketOperationUtils {
exchangeProxyOverhead: _opts.exchangeProxyOverhead,
};
if (nativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders);
}
// Compute an optimized path for on-chain DEX and open-orderbook. This should not include RFQ liquidity.
const marketLiquidityFnAsync =
side === MarketOperation.Sell
? this.getMarketSellLiquidityAsync.bind(this)
: this.getMarketBuyLiquidityAsync.bind(this);
let marketSideLiquidity: MarketSideLiquidity = await marketLiquidityFnAsync(nativeOrders, amount, _opts);
const marketSideLiquidity: MarketSideLiquidity = await marketLiquidityFnAsync(nativeOrders, amount, _opts);
let optimizerResult: OptimizerResult | undefined;
try {
optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, optimizerOpts);
@ -639,34 +599,39 @@ export class MarketOperationUtils {
}
}
// If RFQ liquidity is enabled, make a request to check RFQ liquidity
// Calculate a suggested price. For now, this is simply the overall price of the aggregation.
// We can use this as a comparison price for RFQ
let wholeOrderPrice: BigNumber | undefined;
if (optimizerResult) {
wholeOrderPrice = getComparisonPrices(
optimizerResult.adjustedRate,
amount,
marketSideLiquidity,
_opts.feeSchedule,
).wholeOrder;
}
// If RFQ liquidity is enabled, make a request to check RFQ liquidity against the first optimizer result
const { rfqt } = _opts;
if (rfqt && rfqt.quoteRequestor && marketSideLiquidity.quoteSourceFilters.isAllowed(ERC20BridgeSource.Native)) {
// Calculate a suggested price. For now, this is simply the overall price of the aggregation.
if (optimizerResult) {
wholeOrderPrice = getComparisonPrices(
optimizerResult.adjustedRate,
if (
marketSideLiquidity.isRfqSupported &&
rfqt &&
rfqt.quoteRequestor &&
marketSideLiquidity.quoteSourceFilters.isAllowed(ERC20BridgeSource.Native)
) {
// Timing of RFQT lifecycle
const timeStart = new Date().getTime();
const { makerToken, takerToken } = nativeOrders[0].order;
if (rfqt.isIndicative) {
// An indicative quote is being requested, and indicative quotes price-aware enabled
// Make the RFQT request and then re-run the sampler if new orders come back.
const indicativeQuotes = await rfqt.quoteRequestor.requestRfqtIndicativeQuotesAsync(
makerToken,
takerToken,
amount,
marketSideLiquidity,
_opts.feeSchedule,
).wholeOrder;
}
const { isFirmPriceAwareEnabled, isIndicativePriceAwareEnabled } = getPriceAwareRFQRolloutFlags(
rfqt.priceAwareRFQFlag,
);
if (rfqt.isIndicative && isIndicativePriceAwareEnabled) {
// An indicative quote is beingh requested, and indicative quotes price-aware enabled. Make the RFQT request and then re-run the sampler if new orders come back.
const timeStart = new Date().getTime();
const indicativeQuotes = await getRfqtIndicativeQuotesAsync(
nativeOrders[0].makerAssetData,
nativeOrders[0].takerAssetData,
side,
amount,
wholeOrderPrice,
_opts,
rfqt,
);
const deltaTime = new Date().getTime() - timeStart;
DEFAULT_INFO_LOGGER({
@ -675,57 +640,57 @@ export class MarketOperationUtils {
});
// Re-run optimizer with the new indicative quote
if (indicativeQuotes.length > 0) {
marketSideLiquidity = {
...marketSideLiquidity,
rfqtIndicativeQuotes: indicativeQuotes,
};
marketSideLiquidity.quotes.rfqtIndicativeQuotes = indicativeQuotes;
optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, optimizerOpts);
}
} else if (!rfqt.isIndicative && isFirmPriceAwareEnabled) {
// A firm quote is being requested, and firm quotes price-aware enabled. Ensure that `intentOnFilling` is enabled.
if (rfqt.intentOnFilling) {
// Extra validation happens when requesting a firm quote, such as ensuring that the takerAddress
// is indeed valid.
if (!rfqt.takerAddress || rfqt.takerAddress === NULL_ADDRESS) {
throw new Error('RFQ-T requests must specify a taker address');
}
const timeStart = new Date().getTime();
const firmQuotes = await rfqt.quoteRequestor.requestRfqtFirmQuotesAsync(
nativeOrders[0].makerAssetData,
nativeOrders[0].takerAssetData,
amount,
side,
wholeOrderPrice,
rfqt,
);
const deltaTime = new Date().getTime() - timeStart;
DEFAULT_INFO_LOGGER({
rfqQuoteType: 'firm',
deltaTime,
});
if (firmQuotes.length > 0) {
// Compute the RFQ order fillable amounts. This is done by performing a "soft" order
// validation and by checking order balances that are monitored by our worker.
// If a firm quote validator does not exist, then we assume that all orders are valid.
const firmQuoteSignedOrders = firmQuotes.map(quote => quote.signedOrder);
const rfqOrderFillableAmounts =
rfqt.firmQuoteValidator === undefined
? firmQuoteSignedOrders.map(signedOrder => signedOrder.takerAssetAmount)
: await rfqt.firmQuoteValidator.getRfqtTakerFillableAmountsAsync(firmQuoteSignedOrders);
} else {
// A firm quote is being requested, and firm quotes price-aware enabled.
// Ensure that `intentOnFilling` is enabled and make the request.
const firmQuotes = await rfqt.quoteRequestor.requestRfqtFirmQuotesAsync(
makerToken,
takerToken,
amount,
side,
wholeOrderPrice,
rfqt,
);
const deltaTime = new Date().getTime() - timeStart;
DEFAULT_INFO_LOGGER({
rfqQuoteType: 'firm',
deltaTime,
});
if (firmQuotes.length > 0) {
// Compute the RFQ order fillable amounts. This is done by performing a "soft" order
// validation and by checking order balances that are monitored by our worker.
// If a firm quote validator does not exist, then we assume that all orders are valid.
const rfqTakerFillableAmounts =
rfqt.firmQuoteValidator === undefined
? firmQuotes.map(signedOrder => signedOrder.order.takerAmount)
: await rfqt.firmQuoteValidator.getRfqtTakerFillableAmountsAsync(
firmQuotes.map(q => new RfqOrder(q.order)),
);
marketSideLiquidity = {
...marketSideLiquidity,
nativeOrders: marketSideLiquidity.nativeOrders.concat(firmQuoteSignedOrders),
orderFillableAmounts: marketSideLiquidity.orderFillableAmounts.concat(
rfqOrderFillableAmounts,
const quotesWithOrderFillableAmounts: NativeOrderWithFillableAmounts[] = firmQuotes.map(
(order, i) => ({
...order,
fillableTakerAmount: rfqTakerFillableAmounts[i],
// Adjust the maker amount by the available taker fill amount
fillableMakerAmount: getNativeAdjustedMakerFillAmount(
order.order,
rfqTakerFillableAmounts[i],
),
};
fillableTakerFeeAmount: ZERO_AMOUNT,
}),
);
marketSideLiquidity.quotes.nativeOrders = [
...quotesWithOrderFillableAmounts,
...marketSideLiquidity.quotes.nativeOrders,
];
// Re-run optimizer with the new firm quote. This is the second and last time
// we run the optimized in a block of code. In this case, we don't catch a potential `NoOptimalPath` exception
// and we let it bubble up if it happens.
optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, optimizerOpts);
}
// Re-run optimizer with the new firm quote. This is the second and last time
// we run the optimized in a block of code. In this case, we don't catch a potential `NoOptimalPath` exception
// and we let it bubble up if it happens.
optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, optimizerOpts);
}
}
}

View File

@ -39,7 +39,8 @@ export function getBestTwoHopQuote(
feeSchedule?: FeeSchedule,
exchangeProxyOverhead?: ExchangeProxyOverhead,
): { quote: DexSample<MultiHopFillData> | undefined; adjustedRate: BigNumber } {
const { side, inputAmount, ethToOutputRate, twoHopQuotes } = marketSideLiquidity;
const { side, inputAmount, ethToOutputRate, quotes } = marketSideLiquidity;
const { twoHopQuotes } = quotes;
// Ensure the expected data we require exists. In the case where all hops reverted
// or there were no sources included that allowed for multi hop,
// we can end up with empty, but not undefined, fill data

View File

@ -1,18 +1,15 @@
import { assetDataUtils, ERC20AssetData, generatePseudoRandomSalt, orderCalculationUtils } from '@0x/order-utils';
import { V3RFQIndicativeQuote } from '@0x/quote-server';
import { SignedOrder } from '@0x/types';
import { BridgeSource, FillQuoteTransformerOrderType } from '@0x/protocol-utils';
import { AbiEncoder, BigNumber } from '@0x/utils';
import { AssetSwapperContractAddresses, MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
import { AssetSwapperContractAddresses, MarketOperation } from '../../types';
import {
ERC20_PROXY_ID,
MAINNET_DODO_HELPER,
MAINNET_KYBER_NETWORK_PROXY,
MAINNET_MSTABLE_ROUTER,
MAINNET_OASIS_ROUTER,
MAINNET_UNISWAP_V1_ROUTER,
MAX_UINT256,
NULL_ADDRESS,
NULL_BYTES,
ONE_HOUR_IN_SECONDS,
ONE_SECOND_MS,
WALLET_SIGNATURE,
ZERO_AMOUNT,
} from './constants';
import {
@ -29,7 +26,11 @@ import {
MooniswapFillData,
MultiHopFillData,
NativeCollapsedFill,
NativeLimitOrderFillData,
NativeRfqOrderFillData,
OptimizedMarketBridgeOrder,
OptimizedMarketOrder,
OptimizedMarketOrderBase,
OrderDomain,
ShellFillData,
SnowSwapFillData,
@ -38,95 +39,7 @@ import {
UniswapV2FillData,
} from './types';
// tslint:disable completed-docs no-unnecessary-type-assertion
export function createDummyOrderForSampler(
makerAssetData: string,
takerAssetData: string,
makerAddress: string,
): SignedOrder {
return {
makerAddress,
takerAddress: NULL_ADDRESS,
senderAddress: NULL_ADDRESS,
feeRecipientAddress: NULL_ADDRESS,
salt: ZERO_AMOUNT,
expirationTimeSeconds: ZERO_AMOUNT,
makerAssetData,
takerAssetData,
makerFeeAssetData: NULL_BYTES,
takerFeeAssetData: NULL_BYTES,
makerFee: ZERO_AMOUNT,
takerFee: ZERO_AMOUNT,
makerAssetAmount: ZERO_AMOUNT,
takerAssetAmount: ZERO_AMOUNT,
signature: NULL_BYTES,
chainId: 1,
exchangeAddress: NULL_ADDRESS,
};
}
export function getNativeOrderTokens(order: SignedOrder): [string, string] {
const assets = [order.makerAssetData, order.takerAssetData].map(a => assetDataUtils.decodeAssetDataOrThrow(a)) as [
ERC20AssetData,
ERC20AssetData
];
if (assets.some(a => a.assetProxyId !== ERC20_PROXY_ID)) {
throw new Error(AggregationError.NotERC20AssetData);
}
return assets.map(a => a.tokenAddress.toLowerCase()) as [string, string];
}
export function convertNativeOrderToFullyFillableOptimizedOrders(order: SignedOrder): OptimizedMarketOrder {
return {
...order,
fillableMakerAssetAmount: order.makerAssetAmount,
fillableTakerAssetAmount: order.takerAssetAmount,
fillableTakerFeeAmount: order.takerFee,
fills: [],
};
}
/**
* Augments native orders with fillable amounts and filters out unfillable orders.
*/
export function createSignedOrdersWithFillableAmounts(
side: MarketOperation,
orders: SignedOrder[],
fillableAmounts: BigNumber[],
): SignedOrderWithFillableAmounts[] {
// Quick safety check: ensures that orders maps perfectly to fillable amounts.
if (orders.length !== fillableAmounts.length) {
throw new Error(
`Number of orders was ${orders.length} but fillable amounts was ${
fillableAmounts.length
}. This should never happen`,
);
}
return orders
.map((order: SignedOrder, i: number) => {
const fillableAmount = fillableAmounts[i];
const fillableMakerAssetAmount =
side === MarketOperation.Buy
? fillableAmount
: orderCalculationUtils.getMakerFillAmount(order, fillableAmount);
const fillableTakerAssetAmount =
side === MarketOperation.Sell
? fillableAmount
: orderCalculationUtils.getTakerFillAmount(order, fillableAmount);
const fillableTakerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, fillableTakerAssetAmount);
return {
...order,
fillableMakerAssetAmount,
fillableTakerAssetAmount,
fillableTakerFeeAmount,
};
})
.filter(order => {
return !order.fillableMakerAssetAmount.isZero() && !order.fillableTakerAssetAmount.isZero();
});
}
// tslint:disable completed-docs
export interface CreateOrderFromPathOpts {
side: MarketOperation;
@ -142,10 +55,11 @@ export function createOrdersFromTwoHopSample(
opts: CreateOrderFromPathOpts,
): OptimizedMarketOrder[] {
const [makerToken, takerToken] = getMakerTakerTokens(opts);
const { firstHopSource, secondHopSource, intermediateToken } = sample.fillData!;
const { firstHopSource, secondHopSource, intermediateToken } = sample.fillData;
const firstHopFill: CollapsedFill = {
sourcePathId: '',
source: firstHopSource.source,
type: FillQuoteTransformerOrderType.Bridge,
input: opts.side === MarketOperation.Sell ? sample.input : ZERO_AMOUNT,
output: opts.side === MarketOperation.Sell ? ZERO_AMOUNT : sample.output,
subFills: [],
@ -154,209 +68,158 @@ export function createOrdersFromTwoHopSample(
const secondHopFill: CollapsedFill = {
sourcePathId: '',
source: secondHopSource.source,
type: FillQuoteTransformerOrderType.Bridge,
input: opts.side === MarketOperation.Sell ? MAX_UINT256 : sample.input,
output: opts.side === MarketOperation.Sell ? sample.output : MAX_UINT256,
subFills: [],
fillData: secondHopSource.fillData,
};
return [
createBridgeOrder(firstHopFill, intermediateToken, takerToken, opts),
createBridgeOrder(secondHopFill, makerToken, intermediateToken, opts),
createBridgeOrder(firstHopFill, intermediateToken, takerToken, opts.side),
createBridgeOrder(secondHopFill, makerToken, intermediateToken, opts.side),
];
}
function getBridgeAddressFromFill(fill: CollapsedFill, opts: CreateOrderFromPathOpts): string {
switch (fill.source) {
case ERC20BridgeSource.Eth2Dai:
return opts.contractAddresses.eth2DaiBridge;
case ERC20BridgeSource.Kyber:
return opts.contractAddresses.kyberBridge;
case ERC20BridgeSource.Uniswap:
return opts.contractAddresses.uniswapBridge;
case ERC20BridgeSource.UniswapV2:
return opts.contractAddresses.uniswapV2Bridge;
case ERC20BridgeSource.SushiSwap:
return opts.contractAddresses.sushiswapBridge;
case ERC20BridgeSource.Curve:
return opts.contractAddresses.curveBridge;
case ERC20BridgeSource.Swerve:
return opts.contractAddresses.swerveBridge;
case ERC20BridgeSource.SnowSwap:
return opts.contractAddresses.snowswapBridge;
case ERC20BridgeSource.Bancor:
return opts.contractAddresses.bancorBridge;
export function getERC20BridgeSourceToBridgeSource(source: ERC20BridgeSource): BridgeSource {
switch (source) {
case ERC20BridgeSource.Balancer:
return opts.contractAddresses.balancerBridge;
return BridgeSource.Balancer;
case ERC20BridgeSource.Bancor:
return BridgeSource.Bancor;
// case ERC20BridgeSource.CoFiX:
// return BridgeSource.CoFiX;
case ERC20BridgeSource.Curve:
return BridgeSource.Curve;
case ERC20BridgeSource.Cream:
return opts.contractAddresses.creamBridge;
case ERC20BridgeSource.LiquidityProvider:
return (fill.fillData as LiquidityProviderFillData).poolAddress;
case ERC20BridgeSource.MStable:
return opts.contractAddresses.mStableBridge;
case ERC20BridgeSource.Mooniswap:
return opts.contractAddresses.mooniswapBridge;
case ERC20BridgeSource.Shell:
return opts.contractAddresses.shellBridge;
case ERC20BridgeSource.Dodo:
return opts.contractAddresses.dodoBridge;
return BridgeSource.Cream;
case ERC20BridgeSource.CryptoCom:
return opts.contractAddresses.cryptoComBridge;
return BridgeSource.CryptoCom;
case ERC20BridgeSource.Dodo:
return BridgeSource.Dodo;
case ERC20BridgeSource.Kyber:
return BridgeSource.Kyber;
case ERC20BridgeSource.LiquidityProvider:
return BridgeSource.LiquidityProvider;
case ERC20BridgeSource.Mooniswap:
return BridgeSource.Mooniswap;
case ERC20BridgeSource.MStable:
return BridgeSource.MStable;
case ERC20BridgeSource.Eth2Dai:
return BridgeSource.Oasis;
case ERC20BridgeSource.Shell:
return BridgeSource.Shell;
case ERC20BridgeSource.SnowSwap:
return BridgeSource.Snowswap;
case ERC20BridgeSource.SushiSwap:
return BridgeSource.Sushiswap;
case ERC20BridgeSource.Swerve:
return BridgeSource.Swerve;
case ERC20BridgeSource.Uniswap:
return BridgeSource.Uniswap;
case ERC20BridgeSource.UniswapV2:
return BridgeSource.UniswapV2;
default:
break;
throw new Error(AggregationError.NoBridgeForSource);
}
throw new Error(AggregationError.NoBridgeForSource);
}
export function createBridgeDataForBridgeOrder(order: OptimizedMarketBridgeOrder): string {
let bridgeData: string;
if (
order.source === ERC20BridgeSource.MultiHop ||
order.source === ERC20BridgeSource.MultiBridge ||
order.source === ERC20BridgeSource.Native
) {
throw new Error('Invalid order to encode for Bridge Data');
}
const encoder = BRIDGE_ENCODERS[order.source];
if (!encoder) {
throw new Error(AggregationError.NoBridgeForSource);
}
switch (order.source) {
case ERC20BridgeSource.Curve:
case ERC20BridgeSource.Swerve:
case ERC20BridgeSource.SnowSwap:
const curveFillData = (order as OptimizedMarketBridgeOrder<
CurveFillData | SwerveFillData | SnowSwapFillData
>).fillData;
bridgeData = encoder.encode([
curveFillData.pool.poolAddress,
curveFillData.pool.exchangeFunctionSelector,
curveFillData.fromTokenIdx,
curveFillData.toTokenIdx,
]);
break;
case ERC20BridgeSource.Balancer:
case ERC20BridgeSource.Cream:
const balancerFillData = (order as OptimizedMarketBridgeOrder<BalancerFillData>).fillData;
bridgeData = encoder.encode([balancerFillData.poolAddress]);
break;
case ERC20BridgeSource.Bancor:
const bancorFillData = (order as OptimizedMarketBridgeOrder<BancorFillData>).fillData;
bridgeData = encoder.encode([bancorFillData.networkAddress, bancorFillData.path]);
break;
case ERC20BridgeSource.UniswapV2:
case ERC20BridgeSource.SushiSwap:
case ERC20BridgeSource.CryptoCom:
const uniswapV2FillData = (order as OptimizedMarketBridgeOrder<UniswapV2FillData | SushiSwapFillData>)
.fillData;
bridgeData = encoder.encode([uniswapV2FillData.router, uniswapV2FillData.tokenAddressPath]);
break;
case ERC20BridgeSource.Kyber:
const kyberFillData = (order as OptimizedMarketBridgeOrder<KyberFillData>).fillData;
bridgeData = encoder.encode([MAINNET_KYBER_NETWORK_PROXY, kyberFillData.hint]);
break;
case ERC20BridgeSource.Mooniswap:
const mooniswapFillData = (order as OptimizedMarketBridgeOrder<MooniswapFillData>).fillData;
bridgeData = encoder.encode([mooniswapFillData.poolAddress]);
break;
case ERC20BridgeSource.Dodo:
const dodoFillData = (order as OptimizedMarketBridgeOrder<DODOFillData>).fillData;
bridgeData = encoder.encode([MAINNET_DODO_HELPER, dodoFillData.poolAddress, dodoFillData.isSellBase]);
break;
case ERC20BridgeSource.Shell:
const shellFillData = (order as OptimizedMarketBridgeOrder<ShellFillData>).fillData;
bridgeData = encoder.encode([shellFillData.poolAddress]);
break;
case ERC20BridgeSource.LiquidityProvider:
const lpFillData = (order as OptimizedMarketBridgeOrder<LiquidityProviderFillData>).fillData;
bridgeData = encoder.encode([lpFillData.poolAddress, tokenAddressEncoder.encode([order.takerToken])]);
break;
case ERC20BridgeSource.Uniswap:
bridgeData = encoder.encode([MAINNET_UNISWAP_V1_ROUTER]);
break;
case ERC20BridgeSource.Eth2Dai:
bridgeData = encoder.encode([MAINNET_OASIS_ROUTER]);
break;
case ERC20BridgeSource.MStable:
bridgeData = encoder.encode([MAINNET_MSTABLE_ROUTER]);
break;
default:
throw new Error(AggregationError.NoBridgeForSource);
}
return bridgeData;
}
export function createBridgeOrder(
fill: CollapsedFill,
makerToken: string,
takerToken: string,
opts: CreateOrderFromPathOpts,
): OptimizedMarketOrder {
const bridgeAddress = getBridgeAddressFromFill(fill, opts);
let makerAssetData;
switch (fill.source) {
case ERC20BridgeSource.Curve:
const curveFillData = (fill as CollapsedFill<CurveFillData>).fillData!; // tslint:disable-line:no-non-null-assertion
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createCurveBridgeData(
curveFillData.pool.poolAddress,
curveFillData.pool.exchangeFunctionSelector,
takerToken,
curveFillData.fromTokenIdx,
curveFillData.toTokenIdx,
),
);
break;
case ERC20BridgeSource.Swerve:
const swerveFillData = (fill as CollapsedFill<SwerveFillData>).fillData!; // tslint:disable-line:no-non-null-assertion
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createCurveBridgeData(
swerveFillData.pool.poolAddress,
swerveFillData.pool.exchangeFunctionSelector,
takerToken,
swerveFillData.fromTokenIdx,
swerveFillData.toTokenIdx,
),
);
break;
case ERC20BridgeSource.SnowSwap:
const snowSwapFillData = (fill as CollapsedFill<SnowSwapFillData>).fillData!; // tslint:disable-line:no-non-null-assertion
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createCurveBridgeData(
snowSwapFillData.pool.poolAddress,
snowSwapFillData.pool.exchangeFunctionSelector,
takerToken,
snowSwapFillData.fromTokenIdx,
snowSwapFillData.toTokenIdx,
),
);
break;
case ERC20BridgeSource.Balancer:
const balancerFillData = (fill as CollapsedFill<BalancerFillData>).fillData!; // tslint:disable-line:no-non-null-assertion
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createBalancerBridgeData(takerToken, balancerFillData.poolAddress),
);
break;
case ERC20BridgeSource.Cream:
const creamFillData = (fill as CollapsedFill<BalancerFillData>).fillData!; // tslint:disable-line:no-non-null-assertion
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createBalancerBridgeData(takerToken, creamFillData.poolAddress),
);
break;
case ERC20BridgeSource.Bancor:
const bancorFillData = (fill as CollapsedFill<BancorFillData>).fillData!; // tslint:disable-line:no-non-null-assertion
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createBancorBridgeData(bancorFillData.path, bancorFillData.networkAddress),
);
break;
case ERC20BridgeSource.UniswapV2:
const uniswapV2FillData = (fill as CollapsedFill<UniswapV2FillData>).fillData!; // tslint:disable-line:no-non-null-assertion
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createUniswapV2BridgeData(uniswapV2FillData.tokenAddressPath),
);
break;
case ERC20BridgeSource.SushiSwap:
const sushiSwapFillData = (fill as CollapsedFill<SushiSwapFillData>).fillData!; // tslint:disable-line:no-non-null-assertion
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createSushiSwapBridgeData(sushiSwapFillData.tokenAddressPath, sushiSwapFillData.router),
);
break;
case ERC20BridgeSource.CryptoCom:
const cryptoComFillData = (fill as CollapsedFill<SushiSwapFillData>).fillData!; // tslint:disable-line:no-non-null-assertion
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createSushiSwapBridgeData(cryptoComFillData.tokenAddressPath, cryptoComFillData.router),
);
break;
case ERC20BridgeSource.Kyber:
const kyberFillData = (fill as CollapsedFill<KyberFillData>).fillData!; // tslint:disable-line:no-non-null-assertion
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createKyberBridgeData(takerToken, kyberFillData.hint),
);
break;
case ERC20BridgeSource.Mooniswap:
const mooniswapFillData = (fill as CollapsedFill<MooniswapFillData>).fillData!; // tslint:disable-line:no-non-null-assertion
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createMooniswapBridgeData(takerToken, mooniswapFillData.poolAddress),
);
break;
case ERC20BridgeSource.Dodo:
const dodoFillData = (fill as CollapsedFill<DODOFillData>).fillData!; // tslint:disable-line:no-non-null-assertion
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createDODOBridgeData(takerToken, dodoFillData.poolAddress, dodoFillData.isSellBase),
);
break;
case ERC20BridgeSource.Shell:
const shellFillData = (fill as CollapsedFill<ShellFillData>).fillData!; // tslint:disable-line:no-non-null-assertion
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createShellBridgeData(takerToken, shellFillData.poolAddress),
);
break;
default:
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createBridgeData(takerToken),
);
}
const [slippedMakerAssetAmount, slippedTakerAssetAmount] = getSlippedBridgeAssetAmounts(fill, opts);
side: MarketOperation,
): OptimizedMarketBridgeOrder {
const [makerAmount, takerAmount] = getFillTokenAmounts(fill, side);
return {
makerToken,
takerToken,
makerAmount,
takerAmount,
fillData: fill.fillData,
source: fill.source,
sourcePathId: fill.sourcePathId,
type: FillQuoteTransformerOrderType.Bridge,
fills: [fill],
makerAssetData,
takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken),
makerAddress: bridgeAddress,
makerAssetAmount: slippedMakerAssetAmount,
takerAssetAmount: slippedTakerAssetAmount,
fillableMakerAssetAmount: slippedMakerAssetAmount,
fillableTakerAssetAmount: slippedTakerAssetAmount,
...createCommonBridgeOrderFields(opts.orderDomain),
};
}
@ -366,162 +229,80 @@ export function getMakerTakerTokens(opts: CreateOrderFromPathOpts): [string, str
return [makerToken, takerToken];
}
function createBridgeData(tokenAddress: string): string {
const encoder = AbiEncoder.create([{ name: 'tokenAddress', type: 'address' }]);
return encoder.encode({ tokenAddress });
}
const poolEncoder = AbiEncoder.create([{ name: 'poolAddress', type: 'address' }]);
const curveEncoder = AbiEncoder.create([
{ name: 'curveAddress', type: 'address' },
{ name: 'exchangeFunctionSelector', type: 'bytes4' },
{ name: 'fromTokenIdx', type: 'int128' },
{ name: 'toTokenIdx', type: 'int128' },
]);
const routerAddressPathEncoder = AbiEncoder.create('(address,address[])');
const tokenAddressEncoder = AbiEncoder.create([{ name: 'tokenAddress', type: 'address' }]);
function createBalancerBridgeData(takerToken: string, poolAddress: string): string {
const encoder = AbiEncoder.create([
{ name: 'takerToken', type: 'address' },
{ name: 'poolAddress', type: 'address' },
]);
return encoder.encode({ takerToken, poolAddress });
}
function createShellBridgeData(takerToken: string, poolAddress: string): string {
const encoder = AbiEncoder.create([
{ name: 'takerToken', type: 'address' },
{ name: 'poolAddress', type: 'address' },
]);
return encoder.encode({ takerToken, poolAddress });
}
function createBancorBridgeData(path: string[], networkAddress: string): string {
const encoder = AbiEncoder.create([
{ name: 'path', type: 'address[]' },
{ name: 'networkAddress', type: 'address' },
]);
return encoder.encode({ path, networkAddress });
}
function createKyberBridgeData(fromTokenAddress: string, hint: string): string {
const encoder = AbiEncoder.create([{ name: 'fromTokenAddress', type: 'address' }, { name: 'hint', type: 'bytes' }]);
return encoder.encode({ fromTokenAddress, hint });
}
function createMooniswapBridgeData(takerToken: string, poolAddress: string): string {
const encoder = AbiEncoder.create([
{ name: 'takerToken', type: 'address' },
{ name: 'poolAddress', type: 'address' },
]);
return encoder.encode({ takerToken, poolAddress });
}
function createDODOBridgeData(takerToken: string, poolAddress: string, isSellBase: boolean): string {
const encoder = AbiEncoder.create([
{ name: 'takerToken', type: 'address' },
export const BRIDGE_ENCODERS: {
[key in Exclude<
ERC20BridgeSource,
ERC20BridgeSource.Native | ERC20BridgeSource.MultiHop | ERC20BridgeSource.MultiBridge
>]: AbiEncoder.DataType
} = {
[ERC20BridgeSource.LiquidityProvider]: AbiEncoder.create([
{ name: 'provider', type: 'address' },
{ name: 'data', type: 'bytes' },
]),
[ERC20BridgeSource.Kyber]: AbiEncoder.create([
{ name: 'kyberNetworkProxy', type: 'address' },
{ name: 'hint', type: 'bytes' },
]),
[ERC20BridgeSource.Dodo]: AbiEncoder.create([
{ name: 'helper', type: 'address' },
{ name: 'poolAddress', type: 'address' },
{ name: 'isSellBase', type: 'bool' },
]);
return encoder.encode({ takerToken, poolAddress, isSellBase });
}
]),
// Curve like
[ERC20BridgeSource.Curve]: curveEncoder,
[ERC20BridgeSource.Swerve]: curveEncoder,
[ERC20BridgeSource.SnowSwap]: curveEncoder,
// UniswapV2 like, (router, address[])
[ERC20BridgeSource.Bancor]: routerAddressPathEncoder,
[ERC20BridgeSource.UniswapV2]: routerAddressPathEncoder,
[ERC20BridgeSource.SushiSwap]: routerAddressPathEncoder,
[ERC20BridgeSource.CryptoCom]: routerAddressPathEncoder,
// Generic pools
[ERC20BridgeSource.Shell]: poolEncoder,
[ERC20BridgeSource.Mooniswap]: poolEncoder,
[ERC20BridgeSource.Eth2Dai]: poolEncoder,
[ERC20BridgeSource.MStable]: poolEncoder,
[ERC20BridgeSource.Balancer]: poolEncoder,
[ERC20BridgeSource.Cream]: poolEncoder,
[ERC20BridgeSource.Uniswap]: poolEncoder,
};
function createCurveBridgeData(
curveAddress: string,
exchangeFunctionSelector: string,
takerToken: string,
fromTokenIdx: number,
toTokenIdx: number,
): string {
const encoder = AbiEncoder.create([
{ name: 'curveAddress', type: 'address' },
{ name: 'exchangeFunctionSelector', type: 'bytes4' },
{ name: 'fromTokenAddress', type: 'address' },
{ name: 'fromTokenIdx', type: 'int128' },
{ name: 'toTokenIdx', type: 'int128' },
]);
return encoder.encode([curveAddress, exchangeFunctionSelector, takerToken, fromTokenIdx, toTokenIdx]);
}
function createUniswapV2BridgeData(tokenAddressPath: string[]): string {
const encoder = AbiEncoder.create('(address[])');
return encoder.encode([tokenAddressPath]);
}
function createSushiSwapBridgeData(tokenAddressPath: string[], router: string): string {
const encoder = AbiEncoder.create('(address[],address)');
return encoder.encode([tokenAddressPath, router]);
}
function getSlippedBridgeAssetAmounts(fill: CollapsedFill, opts: CreateOrderFromPathOpts): [BigNumber, BigNumber] {
function getFillTokenAmounts(fill: CollapsedFill, side: MarketOperation): [BigNumber, BigNumber] {
return [
// Maker asset amount.
opts.side === MarketOperation.Sell
? fill.output.times(1 - opts.bridgeSlippage).integerValue(BigNumber.ROUND_DOWN)
: fill.input,
side === MarketOperation.Sell ? fill.output : fill.input,
// Taker asset amount.
opts.side === MarketOperation.Sell
? fill.input
: BigNumber.min(fill.output.times(opts.bridgeSlippage + 1).integerValue(BigNumber.ROUND_UP), MAX_UINT256),
side === MarketOperation.Sell ? fill.input : fill.output,
];
}
type CommonBridgeOrderFields = Pick<
OptimizedMarketOrder,
Exclude<
keyof OptimizedMarketOrder,
| 'fills'
| 'makerAddress'
| 'makerAssetData'
| 'takerAssetData'
| 'makerAssetAmount'
| 'takerAssetAmount'
| 'fillableMakerAssetAmount'
| 'fillableTakerAssetAmount'
>
>;
function createCommonBridgeOrderFields(orderDomain: OrderDomain): CommonBridgeOrderFields {
return {
takerAddress: NULL_ADDRESS,
senderAddress: NULL_ADDRESS,
feeRecipientAddress: NULL_ADDRESS,
salt: generatePseudoRandomSalt(),
// 2 hours from now
expirationTimeSeconds: new BigNumber(Math.floor(Date.now() / ONE_SECOND_MS) + ONE_HOUR_IN_SECONDS * 2),
makerFeeAssetData: NULL_BYTES,
takerFeeAssetData: NULL_BYTES,
makerFee: ZERO_AMOUNT,
takerFee: ZERO_AMOUNT,
fillableTakerFeeAmount: ZERO_AMOUNT,
signature: WALLET_SIGNATURE,
...orderDomain,
};
}
export function createNativeOrder(fill: NativeCollapsedFill): OptimizedMarketOrder {
return {
export function createNativeOptimizedOrder(
fill: NativeCollapsedFill,
side: MarketOperation,
): OptimizedMarketOrderBase<NativeLimitOrderFillData> | OptimizedMarketOrderBase<NativeRfqOrderFillData> {
const fillData = fill.fillData;
const [makerAmount, takerAmount] = getFillTokenAmounts(fill, side);
const base = {
type: fill.type,
source: ERC20BridgeSource.Native,
makerToken: fillData.order.makerToken,
takerToken: fillData.order.takerToken,
makerAmount,
takerAmount,
fills: [fill],
...fill.fillData!.order, // tslint:disable-line:no-non-null-assertion
fillData,
};
}
export function createSignedOrdersFromRfqtIndicativeQuotes(
quotes: V3RFQIndicativeQuote[],
): SignedOrderWithFillableAmounts[] {
return quotes.map(quote => {
return {
fillableMakerAssetAmount: quote.makerAssetAmount,
fillableTakerAssetAmount: quote.takerAssetAmount,
makerAssetAmount: quote.makerAssetAmount,
takerAssetAmount: quote.takerAssetAmount,
makerAssetData: quote.makerAssetData,
takerAssetData: quote.takerAssetData,
takerAddress: NULL_ADDRESS,
makerAddress: NULL_ADDRESS,
senderAddress: NULL_ADDRESS,
feeRecipientAddress: NULL_ADDRESS,
salt: ZERO_AMOUNT,
expirationTimeSeconds: quote.expirationTimeSeconds,
makerFeeAssetData: NULL_BYTES,
takerFeeAssetData: NULL_BYTES,
makerFee: ZERO_AMOUNT,
takerFee: ZERO_AMOUNT,
fillableTakerFeeAmount: ZERO_AMOUNT,
signature: WALLET_SIGNATURE,
chainId: 0,
exchangeAddress: NULL_ADDRESS,
};
});
return fill.type === FillQuoteTransformerOrderType.Rfq
? { ...base, type: FillQuoteTransformerOrderType.Rfq, fillData: fillData as NativeRfqOrderFillData }
: { ...base, type: FillQuoteTransformerOrderType.Limit, fillData: fillData as NativeLimitOrderFillData };
}

View File

@ -3,7 +3,7 @@ import { BigNumber } from '@0x/utils';
import { MarketOperation } from '../../types';
import { POSITIVE_INF, ZERO_AMOUNT } from './constants';
import { createBridgeOrder, createNativeOrder, CreateOrderFromPathOpts, getMakerTakerTokens } from './orders';
import { createBridgeOrder, createNativeOptimizedOrder, CreateOrderFromPathOpts, getMakerTakerTokens } from './orders';
import { getCompleteRate, getRate } from './rate_utils';
import {
CollapsedFill,
@ -105,11 +105,12 @@ export class Path {
this.orders = [];
for (let i = 0; i < collapsedFills.length; ) {
if (collapsedFills[i].source === ERC20BridgeSource.Native) {
this.orders.push(createNativeOrder(collapsedFills[i] as NativeCollapsedFill));
this.orders.push(createNativeOptimizedOrder(collapsedFills[i] as NativeCollapsedFill, opts.side));
++i;
continue;
}
// If there are contiguous bridge orders, we can batch them together.
// TODO jacob pretty sure this is from DFB and we can remove
const contiguousBridgeFills = [collapsedFills[i]];
for (let j = i + 1; j < collapsedFills.length; ++j) {
if (collapsedFills[j].source === ERC20BridgeSource.Native) {
@ -118,7 +119,7 @@ export class Path {
contiguousBridgeFills.push(collapsedFills[j]);
}
this.orders.push(createBridgeOrder(contiguousBridgeFills[0], makerToken, takerToken, opts));
this.orders.push(createBridgeOrder(contiguousBridgeFills[0], makerToken, takerToken, opts.side));
i += 1;
}
return this as CollapsedPath;
@ -236,6 +237,7 @@ export class Path {
(this.collapsedFills as CollapsedFill[]).push({
sourcePathId: fill.sourcePathId,
source: fill.source,
type: fill.type,
fillData: fill.fillData,
input: fill.input,
output: fill.output,

View File

@ -3,7 +3,7 @@ import { BigNumber, decodeBytesAsRevertError, logUtils } from '@0x/utils';
import { ERC20BridgeSamplerContract } from '../../wrappers';
import { ERC20BridgeSource, FillData, SourceInfo, SourceQuoteOperation } from './types';
import { ERC20BridgeSource, FillData, SourceQuoteOperation } from './types';
export type Parameters<T> = T extends (...args: infer TArgs) => any ? TArgs : never;
@ -28,7 +28,7 @@ export class SamplerContractOperation<
private readonly _params: Parameters<TFunc>;
private readonly _callback?: (callResults: string, fillData: TFillData) => BigNumber[];
constructor(opts: SourceInfo<TFillData> & SamplerContractCall<TFunc, TFillData>) {
constructor(opts: { source: ERC20BridgeSource; fillData?: TFillData } & SamplerContractCall<TFunc, TFillData>) {
this.source = opts.source;
this.fillData = opts.fillData || ({} as TFillData); // tslint:disable-line:no-object-literal-type-assertion
this._samplerContract = opts.contract;

View File

@ -1,8 +1,8 @@
import { SignedOrder } from '@0x/types';
import { LimitOrderFields } from '@0x/protocol-utils';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { SamplerCallResult } from '../../types';
import { SamplerCallResult, SignedNativeOrder } from '../../types';
import { ERC20BridgeSamplerContract } from '../../wrappers';
import { BalancerPoolsCache } from './balancer_utils';
@ -14,6 +14,7 @@ import {
MAINNET_MOONISWAP_V2_1_REGISTRY,
MAINNET_MOONISWAP_V2_REGISTRY,
MAINNET_SUSHI_SWAP_ROUTER,
MAINNET_UNISWAP_V2_ROUTER,
MAX_UINT256,
ZERO_AMOUNT,
} from './constants';
@ -93,30 +94,50 @@ export class SamplerOperations {
.catch(/* do nothing */);
}
public getTokenDecimals(makerTokenAddress: string, takerTokenAddress: string): BatchedOperation<BigNumber[]> {
public getTokenDecimals(tokens: string[]): BatchedOperation<BigNumber[]> {
return new SamplerContractOperation({
source: ERC20BridgeSource.Native,
contract: this._samplerContract,
function: this._samplerContract.getTokenDecimals,
params: [makerTokenAddress, takerTokenAddress],
params: [tokens],
});
}
public getOrderFillableTakerAmounts(orders: SignedOrder[], exchangeAddress: string): BatchedOperation<BigNumber[]> {
public isAddressContract(address: string): BatchedOperation<boolean> {
return {
encodeCall: () => this._samplerContract.isContract(address).getABIEncodedTransactionData(),
handleCallResults: (callResults: string) =>
this._samplerContract.getABIDecodedReturnData<boolean>('isContract', callResults),
handleRevert: () => {
/* should never happen */
throw new Error('Invalid address for isAddressContract');
},
};
}
public getLimitOrderFillableTakerAmounts(
orders: SignedNativeOrder[],
exchangeAddress: string,
): BatchedOperation<BigNumber[]> {
return new SamplerContractOperation({
source: ERC20BridgeSource.Native,
contract: this._samplerContract,
function: this._samplerContract.getOrderFillableTakerAssetAmounts,
params: [orders, orders.map(o => o.signature), exchangeAddress],
function: this._samplerContract.getLimitOrderFillableTakerAssetAmounts,
// tslint:disable-next-line:no-unnecessary-type-assertion
params: [orders.map(o => o.order as LimitOrderFields), orders.map(o => o.signature), exchangeAddress],
});
}
public getOrderFillableMakerAmounts(orders: SignedOrder[], exchangeAddress: string): BatchedOperation<BigNumber[]> {
public getLimitOrderFillableMakerAmounts(
orders: SignedNativeOrder[],
exchangeAddress: string,
): BatchedOperation<BigNumber[]> {
return new SamplerContractOperation({
source: ERC20BridgeSource.Native,
contract: this._samplerContract,
function: this._samplerContract.getOrderFillableMakerAssetAmounts,
params: [orders, orders.map(o => o.signature), exchangeAddress],
function: this._samplerContract.getLimitOrderFillableMakerAssetAmounts,
// tslint:disable-next-line:no-unnecessary-type-assertion
params: [orders.map(o => o.order as LimitOrderFields), orders.map(o => o.signature), exchangeAddress],
});
}
@ -196,10 +217,10 @@ export class SamplerOperations {
): SourceQuoteOperation<UniswapV2FillData> {
return new SamplerContractOperation({
source: ERC20BridgeSource.UniswapV2,
fillData: { tokenAddressPath },
fillData: { tokenAddressPath, router: MAINNET_UNISWAP_V2_ROUTER },
contract: this._samplerContract,
function: this._samplerContract.sampleSellsFromUniswapV2,
params: [tokenAddressPath, takerFillAmounts],
params: [MAINNET_UNISWAP_V2_ROUTER, tokenAddressPath, takerFillAmounts],
});
}
@ -209,10 +230,10 @@ export class SamplerOperations {
): SourceQuoteOperation<UniswapV2FillData> {
return new SamplerContractOperation({
source: ERC20BridgeSource.UniswapV2,
fillData: { tokenAddressPath },
fillData: { tokenAddressPath, router: MAINNET_UNISWAP_V2_ROUTER },
contract: this._samplerContract,
function: this._samplerContract.sampleBuysFromUniswapV2,
params: [tokenAddressPath, makerFillAmounts],
params: [MAINNET_UNISWAP_V2_ROUTER, tokenAddressPath, makerFillAmounts],
});
}

View File

@ -1,12 +1,16 @@
import { V3RFQIndicativeQuote } from '@0x/quote-server';
import { MarketOperation, SignedOrder } from '@0x/types';
import {
FillQuoteTransformerLimitOrderInfo,
FillQuoteTransformerOrderType,
FillQuoteTransformerRfqOrderInfo,
} from '@0x/protocol-utils';
import { V4RFQIndicativeQuote } from '@0x/quote-server';
import { MarketOperation } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { RfqtFirmQuoteValidator, RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../../types';
import { NativeOrderWithFillableAmounts, RfqtFirmQuoteValidator, RfqtRequestOpts } from '../../types';
import { QuoteRequestor } from '../../utils/quote_requestor';
import { QuoteReport } from '../quote_report_generator';
import { CollapsedPath } from './path';
import { SourceFilters } from './source_filters';
/**
@ -86,16 +90,18 @@ export interface SnowSwapInfo extends CurveInfo {}
// Internal `fillData` field for `Fill` objects.
export interface FillData {}
export interface SourceInfo<TFillData extends FillData = FillData> {
// `FillData` for native fills. Represents a single native order
export type NativeRfqOrderFillData = FillQuoteTransformerRfqOrderInfo;
export type NativeLimitOrderFillData = FillQuoteTransformerLimitOrderInfo;
export type NativeFillData = NativeRfqOrderFillData | NativeLimitOrderFillData;
// Represents an individual DEX sample from the sampler contract
export interface DexSample<TFillData extends FillData = FillData> {
source: ERC20BridgeSource;
fillData?: TFillData;
fillData: TFillData;
input: BigNumber;
output: BigNumber;
}
// `FillData` for native fills.
export interface NativeFillData extends FillData {
order: SignedOrderWithFillableAmounts;
}
export interface CurveFillData extends FillData {
fromTokenIdx: number;
toTokenIdx: number;
@ -120,12 +126,11 @@ export interface BalancerFillData extends FillData {
export interface UniswapV2FillData extends FillData {
tokenAddressPath: string[];
}
export interface SushiSwapFillData extends UniswapV2FillData {
router: string;
}
export interface SushiSwapFillData extends UniswapV2FillData {}
export interface ShellFillData extends FillData {
poolAddress: string;
}
@ -153,35 +158,25 @@ export interface DODOFillData extends FillData {
poolAddress: string;
isSellBase: boolean;
}
export interface Quote<TFillData = FillData> {
amount: BigNumber;
fillData?: TFillData;
}
export interface HopInfo {
sourceIndex: BigNumber;
returnData: string;
}
export interface MultiHopFillData extends FillData {
firstHopSource: SourceQuoteOperation;
secondHopSource: SourceQuoteOperation;
intermediateToken: string;
}
/**
* Represents an individual DEX sample from the sampler contract.
*/
export interface DexSample<TFillData extends FillData = FillData> extends SourceInfo<TFillData> {
input: BigNumber;
output: BigNumber;
export interface HopInfo {
sourceIndex: BigNumber;
returnData: string;
}
/**
* Represents a node on a fill path.
*/
export interface Fill<TFillData extends FillData = FillData> extends SourceInfo<TFillData> {
export interface Fill<TFillData extends FillData = FillData> {
// basic data for every fill
source: ERC20BridgeSource;
// TODO jacob people seem to agree that orderType here is more readable
type: FillQuoteTransformerOrderType; // should correspond with TFillData
fillData: TFillData;
// Unique ID of the original source path this fill belongs to.
// This is generated when the path is generated and is useful to distinguish
// paths that have the same `source` IDs but are distinct (e.g., Curves).
@ -203,7 +198,10 @@ export interface Fill<TFillData extends FillData = FillData> extends SourceInfo<
/**
* Represents continguous fills on a path that have been merged together.
*/
export interface CollapsedFill<TFillData extends FillData = FillData> extends SourceInfo<TFillData> {
export interface CollapsedFill<TFillData extends FillData = FillData> {
source: ERC20BridgeSource;
type: FillQuoteTransformerOrderType; // should correspond with TFillData
fillData: TFillData;
// Unique ID of the original source path this fill belongs to.
// This is generated when the path is generated and is useful to distinguish
// paths that have the same `source` IDs but are distinct (e.g., Curves).
@ -230,22 +228,48 @@ export interface CollapsedFill<TFillData extends FillData = FillData> extends So
*/
export interface NativeCollapsedFill extends CollapsedFill<NativeFillData> {}
export interface OptimizedMarketOrderBase<TFillData extends FillData = FillData> {
source: ERC20BridgeSource;
fillData: TFillData;
type: FillQuoteTransformerOrderType; // should correspond with TFillData
makerToken: string;
takerToken: string;
makerAmount: BigNumber; // The amount we wish to buy from this order, e.g inclusive of any previous partial fill
takerAmount: BigNumber; // The amount we wish to fill this for, e.g inclusive of any previous partial fill
fills: CollapsedFill[];
}
export interface OptimizedMarketBridgeOrder<TFillData extends FillData = FillData>
extends OptimizedMarketOrderBase<TFillData> {
type: FillQuoteTransformerOrderType.Bridge;
fillData: TFillData;
sourcePathId: string;
}
export interface OptimizedLimitOrder extends OptimizedMarketOrderBase<NativeLimitOrderFillData> {
type: FillQuoteTransformerOrderType.Limit;
fillData: NativeLimitOrderFillData;
}
export interface OptimizedRfqOrder extends OptimizedMarketOrderBase<NativeRfqOrderFillData> {
type: FillQuoteTransformerOrderType.Rfq;
fillData: NativeRfqOrderFillData;
}
/**
* Optimized orders to fill.
*/
export interface OptimizedMarketOrder extends SignedOrderWithFillableAmounts {
/**
* The optimized fills that generated this order.
*/
fills: CollapsedFill[];
}
export type OptimizedMarketOrder =
| OptimizedMarketBridgeOrder<FillData>
| OptimizedMarketOrderBase<NativeLimitOrderFillData>
| OptimizedMarketOrderBase<NativeRfqOrderFillData>;
export interface GetMarketOrdersRfqtOpts extends RfqtRequestOpts {
quoteRequestor?: QuoteRequestor;
firmQuoteValidator?: RfqtFirmQuoteValidator;
}
export type FeeEstimate = (fillData?: FillData) => number | BigNumber;
export type FeeEstimate = (fillData: FillData) => number | BigNumber;
export type FeeSchedule = Partial<{ [key in ERC20BridgeSource]: FeeEstimate }>;
export type ExchangeProxyOverhead = (sourceFlags: number) => BigNumber;
@ -313,6 +337,9 @@ export interface GetMarketOrdersOpts {
* sources. Defaults to `true`.
*/
allowFallback: boolean;
/**
* Options for RFQT such as takerAddress, intent on filling
*/
rfqt?: GetMarketOrdersRfqtOpts;
/**
* Whether to generate a quote report
@ -334,10 +361,9 @@ export interface BatchedOperation<TResult> {
handleRevert(callResults: string): TResult;
}
export interface SourceQuoteOperation<TFillData extends FillData = FillData>
extends BatchedOperation<BigNumber[]>,
SourceInfo<TFillData> {
export interface SourceQuoteOperation<TFillData extends FillData = FillData> extends BatchedOperation<BigNumber[]> {
readonly source: ERC20BridgeSource;
fillData: TFillData;
}
export interface OptimizerResult {
@ -346,9 +372,8 @@ export interface OptimizerResult {
liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>;
marketSideLiquidity: MarketSideLiquidity;
adjustedRate: BigNumber;
unoptimizedPath?: CollapsedPath;
takerAssetToEthRate: BigNumber;
makerAssetToEthRate: BigNumber;
takerTokenToEthRate: BigNumber;
makerTokenToEthRate: BigNumber;
}
export interface OptimizerResultWithReport extends OptimizerResult {
@ -369,16 +394,20 @@ export interface MarketSideLiquidity {
inputAmount: BigNumber;
inputToken: string;
outputToken: string;
dexQuotes: Array<Array<DexSample<FillData>>>;
nativeOrders: SignedOrder[];
orderFillableAmounts: BigNumber[];
ethToOutputRate: BigNumber;
ethToInputRate: BigNumber;
rfqtIndicativeQuotes: V3RFQIndicativeQuote[];
twoHopQuotes: Array<DexSample<MultiHopFillData>>;
quoteSourceFilters: SourceFilters;
makerTokenDecimals: number;
takerTokenDecimals: number;
quotes: RawQuotes;
isRfqSupported: boolean;
}
export interface RawQuotes {
nativeOrders: NativeOrderWithFillableAmounts[];
rfqtIndicativeQuotes: V4RFQIndicativeQuote[];
twoHopQuotes: Array<DexSample<MultiHopFillData>>;
dexQuotes: Array<Array<DexSample<FillData>>>;
}
export interface TokenAdjacencyGraph {

View File

@ -1,29 +0,0 @@
import { orderCalculationUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types';
import * as _ from 'lodash';
import { constants } from '../constants';
import { OrderPrunerPermittedFeeTypes } from '../types';
import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from '../utils/utils';
export const orderPrunerUtils = {
pruneForUsableSignedOrders(
signedOrders: SignedOrder[],
permittedOrderFeeTypes: Set<OrderPrunerPermittedFeeTypes>,
expiryBufferMs: number,
): SignedOrder[] {
const result = _.filter(signedOrders, order => {
return (
orderCalculationUtils.isOpenOrder(order) &&
!orderCalculationUtils.willOrderExpire(order, expiryBufferMs / constants.ONE_SECOND_MS) &&
((permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.NoFees) &&
order.takerFee.eq(constants.ZERO_AMOUNT)) ||
(permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee) &&
isOrderTakerFeePayableWithTakerAsset(order)) ||
(permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee) &&
isOrderTakerFeePayableWithMakerAsset(order)))
);
});
return result;
},
};

View File

@ -1,49 +0,0 @@
import { DevUtilsContract } from '@0x/contract-wrappers';
import { orderCalculationUtils } from '@0x/order-utils';
import { OrderStatus, SignedOrder } from '@0x/types';
import { constants } from '../constants';
import { OrderPrunerOnChainMetadata, SignedOrderWithFillableAmounts } from '../types';
/**
* Utility class to retrieve order state if needed outside of using the ERC20BridgeSampler
*/
export class OrderStateUtils {
private readonly _devUtils: DevUtilsContract;
constructor(devUtils: DevUtilsContract) {
this._devUtils = devUtils;
}
public async getSignedOrdersWithFillableAmountsAsync(
signedOrders: SignedOrder[],
): Promise<SignedOrderWithFillableAmounts[]> {
const signatures = signedOrders.map(o => o.signature);
const [ordersInfo, fillableTakerAssetAmounts, isValidSignatures] = await this._devUtils
.getOrderRelevantStates(signedOrders, signatures)
.callAsync();
const ordersOnChainMetadata: OrderPrunerOnChainMetadata[] = ordersInfo.map((orderInfo, index) => {
return {
...orderInfo,
fillableTakerAssetAmount: fillableTakerAssetAmounts[index],
isValidSignature: isValidSignatures[index],
};
});
// take orders + on chain information and find the valid orders and fillable makerAsset or takerAsset amounts
return signedOrders.map(
(order: SignedOrder, index: number): SignedOrderWithFillableAmounts => {
const orderMetadata = ordersOnChainMetadata[index];
const fillableTakerAssetAmount =
orderMetadata.isValidSignature && orderMetadata.orderStatus === OrderStatus.Fillable
? orderMetadata.fillableTakerAssetAmount
: constants.ZERO_AMOUNT;
return {
...order,
fillableTakerAssetAmount,
fillableMakerAssetAmount: orderCalculationUtils.getMakerFillAmount(order, fillableTakerAssetAmount),
fillableTakerFeeAmount: orderCalculationUtils.getTakerFeeAmount(order, fillableTakerAssetAmount),
};
},
);
}
}

View File

@ -1,7 +1,8 @@
import { SignedOrder } from '@0x/types';
import { FillQuoteTransformerOrderType, Signature } from '@0x/protocol-utils';
import { BigNumber } from '@0x/utils';
import _ = require('lodash');
import { MarketOperation } from '../types';
import { MarketOperation, NativeOrderWithFillableAmounts } from '../types';
import {
CollapsedFill,
@ -10,48 +11,52 @@ import {
FillData,
MultiHopFillData,
NativeCollapsedFill,
NativeFillData,
NativeLimitOrderFillData,
NativeRfqOrderFillData,
} from './market_operation_utils/types';
import { QuoteRequestor } from './quote_requestor';
export interface BridgeReportSource {
liquiditySource: Exclude<ERC20BridgeSource, ERC20BridgeSource.Native>;
export interface QuoteReportEntryBase {
liquiditySource: ERC20BridgeSource;
makerAmount: BigNumber;
takerAmount: BigNumber;
fillData?: FillData;
}
export interface MultiHopReportSource {
liquiditySource: ERC20BridgeSource.MultiHop;
makerAmount: BigNumber;
takerAmount: BigNumber;
hopSources: ERC20BridgeSource[];
fillData: FillData;
}
interface NativeReportSourceBase {
liquiditySource: ERC20BridgeSource.Native;
makerAmount: BigNumber;
takerAmount: BigNumber;
nativeOrder: SignedOrder;
fillableTakerAmount: BigNumber;
export interface BridgeQuoteReportEntry extends QuoteReportEntryBase {
liquiditySource: Exclude<ERC20BridgeSource, ERC20BridgeSource.Native>;
}
export interface NativeOrderbookReportSource extends NativeReportSourceBase {
export interface MultiHopQuoteReportEntry extends QuoteReportEntryBase {
liquiditySource: ERC20BridgeSource.MultiHop;
hopSources: ERC20BridgeSource[];
}
export interface NativeLimitOrderQuoteReportEntry extends QuoteReportEntryBase {
liquiditySource: ERC20BridgeSource.Native;
fillData: NativeFillData;
fillableTakerAmount: BigNumber;
isRfqt: false;
}
export interface NativeRFQTReportSource extends NativeReportSourceBase {
export interface NativeRfqOrderQuoteReportEntry extends QuoteReportEntryBase {
liquiditySource: ERC20BridgeSource.Native;
fillData: NativeFillData;
fillableTakerAmount: BigNumber;
isRfqt: true;
makerUri: string;
comparisonPrice?: number;
}
export type QuoteReportSource =
| BridgeReportSource
| NativeOrderbookReportSource
| NativeRFQTReportSource
| MultiHopReportSource;
export type QuoteReportEntry =
| BridgeQuoteReportEntry
| MultiHopQuoteReportEntry
| NativeLimitOrderQuoteReportEntry
| NativeRfqOrderQuoteReportEntry;
export interface QuoteReport {
sourcesConsidered: QuoteReportSource[];
sourcesDelivered: QuoteReportSource[];
sourcesConsidered: QuoteReportEntry[];
sourcesDelivered: QuoteReportEntry[];
}
/**
@ -62,15 +67,14 @@ export function generateQuoteReport(
marketOperation: MarketOperation,
dexQuotes: DexSample[],
multiHopQuotes: Array<DexSample<MultiHopFillData>>,
nativeOrders: SignedOrder[],
orderFillableAmounts: BigNumber[],
nativeOrders: NativeOrderWithFillableAmounts[],
liquidityDelivered: ReadonlyArray<CollapsedFill> | DexSample<MultiHopFillData>,
comparisonPrice?: BigNumber | undefined,
quoteRequestor?: QuoteRequestor,
): QuoteReport {
const dexReportSourcesConsidered = dexQuotes.map(quote => _dexSampleToReportSource(quote, marketOperation));
const nativeOrderSourcesConsidered = nativeOrders.map((order, idx) =>
_nativeOrderToReportSource(order, orderFillableAmounts[idx], comparisonPrice, quoteRequestor),
const nativeOrderSourcesConsidered = nativeOrders.map(order =>
_nativeOrderToReportEntry(order.type, order as any, order.fillableTakerAmount, comparisonPrice, quoteRequestor),
);
const multiHopSourcesConsidered = multiHopQuotes.map(quote =>
_multiHopSampleToReportSource(quote, marketOperation),
@ -84,17 +88,18 @@ export function generateQuoteReport(
let sourcesDelivered;
if (Array.isArray(liquidityDelivered)) {
// create easy way to look up fillable amounts
const nativeOrderSignaturesToFillableAmounts = _nativeOrderSignaturesToFillableAmounts(
nativeOrders,
orderFillableAmounts,
const nativeOrderSignaturesToFillableAmounts = _.fromPairs(
nativeOrders.map(o => {
return [_nativeDataToId(o), o.fillableTakerAmount];
}),
);
// map sources delivered
sourcesDelivered = liquidityDelivered.map(collapsedFill => {
const foundNativeOrder = _nativeOrderFromCollapsedFill(collapsedFill);
if (foundNativeOrder) {
return _nativeOrderToReportSource(
foundNativeOrder,
nativeOrderSignaturesToFillableAmounts[foundNativeOrder.signature],
if (_isNativeOrderFromCollapsedFill(collapsedFill)) {
return _nativeOrderToReportEntry(
collapsedFill.type,
collapsedFill.fillData,
nativeOrderSignaturesToFillableAmounts[_nativeDataToId(collapsedFill.fillData)],
comparisonPrice,
quoteRequestor,
);
@ -104,6 +109,7 @@ export function generateQuoteReport(
});
} else {
sourcesDelivered = [
// tslint:disable-next-line: no-unnecessary-type-assertion
_multiHopSampleToReportSource(liquidityDelivered as DexSample<MultiHopFillData>, marketOperation),
];
}
@ -113,7 +119,12 @@ export function generateQuoteReport(
};
}
function _dexSampleToReportSource(ds: DexSample, marketOperation: MarketOperation): BridgeReportSource {
function _nativeDataToId(data: { signature: Signature }): string {
const { v, r, s } = data.signature;
return `${v}${r}${s}`;
}
function _dexSampleToReportSource(ds: DexSample, marketOperation: MarketOperation): BridgeQuoteReportEntry {
const liquiditySource = ds.source;
if (liquiditySource === ERC20BridgeSource.Native) {
@ -144,8 +155,8 @@ function _dexSampleToReportSource(ds: DexSample, marketOperation: MarketOperatio
function _multiHopSampleToReportSource(
ds: DexSample<MultiHopFillData>,
marketOperation: MarketOperation,
): MultiHopReportSource {
const { firstHopSource: firstHop, secondHopSource: secondHop } = ds.fillData!;
): MultiHopQuoteReportEntry {
const { firstHopSource: firstHop, secondHopSource: secondHop } = ds.fillData;
// input and output map to different values
// based on the market operation
if (marketOperation === MarketOperation.Buy) {
@ -153,7 +164,7 @@ function _multiHopSampleToReportSource(
liquiditySource: ERC20BridgeSource.MultiHop,
makerAmount: ds.input,
takerAmount: ds.output,
fillData: ds.fillData!,
fillData: ds.fillData,
hopSources: [firstHop.source, secondHop.source],
};
} else if (marketOperation === MarketOperation.Sell) {
@ -161,7 +172,7 @@ function _multiHopSampleToReportSource(
liquiditySource: ERC20BridgeSource.MultiHop,
makerAmount: ds.output,
takerAmount: ds.input,
fillData: ds.fillData!,
fillData: ds.fillData,
hopSources: [firstHop.source, secondHop.source],
};
} else {
@ -169,65 +180,44 @@ function _multiHopSampleToReportSource(
}
}
function _nativeOrderSignaturesToFillableAmounts(
nativeOrders: SignedOrder[],
fillableAmounts: BigNumber[],
): { [orderSignature: string]: BigNumber } {
// create easy way to look up fillable amounts based on native order signatures
if (fillableAmounts.length !== nativeOrders.length) {
// length mismatch, abort
throw new Error('orderFillableAmounts must be the same length as nativeOrders');
}
const nativeOrderSignaturesToFillableAmounts: { [orderSignature: string]: BigNumber } = {};
nativeOrders.forEach((nativeOrder, idx) => {
nativeOrderSignaturesToFillableAmounts[nativeOrder.signature] = fillableAmounts[idx];
});
return nativeOrderSignaturesToFillableAmounts;
function _isNativeOrderFromCollapsedFill(cf: CollapsedFill): cf is NativeCollapsedFill {
const { type } = cf;
return type === FillQuoteTransformerOrderType.Limit || type === FillQuoteTransformerOrderType.Rfq;
}
function _nativeOrderFromCollapsedFill(cf: CollapsedFill): SignedOrder | undefined {
// Cast as NativeCollapsedFill and then check
// if it really is a NativeCollapsedFill
const possibleNativeCollapsedFill = cf as NativeCollapsedFill;
if (possibleNativeCollapsedFill.fillData && possibleNativeCollapsedFill.fillData.order) {
return possibleNativeCollapsedFill.fillData.order;
} else {
return undefined;
}
}
function _nativeOrderToReportSource(
nativeOrder: SignedOrder,
function _nativeOrderToReportEntry(
type: FillQuoteTransformerOrderType,
fillData: NativeLimitOrderFillData | NativeRfqOrderFillData,
fillableAmount: BigNumber,
comparisonPrice?: BigNumber | undefined,
quoteRequestor?: QuoteRequestor,
): NativeRFQTReportSource | NativeOrderbookReportSource {
const nativeOrderBase: NativeReportSourceBase = {
): NativeRfqOrderQuoteReportEntry | NativeLimitOrderQuoteReportEntry {
const nativeOrderBase = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: nativeOrder.makerAssetAmount,
takerAmount: nativeOrder.takerAssetAmount,
makerAmount: fillData.order.makerAmount,
takerAmount: fillData.order.takerAmount,
fillableTakerAmount: fillableAmount,
nativeOrder,
};
// if we find this is an rfqt order, label it as such and associate makerUri
const foundRfqtMakerUri = quoteRequestor && quoteRequestor.getMakerUriForOrderSignature(nativeOrder.signature);
if (foundRfqtMakerUri) {
const rfqtSource: NativeRFQTReportSource = {
const isRfqt = type === FillQuoteTransformerOrderType.Rfq;
const rfqtMakerUri = isRfqt ? quoteRequestor!.getMakerUriForSignature(fillData.signature) : undefined;
if (isRfqt) {
// tslint:disable-next-line: no-object-literal-type-assertion
return {
...nativeOrderBase,
isRfqt: true,
makerUri: foundRfqtMakerUri,
};
if (comparisonPrice) {
rfqtSource.comparisonPrice = comparisonPrice.toNumber();
}
return rfqtSource;
makerUri: rfqtMakerUri || '',
...(comparisonPrice ? { comparisonPrice: comparisonPrice.toNumber() } : {}),
fillData,
} as NativeRfqOrderQuoteReportEntry;
} else {
// if it's not an rfqt order, treat as normal
const regularNativeOrder: NativeOrderbookReportSource = {
// tslint:disable-next-line: no-object-literal-type-assertion
return {
...nativeOrderBase,
isRfqt: false,
};
return regularNativeOrder;
fillData,
} as NativeLimitOrderQuoteReportEntry;
}
}

View File

@ -1,14 +1,13 @@
import { schemas, SchemaValidator } from '@0x/json-schemas';
import { assetDataUtils, orderCalculationUtils, SignedOrder } from '@0x/order-utils';
import { TakerRequestQueryParams, V3RFQFirmQuote, V3RFQIndicativeQuote } from '@0x/quote-server';
import { ERC20AssetData } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { FillQuoteTransformerOrderType, Signature } from '@0x/protocol-utils';
import { TakerRequestQueryParams, V4RFQFirmQuote, V4RFQIndicativeQuote, V4SignedRfqOrder } from '@0x/quote-server';
import { BigNumber, NULL_ADDRESS } from '@0x/utils';
import Axios, { AxiosInstance } from 'axios';
import { Agent as HttpAgent } from 'http';
import { Agent as HttpsAgent } from 'https';
import { constants } from '../constants';
import { LogFunction, MarketOperation, RfqtMakerAssetOfferings, RfqtRequestOpts } from '../types';
import { LogFunction, MarketOperation, RfqtMakerAssetOfferings, RfqtRequestOpts, SignedNativeOrder } from '../types';
import { ONE_SECOND_MS } from './market_operation_utils/constants';
import { RfqMakerBlacklist } from './rfq_maker_blacklist';
@ -25,34 +24,17 @@ const MAKER_TIMEOUT_STREAK_LENGTH = 10;
const MAKER_TIMEOUT_BLACKLIST_DURATION_MINUTES = 10;
const rfqMakerBlacklist = new RfqMakerBlacklist(MAKER_TIMEOUT_STREAK_LENGTH, MAKER_TIMEOUT_BLACKLIST_DURATION_MINUTES);
interface RfqQuote<T> {
response: T;
makerUri: string;
}
/**
* Request quotes from RFQ-T providers
*/
function getTokenAddressOrThrow(assetData: string): string {
const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData);
if (decodedAssetData.hasOwnProperty('tokenAddress')) {
// type cast necessary here as decodeAssetDataOrThrow returns
// an AssetData object, which doesn't necessarily contain a
// token address. (it could possibly be a StaticCallAssetData,
// which lacks an address.) so we'll just assume it's a token
// here. should be safe, with the enclosing guard condition
// and subsequent error.
// tslint:disable-next-line:no-unnecessary-type-assertion
return (decodedAssetData as ERC20AssetData).tokenAddress;
}
throw new Error(`Decoded asset data (${JSON.stringify(decodedAssetData)}) does not contain a token address`);
}
function hasExpectedAssetData(
expectedMakerAssetData: string,
expectedTakerAssetData: string,
makerAssetDataInQuestion: string,
takerAssetDataInQuestion: string,
): boolean {
const hasExpectedMakerAssetData = makerAssetDataInQuestion.toLowerCase() === expectedMakerAssetData.toLowerCase();
const hasExpectedTakerAssetData = takerAssetDataInQuestion.toLowerCase() === expectedTakerAssetData.toLowerCase();
return hasExpectedMakerAssetData && hasExpectedTakerAssetData;
function hasExpectedAddresses(comparisons: Array<[string, string]>): boolean {
return comparisons.every(c => c[0].toLowerCase() === c[1].toLowerCase());
}
function convertIfAxiosError(error: any): Error | object /* axios' .d.ts has AxiosError.toJSON() returning object */ {
@ -84,20 +66,24 @@ function convertIfAxiosError(error: any): Error | object /* axios' .d.ts has Axi
}
}
function nativeDataToId(data: { signature: Signature }): string {
const { v, r, s } = data.signature;
return `${v}${r}${s}`;
}
export class QuoteRequestor {
private readonly _schemaValidator: SchemaValidator = new SchemaValidator();
private readonly _orderSignatureToMakerUri: { [orderSignature: string]: string } = {};
private readonly _orderSignatureToMakerUri: { [signature: string]: string } = {};
public static makeQueryParameters(
txOrigin: string,
takerAddress: string,
marketOperation: MarketOperation,
makerAssetData: string,
takerAssetData: string,
buyTokenAddress: string, // maker token
sellTokenAddress: string, // taker token
assetFillAmount: BigNumber,
comparisonPrice?: BigNumber,
): TakerRequestQueryParams {
const buyTokenAddress = getTokenAddressOrThrow(makerAssetData);
const sellTokenAddress = getTokenAddressOrThrow(takerAssetData);
const { buyAmountBaseUnits, sellAmountBaseUnits } =
marketOperation === MarketOperation.Buy
? {
@ -111,15 +97,14 @@ export class QuoteRequestor {
const requestParamsWithBigNumbers: Pick<
TakerRequestQueryParams,
'buyTokenAddress' | 'sellTokenAddress' | 'takerAddress' | 'comparisonPrice' | 'protocolVersion'
'buyTokenAddress' | 'sellTokenAddress' | 'txOrigin' | 'comparisonPrice' | 'protocolVersion' | 'takerAddress'
> = {
txOrigin,
takerAddress,
comparisonPrice: comparisonPrice === undefined ? undefined : comparisonPrice.toString(),
buyTokenAddress,
sellTokenAddress,
// The request parameter below defines what protocol version the RFQ servers should be returning.
protocolVersion: '3',
protocolVersion: '4',
};
// convert BigNumbers to strings
@ -149,105 +134,95 @@ export class QuoteRequestor {
}
public async requestRfqtFirmQuotesAsync(
makerAssetData: string,
takerAssetData: string,
makerToken: string, // maker token
takerToken: string, // taker token
assetFillAmount: BigNumber,
marketOperation: MarketOperation,
comparisonPrice: BigNumber | undefined,
options: RfqtRequestOpts,
): Promise<V3RFQFirmQuote[]> {
): Promise<SignedNativeOrder[]> {
const _opts: RfqtRequestOpts = { ...constants.DEFAULT_RFQT_REQUEST_OPTS, ...options };
if (
_opts.takerAddress === undefined ||
_opts.takerAddress === '' ||
_opts.takerAddress === '0x' ||
!_opts.takerAddress ||
_opts.takerAddress === constants.NULL_ADDRESS
) {
throw new Error('RFQ-T firm quotes require the presence of a taker address');
if (!_opts.txOrigin || [undefined, '', '0x', NULL_ADDRESS].includes(_opts.txOrigin)) {
throw new Error('RFQ-T firm quotes require the presence of a tx origin');
}
const firmQuoteResponses = await this._getQuotesAsync<V3RFQFirmQuote>( // not yet BigNumber
makerAssetData,
takerAssetData,
const quotesRaw = await this._getQuotesAsync<V4RFQFirmQuote>(
makerToken,
takerToken,
assetFillAmount,
marketOperation,
comparisonPrice,
_opts,
'firm',
);
const quotes = quotesRaw.map(result => ({ ...result, response: result.response.signedOrder }));
const result: V3RFQFirmQuote[] = [];
firmQuoteResponses.forEach(firmQuoteResponse => {
const orderWithStringInts = firmQuoteResponse.response.signedOrder;
// validate
const validationFunction = (o: V4SignedRfqOrder) => {
try {
const hasValidSchema = this._schemaValidator.isValid(orderWithStringInts, schemas.signedOrderSchema);
if (!hasValidSchema) {
throw new Error('Order not valid');
}
} catch (err) {
this._warningLogger(orderWithStringInts, `Invalid RFQ-t order received, filtering out. ${err.message}`);
return;
// Handle the validate throwing, i.e if it isn't an object or json response
return this._schemaValidator.isValid(o, schemas.v4RfqSignedOrderSchema);
} catch (e) {
return false;
}
};
const validQuotes = quotes.filter(result => {
const order = result.response;
if (!validationFunction(order)) {
this._warningLogger(result, 'Invalid RFQ-T firm quote received, filtering out');
return false;
}
if (
!hasExpectedAssetData(
makerAssetData,
takerAssetData,
orderWithStringInts.makerAssetData.toLowerCase(),
orderWithStringInts.takerAssetData.toLowerCase(),
)
!hasExpectedAddresses([
[makerToken, order.makerToken],
[takerToken, order.takerToken],
[_opts.takerAddress, order.taker],
[_opts.txOrigin, order.txOrigin],
])
) {
this._warningLogger(orderWithStringInts, 'Unexpected asset data in RFQ-T order, filtering out');
return;
this._warningLogger(
order,
'Unexpected token, tx origin or taker address in RFQ-T order, filtering out',
);
return false;
}
if (orderWithStringInts.takerAddress.toLowerCase() !== _opts.takerAddress.toLowerCase()) {
this._warningLogger(orderWithStringInts, 'Unexpected takerAddress in RFQ-T order, filtering out');
return;
if (this._isExpirationTooSoon(new BigNumber(order.expiry))) {
this._warningLogger(order, 'Expiry too soon in RFQ-T firm quote, filtering out');
return false;
} else {
return true;
}
const orderWithBigNumberInts: SignedOrder = {
...orderWithStringInts,
makerAssetAmount: new BigNumber(orderWithStringInts.makerAssetAmount),
takerAssetAmount: new BigNumber(orderWithStringInts.takerAssetAmount),
makerFee: new BigNumber(orderWithStringInts.makerFee),
takerFee: new BigNumber(orderWithStringInts.takerFee),
expirationTimeSeconds: new BigNumber(orderWithStringInts.expirationTimeSeconds),
salt: new BigNumber(orderWithStringInts.salt),
};
if (
orderCalculationUtils.willOrderExpire(
orderWithBigNumberInts,
this._expiryBufferMs / constants.ONE_SECOND_MS,
)
) {
this._warningLogger(orderWithBigNumberInts, 'Expiry too soon in RFQ-T order, filtering out');
return;
}
// Store makerUri for looking up later
this._orderSignatureToMakerUri[orderWithBigNumberInts.signature] = firmQuoteResponse.makerUri;
// Passed all validation, add it to result
result.push({ signedOrder: orderWithBigNumberInts });
return;
});
return result;
// Save the maker URI for later and return just the order
const rfqQuotes = validQuotes.map(result => {
const { signature, ...rest } = result.response;
const order: SignedNativeOrder = {
order: {
...rest,
makerAmount: new BigNumber(result.response.makerAmount),
takerAmount: new BigNumber(result.response.takerAmount),
expiry: new BigNumber(result.response.expiry),
salt: new BigNumber(result.response.salt),
},
type: FillQuoteTransformerOrderType.Rfq,
signature,
};
this._orderSignatureToMakerUri[nativeDataToId(result.response)] = result.makerUri;
return order;
});
return rfqQuotes;
}
public async requestRfqtIndicativeQuotesAsync(
makerAssetData: string,
takerAssetData: string,
makerToken: string,
takerToken: string,
assetFillAmount: BigNumber,
marketOperation: MarketOperation,
comparisonPrice: BigNumber | undefined,
options: RfqtRequestOpts,
): Promise<V3RFQIndicativeQuote[]> {
): Promise<V4RFQIndicativeQuote[]> {
const _opts: RfqtRequestOpts = { ...constants.DEFAULT_RFQT_REQUEST_OPTS, ...options };
// Originally a takerAddress was required for indicative quotes, but
// now we've eliminated that requirement. @0x/quote-server, however,
// is still coded to expect a takerAddress. So if the client didn't
@ -256,10 +231,12 @@ export class QuoteRequestor {
if (!_opts.takerAddress) {
_opts.takerAddress = constants.NULL_ADDRESS;
}
const responsesWithStringInts = await this._getQuotesAsync<V3RFQIndicativeQuote>( // not yet BigNumber
makerAssetData,
takerAssetData,
if (!_opts.txOrigin) {
_opts.txOrigin = constants.NULL_ADDRESS;
}
const rawQuotes = await this._getQuotesAsync<V4RFQIndicativeQuote>(
makerToken,
takerToken,
assetFillAmount,
marketOperation,
comparisonPrice,
@ -267,84 +244,78 @@ export class QuoteRequestor {
'indicative',
);
const validResponsesWithStringInts = responsesWithStringInts.filter(result => {
const response = result.response;
if (!this._isValidRfqtIndicativeQuoteResponse(response)) {
this._warningLogger(response, 'Invalid RFQ-T indicative quote received, filtering out');
// validate
const validationFunction = (o: V4RFQIndicativeQuote) => this._isValidRfqtIndicativeQuoteResponse(o);
const validQuotes = rawQuotes.filter(result => {
const order = result.response;
if (!validationFunction(order)) {
this._warningLogger(result, 'Invalid RFQ-T indicative quote received, filtering out');
return false;
}
if (
!hasExpectedAssetData(makerAssetData, takerAssetData, response.makerAssetData, response.takerAssetData)
) {
this._warningLogger(response, 'Unexpected asset data in RFQ-T indicative quote, filtering out');
if (!hasExpectedAddresses([[makerToken, order.makerToken], [takerToken, order.takerToken]])) {
this._warningLogger(order, 'Unexpected token or taker address in RFQ-T order, filtering out');
return false;
}
return true;
});
const validResponses = validResponsesWithStringInts.map(result => {
const response = result.response;
return {
...response,
makerAssetAmount: new BigNumber(response.makerAssetAmount),
takerAssetAmount: new BigNumber(response.takerAssetAmount),
expirationTimeSeconds: new BigNumber(response.expirationTimeSeconds),
};
});
const responses = validResponses.filter(response => {
if (this._isExpirationTooSoon(response.expirationTimeSeconds)) {
this._warningLogger(response, 'Expiry too soon in RFQ-T indicative quote, filtering out');
if (this._isExpirationTooSoon(new BigNumber(order.expiry))) {
this._warningLogger(order, 'Expiry too soon in RFQ-T indicative quote, filtering out');
return false;
} else {
return true;
}
return true;
});
return responses;
const quotes = validQuotes.map(r => r.response);
quotes.forEach(q => {
q.makerAmount = new BigNumber(q.makerAmount);
q.takerAmount = new BigNumber(q.takerAmount);
q.expiry = new BigNumber(q.expiry);
});
return quotes;
}
/**
* Given an order signature, returns the makerUri that the order originated from
*/
public getMakerUriForOrderSignature(orderSignature: string): string | undefined {
return this._orderSignatureToMakerUri[orderSignature];
public getMakerUriForSignature(signature: Signature): string | undefined {
return this._orderSignatureToMakerUri[nativeDataToId({ signature })];
}
private _isValidRfqtIndicativeQuoteResponse(response: V3RFQIndicativeQuote): boolean {
const hasValidMakerAssetAmount =
response.makerAssetAmount !== undefined &&
this._schemaValidator.isValid(response.makerAssetAmount, schemas.wholeNumberSchema);
const hasValidTakerAssetAmount =
response.takerAssetAmount !== undefined &&
this._schemaValidator.isValid(response.takerAssetAmount, schemas.wholeNumberSchema);
const hasValidMakerAssetData =
response.makerAssetData !== undefined &&
this._schemaValidator.isValid(response.makerAssetData, schemas.hexSchema);
const hasValidTakerAssetData =
response.takerAssetData !== undefined &&
this._schemaValidator.isValid(response.takerAssetData, schemas.hexSchema);
const hasValidExpirationTimeSeconds =
response.expirationTimeSeconds !== undefined &&
this._schemaValidator.isValid(response.expirationTimeSeconds, schemas.wholeNumberSchema);
if (
hasValidMakerAssetAmount &&
hasValidTakerAssetAmount &&
hasValidMakerAssetData &&
hasValidTakerAssetData &&
hasValidExpirationTimeSeconds
) {
return true;
private _isValidRfqtIndicativeQuoteResponse(response: V4RFQIndicativeQuote): boolean {
const requiredKeys: Array<keyof V4RFQIndicativeQuote> = [
'makerAmount',
'takerAmount',
'makerToken',
'takerToken',
'expiry',
];
for (const k of requiredKeys) {
if (response[k] === undefined) {
return false;
}
}
return false;
// TODO (jacob): I have a feeling checking 5 schemas is slower then checking one
const hasValidMakerAssetAmount = this._schemaValidator.isValid(response.makerAmount, schemas.wholeNumberSchema);
const hasValidTakerAssetAmount = this._schemaValidator.isValid(response.takerAmount, schemas.wholeNumberSchema);
const hasValidMakerToken = this._schemaValidator.isValid(response.makerToken, schemas.hexSchema);
const hasValidTakerToken = this._schemaValidator.isValid(response.takerToken, schemas.hexSchema);
const hasValidExpirationTimeSeconds = this._schemaValidator.isValid(response.expiry, schemas.wholeNumberSchema);
if (
!hasValidMakerAssetAmount ||
!hasValidTakerAssetAmount ||
!hasValidMakerToken ||
!hasValidTakerToken ||
!hasValidExpirationTimeSeconds
) {
return false;
}
return true;
}
private _makerSupportsPair(makerUrl: string, makerAssetData: string, takerAssetData: string): boolean {
const makerTokenAddress = getTokenAddressOrThrow(makerAssetData);
const takerTokenAddress = getTokenAddressOrThrow(takerAssetData);
private _makerSupportsPair(makerUrl: string, makerToken: string, takerToken: string): boolean {
for (const assetPair of this._rfqtAssetOfferings[makerUrl]) {
if (
(assetPair[0] === makerTokenAddress && assetPair[1] === takerTokenAddress) ||
(assetPair[0] === takerTokenAddress && assetPair[1] === makerTokenAddress)
(assetPair[0] === makerToken && assetPair[1] === takerToken) ||
(assetPair[0] === takerToken && assetPair[1] === makerToken)
) {
return true;
}
@ -359,92 +330,101 @@ export class QuoteRequestor {
}
private async _getQuotesAsync<ResponseT>(
makerAssetData: string,
takerAssetData: string,
makerToken: string,
takerToken: string,
assetFillAmount: BigNumber,
marketOperation: MarketOperation,
comparisonPrice: BigNumber | undefined,
options: RfqtRequestOpts,
quoteType: 'firm' | 'indicative',
): Promise<Array<{ response: ResponseT; makerUri: string }>> {
): Promise<Array<RfqQuote<ResponseT>>> {
const requestParams = QuoteRequestor.makeQueryParameters(
options.txOrigin,
options.takerAddress,
marketOperation,
makerAssetData,
takerAssetData,
makerToken,
takerToken,
assetFillAmount,
comparisonPrice,
);
const quotePath = (() => {
switch (quoteType) {
case 'firm':
return 'quote';
case 'indicative':
return 'price';
default:
throw new Error(`Unexpected quote type ${quoteType}`);
}
})();
const result: Array<{ response: ResponseT; makerUri: string }> = [];
await Promise.all(
Object.keys(this._rfqtAssetOfferings).map(async url => {
const isBlacklisted = rfqMakerBlacklist.isMakerBlacklisted(url);
const partialLogEntry = { url, quoteType, requestParams, isBlacklisted };
if (isBlacklisted) {
this._infoLogger({ rfqtMakerInteraction: { ...partialLogEntry } });
} else if (this._makerSupportsPair(url, makerAssetData, takerAssetData)) {
const timeBeforeAwait = Date.now();
const maxResponseTimeMs =
options.makerEndpointMaxResponseTimeMs === undefined
? constants.DEFAULT_RFQT_REQUEST_OPTS.makerEndpointMaxResponseTimeMs!
: options.makerEndpointMaxResponseTimeMs;
try {
const quotePath = (() => {
switch (quoteType) {
case 'firm':
return 'quote';
case 'indicative':
return 'price';
default:
throw new Error(`Unexpected quote type ${quoteType}`);
}
})();
const response = await quoteRequestorHttpClient.get<ResponseT>(`${url}/${quotePath}`, {
headers: { '0x-api-key': options.apiKey },
params: requestParams,
timeout: maxResponseTimeMs,
});
const latencyMs = Date.now() - timeBeforeAwait;
this._infoLogger({
rfqtMakerInteraction: {
...partialLogEntry,
response: {
included: true,
apiKey: options.apiKey,
takerAddress: requestParams.takerAddress,
statusCode: response.status,
latencyMs,
},
const makerUrls = Object.keys(this._rfqtAssetOfferings);
const quotePromises = makerUrls.map(async url => {
// filter out requests to skip
const isBlacklisted = rfqMakerBlacklist.isMakerBlacklisted(url);
const partialLogEntry = { url, quoteType, requestParams, isBlacklisted };
if (isBlacklisted) {
this._infoLogger({ rfqtMakerInteraction: { ...partialLogEntry } });
return;
} else if (!this._makerSupportsPair(url, makerToken, takerToken)) {
return;
} else {
// make request to MMs
const timeBeforeAwait = Date.now();
const maxResponseTimeMs =
options.makerEndpointMaxResponseTimeMs === undefined
? constants.DEFAULT_RFQT_REQUEST_OPTS.makerEndpointMaxResponseTimeMs!
: options.makerEndpointMaxResponseTimeMs;
try {
const response = await quoteRequestorHttpClient.get<ResponseT>(`${url}/${quotePath}`, {
headers: { '0x-api-key': options.apiKey },
params: requestParams,
timeout: maxResponseTimeMs,
});
const latencyMs = Date.now() - timeBeforeAwait;
this._infoLogger({
rfqtMakerInteraction: {
...partialLogEntry,
response: {
included: true,
apiKey: options.apiKey,
takerAddress: requestParams.takerAddress,
txOrigin: requestParams.txOrigin,
statusCode: response.status,
latencyMs,
},
});
rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs >= maxResponseTimeMs);
result.push({ response: response.data, makerUri: url });
} catch (err) {
const latencyMs = Date.now() - timeBeforeAwait;
this._infoLogger({
rfqtMakerInteraction: {
...partialLogEntry,
response: {
included: false,
apiKey: options.apiKey,
takerAddress: requestParams.takerAddress,
statusCode: err.response ? err.response.status : undefined,
latencyMs,
},
},
});
rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs >= maxResponseTimeMs);
return { response: response.data, makerUri: url };
} catch (err) {
// log error if any
const latencyMs = Date.now() - timeBeforeAwait;
this._infoLogger({
rfqtMakerInteraction: {
...partialLogEntry,
response: {
included: false,
apiKey: options.apiKey,
takerAddress: requestParams.takerAddress,
txOrigin: requestParams.txOrigin,
statusCode: err.response ? err.response.status : undefined,
latencyMs,
},
});
rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs >= maxResponseTimeMs);
this._warningLogger(
convertIfAxiosError(err),
`Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${url} for API key ${
options.apiKey
} for taker address ${options.takerAddress}`,
);
}
},
});
rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs >= maxResponseTimeMs);
this._warningLogger(
convertIfAxiosError(err),
`Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${url} for API key ${
options.apiKey
} for taker address ${options.takerAddress} and tx origin ${options.txOrigin}`,
);
return;
}
}),
);
return result;
}
});
const results = (await Promise.all(quotePromises)).filter(x => x !== undefined);
return results as Array<RfqQuote<ResponseT>>;
}
}

View File

@ -1,10 +1,11 @@
import { FillQuoteTransformerOrderType } from '@0x/protocol-utils';
import { BigNumber } from '@0x/utils';
import { constants } from '../constants';
import { MarketOperation } from '../types';
import { CollapsedFill, ERC20BridgeSource, FeeSchedule, OptimizedMarketOrder } from './market_operation_utils/types';
import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from './utils';
import { FeeSchedule, NativeLimitOrderFillData, OptimizedMarketOrder } from './market_operation_utils/types';
import { getNativeAdjustedTakerFeeAmount } from './utils';
const { PROTOCOL_FEE_MULTIPLIER, ZERO_AMOUNT } = constants;
const { ROUND_DOWN, ROUND_UP } = BigNumber;
@ -73,11 +74,13 @@ export interface QuoteFillInfo {
export interface QuoteFillInfoOpts {
gasSchedule: FeeSchedule;
protocolFeeMultiplier: BigNumber;
slippage: number;
}
const DEFAULT_SIMULATED_FILL_QUOTE_INFO_OPTS: QuoteFillInfoOpts = {
gasSchedule: {},
protocolFeeMultiplier: PROTOCOL_FEE_MULTIPLIER,
slippage: 0,
};
export interface QuoteFillOrderCall {
@ -117,17 +120,20 @@ export function simulateWorstCaseFill(quoteInfo: QuoteFillInfo): QuoteFillResult
...quoteInfo.opts,
};
const protocolFeePerFillOrder = quoteInfo.gasPrice.times(opts.protocolFeeMultiplier);
const bestCase = createBestCaseFillOrderCalls(quoteInfo);
const result = {
...fillQuoteOrders(
createWorstCaseFillOrderCalls(quoteInfo),
quoteInfo.fillAmount,
protocolFeePerFillOrder,
opts.gasSchedule,
),
...fillQuoteOrders(bestCase, quoteInfo.fillAmount, protocolFeePerFillOrder, opts.gasSchedule),
// Worst case gas and protocol fee is hitting all orders.
gas: getTotalGasUsedByFills(getFlattenedFillsFromOrders(quoteInfo.orders), opts.gasSchedule),
protocolFee: protocolFeePerFillOrder.times(quoteInfo.orders.length),
gas: getTotalGasUsedByFills(quoteInfo.orders, opts.gasSchedule),
protocolFee: protocolFeePerFillOrder.times(quoteInfo.orders.filter(o => hasProtocolFee(o)).length),
};
// Adjust the output by 1-slippage for the worst case if it is a sell
// Adjust the output by 1+slippage for the worst case if it is a buy
const outputMultiplier =
quoteInfo.side === MarketOperation.Sell
? new BigNumber(1).minus(opts.slippage)
: new BigNumber(1).plus(opts.slippage);
result.output = result.output.times(outputMultiplier).integerValue();
return fromIntermediateQuoteFillResult(result, quoteInfo);
}
@ -151,8 +157,8 @@ export function fillQuoteOrders(
break;
}
const { source, fillData } = fill;
const fee = gasSchedule[source] === undefined ? 0 : gasSchedule[source]!(fillData);
result.gas += new BigNumber(fee).toNumber();
const gas = gasSchedule[source] === undefined ? 0 : gasSchedule[source]!(fillData);
result.gas += new BigNumber(gas).toNumber();
result.inputBySource[source] = result.inputBySource[source] || ZERO_AMOUNT;
// Actual rates are rarely linear, so fill subfills individually to
@ -179,11 +185,17 @@ export function fillQuoteOrders(
remainingInput = remainingInput.minus(filledInput.plus(filledInputFee));
}
}
result.protocolFee = result.protocolFee.plus(protocolFeePerFillOrder);
// NOTE: V4 Limit orders have Protocol fees
const protocolFee = hasProtocolFee(fo.order) ? protocolFeePerFillOrder : ZERO_AMOUNT;
result.protocolFee = result.protocolFee.plus(protocolFee);
}
return result;
}
function hasProtocolFee(o: OptimizedMarketOrder): boolean {
return o.type === FillQuoteTransformerOrderType.Limit;
}
function solveForInputFillAmount(
remainingInput: BigNumber,
fillableInput: BigNumber,
@ -221,79 +233,33 @@ function createBestCaseFillOrderCalls(quoteInfo: QuoteFillInfo): QuoteFillOrderC
order: o,
...(side === MarketOperation.Sell
? {
totalOrderInput: o.takerAssetAmount,
totalOrderOutput: o.makerAssetAmount,
totalOrderInputFee: isOrderTakerFeePayableWithTakerAsset(o) ? o.takerFee : ZERO_AMOUNT,
totalOrderOutputFee: isOrderTakerFeePayableWithMakerAsset(o) ? o.takerFee.negated() : ZERO_AMOUNT,
totalOrderInput: o.takerAmount,
totalOrderOutput: o.makerAmount,
totalOrderInputFee:
o.type === FillQuoteTransformerOrderType.Limit
? getNativeAdjustedTakerFeeAmount(
(o.fillData as NativeLimitOrderFillData).order,
o.takerAmount,
)
: ZERO_AMOUNT,
totalOrderOutputFee: ZERO_AMOUNT, // makerToken fees are not supported in v4 (sell output)
}
: // Buy
{
totalOrderInput: o.makerAssetAmount,
totalOrderOutput: o.takerAssetAmount,
totalOrderInputFee: isOrderTakerFeePayableWithMakerAsset(o) ? o.takerFee.negated() : ZERO_AMOUNT,
totalOrderOutputFee: isOrderTakerFeePayableWithTakerAsset(o) ? o.takerFee : ZERO_AMOUNT,
totalOrderInput: o.makerAmount,
totalOrderOutput: o.takerAmount,
totalOrderInputFee: ZERO_AMOUNT, // makerToken fees are not supported in v4 (buy input)
totalOrderOutputFee:
o.type === FillQuoteTransformerOrderType.Limit
? getNativeAdjustedTakerFeeAmount(
(o.fillData as NativeLimitOrderFillData).order,
o.takerAmount,
)
: ZERO_AMOUNT,
}),
}));
}
function createWorstCaseFillOrderCalls(quoteInfo: QuoteFillInfo): QuoteFillOrderCall[] {
// Reuse best case fill orders, but apply slippage.
return (
createBestCaseFillOrderCalls(quoteInfo)
.map(fo => ({
...fo,
order: {
...fo.order,
// Apply slippage to order fills and reverse them.
fills: getSlippedOrderFills(fo.order, quoteInfo.side)
.map(f => ({ ...f, subFills: f.subFills.slice().reverse() }))
.reverse(),
},
}))
// Sort by ascending price.
.sort((a, b) =>
a.order.makerAssetAmount
.div(a.order.takerAssetAmount)
.comparedTo(b.order.makerAssetAmount.div(b.order.takerAssetAmount)),
)
);
}
// Apply order slippage to its fill paths.
function getSlippedOrderFills(order: OptimizedMarketOrder, side: MarketOperation): CollapsedFill[] {
// Infer the slippage from the order amounts vs fill amounts.
let inputScaling: BigNumber;
let outputScaling: BigNumber;
const source = order.fills[0].source;
if (source === ERC20BridgeSource.Native) {
// Native orders do not have slippage applied to them.
inputScaling = new BigNumber(1);
outputScaling = new BigNumber(1);
} else {
if (side === MarketOperation.Sell) {
const totalFillableTakerAssetAmount = BigNumber.sum(...order.fills.map(f => f.input));
const totalFillableMakerAssetAmount = BigNumber.sum(...order.fills.map(f => f.output));
inputScaling = order.fillableTakerAssetAmount.div(totalFillableTakerAssetAmount);
outputScaling = order.fillableMakerAssetAmount.div(totalFillableMakerAssetAmount);
} else {
const totalFillableTakerAssetAmount = BigNumber.sum(...order.fills.map(f => f.output));
const totalFillableMakerAssetAmount = BigNumber.sum(...order.fills.map(f => f.input));
inputScaling = order.fillableMakerAssetAmount.div(totalFillableMakerAssetAmount);
outputScaling = order.fillableTakerAssetAmount.div(totalFillableTakerAssetAmount);
}
}
return order.fills.map(f => ({
...f,
input: f.input.times(inputScaling),
output: f.output.times(outputScaling),
subFills: f.subFills.map(sf => ({
...sf,
input: sf.input.times(inputScaling),
output: sf.output.times(outputScaling),
})),
}));
}
function roundInputAmount(amount: BigNumber, side: MarketOperation): BigNumber {
return amount.integerValue(side === MarketOperation.Sell ? ROUND_UP : ROUND_DOWN);
}
@ -349,15 +315,7 @@ function fromIntermediateQuoteFillResult(ir: IntermediateQuoteFillResult, quoteI
};
}
function getFlattenedFillsFromOrders(orders: OptimizedMarketOrder[]): CollapsedFill[] {
const fills: CollapsedFill[] = [];
for (const o of orders) {
fills.push(...o.fills);
}
return fills;
}
function getTotalGasUsedByFills(fills: CollapsedFill[], gasSchedule: FeeSchedule): number {
function getTotalGasUsedByFills(fills: OptimizedMarketOrder[], gasSchedule: FeeSchedule): number {
let gasUsed = 0;
for (const f of fills) {
const fee = gasSchedule[f.source] === undefined ? 0 : gasSchedule[f.source]!(f.fillData);

View File

@ -1,7 +1,12 @@
import axios, { AxiosInstance } from 'axios';
import AxiosMockAdapter from 'axios-mock-adapter';
import { MockedRfqtFirmQuoteResponse } from '../types';
import { MockedRfqtQuoteResponse } from '../types';
export enum RfqtQuoteEndpoint {
Indicative = 'price',
Firm = 'quote',
}
/**
* A helper utility for testing which mocks out
@ -9,15 +14,15 @@ import { MockedRfqtFirmQuoteResponse } from '../types';
*/
export const rfqtMocker = {
/**
* Stubs out responses from RFQ-T providers by mocking out
* HTTP calls via axios. Always restores the mock adapter
* after executing the `performFn`.
* A helper utility for testing which mocks out
* requests to RFQ-t providers
*/
withMockedRfqtFirmQuotes: async (
mockedResponses: MockedRfqtFirmQuoteResponse[],
performFn: () => Promise<void>,
withMockedRfqtQuotes: async (
mockedResponses: MockedRfqtQuoteResponse[],
quoteType: RfqtQuoteEndpoint,
afterResponseCallback: () => Promise<void>,
axiosClient: AxiosInstance = axios,
) => {
): Promise<void> => {
const mockedAxios = new AxiosMockAdapter(axiosClient);
try {
// Mock out RFQT responses
@ -25,33 +30,11 @@ export const rfqtMocker = {
const { endpoint, requestApiKey, requestParams, responseData, responseCode } = mockedResponse;
const requestHeaders = { Accept: 'application/json, text/plain, */*', '0x-api-key': requestApiKey };
mockedAxios
.onGet(`${endpoint}/quote`, { params: requestParams }, requestHeaders)
.onGet(`${endpoint}/${quoteType}`, { params: requestParams }, requestHeaders)
.replyOnce(responseCode, responseData);
}
await performFn();
} finally {
// Ensure we always restore axios afterwards
mockedAxios.restore();
}
},
withMockedRfqtIndicativeQuotes: async (
mockedResponses: MockedRfqtFirmQuoteResponse[],
performFn: () => Promise<void>,
axiosClient: AxiosInstance = axios,
) => {
const mockedAxios = new AxiosMockAdapter(axiosClient);
try {
// Mock out RFQT responses
for (const mockedResponse of mockedResponses) {
const { endpoint, requestApiKey, requestParams, responseData, responseCode } = mockedResponse;
const requestHeaders = { Accept: 'application/json, text/plain, */*', '0x-api-key': requestApiKey };
mockedAxios
.onGet(`${endpoint}/price`, { params: requestParams }, requestHeaders)
.replyOnce(responseCode, responseData);
}
await performFn();
// Perform the callback function, e.g. a test validation
await afterResponseCallback();
} finally {
// Ensure we always restore axios afterwards
mockedAxios.restore();

View File

@ -1,27 +0,0 @@
import { schemas } from '@0x/json-schemas';
import { Order } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { assert } from './assert';
import { getAdjustedMakerAndTakerAmountsFromTakerFees } from './utils';
export const sortingUtils = {
sortOrders<T extends Order>(orders: T[]): T[] {
assert.doesConformToSchema('orders', orders, schemas.ordersSchema);
assert.isValidOrdersForSwapQuoter('orders', orders);
const copiedOrders = _.cloneDeep(orders);
copiedOrders.sort((firstOrder, secondOrder) => {
const firstOrderRate = getTakerFeeAdjustedRateOfOrder(firstOrder);
const secondOrderRate = getTakerFeeAdjustedRateOfOrder(secondOrder);
return firstOrderRate.comparedTo(secondOrderRate);
});
return copiedOrders;
},
};
function getTakerFeeAdjustedRateOfOrder(order: Order): BigNumber {
const [adjustedMakerAssetAmount, adjustedTakerAssetAmount] = getAdjustedMakerAndTakerAmountsFromTakerFees(order);
const rate = adjustedTakerAssetAmount.div(adjustedMakerAssetAmount);
return rate;
}

View File

@ -1,324 +0,0 @@
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { constants } from '../constants';
import {
CalculateSwapQuoteOpts,
MarketBuySwapQuote,
MarketOperation,
MarketSellSwapQuote,
SwapQuote,
SwapQuoteInfo,
SwapQuoteOrdersBreakdown,
SwapQuoterError,
} from '../types';
import { MarketOperationUtils } from './market_operation_utils';
import { SOURCE_FLAGS } from './market_operation_utils/constants';
import {
ERC20BridgeSource,
FeeSchedule,
FillData,
GetMarketOrdersOpts,
OptimizedMarketOrder,
OptimizerResultWithReport,
} from './market_operation_utils/types';
import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation';
import { getTokenFromAssetData, isSupportedAssetDataInOrders } from './utils';
// TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError?
export class SwapQuoteCalculator {
private readonly _marketOperationUtils: MarketOperationUtils;
constructor(marketOperationUtils: MarketOperationUtils) {
this._marketOperationUtils = marketOperationUtils;
}
public async calculateMarketSellSwapQuoteAsync(
prunedOrders: SignedOrder[],
takerAssetFillAmount: BigNumber,
gasPrice: BigNumber,
opts: CalculateSwapQuoteOpts,
): Promise<MarketSellSwapQuote> {
return (await this._calculateSwapQuoteAsync(
prunedOrders,
takerAssetFillAmount,
gasPrice,
MarketOperation.Sell,
opts,
)) as MarketSellSwapQuote;
}
public async calculateMarketBuySwapQuoteAsync(
prunedOrders: SignedOrder[],
takerAssetFillAmount: BigNumber,
gasPrice: BigNumber,
opts: CalculateSwapQuoteOpts,
): Promise<MarketBuySwapQuote> {
return (await this._calculateSwapQuoteAsync(
prunedOrders,
takerAssetFillAmount,
gasPrice,
MarketOperation.Buy,
opts,
)) as MarketBuySwapQuote;
}
public async calculateBatchMarketBuySwapQuoteAsync(
batchPrunedOrders: SignedOrder[][],
takerAssetFillAmounts: BigNumber[],
gasPrice: BigNumber,
opts: CalculateSwapQuoteOpts,
): Promise<Array<MarketBuySwapQuote | undefined>> {
return (await this._calculateBatchBuySwapQuoteAsync(
batchPrunedOrders,
takerAssetFillAmounts,
gasPrice,
MarketOperation.Buy,
opts,
)) as Array<MarketBuySwapQuote | undefined>;
}
private async _calculateBatchBuySwapQuoteAsync(
batchPrunedOrders: SignedOrder[][],
assetFillAmounts: BigNumber[],
gasPrice: BigNumber,
operation: MarketOperation,
opts: CalculateSwapQuoteOpts,
): Promise<Array<SwapQuote | undefined>> {
const optimizerResults = await this._marketOperationUtils.getBatchMarketBuyOrdersAsync(
batchPrunedOrders,
assetFillAmounts,
opts,
);
const batchSwapQuotes = await Promise.all(
optimizerResults.map(async (result, i) => {
if (result) {
const { makerAssetData, takerAssetData } = batchPrunedOrders[i][0];
return createSwapQuote(
result,
makerAssetData,
takerAssetData,
operation,
assetFillAmounts[i],
gasPrice,
opts.gasSchedule,
);
} else {
return undefined;
}
}),
);
return batchSwapQuotes;
}
private async _calculateSwapQuoteAsync(
prunedOrders: SignedOrder[],
assetFillAmount: BigNumber,
gasPrice: BigNumber,
operation: MarketOperation,
opts: CalculateSwapQuoteOpts,
): Promise<SwapQuote> {
// checks if maker asset is ERC20 and taker asset is ERC20
if (!isSupportedAssetDataInOrders(prunedOrders)) {
throw Error(SwapQuoterError.AssetDataUnsupported);
}
// since prunedOrders do not have fillState, we will add a buffer of fillable orders to consider that some native are orders are partially filled
// Scale fees by gas price.
const _opts: GetMarketOrdersOpts = {
...opts,
feeSchedule: _.mapValues(opts.feeSchedule, gasCost => (fillData?: FillData) =>
gasCost === undefined ? 0 : gasPrice.times(gasCost(fillData)),
),
exchangeProxyOverhead: flags => gasPrice.times(opts.exchangeProxyOverhead(flags)),
};
const result =
operation === MarketOperation.Buy
? await this._marketOperationUtils.getMarketBuyOrdersAsync(prunedOrders, assetFillAmount, _opts)
: await this._marketOperationUtils.getMarketSellOrdersAsync(prunedOrders, assetFillAmount, _opts);
const { makerAssetData, takerAssetData } = prunedOrders[0];
const swapQuote = createSwapQuote(
result,
makerAssetData,
takerAssetData,
operation,
assetFillAmount,
gasPrice,
opts.gasSchedule,
);
// Use the raw gas, not scaled by gas price
const exchangeProxyOverhead = opts.exchangeProxyOverhead(result.sourceFlags).toNumber();
swapQuote.bestCaseQuoteInfo.gas += exchangeProxyOverhead;
swapQuote.worstCaseQuoteInfo.gas += exchangeProxyOverhead;
swapQuote.unoptimizedQuoteInfo.gas += exchangeProxyOverhead;
return swapQuote;
}
}
function createSwapQuote(
optimizerResult: OptimizerResultWithReport,
makerAssetData: string,
takerAssetData: string,
operation: MarketOperation,
assetFillAmount: BigNumber,
gasPrice: BigNumber,
gasSchedule: FeeSchedule,
): SwapQuote {
const {
optimizedOrders,
quoteReport,
sourceFlags,
unoptimizedPath,
takerAssetToEthRate,
makerAssetToEthRate,
} = optimizerResult;
const isTwoHop = sourceFlags === SOURCE_FLAGS[ERC20BridgeSource.MultiHop];
// Calculate quote info
const { bestCaseQuoteInfo, worstCaseQuoteInfo, sourceBreakdown } = isTwoHop
? calculateTwoHopQuoteInfo(optimizedOrders, operation, gasSchedule)
: calculateQuoteInfo(optimizedOrders, operation, assetFillAmount, gasPrice, gasSchedule);
// Calculate the unoptimised alternative
const unoptimizedOrders = unoptimizedPath !== undefined ? unoptimizedPath.orders : [];
const unoptimizedFillResult = simulateBestCaseFill({
gasPrice,
orders: unoptimizedOrders,
side: operation,
fillAmount: assetFillAmount,
opts: { gasSchedule },
});
const unoptimizedQuoteInfo = fillResultsToQuoteInfo(unoptimizedFillResult);
// Put together the swap quote
const { makerTokenDecimals, takerTokenDecimals } = optimizerResult.marketSideLiquidity;
const swapQuote = {
makerAssetData,
takerAssetData,
gasPrice,
orders: optimizedOrders,
bestCaseQuoteInfo,
worstCaseQuoteInfo,
unoptimizedQuoteInfo,
unoptimizedOrders,
sourceBreakdown,
makerTokenDecimals,
takerTokenDecimals,
takerAssetToEthRate,
makerAssetToEthRate,
quoteReport,
isTwoHop,
};
if (operation === MarketOperation.Buy) {
return {
...swapQuote,
type: MarketOperation.Buy,
makerAssetFillAmount: assetFillAmount,
};
} else {
return {
...swapQuote,
type: MarketOperation.Sell,
takerAssetFillAmount: assetFillAmount,
};
}
}
function calculateQuoteInfo(
optimizedOrders: OptimizedMarketOrder[],
operation: MarketOperation,
assetFillAmount: BigNumber,
gasPrice: BigNumber,
gasSchedule: FeeSchedule,
): { bestCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo; sourceBreakdown: SwapQuoteOrdersBreakdown } {
const bestCaseFillResult = simulateBestCaseFill({
gasPrice,
orders: optimizedOrders,
side: operation,
fillAmount: assetFillAmount,
opts: { gasSchedule },
});
const worstCaseFillResult = simulateWorstCaseFill({
gasPrice,
orders: optimizedOrders,
side: operation,
fillAmount: assetFillAmount,
opts: { gasSchedule },
});
return {
bestCaseQuoteInfo: fillResultsToQuoteInfo(bestCaseFillResult),
worstCaseQuoteInfo: fillResultsToQuoteInfo(worstCaseFillResult),
sourceBreakdown: getSwapQuoteOrdersBreakdown(bestCaseFillResult.fillAmountBySource),
};
}
function calculateTwoHopQuoteInfo(
optimizedOrders: OptimizedMarketOrder[],
operation: MarketOperation,
gasSchedule: FeeSchedule,
): { bestCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo; sourceBreakdown: SwapQuoteOrdersBreakdown } {
const [firstHopOrder, secondHopOrder] = optimizedOrders;
const [firstHopFill] = firstHopOrder.fills;
const [secondHopFill] = secondHopOrder.fills;
const gas = new BigNumber(
gasSchedule[ERC20BridgeSource.MultiHop]!({
firstHopSource: _.pick(firstHopFill, 'source', 'fillData'),
secondHopSource: _.pick(secondHopFill, 'source', 'fillData'),
}),
).toNumber();
return {
bestCaseQuoteInfo: {
makerAssetAmount: operation === MarketOperation.Sell ? secondHopFill.output : secondHopFill.input,
takerAssetAmount: operation === MarketOperation.Sell ? firstHopFill.input : firstHopFill.output,
totalTakerAssetAmount: operation === MarketOperation.Sell ? firstHopFill.input : firstHopFill.output,
feeTakerAssetAmount: constants.ZERO_AMOUNT,
protocolFeeInWeiAmount: constants.ZERO_AMOUNT,
gas,
},
worstCaseQuoteInfo: {
makerAssetAmount: secondHopOrder.makerAssetAmount,
takerAssetAmount: firstHopOrder.takerAssetAmount,
totalTakerAssetAmount: firstHopOrder.takerAssetAmount,
feeTakerAssetAmount: constants.ZERO_AMOUNT,
protocolFeeInWeiAmount: constants.ZERO_AMOUNT,
gas,
},
sourceBreakdown: {
[ERC20BridgeSource.MultiHop]: {
proportion: new BigNumber(1),
intermediateToken: getTokenFromAssetData(secondHopOrder.takerAssetData),
hops: [firstHopFill.source, secondHopFill.source],
},
},
};
}
function getSwapQuoteOrdersBreakdown(fillAmountBySource: { [source: string]: BigNumber }): SwapQuoteOrdersBreakdown {
const totalFillAmount = BigNumber.sum(...Object.values(fillAmountBySource));
const breakdown: SwapQuoteOrdersBreakdown = {};
Object.entries(fillAmountBySource).forEach(([source, fillAmount]) => {
breakdown[source as keyof SwapQuoteOrdersBreakdown] = fillAmount.div(totalFillAmount);
});
return breakdown;
}
function fillResultsToQuoteInfo(fr: QuoteFillResult): SwapQuoteInfo {
return {
makerAssetAmount: fr.totalMakerAssetAmount,
takerAssetAmount: fr.takerAssetAmount,
totalTakerAssetAmount: fr.totalTakerAssetAmount,
feeTakerAssetAmount: fr.takerFeeTakerAssetAmount,
protocolFeeInWeiAmount: fr.protocolFeeAmount,
gas: fr.gas,
};
}

View File

@ -1,106 +0,0 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { WETH9Contract } from '@0x/contract-wrappers';
import { assetDataUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { SupportedProvider, Web3Wrapper } from '@0x/web3-wrapper';
import { Provider } from 'ethereum-types';
import * as _ from 'lodash';
import { constants } from '../constants';
import {
ExtensionContractType,
GetExtensionContractTypeOpts,
SwapQuote,
SwapQuoteConsumerError,
SwapQuoteExecutionOpts,
} from '../types';
import { assert } from './assert';
import { isExactAssetData } from './utils';
export const swapQuoteConsumerUtils = {
async getTakerAddressOrThrowAsync(
provider: SupportedProvider,
opts: Partial<SwapQuoteExecutionOpts>,
): Promise<string> {
const takerAddress = await swapQuoteConsumerUtils.getTakerAddressAsync(provider, opts);
if (takerAddress === undefined) {
throw new Error(SwapQuoteConsumerError.NoAddressAvailable);
} else {
return takerAddress;
}
},
async getTakerAddressAsync(
provider: SupportedProvider,
opts: Partial<SwapQuoteExecutionOpts>,
): Promise<string | undefined> {
if (opts.takerAddress !== undefined) {
return opts.takerAddress;
} else {
const web3Wrapper = new Web3Wrapper(provider);
const availableAddresses = await web3Wrapper.getAvailableAddressesAsync();
const firstAvailableAddress = _.head(availableAddresses);
if (firstAvailableAddress !== undefined) {
return firstAvailableAddress;
} else {
return undefined;
}
}
},
async getEthAndWethBalanceAsync(
provider: SupportedProvider,
contractAddresses: ContractAddresses,
takerAddress: string,
): Promise<[BigNumber, BigNumber]> {
const weth = new WETH9Contract(contractAddresses.etherToken, provider);
const web3Wrapper = new Web3Wrapper(provider);
const ethBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress);
const wethBalance = await weth.balanceOf(takerAddress).callAsync();
return [ethBalance, wethBalance];
},
isValidForwarderSwapQuote(swapQuote: SwapQuote, wethAssetData: string): boolean {
return swapQuoteConsumerUtils.isValidForwarderSignedOrders(swapQuote.orders, wethAssetData);
},
isValidForwarderSignedOrders(orders: SignedOrder[], wethAssetData: string): boolean {
return _.every(orders, order => swapQuoteConsumerUtils.isValidForwarderSignedOrder(order, wethAssetData));
},
isValidForwarderSignedOrder(order: SignedOrder, wethAssetData: string): boolean {
return isExactAssetData(order.takerAssetData, wethAssetData);
},
async getExtensionContractTypeForSwapQuoteAsync(
quote: SwapQuote,
contractAddresses: ContractAddresses,
provider: Provider,
opts: Partial<GetExtensionContractTypeOpts>,
): Promise<ExtensionContractType> {
const wethAssetData = assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken);
if (swapQuoteConsumerUtils.isValidForwarderSwapQuote(quote, wethAssetData)) {
if (opts.takerAddress !== undefined) {
assert.isETHAddressHex('takerAddress', opts.takerAddress);
}
const ethAmount =
opts.ethAmount ||
quote.worstCaseQuoteInfo.takerAssetAmount.plus(quote.worstCaseQuoteInfo.protocolFeeInWeiAmount);
const takerAddress = await swapQuoteConsumerUtils.getTakerAddressAsync(provider, opts);
const takerEthAndWethBalance =
takerAddress !== undefined
? await swapQuoteConsumerUtils.getEthAndWethBalanceAsync(provider, contractAddresses, takerAddress)
: [constants.ZERO_AMOUNT, constants.ZERO_AMOUNT];
// TODO(david): when considering if there is enough Eth balance, should account for gas costs.
const isEnoughEthAndWethBalance = _.map(takerEthAndWethBalance, (balance: BigNumber) =>
balance.isGreaterThanOrEqualTo(ethAmount),
);
if (isEnoughEthAndWethBalance[1]) {
// should be more gas efficient to use exchange consumer, so if possible use it.
return ExtensionContractType.None;
} else if (isEnoughEthAndWethBalance[0] && !isEnoughEthAndWethBalance[1]) {
return ExtensionContractType.Forwarder;
}
// Note: defaulting to forwarderConsumer if takerAddress is null or not enough balance of either wEth or Eth
return ExtensionContractType.Forwarder;
} else {
return ExtensionContractType.None;
}
},
};

View File

@ -1,134 +1,100 @@
import { assetDataUtils } from '@0x/order-utils';
import { AssetData, AssetProxyId, ERC20AssetData, ERC20BridgeAssetData, Order, SignedOrder } from '@0x/types';
import { BigNumber, NULL_BYTES } from '@0x/utils';
import { CommonOrderFields, FillQuoteTransformerOrderType, LimitOrderFields } from '@0x/protocol-utils';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { constants } from '../constants';
import { PriceAwareRFQFlags } from '../types';
import { NativeOrderFillableAmountFields, SignedNativeOrder } from '../types';
import { ZERO_AMOUNT } from './market_operation_utils/constants';
// tslint:disable: no-unnecessary-type-assertion completed-docs
/**
* Returns 2 flags (one for firm quotes and another for indicative quotes) that serve as rollout flags for the price-aware RFQ feature.
* By default, indicative quotes should *always* go through the new price-aware flow. This means that all indicative RFQ requests made to
* market makers will contain the new price-aware `suggestedPrice` field.
* The `isPriceAwareRFQEnabled` feature object that is passed in by the 0x API will then control whether firm quotes go through price-aware RFQ.
*
* @param isPriceAwareRFQEnabled the feature flag that is passed in by the 0x API.
*/
export function getPriceAwareRFQRolloutFlags(priceAwareRFQFlags?: PriceAwareRFQFlags): PriceAwareRFQFlags {
return priceAwareRFQFlags !== undefined
? priceAwareRFQFlags
: {
isFirmPriceAwareEnabled: false,
isIndicativePriceAwareEnabled: false,
};
}
export function isSupportedAssetDataInOrders(orders: SignedOrder[]): boolean {
const firstOrderMakerAssetData = !!orders[0]
? assetDataUtils.decodeAssetDataOrThrow(orders[0].makerAssetData)
: { assetProxyId: '' };
return orders.every(o => {
const takerAssetData = assetDataUtils.decodeAssetDataOrThrow(o.takerAssetData);
const makerAssetData = assetDataUtils.decodeAssetDataOrThrow(o.makerAssetData);
return (
makerAssetData.assetProxyId === AssetProxyId.ERC20 &&
takerAssetData.assetProxyId === AssetProxyId.ERC20 &&
firstOrderMakerAssetData.assetProxyId === makerAssetData.assetProxyId
); // checks that all native order maker assets are of the same type
});
}
export function numberPercentageToEtherTokenAmountPercentage(percentage: number): BigNumber {
return Web3Wrapper.toBaseUnitAmount(constants.ONE_AMOUNT, constants.ETHER_TOKEN_DECIMALS).multipliedBy(percentage);
}
export function isOrderTakerFeePayableWithMakerAsset<T extends Order>(order: T): boolean {
return !order.takerFee.isZero() && isAssetDataEquivalent(order.takerFeeAssetData, order.makerAssetData);
}
export function isOrderTakerFeePayableWithTakerAsset<T extends Order>(order: T): boolean {
return !order.takerFee.isZero() && isAssetDataEquivalent(order.takerFeeAssetData, order.takerAssetData);
}
export function getAdjustedMakerAndTakerAmountsFromTakerFees<T extends Order>(order: T): [BigNumber, BigNumber] {
const adjustedMakerAssetAmount = isOrderTakerFeePayableWithMakerAsset(order)
? order.makerAssetAmount.minus(order.takerFee)
: order.makerAssetAmount;
const adjustedTakerAssetAmount = isOrderTakerFeePayableWithTakerAsset(order)
? order.takerAssetAmount.plus(order.takerFee)
: order.takerAssetAmount;
return [adjustedMakerAssetAmount, adjustedTakerAssetAmount];
}
export function isExactAssetData(expectedAssetData: string, actualAssetData: string): boolean {
return expectedAssetData === actualAssetData;
export function getAdjustedTakerAmountFromFees<T extends LimitOrderFields>(order: T): BigNumber {
return order.takerAmount.plus(order.takerTokenFeeAmount);
}
/**
* Compare the Asset Data for equivalency. Expected is the asset data the user provided (wanted),
* actual is the asset data found or created.
* Given an amount of taker asset, calculate the the amount of maker asset
* @param order The order
* @param makerFillAmount the amount of taker asset
*/
export function isAssetDataEquivalent(expectedAssetData: string, actualAssetData: string): boolean {
if (isExactAssetData(expectedAssetData, actualAssetData)) {
return true;
}
const decodedExpectedAssetData = assetDataUtils.decodeAssetDataOrThrow(expectedAssetData);
const decodedActualAssetData = assetDataUtils.decodeAssetDataOrThrow(actualAssetData);
// ERC20 === ERC20, ERC20 === ERC20Bridge
if (isERC20EquivalentAssetData(decodedExpectedAssetData) && isERC20EquivalentAssetData(decodedActualAssetData)) {
const doesTokenAddressMatch = decodedExpectedAssetData.tokenAddress === decodedActualAssetData.tokenAddress;
return doesTokenAddressMatch;
}
// ERC1155 === ERC1155
if (
assetDataUtils.isERC1155TokenAssetData(decodedExpectedAssetData) &&
assetDataUtils.isERC1155TokenAssetData(decodedActualAssetData)
) {
const doesTokenAddressMatch = decodedExpectedAssetData.tokenAddress === decodedActualAssetData.tokenAddress;
// IDs may be out of order yet still equivalent
// i.e (["a", "b"], [1,2]) === (["b", "a"], [2, 1])
// (["a", "b"], [2,1]) !== (["b", "a"], [2, 1])
const hasAllIds = decodedExpectedAssetData.tokenIds.every(
id => decodedActualAssetData.tokenIds.findIndex(v => id.eq(v)) !== -1,
);
const hasAllValues = decodedExpectedAssetData.tokenIds.every((id, i) =>
decodedExpectedAssetData.tokenValues[i].eq(
decodedActualAssetData.tokenValues[decodedActualAssetData.tokenIds.findIndex(v => id.eq(v))],
),
);
// If expected contains callback data, ensure it is present
// if actual has callbackdata and expected provided none then ignore it
const hasEquivalentCallback =
decodedExpectedAssetData.callbackData === NULL_BYTES ||
decodedExpectedAssetData.callbackData === decodedActualAssetData.callbackData;
return doesTokenAddressMatch && hasAllIds && hasAllValues && hasEquivalentCallback;
}
// ERC721 === ERC721
if (
assetDataUtils.isERC721TokenAssetData(decodedExpectedAssetData) ||
assetDataUtils.isERC721TokenAssetData(decodedActualAssetData)
) {
// Asset Data should exactly match for ERC721
return isExactAssetData(expectedAssetData, actualAssetData);
}
// TODO(dekz): Unsupported cases
// ERCXX(token) === MAP(token, staticCall)
// MAP(a, b) === MAP(b, a) === MAP(b, a, staticCall)
return false;
export function getNativeAdjustedMakerFillAmount(order: CommonOrderFields, takerFillAmount: BigNumber): BigNumber {
// Round down because exchange rate favors Maker
const makerFillAmount = takerFillAmount
.multipliedBy(order.makerAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_FLOOR);
return makerFillAmount;
}
/**
* Given an amount of maker asset, calculate the equivalent amount in taker asset
* @param order The order
* @param makerFillAmount the amount of maker asset
*/
export function getNativeAdjustedTakerFillAmount(order: CommonOrderFields, makerFillAmount: BigNumber): BigNumber {
// Round up because exchange rate favors Maker
const takerFillAmount = makerFillAmount
.multipliedBy(order.takerAmount)
.div(order.makerAmount)
.integerValue(BigNumber.ROUND_CEIL);
return takerFillAmount;
}
export function isERC20EquivalentAssetData(assetData: AssetData): assetData is ERC20AssetData | ERC20BridgeAssetData {
return assetDataUtils.isERC20TokenAssetData(assetData) || assetDataUtils.isERC20BridgeAssetData(assetData);
/**
* Given an amount of taker asset, calculate the fee amount required for the taker
* @param order The order
* @param takerFillAmount the amount of taker asset
*/
export function getNativeAdjustedTakerFeeAmount(order: LimitOrderFields, takerFillAmount: BigNumber): BigNumber {
// Round down because Taker fee rate favors Taker
const takerFeeAmount = takerFillAmount
.multipliedBy(order.takerTokenFeeAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_FLOOR);
return takerFeeAmount;
}
export function getTokenFromAssetData(assetData: string): string {
const data = assetDataUtils.decodeAssetDataOrThrow(assetData);
if (data.assetProxyId !== AssetProxyId.ERC20 && data.assetProxyId !== AssetProxyId.ERC20Bridge) {
throw new Error(`Unsupported exchange proxy quote asset type: ${data.assetProxyId}`);
const EMPTY_FILLABLE_AMOUNTS: NativeOrderFillableAmountFields = {
fillableMakerAmount: ZERO_AMOUNT,
fillableTakerAmount: ZERO_AMOUNT,
fillableTakerFeeAmount: ZERO_AMOUNT,
};
export function getNativeAdjustedFillableAmountsFromTakerAmount(
order: SignedNativeOrder,
takerFillableAmount: BigNumber,
): NativeOrderFillableAmountFields {
if (takerFillableAmount.isZero()) {
return EMPTY_FILLABLE_AMOUNTS;
}
// tslint:disable-next-line:no-unnecessary-type-assertion
return (data as ERC20AssetData).tokenAddress;
return {
fillableTakerAmount: takerFillableAmount,
fillableMakerAmount: getNativeAdjustedMakerFillAmount(order.order, takerFillableAmount),
fillableTakerFeeAmount:
order.type === FillQuoteTransformerOrderType.Limit
? getNativeAdjustedTakerFeeAmount(order.order as LimitOrderFields, takerFillableAmount)
: ZERO_AMOUNT,
};
}
export function getNativeAdjustedFillableAmountsFromMakerAmount(
order: SignedNativeOrder,
makerFillableAmount: BigNumber,
): NativeOrderFillableAmountFields {
if (makerFillableAmount.isZero()) {
return EMPTY_FILLABLE_AMOUNTS;
}
const takerFillableAmount = getNativeAdjustedTakerFillAmount(order.order, makerFillableAmount);
return {
fillableMakerAmount: makerFillableAmount,
fillableTakerAmount: takerFillableAmount,
fillableTakerFeeAmount:
order.type === FillQuoteTransformerOrderType.Limit
? getNativeAdjustedTakerFeeAmount(order.order as LimitOrderFields, takerFillableAmount)
: ZERO_AMOUNT,
};
}

View File

@ -40,6 +40,7 @@ import * as TestNativeOrderSampler from '../test/generated-artifacts/TestNativeO
import * as TwoHopSampler from '../test/generated-artifacts/TwoHopSampler.json';
import * as UniswapSampler from '../test/generated-artifacts/UniswapSampler.json';
import * as UniswapV2Sampler from '../test/generated-artifacts/UniswapV2Sampler.json';
import * as UtilitySampler from '../test/generated-artifacts/UtilitySampler.json';
export const artifacts = {
ApproximateBuys: ApproximateBuys as ContractArtifact,
BalanceChecker: BalanceChecker as ContractArtifact,
@ -62,6 +63,7 @@ export const artifacts = {
TwoHopSampler: TwoHopSampler as ContractArtifact,
UniswapSampler: UniswapSampler as ContractArtifact,
UniswapV2Sampler: UniswapV2Sampler as ContractArtifact,
UtilitySampler: UtilitySampler as ContractArtifact,
IBalancer: IBalancer as ContractArtifact,
IBancor: IBancor as ContractArtifact,
ICurve: ICurve as ContractArtifact,

View File

@ -1,57 +0,0 @@
import * as chai from 'chai';
import * as _ from 'lodash';
import 'mocha';
import { calculateLiquidity } from '../src/utils/calculate_liquidity';
import { chaiSetup } from './utils/chai_setup';
import { testOrders } from './utils/test_orders';
import { baseUnitAmount } from './utils/utils';
chaiSetup.configure();
const expect = chai.expect;
const {
SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET,
SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET,
} = testOrders;
// tslint:disable:custom-no-magic-numbers
describe('#calculateLiquidity', () => {
it('should provide correct liquidity result with feeless orders', () => {
const prunedSignedOrders = SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS;
const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity(
prunedSignedOrders,
);
expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(11));
expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(9));
});
it('should provide correct liquidity result with orders with takerFees in takerAsset', () => {
const prunedSignedOrders = SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET;
const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity(
prunedSignedOrders,
);
expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(11));
expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(15));
});
it('should provide correct liquidity result with orders with takerFees in makerAsset', () => {
const prunedSignedOrders = SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET;
const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity(
prunedSignedOrders,
);
expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(5));
expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(9));
});
it('should provide correct liquidity result with mixed orders with fees and no fees', () => {
const prunedSignedOrders = _.concat(
SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET,
SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET,
SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
);
const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity(
prunedSignedOrders,
);
expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(27));
expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(33));
});
});

View File

@ -49,12 +49,14 @@ const buyMarketSideLiquidity: MarketSideLiquidity = {
inputAmount: new BigNumber(0),
inputToken: ETH_TOKEN,
outputToken: DAI_TOKEN,
dexQuotes: [dexQuotes],
nativeOrders: [],
orderFillableAmounts: [],
twoHopQuotes: [],
rfqtIndicativeQuotes: [],
quotes: {
twoHopQuotes: [],
rfqtIndicativeQuotes: [],
dexQuotes: [dexQuotes],
nativeOrders: [],
},
quoteSourceFilters: new SourceFilters(),
isRfqSupported: false,
};
const sellMarketSideLiquidity: MarketSideLiquidity = {
@ -68,12 +70,14 @@ const sellMarketSideLiquidity: MarketSideLiquidity = {
inputAmount: new BigNumber(0),
inputToken: ETH_TOKEN,
outputToken: DAI_TOKEN,
dexQuotes: [dexQuotes],
nativeOrders: [],
orderFillableAmounts: [],
twoHopQuotes: [],
rfqtIndicativeQuotes: [],
quotes: {
dexQuotes: [dexQuotes],
nativeOrders: [],
twoHopQuotes: [],
rfqtIndicativeQuotes: [],
},
quoteSourceFilters: new SourceFilters(),
isRfqSupported: false,
};
describe('getComparisonPrices', async () => {

View File

@ -57,4 +57,76 @@ blockchainTests.resets('BalanceChecker contract', env => {
}
});
});
describe('getMinOfBalancesOrAllowances', () => {
it('returns the balance if the allowance can cover it', async () => {
const makerToken = await DummyERC20TokenContract.deployFrom0xArtifactAsync(
erc20Artifacts.DummyERC20Token,
env.provider,
env.txDefaults,
artifacts,
constants.DUMMY_TOKEN_NAME,
constants.DUMMY_TOKEN_SYMBOL,
new BigNumber(18),
constants.DUMMY_TOKEN_TOTAL_SUPPLY,
);
const accounts = await web3Wrapper.getAvailableAddressesAsync();
const owner = accounts[0];
const owner2 = accounts[1];
const allowanceTarget = '0xdef1c0ded9bec7f1a1670819833240f027b25eff';
await makerToken.mint(new BigNumber(100)).awaitTransactionSuccessAsync({ from: owner });
await makerToken.approve(allowanceTarget, new BigNumber(150)).awaitTransactionSuccessAsync({ from: owner });
await makerToken.mint(new BigNumber(150)).awaitTransactionSuccessAsync({ from: owner2 });
await makerToken
.approve(allowanceTarget, new BigNumber(200))
.awaitTransactionSuccessAsync({ from: owner2 });
const testResults = await contract
.getMinOfBalancesOrAllowances(
[owner, owner2],
[makerToken.address, makerToken.address],
allowanceTarget,
)
.callAsync();
expect(testResults).to.eql([new BigNumber(100), new BigNumber(150)]);
});
it('returns the allowance if the allowance < balance', async () => {
const makerToken = await DummyERC20TokenContract.deployFrom0xArtifactAsync(
erc20Artifacts.DummyERC20Token,
env.provider,
env.txDefaults,
artifacts,
constants.DUMMY_TOKEN_NAME,
constants.DUMMY_TOKEN_SYMBOL,
new BigNumber(18),
constants.DUMMY_TOKEN_TOTAL_SUPPLY,
);
const accounts = await web3Wrapper.getAvailableAddressesAsync();
const owner = accounts[0];
const owner2 = accounts[1];
const allowanceTarget = '0xdef1c0ded9bec7f1a1670819833240f027b25eff';
await makerToken.mint(new BigNumber(100)).awaitTransactionSuccessAsync({ from: owner });
await makerToken.approve(allowanceTarget, new BigNumber(50)).awaitTransactionSuccessAsync({ from: owner });
await makerToken.mint(new BigNumber(100)).awaitTransactionSuccessAsync({ from: owner2 });
await makerToken.approve(allowanceTarget, new BigNumber(75)).awaitTransactionSuccessAsync({ from: owner2 });
const testResults = await contract
.getMinOfBalancesOrAllowances(
[owner, owner2],
[makerToken.address, makerToken.address],
allowanceTarget,
)
.callAsync();
expect(testResults).to.eql([new BigNumber(50), new BigNumber(75)]);
});
});
});

View File

@ -6,11 +6,12 @@ import {
getRandomPortion,
randomAddress,
} from '@0x/contracts-test-utils';
import { Order } from '@0x/types';
import { BigNumber, hexUtils } from '@0x/utils';
import { SignatureType } from '@0x/protocol-utils';
import { BigNumber, hexUtils, NULL_BYTES } from '@0x/utils';
import * as _ from 'lodash';
import { SamplerCallResult } from '../../src/types';
import { FillQuoteTransformerOrderType, LimitOrderFields } from '../../src';
import { SamplerCallResult, SignedNativeOrder } from '../../src/types';
import { artifacts } from '../artifacts';
import { DummyLiquidityProviderContract, TestERC20BridgeSamplerContract } from '../wrappers';
@ -30,7 +31,7 @@ blockchainTests('erc20-bridge-sampler', env => {
const ETH2DAI_SALT = '0xb713b61bb9bb2958a0f5d1534b21e94fc68c4c0c034b0902ed844f2f6cd1b4f7';
const UNISWAP_BASE_SALT = '0x1d6a6a0506b0b4a554b907a4c29d9f4674e461989d9c1921feb17b26716385ab';
const UNISWAP_V2_SALT = '0xadc7fcb33c735913b8635927e66896b356a53a912ab2ceff929e60a04b53b3c1';
const ERC20_PROXY_ID = '0xf47261b0';
let UNISWAP_V2_ROUTER = '';
const INVALID_TOKEN_PAIR_ERROR = 'ERC20BridgeSampler/INVALID_TOKEN_PAIR';
const MAKER_TOKEN = randomAddress();
const TAKER_TOKEN = randomAddress();
@ -44,6 +45,7 @@ blockchainTests('erc20-bridge-sampler', env => {
env.txDefaults,
{},
);
UNISWAP_V2_ROUTER = await testContract.uniswapV2Router().callAsync();
});
function getPackedHash(...args: string[]): string {
@ -207,23 +209,19 @@ blockchainTests('erc20-bridge-sampler', env => {
return sold;
}
function getDeterministicFillableTakerAssetAmount(order: Order): BigNumber {
const hash = getPackedHash(hexUtils.leftPad(order.salt));
return new BigNumber(hash).mod(order.takerAssetAmount);
function getDeterministicFillableTakerAssetAmount(order: SignedNativeOrder): BigNumber {
const hash = getPackedHash(hexUtils.leftPad(order.order.salt));
return new BigNumber(hash).mod(order.order.takerAmount);
}
function getDeterministicFillableMakerAssetAmount(order: Order): BigNumber {
function getDeterministicFillableMakerAssetAmount(order: SignedNativeOrder): BigNumber {
const takerAmount = getDeterministicFillableTakerAssetAmount(order);
return order.makerAssetAmount
return order.order.makerAmount
.times(takerAmount)
.div(order.takerAssetAmount)
.div(order.order.takerAmount)
.integerValue(BigNumber.ROUND_UP);
}
function getERC20AssetData(tokenAddress: string): string {
return hexUtils.concat(ERC20_PROXY_ID, hexUtils.leftPad(tokenAddress));
}
function getSampleAmounts(tokenAddress: string, count?: number): BigNumber[] {
const tokenDecimals = getDeterministicTokenDecimals(tokenAddress);
const _upperLimit = getRandomPortion(getRandomInteger(1000, 50000).times(10 ** tokenDecimals));
@ -232,28 +230,30 @@ blockchainTests('erc20-bridge-sampler', env => {
return _.times(_count, i => d.times((i + 1) / _count).integerValue());
}
function createOrder(makerToken: string, takerToken: string): Order {
function createOrder(makerToken: string, takerToken: string): SignedNativeOrder {
return {
chainId: 1337,
exchangeAddress: randomAddress(),
makerAddress: randomAddress(),
takerAddress: randomAddress(),
senderAddress: randomAddress(),
feeRecipientAddress: randomAddress(),
makerAssetAmount: getRandomInteger(1, 1e18),
takerAssetAmount: getRandomInteger(1, 1e18),
makerFee: getRandomInteger(1, 1e18),
takerFee: getRandomInteger(1, 1e18),
makerAssetData: getERC20AssetData(makerToken),
takerAssetData: getERC20AssetData(takerToken),
makerFeeAssetData: getERC20AssetData(randomAddress()),
takerFeeAssetData: getERC20AssetData(randomAddress()),
salt: new BigNumber(hexUtils.random()),
expirationTimeSeconds: getRandomInteger(0, 2 ** 32),
order: {
chainId: 1337,
verifyingContract: randomAddress(),
maker: randomAddress(),
taker: randomAddress(),
pool: NULL_BYTES,
sender: NULL_ADDRESS,
feeRecipient: randomAddress(),
makerAmount: getRandomInteger(1, 1e18),
takerAmount: getRandomInteger(1, 1e18),
takerTokenFeeAmount: getRandomInteger(1, 1e18),
makerToken,
takerToken,
salt: new BigNumber(hexUtils.random()),
expiry: getRandomInteger(0, 2 ** 32),
},
signature: { v: 1, r: NULL_BYTES, s: NULL_BYTES, signatureType: SignatureType.EthSign },
type: FillQuoteTransformerOrderType.Limit,
};
}
function createOrders(makerToken: string, takerToken: string, count?: number): Order[] {
function createOrders(makerToken: string, takerToken: string, count?: number): SignedNativeOrder[] {
return _.times(count || _.random(1, 16), () => createOrder(makerToken, takerToken));
}
@ -291,16 +291,20 @@ blockchainTests('erc20-bridge-sampler', env => {
describe('getOrderFillableTakerAssetAmounts()', () => {
it('returns the expected amount for each order', async () => {
const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN);
const signatures: string[] = _.times(orders.length, i => hexUtils.random());
const expected = orders.map(getDeterministicFillableTakerAssetAmount);
const actual = await testContract
.getOrderFillableTakerAssetAmounts(orders, signatures, NULL_ADDRESS)
.getLimitOrderFillableTakerAssetAmounts(
// tslint:disable-next-line:no-unnecessary-type-assertion
orders.map(o => o.order as LimitOrderFields),
orders.map(o => o.signature),
NULL_ADDRESS,
)
.callAsync();
expect(actual).to.deep.eq(expected);
});
it('returns empty for no orders', async () => {
const actual = await testContract.getOrderFillableTakerAssetAmounts([], [], NULL_ADDRESS).callAsync();
const actual = await testContract.getLimitOrderFillableTakerAssetAmounts([], [], NULL_ADDRESS).callAsync();
expect(actual).to.deep.eq([]);
});
});
@ -308,16 +312,20 @@ blockchainTests('erc20-bridge-sampler', env => {
describe('getOrderFillableMakerAssetAmounts()', () => {
it('returns the expected amount for each order', async () => {
const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN);
const signatures: string[] = _.times(orders.length, i => hexUtils.random());
const expected = orders.map(getDeterministicFillableMakerAssetAmount);
const actual = await testContract
.getOrderFillableMakerAssetAmounts(orders, signatures, NULL_ADDRESS)
.getLimitOrderFillableMakerAssetAmounts(
// tslint:disable-next-line:no-unnecessary-type-assertion
orders.map(o => o.order as LimitOrderFields),
orders.map(o => o.signature),
NULL_ADDRESS,
)
.callAsync();
expect(actual).to.deep.eq(expected);
});
it('returns empty for no orders', async () => {
const actual = await testContract.getOrderFillableMakerAssetAmounts([], [], NULL_ADDRESS).callAsync();
const actual = await testContract.getLimitOrderFillableMakerAssetAmounts([], [], NULL_ADDRESS).callAsync();
expect(actual).to.deep.eq([]);
});
});
@ -858,7 +866,9 @@ blockchainTests('erc20-bridge-sampler', env => {
}
it('can return no quotes', async () => {
const quotes = await testContract.sampleSellsFromUniswapV2([TAKER_TOKEN, MAKER_TOKEN], []).callAsync();
const quotes = await testContract
.sampleSellsFromUniswapV2(UNISWAP_V2_ROUTER, [TAKER_TOKEN, MAKER_TOKEN], [])
.callAsync();
expect(quotes).to.deep.eq([]);
});
@ -866,7 +876,7 @@ blockchainTests('erc20-bridge-sampler', env => {
const sampleAmounts = getSampleAmounts(TAKER_TOKEN);
const expectedQuotes = predictSellQuotes([TAKER_TOKEN, MAKER_TOKEN], sampleAmounts);
const quotes = await testContract
.sampleSellsFromUniswapV2([TAKER_TOKEN, MAKER_TOKEN], sampleAmounts)
.sampleSellsFromUniswapV2(UNISWAP_V2_ROUTER, [TAKER_TOKEN, MAKER_TOKEN], sampleAmounts)
.callAsync();
expect(quotes).to.deep.eq(expectedQuotes);
});
@ -876,7 +886,7 @@ blockchainTests('erc20-bridge-sampler', env => {
const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT);
await enableFailTriggerAsync();
const quotes = await testContract
.sampleSellsFromUniswapV2([TAKER_TOKEN, MAKER_TOKEN], sampleAmounts)
.sampleSellsFromUniswapV2(UNISWAP_V2_ROUTER, [TAKER_TOKEN, MAKER_TOKEN], sampleAmounts)
.callAsync();
expect(quotes).to.deep.eq(expectedQuotes);
});
@ -886,7 +896,11 @@ blockchainTests('erc20-bridge-sampler', env => {
const sampleAmounts = getSampleAmounts(TAKER_TOKEN);
const expectedQuotes = predictSellQuotes([TAKER_TOKEN, intermediateToken, MAKER_TOKEN], sampleAmounts);
const quotes = await testContract
.sampleSellsFromUniswapV2([TAKER_TOKEN, intermediateToken, MAKER_TOKEN], sampleAmounts)
.sampleSellsFromUniswapV2(
UNISWAP_V2_ROUTER,
[TAKER_TOKEN, intermediateToken, MAKER_TOKEN],
sampleAmounts,
)
.callAsync();
expect(quotes).to.deep.eq(expectedQuotes);
});
@ -898,7 +912,9 @@ blockchainTests('erc20-bridge-sampler', env => {
}
it('can return no quotes', async () => {
const quotes = await testContract.sampleBuysFromUniswapV2([TAKER_TOKEN, MAKER_TOKEN], []).callAsync();
const quotes = await testContract
.sampleBuysFromUniswapV2(UNISWAP_V2_ROUTER, [TAKER_TOKEN, MAKER_TOKEN], [])
.callAsync();
expect(quotes).to.deep.eq([]);
});
@ -906,7 +922,7 @@ blockchainTests('erc20-bridge-sampler', env => {
const sampleAmounts = getSampleAmounts(MAKER_TOKEN);
const expectedQuotes = predictBuyQuotes([TAKER_TOKEN, MAKER_TOKEN], sampleAmounts);
const quotes = await testContract
.sampleBuysFromUniswapV2([TAKER_TOKEN, MAKER_TOKEN], sampleAmounts)
.sampleBuysFromUniswapV2(UNISWAP_V2_ROUTER, [TAKER_TOKEN, MAKER_TOKEN], sampleAmounts)
.callAsync();
expect(quotes).to.deep.eq(expectedQuotes);
});
@ -916,7 +932,7 @@ blockchainTests('erc20-bridge-sampler', env => {
const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT);
await enableFailTriggerAsync();
const quotes = await testContract
.sampleBuysFromUniswapV2([TAKER_TOKEN, MAKER_TOKEN], sampleAmounts)
.sampleBuysFromUniswapV2(UNISWAP_V2_ROUTER, [TAKER_TOKEN, MAKER_TOKEN], sampleAmounts)
.callAsync();
expect(quotes).to.deep.eq(expectedQuotes);
});
@ -926,7 +942,11 @@ blockchainTests('erc20-bridge-sampler', env => {
const sampleAmounts = getSampleAmounts(MAKER_TOKEN);
const expectedQuotes = predictBuyQuotes([TAKER_TOKEN, intermediateToken, MAKER_TOKEN], sampleAmounts);
const quotes = await testContract
.sampleBuysFromUniswapV2([TAKER_TOKEN, intermediateToken, MAKER_TOKEN], sampleAmounts)
.sampleBuysFromUniswapV2(
UNISWAP_V2_ROUTER,
[TAKER_TOKEN, intermediateToken, MAKER_TOKEN],
sampleAmounts,
)
.callAsync();
expect(quotes).to.deep.eq(expectedQuotes);
});
@ -935,17 +955,21 @@ blockchainTests('erc20-bridge-sampler', env => {
describe('batchCall()', () => {
it('can call one function', async () => {
const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN);
const signatures: string[] = _.times(orders.length, i => hexUtils.random());
const expected = orders.map(getDeterministicFillableTakerAssetAmount);
const calls = [
testContract
.getOrderFillableTakerAssetAmounts(orders, signatures, NULL_ADDRESS)
.getLimitOrderFillableTakerAssetAmounts(
// tslint:disable-next-line:no-unnecessary-type-assertion
orders.map(o => o.order as LimitOrderFields),
orders.map(o => o.signature),
NULL_ADDRESS,
)
.getABIEncodedTransactionData(),
];
const r = await testContract.batchCall(calls).callAsync();
expect(r).to.be.length(1);
const actual = testContract.getABIDecodedReturnData<BigNumber[]>(
'getOrderFillableTakerAssetAmounts',
'getLimitOrderFillableTakerAssetAmounts',
r[0].data,
);
expect(actual).to.deep.eq(expected);
@ -954,40 +978,53 @@ blockchainTests('erc20-bridge-sampler', env => {
it('can call two functions', async () => {
const numOrders = _.random(1, 10);
const orders = _.times(2, () => createOrders(MAKER_TOKEN, TAKER_TOKEN, numOrders));
const signatures: string[] = _.times(numOrders, i => hexUtils.random());
const expecteds = [
orders[0].map(getDeterministicFillableTakerAssetAmount),
orders[1].map(getDeterministicFillableMakerAssetAmount),
];
const calls = [
testContract
.getOrderFillableTakerAssetAmounts(orders[0], signatures, NULL_ADDRESS)
.getLimitOrderFillableTakerAssetAmounts(
// tslint:disable-next-line:no-unnecessary-type-assertion
orders[0].map(o => o.order as LimitOrderFields),
orders[0].map(o => o.signature),
NULL_ADDRESS,
)
.getABIEncodedTransactionData(),
testContract
.getOrderFillableMakerAssetAmounts(orders[1], signatures, NULL_ADDRESS)
.getLimitOrderFillableMakerAssetAmounts(
// tslint:disable-next-line:no-unnecessary-type-assertion
orders[1].map(o => o.order as LimitOrderFields),
orders[1].map(o => o.signature),
NULL_ADDRESS,
)
.getABIEncodedTransactionData(),
];
const r = await testContract.batchCall(calls).callAsync();
expect(r).to.be.length(2);
expect(testContract.getABIDecodedReturnData('getOrderFillableTakerAssetAmounts', r[0].data)).to.deep.eq(
expecteds[0],
);
expect(testContract.getABIDecodedReturnData('getOrderFillableMakerAssetAmounts', r[1].data)).to.deep.eq(
expecteds[1],
);
expect(
testContract.getABIDecodedReturnData('getLimitOrderFillableTakerAssetAmounts', r[0].data),
).to.deep.eq(expecteds[0]);
expect(
testContract.getABIDecodedReturnData('getLimitOrderFillableMakerAssetAmounts', r[1].data),
).to.deep.eq(expecteds[1]);
});
it('can make recursive calls', async () => {
const numOrders = _.random(1, 10);
const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN, numOrders);
const signatures: string[] = _.times(numOrders, i => hexUtils.random());
const expected = orders.map(getDeterministicFillableTakerAssetAmount);
let r = await testContract
.batchCall([
testContract
.batchCall([
testContract
.getOrderFillableTakerAssetAmounts(orders, signatures, NULL_ADDRESS)
.getLimitOrderFillableTakerAssetAmounts(
// tslint:disable-next-line:no-unnecessary-type-assertion
orders.map(o => o.order as LimitOrderFields),
orders.map(o => o.signature),
NULL_ADDRESS,
)
.getABIEncodedTransactionData(),
])
.getABIEncodedTransactionData(),
@ -996,9 +1033,9 @@ blockchainTests('erc20-bridge-sampler', env => {
expect(r).to.be.length(1);
r = testContract.getABIDecodedReturnData<SamplerCallResult[]>('batchCall', r[0].data);
expect(r).to.be.length(1);
expect(testContract.getABIDecodedReturnData('getOrderFillableTakerAssetAmounts', r[0].data)).to.deep.eq(
expected,
);
expect(
testContract.getABIDecodedReturnData('getLimitOrderFillableTakerAssetAmounts', r[0].data),
).to.deep.eq(expected);
});
});
@ -1014,12 +1051,12 @@ blockchainTests('erc20-bridge-sampler', env => {
const sellAmount = _.last(getSampleAmounts(TAKER_TOKEN))!;
const uniswapV2FirstHopPath = [TAKER_TOKEN, INTERMEDIATE_TOKEN];
const uniswapV2FirstHop = testContract
.sampleSellsFromUniswapV2(uniswapV2FirstHopPath, [constants.ZERO_AMOUNT])
.sampleSellsFromUniswapV2(UNISWAP_V2_ROUTER, uniswapV2FirstHopPath, [constants.ZERO_AMOUNT])
.getABIEncodedTransactionData();
const uniswapV2SecondHopPath = [INTERMEDIATE_TOKEN, randomAddress(), MAKER_TOKEN];
const uniswapV2SecondHop = testContract
.sampleSellsFromUniswapV2(uniswapV2SecondHopPath, [constants.ZERO_AMOUNT])
.sampleSellsFromUniswapV2(UNISWAP_V2_ROUTER, uniswapV2SecondHopPath, [constants.ZERO_AMOUNT])
.getABIEncodedTransactionData();
const eth2DaiFirstHop = testContract
@ -1065,12 +1102,12 @@ blockchainTests('erc20-bridge-sampler', env => {
const buyAmount = _.last(getSampleAmounts(MAKER_TOKEN))!;
const uniswapV2FirstHopPath = [TAKER_TOKEN, INTERMEDIATE_TOKEN];
const uniswapV2FirstHop = testContract
.sampleBuysFromUniswapV2(uniswapV2FirstHopPath, [constants.ZERO_AMOUNT])
.sampleBuysFromUniswapV2(UNISWAP_V2_ROUTER, uniswapV2FirstHopPath, [constants.ZERO_AMOUNT])
.getABIEncodedTransactionData();
const uniswapV2SecondHopPath = [INTERMEDIATE_TOKEN, randomAddress(), MAKER_TOKEN];
const uniswapV2SecondHop = testContract
.sampleBuysFromUniswapV2(uniswapV2SecondHopPath, [constants.ZERO_AMOUNT])
.sampleBuysFromUniswapV2(UNISWAP_V2_ROUTER, uniswapV2SecondHopPath, [constants.ZERO_AMOUNT])
.getABIEncodedTransactionData();
const eth2DaiFirstHop = testContract

View File

@ -7,10 +7,12 @@ import {
getRandomInteger,
randomAddress,
} from '@0x/contracts-test-utils';
import { Order } from '@0x/types';
import { SignatureType } from '@0x/protocol-utils';
import { BigNumber, hexUtils } from '@0x/utils';
import * as _ from 'lodash';
import { LimitOrderFields } from '../../src';
import { NULL_ADDRESS } from '../../src/utils/market_operation_utils/constants';
import { artifacts } from '../artifacts';
import { TestNativeOrderSamplerContract } from '../wrappers';
@ -18,15 +20,12 @@ const { NULL_BYTES, ZERO_AMOUNT } = constants;
// tslint:disable: custom-no-magic-numbers
blockchainTests.resets('NativeOrderSampler contract', env => {
// TODO jacob
blockchainTests.resets.skip('NativeOrderSampler contract', env => {
let testContract: TestNativeOrderSamplerContract;
let makerToken: string;
let takerToken: string;
let feeToken: string;
let erc20Proxy: string;
const ERC20_PROXY_ID = '0xf47261b0';
const VALID_SIGNATURE = '0x01';
const INVALID_SIGNATURE = '0x00';
const VALID_SIGNATURE = { v: 1, r: '0x01', s: '0x01', signatureType: SignatureType.EthSign };
before(async () => {
testContract = await TestNativeOrderSamplerContract.deployFrom0xArtifactAsync(
@ -35,9 +34,8 @@ blockchainTests.resets('NativeOrderSampler contract', env => {
env.txDefaults,
{},
);
erc20Proxy = await testContract.getAssetProxy(ERC20_PROXY_ID).callAsync();
const NUM_TOKENS = new BigNumber(3);
[makerToken, takerToken, feeToken] = await testContract.createTokens(NUM_TOKENS).callAsync();
[makerToken, takerToken] = await testContract.createTokens(NUM_TOKENS).callAsync();
await testContract.createTokens(NUM_TOKENS).awaitTransactionSuccessAsync();
});
@ -51,10 +49,10 @@ blockchainTests.resets('NativeOrderSampler contract', env => {
orderTakerAssetFilledAmount: BigNumber;
}
function getOrderInfo(order: Order): OrderInfo {
function getOrderInfo(order: LimitOrderFields): OrderInfo {
const hash = getPackedHash(hexUtils.leftPad(order.salt));
const orderStatus = order.salt.mod(255).eq(0) ? 3 : 5;
const filledAmount = order.expirationTimeSeconds;
const filledAmount = order.expiry;
return {
orderStatus,
orderHash: hash,
@ -70,61 +68,46 @@ blockchainTests.resets('NativeOrderSampler contract', env => {
return new BigNumber(hexUtils.concat(hexUtils.slice(hexUtils.random(), 0, -1), '0xff'));
}
function getOrderFillableTakerAmount(order: Order): BigNumber {
return order.takerAssetAmount.minus(getOrderInfo(order).orderTakerAssetFilledAmount);
function getLimitOrderFillableTakerAmount(order: LimitOrderFields): BigNumber {
return order.takerAmount.minus(getOrderInfo(order).orderTakerAssetFilledAmount);
}
function getERC20AssetData(tokenAddress: string): string {
return hexUtils.concat(ERC20_PROXY_ID, hexUtils.leftPad(tokenAddress));
}
function createOrder(fields: Partial<Order> = {}, filledTakerAssetAmount: BigNumber = ZERO_AMOUNT): Order {
function createOrder(
fields: Partial<LimitOrderFields> = {},
filledTakerAssetAmount: BigNumber = ZERO_AMOUNT,
): LimitOrderFields {
return {
chainId: 1337,
exchangeAddress: randomAddress(),
makerAddress: randomAddress(),
takerAddress: randomAddress(),
senderAddress: randomAddress(),
feeRecipientAddress: randomAddress(),
makerAssetAmount: getRandomInteger(1e18, 10e18),
takerAssetAmount: getRandomInteger(1e18, 10e18),
makerFee: getRandomInteger(1e18, 10e18),
takerFee: getRandomInteger(1e18, 10e18),
makerAssetData: getERC20AssetData(makerToken),
takerAssetData: getERC20AssetData(takerToken),
makerFeeAssetData: getERC20AssetData(feeToken),
takerFeeAssetData: getERC20AssetData(randomAddress()),
verifyingContract: randomAddress(),
maker: randomAddress(),
taker: randomAddress(),
pool: NULL_BYTES,
sender: NULL_ADDRESS,
feeRecipient: randomAddress(),
makerAmount: getRandomInteger(1, 1e18),
takerAmount: getRandomInteger(1, 1e18),
takerTokenFeeAmount: getRandomInteger(1, 1e18),
makerToken,
takerToken,
salt: createFillableOrderSalt(),
// Expiration time will be used to determine filled amount.
expirationTimeSeconds: filledTakerAssetAmount,
expiry: filledTakerAssetAmount,
...fields,
};
}
async function fundMakerAsync(
order: Order,
assetData: string,
order: LimitOrderFields,
balanceScaling: number = 1,
allowanceScaling: number = 1,
): Promise<void> {
let token;
let amount;
if (assetData === order.makerAssetData) {
token = makerToken;
amount =
order.makerAssetData === order.makerFeeAssetData
? order.makerAssetAmount.plus(order.makerFee)
: order.makerAssetAmount;
} else {
token = feeToken;
amount = order.makerFee;
}
amount = amount.times(getOrderFillableTakerAmount(order).div(BigNumber.max(1, order.takerAssetAmount)));
const token = makerToken;
let amount = order.makerAmount;
amount = amount.times(getLimitOrderFillableTakerAmount(order).div(BigNumber.max(1, order.takerAmount)));
await testContract
.setTokenBalanceAndAllowance(
token,
order.makerAddress,
erc20Proxy,
order.maker,
testContract.address,
amount.times(balanceScaling).integerValue(),
amount.times(allowanceScaling).integerValue(),
)
@ -132,7 +115,7 @@ blockchainTests.resets('NativeOrderSampler contract', env => {
}
describe('getTokenDecimals()', () => {
it('correctly returns the token balances', async () => {
it('correctly returns the token decimals', async () => {
const newMakerToken = await DummyERC20TokenContract.deployFrom0xArtifactAsync(
erc20Artifacts.DummyERC20Token,
env.provider,
@ -154,130 +137,44 @@ blockchainTests.resets('NativeOrderSampler contract', env => {
constants.DUMMY_TOKEN_TOTAL_SUPPLY,
);
const [makerDecimals, takerDecimals] = await testContract
.getTokenDecimals(newMakerToken.address, newTakerToken.address)
.getTokenDecimals([newMakerToken.address, newTakerToken.address])
.callAsync();
expect(makerDecimals.toString()).to.eql('18');
expect(takerDecimals.toString()).to.eql('6');
});
});
describe('getOrderFillableTakerAmount()', () => {
describe('getLimitOrderFillableTakerAmount()', () => {
it('returns the full amount for a fully funded order', async () => {
const order = createOrder();
const expected = getOrderFillableTakerAmount(order);
await fundMakerAsync(order, order.makerAssetData);
await fundMakerAsync(order, order.makerFeeAssetData);
const expected = getLimitOrderFillableTakerAmount(order);
await fundMakerAsync(order);
const actual = await testContract
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
.callAsync();
expect(actual).to.bignumber.eq(expected);
});
it('returns the full amount for a fully funded order without maker fees', async () => {
const order = createOrder({ makerFee: ZERO_AMOUNT });
const expected = getOrderFillableTakerAmount(order);
await fundMakerAsync(order, order.makerAssetData);
await fundMakerAsync(order, order.makerFeeAssetData);
const actual = await testContract
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
.callAsync();
expect(actual).to.bignumber.eq(expected);
});
it('returns the full amount for a fully funded order without maker fee asset data', async () => {
const order = createOrder({ makerFeeAssetData: NULL_BYTES });
const expected = getOrderFillableTakerAmount(order);
await fundMakerAsync(order, order.makerAssetData);
await fundMakerAsync(order, order.makerFeeAssetData);
const actual = await testContract
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
.callAsync();
expect(actual).to.bignumber.eq(expected);
});
it('returns the full amount for a fully funded order with maker fees denominated in the maker asset', async () => {
const order = createOrder({ makerFeeAssetData: getERC20AssetData(makerToken) });
const expected = getOrderFillableTakerAmount(order);
await fundMakerAsync(order, order.makerAssetData);
await fundMakerAsync(order, order.makerFeeAssetData);
const actual = await testContract
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
.getLimitOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
.callAsync();
expect(actual).to.bignumber.eq(expected);
});
it('returns partial amount with insufficient maker asset balance', async () => {
const order = createOrder();
const expected = getOrderFillableTakerAmount(order)
const expected = getLimitOrderFillableTakerAmount(order)
.times(0.5)
.integerValue(BigNumber.ROUND_DOWN);
await fundMakerAsync(order, order.makerAssetData, 0.5);
await fundMakerAsync(order, order.makerFeeAssetData);
await fundMakerAsync(order, 0.5);
const actual = await testContract
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
.getLimitOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
.callAsync();
assertIntegerRoughlyEquals(actual, expected, 100);
});
it('returns partial amount with insufficient maker asset allowance', async () => {
const order = createOrder();
const expected = getOrderFillableTakerAmount(order)
const expected = getLimitOrderFillableTakerAmount(order)
.times(0.5)
.integerValue(BigNumber.ROUND_DOWN);
await fundMakerAsync(order, order.makerAssetData, 1, 0.5);
await fundMakerAsync(order, order.makerFeeAssetData);
await fundMakerAsync(order, 1, 0.5);
const actual = await testContract
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
.callAsync();
assertIntegerRoughlyEquals(actual, expected, 100);
});
it('returns partial amount with insufficient maker fee asset balance', async () => {
const order = createOrder();
const expected = getOrderFillableTakerAmount(order)
.times(0.5)
.integerValue(BigNumber.ROUND_DOWN);
await fundMakerAsync(order, order.makerAssetData);
await fundMakerAsync(order, order.makerFeeAssetData, 0.5);
const actual = await testContract
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
.callAsync();
assertIntegerRoughlyEquals(actual, expected, 100);
});
it('returns partial amount with insufficient maker fee asset allowance', async () => {
const order = createOrder();
const expected = getOrderFillableTakerAmount(order)
.times(0.5)
.integerValue(BigNumber.ROUND_DOWN);
await fundMakerAsync(order, order.makerAssetData);
await fundMakerAsync(order, order.makerFeeAssetData, 1, 0.5);
const actual = await testContract
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
.callAsync();
assertIntegerRoughlyEquals(actual, expected, 100);
});
it('returns partial amount with insufficient maker asset balance (maker asset fees)', async () => {
const order = createOrder({ makerFeeAssetData: getERC20AssetData(makerToken) });
const expected = getOrderFillableTakerAmount(order)
.times(0.5)
.integerValue(BigNumber.ROUND_DOWN);
await fundMakerAsync(order, order.makerAssetData, 0.5);
const actual = await testContract
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
.callAsync();
assertIntegerRoughlyEquals(actual, expected, 100);
});
it('returns partial amount with insufficient maker asset allowance (maker asset fees)', async () => {
const order = createOrder({ makerFeeAssetData: getERC20AssetData(makerToken) });
const expected = getOrderFillableTakerAmount(order)
.times(0.5)
.integerValue(BigNumber.ROUND_DOWN);
await fundMakerAsync(order, order.makerAssetData, 1, 0.5);
const actual = await testContract
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
.getLimitOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
.callAsync();
assertIntegerRoughlyEquals(actual, expected, 100);
});
@ -287,10 +184,9 @@ blockchainTests.resets('NativeOrderSampler contract', env => {
...createOrder(),
salt: createUnfillableOrderSalt(),
};
await fundMakerAsync(order, order.makerAssetData);
await fundMakerAsync(order, order.makerFeeAssetData);
await fundMakerAsync(order);
const fillableTakerAmount = await testContract
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
.getLimitOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
.callAsync();
expect(fillableTakerAmount).to.bignumber.eq(ZERO_AMOUNT);
});
@ -298,12 +194,11 @@ blockchainTests.resets('NativeOrderSampler contract', env => {
it('returns zero for an order with zero maker asset amount', async () => {
const order = {
...createOrder(),
makerAssetAmount: ZERO_AMOUNT,
makerAmount: ZERO_AMOUNT,
};
await fundMakerAsync(order, order.makerAssetData);
await fundMakerAsync(order, order.makerFeeAssetData);
await fundMakerAsync(order);
const fillableTakerAmount = await testContract
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
.getLimitOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
.callAsync();
expect(fillableTakerAmount).to.bignumber.eq(ZERO_AMOUNT);
});
@ -311,32 +206,24 @@ blockchainTests.resets('NativeOrderSampler contract', env => {
it('returns zero for an order with zero taker asset amount', async () => {
const order = {
...createOrder(),
takerAssetAmount: ZERO_AMOUNT,
takerAmount: ZERO_AMOUNT,
};
await fundMakerAsync(order, order.makerAssetData);
await fundMakerAsync(order, order.makerFeeAssetData);
await fundMakerAsync(order);
const fillableTakerAmount = await testContract
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
.getLimitOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
.callAsync();
expect(fillableTakerAmount).to.bignumber.eq(ZERO_AMOUNT);
});
it('returns zero for an order with an empty signature', async () => {
const order = createOrder();
await fundMakerAsync(order, order.makerAssetData);
await fundMakerAsync(order, order.makerFeeAssetData);
await fundMakerAsync(order);
const fillableTakerAmount = await testContract
.getOrderFillableTakerAmount(order, NULL_BYTES, testContract.address)
.callAsync();
expect(fillableTakerAmount).to.bignumber.eq(ZERO_AMOUNT);
});
it('returns zero for an order with an invalid signature', async () => {
const order = createOrder();
await fundMakerAsync(order, order.makerAssetData);
await fundMakerAsync(order, order.makerFeeAssetData);
const fillableTakerAmount = await testContract
.getOrderFillableTakerAmount(order, INVALID_SIGNATURE, testContract.address)
.getLimitOrderFillableTakerAmount(
order,
{ ...VALID_SIGNATURE, r: NULL_BYTES, s: NULL_BYTES },
testContract.address,
)
.callAsync();
expect(fillableTakerAmount).to.bignumber.eq(ZERO_AMOUNT);
});

View File

@ -7,28 +7,28 @@ import {
randomAddress,
toBaseUnitAmount,
} from '@0x/contracts-test-utils';
import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber, hexUtils } from '@0x/utils';
import { FillQuoteTransformerOrderType, LimitOrderFields, SignatureType } from '@0x/protocol-utils';
import { BigNumber, hexUtils, NULL_ADDRESS, NULL_BYTES } from '@0x/utils';
import { Pool } from '@balancer-labs/sor/dist/types';
import * as _ from 'lodash';
import { BalancerPool } from '../src/utils/market_operation_utils/balancer_utils';
import { SignedOrder } from '../src/types';
import { DexOrderSampler, getSampleAmounts } from '../src/utils/market_operation_utils/sampler';
import { ERC20BridgeSource, TokenAdjacencyGraph } from '../src/utils/market_operation_utils/types';
import { MockBalancerPoolsCache } from './utils/mock_balancer_pools_cache';
import { MockSamplerContract } from './utils/mock_sampler_contract';
import { generatePseudoRandomSalt } from './utils/utils';
const CHAIN_ID = 1;
// tslint:disable: custom-no-magic-numbers
describe('DexSampler tests', () => {
const MAKER_TOKEN = randomAddress();
const TAKER_TOKEN = randomAddress();
const MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN);
const TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN);
const wethAddress = getContractAddressesForChainOrThrow(CHAIN_ID).etherToken;
const exchangeAddress = getContractAddressesForChainOrThrow(CHAIN_ID).exchange;
const exchangeProxyAddress = getContractAddressesForChainOrThrow(CHAIN_ID).exchangeProxy;
const tokenAdjacencyGraph: TokenAdjacencyGraph = { default: [wethAddress] };
@ -67,36 +67,39 @@ describe('DexSampler tests', () => {
});
});
function createOrder(overrides?: Partial<SignedOrder>): SignedOrder {
return {
chainId: CHAIN_ID,
exchangeAddress: randomAddress(),
makerAddress: constants.NULL_ADDRESS,
takerAddress: constants.NULL_ADDRESS,
senderAddress: constants.NULL_ADDRESS,
feeRecipientAddress: randomAddress(),
salt: generatePseudoRandomSalt(),
expirationTimeSeconds: getRandomInteger(0, 2 ** 64),
makerAssetData: MAKER_ASSET_DATA,
takerAssetData: TAKER_ASSET_DATA,
makerFeeAssetData: constants.NULL_BYTES,
takerFeeAssetData: constants.NULL_BYTES,
makerAssetAmount: getRandomInteger(1, 1e18),
takerAssetAmount: getRandomInteger(1, 1e18),
makerFee: constants.ZERO_AMOUNT,
takerFee: constants.ZERO_AMOUNT,
signature: hexUtils.random(),
...overrides,
function createOrder(overrides?: Partial<LimitOrderFields>): SignedOrder<LimitOrderFields> {
const o: SignedOrder<LimitOrderFields> = {
order: {
salt: generatePseudoRandomSalt(),
expiry: getRandomInteger(0, 2 ** 64),
makerToken: MAKER_TOKEN,
takerToken: TAKER_TOKEN,
makerAmount: getRandomInteger(1, 1e18),
takerAmount: getRandomInteger(1, 1e18),
takerTokenFeeAmount: constants.ZERO_AMOUNT,
chainId: CHAIN_ID,
pool: NULL_BYTES,
feeRecipient: NULL_ADDRESS,
sender: NULL_ADDRESS,
maker: NULL_ADDRESS,
taker: NULL_ADDRESS,
verifyingContract: exchangeProxyAddress,
...overrides,
},
signature: { v: 1, r: hexUtils.random(), s: hexUtils.random(), signatureType: SignatureType.EthSign },
type: FillQuoteTransformerOrderType.Limit,
};
return o;
}
const ORDERS = _.times(4, () => createOrder());
const SIMPLE_ORDERS = ORDERS.map(o => _.omit(o, ['signature', 'chainId', 'exchangeAddress']));
const SIMPLE_ORDERS = ORDERS.map(o => _.omit(o, ['signature', 'chainId']));
describe('operations', () => {
it('getOrderFillableMakerAmounts()', async () => {
// TODO jacob
it.skip('getLimitOrderFillableMakerAssetAmounts()', async () => {
const expectedFillableAmounts = ORDERS.map(() => getRandomInteger(0, 100e18));
const sampler = new MockSamplerContract({
getOrderFillableMakerAssetAmounts: (orders, signatures) => {
getLimitOrderFillableMakerAssetAmounts: (orders, signatures) => {
expect(orders).to.deep.eq(SIMPLE_ORDERS);
expect(signatures).to.deep.eq(ORDERS.map(o => o.signature));
return expectedFillableAmounts;
@ -112,15 +115,16 @@ describe('DexSampler tests', () => {
async () => undefined,
);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
dexOrderSampler.getOrderFillableMakerAmounts(ORDERS, exchangeAddress),
dexOrderSampler.getLimitOrderFillableMakerAmounts(ORDERS, exchangeAddress),
);
expect(fillableAmounts).to.deep.eq(expectedFillableAmounts);
});
it('getOrderFillableTakerAmounts()', async () => {
// TODO jacob
it.skip('getLimitOrderFillableTakerAssetAmounts()', async () => {
const expectedFillableAmounts = ORDERS.map(() => getRandomInteger(0, 100e18));
const sampler = new MockSamplerContract({
getOrderFillableTakerAssetAmounts: (orders, signatures) => {
getLimitOrderFillableTakerAssetAmounts: (orders, signatures) => {
expect(orders).to.deep.eq(SIMPLE_ORDERS);
expect(signatures).to.deep.eq(ORDERS.map(o => o.signature));
return expectedFillableAmounts;
@ -136,7 +140,7 @@ describe('DexSampler tests', () => {
async () => undefined,
);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
dexOrderSampler.getOrderFillableTakerAmounts(ORDERS, exchangeAddress),
dexOrderSampler.getLimitOrderFillableTakerAmounts(ORDERS, exchangeAddress),
);
expect(fillableAmounts).to.deep.eq(expectedFillableAmounts);
});
@ -324,7 +328,7 @@ describe('DexSampler tests', () => {
const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10);
const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 10);
const sampler = new MockSamplerContract({
sampleSellsFromUniswapV2: (path, fillAmounts) => {
sampleSellsFromUniswapV2: (_router, path, fillAmounts) => {
expect(path).to.deep.eq([expectedMakerToken, expectedTakerToken]);
expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts);
return expectedMakerFillAmounts;
@ -419,6 +423,7 @@ describe('DexSampler tests', () => {
[ERC20BridgeSource.UniswapV2]: getRandomFloat(0, 100),
};
const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3);
let uniswapRouter: string;
const sampler = new MockSamplerContract({
sampleSellsFromUniswap: (takerToken, makerToken, fillAmounts) => {
expect(takerToken).to.eq(expectedTakerToken);
@ -432,7 +437,8 @@ describe('DexSampler tests', () => {
expect(fillAmounts).to.deep.eq(expectedTakerFillAmounts);
return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Eth2Dai]).integerValue());
},
sampleSellsFromUniswapV2: (path, fillAmounts) => {
sampleSellsFromUniswapV2: (router, path, fillAmounts) => {
uniswapRouter = router;
if (path.length === 2) {
expect(path).to.deep.eq([expectedTakerToken, expectedMakerToken]);
} else if (path.length === 3) {
@ -468,7 +474,7 @@ describe('DexSampler tests', () => {
output: a.times(ratesBySource[s]).integerValue(),
fillData:
s === ERC20BridgeSource.UniswapV2
? { tokenAddressPath: [expectedTakerToken, expectedMakerToken] }
? { router: uniswapRouter, tokenAddressPath: [expectedTakerToken, expectedMakerToken] }
: {},
})),
);
@ -478,6 +484,7 @@ describe('DexSampler tests', () => {
input: a,
output: a.times(ratesBySource[ERC20BridgeSource.UniswapV2]).integerValue(),
fillData: {
router: uniswapRouter,
tokenAddressPath: [expectedTakerToken, wethAddress, expectedMakerToken],
},
})),
@ -494,7 +501,7 @@ describe('DexSampler tests', () => {
const expectedTakerToken = randomAddress();
const expectedMakerToken = randomAddress();
const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3);
const pools: BalancerPool[] = [generateBalancerPool(), generateBalancerPool()];
const pools: Pool[] = [generateBalancerPool(), generateBalancerPool()];
const balancerPoolsCache = new MockBalancerPoolsCache({
getPoolsForPairAsync: async (takerToken: string, makerToken: string) => {
expect(takerToken).equal(expectedTakerToken);
@ -528,6 +535,7 @@ describe('DexSampler tests', () => {
[ERC20BridgeSource.UniswapV2]: getRandomFloat(0, 100),
};
const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3);
let uniswapRouter: string;
const sampler = new MockSamplerContract({
sampleBuysFromUniswap: (takerToken, makerToken, fillAmounts) => {
expect(takerToken).to.eq(expectedTakerToken);
@ -541,7 +549,8 @@ describe('DexSampler tests', () => {
expect(fillAmounts).to.deep.eq(expectedMakerFillAmounts);
return fillAmounts.map(a => a.times(ratesBySource[ERC20BridgeSource.Eth2Dai]).integerValue());
},
sampleBuysFromUniswapV2: (path, fillAmounts) => {
sampleBuysFromUniswapV2: (router, path, fillAmounts) => {
uniswapRouter = router;
if (path.length === 2) {
expect(path).to.deep.eq([expectedTakerToken, expectedMakerToken]);
} else if (path.length === 3) {
@ -572,7 +581,7 @@ describe('DexSampler tests', () => {
output: a.times(ratesBySource[s]).integerValue(),
fillData:
s === ERC20BridgeSource.UniswapV2
? { tokenAddressPath: [expectedTakerToken, expectedMakerToken] }
? { router: uniswapRouter, tokenAddressPath: [expectedTakerToken, expectedMakerToken] }
: {},
})),
);
@ -582,6 +591,7 @@ describe('DexSampler tests', () => {
input: a,
output: a.times(ratesBySource[ERC20BridgeSource.UniswapV2]).integerValue(),
fillData: {
router: uniswapRouter,
tokenAddressPath: [expectedTakerToken, wethAddress, expectedMakerToken],
},
})),
@ -594,7 +604,7 @@ describe('DexSampler tests', () => {
const expectedTakerToken = randomAddress();
const expectedMakerToken = randomAddress();
const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3);
const pools: BalancerPool[] = [generateBalancerPool(), generateBalancerPool()];
const pools: Pool[] = [generateBalancerPool(), generateBalancerPool()];
const balancerPoolsCache = new MockBalancerPoolsCache({
getPoolsForPairAsync: async (takerToken: string, makerToken: string) => {
expect(takerToken).equal(expectedTakerToken);
@ -621,16 +631,16 @@ describe('DexSampler tests', () => {
});
describe('batched operations', () => {
it('getOrderFillableMakerAmounts(), getOrderFillableTakerAmounts()', async () => {
it.skip('getLimitOrderFillableMakerAssetAmounts(), getLimitOrderFillableTakerAssetAmounts()', async () => {
const expectedFillableTakerAmounts = ORDERS.map(() => getRandomInteger(0, 100e18));
const expectedFillableMakerAmounts = ORDERS.map(() => getRandomInteger(0, 100e18));
const sampler = new MockSamplerContract({
getOrderFillableMakerAssetAmounts: (orders, signatures) => {
getLimitOrderFillableMakerAssetAmounts: (orders, signatures) => {
expect(orders).to.deep.eq(SIMPLE_ORDERS);
expect(signatures).to.deep.eq(ORDERS.map(o => o.signature));
return expectedFillableMakerAmounts;
},
getOrderFillableTakerAssetAmounts: (orders, signatures) => {
getLimitOrderFillableTakerAssetAmounts: (orders, signatures) => {
expect(orders).to.deep.eq(SIMPLE_ORDERS);
expect(signatures).to.deep.eq(ORDERS.map(o => o.signature));
return expectedFillableTakerAmounts;
@ -646,15 +656,15 @@ describe('DexSampler tests', () => {
async () => undefined,
);
const [fillableMakerAmounts, fillableTakerAmounts] = await dexOrderSampler.executeAsync(
dexOrderSampler.getOrderFillableMakerAmounts(ORDERS, exchangeAddress),
dexOrderSampler.getOrderFillableTakerAmounts(ORDERS, exchangeAddress),
dexOrderSampler.getLimitOrderFillableMakerAmounts(ORDERS, exchangeAddress),
dexOrderSampler.getLimitOrderFillableTakerAmounts(ORDERS, exchangeAddress),
);
expect(fillableMakerAmounts).to.deep.eq(expectedFillableMakerAmounts);
expect(fillableTakerAmounts).to.deep.eq(expectedFillableTakerAmounts);
});
});
});
function generateBalancerPool(): BalancerPool {
function generateBalancerPool(): Pool {
return {
id: randomAddress(),
balanceIn: getRandomInteger(1, 1e18),

View File

@ -1,16 +1,17 @@
import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses';
import { constants as contractConstants, getRandomInteger, Numberish, randomAddress } from '@0x/contracts-test-utils';
import { constants as contractConstants, getRandomInteger, randomAddress } from '@0x/contracts-test-utils';
import {
assetDataUtils,
decodeAffiliateFeeTransformerData,
decodeFillQuoteTransformerData,
decodePayTakerTransformerData,
decodeWethTransformerData,
ETH_TOKEN_ADDRESS,
FillQuoteTransformerLimitOrderInfo,
FillQuoteTransformerOrderType,
FillQuoteTransformerSide,
getTransformerAddress,
} from '@0x/order-utils';
import { Order } from '@0x/types';
LimitOrderFields,
} from '@0x/protocol-utils';
import { AbiEncoder, BigNumber, hexUtils } from '@0x/utils';
import * as chai from 'chai';
import * as _ from 'lodash';
@ -18,11 +19,15 @@ import 'mocha';
import { constants } from '../src/constants';
import { ExchangeProxySwapQuoteConsumer } from '../src/quote_consumers/exchange_proxy_swap_quote_consumer';
import { getSwapMinBuyAmount } from '../src/quote_consumers/utils';
import { MarketBuySwapQuote, MarketOperation, MarketSellSwapQuote } from '../src/types';
import { ERC20BridgeSource, OptimizedMarketOrder } from '../src/utils/market_operation_utils/types';
import {
ERC20BridgeSource,
OptimizedLimitOrder,
OptimizedMarketOrder,
} from '../src/utils/market_operation_utils/types';
import { chaiSetup } from './utils/chai_setup';
import { getRandomAmount, getRandomSignature } from './utils/utils';
chaiSetup.configure();
const expect = chai.expect;
@ -61,76 +66,91 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
consumer = new ExchangeProxySwapQuoteConsumer(fakeProvider, contractAddresses, { chainId: CHAIN_ID });
});
function getRandomAmount(maxAmount: Numberish = '1e18'): BigNumber {
return getRandomInteger(1, maxAmount);
}
function createAssetData(token?: string): string {
return assetDataUtils.encodeERC20AssetData(token || randomAddress());
}
function getRandomOrder(): OptimizedMarketOrder {
function getRandomOrder(orderFields?: Partial<LimitOrderFields>): LimitOrderFields {
return {
fillableMakerAssetAmount: getRandomAmount(),
fillableTakerFeeAmount: getRandomAmount(),
fillableTakerAssetAmount: getRandomAmount(),
fills: [],
chainId: CHAIN_ID,
exchangeAddress: contractAddresses.exchange,
expirationTimeSeconds: getRandomInteger(1, 2e9),
feeRecipientAddress: randomAddress(),
makerAddress: randomAddress(),
makerAssetAmount: getRandomAmount(),
takerAssetAmount: getRandomAmount(),
makerFee: getRandomAmount(),
takerFee: getRandomAmount(),
verifyingContract: contractAddresses.exchangeProxy,
expiry: getRandomInteger(1, 2e9),
feeRecipient: randomAddress(),
sender: randomAddress(),
pool: hexUtils.random(32),
maker: randomAddress(),
makerAmount: getRandomAmount(),
takerAmount: getRandomAmount(),
takerTokenFeeAmount: getRandomAmount(),
salt: getRandomAmount(2e9),
signature: hexUtils.random(66),
senderAddress: NULL_ADDRESS,
takerAddress: NULL_ADDRESS,
makerAssetData: createAssetData(MAKER_TOKEN),
takerAssetData: createAssetData(TAKER_TOKEN),
makerFeeAssetData: createAssetData(),
takerFeeAssetData: createAssetData(),
taker: NULL_ADDRESS,
makerToken: MAKER_TOKEN,
takerToken: TAKER_TOKEN,
...orderFields,
};
}
function getRandomOptimizedMarketOrder(
optimizerFields?: Partial<OptimizedLimitOrder>,
orderFields?: Partial<LimitOrderFields>,
): OptimizedLimitOrder {
const order = getRandomOrder(orderFields);
return {
source: ERC20BridgeSource.Native,
fillData: {
order,
signature: getRandomSignature(),
maxTakerTokenFillAmount: order.takerAmount,
},
type: FillQuoteTransformerOrderType.Limit,
makerToken: order.makerToken,
takerToken: order.takerToken,
makerAmount: order.makerAmount,
takerAmount: order.takerAmount,
fills: [],
...optimizerFields,
};
}
function getRandomQuote(side: MarketOperation): MarketBuySwapQuote | MarketSellSwapQuote {
const order = getRandomOptimizedMarketOrder();
const makerTokenFillAmount = order.makerAmount;
const takerTokenFillAmount = order.takerAmount;
return {
gasPrice: getRandomInteger(1, 1e9),
type: side,
makerAssetData: createAssetData(MAKER_TOKEN),
takerAssetData: createAssetData(TAKER_TOKEN),
orders: [getRandomOrder()],
makerToken: MAKER_TOKEN,
takerToken: TAKER_TOKEN,
orders: [order],
makerTokenDecimals: 18,
takerTokenDecimals: 18,
sourceBreakdown: {} as any,
isTwoHop: false,
bestCaseQuoteInfo: {
feeTakerAssetAmount: getRandomAmount(),
makerAssetAmount: getRandomAmount(),
makerAmount: makerTokenFillAmount,
takerAmount: takerTokenFillAmount,
totalTakerAmount: takerTokenFillAmount,
gas: Math.floor(Math.random() * 8e6),
protocolFeeInWeiAmount: getRandomAmount(),
takerAssetAmount: getRandomAmount(),
totalTakerAssetAmount: getRandomAmount(),
feeTakerTokenAmount: getRandomAmount(),
},
worstCaseQuoteInfo: {
feeTakerAssetAmount: getRandomAmount(),
makerAssetAmount: getRandomAmount(),
makerAmount: makerTokenFillAmount,
takerAmount: takerTokenFillAmount,
totalTakerAmount: takerTokenFillAmount,
gas: Math.floor(Math.random() * 8e6),
protocolFeeInWeiAmount: getRandomAmount(),
takerAssetAmount: getRandomAmount(),
totalTakerAssetAmount: getRandomAmount(),
feeTakerTokenAmount: getRandomAmount(),
},
...(side === MarketOperation.Buy
? { makerAssetFillAmount: getRandomAmount() }
: { takerAssetFillAmount: getRandomAmount() }),
} as any;
? { type: MarketOperation.Buy, makerTokenFillAmount }
: { type: MarketOperation.Sell, takerTokenFillAmount }),
takerTokenToEthRate: getRandomAmount(),
makerTokenToEthRate: getRandomAmount(),
};
}
function getRandomTwoHopQuote(side: MarketOperation): MarketBuySwapQuote | MarketSellSwapQuote {
const intermediateTokenAssetData = createAssetData(INTERMEDIATE_TOKEN);
return {
...getRandomQuote(side),
orders: [
{ ...getRandomOrder(), makerAssetData: intermediateTokenAssetData },
{ ...getRandomOrder(), takerAssetData: intermediateTokenAssetData },
getRandomOptimizedMarketOrder({ makerToken: INTERMEDIATE_TOKEN }, { makerToken: INTERMEDIATE_TOKEN }),
getRandomOptimizedMarketOrder({ takerToken: INTERMEDIATE_TOKEN }, { takerToken: INTERMEDIATE_TOKEN }),
],
isTwoHop: true,
} as any;
@ -144,20 +164,28 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
return getRandomQuote(MarketOperation.Buy) as MarketBuySwapQuote;
}
type PlainOrder = Exclude<Order, ['chainId', 'exchangeAddress']>;
type PlainOrder = Exclude<LimitOrderFields, ['chainId', 'exchangeAddress']>;
function cleanOrders(orders: OptimizedMarketOrder[]): PlainOrder[] {
return orders.map(
o =>
_.omit(o, [
'chainId',
'exchangeAddress',
'fillableMakerAssetAmount',
'fillableTakerAssetAmount',
'fillableTakerFeeAmount',
'fills',
'signature',
]) as PlainOrder,
_.omit(
{
...o.fillData,
order: _.omit((o.fillData as FillQuoteTransformerLimitOrderInfo).order, [
'chainId',
'verifyingContract',
]) as any,
},
[
'fillableMakerAssetAmount',
'fillableTakerAssetAmount',
'fillableTakerFeeAmount',
'fills',
'chainId',
'verifyingContract',
],
) as PlainOrder,
);
}
@ -184,25 +212,25 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
}>;
}
const liquidityProviderEncoder = AbiEncoder.createMethod('sellToLiquidityProvider', [
{ type: 'address', name: 'inputToken' },
{ type: 'address', name: 'outputToken' },
{ type: 'address', name: 'target' },
{ type: 'address', name: 'recipient' },
{ type: 'uint256', name: 'sellAmount' },
{ type: 'uint256', name: 'minBuyAmount' },
{ type: 'bytes', name: 'auxiliaryData' },
]);
// const liquidityProviderEncoder = AbiEncoder.createMethod('sellToLiquidityProvider', [
// { type: 'address', name: 'inputToken' },
// { type: 'address', name: 'outputToken' },
// { type: 'address', name: 'target' },
// { type: 'address', name: 'recipient' },
// { type: 'uint256', name: 'sellAmount' },
// { type: 'uint256', name: 'minBuyAmount' },
// { type: 'bytes', name: 'auxiliaryData' },
// ]);
interface LiquidityProviderArgs {
inputToken: string;
outputToken: string;
target: string;
recipient: string;
sellAmount: BigNumber;
minBuyAmount: BigNumber;
auxiliaryData: string;
}
// interface LiquidityProviderArgs {
// inputToken: string;
// outputToken: string;
// target: string;
// recipient: string;
// sellAmount: BigNumber;
// minBuyAmount: BigNumber;
// auxiliaryData: string;
// }
describe('getCalldataOrThrow()', () => {
it('can produce a sell quote', async () => {
@ -211,8 +239,8 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
const callArgs = transformERC20Encoder.decode(callInfo.calldataHexString) as TransformERC20Args;
expect(callArgs.inputToken).to.eq(TAKER_TOKEN);
expect(callArgs.outputToken).to.eq(MAKER_TOKEN);
expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount);
expect(callArgs.minOutputTokenAmount).to.bignumber.eq(getSwapMinBuyAmount(quote));
expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAmount);
expect(callArgs.minOutputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.makerAmount);
expect(callArgs.transformations).to.be.length(2);
expect(
callArgs.transformations[0].deploymentNonce.toNumber() ===
@ -224,9 +252,11 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
);
const fillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data);
expect(fillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Sell);
expect(fillQuoteTransformerData.fillAmount).to.bignumber.eq(quote.takerAssetFillAmount);
expect(fillQuoteTransformerData.orders).to.deep.eq(cleanOrders(quote.orders));
expect(fillQuoteTransformerData.signatures).to.deep.eq(quote.orders.map(o => o.signature));
expect(fillQuoteTransformerData.fillAmount).to.bignumber.eq(quote.takerTokenFillAmount);
expect(fillQuoteTransformerData.limitOrders).to.deep.eq(cleanOrders(quote.orders));
expect(fillQuoteTransformerData.limitOrders.map(o => o.signature)).to.deep.eq(
(quote.orders as OptimizedLimitOrder[]).map(o => o.fillData.signature),
);
expect(fillQuoteTransformerData.sellToken).to.eq(TAKER_TOKEN);
expect(fillQuoteTransformerData.buyToken).to.eq(MAKER_TOKEN);
const payTakerTransformerData = decodePayTakerTransformerData(callArgs.transformations[1].data);
@ -240,8 +270,8 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
const callArgs = transformERC20Encoder.decode(callInfo.calldataHexString) as TransformERC20Args;
expect(callArgs.inputToken).to.eq(TAKER_TOKEN);
expect(callArgs.outputToken).to.eq(MAKER_TOKEN);
expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount);
expect(callArgs.minOutputTokenAmount).to.bignumber.eq(getSwapMinBuyAmount(quote));
expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAmount);
expect(callArgs.minOutputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.makerAmount);
expect(callArgs.transformations).to.be.length(2);
expect(
callArgs.transformations[0].deploymentNonce.toNumber() ===
@ -253,9 +283,11 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
);
const fillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data);
expect(fillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Buy);
expect(fillQuoteTransformerData.fillAmount).to.bignumber.eq(quote.makerAssetFillAmount);
expect(fillQuoteTransformerData.orders).to.deep.eq(cleanOrders(quote.orders));
expect(fillQuoteTransformerData.signatures).to.deep.eq(quote.orders.map(o => o.signature));
expect(fillQuoteTransformerData.fillAmount).to.bignumber.eq(quote.makerTokenFillAmount);
expect(fillQuoteTransformerData.limitOrders).to.deep.eq(cleanOrders(quote.orders));
expect(fillQuoteTransformerData.limitOrders.map(o => o.signature)).to.deep.eq(
(quote.orders as OptimizedLimitOrder[]).map(o => o.fillData.signature),
);
expect(fillQuoteTransformerData.sellToken).to.eq(TAKER_TOKEN);
expect(fillQuoteTransformerData.buyToken).to.eq(MAKER_TOKEN);
const payTakerTransformerData = decodePayTakerTransformerData(callArgs.transformations[1].data);
@ -281,7 +313,7 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
consumer.transformerNonces.wethTransformer,
);
const wethTransformerData = decodeWethTransformerData(callArgs.transformations[0].data);
expect(wethTransformerData.amount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount);
expect(wethTransformerData.amount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAmount);
expect(wethTransformerData.token).to.eq(ETH_TOKEN_ADDRESS);
});
@ -338,8 +370,8 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
const callArgs = transformERC20Encoder.decode(callInfo.calldataHexString) as TransformERC20Args;
expect(callArgs.inputToken).to.eq(TAKER_TOKEN);
expect(callArgs.outputToken).to.eq(MAKER_TOKEN);
expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount);
expect(callArgs.minOutputTokenAmount).to.bignumber.eq(getSwapMinBuyAmount(quote));
expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAmount);
expect(callArgs.minOutputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.makerAmount);
expect(callArgs.transformations).to.be.length(3);
expect(
callArgs.transformations[0].deploymentNonce.toNumber() ===
@ -356,16 +388,20 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
const [firstHopOrder, secondHopOrder] = quote.orders;
const firstHopFillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data);
expect(firstHopFillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Sell);
expect(firstHopFillQuoteTransformerData.fillAmount).to.bignumber.eq(firstHopOrder.takerAssetAmount);
expect(firstHopFillQuoteTransformerData.orders).to.deep.eq(cleanOrders([firstHopOrder]));
expect(firstHopFillQuoteTransformerData.signatures).to.deep.eq([firstHopOrder.signature]);
expect(firstHopFillQuoteTransformerData.fillAmount).to.bignumber.eq(firstHopOrder.takerAmount);
expect(firstHopFillQuoteTransformerData.limitOrders).to.deep.eq(cleanOrders([firstHopOrder]));
expect(firstHopFillQuoteTransformerData.limitOrders.map(o => o.signature)).to.deep.eq([
(firstHopOrder as OptimizedLimitOrder).fillData.signature,
]);
expect(firstHopFillQuoteTransformerData.sellToken).to.eq(TAKER_TOKEN);
expect(firstHopFillQuoteTransformerData.buyToken).to.eq(INTERMEDIATE_TOKEN);
const secondHopFillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[1].data);
expect(secondHopFillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Sell);
expect(secondHopFillQuoteTransformerData.fillAmount).to.bignumber.eq(contractConstants.MAX_UINT256);
expect(secondHopFillQuoteTransformerData.orders).to.deep.eq(cleanOrders([secondHopOrder]));
expect(secondHopFillQuoteTransformerData.signatures).to.deep.eq([secondHopOrder.signature]);
expect(secondHopFillQuoteTransformerData.limitOrders).to.deep.eq(cleanOrders([secondHopOrder]));
expect(secondHopFillQuoteTransformerData.limitOrders.map(o => o.signature)).to.deep.eq([
(secondHopOrder as OptimizedLimitOrder).fillData.signature,
]);
expect(secondHopFillQuoteTransformerData.sellToken).to.eq(INTERMEDIATE_TOKEN);
expect(secondHopFillQuoteTransformerData.buyToken).to.eq(MAKER_TOKEN);
const payTakerTransformerData = decodePayTakerTransformerData(callArgs.transformations[2].data);
@ -377,36 +413,36 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
INTERMEDIATE_TOKEN,
]);
});
it.skip('Uses the `LiquidityProviderFeature` if given a single LiquidityProvider order', async () => {
const quote = {
...getRandomSellQuote(),
orders: [
{
...getRandomOrder(),
fills: [
{
source: ERC20BridgeSource.LiquidityProvider,
sourcePathId: '',
input: constants.ZERO_AMOUNT,
output: constants.ZERO_AMOUNT,
subFills: [],
},
],
},
],
};
const callInfo = await consumer.getCalldataOrThrowAsync(quote);
const callArgs = liquidityProviderEncoder.decode(callInfo.calldataHexString) as LiquidityProviderArgs;
expect(callArgs).to.deep.equal({
inputToken: TAKER_TOKEN,
outputToken: MAKER_TOKEN,
target: quote.orders[0].makerAddress,
recipient: constants.NULL_ADDRESS,
sellAmount: quote.worstCaseQuoteInfo.totalTakerAssetAmount,
minBuyAmount: getSwapMinBuyAmount(quote),
auxiliaryData: constants.NULL_BYTES,
});
});
// it.skip('Uses the `LiquidityProviderFeature` if given a single LiquidityProvider order', async () => {
// const quote = {
// ...getRandomSellQuote(),
// orders: [
// {
// ...getRandomOrder(),
// fills: [
// {
// source: ERC20BridgeSource.LiquidityProvider,
// sourcePathId: '',
// input: constants.ZERO_AMOUNT,
// output: constants.ZERO_AMOUNT,
// subFills: [],
// },
// ],
// },
// ],
// };
// const callInfo = await consumer.getCalldataOrThrowAsync(quote);
// const callArgs = liquidityProviderEncoder.decode(callInfo.calldataHexString) as LiquidityProviderArgs;
// expect(callArgs).to.deep.equal({
// inputToken: TAKER_TOKEN,
// outputToken: MAKER_TOKEN,
// target: quote.orders[0].makerAddress,
// recipient: constants.NULL_ADDRESS,
// sellAmount: quote.worstCaseQuoteInfo.feeTakerTokenAmount,
// minBuyAmount: getSwapMinBuyAmount(quote),
// auxiliaryData: constants.NULL_BYTES,
// });
// });
it('allows selling the entire balance for CFL', async () => {
const quote = getRandomSellQuote();
const callInfo = await consumer.getCalldataOrThrowAsync(quote, {
@ -416,7 +452,7 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
expect(callArgs.inputToken).to.eq(TAKER_TOKEN);
expect(callArgs.outputToken).to.eq(MAKER_TOKEN);
expect(callArgs.inputTokenAmount).to.bignumber.eq(MAX_UINT256);
expect(callArgs.minOutputTokenAmount).to.bignumber.eq(getSwapMinBuyAmount(quote));
expect(callArgs.minOutputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.makerAmount);
expect(callArgs.transformations).to.be.length(2);
expect(
callArgs.transformations[0].deploymentNonce.toNumber() ===
@ -429,8 +465,10 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
const fillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data);
expect(fillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Sell);
expect(fillQuoteTransformerData.fillAmount).to.bignumber.eq(MAX_UINT256);
expect(fillQuoteTransformerData.orders).to.deep.eq(cleanOrders(quote.orders));
expect(fillQuoteTransformerData.signatures).to.deep.eq(quote.orders.map(o => o.signature));
expect(fillQuoteTransformerData.limitOrders).to.deep.eq(cleanOrders(quote.orders));
expect(fillQuoteTransformerData.limitOrders.map(o => o.signature)).to.deep.eq(
(quote.orders as OptimizedLimitOrder[]).map(o => o.fillData.signature),
);
expect(fillQuoteTransformerData.sellToken).to.eq(TAKER_TOKEN);
expect(fillQuoteTransformerData.buyToken).to.eq(MAKER_TOKEN);
const payTakerTransformerData = decodePayTakerTransformerData(callArgs.transformations[1].data);

View File

@ -1,73 +0,0 @@
import * as chai from 'chai';
import * as _ from 'lodash';
import 'mocha';
import { fillableAmountsUtils } from '../src/utils/fillable_amounts_utils';
import { chaiSetup } from './utils/chai_setup';
import { testOrderFactory } from './utils/test_order_factory';
import { baseUnitAmount } from './utils/utils';
chaiSetup.configure();
const expect = chai.expect;
// tslint:disable:custom-no-magic-numbers
const FAKE_ERC20_TAKER_ASSET_DATA = '0xf47261b02222222222222222222222222222222222222222222222222222222222222222';
const FAKE_ERC20_MAKER_ASSET_DATA = '0xf47261b01111111111111111111111111111111111111111111111111111111111111111';
const TAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER = testOrderFactory.generateTestSignedOrderWithFillableAmounts({
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
takerFeeAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
takerFee: baseUnitAmount(2),
fillableMakerAssetAmount: baseUnitAmount(5),
fillableTakerAssetAmount: baseUnitAmount(10),
fillableTakerFeeAmount: baseUnitAmount(2),
});
const MAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER = testOrderFactory.generateTestSignedOrderWithFillableAmounts({
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
takerFeeAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
takerFee: baseUnitAmount(2),
fillableMakerAssetAmount: baseUnitAmount(10),
fillableTakerAssetAmount: baseUnitAmount(5),
fillableTakerFeeAmount: baseUnitAmount(2),
});
describe('fillableAmountsUtils', () => {
describe('getTakerAssetAmountSwappedAfterOrderFees', () => {
it('should return fillableTakerAssetAmount if takerFee is not denominated in taker', () => {
const availableAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(
MAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER,
);
expect(availableAssetAmount).to.bignumber.eq(
MAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER.fillableTakerAssetAmount,
);
});
it('should return fillableTakerAssetAmount + fillableTakerFeeAmount if takerFee is not denominated in maker', () => {
const availableAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(
TAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER,
);
expect(availableAssetAmount).to.bignumber.eq(baseUnitAmount(12));
});
});
describe('getMakerAssetAmountSwappedAfterOrderFees', () => {
it('should return fillableMakerAssetAmount if takerFee is not denominated in maker', () => {
const availableAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(
TAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER,
);
expect(availableAssetAmount).to.bignumber.eq(
TAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER.fillableMakerAssetAmount,
);
});
it('should return fillableMakerAssetAmount - fillableTakerFeeif takerFee is denominated in maker', () => {
const availableAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(
MAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER,
);
expect(availableAssetAmount).to.bignumber.eq(baseUnitAmount(8));
});
});
});

File diff suppressed because it is too large Load Diff

View File

@ -1,271 +0,0 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { ERC20TokenContract, ExchangeContract } from '@0x/contract-wrappers';
import { constants as devConstants, getLatestBlockTimestampAsync, OrderFactory } from '@0x/contracts-test-utils';
import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils';
import { migrateOnceAsync } from '@0x/migrations';
import { assetDataUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
import { constants } from '../src/constants';
import { OrderPrunerPermittedFeeTypes } from '../src/types';
import { orderPrunerUtils } from '../src/utils/order_prune_utils';
import { chaiSetup } from './utils/chai_setup';
import { provider, web3Wrapper } from './utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID;
const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE);
const PROTOCOL_FEE_MULTIPLIER = 70000;
const PROTOCOL_FEE_PER_FILL = GAS_PRICE.times(PROTOCOL_FEE_MULTIPLIER);
const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers
const EXPIRY_BUFFER_MS = 120000;
// tslint:disable: no-unused-expression
// tslint:disable: custom-no-magic-numbers
describe('orderPrunerUtils', () => {
let erc20MakerTokenContract: ERC20TokenContract;
let erc20TakerTokenContract: ERC20TokenContract;
let exchangeContract: ExchangeContract;
let userAddresses: string[];
let coinbaseAddress: string;
let makerAddress: string;
let takerAddress: string;
let feeRecipient: string;
let makerTokenAddress: string;
let takerTokenAddress: string;
let makerAssetData: string;
let takerAssetData: string;
let orderFactory: OrderFactory;
let contractAddresses: ContractAddresses;
let nonOpenSignedOrder: SignedOrder;
let expiredOpenSignedOrder: SignedOrder;
let partiallyFilledOpenSignedOrderFeeless: SignedOrder;
let partiallyFilledOpenSignedOrderFeeInTakerAsset: SignedOrder;
let partiallyFilledOpenSignedOrderFeeInMakerAsset: SignedOrder;
const chainId = TESTRPC_CHAIN_ID;
const fillableAmount = new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI);
const partialFillAmount = new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI);
const takerFeeAmount = new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI);
before(async () => {
contractAddresses = await migrateOnceAsync(provider);
await blockchainLifecycle.startAsync();
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
[coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses;
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
erc20MakerTokenContract = new ERC20TokenContract(makerTokenAddress, provider);
erc20TakerTokenContract = new ERC20TokenContract(takerTokenAddress, provider);
exchangeContract = new ExchangeContract(contractAddresses.exchange, provider);
[makerAssetData, takerAssetData] = [
assetDataUtils.encodeERC20AssetData(makerTokenAddress),
assetDataUtils.encodeERC20AssetData(takerTokenAddress),
];
// Configure order defaults
const defaultOrderParams = {
...devConstants.STATIC_ORDER_PARAMS,
makerAddress,
takerAddress: constants.NULL_ADDRESS,
makerAssetData,
takerAssetData,
makerFeeAssetData: constants.NULL_ERC20_ASSET_DATA,
takerFeeAssetData: constants.NULL_ERC20_ASSET_DATA,
makerFee: constants.ZERO_AMOUNT,
takerFee: constants.ZERO_AMOUNT,
feeRecipientAddress: feeRecipient,
exchangeAddress: contractAddresses.exchange,
chainId,
};
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
nonOpenSignedOrder = await orderFactory.newSignedOrderAsync({
takerAddress,
});
expiredOpenSignedOrder = await orderFactory.newSignedOrderAsync({
expirationTimeSeconds: new BigNumber(await getLatestBlockTimestampAsync()).plus(60000),
});
// give double fillableAmount to maker and taker as buffer
await erc20MakerTokenContract
.transfer(makerAddress, fillableAmount.multipliedBy(4))
.sendTransactionAsync({ from: coinbaseAddress });
await erc20TakerTokenContract
.transfer(takerAddress, fillableAmount.multipliedBy(4))
.sendTransactionAsync({ from: coinbaseAddress });
await erc20MakerTokenContract
.approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE_IN_BASE_UNITS)
.sendTransactionAsync({ from: makerAddress });
await erc20MakerTokenContract
.approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE_IN_BASE_UNITS)
.sendTransactionAsync({ from: takerAddress });
await erc20TakerTokenContract
.approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE_IN_BASE_UNITS)
.sendTransactionAsync({ from: takerAddress });
partiallyFilledOpenSignedOrderFeeless = await orderFactory.newSignedOrderAsync({
takerAssetAmount: fillableAmount,
makerAssetAmount: fillableAmount,
});
await exchangeContract
.fillOrKillOrder(
partiallyFilledOpenSignedOrderFeeless,
partialFillAmount,
partiallyFilledOpenSignedOrderFeeless.signature,
)
.sendTransactionAsync({
from: takerAddress,
gasPrice: GAS_PRICE,
gas: 4000000,
value: PROTOCOL_FEE_PER_FILL,
});
partiallyFilledOpenSignedOrderFeeInTakerAsset = await orderFactory.newSignedOrderAsync({
takerAssetAmount: fillableAmount,
makerAssetAmount: fillableAmount,
takerFee: takerFeeAmount,
takerFeeAssetData: takerAssetData,
});
await exchangeContract
.fillOrKillOrder(
partiallyFilledOpenSignedOrderFeeInTakerAsset,
partialFillAmount,
partiallyFilledOpenSignedOrderFeeInTakerAsset.signature,
)
.sendTransactionAsync({
from: takerAddress,
gasPrice: GAS_PRICE,
gas: 4000000,
value: PROTOCOL_FEE_PER_FILL,
});
partiallyFilledOpenSignedOrderFeeInMakerAsset = await orderFactory.newSignedOrderAsync({
takerAssetAmount: fillableAmount,
makerAssetAmount: fillableAmount,
takerFee: takerFeeAmount,
takerFeeAssetData: makerAssetData,
});
await exchangeContract
.fillOrKillOrder(
partiallyFilledOpenSignedOrderFeeInMakerAsset,
partialFillAmount,
partiallyFilledOpenSignedOrderFeeInMakerAsset.signature,
)
.sendTransactionAsync({
from: takerAddress,
gasPrice: GAS_PRICE,
gas: 4000000,
value: PROTOCOL_FEE_PER_FILL,
});
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('prunedForUsableSignedOrders', () => {
it('should filter for only feeless orders if options permit only feeless orders', async () => {
const permittedOrderFeeTypes = new Set<OrderPrunerPermittedFeeTypes>([OrderPrunerPermittedFeeTypes.NoFees]);
const orders = [
partiallyFilledOpenSignedOrderFeeInMakerAsset,
partiallyFilledOpenSignedOrderFeeInTakerAsset,
partiallyFilledOpenSignedOrderFeeless,
];
const resultPrunedOrders = orderPrunerUtils.pruneForUsableSignedOrders(
orders,
permittedOrderFeeTypes,
EXPIRY_BUFFER_MS,
);
// checks for one order in results and check for signature of orders
expect(resultPrunedOrders.length).to.be.equal(1);
expect(resultPrunedOrders[0].signature).to.be.deep.equal(partiallyFilledOpenSignedOrderFeeless.signature);
});
it('should filter for only takerFee in takerAsset orders if options permit only takerFee in takerAsset orders', async () => {
const permittedOrderFeeTypes = new Set<OrderPrunerPermittedFeeTypes>([
OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee,
]);
const orders = [
partiallyFilledOpenSignedOrderFeeInMakerAsset,
partiallyFilledOpenSignedOrderFeeInTakerAsset,
partiallyFilledOpenSignedOrderFeeless,
];
const resultPrunedOrders = orderPrunerUtils.pruneForUsableSignedOrders(
orders,
permittedOrderFeeTypes,
EXPIRY_BUFFER_MS,
);
// checks for one order in results and check for signature of orders
expect(resultPrunedOrders.length).to.be.equal(1);
expect(resultPrunedOrders[0].signature).to.be.deep.equal(
partiallyFilledOpenSignedOrderFeeInTakerAsset.signature,
);
});
it('should filter for only makerFee in takerAsset orders if options permit only makerFee orders', async () => {
const permittedOrderFeeTypes = new Set<OrderPrunerPermittedFeeTypes>([
OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee,
]);
const orders = [
partiallyFilledOpenSignedOrderFeeInMakerAsset,
partiallyFilledOpenSignedOrderFeeInTakerAsset,
partiallyFilledOpenSignedOrderFeeless,
];
const resultPrunedOrders = orderPrunerUtils.pruneForUsableSignedOrders(
orders,
permittedOrderFeeTypes,
EXPIRY_BUFFER_MS,
);
// checks for one order in results and check for signature of orders
expect(resultPrunedOrders.length).to.be.equal(1);
expect(resultPrunedOrders[0].signature).to.be.deep.equal(
partiallyFilledOpenSignedOrderFeeInMakerAsset.signature,
);
});
it('should filter out non open orders', async () => {
const permittedOrderFeeTypes = new Set<OrderPrunerPermittedFeeTypes>([
OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee,
OrderPrunerPermittedFeeTypes.NoFees,
OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee,
]);
const orders = [nonOpenSignedOrder];
const resultPrunedOrders = orderPrunerUtils.pruneForUsableSignedOrders(
orders,
permittedOrderFeeTypes,
EXPIRY_BUFFER_MS,
);
expect(resultPrunedOrders).to.be.empty;
});
it('should filter out expired orders', async () => {
const permittedOrderFeeTypes = new Set<OrderPrunerPermittedFeeTypes>([
OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee,
OrderPrunerPermittedFeeTypes.NoFees,
OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee,
]);
const orders = [expiredOpenSignedOrder];
const resultPrunedOrders = orderPrunerUtils.pruneForUsableSignedOrders(
orders,
permittedOrderFeeTypes,
EXPIRY_BUFFER_MS,
);
expect(resultPrunedOrders).to.be.empty;
});
});
});

View File

@ -1,273 +0,0 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { DevUtilsContract, ERC20TokenContract, ExchangeContract } from '@0x/contract-wrappers';
import { constants as devConstants, getLatestBlockTimestampAsync, OrderFactory } from '@0x/contracts-test-utils';
import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils';
import { migrateOnceAsync } from '@0x/migrations';
import { assetDataUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
import { constants } from '../src/constants';
import { SignedOrderWithFillableAmounts } from '../src/types';
import { OrderStateUtils } from '../src/utils/order_state_utils';
import { chaiSetup } from './utils/chai_setup';
import { provider, web3Wrapper } from './utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID;
const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE);
const PROTOCOL_FEE_MULTIPLIER = 70000;
const PROTOCOL_FEE_PER_FILL = GAS_PRICE.times(PROTOCOL_FEE_MULTIPLIER);
const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers
const isSignedOrdersWithFillableAmountsNotFillable = (signedOrders: SignedOrderWithFillableAmounts[]) => {
signedOrders.forEach(order => {
expect(order.fillableMakerAssetAmount).to.bignumber.eq(constants.ZERO_AMOUNT);
expect(order.fillableTakerAssetAmount).to.bignumber.eq(constants.ZERO_AMOUNT);
expect(order.fillableTakerFeeAmount).to.bignumber.eq(constants.ZERO_AMOUNT);
});
};
// tslint:disable: no-unused-expression
// tslint:disable: custom-no-magic-numbers
describe('OrderStateUtils', () => {
let erc20MakerTokenContract: ERC20TokenContract;
let erc20TakerTokenContract: ERC20TokenContract;
let exchangeContract: ExchangeContract;
let userAddresses: string[];
let coinbaseAddress: string;
let makerAddress: string;
let takerAddress: string;
let feeRecipient: string;
let makerTokenAddress: string;
let takerTokenAddress: string;
let makerAssetData: string;
let takerAssetData: string;
let orderFactory: OrderFactory;
let contractAddresses: ContractAddresses;
let orderStateUtils: OrderStateUtils;
let expiredOpenSignedOrder: SignedOrder;
let invalidSignatureOpenSignedOrder: SignedOrder;
let fullyFillableOpenSignedOrder: SignedOrder;
let partiallyFilledOpenSignedOrderFeeless: SignedOrder;
let partiallyFilledOpenSignedOrderFeeInTakerAsset: SignedOrder;
let partiallyFilledOpenSignedOrderFeeInMakerAsset: SignedOrder;
let filledOpenSignedOrder: SignedOrder;
const chainId = TESTRPC_CHAIN_ID;
const fillableAmount = new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI);
const partialFillAmount = new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI);
const takerFeeAmount = new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI);
before(async () => {
contractAddresses = await migrateOnceAsync(provider);
await blockchainLifecycle.startAsync();
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
[coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses;
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
erc20MakerTokenContract = new ERC20TokenContract(makerTokenAddress, provider);
erc20TakerTokenContract = new ERC20TokenContract(takerTokenAddress, provider);
exchangeContract = new ExchangeContract(contractAddresses.exchange, provider);
[makerAssetData, takerAssetData] = [
assetDataUtils.encodeERC20AssetData(makerTokenAddress),
assetDataUtils.encodeERC20AssetData(takerTokenAddress),
];
// Configure order defaults
const defaultOrderParams = {
...devConstants.STATIC_ORDER_PARAMS,
makerAddress,
takerAddress: constants.NULL_ADDRESS,
makerAssetData,
takerAssetData,
makerFeeAssetData: constants.NULL_ERC20_ASSET_DATA,
takerFeeAssetData: constants.NULL_ERC20_ASSET_DATA,
makerFee: constants.ZERO_AMOUNT,
takerFee: constants.ZERO_AMOUNT,
feeRecipientAddress: feeRecipient,
exchangeAddress: contractAddresses.exchange,
chainId,
};
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
expiredOpenSignedOrder = await orderFactory.newSignedOrderAsync({
expirationTimeSeconds: new BigNumber(await getLatestBlockTimestampAsync()).minus(10),
});
invalidSignatureOpenSignedOrder = await orderFactory.newSignedOrderAsync({
takerAddress,
});
invalidSignatureOpenSignedOrder.signature = expiredOpenSignedOrder.signature;
fullyFillableOpenSignedOrder = await orderFactory.newSignedOrderAsync({
takerAssetAmount: fillableAmount,
makerAssetAmount: fillableAmount,
});
// give double fillableAmount to maker and taker as buffer
await erc20MakerTokenContract
.transfer(makerAddress, fillableAmount.multipliedBy(4))
.sendTransactionAsync({ from: coinbaseAddress });
await erc20TakerTokenContract
.transfer(takerAddress, fillableAmount.multipliedBy(4))
.sendTransactionAsync({ from: coinbaseAddress });
await erc20MakerTokenContract
.approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE_IN_BASE_UNITS)
.sendTransactionAsync({ from: makerAddress });
await erc20MakerTokenContract
.approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE_IN_BASE_UNITS)
.sendTransactionAsync({ from: takerAddress });
await erc20TakerTokenContract
.approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE_IN_BASE_UNITS)
.sendTransactionAsync({ from: takerAddress });
partiallyFilledOpenSignedOrderFeeless = await orderFactory.newSignedOrderAsync({
takerAssetAmount: fillableAmount,
makerAssetAmount: fillableAmount,
});
await exchangeContract
.fillOrKillOrder(
partiallyFilledOpenSignedOrderFeeless,
partialFillAmount,
partiallyFilledOpenSignedOrderFeeless.signature,
)
.sendTransactionAsync({
from: takerAddress,
gasPrice: GAS_PRICE,
gas: 4000000,
value: PROTOCOL_FEE_PER_FILL,
});
partiallyFilledOpenSignedOrderFeeInTakerAsset = await orderFactory.newSignedOrderAsync({
takerAssetAmount: fillableAmount,
makerAssetAmount: fillableAmount,
takerFee: takerFeeAmount,
takerFeeAssetData: takerAssetData,
});
await exchangeContract
.fillOrKillOrder(
partiallyFilledOpenSignedOrderFeeInTakerAsset,
partialFillAmount,
partiallyFilledOpenSignedOrderFeeInTakerAsset.signature,
)
.sendTransactionAsync({
from: takerAddress,
gasPrice: GAS_PRICE,
gas: 4000000,
value: PROTOCOL_FEE_PER_FILL,
});
partiallyFilledOpenSignedOrderFeeInMakerAsset = await orderFactory.newSignedOrderAsync({
takerAssetAmount: fillableAmount,
makerAssetAmount: fillableAmount,
takerFee: takerFeeAmount,
takerFeeAssetData: makerAssetData,
});
await exchangeContract
.fillOrKillOrder(
partiallyFilledOpenSignedOrderFeeInMakerAsset,
partialFillAmount,
partiallyFilledOpenSignedOrderFeeInMakerAsset.signature,
)
.sendTransactionAsync({
from: takerAddress,
gasPrice: GAS_PRICE,
gas: 4000000,
value: PROTOCOL_FEE_PER_FILL,
});
filledOpenSignedOrder = await orderFactory.newSignedOrderAsync({
takerAssetAmount: fillableAmount,
makerAssetAmount: fillableAmount,
});
await exchangeContract
.fillOrKillOrder(filledOpenSignedOrder, fillableAmount, filledOpenSignedOrder.signature)
.sendTransactionAsync({
from: takerAddress,
gasPrice: GAS_PRICE,
gas: 4000000,
value: PROTOCOL_FEE_PER_FILL,
});
orderStateUtils = new OrderStateUtils(new DevUtilsContract(contractAddresses.devUtils, provider));
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('#getSignedOrdersWithFillableAmountsAsync', () => {
it('should 0 fillableTakerAssetAmount for expired orders', async () => {
const orders = [expiredOpenSignedOrder];
const resultOrders = await orderStateUtils.getSignedOrdersWithFillableAmountsAsync(orders);
isSignedOrdersWithFillableAmountsNotFillable(resultOrders);
});
it('should filter out invalid signature orders', async () => {
const orders = [invalidSignatureOpenSignedOrder];
const resultOrders = await orderStateUtils.getSignedOrdersWithFillableAmountsAsync(orders);
isSignedOrdersWithFillableAmountsNotFillable(resultOrders);
});
it('should return 0 fillableTakerAssetAmount for fully filled orders', async () => {
const orders = [filledOpenSignedOrder];
const resultOrders = await orderStateUtils.getSignedOrdersWithFillableAmountsAsync(orders);
isSignedOrdersWithFillableAmountsNotFillable(resultOrders);
});
it('should provide correct pruned signed orders for fully fillable orders', async () => {
const orders = [fullyFillableOpenSignedOrder];
const resultOrders = await orderStateUtils.getSignedOrdersWithFillableAmountsAsync(orders);
const order = resultOrders[0];
expect(order.fillableMakerAssetAmount).to.bignumber.equal(fillableAmount);
expect(order.fillableTakerAssetAmount).to.bignumber.equal(fillableAmount);
});
it('should provide correct pruned signed orders for partially fillable orders', async () => {
const orders = [
partiallyFilledOpenSignedOrderFeeless,
partiallyFilledOpenSignedOrderFeeInTakerAsset,
partiallyFilledOpenSignedOrderFeeInMakerAsset,
];
const resultOrders = await orderStateUtils.getSignedOrdersWithFillableAmountsAsync(orders);
expect(resultOrders[0].fillableMakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultOrders[0].fillableTakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultOrders[1].fillableMakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultOrders[1].fillableTakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultOrders[1].fillableTakerFeeAmount).to.bignumber.equal(
new BigNumber(1.6).multipliedBy(ONE_ETH_IN_WEI),
);
expect(resultOrders[2].fillableMakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultOrders[2].fillableTakerAssetAmount).to.bignumber.equal(
fillableAmount.minus(partialFillAmount),
);
expect(resultOrders[2].fillableTakerFeeAmount).to.bignumber.equal(
new BigNumber(1.6).multipliedBy(ONE_ETH_IN_WEI),
);
});
});
});

View File

@ -1,52 +1,58 @@
// tslint:disable:custom-no-magic-numbers
import { SignedOrder } from '@0x/types';
// tslint:disable:no-object-literal-type-assertion
import { FillQuoteTransformerOrderType, LimitOrder, LimitOrderFields, RfqOrder } from '@0x/protocol-utils';
import { BigNumber, hexUtils } from '@0x/utils';
import * as chai from 'chai';
import * as _ from 'lodash';
import 'mocha';
import * as TypeMoq from 'typemoq';
import { MarketOperation } from '../src/types';
import { MarketOperation, NativeOrderWithFillableAmounts } from '../src/types';
import {
CollapsedFill,
DexSample,
ERC20BridgeSource,
MultiHopFillData,
NativeCollapsedFill,
NativeFillData,
NativeLimitOrderFillData,
NativeRfqOrderFillData,
} from '../src/utils/market_operation_utils/types';
import { QuoteRequestor } from '../src/utils/quote_requestor';
import {
BridgeReportSource,
BridgeQuoteReportEntry,
generateQuoteReport,
MultiHopReportSource,
NativeOrderbookReportSource,
NativeRFQTReportSource,
QuoteReportSource,
MultiHopQuoteReportEntry,
NativeLimitOrderQuoteReportEntry,
NativeRfqOrderQuoteReportEntry,
QuoteReportEntry,
} from './../src/utils/quote_report_generator';
import { chaiSetup } from './utils/chai_setup';
import { testOrderFactory } from './utils/test_order_factory';
import { getRandomAmount, getRandomSignature } from './utils/utils';
chaiSetup.configure();
const expect = chai.expect;
const collapsedFillFromNativeOrder = (order: SignedOrder): NativeCollapsedFill => {
function collapsedFillFromNativeOrder(order: NativeOrderWithFillableAmounts): NativeCollapsedFill {
const fillData = {
order: order.order,
signature: order.signature,
maxTakerTokenFillAmount: order.fillableTakerAmount,
};
return {
sourcePathId: hexUtils.random(),
source: ERC20BridgeSource.Native,
input: order.takerAssetAmount,
output: order.makerAssetAmount,
fillData: {
order: {
...order,
fillableMakerAssetAmount: new BigNumber(1),
fillableTakerAssetAmount: new BigNumber(1),
fillableTakerFeeAmount: new BigNumber(1),
},
},
type: order.type,
input: order.order.takerAmount,
output: order.order.makerAmount,
fillData:
order.type === FillQuoteTransformerOrderType.Limit
? (fillData as NativeLimitOrderFillData)
: (fillData as NativeRfqOrderFillData),
subFills: [],
};
};
}
describe('generateQuoteReport', async () => {
it('should generate report properly for sell', () => {
@ -78,37 +84,59 @@ describe('generateQuoteReport', async () => {
};
const dexQuotes: DexSample[] = [kyberSample1, kyberSample2, uniswapSample1, uniswapSample2];
const orderbookOrder1FillableAmount = new BigNumber(1000);
const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({
signature: 'orderbookOrder1',
takerAssetAmount: orderbookOrder1FillableAmount,
});
const orderbookOrder2FillableAmount = new BigNumber(99);
const orderbookOrder2 = testOrderFactory.generateTestSignedOrder({
signature: 'orderbookOrder2',
takerAssetAmount: orderbookOrder2FillableAmount.plus(99),
});
const rfqtOrder1FillableAmount = new BigNumber(100);
const rfqtOrder1 = testOrderFactory.generateTestSignedOrder({
signature: 'rfqtOrder1',
takerAssetAmount: rfqtOrder1FillableAmount,
});
const rfqtOrder2FillableAmount = new BigNumber(1001);
const rfqtOrder2 = testOrderFactory.generateTestSignedOrder({
signature: 'rfqtOrder2',
takerAssetAmount: rfqtOrder2FillableAmount.plus(100),
});
const nativeOrders: SignedOrder[] = [orderbookOrder1, rfqtOrder1, rfqtOrder2, orderbookOrder2];
const orderFillableAmounts: BigNumber[] = [
orderbookOrder1FillableAmount,
rfqtOrder1FillableAmount,
rfqtOrder2FillableAmount,
orderbookOrder2FillableAmount,
const orderbookOrder1: NativeOrderWithFillableAmounts = {
order: new LimitOrder({ takerAmount: new BigNumber(1000) }),
type: FillQuoteTransformerOrderType.Limit,
fillableTakerAmount: new BigNumber(1000),
fillableMakerAmount: getRandomAmount(),
fillableTakerFeeAmount: getRandomAmount(),
signature: getRandomSignature(),
};
const orderbookOrder2: NativeOrderWithFillableAmounts = {
order: new LimitOrder({ takerAmount: new BigNumber(198) }),
type: FillQuoteTransformerOrderType.Limit,
fillableTakerAmount: new BigNumber(99), // takerAmount minus 99
fillableMakerAmount: getRandomAmount(),
fillableTakerFeeAmount: getRandomAmount(),
signature: getRandomSignature(),
};
const rfqtOrder1: NativeOrderWithFillableAmounts = {
order: new RfqOrder({ takerAmount: new BigNumber(100) }),
type: FillQuoteTransformerOrderType.Rfq,
fillableTakerAmount: new BigNumber(100),
fillableMakerAmount: getRandomAmount(),
fillableTakerFeeAmount: getRandomAmount(),
signature: getRandomSignature(),
};
const rfqtOrder2: NativeOrderWithFillableAmounts = {
order: new RfqOrder({ takerAmount: new BigNumber(1101) }),
type: FillQuoteTransformerOrderType.Rfq,
fillableTakerAmount: new BigNumber(1001),
fillableMakerAmount: getRandomAmount(),
fillableTakerFeeAmount: getRandomAmount(),
signature: getRandomSignature(),
};
const nativeOrders: NativeOrderWithFillableAmounts[] = [
orderbookOrder1,
rfqtOrder1,
rfqtOrder2,
orderbookOrder2,
];
// generate path
const uniswap2Fill: CollapsedFill = { ...uniswapSample2, subFills: [], sourcePathId: hexUtils.random() };
const kyber2Fill: CollapsedFill = { ...kyberSample2, subFills: [], sourcePathId: hexUtils.random() };
const uniswap2Fill: CollapsedFill = {
...uniswapSample2,
subFills: [],
sourcePathId: hexUtils.random(),
type: FillQuoteTransformerOrderType.Bridge,
};
const kyber2Fill: CollapsedFill = {
...kyberSample2,
subFills: [],
sourcePathId: hexUtils.random(),
type: FillQuoteTransformerOrderType.Bridge,
};
const orderbookOrder2Fill: CollapsedFill = collapsedFillFromNativeOrder(orderbookOrder2);
const rfqtOrder2Fill: CollapsedFill = collapsedFillFromNativeOrder(rfqtOrder2);
const pathGenerated: CollapsedFill[] = [rfqtOrder2Fill, orderbookOrder2Fill, uniswap2Fill, kyber2Fill];
@ -116,19 +144,13 @@ describe('generateQuoteReport', async () => {
// quote generator mock
const quoteRequestor = TypeMoq.Mock.ofType<QuoteRequestor>();
quoteRequestor
.setup(qr => qr.getMakerUriForOrderSignature(orderbookOrder2.signature))
.returns(() => {
return undefined;
})
.verifiable(TypeMoq.Times.atLeastOnce());
quoteRequestor
.setup(qr => qr.getMakerUriForOrderSignature(rfqtOrder1.signature))
.setup(qr => qr.getMakerUriForSignature(rfqtOrder1.signature))
.returns(() => {
return 'https://rfqt1.provider.club';
})
.verifiable(TypeMoq.Times.atLeastOnce());
quoteRequestor
.setup(qr => qr.getMakerUriForOrderSignature(rfqtOrder2.signature))
.setup(qr => qr.getMakerUriForSignature(rfqtOrder2.signature))
.returns(() => {
return 'https://rfqt2.provider.club';
})
@ -139,72 +161,79 @@ describe('generateQuoteReport', async () => {
dexQuotes,
[],
nativeOrders,
orderFillableAmounts,
pathGenerated,
undefined,
quoteRequestor.object,
);
const rfqtOrder1Source: NativeRFQTReportSource = {
const rfqtOrder1Source: NativeRfqOrderQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: rfqtOrder1.makerAssetAmount,
takerAmount: rfqtOrder1.takerAssetAmount,
nativeOrder: rfqtOrder1,
fillableTakerAmount: rfqtOrder1FillableAmount,
makerAmount: rfqtOrder1.order.makerAmount,
takerAmount: rfqtOrder1.order.takerAmount,
fillableTakerAmount: rfqtOrder1.fillableTakerAmount,
isRfqt: true,
makerUri: 'https://rfqt1.provider.club',
fillData: {
order: rfqtOrder1.order,
} as NativeRfqOrderFillData,
};
const rfqtOrder2Source: NativeRFQTReportSource = {
const rfqtOrder2Source: NativeRfqOrderQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: rfqtOrder2.makerAssetAmount,
takerAmount: rfqtOrder2.takerAssetAmount,
nativeOrder: rfqtOrder2,
fillableTakerAmount: rfqtOrder2FillableAmount,
makerAmount: rfqtOrder2.order.makerAmount,
takerAmount: rfqtOrder2.order.takerAmount,
fillableTakerAmount: rfqtOrder2.fillableTakerAmount,
isRfqt: true,
makerUri: 'https://rfqt2.provider.club',
fillData: {
order: rfqtOrder2.order,
} as NativeRfqOrderFillData,
};
const orderbookOrder1Source: NativeOrderbookReportSource = {
const orderbookOrder1Source: NativeLimitOrderQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder1.makerAssetAmount,
takerAmount: orderbookOrder1.takerAssetAmount,
nativeOrder: orderbookOrder1,
fillableTakerAmount: orderbookOrder1FillableAmount,
makerAmount: orderbookOrder1.order.makerAmount,
takerAmount: orderbookOrder1.order.takerAmount,
fillableTakerAmount: orderbookOrder1.fillableTakerAmount,
isRfqt: false,
fillData: {
order: orderbookOrder1.order,
} as NativeLimitOrderFillData,
};
const orderbookOrder2Source: NativeOrderbookReportSource = {
const orderbookOrder2Source: NativeLimitOrderQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder2.makerAssetAmount,
takerAmount: orderbookOrder2.takerAssetAmount,
nativeOrder: orderbookOrder2,
fillableTakerAmount: orderbookOrder2FillableAmount,
makerAmount: orderbookOrder2.order.makerAmount,
takerAmount: orderbookOrder2.order.takerAmount,
fillableTakerAmount: orderbookOrder2.fillableTakerAmount,
isRfqt: false,
fillData: {
order: orderbookOrder2.order,
} as NativeLimitOrderFillData,
};
const uniswap1Source: BridgeReportSource = {
const uniswap1Source: BridgeQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.UniswapV2,
makerAmount: uniswapSample1.output,
takerAmount: uniswapSample1.input,
fillData: {},
};
const uniswap2Source: BridgeReportSource = {
const uniswap2Source: BridgeQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.UniswapV2,
makerAmount: uniswapSample2.output,
takerAmount: uniswapSample2.input,
fillData: {},
};
const kyber1Source: BridgeReportSource = {
const kyber1Source: BridgeQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Kyber,
makerAmount: kyberSample1.output,
takerAmount: kyberSample1.input,
fillData: {},
};
const kyber2Source: BridgeReportSource = {
const kyber2Source: BridgeQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Kyber,
makerAmount: kyberSample2.output,
takerAmount: kyberSample2.input,
fillData: {},
};
const expectedSourcesConsidered: QuoteReportSource[] = [
const expectedSourcesConsidered: QuoteReportEntry[] = [
kyber1Source,
kyber2Source,
uniswap1Source,
@ -214,39 +243,14 @@ describe('generateQuoteReport', async () => {
rfqtOrder2Source,
orderbookOrder2Source,
];
expect(orderReport.sourcesConsidered.length).to.eql(expectedSourcesConsidered.length);
orderReport.sourcesConsidered.forEach((actualSourcesConsidered, idx) => {
const expectedSourceConsidered = expectedSourcesConsidered[idx];
expect(actualSourcesConsidered).to.eql(
expectedSourceConsidered,
`sourceConsidered incorrect at index ${idx}`,
);
});
const expectedSourcesDelivered: QuoteReportSource[] = [
const expectedSourcesDelivered: QuoteReportEntry[] = [
rfqtOrder2Source,
orderbookOrder2Source,
uniswap2Source,
kyber2Source,
];
expect(orderReport.sourcesDelivered.length).to.eql(expectedSourcesDelivered.length);
orderReport.sourcesDelivered.forEach((actualSourceDelivered, idx) => {
const expectedSourceDelivered = expectedSourcesDelivered[idx];
// remove fillable values
if (actualSourceDelivered.liquiditySource === ERC20BridgeSource.Native) {
actualSourceDelivered.nativeOrder = _.omit(actualSourceDelivered.nativeOrder, [
'fillableMakerAssetAmount',
'fillableTakerAssetAmount',
'fillableTakerFeeAmount',
]) as SignedOrder;
}
expect(actualSourceDelivered).to.eql(expectedSourceDelivered, `sourceDelivered incorrect at index ${idx}`);
});
expectEqualQuoteReportEntries(orderReport.sourcesConsidered, expectedSourcesConsidered, `sourcesConsidered`);
expectEqualQuoteReportEntries(orderReport.sourcesDelivered, expectedSourcesDelivered, `sourcesDelivered`);
quoteRequestor.verifyAll();
});
it('should handle properly for buy without quoteRequestor', () => {
@ -264,95 +268,84 @@ describe('generateQuoteReport', async () => {
fillData: {},
};
const dexQuotes: DexSample[] = [kyberSample1, uniswapSample1];
const orderbookOrder1FillableAmount = new BigNumber(1000);
const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({
signature: 'orderbookOrder1',
takerAssetAmount: orderbookOrder1FillableAmount.plus(101),
});
const orderbookOrder2FillableAmount = new BigNumber(5000);
const orderbookOrder2 = testOrderFactory.generateTestSignedOrder({
signature: 'orderbookOrder2',
takerAssetAmount: orderbookOrder2FillableAmount.plus(101),
});
const nativeOrders: SignedOrder[] = [orderbookOrder1, orderbookOrder2];
const orderFillableAmounts: BigNumber[] = [orderbookOrder1FillableAmount, orderbookOrder2FillableAmount];
const orderbookOrder1: NativeOrderWithFillableAmounts = {
order: new LimitOrder({ takerAmount: new BigNumber(1101) }),
type: FillQuoteTransformerOrderType.Limit,
fillableTakerAmount: new BigNumber(1000),
fillableMakerAmount: getRandomAmount(),
fillableTakerFeeAmount: getRandomAmount(),
signature: getRandomSignature(),
};
const orderbookOrder2: NativeOrderWithFillableAmounts = {
order: new LimitOrder({ takerAmount: new BigNumber(5101) }),
type: FillQuoteTransformerOrderType.Limit,
fillableTakerAmount: new BigNumber(5000), // takerAmount minus 99
fillableMakerAmount: getRandomAmount(),
fillableTakerFeeAmount: getRandomAmount(),
signature: getRandomSignature(),
};
const nativeOrders = [orderbookOrder1, orderbookOrder2];
// generate path
const orderbookOrder1Fill: CollapsedFill = collapsedFillFromNativeOrder(orderbookOrder1);
const uniswap1Fill: CollapsedFill = { ...uniswapSample1, subFills: [], sourcePathId: hexUtils.random() };
const kyber1Fill: CollapsedFill = { ...kyberSample1, subFills: [], sourcePathId: hexUtils.random() };
const uniswap1Fill: CollapsedFill = {
...uniswapSample1,
subFills: [],
sourcePathId: hexUtils.random(),
type: FillQuoteTransformerOrderType.Bridge,
};
const kyber1Fill: CollapsedFill = {
...kyberSample1,
subFills: [],
sourcePathId: hexUtils.random(),
type: FillQuoteTransformerOrderType.Bridge,
};
const pathGenerated: CollapsedFill[] = [orderbookOrder1Fill, uniswap1Fill, kyber1Fill];
const orderReport = generateQuoteReport(
marketOperation,
dexQuotes,
[],
nativeOrders,
orderFillableAmounts,
pathGenerated,
);
const orderReport = generateQuoteReport(marketOperation, dexQuotes, [], nativeOrders, pathGenerated);
const orderbookOrder1Source: NativeOrderbookReportSource = {
const orderbookOrder1Source: NativeLimitOrderQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder1.makerAssetAmount,
takerAmount: orderbookOrder1.takerAssetAmount,
nativeOrder: orderbookOrder1,
fillableTakerAmount: orderbookOrder1FillableAmount,
makerAmount: orderbookOrder1.order.makerAmount,
takerAmount: orderbookOrder1.order.takerAmount,
fillableTakerAmount: orderbookOrder1.fillableTakerAmount,
isRfqt: false,
fillData: {
order: orderbookOrder1.order,
} as NativeLimitOrderFillData,
};
const orderbookOrder2Source: NativeOrderbookReportSource = {
const orderbookOrder2Source: NativeLimitOrderQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder2.makerAssetAmount,
takerAmount: orderbookOrder2.takerAssetAmount,
nativeOrder: orderbookOrder2,
fillableTakerAmount: orderbookOrder2FillableAmount,
makerAmount: orderbookOrder2.order.makerAmount,
takerAmount: orderbookOrder2.order.takerAmount,
fillableTakerAmount: orderbookOrder2.fillableTakerAmount,
isRfqt: false,
fillData: {
order: orderbookOrder2.order,
} as NativeLimitOrderFillData,
};
const uniswap1Source: BridgeReportSource = {
const uniswap1Source: BridgeQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.UniswapV2,
makerAmount: uniswapSample1.input,
takerAmount: uniswapSample1.output,
fillData: {},
};
const kyber1Source: BridgeReportSource = {
const kyber1Source: BridgeQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Kyber,
makerAmount: kyberSample1.input,
takerAmount: kyberSample1.output,
fillData: {},
};
const expectedSourcesConsidered: QuoteReportSource[] = [
const expectedSourcesConsidered: QuoteReportEntry[] = [
kyber1Source,
uniswap1Source,
orderbookOrder1Source,
orderbookOrder2Source,
];
expect(orderReport.sourcesConsidered.length).to.eql(expectedSourcesConsidered.length);
orderReport.sourcesConsidered.forEach((actualSourcesConsidered, idx) => {
const expectedSourceConsidered = expectedSourcesConsidered[idx];
expect(actualSourcesConsidered).to.eql(
expectedSourceConsidered,
`sourceConsidered incorrect at index ${idx}`,
);
});
const expectedSourcesDelivered: QuoteReportSource[] = [orderbookOrder1Source, uniswap1Source, kyber1Source];
expect(orderReport.sourcesDelivered.length).to.eql(expectedSourcesDelivered.length);
orderReport.sourcesDelivered.forEach((actualSourceDelivered, idx) => {
const expectedSourceDelivered = expectedSourcesDelivered[idx];
// remove fillable values
if (actualSourceDelivered.liquiditySource === ERC20BridgeSource.Native) {
actualSourceDelivered.nativeOrder = _.omit(actualSourceDelivered.nativeOrder, [
'fillableMakerAssetAmount',
'fillableTakerAssetAmount',
'fillableTakerFeeAmount',
]) as SignedOrder;
}
expect(actualSourceDelivered).to.eql(expectedSourceDelivered, `sourceDelivered incorrect at index ${idx}`);
});
const expectedSourcesDelivered: QuoteReportEntry[] = [orderbookOrder1Source, uniswap1Source, kyber1Source];
expectEqualQuoteReportEntries(orderReport.sourcesConsidered, expectedSourcesConsidered, `sourcesConsidered`);
expectEqualQuoteReportEntries(orderReport.sourcesDelivered, expectedSourcesDelivered, `sourcesDelivered`);
});
it('should correctly generate report for a two-hop quote', () => {
const marketOperation: MarketOperation = MarketOperation.Sell;
@ -362,29 +355,31 @@ describe('generateQuoteReport', async () => {
output: new BigNumber(10001),
fillData: {},
};
const orderbookOrder1FillableAmount = new BigNumber(1000);
const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({
signature: 'orderbookOrder1',
takerAssetAmount: orderbookOrder1FillableAmount.plus(101),
});
const orderbookOrder1: NativeOrderWithFillableAmounts = {
order: new LimitOrder({ takerAmount: new BigNumber(1101) }),
type: FillQuoteTransformerOrderType.Limit,
fillableTakerAmount: new BigNumber(1000),
fillableMakerAmount: getRandomAmount(),
fillableTakerFeeAmount: getRandomAmount(),
signature: getRandomSignature(),
};
const twoHopFillData: MultiHopFillData = {
intermediateToken: hexUtils.random(20),
firstHopSource: {
source: ERC20BridgeSource.Balancer,
fillData: {},
encodeCall: () => '',
handleCallResults: _callResults => [new BigNumber(1337)],
handleRevert: _c => [],
},
secondHopSource: {
source: ERC20BridgeSource.Curve,
fillData: {},
encodeCall: () => '',
handleCallResults: _callResults => [new BigNumber(1337)],
handleRevert: _c => [],
},
};
const twoHopSample: DexSample<MultiHopFillData> = {
source: ERC20BridgeSource.MultiHop,
input: new BigNumber(3005),
@ -397,24 +392,25 @@ describe('generateQuoteReport', async () => {
[kyberSample1],
[twoHopSample],
[orderbookOrder1],
[orderbookOrder1FillableAmount],
twoHopSample,
);
const orderbookOrder1Source: NativeOrderbookReportSource = {
const orderbookOrder1Source: NativeLimitOrderQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder1.makerAssetAmount,
takerAmount: orderbookOrder1.takerAssetAmount,
nativeOrder: orderbookOrder1,
fillableTakerAmount: orderbookOrder1FillableAmount,
makerAmount: orderbookOrder1.order.makerAmount,
takerAmount: orderbookOrder1.order.takerAmount,
fillableTakerAmount: orderbookOrder1.fillableTakerAmount,
isRfqt: false,
fillData: {
order: orderbookOrder1.order,
} as NativeLimitOrderFillData,
};
const kyber1Source: BridgeReportSource = {
const kyber1Source: BridgeQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.Kyber,
makerAmount: kyberSample1.output,
takerAmount: kyberSample1.input,
fillData: {},
};
const twoHopSource: MultiHopReportSource = {
const twoHopSource: MultiHopQuoteReportEntry = {
liquiditySource: ERC20BridgeSource.MultiHop,
makerAmount: twoHopSample.output,
takerAmount: twoHopSample.input,
@ -422,17 +418,37 @@ describe('generateQuoteReport', async () => {
fillData: twoHopFillData,
};
const expectedSourcesConsidered: QuoteReportSource[] = [kyber1Source, orderbookOrder1Source, twoHopSource];
expect(orderReport.sourcesConsidered.length).to.eql(expectedSourcesConsidered.length);
orderReport.sourcesConsidered.forEach((actualSourcesConsidered, idx) => {
const expectedSourceConsidered = expectedSourcesConsidered[idx];
expect(actualSourcesConsidered).to.eql(
expectedSourceConsidered,
`sourceConsidered incorrect at index ${idx}`,
);
});
const expectedSourcesConsidered: QuoteReportEntry[] = [kyber1Source, orderbookOrder1Source, twoHopSource];
expectEqualQuoteReportEntries(orderReport.sourcesConsidered, expectedSourcesConsidered, `sourcesConsidered`);
expect(orderReport.sourcesDelivered.length).to.eql(1);
expect(orderReport.sourcesDelivered[0]).to.deep.equal(twoHopSource);
});
});
function expectEqualQuoteReportEntries(
actual: QuoteReportEntry[],
expected: QuoteReportEntry[],
variableName: string = 'quote report entries',
): void {
expect(actual.length).to.eql(expected.length);
actual.forEach((actualEntry, idx) => {
const expectedEntry = expected[idx];
// remove fillable values
if (actualEntry.liquiditySource === ERC20BridgeSource.Native) {
actualEntry.fillData.order = _.omit(actualEntry.fillData.order, [
'fillableMakerAmount',
'fillableTakerAmount',
'fillableTakerFeeAmount',
]) as LimitOrderFields;
expect(actualEntry.fillData.order).to.eql(
// tslint:disable-next-line:no-unnecessary-type-assertion
(expectedEntry.fillData as NativeFillData).order,
`${variableName} incorrect at index ${idx}`,
);
}
expect(_.omit(actualEntry, 'fillData')).to.eql(
_.omit(expectedEntry, 'fillData'),
`${variableName} incorrect at index ${idx}`,
);
});
}

View File

@ -1,18 +1,19 @@
import { tokenUtils } from '@0x/dev-utils';
import { assetDataUtils } from '@0x/order-utils';
import { FillQuoteTransformerOrderType, SignatureType } from '@0x/protocol-utils';
import { TakerRequestQueryParams } from '@0x/quote-server';
import { StatusCodes } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import _ = require('lodash');
import 'mocha';
import { constants } from '../src/constants';
import { MarketOperation, MockedRfqtFirmQuoteResponse, MockedRfqtIndicativeQuoteResponse } from '../src/types';
import { MarketOperation, MockedRfqtQuoteResponse } from '../src/types';
import { NULL_ADDRESS } from '../src/utils/market_operation_utils/constants';
import { QuoteRequestor, quoteRequestorHttpClient } from '../src/utils/quote_requestor';
import { rfqtMocker } from '../src/utils/rfqt_mocker';
import { chaiSetup } from './utils/chai_setup';
import { testOrderFactory } from './utils/test_order_factory';
import { RfqtQuoteEndpoint, testHelpers } from './utils/test_helpers';
chaiSetup.configure();
const expect = chai.expect;
@ -25,138 +26,120 @@ function makeThreeMinuteExpiry(): BigNumber {
describe('QuoteRequestor', async () => {
const [makerToken, takerToken, otherToken1] = tokenUtils.getDummyERC20TokenAddresses();
const makerAssetData = assetDataUtils.encodeERC20AssetData(makerToken);
const takerAssetData = assetDataUtils.encodeERC20AssetData(takerToken);
const validSignature = { v: 28, r: '0x', s: '0x', signatureType: SignatureType.EthSign };
describe('requestRfqtFirmQuotesAsync for firm quotes', async () => {
it('should return successful RFQT requests', async () => {
const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
const txOrigin = takerAddress;
const apiKey = 'my-ko0l-api-key';
// Set up RFQT responses
// tslint:disable-next-line:array-type
const mockedRequests: MockedRfqtFirmQuoteResponse[] = [];
const mockedRequests: MockedRfqtQuoteResponse[] = [];
const expectedParams: TakerRequestQueryParams = {
sellTokenAddress: takerToken,
buyTokenAddress: makerToken,
sellAmountBaseUnits: '10000',
comparisonPrice: undefined,
takerAddress,
protocolVersion: '3',
txOrigin,
protocolVersion: '4',
};
// Successful response
const successfulOrder1 = testOrderFactory.generateTestSignedOrder({
makerAssetData,
takerAssetData,
takerAddress,
feeRecipientAddress: '0x0000000000000000000000000000000000000001',
expirationTimeSeconds: makeThreeMinuteExpiry(),
});
mockedRequests.push({
endpoint: 'https://1337.0.0.1',
const mockedDefaults = {
requestApiKey: apiKey,
requestParams: expectedParams,
responseData: { signedOrder: successfulOrder1 },
responseCode: StatusCodes.Success,
};
const validSignedOrder = {
makerToken,
takerToken,
makerAmount: new BigNumber('1000'),
takerAmount: new BigNumber('1000'),
maker: takerAddress,
taker: takerAddress,
pool: '0x',
salt: '0',
chainId: 1,
verifyingContract: takerAddress,
txOrigin,
expiry: makeThreeMinuteExpiry(),
signature: validSignature,
};
// Successful response
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://1337.0.0.1',
responseData: {
signedOrder: validSignedOrder,
},
});
// Another Successful response
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://37.0.0.1',
responseData: { signedOrder: validSignedOrder },
});
// Test out a bad response code, ensure it doesnt cause throw
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://420.0.0.1',
requestApiKey: apiKey,
requestParams: expectedParams,
responseData: { error: 'bad request' },
responseCode: StatusCodes.InternalError,
});
// Test out a successful response code but an invalid order
// Test out a successful response code but a partial order
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://421.0.0.1',
requestApiKey: apiKey,
requestParams: expectedParams,
responseData: { makerAssetData: '123' },
responseCode: StatusCodes.Success,
responseData: { signedOrder: { makerToken: '123' } },
});
// ensure that a non-JSON response doesn't throw an error when trying to parse
// A successful response code and invalid response data (encoding)
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://421.1.0.1',
requestApiKey: apiKey,
requestParams: expectedParams,
responseData: 'this is not JSON!',
responseCode: StatusCodes.Success,
});
// A successful response code and valid order, but for wrong maker asset data
const wrongMakerAssetDataOrder = testOrderFactory.generateTestSignedOrder({
makerAssetData: assetDataUtils.encodeERC20AssetData(otherToken1),
expirationTimeSeconds: makeThreeMinuteExpiry(),
takerAssetData,
});
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://422.0.0.1',
requestApiKey: apiKey,
requestParams: expectedParams,
responseData: { signedOrder: wrongMakerAssetDataOrder },
responseCode: StatusCodes.Success,
responseData: { signedOrder: { ...validSignedOrder, makerToken: '0x1234' } },
});
// A successful response code and valid order, but for wrong taker asset data
const wrongTakerAssetDataOrder = testOrderFactory.generateTestSignedOrder({
makerAssetData,
expirationTimeSeconds: makeThreeMinuteExpiry(),
takerAssetData: assetDataUtils.encodeERC20AssetData(otherToken1),
});
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://423.0.0.1',
requestApiKey: apiKey,
requestParams: expectedParams,
responseData: { signedOrder: wrongTakerAssetDataOrder },
responseCode: StatusCodes.Success,
responseData: { signedOrder: { ...validSignedOrder, takerToken: '0x1234' } },
});
// A successful response code and good order but its unsigned
const unsignedOrder = testOrderFactory.generateTestSignedOrder({
makerAssetData,
takerAssetData,
expirationTimeSeconds: makeThreeMinuteExpiry(),
feeRecipientAddress: '0x0000000000000000000000000000000000000002',
});
delete unsignedOrder.signature;
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://424.0.0.1',
requestApiKey: apiKey,
requestParams: expectedParams,
responseData: { signedOrder: unsignedOrder },
responseCode: StatusCodes.Success,
});
// A successful response code and good order but for the wrong takerAddress
const orderWithNullTaker = testOrderFactory.generateTestSignedOrder({
makerAssetData,
takerAssetData,
expirationTimeSeconds: makeThreeMinuteExpiry(),
takerAddress: constants.NULL_ADDRESS,
feeRecipientAddress: '0x0000000000000000000000000000000000000002',
responseData: { signedOrder: _.omit(validSignedOrder, ['signature']) },
});
// A successful response code and good order but for the wrong txOrigin
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://425.0.0.1',
requestApiKey: apiKey,
requestParams: expectedParams,
responseData: { signedOrder: orderWithNullTaker },
responseCode: StatusCodes.Success,
responseData: { signedOrder: { ...validSignedOrder, txOrigin: NULL_ADDRESS } },
});
// Another Successful response
const successfulOrder2 = testOrderFactory.generateTestSignedOrder({
makerAssetData,
takerAssetData,
takerAddress,
expirationTimeSeconds: makeThreeMinuteExpiry(),
});
mockedRequests.push({
endpoint: 'https://37.0.0.1',
requestApiKey: apiKey,
requestParams: expectedParams,
responseData: { signedOrder: successfulOrder2 },
responseCode: StatusCodes.Success,
});
const normalizedSuccessfulOrder = {
order: {
..._.omit(validSignedOrder, ['signature']),
makerAmount: new BigNumber(validSignedOrder.makerAmount),
takerAmount: new BigNumber(validSignedOrder.takerAmount),
expiry: new BigNumber(validSignedOrder.expiry),
salt: new BigNumber(validSignedOrder.salt),
},
signature: validSignedOrder.signature,
type: FillQuoteTransformerOrderType.Rfq,
};
return rfqtMocker.withMockedRfqtFirmQuotes(
return testHelpers.withMockedRfqtQuotes(
mockedRequests,
RfqtQuoteEndpoint.Firm,
async () => {
const qr = new QuoteRequestor({
'https://1337.0.0.1': [[makerToken, takerToken]],
@ -167,25 +150,23 @@ describe('QuoteRequestor', async () => {
'https://423.0.0.1': [[makerToken, takerToken]],
'https://424.0.0.1': [[makerToken, takerToken]],
'https://425.0.0.1': [[makerToken, takerToken]],
'https://426.0.0.1': [] /* Shouldn't ping an RFQ-T
provider when they don't support the requested asset pair. */,
'https://426.0.0.1': [] /* Shouldn't ping an RFQ-T provider when they don't support the requested asset pair. */,
'https://37.0.0.1': [[makerToken, takerToken]],
});
const resp = await qr.requestRfqtFirmQuotesAsync(
makerAssetData,
takerAssetData,
makerToken,
takerToken,
new BigNumber(10000),
MarketOperation.Sell,
undefined,
{
apiKey,
takerAddress,
txOrigin: takerAddress,
intentOnFilling: true,
},
);
expect(resp.sort()).to.eql(
[{ signedOrder: successfulOrder1 }, { signedOrder: successfulOrder2 }].sort(),
);
expect(resp).to.deep.eq([normalizedSuccessfulOrder, normalizedSuccessfulOrder]);
},
quoteRequestorHttpClient,
);
@ -194,10 +175,11 @@ describe('QuoteRequestor', async () => {
describe('requestRfqtIndicativeQuotesAsync for Indicative quotes', async () => {
it('should optionally accept a "comparisonPrice" parameter', async () => {
const response = QuoteRequestor.makeQueryParameters(
otherToken1,
otherToken1, // tx origin
otherToken1, // taker
MarketOperation.Sell,
makerAssetData,
takerAssetData,
makerToken,
takerToken,
new BigNumber(1000),
new BigNumber(300.2),
);
@ -209,73 +191,71 @@ describe('QuoteRequestor', async () => {
// Set up RFQT responses
// tslint:disable-next-line:array-type
const mockedRequests: MockedRfqtIndicativeQuoteResponse[] = [];
const mockedRequests: MockedRfqtQuoteResponse[] = [];
const expectedParams: TakerRequestQueryParams = {
sellTokenAddress: takerToken,
buyTokenAddress: makerToken,
sellAmountBaseUnits: '10000',
comparisonPrice: undefined,
protocolVersion: '3',
takerAddress,
txOrigin: takerAddress,
protocolVersion: '4',
};
// Successful response
const successfulQuote1 = {
makerAssetData,
takerAssetData,
makerAssetAmount: new BigNumber(expectedParams.sellAmountBaseUnits),
takerAssetAmount: new BigNumber(expectedParams.sellAmountBaseUnits),
expirationTimeSeconds: makeThreeMinuteExpiry(),
};
mockedRequests.push({
endpoint: 'https://1337.0.0.1',
const mockedDefaults = {
requestApiKey: apiKey,
requestParams: expectedParams,
responseData: successfulQuote1,
responseCode: StatusCodes.Success,
};
// Successful response
const successfulQuote1 = {
makerToken,
takerToken,
makerAmount: new BigNumber(expectedParams.sellAmountBaseUnits),
takerAmount: new BigNumber(expectedParams.sellAmountBaseUnits),
expiry: makeThreeMinuteExpiry(),
};
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://1337.0.0.1',
responseData: successfulQuote1,
});
// Test out a bad response code, ensure it doesnt cause throw
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://420.0.0.1',
requestApiKey: apiKey,
requestParams: expectedParams,
responseData: { error: 'bad request' },
responseCode: StatusCodes.InternalError,
});
// Test out a successful response code but an invalid order
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://421.0.0.1',
requestApiKey: apiKey,
requestParams: expectedParams,
responseData: { makerAssetData: '123' },
responseCode: StatusCodes.Success,
responseData: { makerToken: '123' },
});
// A successful response code and valid response data, but for wrong maker asset data
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://422.0.0.1',
requestApiKey: apiKey,
requestParams: expectedParams,
responseData: { ...successfulQuote1, makerAssetData: assetDataUtils.encodeERC20AssetData(otherToken1) },
responseCode: StatusCodes.Success,
responseData: { ...successfulQuote1, makerToken: otherToken1 },
});
// A successful response code and valid response data, but for wrong taker asset data
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://423.0.0.1',
requestApiKey: apiKey,
requestParams: expectedParams,
responseData: { ...successfulQuote1, takerAssetData: assetDataUtils.encodeERC20AssetData(otherToken1) },
responseCode: StatusCodes.Success,
responseData: { ...successfulQuote1, takerToken: otherToken1 },
});
// Another Successful response
mockedRequests.push({
...mockedDefaults,
endpoint: 'https://37.0.0.1',
requestApiKey: apiKey,
requestParams: expectedParams,
responseData: successfulQuote1,
responseCode: StatusCodes.Success,
});
return rfqtMocker.withMockedRfqtIndicativeQuotes(
return testHelpers.withMockedRfqtQuotes(
mockedRequests,
RfqtQuoteEndpoint.Indicative,
async () => {
const qr = new QuoteRequestor({
'https://1337.0.0.1': [[makerToken, takerToken]],
@ -287,14 +267,15 @@ describe('QuoteRequestor', async () => {
'https://37.0.0.1': [[makerToken, takerToken]],
});
const resp = await qr.requestRfqtIndicativeQuotesAsync(
makerAssetData,
takerAssetData,
makerToken,
takerToken,
new BigNumber(10000),
MarketOperation.Sell,
undefined,
{
apiKey,
takerAddress,
txOrigin: takerAddress,
intentOnFilling: true,
},
);
@ -303,28 +284,29 @@ describe('QuoteRequestor', async () => {
quoteRequestorHttpClient,
);
});
it('should return successful RFQT indicative quote requests', async () => {
it('should return successful RFQT indicative quote requests (Buy)', async () => {
const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a';
const apiKey = 'my-ko0l-api-key';
// Set up RFQT responses
// tslint:disable-next-line:array-type
const mockedRequests: MockedRfqtIndicativeQuoteResponse[] = [];
const mockedRequests: MockedRfqtQuoteResponse[] = [];
const expectedParams: TakerRequestQueryParams = {
sellTokenAddress: takerToken,
buyTokenAddress: makerToken,
buyAmountBaseUnits: '10000',
comparisonPrice: undefined,
protocolVersion: '3',
takerAddress,
txOrigin: takerAddress,
protocolVersion: '4',
};
// Successful response
const successfulQuote1 = {
makerAssetData,
takerAssetData,
makerAssetAmount: new BigNumber(expectedParams.buyAmountBaseUnits),
takerAssetAmount: new BigNumber(expectedParams.buyAmountBaseUnits),
expirationTimeSeconds: makeThreeMinuteExpiry(),
makerToken,
takerToken,
makerAmount: new BigNumber(expectedParams.buyAmountBaseUnits),
takerAmount: new BigNumber(expectedParams.buyAmountBaseUnits),
expiry: makeThreeMinuteExpiry(),
};
mockedRequests.push({
endpoint: 'https://1337.0.0.1',
@ -334,19 +316,21 @@ describe('QuoteRequestor', async () => {
responseCode: StatusCodes.Success,
});
return rfqtMocker.withMockedRfqtIndicativeQuotes(
return testHelpers.withMockedRfqtQuotes(
mockedRequests,
RfqtQuoteEndpoint.Indicative,
async () => {
const qr = new QuoteRequestor({ 'https://1337.0.0.1': [[makerToken, takerToken]] });
const resp = await qr.requestRfqtIndicativeQuotesAsync(
makerAssetData,
takerAssetData,
makerToken,
takerToken,
new BigNumber(10000),
MarketOperation.Buy,
undefined,
{
apiKey,
takerAddress,
txOrigin: takerAddress,
intentOnFilling: true,
},
);

View File

@ -1,10 +1,16 @@
import { constants, expect, getRandomInteger, randomAddress } from '@0x/contracts-test-utils';
import { assetDataUtils } from '@0x/order-utils';
import { BigNumber, hexUtils } from '@0x/utils';
import { FillQuoteTransformerOrderType, SignatureType } from '@0x/protocol-utils';
import { BigNumber, hexUtils, NULL_BYTES } from '@0x/utils';
import * as _ from 'lodash';
import { MarketOperation } from '../src/types';
import { CollapsedFill, ERC20BridgeSource, OptimizedMarketOrder } from '../src/utils/market_operation_utils/types';
import {
CollapsedFill,
ERC20BridgeSource,
NativeLimitOrderFillData,
OptimizedMarketOrder,
OptimizedMarketOrderBase,
} from '../src/utils/market_operation_utils/types';
import {
fillQuoteOrders,
QuoteFillOrderCall,
@ -20,9 +26,7 @@ describe('quote_simulation tests', async () => {
const ONE = new BigNumber(1);
const MAKER_TOKEN = randomAddress();
const TAKER_TOKEN = randomAddress();
const DEFAULT_MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN);
const DEFAULT_TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN);
const GAS_SCHEDULE = { [ERC20BridgeSource.Uniswap]: _.constant(1) };
const GAS_SCHEDULE = { [ERC20BridgeSource.Uniswap]: _.constant(1), [ERC20BridgeSource.Native]: _.constant(1) };
// Check if two numbers are within `maxError` error rate within each other.
function assertRoughlyEquals(n1: BigNumber, n2: BigNumber, maxError: BigNumber | number = 1e-10): void {
@ -43,9 +47,10 @@ describe('quote_simulation tests', async () => {
count: number;
fillsCount: number;
side: MarketOperation;
type?: FillQuoteTransformerOrderType;
}> = {},
): QuoteFillOrderCall[] {
const { fillableInput, fillableOutput, inputFeeRate, outputFeeRate, count, fillsCount, side } = {
const { fillableInput, fillableOutput, inputFeeRate, outputFeeRate, count, fillsCount, side, type } = {
fillableInput: getRandomOrderSize(),
fillableOutput: getRandomOrderSize(),
inputFeeRate: 0,
@ -82,6 +87,7 @@ describe('quote_simulation tests', async () => {
filledInput: filledInputs[i],
takerInputFee: inputFees[i].abs(),
takerOutputFee: outputFees[i].abs(),
type,
}),
totalOrderInput: totalInputs[i],
totalOrderOutput: totalOutputs[i],
@ -100,60 +106,63 @@ describe('quote_simulation tests', async () => {
side: MarketOperation;
takerInputFee: BigNumber;
takerOutputFee: BigNumber;
type: FillQuoteTransformerOrderType;
}> = {},
): OptimizedMarketOrder {
const { filledInput, fillsCount, side, takerInputFee, takerOutputFee } = {
side: MarketOperation.Sell,
filledInput: ZERO,
fillsCount: 3,
takerInputFee: ZERO,
takerOutputFee: ZERO,
...opts,
};
): OptimizedMarketOrderBase<NativeLimitOrderFillData> {
const { filledInput, fillsCount, side, takerInputFee, takerOutputFee, type } = _.merge(
{},
{
side: MarketOperation.Sell,
filledInput: ZERO,
fillsCount: 3,
takerInputFee: ZERO,
takerOutputFee: ZERO,
type: FillQuoteTransformerOrderType.Limit,
},
opts,
);
const filledOutput = filledInput
.div(input)
.times(output)
.integerValue(BigNumber.ROUND_DOWN);
const fillableInput = input.minus(filledInput);
const fillableOutput = output.minus(filledOutput);
const makerAssetAmount = side === MarketOperation.Sell ? output : input;
const takerAssetAmount = side === MarketOperation.Sell ? input : output;
const fillableMakerAssetAmount = side === MarketOperation.Sell ? fillableOutput : fillableInput;
const fillableTakerAssetAmount = side === MarketOperation.Sell ? fillableInput : fillableOutput;
const makerAmount = side === MarketOperation.Sell ? output : input;
const takerAmount = side === MarketOperation.Sell ? input : output;
const fillableMakerAmount = side === MarketOperation.Sell ? fillableOutput : fillableInput;
const fillableTakerAmount = side === MarketOperation.Sell ? fillableInput : fillableOutput;
const takerFee = BigNumber.max(takerInputFee, takerOutputFee);
let takerFeeAssetData = '0x';
if (!takerInputFee.eq(0)) {
takerFeeAssetData = side === MarketOperation.Sell ? DEFAULT_TAKER_ASSET_DATA : DEFAULT_MAKER_ASSET_DATA;
} else if (!takerOutputFee.eq(0)) {
takerFeeAssetData = side === MarketOperation.Sell ? DEFAULT_MAKER_ASSET_DATA : DEFAULT_TAKER_ASSET_DATA;
}
const fillableTakerFeeAmount = fillableTakerAssetAmount
.div(takerAssetAmount)
.times(takerFee)
.integerValue(BigNumber.ROUND_DOWN);
return {
makerAssetAmount,
takerAssetAmount,
fillableTakerAssetAmount,
fillableMakerAssetAmount,
fillableTakerFeeAmount,
takerFee,
takerFeeAssetData,
const order: OptimizedMarketOrderBase<NativeLimitOrderFillData> = {
source: ERC20BridgeSource.Native,
makerToken: MAKER_TOKEN,
takerToken: TAKER_TOKEN,
makerAmount: fillableMakerAmount,
takerAmount: fillableTakerAmount,
fillData: {
order: {
makerToken: MAKER_TOKEN,
makerAmount,
takerToken: TAKER_TOKEN,
takerAmount,
maker: NULL_ADDRESS,
taker: NULL_ADDRESS,
sender: NULL_ADDRESS,
salt: ZERO,
chainId: 1,
pool: NULL_BYTES,
verifyingContract: NULL_ADDRESS,
expiry: ZERO,
feeRecipient: NULL_ADDRESS,
takerTokenFeeAmount: takerFee,
},
signature: { v: 1, r: NULL_BYTES, s: NULL_BYTES, signatureType: SignatureType.EthSign },
maxTakerTokenFillAmount: fillableTakerAmount,
},
type,
fills: createOrderCollapsedFills(fillableInput, fillableOutput, fillsCount),
chainId: 1,
exchangeAddress: NULL_ADDRESS,
expirationTimeSeconds: ZERO,
feeRecipientAddress: NULL_ADDRESS,
senderAddress: NULL_ADDRESS,
makerAddress: NULL_ADDRESS,
takerAddress: NULL_ADDRESS,
makerAssetData: DEFAULT_MAKER_ASSET_DATA,
takerAssetData: DEFAULT_TAKER_ASSET_DATA,
makerFeeAssetData: '0x',
salt: ZERO,
makerFee: ZERO,
signature: '0x',
};
return order;
}
const nativeSourcePathId = hexUtils.random();
function createOrderCollapsedFills(input: BigNumber, output: BigNumber, count: number): CollapsedFill[] {
@ -163,8 +172,10 @@ describe('quote_simulation tests', async () => {
const subFillInputs = subdivideAmount(inputs[i], count);
const subFillOutputs = subdivideAmount(outputs[i], count);
return {
type: FillQuoteTransformerOrderType.Bridge,
sourcePathId: nativeSourcePathId,
source: ERC20BridgeSource.Uniswap,
fillData: {},
input: inputs[i],
output: outputs[i],
subFills: _.times(count, j => ({
@ -475,6 +486,28 @@ describe('quote_simulation tests', async () => {
expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.eq(fillsCount);
});
it('does not charge a protocol fee for rfq orders', () => {
const side = randomSide();
const fillsCount = _.random(1, 3);
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const fillOrders = createQuoteFillOrders({
fillableInput,
fillableOutput,
side,
fillsCount,
count: 1,
type: FillQuoteTransformerOrderType.Rfq,
});
const result = fillQuoteOrders(fillOrders, fillableInput, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
expect(totalFilledInput).to.bignumber.eq(fillableInput);
assertRoughlyEquals(totalFilledOutput, fillableOutput);
expect(result.protocolFee).to.bignumber.eq(0);
expect(result.gas).to.eq(fillsCount);
});
});
describe('multiple orders', () => {
@ -666,18 +699,29 @@ describe('quote_simulation tests', async () => {
});
function slipOrder(
order: OptimizedMarketOrder,
order: OptimizedMarketOrderBase<NativeLimitOrderFillData>,
orderSlippage: number,
side: MarketOperation,
): OptimizedMarketOrder {
const makerScaling = side === MarketOperation.Sell ? 1 - orderSlippage : 1;
const takerScaling = side === MarketOperation.Sell ? 1 : orderSlippage + 1;
// tslint:disable:next-line no-unnecessary-type-assertion
const nativeFillData = order.fillData!;
const slippedFillData = {
order: {
...nativeFillData.order,
takerAmount: nativeFillData.order.takerAmount.times(takerScaling),
makerAmount: nativeFillData.order.makerAmount.times(makerScaling),
},
signature: nativeFillData.signature,
maxTakerTokenFillAmount: nativeFillData.maxTakerTokenFillAmount.times(takerScaling),
};
return {
...order,
makerAssetAmount: order.makerAssetAmount.times(makerScaling),
fillableMakerAssetAmount: order.fillableMakerAssetAmount.times(makerScaling),
takerAssetAmount: order.takerAssetAmount.times(takerScaling),
fillableTakerAssetAmount: order.fillableTakerAssetAmount.times(takerScaling),
makerAmount: order.makerAmount.times(makerScaling),
takerAmount: order.takerAmount.times(takerScaling),
fillData: slippedFillData,
};
}
@ -687,11 +731,14 @@ describe('quote_simulation tests', async () => {
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const orderSlippage = getRandomFeeRate();
const orders = createQuoteFillOrders({
const fillOrders = createQuoteFillOrders({
fillableInput,
fillableOutput,
side,
}).map(fo => slipOrder(fo.order, orderSlippage, side));
});
const orders = fillOrders.map(fo =>
slipOrder(fo.order as OptimizedMarketOrderBase<NativeLimitOrderFillData>, orderSlippage, side),
);
const result = simulateBestCaseFill({
orders,
side,
@ -771,8 +818,8 @@ describe('quote_simulation tests', async () => {
}
});
it('can fully fill orders with input fees', async () => {
const side = randomSide();
it('can fully fill sell orders with "input" fees', async () => {
const side = MarketOperation.Sell;
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const inputFeeRate = getRandomFeeRate();
@ -780,9 +827,8 @@ describe('quote_simulation tests', async () => {
fillableInput,
fillableOutput,
inputFeeRate,
side,
}).map(fo => fo.order);
const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate;
const signedInputFeeRate = inputFeeRate;
const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue();
const result = simulateBestCaseFill({
orders,
@ -791,27 +837,17 @@ describe('quote_simulation tests', async () => {
gasPrice: ONE,
opts: { gasSchedule: GAS_SCHEDULE },
});
expect(result.gas).to.eq(countCollapsedFills(orders));
expect(result.protocolFeeAmount).to.bignumber.gt(orders.length);
if (side === MarketOperation.Sell) {
assertRoughlyEquals(result.takerAssetAmount, fillableInput);
assertRoughlyEquals(result.totalTakerAssetAmount, totalFillableInput);
assertRoughlyEquals(result.makerAssetAmount, fillableOutput);
assertRoughlyEquals(result.totalMakerAssetAmount, fillableOutput);
expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount);
expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0);
} else {
assertRoughlyEquals(result.makerAssetAmount, fillableInput);
assertRoughlyEquals(result.totalMakerAssetAmount, totalFillableInput);
assertRoughlyEquals(result.takerAssetAmount, fillableOutput);
assertRoughlyEquals(result.totalTakerAssetAmount, fillableOutput);
expect(result.takerAssetAmount).to.bignumber.eq(result.totalTakerAssetAmount);
expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0);
}
assertRoughlyEquals(result.takerAssetAmount, fillableInput);
assertRoughlyEquals(result.totalTakerAssetAmount, totalFillableInput);
assertRoughlyEquals(result.makerAssetAmount, fillableOutput);
assertRoughlyEquals(result.totalMakerAssetAmount, fillableOutput);
expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount);
expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0);
});
it('can partially fill orders with input fees', async () => {
const side = randomSide();
it('can partially fill sell orders with "input" fees', async () => {
const side = MarketOperation.Sell;
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const inputFeeRate = getRandomFeeRate();
@ -821,7 +857,7 @@ describe('quote_simulation tests', async () => {
inputFeeRate,
side,
}).map(fo => fo.order);
const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate;
const signedInputFeeRate = inputFeeRate;
const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue();
const inputFillAmount = totalFillableInput.times(2 / 3).integerValue();
const result = simulateBestCaseFill({
@ -833,21 +869,14 @@ describe('quote_simulation tests', async () => {
});
expect(result.gas).to.gt(0);
expect(result.protocolFeeAmount).to.bignumber.gt(0);
if (side === MarketOperation.Sell) {
assertRoughlyEquals(result.totalTakerAssetAmount, inputFillAmount);
expect(result.makerAssetAmount).to.bignumber.lt(fillableOutput);
expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount);
expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0);
} else {
assertRoughlyEquals(result.totalMakerAssetAmount, inputFillAmount);
expect(result.takerAssetAmount).to.bignumber.lt(fillableOutput);
expect(result.takerAssetAmount).to.bignumber.eq(result.totalTakerAssetAmount);
expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0);
}
assertRoughlyEquals(result.totalTakerAssetAmount, inputFillAmount);
expect(result.makerAssetAmount).to.bignumber.lt(fillableOutput);
expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount);
expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0);
});
it('can fully fill orders with output fees', async () => {
const side = randomSide();
it('can fully fill buy orders with "output" fees', async () => {
const side = MarketOperation.Buy;
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const outputFeeRate = getRandomFeeRate();
@ -857,7 +886,7 @@ describe('quote_simulation tests', async () => {
outputFeeRate,
side,
}).map(fo => fo.order);
const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate;
const signedOutputFeeRate = outputFeeRate;
const totalFillableOutput = fillableOutput.times(signedOutputFeeRate + 1).integerValue();
const result = simulateBestCaseFill({
orders,
@ -868,25 +897,17 @@ describe('quote_simulation tests', async () => {
});
expect(result.gas).to.eq(countCollapsedFills(orders));
expect(result.protocolFeeAmount).to.bignumber.gt(orders.length);
if (side === MarketOperation.Sell) {
assertRoughlyEquals(result.takerAssetAmount, fillableInput);
assertRoughlyEquals(result.totalTakerAssetAmount, fillableInput);
assertRoughlyEquals(result.makerAssetAmount, fillableOutput);
assertRoughlyEquals(result.totalMakerAssetAmount, totalFillableOutput);
expect(result.takerAssetAmount).to.bignumber.eq(result.totalTakerAssetAmount);
expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0);
} else {
assertRoughlyEquals(result.makerAssetAmount, fillableInput);
assertRoughlyEquals(result.totalMakerAssetAmount, fillableInput);
assertRoughlyEquals(result.takerAssetAmount, fillableOutput);
assertRoughlyEquals(result.totalTakerAssetAmount, totalFillableOutput);
expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount);
expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0);
}
assertRoughlyEquals(result.makerAssetAmount, fillableInput);
assertRoughlyEquals(result.totalMakerAssetAmount, fillableInput);
assertRoughlyEquals(result.takerAssetAmount, fillableOutput);
assertRoughlyEquals(result.totalTakerAssetAmount, totalFillableOutput);
expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount);
expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0);
});
it('can partially fill orders with output fees', async () => {
const side = randomSide();
it('can partially fill buy orders with "output" fees', async () => {
const side = MarketOperation.Buy;
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const outputFeeRate = getRandomFeeRate();
@ -906,17 +927,10 @@ describe('quote_simulation tests', async () => {
});
expect(result.gas).to.gt(0);
expect(result.protocolFeeAmount).to.bignumber.gt(0);
if (side === MarketOperation.Sell) {
assertRoughlyEquals(result.totalTakerAssetAmount, inputFillAmount);
expect(result.makerAssetAmount).to.bignumber.lt(fillableOutput);
expect(result.takerAssetAmount).to.bignumber.eq(result.totalTakerAssetAmount);
expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0);
} else {
assertRoughlyEquals(result.totalMakerAssetAmount, inputFillAmount);
expect(result.takerAssetAmount).to.bignumber.lt(fillableOutput);
expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount);
expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0);
}
assertRoughlyEquals(result.totalMakerAssetAmount, inputFillAmount);
expect(result.takerAssetAmount).to.bignumber.lt(fillableOutput);
expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount);
expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0);
});
});
@ -925,25 +939,26 @@ describe('quote_simulation tests', async () => {
const side = randomSide();
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const orderSlippage = getRandomFeeRate();
const slippage = getRandomFeeRate();
const orders = createQuoteFillOrders({
fillableInput,
fillableOutput,
side,
}).map(fo => slipOrder(fo.order, orderSlippage, side));
}).map(fo => fo.order);
const result = simulateWorstCaseFill({
orders,
side,
fillAmount: fillableInput,
gasPrice: ONE,
opts: { gasSchedule: GAS_SCHEDULE },
opts: { gasSchedule: GAS_SCHEDULE, slippage },
});
if (side === MarketOperation.Sell) {
const slippedOutput = fillableOutput.times(1 - orderSlippage).integerValue();
const slippedOutput = fillableOutput.times(1 - slippage).integerValue();
assertRoughlyEquals(result.totalMakerAssetAmount, slippedOutput);
assertRoughlyEquals(result.totalTakerAssetAmount, fillableInput);
} else {
const slippedOutput = fillableOutput.times(orderSlippage + 1).integerValue();
const slippedOutput = fillableOutput.times(slippage + 1).integerValue();
assertRoughlyEquals(result.totalMakerAssetAmount, fillableInput);
assertRoughlyEquals(result.totalTakerAssetAmount, slippedOutput);
}
@ -958,7 +973,9 @@ describe('quote_simulation tests', async () => {
fillableInput,
fillableOutput,
side,
}).map(fo => slipOrder(fo.order, orderSlippage, side));
}).map(fo =>
slipOrder(fo.order as OptimizedMarketOrderBase<NativeLimitOrderFillData>, orderSlippage, side),
);
orders = [...orders.slice(1), orders[0]];
const bestCase = simulateBestCaseFill({
orders,
@ -972,7 +989,7 @@ describe('quote_simulation tests', async () => {
side,
fillAmount: fillableInput,
gasPrice: ONE,
opts: { gasSchedule: GAS_SCHEDULE },
opts: { gasSchedule: GAS_SCHEDULE, slippage: orderSlippage },
});
const bestPrice = bestCase.makerAssetAmount.div(bestCase.totalTakerAssetAmount);
const worstPrice = worstCase.makerAssetAmount.div(worstCase.totalTakerAssetAmount);

View File

@ -1,134 +0,0 @@
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
import { sortingUtils } from '../src/utils/sorting_utils';
import { chaiSetup } from './utils/chai_setup';
import { testOrderFactory } from './utils/test_order_factory';
chaiSetup.configure();
const expect = chai.expect;
const FAKE_ERC20_TAKER_ASSET_DATA = '0xf47261b02222222222222222222222222222222222222222222222222222222222222222';
const FAKE_ERC20_MAKER_ASSET_DATA = '0xf47261b01111111111111111111111111111111111111111111111111111111111111111';
describe('sortingUtils', () => {
describe('#sortOrders', () => {
// rate: 2 takerAsset / makerAsset
const testOrder1 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(200),
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
});
// rate: 1 takerAsset / makerAsset
const testOrder2 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(100),
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
});
// rate: 2.5 takerAsset / makerAsset
const testOrder3 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(250),
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
});
// rate: 2 takerAsset / makerAsset
const testOrderWithFeeInTakerAsset1 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(100),
takerFee: new BigNumber(100),
takerFeeAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
});
// rate: 1 takerAsset / makerAsset
const testOrderWithFeeInTakerAsset2 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(50),
takerFee: new BigNumber(50),
takerFeeAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
});
// rate: 2.5 takerAsset / makerAsset
const testOrderWithFeeInTakerAsset3 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(100),
takerAssetAmount: new BigNumber(200),
takerFee: new BigNumber(50),
takerFeeAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
});
// rate: 2 takerAsset / makerAsset
const testOrderWithFeeInMakerAsset1 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(200),
takerAssetAmount: new BigNumber(200),
takerFee: new BigNumber(100),
takerFeeAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
});
// rate: 1 takerAsset / makerAsset
const testOrderWithFeeInMakerAsset2 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(150),
takerAssetAmount: new BigNumber(100),
takerFee: new BigNumber(50),
takerFeeAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
});
// rate: 2.5 takerAsset / makerAsset
const testOrderWithFeeInMakerAsset3 = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: new BigNumber(150),
takerAssetAmount: new BigNumber(250),
takerFee: new BigNumber(50),
takerFeeAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
takerAssetData: FAKE_ERC20_TAKER_ASSET_DATA,
makerAssetData: FAKE_ERC20_MAKER_ASSET_DATA,
});
it('correctly sorts by fee adjusted rate (feeless orders)', async () => {
const orders = [testOrder1, testOrder2, testOrder3];
const sortedOrders = sortingUtils.sortOrders(orders);
expect(sortedOrders).to.deep.equal([testOrder2, testOrder1, testOrder3]);
});
it('correctly sorts by fee adjusted rate (takerAsset denominated fee orders)', async () => {
const orders = [
testOrderWithFeeInTakerAsset1,
testOrderWithFeeInTakerAsset2,
testOrderWithFeeInTakerAsset3,
];
const sortedOrders = sortingUtils.sortOrders(orders);
expect(sortedOrders).to.deep.equal([
testOrderWithFeeInTakerAsset2,
testOrderWithFeeInTakerAsset1,
testOrderWithFeeInTakerAsset3,
]);
});
it('correctly sorts by fee adjusted rate (makerAsset denominated fee orders)', async () => {
const orders = [
testOrderWithFeeInMakerAsset1,
testOrderWithFeeInMakerAsset2,
testOrderWithFeeInMakerAsset3,
];
const sortedOrders = sortingUtils.sortOrders(orders);
expect(sortedOrders).to.deep.equal([
testOrderWithFeeInMakerAsset2,
testOrderWithFeeInMakerAsset1,
testOrderWithFeeInMakerAsset3,
]);
});
it('correctly sorts by fee adjusted rate (mixed orders)', async () => {
const orders = [testOrderWithFeeInMakerAsset1, testOrderWithFeeInTakerAsset2, testOrder3];
const sortedOrders = sortingUtils.sortOrders(orders);
expect(sortedOrders).to.deep.equal([
testOrderWithFeeInTakerAsset2,
testOrderWithFeeInMakerAsset1,
testOrder3,
]);
});
});
});

View File

@ -1,234 +0,0 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { WETH9Contract } from '@0x/contract-wrappers';
import { constants as devConstants, OrderFactory } from '@0x/contracts-test-utils';
import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils';
import { migrateOnceAsync } from '@0x/migrations';
import { assetDataUtils } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
import { SwapQuote, SwapQuoteConsumer } from '../src';
import { constants } from '../src/constants';
import { ExtensionContractType, MarketOperation, SignedOrderWithFillableAmounts } from '../src/types';
import { chaiSetup } from './utils/chai_setup';
import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote';
import { provider, web3Wrapper } from './utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
const TESTRPC_CHAIN_ID = 1337;
const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE);
const PARTIAL_PRUNED_SIGNED_ORDERS: Array<Partial<SignedOrderWithFillableAmounts>> = [
{
takerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI),
},
{
takerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI),
},
{
takerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI),
},
];
const PARTIAL_LARGE_PRUNED_SIGNED_ORDERS: Array<Partial<SignedOrderWithFillableAmounts>> = [
{
takerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
},
{
takerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
},
{
takerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
makerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
fillableTakerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
fillableMakerAssetAmount: new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI),
},
];
describe('swapQuoteConsumerUtils', () => {
let wethContract: WETH9Contract;
let userAddresses: string[];
let makerAddress: string;
let takerAddress: string;
let makerTokenAddress: string;
let takerTokenAddress: string;
let makerAssetData: string;
let takerAssetData: string;
let wethAssetData: string;
let contractAddresses: ContractAddresses;
let swapQuoteConsumer: SwapQuoteConsumer;
let orderFactory: OrderFactory;
let forwarderOrderFactory: OrderFactory;
const chainId = TESTRPC_CHAIN_ID;
before(async () => {
contractAddresses = await migrateOnceAsync(provider);
await blockchainLifecycle.startAsync();
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
wethContract = new WETH9Contract(contractAddresses.etherToken, provider);
[takerAddress, makerAddress] = userAddresses;
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
[makerAssetData, takerAssetData, wethAssetData] = [
assetDataUtils.encodeERC20AssetData(makerTokenAddress),
assetDataUtils.encodeERC20AssetData(takerTokenAddress),
assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken),
];
const defaultOrderParams = {
...devConstants.STATIC_ORDER_PARAMS,
makerAddress,
takerAddress: constants.NULL_ADDRESS,
makerAssetData,
takerAssetData,
makerFeeAssetData: constants.NULL_ERC20_ASSET_DATA,
takerFeeAssetData: constants.NULL_ERC20_ASSET_DATA,
makerFee: constants.ZERO_AMOUNT,
takerFee: constants.ZERO_AMOUNT,
feeRecipientAddress: constants.NULL_ADDRESS,
exchangeAddress: contractAddresses.exchange,
chainId,
};
const defaultForwarderOrderParams = {
...defaultOrderParams,
...{
takerAssetData: wethAssetData,
},
};
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
forwarderOrderFactory = new OrderFactory(privateKey, defaultForwarderOrderParams);
swapQuoteConsumer = new SwapQuoteConsumer(provider, {
chainId,
contractAddresses,
});
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('getConsumerTypeForSwapQuoteAsync', () => {
let forwarderOrders: SignedOrderWithFillableAmounts[];
let exchangeOrders: SignedOrderWithFillableAmounts[];
let largeForwarderOrders: SignedOrderWithFillableAmounts[];
let forwarderSwapQuote: SwapQuote;
let exchangeSwapQuote: SwapQuote;
let largeForwarderSwapQuote: SwapQuote;
beforeEach(async () => {
exchangeOrders = [];
for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS) {
const order = await orderFactory.newSignedOrderAsync(partialOrder);
const prunedOrder = {
...order,
...partialOrder,
};
exchangeOrders.push(prunedOrder as SignedOrderWithFillableAmounts);
}
forwarderOrders = [];
for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS) {
const order = await forwarderOrderFactory.newSignedOrderAsync(partialOrder);
const prunedOrder = {
...order,
...partialOrder,
};
forwarderOrders.push(prunedOrder as SignedOrderWithFillableAmounts);
}
largeForwarderOrders = [];
for (const partialOrder of PARTIAL_LARGE_PRUNED_SIGNED_ORDERS) {
const order = await forwarderOrderFactory.newSignedOrderAsync(partialOrder);
const prunedOrder = {
...order,
...partialOrder,
};
largeForwarderOrders.push(prunedOrder as SignedOrderWithFillableAmounts);
}
forwarderSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
makerAssetData,
wethAssetData,
forwarderOrders,
MarketOperation.Sell,
GAS_PRICE,
);
largeForwarderSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
makerAssetData,
wethAssetData,
largeForwarderOrders,
MarketOperation.Sell,
GAS_PRICE,
);
exchangeSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
makerAssetData,
takerAssetData,
exchangeOrders,
MarketOperation.Sell,
GAS_PRICE,
);
});
it('should return exchange consumer if takerAsset is not wEth', async () => {
const extensionContractType = await swapQuoteConsumer.getOptimalExtensionContractTypeAsync(
exchangeSwapQuote,
{ takerAddress },
);
expect(extensionContractType).to.equal(ExtensionContractType.None);
});
it('should return forwarder consumer if takerAsset is wEth and have enough eth balance', async () => {
const extensionContractType = await swapQuoteConsumer.getOptimalExtensionContractTypeAsync(
forwarderSwapQuote,
{ takerAddress },
);
expect(extensionContractType).to.equal(ExtensionContractType.Forwarder);
});
it('should return exchange consumer if takerAsset is wEth and taker has enough weth', async () => {
const etherInWei = new BigNumber(20).multipliedBy(ONE_ETH_IN_WEI);
await wethContract.deposit().sendTransactionAsync({ value: etherInWei, from: takerAddress });
const extensionContractType = await swapQuoteConsumer.getOptimalExtensionContractTypeAsync(
forwarderSwapQuote,
{ takerAddress },
);
expect(extensionContractType).to.equal(ExtensionContractType.None);
});
it('should return forwarder consumer if takerAsset is wEth and takerAddress has no available balance in either weth or eth (defaulting behavior)', async () => {
const etherInWei = new BigNumber(50).multipliedBy(ONE_ETH_IN_WEI);
await wethContract.deposit().sendTransactionAsync({ value: etherInWei, from: takerAddress });
const extensionContractType = await swapQuoteConsumer.getOptimalExtensionContractTypeAsync(
largeForwarderSwapQuote,
{ takerAddress },
);
expect(extensionContractType).to.equal(ExtensionContractType.Forwarder);
});
});
});

View File

@ -1,277 +0,0 @@
import { Orderbook } from '@0x/orderbook';
import { Web3ProviderEngine } from '@0x/subproviders';
import { AssetPairsItem, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
import * as TypeMoq from 'typemoq';
import { SwapQuoter } from '../src';
import { constants } from '../src/constants';
import { LiquidityForTakerMakerAssetDataPair, SignedOrderWithFillableAmounts } from '../src/types';
import { chaiSetup } from './utils/chai_setup';
import { mockAvailableAssetDatas, mockedSwapQuoterWithFillableAmounts, orderbookMock } from './utils/mocks';
import { testOrderFactory } from './utils/test_order_factory';
import { baseUnitAmount } from './utils/utils';
chaiSetup.configure();
const expect = chai.expect;
const FAKE_SRA_URL = 'https://fakeurl.com';
const FAKE_TAKER_ASSET_DATA = '0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48';
const FAKE_MAKER_ASSET_DATA = '0xf47261b00000000000000000000000009f5B0C7e1623793bF0620569b9749e79DF6D0bC5';
const TOKEN_DECIMALS = 18;
const DAI_ASSET_DATA = '0xf47261b000000000000000000000000089d24a6b4ccb1b6faa2625fe562bdd9a23260359"';
const WETH_ASSET_DATA = '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
const WETH_DECIMALS = constants.ETHER_TOKEN_DECIMALS;
const ZERO = new BigNumber(0);
const assetsToAssetPairItems = (makerAssetData: string, takerAssetData: string): AssetPairsItem[] => {
const defaultAssetPairItem = {
minAmount: ZERO,
maxAmount: ZERO,
precision: TOKEN_DECIMALS,
};
return [
{
assetDataA: {
...defaultAssetPairItem,
assetData: makerAssetData,
},
assetDataB: {
...defaultAssetPairItem,
assetData: takerAssetData,
},
},
{
assetDataA: {
...defaultAssetPairItem,
assetData: takerAssetData,
},
assetDataB: {
...defaultAssetPairItem,
assetData: makerAssetData,
},
},
];
};
const expectLiquidityResult = async (
web3Provider: Web3ProviderEngine,
orderbook: Orderbook,
orders: SignedOrderWithFillableAmounts[],
expectedLiquidityResult: LiquidityForTakerMakerAssetDataPair,
) => {
const mockedSwapQuoter = mockedSwapQuoterWithFillableAmounts(
web3Provider,
orderbook,
FAKE_MAKER_ASSET_DATA,
WETH_ASSET_DATA,
orders,
);
const liquidityResult = await mockedSwapQuoter.object.getLiquidityForMakerTakerAssetDataPairAsync(
FAKE_MAKER_ASSET_DATA,
WETH_ASSET_DATA,
);
expect(liquidityResult).to.deep.equal(expectedLiquidityResult);
};
// tslint:disable:custom-no-magic-numbers
describe('SwapQuoter', () => {
describe('getLiquidityForMakerTakerAssetDataPairAsync', () => {
const mockWeb3Provider = TypeMoq.Mock.ofType(Web3ProviderEngine);
const mockOrderbook = orderbookMock();
beforeEach(() => {
mockWeb3Provider.reset();
mockOrderbook.reset();
});
afterEach(() => {
mockWeb3Provider.verifyAll();
mockOrderbook.verifyAll();
});
describe('validation', () => {
it('should ensure takerAssetData is a string', async () => {
const swapQuoter = SwapQuoter.getSwapQuoterForStandardRelayerAPIUrl(
mockWeb3Provider.object,
FAKE_SRA_URL,
);
expect(
swapQuoter.getLiquidityForMakerTakerAssetDataPairAsync(FAKE_MAKER_ASSET_DATA, false as any),
).to.be.rejectedWith('Expected takerAssetData to be of type string, encountered: false');
});
it('should ensure makerAssetData is a string', async () => {
const swapQuoter = SwapQuoter.getSwapQuoterForStandardRelayerAPIUrl(
mockWeb3Provider.object,
FAKE_SRA_URL,
);
expect(
swapQuoter.getLiquidityForMakerTakerAssetDataPairAsync(false as any, FAKE_TAKER_ASSET_DATA),
).to.be.rejectedWith('Expected makerAssetData to be of type string, encountered: false');
});
});
describe('asset pair not supported', () => {
it('should return 0s when no asset pair are supported', async () => {
mockAvailableAssetDatas(mockOrderbook, []);
const swapQuoter = new SwapQuoter(mockWeb3Provider.object, mockOrderbook.object);
const liquidityResult = await swapQuoter.getLiquidityForMakerTakerAssetDataPairAsync(
FAKE_MAKER_ASSET_DATA,
FAKE_TAKER_ASSET_DATA,
);
expect(liquidityResult).to.deep.equal({
makerAssetAvailableInBaseUnits: new BigNumber(0),
takerAssetAvailableInBaseUnits: new BigNumber(0),
});
});
it('should return 0s when only other asset pair supported', async () => {
mockAvailableAssetDatas(mockOrderbook, assetsToAssetPairItems(FAKE_MAKER_ASSET_DATA, DAI_ASSET_DATA));
const swapQuoter = new SwapQuoter(mockWeb3Provider.object, mockOrderbook.object);
const liquidityResult = await swapQuoter.getLiquidityForMakerTakerAssetDataPairAsync(
FAKE_MAKER_ASSET_DATA,
FAKE_TAKER_ASSET_DATA,
);
expect(liquidityResult).to.deep.equal({
makerAssetAvailableInBaseUnits: new BigNumber(0),
takerAssetAvailableInBaseUnits: new BigNumber(0),
});
});
});
describe('assetData is supported', () => {
// orders
const sellTenTokensFor10Weth: SignedOrder = testOrderFactory.generateTestSignedOrder({
makerAssetAmount: baseUnitAmount(10),
takerAssetAmount: baseUnitAmount(10, WETH_DECIMALS),
chainId: 42,
});
beforeEach(() => {
mockAvailableAssetDatas(mockOrderbook, assetsToAssetPairItems(WETH_ASSET_DATA, FAKE_MAKER_ASSET_DATA));
});
it('should return 0s when no orders available', async () => {
const orders: SignedOrderWithFillableAmounts[] = [];
const expectedResult = {
makerAssetAvailableInBaseUnits: new BigNumber(0),
takerAssetAvailableInBaseUnits: new BigNumber(0),
};
await expectLiquidityResult(mockWeb3Provider.object, mockOrderbook.object, orders, expectedResult);
});
it('should return correct computed value when orders provided with full fillableAmounts', async () => {
const orders: SignedOrderWithFillableAmounts[] = [
{
...sellTenTokensFor10Weth,
...{
fillableMakerAssetAmount: sellTenTokensFor10Weth.makerAssetAmount,
fillableTakerAssetAmount: sellTenTokensFor10Weth.takerAssetAmount,
fillableTakerFeeAmount: constants.ZERO_AMOUNT,
},
},
{
...sellTenTokensFor10Weth,
...{
fillableMakerAssetAmount: sellTenTokensFor10Weth.makerAssetAmount,
fillableTakerAssetAmount: sellTenTokensFor10Weth.takerAssetAmount,
fillableTakerFeeAmount: constants.ZERO_AMOUNT,
},
},
];
const expectedMakerAssetAvailable = orders[0].makerAssetAmount.plus(orders[1].makerAssetAmount);
const expectedTakerAssetAvailable = orders[0].takerAssetAmount.plus(orders[1].takerAssetAmount);
const expectedResult = {
makerAssetAvailableInBaseUnits: expectedMakerAssetAvailable,
takerAssetAvailableInBaseUnits: expectedTakerAssetAvailable,
};
await expectLiquidityResult(mockWeb3Provider.object, mockOrderbook.object, orders, expectedResult);
});
it('should return correct computed value with one partial fillableAmounts', async () => {
const orders: SignedOrderWithFillableAmounts[] = [
{
...sellTenTokensFor10Weth,
...{
fillableMakerAssetAmount: baseUnitAmount(1),
fillableTakerAssetAmount: baseUnitAmount(0.5, WETH_DECIMALS),
fillableTakerFeeAmount: constants.ZERO_AMOUNT,
},
},
];
const expectedResult = {
makerAssetAvailableInBaseUnits: baseUnitAmount(1),
takerAssetAvailableInBaseUnits: baseUnitAmount(0.5, WETH_DECIMALS),
};
await expectLiquidityResult(mockWeb3Provider.object, mockOrderbook.object, orders, expectedResult);
});
it('should return correct computed value with multiple orders and fillable amounts', async () => {
const orders: SignedOrderWithFillableAmounts[] = [
{
...sellTenTokensFor10Weth,
...{
fillableMakerAssetAmount: baseUnitAmount(1),
fillableTakerAssetAmount: baseUnitAmount(0.5, WETH_DECIMALS),
fillableTakerFeeAmount: constants.ZERO_AMOUNT,
},
},
{
...sellTenTokensFor10Weth,
...{
fillableMakerAssetAmount: baseUnitAmount(3),
fillableTakerAssetAmount: baseUnitAmount(3, WETH_DECIMALS),
fillableTakerFeeAmount: constants.ZERO_AMOUNT,
},
},
];
const expectedResult = {
makerAssetAvailableInBaseUnits: baseUnitAmount(4),
takerAssetAvailableInBaseUnits: baseUnitAmount(3.5, WETH_DECIMALS),
};
await expectLiquidityResult(mockWeb3Provider.object, mockOrderbook.object, orders, expectedResult);
});
it('should return 0s when no amounts fillable', async () => {
const orders: SignedOrderWithFillableAmounts[] = [
{
...sellTenTokensFor10Weth,
...{
fillableMakerAssetAmount: constants.ZERO_AMOUNT,
fillableTakerAssetAmount: constants.ZERO_AMOUNT,
fillableTakerFeeAmount: constants.ZERO_AMOUNT,
},
},
{
...sellTenTokensFor10Weth,
...{
fillableMakerAssetAmount: constants.ZERO_AMOUNT,
fillableTakerAssetAmount: constants.ZERO_AMOUNT,
fillableTakerFeeAmount: constants.ZERO_AMOUNT,
},
},
];
const expectedResult = {
makerAssetAvailableInBaseUnits: constants.ZERO_AMOUNT,
takerAssetAvailableInBaseUnits: constants.ZERO_AMOUNT,
};
await expectLiquidityResult(mockWeb3Provider.object, mockOrderbook.object, orders, expectedResult);
});
});
});
});

View File

@ -1,8 +1,11 @@
import { BalancerPool, BalancerPoolsCache } from '../../src/utils/market_operation_utils/balancer_utils';
import { Pool } from '@balancer-labs/sor/dist/types';
import { BalancerPoolsCache } from '../../src/utils/market_operation_utils/balancer_utils';
export interface Handlers {
getPoolsForPairAsync: (takerToken: string, makerToken: string) => Promise<BalancerPool[]>;
_fetchPoolsForPairAsync: (takerToken: string, makerToken: string) => Promise<BalancerPool[]>;
getPoolsForPairAsync: (takerToken: string, makerToken: string) => Promise<Pool[]>;
_fetchPoolsForPairAsync: (takerToken: string, makerToken: string) => Promise<Pool[]>;
_loadTopPoolsAsync: () => Promise<void>;
}
export class MockBalancerPoolsCache extends BalancerPoolsCache {
@ -10,15 +13,21 @@ export class MockBalancerPoolsCache extends BalancerPoolsCache {
super();
}
public async getPoolsForPairAsync(takerToken: string, makerToken: string): Promise<BalancerPool[]> {
public async getPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]> {
return this.handlers.getPoolsForPairAsync
? this.handlers.getPoolsForPairAsync(takerToken, makerToken)
: super.getPoolsForPairAsync(takerToken, makerToken);
}
protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<BalancerPool[]> {
protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]> {
return this.handlers._fetchPoolsForPairAsync
? this.handlers._fetchPoolsForPairAsync(takerToken, makerToken)
: super._fetchPoolsForPairAsync(takerToken, makerToken);
}
protected async _loadTopPoolsAsync(): Promise<void> {
if (this.handlers && this.handlers._loadTopPoolsAsync) {
return this.handlers._loadTopPoolsAsync();
}
}
}

View File

@ -1,6 +1,6 @@
import { ContractTxFunctionObj } from '@0x/base-contract';
import { constants } from '@0x/contracts-test-utils';
import { Order } from '@0x/types';
import { LimitOrderFields, Signature } from '@0x/protocol-utils';
import { BigNumber, hexUtils } from '@0x/utils';
import { SamplerCallResult } from '../../src/types';
@ -8,8 +8,8 @@ import { ERC20BridgeSamplerContract } from '../../src/wrappers';
export type GetOrderFillableAssetAmountResult = BigNumber[];
export type GetOrderFillableAssetAmountHandler = (
orders: Order[],
signatures: string[],
orders: LimitOrderFields[],
signatures: Signature[],
devUtilsAddress: string,
) => GetOrderFillableAssetAmountResult;
@ -36,6 +36,7 @@ export type SampleBuysKyberHandler = (
makerToken: string,
makerTokenAmounts: BigNumber[],
) => [string, SampleResults];
export type SampleUniswapV2Handler = (router: string, path: string[], assetAmounts: BigNumber[]) => SampleResults;
export type SampleBuysMultihopHandler = (path: string[], takerTokenAmounts: BigNumber[]) => SampleResults;
export type SampleSellsLPHandler = (
providerAddress: string,
@ -52,16 +53,16 @@ const DUMMY_PROVIDER = {
};
interface Handlers {
getOrderFillableMakerAssetAmounts: GetOrderFillableAssetAmountHandler;
getOrderFillableTakerAssetAmounts: GetOrderFillableAssetAmountHandler;
getLimitOrderFillableMakerAssetAmounts: GetOrderFillableAssetAmountHandler;
getLimitOrderFillableTakerAssetAmounts: GetOrderFillableAssetAmountHandler;
sampleSellsFromKyberNetwork: SampleSellsKyberHandler;
sampleSellsFromLiquidityProvider: SampleSellsLPHandler;
sampleSellsFromEth2Dai: SampleSellsHandler;
sampleSellsFromUniswap: SampleSellsHandler;
sampleSellsFromUniswapV2: SampleSellsMultihopHandler;
sampleSellsFromUniswapV2: SampleUniswapV2Handler;
sampleBuysFromEth2Dai: SampleBuysHandler;
sampleBuysFromUniswap: SampleBuysHandler;
sampleBuysFromUniswapV2: SampleBuysMultihopHandler;
sampleBuysFromUniswapV2: SampleUniswapV2Handler;
sampleBuysFromLiquidityProvider: SampleSellsLPHandler;
}
@ -83,26 +84,26 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
};
}
public getOrderFillableMakerAssetAmounts(
orders: Order[],
signatures: string[],
public getLimitOrderFillableMakerAssetAmounts(
orders: LimitOrderFields[],
signatures: Signature[],
): ContractTxFunctionObj<GetOrderFillableAssetAmountResult> {
return this._wrapCall(
super.getOrderFillableMakerAssetAmounts,
this._handlers.getOrderFillableMakerAssetAmounts,
super.getLimitOrderFillableMakerAssetAmounts,
this._handlers.getLimitOrderFillableMakerAssetAmounts,
orders,
signatures,
constants.NULL_ADDRESS,
);
}
public getOrderFillableTakerAssetAmounts(
orders: Order[],
signatures: string[],
public getLimitOrderFillableTakerAssetAmounts(
orders: LimitOrderFields[],
signatures: Signature[],
): ContractTxFunctionObj<GetOrderFillableAssetAmountResult> {
return this._wrapCall(
super.getOrderFillableTakerAssetAmounts,
this._handlers.getOrderFillableTakerAssetAmounts,
super.getLimitOrderFillableTakerAssetAmounts,
this._handlers.getLimitOrderFillableTakerAssetAmounts,
orders,
signatures,
constants.NULL_ADDRESS,
@ -154,12 +155,14 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
}
public sampleSellsFromUniswapV2(
router: string,
path: string[],
takerAssetAmounts: BigNumber[],
): ContractTxFunctionObj<BigNumber[]> {
return this._wrapCall(
super.sampleSellsFromUniswapV2,
this._handlers.sampleSellsFromUniswapV2,
router,
path,
takerAssetAmounts,
);
@ -209,10 +212,15 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
);
}
public sampleBuysFromUniswapV2(path: string[], makerAssetAmounts: BigNumber[]): ContractTxFunctionObj<BigNumber[]> {
public sampleBuysFromUniswapV2(
router: string,
path: string[],
makerAssetAmounts: BigNumber[],
): ContractTxFunctionObj<BigNumber[]> {
return this._wrapCall(
super.sampleBuysFromUniswapV2,
this._handlers.sampleBuysFromUniswapV2,
router,
path,
makerAssetAmounts,
);

View File

@ -1,98 +0,0 @@
import { constants as devConstants } from '@0x/contracts-test-utils';
import { AcceptedRejectedOrders, Orderbook } from '@0x/orderbook';
import { Web3ProviderEngine } from '@0x/subproviders';
import { APIOrder, AssetPairsItem, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as TypeMoq from 'typemoq';
import { SwapQuoter } from '../../src/swap_quoter';
import { SignedOrderWithFillableAmounts } from '../../src/types';
import { ProtocolFeeUtils } from '../../src/utils/protocol_fee_utils';
// tslint:disable: max-classes-per-file
class OrderbookClass extends Orderbook {
// tslint:disable-next-line:prefer-function-over-method
public async getOrdersAsync(_makerAssetData: string, _takerAssetData: string): Promise<APIOrder[]> {
return [];
}
// tslint:disable-next-line:prefer-function-over-method
public async getAvailableAssetDatasAsync(): Promise<AssetPairsItem[]> {
return [];
}
// tslint:disable-next-line:prefer-function-over-method
public async addOrdersAsync(_orders: SignedOrder[]): Promise<AcceptedRejectedOrders> {
return { accepted: [], rejected: [] };
}
}
export const orderbookMock = () => {
return TypeMoq.Mock.ofType(OrderbookClass, TypeMoq.MockBehavior.Strict);
};
export const mockAvailableAssetDatas = (
mockOrderbook: TypeMoq.IMock<OrderbookClass>,
availableAssetDatas: AssetPairsItem[],
) => {
mockOrderbook
.setup(async op => op.getAvailableAssetDatasAsync())
.returns(async () => availableAssetDatas)
.verifiable(TypeMoq.Times.once());
mockOrderbook
.setup(o => (o as any)._orderProvider)
.returns(() => undefined)
.verifiable(TypeMoq.Times.atLeast(0));
mockOrderbook
.setup(o => (o as any)._orderStore)
.returns(() => undefined)
.verifiable(TypeMoq.Times.atLeast(0));
};
const partiallyMockedSwapQuoter = (provider: Web3ProviderEngine, orderbook: Orderbook): TypeMoq.IMock<SwapQuoter> => {
const rawSwapQuoter = new SwapQuoter(provider, orderbook);
const mockedSwapQuoter = TypeMoq.Mock.ofInstance(rawSwapQuoter, TypeMoq.MockBehavior.Loose, false);
mockedSwapQuoter.callBase = true;
return mockedSwapQuoter;
};
class ProtocolFeeUtilsClass {
public static getInstance(..._args: any[]): any {
return {
getGasPriceEstimationOrThrowAsync: async () =>
Promise.resolve(new BigNumber(devConstants.DEFAULT_GAS_PRICE)),
};
}
// tslint:disable-next-line:prefer-function-over-method
public async getGasPriceEstimationOrThrowAsync(_shouldHardRefresh?: boolean): Promise<BigNumber> {
return new BigNumber(devConstants.DEFAULT_GAS_PRICE);
}
}
export const protocolFeeUtilsMock = (): TypeMoq.IMock<ProtocolFeeUtils> => {
const mockProtocolFeeUtils = TypeMoq.Mock.ofType(ProtocolFeeUtilsClass, TypeMoq.MockBehavior.Loose);
mockProtocolFeeUtils.callBase = true;
return mockProtocolFeeUtils as any;
};
const mockGetSignedOrdersWithFillableAmountsAsyncAsync = (
mockedSwapQuoter: TypeMoq.IMock<SwapQuoter>,
makerAssetData: string,
takerAssetData: string,
signedOrders: SignedOrderWithFillableAmounts[],
): void => {
mockedSwapQuoter
.setup(async a => a.getSignedOrdersWithFillableAmountsAsync(makerAssetData, takerAssetData))
.returns(async () => signedOrders)
.verifiable(TypeMoq.Times.once());
};
export const mockedSwapQuoterWithFillableAmounts = (
provider: Web3ProviderEngine,
orderbook: Orderbook,
makerAssetData: string,
takerAssetData: string,
signedOrders: SignedOrderWithFillableAmounts[],
): TypeMoq.IMock<SwapQuoter> => {
const mockedAssetQuoter = partiallyMockedSwapQuoter(provider, orderbook);
mockGetSignedOrdersWithFillableAmountsAsyncAsync(mockedAssetQuoter, makerAssetData, takerAssetData, signedOrders);
return mockedAssetQuoter;
};

View File

@ -1,27 +1,27 @@
import { BigNumber } from '@0x/utils';
import { ERC20BridgeSource } from '../../src';
import { ERC20BridgeSource, OptimizedMarketOrder } from '../../src';
import { constants } from '../../src/constants';
import { MarketOperation, SignedOrderWithFillableAmounts, SwapQuote } from '../../src/types';
import { MarketOperation, SwapQuote, SwapQuoteBase } from '../../src/types';
/**
* Creates a swap quote given orders.
*/
export async function getFullyFillableSwapQuoteWithNoFeesAsync(
makerAssetData: string,
takerAssetData: string,
orders: SignedOrderWithFillableAmounts[],
makerToken: string,
takerToken: string,
orders: OptimizedMarketOrder[],
operation: MarketOperation,
gasPrice: BigNumber,
): Promise<SwapQuote> {
const makerAssetFillAmount = BigNumber.sum(...[0, ...orders.map(o => o.makerAssetAmount)]);
const totalTakerAssetAmount = BigNumber.sum(...[0, ...orders.map(o => o.takerAssetAmount)]);
const makerAmount = BigNumber.sum(...[0, ...orders.map(o => o.makerAmount)]);
const takerAmount = BigNumber.sum(...[0, ...orders.map(o => o.takerAmount)]);
const protocolFeePerOrder = constants.PROTOCOL_FEE_MULTIPLIER.times(gasPrice);
const quoteInfo = {
makerAssetAmount: makerAssetFillAmount,
feeTakerAssetAmount: constants.ZERO_AMOUNT,
takerAssetAmount: totalTakerAssetAmount,
totalTakerAssetAmount,
makerAmount,
feeTakerTokenAmount: constants.ZERO_AMOUNT,
takerAmount,
totalTakerAmount: takerAmount,
protocolFeeInWeiAmount: protocolFeePerOrder.times(orders.length),
gas: 200e3,
};
@ -30,36 +30,32 @@ export async function getFullyFillableSwapQuoteWithNoFeesAsync(
[ERC20BridgeSource.Native]: new BigNumber(1),
};
const quoteBase = {
makerAssetData,
takerAssetData,
const quoteBase: SwapQuoteBase = {
makerToken,
takerToken,
orders: orders.map(order => ({ ...order, fills: [] })),
gasPrice,
bestCaseQuoteInfo: quoteInfo,
worstCaseQuoteInfo: quoteInfo,
unoptimizedQuoteInfo: quoteInfo,
unoptimizedOrders: orders.map(order => ({ ...order, fills: [] })),
sourceBreakdown: breakdown,
isTwoHop: false,
takerAssetToEthRate: constants.ZERO_AMOUNT,
makerAssetToEthRate: constants.ZERO_AMOUNT,
takerTokenToEthRate: constants.ZERO_AMOUNT,
makerTokenToEthRate: constants.ZERO_AMOUNT,
makerTokenDecimals: 18,
takerTokenDecimals: 18,
};
if (operation === MarketOperation.Buy) {
return {
...quoteBase,
type: MarketOperation.Buy,
makerAssetFillAmount,
makerTokenDecimals: 18,
takerTokenDecimals: 18,
makerTokenFillAmount: makerAmount,
};
} else {
return {
...quoteBase,
type: MarketOperation.Sell,
takerAssetFillAmount: totalTakerAssetAmount,
makerTokenDecimals: 18,
takerTokenDecimals: 18,
takerTokenFillAmount: takerAmount,
};
}
}

View File

@ -1,6 +1,14 @@
import { BigNumber } from '@0x/utils';
import axios, { AxiosInstance } from 'axios';
import AxiosMockAdapter from 'axios-mock-adapter';
import { InsufficientAssetLiquidityError } from '../../src/errors';
import { MockedRfqtQuoteResponse } from '../../src/types';
export enum RfqtQuoteEndpoint {
Indicative = 'price',
Firm = 'quote',
}
export const testHelpers = {
expectInsufficientLiquidityErrorAsync: async (
@ -23,4 +31,31 @@ export const testHelpers = {
expect(wasErrorThrown).to.be.true();
},
/**
* A helper utility for testing which mocks out
* requests to RFQ-t providers
*/
withMockedRfqtQuotes: async (
mockedResponses: MockedRfqtQuoteResponse[],
quoteType: RfqtQuoteEndpoint,
afterResponseCallback: () => Promise<void>,
axiosClient: AxiosInstance = axios,
): Promise<void> => {
const mockedAxios = new AxiosMockAdapter(axiosClient);
try {
// Mock out RFQT responses
for (const mockedResponse of mockedResponses) {
const { endpoint, requestApiKey, requestParams, responseData, responseCode } = mockedResponse;
const requestHeaders = { Accept: 'application/json, text/plain, */*', '0x-api-key': requestApiKey };
mockedAxios
.onGet(`${endpoint}/${quoteType}`, { params: requestParams }, requestHeaders)
.replyOnce(responseCode, responseData);
}
// Perform the callback function, e.g. a test validation
await afterResponseCallback();
} finally {
// Ensure we always restore axios afterwards
mockedAxios.restore();
}
},
};

Some files were not shown because too many files have changed in this diff Show More