From debe40d91254866d4faf5ff48bd95ba40ceb39b0 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 3 Jun 2020 13:59:50 -0400 Subject: [PATCH] `@0x/contracts-erc20-bridge-sampler`: Add UniswapV2. --- contracts/erc20-bridge-sampler/CHANGELOG.json | 4 + .../contracts/src/ERC20BridgeSampler.sol | 71 ++++++++++++++ .../contracts/src/IERC20BridgeSampler.sol | 26 +++++ .../contracts/src/IUniswapV2Router.sol | 33 +++++++ .../contracts/test/TestERC20BridgeSampler.sol | 61 ++++++++++++ contracts/erc20-bridge-sampler/package.json | 2 +- .../erc20-bridge-sampler/test/artifacts.ts | 2 + .../test/erc20-bridge-sampler.ts | 97 +++++++++++++++++++ .../erc20-bridge-sampler/test/wrappers.ts | 1 + contracts/erc20-bridge-sampler/tsconfig.json | 1 + 10 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 contracts/erc20-bridge-sampler/contracts/src/IUniswapV2Router.sol diff --git a/contracts/erc20-bridge-sampler/CHANGELOG.json b/contracts/erc20-bridge-sampler/CHANGELOG.json index 220400b08f..4cc5016e98 100644 --- a/contracts/erc20-bridge-sampler/CHANGELOG.json +++ b/contracts/erc20-bridge-sampler/CHANGELOG.json @@ -17,6 +17,10 @@ { "note": "Use `searchBestRate` in Kyber samples. Return 0 when Uniswap/Eth2Dai reserve", "pr": 2575 + }, + { + "note": "Add UniswapV2", + "pr": "TODO" } ] }, diff --git a/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol b/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol index f5216c3a41..277f5ef050 100644 --- a/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol +++ b/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol @@ -34,6 +34,7 @@ import "./IUniswapExchangeQuotes.sol"; import "./ICurve.sol"; import "./ILiquidityProvider.sol"; import "./ILiquidityProviderRegistry.sol"; +import "./IUniswapV2Router.sol"; contract ERC20BridgeSampler is @@ -46,6 +47,8 @@ contract ERC20BridgeSampler is 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 @@ -645,6 +648,74 @@ contract ERC20BridgeSampler is } } + /// @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[] 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) = + _getUniswapV2RouterAddress().staticcall.gas(UNISWAPV2_CALL_GAS)( + abi.encodeWithSelector( + IUniswapV2Router(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. + /// @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) = + _getUniswapV2RouterAddress().staticcall.gas(UNISWAPV2_CALL_GAS)( + abi.encodeWithSelector( + IUniswapV2Router(0).getAmountsIn.selector, + makerTokenAmounts[i], + path + )); + uint256 sellAmount = 0; + if (didSucceed) { + // solhint-disable-next-line indent + sellAmount = abi.decode(resultData, (uint256[]))[path.length - 1]; + } 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. diff --git a/contracts/erc20-bridge-sampler/contracts/src/IERC20BridgeSampler.sol b/contracts/erc20-bridge-sampler/contracts/src/IERC20BridgeSampler.sol index 0084a720f0..521ca45cc2 100644 --- a/contracts/erc20-bridge-sampler/contracts/src/IERC20BridgeSampler.sol +++ b/contracts/erc20-bridge-sampler/contracts/src/IERC20BridgeSampler.sol @@ -240,4 +240,30 @@ interface IERC20BridgeSampler { 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/IUniswapV2Router.sol b/contracts/erc20-bridge-sampler/contracts/src/IUniswapV2Router.sol new file mode 100644 index 0000000000..64c3e48760 --- /dev/null +++ b/contracts/erc20-bridge-sampler/contracts/src/IUniswapV2Router.sol @@ -0,0 +1,33 @@ +/* + + 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; + + +interface IUniswapV2Router { + + function getAmountsOut(uint256 amountIn, address[] calldata path) + external + view + returns (uint256[] memory amounts); + + function getAmountsIn(uint256 amountOut, address[] calldata path) + external + view + returns (uint256[] memory amounts); +} diff --git a/contracts/erc20-bridge-sampler/contracts/test/TestERC20BridgeSampler.sol b/contracts/erc20-bridge-sampler/contracts/test/TestERC20BridgeSampler.sol index d714b90112..4a23c2bd1d 100644 --- a/contracts/erc20-bridge-sampler/contracts/test/TestERC20BridgeSampler.sol +++ b/contracts/erc20-bridge-sampler/contracts/test/TestERC20BridgeSampler.sol @@ -25,6 +25,7 @@ import "../src/ERC20BridgeSampler.sol"; import "../src/IEth2Dai.sol"; import "../src/IDevUtils.sol"; import "../src/IKyberNetworkProxy.sol"; +import "../src/IUniswapV2Router.sol"; library LibDeterministicQuotes { @@ -194,6 +195,55 @@ contract TestERC20BridgeSamplerUniswapExchange is } +contract TestERC20BridgeSamplerUniswapV2Router is + IUniswapV2Router, + DeploymentConstants, + FailTrigger +{ + bytes32 constant private SALT = 0xadc7fcb33c735913b8635927e66896b356a53a912ab2ceff929e60a04b53b3c1; + + // Deterministic `IUniswapV2Router.getAmountsOut()`. + function getAmountsOut(uint256 amountIn, address[] calldata path) + external + view + returns (uint256[] memory amounts) + { + require(path.length >= 2, "PATH_TOO_SHORT"); + _revertIfShouldFail(); + amounts = new uint256[](path.length); + amounts[0] = amountIn; + for (uint256 i = 0; i < path.length - 1; ++i) { + amounts[i + 1] = LibDeterministicQuotes.getDeterministicSellQuote( + SALT, + path[i], + path[i + 1], + amounts[i] + ); + } + } + + // Deterministic `IUniswapV2Router.getAmountsInt()`. + function getAmountsIn(uint256 amountOut, address[] calldata path) + external + view + returns (uint256[] memory amounts) + { + require(path.length >= 2, "PATH_TOO_SHORT"); + _revertIfShouldFail(); + amounts = new uint256[](path.length); + amounts[0] = amountOut; + for (uint256 i = 0; i < path.length - 1; ++i) { + amounts[i + 1] = LibDeterministicQuotes.getDeterministicBuyQuote( + SALT, + path[i], + path[i + 1], + amounts[i] + ); + } + } +} + + contract TestERC20BridgeSamplerKyberNetwork is IKyberNetwork, DeploymentConstants, @@ -325,6 +375,7 @@ contract TestERC20BridgeSampler is FailTrigger { TestERC20BridgeSamplerUniswapExchangeFactory public uniswap; + TestERC20BridgeSamplerUniswapV2Router public uniswapV2Router; TestERC20BridgeSamplerEth2Dai public eth2Dai; TestERC20BridgeSamplerKyberNetwork public kyber; @@ -332,6 +383,7 @@ contract TestERC20BridgeSampler is constructor() public ERC20BridgeSampler(address(this)) { uniswap = new TestERC20BridgeSamplerUniswapExchangeFactory(); + uniswapV2Router = new TestERC20BridgeSamplerUniswapV2Router(); eth2Dai = new TestERC20BridgeSamplerEth2Dai(); kyber = new TestERC20BridgeSamplerKyberNetwork(); } @@ -399,6 +451,15 @@ contract TestERC20BridgeSampler is return address(uniswap); } + // Overriden to point to a custom contract. + function _getUniswapV2RouterAddress() + internal + view + returns (address uniswapV2RouterAddress) + { + return address(uniswapV2Router); + } + // Overriden to point to a custom contract. function _getKyberNetworkProxyAddress() internal diff --git a/contracts/erc20-bridge-sampler/package.json b/contracts/erc20-bridge-sampler/package.json index 16a6893533..1719b45b9c 100644 --- a/contracts/erc20-bridge-sampler/package.json +++ b/contracts/erc20-bridge-sampler/package.json @@ -38,7 +38,7 @@ "config": { "publicInterfaceContracts": "ERC20BridgeSampler,IERC20BridgeSampler,ILiquidityProvider,ILiquidityProviderRegistry,DummyLiquidityProviderRegistry,DummyLiquidityProvider", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(DummyLiquidityProvider|DummyLiquidityProviderRegistry|ERC20BridgeSampler|ICurve|IDevUtils|IERC20BridgeSampler|IEth2Dai|IKyberNetwork|IKyberNetworkProxy|ILiquidityProvider|ILiquidityProviderRegistry|IUniswapExchangeQuotes|TestERC20BridgeSampler).json" + "abis": "./test/generated-artifacts/@(DummyLiquidityProvider|DummyLiquidityProviderRegistry|ERC20BridgeSampler|ICurve|IDevUtils|IERC20BridgeSampler|IEth2Dai|IKyberNetwork|IKyberNetworkProxy|ILiquidityProvider|ILiquidityProviderRegistry|IUniswapExchangeQuotes|IUniswapV2Router|TestERC20BridgeSampler).json" }, "repository": { "type": "git", diff --git a/contracts/erc20-bridge-sampler/test/artifacts.ts b/contracts/erc20-bridge-sampler/test/artifacts.ts index 3e595b577e..9729273253 100644 --- a/contracts/erc20-bridge-sampler/test/artifacts.ts +++ b/contracts/erc20-bridge-sampler/test/artifacts.ts @@ -17,6 +17,7 @@ import * as IKyberNetworkProxy from '../test/generated-artifacts/IKyberNetworkPr import * as ILiquidityProvider from '../test/generated-artifacts/ILiquidityProvider.json'; import * as ILiquidityProviderRegistry from '../test/generated-artifacts/ILiquidityProviderRegistry.json'; import * as IUniswapExchangeQuotes from '../test/generated-artifacts/IUniswapExchangeQuotes.json'; +import * as IUniswapV2Router from '../test/generated-artifacts/IUniswapV2Router.json'; import * as TestERC20BridgeSampler from '../test/generated-artifacts/TestERC20BridgeSampler.json'; export const artifacts = { DummyLiquidityProvider: DummyLiquidityProvider as ContractArtifact, @@ -31,5 +32,6 @@ export const artifacts = { ILiquidityProvider: ILiquidityProvider as ContractArtifact, ILiquidityProviderRegistry: ILiquidityProviderRegistry as ContractArtifact, IUniswapExchangeQuotes: IUniswapExchangeQuotes as ContractArtifact, + IUniswapV2Router: IUniswapV2Router 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 78254134a2..a289d10dde 100644 --- a/contracts/erc20-bridge-sampler/test/erc20-bridge-sampler.ts +++ b/contracts/erc20-bridge-sampler/test/erc20-bridge-sampler.ts @@ -28,6 +28,7 @@ blockchainTests('erc20-bridge-sampler', env => { const KYBER_SALT = '0x0ff3ca9d46195c39f9a12afb74207b4970349fb3cfb1e459bbf170298d326bc7'; const ETH2DAI_SALT = '0xb713b61bb9bb2958a0f5d1534b21e94fc68c4c0c034b0902ed844f2f6cd1b4f7'; const UNISWAP_BASE_SALT = '0x1d6a6a0506b0b4a554b907a4c29d9f4674e461989d9c1921feb17b26716385ab'; + const UNISWAP_V2_SALT = '0xadc7fcb33c735913b8635927e66896b356a53a912ab2ceff929e60a04b53b3c1'; const ERC20_PROXY_ID = '0xf47261b0'; const INVALID_TOKEN_PAIR_ERROR = 'ERC20BridgeSampler/INVALID_TOKEN_PAIR'; const MAKER_TOKEN = randomAddress(); @@ -191,6 +192,22 @@ blockchainTests('erc20-bridge-sampler', env => { return quotes; } + function getDeterministicUniswapV2SellQuote(path: string[], sellAmount: BigNumber): BigNumber { + let bought = sellAmount; + for (let i = 0; i < path.length - 1; ++i) { + bought = getDeterministicSellQuote(UNISWAP_V2_SALT, path[i], path[i + 1], bought); + } + return bought; + } + + function getDeterministicUniswapV2BuyQuote(path: string[], buyAmount: BigNumber): BigNumber { + let sold = buyAmount; + for (let i = 0; i < path.length - 1; ++i) { + sold = getDeterministicBuyQuote(UNISWAP_V2_SALT, path[i], path[i + 1], sold); + } + return sold; + } + function getDeterministicFillableTakerAssetAmount(order: Order): BigNumber { const hash = getPackedHash(hexUtils.leftPad(order.salt)); const orderStatus = new BigNumber(hash).mod(100).toNumber() > 90 ? 5 : 3; @@ -926,6 +943,86 @@ blockchainTests('erc20-bridge-sampler', env => { }); }); + blockchainTests.resets('sampleSellsFromUniswapV2()', () => { + function predictSellQuotes(path: string[], sellAmounts: BigNumber[]): BigNumber[] { + return sellAmounts.map(a => getDeterministicUniswapV2SellQuote(path, a)); + } + + it('can return no quotes', async () => { + const quotes = await testContract.sampleSellsFromUniswapV2([TAKER_TOKEN, MAKER_TOKEN], []).callAsync(); + expect(quotes).to.deep.eq([]); + }); + + it('can quote token -> token', async () => { + const sampleAmounts = getSampleAmounts(TAKER_TOKEN); + const expectedQuotes = predictSellQuotes([TAKER_TOKEN, MAKER_TOKEN], sampleAmounts); + const quotes = await testContract + .sampleSellsFromUniswapV2([TAKER_TOKEN, MAKER_TOKEN], sampleAmounts) + .callAsync(); + 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 + .sampleSellsFromUniswapV2([TAKER_TOKEN, MAKER_TOKEN], sampleAmounts) + .callAsync(); + expect(quotes).to.deep.eq(expectedQuotes); + }); + + it('can quote token -> token -> token', async () => { + const intermediateToken = randomAddress(); + const sampleAmounts = getSampleAmounts(TAKER_TOKEN); + const expectedQuotes = predictSellQuotes([TAKER_TOKEN, intermediateToken, MAKER_TOKEN], sampleAmounts); + const quotes = await testContract + .sampleSellsFromUniswapV2([TAKER_TOKEN, intermediateToken, MAKER_TOKEN], sampleAmounts) + .callAsync(); + expect(quotes).to.deep.eq(expectedQuotes); + }); + }); + + blockchainTests.resets('sampleBuysFromUniswapV2()', () => { + function predictBuyQuotes(path: string[], buyAmounts: BigNumber[]): BigNumber[] { + return buyAmounts.map(a => getDeterministicUniswapV2BuyQuote(path, a)); + } + + it('can return no quotes', async () => { + const quotes = await testContract.sampleBuysFromUniswapV2([TAKER_TOKEN, MAKER_TOKEN], []).callAsync(); + expect(quotes).to.deep.eq([]); + }); + + it('can quote token -> token', async () => { + const sampleAmounts = getSampleAmounts(MAKER_TOKEN); + const expectedQuotes = predictBuyQuotes([TAKER_TOKEN, MAKER_TOKEN], sampleAmounts); + const quotes = await testContract + .sampleBuysFromUniswapV2([TAKER_TOKEN, MAKER_TOKEN], sampleAmounts) + .callAsync(); + 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 + .sampleBuysFromUniswapV2([TAKER_TOKEN, MAKER_TOKEN], sampleAmounts) + .callAsync(); + expect(quotes).to.deep.eq(expectedQuotes); + }); + + it('can quote token -> token -> token', async () => { + const intermediateToken = randomAddress(); + const sampleAmounts = getSampleAmounts(MAKER_TOKEN); + const expectedQuotes = predictBuyQuotes([TAKER_TOKEN, intermediateToken, MAKER_TOKEN], sampleAmounts); + const quotes = await testContract + .sampleBuysFromUniswapV2([TAKER_TOKEN, intermediateToken, MAKER_TOKEN], sampleAmounts) + .callAsync(); + expect(quotes).to.deep.eq(expectedQuotes); + }); + }); + describe('batchCall()', () => { it('can call one function', async () => { const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN); diff --git a/contracts/erc20-bridge-sampler/test/wrappers.ts b/contracts/erc20-bridge-sampler/test/wrappers.ts index 94707cc529..993d92615d 100644 --- a/contracts/erc20-bridge-sampler/test/wrappers.ts +++ b/contracts/erc20-bridge-sampler/test/wrappers.ts @@ -15,4 +15,5 @@ export * from '../test/generated-wrappers/i_kyber_network_proxy'; export * from '../test/generated-wrappers/i_liquidity_provider'; export * from '../test/generated-wrappers/i_liquidity_provider_registry'; export * from '../test/generated-wrappers/i_uniswap_exchange_quotes'; +export * from '../test/generated-wrappers/i_uniswap_v2_router'; export * from '../test/generated-wrappers/test_erc20_bridge_sampler'; diff --git a/contracts/erc20-bridge-sampler/tsconfig.json b/contracts/erc20-bridge-sampler/tsconfig.json index 0000dc7c83..622eeabd0e 100644 --- a/contracts/erc20-bridge-sampler/tsconfig.json +++ b/contracts/erc20-bridge-sampler/tsconfig.json @@ -21,6 +21,7 @@ "test/generated-artifacts/ILiquidityProvider.json", "test/generated-artifacts/ILiquidityProviderRegistry.json", "test/generated-artifacts/IUniswapExchangeQuotes.json", + "test/generated-artifacts/IUniswapV2Router.json", "test/generated-artifacts/TestERC20BridgeSampler.json" ], "exclude": ["./deploy/solc/solc_bin"]