From 70870ffcd20ccc4dcca87bedc6f6a3d38249a6d8 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 13 Dec 2019 10:53:25 -0800 Subject: [PATCH] Swallow reverts in ERC20BridgeSampler (#2395) * `@0x/erc20-bridge-sampler`: Do not query empty/unsigned orders. Swallow revets on DEX quotes. * `@0x/contracts-utils`: Add `DEV_UTILS_ADDRESS` and `KYBER_ETH_ADDRESS` to `DeploymentConstants`. * `@0x/contracts-erc20-bridge-sampler`: Address review comments. --- contracts/erc20-bridge-sampler/CHANGELOG.json | 9 + .../contracts/src/DeploymentConstants.sol | 92 ---- .../contracts/src/ERC20BridgeSampler.sol | 277 +++++++++--- .../contracts/src/IDevUtils.sol | 45 ++ .../contracts/src/IERC20BridgeSampler.sol | 53 ++- .../contracts/test/TestERC20BridgeSampler.sol | 96 ++-- contracts/erc20-bridge-sampler/package.json | 2 +- .../erc20-bridge-sampler/test/artifacts.ts | 4 +- .../test/erc20-bridge-sampler.ts | 421 +++++++++++++----- .../erc20-bridge-sampler/test/wrappers.ts | 2 +- contracts/erc20-bridge-sampler/tsconfig.json | 2 +- contracts/utils/CHANGELOG.json | 9 + .../contracts/src/DeploymentConstants.sol | 14 + 13 files changed, 716 insertions(+), 310 deletions(-) delete mode 100644 contracts/erc20-bridge-sampler/contracts/src/DeploymentConstants.sol create mode 100644 contracts/erc20-bridge-sampler/contracts/src/IDevUtils.sol diff --git a/contracts/erc20-bridge-sampler/CHANGELOG.json b/contracts/erc20-bridge-sampler/CHANGELOG.json index da633c6395..58bd2367af 100644 --- a/contracts/erc20-bridge-sampler/CHANGELOG.json +++ b/contracts/erc20-bridge-sampler/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "1.0.2", + "changes": [ + { + "note": "Do not query empty/unsigned orders. Swallow revets on DEX quotes.", + "pr": 2365 + } + ] + }, { "timestamp": 1575931811, "version": "1.0.1", diff --git a/contracts/erc20-bridge-sampler/contracts/src/DeploymentConstants.sol b/contracts/erc20-bridge-sampler/contracts/src/DeploymentConstants.sol deleted file mode 100644 index 639329fe79..0000000000 --- a/contracts/erc20-bridge-sampler/contracts/src/DeploymentConstants.sol +++ /dev/null @@ -1,92 +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-asset-proxy/contracts/src/interfaces/IUniswapExchangeFactory.sol"; -import "@0x/contracts-exchange/contracts/src/interfaces/IExchange.sol"; -import "./IEth2Dai.sol"; -import "./IKyberNetwork.sol"; - - -contract DeploymentConstants { - - /// @dev Address of the 0x Exchange contract. - address constant public EXCHANGE_ADDRESS = 0x080bf510FCbF18b91105470639e9561022937712; - /// @dev Address of the Eth2Dai MatchingMarket contract. - address constant public ETH2DAI_ADDRESS = 0x39755357759cE0d7f32dC8dC45414CCa409AE24e; - /// @dev Address of the UniswapExchangeFactory contract. - address constant public UNISWAP_EXCHANGE_FACTORY_ADDRESS = 0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95; - /// @dev Address of the KyberNeworkProxy contract. - address constant public KYBER_NETWORK_PROXY_ADDRESS = 0x818E6FECD516Ecc3849DAf6845e3EC868087B755; - /// @dev Address of the WETH contract. - address constant public WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; - /// @dev Kyber ETH pseudo-address. - address constant public KYBER_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - - /// @dev An overridable way to retrieve the 0x Exchange contract. - /// @return zeroex The 0x Exchange contract. - function _getExchangeContract() - internal - view - returns (IExchange zeroex) - { - return IExchange(EXCHANGE_ADDRESS); - } - - /// @dev An overridable way to retrieve the Eth2Dai exchange contract. - /// @return eth2dai The Eth2Dai exchange contract. - function _getEth2DaiContract() - internal - view - returns (IEth2Dai eth2dai) - { - return IEth2Dai(ETH2DAI_ADDRESS); - } - - /// @dev An overridable way to retrieve the Uniswap exchange factory contract. - /// @return uniswap The UniswapExchangeFactory contract. - function _getUniswapExchangeFactoryContract() - internal - view - returns (IUniswapExchangeFactory uniswap) - { - return IUniswapExchangeFactory(UNISWAP_EXCHANGE_FACTORY_ADDRESS); - } - - /// @dev An overridable way to retrieve the Kyber network proxy contract. - /// @return kyber The KyberNeworkProxy contract. - function _getKyberNetworkContract() - internal - view - returns (IKyberNetwork kyber) - { - return IKyberNetwork(KYBER_NETWORK_PROXY_ADDRESS); - } - - /// @dev An overridable way to retrieve the WETH contract address. - /// @return weth The WETH contract address. - function _getWETHAddress() - internal - view - returns (address weth) - { - return WETH_ADDRESS; - } -} diff --git a/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol b/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol index 235ffa79d5..c922b0765f 100644 --- a/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol +++ b/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol @@ -23,11 +23,13 @@ import "@0x/contracts-asset-proxy/contracts/src/interfaces/IUniswapExchangeFacto import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol"; import "@0x/contracts-exchange/contracts/src/interfaces/IExchange.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 "./IDevUtils.sol"; import "./IERC20BridgeSampler.sol"; import "./IEth2Dai.sol"; import "./IKyberNetwork.sol"; import "./IUniswapExchangeQuotes.sol"; -import "./DeploymentConstants.sol"; contract ERC20BridgeSampler is @@ -38,26 +40,32 @@ contract ERC20BridgeSampler is /// @dev Query native orders and sample sell quotes on multiple DEXes at once. /// @param orders Native orders to query. + /// @param orderSignatures Signatures for each respective order in `orders`. /// @param sources Address of each DEX. Passing in an unsupported DEX will throw. /// @param takerTokenAmounts Taker token sell amount for each sample. - /// @return orderInfos `OrderInfo`s for each order in `orders`. + /// @return orderFillableTakerAssetAmounts How much taker asset can be filled + /// by each order in `orders`. /// @return makerTokenAmountsBySource Maker amounts bought for each source at /// each taker token amount. First indexed by source index, then sample /// index. function queryOrdersAndSampleSells( LibOrder.Order[] memory orders, + bytes[] memory orderSignatures, address[] memory sources, uint256[] memory takerTokenAmounts ) public view returns ( - LibOrder.OrderInfo[] memory orderInfos, + uint256[] memory orderFillableTakerAssetAmounts, uint256[][] memory makerTokenAmountsBySource ) { - require(orders.length != 0, "EMPTY_ORDERS"); - orderInfos = queryOrders(orders); + require(orders.length != 0, "ERC20BridgeSampler/EMPTY_ORDERS"); + orderFillableTakerAssetAmounts = getOrderFillableTakerAssetAmounts( + orders, + orderSignatures + ); makerTokenAmountsBySource = sampleSells( sources, _assetDataToTokenAddress(orders[0].takerAssetData), @@ -68,26 +76,32 @@ contract ERC20BridgeSampler is /// @dev Query native orders and sample buy quotes on multiple DEXes at once. /// @param orders Native orders to query. + /// @param orderSignatures Signatures for each respective order in `orders`. /// @param sources Address of each DEX. Passing in an unsupported DEX will throw. /// @param makerTokenAmounts Maker token buy amount for each sample. - /// @return orderInfos `OrderInfo`s for each order in `orders`. + /// @return orderFillableMakerAssetAmounts How much maker asset can be filled + /// by each order in `orders`. /// @return takerTokenAmountsBySource Taker amounts sold for each source at /// each maker token amount. First indexed by source index, then sample /// index. function queryOrdersAndSampleBuys( LibOrder.Order[] memory orders, + bytes[] memory orderSignatures, address[] memory sources, uint256[] memory makerTokenAmounts ) public view returns ( - LibOrder.OrderInfo[] memory orderInfos, + uint256[] memory orderFillableMakerAssetAmounts, uint256[][] memory makerTokenAmountsBySource ) { - require(orders.length != 0, "EMPTY_ORDERS"); - orderInfos = queryOrders(orders); + require(orders.length != 0, "ERC20BridgeSampler/EMPTY_ORDERS"); + orderFillableMakerAssetAmounts = getOrderFillableMakerAssetAmounts( + orders, + orderSignatures + ); makerTokenAmountsBySource = sampleBuys( sources, _assetDataToTokenAddress(orders[0].takerAssetData), @@ -96,18 +110,77 @@ contract ERC20BridgeSampler is ); } - /// @dev Queries the status of several native orders. + /// @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`. + /// @return orderFillableTakerAssetAmounts How much taker asset can be filled + /// by each order in `orders`. + function getOrderFillableTakerAssetAmounts( + LibOrder.Order[] memory orders, + bytes[] memory orderSignatures + ) + 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; + } + ( + LibOrder.OrderInfo memory orderInfo, + uint256 fillableTakerAssetAmount, + bool isValidSignature + ) = IDevUtils(_getDevUtilsAddress()).getOrderRelevantState( + orders[i], + orderSignatures[i] + ); + // The fillable amount is zero if the order is not fillable or if the + // signature is invalid. + if (orderInfo.orderStatus != uint8(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. - /// @return orderInfos Order info for each respective order. - function queryOrders(LibOrder.Order[] memory orders) + /// @param orderSignatures Signatures for each respective order in `orders`. + /// @return orderFillableMakerAssetAmounts How much maker asset can be filled + /// by each order in `orders`. + function getOrderFillableMakerAssetAmounts( + LibOrder.Order[] memory orders, + bytes[] memory orderSignatures + ) public view - returns (LibOrder.OrderInfo[] memory orderInfos) + returns (uint256[] memory orderFillableMakerAssetAmounts) { - uint256 numOrders = orders.length; - orderInfos = new LibOrder.OrderInfo[](numOrders); - for (uint256 i = 0; i < numOrders; i++) { - orderInfos[i] = _getExchangeContract().getOrderInfo(orders[i]); + orderFillableMakerAssetAmounts = getOrderFillableTakerAssetAmounts( + orders, + orderSignatures + ); + // `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 + ); + } } } @@ -187,18 +260,24 @@ contract ERC20BridgeSampler is returns (uint256[] memory makerTokenAmounts) { _assertValidPair(makerToken, takerToken); - address _takerToken = takerToken == _getWETHAddress() ? KYBER_ETH_ADDRESS : takerToken; - address _makerToken = makerToken == _getWETHAddress() ? KYBER_ETH_ADDRESS : makerToken; + address _takerToken = takerToken == _getWethAddress() ? KYBER_ETH_ADDRESS : takerToken; + address _makerToken = makerToken == _getWethAddress() ? KYBER_ETH_ADDRESS : makerToken; uint256 takerTokenDecimals = _getTokenDecimals(takerToken); uint256 makerTokenDecimals = _getTokenDecimals(makerToken); uint256 numSamples = takerTokenAmounts.length; makerTokenAmounts = new uint256[](numSamples); for (uint256 i = 0; i < numSamples; i++) { - (uint256 rate,) = _getKyberNetworkContract().getExpectedRate( - _takerToken, - _makerToken, - takerTokenAmounts[i] - ); + (bool didSucceed, bytes memory resultData) = + _getKyberNetworkProxyAddress().staticcall(abi.encodeWithSelector( + IKyberNetwork(0).getExpectedRate.selector, + _takerToken, + _makerToken, + takerTokenAmounts[i] + )); + uint256 rate = 0; + if (didSucceed) { + rate = abi.decode(resultData, (uint256)); + } makerTokenAmounts[i] = rate * takerTokenAmounts[i] * @@ -227,11 +306,18 @@ contract ERC20BridgeSampler is uint256 numSamples = takerTokenAmounts.length; makerTokenAmounts = new uint256[](numSamples); for (uint256 i = 0; i < numSamples; i++) { - makerTokenAmounts[i] = _getEth2DaiContract().getBuyAmount( - makerToken, - takerToken, - takerTokenAmounts[i] - ); + (bool didSucceed, bytes memory resultData) = + _getEth2DaiAddress().staticcall(abi.encodeWithSelector( + IEth2Dai(0).getBuyAmount.selector, + makerToken, + takerToken, + takerTokenAmounts[i] + )); + uint256 buyAmount = 0; + if (didSucceed) { + buyAmount = abi.decode(resultData, (uint256)); + } + makerTokenAmounts[i] = buyAmount; } } @@ -254,11 +340,18 @@ contract ERC20BridgeSampler is uint256 numSamples = makerTokenAmounts.length; takerTokenAmounts = new uint256[](numSamples); for (uint256 i = 0; i < numSamples; i++) { - takerTokenAmounts[i] = _getEth2DaiContract().getPayAmount( - takerToken, - makerToken, - makerTokenAmounts[i] - ); + (bool didSucceed, bytes memory resultData) = + _getEth2DaiAddress().staticcall(abi.encodeWithSelector( + IEth2Dai(0).getPayAmount.selector, + takerToken, + makerToken, + makerTokenAmounts[i] + )); + uint256 sellAmount = 0; + if (didSucceed) { + sellAmount = abi.decode(resultData, (uint256)); + } + takerTokenAmounts[i] = sellAmount; } } @@ -280,26 +373,38 @@ contract ERC20BridgeSampler is _assertValidPair(makerToken, takerToken); uint256 numSamples = takerTokenAmounts.length; makerTokenAmounts = new uint256[](numSamples); - IUniswapExchangeQuotes takerTokenExchange = takerToken == _getWETHAddress() ? + IUniswapExchangeQuotes takerTokenExchange = takerToken == _getWethAddress() ? IUniswapExchangeQuotes(0) : _getUniswapExchange(takerToken); - IUniswapExchangeQuotes makerTokenExchange = makerToken == _getWETHAddress() ? + IUniswapExchangeQuotes makerTokenExchange = makerToken == _getWethAddress() ? IUniswapExchangeQuotes(0) : _getUniswapExchange(makerToken); for (uint256 i = 0; i < numSamples; i++) { - if (makerToken == _getWETHAddress()) { - makerTokenAmounts[i] = takerTokenExchange.getTokenToEthInputPrice( + if (makerToken == _getWethAddress()) { + makerTokenAmounts[i] = _callUniswapExchangePriceFunction( + address(takerTokenExchange), + takerTokenExchange.getTokenToEthInputPrice.selector, takerTokenAmounts[i] ); - } else if (takerToken == _getWETHAddress()) { - makerTokenAmounts[i] = makerTokenExchange.getEthToTokenInputPrice( + } else if (takerToken == _getWethAddress()) { + makerTokenAmounts[i] = _callUniswapExchangePriceFunction( + address(makerTokenExchange), + makerTokenExchange.getEthToTokenInputPrice.selector, takerTokenAmounts[i] ); } else { - uint256 ethBought = takerTokenExchange.getTokenToEthInputPrice( + uint256 ethBought = _callUniswapExchangePriceFunction( + address(takerTokenExchange), + takerTokenExchange.getTokenToEthInputPrice.selector, takerTokenAmounts[i] ); - makerTokenAmounts[i] = makerTokenExchange.getEthToTokenInputPrice( - ethBought - ); + if (ethBought != 0) { + makerTokenAmounts[i] = _callUniswapExchangePriceFunction( + address(makerTokenExchange), + makerTokenExchange.getEthToTokenInputPrice.selector, + ethBought + ); + } else { + makerTokenAmounts[i] = 0; + } } } } @@ -322,26 +427,38 @@ contract ERC20BridgeSampler is _assertValidPair(makerToken, takerToken); uint256 numSamples = makerTokenAmounts.length; takerTokenAmounts = new uint256[](numSamples); - IUniswapExchangeQuotes takerTokenExchange = takerToken == _getWETHAddress() ? + IUniswapExchangeQuotes takerTokenExchange = takerToken == _getWethAddress() ? IUniswapExchangeQuotes(0) : _getUniswapExchange(takerToken); - IUniswapExchangeQuotes makerTokenExchange = makerToken == _getWETHAddress() ? + IUniswapExchangeQuotes makerTokenExchange = makerToken == _getWethAddress() ? IUniswapExchangeQuotes(0) : _getUniswapExchange(makerToken); for (uint256 i = 0; i < numSamples; i++) { - if (makerToken == _getWETHAddress()) { - takerTokenAmounts[i] = takerTokenExchange.getTokenToEthOutputPrice( + if (makerToken == _getWethAddress()) { + takerTokenAmounts[i] = _callUniswapExchangePriceFunction( + address(takerTokenExchange), + takerTokenExchange.getTokenToEthOutputPrice.selector, makerTokenAmounts[i] ); - } else if (takerToken == _getWETHAddress()) { - takerTokenAmounts[i] = makerTokenExchange.getEthToTokenOutputPrice( + } else if (takerToken == _getWethAddress()) { + takerTokenAmounts[i] = _callUniswapExchangePriceFunction( + address(makerTokenExchange), + makerTokenExchange.getEthToTokenOutputPrice.selector, makerTokenAmounts[i] ); } else { - uint256 ethSold = makerTokenExchange.getEthToTokenOutputPrice( + uint256 ethSold = _callUniswapExchangePriceFunction( + address(makerTokenExchange), + makerTokenExchange.getEthToTokenOutputPrice.selector, makerTokenAmounts[i] ); - takerTokenAmounts[i] = takerTokenExchange.getTokenToEthOutputPrice( - ethSold - ); + if (ethSold != 0) { + takerTokenAmounts[i] = _callUniswapExchangePriceFunction( + address(takerTokenExchange), + takerTokenExchange.getTokenToEthOutputPrice.selector, + ethSold + ); + } else { + takerTokenAmounts[i] = 0; + } } } } @@ -357,6 +474,34 @@ contract ERC20BridgeSampler is 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) + { + if (uniswapExchangeAddress == address(0)) { + return 0; + } + (bool didSucceed, bytes memory resultData) = + uniswapExchangeAddress.staticcall(abi.encodeWithSelector( + functionSelector, + inputAmount + )); + if (didSucceed) { + outputAmount = abi.decode(resultData, (uint256)); + } + } + /// @dev Samples a supported sell source, defined by its address. /// @param takerToken Address of the taker token (what to sell). /// @param makerToken Address of the maker token (what to buy). @@ -373,16 +518,16 @@ contract ERC20BridgeSampler is view returns (uint256[] memory makerTokenAmounts) { - if (source == address(_getEth2DaiContract())) { + if (source == _getEth2DaiAddress()) { return sampleSellsFromEth2Dai(takerToken, makerToken, takerTokenAmounts); } - if (source == address(_getUniswapExchangeFactoryContract())) { + if (source == _getUniswapExchangeFactoryAddress()) { return sampleSellsFromUniswap(takerToken, makerToken, takerTokenAmounts); } - if (source == address(_getKyberNetworkContract())) { + if (source == _getKyberNetworkProxyAddress()) { return sampleSellsFromKyberNetwork(takerToken, makerToken, takerTokenAmounts); } - revert("UNSUPPORTED_SOURCE"); + revert("ERC20BridgeSampler/UNSUPPORTED_SOURCE"); } /// @dev Samples a supported buy source, defined by its address. @@ -401,13 +546,13 @@ contract ERC20BridgeSampler is view returns (uint256[] memory takerTokenAmounts) { - if (source == address(_getEth2DaiContract())) { + if (source == _getEth2DaiAddress()) { return sampleBuysFromEth2Dai(takerToken, makerToken, makerTokenAmounts); } - if (source == address(_getUniswapExchangeFactoryContract())) { + if (source == _getUniswapExchangeFactoryAddress()) { return sampleBuysFromUniswap(takerToken, makerToken, makerTokenAmounts); } - revert("UNSUPPORTED_SOURCE"); + revert("ERC20BridgeSampler/UNSUPPORTED_SOURCE"); } /// @dev Retrive an existing Uniswap exchange contract. @@ -420,9 +565,9 @@ contract ERC20BridgeSampler is returns (IUniswapExchangeQuotes exchange) { exchange = IUniswapExchangeQuotes( - address(_getUniswapExchangeFactoryContract().getExchange(tokenAddress)) + address(IUniswapExchangeFactory(_getUniswapExchangeFactoryAddress()) + .getExchange(tokenAddress)) ); - require(address(exchange) != address(0), "UNSUPPORTED_UNISWAP_EXCHANGE"); } /// @dev Extract the token address from ERC20 proxy asset data. @@ -433,19 +578,19 @@ contract ERC20BridgeSampler is pure returns (address tokenAddress) { - require(assetData.length == 36, "INVALID_ASSET_DATA"); + require(assetData.length == 36, "ERC20BridgeSampler/INVALID_ASSET_DATA"); bytes4 selector; assembly { selector := and(mload(add(assetData, 0x20)), 0xFFFFFFFF00000000000000000000000000000000000000000000000000000000) tokenAddress := mload(add(assetData, 0x24)) } - require(selector == ERC20_PROXY_ID, "UNSUPPORTED_ASSET_PROXY"); + require(selector == ERC20_PROXY_ID, "ERC20BridgeSampler/UNSUPPORTED_ASSET_PROXY"); } function _assertValidPair(address makerToken, address takerToken) private pure { - require(makerToken != takerToken, "INVALID_TOKEN_PAIR"); + require(makerToken != takerToken, "ERC20BridgeSampler/INVALID_TOKEN_PAIR"); } } diff --git a/contracts/erc20-bridge-sampler/contracts/src/IDevUtils.sol b/contracts/erc20-bridge-sampler/contracts/src/IDevUtils.sol new file mode 100644 index 0000000000..9d6bb53196 --- /dev/null +++ b/contracts/erc20-bridge-sampler/contracts/src/IDevUtils.sol @@ -0,0 +1,45 @@ +/* + + 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 IDevUtils { + + /// @dev Fetches all order-relevant information needed to validate if the supplied order is fillable. + /// @param order The order structure. + /// @param signature Signature provided by maker that proves the order's authenticity. + /// `0x01` can always be provided if the signature does not need to be validated. + /// @return The orderInfo (hash, status, and `takerAssetAmount` already filled for the given order), + /// fillableTakerAssetAmount (amount of the order's `takerAssetAmount` that is fillable given all on-chain state), + /// and isValidSignature (validity of the provided signature). + /// NOTE: If the `takerAssetData` encodes data for multiple assets, `fillableTakerAssetAmount` will represent a "scaled" + /// amount, meaning it must be multiplied by all the individual asset amounts within the `takerAssetData` to get the final + /// amount of each asset that can be filled. + function getOrderRelevantState(LibOrder.Order calldata order, bytes calldata signature) + external + view + returns ( + LibOrder.OrderInfo memory orderInfo, + uint256 fillableTakerAssetAmount, + bool isValidSignature + ); +} diff --git a/contracts/erc20-bridge-sampler/contracts/src/IERC20BridgeSampler.sol b/contracts/erc20-bridge-sampler/contracts/src/IERC20BridgeSampler.sol index bdbc92d393..9680429d32 100644 --- a/contracts/erc20-bridge-sampler/contracts/src/IERC20BridgeSampler.sol +++ b/contracts/erc20-bridge-sampler/contracts/src/IERC20BridgeSampler.sol @@ -19,59 +19,82 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; -import "@0x/contracts-exchange/contracts/src/interfaces/IExchange.sol"; import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol"; interface IERC20BridgeSampler { - /// @dev Query native orders and sample sell orders on multiple DEXes at once. + /// @dev Query native orders and sample sell quotes on multiple DEXes at once. /// @param orders Native orders to query. - /// @param sources Address of each DEX. Passing in an unknown DEX will throw. - /// @param takerTokenAmounts Taker sell amount for each sample. - /// @return orderInfos `OrderInfo`s for each order in `orders`. + /// @param orderSignatures Signatures for each respective order in `orders`. + /// @param sources Address of each DEX. Passing in an unsupported DEX will throw. + /// @param takerTokenAmounts Taker token sell amount for each sample. + /// @return orderFillableTakerAssetAmounts How much taker asset can be filled + /// by each order in `orders`. /// @return makerTokenAmountsBySource Maker amounts bought for each source at /// each taker token amount. First indexed by source index, then sample /// index. function queryOrdersAndSampleSells( LibOrder.Order[] calldata orders, + bytes[] calldata orderSignatures, address[] calldata sources, uint256[] calldata takerTokenAmounts ) external view returns ( - LibOrder.OrderInfo[] memory orderInfos, + uint256[] memory orderFillableTakerAssetAmounts, uint256[][] memory makerTokenAmountsBySource ); - /// @dev Query native orders and sample buy orders on multiple DEXes at once. + /// @dev Query native orders and sample buy quotes on multiple DEXes at once. /// @param orders Native orders to query. - /// @param sources Address of each DEX. Passing in an unknown DEX will throw. - /// @param makerTokenAmounts Maker sell amount for each sample. - /// @return orderInfos `OrderInfo`s for each order in `orders`. + /// @param orderSignatures Signatures for each respective order in `orders`. + /// @param sources Address of each DEX. Passing in an unsupported DEX will throw. + /// @param makerTokenAmounts Maker token buy amount for each sample. + /// @return orderFillableMakerAssetAmounts How much maker asset can be filled + /// by each order in `orders`. /// @return takerTokenAmountsBySource Taker amounts sold for each source at /// each maker token amount. First indexed by source index, then sample /// index. function queryOrdersAndSampleBuys( LibOrder.Order[] calldata orders, + bytes[] calldata orderSignatures, address[] calldata sources, uint256[] calldata makerTokenAmounts ) external view returns ( - LibOrder.OrderInfo[] memory orderInfos, + uint256[] memory orderFillableMakerAssetAmounts, uint256[][] memory makerTokenAmountsBySource ); - /// @dev Queries the status of several native orders. + /// @dev Queries the fillable taker asset amounts of native orders. /// @param orders Native orders to query. - /// @return orderInfos Order info for each respective order. - function queryOrders(LibOrder.Order[] calldata orders) + /// @param orderSignatures Signatures for each respective order in `orders`. + /// @return orderFillableTakerAssetAmounts How much taker asset can be filled + /// by each order in `orders`. + function getOrderFillableTakerAssetAmounts( + LibOrder.Order[] calldata orders, + bytes[] calldata orderSignatures + ) + 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`. + /// @return orderFillableMakerAssetAmounts How much maker asset can be filled + /// by each order in `orders`. + function getOrderFillableMakerAssetAmounts( + LibOrder.Order[] calldata orders, + bytes[] calldata orderSignatures + ) external view - returns (LibOrder.OrderInfo[] memory orderInfos); + returns (uint256[] memory orderFillableMakerAssetAmounts); /// @dev Sample sell quotes on multiple DEXes at once. /// @param sources Address of each DEX. Passing in an unsupported DEX will throw. diff --git a/contracts/erc20-bridge-sampler/contracts/test/TestERC20BridgeSampler.sol b/contracts/erc20-bridge-sampler/contracts/test/TestERC20BridgeSampler.sol index e08b34cc35..dbc7fa0ec7 100644 --- a/contracts/erc20-bridge-sampler/contracts/test/TestERC20BridgeSampler.sol +++ b/contracts/erc20-bridge-sampler/contracts/test/TestERC20BridgeSampler.sol @@ -23,6 +23,7 @@ import "@0x/contracts-exchange/contracts/src/interfaces/IExchange.sol"; import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol"; import "../src/ERC20BridgeSampler.sol"; import "../src/IEth2Dai.sol"; +import "../src/IDevUtils.sol"; import "../src/IKyberNetwork.sol"; @@ -90,9 +91,28 @@ library LibDeterministicQuotes { } +contract FailTrigger { + + // Give this address a balance to force operations to fail. + address payable constant public FAILURE_ADDRESS = 0xe9dB8717BC5DFB20aaf538b4a5a02B7791FF430C; + + // Funds `FAILURE_ADDRESS`. + function enableFailTrigger() external payable { + FAILURE_ADDRESS.transfer(msg.value); + } + + function _revertIfShouldFail() internal view { + if (FAILURE_ADDRESS.balance != 0) { + revert("FAIL_TRIGGERED"); + } + } +} + + contract TestERC20BridgeSamplerUniswapExchange is IUniswapExchangeQuotes, - DeploymentConstants + DeploymentConstants, + FailTrigger { bytes32 constant private BASE_SALT = 0x1d6a6a0506b0b4a554b907a4c29d9f4674e461989d9c1921feb17b26716385ab; @@ -112,10 +132,11 @@ contract TestERC20BridgeSamplerUniswapExchange is view returns (uint256 tokensBought) { + _revertIfShouldFail(); return LibDeterministicQuotes.getDeterministicSellQuote( salt, tokenAddress, - WETH_ADDRESS, + _getWethAddress(), ethSold ); } @@ -128,9 +149,10 @@ contract TestERC20BridgeSamplerUniswapExchange is view returns (uint256 ethSold) { + _revertIfShouldFail(); return LibDeterministicQuotes.getDeterministicBuyQuote( salt, - WETH_ADDRESS, + _getWethAddress(), tokenAddress, tokensBought ); @@ -144,10 +166,11 @@ contract TestERC20BridgeSamplerUniswapExchange is view returns (uint256 ethBought) { + _revertIfShouldFail(); return LibDeterministicQuotes.getDeterministicSellQuote( salt, tokenAddress, - WETH_ADDRESS, + _getWethAddress(), tokensSold ); } @@ -160,9 +183,10 @@ contract TestERC20BridgeSamplerUniswapExchange is view returns (uint256 tokensSold) { + _revertIfShouldFail(); return LibDeterministicQuotes.getDeterministicBuyQuote( salt, - WETH_ADDRESS, + _getWethAddress(), tokenAddress, ethBought ); @@ -172,7 +196,8 @@ contract TestERC20BridgeSamplerUniswapExchange is contract TestERC20BridgeSamplerKyberNetwork is IKyberNetwork, - DeploymentConstants + DeploymentConstants, + FailTrigger { bytes32 constant private SALT = 0x0ff3ca9d46195c39f9a12afb74207b4970349fb3cfb1e459bbf170298d326bc7; address constant public ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; @@ -187,8 +212,9 @@ contract TestERC20BridgeSamplerKyberNetwork is view returns (uint256 expectedRate, uint256) { - fromToken = fromToken == ETH_ADDRESS ? WETH_ADDRESS : fromToken; - toToken = toToken == ETH_ADDRESS ? WETH_ADDRESS : toToken; + _revertIfShouldFail(); + fromToken = fromToken == ETH_ADDRESS ? _getWethAddress() : fromToken; + toToken = toToken == ETH_ADDRESS ? _getWethAddress() : toToken; expectedRate = LibDeterministicQuotes.getDeterministicRate( SALT, fromToken, @@ -199,7 +225,8 @@ contract TestERC20BridgeSamplerKyberNetwork is contract TestERC20BridgeSamplerEth2Dai is - IEth2Dai + IEth2Dai, + FailTrigger { bytes32 constant private SALT = 0xb713b61bb9bb2958a0f5d1534b21e94fc68c4c0c034b0902ed844f2f6cd1b4f7; @@ -213,6 +240,7 @@ contract TestERC20BridgeSamplerEth2Dai is view returns (uint256 buyAmount) { + _revertIfShouldFail(); return LibDeterministicQuotes.getDeterministicSellQuote( SALT, payToken, @@ -231,6 +259,7 @@ contract TestERC20BridgeSamplerEth2Dai is view returns (uint256 payAmount) { + _revertIfShouldFail(); return LibDeterministicQuotes.getDeterministicBuyQuote( SALT, payToken, @@ -269,7 +298,8 @@ contract TestERC20BridgeSamplerUniswapExchangeFactory is contract TestERC20BridgeSampler is - ERC20BridgeSampler + ERC20BridgeSampler, + FailTrigger { TestERC20BridgeSamplerUniswapExchangeFactory public uniswap; TestERC20BridgeSamplerEth2Dai public eth2Dai; @@ -288,18 +318,30 @@ contract TestERC20BridgeSampler is uniswap.createTokenExchanges(tokenAddresses); } - // `IExchange.getOrderInfo()`, overridden to return deterministic order infos. - function getOrderInfo(LibOrder.Order memory order) + // `IDevUtils.getOrderRelevantState()`, overridden to return deterministic + // states. + function getOrderRelevantState( + LibOrder.Order memory order, + bytes memory + ) public - pure - returns (LibOrder.OrderInfo memory orderInfo) + view + returns ( + LibOrder.OrderInfo memory orderInfo, + uint256 fillableTakerAssetAmount, + bool isValidSignature + ) { // The order hash is just the hash of the salt. bytes32 orderHash = keccak256(abi.encode(order.salt)); // Everything else is derived from the hash. orderInfo.orderHash = orderHash; orderInfo.orderStatus = uint8(uint256(orderHash) % uint8(-1)); - orderInfo.orderTakerAssetFilledAmount = uint256(orderHash) % order.takerAssetAmount; + orderInfo.orderTakerAssetFilledAmount = + uint256(orderHash) % order.takerAssetAmount; + fillableTakerAssetAmount = + order.takerAssetAmount - orderInfo.orderTakerAssetFilledAmount; + isValidSignature = uint256(orderHash) % 2 == 1; } // Overriden to return deterministic decimals. @@ -312,38 +354,38 @@ contract TestERC20BridgeSampler is } // Overriden to point to a this contract. - function _getExchangeContract() + function _getDevUtilsAddress() internal view - returns (IExchange zeroex) + returns (address devUtilAddress) { - return IExchange(address(this)); + return address(this); } // Overriden to point to a custom contract. - function _getEth2DaiContract() + function _getEth2DaiAddress() internal view - returns (IEth2Dai eth2dai_) + returns (address eth2daiAddress) { - return eth2Dai; + return address(eth2Dai); } // Overriden to point to a custom contract. - function _getUniswapExchangeFactoryContract() + function _getUniswapExchangeFactoryAddress() internal view - returns (IUniswapExchangeFactory uniswap_) + returns (address uniswapAddress) { - return uniswap; + return address(uniswap); } // Overriden to point to a custom contract. - function _getKyberNetworkContract() + function _getKyberNetworkProxyAddress() internal view - returns (IKyberNetwork kyber_) + returns (address kyberAddress) { - return kyber; + return address(kyber); } } diff --git a/contracts/erc20-bridge-sampler/package.json b/contracts/erc20-bridge-sampler/package.json index c137b64c51..d1e94270a7 100644 --- a/contracts/erc20-bridge-sampler/package.json +++ b/contracts/erc20-bridge-sampler/package.json @@ -38,7 +38,7 @@ "config": { "publicInterfaceContracts": "ERC20BridgeSampler,IERC20BridgeSampler", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(DeploymentConstants|ERC20BridgeSampler|IERC20BridgeSampler|IEth2Dai|IKyberNetwork|IUniswapExchangeQuotes|TestERC20BridgeSampler).json" + "abis": "./test/generated-artifacts/@(ERC20BridgeSampler|IDevUtils|IERC20BridgeSampler|IEth2Dai|IKyberNetwork|IUniswapExchangeQuotes|TestERC20BridgeSampler).json" }, "repository": { "type": "git", diff --git a/contracts/erc20-bridge-sampler/test/artifacts.ts b/contracts/erc20-bridge-sampler/test/artifacts.ts index a6910076ce..d6d708a2b8 100644 --- a/contracts/erc20-bridge-sampler/test/artifacts.ts +++ b/contracts/erc20-bridge-sampler/test/artifacts.ts @@ -5,16 +5,16 @@ */ import { ContractArtifact } from 'ethereum-types'; -import * as DeploymentConstants from '../test/generated-artifacts/DeploymentConstants.json'; import * as ERC20BridgeSampler from '../test/generated-artifacts/ERC20BridgeSampler.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 IKyberNetwork from '../test/generated-artifacts/IKyberNetwork.json'; import * as IUniswapExchangeQuotes from '../test/generated-artifacts/IUniswapExchangeQuotes.json'; import * as TestERC20BridgeSampler from '../test/generated-artifacts/TestERC20BridgeSampler.json'; export const artifacts = { - DeploymentConstants: DeploymentConstants as ContractArtifact, ERC20BridgeSampler: ERC20BridgeSampler as ContractArtifact, + IDevUtils: IDevUtils as ContractArtifact, IERC20BridgeSampler: IERC20BridgeSampler as ContractArtifact, IEth2Dai: IEth2Dai as ContractArtifact, IKyberNetwork: IKyberNetwork 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 f8bf44132f..26c9cae9f9 100644 --- a/contracts/erc20-bridge-sampler/test/erc20-bridge-sampler.ts +++ b/contracts/erc20-bridge-sampler/test/erc20-bridge-sampler.ts @@ -6,7 +6,7 @@ import { getRandomPortion, randomAddress, } from '@0x/contracts-test-utils'; -import { Order, OrderInfo } from '@0x/types'; +import { Order } from '@0x/types'; import { BigNumber, hexUtils } from '@0x/utils'; import * as _ from 'lodash'; @@ -30,12 +30,13 @@ blockchainTests('erc20-bridge-sampler', env => { const INVALID_ASSET_DATA = hexUtils.random(37); const SELL_SOURCES = ['Eth2Dai', 'Kyber', 'Uniswap']; const BUY_SOURCES = ['Eth2Dai', 'Uniswap']; - const EMPTY_ORDERS_ERROR = 'EMPTY_ORDERS'; - const UNSUPPORTED_ASSET_PROXY_ERROR = 'UNSUPPORTED_ASSET_PROXY'; - const INVALID_ASSET_DATA_ERROR = 'INVALID_ASSET_DATA'; - const UNSUPPORTED_UNISWAP_EXCHANGE_ERROR = 'UNSUPPORTED_UNISWAP_EXCHANGE'; - const UNSUPPORTED_SOURCE_ERROR = 'UNSUPPORTED_SOURCE'; - const INVALID_TOKEN_PAIR_ERROR = 'INVALID_TOKEN_PAIR'; + const EMPTY_ORDERS_ERROR = 'ERC20BridgeSampler/EMPTY_ORDERS'; + const UNSUPPORTED_ASSET_PROXY_ERROR = 'ERC20BridgeSampler/UNSUPPORTED_ASSET_PROXY'; + const INVALID_ASSET_DATA_ERROR = 'ERC20BridgeSampler/INVALID_ASSET_DATA'; + const UNSUPPORTED_SOURCE_ERROR = 'ERC20BridgeSampler/UNSUPPORTED_SOURCE'; + const INVALID_TOKEN_PAIR_ERROR = 'ERC20BridgeSampler/INVALID_TOKEN_PAIR'; + const MAKER_TOKEN = randomAddress(); + const TAKER_TOKEN = randomAddress(); before(async () => { testContract = await TestERC20BridgeSamplerContract.deployFrom0xArtifactAsync( @@ -192,13 +193,22 @@ blockchainTests('erc20-bridge-sampler', env => { return quotes; } - function getDeterministicOrderInfo(order: Order): OrderInfo { - const hash = getPackedHash(hexUtils.leftPad(order.salt, 32)); - return { - orderHash: hash, - orderStatus: new BigNumber(hash).mod(255).toNumber(), - orderTakerAssetFilledAmount: new BigNumber(hash).mod(order.takerAssetAmount), - }; + function getDeterministicFillableTakerAssetAmount(order: Order): BigNumber { + const hash = getPackedHash(hexUtils.toHex(order.salt, 32)); + const orderStatus = new BigNumber(hash).mod(255).toNumber(); + const isValidSignature = !!new BigNumber(hash).mod(2).toNumber(); + if (orderStatus !== 3 || !isValidSignature) { + return constants.ZERO_AMOUNT; + } + return order.takerAssetAmount.minus(new BigNumber(hash).mod(order.takerAssetAmount)); + } + + function getDeterministicFillableMakerAssetAmount(order: Order): BigNumber { + const takerAmount = getDeterministicFillableTakerAssetAmount(order); + return order.makerAssetAmount + .times(takerAmount) + .div(order.takerAssetAmount) + .integerValue(BigNumber.ROUND_DOWN); } function getERC20AssetData(tokenAddress: string): string { @@ -238,57 +248,115 @@ blockchainTests('erc20-bridge-sampler', env => { return _.times(count || _.random(1, 16), () => createOrder(makerToken, takerToken)); } - describe('queryOrders()', () => { - const MAKER_TOKEN = randomAddress(); - const TAKER_TOKEN = randomAddress(); + async function enableFailTriggerAsync(): Promise { + await testContract.enableFailTrigger().awaitTransactionSuccessAsync({ value: 1 }); + } - it('returns the results of `getOrderInfo()` for each order', async () => { + describe('getOrderFillableTakerAssetAmounts()', () => { + it('returns the expected amount for each order', async () => { const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN); - const expected = orders.map(getDeterministicOrderInfo); - const actual = await testContract.queryOrders(orders).callAsync(); + const signatures: string[] = _.times(orders.length, hexUtils.random); + const expected = orders.map(getDeterministicFillableTakerAssetAmount); + const actual = await testContract.getOrderFillableTakerAssetAmounts(orders, signatures).callAsync(); expect(actual).to.deep.eq(expected); }); it('returns empty for no orders', async () => { - const actual = await testContract.queryOrders([]).callAsync(); + const actual = await testContract.getOrderFillableTakerAssetAmounts([], []).callAsync(); expect(actual).to.deep.eq([]); }); + + it('returns zero for an order with zero maker asset amount', async () => { + const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN, 1); + orders[0].makerAssetAmount = constants.ZERO_AMOUNT; + const signatures: string[] = _.times(orders.length, hexUtils.random); + const actual = await testContract.getOrderFillableTakerAssetAmounts(orders, signatures).callAsync(); + expect(actual).to.deep.eq([constants.ZERO_AMOUNT]); + }); + + it('returns zero for an order with zero taker asset amount', async () => { + const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN, 1); + orders[0].takerAssetAmount = constants.ZERO_AMOUNT; + const signatures: string[] = _.times(orders.length, hexUtils.random); + const actual = await testContract.getOrderFillableTakerAssetAmounts(orders, signatures).callAsync(); + expect(actual).to.deep.eq([constants.ZERO_AMOUNT]); + }); + + it('returns zero for an order with an empty signature', async () => { + const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN, 1); + const signatures: string[] = _.times(orders.length, () => constants.NULL_BYTES); + const actual = await testContract.getOrderFillableTakerAssetAmounts(orders, signatures).callAsync(); + expect(actual).to.deep.eq([constants.ZERO_AMOUNT]); + }); + }); + + describe('getOrderFillableMakerAssetAmounts()', () => { + it('returns the expected amount for each order', async () => { + const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN); + const signatures: string[] = _.times(orders.length, hexUtils.random); + const expected = orders.map(getDeterministicFillableMakerAssetAmount); + const actual = await testContract.getOrderFillableMakerAssetAmounts(orders, signatures).callAsync(); + expect(actual).to.deep.eq(expected); + }); + + it('returns empty for no orders', async () => { + const actual = await testContract.getOrderFillableMakerAssetAmounts([], []).callAsync(); + expect(actual).to.deep.eq([]); + }); + + it('returns zero for an order with zero maker asset amount', async () => { + const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN, 1); + orders[0].makerAssetAmount = constants.ZERO_AMOUNT; + const signatures: string[] = _.times(orders.length, hexUtils.random); + const actual = await testContract.getOrderFillableMakerAssetAmounts(orders, signatures).callAsync(); + expect(actual).to.deep.eq([constants.ZERO_AMOUNT]); + }); + + it('returns zero for an order with zero taker asset amount', async () => { + const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN, 1); + orders[0].takerAssetAmount = constants.ZERO_AMOUNT; + const signatures: string[] = _.times(orders.length, hexUtils.random); + const actual = await testContract.getOrderFillableMakerAssetAmounts(orders, signatures).callAsync(); + expect(actual).to.deep.eq([constants.ZERO_AMOUNT]); + }); + + it('returns zero for an order with an empty signature', async () => { + const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN, 1); + const signatures: string[] = _.times(orders.length, () => constants.NULL_BYTES); + const actual = await testContract.getOrderFillableMakerAssetAmounts(orders, signatures).callAsync(); + expect(actual).to.deep.eq([constants.ZERO_AMOUNT]); + }); }); describe('queryOrdersAndSampleSells()', () => { - const MAKER_TOKEN = randomAddress(); - const TAKER_TOKEN = randomAddress(); + const ORDERS = createOrders(MAKER_TOKEN, TAKER_TOKEN); + const SIGNATURES: string[] = _.times(ORDERS.length, hexUtils.random); before(async () => { await testContract.createTokenExchanges([MAKER_TOKEN, TAKER_TOKEN]).awaitTransactionSuccessAsync(); }); - it('returns the results of `getOrderInfo()` for each order', async () => { + it('returns expected fillable amounts for each order', async () => { const takerTokenAmounts = getSampleAmounts(TAKER_TOKEN); - const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN); - const expectedOrderInfos = orders.map(getDeterministicOrderInfo); + const expectedFillableAmounts = ORDERS.map(getDeterministicFillableTakerAssetAmount); const [orderInfos] = await testContract - .queryOrdersAndSampleSells(orders, SELL_SOURCES.map(n => allSources[n]), takerTokenAmounts) + .queryOrdersAndSampleSells(ORDERS, SIGNATURES, SELL_SOURCES.map(n => allSources[n]), takerTokenAmounts) .callAsync(); - expect(orderInfos).to.deep.eq(expectedOrderInfos); + expect(orderInfos).to.deep.eq(expectedFillableAmounts); }); it('can return quotes for all sources', async () => { const sampleAmounts = getSampleAmounts(TAKER_TOKEN); const expectedQuotes = getDeterministicSellQuotes(TAKER_TOKEN, MAKER_TOKEN, SELL_SOURCES, sampleAmounts); const [, quotes] = await testContract - .queryOrdersAndSampleSells( - createOrders(MAKER_TOKEN, TAKER_TOKEN), - SELL_SOURCES.map(n => allSources[n]), - sampleAmounts, - ) + .queryOrdersAndSampleSells(ORDERS, SIGNATURES, SELL_SOURCES.map(n => allSources[n]), sampleAmounts) .callAsync(); expect(quotes).to.deep.eq(expectedQuotes); }); it('throws if no orders are passed in', async () => { const tx = testContract - .queryOrdersAndSampleSells([], SELL_SOURCES.map(n => allSources[n]), getSampleAmounts(TAKER_TOKEN)) + .queryOrdersAndSampleSells([], [], SELL_SOURCES.map(n => allSources[n]), getSampleAmounts(TAKER_TOKEN)) .callAsync(); return expect(tx).to.revertWith(EMPTY_ORDERS_ERROR); }); @@ -296,7 +364,8 @@ blockchainTests('erc20-bridge-sampler', env => { it('throws with an unsupported source', async () => { const tx = testContract .queryOrdersAndSampleSells( - createOrders(MAKER_TOKEN, TAKER_TOKEN), + ORDERS, + SIGNATURES, [...SELL_SOURCES.map(n => allSources[n]), randomAddress()], getSampleAmounts(TAKER_TOKEN), ) @@ -307,10 +376,11 @@ blockchainTests('erc20-bridge-sampler', env => { it('throws with non-ERC20 maker asset data', async () => { const tx = testContract .queryOrdersAndSampleSells( - createOrders(MAKER_TOKEN, TAKER_TOKEN).map(o => ({ + ORDERS.map(o => ({ ...o, makerAssetData: INVALID_ASSET_PROXY_ASSET_DATA, })), + SIGNATURES, SELL_SOURCES.map(n => allSources[n]), getSampleAmounts(TAKER_TOKEN), ) @@ -321,10 +391,11 @@ blockchainTests('erc20-bridge-sampler', env => { it('throws with non-ERC20 taker asset data', async () => { const tx = testContract .queryOrdersAndSampleSells( - createOrders(MAKER_TOKEN, TAKER_TOKEN).map(o => ({ + ORDERS.map(o => ({ ...o, takerAssetData: INVALID_ASSET_PROXY_ASSET_DATA, })), + SIGNATURES, SELL_SOURCES.map(n => allSources[n]), getSampleAmounts(TAKER_TOKEN), ) @@ -335,10 +406,11 @@ blockchainTests('erc20-bridge-sampler', env => { it('throws with invalid maker asset data', async () => { const tx = testContract .queryOrdersAndSampleSells( - createOrders(MAKER_TOKEN, TAKER_TOKEN).map(o => ({ + ORDERS.map(o => ({ ...o, makerAssetData: INVALID_ASSET_DATA, })), + SIGNATURES, SELL_SOURCES.map(n => allSources[n]), getSampleAmounts(TAKER_TOKEN), ) @@ -349,10 +421,11 @@ blockchainTests('erc20-bridge-sampler', env => { it('throws with invalid taker asset data', async () => { const tx = testContract .queryOrdersAndSampleSells( - createOrders(MAKER_TOKEN, TAKER_TOKEN).map(o => ({ + ORDERS.map(o => ({ ...o, takerAssetData: INVALID_ASSET_DATA, })), + SIGNATURES, SELL_SOURCES.map(n => allSources[n]), getSampleAmounts(TAKER_TOKEN), ) @@ -362,39 +435,34 @@ blockchainTests('erc20-bridge-sampler', env => { }); describe('queryOrdersAndSampleBuys()', () => { - const MAKER_TOKEN = randomAddress(); - const TAKER_TOKEN = randomAddress(); + const ORDERS = createOrders(MAKER_TOKEN, TAKER_TOKEN); + const SIGNATURES: string[] = _.times(ORDERS.length, hexUtils.random); before(async () => { await testContract.createTokenExchanges([MAKER_TOKEN, TAKER_TOKEN]).awaitTransactionSuccessAsync(); }); - it('returns the results of `getOrderInfo()` for each order', async () => { + it('returns expected fillable amounts for each order', async () => { const takerTokenAmounts = getSampleAmounts(MAKER_TOKEN); - const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN); - const expectedOrderInfos = orders.map(getDeterministicOrderInfo); + const expectedFillableAmounts = ORDERS.map(getDeterministicFillableMakerAssetAmount); const [orderInfos] = await testContract - .queryOrdersAndSampleBuys(orders, BUY_SOURCES.map(n => allSources[n]), takerTokenAmounts) + .queryOrdersAndSampleBuys(ORDERS, SIGNATURES, BUY_SOURCES.map(n => allSources[n]), takerTokenAmounts) .callAsync(); - expect(orderInfos).to.deep.eq(expectedOrderInfos); + expect(orderInfos).to.deep.eq(expectedFillableAmounts); }); it('can return quotes for all sources', async () => { const sampleAmounts = getSampleAmounts(MAKER_TOKEN); const expectedQuotes = getDeterministicBuyQuotes(TAKER_TOKEN, MAKER_TOKEN, BUY_SOURCES, sampleAmounts); const [, quotes] = await testContract - .queryOrdersAndSampleBuys( - createOrders(MAKER_TOKEN, TAKER_TOKEN), - BUY_SOURCES.map(n => allSources[n]), - sampleAmounts, - ) + .queryOrdersAndSampleBuys(ORDERS, SIGNATURES, BUY_SOURCES.map(n => allSources[n]), sampleAmounts) .callAsync(); expect(quotes).to.deep.eq(expectedQuotes); }); it('throws if no orders are passed in', async () => { const tx = testContract - .queryOrdersAndSampleBuys([], BUY_SOURCES.map(n => allSources[n]), getSampleAmounts(MAKER_TOKEN)) + .queryOrdersAndSampleBuys([], [], BUY_SOURCES.map(n => allSources[n]), getSampleAmounts(MAKER_TOKEN)) .callAsync(); return expect(tx).to.revertWith(EMPTY_ORDERS_ERROR); }); @@ -402,7 +470,8 @@ blockchainTests('erc20-bridge-sampler', env => { it('throws with an unsupported source', async () => { const tx = testContract .queryOrdersAndSampleBuys( - createOrders(MAKER_TOKEN, TAKER_TOKEN), + ORDERS, + SIGNATURES, [...BUY_SOURCES.map(n => allSources[n]), randomAddress()], getSampleAmounts(MAKER_TOKEN), ) @@ -414,7 +483,8 @@ blockchainTests('erc20-bridge-sampler', env => { const sources = [...BUY_SOURCES, 'Kyber']; const tx = testContract .queryOrdersAndSampleBuys( - createOrders(MAKER_TOKEN, TAKER_TOKEN), + ORDERS, + SIGNATURES, sources.map(n => allSources[n]), getSampleAmounts(MAKER_TOKEN), ) @@ -425,10 +495,11 @@ blockchainTests('erc20-bridge-sampler', env => { it('throws with non-ERC20 maker asset data', async () => { const tx = testContract .queryOrdersAndSampleBuys( - createOrders(MAKER_TOKEN, TAKER_TOKEN).map(o => ({ + ORDERS.map(o => ({ ...o, makerAssetData: INVALID_ASSET_PROXY_ASSET_DATA, })), + SIGNATURES, BUY_SOURCES.map(n => allSources[n]), getSampleAmounts(MAKER_TOKEN), ) @@ -439,10 +510,11 @@ blockchainTests('erc20-bridge-sampler', env => { it('throws with non-ERC20 taker asset data', async () => { const tx = testContract .queryOrdersAndSampleBuys( - createOrders(MAKER_TOKEN, TAKER_TOKEN).map(o => ({ + ORDERS.map(o => ({ ...o, takerAssetData: INVALID_ASSET_PROXY_ASSET_DATA, })), + SIGNATURES, BUY_SOURCES.map(n => allSources[n]), getSampleAmounts(MAKER_TOKEN), ) @@ -453,10 +525,11 @@ blockchainTests('erc20-bridge-sampler', env => { it('throws with invalid maker asset data', async () => { const tx = testContract .queryOrdersAndSampleBuys( - createOrders(MAKER_TOKEN, TAKER_TOKEN).map(o => ({ + ORDERS.map(o => ({ ...o, makerAssetData: INVALID_ASSET_DATA, })), + SIGNATURES, BUY_SOURCES.map(n => allSources[n]), getSampleAmounts(MAKER_TOKEN), ) @@ -467,10 +540,11 @@ blockchainTests('erc20-bridge-sampler', env => { it('throws with invalid taker asset data', async () => { const tx = testContract .queryOrdersAndSampleBuys( - createOrders(MAKER_TOKEN, TAKER_TOKEN).map(o => ({ + ORDERS.map(o => ({ ...o, takerAssetData: INVALID_ASSET_DATA, })), + SIGNATURES, BUY_SOURCES.map(n => allSources[n]), getSampleAmounts(MAKER_TOKEN), ) @@ -480,9 +554,6 @@ blockchainTests('erc20-bridge-sampler', env => { }); describe('sampleSells()', () => { - const MAKER_TOKEN = randomAddress(); - const TAKER_TOKEN = randomAddress(); - before(async () => { await testContract.createTokenExchanges([MAKER_TOKEN, TAKER_TOKEN]).awaitTransactionSuccessAsync(); }); @@ -528,9 +599,6 @@ blockchainTests('erc20-bridge-sampler', env => { }); describe('sampleBuys()', () => { - const MAKER_TOKEN = randomAddress(); - const TAKER_TOKEN = randomAddress(); - before(async () => { await testContract.createTokenExchanges([MAKER_TOKEN, TAKER_TOKEN]).awaitTransactionSuccessAsync(); }); @@ -583,10 +651,7 @@ blockchainTests('erc20-bridge-sampler', env => { }); }); - describe('sampleSellsFromKyberNetwork()', () => { - const MAKER_TOKEN = randomAddress(); - const TAKER_TOKEN = randomAddress(); - + blockchainTests.resets('sampleSellsFromKyberNetwork()', () => { before(async () => { await testContract.createTokenExchanges([MAKER_TOKEN, TAKER_TOKEN]).awaitTransactionSuccessAsync(); }); @@ -601,7 +666,7 @@ blockchainTests('erc20-bridge-sampler', env => { expect(quotes).to.deep.eq([]); }); - it('can return many quotes', async () => { + it('can quote token - token', async () => { const sampleAmounts = getSampleAmounts(TAKER_TOKEN); const [expectedQuotes] = getDeterministicSellQuotes(TAKER_TOKEN, MAKER_TOKEN, ['Kyber'], sampleAmounts); const quotes = await testContract @@ -610,6 +675,16 @@ blockchainTests('erc20-bridge-sampler', env => { expect(quotes).to.deep.eq(expectedQuotes); }); + it('returns zero if token -> token fails', async () => { + const sampleAmounts = getSampleAmounts(TAKER_TOKEN); + const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); + await enableFailTriggerAsync(); + const quotes = await testContract + .sampleSellsFromKyberNetwork(TAKER_TOKEN, MAKER_TOKEN, sampleAmounts) + .callAsync(); + expect(quotes).to.deep.eq(expectedQuotes); + }); + it('can quote token -> ETH', async () => { const sampleAmounts = getSampleAmounts(TAKER_TOKEN); const [expectedQuotes] = getDeterministicSellQuotes(TAKER_TOKEN, WETH_ADDRESS, ['Kyber'], sampleAmounts); @@ -619,6 +694,16 @@ blockchainTests('erc20-bridge-sampler', env => { expect(quotes).to.deep.eq(expectedQuotes); }); + it('returns zero if token -> ETH fails', async () => { + const sampleAmounts = getSampleAmounts(TAKER_TOKEN); + const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); + await enableFailTriggerAsync(); + const quotes = await testContract + .sampleSellsFromKyberNetwork(TAKER_TOKEN, WETH_ADDRESS, sampleAmounts) + .callAsync(); + expect(quotes).to.deep.eq(expectedQuotes); + }); + it('can quote ETH -> token', async () => { const sampleAmounts = getSampleAmounts(TAKER_TOKEN); const [expectedQuotes] = getDeterministicSellQuotes(WETH_ADDRESS, TAKER_TOKEN, ['Kyber'], sampleAmounts); @@ -627,12 +712,19 @@ blockchainTests('erc20-bridge-sampler', env => { .callAsync(); expect(quotes).to.deep.eq(expectedQuotes); }); - }); - describe('sampleSellsFromEth2Dai()', () => { - const MAKER_TOKEN = randomAddress(); - const TAKER_TOKEN = randomAddress(); + it('returns zero if ETH -> token fails', async () => { + const sampleAmounts = getSampleAmounts(TAKER_TOKEN); + const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); + await enableFailTriggerAsync(); + const quotes = await testContract + .sampleSellsFromKyberNetwork(WETH_ADDRESS, TAKER_TOKEN, sampleAmounts) + .callAsync(); + expect(quotes).to.deep.eq(expectedQuotes); + }); + }); + blockchainTests.resets('sampleSellsFromEth2Dai()', () => { before(async () => { await testContract.createTokenExchanges([MAKER_TOKEN, TAKER_TOKEN]).awaitTransactionSuccessAsync(); }); @@ -647,7 +739,7 @@ blockchainTests('erc20-bridge-sampler', env => { expect(quotes).to.deep.eq([]); }); - it('can return many quotes', async () => { + it('can quote token -> token', async () => { const sampleAmounts = getSampleAmounts(TAKER_TOKEN); const [expectedQuotes] = getDeterministicSellQuotes(TAKER_TOKEN, MAKER_TOKEN, ['Eth2Dai'], sampleAmounts); const quotes = await testContract @@ -656,6 +748,16 @@ blockchainTests('erc20-bridge-sampler', env => { expect(quotes).to.deep.eq(expectedQuotes); }); + it('returns zero if token -> token fails', async () => { + const sampleAmounts = getSampleAmounts(TAKER_TOKEN); + const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); + await enableFailTriggerAsync(); + const quotes = await testContract + .sampleSellsFromEth2Dai(TAKER_TOKEN, MAKER_TOKEN, sampleAmounts) + .callAsync(); + expect(quotes).to.deep.eq(expectedQuotes); + }); + it('can quote token -> ETH', async () => { const sampleAmounts = getSampleAmounts(TAKER_TOKEN); const [expectedQuotes] = getDeterministicSellQuotes(TAKER_TOKEN, WETH_ADDRESS, ['Eth2Dai'], sampleAmounts); @@ -665,6 +767,16 @@ blockchainTests('erc20-bridge-sampler', env => { expect(quotes).to.deep.eq(expectedQuotes); }); + it('returns zero if token -> ETH fails', async () => { + const sampleAmounts = getSampleAmounts(TAKER_TOKEN); + const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); + await enableFailTriggerAsync(); + const quotes = await testContract + .sampleSellsFromEth2Dai(TAKER_TOKEN, WETH_ADDRESS, sampleAmounts) + .callAsync(); + expect(quotes).to.deep.eq(expectedQuotes); + }); + it('can quote ETH -> token', async () => { const sampleAmounts = getSampleAmounts(TAKER_TOKEN); const [expectedQuotes] = getDeterministicSellQuotes(WETH_ADDRESS, TAKER_TOKEN, ['Eth2Dai'], sampleAmounts); @@ -673,12 +785,19 @@ blockchainTests('erc20-bridge-sampler', env => { .callAsync(); expect(quotes).to.deep.eq(expectedQuotes); }); - }); - describe('sampleBuysFromEth2Dai()', () => { - const MAKER_TOKEN = randomAddress(); - const TAKER_TOKEN = randomAddress(); + it('returns zero if ETH -> token fails', async () => { + const sampleAmounts = getSampleAmounts(TAKER_TOKEN); + const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); + await enableFailTriggerAsync(); + const quotes = await testContract + .sampleSellsFromEth2Dai(WETH_ADDRESS, TAKER_TOKEN, sampleAmounts) + .callAsync(); + expect(quotes).to.deep.eq(expectedQuotes); + }); + }); + blockchainTests.resets('sampleBuysFromEth2Dai()', () => { before(async () => { await testContract.createTokenExchanges([MAKER_TOKEN, TAKER_TOKEN]).awaitTransactionSuccessAsync(); }); @@ -693,7 +812,7 @@ blockchainTests('erc20-bridge-sampler', env => { expect(quotes).to.deep.eq([]); }); - it('can return many quotes', async () => { + it('can quote token -> token', async () => { const sampleAmounts = getSampleAmounts(MAKER_TOKEN); const [expectedQuotes] = getDeterministicBuyQuotes(TAKER_TOKEN, MAKER_TOKEN, ['Eth2Dai'], sampleAmounts); const quotes = await testContract @@ -702,6 +821,16 @@ blockchainTests('erc20-bridge-sampler', env => { expect(quotes).to.deep.eq(expectedQuotes); }); + it('returns zero if token -> token fails', async () => { + const sampleAmounts = getSampleAmounts(MAKER_TOKEN); + const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); + await enableFailTriggerAsync(); + const quotes = await testContract + .sampleBuysFromEth2Dai(TAKER_TOKEN, MAKER_TOKEN, sampleAmounts) + .callAsync(); + expect(quotes).to.deep.eq(expectedQuotes); + }); + it('can quote token -> ETH', async () => { const sampleAmounts = getSampleAmounts(MAKER_TOKEN); const [expectedQuotes] = getDeterministicBuyQuotes(TAKER_TOKEN, WETH_ADDRESS, ['Eth2Dai'], sampleAmounts); @@ -711,6 +840,16 @@ blockchainTests('erc20-bridge-sampler', env => { expect(quotes).to.deep.eq(expectedQuotes); }); + it('returns zero if token -> ETH fails', async () => { + const sampleAmounts = getSampleAmounts(MAKER_TOKEN); + const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); + await enableFailTriggerAsync(); + const quotes = await testContract + .sampleBuysFromEth2Dai(TAKER_TOKEN, WETH_ADDRESS, sampleAmounts) + .callAsync(); + expect(quotes).to.deep.eq(expectedQuotes); + }); + it('can quote ETH -> token', async () => { const sampleAmounts = getSampleAmounts(MAKER_TOKEN); const [expectedQuotes] = getDeterministicBuyQuotes(WETH_ADDRESS, TAKER_TOKEN, ['Eth2Dai'], sampleAmounts); @@ -719,12 +858,19 @@ blockchainTests('erc20-bridge-sampler', env => { .callAsync(); expect(quotes).to.deep.eq(expectedQuotes); }); - }); - describe('sampleSellsFromUniswap()', () => { - const MAKER_TOKEN = randomAddress(); - const TAKER_TOKEN = randomAddress(); + it('returns zero if ETH -> token fails', async () => { + const sampleAmounts = getSampleAmounts(MAKER_TOKEN); + const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); + await enableFailTriggerAsync(); + const quotes = await testContract + .sampleBuysFromEth2Dai(WETH_ADDRESS, TAKER_TOKEN, sampleAmounts) + .callAsync(); + expect(quotes).to.deep.eq(expectedQuotes); + }); + }); + blockchainTests.resets('sampleSellsFromUniswap()', () => { before(async () => { await testContract.createTokenExchanges([MAKER_TOKEN, TAKER_TOKEN]).awaitTransactionSuccessAsync(); }); @@ -739,7 +885,7 @@ blockchainTests('erc20-bridge-sampler', env => { expect(quotes).to.deep.eq([]); }); - it('can return many quotes', async () => { + it('can quote token -> token', async () => { const sampleAmounts = getSampleAmounts(TAKER_TOKEN); const [expectedQuotes] = getDeterministicSellQuotes(TAKER_TOKEN, MAKER_TOKEN, ['Uniswap'], sampleAmounts); const quotes = await testContract @@ -748,6 +894,16 @@ blockchainTests('erc20-bridge-sampler', env => { expect(quotes).to.deep.eq(expectedQuotes); }); + it('returns zero if token -> token fails', async () => { + const sampleAmounts = getSampleAmounts(TAKER_TOKEN); + const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); + await enableFailTriggerAsync(); + const quotes = await testContract + .sampleSellsFromUniswap(TAKER_TOKEN, MAKER_TOKEN, sampleAmounts) + .callAsync(); + expect(quotes).to.deep.eq(expectedQuotes); + }); + it('can quote token -> ETH', async () => { const sampleAmounts = getSampleAmounts(TAKER_TOKEN); const [expectedQuotes] = getDeterministicSellQuotes(TAKER_TOKEN, WETH_ADDRESS, ['Uniswap'], sampleAmounts); @@ -757,6 +913,16 @@ blockchainTests('erc20-bridge-sampler', env => { expect(quotes).to.deep.eq(expectedQuotes); }); + it('returns zero if token -> ETH fails', async () => { + const sampleAmounts = getSampleAmounts(TAKER_TOKEN); + const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); + await enableFailTriggerAsync(); + const quotes = await testContract + .sampleSellsFromUniswap(TAKER_TOKEN, WETH_ADDRESS, sampleAmounts) + .callAsync(); + expect(quotes).to.deep.eq(expectedQuotes); + }); + it('can quote ETH -> token', async () => { const sampleAmounts = getSampleAmounts(TAKER_TOKEN); const [expectedQuotes] = getDeterministicSellQuotes(WETH_ADDRESS, TAKER_TOKEN, ['Uniswap'], sampleAmounts); @@ -766,27 +932,38 @@ blockchainTests('erc20-bridge-sampler', env => { expect(quotes).to.deep.eq(expectedQuotes); }); - it('throws if no exchange exists for the maker token', async () => { + it('returns zero if ETH -> token fails', async () => { + const sampleAmounts = getSampleAmounts(TAKER_TOKEN); + const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); + await enableFailTriggerAsync(); + const quotes = await testContract + .sampleSellsFromUniswap(WETH_ADDRESS, TAKER_TOKEN, sampleAmounts) + .callAsync(); + expect(quotes).to.deep.eq(expectedQuotes); + }); + + it('returns zero if no exchange exists for the maker token', async () => { const nonExistantToken = randomAddress(); - const tx = testContract - .sampleSellsFromUniswap(TAKER_TOKEN, nonExistantToken, getSampleAmounts(TAKER_TOKEN)) + const sampleAmounts = getSampleAmounts(TAKER_TOKEN); + const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); + const quotes = await testContract + .sampleSellsFromUniswap(TAKER_TOKEN, nonExistantToken, sampleAmounts) .callAsync(); - return expect(tx).to.revertWith(UNSUPPORTED_UNISWAP_EXCHANGE_ERROR); + expect(quotes).to.deep.eq(expectedQuotes); }); - it('throws if no exchange exists for the taker token', async () => { + it('returns zero if no exchange exists for the taker token', async () => { const nonExistantToken = randomAddress(); - const tx = testContract - .sampleSellsFromUniswap(nonExistantToken, MAKER_TOKEN, getSampleAmounts(nonExistantToken)) + const sampleAmounts = getSampleAmounts(nonExistantToken); + const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); + const quotes = await testContract + .sampleSellsFromUniswap(nonExistantToken, MAKER_TOKEN, sampleAmounts) .callAsync(); - return expect(tx).to.revertWith(UNSUPPORTED_UNISWAP_EXCHANGE_ERROR); + expect(quotes).to.deep.eq(expectedQuotes); }); }); - describe('sampleBuysFromUniswap()', () => { - const MAKER_TOKEN = randomAddress(); - const TAKER_TOKEN = randomAddress(); - + blockchainTests.resets('sampleBuysFromUniswap()', () => { before(async () => { await testContract.createTokenExchanges([MAKER_TOKEN, TAKER_TOKEN]).awaitTransactionSuccessAsync(); }); @@ -801,7 +978,7 @@ blockchainTests('erc20-bridge-sampler', env => { expect(quotes).to.deep.eq([]); }); - it('can return many quotes', async () => { + it('can quote token -> token', async () => { const sampleAmounts = getSampleAmounts(MAKER_TOKEN); const [expectedQuotes] = getDeterministicBuyQuotes(TAKER_TOKEN, MAKER_TOKEN, ['Uniswap'], sampleAmounts); const quotes = await testContract @@ -810,6 +987,16 @@ blockchainTests('erc20-bridge-sampler', env => { expect(quotes).to.deep.eq(expectedQuotes); }); + it('returns zero if token -> token fails', async () => { + const sampleAmounts = getSampleAmounts(MAKER_TOKEN); + const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); + await enableFailTriggerAsync(); + const quotes = await testContract + .sampleBuysFromUniswap(TAKER_TOKEN, MAKER_TOKEN, sampleAmounts) + .callAsync(); + expect(quotes).to.deep.eq(expectedQuotes); + }); + it('can quote token -> ETH', async () => { const sampleAmounts = getSampleAmounts(MAKER_TOKEN); const [expectedQuotes] = getDeterministicBuyQuotes(TAKER_TOKEN, WETH_ADDRESS, ['Uniswap'], sampleAmounts); @@ -819,6 +1006,16 @@ blockchainTests('erc20-bridge-sampler', env => { expect(quotes).to.deep.eq(expectedQuotes); }); + it('returns zero if token -> ETH fails', async () => { + const sampleAmounts = getSampleAmounts(MAKER_TOKEN); + const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); + await enableFailTriggerAsync(); + const quotes = await testContract + .sampleBuysFromUniswap(TAKER_TOKEN, WETH_ADDRESS, sampleAmounts) + .callAsync(); + expect(quotes).to.deep.eq(expectedQuotes); + }); + it('can quote ETH -> token', async () => { const sampleAmounts = getSampleAmounts(MAKER_TOKEN); const [expectedQuotes] = getDeterministicBuyQuotes(WETH_ADDRESS, TAKER_TOKEN, ['Uniswap'], sampleAmounts); @@ -828,20 +1025,34 @@ blockchainTests('erc20-bridge-sampler', env => { expect(quotes).to.deep.eq(expectedQuotes); }); - it('throws if no exchange exists for the maker token', async () => { + it('returns zero if ETH -> token fails', async () => { + const sampleAmounts = getSampleAmounts(MAKER_TOKEN); + const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); + await enableFailTriggerAsync(); + const quotes = await testContract + .sampleBuysFromUniswap(WETH_ADDRESS, TAKER_TOKEN, sampleAmounts) + .callAsync(); + expect(quotes).to.deep.eq(expectedQuotes); + }); + + it('returns zero if no exchange exists for the maker token', async () => { const nonExistantToken = randomAddress(); - const tx = testContract - .sampleBuysFromUniswap(TAKER_TOKEN, nonExistantToken, getSampleAmounts(TAKER_TOKEN)) + const sampleAmounts = getSampleAmounts(nonExistantToken); + const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); + const quotes = await testContract + .sampleBuysFromUniswap(TAKER_TOKEN, nonExistantToken, sampleAmounts) .callAsync(); - return expect(tx).to.revertWith(UNSUPPORTED_UNISWAP_EXCHANGE_ERROR); + expect(quotes).to.deep.eq(expectedQuotes); }); - it('throws if no exchange exists for the taker token', async () => { + it('returns zero if no exchange exists for the taker token', async () => { const nonExistantToken = randomAddress(); - const tx = testContract - .sampleBuysFromUniswap(nonExistantToken, MAKER_TOKEN, getSampleAmounts(nonExistantToken)) + const sampleAmounts = getSampleAmounts(MAKER_TOKEN); + const expectedQuotes = _.times(sampleAmounts.length, () => constants.ZERO_AMOUNT); + const quotes = await testContract + .sampleBuysFromUniswap(nonExistantToken, MAKER_TOKEN, sampleAmounts) .callAsync(); - return expect(tx).to.revertWith(UNSUPPORTED_UNISWAP_EXCHANGE_ERROR); + expect(quotes).to.deep.eq(expectedQuotes); }); }); }); diff --git a/contracts/erc20-bridge-sampler/test/wrappers.ts b/contracts/erc20-bridge-sampler/test/wrappers.ts index f1b8501d01..669266fca6 100644 --- a/contracts/erc20-bridge-sampler/test/wrappers.ts +++ b/contracts/erc20-bridge-sampler/test/wrappers.ts @@ -3,8 +3,8 @@ * Warning: This file is auto-generated by contracts-gen. Don't edit manually. * ----------------------------------------------------------------------------- */ -export * from '../test/generated-wrappers/deployment_constants'; export * from '../test/generated-wrappers/erc20_bridge_sampler'; +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_network'; diff --git a/contracts/erc20-bridge-sampler/tsconfig.json b/contracts/erc20-bridge-sampler/tsconfig.json index 2c25499f48..866692f33f 100644 --- a/contracts/erc20-bridge-sampler/tsconfig.json +++ b/contracts/erc20-bridge-sampler/tsconfig.json @@ -5,8 +5,8 @@ "files": [ "generated-artifacts/ERC20BridgeSampler.json", "generated-artifacts/IERC20BridgeSampler.json", - "test/generated-artifacts/DeploymentConstants.json", "test/generated-artifacts/ERC20BridgeSampler.json", + "test/generated-artifacts/IDevUtils.json", "test/generated-artifacts/IERC20BridgeSampler.json", "test/generated-artifacts/IEth2Dai.json", "test/generated-artifacts/IKyberNetwork.json", diff --git a/contracts/utils/CHANGELOG.json b/contracts/utils/CHANGELOG.json index ee20b3faa4..882a72dc47 100644 --- a/contracts/utils/CHANGELOG.json +++ b/contracts/utils/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "4.0.2", + "changes": [ + { + "note": "Add `DEV_UTILS_ADDRESS` and `KYBER_ETH_ADDRESS` to `DeploymentConstants`.", + "pr": 2365 + } + ] + }, { "timestamp": 1575931811, "version": "4.0.1", diff --git a/contracts/utils/contracts/src/DeploymentConstants.sol b/contracts/utils/contracts/src/DeploymentConstants.sol index ba7bdc8caf..da250bdc4b 100644 --- a/contracts/utils/contracts/src/DeploymentConstants.sol +++ b/contracts/utils/contracts/src/DeploymentConstants.sol @@ -38,6 +38,10 @@ contract DeploymentConstants { address constant private DAI_ADDRESS = 0x6B175474E89094C44Da98b954EedeAC495271d0F; /// @dev Mainnet address of the `Chai` contract address constant private CHAI_ADDRESS = 0x06AF07097C9Eeb7fD685c692751D5C66dB49c215; + /// @dev Address of the 0x DevUtils contract. + address constant private DEV_UTILS_ADDRESS = 0xcCc2431a7335F21d9268bA62F0B32B0f2EFC463f; + /// @dev Kyber ETH pseudo-address. + address constant internal KYBER_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; /// @dev Overridable way to get the `KyberNetworkProxy` address. /// @return kyberAddress The `IKyberNetworkProxy` address. @@ -108,4 +112,14 @@ contract DeploymentConstants { { return CHAI_ADDRESS; } + + /// @dev An overridable way to retrieve the 0x `DevUtils` contract address. + /// @return devUtils The 0x `DevUtils` contract address. + function _getDevUtilsAddress() + internal + view + returns (address devUtils) + { + return DEV_UTILS_ADDRESS; + } }