feat: Balancer V2 integration (#206)

* add balancer v2

* fetch balancer v2 pools from subgraph

* feat: initial stab at a Balancer V2 Sampler WIP

* feat: add sampling for buys and fix build issues WIP [untested]

* fix: BalancerV2Sampler implementation issues, works on Kovan

* chore: BalancerV2 sampling boilerplate

* fix: update Balancer V2 mainnet address

* fix: consolidate differences between the 2 working branches

* fix: use mainnet Balancer V2 subgraph

* fix: stack too deep by minimizing and inline Balancer V2 vault interface

* fix: address review comments and clean up

* fix: sampler vault interface and pools cache assuming a pool has swaps

* address more review comments

* fix: TS type issues and add a comment about deadline argument

* fix: pools_cache_tests incorrect token addresses, prettier incompat

* fix: make ERC20BridgeSampler support BalancerV2 non view sampler fns

* fix: use a struct for passing encoded bridge data for Balancer V2

* chore: add changelog entries

* fix: improve gas accuracy of gas schedule for Balancer V2 & Maker Psm

* fix: don't exclude sources with stale caches & wait for cache refresh

* rebase

* `@0x/asset-swapper`: Fix stack too deep errors in sampler

Co-authored-by: Kim Persson <kimpersson88@gmail.com>
Co-authored-by: Lawrence Forman <me@merklejerk.com>
This commit is contained in:
Xianny 2021-05-05 02:01:28 -07:00 committed by GitHub
parent a2643674ca
commit f9a794af93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 933 additions and 748 deletions

View File

@ -5,6 +5,10 @@
{ {
"note": "Added ETH support to `MixinCurve`", "note": "Added ETH support to `MixinCurve`",
"pr": 220 "pr": 220
},
{
"note": "Add Balancer V2 integration",
"pr": 206
} }
] ]
}, },

View File

@ -23,6 +23,7 @@ pragma experimental ABIEncoderV2;
import "./IBridgeAdapter.sol"; import "./IBridgeAdapter.sol";
import "./BridgeProtocols.sol"; import "./BridgeProtocols.sol";
import "./mixins/MixinBalancer.sol"; import "./mixins/MixinBalancer.sol";
import "./mixins/MixinBalancerV2.sol";
import "./mixins/MixinBancor.sol"; import "./mixins/MixinBancor.sol";
import "./mixins/MixinCoFiX.sol"; import "./mixins/MixinCoFiX.sol";
import "./mixins/MixinCurve.sol"; import "./mixins/MixinCurve.sol";
@ -43,6 +44,7 @@ import "./mixins/MixinZeroExBridge.sol";
contract BridgeAdapter is contract BridgeAdapter is
IBridgeAdapter, IBridgeAdapter,
MixinBalancer, MixinBalancer,
MixinBalancerV2,
MixinBancor, MixinBancor,
MixinCoFiX, MixinCoFiX,
MixinCurve, MixinCurve,
@ -63,6 +65,7 @@ contract BridgeAdapter is
constructor(IEtherTokenV06 weth) constructor(IEtherTokenV06 weth)
public public
MixinBalancer() MixinBalancer()
MixinBalancerV2()
MixinBancor(weth) MixinBancor(weth)
MixinCoFiX() MixinCoFiX()
MixinCurve(weth) MixinCurve(weth)
@ -119,6 +122,13 @@ contract BridgeAdapter is
sellAmount, sellAmount,
order.bridgeData order.bridgeData
); );
} else if (protocolId == BridgeProtocols.BALANCERV2) {
boughtAmount = _tradeBalancerV2(
sellToken,
buyToken,
sellAmount,
order.bridgeData
);
} else if (protocolId == BridgeProtocols.KYBER) { } else if (protocolId == BridgeProtocols.KYBER) {
boughtAmount = _tradeKyber( boughtAmount = _tradeKyber(
sellToken, sellToken,

View File

@ -44,4 +44,5 @@ library BridgeProtocols {
uint128 internal constant COFIX = 14; uint128 internal constant COFIX = 14;
uint128 internal constant NERVE = 15; uint128 internal constant NERVE = 15;
uint128 internal constant MAKERPSM = 16; uint128 internal constant MAKERPSM = 16;
uint128 internal constant BALANCERV2 = 17;
} }

View File

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

View File

@ -43,7 +43,7 @@
"config": { "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", "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: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": { "repository": {
"type": "git", "type": "git",

View File

@ -76,6 +76,7 @@ import * as LiquidityProviderSandbox from '../test/generated-artifacts/Liquidity
import * as LogMetadataTransformer from '../test/generated-artifacts/LogMetadataTransformer.json'; import * as LogMetadataTransformer from '../test/generated-artifacts/LogMetadataTransformer.json';
import * as MetaTransactionsFeature from '../test/generated-artifacts/MetaTransactionsFeature.json'; import * as MetaTransactionsFeature from '../test/generated-artifacts/MetaTransactionsFeature.json';
import * as MixinBalancer from '../test/generated-artifacts/MixinBalancer.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 MixinBancor from '../test/generated-artifacts/MixinBancor.json';
import * as MixinCoFiX from '../test/generated-artifacts/MixinCoFiX.json'; import * as MixinCoFiX from '../test/generated-artifacts/MixinCoFiX.json';
import * as MixinCryptoCom from '../test/generated-artifacts/MixinCryptoCom.json'; import * as MixinCryptoCom from '../test/generated-artifacts/MixinCryptoCom.json';
@ -235,6 +236,7 @@ export const artifacts = {
BridgeProtocols: BridgeProtocols as ContractArtifact, BridgeProtocols: BridgeProtocols as ContractArtifact,
IBridgeAdapter: IBridgeAdapter as ContractArtifact, IBridgeAdapter: IBridgeAdapter as ContractArtifact,
MixinBalancer: MixinBalancer as ContractArtifact, MixinBalancer: MixinBalancer as ContractArtifact,
MixinBalancerV2: MixinBalancerV2 as ContractArtifact,
MixinBancor: MixinBancor as ContractArtifact, MixinBancor: MixinBancor as ContractArtifact,
MixinCoFiX: MixinCoFiX as ContractArtifact, MixinCoFiX: MixinCoFiX as ContractArtifact,
MixinCryptoCom: MixinCryptoCom as ContractArtifact, MixinCryptoCom: MixinCryptoCom as ContractArtifact,

View File

@ -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/log_metadata_transformer';
export * from '../test/generated-wrappers/meta_transactions_feature'; export * from '../test/generated-wrappers/meta_transactions_feature';
export * from '../test/generated-wrappers/mixin_balancer'; 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_bancor';
export * from '../test/generated-wrappers/mixin_co_fi_x'; export * from '../test/generated-wrappers/mixin_co_fi_x';
export * from '../test/generated-wrappers/mixin_crypto_com'; export * from '../test/generated-wrappers/mixin_crypto_com';

View File

@ -105,6 +105,7 @@
"test/generated-artifacts/LogMetadataTransformer.json", "test/generated-artifacts/LogMetadataTransformer.json",
"test/generated-artifacts/MetaTransactionsFeature.json", "test/generated-artifacts/MetaTransactionsFeature.json",
"test/generated-artifacts/MixinBalancer.json", "test/generated-artifacts/MixinBalancer.json",
"test/generated-artifacts/MixinBalancerV2.json",
"test/generated-artifacts/MixinBancor.json", "test/generated-artifacts/MixinBancor.json",
"test/generated-artifacts/MixinCoFiX.json", "test/generated-artifacts/MixinCoFiX.json",
"test/generated-artifacts/MixinCryptoCom.json", "test/generated-artifacts/MixinCryptoCom.json",

View File

@ -29,6 +29,10 @@
{ {
"note": "PLP now includes a fallback due to observed collisions", "note": "PLP now includes a fallback due to observed collisions",
"pr": 223 "pr": 223
},
{
"note": "Add Balancer V2 integration",
"pr": 206
} }
] ]
}, },

View File

@ -6,11 +6,7 @@
"shouldSaveStandardInput": true, "shouldSaveStandardInput": true,
"compilerSettings": { "compilerSettings": {
"evmVersion": "istanbul", "evmVersion": "istanbul",
"optimizer": { "optimizer": { "enabled": true, "runs": 200 },
"enabled": true,
"runs": 62500,
"details": { "yul": true, "deduplicate": true, "cse": true, "constantOptimizer": true }
},
"outputSelection": { "outputSelection": {
"*": { "*": {
"*": [ "*": [

View File

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

View File

@ -22,16 +22,13 @@ pragma experimental ABIEncoderV2;
import "./interfaces/IBancor.sol"; import "./interfaces/IBancor.sol";
contract DeploymentConstants {} contract BancorSampler {
contract BancorSampler is DeploymentConstants
{
/// @dev Base gas limit for Bancor calls. /// @dev Base gas limit for Bancor calls.
uint256 constant private BANCOR_CALL_GAS = 300e3; // 300k uint256 constant private BANCOR_CALL_GAS = 300e3; // 300k
struct BancorSamplerOpts { struct BancorSamplerOpts {
address registry; IBancorRegistry registry;
address[][] paths; address[][] paths;
} }
@ -112,7 +109,7 @@ contract BancorSampler is DeploymentConstants
view view
returns (address bancorNetwork, address[] memory path) returns (address bancorNetwork, address[] memory path)
{ {
bancorNetwork = _getBancorNetwork(opts.registry); bancorNetwork = opts.registry.getAddress(opts.registry.BANCOR_NETWORK());
if (opts.paths.length == 0) { if (opts.paths.length == 0) {
return (bancorNetwork, path); 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());
}
} }

View File

@ -21,6 +21,7 @@ pragma solidity ^0.6;
pragma experimental ABIEncoderV2; pragma experimental ABIEncoderV2;
import "./BalancerSampler.sol"; import "./BalancerSampler.sol";
import "./BalancerV2Sampler.sol";
import "./BancorSampler.sol"; import "./BancorSampler.sol";
import "./CurveSampler.sol"; import "./CurveSampler.sol";
import "./DODOSampler.sol"; import "./DODOSampler.sol";
@ -43,6 +44,7 @@ import "./UtilitySampler.sol";
contract ERC20BridgeSampler is contract ERC20BridgeSampler is
BalancerSampler, BalancerSampler,
BalancerV2Sampler,
BancorSampler, BancorSampler,
CurveSampler, CurveSampler,
DODOSampler, DODOSampler,
@ -73,7 +75,6 @@ contract ERC20BridgeSampler is
/// @return callResults ABI-encoded results data for each call. /// @return callResults ABI-encoded results data for each call.
function batchCall(bytes[] calldata callDatas) function batchCall(bytes[] calldata callDatas)
external external
view
returns (CallResults[] memory callResults) returns (CallResults[] memory callResults)
{ {
callResults = new CallResults[](callDatas.length); callResults = new CallResults[](callDatas.length);
@ -82,7 +83,7 @@ contract ERC20BridgeSampler is
if (callDatas[i].length == 0) { if (callDatas[i].length == 0) {
continue; continue;
} }
(callResults[i].success, callResults[i].data) = address(this).staticcall(callDatas[i]); (callResults[i].success, callResults[i].data) = address(this).call(callDatas[i]);
} }
} }
} }

View File

@ -38,7 +38,7 @@
"config": { "config": {
"publicInterfaceContracts": "ERC20BridgeSampler,BalanceChecker,FakeTaker", "publicInterfaceContracts": "ERC20BridgeSampler,BalanceChecker,FakeTaker",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./test/generated-artifacts/@(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": { "postpublish": {
"assets": [] "assets": []
} }
@ -85,6 +85,8 @@
"ethereum-types": "^3.5.0", "ethereum-types": "^3.5.0",
"ethereumjs-util": "^7.0.10", "ethereumjs-util": "^7.0.10",
"fast-abi": "^0.0.2", "fast-abi": "^0.0.2",
"graphql": "^15.4.0",
"graphql-request": "^3.4.0",
"heartbeats": "^5.0.1", "heartbeats": "^5.0.1",
"lodash": "^4.17.11" "lodash": "^4.17.11"
}, },

View File

@ -143,8 +143,7 @@ export class SwapQuoter {
this.chainId, this.chainId,
samplerContract, samplerContract,
samplerOverrides, samplerOverrides,
undefined, // balancer pool cache undefined, // pools caches for balancer and cream
undefined, // cream pool cache
tokenAdjacencyGraph, tokenAdjacencyGraph,
liquidityProviderRegistry, liquidityProviderRegistry,
this.chainId === ChainId.Mainnet // Enable Bancor only on Mainnet this.chainId === ChainId.Mainnet // Enable Bancor only on Mainnet

View File

@ -67,6 +67,7 @@ export const SELL_SOURCE_FILTER_BY_CHAIN_ID = valueByChainId<SourceFilters>(
ERC20BridgeSource.Kyber, ERC20BridgeSource.Kyber,
ERC20BridgeSource.Curve, ERC20BridgeSource.Curve,
ERC20BridgeSource.Balancer, ERC20BridgeSource.Balancer,
ERC20BridgeSource.BalancerV2,
ERC20BridgeSource.Bancor, ERC20BridgeSource.Bancor,
ERC20BridgeSource.MStable, ERC20BridgeSource.MStable,
ERC20BridgeSource.Mooniswap, ERC20BridgeSource.Mooniswap,
@ -134,6 +135,7 @@ export const BUY_SOURCE_FILTER_BY_CHAIN_ID = valueByChainId<SourceFilters>(
ERC20BridgeSource.Kyber, ERC20BridgeSource.Kyber,
ERC20BridgeSource.Curve, ERC20BridgeSource.Curve,
ERC20BridgeSource.Balancer, ERC20BridgeSource.Balancer,
ERC20BridgeSource.BalancerV2,
// ERC20BridgeSource.Bancor, // FIXME: Bancor Buys not implemented in Sampler // ERC20BridgeSource.Bancor, // FIXME: Bancor Buys not implemented in Sampler
ERC20BridgeSource.MStable, ERC20BridgeSource.MStable,
ERC20BridgeSource.Mooniswap, ERC20BridgeSource.Mooniswap,
@ -1037,9 +1039,17 @@ export const COMPONENT_POOLS_BY_CHAIN_ID = valueByChainId(
}, },
); );
export const BALANCER_V2_VAULT_ADDRESS_BY_CHAIN = valueByChainId<string>(
{
[ChainId.Mainnet]: '0xba12222222228d8ba445958a75a0704d566bf2c8',
},
NULL_ADDRESS,
);
export const BALANCER_SUBGRAPH_URL = 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer'; export const BALANCER_SUBGRAPH_URL = 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer';
export const BALANCER_TOP_POOLS_FETCHED = 250; export const BALANCER_TOP_POOLS_FETCHED = 250;
export const BALANCER_MAX_POOLS_FETCHED = 3; export const BALANCER_MAX_POOLS_FETCHED = 3;
export const BALANCER_V2_SUBGRAPH_URL = 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-v2';
// //
// BSC // BSC
@ -1163,17 +1173,12 @@ export const DEFAULT_GAS_SCHEDULE: Required<FeeSchedule> = {
return gas; return gas;
}, },
[ERC20BridgeSource.Balancer]: () => 120e3, [ERC20BridgeSource.Balancer]: () => 120e3,
[ERC20BridgeSource.BalancerV2]: () => 100e3,
[ERC20BridgeSource.Cream]: () => 120e3, [ERC20BridgeSource.Cream]: () => 120e3,
[ERC20BridgeSource.MStable]: () => 700e3, [ERC20BridgeSource.MStable]: () => 700e3,
[ERC20BridgeSource.MakerPsm]: (fillData?: FillData) => { [ERC20BridgeSource.MakerPsm]: (fillData?: FillData) => {
const psmFillData = fillData as MakerPsmFillData; const psmFillData = fillData as MakerPsmFillData;
return psmFillData.takerToken === psmFillData.gemTokenAddress ? 210e3 : 290e3;
// 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;
}
}, },
[ERC20BridgeSource.Mooniswap]: () => 130e3, [ERC20BridgeSource.Mooniswap]: () => 130e3,
[ERC20BridgeSource.Shell]: () => 170e3, [ERC20BridgeSource.Shell]: () => 170e3,

View File

@ -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<Pool[]> {
const timeout = new Promise<Pool[]>(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<Pool[]> {
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<void> {
// Do nothing
}
// tslint:disable-next-line:prefer-function-over-method
protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]> {
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 [];
}
}
}

View File

@ -46,6 +46,7 @@ import {
OptimizerResult, OptimizerResult,
OptimizerResultWithReport, OptimizerResultWithReport,
OrderDomain, OrderDomain,
SourcesWithPoolsCache,
} from './types'; } from './types';
// tslint:disable:boolean-naming // tslint:disable:boolean-naming
@ -100,29 +101,15 @@ export class MarketOperationUtils {
const quoteSourceFilters = this._sellSources.merge(requestFilters); const quoteSourceFilters = this._sellSources.merge(requestFilters);
const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources); const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources);
const { // Can't sample Balancer or Cream on-chain without the pools cache
onChain: sampleBalancerOnChain, const sourcesWithStaleCaches: SourcesWithPoolsCache[] = (Object.keys(
offChain: sampleBalancerOffChain, this._sampler.poolsCaches,
} = this._sampler.balancerPoolsCache.howToSampleBalancer( ) as SourcesWithPoolsCache[]).filter(s => !this._sampler.poolsCaches[s].isFresh(takerToken, makerToken));
takerToken, // tslint:disable-next-line:promise-function-async
makerToken, const cacheRefreshPromises: Array<Promise<any[]>> = sourcesWithStaleCaches.map(s =>
quoteSourceFilters.isAllowed(ERC20BridgeSource.Balancer), 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 // Used to determine whether the tx origin is an EOA or a contract
const txOrigin = (_opts.rfqt && _opts.rfqt.txOrigin) || NULL_ADDRESS; const txOrigin = (_opts.rfqt && _opts.rfqt.txOrigin) || NULL_ADDRESS;
@ -146,12 +133,7 @@ export class MarketOperationUtils {
this._nativeFeeTokenAmount, this._nativeFeeTokenAmount,
), ),
// Get sell quotes for taker -> maker. // Get sell quotes for taker -> maker.
this._sampler.getSellQuotes( this._sampler.getSellQuotes(quoteSourceFilters.sources, makerToken, takerToken, sampleAmounts),
quoteSourceFilters.exclude(offChainSources).sources,
makerToken,
takerToken,
sampleAmounts,
),
this._sampler.getTwoHopSellQuotes( this._sampler.getTwoHopSellQuotes(
quoteSourceFilters.isAllowed(ERC20BridgeSource.MultiHop) ? quoteSourceFilters.sources : [], quoteSourceFilters.isAllowed(ERC20BridgeSource.MultiHop) ? quoteSourceFilters.sources : [],
makerToken, makerToken,
@ -160,15 +142,6 @@ export class MarketOperationUtils {
), ),
this._sampler.isAddressContract(txOrigin), 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 [ const [
[ [
tokenDecimals, tokenDecimals,
@ -179,9 +152,7 @@ export class MarketOperationUtils {
rawTwoHopQuotes, rawTwoHopQuotes,
isTxOriginContract, isTxOriginContract,
], ],
offChainBalancerQuotes, ] = await Promise.all([samplerPromise, Promise.all(cacheRefreshPromises)]);
offChainCreamQuotes,
] = await Promise.all([samplerPromise, offChainBalancerPromise, offChainCreamPromise]);
// Filter out any invalid two hop quotes where we couldn't find a route // Filter out any invalid two hop quotes where we couldn't find a route
const twoHopQuotes = rawTwoHopQuotes.filter( const twoHopQuotes = rawTwoHopQuotes.filter(
@ -210,7 +181,7 @@ export class MarketOperationUtils {
nativeOrders: limitOrdersWithFillableAmounts, nativeOrders: limitOrdersWithFillableAmounts,
rfqtIndicativeQuotes: [], rfqtIndicativeQuotes: [],
twoHopQuotes, twoHopQuotes,
dexQuotes: dexQuotes.concat([...offChainBalancerQuotes, ...offChainCreamQuotes]), dexQuotes,
}, },
isRfqSupported, isRfqSupported,
}; };
@ -236,29 +207,15 @@ export class MarketOperationUtils {
const quoteSourceFilters = this._buySources.merge(requestFilters); const quoteSourceFilters = this._buySources.merge(requestFilters);
const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources); const feeSourceFilters = this._feeSources.exclude(_opts.excludedFeeSources);
const { // Can't sample Balancer or Cream on-chain without the pools cache
onChain: sampleBalancerOnChain, const sourcesWithStaleCaches: SourcesWithPoolsCache[] = (Object.keys(
offChain: sampleBalancerOffChain, this._sampler.poolsCaches,
} = this._sampler.balancerPoolsCache.howToSampleBalancer( ) as SourcesWithPoolsCache[]).filter(s => !this._sampler.poolsCaches[s].isFresh(takerToken, makerToken));
takerToken, // tslint:disable-next-line:promise-function-async
makerToken, const cacheRefreshPromises: Array<Promise<any[]>> = sourcesWithStaleCaches.map(s =>
quoteSourceFilters.isAllowed(ERC20BridgeSource.Balancer), 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 // Used to determine whether the tx origin is an EOA or a contract
const txOrigin = (_opts.rfqt && _opts.rfqt.txOrigin) || NULL_ADDRESS; const txOrigin = (_opts.rfqt && _opts.rfqt.txOrigin) || NULL_ADDRESS;
@ -282,12 +239,7 @@ export class MarketOperationUtils {
this._nativeFeeTokenAmount, this._nativeFeeTokenAmount,
), ),
// Get buy quotes for taker -> maker. // Get buy quotes for taker -> maker.
this._sampler.getBuyQuotes( this._sampler.getBuyQuotes(quoteSourceFilters.sources, makerToken, takerToken, sampleAmounts),
quoteSourceFilters.exclude(offChainSources).sources,
makerToken,
takerToken,
sampleAmounts,
),
this._sampler.getTwoHopBuyQuotes( this._sampler.getTwoHopBuyQuotes(
quoteSourceFilters.isAllowed(ERC20BridgeSource.MultiHop) ? quoteSourceFilters.sources : [], quoteSourceFilters.isAllowed(ERC20BridgeSource.MultiHop) ? quoteSourceFilters.sources : [],
makerToken, makerToken,
@ -297,14 +249,6 @@ export class MarketOperationUtils {
this._sampler.isAddressContract(txOrigin), 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 [ const [
[ [
tokenDecimals, tokenDecimals,
@ -315,9 +259,7 @@ export class MarketOperationUtils {
rawTwoHopQuotes, rawTwoHopQuotes,
isTxOriginContract, isTxOriginContract,
], ],
offChainBalancerQuotes, ] = await Promise.all([samplerPromise, Promise.all(cacheRefreshPromises)]);
offChainCreamQuotes,
] = await Promise.all([samplerPromise, offChainBalancerPromise, offChainCreamPromise]);
// Filter out any invalid two hop quotes where we couldn't find a route // Filter out any invalid two hop quotes where we couldn't find a route
const twoHopQuotes = rawTwoHopQuotes.filter( const twoHopQuotes = rawTwoHopQuotes.filter(
@ -346,7 +288,7 @@ export class MarketOperationUtils {
nativeOrders: limitOrdersWithFillableAmounts, nativeOrders: limitOrdersWithFillableAmounts,
rfqtIndicativeQuotes: [], rfqtIndicativeQuotes: [],
twoHopQuotes, twoHopQuotes,
dexQuotes: dexQuotes.concat(offChainBalancerQuotes, offChainCreamQuotes), dexQuotes,
}, },
isRfqSupported, isRfqSupported,
}; };

View File

@ -7,6 +7,7 @@ import { MAX_UINT256, ZERO_AMOUNT } from './constants';
import { import {
AggregationError, AggregationError,
BalancerFillData, BalancerFillData,
BalancerV2FillData,
BancorFillData, BancorFillData,
CollapsedFill, CollapsedFill,
CurveFillData, CurveFillData,
@ -75,6 +76,8 @@ export function getErc20BridgeSourceToBridgeSource(source: ERC20BridgeSource): s
switch (source) { switch (source) {
case ERC20BridgeSource.Balancer: case ERC20BridgeSource.Balancer:
return encodeBridgeSourceId(BridgeProtocol.Balancer, 'Balancer'); return encodeBridgeSourceId(BridgeProtocol.Balancer, 'Balancer');
case ERC20BridgeSource.BalancerV2:
return encodeBridgeSourceId(BridgeProtocol.BalancerV2, 'BalancerV2');
case ERC20BridgeSource.Bancor: case ERC20BridgeSource.Bancor:
return encodeBridgeSourceId(BridgeProtocol.Bancor, 'Bancor'); return encodeBridgeSourceId(BridgeProtocol.Bancor, 'Bancor');
// case ERC20BridgeSource.CoFiX: // case ERC20BridgeSource.CoFiX:
@ -189,6 +192,11 @@ export function createBridgeDataForBridgeOrder(order: OptimizedMarketBridgeOrder
const balancerFillData = (order as OptimizedMarketBridgeOrder<BalancerFillData>).fillData; const balancerFillData = (order as OptimizedMarketBridgeOrder<BalancerFillData>).fillData;
bridgeData = encoder.encode([balancerFillData.poolAddress]); bridgeData = encoder.encode([balancerFillData.poolAddress]);
break; break;
case ERC20BridgeSource.BalancerV2:
const balancerV2FillData = (order as OptimizedMarketBridgeOrder<BalancerV2FillData>).fillData;
const { vault, poolId } = balancerV2FillData;
bridgeData = encoder.encode([vault, poolId]);
break;
case ERC20BridgeSource.Bancor: case ERC20BridgeSource.Bancor:
const bancorFillData = (order as OptimizedMarketBridgeOrder<BancorFillData>).fillData; const bancorFillData = (order as OptimizedMarketBridgeOrder<BancorFillData>).fillData;
bridgeData = encoder.encode([bancorFillData.networkAddress, bancorFillData.path]); bridgeData = encoder.encode([bancorFillData.networkAddress, bancorFillData.path]);
@ -296,6 +304,7 @@ const makerPsmEncoder = AbiEncoder.create([
{ name: 'psmAddress', type: 'address' }, { name: 'psmAddress', type: 'address' },
{ name: 'gemTokenAddress', 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 routerAddressPathEncoder = AbiEncoder.create('(address,address[])');
const tokenAddressEncoder = AbiEncoder.create([{ name: 'tokenAddress', type: 'address' }]); const tokenAddressEncoder = AbiEncoder.create([{ name: 'tokenAddress', type: 'address' }]);
@ -358,6 +367,7 @@ export const BRIDGE_ENCODERS: {
[ERC20BridgeSource.Uniswap]: poolEncoder, [ERC20BridgeSource.Uniswap]: poolEncoder,
// Custom integrations // Custom integrations
[ERC20BridgeSource.MakerPsm]: makerPsmEncoder, [ERC20BridgeSource.MakerPsm]: makerPsmEncoder,
[ERC20BridgeSource.BalancerV2]: balancerV2Encoder,
}; };
function getFillTokenAmounts(fill: CollapsedFill, side: MarketOperation): [BigNumber, BigNumber] { function getFillTokenAmounts(fill: CollapsedFill, side: MarketOperation): [BigNumber, BigNumber] {

View File

@ -1,19 +1,12 @@
import { getPoolsWithTokens, parsePoolData } from '@balancer-labs/sor'; import { getPoolsWithTokens, parsePoolData } from '@balancer-labs/sor';
import { Pool } from '@balancer-labs/sor/dist/types'; 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 import { CacheValue, PoolsCache } from './pools_cache';
interface CacheValue {
timestamp: number;
pools: Pool[];
}
// tslint:disable:custom-no-magic-numbers // tslint:disable:custom-no-magic-numbers
const FIVE_SECONDS_MS = 5 * 1000;
const ONE_DAY_MS = 24 * 60 * 60 * 1000; const ONE_DAY_MS = 24 * 60 * 60 * 1000;
const DEFAULT_TIMEOUT_MS = 1000;
// tslint:enable:custom-no-magic-numbers // tslint:enable:custom-no-magic-numbers
interface BalancerPoolResponse { interface BalancerPoolResponse {
@ -23,89 +16,19 @@ interface BalancerPoolResponse {
tokensList: string[]; tokensList: string[];
totalWeight: string; totalWeight: string;
} }
export class BalancerPoolsCache extends PoolsCache {
export class BalancerPoolsCache {
constructor( 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 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(); void this._loadTopPoolsAsync();
// Reload the top pools every 12 hours // Reload the top pools every 12 hours
setInterval(async () => void this._loadTopPoolsAsync(), ONE_DAY_MS / 2); setInterval(async () => void this._loadTopPoolsAsync(), ONE_DAY_MS / 2);
} }
public async getPoolsForPairAsync(
takerToken: string,
makerToken: string,
timeoutMs: number = DEFAULT_TIMEOUT_MS,
): Promise<Pool[]> {
const timeout = new Promise<Pool[]>(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<Pool[]> {
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<Pool[]> { protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]> {
try { try {
const poolData = (await getPoolsWithTokens(takerToken, makerToken)).pools; const poolData = (await getPoolsWithTokens(takerToken, makerToken)).pools;
@ -153,7 +76,7 @@ export class BalancerPoolsCache {
const query = ` const query = `
query { query {
pools (first: ${ pools (first: ${
this.topPoolsFetched this._topPoolsFetched
}, where: {publicSwap: true, liquidity_gt: 0}, orderBy: swapsCount, orderDirection: desc) { }, where: {publicSwap: true, liquidity_gt: 0}, orderBy: swapsCount, orderDirection: desc) {
id id
publicSwap publicSwap
@ -172,7 +95,7 @@ export class BalancerPoolsCache {
} }
`; `;
try { try {
const response = await fetch(this.subgraphUrl, { const response = await fetch(this._subgraphUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',

View File

@ -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<Pool[]> {
// 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<Pool[]> {
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,
};
});
}
}

View File

@ -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<Pool[]> {
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 [];
}
}
}

View File

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

View File

@ -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<Pool[]> {
const timeout = new Promise<Pool[]>(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<Pool[]> {
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<Pool[]>;
}

View File

@ -4,11 +4,10 @@ import { BigNumber, NULL_BYTES } from '@0x/utils';
import { SamplerOverrides } from '../../types'; import { SamplerOverrides } from '../../types';
import { ERC20BridgeSamplerContract } from '../../wrappers'; import { ERC20BridgeSamplerContract } from '../../wrappers';
import { BalancerPoolsCache } from './balancer_utils';
import { BancorService } from './bancor_service'; import { BancorService } from './bancor_service';
import { CreamPoolsCache } from './cream_utils'; import { PoolsCache } from './pools_cache';
import { SamplerOperations } from './sampler_operations'; import { SamplerOperations } from './sampler_operations';
import { BatchedOperation, LiquidityProviderRegistry, TokenAdjacencyGraph } from './types'; import { BatchedOperation, ERC20BridgeSource, LiquidityProviderRegistry, TokenAdjacencyGraph } from './types';
/** /**
* Generate sample amounts up to `maxFillAmount`. * Generate sample amounts up to `maxFillAmount`.
@ -37,21 +36,12 @@ export class DexOrderSampler extends SamplerOperations {
public readonly chainId: ChainId, public readonly chainId: ChainId,
_samplerContract: ERC20BridgeSamplerContract, _samplerContract: ERC20BridgeSamplerContract,
private readonly _samplerOverrides?: SamplerOverrides, private readonly _samplerOverrides?: SamplerOverrides,
balancerPoolsCache?: BalancerPoolsCache, poolsCaches?: { [key in ERC20BridgeSource]: PoolsCache },
creamPoolsCache?: CreamPoolsCache,
tokenAdjacencyGraph?: TokenAdjacencyGraph, tokenAdjacencyGraph?: TokenAdjacencyGraph,
liquidityProviderRegistry?: LiquidityProviderRegistry, liquidityProviderRegistry?: LiquidityProviderRegistry,
bancorServiceFn: () => Promise<BancorService | undefined> = async () => undefined, bancorServiceFn: () => Promise<BancorService | undefined> = async () => undefined,
) { ) {
super( super(chainId, _samplerContract, poolsCaches, tokenAdjacencyGraph, liquidityProviderRegistry, bancorServiceFn);
chainId,
_samplerContract,
balancerPoolsCache,
creamPoolsCache,
tokenAdjacencyGraph,
liquidityProviderRegistry,
bancorServiceFn,
);
} }
/* Type overloads for `executeAsync()`. Could skip this if we would upgrade TS. */ /* Type overloads for `executeAsync()`. Could skip this if we would upgrade TS. */

View File

@ -6,7 +6,6 @@ import * as _ from 'lodash';
import { SamplerCallResult, SignedNativeOrder } from '../../types'; import { SamplerCallResult, SignedNativeOrder } from '../../types';
import { ERC20BridgeSamplerContract } from '../../wrappers'; import { ERC20BridgeSamplerContract } from '../../wrappers';
import { BalancerPoolsCache } from './balancer_utils';
import { BancorService } from './bancor_service'; import { BancorService } from './bancor_service';
import { import {
getCurveLikeInfosForPair, getCurveLikeInfosForPair,
@ -19,6 +18,7 @@ import {
uniswapV2LikeRouterAddress, uniswapV2LikeRouterAddress,
} from './bridge_source_utils'; } from './bridge_source_utils';
import { import {
BALANCER_V2_VAULT_ADDRESS_BY_CHAIN,
BANCOR_REGISTRY_BY_CHAIN_ID, BANCOR_REGISTRY_BY_CHAIN_ID,
DODO_CONFIG_BY_CHAIN_ID, DODO_CONFIG_BY_CHAIN_ID,
DODOV2_FACTORIES_BY_CHAIN_ID, DODOV2_FACTORIES_BY_CHAIN_ID,
@ -38,13 +38,15 @@ import {
UNISWAPV1_ROUTER_BY_CHAIN_ID, UNISWAPV1_ROUTER_BY_CHAIN_ID,
ZERO_AMOUNT, ZERO_AMOUNT,
} from './constants'; } from './constants';
import { CreamPoolsCache } from './cream_utils';
import { getLiquidityProvidersForPair } from './liquidity_provider_utils'; import { getLiquidityProvidersForPair } from './liquidity_provider_utils';
import { getIntermediateTokens } from './multihop_utils'; import { getIntermediateTokens } from './multihop_utils';
import { BalancerPoolsCache, BalancerV2PoolsCache, CreamPoolsCache, PoolsCache } from './pools_cache';
import { SamplerContractOperation } from './sampler_contract_operation'; import { SamplerContractOperation } from './sampler_contract_operation';
import { SourceFilters } from './source_filters'; import { SourceFilters } from './source_filters';
import { import {
BalancerFillData, BalancerFillData,
BalancerV2FillData,
BalancerV2PoolInfo,
BancorFillData, BancorFillData,
BatchedOperation, BatchedOperation,
CurveFillData, CurveFillData,
@ -64,6 +66,7 @@ import {
PsmInfo, PsmInfo,
ShellFillData, ShellFillData,
SourceQuoteOperation, SourceQuoteOperation,
SourcesWithPoolsCache,
TokenAdjacencyGraph, TokenAdjacencyGraph,
UniswapV2FillData, UniswapV2FillData,
} from './types'; } from './types';
@ -88,6 +91,7 @@ export const BATCH_SOURCE_FILTERS = SourceFilters.all().exclude([ERC20BridgeSour
*/ */
export class SamplerOperations { export class SamplerOperations {
public readonly liquidityProviderRegistry: LiquidityProviderRegistry; public readonly liquidityProviderRegistry: LiquidityProviderRegistry;
public readonly poolsCaches: { [key in SourcesWithPoolsCache]: PoolsCache };
protected _bancorService?: BancorService; protected _bancorService?: BancorService;
public static constant<T>(result: T): BatchedOperation<T> { public static constant<T>(result: T): BatchedOperation<T> {
return { return {
@ -100,8 +104,7 @@ export class SamplerOperations {
constructor( constructor(
public readonly chainId: ChainId, public readonly chainId: ChainId,
protected readonly _samplerContract: ERC20BridgeSamplerContract, protected readonly _samplerContract: ERC20BridgeSamplerContract,
public readonly balancerPoolsCache: BalancerPoolsCache = new BalancerPoolsCache(), poolsCaches?: { [key in SourcesWithPoolsCache]: PoolsCache },
public readonly creamPoolsCache: CreamPoolsCache = new CreamPoolsCache(),
protected readonly tokenAdjacencyGraph: TokenAdjacencyGraph = { default: [] }, protected readonly tokenAdjacencyGraph: TokenAdjacencyGraph = { default: [] },
liquidityProviderRegistry: LiquidityProviderRegistry = {}, liquidityProviderRegistry: LiquidityProviderRegistry = {},
bancorServiceFn: () => Promise<BancorService | undefined> = async () => undefined, bancorServiceFn: () => Promise<BancorService | undefined> = async () => undefined,
@ -110,6 +113,13 @@ export class SamplerOperations {
...LIQUIDITY_PROVIDER_REGISTRY_BY_CHAIN_ID[chainId], ...LIQUIDITY_PROVIDER_REGISTRY_BY_CHAIN_ID[chainId],
...liquidityProviderRegistry, ...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 // Initialize the Bancor service, fetching paths in the background
bancorServiceFn() bancorServiceFn()
.then(service => (this._bancorService = service)) .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<BalancerV2FillData> {
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<BalancerV2FillData> {
return new SamplerContractOperation({
source,
fillData: poolInfo,
contract: this._samplerContract,
function: this._samplerContract.sampleBuysFromBalancerV2,
params: [poolInfo, takerToken, makerToken, makerFillAmounts],
});
}
public getBalancerSellQuotes( public getBalancerSellQuotes(
poolAddress: string, poolAddress: string,
makerToken: string, makerToken: string,
@ -505,79 +547,6 @@ export class SamplerOperations {
}); });
} }
public async getBalancerSellQuotesOffChainAsync(
makerToken: string,
takerToken: string,
_takerFillAmounts: BigNumber[],
): Promise<Array<Array<DexSample<BalancerFillData>>>> {
// 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<Array<Array<DexSample<BalancerFillData>>>> {
// 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<Array<Array<DexSample<BalancerFillData>>>> {
// 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<Array<Array<DexSample<BalancerFillData>>>> {
// 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( public getMStableSellQuotes(
router: string, router: string,
makerToken: string, makerToken: string,
@ -1188,29 +1157,56 @@ export class SamplerOperations {
), ),
]; ];
case ERC20BridgeSource.Balancer: case ERC20BridgeSource.Balancer:
return this.balancerPoolsCache return (
.getCachedPoolAddressesForPair(takerToken, makerToken)! this.poolsCaches[ERC20BridgeSource.Balancer].getCachedPoolAddressesForPair(
.map(poolAddress => takerToken,
this.getBalancerSellQuotes( makerToken,
poolAddress, ) || []
makerToken, ).map(poolAddress =>
takerToken, this.getBalancerSellQuotes(
takerFillAmounts, poolAddress,
ERC20BridgeSource.Balancer, 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: case ERC20BridgeSource.Cream:
return this.creamPoolsCache return (
.getCachedPoolAddressesForPair(takerToken, makerToken)! this.poolsCaches[ERC20BridgeSource.Cream].getCachedPoolAddressesForPair(
.map(poolAddress => takerToken,
this.getBalancerSellQuotes( makerToken,
poolAddress, ) || []
makerToken, ).map(poolAddress =>
takerToken, this.getBalancerSellQuotes(
takerFillAmounts, poolAddress,
ERC20BridgeSource.Cream, makerToken,
), takerToken,
); takerFillAmounts,
ERC20BridgeSource.Cream,
),
);
case ERC20BridgeSource.Dodo: case ERC20BridgeSource.Dodo:
if (!isValidAddress(DODO_CONFIG_BY_CHAIN_ID[this.chainId].registry)) { if (!isValidAddress(DODO_CONFIG_BY_CHAIN_ID[this.chainId].registry)) {
return []; return [];
@ -1403,29 +1399,55 @@ export class SamplerOperations {
), ),
]; ];
case ERC20BridgeSource.Balancer: case ERC20BridgeSource.Balancer:
return this.balancerPoolsCache return (
.getCachedPoolAddressesForPair(takerToken, makerToken)! this.poolsCaches[ERC20BridgeSource.Balancer].getCachedPoolAddressesForPair(
.map(poolAddress => takerToken,
this.getBalancerBuyQuotes( makerToken,
poolAddress, ) || []
makerToken, ).map(poolAddress =>
takerToken, this.getBalancerBuyQuotes(
makerFillAmounts, poolAddress,
ERC20BridgeSource.Balancer, 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: case ERC20BridgeSource.Cream:
return this.creamPoolsCache return (
.getCachedPoolAddressesForPair(takerToken, makerToken)! this.poolsCaches[ERC20BridgeSource.Cream].getCachedPoolAddressesForPair(
.map(poolAddress => takerToken,
this.getBalancerBuyQuotes( makerToken,
poolAddress, ) || []
makerToken, ).map(poolAddress =>
takerToken, this.getBalancerBuyQuotes(
makerFillAmounts, poolAddress,
ERC20BridgeSource.Cream, makerToken,
), takerToken,
); makerFillAmounts,
ERC20BridgeSource.Cream,
),
);
case ERC20BridgeSource.Dodo: case ERC20BridgeSource.Dodo:
if (!isValidAddress(DODO_CONFIG_BY_CHAIN_ID[this.chainId].registry)) { if (!isValidAddress(DODO_CONFIG_BY_CHAIN_ID[this.chainId].registry)) {
return []; return [];

View File

@ -45,6 +45,7 @@ export enum ERC20BridgeSource {
LiquidityProvider = 'LiquidityProvider', LiquidityProvider = 'LiquidityProvider',
MultiBridge = 'MultiBridge', MultiBridge = 'MultiBridge',
Balancer = 'Balancer', Balancer = 'Balancer',
BalancerV2 = 'Balancer_V2',
Cream = 'CREAM', Cream = 'CREAM',
Bancor = 'Bancor', Bancor = 'Bancor',
MakerPsm = 'MakerPsm', MakerPsm = 'MakerPsm',
@ -76,6 +77,7 @@ export enum ERC20BridgeSource {
CheeseSwap = 'CheeseSwap', CheeseSwap = 'CheeseSwap',
JulSwap = 'JulSwap', JulSwap = 'JulSwap',
} }
export type SourcesWithPoolsCache = ERC20BridgeSource.Balancer | ERC20BridgeSource.BalancerV2 | ERC20BridgeSource.Cream;
// tslint:disable: enum-naming // tslint:disable: enum-naming
/** /**
@ -120,6 +122,14 @@ export interface PsmInfo {
gemTokenAddress: string; gemTokenAddress: string;
} }
/**
* Configuration info for a Balancer V2 pool.
*/
export interface BalancerV2PoolInfo {
poolId: string;
vault: string;
}
// Internal `fillData` field for `Fill` objects. // Internal `fillData` field for `Fill` objects.
export interface FillData {} export interface FillData {}
@ -145,6 +155,11 @@ export interface BalancerFillData extends FillData {
poolAddress: string; poolAddress: string;
} }
export interface BalancerV2FillData extends FillData {
vault: string;
poolId: string;
}
export interface UniswapV2FillData extends FillData { export interface UniswapV2FillData extends FillData {
tokenAddressPath: string[]; tokenAddressPath: string[];
router: string; router: string;

View File

@ -8,6 +8,7 @@ import { ContractArtifact } from 'ethereum-types';
import * as ApproximateBuys from '../test/generated-artifacts/ApproximateBuys.json'; import * as ApproximateBuys from '../test/generated-artifacts/ApproximateBuys.json';
import * as BalanceChecker from '../test/generated-artifacts/BalanceChecker.json'; import * as BalanceChecker from '../test/generated-artifacts/BalanceChecker.json';
import * as BalancerSampler from '../test/generated-artifacts/BalancerSampler.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 BancorSampler from '../test/generated-artifacts/BancorSampler.json';
import * as CurveSampler from '../test/generated-artifacts/CurveSampler.json'; import * as CurveSampler from '../test/generated-artifacts/CurveSampler.json';
import * as DODOSampler from '../test/generated-artifacts/DODOSampler.json'; import * as DODOSampler from '../test/generated-artifacts/DODOSampler.json';
@ -48,6 +49,7 @@ export const artifacts = {
ApproximateBuys: ApproximateBuys as ContractArtifact, ApproximateBuys: ApproximateBuys as ContractArtifact,
BalanceChecker: BalanceChecker as ContractArtifact, BalanceChecker: BalanceChecker as ContractArtifact,
BalancerSampler: BalancerSampler as ContractArtifact, BalancerSampler: BalancerSampler as ContractArtifact,
BalancerV2Sampler: BalancerV2Sampler as ContractArtifact,
BancorSampler: BancorSampler as ContractArtifact, BancorSampler: BancorSampler as ContractArtifact,
CurveSampler: CurveSampler as ContractArtifact, CurveSampler: CurveSampler as ContractArtifact,
DODOSampler: DODOSampler as ContractArtifact, DODOSampler: DODOSampler as ContractArtifact,

View File

@ -9,14 +9,12 @@ import {
} from '@0x/contracts-test-utils'; } from '@0x/contracts-test-utils';
import { FillQuoteTransformerOrderType, LimitOrderFields, SignatureType } from '@0x/protocol-utils'; import { FillQuoteTransformerOrderType, LimitOrderFields, SignatureType } from '@0x/protocol-utils';
import { BigNumber, hexUtils, NULL_ADDRESS } from '@0x/utils'; import { BigNumber, hexUtils, NULL_ADDRESS } from '@0x/utils';
import { Pool } from '@balancer-labs/sor/dist/types';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { SignedOrder } from '../src/types'; import { SignedOrder } from '../src/types';
import { DexOrderSampler, getSampleAmounts } from '../src/utils/market_operation_utils/sampler'; import { DexOrderSampler, getSampleAmounts } from '../src/utils/market_operation_utils/sampler';
import { ERC20BridgeSource, TokenAdjacencyGraph } from '../src/utils/market_operation_utils/types'; 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 { MockSamplerContract } from './utils/mock_sampler_contract';
import { generatePseudoRandomSalt } from './utils/utils'; import { generatePseudoRandomSalt } from './utils/utils';
@ -112,7 +110,6 @@ describe('DexSampler tests', () => {
undefined, undefined,
undefined, undefined,
undefined, undefined,
undefined,
async () => undefined, async () => undefined,
); );
const [fillableAmounts] = await dexOrderSampler.executeAsync( const [fillableAmounts] = await dexOrderSampler.executeAsync(
@ -137,7 +134,6 @@ describe('DexSampler tests', () => {
undefined, undefined,
undefined, undefined,
undefined, undefined,
undefined,
async () => undefined, async () => undefined,
); );
const [fillableAmounts] = await dexOrderSampler.executeAsync( const [fillableAmounts] = await dexOrderSampler.executeAsync(
@ -166,7 +162,6 @@ describe('DexSampler tests', () => {
undefined, undefined,
undefined, undefined,
undefined, undefined,
undefined,
async () => undefined, async () => undefined,
); );
const [fillableAmounts] = await dexOrderSampler.executeAsync( const [fillableAmounts] = await dexOrderSampler.executeAsync(
@ -200,7 +195,6 @@ describe('DexSampler tests', () => {
undefined, undefined,
undefined, undefined,
undefined, undefined,
undefined,
{ {
[poolAddress]: { tokens: [expectedMakerToken, expectedTakerToken], gasCost }, [poolAddress]: { tokens: [expectedMakerToken, expectedTakerToken], gasCost },
}, },
@ -245,7 +239,6 @@ describe('DexSampler tests', () => {
undefined, undefined,
undefined, undefined,
undefined, undefined,
undefined,
{ {
[poolAddress]: { tokens: [expectedMakerToken, expectedTakerToken], gasCost }, [poolAddress]: { tokens: [expectedMakerToken, expectedTakerToken], gasCost },
}, },
@ -291,7 +284,6 @@ describe('DexSampler tests', () => {
undefined, undefined,
undefined, undefined,
undefined, undefined,
undefined,
async () => undefined, async () => undefined,
); );
const [fillableAmounts] = await dexOrderSampler.executeAsync( const [fillableAmounts] = await dexOrderSampler.executeAsync(
@ -325,7 +317,6 @@ describe('DexSampler tests', () => {
undefined, undefined,
undefined, undefined,
undefined, undefined,
undefined,
async () => undefined, async () => undefined,
); );
const [fillableAmounts] = await dexOrderSampler.executeAsync( const [fillableAmounts] = await dexOrderSampler.executeAsync(
@ -358,7 +349,6 @@ describe('DexSampler tests', () => {
undefined, undefined,
undefined, undefined,
undefined, undefined,
undefined,
async () => undefined, async () => undefined,
); );
const [fillableAmounts] = await dexOrderSampler.executeAsync( const [fillableAmounts] = await dexOrderSampler.executeAsync(
@ -391,7 +381,6 @@ describe('DexSampler tests', () => {
undefined, undefined,
undefined, undefined,
undefined, undefined,
undefined,
async () => undefined, async () => undefined,
); );
const [fillableAmounts] = await dexOrderSampler.executeAsync( const [fillableAmounts] = await dexOrderSampler.executeAsync(
@ -425,7 +414,6 @@ describe('DexSampler tests', () => {
undefined, undefined,
undefined, undefined,
undefined, undefined,
undefined,
async () => undefined, async () => undefined,
); );
const [fillableAmounts] = await dexOrderSampler.executeAsync( const [fillableAmounts] = await dexOrderSampler.executeAsync(
@ -490,7 +478,6 @@ describe('DexSampler tests', () => {
sampler, sampler,
undefined, undefined,
undefined, undefined,
undefined,
tokenAdjacencyGraph, tokenAdjacencyGraph,
undefined, undefined,
async () => undefined, async () => undefined,
@ -542,38 +529,6 @@ describe('DexSampler tests', () => {
expect(quotes).to.have.lengthOf(sources.length + additionalSourceCount); expect(quotes).to.have.lengthOf(sources.length + additionalSourceCount);
expect(quotes).to.deep.eq(expectedQuotes.concat(uniswapV2ETHQuotes)); 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 () => { it('getBuyQuotes()', async () => {
const expectedTakerToken = randomAddress(); const expectedTakerToken = randomAddress();
const expectedMakerToken = randomAddress(); const expectedMakerToken = randomAddress();
@ -620,7 +575,6 @@ describe('DexSampler tests', () => {
sampler, sampler,
undefined, undefined,
undefined, undefined,
undefined,
tokenAdjacencyGraph, tokenAdjacencyGraph,
undefined, undefined,
async () => undefined, async () => undefined,
@ -665,80 +619,39 @@ describe('DexSampler tests', () => {
expect(quotes).to.have.lengthOf(sources.length + 1); expect(quotes).to.have.lengthOf(sources.length + 1);
expect(quotes).to.deep.eq(expectedQuotes.concat(uniswapV2ETHQuotes)); expect(quotes).to.deep.eq(expectedQuotes.concat(uniswapV2ETHQuotes));
}); });
it('getBuyQuotes() uses samples from Balancer', async () => { describe('batched operations', () => {
const expectedTakerToken = randomAddress(); it('getLimitOrderFillableMakerAssetAmounts(), getLimitOrderFillableTakerAssetAmounts()', async () => {
const expectedMakerToken = randomAddress(); const expectedFillableTakerAmounts = ORDERS.map(() => getRandomInteger(0, 100e18));
const expectedMakerFillAmounts = getSampleAmounts(new BigNumber(100e18), 3); const expectedFillableMakerAmounts = ORDERS.map(() => getRandomInteger(0, 100e18));
const pools: Pool[] = [generateBalancerPool(), generateBalancerPool()]; const sampler = new MockSamplerContract({
const balancerPoolsCache = new MockBalancerPoolsCache({ getLimitOrderFillableMakerAssetAmounts: (orders, signatures) => {
getPoolsForPairAsync: async (takerToken: string, makerToken: string) => { expect(orders).to.deep.eq(SIMPLE_ORDERS);
expect(takerToken).equal(expectedTakerToken); expect(signatures).to.deep.eq(ORDERS.map(o => o.signature));
expect(makerToken).equal(expectedMakerToken); return expectedFillableMakerAmounts;
return Promise.resolve(pools); },
}, 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 // tslint:disable-next-line: max-file-line-count

View File

@ -11,21 +11,21 @@ import {
import { FillQuoteTransformerOrderType, LimitOrder, RfqOrder, SignatureType } from '@0x/protocol-utils'; import { FillQuoteTransformerOrderType, LimitOrder, RfqOrder, SignatureType } from '@0x/protocol-utils';
import { BigNumber, hexUtils, NULL_BYTES } from '@0x/utils'; import { BigNumber, hexUtils, NULL_BYTES } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper'; import { Web3Wrapper } from '@0x/web3-wrapper';
import { Pool } from '@balancer-labs/sor/dist/types';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as TypeMoq from 'typemoq'; import * as TypeMoq from 'typemoq';
import { MarketOperation, QuoteRequestor, RfqRequestOpts, SignedNativeOrder } from '../src'; import { MarketOperation, QuoteRequestor, RfqRequestOpts, SignedNativeOrder } from '../src';
import { NativeOrderWithFillableAmounts } from '../src/types'; import { NativeOrderWithFillableAmounts } from '../src/types';
import { MarketOperationUtils } from '../src/utils/market_operation_utils/'; import { MarketOperationUtils } from '../src/utils/market_operation_utils/';
import { BalancerPoolsCache } from '../src/utils/market_operation_utils/balancer_utils';
import { import {
BUY_SOURCE_FILTER_BY_CHAIN_ID, BUY_SOURCE_FILTER_BY_CHAIN_ID,
POSITIVE_INF, POSITIVE_INF,
SELL_SOURCE_FILTER_BY_CHAIN_ID, SELL_SOURCE_FILTER_BY_CHAIN_ID,
SOURCE_FLAGS, SOURCE_FLAGS,
} from '../src/utils/market_operation_utils/constants'; } 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 { 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 { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
import { BATCH_SOURCE_FILTERS } from '../src/utils/market_operation_utils/sampler_operations'; import { BATCH_SOURCE_FILTERS } from '../src/utils/market_operation_utils/sampler_operations';
import { SourceFilters } from '../src/utils/market_operation_utils/source_filters'; 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); 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<Pool[]> {
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 // tslint:disable: custom-no-magic-numbers promise-function-async
describe('MarketOperationUtils tests', () => { describe('MarketOperationUtils tests', () => {
const CHAIN_ID = ChainId.Mainnet; const CHAIN_ID = ChainId.Mainnet;
@ -292,6 +318,11 @@ describe('MarketOperationUtils tests', () => {
const DEFAULT_FILL_DATA: FillDataBySource = { const DEFAULT_FILL_DATA: FillDataBySource = {
[ERC20BridgeSource.UniswapV2]: { tokenAddressPath: [] }, [ERC20BridgeSource.UniswapV2]: { tokenAddressPath: [] },
[ERC20BridgeSource.Balancer]: { poolAddress: randomAddress() }, [ERC20BridgeSource.Balancer]: { poolAddress: randomAddress() },
[ERC20BridgeSource.BalancerV2]: {
vault: randomAddress(),
poolId: randomAddress(),
deadline: Math.floor(Date.now() / 1000) + 300,
},
[ERC20BridgeSource.Bancor]: { path: [], networkAddress: randomAddress() }, [ERC20BridgeSource.Bancor]: { path: [], networkAddress: randomAddress() },
[ERC20BridgeSource.Kyber]: { hint: '0x', reserveId: '0x', networkAddress: randomAddress() }, [ERC20BridgeSource.Kyber]: { hint: '0x', reserveId: '0x', networkAddress: randomAddress() },
[ERC20BridgeSource.Curve]: { [ERC20BridgeSource.Curve]: {
@ -381,53 +412,6 @@ describe('MarketOperationUtils tests', () => {
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES), getSellQuotes: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES),
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES), getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES),
getMedianSellRate: createGetMedianSellRate(1), 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[]) => [], getTwoHopSellQuotes: (..._params: any[]) => [],
getTwoHopBuyQuotes: (..._params: any[]) => [], getTwoHopBuyQuotes: (..._params: any[]) => [],
isAddressContract: (..._params: any[]) => false, isAddressContract: (..._params: any[]) => false,
@ -440,8 +424,11 @@ describe('MarketOperationUtils tests', () => {
async executeBatchAsync(ops: any[]): Promise<any[]> { async executeBatchAsync(ops: any[]): Promise<any[]> {
return ops; return ops;
}, },
balancerPoolsCache: new BalancerPoolsCache(), poolsCaches: {
creamPoolsCache: new CreamPoolsCache(), [ERC20BridgeSource.BalancerV2]: mockPoolsCache,
[ERC20BridgeSource.Balancer]: mockPoolsCache,
[ERC20BridgeSource.Cream]: mockPoolsCache,
},
liquidityProviderRegistry: {}, liquidityProviderRegistry: {},
chainId: CHAIN_ID, chainId: CHAIN_ID,
} as any) as DexOrderSampler; } as any) as DexOrderSampler;
@ -520,22 +507,6 @@ describe('MarketOperationUtils tests', () => {
sourcesPolled.push(ERC20BridgeSource.MultiHop); sourcesPolled.push(ERC20BridgeSource.MultiHop);
return DEFAULT_OPS.getTwoHopSellQuotes(...args); 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, { await getMarketSellOrdersAsync(marketOperationUtils, ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
@ -566,22 +537,6 @@ describe('MarketOperationUtils tests', () => {
} }
return DEFAULT_OPS.getTwoHopSellQuotes(...args); 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, { await getMarketSellOrdersAsync(marketOperationUtils, ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
@ -612,22 +567,6 @@ describe('MarketOperationUtils tests', () => {
} }
return DEFAULT_OPS.getTwoHopSellQuotes(sources, ...args); 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, { await getMarketSellOrdersAsync(marketOperationUtils, ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
@ -1429,22 +1368,6 @@ describe('MarketOperationUtils tests', () => {
} }
return DEFAULT_OPS.getTwoHopBuyQuotes(..._args); 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, { await getMarketBuyOrdersAsync(marketOperationUtils, ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
@ -1475,22 +1398,6 @@ describe('MarketOperationUtils tests', () => {
} }
return DEFAULT_OPS.getTwoHopBuyQuotes(..._args); 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, { await getMarketBuyOrdersAsync(marketOperationUtils, ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS, ...DEFAULT_OPTS,
@ -1521,22 +1428,6 @@ describe('MarketOperationUtils tests', () => {
} }
return DEFAULT_OPS.getTwoHopBuyQuotes(..._args); 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, { await getMarketBuyOrdersAsync(marketOperationUtils, ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS, ...DEFAULT_OPTS,

View File

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

View File

@ -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<Pool[]>;
_fetchPoolsForPairAsync: (takerToken: string, makerToken: string) => Promise<Pool[]>;
_loadTopPoolsAsync: () => Promise<void>;
}
export class MockBalancerPoolsCache extends BalancerPoolsCache {
constructor(public handlers: Partial<Handlers>) {
super();
}
public async getPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]> {
return this.handlers.getPoolsForPairAsync
? this.handlers.getPoolsForPairAsync(takerToken, makerToken)
: super.getPoolsForPairAsync(takerToken, makerToken);
}
protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]> {
return this.handlers._fetchPoolsForPairAsync
? this.handlers._fetchPoolsForPairAsync(takerToken, makerToken)
: super._fetchPoolsForPairAsync(takerToken, makerToken);
}
protected async _loadTopPoolsAsync(): Promise<void> {
if (this.handlers && this.handlers._loadTopPoolsAsync) {
return this.handlers._loadTopPoolsAsync();
}
}
}

View File

@ -6,6 +6,7 @@
export * from '../test/generated-wrappers/approximate_buys'; export * from '../test/generated-wrappers/approximate_buys';
export * from '../test/generated-wrappers/balance_checker'; export * from '../test/generated-wrappers/balance_checker';
export * from '../test/generated-wrappers/balancer_sampler'; 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/bancor_sampler';
export * from '../test/generated-wrappers/curve_sampler'; export * from '../test/generated-wrappers/curve_sampler';
export * from '../test/generated-wrappers/d_o_d_o_sampler'; export * from '../test/generated-wrappers/d_o_d_o_sampler';

View File

@ -9,6 +9,7 @@
"test/generated-artifacts/ApproximateBuys.json", "test/generated-artifacts/ApproximateBuys.json",
"test/generated-artifacts/BalanceChecker.json", "test/generated-artifacts/BalanceChecker.json",
"test/generated-artifacts/BalancerSampler.json", "test/generated-artifacts/BalancerSampler.json",
"test/generated-artifacts/BalancerV2Sampler.json",
"test/generated-artifacts/BancorSampler.json", "test/generated-artifacts/BancorSampler.json",
"test/generated-artifacts/CurveSampler.json", "test/generated-artifacts/CurveSampler.json",
"test/generated-artifacts/DODOSampler.json", "test/generated-artifacts/DODOSampler.json",

View File

@ -1,4 +1,13 @@
[ [
{
"version": "1.6.0",
"changes": [
{
"note": "Add BalancerV2, remove Smoothy, Component and Saddle in BridgeProtocol enum",
"pr": 206
}
]
},
{ {
"timestamp": 1619596077, "timestamp": 1619596077,
"version": "1.5.1", "version": "1.5.1",

View File

@ -126,9 +126,7 @@ export enum BridgeProtocol {
CoFiX, CoFiX,
Nerve, Nerve,
MakerPsm, MakerPsm,
Smoothy, BalancerV2,
Component,
Saddle,
} }
// tslint:enable: enum-naming // tslint:enable: enum-naming

View File

@ -4540,7 +4540,7 @@ columnify@^1.5.4:
strip-ansi "^3.0.0" strip-ansi "^3.0.0"
wcwidth "^1.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" version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
dependencies: dependencies:
@ -4879,6 +4879,13 @@ cross-fetch@^2.1.0, cross-fetch@^2.1.1:
node-fetch "2.1.2" node-fetch "2.1.2"
whatwg-fetch "2.0.4" 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: cross-spawn@^4:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" 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" esprima-extract-comments "^1.1.0"
parse-code-context "^1.0.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: extsprintf@1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
@ -6680,6 +6692,15 @@ forever@^0.15.3:
utile "~0.2.1" utile "~0.2.1"
winston "~0.8.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: form-data@~2.3.2:
version "2.3.3" version "2.3.3"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" 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" version "4.2.4"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" 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: growl@1.10.3:
version "1.10.3" version "1.10.3"
resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" 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" version "2.1.2"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5" 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: node-fetch@^1.0.1, node-fetch@~1.7.1:
version "1.7.3" version "1.7.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" 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" encoding "^0.1.11"
is-stream "^1.0.1" 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: node-gyp-build@^4.2.0:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739"