From 44262bf747f977c380d01e970467b8c8404e722c Mon Sep 17 00:00:00 2001 From: Michael Zhu Date: Thu, 11 Jun 2020 11:10:34 -0700 Subject: [PATCH] MultiBridge support in AssetSwapper --- contracts/erc20-bridge-sampler/CHANGELOG.json | 4 ++ .../contracts/src/ERC20BridgeSampler.sol | 53 ++++++++++++++++- .../contracts/src/IERC20BridgeSampler.sol | 20 +++++++ .../contracts/src/IMultiBridge.sol | 58 +++++++++++++++++++ contracts/erc20-bridge-sampler/package.json | 2 +- .../erc20-bridge-sampler/test/artifacts.ts | 2 + .../erc20-bridge-sampler/test/wrappers.ts | 1 + contracts/erc20-bridge-sampler/tsconfig.json | 1 + packages/asset-swapper/CHANGELOG.json | 4 ++ packages/asset-swapper/src/types.ts | 1 + .../src/utils/market_operation_utils/fills.ts | 16 +++-- .../src/utils/market_operation_utils/index.ts | 44 ++++++++------ .../multibridge_utils.ts | 20 +++++++ .../utils/market_operation_utils/orders.ts | 23 ++++++++ .../sampler_operations.ts | 54 ++++++++++++++--- .../src/utils/market_operation_utils/types.ts | 3 + .../asset-swapper/test/dex_sampler_test.ts | 42 ++++++++++++++ .../test/market_operation_utils_test.ts | 7 ++- .../test/utils/mock_sampler_contract.ts | 26 +++++++++ packages/contract-addresses/addresses.json | 15 +++-- packages/contract-addresses/src/index.ts | 1 + packages/migrations/src/migration.ts | 1 + 22 files changed, 358 insertions(+), 40 deletions(-) create mode 100644 contracts/erc20-bridge-sampler/contracts/src/IMultiBridge.sol create mode 100644 packages/asset-swapper/src/utils/market_operation_utils/multibridge_utils.ts diff --git a/contracts/erc20-bridge-sampler/CHANGELOG.json b/contracts/erc20-bridge-sampler/CHANGELOG.json index cbaabc5d2b..f4c7de28d6 100644 --- a/contracts/erc20-bridge-sampler/CHANGELOG.json +++ b/contracts/erc20-bridge-sampler/CHANGELOG.json @@ -21,6 +21,10 @@ { "note": "Add UniswapV2", "pr": 2595 + }, + { + "note": "Sample from MultiBridge", + "pr": 2593 } ] }, diff --git a/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol b/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol index ff0e21a015..42d9148e75 100644 --- a/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol +++ b/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol @@ -35,6 +35,7 @@ import "./ICurve.sol"; import "./ILiquidityProvider.sol"; import "./ILiquidityProviderRegistry.sol"; import "./IUniswapV2Router01.sol"; +import "./IMultiBridge.sol"; contract ERC20BridgeSampler is @@ -55,7 +56,7 @@ contract ERC20BridgeSampler is /// So a reasonable ceil is 150k per token. Biggest Curve has 4 tokens. uint256 constant internal CURVE_CALL_GAS = 600e3; // 600k /// @dev Default gas limit for liquidity provider calls. - uint256 constant internal DEFAULT_CALL_GAS = 200e3; // 200k + uint256 constant internal DEFAULT_CALL_GAS = 400e3; // 400k /// @dev The Kyber Uniswap Reserve address address constant internal KYBER_UNIWAP_RESERVE = 0x31E085Afd48a1d6e51Cc193153d625e8f0514C7F; /// @dev The Kyber Eth2Dai Reserve address @@ -593,6 +594,56 @@ contract ERC20BridgeSampler is } } + /// @dev Sample sell quotes from MultiBridge. + /// @param multibridge Address of the MultiBridge contract. + /// @param takerToken Address of the taker token (what to sell). + /// @param intermediateToken The address of the intermediate token to + /// use in an indirect route. + /// @param makerToken Address of the maker token (what to buy). + /// @param takerTokenAmounts Taker token sell amount for each sample. + /// @return makerTokenAmounts Maker amounts bought at each taker token + /// amount. + function sampleSellsFromMultiBridge( + address multibridge, + address takerToken, + address intermediateToken, + address makerToken, + uint256[] memory takerTokenAmounts + ) + public + view + returns (uint256[] memory makerTokenAmounts) + { + // Initialize array of maker token amounts. + uint256 numSamples = takerTokenAmounts.length; + makerTokenAmounts = new uint256[](numSamples); + + // If no address provided, return all zeros. + if (multibridge == address(0)) { + return makerTokenAmounts; + } + + for (uint256 i = 0; i < numSamples; i++) { + (bool didSucceed, bytes memory resultData) = + multibridge.staticcall.gas(DEFAULT_CALL_GAS)( + abi.encodeWithSelector( + IMultiBridge(0).getSellQuote.selector, + takerToken, + intermediateToken, + makerToken, + takerTokenAmounts[i] + )); + uint256 buyAmount = 0; + if (didSucceed) { + buyAmount = abi.decode(resultData, (uint256)); + } else { + // Exit early if the amount is too high for the liquidity provider to serve + break; + } + makerTokenAmounts[i] = buyAmount; + } + } + /// @dev Sample buy quotes from an arbitrary on-chain liquidity provider. /// @param registryAddress Address of the liquidity provider registry contract. /// @param takerToken Address of the taker token (what to sell). diff --git a/contracts/erc20-bridge-sampler/contracts/src/IERC20BridgeSampler.sol b/contracts/erc20-bridge-sampler/contracts/src/IERC20BridgeSampler.sol index 521ca45cc2..546a5c2563 100644 --- a/contracts/erc20-bridge-sampler/contracts/src/IERC20BridgeSampler.sol +++ b/contracts/erc20-bridge-sampler/contracts/src/IERC20BridgeSampler.sol @@ -206,6 +206,26 @@ interface IERC20BridgeSampler { view returns (uint256[] memory makerTokenAmounts); + /// @dev Sample sell quotes from MultiBridge. + /// @param multibridge Address of the MultiBridge contract. + /// @param takerToken Address of the taker token (what to sell). + /// @param intermediateToken The address of the intermediate token to + /// use in an indirect route. + /// @param makerToken Address of the maker token (what to buy). + /// @param takerTokenAmounts Taker token sell amount for each sample. + /// @return makerTokenAmounts Maker amounts bought at each taker token + /// amount. + function sampleSellsFromMultiBridge( + address multibridge, + address takerToken, + address intermediateToken, + address makerToken, + uint256[] calldata takerTokenAmounts + ) + external + view + returns (uint256[] memory makerTokenAmounts); + /// @dev Sample buy quotes from an arbitrary on-chain liquidity provider. /// @param registryAddress Address of the liquidity provider registry contract. /// @param takerToken Address of the taker token (what to sell). diff --git a/contracts/erc20-bridge-sampler/contracts/src/IMultiBridge.sol b/contracts/erc20-bridge-sampler/contracts/src/IMultiBridge.sol new file mode 100644 index 0000000000..9c3ad44217 --- /dev/null +++ b/contracts/erc20-bridge-sampler/contracts/src/IMultiBridge.sol @@ -0,0 +1,58 @@ +/* + + 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.5.9; + + +interface IMultiBridge { + + /// @dev Transfers `amount` of the ERC20 `tokenAddress` from `from` to `to`. + /// @param tokenAddress The address of the ERC20 token to transfer. + /// @param from Address to transfer asset from. + /// @param to Address to transfer asset to. + /// @param amount Amount of asset to transfer. + /// @param bridgeData Arbitrary asset data needed by the bridge contract. + /// @return success The magic bytes `0xdc1600f3` if successful. + function bridgeTransferFrom( + address tokenAddress, + address from, + address to, + uint256 amount, + bytes calldata bridgeData + ) + external + returns (bytes4 success); + + /// @dev Quotes the amount of `makerToken` that would be obtained by + /// selling `sellAmount` of `takerToken`. + /// @param takerToken Address of the taker token (what to sell). + /// @param intermediateToken The address of the intermediate token to + /// use in an indirect route. + /// @param makerToken Address of the maker token (what to buy). + /// @param sellAmount Amount of `takerToken` to sell. + /// @return makerTokenAmount Amount of `makerToken` that would be obtained. + function getSellQuote( + address takerToken, + address intermediateToken, + address makerToken, + uint256 sellAmount + ) + external + view + returns (uint256 makerTokenAmount); +} diff --git a/contracts/erc20-bridge-sampler/package.json b/contracts/erc20-bridge-sampler/package.json index 9fe7b39861..2241ff4908 100644 --- a/contracts/erc20-bridge-sampler/package.json +++ b/contracts/erc20-bridge-sampler/package.json @@ -38,7 +38,7 @@ "config": { "publicInterfaceContracts": "ERC20BridgeSampler,IERC20BridgeSampler,ILiquidityProvider,ILiquidityProviderRegistry,DummyLiquidityProviderRegistry,DummyLiquidityProvider", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(DummyLiquidityProvider|DummyLiquidityProviderRegistry|ERC20BridgeSampler|ICurve|IDevUtils|IERC20BridgeSampler|IEth2Dai|IKyberNetwork|IKyberNetworkProxy|ILiquidityProvider|ILiquidityProviderRegistry|IUniswapExchangeQuotes|IUniswapV2Router01|TestERC20BridgeSampler).json" + "abis": "./test/generated-artifacts/@(DummyLiquidityProvider|DummyLiquidityProviderRegistry|ERC20BridgeSampler|ICurve|IDevUtils|IERC20BridgeSampler|IEth2Dai|IKyberNetwork|IKyberNetworkProxy|ILiquidityProvider|ILiquidityProviderRegistry|IMultiBridge|IUniswapExchangeQuotes|IUniswapV2Router01|TestERC20BridgeSampler).json" }, "repository": { "type": "git", diff --git a/contracts/erc20-bridge-sampler/test/artifacts.ts b/contracts/erc20-bridge-sampler/test/artifacts.ts index e09dfbbde6..d05f5c5651 100644 --- a/contracts/erc20-bridge-sampler/test/artifacts.ts +++ b/contracts/erc20-bridge-sampler/test/artifacts.ts @@ -16,6 +16,7 @@ import * as IKyberNetwork from '../test/generated-artifacts/IKyberNetwork.json'; import * as IKyberNetworkProxy from '../test/generated-artifacts/IKyberNetworkProxy.json'; import * as ILiquidityProvider from '../test/generated-artifacts/ILiquidityProvider.json'; import * as ILiquidityProviderRegistry from '../test/generated-artifacts/ILiquidityProviderRegistry.json'; +import * as IMultiBridge from '../test/generated-artifacts/IMultiBridge.json'; import * as IUniswapExchangeQuotes from '../test/generated-artifacts/IUniswapExchangeQuotes.json'; import * as IUniswapV2Router01 from '../test/generated-artifacts/IUniswapV2Router01.json'; import * as TestERC20BridgeSampler from '../test/generated-artifacts/TestERC20BridgeSampler.json'; @@ -31,6 +32,7 @@ export const artifacts = { IKyberNetworkProxy: IKyberNetworkProxy as ContractArtifact, ILiquidityProvider: ILiquidityProvider as ContractArtifact, ILiquidityProviderRegistry: ILiquidityProviderRegistry as ContractArtifact, + IMultiBridge: IMultiBridge as ContractArtifact, IUniswapExchangeQuotes: IUniswapExchangeQuotes as ContractArtifact, IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact, TestERC20BridgeSampler: TestERC20BridgeSampler as ContractArtifact, diff --git a/contracts/erc20-bridge-sampler/test/wrappers.ts b/contracts/erc20-bridge-sampler/test/wrappers.ts index d292624166..aba6dd2e0d 100644 --- a/contracts/erc20-bridge-sampler/test/wrappers.ts +++ b/contracts/erc20-bridge-sampler/test/wrappers.ts @@ -14,6 +14,7 @@ export * from '../test/generated-wrappers/i_kyber_network'; export * from '../test/generated-wrappers/i_kyber_network_proxy'; export * from '../test/generated-wrappers/i_liquidity_provider'; export * from '../test/generated-wrappers/i_liquidity_provider_registry'; +export * from '../test/generated-wrappers/i_multi_bridge'; export * from '../test/generated-wrappers/i_uniswap_exchange_quotes'; export * from '../test/generated-wrappers/i_uniswap_v2_router01'; export * from '../test/generated-wrappers/test_erc20_bridge_sampler'; diff --git a/contracts/erc20-bridge-sampler/tsconfig.json b/contracts/erc20-bridge-sampler/tsconfig.json index f0e03e1174..efb2203777 100644 --- a/contracts/erc20-bridge-sampler/tsconfig.json +++ b/contracts/erc20-bridge-sampler/tsconfig.json @@ -20,6 +20,7 @@ "test/generated-artifacts/IKyberNetworkProxy.json", "test/generated-artifacts/ILiquidityProvider.json", "test/generated-artifacts/ILiquidityProviderRegistry.json", + "test/generated-artifacts/IMultiBridge.json", "test/generated-artifacts/IUniswapExchangeQuotes.json", "test/generated-artifacts/IUniswapV2Router01.json", "test/generated-artifacts/TestERC20BridgeSampler.json" diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index d5dcba1d85..b4f74fb29e 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -81,6 +81,10 @@ { "note": "Add support for Uniswap V2", "pr": 2599 + }, + { + "note": "Add support for MultiBridge", + "pr": 2593 } ] }, diff --git a/packages/asset-swapper/src/types.ts b/packages/asset-swapper/src/types.ts index 5ba75c7487..2168991dad 100644 --- a/packages/asset-swapper/src/types.ts +++ b/packages/asset-swapper/src/types.ts @@ -233,6 +233,7 @@ export interface SwapQuoterOpts extends OrderPrunerOpts { contractAddresses?: ContractAddresses; samplerGasLimit?: number; liquidityProviderRegistryAddress?: string; + multiBridgeAddress?: string; rfqt?: { takerApiKeyWhitelist: string[]; makerAssetOfferings: RfqtMakerAssetOfferings; diff --git a/packages/asset-swapper/src/utils/market_operation_utils/fills.ts b/packages/asset-swapper/src/utils/market_operation_utils/fills.ts index 0ff7e629f9..cfab8b4796 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/fills.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/fills.ts @@ -158,10 +158,16 @@ function dexQuotesToPaths( } function sourceToFillFlags(source: ERC20BridgeSource): number { - if (source === ERC20BridgeSource.Kyber) { - return FillFlags.Kyber; + switch (source) { + case ERC20BridgeSource.Uniswap: + return FillFlags.ConflictsWithMultiBridge; + case ERC20BridgeSource.LiquidityProvider: + return FillFlags.ConflictsWithMultiBridge; + case ERC20BridgeSource.MultiBridge: + return FillFlags.MultiBridge; + default: + return 0; } - return 0; } export function getPathSize(path: Fill[], targetInput: BigNumber = POSITIVE_INF): [BigNumber, BigNumber] { @@ -217,8 +223,8 @@ export function isValidPath(path: Fill[], skipDuplicateCheck: boolean = false): } flags |= path[i].flags; } - const conflictFlags = FillFlags.Kyber | FillFlags.ConflictsWithKyber; - return (flags & conflictFlags) !== conflictFlags; + const multiBridgeConflict = FillFlags.MultiBridge | FillFlags.ConflictsWithMultiBridge; + return (flags & multiBridgeConflict) !== multiBridgeConflict; } export function clipPathToInput(path: Fill[], targetInput: BigNumber = POSITIVE_INF): Fill[] { diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index c7031351a8..14730078d0 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -47,6 +47,7 @@ async function getRfqtIndicativeQuotesAsync( export class MarketOperationUtils { private readonly _wethAddress: string; + private readonly _multiBridge: string; constructor( private readonly _sampler: DexOrderSampler, @@ -55,6 +56,7 @@ export class MarketOperationUtils { private readonly _liquidityProviderRegistry: string = NULL_ADDRESS, ) { this._wethAddress = contractAddresses.etherToken.toLowerCase(); + this._multiBridge = contractAddresses.multiBridge.toLowerCase(); } /** @@ -75,6 +77,11 @@ export class MarketOperationUtils { } const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]); + const optionalSources = (this._liquidityProviderRegistry !== NULL_ADDRESS + ? [ERC20BridgeSource.LiquidityProvider] + : [] + ).concat(this._multiBridge !== NULL_ADDRESS ? [ERC20BridgeSource.MultiBridge] : []); + // Call the sampler contract. const samplerPromise = this._sampler.executeAsync( // Get native order fillable amounts. @@ -87,25 +94,23 @@ export class MarketOperationUtils { ), // Get ETH -> maker token price. DexOrderSampler.ops.getMedianSellRate( - difference(FEE_QUOTE_SOURCES, _opts.excludedSources).concat( - this._liquidityProviderSourceIfAvailable(_opts.excludedSources), - ), + difference(FEE_QUOTE_SOURCES.concat(optionalSources), _opts.excludedSources), makerToken, this._wethAddress, ONE_ETHER, this._wethAddress, this._liquidityProviderRegistry, + this._multiBridge, ), // Get sell quotes for taker -> maker. DexOrderSampler.ops.getSellQuotes( - difference(SELL_SOURCES, _opts.excludedSources).concat( - this._liquidityProviderSourceIfAvailable(_opts.excludedSources), - ), + difference(SELL_SOURCES.concat(optionalSources), _opts.excludedSources), makerToken, takerToken, getSampleAmounts(takerAmount, _opts.numSamples, _opts.sampleDistributionBase), this._wethAddress, this._liquidityProviderRegistry, + this._multiBridge, ), ); const rfqtPromise = getRfqtIndicativeQuotesAsync( @@ -125,6 +130,7 @@ export class MarketOperationUtils { dexQuotes, rfqtIndicativeQuotes, liquidityProviderAddress, + multiBridgeAddress: this._multiBridge, inputToken: takerToken, outputToken: makerToken, side: MarketOperation.Sell, @@ -157,6 +163,10 @@ export class MarketOperationUtils { } const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]); + const optionalSources = (this._liquidityProviderRegistry !== NULL_ADDRESS + ? [ERC20BridgeSource.LiquidityProvider] + : [] + ).concat(this._multiBridge !== NULL_ADDRESS ? [ERC20BridgeSource.MultiBridge] : []); // Call the sampler contract. const samplerPromise = this._sampler.executeAsync( // Get native order fillable amounts. @@ -169,19 +179,21 @@ export class MarketOperationUtils { ), // Get ETH -> taker token price. DexOrderSampler.ops.getMedianSellRate( - difference(FEE_QUOTE_SOURCES, _opts.excludedSources).concat( - this._liquidityProviderSourceIfAvailable(_opts.excludedSources), - ), + difference(FEE_QUOTE_SOURCES.concat(optionalSources), _opts.excludedSources), takerToken, this._wethAddress, ONE_ETHER, this._wethAddress, this._liquidityProviderRegistry, + this._multiBridge, ), // Get buy quotes for taker -> maker. DexOrderSampler.ops.getBuyQuotes( - difference(BUY_SOURCES, _opts.excludedSources).concat( - this._liquidityProviderSourceIfAvailable(_opts.excludedSources), + difference( + BUY_SOURCES.concat( + this._liquidityProviderRegistry !== NULL_ADDRESS ? [ERC20BridgeSource.LiquidityProvider] : [], + ), + _opts.excludedSources, ), makerToken, takerToken, @@ -208,6 +220,7 @@ export class MarketOperationUtils { dexQuotes, rfqtIndicativeQuotes, liquidityProviderAddress, + multiBridgeAddress: this._multiBridge, inputToken: makerToken, outputToken: takerToken, side: MarketOperation.Buy, @@ -324,6 +337,7 @@ export class MarketOperationUtils { allowFallback?: boolean; shouldBatchBridgeOrders?: boolean; liquidityProviderAddress?: string; + multiBridgeAddress?: string; }): OptimizedMarketOrder[] { const { inputToken, outputToken, side, inputAmount } = opts; const maxFallbackSlippage = opts.maxFallbackSlippage || 0; @@ -385,16 +399,10 @@ export class MarketOperationUtils { contractAddresses: this.contractAddresses, bridgeSlippage: opts.bridgeSlippage || 0, liquidityProviderAddress: opts.liquidityProviderAddress, + multiBridgeAddress: opts.multiBridgeAddress, shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders, }); } - - private _liquidityProviderSourceIfAvailable(excludedSources: ERC20BridgeSource[]): ERC20BridgeSource[] { - return this._liquidityProviderRegistry !== NULL_ADDRESS && - !excludedSources.includes(ERC20BridgeSource.LiquidityProvider) - ? [ERC20BridgeSource.LiquidityProvider] - : []; - } } // tslint:disable: max-file-line-count diff --git a/packages/asset-swapper/src/utils/market_operation_utils/multibridge_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/multibridge_utils.ts new file mode 100644 index 0000000000..8db19a8249 --- /dev/null +++ b/packages/asset-swapper/src/utils/market_operation_utils/multibridge_utils.ts @@ -0,0 +1,20 @@ +import { NULL_ADDRESS } from './constants'; + +// tslint:disable completed-docs + +export const TOKENS = { + WETH: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + DAI: '0x6b175474e89094c44da98b954eedeac495271d0f', + USDC: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + MKR: '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2', +}; + +export function getMultiBridgeIntermediateToken(takerToken: string, makerToken: string): string { + let intermediateToken = NULL_ADDRESS; + if (takerToken !== TOKENS.WETH && makerToken !== TOKENS.WETH) { + intermediateToken = TOKENS.WETH; + } else if (takerToken === TOKENS.USDC || makerToken === TOKENS.USDC) { + intermediateToken = TOKENS.DAI; + } + return intermediateToken; +} diff --git a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts index 32518e72d8..146c737b3b 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -17,6 +17,7 @@ import { ZERO_AMOUNT, } from './constants'; import { collapsePath } from './fills'; +import { getMultiBridgeIntermediateToken } from './multibridge_utils'; import { AggregationError, CollapsedFill, @@ -141,6 +142,7 @@ export interface CreateOrderFromPathOpts { bridgeSlippage: number; shouldBatchBridgeOrders: boolean; liquidityProviderAddress?: string; + multiBridgeAddress?: string; } // Convert sell fills into orders. @@ -195,6 +197,11 @@ function getBridgeAddressFromSource(source: ERC20BridgeSource, opts: CreateOrder throw new Error('Cannot create a LiquidityProvider order without a LiquidityProvider pool address.'); } return opts.liquidityProviderAddress; + case ERC20BridgeSource.MultiBridge: + if (opts.multiBridgeAddress === undefined) { + throw new Error('Cannot create a MultiBridge order without a MultiBridge address.'); + } + return opts.multiBridgeAddress; default: break; } @@ -242,6 +249,13 @@ function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts): createUniswapV2BridgeData([makerToken, opts.contractAddresses.etherToken, takerToken]), ); break; + case ERC20BridgeSource.MultiBridge: + makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( + makerToken, + bridgeAddress, + createMultiBridgeData(takerToken, makerToken), + ); + break; default: makerAssetData = assetDataUtils.encodeERC20BridgeAssetData( makerToken, @@ -315,6 +329,15 @@ function createBridgeData(tokenAddress: string): string { return encoder.encode({ tokenAddress }); } +function createMultiBridgeData(takerToken: string, makerToken: string): string { + const intermediateToken = getMultiBridgeIntermediateToken(takerToken, makerToken); + const encoder = AbiEncoder.create([ + { name: 'takerToken', type: 'address' }, + { name: 'intermediateToken', type: 'address' }, + ]); + return encoder.encode({ takerToken, intermediateToken }); +} + function createCurveBridgeData( curveAddress: string, fromTokenIdx: number, diff --git a/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts b/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts index bb7acfbf7a..a2f3ce1173 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts @@ -2,6 +2,7 @@ import { BigNumber, ERC20BridgeSource, SignedOrder } from '../..'; import { getCurveInfo, isCurveSource } from '../source_utils'; import { DEFAULT_FAKE_BUY_OPTS } from './constants'; +import { getMultiBridgeIntermediateToken } from './multibridge_utils'; import { BatchedOperation, DexSample, FakeBuyOpts } from './types'; /** @@ -123,7 +124,7 @@ export const samplerOperations = { }; }, getLiquidityProviderSellQuotes( - liquidityProviderRegistryAddress: string, + registryAddress: string, makerToken: string, takerToken: string, takerFillAmounts: BigNumber[], @@ -131,9 +132,31 @@ export const samplerOperations = { return { encodeCall: contract => { return contract - .sampleSellsFromLiquidityProviderRegistry( - liquidityProviderRegistryAddress, + .sampleSellsFromLiquidityProviderRegistry(registryAddress, takerToken, makerToken, takerFillAmounts) + .getABIEncodedTransactionData(); + }, + handleCallResultsAsync: async (contract, callResults) => { + return contract.getABIDecodedReturnData( + 'sampleSellsFromLiquidityProviderRegistry', + callResults, + ); + }, + }; + }, + getMultiBridgeSellQuotes( + multiBridgeAddress: string, + makerToken: string, + intermediateToken: string, + takerToken: string, + takerFillAmounts: BigNumber[], + ): BatchedOperation { + return { + encodeCall: contract => { + return contract + .sampleSellsFromMultiBridge( + multiBridgeAddress, takerToken, + intermediateToken, makerToken, takerFillAmounts, ) @@ -148,7 +171,7 @@ export const samplerOperations = { }; }, getLiquidityProviderBuyQuotes( - liquidityProviderRegistryAddress: string, + registryAddress: string, makerToken: string, takerToken: string, makerFillAmounts: BigNumber[], @@ -158,7 +181,7 @@ export const samplerOperations = { encodeCall: contract => { return contract .sampleBuysFromLiquidityProviderRegistry( - liquidityProviderRegistryAddress, + registryAddress, takerToken, makerToken, makerFillAmounts, @@ -256,7 +279,8 @@ export const samplerOperations = { takerToken: string, takerFillAmount: BigNumber, wethAddress: string, - liquidityProviderRegistryAddress?: string | undefined, + liquidityProviderRegistryAddress?: string, + multiBridgeAddress?: string, ): BatchedOperation { if (makerToken.toLowerCase() === takerToken.toLowerCase()) { return samplerOperations.constant(new BigNumber(1)); @@ -268,6 +292,7 @@ export const samplerOperations = { [takerFillAmount], wethAddress, liquidityProviderRegistryAddress, + multiBridgeAddress, ); return { encodeCall: contract => { @@ -324,7 +349,8 @@ export const samplerOperations = { takerToken: string, takerFillAmounts: BigNumber[], wethAddress: string, - liquidityProviderRegistryAddress?: string | undefined, + liquidityProviderRegistryAddress?: string, + multiBridgeAddress?: string, ): BatchedOperation { const subOps = sources .map(source => { @@ -367,6 +393,18 @@ export const samplerOperations = { takerToken, takerFillAmounts, ); + } else if (source === ERC20BridgeSource.MultiBridge) { + if (multiBridgeAddress === undefined) { + throw new Error('Cannot sample liquidity from MultiBridge if an address is not provided.'); + } + const intermediateToken = getMultiBridgeIntermediateToken(takerToken, makerToken); + batchedOperation = samplerOperations.getMultiBridgeSellQuotes( + multiBridgeAddress, + makerToken, + intermediateToken, + takerToken, + takerFillAmounts, + ); } else { throw new Error(`Unsupported sell sample source: ${source}`); } @@ -404,7 +442,7 @@ export const samplerOperations = { takerToken: string, makerFillAmounts: BigNumber[], wethAddress: string, - liquidityProviderRegistryAddress?: string | undefined, + liquidityProviderRegistryAddress?: string, fakeBuyOpts: FakeBuyOpts = DEFAULT_FAKE_BUY_OPTS, ): BatchedOperation { const subOps = sources diff --git a/packages/asset-swapper/src/utils/market_operation_utils/types.ts b/packages/asset-swapper/src/utils/market_operation_utils/types.ts index a4fad71983..d086d0fc93 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -38,6 +38,7 @@ export enum ERC20BridgeSource { CurveUsdcDaiUsdtBusd = 'Curve_USDC_DAI_USDT_BUSD', CurveUsdcDaiUsdtSusd = 'Curve_USDC_DAI_USDT_SUSD', LiquidityProvider = 'LiquidityProvider', + MultiBridge = 'MultiBridge', } // Internal `fillData` field for `Fill` objects. @@ -67,6 +68,8 @@ export interface DexSample { export enum FillFlags { ConflictsWithKyber = 0x1, Kyber = 0x2, + ConflictsWithMultiBridge = 0x4, + MultiBridge = 0x8, } /** diff --git a/packages/asset-swapper/test/dex_sampler_test.ts b/packages/asset-swapper/test/dex_sampler_test.ts index 4b11767461..ef42cffaec 100644 --- a/packages/asset-swapper/test/dex_sampler_test.ts +++ b/packages/asset-swapper/test/dex_sampler_test.ts @@ -212,6 +212,48 @@ describe('DexSampler tests', () => { ]); }); + it('getMultiBridgeSellQuotes()', async () => { + const expectedTakerToken = randomAddress(); + const expectedMakerToken = randomAddress(); + const multiBridge = randomAddress(); + + const sampler = new MockSamplerContract({ + sampleSellsFromMultiBridge: ( + multiBridgeAddress, + takerToken, + _intermediateToken, + makerToken, + _fillAmounts, + ) => { + expect(multiBridgeAddress).to.eq(multiBridge); + expect(takerToken).to.eq(expectedTakerToken); + expect(makerToken).to.eq(expectedMakerToken); + return [toBaseUnitAmount(1001)]; + }, + }); + const dexOrderSampler = new DexOrderSampler(sampler); + const [result] = await dexOrderSampler.executeAsync( + DexOrderSampler.ops.getSellQuotes( + [ERC20BridgeSource.MultiBridge], + expectedMakerToken, + expectedTakerToken, + [toBaseUnitAmount(1000)], + randomAddress(), + randomAddress(), + multiBridge, + ), + ); + expect(result).to.deep.equal([ + [ + { + source: 'MultiBridge', + output: toBaseUnitAmount(1001), + input: toBaseUnitAmount(1000), + }, + ], + ]); + }); + it('getEth2DaiSellQuotes()', async () => { const expectedTakerToken = randomAddress(); const expectedMakerToken = randomAddress(); diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index 4549dd6966..d0a141d7ba 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -30,7 +30,7 @@ import { DexSample, ERC20BridgeSource, NativeFillData } from '../src/utils/marke // tslint:disable: custom-no-magic-numbers describe('MarketOperationUtils tests', () => { const CHAIN_ID = 1; - const contractAddresses = getContractAddressesForChainOrThrow(CHAIN_ID); + const contractAddresses = { ...getContractAddressesForChainOrThrow(CHAIN_ID), multiBridge: NULL_ADDRESS }; const ETH2DAI_BRIDGE_ADDRESS = contractAddresses.eth2DaiBridge; const KYBER_BRIDGE_ADDRESS = contractAddresses.kyberBridge; const UNISWAP_BRIDGE_ADDRESS = contractAddresses.uniswapBridge; @@ -259,7 +259,9 @@ describe('MarketOperationUtils tests', () => { const fn = (registryAddress: string, takerToken: string, makerToken: string): string => { callArgs.makerToken = makerToken; callArgs.takerToken = takerToken; - callArgs.registryAddress = registryAddress; + if (registryAddress !== constants.NULL_ADDRESS) { + callArgs.registryAddress = registryAddress; + } return liquidityProviderAddress; }; return [callArgs, fn]; @@ -294,6 +296,7 @@ describe('MarketOperationUtils tests', () => { [ERC20BridgeSource.CurveUsdcDaiUsdtBusd]: _.times(NUM_SAMPLES, () => 0), [ERC20BridgeSource.CurveUsdcDaiUsdtSusd]: _.times(NUM_SAMPLES, () => 0), [ERC20BridgeSource.LiquidityProvider]: _.times(NUM_SAMPLES, () => 0), + [ERC20BridgeSource.MultiBridge]: _.times(NUM_SAMPLES, () => 0), }; const DEFAULT_OPS = { diff --git a/packages/asset-swapper/test/utils/mock_sampler_contract.ts b/packages/asset-swapper/test/utils/mock_sampler_contract.ts index cfa68f7efd..49f590b0d5 100644 --- a/packages/asset-swapper/test/utils/mock_sampler_contract.ts +++ b/packages/asset-swapper/test/utils/mock_sampler_contract.ts @@ -29,6 +29,13 @@ export type SampleSellsLPHandler = ( takerTokenAmounts: BigNumber[], ) => SampleResults; export type SampleSellsMultihopHandler = (path: string[], takerTokenAmounts: BigNumber[]) => SampleResults; +export type SampleSellsMBHandler = ( + multiBridgeAddress: string, + takerToken: string, + intermediateToken: string, + makerToken: string, + takerTokenAmounts: BigNumber[], +) => SampleResults; const DUMMY_PROVIDER = { sendAsync: (...args: any[]): any => { @@ -41,6 +48,7 @@ interface Handlers { getOrderFillableTakerAssetAmounts: GetOrderFillableAssetAmountHandler; sampleSellsFromKyberNetwork: SampleSellsHandler; sampleSellsFromLiquidityProviderRegistry: SampleSellsLPHandler; + sampleSellsFromMultiBridge: SampleSellsMBHandler; sampleSellsFromEth2Dai: SampleSellsHandler; sampleSellsFromUniswap: SampleSellsHandler; sampleSellsFromUniswapV2: SampleSellsMultihopHandler; @@ -159,6 +167,24 @@ export class MockSamplerContract extends IERC20BridgeSamplerContract { ); } + public sampleSellsFromMultiBridge( + multiBridgeAddress: string, + takerToken: string, + intermediateToken: string, + makerToken: string, + takerAssetAmounts: BigNumber[], + ): ContractFunctionObj { + return this._wrapCall( + super.sampleSellsFromMultiBridge, + this._handlers.sampleSellsFromMultiBridge, + multiBridgeAddress, + takerToken, + intermediateToken, + makerToken, + takerAssetAmounts, + ); + } + public sampleBuysFromEth2Dai( takerToken: string, makerToken: string, diff --git a/packages/contract-addresses/addresses.json b/packages/contract-addresses/addresses.json index 6bc26d85af..95f0784e5a 100644 --- a/packages/contract-addresses/addresses.json +++ b/packages/contract-addresses/addresses.json @@ -31,7 +31,8 @@ "chainlinkStopLimit": "0xeb27220f95f364e1d9531992c48613f231839f53", "curveBridge": "0x6dc7950423ada9f56fb2c93a23edb787f1e29088", "maximumGasPrice": "0xe2bfd35306495d11e3c9db0d8de390cda24563cf", - "dexForwarderBridge": "0x5591360f8c7640fea5771c9682d6b5ecb776e1f8" + "dexForwarderBridge": "0x5591360f8c7640fea5771c9682d6b5ecb776e1f8", + "multiBridge": "0xc03117a8c9bde203f70aa911cb64a7a0df5ba1e1" }, "3": { "erc20Proxy": "0xb1408f4c245a23c31b98d2c626777d4c0d766caa", @@ -65,7 +66,8 @@ "chainlinkStopLimit": "0x67a094cf028221ffdd93fc658f963151d05e2a74", "curveBridge": "0x0000000000000000000000000000000000000000", "maximumGasPrice": "0x407b4128e9ecad8769b2332312a9f655cb9f5f3a", - "dexForwarderBridge": "0x3be8e59038d8c4e8d8776ca40ef2f024bad95ad1" + "dexForwarderBridge": "0x3be8e59038d8c4e8d8776ca40ef2f024bad95ad1", + "multiBridge": "0x0000000000000000000000000000000000000000" }, "4": { "exchangeV2": "0xbff9493f92a3df4b0429b6d00743b3cfb4c85831", @@ -99,7 +101,8 @@ "chainlinkStopLimit": "0x407b4128e9ecad8769b2332312a9f655cb9f5f3a", "curveBridge": "0x0000000000000000000000000000000000000000", "maximumGasPrice": "0x47697b44bd89051e93b4d5857ba8e024800a74ac", - "dexForwarderBridge": "0x0000000000000000000000000000000000000000" + "dexForwarderBridge": "0x0000000000000000000000000000000000000000", + "multiBridge": "0x0000000000000000000000000000000000000000" }, "42": { "erc20Proxy": "0xf1ec01d6236d3cd881a0bf0130ea25fe4234003e", @@ -133,7 +136,8 @@ "chainlinkStopLimit": "0x0000000000000000000000000000000000000000", "curveBridge": "0x90c62c91a9f655f4f739e6cee85c84f9ccf47323", "maximumGasPrice": "0x67a094cf028221ffdd93fc658f963151d05e2a74", - "dexForwarderBridge": "0x6cce442a48ab07635462a40594054f34f44195ff" + "dexForwarderBridge": "0x6cce442a48ab07635462a40594054f34f44195ff", + "multiBridge": "0x0000000000000000000000000000000000000000" }, "1337": { "erc20Proxy": "0x1dc4c1cefef38a777b15aa20260a54e584b16c48", @@ -167,6 +171,7 @@ "chainlinkStopLimit": "0x0000000000000000000000000000000000000000", "curveBridge": "0x0000000000000000000000000000000000000000", "maximumGasPrice": "0x0000000000000000000000000000000000000000", - "dexForwarderBridge": "0x0000000000000000000000000000000000000000" + "dexForwarderBridge": "0x0000000000000000000000000000000000000000", + "multiBridge": "0x0000000000000000000000000000000000000000" } } diff --git a/packages/contract-addresses/src/index.ts b/packages/contract-addresses/src/index.ts index 97abe2d50c..627f03527e 100644 --- a/packages/contract-addresses/src/index.ts +++ b/packages/contract-addresses/src/index.ts @@ -33,6 +33,7 @@ export interface ContractAddresses { chainlinkStopLimit: string; maximumGasPrice: string; dexForwarderBridge: string; + multiBridge: string; } export enum ChainId { diff --git a/packages/migrations/src/migration.ts b/packages/migrations/src/migration.ts index 59e59604bb..94561d79bf 100644 --- a/packages/migrations/src/migration.ts +++ b/packages/migrations/src/migration.ts @@ -323,6 +323,7 @@ export async function runMigrationsAsync( chainlinkStopLimit: constants.NULL_ADDRESS, maximumGasPrice: constants.NULL_ADDRESS, dexForwarderBridge: constants.NULL_ADDRESS, + multiBridge: constants.NULL_ADDRESS, }; return contractAddresses; }