@0x/contracts-erc20-bridge-sampler: Add UniswapV2.

This commit is contained in:
Lawrence Forman 2020-06-03 13:59:50 -04:00
parent 429f2bb8dd
commit 4066c17a0f
11 changed files with 298 additions and 2 deletions

View File

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

View File

@ -34,6 +34,7 @@ import "./IUniswapExchangeQuotes.sol";
import "./ICurve.sol"; import "./ICurve.sol";
import "./ILiquidityProvider.sol"; import "./ILiquidityProvider.sol";
import "./ILiquidityProviderRegistry.sol"; import "./ILiquidityProviderRegistry.sol";
import "./IUniswapV2Router.sol";
contract ERC20BridgeSampler is contract ERC20BridgeSampler is
@ -46,6 +47,8 @@ contract ERC20BridgeSampler is
uint256 constant internal KYBER_CALL_GAS = 1500e3; // 1.5m uint256 constant internal KYBER_CALL_GAS = 1500e3; // 1.5m
/// @dev Gas limit for Uniswap calls. /// @dev Gas limit for Uniswap calls.
uint256 constant internal UNISWAP_CALL_GAS = 150e3; // 150k 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. /// @dev Base gas limit for Eth2Dai calls.
uint256 constant internal ETH2DAI_CALL_GAS = 1000e3; // 1m uint256 constant internal ETH2DAI_CALL_GAS = 1000e3; // 1m
/// @dev Base gas limit for Curve calls. Some Curves have multiple tokens /// @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. /// @dev Overridable way to get token decimals.
/// @param tokenAddress Address of the token. /// @param tokenAddress Address of the token.
/// @return decimals The decimal places for the token. /// @return decimals The decimal places for the token.

View File

@ -240,4 +240,30 @@ interface IERC20BridgeSampler {
external external
view view
returns (address providerAddress); 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 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);
}

View File

@ -25,6 +25,7 @@ import "../src/ERC20BridgeSampler.sol";
import "../src/IEth2Dai.sol"; import "../src/IEth2Dai.sol";
import "../src/IDevUtils.sol"; import "../src/IDevUtils.sol";
import "../src/IKyberNetworkProxy.sol"; import "../src/IKyberNetworkProxy.sol";
import "../src/IUniswapV2Router.sol";
library LibDeterministicQuotes { 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 contract TestERC20BridgeSamplerKyberNetwork is
IKyberNetwork, IKyberNetwork,
DeploymentConstants, DeploymentConstants,
@ -325,6 +375,7 @@ contract TestERC20BridgeSampler is
FailTrigger FailTrigger
{ {
TestERC20BridgeSamplerUniswapExchangeFactory public uniswap; TestERC20BridgeSamplerUniswapExchangeFactory public uniswap;
TestERC20BridgeSamplerUniswapV2Router public uniswapV2Router;
TestERC20BridgeSamplerEth2Dai public eth2Dai; TestERC20BridgeSamplerEth2Dai public eth2Dai;
TestERC20BridgeSamplerKyberNetwork public kyber; TestERC20BridgeSamplerKyberNetwork public kyber;
@ -332,6 +383,7 @@ contract TestERC20BridgeSampler is
constructor() public ERC20BridgeSampler(address(this)) { constructor() public ERC20BridgeSampler(address(this)) {
uniswap = new TestERC20BridgeSamplerUniswapExchangeFactory(); uniswap = new TestERC20BridgeSamplerUniswapExchangeFactory();
uniswapV2Router = new TestERC20BridgeSamplerUniswapV2Router();
eth2Dai = new TestERC20BridgeSamplerEth2Dai(); eth2Dai = new TestERC20BridgeSamplerEth2Dai();
kyber = new TestERC20BridgeSamplerKyberNetwork(); kyber = new TestERC20BridgeSamplerKyberNetwork();
} }
@ -399,6 +451,15 @@ contract TestERC20BridgeSampler is
return address(uniswap); 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. // Overriden to point to a custom contract.
function _getKyberNetworkProxyAddress() function _getKyberNetworkProxyAddress()
internal internal

View File

@ -38,7 +38,7 @@
"config": { "config": {
"publicInterfaceContracts": "ERC20BridgeSampler,IERC20BridgeSampler,ILiquidityProvider,ILiquidityProviderRegistry,DummyLiquidityProviderRegistry,DummyLiquidityProvider", "publicInterfaceContracts": "ERC20BridgeSampler,IERC20BridgeSampler,ILiquidityProvider,ILiquidityProviderRegistry,DummyLiquidityProviderRegistry,DummyLiquidityProvider",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", "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": { "repository": {
"type": "git", "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 ILiquidityProvider from '../test/generated-artifacts/ILiquidityProvider.json';
import * as ILiquidityProviderRegistry from '../test/generated-artifacts/ILiquidityProviderRegistry.json'; import * as ILiquidityProviderRegistry from '../test/generated-artifacts/ILiquidityProviderRegistry.json';
import * as IUniswapExchangeQuotes from '../test/generated-artifacts/IUniswapExchangeQuotes.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'; import * as TestERC20BridgeSampler from '../test/generated-artifacts/TestERC20BridgeSampler.json';
export const artifacts = { export const artifacts = {
DummyLiquidityProvider: DummyLiquidityProvider as ContractArtifact, DummyLiquidityProvider: DummyLiquidityProvider as ContractArtifact,
@ -31,5 +32,6 @@ export const artifacts = {
ILiquidityProvider: ILiquidityProvider as ContractArtifact, ILiquidityProvider: ILiquidityProvider as ContractArtifact,
ILiquidityProviderRegistry: ILiquidityProviderRegistry as ContractArtifact, ILiquidityProviderRegistry: ILiquidityProviderRegistry as ContractArtifact,
IUniswapExchangeQuotes: IUniswapExchangeQuotes as ContractArtifact, IUniswapExchangeQuotes: IUniswapExchangeQuotes as ContractArtifact,
IUniswapV2Router: IUniswapV2Router as ContractArtifact,
TestERC20BridgeSampler: TestERC20BridgeSampler as ContractArtifact, TestERC20BridgeSampler: TestERC20BridgeSampler as ContractArtifact,
}; };

View File

@ -28,6 +28,7 @@ blockchainTests('erc20-bridge-sampler', env => {
const KYBER_SALT = '0x0ff3ca9d46195c39f9a12afb74207b4970349fb3cfb1e459bbf170298d326bc7'; const KYBER_SALT = '0x0ff3ca9d46195c39f9a12afb74207b4970349fb3cfb1e459bbf170298d326bc7';
const ETH2DAI_SALT = '0xb713b61bb9bb2958a0f5d1534b21e94fc68c4c0c034b0902ed844f2f6cd1b4f7'; const ETH2DAI_SALT = '0xb713b61bb9bb2958a0f5d1534b21e94fc68c4c0c034b0902ed844f2f6cd1b4f7';
const UNISWAP_BASE_SALT = '0x1d6a6a0506b0b4a554b907a4c29d9f4674e461989d9c1921feb17b26716385ab'; const UNISWAP_BASE_SALT = '0x1d6a6a0506b0b4a554b907a4c29d9f4674e461989d9c1921feb17b26716385ab';
const UNISWAP_V2_SALT = '0xadc7fcb33c735913b8635927e66896b356a53a912ab2ceff929e60a04b53b3c1';
const ERC20_PROXY_ID = '0xf47261b0'; const ERC20_PROXY_ID = '0xf47261b0';
const INVALID_TOKEN_PAIR_ERROR = 'ERC20BridgeSampler/INVALID_TOKEN_PAIR'; const INVALID_TOKEN_PAIR_ERROR = 'ERC20BridgeSampler/INVALID_TOKEN_PAIR';
const MAKER_TOKEN = randomAddress(); const MAKER_TOKEN = randomAddress();
@ -191,6 +192,22 @@ blockchainTests('erc20-bridge-sampler', env => {
return quotes; 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 { function getDeterministicFillableTakerAssetAmount(order: Order): BigNumber {
const hash = getPackedHash(hexUtils.leftPad(order.salt)); const hash = getPackedHash(hexUtils.leftPad(order.salt));
const orderStatus = new BigNumber(hash).mod(100).toNumber() > 90 ? 5 : 3; 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()', () => { describe('batchCall()', () => {
it('can call one function', async () => { it('can call one function', async () => {
const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN); 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';
export * from '../test/generated-wrappers/i_liquidity_provider_registry'; 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_exchange_quotes';
export * from '../test/generated-wrappers/i_uniswap_v2_router';
export * from '../test/generated-wrappers/test_erc20_bridge_sampler'; export * from '../test/generated-wrappers/test_erc20_bridge_sampler';

View File

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

View File

@ -20,7 +20,7 @@
}, },
{ {
"note": "Add UniswapV2 addresses to `DeploymentConstants`", "note": "Add UniswapV2 addresses to `DeploymentConstants`",
"pr": "TODO" "pr": 2595
} }
] ]
}, },