From 4dac6201569f20985f418401d24ed88e636f533a Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 14 Jul 2020 16:32:16 -0400 Subject: [PATCH] `@0x/contracts-asset-proxy`: Update `CurveBridge` to support more varied curves. `@0x/contracts-erc20-bridge-sampler`: Refactor. `@0x/contracts-erc20-bridge-sampler`: Add support for more varied curves. `@0x/contracts-integrations`: Update curve bridge tests. --- contracts/asset-proxy/CHANGELOG.json | 9 + .../contracts/src/bridges/CurveBridge.sol | 51 +- .../contracts/src/interfaces/ICurve.sol | 16 - contracts/erc20-bridge-sampler/CHANGELOG.json | 9 + .../contracts/src/ApproximateBuys.sol | 123 ++ .../contracts/src/CurveSampler.sol | 156 +++ .../contracts/src/ERC20BridgeSampler.sol | 1017 +---------------- .../contracts/src/Eth2DaiSampler.sol | 140 +++ .../contracts/src/ICurve.sol | 16 - .../contracts/src/IERC20BridgeSampler.sol | 293 ----- .../contracts/src/KyberSampler.sol | 218 ++++ .../src/LiquidityProviderSampler.sol | 168 +++ .../contracts/src/MultiBridgeSampler.sol | 79 ++ .../contracts/src/NativeOrderSampler.sol | 125 ++ .../contracts/src/SamplerUtils.sol | 56 + .../contracts/src/UniswapSampler.sol | 203 ++++ .../contracts/src/UniswapV2Sampler.sol | 99 ++ contracts/erc20-bridge-sampler/package.json | 4 +- .../erc20-bridge-sampler/src/artifacts.ts | 2 - .../erc20-bridge-sampler/src/wrappers.ts | 1 - .../erc20-bridge-sampler/test/artifacts.ts | 22 +- .../test/erc20-bridge-sampler.ts | 33 +- .../erc20-bridge-sampler/test/wrappers.ts | 11 +- contracts/erc20-bridge-sampler/tsconfig.json | 14 +- contracts/integrations/CHANGELOG.json | 9 + contracts/integrations/package.json | 1 + .../bridge_sampler_mainnet_test.ts | 34 +- .../test/bridges/curve_bridge_mainnet_test.ts | 131 ++- 28 files changed, 1577 insertions(+), 1463 deletions(-) create mode 100644 contracts/erc20-bridge-sampler/contracts/src/ApproximateBuys.sol create mode 100644 contracts/erc20-bridge-sampler/contracts/src/CurveSampler.sol create mode 100644 contracts/erc20-bridge-sampler/contracts/src/Eth2DaiSampler.sol delete mode 100644 contracts/erc20-bridge-sampler/contracts/src/IERC20BridgeSampler.sol create mode 100644 contracts/erc20-bridge-sampler/contracts/src/KyberSampler.sol create mode 100644 contracts/erc20-bridge-sampler/contracts/src/LiquidityProviderSampler.sol create mode 100644 contracts/erc20-bridge-sampler/contracts/src/MultiBridgeSampler.sol create mode 100644 contracts/erc20-bridge-sampler/contracts/src/NativeOrderSampler.sol create mode 100644 contracts/erc20-bridge-sampler/contracts/src/SamplerUtils.sol create mode 100644 contracts/erc20-bridge-sampler/contracts/src/UniswapSampler.sol create mode 100644 contracts/erc20-bridge-sampler/contracts/src/UniswapV2Sampler.sol diff --git a/contracts/asset-proxy/CHANGELOG.json b/contracts/asset-proxy/CHANGELOG.json index 2828349ac6..37402629cf 100644 --- a/contracts/asset-proxy/CHANGELOG.json +++ b/contracts/asset-proxy/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "3.5.0", + "changes": [ + { + "note": "Update `CurveBridge` to support more varied curves", + "pr": 2633 + } + ] + }, { "version": "3.4.0", "changes": [ diff --git a/contracts/asset-proxy/contracts/src/bridges/CurveBridge.sol b/contracts/asset-proxy/contracts/src/bridges/CurveBridge.sol index 05150f8bcc..9515fef349 100644 --- a/contracts/asset-proxy/contracts/src/bridges/CurveBridge.sol +++ b/contracts/asset-proxy/contracts/src/bridges/CurveBridge.sol @@ -26,7 +26,6 @@ import "@0x/contracts-exchange-libs/contracts/src/IWallet.sol"; import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; import "../interfaces/IERC20Bridge.sol"; import "../interfaces/ICurve.sol"; -import "./MixinGasToken.sol"; // solhint-disable not-rely-on-time @@ -34,14 +33,14 @@ import "./MixinGasToken.sol"; contract CurveBridge is IERC20Bridge, IWallet, - DeploymentConstants, - MixinGasToken + DeploymentConstants { struct CurveBridgeData { address curveAddress; + bytes4 exchangeFunctionSelector; + address fromTokenAddress; int128 fromCoinIdx; int128 toCoinIdx; - int128 version; } /// @dev Callback for `ICurve`. Tries to buy `amount` of @@ -62,39 +61,31 @@ contract CurveBridge is bytes calldata bridgeData ) external - freesGasTokensFromCollector returns (bytes4 success) { // Decode the bridge data to get the Curve metadata. CurveBridgeData memory data = abi.decode(bridgeData, (CurveBridgeData)); - address fromTokenAddress = ICurve(data.curveAddress).underlying_coins(data.fromCoinIdx); - require(toTokenAddress != fromTokenAddress, "CurveBridge/INVALID_PAIR"); - uint256 fromTokenBalance = IERC20Token(fromTokenAddress).balanceOf(address(this)); + require(toTokenAddress != data.fromTokenAddress, "CurveBridge/INVALID_PAIR"); + uint256 fromTokenBalance = IERC20Token(data.fromTokenAddress).balanceOf(address(this)); // Grant an allowance to the exchange to spend `fromTokenAddress` token. - LibERC20Token.approveIfBelow(fromTokenAddress, data.curveAddress, fromTokenBalance); + LibERC20Token.approveIfBelow(data.fromTokenAddress, data.curveAddress, fromTokenBalance); // Try to sell all of this contract's `fromTokenAddress` token balance. - if (data.version == 0) { - ICurve(data.curveAddress).exchange_underlying( - data.fromCoinIdx, - data.toCoinIdx, - // dx - fromTokenBalance, - // min dy - amount, - // expires - block.timestamp + 1 - ); - } else { - ICurve(data.curveAddress).exchange_underlying( - data.fromCoinIdx, - data.toCoinIdx, - // dx - fromTokenBalance, - // min dy - amount - ); + { + (bool didSucceed, bytes memory resultData) = + data.curveAddress.call(abi.encodeWithSelector( + data.exchangeFunctionSelector, + data.fromCoinIdx, + data.toCoinIdx, + // dx + fromTokenBalance, + // min dy + amount + )); + if (!didSucceed) { + assembly { revert(add(resultData, 32), mload(resultData)) } + } } uint256 toTokenBalance = IERC20Token(toTokenAddress).balanceOf(address(this)); @@ -102,7 +93,7 @@ contract CurveBridge is LibERC20Token.transfer(toTokenAddress, to, toTokenBalance); emit ERC20BridgeTransfer( - fromTokenAddress, + data.fromTokenAddress, toTokenAddress, fromTokenBalance, toTokenBalance, diff --git a/contracts/asset-proxy/contracts/src/interfaces/ICurve.sol b/contracts/asset-proxy/contracts/src/interfaces/ICurve.sol index 5ca526364c..2f59939ce7 100644 --- a/contracts/asset-proxy/contracts/src/interfaces/ICurve.sol +++ b/contracts/asset-proxy/contracts/src/interfaces/ICurve.sol @@ -22,22 +22,6 @@ pragma solidity ^0.5.9; // solhint-disable func-name-mixedcase interface ICurve { - /// @dev Sell `sellAmount` of `fromToken` token and receive `toToken` token. - /// This function exists on early versions of Curve (USDC/DAI) - /// @param i The token index being sold. - /// @param j The token index being bought. - /// @param sellAmount The amount of token being bought. - /// @param minBuyAmount The minimum buy amount of the token being bought. - /// @param deadline The time in seconds when this operation should expire. - function exchange_underlying( - int128 i, - int128 j, - uint256 sellAmount, - uint256 minBuyAmount, - uint256 deadline - ) - external; - /// @dev Sell `sellAmount` of `fromToken` token and receive `toToken` token. /// This function exists on later versions of Curve (USDC/DAI/USDT) /// @param i The token index being sold. diff --git a/contracts/erc20-bridge-sampler/CHANGELOG.json b/contracts/erc20-bridge-sampler/CHANGELOG.json index 8f59f58093..cb32a6c40b 100644 --- a/contracts/erc20-bridge-sampler/CHANGELOG.json +++ b/contracts/erc20-bridge-sampler/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "1.8.0", + "changes": [ + { + "note": "Refactor and support more varied curves", + "pr": 2633 + } + ] + }, { "version": "1.7.0", "changes": [ diff --git a/contracts/erc20-bridge-sampler/contracts/src/ApproximateBuys.sol b/contracts/erc20-bridge-sampler/contracts/src/ApproximateBuys.sol new file mode 100644 index 0000000000..7df6aef6c1 --- /dev/null +++ b/contracts/erc20-bridge-sampler/contracts/src/ApproximateBuys.sol @@ -0,0 +1,123 @@ +/* + + Copyright 2019 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; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol"; +import "@0x/contracts-utils/contracts/src/LibBytes.sol"; + + +contract ApproximateBuys { + + /// @dev Information computing buy quotes for sources that do not have native + /// buy quote support. + struct ApproximateBuyQuoteOpts { + // Arbitrary maker token data to pass to `getSellQuoteCallback`. + bytes makerTokenData; + // Arbitrary taker token data to pass to `getSellQuoteCallback`. + bytes takerTokenData; + // Callback to retrieve a sell quote. + function (bytes memory, bytes memory, uint256) + internal + view + returns (uint256) getSellQuoteCallback; + } + + uint256 private constant ONE_HUNDED_PERCENT_BPS = 1e4; + /// @dev Maximum approximate (positive) error rate when approximating a buy quote. + uint256 private constant APPROXIMATE_BUY_TARGET_EPSILON_BPS = 0.0005e4; + /// @dev Maximum iterations to perform when approximating a buy quote. + uint256 private constant APPROXIMATE_BUY_MAX_ITERATIONS = 5; + + function _sampleApproximateBuys( + ApproximateBuyQuoteOpts memory opts, + uint256[] memory makerTokenAmounts + ) + internal + view + returns (uint256[] memory takerTokenAmounts) + { + takerTokenAmounts = new uint256[](makerTokenAmounts.length); + if (makerTokenAmounts.length == 0) { + return takerTokenAmounts; + } + + uint256 sellAmount = opts.getSellQuoteCallback( + opts.makerTokenData, + opts.takerTokenData, + makerTokenAmounts[0] + ); + if (sellAmount == 0) { + return takerTokenAmounts; + } + + uint256 buyAmount = opts.getSellQuoteCallback( + opts.takerTokenData, + opts.makerTokenData, + sellAmount + ); + if (buyAmount == 0) { + return takerTokenAmounts; + } + + for (uint256 i = 0; i < makerTokenAmounts.length; i++) { + for (uint256 iter = 0; iter < APPROXIMATE_BUY_MAX_ITERATIONS; iter++) { + // adjustedSellAmount = previousSellAmount * (target/actual) * JUMP_MULTIPLIER + sellAmount = LibMath.getPartialAmountCeil( + makerTokenAmounts[i], + buyAmount, + sellAmount + ); + sellAmount = LibMath.getPartialAmountCeil( + (ONE_HUNDED_PERCENT_BPS + APPROXIMATE_BUY_TARGET_EPSILON_BPS), + ONE_HUNDED_PERCENT_BPS, + sellAmount + ); + uint256 _buyAmount = opts.getSellQuoteCallback( + opts.takerTokenData, + opts.makerTokenData, + sellAmount + ); + if (_buyAmount == 0) { + break; + } + // We re-use buyAmount next iteration, only assign if it is + // non zero + buyAmount = _buyAmount; + // If we've reached our goal, exit early + if (buyAmount >= makerTokenAmounts[i]) { + uint256 eps = + (buyAmount - makerTokenAmounts[i]) * ONE_HUNDED_PERCENT_BPS / + makerTokenAmounts[i]; + if (eps <= APPROXIMATE_BUY_TARGET_EPSILON_BPS) { + break; + } + } + } + // We do our best to close in on the requested amount, but we can either over buy or under buy and exit + // if we hit a max iteration limit + // We scale the sell amount to get the approximate target + takerTokenAmounts[i] = LibMath.getPartialAmountCeil( + makerTokenAmounts[i], + buyAmount, + sellAmount + ); + } + } +} diff --git a/contracts/erc20-bridge-sampler/contracts/src/CurveSampler.sol b/contracts/erc20-bridge-sampler/contracts/src/CurveSampler.sol new file mode 100644 index 0000000000..b5cce7c016 --- /dev/null +++ b/contracts/erc20-bridge-sampler/contracts/src/CurveSampler.sol @@ -0,0 +1,156 @@ +/* + + Copyright 2019 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; +pragma experimental ABIEncoderV2; + +import "./ICurve.sol"; +import "./ApproximateBuys.sol"; +import "./SamplerUtils.sol"; + + +contract CurveSampler is + SamplerUtils, + ApproximateBuys +{ + /// @dev Information for sampling from curve sources. + struct CurveInfo { + address poolAddress; + bytes4 sellQuoteFunctionSelector; + bytes4 buyQuoteFunctionSelector; + } + + /// @dev Base gas limit for Curve calls. Some Curves have multiple tokens + /// So a reasonable ceil is 150k per token. Biggest Curve has 4 tokens. + uint256 constant private CURVE_CALL_GAS = 600e3; // 600k + + /// @dev Sample sell quotes from Curve. + /// @param curveInfo Curve information specific to this token pair. + /// @param fromTokenIdx Index of the taker token (what to sell). + /// @param toTokenIdx Index 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 sampleSellsFromCurve( + CurveInfo memory curveInfo, + int128 fromTokenIdx, + int128 toTokenIdx, + uint256[] memory takerTokenAmounts + ) + public + view + returns (uint256[] memory makerTokenAmounts) + { + uint256 numSamples = takerTokenAmounts.length; + makerTokenAmounts = new uint256[](numSamples); + for (uint256 i = 0; i < numSamples; i++) { + (bool didSucceed, bytes memory resultData) = + curveInfo.poolAddress.staticcall.gas(CURVE_CALL_GAS)( + abi.encodeWithSelector( + curveInfo.sellQuoteFunctionSelector, + fromTokenIdx, + toTokenIdx, + takerTokenAmounts[i] + )); + uint256 buyAmount = 0; + if (didSucceed) { + buyAmount = abi.decode(resultData, (uint256)); + } else { + break; + } + makerTokenAmounts[i] = buyAmount; + } + } + + /// @dev Sample buy quotes from Curve. + /// @param curveInfo Curve information specific to this token pair. + /// @param fromTokenIdx Index of the taker token (what to sell). + /// @param toTokenIdx Index of the maker token (what to buy). + /// @param makerTokenAmounts Maker token buy amount for each sample. + /// @return takerTokenAmounts Taker amounts sold at each maker token + /// amount. + function sampleBuysFromCurve( + CurveInfo memory curveInfo, + int128 fromTokenIdx, + int128 toTokenIdx, + uint256[] memory makerTokenAmounts + ) + public + view + returns (uint256[] memory takerTokenAmounts) + { + if (curveInfo.buyQuoteFunctionSelector == bytes4(0)) { + // Buys not supported on this curve, so approximate it. + return _sampleApproximateBuys( + ApproximateBuyQuoteOpts({ + makerTokenData: abi.encode(toTokenIdx, curveInfo), + takerTokenData: abi.encode(fromTokenIdx, curveInfo), + getSellQuoteCallback: _sampleSellForApproximateBuyFromCurve + }), + makerTokenAmounts + ); + } + uint256 numSamples = makerTokenAmounts.length; + takerTokenAmounts = new uint256[](numSamples); + for (uint256 i = 0; i < numSamples; i++) { + (bool didSucceed, bytes memory resultData) = + curveInfo.poolAddress.staticcall.gas(CURVE_CALL_GAS)( + abi.encodeWithSelector( + curveInfo.buyQuoteFunctionSelector, + fromTokenIdx, + toTokenIdx, + makerTokenAmounts[i] + )); + uint256 sellAmount = 0; + if (didSucceed) { + sellAmount = abi.decode(resultData, (uint256)); + } else { + break; + } + takerTokenAmounts[i] = sellAmount; + } + } + + function _sampleSellForApproximateBuyFromCurve( + bytes memory takerTokenData, + bytes memory makerTokenData, + uint256 sellAmount + ) + private + view + returns (uint256 buyAmount) + { + (int128 takerTokenIdx, CurveInfo memory curveInfo) = + abi.decode(takerTokenData, (int128, CurveInfo)); + (int128 makerTokenIdx) = + abi.decode(makerTokenData, (int128)); + (bool success, bytes memory resultData) = + address(this).staticcall(abi.encodeWithSelector( + this.sampleSellsFromCurve.selector, + curveInfo, + takerTokenIdx, + makerTokenIdx, + _toSingleValueArray(sellAmount) + )); + if (!success) { + return 0; + } + // solhint-disable-next-line indent + return abi.decode(resultData, (uint256[]))[0]; + } +} diff --git a/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol b/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol index 5098c07a02..bd0577121e 100644 --- a/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol +++ b/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol @@ -19,53 +19,27 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; -import "@0x/contracts-asset-proxy/contracts/src/interfaces/IUniswapExchangeFactory.sol"; -import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol"; -import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol"; -import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol"; -import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; -import "@0x/contracts-utils/contracts/src/LibBytes.sol"; -import "./IDevUtils.sol"; -import "./IERC20BridgeSampler.sol"; -import "./IEth2Dai.sol"; -import "./IKyberNetwork.sol"; -import "./IKyberNetworkProxy.sol"; -import "./IKyberStorage.sol"; -import "./IKyberHintHandler.sol"; -import "./IUniswapExchangeQuotes.sol"; -import "./ICurve.sol"; -import "./ILiquidityProvider.sol"; -import "./ILiquidityProviderRegistry.sol"; -import "./IUniswapV2Router01.sol"; -import "./IMultiBridge.sol"; +import "./ApproximateBuys.sol"; +import "./CurveSampler.sol"; +import "./Eth2DaiSampler.sol"; +import "./KyberSampler.sol"; +import "./LiquidityProviderSampler.sol"; +import "./MultiBridgeSampler.sol"; +import "./NativeOrderSampler.sol"; +import "./UniswapSampler.sol"; +import "./UniswapV2Sampler.sol"; contract ERC20BridgeSampler is - IERC20BridgeSampler, - DeploymentConstants + Eth2DaiSampler, + UniswapSampler, + KyberSampler, + CurveSampler, + LiquidityProviderSampler, + UniswapV2Sampler, + MultiBridgeSampler, + NativeOrderSampler { - /// @dev Gas limit for DevUtils calls. - uint256 constant internal DEV_UTILS_CALL_GAS = 500e3; // 500k - /// @dev Gas limit for Kyber calls. - uint256 constant internal KYBER_CALL_GAS = 1500e3; // 1.5m - /// @dev Gas limit for Uniswap calls. - uint256 constant internal UNISWAP_CALL_GAS = 150e3; // 150k - /// @dev Gas limit for UniswapV2 calls. - uint256 constant internal UNISWAPV2_CALL_GAS = 150e3; // 150k - /// @dev Base gas limit for Eth2Dai calls. - uint256 constant internal ETH2DAI_CALL_GAS = 1000e3; // 1m - /// @dev Base gas limit for Curve calls. Some Curves have multiple tokens - /// 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 = 400e3; // 400k - /// @dev The Kyber Uniswap Reserve address - address constant internal KYBER_UNISWAP_RESERVE = 0x31E085Afd48a1d6e51Cc193153d625e8f0514C7F; - /// @dev The Kyber Uniswap V2 Reserve address - address constant internal KYBER_UNISWAPV2_RESERVE = 0x10908C875D865C66f271F5d3949848971c9595C9; - /// @dev The Kyber Eth2Dai Reserve address - address constant internal KYBER_ETH2DAI_RESERVE = 0x1E158c0e93c30d24e918Ef83d1e0bE23595C3c0f; - /// @dev Call multiple public functions on this contract in a single transaction. /// @param callDatas ABI-encoded call data for each function call. /// @return callResults ABI-encoded results data for each call. @@ -83,961 +57,4 @@ contract ERC20BridgeSampler is callResults[i] = resultData; } } - - /// @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 orderSignatures Signatures for each respective order in `orders`. - /// @param devUtilsAddress Address to the DevUtils contract. - /// @return orderFillableTakerAssetAmounts How much taker asset can be filled - /// by each order in `orders`. - function getOrderFillableTakerAssetAmounts( - LibOrder.Order[] memory orders, - bytes[] memory orderSignatures, - address devUtilsAddress - ) - public - view - returns (uint256[] memory orderFillableTakerAssetAmounts) - { - orderFillableTakerAssetAmounts = new uint256[](orders.length); - for (uint256 i = 0; i != orders.length; i++) { - // Ignore orders with no signature or empty maker/taker amounts. - if (orderSignatures[i].length == 0 || - orders[i].makerAssetAmount == 0 || - orders[i].takerAssetAmount == 0) { - orderFillableTakerAssetAmounts[i] = 0; - continue; - } - // solhint-disable indent - (bool didSucceed, bytes memory resultData) = - devUtilsAddress - .staticcall - .gas(DEV_UTILS_CALL_GAS) - (abi.encodeWithSelector( - IDevUtils(devUtilsAddress).getOrderRelevantState.selector, - orders[i], - orderSignatures[i] - )); - // solhint-enable indent - if (!didSucceed) { - orderFillableTakerAssetAmounts[i] = 0; - continue; - } - ( - LibOrder.OrderInfo memory orderInfo, - uint256 fillableTakerAssetAmount, - bool isValidSignature - ) = abi.decode( - resultData, - (LibOrder.OrderInfo, uint256, bool) - ); - // The fillable amount is zero if the order is not fillable or if the - // signature is invalid. - if (orderInfo.orderStatus != LibOrder.OrderStatus.FILLABLE || - !isValidSignature) { - orderFillableTakerAssetAmounts[i] = 0; - } else { - orderFillableTakerAssetAmounts[i] = fillableTakerAssetAmount; - } - } - } - - /// @dev Queries the fillable taker asset amounts of native orders. - /// Effectively ignores orders that have empty signatures or - /// @param orders Native orders to query. - /// @param orderSignatures Signatures for each respective order in `orders`. - /// @param devUtilsAddress Address to the DevUtils contract. - /// @return orderFillableMakerAssetAmounts How much maker asset can be filled - /// by each order in `orders`. - function getOrderFillableMakerAssetAmounts( - LibOrder.Order[] memory orders, - bytes[] memory orderSignatures, - address devUtilsAddress - ) - public - view - returns (uint256[] memory orderFillableMakerAssetAmounts) - { - orderFillableMakerAssetAmounts = getOrderFillableTakerAssetAmounts( - orders, - orderSignatures, - devUtilsAddress - ); - // `orderFillableMakerAssetAmounts` now holds taker asset amounts, so - // convert them to maker asset amounts. - for (uint256 i = 0; i < orders.length; ++i) { - if (orderFillableMakerAssetAmounts[i] != 0) { - orderFillableMakerAssetAmounts[i] = LibMath.getPartialAmountCeil( - orderFillableMakerAssetAmounts[i], - orders[i].takerAssetAmount, - orders[i].makerAssetAmount - ); - } - } - } - - /// @dev Sample sell quotes from Kyber. - /// @param takerToken Address of the taker token (what to sell). - /// @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 sampleSellsFromKyberNetwork( - address takerToken, - address makerToken, - uint256[] memory takerTokenAmounts - ) - public - view - returns (uint256[] memory makerTokenAmounts) - { - _assertValidPair(makerToken, takerToken); - uint256 numSamples = takerTokenAmounts.length; - makerTokenAmounts = new uint256[](numSamples); - address wethAddress = _getWethAddress(); - uint256 value; - for (uint256 i = 0; i < numSamples; i++) { - if (takerToken == wethAddress || makerToken == wethAddress) { - // Direct ETH based trade - value = _sampleSellFromKyberNetwork(takerToken, makerToken, takerTokenAmounts[i]); - } else { - // Hop to ETH - value = _sampleSellFromKyberNetwork(takerToken, wethAddress, takerTokenAmounts[i]); - if (value != 0) { - value = _sampleSellFromKyberNetwork(wethAddress, makerToken, value); - } - } - makerTokenAmounts[i] = value; - } - } - - /// @dev Sample buy quotes from Kyber. - /// @param takerToken Address of the taker token (what to sell). - /// @param makerToken Address of the maker token (what to buy). - /// @param makerTokenAmounts Maker token buy amount for each sample. - /// @param opts `FakeBuyOptions` specifying target slippage and max iterations. - /// @return takerTokenAmounts Taker amounts sold at each maker token - /// amount. - function sampleBuysFromKyberNetwork( - address takerToken, - address makerToken, - uint256[] memory makerTokenAmounts, - FakeBuyOptions memory opts - ) - public - view - returns (uint256[] memory takerTokenAmounts) - { - return _sampleApproximateBuysFromSource( - takerToken, - makerToken, - makerTokenAmounts, - opts, - this.sampleSellsFromKyberNetwork.selector, - address(0) // PLP registry address - ); - } - - /// @dev Sample sell quotes from Eth2Dai/Oasis. - /// @param takerToken Address of the taker token (what to sell). - /// @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 sampleSellsFromEth2Dai( - address takerToken, - address makerToken, - uint256[] memory takerTokenAmounts - ) - public - view - returns (uint256[] memory makerTokenAmounts) - { - _assertValidPair(makerToken, takerToken); - uint256 numSamples = takerTokenAmounts.length; - makerTokenAmounts = new uint256[](numSamples); - for (uint256 i = 0; i < numSamples; i++) { - (bool didSucceed, bytes memory resultData) = - _getEth2DaiAddress().staticcall.gas(ETH2DAI_CALL_GAS)( - abi.encodeWithSelector( - IEth2Dai(0).getBuyAmount.selector, - makerToken, - takerToken, - takerTokenAmounts[i] - )); - uint256 buyAmount = 0; - if (didSucceed) { - buyAmount = abi.decode(resultData, (uint256)); - } else{ - break; - } - makerTokenAmounts[i] = buyAmount; - } - } - - /// @dev Sample sell quotes from Eth2Dai/Oasis using a hop to an intermediate token. - /// I.e WBTC/DAI via ETH or WBTC/ETH via DAI - /// @param takerToken Address of the taker token (what to sell). - /// @param makerToken Address of the maker token (what to buy). - /// @param intermediateToken Address of the token to hop to. - /// @param takerTokenAmounts Taker token sell amount for each sample. - /// @return makerTokenAmounts Maker amounts bought at each taker token - /// amount. - function sampleSellsFromEth2DaiHop( - address takerToken, - address makerToken, - address intermediateToken, - uint256[] memory takerTokenAmounts - ) - public - view - returns (uint256[] memory makerTokenAmounts) - { - if (makerToken == intermediateToken || takerToken == intermediateToken) { - return makerTokenAmounts; - } - uint256[] memory intermediateAmounts = sampleSellsFromEth2Dai(takerToken, intermediateToken, takerTokenAmounts); - makerTokenAmounts = sampleSellsFromEth2Dai(intermediateToken, makerToken, intermediateAmounts); - } - - /// @dev Sample buy quotes from Eth2Dai/Oasis. - /// @param takerToken Address of the taker token (what to sell). - /// @param makerToken Address of the maker token (what to buy). - /// @param takerTokenAmounts Maker token sell amount for each sample. - /// @return takerTokenAmounts Taker amounts sold at each maker token - /// amount. - function sampleBuysFromEth2Dai( - address takerToken, - address makerToken, - uint256[] memory makerTokenAmounts - ) - public - view - returns (uint256[] memory takerTokenAmounts) - { - _assertValidPair(makerToken, takerToken); - uint256 numSamples = makerTokenAmounts.length; - takerTokenAmounts = new uint256[](numSamples); - for (uint256 i = 0; i < numSamples; i++) { - (bool didSucceed, bytes memory resultData) = - _getEth2DaiAddress().staticcall.gas(ETH2DAI_CALL_GAS)( - abi.encodeWithSelector( - IEth2Dai(0).getPayAmount.selector, - takerToken, - makerToken, - makerTokenAmounts[i] - )); - uint256 sellAmount = 0; - if (didSucceed) { - sellAmount = abi.decode(resultData, (uint256)); - } else { - break; - } - takerTokenAmounts[i] = sellAmount; - } - } - - /// @dev Sample sell quotes from Uniswap. - /// @param takerToken Address of the taker token (what to sell). - /// @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 sampleSellsFromUniswap( - address takerToken, - address makerToken, - uint256[] memory takerTokenAmounts - ) - public - view - returns (uint256[] memory makerTokenAmounts) - { - _assertValidPair(makerToken, takerToken); - uint256 numSamples = takerTokenAmounts.length; - makerTokenAmounts = new uint256[](numSamples); - IUniswapExchangeQuotes takerTokenExchange = takerToken == _getWethAddress() ? - IUniswapExchangeQuotes(0) : _getUniswapExchange(takerToken); - IUniswapExchangeQuotes makerTokenExchange = makerToken == _getWethAddress() ? - IUniswapExchangeQuotes(0) : _getUniswapExchange(makerToken); - for (uint256 i = 0; i < numSamples; i++) { - bool didSucceed = true; - if (makerToken == _getWethAddress()) { - (makerTokenAmounts[i], didSucceed) = _callUniswapExchangePriceFunction( - address(takerTokenExchange), - takerTokenExchange.getTokenToEthInputPrice.selector, - takerTokenAmounts[i] - ); - } else if (takerToken == _getWethAddress()) { - (makerTokenAmounts[i], didSucceed) = _callUniswapExchangePriceFunction( - address(makerTokenExchange), - makerTokenExchange.getEthToTokenInputPrice.selector, - takerTokenAmounts[i] - ); - } else { - uint256 ethBought; - (ethBought, didSucceed) = _callUniswapExchangePriceFunction( - address(takerTokenExchange), - takerTokenExchange.getTokenToEthInputPrice.selector, - takerTokenAmounts[i] - ); - if (ethBought != 0) { - (makerTokenAmounts[i], didSucceed) = _callUniswapExchangePriceFunction( - address(makerTokenExchange), - makerTokenExchange.getEthToTokenInputPrice.selector, - ethBought - ); - } else { - makerTokenAmounts[i] = 0; - } - } - if (!didSucceed) { - break; - } - } - } - - /// @dev Sample buy quotes from Uniswap. - /// @param takerToken Address of the taker token (what to sell). - /// @param makerToken Address of the maker token (what to buy). - /// @param makerTokenAmounts Maker token sell amount for each sample. - /// @return takerTokenAmounts Taker amounts sold at each maker token - /// amount. - function sampleBuysFromUniswap( - address takerToken, - address makerToken, - uint256[] memory makerTokenAmounts - ) - public - view - returns (uint256[] memory takerTokenAmounts) - { - _assertValidPair(makerToken, takerToken); - uint256 numSamples = makerTokenAmounts.length; - takerTokenAmounts = new uint256[](numSamples); - IUniswapExchangeQuotes takerTokenExchange = takerToken == _getWethAddress() ? - IUniswapExchangeQuotes(0) : _getUniswapExchange(takerToken); - IUniswapExchangeQuotes makerTokenExchange = makerToken == _getWethAddress() ? - IUniswapExchangeQuotes(0) : _getUniswapExchange(makerToken); - for (uint256 i = 0; i < numSamples; i++) { - bool didSucceed = true; - if (makerToken == _getWethAddress()) { - (takerTokenAmounts[i], didSucceed) = _callUniswapExchangePriceFunction( - address(takerTokenExchange), - takerTokenExchange.getTokenToEthOutputPrice.selector, - makerTokenAmounts[i] - ); - } else if (takerToken == _getWethAddress()) { - (takerTokenAmounts[i], didSucceed) = _callUniswapExchangePriceFunction( - address(makerTokenExchange), - makerTokenExchange.getEthToTokenOutputPrice.selector, - makerTokenAmounts[i] - ); - } else { - uint256 ethSold; - (ethSold, didSucceed) = _callUniswapExchangePriceFunction( - address(makerTokenExchange), - makerTokenExchange.getEthToTokenOutputPrice.selector, - makerTokenAmounts[i] - ); - if (ethSold != 0) { - (takerTokenAmounts[i], didSucceed) = _callUniswapExchangePriceFunction( - address(takerTokenExchange), - takerTokenExchange.getTokenToEthOutputPrice.selector, - ethSold - ); - } else { - takerTokenAmounts[i] = 0; - } - } - if (!didSucceed) { - break; - } - } - } - - /// @dev Sample sell quotes from Curve. - /// @param curveAddress Address of the Curve contract. - /// @param fromTokenIdx Index of the taker token (what to sell). - /// @param toTokenIdx Index 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 sampleSellsFromCurve( - address curveAddress, - int128 fromTokenIdx, - int128 toTokenIdx, - uint256[] memory takerTokenAmounts - ) - public - view - returns (uint256[] memory makerTokenAmounts) - { - uint256 numSamples = takerTokenAmounts.length; - makerTokenAmounts = new uint256[](numSamples); - for (uint256 i = 0; i < numSamples; i++) { - (bool didSucceed, bytes memory resultData) = - curveAddress.staticcall.gas(CURVE_CALL_GAS)( - abi.encodeWithSelector( - ICurve(0).get_dy_underlying.selector, - fromTokenIdx, - toTokenIdx, - takerTokenAmounts[i] - )); - uint256 buyAmount = 0; - if (didSucceed) { - buyAmount = abi.decode(resultData, (uint256)); - } else { - break; - } - makerTokenAmounts[i] = buyAmount; - } - } - - /// @dev Sample buy quotes from Curve. - /// @param curveAddress Address of the Curve contract. - /// @param fromTokenIdx Index of the taker token (what to sell). - /// @param toTokenIdx Index of the maker token (what to buy). - /// @param makerTokenAmounts Maker token buy amount for each sample. - /// @return takerTokenAmounts Taker amounts sold at each maker token - /// amount. - function sampleBuysFromCurve( - address curveAddress, - int128 fromTokenIdx, - int128 toTokenIdx, - uint256[] memory makerTokenAmounts - ) - public - view - returns (uint256[] memory takerTokenAmounts) - { - uint256 numSamples = makerTokenAmounts.length; - takerTokenAmounts = new uint256[](numSamples); - for (uint256 i = 0; i < numSamples; i++) { - (bool didSucceed, bytes memory resultData) = - curveAddress.staticcall.gas(CURVE_CALL_GAS)( - abi.encodeWithSelector( - ICurve(0).get_dx_underlying.selector, - fromTokenIdx, - toTokenIdx, - makerTokenAmounts[i] - )); - uint256 sellAmount = 0; - if (didSucceed) { - sellAmount = abi.decode(resultData, (uint256)); - } else { - break; - } - takerTokenAmounts[i] = sellAmount; - } - } - - /// @dev Sample sell 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). - /// @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 sampleSellsFromLiquidityProviderRegistry( - address registryAddress, - address takerToken, - 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); - - // Query registry for provider address. - address providerAddress = getLiquidityProviderFromRegistry( - registryAddress, - takerToken, - makerToken - ); - // If provider doesn't exist, return all zeros. - if (providerAddress == address(0)) { - return makerTokenAmounts; - } - - for (uint256 i = 0; i < numSamples; i++) { - (bool didSucceed, bytes memory resultData) = - providerAddress.staticcall.gas(DEFAULT_CALL_GAS)( - abi.encodeWithSelector( - ILiquidityProvider(0).getSellQuote.selector, - takerToken, - 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 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). - /// @param makerToken Address of the maker token (what to buy). - /// @param makerTokenAmounts Maker token buy amount for each sample. - /// @param opts `FakeBuyOptions` specifying target slippage and max iterations. - /// @return takerTokenAmounts Taker amounts sold at each maker token - /// amount. - function sampleBuysFromLiquidityProviderRegistry( - address registryAddress, - address takerToken, - address makerToken, - uint256[] memory makerTokenAmounts, - FakeBuyOptions memory opts - ) - public - view - returns (uint256[] memory takerTokenAmounts) - { - return _sampleApproximateBuysFromSource( - takerToken, - makerToken, - makerTokenAmounts, - opts, - this.sampleSellsFromLiquidityProviderRegistry.selector, - registryAddress - ); - } - - /// @dev Returns the address of a liquidity provider for the given market - /// (takerToken, makerToken), from a registry of liquidity providers. - /// Returns address(0) if no such provider exists in the registry. - /// @param takerToken Taker asset managed by liquidity provider. - /// @param makerToken Maker asset managed by liquidity provider. - /// @return providerAddress Address of the liquidity provider. - function getLiquidityProviderFromRegistry( - address registryAddress, - address takerToken, - address makerToken - ) - public - view - returns (address providerAddress) - { - bytes memory callData = abi.encodeWithSelector( - ILiquidityProviderRegistry(0).getLiquidityProviderForMarket.selector, - takerToken, - makerToken - ); - (bool didSucceed, bytes memory returnData) = registryAddress.staticcall(callData); - if (didSucceed && returnData.length == 32) { - return LibBytes.readAddress(returnData, 12); - } - } - - /// @dev Sample sell quotes from UniswapV2. - /// @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[] memory path, - uint256[] memory takerTokenAmounts - ) - public - view - returns (uint256[] memory makerTokenAmounts) - { - uint256 numSamples = takerTokenAmounts.length; - makerTokenAmounts = new uint256[](numSamples); - for (uint256 i = 0; i < numSamples; i++) { - (bool didSucceed, bytes memory resultData) = - _getUniswapV2Router01Address().staticcall.gas(UNISWAPV2_CALL_GAS)( - abi.encodeWithSelector( - IUniswapV2Router01(0).getAmountsOut.selector, - takerTokenAmounts[i], - path - )); - uint256 buyAmount = 0; - if (didSucceed) { - // solhint-disable-next-line indent - buyAmount = abi.decode(resultData, (uint256[]))[path.length - 1]; - } else { - break; - } - makerTokenAmounts[i] = buyAmount; - } - } - - /// @dev Sample buy quotes from UniswapV2. - /// @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[] memory path, - uint256[] memory makerTokenAmounts - ) - public - view - returns (uint256[] memory takerTokenAmounts) - { - uint256 numSamples = makerTokenAmounts.length; - takerTokenAmounts = new uint256[](numSamples); - for (uint256 i = 0; i < numSamples; i++) { - (bool didSucceed, bytes memory resultData) = - _getUniswapV2Router01Address().staticcall.gas(UNISWAPV2_CALL_GAS)( - abi.encodeWithSelector( - IUniswapV2Router01(0).getAmountsIn.selector, - makerTokenAmounts[i], - path - )); - uint256 sellAmount = 0; - if (didSucceed) { - // solhint-disable-next-line indent - sellAmount = abi.decode(resultData, (uint256[]))[0]; - } else { - break; - } - takerTokenAmounts[i] = sellAmount; - } - } - - /// @dev Overridable way to get token decimals. - /// @param tokenAddress Address of the token. - /// @return decimals The decimal places for the token. - function _getTokenDecimals(address tokenAddress) - internal - view - returns (uint8 decimals) - { - return LibERC20Token.decimals(tokenAddress); - } - - /// @dev Gracefully calls a Uniswap pricing function. - /// @param uniswapExchangeAddress Address of an `IUniswapExchangeQuotes` exchange. - /// @param functionSelector Selector of the target function. - /// @param inputAmount Quantity parameter particular to the pricing function. - /// @return outputAmount The returned amount from the function call. Will be - /// zero if the call fails or if `uniswapExchangeAddress` is zero. - function _callUniswapExchangePriceFunction( - address uniswapExchangeAddress, - bytes4 functionSelector, - uint256 inputAmount - ) - private - view - returns (uint256 outputAmount, bool didSucceed) - { - if (uniswapExchangeAddress == address(0)) { - return (outputAmount, didSucceed); - } - bytes memory resultData; - (didSucceed, resultData) = - uniswapExchangeAddress.staticcall.gas(UNISWAP_CALL_GAS)( - abi.encodeWithSelector( - functionSelector, - inputAmount - )); - if (didSucceed) { - outputAmount = abi.decode(resultData, (uint256)); - } - } - - /// @dev Retrive an existing Uniswap exchange contract. - /// Throws if the exchange does not exist. - /// @param tokenAddress Address of the token contract. - /// @return exchange `IUniswapExchangeQuotes` for the token. - function _getUniswapExchange(address tokenAddress) - private - view - returns (IUniswapExchangeQuotes exchange) - { - exchange = IUniswapExchangeQuotes( - address(IUniswapExchangeFactory(_getUniswapExchangeFactoryAddress()) - .getExchange(tokenAddress)) - ); - } - - /// @dev Assert that the tokens in a trade pair are valid. - /// @param makerToken Address of the maker token. - /// @param takerToken Address of the taker token. - function _assertValidPair(address makerToken, address takerToken) - private - pure - { - require(makerToken != takerToken, "ERC20BridgeSampler/INVALID_TOKEN_PAIR"); - } - - function _sampleSellForApproximateBuy( - address takerToken, - address makerToken, - uint256 takerTokenAmount, - bytes4 selector, - address plpRegistryAddress - ) - private - view - returns (uint256 makerTokenAmount) - { - bytes memory callData; - uint256[] memory tmpTakerAmounts = new uint256[](1); - tmpTakerAmounts[0] = takerTokenAmount; - if (selector == this.sampleSellsFromKyberNetwork.selector) { - callData = abi.encodeWithSelector( - this.sampleSellsFromKyberNetwork.selector, - takerToken, - makerToken, - tmpTakerAmounts - ); - } else { - callData = abi.encodeWithSelector( - this.sampleSellsFromLiquidityProviderRegistry.selector, - plpRegistryAddress, - takerToken, - makerToken, - tmpTakerAmounts - ); - } - (bool success, bytes memory resultData) = address(this).staticcall(callData); - if (!success) { - return 0; - } - // solhint-disable indent - makerTokenAmount = abi.decode(resultData, (uint256[]))[0]; - } - - function _sampleApproximateBuysFromSource( - address takerToken, - address makerToken, - uint256[] memory makerTokenAmounts, - FakeBuyOptions memory opts, - bytes4 selector, - address plpRegistryAddress - ) - private - view - returns (uint256[] memory takerTokenAmounts) - { - _assertValidPair(makerToken, takerToken); - if (makerTokenAmounts.length == 0) { - return takerTokenAmounts; - } - uint256 sellAmount; - uint256 buyAmount; - uint256 slippageFromTarget; - takerTokenAmounts = new uint256[](makerTokenAmounts.length); - sellAmount = _sampleSellForApproximateBuy( - makerToken, - takerToken, - makerTokenAmounts[0], - selector, - plpRegistryAddress - ); - - if (sellAmount == 0) { - return takerTokenAmounts; - } - - buyAmount = _sampleSellForApproximateBuy( - takerToken, - makerToken, - sellAmount, - selector, - plpRegistryAddress - ); - if (buyAmount == 0) { - return takerTokenAmounts; - } - - for (uint256 i = 0; i < makerTokenAmounts.length; i++) { - for (uint256 iter = 0; iter < opts.maxIterations; iter++) { - // adjustedSellAmount = previousSellAmount * (target/actual) * JUMP_MULTIPLIER - sellAmount = LibMath.getPartialAmountCeil( - makerTokenAmounts[i], - buyAmount, - sellAmount - ); - sellAmount = LibMath.getPartialAmountCeil( - (10000 + opts.targetSlippageBps), - 10000, - sellAmount - ); - uint256 _buyAmount = _sampleSellForApproximateBuy( - takerToken, - makerToken, - sellAmount, - selector, - plpRegistryAddress - ); - if (_buyAmount == 0) { - break; - } - // We re-use buyAmount next iteration, only assign if it is - // non zero - buyAmount = _buyAmount; - // If we've reached our goal, exit early - if (buyAmount >= makerTokenAmounts[i]) { - uint256 slippageFromTarget = (buyAmount - makerTokenAmounts[i]) * 10000 / - makerTokenAmounts[i]; - if (slippageFromTarget <= opts.targetSlippageBps) { - break; - } - } - } - // We do our best to close in on the requested amount, but we can either over buy or under buy and exit - // if we hit a max iteration limit - // We scale the sell amount to get the approximate target - takerTokenAmounts[i] = LibMath.getPartialAmountCeil( - makerTokenAmounts[i], - buyAmount, - sellAmount - ); - } - } - - function _appendToList(bytes32[] memory list, bytes32 item) private view returns (bytes32[] memory appendedList) - { - appendedList = new bytes32[](list.length + 1); - for (uint256 i = 0; i < list.length; i++) { - appendedList[i] = list[i]; - } - appendedList[appendedList.length - 1] = item; - } - - function _getKyberAddresses() - private - view - returns (IKyberHintHandler kyberHint, IKyberStorage kyberStorage) - { - (, , kyberHint, kyberStorage, ,) = IKyberNetwork( - IKyberNetworkProxy(_getKyberNetworkProxyAddress()).kyberNetwork()).getContracts(); - return (IKyberHintHandler(kyberHint), IKyberStorage(kyberStorage)); - } - - function _sampleSellFromKyberNetwork( - address takerToken, - address makerToken, - uint256 takerTokenAmount - ) - private - view - returns (uint256 makerTokenAmount) - { - (IKyberHintHandler kyberHint, IKyberStorage kyberStorage) = _getKyberAddresses(); - // Ban reserves which can clash with our internal aggregation - bytes32[] memory reserveIds = kyberStorage.getReserveIdsPerTokenSrc( - takerToken == _getWethAddress() ? makerToken : takerToken - ); - bytes32[] memory bannedReserveIds = new bytes32[](0); - // Poor mans resize and append - for (uint256 i = 0; i < reserveIds.length; i++) { - if ( - reserveIds[i] == kyberStorage.getReserveId(KYBER_UNISWAP_RESERVE) || - reserveIds[i] == kyberStorage.getReserveId(KYBER_UNISWAPV2_RESERVE) || - reserveIds[i] == kyberStorage.getReserveId(KYBER_ETH2DAI_RESERVE) - ) { - bannedReserveIds = _appendToList(bannedReserveIds, reserveIds[i]); - } - } - // Sampler either detects X->ETH/ETH->X - // or subsamples as X->ETH-Y. So token->token here is not possible - bytes memory hint; - if (takerToken == _getWethAddress()) { - // ETH -> X - hint = kyberHint.buildEthToTokenHint( - makerToken, - IKyberHintHandler.TradeType.MaskOut, - bannedReserveIds, - new uint256[](0)); - } else { - // X->ETH - hint = kyberHint.buildEthToTokenHint( - takerToken, - IKyberHintHandler.TradeType.MaskOut, - bannedReserveIds, - new uint256[](0)); - } - (bool didSucceed, bytes memory resultData) = - _getKyberNetworkProxyAddress().staticcall.gas(KYBER_CALL_GAS)( - abi.encodeWithSelector( - IKyberNetworkProxy(0).getExpectedRateAfterFee.selector, - takerToken == _getWethAddress() ? KYBER_ETH_ADDRESS : takerToken, - makerToken == _getWethAddress() ? KYBER_ETH_ADDRESS : makerToken, - takerTokenAmount, - 0, // fee - hint - )); - uint256 rate = 0; - if (didSucceed) { - (rate) = abi.decode(resultData, (uint256)); - } else { - return 0; - } - - uint256 makerTokenDecimals = _getTokenDecimals(makerToken); - uint256 takerTokenDecimals = _getTokenDecimals(takerToken); - makerTokenAmount = - rate * - takerTokenAmount * - 10 ** makerTokenDecimals / - 10 ** takerTokenDecimals / - 10 ** 18; - return makerTokenAmount; - } } diff --git a/contracts/erc20-bridge-sampler/contracts/src/Eth2DaiSampler.sol b/contracts/erc20-bridge-sampler/contracts/src/Eth2DaiSampler.sol new file mode 100644 index 0000000000..13ca9075cf --- /dev/null +++ b/contracts/erc20-bridge-sampler/contracts/src/Eth2DaiSampler.sol @@ -0,0 +1,140 @@ +/* + + Copyright 2019 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; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; +import "./IEth2Dai.sol"; +import "./SamplerUtils.sol"; + + +contract Eth2DaiSampler is + DeploymentConstants, + SamplerUtils +{ + /// @dev Base gas limit for Eth2Dai calls. + uint256 constant private ETH2DAI_CALL_GAS = 1000e3; // 1m + + /// @dev Sample sell quotes from Eth2Dai/Oasis. + /// @param takerToken Address of the taker token (what to sell). + /// @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 sampleSellsFromEth2Dai( + address takerToken, + address makerToken, + uint256[] memory takerTokenAmounts + ) + public + view + returns (uint256[] memory makerTokenAmounts) + { + _assertValidPair(makerToken, takerToken); + uint256 numSamples = takerTokenAmounts.length; + makerTokenAmounts = new uint256[](numSamples); + for (uint256 i = 0; i < numSamples; i++) { + (bool didSucceed, bytes memory resultData) = + _getEth2DaiAddress().staticcall.gas(ETH2DAI_CALL_GAS)( + abi.encodeWithSelector( + IEth2Dai(0).getBuyAmount.selector, + makerToken, + takerToken, + takerTokenAmounts[i] + )); + uint256 buyAmount = 0; + if (didSucceed) { + buyAmount = abi.decode(resultData, (uint256)); + } else{ + break; + } + makerTokenAmounts[i] = buyAmount; + } + } + + /// @dev Sample sell quotes from Eth2Dai/Oasis using a hop to an intermediate token. + /// I.e WBTC/DAI via ETH or WBTC/ETH via DAI + /// @param takerToken Address of the taker token (what to sell). + /// @param makerToken Address of the maker token (what to buy). + /// @param intermediateToken Address of the token to hop to. + /// @param takerTokenAmounts Taker token sell amount for each sample. + /// @return makerTokenAmounts Maker amounts bought at each taker token + /// amount. + function sampleSellsFromEth2DaiHop( + address takerToken, + address makerToken, + address intermediateToken, + uint256[] memory takerTokenAmounts + ) + public + view + returns (uint256[] memory makerTokenAmounts) + { + if (makerToken == intermediateToken || takerToken == intermediateToken) { + return makerTokenAmounts; + } + uint256[] memory intermediateAmounts = sampleSellsFromEth2Dai( + takerToken, + intermediateToken, + takerTokenAmounts + ); + makerTokenAmounts = sampleSellsFromEth2Dai( + intermediateToken, + makerToken, + intermediateAmounts + ); + } + + /// @dev Sample buy quotes from Eth2Dai/Oasis. + /// @param takerToken Address of the taker token (what to sell). + /// @param makerToken Address of the maker token (what to buy). + /// @param takerTokenAmounts Maker token sell amount for each sample. + /// @return takerTokenAmounts Taker amounts sold at each maker token + /// amount. + function sampleBuysFromEth2Dai( + address takerToken, + address makerToken, + uint256[] memory makerTokenAmounts + ) + public + view + returns (uint256[] memory takerTokenAmounts) + { + _assertValidPair(makerToken, takerToken); + uint256 numSamples = makerTokenAmounts.length; + takerTokenAmounts = new uint256[](numSamples); + for (uint256 i = 0; i < numSamples; i++) { + (bool didSucceed, bytes memory resultData) = + _getEth2DaiAddress().staticcall.gas(ETH2DAI_CALL_GAS)( + abi.encodeWithSelector( + IEth2Dai(0).getPayAmount.selector, + takerToken, + makerToken, + makerTokenAmounts[i] + )); + uint256 sellAmount = 0; + if (didSucceed) { + sellAmount = abi.decode(resultData, (uint256)); + } else { + break; + } + takerTokenAmounts[i] = sellAmount; + } + } +} diff --git a/contracts/erc20-bridge-sampler/contracts/src/ICurve.sol b/contracts/erc20-bridge-sampler/contracts/src/ICurve.sol index ac2645106b..13a0d75ea0 100644 --- a/contracts/erc20-bridge-sampler/contracts/src/ICurve.sol +++ b/contracts/erc20-bridge-sampler/contracts/src/ICurve.sol @@ -22,22 +22,6 @@ pragma solidity ^0.5.9; // solhint-disable func-name-mixedcase interface ICurve { - /// @dev Sell `sellAmount` of `fromToken` token and receive `toToken` token. - /// This function exists on early versions of Curve (USDC/DAI) - /// @param i The token index being sold. - /// @param j The token index being bought. - /// @param sellAmount The amount of token being bought. - /// @param minBuyAmount The minimum buy amount of the token being bought. - /// @param deadline The time in seconds when this operation should expire. - function exchange_underlying( - int128 i, - int128 j, - uint256 sellAmount, - uint256 minBuyAmount, - uint256 deadline - ) - external; - /// @dev Sell `sellAmount` of `fromToken` token and receive `toToken` token. /// This function exists on later versions of Curve (USDC/DAI/USDT) /// @param i The token index being sold. diff --git a/contracts/erc20-bridge-sampler/contracts/src/IERC20BridgeSampler.sol b/contracts/erc20-bridge-sampler/contracts/src/IERC20BridgeSampler.sol deleted file mode 100644 index 69a6895c76..0000000000 --- a/contracts/erc20-bridge-sampler/contracts/src/IERC20BridgeSampler.sol +++ /dev/null @@ -1,293 +0,0 @@ -/* - - Copyright 2019 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; -pragma experimental ABIEncoderV2; - -import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol"; - - -interface IERC20BridgeSampler { - - struct FakeBuyOptions { - uint256 targetSlippageBps; - uint256 maxIterations; - } - - /// @dev Call multiple public functions on this contract in a single transaction. - /// @param callDatas ABI-encoded call data for each function call. - /// @return callResults ABI-encoded results data for each call. - function batchCall(bytes[] calldata callDatas) - external - view - returns (bytes[] memory callResults); - - /// @dev Queries the fillable taker asset amounts of native orders. - /// @param orders Native orders to query. - /// @param orderSignatures Signatures for each respective order in `orders`. - /// @param devUtilsAddress Address to the DevUtils contract. - /// @return orderFillableTakerAssetAmounts How much taker asset can be filled - /// by each order in `orders`. - function getOrderFillableTakerAssetAmounts( - LibOrder.Order[] calldata orders, - bytes[] calldata orderSignatures, - address devUtilsAddress - ) - external - view - returns (uint256[] memory orderFillableTakerAssetAmounts); - - /// @dev Queries the fillable maker asset amounts of native orders. - /// @param orders Native orders to query. - /// @param orderSignatures Signatures for each respective order in `orders`. - /// @param devUtilsAddress Address to the DevUtils contract. - /// @return orderFillableMakerAssetAmounts How much maker asset can be filled - /// by each order in `orders`. - function getOrderFillableMakerAssetAmounts( - LibOrder.Order[] calldata orders, - bytes[] calldata orderSignatures, - address devUtilsAddress - ) - external - view - returns (uint256[] memory orderFillableMakerAssetAmounts); - - /// @dev Sample sell quotes from Kyber. - /// @param takerToken Address of the taker token (what to sell). - /// @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 sampleSellsFromKyberNetwork( - address takerToken, - address makerToken, - uint256[] calldata takerTokenAmounts - ) - external - view - returns (uint256[] memory makerTokenAmounts); - - /// @dev Sample buy quotes from Kyber. - /// @param takerToken Address of the taker token (what to sell). - /// @param makerToken Address of the maker token (what to buy). - /// @param makerTokenAmounts Maker token buy amount for each sample. - /// @param opts `FakeBuyOptions` specifying target slippage and max iterations. - /// @return takerTokenAmounts Taker amounts sold at each maker token - /// amount. - function sampleBuysFromKyberNetwork( - address takerToken, - address makerToken, - uint256[] calldata makerTokenAmounts, - FakeBuyOptions calldata opts - ) - external - view - returns (uint256[] memory takerTokenAmounts); - - /// @dev Sample sell quotes from Eth2Dai/Oasis. - /// @param takerToken Address of the taker token (what to sell). - /// @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 sampleSellsFromEth2Dai( - address takerToken, - address makerToken, - uint256[] calldata takerTokenAmounts - ) - external - view - returns (uint256[] memory makerTokenAmounts); - - /// @dev Sample sell quotes from Uniswap. - /// @param takerToken Address of the taker token (what to sell). - /// @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 sampleSellsFromUniswap( - address takerToken, - address makerToken, - uint256[] calldata takerTokenAmounts - ) - external - view - returns (uint256[] memory makerTokenAmounts); - - /// @dev Sample buy quotes from Uniswap. - /// @param takerToken Address of the taker token (what to sell). - /// @param makerToken Address of the maker token (what to buy). - /// @param makerTokenAmounts Maker token buy amount for each sample. - /// @return takerTokenAmounts Taker amounts sold at each maker token - /// amount. - function sampleBuysFromUniswap( - address takerToken, - address makerToken, - uint256[] calldata makerTokenAmounts - ) - external - view - returns (uint256[] memory takerTokenAmounts); - - /// @dev Sample buy quotes from Eth2Dai/Oasis. - /// @param takerToken Address of the taker token (what to sell). - /// @param makerToken Address of the maker token (what to buy). - /// @param makerTokenAmounts Maker token buy amount for each sample. - /// @return takerTokenAmounts Taker amounts sold at each maker token - /// amount. - function sampleBuysFromEth2Dai( - address takerToken, - address makerToken, - uint256[] calldata makerTokenAmounts - ) - external - view - returns (uint256[] memory takerTokenAmounts); - - /// @dev Sample sell quotes from Curve. - /// @param curveAddress Address of the Curve contract. - /// @param fromTokenIdx Index of the taker token (what to sell). - /// @param toTokenIdx Index 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 sampleSellsFromCurve( - address curveAddress, - int128 fromTokenIdx, - int128 toTokenIdx, - uint256[] calldata takerTokenAmounts - ) - external - view - returns (uint256[] memory makerTokenAmounts); - - /// @dev Sample buy quotes from Curve. - /// @param curveAddress Address of the Curve contract. - /// @param fromTokenIdx Index of the taker token (what to sell). - /// @param toTokenIdx Index of the maker token (what to buy). - /// @param makerTokenAmounts Maker token buy amount for each sample. - /// @return takerTokenAmounts Taker amounts sold at each maker token - /// amount. - function sampleBuysFromCurve( - address curveAddress, - int128 fromTokenIdx, - int128 toTokenIdx, - uint256[] calldata makerTokenAmounts - ) - external - view - returns (uint256[] memory takerTokenAmounts); - - /// @dev Sample sell 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). - /// @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 sampleSellsFromLiquidityProviderRegistry( - address registryAddress, - address takerToken, - address makerToken, - uint256[] calldata takerTokenAmounts - ) - external - 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). - /// @param makerToken Address of the maker token (what to buy). - /// @param makerTokenAmounts Maker token buy amount for each sample. - /// @param opts `FakeBuyOptions` specifying target slippage and max iterations. - /// @return takerTokenAmounts Taker amounts sold at each maker token - /// amount. - function sampleBuysFromLiquidityProviderRegistry( - address registryAddress, - address takerToken, - address makerToken, - uint256[] calldata makerTokenAmounts, - FakeBuyOptions calldata opts - - ) - external - view - returns (uint256[] memory takerTokenAmounts); - - /// @dev Returns the address of a liquidity provider for the given market - /// (takerToken, makerToken), from a registry of liquidity providers. - /// Returns address(0) if no such provider exists in the registry. - /// @param takerToken Taker asset managed by liquidity provider. - /// @param makerToken Maker asset managed by liquidity provider. - /// @return providerAddress Address of the liquidity provider. - function getLiquidityProviderFromRegistry( - address registryAddress, - address takerToken, - address makerToken - ) - external - view - returns (address providerAddress); - - /// @dev Sample sell quotes from UniswapV2. - /// @param path Token route. - /// @param takerTokenAmounts Taker token sell amount for each sample. - /// @return makerTokenAmounts Maker amounts bought at each taker token - /// amount. - function sampleSellsFromUniswapV2( - address[] calldata path, - uint256[] calldata takerTokenAmounts - ) - external - view - returns (uint256[] memory makerTokenAmounts); - - /// @dev Sample buy quotes from UniswapV2. - /// @param path Token route. - /// @param makerTokenAmounts Maker token buy amount for each sample. - /// @return takerTokenAmounts Taker amounts sold at each maker token - /// amount. - function sampleBuysFromUniswapV2( - address[] calldata path, - uint256[] calldata makerTokenAmounts - ) - external - view - returns (uint256[] memory takerTokenAmounts); -} diff --git a/contracts/erc20-bridge-sampler/contracts/src/KyberSampler.sol b/contracts/erc20-bridge-sampler/contracts/src/KyberSampler.sol new file mode 100644 index 0000000000..59fca06978 --- /dev/null +++ b/contracts/erc20-bridge-sampler/contracts/src/KyberSampler.sol @@ -0,0 +1,218 @@ +/* + + Copyright 2019 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; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; +import "./IKyberNetwork.sol"; +import "./IKyberNetworkProxy.sol"; +import "./IKyberStorage.sol"; +import "./IKyberHintHandler.sol"; +import "./ApproximateBuys.sol"; +import "./SamplerUtils.sol"; + + +contract KyberSampler is + DeploymentConstants, + SamplerUtils, + ApproximateBuys +{ + /// @dev Gas limit for Kyber calls. + uint256 constant private KYBER_CALL_GAS = 1500e3; // 1.5m + /// @dev The Kyber Uniswap Reserve address + address constant private KYBER_UNISWAP_RESERVE = 0x31E085Afd48a1d6e51Cc193153d625e8f0514C7F; + /// @dev The Kyber Uniswap V2 Reserve address + address constant private KYBER_UNISWAPV2_RESERVE = 0x10908C875D865C66f271F5d3949848971c9595C9; + /// @dev The Kyber Eth2Dai Reserve address + address constant private KYBER_ETH2DAI_RESERVE = 0x1E158c0e93c30d24e918Ef83d1e0bE23595C3c0f; + + /// @dev Sample sell quotes from Kyber. + /// @param takerToken Address of the taker token (what to sell). + /// @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 sampleSellsFromKyberNetwork( + address takerToken, + address makerToken, + uint256[] memory takerTokenAmounts + ) + public + view + returns (uint256[] memory makerTokenAmounts) + { + _assertValidPair(makerToken, takerToken); + uint256 numSamples = takerTokenAmounts.length; + makerTokenAmounts = new uint256[](numSamples); + address wethAddress = _getWethAddress(); + uint256 value; + for (uint256 i = 0; i < numSamples; i++) { + if (takerToken == wethAddress || makerToken == wethAddress) { + // Direct ETH based trade + value = _sampleSellFromKyberNetwork(takerToken, makerToken, takerTokenAmounts[i]); + } else { + // Hop to ETH + value = _sampleSellFromKyberNetwork(takerToken, wethAddress, takerTokenAmounts[i]); + if (value != 0) { + value = _sampleSellFromKyberNetwork(wethAddress, makerToken, value); + } + } + makerTokenAmounts[i] = value; + } + } + + /// @dev Sample buy quotes from Kyber. + /// @param takerToken Address of the taker token (what to sell). + /// @param makerToken Address of the maker token (what to buy). + /// @param makerTokenAmounts Maker token buy amount for each sample. + /// @return takerTokenAmounts Taker amounts sold at each maker token + /// amount. + function sampleBuysFromKyberNetwork( + address takerToken, + address makerToken, + uint256[] memory makerTokenAmounts + ) + public + view + returns (uint256[] memory takerTokenAmounts) + { + _assertValidPair(makerToken, takerToken); + return _sampleApproximateBuys( + ApproximateBuyQuoteOpts({ + makerTokenData: abi.encode(makerToken), + takerTokenData: abi.encode(takerToken), + getSellQuoteCallback: _sampleSellForApproximateBuyFromKyber + }), + makerTokenAmounts + ); + } + + function _sampleSellForApproximateBuyFromKyber( + bytes memory takerTokenData, + bytes memory makerTokenData, + uint256 sellAmount + ) + private + view + returns (uint256 buyAmount) + { + (bool success, bytes memory resultData) = + address(this).staticcall(abi.encodeWithSelector( + this.sampleSellsFromKyberNetwork.selector, + abi.decode(takerTokenData, (address)), + abi.decode(makerTokenData, (address)), + _toSingleValueArray(sellAmount) + )); + if (!success) { + return 0; + } + // solhint-disable-next-line indent + return abi.decode(resultData, (uint256[]))[0]; + } + + function _appendToList(bytes32[] memory list, bytes32 item) private view returns (bytes32[] memory appendedList) + { + appendedList = new bytes32[](list.length + 1); + for (uint256 i = 0; i < list.length; i++) { + appendedList[i] = list[i]; + } + appendedList[appendedList.length - 1] = item; + } + + function _getKyberAddresses() + private + view + returns (IKyberHintHandler kyberHint, IKyberStorage kyberStorage) + { + (, , kyberHint, kyberStorage, ,) = IKyberNetwork( + IKyberNetworkProxy(_getKyberNetworkProxyAddress()).kyberNetwork()).getContracts(); + return (IKyberHintHandler(kyberHint), IKyberStorage(kyberStorage)); + } + + function _sampleSellFromKyberNetwork( + address takerToken, + address makerToken, + uint256 takerTokenAmount + ) + private + view + returns (uint256 makerTokenAmount) + { + (IKyberHintHandler kyberHint, IKyberStorage kyberStorage) = _getKyberAddresses(); + // Ban reserves which can clash with our internal aggregation + bytes32[] memory reserveIds = kyberStorage.getReserveIdsPerTokenSrc( + takerToken == _getWethAddress() ? makerToken : takerToken + ); + bytes32[] memory bannedReserveIds = new bytes32[](0); + // Poor mans resize and append + for (uint256 i = 0; i < reserveIds.length; i++) { + if ( + reserveIds[i] == kyberStorage.getReserveId(KYBER_UNISWAP_RESERVE) || + reserveIds[i] == kyberStorage.getReserveId(KYBER_UNISWAPV2_RESERVE) || + reserveIds[i] == kyberStorage.getReserveId(KYBER_ETH2DAI_RESERVE) + ) { + bannedReserveIds = _appendToList(bannedReserveIds, reserveIds[i]); + } + } + // Sampler either detects X->ETH/ETH->X + // or subsamples as X->ETH-Y. So token->token here is not possible + bytes memory hint; + if (takerToken == _getWethAddress()) { + // ETH -> X + hint = kyberHint.buildEthToTokenHint( + makerToken, + IKyberHintHandler.TradeType.MaskOut, + bannedReserveIds, + new uint256[](0)); + } else { + // X->ETH + hint = kyberHint.buildEthToTokenHint( + takerToken, + IKyberHintHandler.TradeType.MaskOut, + bannedReserveIds, + new uint256[](0)); + } + (bool didSucceed, bytes memory resultData) = + _getKyberNetworkProxyAddress().staticcall.gas(KYBER_CALL_GAS)( + abi.encodeWithSelector( + IKyberNetworkProxy(0).getExpectedRateAfterFee.selector, + takerToken == _getWethAddress() ? KYBER_ETH_ADDRESS : takerToken, + makerToken == _getWethAddress() ? KYBER_ETH_ADDRESS : makerToken, + takerTokenAmount, + 0, // fee + hint + )); + uint256 rate = 0; + if (didSucceed) { + (rate) = abi.decode(resultData, (uint256)); + } else { + return 0; + } + + uint256 makerTokenDecimals = _getTokenDecimals(makerToken); + uint256 takerTokenDecimals = _getTokenDecimals(takerToken); + makerTokenAmount = + rate * + takerTokenAmount * + 10 ** makerTokenDecimals / + 10 ** takerTokenDecimals / + 10 ** 18; + return makerTokenAmount; + } +} diff --git a/contracts/erc20-bridge-sampler/contracts/src/LiquidityProviderSampler.sol b/contracts/erc20-bridge-sampler/contracts/src/LiquidityProviderSampler.sol new file mode 100644 index 0000000000..b10b5912b0 --- /dev/null +++ b/contracts/erc20-bridge-sampler/contracts/src/LiquidityProviderSampler.sol @@ -0,0 +1,168 @@ +/* + + Copyright 2019 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; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-utils/contracts/src/LibBytes.sol"; +import "./ILiquidityProvider.sol"; +import "./ILiquidityProviderRegistry.sol"; +import "./ApproximateBuys.sol"; +import "./SamplerUtils.sol"; + + +contract LiquidityProviderSampler is + SamplerUtils, + ApproximateBuys +{ + /// @dev Default gas limit for liquidity provider calls. + uint256 constant private DEFAULT_CALL_GAS = 400e3; // 400k + + /// @dev Sample sell 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). + /// @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 sampleSellsFromLiquidityProviderRegistry( + address registryAddress, + address takerToken, + 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); + + // Query registry for provider address. + address providerAddress = getLiquidityProviderFromRegistry( + registryAddress, + takerToken, + makerToken + ); + // If provider doesn't exist, return all zeros. + if (providerAddress == address(0)) { + return makerTokenAmounts; + } + + for (uint256 i = 0; i < numSamples; i++) { + (bool didSucceed, bytes memory resultData) = + providerAddress.staticcall.gas(DEFAULT_CALL_GAS)( + abi.encodeWithSelector( + ILiquidityProvider(0).getSellQuote.selector, + takerToken, + 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). + /// @param makerToken Address of the maker token (what to buy). + /// @param makerTokenAmounts Maker token buy amount for each sample. + /// @return takerTokenAmounts Taker amounts sold at each maker token + /// amount. + function sampleBuysFromLiquidityProviderRegistry( + address registryAddress, + address takerToken, + address makerToken, + uint256[] memory makerTokenAmounts + ) + public + view + returns (uint256[] memory takerTokenAmounts) + { + return _sampleApproximateBuys( + ApproximateBuyQuoteOpts({ + makerTokenData: abi.encode(makerToken, registryAddress), + takerTokenData: abi.encode(takerToken, registryAddress), + getSellQuoteCallback: _sampleSellForApproximateBuyFromLiquidityProviderRegistry + }), + makerTokenAmounts + ); + } + + /// @dev Returns the address of a liquidity provider for the given market + /// (takerToken, makerToken), from a registry of liquidity providers. + /// Returns address(0) if no such provider exists in the registry. + /// @param takerToken Taker asset managed by liquidity provider. + /// @param makerToken Maker asset managed by liquidity provider. + /// @return providerAddress Address of the liquidity provider. + function getLiquidityProviderFromRegistry( + address registryAddress, + address takerToken, + address makerToken + ) + public + view + returns (address providerAddress) + { + bytes memory callData = abi.encodeWithSelector( + ILiquidityProviderRegistry(0).getLiquidityProviderForMarket.selector, + takerToken, + makerToken + ); + (bool didSucceed, bytes memory returnData) = registryAddress.staticcall(callData); + if (didSucceed && returnData.length == 32) { + return LibBytes.readAddress(returnData, 12); + } + } + + function _sampleSellForApproximateBuyFromLiquidityProviderRegistry( + bytes memory takerTokenData, + bytes memory makerTokenData, + uint256 sellAmount + ) + private + view + returns (uint256 buyAmount) + { + (address takerToken, address plpRegistryAddress) = + abi.decode(takerTokenData, (address, address)); + (address makerToken) = + abi.decode(makerTokenData, (address)); + (bool success, bytes memory resultData) = + address(this).staticcall(abi.encodeWithSelector( + this.sampleSellsFromLiquidityProviderRegistry.selector, + plpRegistryAddress, + takerToken, + makerToken, + _toSingleValueArray(sellAmount) + )); + if (!success) { + return 0; + } + // solhint-disable-next-line indent + return abi.decode(resultData, (uint256[]))[0]; + } +} diff --git a/contracts/erc20-bridge-sampler/contracts/src/MultiBridgeSampler.sol b/contracts/erc20-bridge-sampler/contracts/src/MultiBridgeSampler.sol new file mode 100644 index 0000000000..0e85f34f0c --- /dev/null +++ b/contracts/erc20-bridge-sampler/contracts/src/MultiBridgeSampler.sol @@ -0,0 +1,79 @@ +/* + + Copyright 2019 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; +pragma experimental ABIEncoderV2; + +import "./IMultiBridge.sol"; + + +contract MultiBridgeSampler { + + /// @dev Default gas limit for multibridge calls. + uint256 constant private DEFAULT_CALL_GAS = 400e3; // 400k + + /// @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; + } + } +} diff --git a/contracts/erc20-bridge-sampler/contracts/src/NativeOrderSampler.sol b/contracts/erc20-bridge-sampler/contracts/src/NativeOrderSampler.sol new file mode 100644 index 0000000000..a7798f7d42 --- /dev/null +++ b/contracts/erc20-bridge-sampler/contracts/src/NativeOrderSampler.sol @@ -0,0 +1,125 @@ +/* + + Copyright 2019 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; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol"; +import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol"; +import "./IDevUtils.sol"; + + +contract NativeOrderSampler { + + /// @dev Gas limit for DevUtils calls. + uint256 constant internal DEV_UTILS_CALL_GAS = 500e3; // 500k + + /// @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 orderSignatures Signatures for each respective order in `orders`. + /// @param devUtilsAddress Address to the DevUtils contract. + /// @return orderFillableTakerAssetAmounts How much taker asset can be filled + /// by each order in `orders`. + function getOrderFillableTakerAssetAmounts( + LibOrder.Order[] memory orders, + bytes[] memory orderSignatures, + address devUtilsAddress + ) + public + view + returns (uint256[] memory orderFillableTakerAssetAmounts) + { + orderFillableTakerAssetAmounts = new uint256[](orders.length); + for (uint256 i = 0; i != orders.length; i++) { + // Ignore orders with no signature or empty maker/taker amounts. + if (orderSignatures[i].length == 0 || + orders[i].makerAssetAmount == 0 || + orders[i].takerAssetAmount == 0) { + orderFillableTakerAssetAmounts[i] = 0; + continue; + } + // solhint-disable indent + (bool didSucceed, bytes memory resultData) = + devUtilsAddress + .staticcall + .gas(DEV_UTILS_CALL_GAS) + (abi.encodeWithSelector( + IDevUtils(devUtilsAddress).getOrderRelevantState.selector, + orders[i], + orderSignatures[i] + )); + // solhint-enable indent + if (!didSucceed) { + orderFillableTakerAssetAmounts[i] = 0; + continue; + } + ( + LibOrder.OrderInfo memory orderInfo, + uint256 fillableTakerAssetAmount, + bool isValidSignature + ) = abi.decode( + resultData, + (LibOrder.OrderInfo, uint256, bool) + ); + // The fillable amount is zero if the order is not fillable or if the + // signature is invalid. + if (orderInfo.orderStatus != LibOrder.OrderStatus.FILLABLE || + !isValidSignature) { + orderFillableTakerAssetAmounts[i] = 0; + } else { + orderFillableTakerAssetAmounts[i] = fillableTakerAssetAmount; + } + } + } + + /// @dev Queries the fillable taker asset amounts of native orders. + /// Effectively ignores orders that have empty signatures or + /// @param orders Native orders to query. + /// @param orderSignatures Signatures for each respective order in `orders`. + /// @param devUtilsAddress Address to the DevUtils contract. + /// @return orderFillableMakerAssetAmounts How much maker asset can be filled + /// by each order in `orders`. + function getOrderFillableMakerAssetAmounts( + LibOrder.Order[] memory orders, + bytes[] memory orderSignatures, + address devUtilsAddress + ) + public + view + returns (uint256[] memory orderFillableMakerAssetAmounts) + { + orderFillableMakerAssetAmounts = getOrderFillableTakerAssetAmounts( + orders, + orderSignatures, + devUtilsAddress + ); + // `orderFillableMakerAssetAmounts` now holds taker asset amounts, so + // convert them to maker asset amounts. + for (uint256 i = 0; i < orders.length; ++i) { + if (orderFillableMakerAssetAmounts[i] != 0) { + orderFillableMakerAssetAmounts[i] = LibMath.getPartialAmountCeil( + orderFillableMakerAssetAmounts[i], + orders[i].takerAssetAmount, + orders[i].makerAssetAmount + ); + } + } + } +} diff --git a/contracts/erc20-bridge-sampler/contracts/src/SamplerUtils.sol b/contracts/erc20-bridge-sampler/contracts/src/SamplerUtils.sol new file mode 100644 index 0000000000..01c5dbbdb8 --- /dev/null +++ b/contracts/erc20-bridge-sampler/contracts/src/SamplerUtils.sol @@ -0,0 +1,56 @@ +/* + + Copyright 2019 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; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol"; + + +contract SamplerUtils { + + /// @dev Overridable way to get token decimals. + /// @param tokenAddress Address of the token. + /// @return decimals The decimal places for the token. + function _getTokenDecimals(address tokenAddress) + internal + view + returns (uint8 decimals) + { + return LibERC20Token.decimals(tokenAddress); + } + + function _toSingleValueArray(uint256 v) + internal + pure + returns (uint256[] memory arr) + { + arr = new uint256[](1); + arr[0] = v; + } + + /// @dev Assert that the tokens in a trade pair are valid. + /// @param makerToken Address of the maker token. + /// @param takerToken Address of the taker token. + function _assertValidPair(address makerToken, address takerToken) + internal + pure + { + require(makerToken != takerToken, "ERC20BridgeSampler/INVALID_TOKEN_PAIR"); + } +} diff --git a/contracts/erc20-bridge-sampler/contracts/src/UniswapSampler.sol b/contracts/erc20-bridge-sampler/contracts/src/UniswapSampler.sol new file mode 100644 index 0000000000..1a0167cc09 --- /dev/null +++ b/contracts/erc20-bridge-sampler/contracts/src/UniswapSampler.sol @@ -0,0 +1,203 @@ +/* + + Copyright 2019 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; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-asset-proxy/contracts/src/interfaces/IUniswapExchangeFactory.sol"; +import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol"; +import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol"; +import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol"; +import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; +import "@0x/contracts-utils/contracts/src/LibBytes.sol"; +import "./IUniswapExchangeQuotes.sol"; +import "./SamplerUtils.sol"; + + +contract UniswapSampler is + DeploymentConstants, + SamplerUtils +{ + /// @dev Gas limit for Uniswap calls. + uint256 constant private UNISWAP_CALL_GAS = 150e3; // 150k + /// @dev Gas limit for UniswapV2 calls. + uint256 constant private UNISWAPV2_CALL_GAS = 150e3; // 150k + + /// @dev Sample sell quotes from Uniswap. + /// @param takerToken Address of the taker token (what to sell). + /// @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 sampleSellsFromUniswap( + address takerToken, + address makerToken, + uint256[] memory takerTokenAmounts + ) + public + view + returns (uint256[] memory makerTokenAmounts) + { + _assertValidPair(makerToken, takerToken); + uint256 numSamples = takerTokenAmounts.length; + makerTokenAmounts = new uint256[](numSamples); + IUniswapExchangeQuotes takerTokenExchange = takerToken == _getWethAddress() ? + IUniswapExchangeQuotes(0) : _getUniswapExchange(takerToken); + IUniswapExchangeQuotes makerTokenExchange = makerToken == _getWethAddress() ? + IUniswapExchangeQuotes(0) : _getUniswapExchange(makerToken); + for (uint256 i = 0; i < numSamples; i++) { + bool didSucceed = true; + if (makerToken == _getWethAddress()) { + (makerTokenAmounts[i], didSucceed) = _callUniswapExchangePriceFunction( + address(takerTokenExchange), + takerTokenExchange.getTokenToEthInputPrice.selector, + takerTokenAmounts[i] + ); + } else if (takerToken == _getWethAddress()) { + (makerTokenAmounts[i], didSucceed) = _callUniswapExchangePriceFunction( + address(makerTokenExchange), + makerTokenExchange.getEthToTokenInputPrice.selector, + takerTokenAmounts[i] + ); + } else { + uint256 ethBought; + (ethBought, didSucceed) = _callUniswapExchangePriceFunction( + address(takerTokenExchange), + takerTokenExchange.getTokenToEthInputPrice.selector, + takerTokenAmounts[i] + ); + if (ethBought != 0) { + (makerTokenAmounts[i], didSucceed) = _callUniswapExchangePriceFunction( + address(makerTokenExchange), + makerTokenExchange.getEthToTokenInputPrice.selector, + ethBought + ); + } else { + makerTokenAmounts[i] = 0; + } + } + if (!didSucceed) { + break; + } + } + } + + /// @dev Sample buy quotes from Uniswap. + /// @param takerToken Address of the taker token (what to sell). + /// @param makerToken Address of the maker token (what to buy). + /// @param makerTokenAmounts Maker token sell amount for each sample. + /// @return takerTokenAmounts Taker amounts sold at each maker token + /// amount. + function sampleBuysFromUniswap( + address takerToken, + address makerToken, + uint256[] memory makerTokenAmounts + ) + public + view + returns (uint256[] memory takerTokenAmounts) + { + _assertValidPair(makerToken, takerToken); + uint256 numSamples = makerTokenAmounts.length; + takerTokenAmounts = new uint256[](numSamples); + IUniswapExchangeQuotes takerTokenExchange = takerToken == _getWethAddress() ? + IUniswapExchangeQuotes(0) : _getUniswapExchange(takerToken); + IUniswapExchangeQuotes makerTokenExchange = makerToken == _getWethAddress() ? + IUniswapExchangeQuotes(0) : _getUniswapExchange(makerToken); + for (uint256 i = 0; i < numSamples; i++) { + bool didSucceed = true; + if (makerToken == _getWethAddress()) { + (takerTokenAmounts[i], didSucceed) = _callUniswapExchangePriceFunction( + address(takerTokenExchange), + takerTokenExchange.getTokenToEthOutputPrice.selector, + makerTokenAmounts[i] + ); + } else if (takerToken == _getWethAddress()) { + (takerTokenAmounts[i], didSucceed) = _callUniswapExchangePriceFunction( + address(makerTokenExchange), + makerTokenExchange.getEthToTokenOutputPrice.selector, + makerTokenAmounts[i] + ); + } else { + uint256 ethSold; + (ethSold, didSucceed) = _callUniswapExchangePriceFunction( + address(makerTokenExchange), + makerTokenExchange.getEthToTokenOutputPrice.selector, + makerTokenAmounts[i] + ); + if (ethSold != 0) { + (takerTokenAmounts[i], didSucceed) = _callUniswapExchangePriceFunction( + address(takerTokenExchange), + takerTokenExchange.getTokenToEthOutputPrice.selector, + ethSold + ); + } else { + takerTokenAmounts[i] = 0; + } + } + if (!didSucceed) { + break; + } + } + } + + /// @dev Gracefully calls a Uniswap pricing function. + /// @param uniswapExchangeAddress Address of an `IUniswapExchangeQuotes` exchange. + /// @param functionSelector Selector of the target function. + /// @param inputAmount Quantity parameter particular to the pricing function. + /// @return outputAmount The returned amount from the function call. Will be + /// zero if the call fails or if `uniswapExchangeAddress` is zero. + function _callUniswapExchangePriceFunction( + address uniswapExchangeAddress, + bytes4 functionSelector, + uint256 inputAmount + ) + private + view + returns (uint256 outputAmount, bool didSucceed) + { + if (uniswapExchangeAddress == address(0)) { + return (outputAmount, didSucceed); + } + bytes memory resultData; + (didSucceed, resultData) = + uniswapExchangeAddress.staticcall.gas(UNISWAP_CALL_GAS)( + abi.encodeWithSelector( + functionSelector, + inputAmount + )); + if (didSucceed) { + outputAmount = abi.decode(resultData, (uint256)); + } + } + + /// @dev Retrive an existing Uniswap exchange contract. + /// Throws if the exchange does not exist. + /// @param tokenAddress Address of the token contract. + /// @return exchange `IUniswapExchangeQuotes` for the token. + function _getUniswapExchange(address tokenAddress) + private + view + returns (IUniswapExchangeQuotes exchange) + { + exchange = IUniswapExchangeQuotes( + address(IUniswapExchangeFactory(_getUniswapExchangeFactoryAddress()) + .getExchange(tokenAddress)) + ); + } +} diff --git a/contracts/erc20-bridge-sampler/contracts/src/UniswapV2Sampler.sol b/contracts/erc20-bridge-sampler/contracts/src/UniswapV2Sampler.sol new file mode 100644 index 0000000000..09c0963cc3 --- /dev/null +++ b/contracts/erc20-bridge-sampler/contracts/src/UniswapV2Sampler.sol @@ -0,0 +1,99 @@ +/* + + Copyright 2019 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; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; +import "./IUniswapV2Router01.sol"; + + +contract UniswapV2Sampler is + DeploymentConstants +{ + /// @dev Gas limit for UniswapV2 calls. + uint256 constant private UNISWAPV2_CALL_GAS = 150e3; // 150k + + /// @dev Sample sell quotes from UniswapV2. + /// @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[] memory path, + uint256[] memory takerTokenAmounts + ) + public + view + returns (uint256[] memory makerTokenAmounts) + { + uint256 numSamples = takerTokenAmounts.length; + makerTokenAmounts = new uint256[](numSamples); + for (uint256 i = 0; i < numSamples; i++) { + (bool didSucceed, bytes memory resultData) = + _getUniswapV2Router01Address().staticcall.gas(UNISWAPV2_CALL_GAS)( + abi.encodeWithSelector( + IUniswapV2Router01(0).getAmountsOut.selector, + takerTokenAmounts[i], + path + )); + uint256 buyAmount = 0; + if (didSucceed) { + // solhint-disable-next-line indent + buyAmount = abi.decode(resultData, (uint256[]))[path.length - 1]; + } else { + break; + } + makerTokenAmounts[i] = buyAmount; + } + } + + /// @dev Sample buy quotes from UniswapV2. + /// @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[] memory path, + uint256[] memory makerTokenAmounts + ) + public + view + returns (uint256[] memory takerTokenAmounts) + { + uint256 numSamples = makerTokenAmounts.length; + takerTokenAmounts = new uint256[](numSamples); + for (uint256 i = 0; i < numSamples; i++) { + (bool didSucceed, bytes memory resultData) = + _getUniswapV2Router01Address().staticcall.gas(UNISWAPV2_CALL_GAS)( + abi.encodeWithSelector( + IUniswapV2Router01(0).getAmountsIn.selector, + makerTokenAmounts[i], + path + )); + uint256 sellAmount = 0; + if (didSucceed) { + // solhint-disable-next-line indent + sellAmount = abi.decode(resultData, (uint256[]))[0]; + } else { + break; + } + takerTokenAmounts[i] = sellAmount; + } + } +} diff --git a/contracts/erc20-bridge-sampler/package.json b/contracts/erc20-bridge-sampler/package.json index d537beafd7..06c7d36e04 100644 --- a/contracts/erc20-bridge-sampler/package.json +++ b/contracts/erc20-bridge-sampler/package.json @@ -36,9 +36,9 @@ "compile:truffle": "truffle compile" }, "config": { - "publicInterfaceContracts": "ERC20BridgeSampler,IERC20BridgeSampler,ILiquidityProvider,ILiquidityProviderRegistry,DummyLiquidityProviderRegistry,DummyLiquidityProvider", + "publicInterfaceContracts": "ERC20BridgeSampler,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|IKyberHintHandler|IKyberNetwork|IKyberNetworkProxy|IKyberStorage|ILiquidityProvider|ILiquidityProviderRegistry|IMultiBridge|IUniswapExchangeQuotes|IUniswapV2Router01|TestERC20BridgeSampler).json" + "abis": "./test/generated-artifacts/@(ApproximateBuys|CurveSampler|DummyLiquidityProvider|DummyLiquidityProviderRegistry|ERC20BridgeSampler|Eth2DaiSampler|ICurve|IDevUtils|IEth2Dai|IKyberHintHandler|IKyberNetwork|IKyberNetworkProxy|IKyberStorage|ILiquidityProvider|ILiquidityProviderRegistry|IMultiBridge|IUniswapExchangeQuotes|IUniswapV2Router01|KyberSampler|LiquidityProviderSampler|MultiBridgeSampler|NativeOrderSampler|SamplerUtils|TestERC20BridgeSampler|UniswapSampler|UniswapV2Sampler).json" }, "repository": { "type": "git", diff --git a/contracts/erc20-bridge-sampler/src/artifacts.ts b/contracts/erc20-bridge-sampler/src/artifacts.ts index 837f883201..249ae04744 100644 --- a/contracts/erc20-bridge-sampler/src/artifacts.ts +++ b/contracts/erc20-bridge-sampler/src/artifacts.ts @@ -8,12 +8,10 @@ import { ContractArtifact } from 'ethereum-types'; import * as DummyLiquidityProvider from '../generated-artifacts/DummyLiquidityProvider.json'; import * as DummyLiquidityProviderRegistry from '../generated-artifacts/DummyLiquidityProviderRegistry.json'; import * as ERC20BridgeSampler from '../generated-artifacts/ERC20BridgeSampler.json'; -import * as IERC20BridgeSampler from '../generated-artifacts/IERC20BridgeSampler.json'; import * as ILiquidityProvider from '../generated-artifacts/ILiquidityProvider.json'; import * as ILiquidityProviderRegistry from '../generated-artifacts/ILiquidityProviderRegistry.json'; export const artifacts = { ERC20BridgeSampler: ERC20BridgeSampler as ContractArtifact, - IERC20BridgeSampler: IERC20BridgeSampler as ContractArtifact, ILiquidityProvider: ILiquidityProvider as ContractArtifact, ILiquidityProviderRegistry: ILiquidityProviderRegistry as ContractArtifact, DummyLiquidityProviderRegistry: DummyLiquidityProviderRegistry as ContractArtifact, diff --git a/contracts/erc20-bridge-sampler/src/wrappers.ts b/contracts/erc20-bridge-sampler/src/wrappers.ts index bb1fd88c01..9e18a8fe91 100644 --- a/contracts/erc20-bridge-sampler/src/wrappers.ts +++ b/contracts/erc20-bridge-sampler/src/wrappers.ts @@ -6,6 +6,5 @@ export * from '../generated-wrappers/dummy_liquidity_provider'; export * from '../generated-wrappers/dummy_liquidity_provider_registry'; export * from '../generated-wrappers/erc20_bridge_sampler'; -export * from '../generated-wrappers/i_erc20_bridge_sampler'; export * from '../generated-wrappers/i_liquidity_provider'; export * from '../generated-wrappers/i_liquidity_provider_registry'; diff --git a/contracts/erc20-bridge-sampler/test/artifacts.ts b/contracts/erc20-bridge-sampler/test/artifacts.ts index 729ec25186..378361abeb 100644 --- a/contracts/erc20-bridge-sampler/test/artifacts.ts +++ b/contracts/erc20-bridge-sampler/test/artifacts.ts @@ -5,12 +5,14 @@ */ import { ContractArtifact } from 'ethereum-types'; +import * as ApproximateBuys from '../test/generated-artifacts/ApproximateBuys.json'; +import * as CurveSampler from '../test/generated-artifacts/CurveSampler.json'; import * as DummyLiquidityProvider from '../test/generated-artifacts/DummyLiquidityProvider.json'; import * as DummyLiquidityProviderRegistry from '../test/generated-artifacts/DummyLiquidityProviderRegistry.json'; import * as ERC20BridgeSampler from '../test/generated-artifacts/ERC20BridgeSampler.json'; +import * as Eth2DaiSampler from '../test/generated-artifacts/Eth2DaiSampler.json'; import * as ICurve from '../test/generated-artifacts/ICurve.json'; import * as IDevUtils from '../test/generated-artifacts/IDevUtils.json'; -import * as IERC20BridgeSampler from '../test/generated-artifacts/IERC20BridgeSampler.json'; import * as IEth2Dai from '../test/generated-artifacts/IEth2Dai.json'; import * as IKyberHintHandler from '../test/generated-artifacts/IKyberHintHandler.json'; import * as IKyberNetwork from '../test/generated-artifacts/IKyberNetwork.json'; @@ -21,14 +23,23 @@ import * as ILiquidityProviderRegistry from '../test/generated-artifacts/ILiquid 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 KyberSampler from '../test/generated-artifacts/KyberSampler.json'; +import * as LiquidityProviderSampler from '../test/generated-artifacts/LiquidityProviderSampler.json'; +import * as MultiBridgeSampler from '../test/generated-artifacts/MultiBridgeSampler.json'; +import * as NativeOrderSampler from '../test/generated-artifacts/NativeOrderSampler.json'; +import * as SamplerUtils from '../test/generated-artifacts/SamplerUtils.json'; import * as TestERC20BridgeSampler from '../test/generated-artifacts/TestERC20BridgeSampler.json'; +import * as UniswapSampler from '../test/generated-artifacts/UniswapSampler.json'; +import * as UniswapV2Sampler from '../test/generated-artifacts/UniswapV2Sampler.json'; export const artifacts = { + ApproximateBuys: ApproximateBuys as ContractArtifact, + CurveSampler: CurveSampler as ContractArtifact, DummyLiquidityProvider: DummyLiquidityProvider as ContractArtifact, DummyLiquidityProviderRegistry: DummyLiquidityProviderRegistry as ContractArtifact, ERC20BridgeSampler: ERC20BridgeSampler as ContractArtifact, + Eth2DaiSampler: Eth2DaiSampler as ContractArtifact, ICurve: ICurve as ContractArtifact, IDevUtils: IDevUtils as ContractArtifact, - IERC20BridgeSampler: IERC20BridgeSampler as ContractArtifact, IEth2Dai: IEth2Dai as ContractArtifact, IKyberHintHandler: IKyberHintHandler as ContractArtifact, IKyberNetwork: IKyberNetwork as ContractArtifact, @@ -39,5 +50,12 @@ export const artifacts = { IMultiBridge: IMultiBridge as ContractArtifact, IUniswapExchangeQuotes: IUniswapExchangeQuotes as ContractArtifact, IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact, + KyberSampler: KyberSampler as ContractArtifact, + LiquidityProviderSampler: LiquidityProviderSampler as ContractArtifact, + MultiBridgeSampler: MultiBridgeSampler as ContractArtifact, + NativeOrderSampler: NativeOrderSampler as ContractArtifact, + SamplerUtils: SamplerUtils as ContractArtifact, + UniswapSampler: UniswapSampler as ContractArtifact, + UniswapV2Sampler: UniswapV2Sampler as ContractArtifact, TestERC20BridgeSampler: TestERC20BridgeSampler as ContractArtifact, }; diff --git a/contracts/erc20-bridge-sampler/test/erc20-bridge-sampler.ts b/contracts/erc20-bridge-sampler/test/erc20-bridge-sampler.ts index a20b66cbd9..e4783468ac 100644 --- a/contracts/erc20-bridge-sampler/test/erc20-bridge-sampler.ts +++ b/contracts/erc20-bridge-sampler/test/erc20-bridge-sampler.ts @@ -34,10 +34,6 @@ blockchainTests('erc20-bridge-sampler', env => { const MAKER_TOKEN = randomAddress(); const TAKER_TOKEN = randomAddress(); let devUtilsAddress: string; - const FAKE_BUY_OPTS = { - targetSlippageBps: new BigNumber(5), - maxIterations: new BigNumber(5), - }; before(async () => { testContract = await TestERC20BridgeSamplerContract.deployFrom0xArtifactAsync( @@ -443,14 +439,12 @@ blockchainTests('erc20-bridge-sampler', env => { }); it('throws if tokens are the same', async () => { - const tx = testContract.sampleBuysFromKyberNetwork(MAKER_TOKEN, MAKER_TOKEN, [], FAKE_BUY_OPTS).callAsync(); + const tx = testContract.sampleBuysFromKyberNetwork(MAKER_TOKEN, MAKER_TOKEN, []).callAsync(); return expect(tx).to.revertWith(INVALID_TOKEN_PAIR_ERROR); }); it('can return no quotes', async () => { - const quotes = await testContract - .sampleBuysFromKyberNetwork(TAKER_TOKEN, MAKER_TOKEN, [], FAKE_BUY_OPTS) - .callAsync(); + const quotes = await testContract.sampleBuysFromKyberNetwork(TAKER_TOKEN, MAKER_TOKEN, []).callAsync(); expect(quotes).to.deep.eq([]); }); const expectQuotesWithinRange = ( @@ -485,7 +479,7 @@ blockchainTests('erc20-bridge-sampler', env => { const [ethToMakerQuotes] = getDeterministicBuyQuotes(WETH_ADDRESS, MAKER_TOKEN, ['Kyber'], sampleAmounts); const [expectedQuotes] = getDeterministicBuyQuotes(TAKER_TOKEN, WETH_ADDRESS, ['Kyber'], ethToMakerQuotes); const quotes = await testContract - .sampleBuysFromKyberNetwork(TAKER_TOKEN, MAKER_TOKEN, sampleAmounts, FAKE_BUY_OPTS) + .sampleBuysFromKyberNetwork(TAKER_TOKEN, MAKER_TOKEN, sampleAmounts) .callAsync(); expectQuotesWithinRange(quotes, expectedQuotes, ACCEPTABLE_SLIPPAGE); }); @@ -495,7 +489,7 @@ blockchainTests('erc20-bridge-sampler', env => { const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); await enableFailTriggerAsync(); const quotes = await testContract - .sampleBuysFromKyberNetwork(TAKER_TOKEN, MAKER_TOKEN, sampleAmounts, FAKE_BUY_OPTS) + .sampleBuysFromKyberNetwork(TAKER_TOKEN, MAKER_TOKEN, sampleAmounts) .callAsync(); expect(quotes).to.deep.eq(expectedQuotes); }); @@ -504,7 +498,7 @@ blockchainTests('erc20-bridge-sampler', env => { const sampleAmounts = getSampleAmounts(TAKER_TOKEN); const [expectedQuotes] = getDeterministicBuyQuotes(TAKER_TOKEN, WETH_ADDRESS, ['Kyber'], sampleAmounts); const quotes = await testContract - .sampleBuysFromKyberNetwork(TAKER_TOKEN, WETH_ADDRESS, sampleAmounts, FAKE_BUY_OPTS) + .sampleBuysFromKyberNetwork(TAKER_TOKEN, WETH_ADDRESS, sampleAmounts) .callAsync(); expectQuotesWithinRange(quotes, expectedQuotes, ACCEPTABLE_SLIPPAGE); }); @@ -514,7 +508,7 @@ blockchainTests('erc20-bridge-sampler', env => { const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); await enableFailTriggerAsync(); const quotes = await testContract - .sampleBuysFromKyberNetwork(TAKER_TOKEN, WETH_ADDRESS, sampleAmounts, FAKE_BUY_OPTS) + .sampleBuysFromKyberNetwork(TAKER_TOKEN, WETH_ADDRESS, sampleAmounts) .callAsync(); expect(quotes).to.deep.eq(expectedQuotes); }); @@ -523,7 +517,7 @@ blockchainTests('erc20-bridge-sampler', env => { const sampleAmounts = getSampleAmounts(TAKER_TOKEN); const [expectedQuotes] = getDeterministicBuyQuotes(WETH_ADDRESS, TAKER_TOKEN, ['Kyber'], sampleAmounts); const quotes = await testContract - .sampleBuysFromKyberNetwork(WETH_ADDRESS, TAKER_TOKEN, sampleAmounts, FAKE_BUY_OPTS) + .sampleBuysFromKyberNetwork(WETH_ADDRESS, TAKER_TOKEN, sampleAmounts) .callAsync(); expectQuotesWithinRange(quotes, expectedQuotes, ACCEPTABLE_SLIPPAGE); }); @@ -533,7 +527,7 @@ blockchainTests('erc20-bridge-sampler', env => { const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); await enableFailTriggerAsync(); const quotes = await testContract - .sampleBuysFromKyberNetwork(WETH_ADDRESS, TAKER_TOKEN, sampleAmounts, FAKE_BUY_OPTS) + .sampleBuysFromKyberNetwork(WETH_ADDRESS, TAKER_TOKEN, sampleAmounts) .callAsync(); expect(quotes).to.deep.eq(expectedQuotes); }); @@ -924,13 +918,7 @@ blockchainTests('erc20-bridge-sampler', env => { it('should be able to query buys from the liquidity provider', async () => { const result = await testContract - .sampleBuysFromLiquidityProviderRegistry( - registryContract.address, - yAsset, - xAsset, - sampleAmounts, - FAKE_BUY_OPTS, - ) + .sampleBuysFromLiquidityProviderRegistry(registryContract.address, yAsset, xAsset, sampleAmounts) .callAsync(); result.forEach((value, idx) => { expect(value).is.bignumber.eql(sampleAmounts[idx].plus(1)); @@ -944,7 +932,6 @@ blockchainTests('erc20-bridge-sampler', env => { yAsset, randomAddress(), sampleAmounts, - FAKE_BUY_OPTS, ) .callAsync(); result.forEach(value => { @@ -954,7 +941,7 @@ blockchainTests('erc20-bridge-sampler', env => { it('should just return zeros if the registry does not exist', async () => { const result = await testContract - .sampleBuysFromLiquidityProviderRegistry(randomAddress(), yAsset, xAsset, sampleAmounts, FAKE_BUY_OPTS) + .sampleBuysFromLiquidityProviderRegistry(randomAddress(), yAsset, xAsset, sampleAmounts) .callAsync(); result.forEach(value => { expect(value).is.bignumber.eql(constants.ZERO_AMOUNT); diff --git a/contracts/erc20-bridge-sampler/test/wrappers.ts b/contracts/erc20-bridge-sampler/test/wrappers.ts index ec7b3d1b5b..33964f8fcb 100644 --- a/contracts/erc20-bridge-sampler/test/wrappers.ts +++ b/contracts/erc20-bridge-sampler/test/wrappers.ts @@ -3,12 +3,14 @@ * Warning: This file is auto-generated by contracts-gen. Don't edit manually. * ----------------------------------------------------------------------------- */ +export * from '../test/generated-wrappers/approximate_buys'; +export * from '../test/generated-wrappers/curve_sampler'; export * from '../test/generated-wrappers/dummy_liquidity_provider'; export * from '../test/generated-wrappers/dummy_liquidity_provider_registry'; export * from '../test/generated-wrappers/erc20_bridge_sampler'; +export * from '../test/generated-wrappers/eth2_dai_sampler'; export * from '../test/generated-wrappers/i_curve'; export * from '../test/generated-wrappers/i_dev_utils'; -export * from '../test/generated-wrappers/i_erc20_bridge_sampler'; export * from '../test/generated-wrappers/i_eth2_dai'; export * from '../test/generated-wrappers/i_kyber_hint_handler'; export * from '../test/generated-wrappers/i_kyber_network'; @@ -19,4 +21,11 @@ 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/kyber_sampler'; +export * from '../test/generated-wrappers/liquidity_provider_sampler'; +export * from '../test/generated-wrappers/multi_bridge_sampler'; +export * from '../test/generated-wrappers/native_order_sampler'; +export * from '../test/generated-wrappers/sampler_utils'; export * from '../test/generated-wrappers/test_erc20_bridge_sampler'; +export * from '../test/generated-wrappers/uniswap_sampler'; +export * from '../test/generated-wrappers/uniswap_v2_sampler'; diff --git a/contracts/erc20-bridge-sampler/tsconfig.json b/contracts/erc20-bridge-sampler/tsconfig.json index 9aec3552fc..9ad2490b42 100644 --- a/contracts/erc20-bridge-sampler/tsconfig.json +++ b/contracts/erc20-bridge-sampler/tsconfig.json @@ -6,15 +6,16 @@ "generated-artifacts/DummyLiquidityProvider.json", "generated-artifacts/DummyLiquidityProviderRegistry.json", "generated-artifacts/ERC20BridgeSampler.json", - "generated-artifacts/IERC20BridgeSampler.json", "generated-artifacts/ILiquidityProvider.json", "generated-artifacts/ILiquidityProviderRegistry.json", + "test/generated-artifacts/ApproximateBuys.json", + "test/generated-artifacts/CurveSampler.json", "test/generated-artifacts/DummyLiquidityProvider.json", "test/generated-artifacts/DummyLiquidityProviderRegistry.json", "test/generated-artifacts/ERC20BridgeSampler.json", + "test/generated-artifacts/Eth2DaiSampler.json", "test/generated-artifacts/ICurve.json", "test/generated-artifacts/IDevUtils.json", - "test/generated-artifacts/IERC20BridgeSampler.json", "test/generated-artifacts/IEth2Dai.json", "test/generated-artifacts/IKyberHintHandler.json", "test/generated-artifacts/IKyberNetwork.json", @@ -25,7 +26,14 @@ "test/generated-artifacts/IMultiBridge.json", "test/generated-artifacts/IUniswapExchangeQuotes.json", "test/generated-artifacts/IUniswapV2Router01.json", - "test/generated-artifacts/TestERC20BridgeSampler.json" + "test/generated-artifacts/KyberSampler.json", + "test/generated-artifacts/LiquidityProviderSampler.json", + "test/generated-artifacts/MultiBridgeSampler.json", + "test/generated-artifacts/NativeOrderSampler.json", + "test/generated-artifacts/SamplerUtils.json", + "test/generated-artifacts/TestERC20BridgeSampler.json", + "test/generated-artifacts/UniswapSampler.json", + "test/generated-artifacts/UniswapV2Sampler.json" ], "exclude": ["./deploy/solc/solc_bin"] } diff --git a/contracts/integrations/CHANGELOG.json b/contracts/integrations/CHANGELOG.json index 0c45587723..50c5c3863e 100644 --- a/contracts/integrations/CHANGELOG.json +++ b/contracts/integrations/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "2.7.0", + "changes": [ + { + "note": "Update curveBridge tests", + "pr": 2633 + } + ] + }, { "version": "2.6.0", "changes": [ diff --git a/contracts/integrations/package.json b/contracts/integrations/package.json index 1f93584714..5dcadf5a4b 100644 --- a/contracts/integrations/package.json +++ b/contracts/integrations/package.json @@ -102,6 +102,7 @@ "@0x/contracts-multisig": "^4.1.7", "@0x/contracts-staking": "^2.0.14", "@0x/contracts-test-utils": "^5.3.4", + "@0x/subproviders": "^6.1.1", "@0x/types": "^3.2.0", "@0x/typescript-typings": "^5.1.1", "@0x/utils": "^5.5.1", diff --git a/contracts/integrations/test/bridge_sampler/bridge_sampler_mainnet_test.ts b/contracts/integrations/test/bridge_sampler/bridge_sampler_mainnet_test.ts index 05ea74122a..5fe0d3f7d4 100644 --- a/contracts/integrations/test/bridge_sampler/bridge_sampler_mainnet_test.ts +++ b/contracts/integrations/test/bridge_sampler/bridge_sampler_mainnet_test.ts @@ -1,16 +1,11 @@ import { artifacts, ERC20BridgeSamplerContract } from '@0x/contracts-erc20-bridge-sampler'; import { blockchainTests, describe, expect, toBaseUnitAmount, Web3ProviderEngine } from '@0x/contracts-test-utils'; -import { RPCSubprovider } from '@0x/dev-utils/node_modules/@0x/subproviders'; +import { RPCSubprovider } from '@0x/subproviders'; import { BigNumber, providerUtils } from '@0x/utils'; export const VB = '0x6cc5f688a315f3dc28a7781717a9a798a59fda7b'; -blockchainTests.configure({ - fork: { - unlockedAccounts: [VB], - }, -}); -blockchainTests.fork.resets('Mainnet Sampler Tests', env => { +blockchainTests.skip('Mainnet Sampler Tests', env => { let testContract: ERC20BridgeSamplerContract; const fakeSamplerAddress = '0x1111111111111111111111111111111111111111'; const overrides = { @@ -21,7 +16,7 @@ blockchainTests.fork.resets('Mainnet Sampler Tests', env => { before(async () => { const provider = new Web3ProviderEngine(); // tslint:disable-next-line:no-non-null-assertion - provider.addProvider(new RPCSubprovider(process.env.FORK_RPC_URL!)); + provider.addProvider(new RPCSubprovider(process.env.RPC_URL!)); providerUtils.startProviderEngine(provider); testContract = new ERC20BridgeSamplerContract(fakeSamplerAddress, provider, { ...env.txDefaults, @@ -29,14 +24,19 @@ blockchainTests.fork.resets('Mainnet Sampler Tests', env => { }); }); describe('Curve', () => { - const CURVE_ADDRESS = '0xa2b47e3d5c44877cca798226b7b8118f9bfb7a56'; + const CURVE_ADDRESS = '0x45f783cce6b7ff23b2ab2d70e416cdb7d6055f51'; const DAI_TOKEN_INDEX = new BigNumber(0); const USDC_TOKEN_INDEX = new BigNumber(1); + const CURVE_INFO = { + poolAddress: CURVE_ADDRESS, + sellQuoteFunctionSelector: '0x07211ef7', + buyQuoteFunctionSelector: '0x0e71d1b9', + }; describe('sampleSellsFromCurve()', () => { it('samples sells from Curve DAI->USDC', async () => { const samples = await testContract - .sampleSellsFromCurve(CURVE_ADDRESS, DAI_TOKEN_INDEX, USDC_TOKEN_INDEX, [toBaseUnitAmount(1)]) + .sampleSellsFromCurve(CURVE_INFO, DAI_TOKEN_INDEX, USDC_TOKEN_INDEX, [toBaseUnitAmount(1)]) .callAsync({ overrides }); expect(samples.length).to.be.bignumber.greaterThan(0); expect(samples[0]).to.be.bignumber.greaterThan(0); @@ -44,7 +44,7 @@ blockchainTests.fork.resets('Mainnet Sampler Tests', env => { it('samples sells from Curve USDC->DAI', async () => { const samples = await testContract - .sampleSellsFromCurve(CURVE_ADDRESS, USDC_TOKEN_INDEX, DAI_TOKEN_INDEX, [toBaseUnitAmount(1, 6)]) + .sampleSellsFromCurve(CURVE_INFO, USDC_TOKEN_INDEX, DAI_TOKEN_INDEX, [toBaseUnitAmount(1, 6)]) .callAsync({ overrides }); expect(samples.length).to.be.bignumber.greaterThan(0); expect(samples[0]).to.be.bignumber.greaterThan(0); @@ -56,7 +56,7 @@ blockchainTests.fork.resets('Mainnet Sampler Tests', env => { // From DAI to USDC // I want to buy 1 USDC const samples = await testContract - .sampleBuysFromCurve(CURVE_ADDRESS, DAI_TOKEN_INDEX, USDC_TOKEN_INDEX, [toBaseUnitAmount(1, 6)]) + .sampleBuysFromCurve(CURVE_INFO, DAI_TOKEN_INDEX, USDC_TOKEN_INDEX, [toBaseUnitAmount(1, 6)]) .callAsync({ overrides }); expect(samples.length).to.be.bignumber.greaterThan(0); expect(samples[0]).to.be.bignumber.greaterThan(0); @@ -66,7 +66,7 @@ blockchainTests.fork.resets('Mainnet Sampler Tests', env => { // From USDC to DAI // I want to buy 1 DAI const samples = await testContract - .sampleBuysFromCurve(CURVE_ADDRESS, USDC_TOKEN_INDEX, DAI_TOKEN_INDEX, [toBaseUnitAmount(1)]) + .sampleBuysFromCurve(CURVE_INFO, USDC_TOKEN_INDEX, DAI_TOKEN_INDEX, [toBaseUnitAmount(1)]) .callAsync({ overrides }); expect(samples.length).to.be.bignumber.greaterThan(0); expect(samples[0]).to.be.bignumber.greaterThan(0); @@ -74,10 +74,6 @@ blockchainTests.fork.resets('Mainnet Sampler Tests', env => { }); }); describe('Kyber', () => { - const FAKE_BUY_OPTS = { - targetSlippageBps: new BigNumber(5), - maxIterations: new BigNumber(5), - }; const WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; const DAI = '0x6b175474e89094c44da98b954eedeac495271d0f'; const USDC = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; @@ -110,7 +106,7 @@ blockchainTests.fork.resets('Mainnet Sampler Tests', env => { // From ETH to DAI // I want to buy 1 DAI const samples = await testContract - .sampleBuysFromKyberNetwork(WETH, DAI, [toBaseUnitAmount(1)], FAKE_BUY_OPTS) + .sampleBuysFromKyberNetwork(WETH, DAI, [toBaseUnitAmount(1)]) .callAsync({ overrides }); expect(samples.length).to.be.bignumber.greaterThan(0); expect(samples[0]).to.be.bignumber.greaterThan(0); @@ -120,7 +116,7 @@ blockchainTests.fork.resets('Mainnet Sampler Tests', env => { // From USDC to DAI // I want to buy 1 WETH const samples = await testContract - .sampleBuysFromKyberNetwork(DAI, WETH, [toBaseUnitAmount(1)], FAKE_BUY_OPTS) + .sampleBuysFromKyberNetwork(DAI, WETH, [toBaseUnitAmount(1)]) .callAsync({ overrides }); expect(samples.length).to.be.bignumber.greaterThan(0); expect(samples[0]).to.be.bignumber.greaterThan(0); diff --git a/contracts/integrations/test/bridges/curve_bridge_mainnet_test.ts b/contracts/integrations/test/bridges/curve_bridge_mainnet_test.ts index 8954b7b1f1..bfcbf3cea4 100644 --- a/contracts/integrations/test/bridges/curve_bridge_mainnet_test.ts +++ b/contracts/integrations/test/bridges/curve_bridge_mainnet_test.ts @@ -4,107 +4,128 @@ import { ERC20TokenContract } from '@0x/contracts-erc20'; import { blockchainTests, constants, describe, toBaseUnitAmount } from '@0x/contracts-test-utils'; import { AbiEncoder } from '@0x/utils'; +const USDC_WALLET = '0xF977814e90dA44bFA03b6295A0616a897441aceC'; +const DAI_WALLET = '0x6cc5f688a315f3dc28a7781717a9a798a59fda7b'; +const WBTC_WALLET = '0x56178a0d5F301bAf6CF3e1Cd53d9863437345Bf9'; +blockchainTests.configure({ + fork: { + unlockedAccounts: [USDC_WALLET, DAI_WALLET, WBTC_WALLET], + }, +}); + blockchainTests.fork.resets('Mainnet curve bridge tests', env => { let testContract: CurveBridgeContract; - const receiver = '0x986ccf5234d9cfbb25246f1a5bfa51f4ccfcb308'; - const usdcWallet = '0xF977814e90dA44bFA03b6295A0616a897441aceC'; - const usdcAddress = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; - const daiWallet = '0x6cc5f688a315f3dc28a7781717a9a798a59fda7b'; - const daiAddress = '0x6B175474E89094C44Da98b954EedeAC495271d0F'; - const curveAddressUsdcDai = '0x2e60CF74d81ac34eB21eEff58Db4D385920ef419'; - const curveAddressUsdcDaiUsdt = '0x52EA46506B9CC5Ef470C5bf89f17Dc28bB35D85C'; - const daiTokenIdx = 0; - const usdcTokenIdx = 1; + const RECEIVER = '0x986ccf5234d9cfbb25246f1a5bfa51f4ccfcb308'; const bridgeDataEncoder = AbiEncoder.create([ { name: 'curveAddress', type: 'address' }, + { name: 'exchangeFunctionSelector', type: 'bytes4' }, + { name: 'fromTokenAddress', type: 'address' }, { name: 'fromTokenIdx', type: 'int128' }, { name: 'toTokenIdx', type: 'int128' }, - { name: 'version', type: 'int128' }, ]); before(async () => { testContract = await CurveBridgeContract.deployFrom0xArtifactAsync( assetProxyArtifacts.CurveBridge, env.provider, - { ...env.txDefaults, from: daiWallet }, + { ...env.txDefaults }, {}, ); }); describe('bridgeTransferFrom()', () => { - describe('Version 0', () => { - const version = 0; + describe('exchange_underlying()', () => { + const USDC_ADDRESS = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; + const DAI_ADDRESS = '0x6B175474E89094C44Da98b954EedeAC495271d0F'; + const DAI_TOKEN_IDX = 0; + const USDC_TOKEN_IDX = 1; + const EXCHANGE_UNDERLYING_SELECTOR = '0xa6417ed6'; + const CURVE_ADDRESS = '0x45f783cce6b7ff23b2ab2d70e416cdb7d6055f51'; it('succeeds exchanges DAI for USDC', async () => { - const bridgeData = bridgeDataEncoder.encode([curveAddressUsdcDai, daiTokenIdx, usdcTokenIdx, version]); + const bridgeData = bridgeDataEncoder.encode([ + CURVE_ADDRESS, + EXCHANGE_UNDERLYING_SELECTOR, + DAI_ADDRESS, + DAI_TOKEN_IDX, + USDC_TOKEN_IDX, + ]); // Fund the Bridge - const dai = new ERC20TokenContract(daiAddress, env.provider, { ...env.txDefaults, from: daiWallet }); + const dai = new ERC20TokenContract(DAI_ADDRESS, env.provider, { ...env.txDefaults, from: DAI_WALLET }); await dai .transfer(testContract.address, toBaseUnitAmount(1)) - .awaitTransactionSuccessAsync({ from: daiWallet }, { shouldValidate: false }); + .awaitTransactionSuccessAsync({}, { shouldValidate: false }); // Exchange via Curve await testContract .bridgeTransferFrom( - usdcAddress, + USDC_ADDRESS, constants.NULL_ADDRESS, - receiver, + RECEIVER, constants.ZERO_AMOUNT, bridgeData, ) - .awaitTransactionSuccessAsync({ from: daiWallet, gasPrice: 1 }, { shouldValidate: false }); + .awaitTransactionSuccessAsync({}, { shouldValidate: false }); }); it('succeeds exchanges USDC for DAI', async () => { - const bridgeData = bridgeDataEncoder.encode([curveAddressUsdcDai, usdcTokenIdx, daiTokenIdx, version]); + const bridgeData = bridgeDataEncoder.encode([ + CURVE_ADDRESS, + EXCHANGE_UNDERLYING_SELECTOR, + USDC_ADDRESS, + USDC_TOKEN_IDX, + DAI_TOKEN_IDX, + ]); // Fund the Bridge - const usdc = new ERC20TokenContract(usdcAddress, env.provider, { ...env.txDefaults, from: usdcWallet }); + const usdc = new ERC20TokenContract(USDC_ADDRESS, env.provider, { + ...env.txDefaults, + from: USDC_WALLET, + }); await usdc .transfer(testContract.address, toBaseUnitAmount(1, 6)) - .awaitTransactionSuccessAsync({ from: usdcWallet }, { shouldValidate: false }); + .awaitTransactionSuccessAsync({}, { shouldValidate: false }); // Exchange via Curve await testContract - .bridgeTransferFrom(daiAddress, constants.NULL_ADDRESS, receiver, constants.ZERO_AMOUNT, bridgeData) - .awaitTransactionSuccessAsync({ from: usdcWallet, gasPrice: 1 }, { shouldValidate: false }); + .bridgeTransferFrom( + DAI_ADDRESS, + constants.NULL_ADDRESS, + RECEIVER, + constants.ZERO_AMOUNT, + bridgeData, + ) + .awaitTransactionSuccessAsync({}, { shouldValidate: false }); }); }); - describe('Version 1', () => { - const version = 1; - it('succeeds exchanges DAI for USDC', async () => { + + describe('exchange()', () => { + const WBTC_ADDRESS = '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'; + const RENBTC_ADDRESS = '0xeb4c2781e4eba804ce9a9803c67d0893436bb27d'; + const RENBTC_TOKEN_IDX = 0; + const WBTC_TOKEN_IDX = 1; + const EXCHANGE_SELECTOR = '0x3df02124'; + const CURVE_ADDRESS = '0x7fc77b5c7614e1533320ea6ddc2eb61fa00a9714'; + it('succeeds exchanges WBTC for renBTC', async () => { const bridgeData = bridgeDataEncoder.encode([ - curveAddressUsdcDaiUsdt, - daiTokenIdx, - usdcTokenIdx, - version, + CURVE_ADDRESS, + EXCHANGE_SELECTOR, + WBTC_ADDRESS, + WBTC_TOKEN_IDX, + RENBTC_TOKEN_IDX, ]); // Fund the Bridge - const dai = new ERC20TokenContract(daiAddress, env.provider, { ...env.txDefaults, from: daiWallet }); - await dai - .transfer(testContract.address, toBaseUnitAmount(1)) - .awaitTransactionSuccessAsync({ from: daiWallet }, { shouldValidate: false }); + const wbtc = new ERC20TokenContract(WBTC_ADDRESS, env.provider, { + ...env.txDefaults, + from: WBTC_WALLET, + }); + await wbtc + .transfer(testContract.address, toBaseUnitAmount(1, 8)) + .awaitTransactionSuccessAsync({}, { shouldValidate: false }); // Exchange via Curve await testContract .bridgeTransferFrom( - usdcAddress, + RENBTC_ADDRESS, constants.NULL_ADDRESS, - receiver, + RECEIVER, constants.ZERO_AMOUNT, bridgeData, ) - .awaitTransactionSuccessAsync({ from: daiWallet, gasPrice: 1 }, { shouldValidate: false }); - }); - it('succeeds exchanges USDC for DAI', async () => { - const bridgeData = bridgeDataEncoder.encode([ - curveAddressUsdcDaiUsdt, - usdcTokenIdx, - daiTokenIdx, - version, - ]); - // Fund the Bridge - const usdc = new ERC20TokenContract(usdcAddress, env.provider, { ...env.txDefaults, from: usdcWallet }); - await usdc - .transfer(testContract.address, toBaseUnitAmount(1, 6)) - .awaitTransactionSuccessAsync({ from: usdcWallet }, { shouldValidate: false }); - // Exchange via Curve - await testContract - .bridgeTransferFrom(daiAddress, constants.NULL_ADDRESS, receiver, constants.ZERO_AMOUNT, bridgeData) - .awaitTransactionSuccessAsync({ from: usdcWallet, gasPrice: 1 }, { shouldValidate: false }); + .awaitTransactionSuccessAsync({ gas: 6e6 }, { shouldValidate: false }); }); }); });