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 { 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 { 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 { 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, ); } }); } } }); });