diff --git a/contracts/zero-ex/CHANGELOG.json b/contracts/zero-ex/CHANGELOG.json index fd3e8abbf2..1e7c61b401 100644 --- a/contracts/zero-ex/CHANGELOG.json +++ b/contracts/zero-ex/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Added ETH support to `MixinCurve`", "pr": 220 + }, + { + "note": "Add Balancer V2 integration", + "pr": 206 } ] }, diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/BridgeAdapter.sol b/contracts/zero-ex/contracts/src/transformers/bridges/BridgeAdapter.sol index af2d446115..beaca4db70 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/BridgeAdapter.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/BridgeAdapter.sol @@ -23,6 +23,7 @@ pragma experimental ABIEncoderV2; import "./IBridgeAdapter.sol"; import "./BridgeProtocols.sol"; import "./mixins/MixinBalancer.sol"; +import "./mixins/MixinBalancerV2.sol"; import "./mixins/MixinBancor.sol"; import "./mixins/MixinCoFiX.sol"; import "./mixins/MixinCurve.sol"; @@ -43,6 +44,7 @@ import "./mixins/MixinZeroExBridge.sol"; contract BridgeAdapter is IBridgeAdapter, MixinBalancer, + MixinBalancerV2, MixinBancor, MixinCoFiX, MixinCurve, @@ -63,6 +65,7 @@ contract BridgeAdapter is constructor(IEtherTokenV06 weth) public MixinBalancer() + MixinBalancerV2() MixinBancor(weth) MixinCoFiX() MixinCurve(weth) @@ -119,6 +122,13 @@ contract BridgeAdapter is sellAmount, order.bridgeData ); + } else if (protocolId == BridgeProtocols.BALANCERV2) { + boughtAmount = _tradeBalancerV2( + sellToken, + buyToken, + sellAmount, + order.bridgeData + ); } else if (protocolId == BridgeProtocols.KYBER) { boughtAmount = _tradeKyber( sellToken, diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/BridgeProtocols.sol b/contracts/zero-ex/contracts/src/transformers/bridges/BridgeProtocols.sol index 60630fa7cb..734d1c87de 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/BridgeProtocols.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/BridgeProtocols.sol @@ -44,4 +44,5 @@ library BridgeProtocols { uint128 internal constant COFIX = 14; uint128 internal constant NERVE = 15; uint128 internal constant MAKERPSM = 16; + uint128 internal constant BALANCERV2 = 17; } diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinBalancerV2.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinBalancerV2.sol new file mode 100644 index 0000000000..599d3c0a7c --- /dev/null +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinBalancerV2.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + + Copyright 2020 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.6.5; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; + + +interface IBalancerV2Vault { + + enum SwapKind { GIVEN_IN, GIVEN_OUT } + /** + * @dev Performs a swap with a single Pool. + * + * If the swap is given in (the number of tokens to send to the Pool is known), returns the amount of tokens + * taken from the Pool, which must be greater than or equal to `limit`. + * + * If the swap is given out (the number of tokens to take from the Pool is known), returns the amount of + * tokens sent to the Pool, which must be less than or equal to `limit`. + * + * Internal Balance usage and the recipient are determined by the `funds` struct. + * + * Emits a `Swap` event. + * For full documentation see https://github.com/balancer-labs/balancer-core-v2/blob/master/contracts/vault/interfaces/IVault.sol + */ + function swap( + SingleSwap calldata request, + FundManagement calldata funds, + uint256 limit, + uint256 deadline + ) external payable returns (uint256); + + struct SingleSwap { + bytes32 poolId; + SwapKind kind; + IERC20TokenV06 assetIn; + IERC20TokenV06 assetOut; + uint256 amount; + bytes userData; + } + + struct FundManagement { + address sender; + bool fromInternalBalance; + address payable recipient; + bool toInternalBalance; + } +} + +contract MixinBalancerV2 { + + using LibERC20TokenV06 for IERC20TokenV06; + + struct BalancerV2BridgeData { + IBalancerV2Vault vault; + bytes32 poolId; + } + + function _tradeBalancerV2( + IERC20TokenV06 sellToken, + IERC20TokenV06 buyToken, + uint256 sellAmount, + bytes memory bridgeData + ) + internal + returns (uint256 boughtAmount) + { + // Decode the bridge data. + BalancerV2BridgeData memory data = abi.decode(bridgeData, (BalancerV2BridgeData)); + + // Grant an allowance to the exchange to spend `fromTokenAddress` token. + sellToken.approveIfBelow(address(data.vault), sellAmount); + + // Sell the entire sellAmount + IBalancerV2Vault.SingleSwap memory request = IBalancerV2Vault.SingleSwap({ + poolId: data.poolId, + kind: IBalancerV2Vault.SwapKind.GIVEN_IN, + assetIn: sellToken, + assetOut: buyToken, + amount: sellAmount, // amount in + userData: "" + }); + + IBalancerV2Vault.FundManagement memory funds = IBalancerV2Vault.FundManagement({ + sender: address(this), + fromInternalBalance: false, + recipient: payable(address(this)), + toInternalBalance: false + }); + + boughtAmount = data.vault.swap( + request, + funds, + 1, // min amount out + block.timestamp // expires after this block + ); + return boughtAmount; + } +} diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index 113182e268..a3f59f85e6 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -43,7 +43,7 @@ "config": { "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,PositiveSlippageFeeTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,AffiliateFeeTransformer,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature,ILiquidityProviderFeature,NativeOrdersFeature,INativeOrdersFeature,FeeCollectorController,FeeCollector,CurveLiquidityProvider,BatchFillNativeOrdersFeature,IBatchFillNativeOrdersFeature,MultiplexFeature,IMultiplexFeature", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|BatchFillNativeOrdersFeature|BootstrapFeature|BridgeAdapter|BridgeProtocols|CurveLiquidityProvider|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IBatchFillNativeOrdersFeature|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|IMooniswapPool|IMultiplexFeature|INativeOrdersEvents|INativeOrdersFeature|IOwnableFeature|IPancakeSwapFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IUniswapV2Pair|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBancor|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinDodo|MixinDodoV2|MixinKyber|MixinMStable|MixinMakerPSM|MixinMooniswap|MixinNerve|MixinOasis|MixinShell|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|MooniswapLiquidityProvider|MultiplexFeature|NativeOrdersCancellation|NativeOrdersFeature|NativeOrdersInfo|NativeOrdersProtocolFees|NativeOrdersSettlement|OwnableFeature|PancakeSwapFeature|PayTakerTransformer|PermissionlessTransformerDeployer|PositiveSlippageFeeTransformer|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestCurve|TestDelegateCaller|TestFeeCollectorController|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestMooniswap|TestNativeOrdersFeature|TestOrderSignerRegistryWithContractWallet|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx|ZeroExOptimized).json" + "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|BatchFillNativeOrdersFeature|BootstrapFeature|BridgeAdapter|BridgeProtocols|CurveLiquidityProvider|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IBatchFillNativeOrdersFeature|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|IMooniswapPool|IMultiplexFeature|INativeOrdersEvents|INativeOrdersFeature|IOwnableFeature|IPancakeSwapFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IUniswapV2Pair|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBalancerV2|MixinBancor|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinDodo|MixinDodoV2|MixinKyber|MixinMStable|MixinMakerPSM|MixinMooniswap|MixinNerve|MixinOasis|MixinShell|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|MooniswapLiquidityProvider|MultiplexFeature|NativeOrdersCancellation|NativeOrdersFeature|NativeOrdersInfo|NativeOrdersProtocolFees|NativeOrdersSettlement|OwnableFeature|PancakeSwapFeature|PayTakerTransformer|PermissionlessTransformerDeployer|PositiveSlippageFeeTransformer|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestCurve|TestDelegateCaller|TestFeeCollectorController|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestMooniswap|TestNativeOrdersFeature|TestOrderSignerRegistryWithContractWallet|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx|ZeroExOptimized).json" }, "repository": { "type": "git", diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index a703fecefa..46d8c24e15 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -76,6 +76,7 @@ import * as LiquidityProviderSandbox from '../test/generated-artifacts/Liquidity import * as LogMetadataTransformer from '../test/generated-artifacts/LogMetadataTransformer.json'; import * as MetaTransactionsFeature from '../test/generated-artifacts/MetaTransactionsFeature.json'; import * as MixinBalancer from '../test/generated-artifacts/MixinBalancer.json'; +import * as MixinBalancerV2 from '../test/generated-artifacts/MixinBalancerV2.json'; import * as MixinBancor from '../test/generated-artifacts/MixinBancor.json'; import * as MixinCoFiX from '../test/generated-artifacts/MixinCoFiX.json'; import * as MixinCryptoCom from '../test/generated-artifacts/MixinCryptoCom.json'; @@ -235,6 +236,7 @@ export const artifacts = { BridgeProtocols: BridgeProtocols as ContractArtifact, IBridgeAdapter: IBridgeAdapter as ContractArtifact, MixinBalancer: MixinBalancer as ContractArtifact, + MixinBalancerV2: MixinBalancerV2 as ContractArtifact, MixinBancor: MixinBancor as ContractArtifact, MixinCoFiX: MixinCoFiX as ContractArtifact, MixinCryptoCom: MixinCryptoCom as ContractArtifact, diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 8f24a81317..fd41960e6f 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -74,6 +74,7 @@ export * from '../test/generated-wrappers/liquidity_provider_sandbox'; export * from '../test/generated-wrappers/log_metadata_transformer'; export * from '../test/generated-wrappers/meta_transactions_feature'; export * from '../test/generated-wrappers/mixin_balancer'; +export * from '../test/generated-wrappers/mixin_balancer_v2'; export * from '../test/generated-wrappers/mixin_bancor'; export * from '../test/generated-wrappers/mixin_co_fi_x'; export * from '../test/generated-wrappers/mixin_crypto_com'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index 6f03092d4a..28cbba6dac 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -105,6 +105,7 @@ "test/generated-artifacts/LogMetadataTransformer.json", "test/generated-artifacts/MetaTransactionsFeature.json", "test/generated-artifacts/MixinBalancer.json", + "test/generated-artifacts/MixinBalancerV2.json", "test/generated-artifacts/MixinBancor.json", "test/generated-artifacts/MixinCoFiX.json", "test/generated-artifacts/MixinCryptoCom.json", diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index 14a93635b4..05bb71e461 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -29,6 +29,10 @@ { "note": "PLP now includes a fallback due to observed collisions", "pr": 223 + }, + { + "note": "Add Balancer V2 integration", + "pr": 206 } ] }, diff --git a/packages/asset-swapper/compiler.json b/packages/asset-swapper/compiler.json index c15d3cd1c6..0f1c50dfbb 100644 --- a/packages/asset-swapper/compiler.json +++ b/packages/asset-swapper/compiler.json @@ -6,11 +6,7 @@ "shouldSaveStandardInput": true, "compilerSettings": { "evmVersion": "istanbul", - "optimizer": { - "enabled": true, - "runs": 62500, - "details": { "yul": true, "deduplicate": true, "cse": true, "constantOptimizer": true } - }, + "optimizer": { "enabled": true, "runs": 200 }, "outputSelection": { "*": { "*": [ diff --git a/packages/asset-swapper/contracts/src/BalancerV2Sampler.sol b/packages/asset-swapper/contracts/src/BalancerV2Sampler.sol new file mode 100644 index 0000000000..ae01045c00 --- /dev/null +++ b/packages/asset-swapper/contracts/src/BalancerV2Sampler.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + + Copyright 2021 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.6; +pragma experimental ABIEncoderV2; + +import "./SamplerUtils.sol"; + +/// @dev Minimal Balancer V2 Vault interface +/// for documentation refer to https://github.com/balancer-labs/balancer-core-v2/blob/master/contracts/vault/interfaces/IVault.sol +interface IBalancerV2Vault { + enum SwapKind { GIVEN_IN, GIVEN_OUT } + + struct BatchSwapStep { + bytes32 poolId; + uint256 assetInIndex; + uint256 assetOutIndex; + uint256 amount; + bytes userData; + } + + struct FundManagement { + address sender; + bool fromInternalBalance; + address payable recipient; + bool toInternalBalance; + } + + function queryBatchSwap( + SwapKind kind, + BatchSwapStep[] calldata swaps, + IAsset[] calldata assets, + FundManagement calldata funds + ) external returns (int256[] memory assetDeltas); +} +interface IAsset { + // solhint-disable-previous-line no-empty-blocks +} + +contract BalancerV2Sampler is SamplerUtils { + + struct BalancerV2PoolInfo { + bytes32 poolId; + address vault; + } + + /// @dev Sample sell quotes from Balancer V2. + /// @param poolInfo Struct with pool related data + /// @param takerToken Address of the taker token (what to sell). + /// @param makerToken Address 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 sampleSellsFromBalancerV2( + BalancerV2PoolInfo memory poolInfo, + address takerToken, + address makerToken, + uint256[] memory takerTokenAmounts + ) + public + returns (uint256[] memory makerTokenAmounts) + { + _assertValidPair(makerToken, takerToken); + IBalancerV2Vault vault = IBalancerV2Vault(poolInfo.vault); + IAsset[] memory swapAssets = new IAsset[](2); + swapAssets[0] = IAsset(takerToken); + swapAssets[1] = IAsset(makerToken); + + uint256 numSamples = takerTokenAmounts.length; + makerTokenAmounts = new uint256[](numSamples); + IBalancerV2Vault.FundManagement memory swapFunds = + _createSwapFunds(); + + for (uint256 i = 0; i < numSamples; i++) { + IBalancerV2Vault.BatchSwapStep[] memory swapSteps = + _createSwapSteps(poolInfo, takerTokenAmounts[i]); + + try + // For sells we specify the takerToken which is what the vault will receive from the trade + vault.queryBatchSwap(IBalancerV2Vault.SwapKind.GIVEN_IN, swapSteps, swapAssets, swapFunds) + // amounts represent pool balance deltas from the swap (incoming balance, outgoing balance) + returns (int256[] memory amounts) { + // Outgoing balance is negative so we need to flip the sign + int256 amountOutFromPool = amounts[1] * -1; + if (amountOutFromPool <= 0) { + break; + } + makerTokenAmounts[i] = uint256(amountOutFromPool); + } catch (bytes memory) { + // Swallow failures, leaving all results as zero. + break; + } + } + } + + /// @dev Sample buy quotes from Balancer V2. + /// @param poolInfo Struct with pool related data + /// @param takerToken Address of the taker token (what to sell). + /// @param makerToken Address of the maker token (what to buy). + /// @param makerTokenAmounts Maker token buy amount for each sample. + /// @return takerTokenAmounts Taker amounts sold at each maker token + /// amount. + function sampleBuysFromBalancerV2( + BalancerV2PoolInfo memory poolInfo, + address takerToken, + address makerToken, + uint256[] memory makerTokenAmounts + ) + public + returns (uint256[] memory takerTokenAmounts) + { + _assertValidPair(makerToken, takerToken); + IBalancerV2Vault vault = IBalancerV2Vault(poolInfo.vault); + IAsset[] memory swapAssets = new IAsset[](2); + swapAssets[0] = IAsset(takerToken); + swapAssets[1] = IAsset(makerToken); + + uint256 numSamples = makerTokenAmounts.length; + takerTokenAmounts = new uint256[](numSamples); + IBalancerV2Vault.FundManagement memory swapFunds = + _createSwapFunds(); + + for (uint256 i = 0; i < numSamples; i++) { + IBalancerV2Vault.BatchSwapStep[] memory swapSteps = + _createSwapSteps(poolInfo, makerTokenAmounts[i]); + + try + // For buys we specify the makerToken which is what taker will receive from the trade + vault.queryBatchSwap(IBalancerV2Vault.SwapKind.GIVEN_OUT, swapSteps, swapAssets, swapFunds) + returns (int256[] memory amounts) { + int256 amountIntoPool = amounts[0]; + if (amountIntoPool <= 0) { + break; + } + takerTokenAmounts[i] = uint256(amountIntoPool); + } catch (bytes memory) { + // Swallow failures, leaving all results as zero. + break; + } + } + } + + function _createSwapSteps( + BalancerV2PoolInfo memory poolInfo, + uint256 amount + ) private pure returns (IBalancerV2Vault.BatchSwapStep[] memory) { + IBalancerV2Vault.BatchSwapStep[] memory swapSteps = + new IBalancerV2Vault.BatchSwapStep[](1); + swapSteps[0] = IBalancerV2Vault.BatchSwapStep({ + poolId: poolInfo.poolId, + assetInIndex: 0, + assetOutIndex: 1, + amount: amount, + userData: "" + }); + + return swapSteps; + } + + function _createSwapFunds() + private + view + returns (IBalancerV2Vault.FundManagement memory) + { + return + IBalancerV2Vault.FundManagement({ + sender: address(this), + fromInternalBalance: false, + recipient: payable(address(this)), + toInternalBalance: false + }); + } +} diff --git a/packages/asset-swapper/contracts/src/BancorSampler.sol b/packages/asset-swapper/contracts/src/BancorSampler.sol index 3ef40cbe8e..e69c692364 100644 --- a/packages/asset-swapper/contracts/src/BancorSampler.sol +++ b/packages/asset-swapper/contracts/src/BancorSampler.sol @@ -22,16 +22,13 @@ pragma experimental ABIEncoderV2; import "./interfaces/IBancor.sol"; -contract DeploymentConstants {} - -contract BancorSampler is DeploymentConstants -{ +contract BancorSampler { /// @dev Base gas limit for Bancor calls. uint256 constant private BANCOR_CALL_GAS = 300e3; // 300k struct BancorSamplerOpts { - address registry; + IBancorRegistry registry; address[][] paths; } @@ -112,7 +109,7 @@ contract BancorSampler is DeploymentConstants view returns (address bancorNetwork, address[] memory path) { - bancorNetwork = _getBancorNetwork(opts.registry); + bancorNetwork = opts.registry.getAddress(opts.registry.BANCOR_NETWORK()); if (opts.paths.length == 0) { return (bancorNetwork, path); } @@ -140,13 +137,4 @@ contract BancorSampler is DeploymentConstants } } } - - function _getBancorNetwork(address registry) - private - view - returns (address) - { - IBancorRegistry registry = IBancorRegistry(registry); - return registry.getAddress(registry.BANCOR_NETWORK()); - } } diff --git a/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol b/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol index fc15071357..5383f4abd5 100644 --- a/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol +++ b/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol @@ -21,6 +21,7 @@ pragma solidity ^0.6; pragma experimental ABIEncoderV2; import "./BalancerSampler.sol"; +import "./BalancerV2Sampler.sol"; import "./BancorSampler.sol"; import "./CurveSampler.sol"; import "./DODOSampler.sol"; @@ -43,6 +44,7 @@ import "./UtilitySampler.sol"; contract ERC20BridgeSampler is BalancerSampler, + BalancerV2Sampler, BancorSampler, CurveSampler, DODOSampler, @@ -73,7 +75,6 @@ contract ERC20BridgeSampler is /// @return callResults ABI-encoded results data for each call. function batchCall(bytes[] calldata callDatas) external - view returns (CallResults[] memory callResults) { callResults = new CallResults[](callDatas.length); @@ -82,7 +83,7 @@ contract ERC20BridgeSampler is if (callDatas[i].length == 0) { continue; } - (callResults[i].success, callResults[i].data) = address(this).staticcall(callDatas[i]); + (callResults[i].success, callResults[i].data) = address(this).call(callDatas[i]); } } } diff --git a/packages/asset-swapper/package.json b/packages/asset-swapper/package.json index db749d7565..02e6469719 100644 --- a/packages/asset-swapper/package.json +++ b/packages/asset-swapper/package.json @@ -38,7 +38,7 @@ "config": { "publicInterfaceContracts": "ERC20BridgeSampler,BalanceChecker,FakeTaker", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(ApproximateBuys|BalanceChecker|BalancerSampler|BancorSampler|CurveSampler|DODOSampler|DODOV2Sampler|DummyLiquidityProvider|ERC20BridgeSampler|Eth2DaiSampler|FakeTaker|IBalancer|IBancor|ICurve|IEth2Dai|IKyberNetwork|IMStable|IMooniswap|IMultiBridge|IShell|ISmoothy|IUniswapExchangeQuotes|IUniswapV2Router01|KyberSampler|LiquidityProviderSampler|MStableSampler|MakerPSMSampler|MooniswapSampler|MultiBridgeSampler|NativeOrderSampler|SamplerUtils|ShellSampler|SmoothySampler|TestERC20BridgeSampler|TestNativeOrderSampler|TwoHopSampler|UniswapSampler|UniswapV2Sampler|UtilitySampler).json", + "abis": "./test/generated-artifacts/@(ApproximateBuys|BalanceChecker|BalancerSampler|BalancerV2Sampler|BancorSampler|CurveSampler|DODOSampler|DODOV2Sampler|DummyLiquidityProvider|ERC20BridgeSampler|Eth2DaiSampler|FakeTaker|IBalancer|IBancor|ICurve|IEth2Dai|IKyberNetwork|IMStable|IMooniswap|IMultiBridge|IShell|ISmoothy|IUniswapExchangeQuotes|IUniswapV2Router01|KyberSampler|LiquidityProviderSampler|MStableSampler|MakerPSMSampler|MooniswapSampler|MultiBridgeSampler|NativeOrderSampler|SamplerUtils|ShellSampler|SmoothySampler|TestERC20BridgeSampler|TestNativeOrderSampler|TwoHopSampler|UniswapSampler|UniswapV2Sampler|UtilitySampler).json", "postpublish": { "assets": [] } @@ -85,6 +85,8 @@ "ethereum-types": "^3.5.0", "ethereumjs-util": "^7.0.10", "fast-abi": "^0.0.2", + "graphql": "^15.4.0", + "graphql-request": "^3.4.0", "heartbeats": "^5.0.1", "lodash": "^4.17.11" }, diff --git a/packages/asset-swapper/src/swap_quoter.ts b/packages/asset-swapper/src/swap_quoter.ts index aa54557b95..3b8ca427a5 100644 --- a/packages/asset-swapper/src/swap_quoter.ts +++ b/packages/asset-swapper/src/swap_quoter.ts @@ -143,8 +143,7 @@ export class SwapQuoter { this.chainId, samplerContract, samplerOverrides, - undefined, // balancer pool cache - undefined, // cream pool cache + undefined, // pools caches for balancer and cream tokenAdjacencyGraph, liquidityProviderRegistry, this.chainId === ChainId.Mainnet // Enable Bancor only on Mainnet 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 8dfe19bf4b..3735b6e65e 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts @@ -67,6 +67,7 @@ export const SELL_SOURCE_FILTER_BY_CHAIN_ID = valueByChainId( ERC20BridgeSource.Kyber, ERC20BridgeSource.Curve, ERC20BridgeSource.Balancer, + ERC20BridgeSource.BalancerV2, ERC20BridgeSource.Bancor, ERC20BridgeSource.MStable, ERC20BridgeSource.Mooniswap, @@ -134,6 +135,7 @@ export const BUY_SOURCE_FILTER_BY_CHAIN_ID = valueByChainId( ERC20BridgeSource.Kyber, ERC20BridgeSource.Curve, ERC20BridgeSource.Balancer, + ERC20BridgeSource.BalancerV2, // ERC20BridgeSource.Bancor, // FIXME: Bancor Buys not implemented in Sampler ERC20BridgeSource.MStable, ERC20BridgeSource.Mooniswap, @@ -1037,9 +1039,17 @@ export const COMPONENT_POOLS_BY_CHAIN_ID = valueByChainId( }, ); +export const BALANCER_V2_VAULT_ADDRESS_BY_CHAIN = valueByChainId( + { + [ChainId.Mainnet]: '0xba12222222228d8ba445958a75a0704d566bf2c8', + }, + NULL_ADDRESS, +); + export const BALANCER_SUBGRAPH_URL = 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer'; export const BALANCER_TOP_POOLS_FETCHED = 250; export const BALANCER_MAX_POOLS_FETCHED = 3; +export const BALANCER_V2_SUBGRAPH_URL = 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-v2'; // // BSC @@ -1163,17 +1173,12 @@ export const DEFAULT_GAS_SCHEDULE: Required = { return gas; }, [ERC20BridgeSource.Balancer]: () => 120e3, + [ERC20BridgeSource.BalancerV2]: () => 100e3, [ERC20BridgeSource.Cream]: () => 120e3, [ERC20BridgeSource.MStable]: () => 700e3, [ERC20BridgeSource.MakerPsm]: (fillData?: FillData) => { const psmFillData = fillData as MakerPsmFillData; - - // TODO(kimpers): update with more accurate numbers after allowances have been set - if (psmFillData.takerToken === psmFillData.gemTokenAddress) { - return psmFillData.isSellOperation ? 389e3 : 423e3; - } else { - return 444e3; - } + return psmFillData.takerToken === psmFillData.gemTokenAddress ? 210e3 : 290e3; }, [ERC20BridgeSource.Mooniswap]: () => 130e3, [ERC20BridgeSource.Shell]: () => 170e3, diff --git a/packages/asset-swapper/src/utils/market_operation_utils/cream_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/cream_utils.ts deleted file mode 100644 index 656d2a96c3..0000000000 --- a/packages/asset-swapper/src/utils/market_operation_utils/cream_utils.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Pool } from '@balancer-labs/sor/dist/types'; -import { getPoolsWithTokens, parsePoolData } from 'cream-sor'; - -import { BALANCER_MAX_POOLS_FETCHED } from './constants'; - -// tslint:disable:boolean-naming - -interface CacheValue { - timestamp: number; - pools: Pool[]; -} - -// tslint:disable:custom-no-magic-numbers -const FIVE_SECONDS_MS = 5 * 1000; -const ONE_DAY_MS = 24 * 60 * 60 * 1000; -const DEFAULT_TIMEOUT_MS = 1000; -// tslint:enable:custom-no-magic-numbers - -export class CreamPoolsCache { - constructor( - private readonly _cache: { [key: string]: CacheValue } = {}, - private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED, - ) {} - - public async getPoolsForPairAsync( - takerToken: string, - makerToken: string, - timeoutMs: number = DEFAULT_TIMEOUT_MS, - ): Promise { - const timeout = new Promise(resolve => setTimeout(resolve, timeoutMs, [])); - return Promise.race([this._getPoolsForPairAsync(takerToken, makerToken), timeout]); - } - - public getCachedPoolAddressesForPair( - takerToken: string, - makerToken: string, - cacheExpiryMs?: number, - ): string[] | undefined { - const key = JSON.stringify([takerToken, makerToken]); - const value = this._cache[key]; - if (cacheExpiryMs === undefined) { - return value === undefined ? [] : value.pools.map(pool => pool.id); - } - const minTimestamp = Date.now() - cacheExpiryMs; - if (value === undefined || value.timestamp < minTimestamp) { - return undefined; - } else { - return value.pools.map(pool => pool.id); - } - } - - public howToSampleCream( - takerToken: string, - makerToken: string, - isAllowedSource: boolean, - ): { onChain: boolean; offChain: boolean } { - // If CREAM is excluded as a source, do not sample. - if (!isAllowedSource) { - return { onChain: false, offChain: false }; - } - const cachedCreamPools = this.getCachedPoolAddressesForPair(takerToken, makerToken, ONE_DAY_MS); - // Sample CREAM on-chain (i.e. via the ERC20BridgeSampler contract) if: - // - Cached values are not stale - // - There is at least one CREAM pool for this pair - const onChain = cachedCreamPools !== undefined && cachedCreamPools.length > 0; - // Sample CREAM off-chain (i.e. via GraphQL query + `computeCreamBuy/SellQuote`) - // if cached values are stale - const offChain = cachedCreamPools === undefined; - return { onChain, offChain }; - } - - protected async _getPoolsForPairAsync( - takerToken: string, - makerToken: string, - cacheExpiryMs: number = FIVE_SECONDS_MS, - ): Promise { - const key = JSON.stringify([takerToken, makerToken]); - const value = this._cache[key]; - const minTimestamp = Date.now() - cacheExpiryMs; - if (value === undefined || value.timestamp < minTimestamp) { - const pools = await this._fetchPoolsForPairAsync(takerToken, makerToken); - const timestamp = Date.now(); - this._cache[key] = { - pools, - timestamp, - }; - } - return this._cache[key].pools; - } - - // tslint:disable-next-line: prefer-function-over-method - protected async _loadTopPoolsAsync(): Promise { - // Do nothing - } - - // tslint:disable-next-line:prefer-function-over-method - protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise { - try { - const poolData = (await getPoolsWithTokens(takerToken, makerToken)).pools; - // Sort by maker token balance (descending) - const pools = parsePoolData(poolData, takerToken, makerToken).sort((a, b) => - b.balanceOut.minus(a.balanceOut).toNumber(), - ); - return pools.length > this.maxPoolsFetched ? pools.slice(0, this.maxPoolsFetched) : pools; - } catch (err) { - return []; - } - } -} diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index 490e4d74a2..50e7f0273d 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -46,6 +46,7 @@ import { OptimizerResult, OptimizerResultWithReport, OrderDomain, + SourcesWithPoolsCache, } from './types'; // tslint:disable:boolean-naming @@ -100,29 +101,15 @@ export class MarketOperationUtils { const quoteSourceFilters = this._sellSources.merge(requestFilters); const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources); - const { - onChain: sampleBalancerOnChain, - offChain: sampleBalancerOffChain, - } = this._sampler.balancerPoolsCache.howToSampleBalancer( - takerToken, - makerToken, - quoteSourceFilters.isAllowed(ERC20BridgeSource.Balancer), + // Can't sample Balancer or Cream on-chain without the pools cache + const sourcesWithStaleCaches: SourcesWithPoolsCache[] = (Object.keys( + this._sampler.poolsCaches, + ) as SourcesWithPoolsCache[]).filter(s => !this._sampler.poolsCaches[s].isFresh(takerToken, makerToken)); + // tslint:disable-next-line:promise-function-async + const cacheRefreshPromises: Array> = sourcesWithStaleCaches.map(s => + this._sampler.poolsCaches[s].getFreshPoolsForPairAsync(takerToken, makerToken), ); - const { - onChain: sampleCreamOnChain, - offChain: sampleCreamOffChain, - } = this._sampler.creamPoolsCache.howToSampleCream( - takerToken, - makerToken, - quoteSourceFilters.isAllowed(ERC20BridgeSource.Cream), - ); - - const offChainSources = [ - ...(!sampleCreamOnChain ? [ERC20BridgeSource.Cream] : []), - ...(!sampleBalancerOnChain ? [ERC20BridgeSource.Balancer] : []), - ]; - // Used to determine whether the tx origin is an EOA or a contract const txOrigin = (_opts.rfqt && _opts.rfqt.txOrigin) || NULL_ADDRESS; @@ -146,12 +133,7 @@ export class MarketOperationUtils { this._nativeFeeTokenAmount, ), // Get sell quotes for taker -> maker. - this._sampler.getSellQuotes( - quoteSourceFilters.exclude(offChainSources).sources, - makerToken, - takerToken, - sampleAmounts, - ), + this._sampler.getSellQuotes(quoteSourceFilters.sources, makerToken, takerToken, sampleAmounts), this._sampler.getTwoHopSellQuotes( quoteSourceFilters.isAllowed(ERC20BridgeSource.MultiHop) ? quoteSourceFilters.sources : [], makerToken, @@ -160,15 +142,6 @@ export class MarketOperationUtils { ), this._sampler.isAddressContract(txOrigin), ); - - const offChainBalancerPromise = sampleBalancerOffChain - ? this._sampler.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, sampleAmounts) - : Promise.resolve([]); - - const offChainCreamPromise = sampleCreamOffChain - ? this._sampler.getCreamSellQuotesOffChainAsync(makerToken, takerToken, sampleAmounts) - : Promise.resolve([]); - const [ [ tokenDecimals, @@ -179,9 +152,7 @@ export class MarketOperationUtils { rawTwoHopQuotes, isTxOriginContract, ], - offChainBalancerQuotes, - offChainCreamQuotes, - ] = await Promise.all([samplerPromise, offChainBalancerPromise, offChainCreamPromise]); + ] = await Promise.all([samplerPromise, Promise.all(cacheRefreshPromises)]); // Filter out any invalid two hop quotes where we couldn't find a route const twoHopQuotes = rawTwoHopQuotes.filter( @@ -210,7 +181,7 @@ export class MarketOperationUtils { nativeOrders: limitOrdersWithFillableAmounts, rfqtIndicativeQuotes: [], twoHopQuotes, - dexQuotes: dexQuotes.concat([...offChainBalancerQuotes, ...offChainCreamQuotes]), + dexQuotes, }, isRfqSupported, }; @@ -236,29 +207,15 @@ export class MarketOperationUtils { const quoteSourceFilters = this._buySources.merge(requestFilters); const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources); - const { - onChain: sampleBalancerOnChain, - offChain: sampleBalancerOffChain, - } = this._sampler.balancerPoolsCache.howToSampleBalancer( - takerToken, - makerToken, - quoteSourceFilters.isAllowed(ERC20BridgeSource.Balancer), + // Can't sample Balancer or Cream on-chain without the pools cache + const sourcesWithStaleCaches: SourcesWithPoolsCache[] = (Object.keys( + this._sampler.poolsCaches, + ) as SourcesWithPoolsCache[]).filter(s => !this._sampler.poolsCaches[s].isFresh(takerToken, makerToken)); + // tslint:disable-next-line:promise-function-async + const cacheRefreshPromises: Array> = sourcesWithStaleCaches.map(s => + this._sampler.poolsCaches[s].getFreshPoolsForPairAsync(takerToken, makerToken), ); - const { - onChain: sampleCreamOnChain, - offChain: sampleCreamOffChain, - } = this._sampler.creamPoolsCache.howToSampleCream( - takerToken, - makerToken, - quoteSourceFilters.isAllowed(ERC20BridgeSource.Cream), - ); - - const offChainSources = [ - ...(!sampleCreamOnChain ? [ERC20BridgeSource.Cream] : []), - ...(!sampleBalancerOnChain ? [ERC20BridgeSource.Balancer] : []), - ]; - // Used to determine whether the tx origin is an EOA or a contract const txOrigin = (_opts.rfqt && _opts.rfqt.txOrigin) || NULL_ADDRESS; @@ -282,12 +239,7 @@ export class MarketOperationUtils { this._nativeFeeTokenAmount, ), // Get buy quotes for taker -> maker. - this._sampler.getBuyQuotes( - quoteSourceFilters.exclude(offChainSources).sources, - makerToken, - takerToken, - sampleAmounts, - ), + this._sampler.getBuyQuotes(quoteSourceFilters.sources, makerToken, takerToken, sampleAmounts), this._sampler.getTwoHopBuyQuotes( quoteSourceFilters.isAllowed(ERC20BridgeSource.MultiHop) ? quoteSourceFilters.sources : [], makerToken, @@ -297,14 +249,6 @@ export class MarketOperationUtils { this._sampler.isAddressContract(txOrigin), ); - const offChainBalancerPromise = sampleBalancerOffChain - ? this._sampler.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, sampleAmounts) - : Promise.resolve([]); - - const offChainCreamPromise = sampleCreamOffChain - ? this._sampler.getCreamBuyQuotesOffChainAsync(makerToken, takerToken, sampleAmounts) - : Promise.resolve([]); - const [ [ tokenDecimals, @@ -315,9 +259,7 @@ export class MarketOperationUtils { rawTwoHopQuotes, isTxOriginContract, ], - offChainBalancerQuotes, - offChainCreamQuotes, - ] = await Promise.all([samplerPromise, offChainBalancerPromise, offChainCreamPromise]); + ] = await Promise.all([samplerPromise, Promise.all(cacheRefreshPromises)]); // Filter out any invalid two hop quotes where we couldn't find a route const twoHopQuotes = rawTwoHopQuotes.filter( @@ -346,7 +288,7 @@ export class MarketOperationUtils { nativeOrders: limitOrdersWithFillableAmounts, rfqtIndicativeQuotes: [], twoHopQuotes, - dexQuotes: dexQuotes.concat(offChainBalancerQuotes, offChainCreamQuotes), + dexQuotes, }, isRfqSupported, }; diff --git a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts index d14438bcd0..54779cc1e4 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -7,6 +7,7 @@ import { MAX_UINT256, ZERO_AMOUNT } from './constants'; import { AggregationError, BalancerFillData, + BalancerV2FillData, BancorFillData, CollapsedFill, CurveFillData, @@ -75,6 +76,8 @@ export function getErc20BridgeSourceToBridgeSource(source: ERC20BridgeSource): s switch (source) { case ERC20BridgeSource.Balancer: return encodeBridgeSourceId(BridgeProtocol.Balancer, 'Balancer'); + case ERC20BridgeSource.BalancerV2: + return encodeBridgeSourceId(BridgeProtocol.BalancerV2, 'BalancerV2'); case ERC20BridgeSource.Bancor: return encodeBridgeSourceId(BridgeProtocol.Bancor, 'Bancor'); // case ERC20BridgeSource.CoFiX: @@ -189,6 +192,11 @@ export function createBridgeDataForBridgeOrder(order: OptimizedMarketBridgeOrder const balancerFillData = (order as OptimizedMarketBridgeOrder).fillData; bridgeData = encoder.encode([balancerFillData.poolAddress]); break; + case ERC20BridgeSource.BalancerV2: + const balancerV2FillData = (order as OptimizedMarketBridgeOrder).fillData; + const { vault, poolId } = balancerV2FillData; + bridgeData = encoder.encode([vault, poolId]); + break; case ERC20BridgeSource.Bancor: const bancorFillData = (order as OptimizedMarketBridgeOrder).fillData; bridgeData = encoder.encode([bancorFillData.networkAddress, bancorFillData.path]); @@ -296,6 +304,7 @@ const makerPsmEncoder = AbiEncoder.create([ { name: 'psmAddress', type: 'address' }, { name: 'gemTokenAddress', type: 'address' }, ]); +const balancerV2Encoder = AbiEncoder.create([{ name: 'vault', type: 'address' }, { name: 'poolId', type: 'bytes32' }]); const routerAddressPathEncoder = AbiEncoder.create('(address,address[])'); const tokenAddressEncoder = AbiEncoder.create([{ name: 'tokenAddress', type: 'address' }]); @@ -358,6 +367,7 @@ export const BRIDGE_ENCODERS: { [ERC20BridgeSource.Uniswap]: poolEncoder, // Custom integrations [ERC20BridgeSource.MakerPsm]: makerPsmEncoder, + [ERC20BridgeSource.BalancerV2]: balancerV2Encoder, }; function getFillTokenAmounts(fill: CollapsedFill, side: MarketOperation): [BigNumber, BigNumber] { diff --git a/packages/asset-swapper/src/utils/market_operation_utils/balancer_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/balancer_utils.ts similarity index 51% rename from packages/asset-swapper/src/utils/market_operation_utils/balancer_utils.ts rename to packages/asset-swapper/src/utils/market_operation_utils/pools_cache/balancer_utils.ts index 9e22ea2d30..2099526d7d 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/balancer_utils.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/balancer_utils.ts @@ -1,19 +1,12 @@ import { getPoolsWithTokens, parsePoolData } from '@balancer-labs/sor'; import { Pool } from '@balancer-labs/sor/dist/types'; -import { BALANCER_MAX_POOLS_FETCHED, BALANCER_SUBGRAPH_URL, BALANCER_TOP_POOLS_FETCHED } from './constants'; +import { BALANCER_MAX_POOLS_FETCHED, BALANCER_SUBGRAPH_URL, BALANCER_TOP_POOLS_FETCHED } from '../constants'; -// tslint:disable:boolean-naming - -interface CacheValue { - timestamp: number; - pools: Pool[]; -} +import { CacheValue, PoolsCache } from './pools_cache'; // tslint:disable:custom-no-magic-numbers -const FIVE_SECONDS_MS = 5 * 1000; const ONE_DAY_MS = 24 * 60 * 60 * 1000; -const DEFAULT_TIMEOUT_MS = 1000; // tslint:enable:custom-no-magic-numbers interface BalancerPoolResponse { @@ -23,89 +16,19 @@ interface BalancerPoolResponse { tokensList: string[]; totalWeight: string; } - -export class BalancerPoolsCache { +export class BalancerPoolsCache extends PoolsCache { constructor( - private readonly _cache: { [key: string]: CacheValue } = {}, + private readonly _subgraphUrl: string = BALANCER_SUBGRAPH_URL, + cache: { [key: string]: CacheValue } = {}, private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED, - private readonly subgraphUrl: string = BALANCER_SUBGRAPH_URL, - private readonly topPoolsFetched: number = BALANCER_TOP_POOLS_FETCHED, + private readonly _topPoolsFetched: number = BALANCER_TOP_POOLS_FETCHED, ) { + super(cache); void this._loadTopPoolsAsync(); // Reload the top pools every 12 hours setInterval(async () => void this._loadTopPoolsAsync(), ONE_DAY_MS / 2); } - public async getPoolsForPairAsync( - takerToken: string, - makerToken: string, - timeoutMs: number = DEFAULT_TIMEOUT_MS, - ): Promise { - const timeout = new Promise(resolve => setTimeout(resolve, timeoutMs, [])); - return Promise.race([this._getPoolsForPairAsync(takerToken, makerToken), timeout]); - } - - public getCachedPoolAddressesForPair( - takerToken: string, - makerToken: string, - cacheExpiryMs?: number, - ): string[] | undefined { - const key = JSON.stringify([takerToken, makerToken]); - const value = this._cache[key]; - if (cacheExpiryMs === undefined) { - return value === undefined ? [] : value.pools.map(pool => pool.id); - } - const minTimestamp = Date.now() - cacheExpiryMs; - if (value === undefined || value.timestamp < minTimestamp) { - return undefined; - } else { - return value.pools.map(pool => pool.id); - } - } - - public howToSampleBalancer( - takerToken: string, - makerToken: string, - isAllowedSource: boolean, - ): { onChain: boolean; offChain: boolean } { - // If Balancer is excluded as a source, do not sample. - if (!isAllowedSource) { - return { onChain: false, offChain: false }; - } - const cachedBalancerPools = this.getCachedPoolAddressesForPair(takerToken, makerToken, ONE_DAY_MS); - // Sample Balancer on-chain (i.e. via the ERC20BridgeSampler contract) if: - // - Cached values are not stale - // - There is at least one Balancer pool for this pair - const onChain = cachedBalancerPools !== undefined && cachedBalancerPools.length > 0; - // Sample Balancer off-chain (i.e. via GraphQL query + `computeBalancerBuy/SellQuote`) - // if cached values are stale - const offChain = cachedBalancerPools === undefined; - return { onChain, offChain }; - } - - protected async _getPoolsForPairAsync( - takerToken: string, - makerToken: string, - cacheExpiryMs: number = FIVE_SECONDS_MS, - ): Promise { - const key = JSON.stringify([takerToken, makerToken]); - const value = this._cache[key]; - const minTimestamp = Date.now() - cacheExpiryMs; - if (value === undefined || value.timestamp < minTimestamp) { - const pools = await this._fetchPoolsForPairAsync(takerToken, makerToken); - this._cachePoolsForPair(takerToken, makerToken, pools); - } - return this._cache[key].pools; - } - - protected _cachePoolsForPair(takerToken: string, makerToken: string, pools: Pool[]): void { - const key = JSON.stringify([takerToken, makerToken]); - this._cache[key] = { - pools, - timestamp: Date.now(), - }; - } - protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise { try { const poolData = (await getPoolsWithTokens(takerToken, makerToken)).pools; @@ -153,7 +76,7 @@ export class BalancerPoolsCache { const query = ` query { pools (first: ${ - this.topPoolsFetched + this._topPoolsFetched }, where: {publicSwap: true, liquidity_gt: 0}, orderBy: swapsCount, orderDirection: desc) { id publicSwap @@ -172,7 +95,7 @@ export class BalancerPoolsCache { } `; try { - const response = await fetch(this.subgraphUrl, { + const response = await fetch(this._subgraphUrl, { method: 'POST', headers: { Accept: 'application/json', diff --git a/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/balancer_v2_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/balancer_v2_utils.ts new file mode 100644 index 0000000000..0a5f60da8c --- /dev/null +++ b/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/balancer_v2_utils.ts @@ -0,0 +1,82 @@ +// import { getPoolsWithTokens, parsePoolData } from '@balancer-labs/sor'; // TODO - upgrade to v2 +import { BigNumber } from '@0x/utils'; +import { Pool } from '@balancer-labs/sor/dist/types'; +import { request } from 'graphql-request'; + +import { BALANCER_MAX_POOLS_FETCHED, BALANCER_V2_SUBGRAPH_URL } from '../constants'; + +import { CacheValue, PoolsCache } from './pools_cache'; + +export class BalancerV2PoolsCache extends PoolsCache { + constructor( + private readonly subgraphUrl: string = BALANCER_V2_SUBGRAPH_URL, + private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED, + cache: { [key: string]: CacheValue } = {}, + ) { + super(cache); + } + + // protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise { + // try { + // const poolData = (await getPoolsWithTokens(takerToken, makerToken)).pools; + // // Sort by maker token balance (descending) + // const pools = parsePoolData(poolData, takerToken, makerToken).sort((a, b) => + // b.balanceOut.minus(a.balanceOut).toNumber(), + // ); + // return pools.length > this.maxPoolsFetched ? pools.slice(0, this.maxPoolsFetched) : pools; + // } catch (err) { + // return []; + // } + // } + + protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise { + const query = ` + query getPools { + pools( + first: ${this.maxPoolsFetched}, + where: { + tokensList_contains: ["${takerToken}", "${makerToken}"] + } + ) { + id + tokens { + address + balance + weight + } + swapFee + swaps( + orderBy: timestamp, orderDirection: desc, first: 1, + where:{ + tokenIn: "${takerToken}", + tokenOut: "${makerToken}" + } + ) { + tokenAmountIn + tokenAmountOut + } + } + } + `; + const { pools } = await request(this.subgraphUrl, query); + return pools.map((pool: any) => { + const tToken = pool.tokens.find((t: any) => t.address === takerToken); + const mToken = pool.tokens.find((t: any) => t.address === makerToken); + const swap = pool.swaps[0]; + const tokenAmountOut = swap ? swap.tokenAmountOut : undefined; + const tokenAmountIn = swap ? swap.tokenAmountIn : undefined; + const spotPrice = + tokenAmountOut && tokenAmountIn ? new BigNumber(tokenAmountOut).div(tokenAmountIn) : undefined; // TODO: xianny check + + return { + id: pool.id, + balanceIn: new BigNumber(tToken.balance), + balanceOut: new BigNumber(mToken.balance), + weightIn: new BigNumber(tToken.weight), + weightOut: new BigNumber(mToken.weight), + swapFee: new BigNumber(pool.swapFee), + spotPrice, + }; + }); + } +} diff --git a/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/cream_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/cream_utils.ts new file mode 100644 index 0000000000..cc21267aee --- /dev/null +++ b/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/cream_utils.ts @@ -0,0 +1,28 @@ +import { Pool } from '@balancer-labs/sor/dist/types'; +import { getPoolsWithTokens, parsePoolData } from 'cream-sor'; + +import { BALANCER_MAX_POOLS_FETCHED } from '../constants'; + +import { CacheValue, PoolsCache } from './pools_cache'; + +export class CreamPoolsCache extends PoolsCache { + constructor( + _cache: { [key: string]: CacheValue } = {}, + private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED, + ) { + super(_cache); + } + + protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise { + try { + const poolData = (await getPoolsWithTokens(takerToken, makerToken)).pools; + // Sort by maker token balance (descending) + const pools = parsePoolData(poolData, takerToken, makerToken).sort((a, b) => + b.balanceOut.minus(a.balanceOut).toNumber(), + ); + return pools.slice(0, this.maxPoolsFetched); + } catch (err) { + return []; + } + } +} diff --git a/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/index.ts new file mode 100644 index 0000000000..a881d890bf --- /dev/null +++ b/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/index.ts @@ -0,0 +1,4 @@ +export { BalancerPoolsCache } from './balancer_utils'; +export { BalancerV2PoolsCache } from './balancer_v2_utils'; +export { CreamPoolsCache } from './cream_utils'; +export { PoolsCache } from './pools_cache'; diff --git a/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/pools_cache.ts b/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/pools_cache.ts new file mode 100644 index 0000000000..ddf65b2c2c --- /dev/null +++ b/packages/asset-swapper/src/utils/market_operation_utils/pools_cache/pools_cache.ts @@ -0,0 +1,73 @@ +import { Pool } from '@balancer-labs/sor/dist/types'; +export { Pool }; +export interface CacheValue { + timestamp: number; + pools: Pool[]; +} + +// tslint:disable:custom-no-magic-numbers +const FIVE_SECONDS_MS = 5 * 1000; +const ONE_DAY_MS = 24 * 60 * 60 * 1000; +const DEFAULT_TIMEOUT_MS = 1000; +// tslint:enable:custom-no-magic-numbers + +export abstract class PoolsCache { + constructor(protected readonly _cache: { [key: string]: CacheValue }) {} + + public async getFreshPoolsForPairAsync( + takerToken: string, + makerToken: string, + timeoutMs: number = DEFAULT_TIMEOUT_MS, + ): Promise { + const timeout = new Promise(resolve => setTimeout(resolve, timeoutMs, [])); + return Promise.race([this._getAndSaveFreshPoolsForPairAsync(takerToken, makerToken), timeout]); + } + + public getCachedPoolAddressesForPair( + takerToken: string, + makerToken: string, + cacheExpiryMs?: number, + ): string[] | undefined { + const key = JSON.stringify([takerToken, makerToken]); + const value = this._cache[key]; + if (cacheExpiryMs === undefined) { + return value === undefined ? [] : value.pools.map(pool => pool.id); + } + const minTimestamp = Date.now() - cacheExpiryMs; + if (value === undefined || value.timestamp < minTimestamp) { + return undefined; + } else { + return value.pools.map(pool => pool.id); + } + } + + public isFresh(takerToken: string, makerToken: string): boolean { + const cached = this.getCachedPoolAddressesForPair(takerToken, makerToken, ONE_DAY_MS); + return cached !== undefined && cached.length > 0; + } + + protected async _getAndSaveFreshPoolsForPairAsync( + takerToken: string, + makerToken: string, + cacheExpiryMs: number = FIVE_SECONDS_MS, + ): Promise { + const key = JSON.stringify([takerToken, makerToken]); + const value = this._cache[key]; + const minTimestamp = Date.now() - cacheExpiryMs; + if (value === undefined || value.timestamp < minTimestamp) { + const pools = await this._fetchPoolsForPairAsync(takerToken, makerToken); + this._cachePoolsForPair(takerToken, makerToken, pools); + } + return this._cache[key].pools; + } + + protected _cachePoolsForPair(takerToken: string, makerToken: string, pools: Pool[]): void { + const key = JSON.stringify([takerToken, makerToken]); + this._cache[key] = { + pools, + timestamp: Date.now(), + }; + } + + protected abstract _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise; +} 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 16dd060904..7afffec2f1 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/sampler.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/sampler.ts @@ -4,11 +4,10 @@ import { BigNumber, NULL_BYTES } from '@0x/utils'; import { SamplerOverrides } from '../../types'; import { ERC20BridgeSamplerContract } from '../../wrappers'; -import { BalancerPoolsCache } from './balancer_utils'; import { BancorService } from './bancor_service'; -import { CreamPoolsCache } from './cream_utils'; +import { PoolsCache } from './pools_cache'; import { SamplerOperations } from './sampler_operations'; -import { BatchedOperation, LiquidityProviderRegistry, TokenAdjacencyGraph } from './types'; +import { BatchedOperation, ERC20BridgeSource, LiquidityProviderRegistry, TokenAdjacencyGraph } from './types'; /** * Generate sample amounts up to `maxFillAmount`. @@ -37,21 +36,12 @@ export class DexOrderSampler extends SamplerOperations { public readonly chainId: ChainId, _samplerContract: ERC20BridgeSamplerContract, private readonly _samplerOverrides?: SamplerOverrides, - balancerPoolsCache?: BalancerPoolsCache, - creamPoolsCache?: CreamPoolsCache, + poolsCaches?: { [key in ERC20BridgeSource]: PoolsCache }, tokenAdjacencyGraph?: TokenAdjacencyGraph, liquidityProviderRegistry?: LiquidityProviderRegistry, bancorServiceFn: () => Promise = async () => undefined, ) { - super( - chainId, - _samplerContract, - balancerPoolsCache, - creamPoolsCache, - tokenAdjacencyGraph, - liquidityProviderRegistry, - bancorServiceFn, - ); + super(chainId, _samplerContract, poolsCaches, tokenAdjacencyGraph, liquidityProviderRegistry, bancorServiceFn); } /* Type overloads for `executeAsync()`. Could skip this if we would upgrade TS. */ diff --git a/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts b/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts index e45ca2f922..fad9fca4d6 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts @@ -6,7 +6,6 @@ import * as _ from 'lodash'; import { SamplerCallResult, SignedNativeOrder } from '../../types'; import { ERC20BridgeSamplerContract } from '../../wrappers'; -import { BalancerPoolsCache } from './balancer_utils'; import { BancorService } from './bancor_service'; import { getCurveLikeInfosForPair, @@ -19,6 +18,7 @@ import { uniswapV2LikeRouterAddress, } from './bridge_source_utils'; import { + BALANCER_V2_VAULT_ADDRESS_BY_CHAIN, BANCOR_REGISTRY_BY_CHAIN_ID, DODO_CONFIG_BY_CHAIN_ID, DODOV2_FACTORIES_BY_CHAIN_ID, @@ -38,13 +38,15 @@ import { UNISWAPV1_ROUTER_BY_CHAIN_ID, ZERO_AMOUNT, } from './constants'; -import { CreamPoolsCache } from './cream_utils'; import { getLiquidityProvidersForPair } from './liquidity_provider_utils'; import { getIntermediateTokens } from './multihop_utils'; +import { BalancerPoolsCache, BalancerV2PoolsCache, CreamPoolsCache, PoolsCache } from './pools_cache'; import { SamplerContractOperation } from './sampler_contract_operation'; import { SourceFilters } from './source_filters'; import { BalancerFillData, + BalancerV2FillData, + BalancerV2PoolInfo, BancorFillData, BatchedOperation, CurveFillData, @@ -64,6 +66,7 @@ import { PsmInfo, ShellFillData, SourceQuoteOperation, + SourcesWithPoolsCache, TokenAdjacencyGraph, UniswapV2FillData, } from './types'; @@ -88,6 +91,7 @@ export const BATCH_SOURCE_FILTERS = SourceFilters.all().exclude([ERC20BridgeSour */ export class SamplerOperations { public readonly liquidityProviderRegistry: LiquidityProviderRegistry; + public readonly poolsCaches: { [key in SourcesWithPoolsCache]: PoolsCache }; protected _bancorService?: BancorService; public static constant(result: T): BatchedOperation { return { @@ -100,8 +104,7 @@ export class SamplerOperations { constructor( public readonly chainId: ChainId, protected readonly _samplerContract: ERC20BridgeSamplerContract, - public readonly balancerPoolsCache: BalancerPoolsCache = new BalancerPoolsCache(), - public readonly creamPoolsCache: CreamPoolsCache = new CreamPoolsCache(), + poolsCaches?: { [key in SourcesWithPoolsCache]: PoolsCache }, protected readonly tokenAdjacencyGraph: TokenAdjacencyGraph = { default: [] }, liquidityProviderRegistry: LiquidityProviderRegistry = {}, bancorServiceFn: () => Promise = async () => undefined, @@ -110,6 +113,13 @@ export class SamplerOperations { ...LIQUIDITY_PROVIDER_REGISTRY_BY_CHAIN_ID[chainId], ...liquidityProviderRegistry, }; + this.poolsCaches = poolsCaches + ? poolsCaches + : { + [ERC20BridgeSource.BalancerV2]: new BalancerV2PoolsCache(), + [ERC20BridgeSource.Balancer]: new BalancerPoolsCache(), + [ERC20BridgeSource.Cream]: new CreamPoolsCache(), + }; // Initialize the Bancor service, fetching paths in the background bancorServiceFn() .then(service => (this._bancorService = service)) @@ -473,6 +483,38 @@ export class SamplerOperations { }); } + public getBalancerV2SellQuotes( + poolInfo: BalancerV2PoolInfo, + makerToken: string, + takerToken: string, + takerFillAmounts: BigNumber[], + source: ERC20BridgeSource, + ): SourceQuoteOperation { + return new SamplerContractOperation({ + source, + fillData: poolInfo, + contract: this._samplerContract, + function: this._samplerContract.sampleSellsFromBalancerV2, + params: [poolInfo, takerToken, makerToken, takerFillAmounts], + }); + } + + public getBalancerV2BuyQuotes( + poolInfo: BalancerV2PoolInfo, + makerToken: string, + takerToken: string, + makerFillAmounts: BigNumber[], + source: ERC20BridgeSource, + ): SourceQuoteOperation { + return new SamplerContractOperation({ + source, + fillData: poolInfo, + contract: this._samplerContract, + function: this._samplerContract.sampleBuysFromBalancerV2, + params: [poolInfo, takerToken, makerToken, makerFillAmounts], + }); + } + public getBalancerSellQuotes( poolAddress: string, makerToken: string, @@ -505,79 +547,6 @@ export class SamplerOperations { }); } - public async getBalancerSellQuotesOffChainAsync( - makerToken: string, - takerToken: string, - _takerFillAmounts: BigNumber[], - ): Promise>>> { - // Prime the cache but do not sample off chain - await this.balancerPoolsCache.getPoolsForPairAsync(takerToken, makerToken); - return []; - // return pools.map(pool => - // takerFillAmounts.map(amount => ({ - // source: ERC20BridgeSource.Balancer, - // output: computeBalancerSellQuote(pool, amount), - // input: amount, - // fillData: { poolAddress: pool.id }, - // })), - // ); - } - - public async getBalancerBuyQuotesOffChainAsync( - makerToken: string, - takerToken: string, - _makerFillAmounts: BigNumber[], - ): Promise>>> { - // Prime the pools but do not sample off chain - // Prime the cache but do not sample off chain - await this.balancerPoolsCache.getPoolsForPairAsync(takerToken, makerToken); - return []; - // return pools.map(pool => - // makerFillAmounts.map(amount => ({ - // source: ERC20BridgeSource.Balancer, - // output: computeBalancerBuyQuote(pool, amount), - // input: amount, - // fillData: { poolAddress: pool.id }, - // })), - // ); - } - - public async getCreamSellQuotesOffChainAsync( - makerToken: string, - takerToken: string, - _takerFillAmounts: BigNumber[], - ): Promise>>> { - // Prime the cache but do not sample off chain - await this.creamPoolsCache.getPoolsForPairAsync(takerToken, makerToken); - return []; - // return pools.map(pool => - // takerFillAmounts.map(amount => ({ - // source: ERC20BridgeSource.Cream, - // output: computeBalancerSellQuote(pool, amount), - // input: amount, - // fillData: { poolAddress: pool.id }, - // })), - // ); - } - - public async getCreamBuyQuotesOffChainAsync( - makerToken: string, - takerToken: string, - _makerFillAmounts: BigNumber[], - ): Promise>>> { - // Prime the cache but do not sample off chain - await this.creamPoolsCache.getPoolsForPairAsync(takerToken, makerToken); - return []; - // return pools.map(pool => - // makerFillAmounts.map(amount => ({ - // source: ERC20BridgeSource.Cream, - // output: computeBalancerBuyQuote(pool, amount), - // input: amount, - // fillData: { poolAddress: pool.id }, - // })), - // ); - } - public getMStableSellQuotes( router: string, makerToken: string, @@ -1188,29 +1157,56 @@ export class SamplerOperations { ), ]; case ERC20BridgeSource.Balancer: - return this.balancerPoolsCache - .getCachedPoolAddressesForPair(takerToken, makerToken)! - .map(poolAddress => - this.getBalancerSellQuotes( - poolAddress, - makerToken, - takerToken, - takerFillAmounts, - ERC20BridgeSource.Balancer, - ), - ); + return ( + this.poolsCaches[ERC20BridgeSource.Balancer].getCachedPoolAddressesForPair( + takerToken, + makerToken, + ) || [] + ).map(poolAddress => + this.getBalancerSellQuotes( + poolAddress, + makerToken, + takerToken, + takerFillAmounts, + ERC20BridgeSource.Balancer, + ), + ); + case ERC20BridgeSource.BalancerV2: + const poolIds = + this.poolsCaches[ERC20BridgeSource.BalancerV2].getCachedPoolAddressesForPair( + takerToken, + makerToken, + ) || []; + + const vault = BALANCER_V2_VAULT_ADDRESS_BY_CHAIN[this.chainId]; + if (vault === NULL_ADDRESS) { + return []; + } + return poolIds.map(poolId => + this.getBalancerV2SellQuotes( + { poolId, vault }, + makerToken, + takerToken, + takerFillAmounts, + ERC20BridgeSource.BalancerV2, + ), + ); + case ERC20BridgeSource.Cream: - return this.creamPoolsCache - .getCachedPoolAddressesForPair(takerToken, makerToken)! - .map(poolAddress => - this.getBalancerSellQuotes( - poolAddress, - makerToken, - takerToken, - takerFillAmounts, - ERC20BridgeSource.Cream, - ), - ); + return ( + this.poolsCaches[ERC20BridgeSource.Cream].getCachedPoolAddressesForPair( + takerToken, + makerToken, + ) || [] + ).map(poolAddress => + this.getBalancerSellQuotes( + poolAddress, + makerToken, + takerToken, + takerFillAmounts, + ERC20BridgeSource.Cream, + ), + ); case ERC20BridgeSource.Dodo: if (!isValidAddress(DODO_CONFIG_BY_CHAIN_ID[this.chainId].registry)) { return []; @@ -1403,29 +1399,55 @@ export class SamplerOperations { ), ]; case ERC20BridgeSource.Balancer: - return this.balancerPoolsCache - .getCachedPoolAddressesForPair(takerToken, makerToken)! - .map(poolAddress => - this.getBalancerBuyQuotes( - poolAddress, - makerToken, - takerToken, - makerFillAmounts, - ERC20BridgeSource.Balancer, - ), - ); + return ( + this.poolsCaches[ERC20BridgeSource.Balancer].getCachedPoolAddressesForPair( + takerToken, + makerToken, + ) || [] + ).map(poolAddress => + this.getBalancerBuyQuotes( + poolAddress, + makerToken, + takerToken, + makerFillAmounts, + ERC20BridgeSource.Balancer, + ), + ); + case ERC20BridgeSource.BalancerV2: + const poolIds = + this.poolsCaches[ERC20BridgeSource.BalancerV2].getCachedPoolAddressesForPair( + takerToken, + makerToken, + ) || []; + + const vault = BALANCER_V2_VAULT_ADDRESS_BY_CHAIN[this.chainId]; + if (vault === NULL_ADDRESS) { + return []; + } + return poolIds.map(poolId => + this.getBalancerV2BuyQuotes( + { poolId, vault }, + makerToken, + takerToken, + makerFillAmounts, + ERC20BridgeSource.BalancerV2, + ), + ); case ERC20BridgeSource.Cream: - return this.creamPoolsCache - .getCachedPoolAddressesForPair(takerToken, makerToken)! - .map(poolAddress => - this.getBalancerBuyQuotes( - poolAddress, - makerToken, - takerToken, - makerFillAmounts, - ERC20BridgeSource.Cream, - ), - ); + return ( + this.poolsCaches[ERC20BridgeSource.Cream].getCachedPoolAddressesForPair( + takerToken, + makerToken, + ) || [] + ).map(poolAddress => + this.getBalancerBuyQuotes( + poolAddress, + makerToken, + takerToken, + makerFillAmounts, + ERC20BridgeSource.Cream, + ), + ); case ERC20BridgeSource.Dodo: if (!isValidAddress(DODO_CONFIG_BY_CHAIN_ID[this.chainId].registry)) { return []; 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 4bc1814faa..d1e10dc3be 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -45,6 +45,7 @@ export enum ERC20BridgeSource { LiquidityProvider = 'LiquidityProvider', MultiBridge = 'MultiBridge', Balancer = 'Balancer', + BalancerV2 = 'Balancer_V2', Cream = 'CREAM', Bancor = 'Bancor', MakerPsm = 'MakerPsm', @@ -76,6 +77,7 @@ export enum ERC20BridgeSource { CheeseSwap = 'CheeseSwap', JulSwap = 'JulSwap', } +export type SourcesWithPoolsCache = ERC20BridgeSource.Balancer | ERC20BridgeSource.BalancerV2 | ERC20BridgeSource.Cream; // tslint:disable: enum-naming /** @@ -120,6 +122,14 @@ export interface PsmInfo { gemTokenAddress: string; } +/** + * Configuration info for a Balancer V2 pool. + */ +export interface BalancerV2PoolInfo { + poolId: string; + vault: string; +} + // Internal `fillData` field for `Fill` objects. export interface FillData {} @@ -145,6 +155,11 @@ export interface BalancerFillData extends FillData { poolAddress: string; } +export interface BalancerV2FillData extends FillData { + vault: string; + poolId: string; +} + export interface UniswapV2FillData extends FillData { tokenAddressPath: string[]; router: string; diff --git a/packages/asset-swapper/test/artifacts.ts b/packages/asset-swapper/test/artifacts.ts index 26fd731eae..a07afaed70 100644 --- a/packages/asset-swapper/test/artifacts.ts +++ b/packages/asset-swapper/test/artifacts.ts @@ -8,6 +8,7 @@ import { ContractArtifact } from 'ethereum-types'; import * as ApproximateBuys from '../test/generated-artifacts/ApproximateBuys.json'; import * as BalanceChecker from '../test/generated-artifacts/BalanceChecker.json'; import * as BalancerSampler from '../test/generated-artifacts/BalancerSampler.json'; +import * as BalancerV2Sampler from '../test/generated-artifacts/BalancerV2Sampler.json'; import * as BancorSampler from '../test/generated-artifacts/BancorSampler.json'; import * as CurveSampler from '../test/generated-artifacts/CurveSampler.json'; import * as DODOSampler from '../test/generated-artifacts/DODOSampler.json'; @@ -48,6 +49,7 @@ export const artifacts = { ApproximateBuys: ApproximateBuys as ContractArtifact, BalanceChecker: BalanceChecker as ContractArtifact, BalancerSampler: BalancerSampler as ContractArtifact, + BalancerV2Sampler: BalancerV2Sampler as ContractArtifact, BancorSampler: BancorSampler as ContractArtifact, CurveSampler: CurveSampler as ContractArtifact, DODOSampler: DODOSampler as ContractArtifact, diff --git a/packages/asset-swapper/test/dex_sampler_test.ts b/packages/asset-swapper/test/dex_sampler_test.ts index eb40a190b5..b98488c174 100644 --- a/packages/asset-swapper/test/dex_sampler_test.ts +++ b/packages/asset-swapper/test/dex_sampler_test.ts @@ -9,14 +9,12 @@ import { } from '@0x/contracts-test-utils'; import { FillQuoteTransformerOrderType, LimitOrderFields, SignatureType } from '@0x/protocol-utils'; import { BigNumber, hexUtils, NULL_ADDRESS } from '@0x/utils'; -import { Pool } from '@balancer-labs/sor/dist/types'; import * as _ from 'lodash'; import { SignedOrder } from '../src/types'; import { DexOrderSampler, getSampleAmounts } from '../src/utils/market_operation_utils/sampler'; import { ERC20BridgeSource, TokenAdjacencyGraph } from '../src/utils/market_operation_utils/types'; -import { MockBalancerPoolsCache } from './utils/mock_balancer_pools_cache'; import { MockSamplerContract } from './utils/mock_sampler_contract'; import { generatePseudoRandomSalt } from './utils/utils'; @@ -112,7 +110,6 @@ describe('DexSampler tests', () => { undefined, undefined, undefined, - undefined, async () => undefined, ); const [fillableAmounts] = await dexOrderSampler.executeAsync( @@ -137,7 +134,6 @@ describe('DexSampler tests', () => { undefined, undefined, undefined, - undefined, async () => undefined, ); const [fillableAmounts] = await dexOrderSampler.executeAsync( @@ -166,7 +162,6 @@ describe('DexSampler tests', () => { undefined, undefined, undefined, - undefined, async () => undefined, ); const [fillableAmounts] = await dexOrderSampler.executeAsync( @@ -200,7 +195,6 @@ describe('DexSampler tests', () => { undefined, undefined, undefined, - undefined, { [poolAddress]: { tokens: [expectedMakerToken, expectedTakerToken], gasCost }, }, @@ -245,7 +239,6 @@ describe('DexSampler tests', () => { undefined, undefined, undefined, - undefined, { [poolAddress]: { tokens: [expectedMakerToken, expectedTakerToken], gasCost }, }, @@ -291,7 +284,6 @@ describe('DexSampler tests', () => { undefined, undefined, undefined, - undefined, async () => undefined, ); const [fillableAmounts] = await dexOrderSampler.executeAsync( @@ -325,7 +317,6 @@ describe('DexSampler tests', () => { undefined, undefined, undefined, - undefined, async () => undefined, ); const [fillableAmounts] = await dexOrderSampler.executeAsync( @@ -358,7 +349,6 @@ describe('DexSampler tests', () => { undefined, undefined, undefined, - undefined, async () => undefined, ); const [fillableAmounts] = await dexOrderSampler.executeAsync( @@ -391,7 +381,6 @@ describe('DexSampler tests', () => { undefined, undefined, undefined, - undefined, async () => undefined, ); const [fillableAmounts] = await dexOrderSampler.executeAsync( @@ -425,7 +414,6 @@ describe('DexSampler tests', () => { undefined, undefined, undefined, - undefined, async () => undefined, ); const [fillableAmounts] = await dexOrderSampler.executeAsync( @@ -490,7 +478,6 @@ describe('DexSampler tests', () => { sampler, undefined, undefined, - undefined, tokenAdjacencyGraph, undefined, async () => undefined, @@ -542,38 +529,6 @@ describe('DexSampler tests', () => { expect(quotes).to.have.lengthOf(sources.length + additionalSourceCount); expect(quotes).to.deep.eq(expectedQuotes.concat(uniswapV2ETHQuotes)); }); - it('getSellQuotes() fetches pools but not samples from Balancer', async () => { - // HACK - // We disabled the off-chain sampling due to incorrect data observed between - // on-chain and off-chain sampling - const expectedTakerToken = randomAddress(); - const expectedMakerToken = randomAddress(); - const expectedTakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3); - const pools: Pool[] = [generateBalancerPool(), generateBalancerPool()]; - const balancerPoolsCache = new MockBalancerPoolsCache({ - getPoolsForPairAsync: async (takerToken: string, makerToken: string) => { - expect(takerToken).equal(expectedTakerToken); - expect(makerToken).equal(expectedMakerToken); - return Promise.resolve(pools); - }, - }); - const dexOrderSampler = new DexOrderSampler( - chainId, - new MockSamplerContract({}), - undefined, - balancerPoolsCache, - undefined, - undefined, - undefined, - async () => undefined, - ); - const quotes = await dexOrderSampler.getBalancerSellQuotesOffChainAsync( - expectedMakerToken, - expectedTakerToken, - expectedTakerFillAmounts, - ); - expect(quotes).to.have.lengthOf(0); - }); it('getBuyQuotes()', async () => { const expectedTakerToken = randomAddress(); const expectedMakerToken = randomAddress(); @@ -620,7 +575,6 @@ describe('DexSampler tests', () => { sampler, undefined, undefined, - undefined, tokenAdjacencyGraph, undefined, async () => undefined, @@ -665,80 +619,39 @@ describe('DexSampler tests', () => { expect(quotes).to.have.lengthOf(sources.length + 1); expect(quotes).to.deep.eq(expectedQuotes.concat(uniswapV2ETHQuotes)); }); - it('getBuyQuotes() uses samples from Balancer', async () => { - const expectedTakerToken = randomAddress(); - const expectedMakerToken = randomAddress(); - const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3); - const pools: Pool[] = [generateBalancerPool(), generateBalancerPool()]; - const balancerPoolsCache = new MockBalancerPoolsCache({ - getPoolsForPairAsync: async (takerToken: string, makerToken: string) => { - expect(takerToken).equal(expectedTakerToken); - expect(makerToken).equal(expectedMakerToken); - return Promise.resolve(pools); - }, + describe('batched operations', () => { + it('getLimitOrderFillableMakerAssetAmounts(), getLimitOrderFillableTakerAssetAmounts()', async () => { + const expectedFillableTakerAmounts = ORDERS.map(() => getRandomInteger(0, 100e18)); + const expectedFillableMakerAmounts = ORDERS.map(() => getRandomInteger(0, 100e18)); + const sampler = new MockSamplerContract({ + getLimitOrderFillableMakerAssetAmounts: (orders, signatures) => { + expect(orders).to.deep.eq(SIMPLE_ORDERS); + expect(signatures).to.deep.eq(ORDERS.map(o => o.signature)); + return expectedFillableMakerAmounts; + }, + getLimitOrderFillableTakerAssetAmounts: (orders, signatures) => { + expect(orders).to.deep.eq(SIMPLE_ORDERS); + expect(signatures).to.deep.eq(ORDERS.map(o => o.signature)); + return expectedFillableTakerAmounts; + }, + }); + const dexOrderSampler = new DexOrderSampler( + chainId, + sampler, + undefined, + undefined, + undefined, + undefined, + async () => undefined, + ); + const [fillableMakerAmounts, fillableTakerAmounts] = await dexOrderSampler.executeAsync( + dexOrderSampler.getLimitOrderFillableMakerAmounts(ORDERS, exchangeProxyAddress), + dexOrderSampler.getLimitOrderFillableTakerAmounts(ORDERS, exchangeProxyAddress), + ); + expect(fillableMakerAmounts).to.deep.eq(expectedFillableMakerAmounts); + expect(fillableTakerAmounts).to.deep.eq(expectedFillableTakerAmounts); }); - const dexOrderSampler = new DexOrderSampler( - chainId, - new MockSamplerContract({}), - undefined, - balancerPoolsCache, - undefined, - undefined, - undefined, - async () => undefined, - ); - const quotes = await dexOrderSampler.getBalancerBuyQuotesOffChainAsync( - expectedMakerToken, - expectedTakerToken, - expectedMakerFillAmounts, - ); - expect(quotes).to.have.lengthOf(0); - }); - }); - - describe('batched operations', () => { - it('getLimitOrderFillableMakerAssetAmounts(), getLimitOrderFillableTakerAssetAmounts()', async () => { - const expectedFillableTakerAmounts = ORDERS.map(() => getRandomInteger(0, 100e18)); - const expectedFillableMakerAmounts = ORDERS.map(() => getRandomInteger(0, 100e18)); - const sampler = new MockSamplerContract({ - getLimitOrderFillableMakerAssetAmounts: (orders, signatures) => { - expect(orders).to.deep.eq(SIMPLE_ORDERS); - expect(signatures).to.deep.eq(ORDERS.map(o => o.signature)); - return expectedFillableMakerAmounts; - }, - getLimitOrderFillableTakerAssetAmounts: (orders, signatures) => { - expect(orders).to.deep.eq(SIMPLE_ORDERS); - expect(signatures).to.deep.eq(ORDERS.map(o => o.signature)); - return expectedFillableTakerAmounts; - }, - }); - const dexOrderSampler = new DexOrderSampler( - chainId, - sampler, - undefined, - undefined, - undefined, - undefined, - undefined, - async () => undefined, - ); - const [fillableMakerAmounts, fillableTakerAmounts] = await dexOrderSampler.executeAsync( - dexOrderSampler.getLimitOrderFillableMakerAmounts(ORDERS, exchangeProxyAddress), - dexOrderSampler.getLimitOrderFillableTakerAmounts(ORDERS, exchangeProxyAddress), - ); - expect(fillableMakerAmounts).to.deep.eq(expectedFillableMakerAmounts); - expect(fillableTakerAmounts).to.deep.eq(expectedFillableTakerAmounts); }); }); }); -function generateBalancerPool(): Pool { - return { - id: randomAddress(), - balanceIn: getRandomInteger(1, 1e18), - balanceOut: getRandomInteger(1, 1e18), - weightIn: getRandomInteger(0, 1e5), - weightOut: getRandomInteger(0, 1e5), - swapFee: getRandomInteger(0, 1e5), - }; -} // tslint:disable-next-line: max-file-line-count diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index 8401404f61..d678f4a5c3 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -11,21 +11,21 @@ import { import { FillQuoteTransformerOrderType, LimitOrder, RfqOrder, SignatureType } from '@0x/protocol-utils'; import { BigNumber, hexUtils, NULL_BYTES } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; +import { Pool } from '@balancer-labs/sor/dist/types'; import * as _ from 'lodash'; import * as TypeMoq from 'typemoq'; import { MarketOperation, QuoteRequestor, RfqRequestOpts, SignedNativeOrder } from '../src'; import { NativeOrderWithFillableAmounts } from '../src/types'; import { MarketOperationUtils } from '../src/utils/market_operation_utils/'; -import { BalancerPoolsCache } from '../src/utils/market_operation_utils/balancer_utils'; import { BUY_SOURCE_FILTER_BY_CHAIN_ID, POSITIVE_INF, SELL_SOURCE_FILTER_BY_CHAIN_ID, SOURCE_FLAGS, } from '../src/utils/market_operation_utils/constants'; -import { CreamPoolsCache } from '../src/utils/market_operation_utils/cream_utils'; import { createFills } from '../src/utils/market_operation_utils/fills'; +import { PoolsCache } from '../src/utils/market_operation_utils/pools_cache'; import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler'; import { BATCH_SOURCE_FILTERS } from '../src/utils/market_operation_utils/sampler_operations'; import { SourceFilters } from '../src/utils/market_operation_utils/source_filters'; @@ -97,6 +97,32 @@ async function getMarketBuyOrdersAsync( return utils.getOptimizerResultAsync(nativeOrders, makerAmount, MarketOperation.Buy, opts); } +class MockPoolsCache extends PoolsCache { + constructor(private readonly _handler: (takerToken: string, makerToken: string) => Pool[]) { + super({}); + } + protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise { + return this._handler(takerToken, makerToken); + } +} + +// Return some pool so that sampling functions are called for Balancer, BalancerV2, and Cream +// tslint:disable:custom-no-magic-numbers +const mockPoolsCache = new MockPoolsCache((_takerToken: string, _makerToken: string) => { + return [ + { + id: '0xe4b2554b622cc342ac7d6dc19b594553577941df000200000000000000000003', + balanceIn: new BigNumber('13655.491506618973154788'), + balanceOut: new BigNumber('8217005.926472'), + weightIn: new BigNumber('0.5'), + weightOut: new BigNumber('0.5'), + swapFee: new BigNumber('0.008'), + spotPrice: new BigNumber(596.92685), + }, + ]; +}); +// tslint:enable:custom-no-magic-numbers + // tslint:disable: custom-no-magic-numbers promise-function-async describe('MarketOperationUtils tests', () => { const CHAIN_ID = ChainId.Mainnet; @@ -292,6 +318,11 @@ describe('MarketOperationUtils tests', () => { const DEFAULT_FILL_DATA: FillDataBySource = { [ERC20BridgeSource.UniswapV2]: { tokenAddressPath: [] }, [ERC20BridgeSource.Balancer]: { poolAddress: randomAddress() }, + [ERC20BridgeSource.BalancerV2]: { + vault: randomAddress(), + poolId: randomAddress(), + deadline: Math.floor(Date.now() / 1000) + 300, + }, [ERC20BridgeSource.Bancor]: { path: [], networkAddress: randomAddress() }, [ERC20BridgeSource.Kyber]: { hint: '0x', reserveId: '0x', networkAddress: randomAddress() }, [ERC20BridgeSource.Curve]: { @@ -381,53 +412,6 @@ describe('MarketOperationUtils tests', () => { getSellQuotes: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES), getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES), getMedianSellRate: createGetMedianSellRate(1), - getBalancerSellQuotesOffChainAsync: ( - _makerToken: string, - _takerToken: string, - takerFillAmounts: BigNumber[], - ) => [ - createSamplesFromRates( - ERC20BridgeSource.Balancer, - takerFillAmounts, - createDecreasingRates(takerFillAmounts.length), - DEFAULT_FILL_DATA[ERC20BridgeSource.Balancer], - ), - ], - getBalancerBuyQuotesOffChainAsync: ( - _makerToken: string, - _takerToken: string, - makerFillAmounts: BigNumber[], - ) => [ - createSamplesFromRates( - ERC20BridgeSource.Balancer, - makerFillAmounts, - createDecreasingRates(makerFillAmounts.length).map(r => new BigNumber(1).div(r)), - DEFAULT_FILL_DATA[ERC20BridgeSource.Balancer], - ), - ], - getCreamSellQuotesOffChainAsync: (_makerToken: string, _takerToken: string, takerFillAmounts: BigNumber[]) => [ - createSamplesFromRates( - ERC20BridgeSource.Cream, - takerFillAmounts, - createDecreasingRates(takerFillAmounts.length), - DEFAULT_FILL_DATA[ERC20BridgeSource.Cream], - ), - ], - getCreamBuyQuotesOffChainAsync: (_makerToken: string, _takerToken: string, makerFillAmounts: BigNumber[]) => [ - createSamplesFromRates( - ERC20BridgeSource.Cream, - makerFillAmounts, - createDecreasingRates(makerFillAmounts.length).map(r => new BigNumber(1).div(r)), - DEFAULT_FILL_DATA[ERC20BridgeSource.Cream], - ), - ], - getBancorSellQuotesOffChainAsync: (_makerToken: string, _takerToken: string, takerFillAmounts: BigNumber[]) => - createSamplesFromRates( - ERC20BridgeSource.Bancor, - takerFillAmounts, - createDecreasingRates(takerFillAmounts.length), - DEFAULT_FILL_DATA[ERC20BridgeSource.Bancor], - ), getTwoHopSellQuotes: (..._params: any[]) => [], getTwoHopBuyQuotes: (..._params: any[]) => [], isAddressContract: (..._params: any[]) => false, @@ -440,8 +424,11 @@ describe('MarketOperationUtils tests', () => { async executeBatchAsync(ops: any[]): Promise { return ops; }, - balancerPoolsCache: new BalancerPoolsCache(), - creamPoolsCache: new CreamPoolsCache(), + poolsCaches: { + [ERC20BridgeSource.BalancerV2]: mockPoolsCache, + [ERC20BridgeSource.Balancer]: mockPoolsCache, + [ERC20BridgeSource.Cream]: mockPoolsCache, + }, liquidityProviderRegistry: {}, chainId: CHAIN_ID, } as any) as DexOrderSampler; @@ -520,22 +507,6 @@ describe('MarketOperationUtils tests', () => { sourcesPolled.push(ERC20BridgeSource.MultiHop); return DEFAULT_OPS.getTwoHopSellQuotes(...args); }, - getBalancerSellQuotesOffChainAsync: ( - makerToken: string, - takerToken: string, - takerFillAmounts: BigNumber[], - ) => { - sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer); - return DEFAULT_OPS.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, takerFillAmounts); - }, - getCreamSellQuotesOffChainAsync: ( - makerToken: string, - takerToken: string, - takerFillAmounts: BigNumber[], - ) => { - sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Cream); - return DEFAULT_OPS.getCreamSellQuotesOffChainAsync(makerToken, takerToken, takerFillAmounts); - }, }); await getMarketSellOrdersAsync(marketOperationUtils, ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, @@ -566,22 +537,6 @@ describe('MarketOperationUtils tests', () => { } return DEFAULT_OPS.getTwoHopSellQuotes(...args); }, - getBalancerSellQuotesOffChainAsync: ( - makerToken: string, - takerToken: string, - takerFillAmounts: BigNumber[], - ) => { - sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer); - return DEFAULT_OPS.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, takerFillAmounts); - }, - getCreamSellQuotesOffChainAsync: ( - makerToken: string, - takerToken: string, - takerFillAmounts: BigNumber[], - ) => { - sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Cream); - return DEFAULT_OPS.getCreamSellQuotesOffChainAsync(makerToken, takerToken, takerFillAmounts); - }, }); await getMarketSellOrdersAsync(marketOperationUtils, ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, @@ -612,22 +567,6 @@ describe('MarketOperationUtils tests', () => { } return DEFAULT_OPS.getTwoHopSellQuotes(sources, ...args); }, - getBalancerSellQuotesOffChainAsync: ( - makerToken: string, - takerToken: string, - takerFillAmounts: BigNumber[], - ) => { - sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer); - return DEFAULT_OPS.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, takerFillAmounts); - }, - getCreamSellQuotesOffChainAsync: ( - makerToken: string, - takerToken: string, - takerFillAmounts: BigNumber[], - ) => { - sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Cream); - return DEFAULT_OPS.getCreamSellQuotesOffChainAsync(makerToken, takerToken, takerFillAmounts); - }, }); await getMarketSellOrdersAsync(marketOperationUtils, ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, @@ -1429,22 +1368,6 @@ describe('MarketOperationUtils tests', () => { } return DEFAULT_OPS.getTwoHopBuyQuotes(..._args); }, - getBalancerBuyQuotesOffChainAsync: ( - makerToken: string, - takerToken: string, - makerFillAmounts: BigNumber[], - ) => { - sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer); - return DEFAULT_OPS.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, makerFillAmounts); - }, - getCreamBuyQuotesOffChainAsync: ( - makerToken: string, - takerToken: string, - makerFillAmounts: BigNumber[], - ) => { - sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Cream); - return DEFAULT_OPS.getCreamBuyQuotesOffChainAsync(makerToken, takerToken, makerFillAmounts); - }, }); await getMarketBuyOrdersAsync(marketOperationUtils, ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, @@ -1475,22 +1398,6 @@ describe('MarketOperationUtils tests', () => { } return DEFAULT_OPS.getTwoHopBuyQuotes(..._args); }, - getBalancerBuyQuotesOffChainAsync: ( - makerToken: string, - takerToken: string, - makerFillAmounts: BigNumber[], - ) => { - sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer); - return DEFAULT_OPS.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, makerFillAmounts); - }, - getCreamBuyQuotesOffChainAsync: ( - makerToken: string, - takerToken: string, - makerFillAmounts: BigNumber[], - ) => { - sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Cream); - return DEFAULT_OPS.getCreamBuyQuotesOffChainAsync(makerToken, takerToken, makerFillAmounts); - }, }); await getMarketBuyOrdersAsync(marketOperationUtils, ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, @@ -1521,22 +1428,6 @@ describe('MarketOperationUtils tests', () => { } return DEFAULT_OPS.getTwoHopBuyQuotes(..._args); }, - getBalancerBuyQuotesOffChainAsync: ( - makerToken: string, - takerToken: string, - makerFillAmounts: BigNumber[], - ) => { - sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer); - return DEFAULT_OPS.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, makerFillAmounts); - }, - getCreamBuyQuotesOffChainAsync: ( - makerToken: string, - takerToken: string, - makerFillAmounts: BigNumber[], - ) => { - sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Cream); - return DEFAULT_OPS.getCreamBuyQuotesOffChainAsync(makerToken, takerToken, makerFillAmounts); - }, }); await getMarketBuyOrdersAsync(marketOperationUtils, ORDERS, FILL_AMOUNT, { ...DEFAULT_OPTS, diff --git a/packages/asset-swapper/test/pools_cache_test.ts b/packages/asset-swapper/test/pools_cache_test.ts new file mode 100644 index 0000000000..0c7e4cf99e --- /dev/null +++ b/packages/asset-swapper/test/pools_cache_test.ts @@ -0,0 +1,68 @@ +import * as chai from 'chai'; +import 'mocha'; + +import { + BalancerPoolsCache, + BalancerV2PoolsCache, + CreamPoolsCache, + PoolsCache, +} from '../src/utils/market_operation_utils/pools_cache'; + +import { chaiSetup } from './utils/chai_setup'; + +chaiSetup.configure(); +const expect = chai.expect; + +const usdcAddress = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; +const daiAddress = '0x6b175474e89094c44da98b954eedeac495271d0f'; +const wethAddress = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; +const wbtcAddress = '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'; +const balAddress = '0xba100000625a3754423978a60c9317c58a424e3d'; +const creamAddress = '0x2ba592f78db6436527729929aaf6c908497cb200'; + +const timeoutMs = 5000; +const poolKeys: string[] = ['id', 'balanceIn', 'balanceOut', 'weightIn', 'weightOut', 'swapFee']; + +describe('Pools Caches for Balancer-based sampling', () => { + async function fetchAndAssertPoolsAsync(cache: PoolsCache, takerToken: string, makerToken: string): Promise { + const pools = await cache.getFreshPoolsForPairAsync(takerToken, makerToken, timeoutMs); + expect(pools.length).greaterThan(0, `Failed to find any pools for ${takerToken} and ${makerToken}`); + expect(pools[0]).not.undefined(); + expect(Object.keys(pools[0])).to.include.members(poolKeys); + const cachedPoolIds = cache.getCachedPoolAddressesForPair(takerToken, makerToken); + expect(cachedPoolIds).to.deep.equal(pools.map(p => p.id)); + } + + describe('BalancerPoolsCache', () => { + const cache = new BalancerPoolsCache(); + it('fetches pools', async () => { + const pairs = [[usdcAddress, daiAddress], [usdcAddress, wethAddress], [daiAddress, wethAddress]]; + await Promise.all( + // tslint:disable-next-line:promise-function-async + pairs.map(([takerToken, makerToken]) => fetchAndAssertPoolsAsync(cache, takerToken, makerToken)), + ); + }); + }); + + describe('BalancerV2PoolsCache', () => { + const cache = new BalancerV2PoolsCache(); + it('fetches pools', async () => { + const pairs = [[wethAddress, wbtcAddress], [wethAddress, balAddress]]; + await Promise.all( + // tslint:disable-next-line:promise-function-async + pairs.map(([takerToken, makerToken]) => fetchAndAssertPoolsAsync(cache, takerToken, makerToken)), + ); + }); + }); + + describe('CreamPoolsCache', () => { + const cache = new CreamPoolsCache(); + it('fetches pools', async () => { + const pairs = [[usdcAddress, creamAddress], [creamAddress, wethAddress]]; + await Promise.all( + // tslint:disable-next-line:promise-function-async + pairs.map(([takerToken, makerToken]) => fetchAndAssertPoolsAsync(cache, takerToken, makerToken)), + ); + }); + }); +}); diff --git a/packages/asset-swapper/test/utils/mock_balancer_pools_cache.ts b/packages/asset-swapper/test/utils/mock_balancer_pools_cache.ts deleted file mode 100644 index a5b12a2dcd..0000000000 --- a/packages/asset-swapper/test/utils/mock_balancer_pools_cache.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Pool } from '@balancer-labs/sor/dist/types'; - -import { BalancerPoolsCache } from '../../src/utils/market_operation_utils/balancer_utils'; - -export interface Handlers { - getPoolsForPairAsync: (takerToken: string, makerToken: string) => Promise; - _fetchPoolsForPairAsync: (takerToken: string, makerToken: string) => Promise; - _loadTopPoolsAsync: () => Promise; -} - -export class MockBalancerPoolsCache extends BalancerPoolsCache { - constructor(public handlers: Partial) { - super(); - } - - public async getPoolsForPairAsync(takerToken: string, makerToken: string): Promise { - return this.handlers.getPoolsForPairAsync - ? this.handlers.getPoolsForPairAsync(takerToken, makerToken) - : super.getPoolsForPairAsync(takerToken, makerToken); - } - - protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise { - return this.handlers._fetchPoolsForPairAsync - ? this.handlers._fetchPoolsForPairAsync(takerToken, makerToken) - : super._fetchPoolsForPairAsync(takerToken, makerToken); - } - - protected async _loadTopPoolsAsync(): Promise { - if (this.handlers && this.handlers._loadTopPoolsAsync) { - return this.handlers._loadTopPoolsAsync(); - } - } -} diff --git a/packages/asset-swapper/test/wrappers.ts b/packages/asset-swapper/test/wrappers.ts index 338e0f9885..8d3699ac75 100644 --- a/packages/asset-swapper/test/wrappers.ts +++ b/packages/asset-swapper/test/wrappers.ts @@ -6,6 +6,7 @@ export * from '../test/generated-wrappers/approximate_buys'; export * from '../test/generated-wrappers/balance_checker'; export * from '../test/generated-wrappers/balancer_sampler'; +export * from '../test/generated-wrappers/balancer_v2_sampler'; export * from '../test/generated-wrappers/bancor_sampler'; export * from '../test/generated-wrappers/curve_sampler'; export * from '../test/generated-wrappers/d_o_d_o_sampler'; diff --git a/packages/asset-swapper/tsconfig.json b/packages/asset-swapper/tsconfig.json index 1103ae24bd..c51b95867d 100644 --- a/packages/asset-swapper/tsconfig.json +++ b/packages/asset-swapper/tsconfig.json @@ -9,6 +9,7 @@ "test/generated-artifacts/ApproximateBuys.json", "test/generated-artifacts/BalanceChecker.json", "test/generated-artifacts/BalancerSampler.json", + "test/generated-artifacts/BalancerV2Sampler.json", "test/generated-artifacts/BancorSampler.json", "test/generated-artifacts/CurveSampler.json", "test/generated-artifacts/DODOSampler.json", diff --git a/packages/protocol-utils/CHANGELOG.json b/packages/protocol-utils/CHANGELOG.json index 36d0f2aed0..a421a6afaa 100644 --- a/packages/protocol-utils/CHANGELOG.json +++ b/packages/protocol-utils/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "1.6.0", + "changes": [ + { + "note": "Add BalancerV2, remove Smoothy, Component and Saddle in BridgeProtocol enum", + "pr": 206 + } + ] + }, { "timestamp": 1619596077, "version": "1.5.1", diff --git a/packages/protocol-utils/src/transformer_utils.ts b/packages/protocol-utils/src/transformer_utils.ts index 3c91817de9..9f928067f6 100644 --- a/packages/protocol-utils/src/transformer_utils.ts +++ b/packages/protocol-utils/src/transformer_utils.ts @@ -126,9 +126,7 @@ export enum BridgeProtocol { CoFiX, Nerve, MakerPsm, - Smoothy, - Component, - Saddle, + BalancerV2, } // tslint:enable: enum-naming diff --git a/yarn.lock b/yarn.lock index cfb7bdfcf1..622498d752 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4540,7 +4540,7 @@ columnify@^1.5.4: strip-ansi "^3.0.0" wcwidth "^1.0.0" -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" dependencies: @@ -4879,6 +4879,13 @@ cross-fetch@^2.1.0, cross-fetch@^2.1.1: node-fetch "2.1.2" whatwg-fetch "2.0.4" +cross-fetch@^3.0.6: + version "3.1.4" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39" + integrity sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ== + dependencies: + node-fetch "2.6.1" + cross-spawn@^4: version "4.0.2" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" @@ -6375,6 +6382,11 @@ extract-comments@^1.1.0: esprima-extract-comments "^1.1.0" parse-code-context "^1.0.0" +extract-files@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-9.0.0.tgz#8a7744f2437f81f5ed3250ed9f1550de902fe54a" + integrity sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ== + extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" @@ -6680,6 +6692,15 @@ forever@^0.15.3: utile "~0.2.1" winston "~0.8.1" +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -7218,6 +7239,20 @@ graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.1 version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" +graphql-request@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-3.4.0.tgz#3a400cd5511eb3c064b1873afb059196bbea9c2b" + integrity sha512-acrTzidSlwAj8wBNO7Q/UQHS8T+z5qRGquCQRv9J1InwR01BBWV9ObnoE+JS5nCCEj8wSGS0yrDXVDoRiKZuOg== + dependencies: + cross-fetch "^3.0.6" + extract-files "^9.0.0" + form-data "^3.0.0" + +graphql@^15.4.0: + version "15.5.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.5.0.tgz#39d19494dbe69d1ea719915b578bf920344a69d5" + integrity sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA== + growl@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" @@ -9567,6 +9602,10 @@ node-fetch@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5" +node-fetch@2.6.1, node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + node-fetch@^1.0.1, node-fetch@~1.7.1: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" @@ -9574,10 +9613,6 @@ node-fetch@^1.0.1, node-fetch@~1.7.1: encoding "^0.1.11" is-stream "^1.0.1" -node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - node-gyp-build@^4.2.0: version "4.2.3" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739"