* `@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,
 | 
						|
                        );
 | 
						|
                    }
 | 
						|
                });
 | 
						|
            }
 | 
						|
        }
 | 
						|
    });
 | 
						|
});
 |