Curve ERC20Bridge (#2480)

* Curve ERC20Bridge

* ERC20BridgeSampler Curve (#2483)

* ERC20Sampler Curve

* Use Bridge Sources for each Curve

* Support multiple versions of the Curve contract

* CHANGELOG and redeployed Curve (mainnet)

* Fix Market ops utils test

* Added Curve DAI USDC USDT TUSD

* Bump sampler gas limit default

* Decode the Curve in tests

* Disable Curve in Buy tests

* blockchainTests.fork.resets Curve and Sampler
This commit is contained in:
Jacob Evans 2020-02-15 17:02:19 +11:00 committed by GitHub
parent dcce8276b8
commit e05a03a842
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 839 additions and 35 deletions

View File

@ -0,0 +1,108 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol";
import "@0x/contracts-exchange-libs/contracts/src/IWallet.sol";
import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol";
import "../interfaces/IERC20Bridge.sol";
import "../interfaces/ICurve.sol";
// solhint-disable not-rely-on-time
// solhint-disable space-after-comma
contract CurveBridge is
IERC20Bridge,
IWallet,
DeploymentConstants
{
/// @dev Callback for `ICurve`. Tries to buy `amount` of
/// `toTokenAddress` tokens by selling the entirety of the opposing asset
/// (DAI, USDC) to the Curve contract, then transfers the bought
/// tokens to `to`.
/// @param toTokenAddress The token to give to `to` (i.e DAI, USDC, USDT).
/// @param to The recipient of the bought tokens.
/// @param amount Minimum amount of `toTokenAddress` tokens to buy.
/// @param bridgeData The abi-encoeded "from" token address.
/// @return success The magic bytes if successful.
function bridgeTransferFrom(
address toTokenAddress,
address /* from */,
address to,
uint256 amount,
bytes calldata bridgeData
)
external
returns (bytes4 success)
{
// Decode the bridge data to get the Curve metadata.
(address curveAddress, int128 fromCoinIdx, int128 toCoinIdx, int128 version) = abi.decode(bridgeData, (address, int128, int128, int128));
ICurve exchange = ICurve(curveAddress);
address fromTokenAddress = exchange.underlying_coins(fromCoinIdx);
require(toTokenAddress != fromTokenAddress, "CurveBridge/INVALID_PAIR");
// Grant an allowance to the exchange to spend `fromTokenAddress` token.
LibERC20Token.approve(fromTokenAddress, address(exchange), uint256(-1));
// Try to sell all of this contract's `fromTokenAddress` token balance.
if (version == 0) {
exchange.exchange_underlying(
fromCoinIdx,
toCoinIdx,
// dx
IERC20Token(fromTokenAddress).balanceOf(address(this)),
// min dy
amount,
// expires
block.timestamp + 1
);
} else {
exchange.exchange_underlying(
fromCoinIdx,
toCoinIdx,
// dx
IERC20Token(fromTokenAddress).balanceOf(address(this)),
// min dy
amount
);
}
uint256 toTokenBalance = IERC20Token(toTokenAddress).balanceOf(address(this));
// Transfer the converted `toToken`s to `to`.
LibERC20Token.transfer(toTokenAddress, to, toTokenBalance);
return BRIDGE_SUCCESS;
}
/// @dev `SignatureType.Wallet` callback, so that this bridge can be the maker
/// and sign for itself in orders. Always succeeds.
/// @return magicValue Magic success bytes, always.
function isValidSignature(
bytes32,
bytes calldata
)
external
view
returns (bytes4 magicValue)
{
return LEGACY_WALLET_MAGIC_VALUE;
}
}

View File

@ -0,0 +1,87 @@
/*
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;
// solhint-disable func-name-mixedcase
interface ICurve {
/// @dev Sell `sellAmount` of `fromToken` token and receive `toToken` token.
/// This function exists on early versions of Curve (USDC/DAI)
/// @param i The token index being sold.
/// @param j The token index being bought.
/// @param sellAmount The amount of token being bought.
/// @param minBuyAmount The minimum buy amount of the token being bought.
/// @param deadline The time in seconds when this operation should expire.
function exchange_underlying(
int128 i,
int128 j,
uint256 sellAmount,
uint256 minBuyAmount,
uint256 deadline
)
external;
/// @dev Sell `sellAmount` of `fromToken` token and receive `toToken` token.
/// This function exists on later versions of Curve (USDC/DAI/USDT)
/// @param i The token index being sold.
/// @param j The token index being bought.
/// @param sellAmount The amount of token being bought.
/// @param minBuyAmount The minimum buy amount of the token being bought.
function exchange_underlying(
int128 i,
int128 j,
uint256 sellAmount,
uint256 minBuyAmount
)
external;
/// @dev Get the amount of `toToken` by selling `sellAmount` of `fromToken`
/// @param i The token index being sold.
/// @param j The token index being bought.
/// @param sellAmount The amount of token being bought.
function get_dy_underlying(
int128 i,
int128 j,
uint256 sellAmount
)
external
returns (uint256 dy);
/// @dev Get the amount of `fromToken` by buying `buyAmount` of `toToken`
/// This function exists on later versions of Curve (USDC/DAI/USDT)
/// @param i The token index being sold.
/// @param j The token index being bought.
/// @param buyAmount The amount of token being bought.
function get_dx_underlying(
int128 i,
int128 j,
uint256 buyAmount
)
external
returns (uint256 dx);
/// @dev Get the underlying token address from the token index
/// @param i The token index.
function underlying_coins(
int128 i
)
external
returns (address tokenAddress);
}

View File

@ -38,7 +38,7 @@
"docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES"
},
"config": {
"abis": "./test/generated-artifacts/@(ChaiBridge|DydxBridge|ERC1155Proxy|ERC20BridgeProxy|ERC20Proxy|ERC721Proxy|Eth2DaiBridge|IAssetData|IAssetProxy|IAssetProxyDispatcher|IAuthorizable|IChai|IDydx|IDydxBridge|IERC20Bridge|IEth2Dai|IKyberNetworkProxy|IUniswapExchange|IUniswapExchangeFactory|KyberBridge|MixinAssetProxyDispatcher|MixinAuthorizable|MultiAssetProxy|Ownable|StaticCallProxy|TestChaiBridge|TestDydxBridge|TestERC20Bridge|TestEth2DaiBridge|TestKyberBridge|TestStaticCallTarget|TestUniswapBridge|UniswapBridge).json",
"abis": "./test/generated-artifacts/@(ChaiBridge|CurveBridge|DydxBridge|ERC1155Proxy|ERC20BridgeProxy|ERC20Proxy|ERC721Proxy|Eth2DaiBridge|IAssetData|IAssetProxy|IAssetProxyDispatcher|IAuthorizable|IChai|ICurve|IDydx|IDydxBridge|IERC20Bridge|IEth2Dai|IKyberNetworkProxy|IUniswapExchange|IUniswapExchangeFactory|KyberBridge|MixinAssetProxyDispatcher|MixinAuthorizable|MultiAssetProxy|Ownable|StaticCallProxy|TestChaiBridge|TestDydxBridge|TestERC20Bridge|TestEth2DaiBridge|TestKyberBridge|TestStaticCallTarget|TestUniswapBridge|UniswapBridge).json",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually."
},
"repository": {

View File

@ -6,6 +6,7 @@
import { ContractArtifact } from 'ethereum-types';
import * as ChaiBridge from '../generated-artifacts/ChaiBridge.json';
import * as CurveBridge from '../generated-artifacts/CurveBridge.json';
import * as DydxBridge from '../generated-artifacts/DydxBridge.json';
import * as ERC1155Proxy from '../generated-artifacts/ERC1155Proxy.json';
import * as ERC20BridgeProxy from '../generated-artifacts/ERC20BridgeProxy.json';
@ -17,6 +18,7 @@ import * as IAssetProxy from '../generated-artifacts/IAssetProxy.json';
import * as IAssetProxyDispatcher from '../generated-artifacts/IAssetProxyDispatcher.json';
import * as IAuthorizable from '../generated-artifacts/IAuthorizable.json';
import * as IChai from '../generated-artifacts/IChai.json';
import * as ICurve from '../generated-artifacts/ICurve.json';
import * as IDydx from '../generated-artifacts/IDydx.json';
import * as IDydxBridge from '../generated-artifacts/IDydxBridge.json';
import * as IERC20Bridge from '../generated-artifacts/IERC20Bridge.json';
@ -49,6 +51,7 @@ export const artifacts = {
MultiAssetProxy: MultiAssetProxy as ContractArtifact,
StaticCallProxy: StaticCallProxy as ContractArtifact,
ChaiBridge: ChaiBridge as ContractArtifact,
CurveBridge: CurveBridge as ContractArtifact,
DydxBridge: DydxBridge as ContractArtifact,
Eth2DaiBridge: Eth2DaiBridge as ContractArtifact,
KyberBridge: KyberBridge as ContractArtifact,
@ -58,6 +61,7 @@ export const artifacts = {
IAssetProxyDispatcher: IAssetProxyDispatcher as ContractArtifact,
IAuthorizable: IAuthorizable as ContractArtifact,
IChai: IChai as ContractArtifact,
ICurve: ICurve as ContractArtifact,
IDydx: IDydx as ContractArtifact,
IDydxBridge: IDydxBridge as ContractArtifact,
IERC20Bridge: IERC20Bridge as ContractArtifact,

View File

@ -4,6 +4,7 @@
* -----------------------------------------------------------------------------
*/
export * from '../generated-wrappers/chai_bridge';
export * from '../generated-wrappers/curve_bridge';
export * from '../generated-wrappers/dydx_bridge';
export * from '../generated-wrappers/erc1155_proxy';
export * from '../generated-wrappers/erc20_bridge_proxy';
@ -15,6 +16,7 @@ export * from '../generated-wrappers/i_asset_proxy';
export * from '../generated-wrappers/i_asset_proxy_dispatcher';
export * from '../generated-wrappers/i_authorizable';
export * from '../generated-wrappers/i_chai';
export * from '../generated-wrappers/i_curve';
export * from '../generated-wrappers/i_dydx';
export * from '../generated-wrappers/i_dydx_bridge';
export * from '../generated-wrappers/i_erc20_bridge';

View File

@ -6,6 +6,7 @@
import { ContractArtifact } from 'ethereum-types';
import * as ChaiBridge from '../test/generated-artifacts/ChaiBridge.json';
import * as CurveBridge from '../test/generated-artifacts/CurveBridge.json';
import * as DydxBridge from '../test/generated-artifacts/DydxBridge.json';
import * as ERC1155Proxy from '../test/generated-artifacts/ERC1155Proxy.json';
import * as ERC20BridgeProxy from '../test/generated-artifacts/ERC20BridgeProxy.json';
@ -17,6 +18,7 @@ import * as IAssetProxy from '../test/generated-artifacts/IAssetProxy.json';
import * as IAssetProxyDispatcher from '../test/generated-artifacts/IAssetProxyDispatcher.json';
import * as IAuthorizable from '../test/generated-artifacts/IAuthorizable.json';
import * as IChai from '../test/generated-artifacts/IChai.json';
import * as ICurve from '../test/generated-artifacts/ICurve.json';
import * as IDydx from '../test/generated-artifacts/IDydx.json';
import * as IDydxBridge from '../test/generated-artifacts/IDydxBridge.json';
import * as IERC20Bridge from '../test/generated-artifacts/IERC20Bridge.json';
@ -49,6 +51,7 @@ export const artifacts = {
MultiAssetProxy: MultiAssetProxy as ContractArtifact,
StaticCallProxy: StaticCallProxy as ContractArtifact,
ChaiBridge: ChaiBridge as ContractArtifact,
CurveBridge: CurveBridge as ContractArtifact,
DydxBridge: DydxBridge as ContractArtifact,
Eth2DaiBridge: Eth2DaiBridge as ContractArtifact,
KyberBridge: KyberBridge as ContractArtifact,
@ -58,6 +61,7 @@ export const artifacts = {
IAssetProxyDispatcher: IAssetProxyDispatcher as ContractArtifact,
IAuthorizable: IAuthorizable as ContractArtifact,
IChai: IChai as ContractArtifact,
ICurve: ICurve as ContractArtifact,
IDydx: IDydx as ContractArtifact,
IDydxBridge: IDydxBridge as ContractArtifact,
IERC20Bridge: IERC20Bridge as ContractArtifact,

View File

@ -4,6 +4,7 @@
* -----------------------------------------------------------------------------
*/
export * from '../test/generated-wrappers/chai_bridge';
export * from '../test/generated-wrappers/curve_bridge';
export * from '../test/generated-wrappers/dydx_bridge';
export * from '../test/generated-wrappers/erc1155_proxy';
export * from '../test/generated-wrappers/erc20_bridge_proxy';
@ -15,6 +16,7 @@ export * from '../test/generated-wrappers/i_asset_proxy';
export * from '../test/generated-wrappers/i_asset_proxy_dispatcher';
export * from '../test/generated-wrappers/i_authorizable';
export * from '../test/generated-wrappers/i_chai';
export * from '../test/generated-wrappers/i_curve';
export * from '../test/generated-wrappers/i_dydx';
export * from '../test/generated-wrappers/i_dydx_bridge';
export * from '../test/generated-wrappers/i_erc20_bridge';

View File

@ -4,6 +4,7 @@
"include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"],
"files": [
"generated-artifacts/ChaiBridge.json",
"generated-artifacts/CurveBridge.json",
"generated-artifacts/DydxBridge.json",
"generated-artifacts/ERC1155Proxy.json",
"generated-artifacts/ERC20BridgeProxy.json",
@ -15,6 +16,7 @@
"generated-artifacts/IAssetProxyDispatcher.json",
"generated-artifacts/IAuthorizable.json",
"generated-artifacts/IChai.json",
"generated-artifacts/ICurve.json",
"generated-artifacts/IDydx.json",
"generated-artifacts/IDydxBridge.json",
"generated-artifacts/IERC20Bridge.json",
@ -37,6 +39,7 @@
"generated-artifacts/TestUniswapBridge.json",
"generated-artifacts/UniswapBridge.json",
"test/generated-artifacts/ChaiBridge.json",
"test/generated-artifacts/CurveBridge.json",
"test/generated-artifacts/DydxBridge.json",
"test/generated-artifacts/ERC1155Proxy.json",
"test/generated-artifacts/ERC20BridgeProxy.json",
@ -48,6 +51,7 @@
"test/generated-artifacts/IAssetProxyDispatcher.json",
"test/generated-artifacts/IAuthorizable.json",
"test/generated-artifacts/IChai.json",
"test/generated-artifacts/ICurve.json",
"test/generated-artifacts/IDydx.json",
"test/generated-artifacts/IDydxBridge.json",
"test/generated-artifacts/IERC20Bridge.json",

View File

@ -1,4 +1,13 @@
[
{
"version": "1.4.0",
"changes": [
{
"note": "Added Curve contract sampling",
"pr": 2483
}
]
},
{
"version": "1.3.0",
"changes": [

View File

@ -29,6 +29,7 @@ import "./IERC20BridgeSampler.sol";
import "./IEth2Dai.sol";
import "./IKyberNetwork.sol";
import "./IUniswapExchangeQuotes.sol";
import "./ICurve.sol";
contract ERC20BridgeSampler is
@ -43,6 +44,9 @@ contract ERC20BridgeSampler is
uint256 constant internal UNISWAP_CALL_GAS = 150e3; // 150k
/// @dev Base gas limit for Eth2Dai calls.
uint256 constant internal ETH2DAI_CALL_GAS = 1000e3; // 1m
/// @dev Base gas limit for Curve calls. Some Curves have multiple tokens
/// So a reasonable ceil is 150k per token. Biggest Curve has 4 tokens.
uint256 constant internal CURVE_CALL_GAS = 600e3; // 600k
/// @dev Call multiple public functions on this contract in a single transaction.
/// @param callDatas ABI-encoded call data for each function call.
@ -389,6 +393,44 @@ contract ERC20BridgeSampler is
}
}
/// @dev Sample sell quotes from Curve.
/// @param curveAddress Address of the Curve contract.
/// @param fromTokenIdx Index of the taker token (what to sell).
/// @param toTokenIdx Index of the maker token (what to buy).
/// @param takerTokenAmounts Taker token sell amount for each sample.
/// @return makerTokenAmounts Maker amounts bought at each taker token
/// amount.
function sampleSellsFromCurve(
address curveAddress,
int128 fromTokenIdx,
int128 toTokenIdx,
uint256[] memory takerTokenAmounts
)
public
view
returns (uint256[] memory makerTokenAmounts)
{
uint256 numSamples = takerTokenAmounts.length;
makerTokenAmounts = new uint256[](numSamples);
for (uint256 i = 0; i < numSamples; i++) {
(bool didSucceed, bytes memory resultData) =
curveAddress.staticcall.gas(CURVE_CALL_GAS)(
abi.encodeWithSelector(
ICurve(0).get_dy_underlying.selector,
fromTokenIdx,
toTokenIdx,
takerTokenAmounts[i]
));
uint256 buyAmount = 0;
if (didSucceed) {
buyAmount = abi.decode(resultData, (uint256));
} else {
break;
}
makerTokenAmounts[i] = buyAmount;
}
}
/// @dev Overridable way to get token decimals.
/// @param tokenAddress Address of the token.
/// @return decimals The decimal places for the token.

View File

@ -0,0 +1,87 @@
/*
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;
// solhint-disable func-name-mixedcase
interface ICurve {
/// @dev Sell `sellAmount` of `fromToken` token and receive `toToken` token.
/// This function exists on early versions of Curve (USDC/DAI)
/// @param i The token index being sold.
/// @param j The token index being bought.
/// @param sellAmount The amount of token being bought.
/// @param minBuyAmount The minimum buy amount of the token being bought.
/// @param deadline The time in seconds when this operation should expire.
function exchange_underlying(
int128 i,
int128 j,
uint256 sellAmount,
uint256 minBuyAmount,
uint256 deadline
)
external;
/// @dev Sell `sellAmount` of `fromToken` token and receive `toToken` token.
/// This function exists on later versions of Curve (USDC/DAI/USDT)
/// @param i The token index being sold.
/// @param j The token index being bought.
/// @param sellAmount The amount of token being bought.
/// @param minBuyAmount The minimum buy amount of the token being bought.
function exchange_underlying(
int128 i,
int128 j,
uint256 sellAmount,
uint256 minBuyAmount
)
external;
/// @dev Get the amount of `toToken` by selling `sellAmount` of `fromToken`
/// @param i The token index being sold.
/// @param j The token index being bought.
/// @param sellAmount The amount of token being bought.
function get_dy_underlying(
int128 i,
int128 j,
uint256 sellAmount
)
external
returns (uint256 dy);
/// @dev Get the amount of `fromToken` by buying `buyAmount` of `toToken`
/// This function exists on later versions of Curve (USDC/DAI/USDT)
/// @param i The token index being sold.
/// @param j The token index being bought.
/// @param buyAmount The amount of token being bought.
function get_dx_underlying(
int128 i,
int128 j,
uint256 buyAmount
)
external
returns (uint256 dx);
/// @dev Get the underlying token address from the token index
/// @param i The token index.
function underlying_coins(
int128 i
)
external
returns (address tokenAddress);
}

View File

@ -132,4 +132,21 @@ interface IERC20BridgeSampler {
external
view
returns (uint256[] memory takerTokenAmounts);
/// @dev Sample sell quotes from Curve.
/// @param curveAddress Address of the Curve contract.
/// @param fromTokenIdx Index of the taker token (what to sell).
/// @param toTokenIdx Index of the maker token (what to buy).
/// @param takerTokenAmounts Taker token sell amount for each sample.
/// @return makerTokenAmounts Maker amounts bought at each taker token
/// amount.
function sampleSellsFromCurve(
address curveAddress,
int128 fromTokenIdx,
int128 toTokenIdx,
uint256[] calldata takerTokenAmounts
)
external
view
returns (uint256[] memory makerTokenAmounts);
}

View File

@ -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/@(ERC20BridgeSampler|IDevUtils|IERC20BridgeSampler|IEth2Dai|IKyberNetwork|IUniswapExchangeQuotes|TestERC20BridgeSampler).json"
"abis": "./test/generated-artifacts/@(ERC20BridgeSampler|ICurve|IDevUtils|IERC20BridgeSampler|IEth2Dai|IKyberNetwork|IUniswapExchangeQuotes|TestERC20BridgeSampler).json"
},
"repository": {
"type": "git",

View File

@ -6,6 +6,7 @@
import { ContractArtifact } from 'ethereum-types';
import * as ERC20BridgeSampler from '../test/generated-artifacts/ERC20BridgeSampler.json';
import * as ICurve from '../test/generated-artifacts/ICurve.json';
import * as IDevUtils from '../test/generated-artifacts/IDevUtils.json';
import * as IERC20BridgeSampler from '../test/generated-artifacts/IERC20BridgeSampler.json';
import * as IEth2Dai from '../test/generated-artifacts/IEth2Dai.json';
@ -14,6 +15,7 @@ import * as IUniswapExchangeQuotes from '../test/generated-artifacts/IUniswapExc
import * as TestERC20BridgeSampler from '../test/generated-artifacts/TestERC20BridgeSampler.json';
export const artifacts = {
ERC20BridgeSampler: ERC20BridgeSampler as ContractArtifact,
ICurve: ICurve as ContractArtifact,
IDevUtils: IDevUtils as ContractArtifact,
IERC20BridgeSampler: IERC20BridgeSampler as ContractArtifact,
IEth2Dai: IEth2Dai as ContractArtifact,

View File

@ -4,6 +4,7 @@
* -----------------------------------------------------------------------------
*/
export * from '../test/generated-wrappers/erc20_bridge_sampler';
export * from '../test/generated-wrappers/i_curve';
export * from '../test/generated-wrappers/i_dev_utils';
export * from '../test/generated-wrappers/i_erc20_bridge_sampler';
export * from '../test/generated-wrappers/i_eth2_dai';

View File

@ -6,6 +6,7 @@
"generated-artifacts/ERC20BridgeSampler.json",
"generated-artifacts/IERC20BridgeSampler.json",
"test/generated-artifacts/ERC20BridgeSampler.json",
"test/generated-artifacts/ICurve.json",
"test/generated-artifacts/IDevUtils.json",
"test/generated-artifacts/IERC20BridgeSampler.json",
"test/generated-artifacts/IEth2Dai.json",

View File

@ -11,8 +11,12 @@
"pr": 2479
},
{
"note": "Addeded decoders for stop-limit data",
"note": "Addded decoders for stop-limit data",
"pr": 2484
},
{
"note": "Added ERC20Sampler and Curve Mainnet test",
"pr": 2483
}
]
},

View File

@ -57,6 +57,7 @@
"@0x/contracts-broker": "^1.0.2",
"@0x/contracts-coordinator": "^3.1.0",
"@0x/contracts-dev-utils": "^1.1.0",
"@0x/contracts-erc20-bridge-sampler": "^1.3.0",
"@0x/contracts-exchange-forwarder": "^4.2.0",
"@0x/contracts-exchange-libs": "^4.3.0",
"@0x/contracts-extensions": "^6.1.0",

View File

@ -0,0 +1,37 @@
import { artifacts, ERC20BridgeSamplerContract } from '@0x/contracts-erc20-bridge-sampler';
import { blockchainTests, describe, expect, toBaseUnitAmount } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
blockchainTests.fork.resets('Mainnet Sampler Tests', env => {
let testContract: ERC20BridgeSamplerContract;
before(async () => {
testContract = await ERC20BridgeSamplerContract.deployFrom0xArtifactAsync(
artifacts.ERC20BridgeSampler,
env.provider,
{ ...env.txDefaults, from: '0x6cc5f688a315f3dc28a7781717a9a798a59fda7b' },
{},
);
});
describe('sampleSellsFromCurve()', () => {
const curveAddress = '0x45f783cce6b7ff23b2ab2d70e416cdb7d6055f51';
const daiTokenIdx = new BigNumber(0);
const usdcTokenIdx = new BigNumber(1);
it('samples sells from Curve DAI->USDC', async () => {
const samples = await testContract
.sampleSellsFromCurve(curveAddress, daiTokenIdx, usdcTokenIdx, [toBaseUnitAmount(1)])
.callAsync();
expect(samples.length).to.be.bignumber.greaterThan(0);
expect(samples[0]).to.be.bignumber.greaterThan(0);
});
it('samples sells from Curve USDC->DAI', async () => {
const samples = await testContract
.sampleSellsFromCurve(curveAddress, usdcTokenIdx, daiTokenIdx, [toBaseUnitAmount(1, 6)])
.callAsync();
expect(samples.length).to.be.bignumber.greaterThan(0);
expect(samples[0]).to.be.bignumber.greaterThan(0);
});
});
});

View File

@ -0,0 +1,111 @@
import { artifacts as assetProxyArtifacts } from '@0x/contracts-asset-proxy';
import { CurveBridgeContract } from '@0x/contracts-asset-proxy/lib/src/wrappers';
import { ERC20TokenContract } from '@0x/contracts-erc20';
import { blockchainTests, constants, describe, toBaseUnitAmount } from '@0x/contracts-test-utils';
import { AbiEncoder } from '@0x/utils';
blockchainTests.fork.resets('Mainnet curve bridge tests', env => {
let testContract: CurveBridgeContract;
const receiver = '0x986ccf5234d9cfbb25246f1a5bfa51f4ccfcb308';
const usdcWallet = '0xF977814e90dA44bFA03b6295A0616a897441aceC';
const usdcAddress = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48';
const daiWallet = '0x6cc5f688a315f3dc28a7781717a9a798a59fda7b';
const daiAddress = '0x6B175474E89094C44Da98b954EedeAC495271d0F';
const curveAddressUsdcDai = '0x2e60CF74d81ac34eB21eEff58Db4D385920ef419';
const curveAddressUsdcDaiUsdt = '0x52EA46506B9CC5Ef470C5bf89f17Dc28bB35D85C';
const daiTokenIdx = 0;
const usdcTokenIdx = 1;
const bridgeDataEncoder = AbiEncoder.create([
{ name: 'curveAddress', type: 'address' },
{ name: 'fromTokenIdx', type: 'int128' },
{ name: 'toTokenIdx', type: 'int128' },
{ name: 'version', type: 'int128' },
]);
before(async () => {
testContract = await CurveBridgeContract.deployFrom0xArtifactAsync(
assetProxyArtifacts.CurveBridge,
env.provider,
{ ...env.txDefaults, from: daiWallet },
{},
);
});
describe('bridgeTransferFrom()', () => {
describe('Version 0', () => {
const version = 0;
it('succeeds exchanges DAI for USDC', async () => {
const bridgeData = bridgeDataEncoder.encode([curveAddressUsdcDai, daiTokenIdx, usdcTokenIdx, version]);
// Fund the Bridge
const dai = new ERC20TokenContract(daiAddress, env.provider, { ...env.txDefaults, from: daiWallet });
await dai
.transfer(testContract.address, toBaseUnitAmount(1))
.awaitTransactionSuccessAsync({ from: daiWallet }, { shouldValidate: false });
// Exchange via Curve
await testContract
.bridgeTransferFrom(
usdcAddress,
constants.NULL_ADDRESS,
receiver,
constants.ZERO_AMOUNT,
bridgeData,
)
.awaitTransactionSuccessAsync({ from: daiWallet, gasPrice: 1 }, { shouldValidate: false });
});
it('succeeds exchanges USDC for DAI', async () => {
const bridgeData = bridgeDataEncoder.encode([curveAddressUsdcDai, usdcTokenIdx, daiTokenIdx, version]);
// Fund the Bridge
const usdc = new ERC20TokenContract(usdcAddress, env.provider, { ...env.txDefaults, from: usdcWallet });
await usdc
.transfer(testContract.address, toBaseUnitAmount(1, 6))
.awaitTransactionSuccessAsync({ from: usdcWallet }, { shouldValidate: false });
// Exchange via Curve
await testContract
.bridgeTransferFrom(daiAddress, constants.NULL_ADDRESS, receiver, constants.ZERO_AMOUNT, bridgeData)
.awaitTransactionSuccessAsync({ from: usdcWallet, gasPrice: 1 }, { shouldValidate: false });
});
});
describe('Version 1', () => {
const version = 1;
it('succeeds exchanges DAI for USDC', async () => {
const bridgeData = bridgeDataEncoder.encode([
curveAddressUsdcDaiUsdt,
daiTokenIdx,
usdcTokenIdx,
version,
]);
// Fund the Bridge
const dai = new ERC20TokenContract(daiAddress, env.provider, { ...env.txDefaults, from: daiWallet });
await dai
.transfer(testContract.address, toBaseUnitAmount(1))
.awaitTransactionSuccessAsync({ from: daiWallet }, { shouldValidate: false });
// Exchange via Curve
await testContract
.bridgeTransferFrom(
usdcAddress,
constants.NULL_ADDRESS,
receiver,
constants.ZERO_AMOUNT,
bridgeData,
)
.awaitTransactionSuccessAsync({ from: daiWallet, gasPrice: 1 }, { shouldValidate: false });
});
it('succeeds exchanges USDC for DAI', async () => {
const bridgeData = bridgeDataEncoder.encode([
curveAddressUsdcDaiUsdt,
usdcTokenIdx,
daiTokenIdx,
version,
]);
// Fund the Bridge
const usdc = new ERC20TokenContract(usdcAddress, env.provider, { ...env.txDefaults, from: usdcWallet });
await usdc
.transfer(testContract.address, toBaseUnitAmount(1, 6))
.awaitTransactionSuccessAsync({ from: usdcWallet }, { shouldValidate: false });
// Exchange via Curve
await testContract
.bridgeTransferFrom(daiAddress, constants.NULL_ADDRESS, receiver, constants.ZERO_AMOUNT, bridgeData)
.awaitTransactionSuccessAsync({ from: usdcWallet, gasPrice: 1 }, { shouldValidate: false });
});
});
});
});

View File

@ -24,6 +24,7 @@ export let providerConfigs: Web3Config = {
'0x6cc5f688a315f3dc28a7781717a9a798a59fda7b',
'0x55dc8f21d20d4c6ed3c82916a438a413ca68e335',
'0x8ed95d1746bf1e4dab58d8ed4724f1ef95b20db0', // ERC20BridgeProxy
'0xf977814e90da44bfa03b6295a0616a897441acec', // Binance: USDC, TUSD
],
};

View File

@ -5,6 +5,10 @@
{
"note": "Use `batchCall()` version of the `ERC20BridgeSampler` contract",
"pr": 2477
},
{
"note": "Support for sampling Curve contracts",
"pr": 2483
}
]
},

View File

@ -12,6 +12,7 @@ import {
} from './types';
import { constants as marketOperationUtilConstants } from './utils/market_operation_utils/constants';
import { ERC20BridgeSource } from './utils/market_operation_utils/types';
const ETH_GAS_STATION_API_BASE_URL = 'https://ethgasstation.info';
const NULL_BYTES = '0x';
@ -42,7 +43,7 @@ const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = {
orderRefreshIntervalMs: 10000, // 10 seconds
},
...DEFAULT_ORDER_PRUNER_OPTS,
samplerGasLimit: 36e6,
samplerGasLimit: 59e6,
};
const DEFAULT_FORWARDER_EXTENSION_CONTRACT_OPTS: ForwarderExtensionContractOpts = {
@ -64,6 +65,34 @@ const DEFAULT_SWAP_QUOTE_REQUEST_OPTS: SwapQuoteRequestOpts = {
...marketOperationUtilConstants.DEFAULT_GET_MARKET_ORDERS_OPTS,
};
// Mainnet Curve configuration
const DEFAULT_CURVE_OPTS: { [source: string]: { version: number; curveAddress: string; tokens: string[] } } = {
[ERC20BridgeSource.CurveUsdcDai]: {
version: 0,
curveAddress: '0x2e60cf74d81ac34eb21eeff58db4d385920ef419',
tokens: ['0x6b175474e89094c44da98b954eedeac495271d0f', '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'],
},
[ERC20BridgeSource.CurveUsdcDaiUsdt]: {
version: 1,
curveAddress: '0x52ea46506b9cc5ef470c5bf89f17dc28bb35d85c',
tokens: [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7',
],
},
[ERC20BridgeSource.CurveUsdcDaiUsdtTusd]: {
version: 1,
curveAddress: '0x45f783cce6b7ff23b2ab2d70e416cdb7d6055f51',
tokens: [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7',
'0x0000000000085d4780b73119b644ae5ecd22b376',
],
},
};
export const constants = {
ETH_GAS_STATION_API_BASE_URL,
PROTOCOL_FEE_MULTIPLIER,
@ -84,4 +113,5 @@ export const constants = {
PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS,
MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE,
BRIDGE_ASSET_DATA_PREFIX: '0xdc1600f3',
DEFAULT_CURVE_OPTS,
};

View File

@ -7,7 +7,14 @@ const INFINITE_TIMESTAMP_SEC = new BigNumber(2524604400);
/**
* Valid sources for market sell.
*/
export const SELL_SOURCES = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Kyber];
export const SELL_SOURCES = [
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Kyber,
ERC20BridgeSource.CurveUsdcDai,
ERC20BridgeSource.CurveUsdcDaiUsdt,
ERC20BridgeSource.CurveUsdcDaiUsdtTusd,
];
/**
* Valid sources for market buy.

View File

@ -90,6 +90,10 @@ export class CreateOrderUtils {
return this._contractAddress.kyberBridge;
case ERC20BridgeSource.Uniswap:
return this._contractAddress.uniswapBridge;
case ERC20BridgeSource.CurveUsdcDai:
case ERC20BridgeSource.CurveUsdcDaiUsdt:
case ERC20BridgeSource.CurveUsdcDaiUsdtTusd:
return this._contractAddress.curveBridge;
default:
break;
}
@ -106,13 +110,30 @@ function createBridgeOrder(
slippage: number,
isBuy: boolean = false,
): OptimizedMarketOrder {
return {
makerAddress: bridgeAddress,
makerAssetData: assetDataUtils.encodeERC20BridgeAssetData(
let makerAssetData;
if (
fill.source === ERC20BridgeSource.CurveUsdcDai ||
fill.source === ERC20BridgeSource.CurveUsdcDaiUsdt ||
fill.source === ERC20BridgeSource.CurveUsdcDaiUsdtTusd
) {
const { curveAddress, tokens, version } = constants.DEFAULT_CURVE_OPTS[fill.source];
const fromTokenIdx = tokens.indexOf(takerToken);
const toTokenIdx = tokens.indexOf(makerToken);
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createCurveBridgeData(curveAddress, fromTokenIdx, toTokenIdx, version),
);
} else {
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createBridgeData(takerToken),
),
);
}
return {
makerAddress: bridgeAddress,
makerAssetData,
takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken),
...createCommonOrderFields(orderDomain, fill, slippage, isBuy),
};
@ -123,6 +144,21 @@ function createBridgeData(tokenAddress: string): string {
return encoder.encode({ tokenAddress });
}
function createCurveBridgeData(
curveAddress: string,
fromTokenIdx: number,
toTokenIdx: number,
version: number,
): string {
const curveBridgeDataEncoder = AbiEncoder.create([
{ name: 'curveAddress', type: 'address' },
{ name: 'fromTokenIdx', type: 'int128' },
{ name: 'toTokenIdx', type: 'int128' },
{ name: 'version', type: 'int128' },
]);
return curveBridgeDataEncoder.encode([curveAddress, fromTokenIdx, toTokenIdx, version]);
}
type CommonOrderFields = Pick<
OptimizedMarketOrder,
Exclude<keyof OptimizedMarketOrder, 'makerAddress' | 'makerAssetData' | 'takerAssetData'>

View File

@ -2,6 +2,8 @@ import { IERC20BridgeSamplerContract } from '@0x/contract-wrappers';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { constants } from '../../constants';
import { DexSample, ERC20BridgeSource } from './types';
/**
@ -89,6 +91,28 @@ const samplerOperations = {
},
};
},
getCurveSellQuotes(
curveAddress: string,
fromTokenIdx: number,
toTokenIdx: number,
takerFillAmounts: BigNumber[],
): BatchedOperation<BigNumber[]> {
return {
encodeCall: contract => {
return contract
.sampleSellsFromCurve(
curveAddress,
new BigNumber(fromTokenIdx),
new BigNumber(toTokenIdx),
takerFillAmounts,
)
.getABIEncodedTransactionData();
},
handleCallResultsAsync: async (contract, callResults) => {
return contract.getABIDecodedReturnData<BigNumber[]>('sampleSellsFromCurve', callResults);
},
};
},
getUniswapBuyQuotes(
makerToken: string,
takerToken: string,
@ -127,30 +151,55 @@ const samplerOperations = {
takerToken: string,
takerFillAmounts: BigNumber[],
): BatchedOperation<DexSample[][]> {
const subOps = sources.map(source => {
if (source === ERC20BridgeSource.Eth2Dai) {
return samplerOperations.getEth2DaiSellQuotes(makerToken, takerToken, takerFillAmounts);
} else if (source === ERC20BridgeSource.Uniswap) {
return samplerOperations.getUniswapSellQuotes(makerToken, takerToken, takerFillAmounts);
} else if (source === ERC20BridgeSource.Kyber) {
return samplerOperations.getKyberSellQuotes(makerToken, takerToken, takerFillAmounts);
} else {
throw new Error(`Unsupported sell sample source: ${source}`);
}
});
const subOps = sources
.map(source => {
let batchedOperation;
if (source === ERC20BridgeSource.Eth2Dai) {
batchedOperation = samplerOperations.getEth2DaiSellQuotes(makerToken, takerToken, takerFillAmounts);
} else if (source === ERC20BridgeSource.Uniswap) {
batchedOperation = samplerOperations.getUniswapSellQuotes(makerToken, takerToken, takerFillAmounts);
} else if (source === ERC20BridgeSource.Kyber) {
batchedOperation = samplerOperations.getKyberSellQuotes(makerToken, takerToken, takerFillAmounts);
} else if (
source === ERC20BridgeSource.CurveUsdcDai ||
source === ERC20BridgeSource.CurveUsdcDaiUsdt ||
source === ERC20BridgeSource.CurveUsdcDaiUsdtTusd
) {
const { curveAddress, tokens } = constants.DEFAULT_CURVE_OPTS[source];
const fromTokenIdx = tokens.indexOf(takerToken);
const toTokenIdx = tokens.indexOf(makerToken);
if (fromTokenIdx !== -1 && toTokenIdx !== -1) {
batchedOperation = samplerOperations.getCurveSellQuotes(
curveAddress,
fromTokenIdx,
toTokenIdx,
takerFillAmounts,
);
}
} else {
throw new Error(`Unsupported sell sample source: ${source}`);
}
return { batchedOperation, source };
})
.filter(op => op.batchedOperation) as Array<{
batchedOperation: BatchedOperation<BigNumber[]>;
source: ERC20BridgeSource;
}>;
return {
encodeCall: contract => {
const subCalls = subOps.map(op => op.encodeCall(contract));
const subCalls = subOps.map(op => op.batchedOperation.encodeCall(contract));
return contract.batchCall(subCalls).getABIEncodedTransactionData();
},
handleCallResultsAsync: async (contract, callResults) => {
const rawSubCallResults = contract.getABIDecodedReturnData<string[]>('batchCall', callResults);
const samples = await Promise.all(
subOps.map(async (op, i) => op.handleCallResultsAsync(contract, rawSubCallResults[i])),
subOps.map(async (op, i) =>
op.batchedOperation.handleCallResultsAsync(contract, rawSubCallResults[i]),
),
);
return sources.map((source, i) => {
return subOps.map((op, i) => {
return samples[i].map((output, j) => ({
source,
source: op.source,
output,
input: takerFillAmounts[j],
}));

View File

@ -28,6 +28,9 @@ export enum ERC20BridgeSource {
Uniswap = 'Uniswap',
Eth2Dai = 'Eth2Dai',
Kyber = 'Kyber',
CurveUsdcDai = 'Curve_USDC_DAI',
CurveUsdcDaiUsdt = 'Curve_USDC_DAI_USDT',
CurveUsdcDaiUsdtTusd = 'Curve_USDC_DAI_USDT_TUSD',
}
// Internal `fillData` field for `Fill` objects.

View File

@ -14,6 +14,7 @@ import { SignedOrder } from '@0x/types';
import { BigNumber, hexUtils } from '@0x/utils';
import * as _ from 'lodash';
import { constants as assetSwapperConstants } from '../src/constants';
import { MarketOperationUtils } from '../src/utils/market_operation_utils/';
import { constants as marketOperationUtilConstants } from '../src/utils/market_operation_utils/constants';
import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
@ -28,6 +29,7 @@ describe('MarketOperationUtils tests', () => {
const ETH2DAI_BRIDGE_ADDRESS = contractAddresses.eth2DaiBridge;
const KYBER_BRIDGE_ADDRESS = contractAddresses.kyberBridge;
const UNISWAP_BRIDGE_ADDRESS = contractAddresses.uniswapBridge;
const CURVE_BRIDGE_ADDRESS = contractAddresses.curveBridge;
const MAKER_TOKEN = randomAddress();
const TAKER_TOKEN = randomAddress();
@ -78,6 +80,11 @@ describe('MarketOperationUtils tests', () => {
return ERC20BridgeSource.Eth2Dai;
case UNISWAP_BRIDGE_ADDRESS.toLowerCase():
return ERC20BridgeSource.Uniswap;
case CURVE_BRIDGE_ADDRESS.toLowerCase():
const curveSource = Object.keys(assetSwapperConstants.DEFAULT_CURVE_OPTS).filter(
k => assetData.indexOf(assetSwapperConstants.DEFAULT_CURVE_OPTS[k].curveAddress.slice(2)) !== -1,
);
return curveSource[0] as ERC20BridgeSource;
default:
break;
}
@ -116,13 +123,15 @@ describe('MarketOperationUtils tests', () => {
type GetQuotesOperation = (makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => BigNumber[];
function createGetSellQuotesOperationFromRates(rates: Numberish[]): GetQuotesOperation {
return (makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => {
return (...args) => {
const fillAmounts = args.pop() as BigNumber[];
return fillAmounts.map((a, i) => a.times(rates[i]).integerValue());
};
}
function createGetBuyQuotesOperationFromRates(rates: Numberish[]): GetQuotesOperation {
return (makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => {
return (...args) => {
const fillAmounts = args.pop() as BigNumber[];
return fillAmounts.map((a, i) => a.div(rates[i]).integerValue());
};
}
@ -179,6 +188,9 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Eth2Dai]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.Kyber]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.Uniswap]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.CurveUsdcDai]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.CurveUsdcDaiUsdt]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.CurveUsdcDaiUsdtTusd]: createDecreasingRates(NUM_SAMPLES),
};
function findSourceWithMaxOutput(rates: RatesBySource): ERC20BridgeSource {
@ -209,6 +221,7 @@ describe('MarketOperationUtils tests', () => {
getEth2DaiSellQuotes: createGetSellQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.Eth2Dai]),
getUniswapBuyQuotes: createGetBuyQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.Uniswap]),
getEth2DaiBuyQuotes: createGetBuyQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.Eth2Dai]),
getCurveSellQuotes: createGetSellQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.CurveUsdcDai]),
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES),
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES),
};
@ -386,6 +399,9 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Kyber] = [0.7, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.CurveUsdcDai] = [0, 0, 0, 0];
rates[ERC20BridgeSource.CurveUsdcDaiUsdt] = [0, 0, 0, 0];
rates[ERC20BridgeSource.CurveUsdcDaiUsdtTusd] = [0, 0, 0, 0];
replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
});
@ -411,6 +427,9 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Kyber] = [0.4, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.CurveUsdcDai] = [0, 0, 0, 0];
rates[ERC20BridgeSource.CurveUsdcDaiUsdt] = [0, 0, 0, 0];
rates[ERC20BridgeSource.CurveUsdcDaiUsdtTusd] = [0, 0, 0, 0];
replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
});
@ -436,6 +455,9 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Uniswap] = [0.15, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Eth2Dai] = [0.15, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Kyber] = [0.7, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.CurveUsdcDai] = [0, 0, 0, 0];
rates[ERC20BridgeSource.CurveUsdcDaiUsdt] = [0, 0, 0, 0];
rates[ERC20BridgeSource.CurveUsdcDaiUsdtTusd] = [0, 0, 0, 0];
replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
});
@ -516,7 +538,15 @@ describe('MarketOperationUtils tests', () => {
});
it('returns the most cost-effective single source if `runLimit == 0`', async () => {
const bestSource = findSourceWithMaxOutput(_.omit(DEFAULT_RATES, ERC20BridgeSource.Kyber));
const bestSource = findSourceWithMaxOutput(
_.omit(
DEFAULT_RATES,
ERC20BridgeSource.Kyber,
ERC20BridgeSource.CurveUsdcDai,
ERC20BridgeSource.CurveUsdcDaiUsdt,
ERC20BridgeSource.CurveUsdcDaiUsdtTusd,
),
);
expect(bestSource).to.exist('');
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS,

View File

@ -3,8 +3,16 @@
"version": "4.6.0",
"changes": [
{
"note": "Added ChainlinkStopLimit addresses (mainnet, ropsten, rinkeby)",
"note": "Added `ChainlinkStopLimit` addresses (mainnet, ropsten, rinkeby)",
"pr": 2473
},
{
"note": "Added `CurveBridge` address (mainnet)",
"pr": 2483
},
{
"note": "Update `ERC20BridgeSampler` address (mainnet, kovan)",
"pr": 2483
}
]
},

View File

@ -20,14 +20,15 @@
"devUtils": "0xb1a3d901bad1df7d710fc8d008db7cdd6bbbffe6",
"erc20BridgeProxy": "0x8ed95d1746bf1e4dab58d8ed4724f1ef95b20db0",
"uniswapBridge": "0x533344cfdf2a3e911e2cf4c6f5ed08e791f5355f",
"erc20BridgeSampler": "0x774c53ee7604af93cd3ed1cd25a788a9e0c06fb2",
"erc20BridgeSampler": "0x43cfd1027bcc01d3df714d9e7bf989622e4bbec1",
"kyberBridge": "0xf342f3a80fdc9b48713d58fe97e17f5cc764ee62",
"eth2DaiBridge": "0xe3379a1956f4a79f39eb2e87bb441419e167538e",
"chaiBridge": "0x77c31eba23043b9a72d13470f3a3a311344d7438",
"dydxBridge": "0x55dc8f21d20d4c6ed3c82916a438a413ca68e335",
"godsUnchainedValidator": "0x09A379Ef7218BCFD8913fAa8B281ebc5A2E0bC04",
"broker": "0xd4690a51044db77D91d7Aa8f7a3a5ad5dA331Af0",
"chainlinkStopLimit": "0xeb27220f95f364e1d9531992c48613f231839f53"
"chainlinkStopLimit": "0xeb27220f95f364e1d9531992c48613f231839f53",
"curveBridge": "0xe335bdd1fb0ee30f9a9a434f18f8b118dec32df7"
},
"3": {
"erc20Proxy": "0xb1408f4c245a23c31b98d2c626777d4c0d766caa",
@ -57,7 +58,8 @@
"dydxBridge": "0x0000000000000000000000000000000000000000",
"godsUnchainedValidator": "0xd4690a51044db77D91d7Aa8f7a3a5ad5dA331Af0",
"broker": "0x4Aa817C6f383C8e8aE77301d18Ce48efb16Fd2BE",
"chainlinkStopLimit": "0x67a094cf028221ffdd93fc658f963151d05e2a74"
"chainlinkStopLimit": "0x67a094cf028221ffdd93fc658f963151d05e2a74",
"curveBridge": "0x0000000000000000000000000000000000000000"
},
"4": {
"exchangeV2": "0xbff9493f92a3df4b0429b6d00743b3cfb4c85831",
@ -87,7 +89,8 @@
"dydxBridge": "0x0000000000000000000000000000000000000000",
"godsUnchainedValidator": "0x0000000000000000000000000000000000000000",
"broker": "0x0000000000000000000000000000000000000000",
"chainlinkStopLimit": "0x407b4128e9ecad8769b2332312a9f655cb9f5f3a"
"chainlinkStopLimit": "0x407b4128e9ecad8769b2332312a9f655cb9f5f3a",
"curveBridge": "0x0000000000000000000000000000000000000000"
},
"42": {
"erc20Proxy": "0xf1ec01d6236d3cd881a0bf0130ea25fe4234003e",
@ -111,13 +114,14 @@
"erc20BridgeProxy": "0xfb2dd2a1366de37f7241c83d47da58fd503e2c64",
"uniswapBridge": "0x8224aa8fe5c9f07d5a59c735386ff6cc6aaeb568",
"eth2DaiBridge": "0x9485d65c6a2fae0d519cced5bd830e57c41998a9",
"erc20BridgeSampler": "0xca6485a7d0f1a42192072dff7518324513294adf",
"erc20BridgeSampler": "0x0937795148f54f08538390d936e898163684b33f",
"kyberBridge": "0xde7b2747624a647600fdb349184d0448ab954929",
"chaiBridge": "0x0000000000000000000000000000000000000000",
"dydxBridge": "0x0000000000000000000000000000000000000000",
"godsUnchainedValidator": "0x0000000000000000000000000000000000000000",
"broker": "0x0000000000000000000000000000000000000000",
"chainlinkStopLimit": "0x0000000000000000000000000000000000000000"
"chainlinkStopLimit": "0x0000000000000000000000000000000000000000",
"curveBridge": "0x0000000000000000000000000000000000000000"
},
"1337": {
"erc20Proxy": "0x1dc4c1cefef38a777b15aa20260a54e584b16c48",
@ -147,6 +151,7 @@
"dydxBridge": "0x0000000000000000000000000000000000000000",
"godsUnchainedValidator": "0x0000000000000000000000000000000000000000",
"broker": "0x0000000000000000000000000000000000000000",
"chainlinkStopLimit": "0x0000000000000000000000000000000000000000"
"chainlinkStopLimit": "0x0000000000000000000000000000000000000000",
"curveBridge": "0x0000000000000000000000000000000000000000"
}
}

View File

@ -28,6 +28,7 @@ export interface ContractAddresses {
kyberBridge: string;
chaiBridge: string;
dydxBridge: string;
curveBridge: string;
}
export enum ChainId {

View File

@ -106,6 +106,20 @@
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "curveAddress", "type": "address" },
{ "internalType": "int128", "name": "fromTokenIdx", "type": "int128" },
{ "internalType": "int128", "name": "toTokenIdx", "type": "int128" },
{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleSellsFromCurve",
"outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
@ -187,6 +201,16 @@
},
"return": "takerTokenAmounts Taker amounts sold at each maker token amount."
},
"sampleSellsFromCurve(address,int128,int128,uint256[])": {
"details": "Sample sell quotes from Curve.",
"params": {
"curveAddress": "Address of the Curve contract.",
"fromTokenIdx": "Index of the taker token (what to sell).",
"takerTokenAmounts": "Taker token sell amount for each sample.",
"toTokenIdx": "Index of the maker token (what to buy)."
},
"return": "makerTokenAmounts Maker amounts bought at each taker token amount."
},
"sampleSellsFromEth2Dai(address,address,uint256[])": {
"details": "Sample sell quotes from Eth2Dai/Oasis.",
"params": {

View File

@ -396,6 +396,37 @@ export class IERC20BridgeSamplerContract extends BaseContract {
stateMutability: 'view',
type: 'function',
},
{
constant: true,
inputs: [
{
name: 'curveAddress',
type: 'address',
},
{
name: 'fromTokenIdx',
type: 'int128',
},
{
name: 'toTokenIdx',
type: 'int128',
},
{
name: 'takerTokenAmounts',
type: 'uint256[]',
},
],
name: 'sampleSellsFromCurve',
outputs: [
{
name: 'makerTokenAmounts',
type: 'uint256[]',
},
],
payable: false,
stateMutability: 'view',
type: 'function',
},
{
constant: true,
inputs: [
@ -751,6 +782,48 @@ export class IERC20BridgeSamplerContract extends BaseContract {
},
};
}
/**
* Sample sell quotes from Curve.
* @param curveAddress Address of the Curve contract.
* @param fromTokenIdx Index of the taker token (what to sell).
* @param toTokenIdx Index of the maker token (what to buy).
* @param takerTokenAmounts Taker token sell amount for each sample.
* @returns makerTokenAmounts Maker amounts bought at each taker token amount.
*/
public sampleSellsFromCurve(
curveAddress: string,
fromTokenIdx: BigNumber,
toTokenIdx: BigNumber,
takerTokenAmounts: BigNumber[],
): ContractFunctionObj<BigNumber[]> {
const self = (this as any) as IERC20BridgeSamplerContract;
assert.isString('curveAddress', curveAddress);
assert.isBigNumber('fromTokenIdx', fromTokenIdx);
assert.isBigNumber('toTokenIdx', toTokenIdx);
assert.isArray('takerTokenAmounts', takerTokenAmounts);
const functionSignature = 'sampleSellsFromCurve(address,int128,int128,uint256[])';
return {
async callAsync(callData: Partial<CallData> = {}, defaultBlock?: BlockParam): Promise<BigNumber[]> {
BaseContract._assertCallParams(callData, defaultBlock);
const rawCallResult = await self._performCallAsync(
{ ...callData, data: this.getABIEncodedTransactionData() },
defaultBlock,
);
const abiEncoder = self._lookupAbiEncoder(functionSignature);
BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder);
return abiEncoder.strictDecodeReturnValue<BigNumber[]>(rawCallResult);
},
getABIEncodedTransactionData(): string {
return self._strictEncodeArguments(functionSignature, [
curveAddress.toLowerCase(),
fromTokenIdx,
toTokenIdx,
takerTokenAmounts,
]);
},
};
}
/**
* Sample sell quotes from Eth2Dai/Oasis.
* @param takerToken Address of the taker token (what to sell).

View File

@ -1,4 +1,13 @@
[
{
"version": "6.2.0",
"changes": [
{
"note": "Added `CurveBridge` address (null)",
"pr": 2483
}
]
},
{
"version": "6.1.0",
"changes": [

View File

@ -302,6 +302,7 @@ export async function runMigrationsAsync(
erc20BridgeSampler: constants.NULL_ADDRESS,
chaiBridge: constants.NULL_ADDRESS,
dydxBridge: constants.NULL_ADDRESS,
curveBridge: constants.NULL_ADDRESS,
};
return contractAddresses;
}