diff --git a/contracts/asset-proxy/contracts/src/bridges/CurveBridge.sol b/contracts/asset-proxy/contracts/src/bridges/CurveBridge.sol index 42de35078c..9bbe57f9b1 100644 --- a/contracts/asset-proxy/contracts/src/bridges/CurveBridge.sol +++ b/contracts/asset-proxy/contracts/src/bridges/CurveBridge.sol @@ -37,9 +37,9 @@ contract CurveBridge is { /// @dev Callback for `ICurve`. Tries to buy `amount` of /// `toTokenAddress` tokens by selling the entirety of the opposing asset - /// (DAI or USDC) to the Curve contract, then transfers the bought + /// (DAI, USDC) to the Curve contract, then transfers the bought /// tokens to `to`. - /// @param toTokenAddress The token to give to `to` (either DAI or USDC). + /// @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. @@ -55,24 +55,37 @@ contract CurveBridge is returns (bytes4 success) { // Decode the bridge data to get the Curve metadata. - (address curveAddress, int128 fromCoinIdx, int128 toCoinIdx) = abi.decode(bridgeData, (address, int128, int128)); + (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. - exchange.exchange_underlying( - fromCoinIdx, - toCoinIdx, - // dx - IERC20Token(fromTokenAddress).balanceOf(address(this)), - // min dy - amount, - // expires - block.timestamp + 1 - ); + 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); diff --git a/contracts/asset-proxy/contracts/src/interfaces/ICurve.sol b/contracts/asset-proxy/contracts/src/interfaces/ICurve.sol index db7d147b59..ac2645106b 100644 --- a/contracts/asset-proxy/contracts/src/interfaces/ICurve.sol +++ b/contracts/asset-proxy/contracts/src/interfaces/ICurve.sol @@ -23,6 +23,7 @@ pragma solidity ^0.5.9; 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. @@ -37,6 +38,20 @@ interface ICurve { ) 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. @@ -49,6 +64,19 @@ interface ICurve { 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( diff --git a/contracts/erc20-bridge-sampler/CHANGELOG.json b/contracts/erc20-bridge-sampler/CHANGELOG.json index 619e31bd8b..f89235193a 100644 --- a/contracts/erc20-bridge-sampler/CHANGELOG.json +++ b/contracts/erc20-bridge-sampler/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "1.4.0", + "changes": [ + { + "note": "Added Curve contract sampling", + "pr": 2483 + } + ] + }, { "version": "1.3.0", "changes": [ diff --git a/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol b/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol index 357ac9d8e9..9ce4e748eb 100644 --- a/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol +++ b/contracts/erc20-bridge-sampler/contracts/src/ERC20BridgeSampler.sol @@ -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. diff --git a/contracts/erc20-bridge-sampler/contracts/src/ICurve.sol b/contracts/erc20-bridge-sampler/contracts/src/ICurve.sol new file mode 100644 index 0000000000..ac2645106b --- /dev/null +++ b/contracts/erc20-bridge-sampler/contracts/src/ICurve.sol @@ -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); +} diff --git a/contracts/erc20-bridge-sampler/contracts/src/IERC20BridgeSampler.sol b/contracts/erc20-bridge-sampler/contracts/src/IERC20BridgeSampler.sol index a5b69047d0..5d0435aab2 100644 --- a/contracts/erc20-bridge-sampler/contracts/src/IERC20BridgeSampler.sol +++ b/contracts/erc20-bridge-sampler/contracts/src/IERC20BridgeSampler.sol @@ -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); } diff --git a/contracts/erc20-bridge-sampler/package.json b/contracts/erc20-bridge-sampler/package.json index 9c21529831..a313cc8776 100644 --- a/contracts/erc20-bridge-sampler/package.json +++ b/contracts/erc20-bridge-sampler/package.json @@ -38,7 +38,7 @@ "config": { "publicInterfaceContracts": "ERC20BridgeSampler,IERC20BridgeSampler", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(ERC20BridgeSampler|IDevUtils|IERC20BridgeSampler|IEth2Dai|IKyberNetwork|IUniswapExchangeQuotes|TestERC20BridgeSampler).json" + "abis": "./test/generated-artifacts/@(ERC20BridgeSampler|ICurve|IDevUtils|IERC20BridgeSampler|IEth2Dai|IKyberNetwork|IUniswapExchangeQuotes|TestERC20BridgeSampler).json" }, "repository": { "type": "git", diff --git a/contracts/erc20-bridge-sampler/test/artifacts.ts b/contracts/erc20-bridge-sampler/test/artifacts.ts index d6d708a2b8..d24ebaf458 100644 --- a/contracts/erc20-bridge-sampler/test/artifacts.ts +++ b/contracts/erc20-bridge-sampler/test/artifacts.ts @@ -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, diff --git a/contracts/erc20-bridge-sampler/test/wrappers.ts b/contracts/erc20-bridge-sampler/test/wrappers.ts index 669266fca6..5c72640a1b 100644 --- a/contracts/erc20-bridge-sampler/test/wrappers.ts +++ b/contracts/erc20-bridge-sampler/test/wrappers.ts @@ -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'; diff --git a/contracts/erc20-bridge-sampler/tsconfig.json b/contracts/erc20-bridge-sampler/tsconfig.json index 866692f33f..9f3f4de903 100644 --- a/contracts/erc20-bridge-sampler/tsconfig.json +++ b/contracts/erc20-bridge-sampler/tsconfig.json @@ -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", diff --git a/contracts/integrations/CHANGELOG.json b/contracts/integrations/CHANGELOG.json index 728bd9feff..af71107982 100644 --- a/contracts/integrations/CHANGELOG.json +++ b/contracts/integrations/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Added ChainlinkStopLimit contract and tests", "pr": 2473 + }, + { + "note": "Added ERC20Sampler and Curve Mainnet test", + "pr": 2483 } ] }, diff --git a/contracts/integrations/package.json b/contracts/integrations/package.json index 681ee4ee0b..d8f3dd683f 100644 --- a/contracts/integrations/package.json +++ b/contracts/integrations/package.json @@ -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", diff --git a/contracts/integrations/test/bridge_sampler/bridge_sampler_mainnet_test.ts b/contracts/integrations/test/bridge_sampler/bridge_sampler_mainnet_test.ts new file mode 100644 index 0000000000..30fa7d1add --- /dev/null +++ b/contracts/integrations/test/bridge_sampler/bridge_sampler_mainnet_test.ts @@ -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.skip('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); + }); + }); +}); diff --git a/contracts/integrations/test/bridges/curve_bridge_mainnet_test.ts b/contracts/integrations/test/bridges/curve_bridge_mainnet_test.ts index ecf2ecf450..e93988b76b 100644 --- a/contracts/integrations/test/bridges/curve_bridge_mainnet_test.ts +++ b/contracts/integrations/test/bridges/curve_bridge_mainnet_test.ts @@ -11,13 +11,15 @@ blockchainTests.fork.skip('Mainnet curve bridge tests', env => { const usdcAddress = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; const daiWallet = '0x6cc5f688a315f3dc28a7781717a9a798a59fda7b'; const daiAddress = '0x6B175474E89094C44Da98b954EedeAC495271d0F'; - const curveAddress = '0x2e60CF74d81ac34eB21eEff58Db4D385920ef419'; + 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( @@ -29,29 +31,81 @@ blockchainTests.fork.skip('Mainnet curve bridge tests', env => { }); describe('bridgeTransferFrom()', () => { - it('succeeds exchanges DAI for USDC', async () => { - const bridgeData = bridgeDataEncoder.encode([curveAddress, daiTokenIdx, usdcTokenIdx]); - // 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 }); + 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 }); + }); }); - it('succeeds exchanges USDC for DAI', async () => { - const bridgeData = bridgeDataEncoder.encode([curveAddress, usdcTokenIdx, daiTokenIdx]); - // 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 }); + }); }); }); }); diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index 6e578cbcce..89d25cc583 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Use `batchCall()` version of the `ERC20BridgeSampler` contract", "pr": 2477 + }, + { + "note": "Support for sampling Curve contracts", + "pr": 2483 } ] }, diff --git a/packages/asset-swapper/src/constants.ts b/packages/asset-swapper/src/constants.ts index 6ab389a25c..c14d29ca33 100644 --- a/packages/asset-swapper/src/constants.ts +++ b/packages/asset-swapper/src/constants.ts @@ -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, }; diff --git a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts index 485e2c89d7..2f63743b6b 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts @@ -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. diff --git a/packages/asset-swapper/src/utils/market_operation_utils/create_order.ts b/packages/asset-swapper/src/utils/market_operation_utils/create_order.ts index bf64552106..aea5e7497e 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/create_order.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/create_order.ts @@ -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 diff --git a/packages/asset-swapper/src/utils/market_operation_utils/sampler.ts b/packages/asset-swapper/src/utils/market_operation_utils/sampler.ts index e7a687c55b..e54c9109ad 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/sampler.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/sampler.ts @@ -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 { + return { + encodeCall: contract => { + return contract + .sampleSellsFromCurve( + curveAddress, + new BigNumber(fromTokenIdx), + new BigNumber(toTokenIdx), + takerFillAmounts, + ) + .getABIEncodedTransactionData(); + }, + handleCallResultsAsync: async (contract, callResults) => { + return contract.getABIDecodedReturnData('sampleSellsFromCurve', callResults); + }, + }; + }, getUniswapBuyQuotes( makerToken: string, takerToken: string, @@ -127,30 +151,55 @@ const samplerOperations = { takerToken: string, takerFillAmounts: BigNumber[], ): BatchedOperation { - 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; + 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('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], })); diff --git a/packages/asset-swapper/src/utils/market_operation_utils/types.ts b/packages/asset-swapper/src/utils/market_operation_utils/types.ts index 0629579c74..d9136f85ce 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -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. diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index c591e482de..942a9d4edf 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -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, diff --git a/packages/contract-addresses/CHANGELOG.json b/packages/contract-addresses/CHANGELOG.json index f8d7c5b6df..4c8dd56a06 100644 --- a/packages/contract-addresses/CHANGELOG.json +++ b/packages/contract-addresses/CHANGELOG.json @@ -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 } ] }, diff --git a/packages/contract-addresses/addresses.json b/packages/contract-addresses/addresses.json index 6ba45911a7..a5df48e9f3 100644 --- a/packages/contract-addresses/addresses.json +++ b/packages/contract-addresses/addresses.json @@ -20,14 +20,15 @@ "devUtils": "0x161793cdca4ff9e766a706c2c49c36ac1340bbcd", "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" } } diff --git a/packages/contract-addresses/src/index.ts b/packages/contract-addresses/src/index.ts index 56d231d8b0..85ea64fddd 100644 --- a/packages/contract-addresses/src/index.ts +++ b/packages/contract-addresses/src/index.ts @@ -28,6 +28,7 @@ export interface ContractAddresses { kyberBridge: string; chaiBridge: string; dydxBridge: string; + curveBridge: string; } export enum ChainId { diff --git a/packages/contract-artifacts/artifacts/IERC20BridgeSampler.json b/packages/contract-artifacts/artifacts/IERC20BridgeSampler.json index b97789a71d..bcf5c1fd6a 100644 --- a/packages/contract-artifacts/artifacts/IERC20BridgeSampler.json +++ b/packages/contract-artifacts/artifacts/IERC20BridgeSampler.json @@ -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": { diff --git a/packages/contract-wrappers/src/generated-wrappers/i_erc20_bridge_sampler.ts b/packages/contract-wrappers/src/generated-wrappers/i_erc20_bridge_sampler.ts index 25ae1a6f86..2b68743e82 100644 --- a/packages/contract-wrappers/src/generated-wrappers/i_erc20_bridge_sampler.ts +++ b/packages/contract-wrappers/src/generated-wrappers/i_erc20_bridge_sampler.ts @@ -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 { + 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 = {}, defaultBlock?: BlockParam): Promise { + 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(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). diff --git a/packages/migrations/CHANGELOG.json b/packages/migrations/CHANGELOG.json index bc57f6c569..84f4ece1a7 100644 --- a/packages/migrations/CHANGELOG.json +++ b/packages/migrations/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "6.2.0", + "changes": [ + { + "note": "Added `CurveBridge` address (null)", + "pr": 2483 + } + ] + }, { "version": "6.1.0", "changes": [ diff --git a/packages/migrations/src/migration.ts b/packages/migrations/src/migration.ts index 6a8044c9c0..c6655b99d9 100644 --- a/packages/migrations/src/migration.ts +++ b/packages/migrations/src/migration.ts @@ -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; }