* `@0x/contracts-erc20-bridge-sampler`: Add gas limits to external quote calls. `@0x/contract-addresses`: Point `erc20BridgeSampler` to new version. * `@0x/contracts-utils`: Add kovan addresses to `DeploymentConstants`. `@0x/contract-addresses`: Add kovan `ERC20BridgeSampler` address. * `@0x/contracts-erc20-bridge-sampler`: Fix changelog. * `@0x/asset-swapper`: Ignore zero sample results from the sampler contract. `@0x/asset-swapper`: Allow skipping Uniswap when dealing with low precision amounts with `minUniswapDecimals` option. `@0x/asset-swapper`: Increase default `runLimit` from `1024` to `4096`. `@0x/asset-swapper`: Increase default `numSamples` from `8` to `10` `@0x/asset-swapper`: Fix ordering of optimized orders. `@0x/asset-swapper`: Fix best and worst quotes being reversed sometimes. `@0x/asset-swapper`: Fix rounding of quoted asset amounts. * `@0x/asset-swapper`: Change default `minUniswapDecimals` option from 8 to 7. * `@0x/asset-swapper`: Revert uniswap decimals fix. * `@0x/contracts-test-utils`: Add `blockchainTests.live()` for live network tests. `@0x/contracts-test-utils`: Add modifiers to `blockchainTests.fork()`. `@0x/contracts-integrations`: Add aggregator mainnet tests. * `@0x/contracts-integrations`: Fix `fork/resets` modifier ordering on dydx tests. `@0x/contracts-integrations`: Move and tweak aggregation tests. * `@0x/contracts-integrations`: Handle non-responsive third-party SRA ordebooks with a little more grace. * `@0x/contracts-integrations`: Fix linter error. * `@0x/contracts-test-utils`: Consolidate fork provider logic into `mocha_blockchain.ts`. * `@0x/contracts-integrations`: Run prettier on aggregation fill tests. * `@0x/dev-utils`: Add `locked` to `Web3Config`. * `@0x/contracts-integrations`: Update mainnet fork tests. `@0x/contracts-test-utils`: Fix forked tests being skipped. `@0x/contracts-erc20-bridge-sampler`: Regenerate artifacts. * `@0x/contracts-test-utils`: Remove unecessary `locked` option when creating forked ganache provider. * Fix redundant zero check * Set fee amount in fillable amounts test Co-authored-by: Jacob Evans <dekz@dekz.net>
240 lines
11 KiB
TypeScript
240 lines
11 KiB
TypeScript
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,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
});
|