Merge pull request #2595 from 0xProject/feat/erc20-bridge-sampler/uniswapv2-tests

Add Uniswap V2 to ERC20BridgeSampler
This commit is contained in:
Lawrence Forman
2020-06-04 12:14:07 -04:00
committed by GitHub
18 changed files with 617 additions and 3 deletions

View File

@@ -17,6 +17,10 @@
{
"note": "Use `searchBestRate` in Kyber samples. Return 0 when Uniswap/Eth2Dai reserve",
"pr": 2575
},
{
"note": "Add UniswapV2",
"pr": 2595
}
]
},

View File

@@ -34,6 +34,7 @@ import "./IUniswapExchangeQuotes.sol";
import "./ICurve.sol";
import "./ILiquidityProvider.sol";
import "./ILiquidityProviderRegistry.sol";
import "./IUniswapV2Router01.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) =
_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.
/// @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[]))[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.

View File

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

View File

@@ -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 IUniswapV2Router01 {
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);
}

View File

@@ -25,6 +25,7 @@ import "../src/ERC20BridgeSampler.sol";
import "../src/IEth2Dai.sol";
import "../src/IDevUtils.sol";
import "../src/IKyberNetworkProxy.sol";
import "../src/IUniswapV2Router01.sol";
library LibDeterministicQuotes {
@@ -194,6 +195,55 @@ contract TestERC20BridgeSamplerUniswapExchange is
}
contract TestERC20BridgeSamplerUniswapV2Router01 is
IUniswapV2Router01,
DeploymentConstants,
FailTrigger
{
bytes32 constant private SALT = 0xadc7fcb33c735913b8635927e66896b356a53a912ab2ceff929e60a04b53b3c1;
// Deterministic `IUniswapV2Router01.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 `IUniswapV2Router01.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;
TestERC20BridgeSamplerUniswapV2Router01 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 TestERC20BridgeSamplerUniswapV2Router01();
eth2Dai = new TestERC20BridgeSamplerEth2Dai();
kyber = new TestERC20BridgeSamplerKyberNetwork();
}
@@ -399,6 +451,15 @@ contract TestERC20BridgeSampler is
return address(uniswap);
}
// Overriden to point to a custom contract.
function _getUniswapV2Router01Address()
internal
view
returns (address uniswapV2RouterAddress)
{
return address(uniswapV2Router);
}
// Overriden to point to a custom contract.
function _getKyberNetworkProxyAddress()
internal

View File

@@ -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|IUniswapV2Router01|TestERC20BridgeSampler).json"
},
"repository": {
"type": "git",

View File

@@ -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 IUniswapV2Router01 from '../test/generated-artifacts/IUniswapV2Router01.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,
IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact,
TestERC20BridgeSampler: TestERC20BridgeSampler as ContractArtifact,
};

View File

@@ -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);

View File

@@ -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_router01';
export * from '../test/generated-wrappers/test_erc20_bridge_sampler';

View File

@@ -21,6 +21,7 @@
"test/generated-artifacts/ILiquidityProvider.json",
"test/generated-artifacts/ILiquidityProviderRegistry.json",
"test/generated-artifacts/IUniswapExchangeQuotes.json",
"test/generated-artifacts/IUniswapV2Router01.json",
"test/generated-artifacts/TestERC20BridgeSampler.json"
],
"exclude": ["./deploy/solc/solc_bin"]

View File

@@ -17,6 +17,10 @@
{
"note": "Add more solidity 0.6 contracts",
"pr": 2545
},
{
"note": "Add UniswapV2 addresses to `DeploymentConstants`",
"pr": 2595
}
]
},

View File

@@ -32,6 +32,10 @@ contract DeploymentConstants {
address constant private UNISWAP_EXCHANGE_FACTORY_ADDRESS = 0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95;
// /// @dev Kovan address of the `UniswapExchangeFactory` contract.
// address constant private UNISWAP_EXCHANGE_FACTORY_ADDRESS = 0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30;
/// @dev Mainnet address of the `UniswapV2Router01` contract.
address constant private UNISWAP_V2_ROUTER_01_ADDRESS = 0xf164fC0Ec4E93095b804a4795bBe1e041497b92a;
// /// @dev Kovan address of the `UniswapV2Router01` contract.
// address constant private UNISWAP_V2_ROUTER_01ADDRESS = 0xf164fC0Ec4E93095b804a4795bBe1e041497b92a;
/// @dev Mainnet address of the Eth2Dai `MatchingMarket` contract.
address constant private ETH2DAI_ADDRESS = 0x794e6e91555438aFc3ccF1c5076A74F42133d08D;
// /// @dev Kovan address of the Eth2Dai `MatchingMarket` contract.
@@ -93,6 +97,16 @@ contract DeploymentConstants {
return UNISWAP_EXCHANGE_FACTORY_ADDRESS;
}
/// @dev Overridable way to get the `UniswapV2Router01` address.
/// @return uniswapRouterAddress The `UniswapV2Router01` address.
function _getUniswapV2Router01Address()
internal
view
returns (address uniswapRouterAddress)
{
return UNISWAP_V2_ROUTER_01_ADDRESS;
}
/// @dev An overridable way to retrieve the Eth2Dai `MatchingMarket` contract.
/// @return eth2daiAddress The Eth2Dai `MatchingMarket` contract.
function _getEth2DaiAddress()