From afd468d6cbe225ff4e7f5e7985c2379d3650fbc3 Mon Sep 17 00:00:00 2001 From: ReflectiveChimp <55021052+ReflectiveChimp@users.noreply.github.com> Date: Mon, 9 Sep 2024 16:47:33 +0100 Subject: [PATCH 01/20] fix aura-aurabal tokenProviderId --- src/config/vault/ethereum.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/vault/ethereum.json b/src/config/vault/ethereum.json index 84fa56202..f6774bb3c 100644 --- a/src/config/vault/ethereum.json +++ b/src/config/vault/ethereum.json @@ -4523,7 +4523,7 @@ "token": "auraBAL", "tokenAddress": "0x616e8BfA43F920657B3497DBf40D6b1A02D4608d", "tokenDecimals": 18, - "tokenProviderId": "balancer", + "tokenProviderId": "aura", "earnedToken": "mooAuraBAL", "earnedTokenAddress": "0x44B6A414e73fb7387Ca250F44D7e43cD7d6992c2", "earnContractAddress": "0x44B6A414e73fb7387Ca250F44D7e43cD7d6992c2", From fa1db5b71596ee3fa58a93ad77faae67dae8a26a Mon Sep 17 00:00:00 2001 From: ReflectiveChimp <55021052+ReflectiveChimp@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:16:24 +0100 Subject: [PATCH 02/20] Add balancer amm configs --- package.json | 2 +- src/config/zap/amm/arbitrum.json | 6 ++++++ src/config/zap/amm/avax.json | 6 ++++++ src/config/zap/amm/base.json | 6 ++++++ src/config/zap/amm/ethereum.json | 6 ++++++ src/config/zap/amm/fantom.json | 6 ++++++ src/config/zap/amm/gnosis.json | 9 ++++++++- src/config/zap/amm/optimism.json | 6 ++++++ src/config/zap/amm/polygon.json | 6 ++++++ src/features/data/apis/config-types.ts | 7 ++++++- src/features/data/entities/zap.ts | 8 +++++++- 11 files changed, 64 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 3f1c7225e..758e87f31 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "beefy-pro", + "name": "beefy-v2", "homepage": "/", "version": "0.1.0", "private": true, diff --git a/src/config/zap/amm/arbitrum.json b/src/config/zap/amm/arbitrum.json index 7c6597ce4..170f36284 100644 --- a/src/config/zap/amm/arbitrum.json +++ b/src/config/zap/amm/arbitrum.json @@ -94,5 +94,11 @@ "type": "gamma", "name": "Gamma Pancakeswap", "proxyAddress": "0x4fd87c7FA22D4E8aD933aC4C709C83cEFDCE8B00" + }, + { + "id": "arbitrum-balancer", + "type": "balancer", + "name": "Balancer", + "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8" } ] diff --git a/src/config/zap/amm/avax.json b/src/config/zap/amm/avax.json index df21be6e3..e00d5d5e4 100644 --- a/src/config/zap/amm/avax.json +++ b/src/config/zap/amm/avax.json @@ -66,5 +66,11 @@ "swapFeeNumerator": "3", "swapFeeDenominator": "1000", "getAmountOutMode": "getAmountOut" + }, + { + "id": "avax-balancer", + "type": "balancer", + "name": "Balancer", + "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8" } ] diff --git a/src/config/zap/amm/base.json b/src/config/zap/amm/base.json index c780e7c06..3517da7ee 100644 --- a/src/config/zap/amm/base.json +++ b/src/config/zap/amm/base.json @@ -94,5 +94,11 @@ "type": "gamma", "name": "Gamma Sushi", "proxyAddress": "0xc40F63879630dFF5b69dd6d287f7735E65e90702" + }, + { + "id": "base-balancer", + "type": "balancer", + "name": "Balancer", + "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8" } ] diff --git a/src/config/zap/amm/ethereum.json b/src/config/zap/amm/ethereum.json index 9e098d163..5c54be329 100644 --- a/src/config/zap/amm/ethereum.json +++ b/src/config/zap/amm/ethereum.json @@ -50,5 +50,11 @@ "type": "gamma", "name": "Gamma Uniswap", "proxyAddress": "0x911BfbAca43f117E52197AE62D439d6A645C8886" + }, + { + "id": "ethereum-balancer", + "type": "balancer", + "name": "Balancer", + "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8" } ] diff --git a/src/config/zap/amm/fantom.json b/src/config/zap/amm/fantom.json index a9f2e1a28..25fdcc3f9 100644 --- a/src/config/zap/amm/fantom.json +++ b/src/config/zap/amm/fantom.json @@ -130,5 +130,11 @@ "swapFeeNumerator": "3", "swapFeeDenominator": "10000", "getAmountOutMode": "getAmountOut" + }, + { + "id": "fantom-beethovenx", + "type": "balancer", + "name": "Beethoven X", + "vaultAddress": "0x20dd72Ed959b6147912C2e529F0a0C651c33c9ce" } ] diff --git a/src/config/zap/amm/gnosis.json b/src/config/zap/amm/gnosis.json index fe51488c7..f9d9acc8b 100644 --- a/src/config/zap/amm/gnosis.json +++ b/src/config/zap/amm/gnosis.json @@ -1 +1,8 @@ -[] +[ + { + "id": "gnosis-balancer", + "type": "balancer", + "name": "Balancer", + "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8" + } +] diff --git a/src/config/zap/amm/optimism.json b/src/config/zap/amm/optimism.json index 14b2f5ee6..e911a3d2f 100644 --- a/src/config/zap/amm/optimism.json +++ b/src/config/zap/amm/optimism.json @@ -28,5 +28,11 @@ "name": "Gamma Uniswap V3", "type": "gamma", "proxyAddress": "0x1E97925c365cd96D74Ec55A04569915c4D65e5e0" + }, + { + "id": "optimism-beethovenx", + "type": "balancer", + "name": "Beethoven X", + "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8" } ] diff --git a/src/config/zap/amm/polygon.json b/src/config/zap/amm/polygon.json index 7063a682f..d8876fed1 100644 --- a/src/config/zap/amm/polygon.json +++ b/src/config/zap/amm/polygon.json @@ -156,5 +156,11 @@ "type": "gamma", "name": "Gamma Uniswap", "proxyAddress": "0x48975Ea6aA25914927241C3A9F493BfEEb8CA591" + }, + { + "id": "polygon-balancer", + "type": "balancer", + "name": "Balancer", + "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8" } ] diff --git a/src/features/data/apis/config-types.ts b/src/features/data/apis/config-types.ts index 7a9bb1d74..4ae80b984 100644 --- a/src/features/data/apis/config-types.ts +++ b/src/features/data/apis/config-types.ts @@ -252,8 +252,13 @@ export interface AmmConfigGamma extends AmmConfigBase { proxyAddress: string; } +export interface AmmConfigBalancer extends AmmConfigBase { + readonly type: 'balancer'; + vaultAddress: string; +} + export type AmmConfigUniswapV2Like = AmmConfigUniswapV2 | AmmConfigSolidly; -export type AmmConfig = AmmConfigUniswapV2Like | AmmConfigGamma; +export type AmmConfig = AmmConfigUniswapV2Like | AmmConfigGamma | AmmConfigBalancer; export function isSolidlyAmmConfig(amm: AmmConfig): amm is AmmConfigSolidly { return amm.type === 'solidly'; diff --git a/src/features/data/entities/zap.ts b/src/features/data/entities/zap.ts index 142bf06cf..7234e992f 100644 --- a/src/features/data/entities/zap.ts +++ b/src/features/data/entities/zap.ts @@ -1,4 +1,5 @@ import type { + AmmConfigBalancer, AmmConfigGamma, AmmConfigSolidly, AmmConfigUniswapV2, @@ -12,7 +13,8 @@ export type AmmEntityUniswapV2 = AmmConfigUniswapV2; export type AmmEntitySolidly = AmmConfigSolidly; export type AmmEntityUniswapLike = AmmEntityUniswapV2 | AmmEntitySolidly; export type AmmEntityGamma = AmmConfigGamma; -export type AmmEntity = AmmEntityUniswapLike | AmmEntityGamma; +export type AmmEntityBalancer = AmmConfigBalancer; +export type AmmEntity = AmmEntityUniswapLike | AmmEntityGamma | AmmConfigBalancer; export function isSolidlyAmm(amm: AmmEntity): amm is AmmEntitySolidly { return amm.type === 'solidly'; @@ -29,3 +31,7 @@ export function isUniswapLikeAmm(amm: AmmEntity): amm is AmmEntityUniswapLike { export function isGammaAmm(amm: AmmEntity): amm is AmmEntityGamma { return amm.type === 'gamma'; } + +export function isBalancerAmm(amm: AmmEntity): amm is AmmEntityBalancer { + return amm.type === 'balancer'; +} From 68bf97152e94e520e198dc65f40454aca67c2a73 Mon Sep 17 00:00:00 2001 From: ReflectiveChimp <55021052+ReflectiveChimp@users.noreply.github.com> Date: Wed, 11 Sep 2024 17:11:05 +0100 Subject: [PATCH 03/20] balancer zap: composable stable --- package.json | 1 + scripts/addBalancerZap.ts | 608 +++++++++ src/config/abi/BalancerVaultAbi.ts | 783 ++++++++++++ src/config/vault/arbitrum.json | 258 +++- src/config/vault/avax.json | 17 +- src/config/vault/base.json | 34 +- src/config/vault/ethereum.json | 232 +++- src/config/vault/fantom.json | 35 +- src/config/vault/gnosis.json | 104 +- src/config/vault/optimism.json | 105 +- src/config/vault/polygon.json | 52 +- .../balancer/BalancerComposableStablePool.ts | 372 ++++++ src/features/data/apis/amm/balancer/types.ts | 3 + .../data/apis/transact/helpers/amounts.ts | 7 + .../strategies/balancer/BalancerStrategy.ts | 1112 +++++++++++++++++ .../transact/strategies/balancer/types.ts | 115 ++ .../data/apis/transact/strategies/index.ts | 1 + .../transact/strategies/strategy-configs.ts | 12 +- .../data/apis/transact/transact-types.ts | 37 +- .../transact/vaults/CowcentratedVaultType.ts | 14 +- .../Transact/TransactDebugger/BalancerZap.tsx | 140 +++ .../TransactDebugger/TransactDebugger.tsx | 6 + src/helpers/big-number.ts | 10 +- src/helpers/date.ts | 4 + src/helpers/web3.ts | 31 +- 25 files changed, 3973 insertions(+), 120 deletions(-) create mode 100644 scripts/addBalancerZap.ts create mode 100644 src/config/abi/BalancerVaultAbi.ts create mode 100644 src/features/data/apis/amm/balancer/BalancerComposableStablePool.ts create mode 100644 src/features/data/apis/amm/balancer/types.ts create mode 100644 src/features/data/apis/transact/strategies/balancer/BalancerStrategy.ts create mode 100644 src/features/data/apis/transact/strategies/balancer/types.ts create mode 100644 src/features/vault/components/Actions/Transact/TransactDebugger/BalancerZap.tsx diff --git a/package.json b/package.json index 758e87f31..71d1bf8c1 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "addClmRewardPools": "ts-node -r dotenv/config ./scripts/addClmRewardPools.ts", "updateRewardPoolRewards": "ts-node -r dotenv/config ./scripts/updateRewardPoolRewards.ts", "addCurveZap": "ts-node -r dotenv/config ./scripts/addCurveZap.ts", + "addBalancerZap": "ts-node -r dotenv/config ./scripts/addBalancerZap.ts", "checkVaultTokenDecimals": "ts-node -r dotenv/config ./scripts/checkVaultTokenDecimals.ts && prettier --write ./src/config/vault/*.json", "checkZapAddresses": "ts-node -r dotenv/config ./scripts/checkZapAddresses.ts", "makeExcludeConfig": "ts-node -r dotenv/config ./scripts/makeExcludeConfig.ts", diff --git a/scripts/addBalancerZap.ts b/scripts/addBalancerZap.ts new file mode 100644 index 000000000..020b65760 --- /dev/null +++ b/scripts/addBalancerZap.ts @@ -0,0 +1,608 @@ +import { ArgumentConfig, parse } from 'ts-command-line-args'; +import { + addressBookToAppId, + AppChainId, + appToAddressBookId, + ChainConfig, + chainRpcs, + getAmmsForChain, + getChain, + getVaultsForChain, +} from './common/config'; +import type { AmmConfigBalancer, VaultConfig } from '../src/features/data/apis/config-types'; +import { isNonEmptyArray, NonEmptyArray } from './common/utils'; +import { OptionalRecord } from '../src/features/data/utils/types-utils'; +import { Address, createPublicClient, getAddress, Hex, http, parseAbi } from 'viem'; +import PQueue from 'p-queue'; +import path, { dirname } from 'node:path'; +import { fileReadable, loadJson, saveJson } from './common/files'; +import { mkdir } from 'node:fs/promises'; +import { createCachedFactory } from '../src/features/data/utils/factory-utils'; +import { addressBook } from 'blockchain-addressbook'; +import { sortBy } from 'lodash'; +import platforms from '../src/config/platforms.json'; +import { BalancerStrategyConfig } from '../src/features/data/apis/transact/strategies/strategy-configs'; +import { sortVaultKeys } from './common/vault-fields'; + +const cacheBasePath = path.join(__dirname, '.cache', 'balancer'); +const cacheApiPath = path.join(cacheBasePath, 'api'); +const cacheRpcPath = path.join(cacheBasePath, 'rpc'); +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' as const; +const ZERO_BYTES32 = `0x${'0'.repeat(32 * 2)}` as const; + +type RunArgs = { + help?: boolean; + chain: string; + vault: string; + update?: boolean; + quiet?: boolean; +}; + +type BalancerChainId = + | 'ARBITRUM' + | 'AVALANCHE' + | 'BASE' + | 'FANTOM' + | 'FRAXTAL' + | 'GNOSIS' + | 'MAINNET' + | 'MODE' + | 'OPTIMISM' + | 'POLYGON' + | 'ZKEVM'; + +type BalancerPoolType = 'META_STABLE' | 'COMPOSABLE_STABLE' | 'WEIGHTED' | 'GYROE' | 'GYRO'; + +type BalancerPoolToken = { + index: number; + address: TAddress; + symbol: string; + decimals: number; + hasNestedPool: boolean; +}; + +type BalancerApiPool = { + id: Hex; + address: TAddress; + chain: BalancerChainId; + name: string; + symbol: string; + decimals: number; + type: TType; + version: number; + protocolVersion: number; + poolTokens: Array>; + factory: TAddress; + owner: TAddress; +}; + +type Pool = BalancerApiPool & { vaultAddress: Address; chainId: AppChainId }; + +const chainIdToBalancerChainId: OptionalRecord = { + ethereum: 'MAINNET', + polygon: 'POLYGON', + optimism: 'OPTIMISM', + fantom: 'FANTOM', + arbitrum: 'ARBITRUM', + avax: 'AVALANCHE', + zkevm: 'ZKEVM', + base: 'BASE', + gnosis: 'GNOSIS', + fraxtal: 'FRAXTAL', + mode: 'MODE', +}; + +const supportedProtocolVersions = new Set([2]); + +const supportedPoolTypes: OptionalRecord = { + COMPOSABLE_STABLE: { min: 3, max: 6 }, + // 'WEIGHTED': { min: 1, max: 4 }, + // 'GYROE': { min: 2, max: 2 }, + // 'GYRO': { min: 2, max: 2 }, + // 'META_STABLE': { min: 1, max: 1 }, +}; + +const balancerPoolQuery = ` +query Pool($id: String!, $chain: GqlChain){ + poolGetPool(id: $id, chain: $chain) { + id + address + chain + name + symbol + decimals + type + version + protocolVersion + factory + owner + poolTokens { + index + address + symbol + decimals + hasNestedPool + } + } +}`; + +const runArgsConfig: ArgumentConfig = { + help: { + type: Boolean, + alias: 'h', + description: 'Display this usage guide.', + optional: true, + }, + chain: { + type: String, + alias: 'c', + description: 'Which chain json file to process', + }, + vault: { + type: String, + alias: 'v', + description: 'Which vault id to process', + }, + update: { + type: Boolean, + alias: 'u', + description: 'Update the cache', + optional: true, + }, + quiet: { + type: Boolean, + alias: 'q', + description: 'Only output warnings, errors and the zap json', + optional: true, + }, +}; + +function isDefined(value: T): value is Exclude { + return value !== undefined && value !== null; +} + +function getRunArgs() { + return parse(runArgsConfig, { + helpArg: 'help', + headerContentSections: [ + { + header: 'yarn addBalancerZap', + content: 'Create zap config for a balancer vault.', + }, + ], + }); +} + +async function getVaults(chainId: string): Promise> { + const vaults = await getVaultsForChain(chainId); + if (!isNonEmptyArray(vaults)) { + throw new Error(`No vaults found for chain ${chainId}`); + } + return vaults; +} + +async function getVault(chainId: string, vaultId: string) { + const vaults = await getVaults(chainId); + const vault = vaults.find(v => v.id === vaultId); + if (!vault) { + throw new Error(`Vault ${vaultId} not found for chain ${chainId}`); + } + if (!vault.tokenAddress) { + throw new Error(`Vault ${vaultId} has no token address`); + } + + return vault; +} + +function createViemClient(chainId: AppChainId, chain: ChainConfig) { + return createPublicClient({ + batch: { + multicall: { + batchSize: 128, + wait: 100, + }, + }, + chain: { + id: chain.chainId, + name: chain.name, + nativeCurrency: { + decimals: 18, + name: chain.walletSettings.nativeCurrency.name, + symbol: chain.walletSettings.nativeCurrency.symbol, + }, + rpcUrls: { + public: { http: [chainRpcs[chainId]] }, + default: { http: [chainRpcs[chainId]] }, + }, + blockExplorers: { + default: { name: `${chain.name} Explorer`, url: chain.explorerUrl }, + }, + contracts: { + multicall3: { + address: chain.multicall3Address, + }, + }, + }, + transport: http(), + }); +} + +const getViemClient = createCachedFactory( + (chainId: AppChainId) => createViemClient(chainId, getChain(chainId)), + (chainId: AppChainId) => chainId +); + +function withFileCache any>( + factoryFn: FN, + cachePathFn: (...args: Parameters) => string +) { + return async ( + forceUpdate: boolean, + ...args: Parameters + ): Promise>> => { + const cachePath = cachePathFn(...args); + + if (!forceUpdate) { + try { + if (await fileReadable(cachePath)) { + return await loadJson(cachePath); + } + } catch (e) { + console.error('Failed to read cache', cachePath, e); + } + } + + const data = await factoryFn(...args); + await mkdir(dirname(cachePath), { recursive: true }); + await saveJson(cachePath, data, true); + return data; + }; +} + +const fetchPoolRpcData = withFileCache( + async (poolAddress: Address, chainId: AppChainId) => { + const client = getViemClient(chainId); + const [poolId, vaultAddress] = await Promise.all([ + client.readContract({ + address: poolAddress, + abi: parseAbi(['function getPoolId() public view returns (bytes32)']), + functionName: 'getPoolId', + }), + client.readContract({ + address: poolAddress, + abi: parseAbi(['function getVault() public view returns (address)']), + functionName: 'getVault', + }), + ]); + + if (!vaultAddress || vaultAddress === ZERO_ADDRESS) { + throw new Error(`No vault address found via vault.want().getVault()`); + } + if (!poolId || poolId === ZERO_BYTES32) { + throw new Error(`No pool id found via vault.want().getPoolId()`); + } + + return { poolId, vaultAddress }; + }, + (poolAddress: Address, chainId: AppChainId) => + path.join(cacheRpcPath, chainId, `pool-${poolAddress}.json`) +); + +const balancerApiQueue = new PQueue({ + concurrency: 1, + interval: 1000, + intervalCap: 1, + autoStart: true, + carryoverConcurrencyCount: true, + throwOnTimeout: true, +}); + +// TODO remove +balancerApiQueue.on('next', () => { + console.log( + `[BalancerApi] Pending: ${balancerApiQueue.pending + 1} Size: ${balancerApiQueue.size}` + ); +}); + +class BalancerApiPoolValidateError extends Error { + constructor(public readonly pool: BalancerApiPool, message: string) { + super(message); + this.name = 'BalancerApiPoolValidateError'; + } + + toString() { + return `${this.name}: ${this.message}\n${JSON.stringify(this.pool, null, 2)}`; + } +} + +function validateBalancerApiPool(pool: BalancerApiPool): BalancerApiPool { + if (!supportedProtocolVersions.has(pool.protocolVersion)) { + throw new BalancerApiPoolValidateError( + pool, + `Unsupported protocol version ${pool.protocolVersion}` + ); + } + + const poolTypeSupport = supportedPoolTypes[pool.type]; + if (!poolTypeSupport) { + throw new BalancerApiPoolValidateError(pool, `Unsupported pool type ${pool.type}`); + } + + if (pool.version < poolTypeSupport.min || pool.version > poolTypeSupport.max) { + throw new BalancerApiPoolValidateError( + pool, + `Unsupported pool type version ${pool.type} version ${pool.version} [supported: ${poolTypeSupport.min} -> ${poolTypeSupport.max}]` + ); + } + + return { + ...pool, + address: getAddress(pool.address), + owner: getAddress(pool.owner), + factory: getAddress(pool.factory), + type: pool.type as BalancerPoolType, + poolTokens: sortBy( + pool.poolTokens.map(t => ({ + ...t, + address: getAddress(t.address), + })), + p => p.index + ), + }; +} + +const fetchPoolApiData = withFileCache( + async ( + poolId: Hex, + balancerChainId: BalancerChainId + ): Promise> => { + const response = await balancerApiQueue.add(() => + fetch('https://api-v3.balancer.fi/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ + query: balancerPoolQuery, + variables: { id: poolId, chain: balancerChainId }, + }), + }) + ); + + if (!response.ok) { + throw new Error( + `Failed to fetch pool data from balancer api: ${response.status} ${response.statusText}` + ); + } + + const result = (await response.json()) as { + data?: { poolGetPool?: BalancerApiPool }; + }; + if (result?.data?.poolGetPool?.id !== poolId) { + throw new Error('No pool result from balancer api'); + } + + return result.data.poolGetPool; + }, + (poolId: Hex, balancerChainId: BalancerChainId) => + path.join(cacheApiPath, balancerChainId, `${poolId}.json`) +); + +const getPoolApiData = createCachedFactory( + async (forceUpdate: boolean, poolId: Hex, balancerChainId: BalancerChainId) => + validateBalancerApiPool(await fetchPoolApiData(forceUpdate, poolId, balancerChainId)), + (_, poolId: Hex, balancerChainId: BalancerChainId) => `${balancerChainId}:${poolId}` +); + +const getPoolRpcData = createCachedFactory( + fetchPoolRpcData, + (_, poolAddress: Address, chainId: AppChainId) => `${chainId}:${poolAddress}` +); + +function checkPoolTokensAgainstAddressBook(pool: Pool): void { + const { tokenAddressMap } = addressBook[appToAddressBookId(pool.chainId)]; + // Tokens in the pool that are not the pool token + const tokens = pool.poolTokens + .filter(t => t.address !== pool.address && !t.hasNestedPool) + .map(t => { + const abToken = tokenAddressMap[t.address]; + if (abToken) { + return { + poolToken: t, + inAddressBook: true as const, + abToken, + }; + } + + return { + poolToken: t, + inAddressBook: false as const, + }; + }); + + // Tokens in address book + const zapTokens = tokens.filter( + (t): t is Extract => t.inAddressBook + ); + if (!zapTokens.length) { + throw new Error( + `No tokens [${tokens + .map(t => `${t.poolToken.symbol} (${t.poolToken.address})`) + .join(', ')}] found in ${ + pool.chainId + } address book. [At least 1 non-nested pool token is requried for zap]` + ); + } + + const tokenErrors = zapTokens + .map(({ poolToken, abToken }) => { + if (abToken.decimals !== poolToken.decimals) { + return `Address book token decimals mismatch ${poolToken.symbol} (${poolToken.address}) ${poolToken.decimals} vs ${abToken.decimals}`; + } + return undefined; + }) + .filter(isDefined); + if (tokenErrors.length) { + throw new Error('Token errors:\n' + tokenErrors.join('\n')); + } + + if (zapTokens.length !== tokens.length) { + console.warn(`Some tokens not found in address book:`); + console.warn( + `${tokens + .filter(t => !t.inAddressBook) + .map(({ poolToken }) => ` ${poolToken.symbol} (${poolToken.address})`) + .join('\n')}` + ); + } +} + +async function findAmmForPool(pool: Pool, tokenProviderId: string): Promise { + const platform = platforms.find(p => p.id === tokenProviderId); + if (!platform) { + throw new Error(`No platform found for token provider id ${tokenProviderId}`); + } + + const amms = await getAmmsForChain(pool.chainId); + if (!amms.length) { + throw new Error(`No AMMs found for chain ${pool.chainId}`); + } + + const amm = amms.find( + (a): a is AmmConfigBalancer => a.type === 'balancer' && a.vaultAddress === pool.vaultAddress + ); + if (!amm) { + console.log( + JSON.stringify({ + id: `${pool.chainId}-${tokenProviderId}`, + type: 'balancer', + name: platform.name, + vaultAddress: pool.vaultAddress, + }) + ); + throw new Error( + `No balancer AMM with vaultAddress ${pool.vaultAddress} found on chain ${pool.chainId}` + ); + } + + return amm; +} + +export async function discoverBalancerZap(args: RunArgs) { + const chainId = addressBookToAppId(args.chain); + const chain = getChain(chainId); + const vault = await getVault(chainId, args.vault); + if (!vault.tokenAddress) { + throw new Error(`No vault address found for vault ${vault.id}`); + } + if (!vault.tokenProviderId) { + throw new Error(`No token provider id found for vault ${vault.id}`); + } + const poolAddress = getAddress(vault.tokenAddress); + + if (!args.quiet) { + console.log('=== Vault ==='); + console.log('Id:', vault.id); + console.log('Chain:', chain.name); + console.log('Assets:', vault.assets?.length ? vault.assets.join(', ') : 'not set'); + console.log('Want:', poolAddress); + } + const balancerChainId = chainIdToBalancerChainId[chainId]; + if (!balancerChainId) { + throw new Error(`No balancer chain id found for chain ${chainId}`); + } + + const { poolId, vaultAddress } = await getPoolRpcData(!!args.update, poolAddress, chainId); + if (!args.quiet) { + console.log('=== Pool ==='); + console.log('Id:', poolId); + console.log('Vault:', vaultAddress); + } + + const apiPool = await getPoolApiData(!!args.update, poolId, balancerChainId); + const pool: Pool = { + ...apiPool, + chainId, + vaultAddress, + }; + + if (!args.quiet) { + console.log('Name:', apiPool.name); + console.log('Symbol:', apiPool.symbol); + console.log('Type:', `${apiPool.type} v${apiPool.version}`); + console.log('Tokens:'); + console.log( + apiPool.poolTokens + .map( + t => + ` ${t.index} ${t.symbol} (${t.address})${ + t.address === apiPool.address ? ' [self]' : '' + }` + ) + .join('\n') + ); + } + + checkPoolTokensAgainstAddressBook(pool); + + const amm = await findAmmForPool(pool, vault.tokenProviderId); + if (!args.quiet) { + console.log('=== AMM ==='); + console.log('Id:', amm.id); + console.log('Name:', amm.name); + console.log('Vault:', amm.vaultAddress); + } + + const zap: BalancerStrategyConfig = { + strategyId: 'balancer', + ammId: amm.id, + poolId: apiPool.id, + poolType: apiPool.type.toLowerCase().replaceAll('_', '-') as BalancerStrategyConfig['poolType'], // TODO types + tokens: apiPool.poolTokens.map(t => t.address), + }; + + return zap; +} + +async function saveZap(chainId: string, vaultId: string, zap: BalancerStrategyConfig) { + const path = `./src/config/vault/${addressBookToAppId(chainId)}.json`; + const vaults = await loadJson(path); + let found = false; + const modified = vaults.map(vault => { + if (vault.id === vaultId) { + found = true; + return sortVaultKeys({ + ...vault, + zaps: [zap], + }); + } + return vault; + }); + if (!found) { + throw new Error(`Vault ${vaultId} not found`); + } + + await saveJson(path, modified, 'prettier'); +} + +async function start() { + const args = getRunArgs(); + if (args.help) { + return; + } + + const zap = await discoverBalancerZap(args); + if (!args.quiet) { + console.log('Zap:', JSON.stringify(zap, null, 2)); + } + + await saveZap(args.chain, args.vault, zap); +} + +if (require.main === module) { + start().catch(e => { + console.error(e); + process.exit(1); + }); +} diff --git a/src/config/abi/BalancerVaultAbi.ts b/src/config/abi/BalancerVaultAbi.ts new file mode 100644 index 000000000..e2bd3500a --- /dev/null +++ b/src/config/abi/BalancerVaultAbi.ts @@ -0,0 +1,783 @@ +import type { Abi } from 'viem'; + +export const BalancerVaultAbi = [ + { + inputs: [ + { + internalType: 'contract IAuthorizer', + name: 'authorizer', + type: 'address', + }, + { internalType: 'contract IWETH', name: 'weth', type: 'address' }, + { + internalType: 'uint256', + name: 'pauseWindowDuration', + type: 'uint256', + }, + { internalType: 'uint256', name: 'bufferPeriodDuration', type: 'uint256' }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'contract IAuthorizer', + name: 'newAuthorizer', + type: 'address', + }, + ], + name: 'AuthorizerChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'contract IERC20', + name: 'token', + type: 'address', + }, + { indexed: true, internalType: 'address', name: 'sender', type: 'address' }, + { + indexed: false, + internalType: 'address', + name: 'recipient', + type: 'address', + }, + { indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'ExternalBalanceTransfer', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'contract IFlashLoanRecipient', + name: 'recipient', + type: 'address', + }, + { indexed: true, internalType: 'contract IERC20', name: 'token', type: 'address' }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { indexed: false, internalType: 'uint256', name: 'feeAmount', type: 'uint256' }, + ], + name: 'FlashLoan', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'user', type: 'address' }, + { + indexed: true, + internalType: 'contract IERC20', + name: 'token', + type: 'address', + }, + { indexed: false, internalType: 'int256', name: 'delta', type: 'int256' }, + ], + name: 'InternalBalanceChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: 'bool', name: 'paused', type: 'bool' }], + name: 'PausedStateChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { + indexed: true, + internalType: 'address', + name: 'liquidityProvider', + type: 'address', + }, + { + indexed: false, + internalType: 'contract IERC20[]', + name: 'tokens', + type: 'address[]', + }, + { indexed: false, internalType: 'int256[]', name: 'deltas', type: 'int256[]' }, + { + indexed: false, + internalType: 'uint256[]', + name: 'protocolFeeAmounts', + type: 'uint256[]', + }, + ], + name: 'PoolBalanceChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { + indexed: true, + internalType: 'address', + name: 'assetManager', + type: 'address', + }, + { indexed: true, internalType: 'contract IERC20', name: 'token', type: 'address' }, + { + indexed: false, + internalType: 'int256', + name: 'cashDelta', + type: 'int256', + }, + { indexed: false, internalType: 'int256', name: 'managedDelta', type: 'int256' }, + ], + name: 'PoolBalanceManaged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { + indexed: true, + internalType: 'address', + name: 'poolAddress', + type: 'address', + }, + { + indexed: false, + internalType: 'enum IVault.PoolSpecialization', + name: 'specialization', + type: 'uint8', + }, + ], + name: 'PoolRegistered', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'relayer', type: 'address' }, + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + { indexed: false, internalType: 'bool', name: 'approved', type: 'bool' }, + ], + name: 'RelayerApprovalChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { + indexed: true, + internalType: 'contract IERC20', + name: 'tokenIn', + type: 'address', + }, + { indexed: true, internalType: 'contract IERC20', name: 'tokenOut', type: 'address' }, + { + indexed: false, + internalType: 'uint256', + name: 'amountIn', + type: 'uint256', + }, + { indexed: false, internalType: 'uint256', name: 'amountOut', type: 'uint256' }, + ], + name: 'Swap', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { + indexed: false, + internalType: 'contract IERC20[]', + name: 'tokens', + type: 'address[]', + }, + ], + name: 'TokensDeregistered', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { + indexed: false, + internalType: 'contract IERC20[]', + name: 'tokens', + type: 'address[]', + }, + { indexed: false, internalType: 'address[]', name: 'assetManagers', type: 'address[]' }, + ], + name: 'TokensRegistered', + type: 'event', + }, + { + inputs: [], + name: 'WETH', + outputs: [{ internalType: 'contract IWETH', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'enum IVault.SwapKind', + name: 'kind', + type: 'uint8', + }, + { + components: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { + internalType: 'uint256', + name: 'assetInIndex', + type: 'uint256', + }, + { internalType: 'uint256', name: 'assetOutIndex', type: 'uint256' }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + internalType: 'struct IVault.BatchSwapStep[]', + name: 'swaps', + type: 'tuple[]', + }, + { + internalType: 'contract IAsset[]', + name: 'assets', + type: 'address[]', + }, + { + components: [ + { internalType: 'address', name: 'sender', type: 'address' }, + { + internalType: 'bool', + name: 'fromInternalBalance', + type: 'bool', + }, + { internalType: 'address payable', name: 'recipient', type: 'address' }, + { + internalType: 'bool', + name: 'toInternalBalance', + type: 'bool', + }, + ], + internalType: 'struct IVault.FundManagement', + name: 'funds', + type: 'tuple', + }, + { internalType: 'int256[]', name: 'limits', type: 'int256[]' }, + { + internalType: 'uint256', + name: 'deadline', + type: 'uint256', + }, + ], + name: 'batchSwap', + outputs: [{ internalType: 'int256[]', name: 'assetDeltas', type: 'int256[]' }], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'poolId', + type: 'bytes32', + }, + { internalType: 'contract IERC20[]', name: 'tokens', type: 'address[]' }, + ], + name: 'deregisterTokens', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + internalType: 'address payable', + name: 'recipient', + type: 'address', + }, + { + components: [ + { + internalType: 'contract IAsset[]', + name: 'assets', + type: 'address[]', + }, + { internalType: 'uint256[]', name: 'minAmountsOut', type: 'uint256[]' }, + { + internalType: 'bytes', + name: 'userData', + type: 'bytes', + }, + { internalType: 'bool', name: 'toInternalBalance', type: 'bool' }, + ], + internalType: 'struct IVault.ExitPoolRequest', + name: 'request', + type: 'tuple', + }, + ], + name: 'exitPool', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'contract IFlashLoanRecipient', + name: 'recipient', + type: 'address', + }, + { internalType: 'contract IERC20[]', name: 'tokens', type: 'address[]' }, + { + internalType: 'uint256[]', + name: 'amounts', + type: 'uint256[]', + }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'flashLoan', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes4', name: 'selector', type: 'bytes4' }], + name: 'getActionId', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getAuthorizer', + outputs: [{ internalType: 'contract IAuthorizer', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getDomainSeparator', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'user', type: 'address' }, + { + internalType: 'contract IERC20[]', + name: 'tokens', + type: 'address[]', + }, + ], + name: 'getInternalBalance', + outputs: [{ internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'user', type: 'address' }], + name: 'getNextNonce', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getPausedState', + outputs: [ + { internalType: 'bool', name: 'paused', type: 'bool' }, + { + internalType: 'uint256', + name: 'pauseWindowEndTime', + type: 'uint256', + }, + { internalType: 'uint256', name: 'bufferPeriodEndTime', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: 'poolId', type: 'bytes32' }], + name: 'getPool', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + { internalType: 'enum IVault.PoolSpecialization', name: '', type: 'uint8' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { + internalType: 'contract IERC20', + name: 'token', + type: 'address', + }, + ], + name: 'getPoolTokenInfo', + outputs: [ + { internalType: 'uint256', name: 'cash', type: 'uint256' }, + { + internalType: 'uint256', + name: 'managed', + type: 'uint256', + }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { + internalType: 'address', + name: 'assetManager', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: 'poolId', type: 'bytes32' }], + name: 'getPoolTokens', + outputs: [ + { + internalType: 'contract IERC20[]', + name: 'tokens', + type: 'address[]', + }, + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { + internalType: 'uint256', + name: 'lastChangeBlock', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getProtocolFeesCollector', + outputs: [{ internalType: 'contract ProtocolFeesCollector', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'user', type: 'address' }, + { + internalType: 'address', + name: 'relayer', + type: 'address', + }, + ], + name: 'hasApprovedRelayer', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + internalType: 'address', + name: 'recipient', + type: 'address', + }, + { + components: [ + { + internalType: 'contract IAsset[]', + name: 'assets', + type: 'address[]', + }, + { internalType: 'uint256[]', name: 'maxAmountsIn', type: 'uint256[]' }, + { + internalType: 'bytes', + name: 'userData', + type: 'bytes', + }, + { internalType: 'bool', name: 'fromInternalBalance', type: 'bool' }, + ], + internalType: 'struct IVault.JoinPoolRequest', + name: 'request', + type: 'tuple', + }, + ], + name: 'joinPool', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'enum IVault.PoolBalanceOpKind', + name: 'kind', + type: 'uint8', + }, + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { + internalType: 'contract IERC20', + name: 'token', + type: 'address', + }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + internalType: 'struct IVault.PoolBalanceOp[]', + name: 'ops', + type: 'tuple[]', + }, + ], + name: 'managePoolBalance', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'enum IVault.UserBalanceOpKind', + name: 'kind', + type: 'uint8', + }, + { internalType: 'contract IAsset', name: 'asset', type: 'address' }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { internalType: 'address', name: 'sender', type: 'address' }, + { + internalType: 'address payable', + name: 'recipient', + type: 'address', + }, + ], + internalType: 'struct IVault.UserBalanceOp[]', + name: 'ops', + type: 'tuple[]', + }, + ], + name: 'manageUserBalance', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'enum IVault.SwapKind', + name: 'kind', + type: 'uint8', + }, + { + components: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { + internalType: 'uint256', + name: 'assetInIndex', + type: 'uint256', + }, + { internalType: 'uint256', name: 'assetOutIndex', type: 'uint256' }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + internalType: 'struct IVault.BatchSwapStep[]', + name: 'swaps', + type: 'tuple[]', + }, + { + internalType: 'contract IAsset[]', + name: 'assets', + type: 'address[]', + }, + { + components: [ + { internalType: 'address', name: 'sender', type: 'address' }, + { + internalType: 'bool', + name: 'fromInternalBalance', + type: 'bool', + }, + { internalType: 'address payable', name: 'recipient', type: 'address' }, + { + internalType: 'bool', + name: 'toInternalBalance', + type: 'bool', + }, + ], + internalType: 'struct IVault.FundManagement', + name: 'funds', + type: 'tuple', + }, + ], + name: 'queryBatchSwap', + outputs: [{ internalType: 'int256[]', name: '', type: 'int256[]' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'enum IVault.PoolSpecialization', name: 'specialization', type: 'uint8' }, + ], + name: 'registerPool', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'poolId', + type: 'bytes32', + }, + { internalType: 'contract IERC20[]', name: 'tokens', type: 'address[]' }, + { + internalType: 'address[]', + name: 'assetManagers', + type: 'address[]', + }, + ], + name: 'registerTokens', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'contract IAuthorizer', name: 'newAuthorizer', type: 'address' }], + name: 'setAuthorizer', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bool', name: 'paused', type: 'bool' }], + name: 'setPaused', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'sender', type: 'address' }, + { + internalType: 'address', + name: 'relayer', + type: 'address', + }, + { internalType: 'bool', name: 'approved', type: 'bool' }, + ], + name: 'setRelayerApproval', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'bytes32', + name: 'poolId', + type: 'bytes32', + }, + { internalType: 'enum IVault.SwapKind', name: 'kind', type: 'uint8' }, + { + internalType: 'contract IAsset', + name: 'assetIn', + type: 'address', + }, + { internalType: 'contract IAsset', name: 'assetOut', type: 'address' }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + internalType: 'struct IVault.SingleSwap', + name: 'singleSwap', + type: 'tuple', + }, + { + components: [ + { internalType: 'address', name: 'sender', type: 'address' }, + { + internalType: 'bool', + name: 'fromInternalBalance', + type: 'bool', + }, + { internalType: 'address payable', name: 'recipient', type: 'address' }, + { + internalType: 'bool', + name: 'toInternalBalance', + type: 'bool', + }, + ], + internalType: 'struct IVault.FundManagement', + name: 'funds', + type: 'tuple', + }, + { internalType: 'uint256', name: 'limit', type: 'uint256' }, + { + internalType: 'uint256', + name: 'deadline', + type: 'uint256', + }, + ], + name: 'swap', + outputs: [{ internalType: 'uint256', name: 'amountCalculated', type: 'uint256' }], + stateMutability: 'payable', + type: 'function', + }, + { stateMutability: 'payable', type: 'receive' }, +] as const satisfies Abi; diff --git a/src/config/vault/arbitrum.json b/src/config/vault/arbitrum.json index 1c7030900..255fd585e 100644 --- a/src/config/vault/arbitrum.json +++ b/src/config/vault/arbitrum.json @@ -3544,9 +3544,9 @@ "tokenAddress": "0x59743f1812bb85Db83e9e4EE061D124AAa642900", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xdB306FF22760C79faa69E3139760AF9151902ECE", "earnedToken": "mooAuraArbsUSDe-sFRAX", "earnedTokenAddress": "0xdB306FF22760C79faa69E3139760AF9151902ECE", - "earnContractAddress": "0xdB306FF22760C79faa69E3139760AF9151902ECE", "oracle": "lps", "oracleId": "aura-arb-susde-sfrax", "status": "active", @@ -3564,7 +3564,20 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x59743f1812bb85db83e9e4ee061d124aaa64290000000000000000000000052b/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x59743f1812bb85db83e9e4ee061d124aaa64290000000000000000000000052b/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0x59743f1812bb85db83e9e4ee061d124aaa64290000000000000000000000052b", + "poolType": "composable-stable", + "tokens": [ + "0x211Cc4DD073734dA055fbF44a2b4667d5E5fE5d2", + "0x59743f1812bb85Db83e9e4EE061D124AAa642900", + "0xe3b3FE7bcA19cA77Ad877A5Bebab186bEcfAD906" + ] + } + ] }, { "id": "aura-arb-frax-sfrax", @@ -3574,9 +3587,9 @@ "tokenAddress": "0xdfa752ca3fF49d4B6dBE08E2d5a111f51773D395", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x2AE1581655664E9570Eb2c280A4fCF047957a25e", "earnedToken": "mooAuraArbFRAX-sFRAX", "earnedTokenAddress": "0x2AE1581655664E9570Eb2c280A4fCF047957a25e", - "earnContractAddress": "0x2AE1581655664E9570Eb2c280A4fCF047957a25e", "oracle": "lps", "oracleId": "aura-arb-frax-sfrax", "status": "active", @@ -3594,7 +3607,20 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xdfa752ca3ff49d4b6dbe08e2d5a111f51773d3950000000000000000000004e8/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xdfa752ca3ff49d4b6dbe08e2d5a111f51773d3950000000000000000000004e8/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0xdfa752ca3ff49d4b6dbe08e2d5a111f51773d3950000000000000000000004e8", + "poolType": "composable-stable", + "tokens": [ + "0x17FC002b466eEc40DaE837Fc4bE5c67993ddBd6F", + "0xdfa752ca3fF49d4B6dBE08E2d5a111f51773D395", + "0xe3b3FE7bcA19cA77Ad877A5Bebab186bEcfAD906" + ] + } + ] }, { "id": "ramses-cl-rseth-weth-vault", @@ -5676,9 +5702,9 @@ "tokenAddress": "0xE8a6026365254f779b6927f00f8724EA1B8aE5E0", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x93a3c287A826a8307C6b3fE1A79D06cC8966d900", "earnedToken": "mooBalancerArbUSDC-gUSDC", "earnedTokenAddress": "0x93a3c287A826a8307C6b3fE1A79D06cC8966d900", - "earnContractAddress": "0x93a3c287A826a8307C6b3fE1A79D06cC8966d900", "oracle": "lps", "oracleId": "balancer-arb-usdc-gusdc", "status": "active", @@ -5690,7 +5716,20 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xe8a6026365254f779b6927f00f8724ea1b8ae5e0000000000000000000000580/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xe8a6026365254f779b6927f00f8724ea1b8ae5e0000000000000000000000580/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0xe8a6026365254f779b6927f00f8724ea1b8ae5e0000000000000000000000580", + "poolType": "composable-stable", + "tokens": [ + "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "0xd3443ee1e91aF28e5FB858Fbd0D72A63bA8046E0", + "0xE8a6026365254f779b6927f00f8724EA1B8aE5E0" + ] + } + ] }, { "id": "curve-arb-eth+-eth-v2", @@ -8302,9 +8341,9 @@ "tokenAddress": "0x90e6CB5249f5e1572afBF8A96D8A1ca6aCFFd739", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x764e4e75e3738615CDBFAeaE0C8527b1616e1123", "earnedToken": "mooAuraArbrsETH-WETH", "earnedTokenAddress": "0x764e4e75e3738615CDBFAeaE0C8527b1616e1123", - "earnContractAddress": "0x764e4e75e3738615CDBFAeaE0C8527b1616e1123", "oracle": "lps", "oracleId": "aura-arb-rseth-weth", "status": "active", @@ -8323,7 +8362,20 @@ "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x90e6cb5249f5e1572afbf8a96d8a1ca6acffd73900000000000000000000055c/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x90e6cb5249f5e1572afbf8a96d8a1ca6acffd73900000000000000000000055c/remove-liquidity", "pointStructureIds": ["kelp"], - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0x90e6cb5249f5e1572afbf8a96d8a1ca6acffd73900000000000000000000055c", + "poolType": "composable-stable", + "tokens": [ + "0x4186BFC76E2E237523CBC30FD220FE055156b41F", + "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + "0x90e6CB5249f5e1572afBF8A96D8A1ca6aCFFd739" + ] + } + ] }, { "id": "aura-arb-wsteth-ethx", @@ -8333,9 +8385,9 @@ "tokenAddress": "0x7B54C44fBe6Db6D97FD22b8756f89c0aF16202Cc", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x153b2b0dE2C5eB621cdA52af01514Ffde514Bd3c", "earnedToken": "mooAuraArbwstETH-ETHx", "earnedTokenAddress": "0x153b2b0dE2C5eB621cdA52af01514Ffde514Bd3c", - "earnContractAddress": "0x153b2b0dE2C5eB621cdA52af01514Ffde514Bd3c", "oracle": "lps", "oracleId": "aura-arb-wsteth-ethx", "status": "active", @@ -8353,7 +8405,20 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x7b54c44fbe6db6d97fd22b8756f89c0af16202cc00000000000000000000053c/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x7b54c44fbe6db6d97fd22b8756f89c0af16202cc00000000000000000000053c/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0x7b54c44fbe6db6d97fd22b8756f89c0af16202cc00000000000000000000053c", + "poolType": "composable-stable", + "tokens": [ + "0x5979D7b546E38E414F7E9822514be443A4800529", + "0x7B54C44fBe6Db6D97FD22b8756f89c0aF16202Cc", + "0xED65C5085a18Fa160Af0313E60dcc7905E944Dc7" + ] + } + ] }, { "id": "curve-arb-tbtc-ng", @@ -11612,9 +11677,9 @@ "tokenAddress": "0xB61371Ab661B1ACec81C699854D2f911070C059E", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xEFAd727469e7e4e410376986AB0af8B6F9559fDc", "earnedToken": "mooAuraArbezETH-wstETH", "earnedTokenAddress": "0xEFAd727469e7e4e410376986AB0af8B6F9559fDc", - "earnContractAddress": "0xEFAd727469e7e4e410376986AB0af8B6F9559fDc", "oracle": "lps", "oracleId": "aura-arb-ezeth-wsteth", "status": "active", @@ -11633,7 +11698,20 @@ "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xb61371ab661b1acec81c699854d2f911070c059e000000000000000000000516/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xb61371ab661b1acec81c699854d2f911070c059e000000000000000000000516/remove-liquidity", "pointStructureIds": ["renzo"], - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0xb61371ab661b1acec81c699854d2f911070c059e000000000000000000000516", + "poolType": "composable-stable", + "tokens": [ + "0x2416092f143378750bb29b79eD961ab195CcEea5", + "0x5979D7b546E38E414F7E9822514be443A4800529", + "0xB61371Ab661B1ACec81C699854D2f911070C059E" + ] + } + ] }, { "id": "curve-lend-arb-arb-crvusd", @@ -12497,9 +12575,9 @@ "tokenAddress": "0x2d6CeD12420a9AF5a83765a8c48Be2aFcD1A8FEb", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x415B62278E684E9BB878e1DFfa9e191BB3B82bb1", "earnedToken": "mooAuraArbcbETH/wstETH/rETHV2", "earnedTokenAddress": "0x415B62278E684E9BB878e1DFfa9e191BB3B82bb1", - "earnContractAddress": "0x415B62278E684E9BB878e1DFfa9e191BB3B82bb1", "oracle": "lps", "oracleId": "aura-arb-cbeth-wsteth-reth-v2", "status": "active", @@ -12517,7 +12595,21 @@ "strategyTypeId": "multi-lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x2d6ced12420a9af5a83765a8c48be2afcd1a8feb000000000000000000000500/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x2d6ced12420a9af5a83765a8c48be2afcd1a8feb000000000000000000000500/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0x2d6ced12420a9af5a83765a8c48be2afcd1a8feb000000000000000000000500", + "poolType": "composable-stable", + "tokens": [ + "0x1DEBd73E752bEaF79865Fd6446b0c970EaE7732f", + "0x2d6CeD12420a9AF5a83765a8c48Be2aFcD1A8FEb", + "0x5979D7b546E38E414F7E9822514be443A4800529", + "0xEC70Dcb4A1EFa46b8F2D97C310C9c4790ba5ffA8" + ] + } + ] }, { "id": "aura-arb-wsteth-sfrxeth", @@ -12527,9 +12619,9 @@ "tokenAddress": "0xc2598280bFeA1Fe18dFcaBD21C7165c40c6859d3", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xC0F3f8B58E6b60619F9109f94c3E0ca2188DA726", "earnedToken": "mooAuraArbwstETH-sfrxETH", "earnedTokenAddress": "0xC0F3f8B58E6b60619F9109f94c3E0ca2188DA726", - "earnContractAddress": "0xC0F3f8B58E6b60619F9109f94c3E0ca2188DA726", "oracle": "lps", "oracleId": "aura-arb-wsteth-sfrxeth", "status": "active", @@ -12547,7 +12639,20 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xc2598280bfea1fe18dfcabd21c7165c40c6859d30000000000000000000004f3/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xc2598280bfea1fe18dfcabd21c7165c40c6859d30000000000000000000004f3/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0xc2598280bfea1fe18dfcabd21c7165c40c6859d30000000000000000000004f3", + "poolType": "composable-stable", + "tokens": [ + "0x5979D7b546E38E414F7E9822514be443A4800529", + "0x95aB45875cFFdba1E5f451B950bC2E42c0053f39", + "0xc2598280bFeA1Fe18dFcaBD21C7165c40c6859d3" + ] + } + ] }, { "id": "equilibria-arb-eeth-seth", @@ -12620,9 +12725,9 @@ "tokenAddress": "0xd0EC47c54cA5e20aaAe4616c25C825c7f48D4069", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x81FdC9a8a233519D00f8eDFa0f8bFB7618dBf26b", "earnedToken": "mooAuraArbrETH-ETHV2", "earnedTokenAddress": "0x81FdC9a8a233519D00f8eDFa0f8bFB7618dBf26b", - "earnContractAddress": "0x81FdC9a8a233519D00f8eDFa0f8bFB7618dBf26b", "oracle": "lps", "oracleId": "aura-arb-weth-reth-v2", "status": "active", @@ -12640,7 +12745,20 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xd0ec47c54ca5e20aaae4616c25c825c7f48d40690000000000000000000004ef/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xd0ec47c54ca5e20aaae4616c25c825c7f48d40690000000000000000000004ef/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0xd0ec47c54ca5e20aaae4616c25c825c7f48d40690000000000000000000004ef", + "poolType": "composable-stable", + "tokens": [ + "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + "0xd0EC47c54cA5e20aaAe4616c25C825c7f48D4069", + "0xEC70Dcb4A1EFa46b8F2D97C310C9c4790ba5ffA8" + ] + } + ] }, { "id": "equilibria-arb-seth", @@ -13229,9 +13347,9 @@ "tokenAddress": "0x2CE4457aCac29dA4736aE6f5Cd9F583a6b335c27", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x503ebD3E15A19C43F76A27263466be0C7fB4b401", "earnedToken": "mooBalancersFRAX-4POOL", "earnedTokenAddress": "0x503ebD3E15A19C43F76A27263466be0C7fB4b401", - "earnContractAddress": "0x503ebD3E15A19C43F76A27263466be0C7fB4b401", "oracle": "lps", "oracleId": "balancer-sfrax-4pool", "status": "active", @@ -13242,7 +13360,20 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x2ce4457acac29da4736ae6f5cd9f583a6b335c270000000000000000000004dc/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x2ce4457acac29da4736ae6f5cd9f583a6b335c270000000000000000000004dc/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0x2ce4457acac29da4736ae6f5cd9f583a6b335c270000000000000000000004dc", + "poolType": "composable-stable", + "tokens": [ + "0x2CE4457aCac29dA4736aE6f5Cd9F583a6b335c27", + "0x423A1323c871aBC9d89EB06855bF5347048Fc4A5", + "0xe3b3FE7bcA19cA77Ad877A5Bebab186bEcfAD906" + ] + } + ] }, { "id": "uniswap-gamma-gns-usdc-narrow", @@ -14579,9 +14710,9 @@ "tokenAddress": "0x423A1323c871aBC9d89EB06855bF5347048Fc4A5", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xa0d758b81f8ed6635CDE1DA91C8fe1bD48C28A09", "earnedToken": "mooBalancerArb4POOL", "earnedTokenAddress": "0xa0d758b81f8ed6635CDE1DA91C8fe1bD48C28A09", - "earnContractAddress": "0xa0d758b81f8ed6635CDE1DA91C8fe1bD48C28A09", "oracle": "lps", "oracleId": "balancer-usdc-dai-usdt-usdce", "status": "active", @@ -14600,7 +14731,22 @@ "strategyTypeId": "multi-lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x423a1323c871abc9d89eb06855bf5347048fc4a5000000000000000000000496/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x423a1323c871abc9d89eb06855bf5347048fc4a5000000000000000000000496/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0x423a1323c871abc9d89eb06855bf5347048fc4a5000000000000000000000496", + "poolType": "composable-stable", + "tokens": [ + "0x423A1323c871aBC9d89EB06855bF5347048Fc4A5", + "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8" + ] + } + ] }, { "id": "balancer-weth-reth", @@ -14642,9 +14788,9 @@ "tokenAddress": "0x9791d590788598535278552EEcD4b211bFc790CB", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x85B10228cd93A6e5E354Ff0f2c60875E8E62F65A", "earnedToken": "mooBalancerArbwstETH-ETHV3", "earnedTokenAddress": "0x85B10228cd93A6e5E354Ff0f2c60875E8E62F65A", - "earnContractAddress": "0x85B10228cd93A6e5E354Ff0f2c60875E8E62F65A", "oracle": "lps", "oracleId": "balancer-wsteth-weth-v3", "status": "active", @@ -14663,7 +14809,20 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x9791d590788598535278552eecd4b211bfc790cb000000000000000000000498/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x9791d590788598535278552eecd4b211bfc790cb000000000000000000000498/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0x9791d590788598535278552eecd4b211bfc790cb000000000000000000000498", + "poolType": "composable-stable", + "tokens": [ + "0x5979D7b546E38E414F7E9822514be443A4800529", + "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + "0x9791d590788598535278552EEcD4b211bFc790CB" + ] + } + ] }, { "id": "aura-cbeth-wsteth-reth", @@ -14705,9 +14864,9 @@ "tokenAddress": "0x3FD4954a851eaD144c2FF72B1f5a38Ea5976Bd54", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x5892bA611fdC1598b72a30D087d28c989d429eF7", "earnedToken": "mooAuraArbwstETH-ankrETH", "earnedTokenAddress": "0x5892bA611fdC1598b72a30D087d28c989d429eF7", - "earnContractAddress": "0x5892bA611fdC1598b72a30D087d28c989d429eF7", "oracle": "lps", "oracleId": "aura-wsteth-ankreth", "status": "active", @@ -14725,7 +14884,20 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x3fd4954a851ead144c2ff72b1f5a38ea5976bd54000000000000000000000480/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x3fd4954a851ead144c2ff72b1f5a38ea5976bd54000000000000000000000480/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0x3fd4954a851ead144c2ff72b1f5a38ea5976bd54000000000000000000000480", + "poolType": "composable-stable", + "tokens": [ + "0x3FD4954a851eaD144c2FF72B1f5a38Ea5976Bd54", + "0x5979D7b546E38E414F7E9822514be443A4800529", + "0xe05A08226c49b636ACf99c40Da8DC6aF83CE5bB3" + ] + } + ] }, { "id": "aura-rdnt-weth", @@ -14891,9 +15063,9 @@ "tokenAddress": "0x8bc65Eed474D1A00555825c91FeAb6A8255C2107", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xd37025aC6227334C7762AeD5929Ce3272fbb6fdC", "earnedToken": "mooAuraArbDOLA-USDC", "earnedTokenAddress": "0xd37025aC6227334C7762AeD5929Ce3272fbb6fdC", - "earnContractAddress": "0xd37025aC6227334C7762AeD5929Ce3272fbb6fdC", "oracle": "lps", "oracleId": "aura-dola-usdc-v2", "status": "active", @@ -14911,7 +15083,20 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x8bc65eed474d1a00555825c91feab6a8255c2107000000000000000000000453/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x8bc65eed474d1a00555825c91feab6a8255c2107000000000000000000000453/awithdraw/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0x8bc65eed474d1a00555825c91feab6a8255c2107000000000000000000000453", + "poolType": "composable-stable", + "tokens": [ + "0x6A7661795C374c0bFC635934efAddFf3A7Ee23b6", + "0x8bc65Eed474D1A00555825c91FeAb6A8255C2107", + "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" + ] + } + ] }, { "id": "joe-auto-weth-usdc.e", @@ -15017,9 +15202,9 @@ "tokenAddress": "0x542F16DA0efB162D20bF4358EfA095B70A100f9E", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xBDF4e730ED5152a7AC646BB7b514Ed624E1147C4", "earnedToken": "mooBalancertBTC-WBTC", "earnedTokenAddress": "0xBDF4e730ED5152a7AC646BB7b514Ed624E1147C4", - "earnContractAddress": "0xBDF4e730ED5152a7AC646BB7b514Ed624E1147C4", "oracle": "lps", "oracleId": "balancer-tbtc-wbtc", "status": "active", @@ -15037,7 +15222,20 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x542f16da0efb162d20bf4358efa095b70a100f9e000000000000000000000436/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x542f16da0efb162d20bf4358efa095b70a100f9e000000000000000000000436/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0x542f16da0efb162d20bf4358efa095b70a100f9e000000000000000000000436", + "poolType": "composable-stable", + "tokens": [ + "0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f", + "0x542F16DA0efB162D20bF4358EfA095B70A100f9E", + "0x6c84a8f1c29108F47a79964b5Fe888D4f4D0dE40" + ] + } + ] }, { "id": "balancer-tbtc-weth", diff --git a/src/config/vault/avax.json b/src/config/vault/avax.json index 4c0fc7d9d..e6831d19c 100644 --- a/src/config/vault/avax.json +++ b/src/config/vault/avax.json @@ -73,9 +73,9 @@ "tokenAddress": "0xfD2620C9cfceC7D152467633B3B0Ca338D3d78cc", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x883b1899e6F8a896Ee22654A6deBA41Ef4BA47Cf", "earnedToken": "mooBalancerAvaxsAVAX-WAVAX", "earnedTokenAddress": "0x883b1899e6F8a896Ee22654A6deBA41Ef4BA47Cf", - "earnContractAddress": "0x883b1899e6F8a896Ee22654A6deBA41Ef4BA47Cf", "oracle": "lps", "oracleId": "balancer-avax-savax-avax", "status": "active", @@ -86,7 +86,20 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/avalanche/v2/0xfd2620c9cfcec7d152467633b3b0ca338d3d78cc00000000000000000000001c/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/avalanche/v2/0xfd2620c9cfcec7d152467633b3b0ca338d3d78cc00000000000000000000001c/remove-liquidity", - "network": "avax" + "network": "avax", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "avax-balancer", + "poolId": "0xfd2620c9cfcec7d152467633b3b0ca338d3d78cc00000000000000000000001c", + "poolType": "composable-stable", + "tokens": [ + "0x2b2C81e08f1Af8835a78Bb2A90AE924ACE0eA4bE", + "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", + "0xfD2620C9cfceC7D152467633B3B0Ca338D3d78cc" + ] + } + ] }, { "id": "png-weth.e-wavax", diff --git a/src/config/vault/base.json b/src/config/vault/base.json index 49703b508..0020498a9 100644 --- a/src/config/vault/base.json +++ b/src/config/vault/base.json @@ -3736,9 +3736,9 @@ "tokenAddress": "0xaB99a3e856dEb448eD99713dfce62F937E2d4D74", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xc52393b27FeE4355Fe6a5DC92D25BC2Ed1B418Cb", "earnedToken": "mooAuraBaseweETH-WETH", "earnedTokenAddress": "0xc52393b27FeE4355Fe6a5DC92D25BC2Ed1B418Cb", - "earnContractAddress": "0xc52393b27FeE4355Fe6a5DC92D25BC2Ed1B418Cb", "oracle": "lps", "oracleId": "aura-base-weeth-weth", "status": "active", @@ -3757,8 +3757,21 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/base/v2/0xab99a3e856deb448ed99713dfce62f937e2d4d74000000000000000000000118/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/base/v2/0xab99a3e856deb448ed99713dfce62f937e2d4d74000000000000000000000118/remove-liquidity", + "pointStructureIds": ["etherfi"], "network": "base", - "pointStructureIds": ["etherfi"] + "zaps": [ + { + "strategyId": "balancer", + "ammId": "base-balancer", + "poolId": "0xab99a3e856deb448ed99713dfce62f937e2d4d74000000000000000000000118", + "poolType": "composable-stable", + "tokens": [ + "0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A", + "0x4200000000000000000000000000000000000006", + "0xaB99a3e856dEb448eD99713dfce62F937E2d4D74" + ] + } + ] }, { "id": "aerodrome-weth-ghst", @@ -7131,9 +7144,9 @@ "tokenAddress": "0xC771c1a5905420DAEc317b154EB13e4198BA97D0", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x77B3B76A68B89541Cbb60d3609988376290Cb929", "earnedToken": "mooBalancerBaseWETH-rETH", "earnedTokenAddress": "0x77B3B76A68B89541Cbb60d3609988376290Cb929", - "earnContractAddress": "0x77B3B76A68B89541Cbb60d3609988376290Cb929", "oracle": "lps", "oracleId": "balancer-base-weth-reth", "status": "active", @@ -7152,7 +7165,20 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/base/v2/0xc771c1a5905420daec317b154eb13e4198ba97d0000000000000000000000023/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/base/v2/0xc771c1a5905420daec317b154eb13e4198ba97d0000000000000000000000023/remove-liquidity", - "network": "base" + "network": "base", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "base-balancer", + "poolId": "0xc771c1a5905420daec317b154eb13e4198ba97d0000000000000000000000023", + "poolType": "composable-stable", + "tokens": [ + "0x4200000000000000000000000000000000000006", + "0xB6fe221Fe9EeF5aBa221c348bA20A1Bf5e73624c", + "0xC771c1a5905420DAEc317b154EB13e4198BA97D0" + ] + } + ] }, { "id": "baseswap-weth-usdc", diff --git a/src/config/vault/ethereum.json b/src/config/vault/ethereum.json index f6774bb3c..319e5747e 100644 --- a/src/config/vault/ethereum.json +++ b/src/config/vault/ethereum.json @@ -32,9 +32,9 @@ "tokenAddress": "0x264062CA46A1322c2E6464471764089E01F22F19", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x47fbBc12e74825bFE8475c4b8BcC57d880C99025", "earnedToken": "mooAuraDOLA/sDOLA", "earnedTokenAddress": "0x47fbBc12e74825bFE8475c4b8BcC57d880C99025", - "earnContractAddress": "0x47fbBc12e74825bFE8475c4b8BcC57d880C99025", "oracle": "lps", "oracleId": "aura-dola-sdola", "status": "active", @@ -45,7 +45,20 @@ "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x264062ca46a1322c2e6464471764089e01f22f1900000000000000000000066b/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x264062ca46a1322c2e6464471764089e01f22f1900000000000000000000066b/remove-liquidity", - "network": "ethereum" + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "ethereum-balancer", + "poolId": "0x264062ca46a1322c2e6464471764089e01f22f1900000000000000000000066b", + "poolType": "composable-stable", + "tokens": [ + "0x264062CA46A1322c2E6464471764089E01F22F19", + "0x865377367054516e17014CcdED1e7d814EDC9ce4", + "0xb45ad160634c528Cc3D2926d9807104FA3157305" + ] + } + ] }, { "id": "aura-pxeth-weth", @@ -55,9 +68,9 @@ "tokenAddress": "0x88794C65550DeB6b4087B7552eCf295113794410", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x3Dc441e6139e4aF547F48bA672bD9800E6F30b7c", "earnedToken": "mooAurapxETH/WETH", "earnedTokenAddress": "0x3Dc441e6139e4aF547F48bA672bD9800E6F30b7c", - "earnContractAddress": "0x3Dc441e6139e4aF547F48bA672bD9800E6F30b7c", "oracle": "lps", "oracleId": "aura-pxeth-weth", "status": "active", @@ -68,7 +81,20 @@ "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x88794c65550deb6b4087b7552ecf295113794410000000000000000000000648/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x88794c65550deb6b4087b7552ecf295113794410000000000000000000000648/remove-liquidity", - "network": "ethereum" + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "ethereum-balancer", + "poolId": "0x88794c65550deb6b4087b7552ecf295113794410000000000000000000000648", + "poolType": "composable-stable", + "tokens": [ + "0x04C154b66CB340F3Ae24111CC767e0184Ed00Cc6", + "0x88794C65550DeB6b4087B7552eCf295113794410", + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + ] + } + ] }, { "id": "aura-rseth-weth", @@ -78,9 +104,9 @@ "tokenAddress": "0x58AAdFB1Afac0ad7fca1148f3cdE6aEDF5236B6D", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x967f88e651db83B1A74058D7499263eA7b3066E2", "earnedToken": "mooAurarsETH/WETH", "earnedTokenAddress": "0x967f88e651db83B1A74058D7499263eA7b3066E2", - "earnContractAddress": "0x967f88e651db83B1A74058D7499263eA7b3066E2", "oracle": "lps", "oracleId": "aura-rseth-weth", "status": "active", @@ -88,11 +114,24 @@ "platformId": "aura", "assets": ["rsETH", "WETH"], "risks": ["COMPLEXITY_LOW", "IL_NONE", "MCAP_SMALL", "AUDIT", "CONTRACTS_VERIFIED"], - "pointStructureIds": ["kelp"], "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x58aadfb1afac0ad7fca1148f3cde6aedf5236b6d00000000000000000000067f/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x58aadfb1afac0ad7fca1148f3cde6aedf5236b6d00000000000000000000067f/remove-liquidity", - "network": "ethereum" + "pointStructureIds": ["kelp"], + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "ethereum-balancer", + "poolId": "0x58aadfb1afac0ad7fca1148f3cde6aedf5236b6d00000000000000000000067f", + "poolType": "composable-stable", + "tokens": [ + "0x58AAdFB1Afac0ad7fca1148f3cdE6aEDF5236B6D", + "0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7", + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + ] + } + ] }, { "id": "aura-susde-usdc", @@ -102,9 +141,9 @@ "tokenAddress": "0xb819feeF8F0fcDC268AfE14162983A69f6BF179E", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x92A14518434a46E88CB4C3918AD33B3344099E02", "earnedToken": "mooAurasUSDe-USDC", "earnedTokenAddress": "0x92A14518434a46E88CB4C3918AD33B3344099E02", - "earnContractAddress": "0x92A14518434a46E88CB4C3918AD33B3344099E02", "oracle": "lps", "oracleId": "aura-susde-usdc", "status": "active", @@ -115,7 +154,20 @@ "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0xb819feef8f0fcdc268afe14162983a69f6bf179e000000000000000000000689/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0xb819feef8f0fcdc268afe14162983a69f6bf179e000000000000000000000689/remove-liquidity", - "network": "ethereum" + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "ethereum-balancer", + "poolId": "0xb819feef8f0fcdc268afe14162983a69f6bf179e000000000000000000000689", + "poolType": "composable-stable", + "tokens": [ + "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "0xb819feeF8F0fcDC268AfE14162983A69f6BF179E" + ] + } + ] }, { "id": "aura-shezeth-wsteth", @@ -125,9 +177,9 @@ "tokenAddress": "0xDb1f2e1655477d08FB0992f82EEDe0053B8Cd382", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x0258B57559e4EC229e3a710D2a55447eaea2312D", "earnedToken": "mooAuraShezETH-wstETH", "earnedTokenAddress": "0x0258B57559e4EC229e3a710D2a55447eaea2312D", - "earnContractAddress": "0x0258B57559e4EC229e3a710D2a55447eaea2312D", "oracle": "lps", "oracleId": "aura-shezeth-wsteth", "status": "active", @@ -138,7 +190,20 @@ "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0xdb1f2e1655477d08fb0992f82eede0053b8cd3820000000000000000000006ae/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0xdb1f2e1655477d08fb0992f82eede0053b8cd3820000000000000000000006ae/remove-liquidity", - "network": "ethereum" + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "ethereum-balancer", + "poolId": "0xdb1f2e1655477d08fb0992f82eede0053b8cd3820000000000000000000006ae", + "poolType": "composable-stable", + "tokens": [ + "0x63a0964A36c34E81959da5894ad888800e17405b", + "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + "0xDb1f2e1655477d08FB0992f82EEDe0053B8Cd382" + ] + } + ] }, { "id": "aura-shezusd-sdai", @@ -148,9 +213,9 @@ "tokenAddress": "0xEd0DF9Cd16D806E8A523805e53cf0c56E6dB4D1d", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xD7475b5941536ea4236e923997107BAaDa1Fb5E7", "earnedToken": "mooAuraShezUSD-sDAI", "earnedTokenAddress": "0xD7475b5941536ea4236e923997107BAaDa1Fb5E7", - "earnContractAddress": "0xD7475b5941536ea4236e923997107BAaDa1Fb5E7", "oracle": "lps", "oracleId": "aura-shezusd-sdai", "status": "active", @@ -161,7 +226,20 @@ "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0xed0df9cd16d806e8a523805e53cf0c56e6db4d1d000000000000000000000687/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0xed0df9cd16d806e8a523805e53cf0c56e6db4d1d000000000000000000000687/remove-liquidity", - "network": "ethereum" + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "ethereum-balancer", + "poolId": "0xed0df9cd16d806e8a523805e53cf0c56e6db4d1d000000000000000000000687", + "poolType": "composable-stable", + "tokens": [ + "0x83F20F44975D03b1b09e64809B757c47f942BEeA", + "0xD60EeA80C83779a8A5BFCDAc1F3323548e6BB62d", + "0xEd0DF9Cd16D806E8A523805e53cf0c56E6dB4D1d" + ] + } + ] }, { "id": "pendle-ageth-26dec24", @@ -1609,9 +1687,9 @@ "tokenAddress": "0x848a5564158d84b8A8fb68ab5D004Fae11619A54", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x5dA90BA82bED0AB701E6762D2bF44E08634d9776", "earnedToken": "mooAuraweETH/ezETH/rsETH", "earnedTokenAddress": "0x5dA90BA82bED0AB701E6762D2bF44E08634d9776", - "earnContractAddress": "0x5dA90BA82bED0AB701E6762D2bF44E08634d9776", "oracle": "lps", "oracleId": "aura-weeth-ezeth-rseth", "status": "active", @@ -1627,10 +1705,24 @@ "NO_TIMELOCK" ], "strategyTypeId": "multi-lp-locked", - "pointStructureIds": ["renzo", "kelp", "etherfi", "swell"], "addLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x848a5564158d84b8a8fb68ab5d004fae11619a5400000000000000000000066a/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x848a5564158d84b8a8fb68ab5d004fae11619a5400000000000000000000066a/remove-liquidity", - "network": "ethereum" + "pointStructureIds": ["renzo", "kelp", "etherfi", "swell"], + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "ethereum-balancer", + "poolId": "0x848a5564158d84b8a8fb68ab5d004fae11619a5400000000000000000000066a", + "poolType": "composable-stable", + "tokens": [ + "0x848a5564158d84b8A8fb68ab5D004Fae11619A54", + "0xbf5495Efe5DB9ce00f80364C8B423567e58d2110", + "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee", + "0xFAe103DC9cf190eD75350761e95403b7b8aFa6c0" + ] + } + ] }, { "id": "beqiv2-pool", @@ -1748,9 +1840,9 @@ "tokenAddress": "0x05ff47AFADa98a98982113758878F9A8B9FddA0a", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x1153211f7E810C73cC45eE09FF9A0742fBB6b467", "earnedToken": "mooAuraweETH-rETH", "earnedTokenAddress": "0x1153211f7E810C73cC45eE09FF9A0742fBB6b467", - "earnContractAddress": "0x1153211f7E810C73cC45eE09FF9A0742fBB6b467", "oracle": "lps", "oracleId": "aura-weeth-reth", "status": "active", @@ -1766,10 +1858,23 @@ "NO_TIMELOCK" ], "strategyTypeId": "multi-lp-locked", - "pointStructureIds": ["etherfi"], "addLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x05ff47afada98a98982113758878f9a8b9fdda0a000000000000000000000645/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x05ff47afada98a98982113758878f9a8b9fdda0a000000000000000000000645/remove-liquidity", - "network": "ethereum" + "pointStructureIds": ["etherfi"], + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "ethereum-balancer", + "poolId": "0x05ff47afada98a98982113758878f9a8b9fdda0a000000000000000000000645", + "poolType": "composable-stable", + "tokens": [ + "0x05ff47AFADa98a98982113758878F9A8B9FddA0a", + "0xae78736Cd615f374D3085123A210448E74Fc6393", + "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee" + ] + } + ] }, { "id": "aura-ezeth-eth", @@ -1779,9 +1884,9 @@ "tokenAddress": "0x596192bB6e41802428Ac943D2f1476C1Af25CC0E", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x3E1c2C604f60ef142AADAA51aa864f8438f2aaC1", "earnedToken": "mooAuraezETH-ETH", "earnedTokenAddress": "0x3E1c2C604f60ef142AADAA51aa864f8438f2aaC1", - "earnContractAddress": "0x3E1c2C604f60ef142AADAA51aa864f8438f2aaC1", "oracle": "lps", "oracleId": "aura-ezeth-eth", "status": "active", @@ -1797,10 +1902,23 @@ "NO_TIMELOCK" ], "strategyTypeId": "multi-lp-locked", - "pointStructureIds": ["renzo"], "addLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x596192bb6e41802428ac943d2f1476c1af25cc0e000000000000000000000659/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x596192bb6e41802428ac943d2f1476c1af25cc0e000000000000000000000659/remove-liquidity", - "network": "ethereum" + "pointStructureIds": ["renzo"], + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "ethereum-balancer", + "poolId": "0x596192bb6e41802428ac943d2f1476c1af25cc0e000000000000000000000659", + "poolType": "composable-stable", + "tokens": [ + "0x596192bB6e41802428Ac943D2f1476C1Af25CC0E", + "0xbf5495Efe5DB9ce00f80364C8B423567e58d2110", + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + ] + } + ] }, { "id": "conic-eth", @@ -2007,9 +2125,9 @@ "tokenAddress": "0xdfE6e7e18f6Cc65FA13C8D8966013d4FdA74b6ba", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xB2975e68b3FAc3b8ff444660cc4cee907367EB0C", "earnedToken": "mooAuraankrETH-wstETH", "earnedTokenAddress": "0xB2975e68b3FAc3b8ff444660cc4cee907367EB0C", - "earnContractAddress": "0xB2975e68b3FAc3b8ff444660cc4cee907367EB0C", "oracle": "lps", "oracleId": "aura-ankreth-wsteth", "status": "active", @@ -2020,7 +2138,20 @@ "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0xdfe6e7e18f6cc65fa13c8d8966013d4fda74b6ba000000000000000000000558/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0xdfe6e7e18f6cc65fa13c8d8966013d4fda74b6ba000000000000000000000558/remove-liquidity", - "network": "ethereum" + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "ethereum-balancer", + "poolId": "0xdfe6e7e18f6cc65fa13c8d8966013d4fda74b6ba000000000000000000000558", + "poolType": "composable-stable", + "tokens": [ + "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + "0xdfE6e7e18f6Cc65FA13C8D8966013d4FdA74b6ba", + "0xE95A203B1a91a908F9B9CE46459d101078c2c3cb" + ] + } + ] }, { "id": "aura-sdai-3pool", @@ -2030,9 +2161,9 @@ "tokenAddress": "0x49cbD67651fbabCE12d1df18499896ec87BEf46f", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x8D719AD5Cc19d5413A9D11Bd39813847f352De65", "earnedToken": "mooAurasDAI-3Pool", "earnedTokenAddress": "0x8D719AD5Cc19d5413A9D11Bd39813847f352De65", - "earnContractAddress": "0x8D719AD5Cc19d5413A9D11Bd39813847f352De65", "oracle": "lps", "oracleId": "aura-sdai-3pool", "status": "active", @@ -2043,7 +2174,20 @@ "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x49cbd67651fbabce12d1df18499896ec87bef46f00000000000000000000064a/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x49cbd67651fbabce12d1df18499896ec87bef46f00000000000000000000064a/remove-liquidity", - "network": "ethereum" + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "ethereum-balancer", + "poolId": "0x49cbd67651fbabce12d1df18499896ec87bef46f00000000000000000000064a", + "poolType": "composable-stable", + "tokens": [ + "0x49cbD67651fbabCE12d1df18499896ec87BEf46f", + "0x79c58f70905F734641735BC61e45c19dD9Ad60bC", + "0x83F20F44975D03b1b09e64809B757c47f942BEeA" + ] + } + ] }, { "id": "convex-eth+", @@ -2750,9 +2894,9 @@ "tokenAddress": "0x8353157092ED8Be69a9DF8F95af097bbF33Cb2aF", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x234Fd76985dA4fD505DbAF7A48e119Cd5dFD5C8F", "earnedToken": "mooAuraGHO/USDC/USDT", "earnedTokenAddress": "0x234Fd76985dA4fD505DbAF7A48e119Cd5dFD5C8F", - "earnContractAddress": "0x234Fd76985dA4fD505DbAF7A48e119Cd5dFD5C8F", "oracle": "lps", "oracleId": "aura-gho-usdc-usdt", "status": "active", @@ -2763,7 +2907,21 @@ "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x8353157092ed8be69a9df8f95af097bbf33cb2af0000000000000000000005d9/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x8353157092ed8be69a9df8f95af097bbf33cb2af0000000000000000000005d9/remove-liquidity", - "network": "ethereum" + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "ethereum-balancer", + "poolId": "0x8353157092ed8be69a9df8f95af097bbf33cb2af0000000000000000000005d9", + "poolType": "composable-stable", + "tokens": [ + "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f", + "0x8353157092ED8Be69a9DF8F95af097bbF33Cb2aF", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "0xdAC17F958D2ee523a2206206994597C13D831ec7" + ] + } + ] }, { "id": "aura-gyro-wsteth-eth", @@ -2918,9 +3076,9 @@ "tokenAddress": "0x42ED016F826165C2e5976fe5bC3df540C5aD0Af7", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xd4D620B23E91031fa08045b6083878f42558d6b9", "earnedToken": "mooAurawstETH/sfrxETH/rETH V3", "earnedTokenAddress": "0xd4D620B23E91031fa08045b6083878f42558d6b9", - "earnContractAddress": "0xd4D620B23E91031fa08045b6083878f42558d6b9", "oracle": "lps", "oracleId": "aura-wsteth-reth-sfrxeth-v3", "status": "active", @@ -2931,7 +3089,21 @@ "strategyTypeId": "multi-lp", "addLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x42ed016f826165c2e5976fe5bc3df540c5ad0af700000000000000000000058b/invest/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x42ed016f826165c2e5976fe5bc3df540c5ad0af700000000000000000000058b/remove-liquidity", - "network": "ethereum" + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "ethereum-balancer", + "poolId": "0x42ed016f826165c2e5976fe5bc3df540c5ad0af700000000000000000000058b", + "poolType": "composable-stable", + "tokens": [ + "0x42ED016F826165C2e5976fe5bC3df540C5aD0Af7", + "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + "0xac3E018457B222d93114458476f3E3416Abbe38F", + "0xae78736Cd615f374D3085123A210448E74Fc6393" + ] + } + ] }, { "id": "conic-crvusd-eol", diff --git a/src/config/vault/fantom.json b/src/config/vault/fantom.json index 28ce2bb8f..30a6bf5c3 100644 --- a/src/config/vault/fantom.json +++ b/src/config/vault/fantom.json @@ -166,9 +166,9 @@ "tokenAddress": "0xaAc7B8d6D7009428d6DdaD895bdf50C6FCBbe2C0", "tokenDecimals": 18, "tokenProviderId": "beethovenx", + "earnContractAddress": "0xEd6e49d62e960963D7376240A0b6aF869c0b5c04", "earnedToken": "mooBeetsTriooftheStables", "earnedTokenAddress": "0xEd6e49d62e960963D7376240A0b6aF869c0b5c04", - "earnContractAddress": "0xEd6e49d62e960963D7376240A0b6aF869c0b5c04", "oracle": "lps", "oracleId": "beets-trio-of-the-stables", "status": "active", @@ -186,7 +186,21 @@ "strategyTypeId": "multi-lp", "addLiquidityUrl": "https://beets.fi/pool/0xaac7b8d6d7009428d6ddad895bdf50c6fcbbe2c000000000000000000000080d", "removeLiquidityUrl": "https://beets.fi/pool/0xaac7b8d6d7009428d6ddad895bdf50c6fcbbe2c000000000000000000000080d", - "network": "fantom" + "network": "fantom", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "fantom-beethovenx", + "poolId": "0xaac7b8d6d7009428d6ddad895bdf50c6fcbbe2c000000000000000000000080d", + "poolType": "composable-stable", + "tokens": [ + "0x1B6382DBDEa11d97f24495C9A90b7c88469134a4", + "0x28a92dde19D9989F39A49905d7C9C2FAc7799bDf", + "0x2F733095B80A04b38b0D10cC884524a3d09b836a", + "0xaAc7B8d6D7009428d6DdaD895bdf50C6FCBbe2C0" + ] + } + ] }, { "id": "equalizer-ichi-wftm-lzusdc", @@ -324,9 +338,9 @@ "tokenAddress": "0x593000b762dE3c465855336E95c8BB46080AF064", "tokenDecimals": 18, "tokenProviderId": "beethovenx", + "earnContractAddress": "0xEEbCc5Bd9603424fc71Fa5aC48C574235a00aa49", "earnedToken": "mooBeetsFantomMenace", "earnedTokenAddress": "0xEEbCc5Bd9603424fc71Fa5aC48C574235a00aa49", - "earnContractAddress": "0xEEbCc5Bd9603424fc71Fa5aC48C574235a00aa49", "oracle": "lps", "oracleId": "beets-fantom-menace", "status": "active", @@ -344,7 +358,20 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://beets.fi/pool/0x593000b762de3c465855336e95c8bb46080af064000000000000000000000760", "removeLiquidityUrl": "https://beets.fi/pool/0x593000b762de3c465855336e95c8bb46080af064000000000000000000000760", - "network": "fantom" + "network": "fantom", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "fantom-beethovenx", + "poolId": "0x593000b762de3c465855336e95c8bb46080af064000000000000000000000760", + "poolType": "composable-stable", + "tokens": [ + "0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83", + "0x593000b762dE3c465855336E95c8BB46080AF064", + "0xd7028092c830b5C8FcE061Af2E593413EbbC1fc1" + ] + } + ] }, { "id": "beets-love-thy-stables", diff --git a/src/config/vault/gnosis.json b/src/config/vault/gnosis.json index e6051ace3..ad84594a7 100644 --- a/src/config/vault/gnosis.json +++ b/src/config/vault/gnosis.json @@ -67,9 +67,9 @@ "tokenAddress": "0xfC095C811fE836Ed12f247BCf042504342B73FB7", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xe21396A3F0e752AC03b195Cf140de84E58E58771", "earnedToken": "mooAuraGnosisUSDC.e/USDT/sDAI", "earnedTokenAddress": "0xe21396A3F0e752AC03b195Cf140de84E58E58771", - "earnContractAddress": "0xe21396A3F0e752AC03b195Cf140de84E58E58771", "oracle": "lps", "oracleId": "aura-gnosis-usdc.e-usdt-sdai", "status": "active", @@ -87,7 +87,21 @@ "strategyTypeId": "multi-lp", "addLiquidityUrl": "https://balancer.fi/pools/gnosis/v2/0xfc095c811fe836ed12f247bcf042504342b73fb700000000000000000000009f/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/gnosis/v2/0xfc095c811fe836ed12f247bcf042504342b73fb700000000000000000000009f/remove-liquidity", - "network": "gnosis" + "network": "gnosis", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "gnosis-balancer", + "poolId": "0xfc095c811fe836ed12f247bcf042504342b73fb700000000000000000000009f", + "poolType": "composable-stable", + "tokens": [ + "0x2a22f9c3b484c3629090FeED35F17Ff8F88f76F0", + "0x4ECaBa5870353805a9F068101A40E0f32ed605C6", + "0xaf204776c7245bF4147c2612BF6e5972Ee483701", + "0xfC095C811fE836Ed12f247BCf042504342B73FB7" + ] + } + ] }, { "id": "aura-gnosis-steur-eure", @@ -97,9 +111,9 @@ "tokenAddress": "0x06135A9Ae830476d3a941baE9010B63732a055F4", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x78c90c28eDc5F4B266bfD40b01e7dE8Ebb3682F7", "earnedToken": "mooAuraGnosisstEUR-EURe", "earnedTokenAddress": "0x78c90c28eDc5F4B266bfD40b01e7dE8Ebb3682F7", - "earnContractAddress": "0x78c90c28eDc5F4B266bfD40b01e7dE8Ebb3682F7", "oracle": "lps", "oracleId": "aura-gnosis-steur-eure", "status": "active", @@ -117,7 +131,20 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/gnosis/v2/0x06135a9ae830476d3a941bae9010b63732a055f4000000000000000000000065/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/gnosis/v2/0x06135a9ae830476d3a941bae9010b63732a055f4000000000000000000000065/remove-liquidity", - "network": "gnosis" + "network": "gnosis", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "gnosis-balancer", + "poolId": "0x06135a9ae830476d3a941bae9010b63732a055f4000000000000000000000065", + "poolType": "composable-stable", + "tokens": [ + "0x004626A008B1aCdC4c74ab51644093b155e59A23", + "0x06135A9Ae830476d3a941baE9010B63732a055F4", + "0xcB444e90D8198415266c6a2724b7900fb12FC56E" + ] + } + ] }, { "id": "aura-gnosis-crvusd-sdai", @@ -127,9 +154,9 @@ "tokenAddress": "0xc9F00C3a713008DDf69b768d90d4978549bFDF94", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xf9aC09EC1Ab2e4Cd5ca2Cf9451d3A49Fd6B25CC5", "earnedToken": "mooAuraGnosiscrvUSD-sDAI", "earnedTokenAddress": "0xf9aC09EC1Ab2e4Cd5ca2Cf9451d3A49Fd6B25CC5", - "earnContractAddress": "0xf9aC09EC1Ab2e4Cd5ca2Cf9451d3A49Fd6B25CC5", "oracle": "lps", "oracleId": "aura-gnosis-crvusd-sdai", "status": "active", @@ -147,7 +174,20 @@ "strategyTypeId": "multi-lp", "addLiquidityUrl": "https://balancer.fi/pools/gnosis/v2/0xc9f00c3a713008ddf69b768d90d4978549bfdf9400000000000000000000006d/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/gnosis/v2/0xc9f00c3a713008ddf69b768d90d4978549bfdf9400000000000000000000006d/remove-liquidity", - "network": "gnosis" + "network": "gnosis", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "gnosis-balancer", + "poolId": "0xc9f00c3a713008ddf69b768d90d4978549bfdf9400000000000000000000006d", + "poolType": "composable-stable", + "tokens": [ + "0xaBEf652195F98A91E490f047A5006B71c85f058d", + "0xaf204776c7245bF4147c2612BF6e5972Ee483701", + "0xc9F00C3a713008DDf69b768d90d4978549bFDF94" + ] + } + ] }, { "id": "aura-gnosis-wsteth-cow", @@ -217,9 +257,9 @@ "tokenAddress": "0x7644fA5D0eA14FcF3E813Fdf93ca9544f8567655", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x4B6EDd5E8f68A7B6502AF7872CEDc5c31C343b00", "earnedToken": "mooAuraGnosisUSDC/USDT/sDAI", "earnedTokenAddress": "0x4B6EDd5E8f68A7B6502AF7872CEDc5c31C343b00", - "earnContractAddress": "0x4B6EDd5E8f68A7B6502AF7872CEDc5c31C343b00", "oracle": "lps", "oracleId": "aura-gnosis-sdai-usdt-usdc", "status": "active", @@ -237,7 +277,21 @@ "strategyTypeId": "multi-lp", "addLiquidityUrl": "https://balancer.fi/pools/gnosis/v2/0x7644fa5d0ea14fcf3e813fdf93ca9544f8567655000000000000000000000066/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/gnosis/v2/0x7644fa5d0ea14fcf3e813fdf93ca9544f8567655000000000000000000000066/remove-liquidity", - "network": "gnosis" + "network": "gnosis", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "gnosis-balancer", + "poolId": "0x7644fa5d0ea14fcf3e813fdf93ca9544f8567655000000000000000000000066", + "poolType": "composable-stable", + "tokens": [ + "0x4ECaBa5870353805a9F068101A40E0f32ed605C6", + "0x7644fA5D0eA14FcF3E813Fdf93ca9544f8567655", + "0xaf204776c7245bF4147c2612BF6e5972Ee483701", + "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83" + ] + } + ] }, { "id": "aura-gnosis-gno-cow", @@ -279,9 +333,9 @@ "tokenAddress": "0xDd439304A77f54B1F7854751Ac1169b279591Ef7", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x3e442AF13Db47e27B48dE6Caf2708b2d180B10F9", "earnedToken": "mooAuraGnosissDAI-EURe", "earnedTokenAddress": "0x3e442AF13Db47e27B48dE6Caf2708b2d180B10F9", - "earnContractAddress": "0x3e442AF13Db47e27B48dE6Caf2708b2d180B10F9", "oracle": "lps", "oracleId": "aura-gnosis-sdai-eure", "status": "active", @@ -299,7 +353,20 @@ "strategyTypeId": "multi-lp", "addLiquidityUrl": "https://balancer.fi/pools/gnosis/v2/0xdd439304a77f54b1f7854751ac1169b279591ef7000000000000000000000064/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/gnosis/v2/0xdd439304a77f54b1f7854751ac1169b279591ef7000000000000000000000064/remove-liquidity", - "network": "gnosis" + "network": "gnosis", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "gnosis-balancer", + "poolId": "0xdd439304a77f54b1f7854751ac1169b279591ef7000000000000000000000064", + "poolType": "composable-stable", + "tokens": [ + "0xaf204776c7245bF4147c2612BF6e5972Ee483701", + "0xcB444e90D8198415266c6a2724b7900fb12FC56E", + "0xDd439304A77f54B1F7854751Ac1169b279591Ef7" + ] + } + ] }, { "id": "aura-gnosis-wsteth-bal-aura", @@ -369,9 +436,9 @@ "tokenAddress": "0xbAd20c15A773bf03ab973302F61FAbceA5101f0A", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xa12FB8417cFf62F717235009C58140c8F91C660E", "earnedToken": "mooAuraGnosiswstETH-ETH", "earnedTokenAddress": "0xa12FB8417cFf62F717235009C58140c8F91C660E", - "earnContractAddress": "0xa12FB8417cFf62F717235009C58140c8F91C660E", "oracle": "lps", "oracleId": "aura-gnosis-wsteth-weth", "status": "active", @@ -389,6 +456,19 @@ "strategyTypeId": "multi-lp", "addLiquidityUrl": "https://balancer.fi/pools/gnosis/v2/0xbad20c15a773bf03ab973302f61fabcea5101f0a000000000000000000000034/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/gnosis/v2/0xbad20c15a773bf03ab973302f61fabcea5101f0a000000000000000000000034/remove-liquidity", - "network": "gnosis" + "network": "gnosis", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "gnosis-balancer", + "poolId": "0xbad20c15a773bf03ab973302f61fabcea5101f0a000000000000000000000034", + "poolType": "composable-stable", + "tokens": [ + "0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1", + "0x6C76971f98945AE98dD7d4DFcA8711ebea946eA6", + "0xbAd20c15A773bf03ab973302F61FAbceA5101f0A" + ] + } + ] } ] diff --git a/src/config/vault/optimism.json b/src/config/vault/optimism.json index 54d59c2ac..12e7d3bf2 100644 --- a/src/config/vault/optimism.json +++ b/src/config/vault/optimism.json @@ -838,9 +838,9 @@ "tokenAddress": "0x2Bb4712247D5F451063b5E4f6948abDfb925d93D", "tokenDecimals": 18, "tokenProviderId": "beethovenx", + "earnContractAddress": "0xCF6fC7C5b95caC374400c16CBecd24CBa0cfeAD6", "earnedToken": "mooAuraOPwstETH-weETH", "earnedTokenAddress": "0xCF6fC7C5b95caC374400c16CBecd24CBa0cfeAD6", - "earnContractAddress": "0xCF6fC7C5b95caC374400c16CBecd24CBa0cfeAD6", "oracle": "lps", "oracleId": "aura-op-wsteth-weeth", "status": "active", @@ -851,8 +851,21 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://op.beets.fi/pool/0x2bb4712247d5f451063b5e4f6948abdfb925d93d000000000000000000000136", "removeLiquidityUrl": "https://op.beets.fi/pool/0x2bb4712247d5f451063b5e4f6948abdfb925d93d000000000000000000000136", + "pointStructureIds": ["etherfi"], "network": "optimism", - "pointStructureIds": ["etherfi"] + "zaps": [ + { + "strategyId": "balancer", + "ammId": "optimism-beethovenx", + "poolId": "0x2bb4712247d5f451063b5e4f6948abdfb925d93d000000000000000000000136", + "poolType": "composable-stable", + "tokens": [ + "0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb", + "0x2Bb4712247D5F451063b5E4f6948abDfb925d93D", + "0x5A7fACB970D094B6C7FF1df0eA68D99E6e73CBFF" + ] + } + ] }, { "id": "sushi-cow-op-weth-wbtc-rp", @@ -3184,9 +3197,9 @@ "tokenAddress": "0x73A7fe27fe9545D53924E529Acf11F3073841b9e", "tokenDecimals": 18, "tokenProviderId": "beethovenx", + "earnContractAddress": "0x2160BEDE9d5559bA559Eaf88052b46b8364eE245", "earnedToken": "mooAuraOPWETH-wrsETH", "earnedTokenAddress": "0x2160BEDE9d5559bA559Eaf88052b46b8364eE245", - "earnContractAddress": "0x2160BEDE9d5559bA559Eaf88052b46b8364eE245", "oracle": "lps", "oracleId": "aura-op-weth-wrseth", "status": "active", @@ -3197,8 +3210,21 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://op.beets.fi/pool/0x73a7fe27fe9545d53924e529acf11f3073841b9e000000000000000000000133", "removeLiquidityUrl": "https://op.beets.fi/pool/0x73a7fe27fe9545d53924e529acf11f3073841b9e000000000000000000000133", + "pointStructureIds": ["kelp"], "network": "optimism", - "pointStructureIds": ["kelp"] + "zaps": [ + { + "strategyId": "balancer", + "ammId": "optimism-beethovenx", + "poolId": "0x73a7fe27fe9545d53924e529acf11f3073841b9e000000000000000000000133", + "poolType": "composable-stable", + "tokens": [ + "0x4200000000000000000000000000000000000006", + "0x73A7fe27fe9545D53924E529Acf11F3073841b9e", + "0x87eEE96D50Fb761AD85B1c982d28A042169d61b1" + ] + } + ] }, { "id": "stargate-v2-op-weth", @@ -4817,9 +4843,9 @@ "tokenAddress": "0xA71021492a3966EeC735Ed1B505aFa097c7cFe6f", "tokenDecimals": 18, "tokenProviderId": "beethovenx", + "earnContractAddress": "0x416c578DFBEB2f1b07b0165a2e5F4C467F4Ed2F8", "earnedToken": "mooAuraOPFraximalistEthereum", "earnedTokenAddress": "0x416c578DFBEB2f1b07b0165a2e5F4C467F4Ed2F8", - "earnContractAddress": "0x416c578DFBEB2f1b07b0165a2e5F4C467F4Ed2F8", "oracle": "lps", "oracleId": "aura-op-fraximalist-ethereum", "status": "active", @@ -4830,7 +4856,20 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://op.beets.fi/pool/0xa71021492a3966eec735ed1b505afa097c7cfe6f00000000000000000000010d", "removeLiquidityUrl": "https://op.beets.fi/pool/0xa71021492a3966eec735ed1b505afa097c7cfe6f00000000000000000000010d", - "network": "optimism" + "network": "optimism", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "optimism-beethovenx", + "poolId": "0xa71021492a3966eec735ed1b505afa097c7cfe6f00000000000000000000010d", + "poolType": "composable-stable", + "tokens": [ + "0x484c2D6e3cDd945a8B2DF735e079178C1036578c", + "0x6806411765Af15Bddd26f8f544A34cC40cb9838B", + "0xA71021492a3966EeC735Ed1B505aFa097c7cFe6f" + ] + } + ] }, { "id": "velodrome-v2-kuji-weth", @@ -5255,9 +5294,9 @@ "tokenAddress": "0x5F8893506Ddc4C271837187d14A9C87964a074Dc", "tokenDecimals": 18, "tokenProviderId": "beethovenx", + "earnContractAddress": "0x9236cd1Cb2df141E842F825B95f51742CC930814", "earnedToken": "mooAuraOPEthereumTriplets", "earnedTokenAddress": "0x9236cd1Cb2df141E842F825B95f51742CC930814", - "earnContractAddress": "0x9236cd1Cb2df141E842F825B95f51742CC930814", "oracle": "lps", "oracleId": "aura-op-ethereum-triplets", "status": "active", @@ -5268,7 +5307,21 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://op.beets.fi/pool/0x5f8893506ddc4c271837187d14a9c87964a074dc000000000000000000000106", "removeLiquidityUrl": "https://op.beets.fi/pool/0x5f8893506ddc4c271837187d14a9c87964a074dc000000000000000000000106", - "network": "optimism" + "network": "optimism", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "optimism-beethovenx", + "poolId": "0x5f8893506ddc4c271837187d14a9c87964a074dc000000000000000000000106", + "poolType": "composable-stable", + "tokens": [ + "0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb", + "0x484c2D6e3cDd945a8B2DF735e079178C1036578c", + "0x5F8893506Ddc4C271837187d14A9C87964a074Dc", + "0x9Bcef72be871e61ED4fBbc7630889beE758eb81D" + ] + } + ] }, { "id": "aura-op-ovn-wusd+", @@ -5334,9 +5387,9 @@ "tokenAddress": "0x004700ba0a4f5f22e1E78a277fCA55e36F47E09C", "tokenDecimals": 18, "tokenProviderId": "beethovenx", + "earnContractAddress": "0xb996DD8f4ec36A34CeDFD8687aDa991bC2397232", "earnedToken": "mooAuraOPrETH-ankrETH", "earnedTokenAddress": "0xb996DD8f4ec36A34CeDFD8687aDa991bC2397232", - "earnContractAddress": "0xb996DD8f4ec36A34CeDFD8687aDa991bC2397232", "oracle": "lps", "oracleId": "aura-op-reth-ankreth", "status": "active", @@ -5347,7 +5400,20 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://op.beets.fi/pool/0x004700ba0a4f5f22e1e78a277fca55e36f47e09c000000000000000000000104", "removeLiquidityUrl": "https://op.beets.fi/pool/0x004700ba0a4f5f22e1e78a277fca55e36f47e09c000000000000000000000104", - "network": "optimism" + "network": "optimism", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "optimism-beethovenx", + "poolId": "0x004700ba0a4f5f22e1e78a277fca55e36f47e09c000000000000000000000104", + "poolType": "composable-stable", + "tokens": [ + "0x004700ba0a4f5f22e1E78a277fCA55e36F47E09C", + "0x9Bcef72be871e61ED4fBbc7630889beE758eb81D", + "0xe05A08226c49b636ACf99c40Da8DC6aF83CE5bB3" + ] + } + ] }, { "id": "aura-op-usdc-usdce-usdt-dai", @@ -5357,9 +5423,9 @@ "tokenAddress": "0x9Da11Ff60bfc5aF527f58fd61679c3AC98d040d9", "tokenDecimals": 18, "tokenProviderId": "beethovenx", + "earnContractAddress": "0xC51e661EEf8d5F130Dc1bdD5Be28c1DBcE930D20", "earnedToken": "mooAuraOPUSDC/USDCe/USDT/DAI", "earnedTokenAddress": "0xC51e661EEf8d5F130Dc1bdD5Be28c1DBcE930D20", - "earnContractAddress": "0xC51e661EEf8d5F130Dc1bdD5Be28c1DBcE930D20", "oracle": "lps", "oracleId": "aura-op-usdc-usdce-usdt-dai", "status": "active", @@ -5370,7 +5436,22 @@ "strategyTypeId": "multi-lp", "addLiquidityUrl": "https://op.beets.fi/pool/0x9da11ff60bfc5af527f58fd61679c3ac98d040d9000000000000000000000100", "removeLiquidityUrl": "https://op.beets.fi/pool/0x9da11ff60bfc5af527f58fd61679c3ac98d040d9000000000000000000000100", - "network": "optimism" + "network": "optimism", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "optimism-beethovenx", + "poolId": "0x9da11ff60bfc5af527f58fd61679c3ac98d040d9000000000000000000000100", + "poolType": "composable-stable", + "tokens": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "0x7F5c764cBc14f9669B88837ca1490cCa17c31607", + "0x94b008aA00579c1307B0EF2c499aD98a8ce58e58", + "0x9Da11Ff60bfc5aF527f58fd61679c3AC98d040d9", + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ] + } + ] }, { "id": "aura--op-dola-usdce", diff --git a/src/config/vault/polygon.json b/src/config/vault/polygon.json index f4ef0c77a..1918c019f 100644 --- a/src/config/vault/polygon.json +++ b/src/config/vault/polygon.json @@ -662,9 +662,9 @@ "tokenAddress": "0x4B7586A4F49841447150D3d92d9E9e000f766c30", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xF4C44A0A7b48A016861eE5cAe3dE82126366cCC9", "earnedToken": "mooBalancerPolygonUSDC/DAI/USDT", "earnedTokenAddress": "0xF4C44A0A7b48A016861eE5cAe3dE82126366cCC9", - "earnContractAddress": "0xF4C44A0A7b48A016861eE5cAe3dE82126366cCC9", "oracle": "lps", "oracleId": "balancer-polygon-usdc-dai-usdt", "status": "active", @@ -682,7 +682,21 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/polygon/v2/0x4b7586a4f49841447150d3d92d9e9e000f766c30000000000000000000000e8a/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/polygon/v2/0x4b7586a4f49841447150d3d92d9e9e000f766c30000000000000000000000e8a/remove-liquidity", - "network": "polygon" + "network": "polygon", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "polygon-balancer", + "poolId": "0x4b7586a4f49841447150d3d92d9e9e000f766c30000000000000000000000e8a", + "poolType": "composable-stable", + "tokens": [ + "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + "0x4B7586A4F49841447150D3d92d9E9e000f766c30", + "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063", + "0xc2132D05D31c914a87C6611C10748AEb04B58e8F" + ] + } + ] }, { "id": "aura-polygon-wmatic-maticx", @@ -692,9 +706,9 @@ "tokenAddress": "0xcd78A20c597E367A4e478a2411cEB790604D7c8F", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xF3B13e9D8133CeF4eb77Fc2134c5Af82d686bC17", "earnedToken": "mooAuraPolygonWMATIC-MaticX", "earnedTokenAddress": "0xF3B13e9D8133CeF4eb77Fc2134c5Af82d686bC17", - "earnContractAddress": "0xF3B13e9D8133CeF4eb77Fc2134c5Af82d686bC17", "oracle": "lps", "oracleId": "aura-polygon-wmatic-maticx", "status": "active", @@ -705,7 +719,20 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/polygon/v2/0xcd78a20c597e367a4e478a2411ceb790604d7c8f000000000000000000000c22/invest/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/polygon/v2/0xcd78a20c597e367a4e478a2411ceb790604d7c8f000000000000000000000c22/remove-liquidity", - "network": "polygon" + "network": "polygon", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "polygon-balancer", + "poolId": "0xcd78a20c597e367a4e478a2411ceb790604d7c8f000000000000000000000c22", + "poolType": "composable-stable", + "tokens": [ + "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", + "0xcd78A20c597E367A4e478a2411cEB790604D7c8F", + "0xfa68FB4628DFF1028CFEc22b4162FCcd0d45efb6" + ] + } + ] }, { "id": "uniswap-cow-poly-wbtc-usdc", @@ -3764,9 +3791,9 @@ "tokenAddress": "0x513CdEE00251F39DE280d9E5f771A6eaFebCc88E", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xB476B7A027da3D9fB5A5c9CB6078A34f7289B476", "earnedToken": "mooBalancerjEUR-PARv2", "earnedTokenAddress": "0xB476B7A027da3D9fB5A5c9CB6078A34f7289B476", - "earnContractAddress": "0xB476B7A027da3D9fB5A5c9CB6078A34f7289B476", "oracle": "lps", "oracleId": "balancer-jeur-par-v2", "status": "active", @@ -3785,7 +3812,20 @@ "strategyTypeId": "multi-lp", "addLiquidityUrl": "https://balancer.fi/pools/polygon/v2/0x513cdee00251f39de280d9e5f771a6eafebcc88e000000000000000000000a6b/invest/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/polygon/v2/0x513cdee00251f39de280d9e5f771a6eafebcc88e000000000000000000000a6b/remove-liquidity", - "network": "polygon" + "network": "polygon", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "polygon-balancer", + "poolId": "0x513cdee00251f39de280d9e5f771a6eafebcc88e000000000000000000000a6b", + "poolType": "composable-stable", + "tokens": [ + "0x4e3Decbb3645551B8A19f0eA1678079FCB33fB4c", + "0x513CdEE00251F39DE280d9E5f771A6eaFebCc88E", + "0xE2Aa7db6dA1dAE97C5f5C6914d285fBfCC32A128" + ] + } + ] }, { "id": "balancer-poly-eth-frxeth", diff --git a/src/features/data/apis/amm/balancer/BalancerComposableStablePool.ts b/src/features/data/apis/amm/balancer/BalancerComposableStablePool.ts new file mode 100644 index 000000000..cabaaebc0 --- /dev/null +++ b/src/features/data/apis/amm/balancer/BalancerComposableStablePool.ts @@ -0,0 +1,372 @@ +import type { IBalancerPool } from './types'; +import type { ChainEntity } from '../../../entities/chain'; +import BigNumber from 'bignumber.js'; +import { getWeb3Instance } from '../../instances'; +import { viemToWeb3Abi } from '../../../../../helpers/web3'; +import { ZERO_ADDRESS } from '../../../../../helpers/addresses'; +import { BalancerVaultAbi } from '../../../../../config/abi/BalancerVaultAbi'; +import { BIG_ZERO } from '../../../../../helpers/big-number'; +import type { ZapStep } from '../../transact/zap/types'; +import { getUnixNow } from '../../../../../helpers/date'; +import abiCoder from 'web3-eth-abi'; +import type { AbiItem } from 'web3-utils'; +import { getInsertIndex } from '../../transact/helpers/zap'; + +export type BalancerComposableStablePoolConfig = { + chain: ChainEntity; + /** address */ + vaultAddress: string; + /** address */ + poolAddress: string; + /** bytes32 */ + poolId: string; + /** address[] */ + tokens: string[]; +}; + +enum SwapKind { + GIVEN_IN = 0, + GIVEN_OUT = 1, +} + +type SingleSwap = { + /** bytes32 */ + poolId: string; + /** uint8 */ + kind: SwapKind; + /** address */ + assetIn: string; + /** address */ + assetOut: string; + /** uint256 */ + amount: string; + /** bytes */ + userData: string; +}; + +type BatchSwapStep = { + /** bytes32 */ + poolId: string; + /** uint256 */ + assetInIndex: number; + /** uint256 */ + assetOutIndex: number; + /** uint256 */ + amount: string; + /** bytes */ + userData: string; +}; + +type FundManagement = { + /** address */ + sender: string; + /** bool */ + fromInternalBalance: boolean; + /** address */ + recipient: string; + /** bool */ + toInternalBalance: boolean; +}; + +type QueryBatchSwapArgs = { + /** uint8 */ + kind: SwapKind; + /** tuple[] */ + swaps: BatchSwapStep[]; + /** address[] */ + assets: string[]; + /** tuple */ + funds: FundManagement; +}; + +type SwapArgs = { + /** tuple */ + singleSwap: SingleSwap; + /** tuple */ + funds: FundManagement; + /** uint256 */ + limit: string; + /** uint256 */ + deadline: number; +}; + +type BatchSwapArgs = { + /** uint8 */ + kind: SwapKind; + /** tuple[] */ + swaps: BatchSwapStep[]; + /** address[] */ + assets: string[]; + /** tuple */ + funds: FundManagement; + /** int256[] : +ve for tokens sent to the pool, -ve for tokens received from the pool */ + limits: string[]; + /** uint256 */ + deadline: number; +}; + +const queryFunds: FundManagement = { + sender: ZERO_ADDRESS, + fromInternalBalance: false, + recipient: ZERO_ADDRESS, + toInternalBalance: false, +}; + +const THIRTY_MINUTES_IN_SECONDS = 30 * 60; + +export class BalancerComposableStablePool implements IBalancerPool { + public readonly type = 'balancer'; + public readonly poolTokenIndex: number; + + constructor(protected readonly config: BalancerComposableStablePoolConfig) { + this.poolTokenIndex = this.getTokenIndex(this.config.poolAddress); + } + + async quoteAddLiquidityOneToken(tokenIn: string, amountIn: BigNumber): Promise { + this.checkToken(tokenIn, 'tokenIn'); + this.checkAmount(amountIn, 'amountIn'); + + const args: QueryBatchSwapArgs = { + kind: SwapKind.GIVEN_IN, + swaps: [ + { + poolId: this.config.poolId, + assetInIndex: 0, + assetOutIndex: 1, + amount: amountIn.toString(10), + userData: '0x', + }, + ], + assets: [tokenIn, this.config.poolAddress], + funds: queryFunds, + }; + + const [vaultInputDelta, vaultOutputDelta] = await this.queryBatchSwap(args); + + if (!vaultInputDelta.eq(amountIn)) { + throw new Error('Not all input used'); + } + + if (vaultOutputDelta.gte(BIG_ZERO)) { + throw new Error('Output is negative'); + } + + return vaultOutputDelta.abs(); + } + + async quoteRemoveLiquidityOneToken(amountIn: BigNumber, tokenOut: string): Promise { + this.checkAmount(amountIn, 'amountIn'); + this.checkToken(tokenOut, 'tokenOut'); + + const args: QueryBatchSwapArgs = { + kind: SwapKind.GIVEN_IN, + swaps: [ + { + poolId: this.config.poolId, + assetInIndex: 0, + assetOutIndex: 1, + amount: amountIn.toString(10), + userData: '0x', + }, + ], + assets: [this.config.poolAddress, tokenOut], + funds: queryFunds, + }; + + const [vaultInputDelta, vaultOutputDelta] = await this.queryBatchSwap(args); + + if (!vaultInputDelta.eq(amountIn)) { + throw new Error('Not all input used'); + } + + if (vaultOutputDelta.gte(BIG_ZERO)) { + throw new Error('Output is negative'); + } + + return vaultOutputDelta.abs(); + } + + async getAddLiquidityOneTokenZap( + tokenIn: string, + amountIn: BigNumber, + amountOutMin: BigNumber, + from: string, + insertBalance: boolean, + deadlineSeconds: number = THIRTY_MINUTES_IN_SECONDS + ): Promise { + return this.getSwapZap( + tokenIn, + amountIn, + this.config.poolAddress, + amountOutMin, + from, + insertBalance, + deadlineSeconds + ); + } + + async getRemoveLiquidityOneTokenZap( + amountIn: BigNumber, + tokenOut: string, + amountOutMin: BigNumber, + from: string, + insertBalance: boolean, + deadlineSeconds: number = THIRTY_MINUTES_IN_SECONDS + ): Promise { + return this.getSwapZap( + this.config.poolAddress, + amountIn, + tokenOut, + amountOutMin, + from, + insertBalance, + deadlineSeconds + ); + } + + protected async getSwapZap( + tokenIn: string, + amountIn: BigNumber, + tokenOut: string, + amountOutMin: BigNumber, + from: string, + insertBalance: boolean, + deadlineSeconds: number = THIRTY_MINUTES_IN_SECONDS + ): Promise { + this.checkToken(tokenIn, 'tokenIn'); + this.checkAmount(amountIn, 'amountIn'); + this.checkToken(tokenOut, 'tokenOut'); + this.checkAmount(amountOutMin, 'amountOutMin'); + + const args: SwapArgs = { + singleSwap: { + poolId: this.config.poolId, + kind: SwapKind.GIVEN_IN, + assetIn: tokenIn, + assetOut: tokenOut, + amount: amountIn.toString(10), + userData: '0x', + }, + funds: { + sender: from, + fromInternalBalance: false, + recipient: from, + toInternalBalance: false, + }, + limit: amountOutMin.toString(10), + deadline: getUnixNow() + deadlineSeconds, + }; + + /* + the byte offset at which amountIn is inserted in the calldata + calculated using 32-byte words: + 00 : swap offset (07) + valueAt(00) + 0 : singleSwap.poolId + .. + valueAt(00) + 4 : singleSwap.amount + */ + const amountInIndex = getInsertIndex(7 + 4); + + return { + target: this.config.vaultAddress, + value: '0', + data: this.encodeSwap(args), + tokens: insertBalance ? [{ token: tokenIn, index: amountInIndex }] : [], + }; + } + + protected encodeSwap(args: SwapArgs): string { + const methodAbi = BalancerVaultAbi.find(abi => abi.type === 'function' && abi.name === 'swap'); + if (!methodAbi) { + throw new Error('Method swap not found'); + } + + return abiCoder.encodeFunctionCall(methodAbi as AbiItem, [ + [ + args.singleSwap.poolId, + args.singleSwap.kind, + args.singleSwap.assetIn, + args.singleSwap.assetOut, + args.singleSwap.amount, + args.singleSwap.userData, + ], + [ + args.funds.sender, + args.funds.fromInternalBalance, + args.funds.recipient, + args.funds.toInternalBalance, + ], + args.limit, + args.deadline, + ]); + } + + protected encodeBatchSwap(args: BatchSwapArgs): string { + const methodAbi = BalancerVaultAbi.find( + abi => abi.type === 'function' && abi.name === 'batchSwap' + ); + if (!methodAbi) { + throw new Error('Method batchSwap not found'); + } + + return abiCoder.encodeFunctionCall(methodAbi as AbiItem, [ + args.kind, + args.swaps.map(swap => [ + swap.poolId, + swap.assetInIndex, + swap.assetOutIndex, + swap.amount, + swap.userData, + ]), + args.assets, + [ + args.funds.sender, + args.funds.fromInternalBalance, + args.funds.recipient, + args.funds.toInternalBalance, + ], + args.limits, + args.deadline, + ]); + } + + protected async queryBatchSwap(args: QueryBatchSwapArgs): Promise { + const web3 = await getWeb3Instance(this.config.chain); + const vault = new web3.eth.Contract(viemToWeb3Abi(BalancerVaultAbi), this.config.vaultAddress); + + const result: Array | undefined = await vault.methods + .queryBatchSwap(args.kind, args.swaps, args.assets, args.funds) + .call(); + + if (!result || !Array.isArray(result) || result.length !== args.assets.length) { + throw new Error('Invalid result'); + } + + return result.map(value => new BigNumber(value)); + } + + protected getTokenIndex(address: string): number { + const index = this.config.tokens.findIndex(token => token === address); + if (index === -1) { + throw new Error(`Address ${address} not found in tokens`); + } + return index; + } + + protected checkAmount(amount: BigNumber, label: string = 'amount') { + if (amount.lte(BIG_ZERO)) { + throw new Error(`${label} must be greater than 0`); + } + + if ((amount.decimalPlaces() || 0) > 0) { + throw new Error(`${label} must be in wei`); + } + } + + protected checkToken(token: string, label: string = 'token') { + const index = this.config.tokens.findIndex(t => t === token); + if (index === -1) { + throw new Error(`${label} must be a pool token`); + } + } +} diff --git a/src/features/data/apis/amm/balancer/types.ts b/src/features/data/apis/amm/balancer/types.ts new file mode 100644 index 000000000..68f441fc0 --- /dev/null +++ b/src/features/data/apis/amm/balancer/types.ts @@ -0,0 +1,3 @@ +export interface IBalancerPool { + readonly type: 'balancer'; +} diff --git a/src/features/data/apis/transact/helpers/amounts.ts b/src/features/data/apis/transact/helpers/amounts.ts index ac8b3312e..df76c8d92 100644 --- a/src/features/data/apis/transact/helpers/amounts.ts +++ b/src/features/data/apis/transact/helpers/amounts.ts @@ -11,6 +11,13 @@ export function slipBy(amount: BigNumber, slippage: number, decimals: number): B return amount.multipliedBy(1 - slippage).decimalPlaces(decimals, BigNumber.ROUND_FLOOR); } +export function slipTokenAmountBy(tokenAmount: TokenAmount, slippage: number): TokenAmount { + return { + token: tokenAmount.token, + amount: slipBy(tokenAmount.amount, slippage, tokenAmount.token.decimals), + }; +} + export function slipAllBy(inputs: TokenAmount[], slippage: number): TokenAmount[] { return inputs.map(({ token, amount }) => ({ token, diff --git a/src/features/data/apis/transact/strategies/balancer/BalancerStrategy.ts b/src/features/data/apis/transact/strategies/balancer/BalancerStrategy.ts new file mode 100644 index 000000000..ea8cbd263 --- /dev/null +++ b/src/features/data/apis/transact/strategies/balancer/BalancerStrategy.ts @@ -0,0 +1,1112 @@ +import type { Namespace, TFunction } from 'react-i18next'; +import { + isTokenEqual, + isTokenErc20, + isTokenNative, + type TokenEntity, + type TokenErc20, + type TokenNative, +} from '../../../../entities/token'; +import type { Step } from '../../../../reducers/wallet/stepper'; +import { + type BalancerDepositOption, + type BalancerDepositQuote, + type BalancerWithdrawOption, + type BalancerWithdrawQuote, + type InputTokenAmount, + isZapQuoteStepBuild, + isZapQuoteStepSplit, + isZapQuoteStepSwap, + isZapQuoteStepSwapAggregator, + isZapQuoteStepWithdraw, + type TokenAmount, + type ZapQuoteStep, + type ZapQuoteStepBuild, + type ZapQuoteStepSplit, + type ZapQuoteStepSwap, + type ZapQuoteStepSwapAggregator, +} from '../../transact-types'; +import type { IZapStrategy, IZapStrategyStatic, ZapTransactHelpers } from '../IStrategy'; +import type { ChainEntity } from '../../../../entities/chain'; +import { + createOptionId, + createQuoteId, + createSelectionId, + onlyOneInput, + onlyOneToken, + onlyOneTokenAmount, +} from '../../helpers/options'; +import { + selectChainNativeToken, + selectChainWrappedNativeToken, + selectIsTokenLoaded, + selectTokenByAddressOrUndefined, + selectTokenPriceByTokenOracleId, +} from '../../../../selectors/tokens'; +import { selectChainById } from '../../../../selectors/chains'; +import { TransactMode } from '../../../../reducers/wallet/transact-types'; +import { first, uniqBy } from 'lodash-es'; +import { + BIG_ZERO, + bigNumberToStringDeep, + fromWei, + fromWeiToTokenAmount, + toWeiFromTokenAmount, + toWeiString, +} from '../../../../../../helpers/big-number'; +import { calculatePriceImpact, highestFeeOrZero } from '../../helpers/quotes'; +import type BigNumber from 'bignumber.js'; +import type { BeefyState, BeefyThunk } from '../../../../../../redux-types'; +import type { BalancerTokenOption } from './types'; +import type { QuoteResponse } from '../../swap/ISwapProvider'; +import type { + OrderInput, + OrderOutput, + UserlessZapRequest, + ZapStep, + ZapStepResponse, +} from '../../zap/types'; +import { fetchZapAggregatorSwap } from '../../zap/swap'; +import { selectTransactSlippage } from '../../../../selectors/transact'; +import { Balances } from '../../helpers/Balances'; +import { getTokenAddress, NO_RELAY } from '../../helpers/zap'; +import { slipBy, slipTokenAmountBy } from '../../helpers/amounts'; +import { allTokensAreDistinct, pickTokens } from '../../helpers/tokens'; +import { walletActions } from '../../../../actions/wallet-actions'; +import { isStandardVault, type VaultStandard } from '../../../../entities/vault'; +import { getVaultWithdrawnFromState } from '../../helpers/vault'; +import { isFulfilledResult } from '../../../../../../helpers/promises'; +import { isDefined } from '../../../../utils/array-utils'; +import { isStandardVaultType, type IStandardVaultType } from '../../vaults/IVaultType'; +import type { BalancerStrategyConfig } from '../strategy-configs'; +import { BalancerComposableStablePool } from '../../../amm/balancer/BalancerComposableStablePool'; +import { type AmmEntityBalancer, isBalancerAmm } from '../../../../entities/zap'; +import { selectAmmById } from '../../../../selectors/zap'; +import { createFactory } from '../../../../utils/factory-utils'; + +type ZapHelpers = { + chain: ChainEntity; + slippage: number; + poolAddress: string; + state: BeefyState; +}; + +type DepositLiquidity = { + /** Liquidity input (coin for deposit, lp for withdraw) */ + input: TokenAmount; + /** Liquidity output (lp for deposit, coin for withdraw) */ + output: TokenAmount; + /** Which method we are using to deposit/withdraw liquidity */ + via: BalancerTokenOption; + /** Quote for swapping to/from coin if required */ + quote?: QuoteResponse; +}; + +type WithdrawLiquidity = DepositLiquidity & { + /** How much token we have after the split */ + split: TokenAmount; +}; + +const strategyId = 'balancer' as const; +type StrategyId = typeof strategyId; + +class BalancerStrategyImpl implements IZapStrategy { + public static readonly id = strategyId; + public readonly id = strategyId; + + protected readonly native: TokenNative; + protected readonly wnative: TokenErc20; + protected readonly possibleTokens: BalancerTokenOption[]; + protected readonly chain: ChainEntity; + protected readonly depositToken: TokenEntity; + protected readonly poolAddress: string; + protected readonly vault: VaultStandard; + protected readonly vaultType: IStandardVaultType; + protected readonly amm: AmmEntityBalancer; + + constructor(protected options: BalancerStrategyConfig, protected helpers: ZapTransactHelpers) { + const { vault, vaultType, getState } = this.helpers; + + if (!isStandardVault(vault)) { + throw new Error('Vault is not a standard vault'); + } + if (!isStandardVaultType(vaultType)) { + throw new Error('Vault type is not standard'); + } + + const state = getState(); + for (let i = 0; i < vault.assetIds.length; ++i) { + if (!selectIsTokenLoaded(state, vault.chainId, vault.assetIds[i])) { + throw new Error(`Vault ${vault.id}: Asset ${vault.assetIds[i]} not loaded`); + } + } + + const amm = selectAmmById(state, this.options.ammId); + if (!amm) { + throw new Error(`Vault ${vault.id}: AMM ${this.options.ammId} not found`); + } + if (!isBalancerAmm(amm)) { + throw new Error(`Vault ${vault.id}: AMM ${this.options.ammId} is not balancer type`); + } + + this.amm = amm; + this.vault = vault; + this.vaultType = vaultType; + this.native = selectChainNativeToken(state, vault.chainId); + this.wnative = selectChainWrappedNativeToken(state, vault.chainId); + this.depositToken = vaultType.depositToken; + this.chain = selectChainById(state, vault.chainId); + this.possibleTokens = this.selectAvailableTokens(state, this.chain.id, this.options.tokens); + + if (!this.possibleTokens.length) { + throw new Error( + `Vault ${ + vault.id + }: No tokens configured are available in addressbook, wanted one of ${this.options.tokens.join( + ', ' + )}` + ); + } + } + + /** + * Tokens are available so long as they are in the address book + */ + protected selectAvailableTokens( + state: BeefyState, + chainId: ChainEntity['id'], + tokenAddresses: string[] + ): BalancerTokenOption[] { + return tokenAddresses + .map((address, i) => { + const token = selectTokenByAddressOrUndefined(state, chainId, address); + if (!token) { + return undefined; + } + + const price = selectTokenPriceByTokenOracleId(state, token.oracleId); + if (!price || price.lte(BIG_ZERO)) { + return undefined; + } + + return { + index: i, + token, + price, + }; + }) + .filter(isDefined) + .filter(t => !isTokenEqual(t.token, this.depositToken)); + } + + public async fetchDepositOptions(): Promise { + const outputs = [this.vaultType.depositToken]; + + const baseOptions: BalancerDepositOption[] = this.possibleTokens.map(depositToken => { + const inputs = [depositToken.token]; + const selectionId = createSelectionId(this.vault.chainId, inputs); + + return { + id: createOptionId(this.id, this.vault.id, selectionId, 'direct'), + vaultId: this.vault.id, + chainId: this.vault.chainId, + selectionId, + selectionOrder: 2, + inputs, + wantedOutputs: outputs, + mode: TransactMode.Deposit, + strategyId: 'balancer', + via: 'direct', + viaToken: depositToken, + }; + }); + + const { any: allAggregatorTokens, map: tokenToDepositTokens } = + await this.aggregatorTokenSupport(); + + const aggregatorOptions: BalancerDepositOption[] = allAggregatorTokens + .filter(token => tokenToDepositTokens[token.address].length > 0) + .map(token => { + const inputs = [token]; + const selectionId = createSelectionId(this.vault.chainId, inputs); + const possible = tokenToDepositTokens[token.address]; + + if (possible.length === 0) { + console.error({ vault: this.vault.id, token, possible }); + throw new Error(`No other tokens supported for ${token.symbol}`); + } + + return { + id: createOptionId(this.id, this.vault.id, selectionId, 'aggregator'), + vaultId: this.vault.id, + chainId: this.vault.chainId, + selectionId, + selectionOrder: 3, + inputs, + wantedOutputs: outputs, + mode: TransactMode.Deposit, + strategyId: 'balancer', + via: 'aggregator', + viaTokens: possible, + }; + }); + + return baseOptions.concat(aggregatorOptions); + } + + protected getPool = createFactory(() => { + switch (this.options.poolType) { + case 'composable-stable': { + return new BalancerComposableStablePool({ + chain: this.chain, + vaultAddress: this.amm.vaultAddress, + poolAddress: this.depositToken.address, + poolId: this.options.poolId, + tokens: this.options.tokens, + }); + } + default: { + throw new Error(`Unsupported balancer pool type ${this.options.poolType}`); + } + } + }); + + protected async quoteAddLiquidityOneToken(input: TokenAmount): Promise { + const pool = this.getPool(); + const liquidity = await pool.quoteAddLiquidityOneToken( + input.token.address, + toWeiFromTokenAmount(input) + ); + return fromWeiToTokenAmount(liquidity, this.depositToken); + } + + protected async getDepositLiquidityDirect( + input: InputTokenAmount, + depositVia: BalancerTokenOption + ): Promise { + if (!isTokenEqual(input.token, depositVia.token)) { + throw new Error( + `Balancer strategy: Direct deposit called with input token ${input.token.symbol} but expected ${depositVia.token.symbol}` + ); + } + + return { + input, + output: await this.quoteAddLiquidityOneToken(input), + via: depositVia, + }; + } + + protected async getDepositLiquidityAggregator( + state: BeefyState, + input: InputTokenAmount, + depositVias: BalancerTokenOption[] + ): Promise { + const { swapAggregator } = this.helpers; + + // Fetch quotes from input token, to each possible deposit via token + const maybeQuotes = await Promise.allSettled( + depositVias.map(async depositVia => { + const quotes = await swapAggregator.fetchQuotes( + { + vaultId: this.vault.id, + fromToken: input.token, + fromAmount: input.amount, + toToken: depositVia.token, + }, + state + ); + const bestQuote = first(quotes); + if (!bestQuote) { + throw new Error(`No quote for ${input.token.symbol} to ${depositVia.token.symbol}`); + } + return { via: depositVia, quote: bestQuote }; + }) + ); + const quotes = maybeQuotes + .filter(isFulfilledResult) + .map(r => r.value) + .filter(isDefined); + if (!quotes.length) { + throw new Error(`No quotes for ${input.token.symbol} to any deposit via token`); + } + + // For the best quote per deposit via token, calculate how much liquidity we get + const withLiquidity = await Promise.all( + quotes.map(async ({ via, quote }) => { + const input = { token: quote.toToken, amount: quote.toAmount }; + return { + via, + quote, + input, + output: await this.quoteAddLiquidityOneToken(input), + }; + }) + ); + + // sort by most liquidity + withLiquidity.sort((a, b) => b.output.amount.comparedTo(a.output.amount)); + + // Get the one which gives the most liquidity + return withLiquidity[0]; + } + + protected async getDepositLiquidity( + state: BeefyState, + input: InputTokenAmount, + option: BalancerDepositOption + ): Promise { + if (option.via === 'direct') { + return this.getDepositLiquidityDirect(input, option.viaToken); + } + return this.getDepositLiquidityAggregator(state, input, option.viaTokens); + } + + public async fetchDepositQuote( + inputs: InputTokenAmount[], + option: BalancerDepositOption + ): Promise { + const { zap, getState } = this.helpers; + const state = getState(); + const input = onlyOneInput(inputs); + if (input.amount.lte(BIG_ZERO)) { + throw new Error('BalancerStrategy: Quote called with 0 input amount'); + } + + // Token allowances + const allowances = isTokenErc20(input.token) + ? [ + { + token: input.token, + amount: input.amount, + spenderAddress: zap.manager, + }, + ] + : []; + + // Fetch liquidity (and swap quote if aggregator) + const depositLiquidity = await this.getDepositLiquidity(state, input, option); + + // Build quote steps + const steps: ZapQuoteStep[] = []; + + if (depositLiquidity.quote) { + steps.push({ + type: 'swap', + fromToken: depositLiquidity.quote.fromToken, + fromAmount: depositLiquidity.quote.fromAmount, + toToken: depositLiquidity.quote.toToken, + toAmount: depositLiquidity.quote.toAmount, + via: 'aggregator', + providerId: depositLiquidity.quote.providerId, + fee: depositLiquidity.quote.fee, + quote: depositLiquidity.quote, + }); + } + + steps.push({ + type: 'build', + inputs: [depositLiquidity.input], + outputToken: depositLiquidity.output.token, + outputAmount: depositLiquidity.output.amount, + }); + + steps.push({ + type: 'deposit', + inputs: [ + { + token: depositLiquidity.output.token, + amount: depositLiquidity.output.amount, + }, + ], + }); + + // Build quote outputs + const outputs: TokenAmount[] = [depositLiquidity.output]; + const returned: TokenAmount[] = []; + + // Build quote + return { + id: createQuoteId(option.id), + strategyId: 'balancer', + priceImpact: calculatePriceImpact(inputs, outputs, returned, state), // includes the zap fee + option, + inputs, + outputs, + returned, + allowances, + steps, + fee: highestFeeOrZero(steps), + via: option.via, + viaToken: depositLiquidity.via, + }; + } + + protected async fetchZapSwap( + quoteStep: ZapQuoteStepSwap, + zapHelpers: ZapHelpers, + insertBalance: boolean + ): Promise { + if (isZapQuoteStepSwapAggregator(quoteStep)) { + return this.fetchZapSwapAggregator(quoteStep, zapHelpers, insertBalance); + } else { + throw new Error('Unknown zap quote swap step type'); + } + } + + protected async fetchZapSwapAggregator( + quoteStep: ZapQuoteStepSwapAggregator, + zapHelpers: ZapHelpers, + insertBalance: boolean + ): Promise { + const { swapAggregator, zap } = this.helpers; + const { slippage, state } = zapHelpers; + + return await fetchZapAggregatorSwap( + { + quote: quoteStep.quote, + inputs: [{ token: quoteStep.fromToken, amount: quoteStep.fromAmount }], + outputs: [{ token: quoteStep.toToken, amount: quoteStep.toAmount }], + maxSlippage: slippage, + zapRouter: zap.router, + providerId: quoteStep.providerId, + insertBalance, + }, + swapAggregator, + state + ); + } + + protected async fetchZapBuild( + quoteStep: ZapQuoteStepBuild, + depositVia: BalancerTokenOption, + minInputAmount: BigNumber, + zapHelpers: ZapHelpers, + insertBalance: boolean = false + ): Promise { + const { slippage } = zapHelpers; + const input = { token: depositVia.token, amount: minInputAmount }; + const liquidity = await this.quoteAddLiquidityOneToken(input); + const minLiquidity = slipTokenAmountBy(liquidity, slippage); + const pool = this.getPool(); + + return { + inputs: [input], + outputs: [liquidity], + minOutputs: [minLiquidity], + returned: [], + zaps: [ + await pool.getAddLiquidityOneTokenZap( + input.token.address, + toWeiFromTokenAmount(input), + toWeiFromTokenAmount(minLiquidity), + this.helpers.zap.router, + insertBalance + ), + ], + }; + } + + public async fetchDepositStep( + quote: BalancerDepositQuote, + t: TFunction> + ): Promise { + const zapAction: BeefyThunk = async (dispatch, getState, extraArgument) => { + const state = getState(); + const chain = selectChainById(state, this.vault.chainId); + const slippage = selectTransactSlippage(state); + const zapHelpers: ZapHelpers = { + chain, + slippage, + state, + poolAddress: this.depositToken.address, + }; + const steps: ZapStep[] = []; + const minBalances = new Balances(quote.inputs); + const swapQuotes = quote.steps.filter(isZapQuoteStepSwap); + const buildQuote = quote.steps.find(isZapQuoteStepBuild); + + if (!buildQuote) { + throw new Error('BalancerStrategy: No build step in quote'); + } + + // wrap and asset swap, 2 max + if (swapQuotes.length > 2) { + throw new Error('BalancerStrategy: Too many swaps'); + } + + // Swaps + if (swapQuotes.length) { + if (swapQuotes.length > 1) { + throw new Error('BalancerStrategy: Too many swaps in quote'); + } + + const swapQuote = swapQuotes[0]; + const swap = await this.fetchZapSwap(swapQuote, zapHelpers, true); + // add step to order + swap.zaps.forEach(zap => steps.push(zap)); + // track minimum balances for use in further steps + minBalances.subtractMany(swap.inputs); + minBalances.addMany(swap.minOutputs); + } + + // Build LP + const buildZap = await this.fetchZapBuild( + buildQuote, + quote.viaToken, + minBalances.get(quote.viaToken.token), + zapHelpers, + true + ); + console.debug('fetchDepositStep::buildZap', bigNumberToStringDeep(buildZap)); + buildZap.zaps.forEach(step => steps.push(step)); + minBalances.subtractMany(buildZap.inputs); + minBalances.addMany(buildZap.minOutputs); + + // Deposit in vault + const vaultDeposit = await this.vaultType.fetchZapDeposit({ + inputs: [ + { + token: buildQuote.outputToken, + amount: minBalances.get(buildQuote.outputToken), // min expected in case add liquidity slipped + max: true, // but we call depositAll + }, + ], + }); + console.log('fetchDepositStep::vaultDeposit', vaultDeposit); + steps.push(vaultDeposit.zap); + + // Build order + const inputs: OrderInput[] = quote.inputs.map(input => ({ + token: getTokenAddress(input.token), + amount: toWeiString(input.amount, input.token.decimals), + })); + + const requiredOutputs: OrderOutput[] = vaultDeposit.outputs.map(output => ({ + token: getTokenAddress(output.token), + minOutputAmount: toWeiString( + slipBy(output.amount, slippage, output.token.decimals), + output.token.decimals + ), + })); + + // We need to list all inputs, and mid-route outputs, as outputs so dust gets returned + const dustOutputs: OrderOutput[] = pickTokens( + quote.outputs, + quote.inputs, + quote.returned + ).map(token => ({ + token: getTokenAddress(token), + minOutputAmount: '0', + })); + + swapQuotes.forEach(quoteStep => { + dustOutputs.push({ + token: getTokenAddress(quoteStep.fromToken), + minOutputAmount: '0', + }); + dustOutputs.push({ + token: getTokenAddress(quoteStep.toToken), + minOutputAmount: '0', + }); + }); + dustOutputs.push({ + token: getTokenAddress(buildQuote.outputToken), + minOutputAmount: '0', + }); + + // @dev uniqBy: first occurrence of each element is kept. + const outputs = uniqBy(requiredOutputs.concat(dustOutputs), output => output.token); + + // Perform TX + const zapRequest: UserlessZapRequest = { + order: { + inputs, + outputs, + relay: NO_RELAY, + }, + steps, + }; + + const expectedTokens = vaultDeposit.outputs.map(output => output.token); + const walletAction = walletActions.zapExecuteOrder( + quote.option.vaultId, + zapRequest, + expectedTokens + ); + + return walletAction(dispatch, getState, extraArgument); + }; + + return { + step: 'zap-in', + message: t('Vault-TxnConfirm', { type: t('Deposit-noun') }), + action: zapAction, + pending: false, + extraInfo: { zap: true, vaultId: quote.option.vaultId }, + }; + } + + async fetchWithdrawOptions(): Promise { + const inputs = [this.vaultType.depositToken]; + + const baseOptions: BalancerWithdrawOption[] = this.possibleTokens.map(depositToken => { + const outputs = [depositToken.token]; + const selectionId = createSelectionId(this.vault.chainId, outputs); + + return { + id: createOptionId(this.id, this.vault.id, selectionId, 'direct'), + vaultId: this.vault.id, + chainId: this.vault.chainId, + selectionId, + selectionOrder: 2, + inputs, + wantedOutputs: outputs, + mode: TransactMode.Withdraw, + strategyId: 'balancer', + via: 'direct', + viaToken: depositToken, + }; + }); + + const { any: allAggregatorTokens, map: tokenToDepositTokens } = + await this.aggregatorTokenSupport(); + + const aggregatorOptions: BalancerWithdrawOption[] = allAggregatorTokens + .filter(token => tokenToDepositTokens[token.address].length > 0) + .map(token => { + const outputs = [token]; + const selectionId = createSelectionId(this.vault.chainId, outputs); + const possible = tokenToDepositTokens[token.address]; + + if (possible.length === 0) { + console.error({ vault: this.vault.id, token, possible }); + throw new Error(`No other tokens supported for ${token.symbol}`); + } + + return { + id: createOptionId(this.id, this.vault.id, selectionId, 'aggregator'), + vaultId: this.vault.id, + chainId: this.vault.chainId, + selectionId, + selectionOrder: 3, + inputs, + wantedOutputs: outputs, + mode: TransactMode.Withdraw, + strategyId: 'balancer', + via: 'aggregator', + viaTokens: possible, + }; + }); + + return baseOptions.concat(aggregatorOptions); + } + + protected async quoteRemoveLiquidityOneToken( + input: TokenAmount, + wanted: TokenEntity + ): Promise { + const pool = this.getPool(); + const amountOut = await pool.quoteRemoveLiquidityOneToken( + toWeiFromTokenAmount(input), + wanted.address + ); + return fromWeiToTokenAmount(amountOut, wanted); + } + + protected async getWithdrawLiquidityDirect( + input: TokenAmount, + wanted: TokenEntity, + withdrawVia: BalancerTokenOption + ): Promise { + if (!isTokenEqual(wanted, withdrawVia.token)) { + throw new Error( + `Curve strategy: Direct withdraw called with wanted token ${input.token.symbol} but expected ${withdrawVia.token.symbol}` + ); + } + + const split = await this.quoteRemoveLiquidityOneToken(input, wanted); + + // no further steps so output is same as split + return { + input, + split, + output: split, + via: withdrawVia, + }; + } + + protected async getWithdrawLiquidityAggregator( + state: BeefyState, + input: TokenAmount, + wanted: TokenEntity, + withdrawVias: BalancerTokenOption[] + ): Promise { + const { swapAggregator } = this.helpers; + const slippage = selectTransactSlippage(state); + + // Fetch withdraw liquidity quotes for each possible withdraw via token + const quotes = await Promise.all( + withdrawVias.map(async withdrawVia => { + const split = await this.quoteRemoveLiquidityOneToken(input, withdrawVia.token); + return { via: withdrawVia, split }; + }) + ); + + // Fetch swap quote between withdrawn token and wanted token + const withSwaps = await Promise.all( + quotes.map(async ({ via, split }) => { + const quotes = await swapAggregator.fetchQuotes( + { + vaultId: this.vault.id, + fromToken: split.token, + fromAmount: slipBy(split.amount, slippage, split.token.decimals), // we have to assume it will slip 100% since we can't modify the call data later + toToken: wanted, + }, + state + ); + const quote = first(quotes); + + return { + via, + quote, + input, + split, + output: { token: wanted, amount: quote ? quote.toAmount : BIG_ZERO }, + }; + }) + ); + + // sort by most output + withSwaps.sort((a, b) => b.output.amount.comparedTo(a.output.amount)); + + // Get the one which gives the most output + return withSwaps[0]; + } + + protected async getWithdrawLiquidity( + state: BeefyState, + input: TokenAmount, + wanted: TokenEntity, + option: BalancerWithdrawOption + ): Promise { + if (option.via === 'direct') { + return this.getWithdrawLiquidityDirect(input, wanted, option.viaToken); + } + return this.getWithdrawLiquidityAggregator(state, input, wanted, option.viaTokens); + } + + public async fetchWithdrawQuote( + inputs: InputTokenAmount[], + option: BalancerWithdrawOption + ): Promise { + const input = onlyOneInput(inputs); + if (input.amount.lte(BIG_ZERO)) { + throw new Error('Quote called with 0 input amount'); + } + + if (option.wantedOutputs.length !== 1) { + throw new Error('Can only swap to 1 output token'); + } + + const { zap, getState } = this.helpers; + + // Common: Withdraw from vault + const state = getState(); + const { withdrawnAmountAfterFeeWei, withdrawnToken, shareToken, sharesToWithdrawWei } = + getVaultWithdrawnFromState(input, this.vault, state); + const withdrawnAmountAfterFee = fromWei(withdrawnAmountAfterFeeWei, withdrawnToken.decimals); + const liquidityWithdrawn = { amount: withdrawnAmountAfterFee, token: withdrawnToken }; + const wantedToken = onlyOneToken(option.wantedOutputs); + const returned: TokenAmount[] = []; + + // Common: Token Allowances + const allowances = [ + { + token: shareToken, + amount: fromWei(sharesToWithdrawWei, shareToken.decimals), + spenderAddress: zap.manager, + }, + ]; + + // Fetch remove liquidity (and swap quote if aggregator) + const withdrawnLiquidity = await this.getWithdrawLiquidity( + state, + liquidityWithdrawn, + wantedToken, + option + ); + + // Build quote steps + const steps: ZapQuoteStep[] = [ + { + type: 'withdraw', + outputs: [ + { + token: this.vaultType.depositToken, + amount: withdrawnAmountAfterFee, + }, + ], + }, + ]; + + steps.push({ + type: 'split', + inputToken: withdrawnLiquidity.input.token, + inputAmount: withdrawnLiquidity.input.amount, + outputs: [withdrawnLiquidity.split], + }); + + if (withdrawnLiquidity.quote) { + steps.push({ + type: 'swap', + fromToken: withdrawnLiquidity.quote.fromToken, + fromAmount: withdrawnLiquidity.quote.fromAmount, + toToken: withdrawnLiquidity.quote.toToken, + toAmount: withdrawnLiquidity.quote.toAmount, + via: 'aggregator', + providerId: withdrawnLiquidity.quote.providerId, + fee: withdrawnLiquidity.quote.fee, + quote: withdrawnLiquidity.quote, + }); + + const unused = withdrawnLiquidity.split.amount.minus(withdrawnLiquidity.quote.fromAmount); + if (unused.gt(BIG_ZERO)) { + returned.push({ token: withdrawnLiquidity.split.token, amount: unused }); + } + } + + if (returned.length > 0) { + steps.push({ + type: 'unused', + outputs: returned, + }); + } + + const outputs: TokenAmount[] = [withdrawnLiquidity.output]; + + return { + id: createQuoteId(option.id), + strategyId: 'balancer', + priceImpact: calculatePriceImpact(inputs, outputs, returned, state), + option, + inputs, + outputs, + returned, + allowances, + steps, + via: option.via, + viaToken: withdrawnLiquidity.via, + fee: highestFeeOrZero(steps), + }; + } + + protected async fetchZapSplit( + quoteStep: ZapQuoteStepSplit, + inputs: TokenAmount[], + via: BalancerTokenOption, + zapHelpers: ZapHelpers, + insertBalance: boolean = false + ): Promise { + const { slippage } = zapHelpers; + const input = onlyOneTokenAmount(inputs); + const output = await this.quoteRemoveLiquidityOneToken(input, via.token); + const minOutput = slipTokenAmountBy(output, slippage); + const pool = this.getPool(); + + return { + inputs, + outputs: [output], + minOutputs: [minOutput], + returned: [], + zaps: [ + await pool.getRemoveLiquidityOneTokenZap( + toWeiFromTokenAmount(input), + output.token.address, + toWeiFromTokenAmount(minOutput), + this.helpers.zap.router, + insertBalance + ), + ], + }; + } + + public async fetchWithdrawStep( + quote: BalancerWithdrawQuote, + t: TFunction> + ): Promise { + const zapAction: BeefyThunk = async (dispatch, getState, extraArgument) => { + const state = getState(); + const chain = selectChainById(state, this.vault.chainId); + const slippage = selectTransactSlippage(state); + const zapHelpers: ZapHelpers = { + chain, + slippage, + state, + poolAddress: this.depositToken.address, + }; + const withdrawQuote = quote.steps.find(isZapQuoteStepWithdraw); + const swapQuotes = quote.steps.filter(isZapQuoteStepSwap); + const splitQuote = quote.steps.find(isZapQuoteStepSplit); + + if (!withdrawQuote || !splitQuote) { + throw new Error('Withdraw quote missing withdraw or split step'); + } + + // Step 1. Withdraw from vault + const vaultWithdraw = await this.vaultType.fetchZapWithdraw({ + inputs: quote.inputs, + }); + if (vaultWithdraw.outputs.length !== 1) { + throw new Error('Withdraw output count mismatch'); + } + + const withdrawOutput = onlyOneTokenAmount(vaultWithdraw.outputs); + if (!isTokenEqual(withdrawOutput.token, splitQuote.inputToken)) { + throw new Error('Withdraw output token mismatch'); + } + + if (withdrawOutput.amount.lt(withdrawQuote.toAmount)) { + throw new Error('Withdraw output amount mismatch'); + } + + const steps: ZapStep[] = [vaultWithdraw.zap]; + + // Step 2. Split lp + const splitZap = await this.fetchZapSplit( + splitQuote, + [withdrawOutput], + quote.viaToken, + zapHelpers, + true + ); + splitZap.zaps.forEach(step => steps.push(step)); + + // Step 3. Swaps + // 0 swaps is valid when we break only + if (swapQuotes.length > 0) { + if (swapQuotes.length > splitZap.minOutputs.length) { + throw new Error('More swap quotes than expected outputs'); + } + + const insertBalance = allTokensAreDistinct( + swapQuotes.map(quoteStep => quoteStep.fromToken) + ); + // On withdraw zap the last swap can use 100% of balance even if token was used in previous swaps (since there are no further steps) + const lastSwapIndex = swapQuotes.length - 1; + + const swapZaps = await Promise.all( + swapQuotes.map((quoteStep, i) => { + const input = splitZap.minOutputs.find(o => isTokenEqual(o.token, quoteStep.fromToken)); + if (!input) { + throw new Error('Swap input not found in split outputs'); + } + return this.fetchZapSwap(quoteStep, zapHelpers, insertBalance || lastSwapIndex === i); + }) + ); + swapZaps.forEach(swap => swap.zaps.forEach(step => steps.push(step))); + } + + // Build order + const inputs: OrderInput[] = vaultWithdraw.inputs.map(input => ({ + token: getTokenAddress(input.token), + amount: toWeiString(input.amount, input.token.decimals), + })); + + const requiredOutputs: OrderOutput[] = quote.outputs.map(output => ({ + token: getTokenAddress(output.token), + minOutputAmount: toWeiString( + slipBy(output.amount, slippage, output.token.decimals), + output.token.decimals + ), + })); + + // We need to list all inputs, and mid-route outputs, as outputs so dust gets returned + const dustOutputs: OrderOutput[] = pickTokens( + vaultWithdraw.inputs, + quote.outputs, + quote.inputs, + quote.returned, + splitQuote.outputs + ).map(token => ({ + token: getTokenAddress(token), + minOutputAmount: '0', + })); + + swapQuotes.forEach(quoteStep => { + dustOutputs.push({ + token: getTokenAddress(quoteStep.fromToken), + minOutputAmount: '0', + }); + dustOutputs.push({ + token: getTokenAddress(quoteStep.toToken), + minOutputAmount: '0', + }); + }); + + // @dev uniqBy: first occurrence of each element is kept -> required outputs are kept + const outputs = uniqBy(requiredOutputs.concat(dustOutputs), output => output.token); + + // Perform TX + const zapRequest: UserlessZapRequest = { + order: { + inputs, + outputs, + relay: NO_RELAY, + }, + steps, + }; + + const expectedTokens = quote.outputs.map(output => output.token); + const walletAction = walletActions.zapExecuteOrder( + quote.option.vaultId, + zapRequest, + expectedTokens + ); + + return walletAction(dispatch, getState, extraArgument); + }; + + return { + step: 'zap-out', + message: t('Vault-TxnConfirm', { type: t('Withdraw-noun') }), + action: zapAction, + pending: false, + extraInfo: { zap: true, vaultId: quote.option.vaultId }, + }; + } + + protected async aggregatorTokenSupport() { + const { swapAggregator, getState } = this.helpers; + const state = getState(); + const supportedAggregatorTokens = await swapAggregator.fetchTokenSupport( + this.possibleTokens.map(option => option.token), + this.vault.id, + this.vault.chainId, + state, + this.options.swap + ); + + return { + ...supportedAggregatorTokens, + map: Object.fromEntries( + supportedAggregatorTokens.any.map( + t => + [ + t.address, + this.possibleTokens.filter( + (o, i) => + // disable native as swap target, as zap can't insert balance of native in to call data + !isTokenNative(o.token) && + !isTokenEqual(o.token, t) && + supportedAggregatorTokens.tokens[i].length > 1 && + supportedAggregatorTokens.tokens[i].some(t => isTokenEqual(t, o.token)) + ), + ] as [string, BalancerTokenOption[]] + ) + ), + }; + } +} + +export const BalancerStrategy = BalancerStrategyImpl satisfies IZapStrategyStatic; diff --git a/src/features/data/apis/transact/strategies/balancer/types.ts b/src/features/data/apis/transact/strategies/balancer/types.ts new file mode 100644 index 000000000..5496d1b56 --- /dev/null +++ b/src/features/data/apis/transact/strategies/balancer/types.ts @@ -0,0 +1,115 @@ +import type { TokenEntity } from '../../../../entities/token'; +import BigNumber from 'bignumber.js'; + +export type CurveMethodTypes = + | 'fixed' + | 'fixed-deposit-int128' + | 'fixed-deposit-uint256' + | 'fixed-deposit-underlying' + | 'dynamic-deposit' + | 'pool-fixed' + | 'pool-fixed-deposit' + | 'pool-dynamic-deposit'; + +type CurveMethodSignatures = { + depositQuote: string; + deposit: string; + withdrawQuote: string; + withdraw: string; +}; + +const curveMethodTypeToSignatures = { + fixed: { + depositQuote: 'calc_token_amount:fixed_amounts', + deposit: 'add_liquidity:fixed_amounts/min_amount', + withdrawQuote: 'calc_withdraw_one_coin:amount/uint256_index', + withdraw: 'remove_liquidity_one_coin:amount/uint256_index/min_amount', + }, + 'fixed-deposit-int128': { + depositQuote: 'calc_token_amount:fixed_amounts/is_deposit', + deposit: 'add_liquidity:fixed_amounts/min_amount', + withdrawQuote: 'calc_withdraw_one_coin:amount/int128_index', + withdraw: 'remove_liquidity_one_coin:amount/int128_index/min_amount', + }, + 'fixed-deposit-uint256': { + depositQuote: 'calc_token_amount:fixed_amounts/is_deposit', + deposit: 'add_liquidity:fixed_amounts/min_amount', + withdrawQuote: 'calc_withdraw_one_coin:amount/uint256_index', + withdraw: 'remove_liquidity_one_coin:amount/uint256_index/min_amount', + }, + 'fixed-deposit-underlying': { + depositQuote: 'calc_token_amount:fixed_amounts/is_deposit', + deposit: 'add_liquidity:fixed_amounts/min_amount/use_underlying', + withdrawQuote: 'calc_withdraw_one_coin:amount/int128_index', + withdraw: 'remove_liquidity_one_coin:amount/int128_index/min_amount/use_underlying', + }, + 'dynamic-deposit': { + depositQuote: 'calc_token_amount:dynamic_amounts/is_deposit', + deposit: 'add_liquidity:dynamic_amounts/min_amount', + withdrawQuote: 'calc_withdraw_one_coin:amount/int128_index', + withdraw: 'remove_liquidity_one_coin:amount/int128_index/min_amount', + }, + 'pool-fixed': { + depositQuote: 'calc_token_amount:pool/fixed_amounts', + deposit: 'add_liquidity:pool/fixed_amounts/min_amount', + withdrawQuote: 'calc_withdraw_one_coin:pool/amount/uint256_index', + withdraw: 'remove_liquidity_one_coin:pool/amount/uint256_index/min_amount', + }, + 'pool-fixed-deposit': { + depositQuote: 'calc_token_amount:pool/fixed_amounts/is_deposit', + deposit: 'add_liquidity:pool/fixed_amounts/min_amount', + withdrawQuote: 'calc_withdraw_one_coin:pool/amount/int128_index', + withdraw: 'remove_liquidity_one_coin:pool/amount/int128_index/min_amount', + }, + 'pool-dynamic-deposit': { + depositQuote: 'calc_token_amount:pool/dynamic_amounts/is_deposit', + deposit: 'add_liquidity:pool/dynamic_amounts/min_amount', + withdrawQuote: 'calc_withdraw_one_coin:pool/amount/int128_index', + withdraw: 'remove_liquidity_one_coin:pool/amount/int128_index/min_amount', + }, +} as const satisfies Record; + +export type CurveMethodTypeToSignaturesMap = typeof curveMethodTypeToSignatures; + +type MakeCurveMethod = { + type: T; + target: string; + coins: string[]; +}; + +type CurveMethodFixed = MakeCurveMethod<'fixed'>; +type CurveMethodFixedDepositInt128 = MakeCurveMethod<'fixed-deposit-int128'>; +type CurveMethodFixedDepositUint256 = MakeCurveMethod<'fixed-deposit-uint256'>; +type CurveMethodFixedDepositUnderlying = MakeCurveMethod<'fixed-deposit-underlying'>; +type CurveMethodDynamicDeposit = MakeCurveMethod<'dynamic-deposit'>; +type CurveMethodPoolFixed = MakeCurveMethod<'pool-fixed'>; +type CurveMethodPoolFixedDeposit = MakeCurveMethod<'pool-fixed-deposit'>; +type CurveMethodPoolDynamicDeposit = MakeCurveMethod<'pool-dynamic-deposit'>; + +export type CurveMethod = + | CurveMethodFixed + | CurveMethodFixedDepositInt128 + | CurveMethodFixedDepositUint256 + | CurveMethodFixedDepositUnderlying + | CurveMethodDynamicDeposit + | CurveMethodPoolFixed + | CurveMethodPoolFixedDeposit + | CurveMethodPoolDynamicDeposit; + +export function getMethodSignaturesForType( + type: T +): CurveMethodTypeToSignaturesMap[T] { + return curveMethodTypeToSignatures[type]; +} + +export function getCurveMethodsSignatures( + method: MakeCurveMethod +): CurveMethodTypeToSignaturesMap[T] { + return curveMethodTypeToSignatures[method.type]; +} + +export type BalancerTokenOption = { + index: number; + token: TokenEntity; + price: BigNumber; +}; diff --git a/src/features/data/apis/transact/strategies/index.ts b/src/features/data/apis/transact/strategies/index.ts index 461c3a207..37d62fa46 100644 --- a/src/features/data/apis/transact/strategies/index.ts +++ b/src/features/data/apis/transact/strategies/index.ts @@ -20,6 +20,7 @@ const strategyLoadersByIdUnchecked = { (await import('./cowcentrated/CowcentratedStrategy')).CowcentratedStrategy, 'reward-pool-to-vault': async () => (await import('./RewardPoolToVaultStrategy')).RewardPoolToVaultStrategy, + balancer: async () => (await import('./balancer/BalancerStrategy')).BalancerStrategy, } as const satisfies Record Promise>; type StrategyIdToStaticPromise = typeof strategyLoadersByIdUnchecked; diff --git a/src/features/data/apis/transact/strategies/strategy-configs.ts b/src/features/data/apis/transact/strategies/strategy-configs.ts index ea69e0462..6d3d569ed 100644 --- a/src/features/data/apis/transact/strategies/strategy-configs.ts +++ b/src/features/data/apis/transact/strategies/strategy-configs.ts @@ -1,5 +1,6 @@ import type { AmmEntity, + AmmEntityBalancer, AmmEntityGamma, AmmEntitySolidly, AmmEntityUniswapV2, @@ -37,6 +38,14 @@ export type CurveStrategyConfig = { methods: CurveMethod[]; } & OptionalStrategySwapConfig; +export type BalancerStrategyConfig = { + strategyId: 'balancer'; + ammId: AmmEntityBalancer['id']; + poolId: string; + poolType: 'composable-stable'; + tokens: string[]; +} & OptionalStrategySwapConfig; + export type GammaStrategyConfig = { strategyId: 'gamma'; ammId: AmmEntityGamma['id']; @@ -74,7 +83,8 @@ export type ZapStrategyConfig = | CowcentratedStrategyConfig | GovComposerStrategyConfig | VaultComposerStrategyConfig - | RewardPoolToVaultStrategyConfig; + | RewardPoolToVaultStrategyConfig + | BalancerStrategyConfig; export type ZapStrategyId = ZapStrategyConfig['strategyId']; diff --git a/src/features/data/apis/transact/transact-types.ts b/src/features/data/apis/transact/transact-types.ts index 866c64b80..ba47ebde7 100644 --- a/src/features/data/apis/transact/transact-types.ts +++ b/src/features/data/apis/transact/transact-types.ts @@ -15,6 +15,7 @@ import type { import type { PlatformEntity } from '../../entities/platform'; import type { CurveTokenOption } from './strategies/curve/types'; import type { ZapStrategyId } from './strategies/strategy-configs'; +import type { BalancerTokenOption } from './strategies/balancer/types'; export type TokenAmount = { amount: BigNumber; @@ -177,6 +178,20 @@ export type CurveWithdrawOption = ZapBaseWithdrawOption & { | { via: 'aggregator'; viaTokens: CurveTokenOption[] } ); +export type BalancerDepositOption = ZapBaseDepositOption & { + strategyId: 'balancer'; +} & ( + | { via: 'direct'; viaToken: BalancerTokenOption } + | { via: 'aggregator'; viaTokens: BalancerTokenOption[] } + ); + +export type BalancerWithdrawOption = ZapBaseWithdrawOption & { + strategyId: 'balancer'; +} & ( + | { via: 'direct'; viaToken: BalancerTokenOption } + | { via: 'aggregator'; viaTokens: BalancerTokenOption[] } + ); + export type ConicDepositOption = ZapBaseDepositOption & { strategyId: 'conic'; }; @@ -238,7 +253,8 @@ export type DepositOption = | ConicDepositOption | GovComposerDepositOption | VaultComposerDepositOption - | RewardPoolToVaultDepositOption; + | RewardPoolToVaultDepositOption + | BalancerDepositOption; export type WithdrawOption = | StandardVaultWithdrawOption @@ -253,7 +269,8 @@ export type WithdrawOption = | ConicWithdrawOption | GovComposerWithdrawOption | VaultComposerWithdrawOption - | RewardPoolToVaultWithdrawOption; + | RewardPoolToVaultWithdrawOption + | BalancerWithdrawOption; export type TransactOption = DepositOption | WithdrawOption; @@ -461,6 +478,11 @@ export type CurveDepositQuote = BaseZapQuote & { viaToken: CurveTokenOption; }; +export type BalancerDepositQuote = BaseZapQuote & { + via: 'aggregator' | 'direct'; + viaToken: BalancerTokenOption; +}; + export type GammaDepositQuote = BaseZapQuote & { lpQuotes: (QuoteResponse | undefined)[]; }; @@ -484,7 +506,8 @@ export type ZapDepositQuote = | CowcentratedZapDepositQuote | GovComposerZapDepositQuote | VaultComposerZapDepositQuote - | RewardPoolToVaultDepositQuote; + | RewardPoolToVaultDepositQuote + | BalancerDepositQuote; export type DepositQuote = VaultDepositQuote | ZapDepositQuote; @@ -530,6 +553,11 @@ export type CurveWithdrawQuote = BaseZapQuote & { viaToken: CurveTokenOption; }; +export type BalancerWithdrawQuote = BaseZapQuote & { + via: 'aggregator' | 'direct'; + viaToken: BalancerTokenOption; +}; + export type GammaBreakWithdrawQuote = BaseZapQuote; export type GammaAggregatorWithdrawQuote = BaseZapQuote & { lpQuotes: QuoteResponse[]; @@ -570,7 +598,8 @@ export type ZapWithdrawQuote = | ConicWithdrawQuote | CowcentratedZapWithdrawQuote | GovComposerZapWithdrawQuote - | VaultComposerZapWithdrawQuote; + | VaultComposerZapWithdrawQuote + | BalancerWithdrawQuote; export type WithdrawQuote = VaultWithdrawQuote | ZapWithdrawQuote; diff --git a/src/features/data/apis/transact/vaults/CowcentratedVaultType.ts b/src/features/data/apis/transact/vaults/CowcentratedVaultType.ts index bf4a7b311..2c7a63938 100644 --- a/src/features/data/apis/transact/vaults/CowcentratedVaultType.ts +++ b/src/features/data/apis/transact/vaults/CowcentratedVaultType.ts @@ -1,5 +1,5 @@ import type { BigNumber } from 'bignumber.js'; -import { BIG_ZERO, fromWei, tokenAmountToWei } from '../../../../../helpers/big-number'; +import { BIG_ZERO, fromWei, toWeiFromTokenAmount } from '../../../../../helpers/big-number'; import type { BeefyStateFn } from '../../../../../redux-types'; import { isTokenEqual, @@ -309,9 +309,9 @@ export class CowcentratedVaultType implements ICowcentratedVaultType { minOutputs, zap: this.buildZapDepositTx( this.shareToken.address, - tokenAmountToWei(request.inputs[0]), - tokenAmountToWei(request.inputs[1]), - tokenAmountToWei(minOutputs[0]), + toWeiFromTokenAmount(request.inputs[0]), + toWeiFromTokenAmount(request.inputs[1]), + toWeiFromTokenAmount(minOutputs[0]), request.inputs[0].token.address, request.inputs[1].token.address, true @@ -355,9 +355,9 @@ export class CowcentratedVaultType implements ICowcentratedVaultType { minOutputs, zap: this.buildZapWithdrawTx( this.shareToken.address, - tokenAmountToWei(input), - tokenAmountToWei(minOutputs[0]), - tokenAmountToWei(minOutputs[1]), + toWeiFromTokenAmount(input), + toWeiFromTokenAmount(minOutputs[0]), + toWeiFromTokenAmount(minOutputs[1]), input.max ), }; diff --git a/src/features/vault/components/Actions/Transact/TransactDebugger/BalancerZap.tsx b/src/features/vault/components/Actions/Transact/TransactDebugger/BalancerZap.tsx new file mode 100644 index 000000000..92453b539 --- /dev/null +++ b/src/features/vault/components/Actions/Transact/TransactDebugger/BalancerZap.tsx @@ -0,0 +1,140 @@ +/* eslint-disable */ +import { Fragment, memo, useEffect, useState } from 'react'; +import { makeStyles } from '@material-ui/core'; +import { useAppSelector, useAppStore } from '../../../../../../store'; +import { styles } from './styles'; +import { selectVaultById } from '../../../../../data/selectors/vaults'; +import { isStandardVault, type VaultStandard } from '../../../../../data/entities/vault'; +import { + selectTokenByAddressOrUndefined, + selectTokenPriceByTokenOracleId, +} from '../../../../../data/selectors/tokens'; +import type { ISwapAggregator } from '../../../../../data/apis/transact/swap/ISwapAggregator'; +import { isTokenEqual, type TokenEntity } from '../../../../../data/entities/token'; +import { getSwapAggregator } from '../../../../../data/apis/instances'; +import { formatLargeUsd } from '../../../../../../helpers/format'; +import { BIG_ZERO } from '../../../../../../helpers/big-number'; +import { + selectIsAddressBookLoaded, + selectIsZapLoaded, +} from '../../../../../data/selectors/data-loader'; +import type { BalancerStrategyConfig } from '../../../../../data/apis/transact/strategies/strategy-configs'; +import { isDefined } from '../../../../../data/utils/array-utils'; + +const useStyles = makeStyles(styles); + +type BalancerZapProps = { + vaultId: string; +}; +const BalancerZap = memo(function BalancerZap({ vaultId }) { + const classes = useStyles(); + const vault = useAppSelector(state => selectVaultById(state, vaultId)); + const zap = isStandardVault(vault) + ? vault.zaps.find((zap): zap is BalancerStrategyConfig => zap.strategyId === 'balancer') + : undefined; + + return ( +
+

Balancer Zap

+ {zap && isStandardVault(vault) ? : } +
+ ); +}); + +const NoZap = memo(function NoZap() { + return
No balancer zap strategy configured
; +}); + +type ZapLoaderProps = { + vault: VaultStandard; + zap: BalancerStrategyConfig; +}; + +type ZapProps = { + aggregatorSupportedTokens: TokenEntity[]; + vault: VaultStandard; + zap: BalancerStrategyConfig; +}; + +const ZapLoader = memo(function ZapLoader({ vault, zap }) { + const store = useAppStore(); + const swapLoaded = useAppSelector( + state => selectIsZapLoaded(state) && selectIsAddressBookLoaded(state, vault.chainId) + ); + const tokens = useAppSelector(state => + zap.tokens + .map(address => selectTokenByAddressOrUndefined(state, vault.chainId, address)) + .filter(isDefined) + ); + const [tokensSupported, setTokensSupported] = useState(undefined); + + useEffect(() => { + if (swapLoaded) { + getSwapAggregator() + .then((aggregator: ISwapAggregator) => { + return aggregator.fetchTokenSupport( + tokens, + vault.id, + vault.chainId, + store.getState(), + zap.swap + ); + }) + .then(support => { + setTokensSupported(support.any); + }) + .catch(error => console.error(error)); + + return () => setTokensSupported(undefined); + } else { + setTokensSupported(undefined); + } + }, [setTokensSupported, vault.id, swapLoaded]); + + if (swapLoaded && tokensSupported) { + return ; + } + + return
Loading curve zap debugger...
; +}); + +const Zap = memo(function Zap({ aggregatorSupportedTokens, vault, zap }) { + const classes = useStyles(); + const tokens = useAppSelector(state => + zap.tokens.map(address => { + const token = selectTokenByAddressOrUndefined(state, vault.chainId, address); + const inAddressBook = !!token; + const inAggregator = + inAddressBook && + aggregatorSupportedTokens.some(supported => isTokenEqual(supported, token)); + const price = token ? selectTokenPriceByTokenOracleId(state, token.oracleId) : undefined; + return { address, token, inAddressBook, inAggregator, price }; + }) + ); + + return ( +
+

{zap.poolType}

+
{zap.poolId}
+
+
Address
+
AddressBook
+
Price
+
Swap
+ {tokens.map(token => ( + +
+ {token.token && token.price && token.price.gt(BIG_ZERO) ? '✔' : '❌'} {token.address} +
+
{token.token ? token.token.symbol : '❌'}
+
{token.price ? formatLargeUsd(token.price) : '❌'}
+
{token.inAggregator ? '✔' : '⚠️'}
+
+ ))} +
+
+ ); +}); + +// eslint-disable-next-line no-restricted-syntax +export default BalancerZap; diff --git a/src/features/vault/components/Actions/Transact/TransactDebugger/TransactDebugger.tsx b/src/features/vault/components/Actions/Transact/TransactDebugger/TransactDebugger.tsx index 2572a5fff..0032a3604 100644 --- a/src/features/vault/components/Actions/Transact/TransactDebugger/TransactDebugger.tsx +++ b/src/features/vault/components/Actions/Transact/TransactDebugger/TransactDebugger.tsx @@ -7,6 +7,7 @@ import { selectVaultById } from '../../../../../data/selectors/vaults'; import { selectTokenByAddressOrUndefined } from '../../../../../data/selectors/tokens'; const CurveZap = lazy(() => import(`./CurveZap`)); +const BalancerZap = lazy(() => import(`./BalancerZap`)); const useStyles = makeStyles(styles); @@ -24,6 +25,11 @@ const TransactDebugger = memo(function TransactDebugger({ return (
{depositToken && depositToken.providerId === 'curve' ? : null} + {depositToken && + depositToken.providerId && + ['balancer', 'beethovenx'].includes(depositToken.providerId) ? ( + + ) : null}
); diff --git a/src/helpers/big-number.ts b/src/helpers/big-number.ts index ec474206e..d29222d58 100644 --- a/src/helpers/big-number.ts +++ b/src/helpers/big-number.ts @@ -1,6 +1,7 @@ import { BigNumber } from 'bignumber.js'; import { mapValues } from 'lodash-es'; import type { TokenAmount } from '../features/data/apis/transact/transact-types'; +import type { TokenEntity } from '../features/data/entities/token'; export type BigNumberish = BigNumber.Value; @@ -57,7 +58,7 @@ export function toWeiFromString(value: string, decimals: number): BigNumber { return toWei(new BigNumber(value), decimals); } -export function tokenAmountToWei(tokenAmount: TokenAmount): BigNumber { +export function toWeiFromTokenAmount(tokenAmount: TokenAmount): BigNumber { return toWei(tokenAmount.amount, tokenAmount.token.decimals); } @@ -73,6 +74,13 @@ export function fromWeiString(value: string, decimals: number): BigNumber { return fromWei(new BigNumber(value), decimals); } +export function fromWeiToTokenAmount(value: BigNumber, token: TokenEntity): TokenAmount { + return { + token, + amount: fromWei(value, token.decimals), + }; +} + /** * Recursively maps over an object and replaces any BigNumber object with string value * e.g. "BN(123.567)" diff --git a/src/helpers/date.ts b/src/helpers/date.ts index df0ba1232..9c6b9e60c 100644 --- a/src/helpers/date.ts +++ b/src/helpers/date.ts @@ -151,3 +151,7 @@ export function isLessThanDurationAgo(date: Date, duration: Duration): boolean { export function isLessThanDurationAgoUnix(unixDate: number, duration: Duration): boolean { return isLessThanDurationAgo(fromUnixTime(unixDate), duration); } + +export function getUnixNow(): number { + return Math.trunc(Date.now() / 1000); +} diff --git a/src/helpers/web3.ts b/src/helpers/web3.ts index 65ddb4c1e..6830ea3c9 100644 --- a/src/helpers/web3.ts +++ b/src/helpers/web3.ts @@ -4,7 +4,7 @@ import ContractConstructor from 'web3-eth-contract'; import type { AbiItem } from 'web3-utils'; import utils from 'web3-utils'; import { formatters } from 'web3-core-helpers'; -import type { Transaction, provider } from 'web3-core'; +import type { provider, Transaction } from 'web3-core'; import BigNumber from 'bignumber.js'; import { maybeHexToNumber } from './format'; import type { Method } from 'web3-core-method'; @@ -261,14 +261,31 @@ function rateLimitProvider(provider: PrivateProvider, queue: PQueue): PrivatePro return queue .add( () => - new Promise(resolve => { - originalMethod(payload, (err, data) => { - callback(err, data); - resolve(); - }); + new Promise((resolve, reject) => { + try { + originalMethod(payload, (err: unknown, data: unknown) => { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + } catch (err) { + reject(err); + } }) ) - .catch(err => console.error('queue.add threw', method, err)); + .then( + data => { + callback(null, data); + }, + err => { + callback(err, null); + } + ) + .catch(err => { + console.error('rateLimitProvider: unexpected throw', method, err); + }); }; } } From eb28054d58ebc81f5c8cbf46fe02428611bf0a3b Mon Sep 17 00:00:00 2001 From: ReflectiveChimp <55021052+ReflectiveChimp@users.noreply.github.com> Date: Thu, 12 Sep 2024 12:42:11 +0100 Subject: [PATCH 04/20] add BalancerQueries contract addresses --- src/config/zap/amm/arbitrum.json | 3 ++- src/config/zap/amm/avax.json | 3 ++- src/config/zap/amm/base.json | 3 ++- src/config/zap/amm/ethereum.json | 3 ++- src/config/zap/amm/fantom.json | 3 ++- src/config/zap/amm/gnosis.json | 3 ++- src/config/zap/amm/optimism.json | 3 ++- src/config/zap/amm/polygon.json | 3 ++- src/features/data/apis/config-types.ts | 3 +++ 9 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/config/zap/amm/arbitrum.json b/src/config/zap/amm/arbitrum.json index 170f36284..0540b88fe 100644 --- a/src/config/zap/amm/arbitrum.json +++ b/src/config/zap/amm/arbitrum.json @@ -99,6 +99,7 @@ "id": "arbitrum-balancer", "type": "balancer", "name": "Balancer", - "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8" + "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", + "queryAddress": "0xE39B5e3B6D74016b2F6A9673D7d7493B6DF549d5" } ] diff --git a/src/config/zap/amm/avax.json b/src/config/zap/amm/avax.json index e00d5d5e4..d82b15d01 100644 --- a/src/config/zap/amm/avax.json +++ b/src/config/zap/amm/avax.json @@ -71,6 +71,7 @@ "id": "avax-balancer", "type": "balancer", "name": "Balancer", - "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8" + "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", + "queryAddress": "0xE39B5e3B6D74016b2F6A9673D7d7493B6DF549d5" } ] diff --git a/src/config/zap/amm/base.json b/src/config/zap/amm/base.json index 3517da7ee..8940f2dca 100644 --- a/src/config/zap/amm/base.json +++ b/src/config/zap/amm/base.json @@ -99,6 +99,7 @@ "id": "base-balancer", "type": "balancer", "name": "Balancer", - "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8" + "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", + "queryAddress": "0xE39B5e3B6D74016b2F6A9673D7d7493B6DF549d5" } ] diff --git a/src/config/zap/amm/ethereum.json b/src/config/zap/amm/ethereum.json index 5c54be329..5428e851d 100644 --- a/src/config/zap/amm/ethereum.json +++ b/src/config/zap/amm/ethereum.json @@ -55,6 +55,7 @@ "id": "ethereum-balancer", "type": "balancer", "name": "Balancer", - "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8" + "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", + "queryAddress": "0xE39B5e3B6D74016b2F6A9673D7d7493B6DF549d5" } ] diff --git a/src/config/zap/amm/fantom.json b/src/config/zap/amm/fantom.json index 25fdcc3f9..04f2b759a 100644 --- a/src/config/zap/amm/fantom.json +++ b/src/config/zap/amm/fantom.json @@ -135,6 +135,7 @@ "id": "fantom-beethovenx", "type": "balancer", "name": "Beethoven X", - "vaultAddress": "0x20dd72Ed959b6147912C2e529F0a0C651c33c9ce" + "vaultAddress": "0x20dd72Ed959b6147912C2e529F0a0C651c33c9ce", + "queryAddress": "0x1B0A42663DF1edeA171cD8732d288a81EFfF6d23" } ] diff --git a/src/config/zap/amm/gnosis.json b/src/config/zap/amm/gnosis.json index f9d9acc8b..b5d5d4d12 100644 --- a/src/config/zap/amm/gnosis.json +++ b/src/config/zap/amm/gnosis.json @@ -3,6 +3,7 @@ "id": "gnosis-balancer", "type": "balancer", "name": "Balancer", - "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8" + "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", + "queryAddress": "0xE39B5e3B6D74016b2F6A9673D7d7493B6DF549d5" } ] diff --git a/src/config/zap/amm/optimism.json b/src/config/zap/amm/optimism.json index e911a3d2f..9388d3883 100644 --- a/src/config/zap/amm/optimism.json +++ b/src/config/zap/amm/optimism.json @@ -33,6 +33,7 @@ "id": "optimism-beethovenx", "type": "balancer", "name": "Beethoven X", - "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8" + "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", + "queryAddress": "0xE39B5e3B6D74016b2F6A9673D7d7493B6DF549d5" } ] diff --git a/src/config/zap/amm/polygon.json b/src/config/zap/amm/polygon.json index d8876fed1..5c68d32f0 100644 --- a/src/config/zap/amm/polygon.json +++ b/src/config/zap/amm/polygon.json @@ -161,6 +161,7 @@ "id": "polygon-balancer", "type": "balancer", "name": "Balancer", - "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8" + "vaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", + "queryAddress": "0xE39B5e3B6D74016b2F6A9673D7d7493B6DF549d5" } ] diff --git a/src/features/data/apis/config-types.ts b/src/features/data/apis/config-types.ts index 4ae80b984..25e0caaee 100644 --- a/src/features/data/apis/config-types.ts +++ b/src/features/data/apis/config-types.ts @@ -254,7 +254,10 @@ export interface AmmConfigGamma extends AmmConfigBase { export interface AmmConfigBalancer extends AmmConfigBase { readonly type: 'balancer'; + /** address of Vault contract */ vaultAddress: string; + /** address of BalancerQueries contract */ + queryAddress: string; } export type AmmConfigUniswapV2Like = AmmConfigUniswapV2 | AmmConfigSolidly; From 66da4fc6d18f6800dbf8afef32fbaae6546e8b18 Mon Sep 17 00:00:00 2001 From: ReflectiveChimp <55021052+ReflectiveChimp@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:13:32 +0100 Subject: [PATCH 05/20] gyro - via aggregator only --- scripts/addBalancerZap.ts | 49 +- src/config/abi/BalancerGyroEPoolAbi.ts | 709 +++++++++++ src/config/abi/BalancerQueriesAbi.ts | 221 ++++ src/config/config.tsx | 4 +- src/config/vault/arbitrum.json | 222 +++- src/config/vault/avax.json | 2 +- src/config/vault/base.json | 4 +- src/config/vault/ethereum.json | 122 +- src/config/vault/fantom.json | 4 +- src/config/vault/gnosis.json | 44 +- src/config/vault/optimism.json | 44 +- src/config/vault/polygon.json | 22 +- .../balancer/BalancerComposableStablePool.ts | 372 ------ .../composable-stable/ComposableStablePool.ts | 185 +++ .../data/apis/amm/balancer/gyroe/GyroEPool.ts | 318 +++++ .../apis/amm/balancer/gyroe/GyroFixedPoint.ts | 65 ++ .../data/apis/amm/balancer/gyroe/types.ts | 4 + .../data/apis/amm/balancer/vault/Vault.ts | 397 +++++++ .../data/apis/amm/balancer/vault/types.ts | 212 ++++ .../balancer/weighted/WeightedPoolEncoder.ts | 89 ++ .../data/apis/amm/balancer/weighted/types.ts | 14 + .../balancer/BalancerPoolStrategy.ts | 1032 +++++++++++++++++ ...cerStrategy.ts => BalancerSwapStrategy.ts} | 140 ++- .../transact/strategies/balancer/types.ts | 109 +- .../data/apis/transact/strategies/index.ts | 5 +- .../transact/strategies/strategy-configs.ts | 15 +- .../data/apis/transact/transact-types.ts | 38 +- .../Transact/TransactDebugger/BalancerZap.tsx | 15 +- src/helpers/big-number.ts | 31 + src/helpers/tokens.ts | 14 + 30 files changed, 3836 insertions(+), 666 deletions(-) create mode 100644 src/config/abi/BalancerGyroEPoolAbi.ts create mode 100644 src/config/abi/BalancerQueriesAbi.ts delete mode 100644 src/features/data/apis/amm/balancer/BalancerComposableStablePool.ts create mode 100644 src/features/data/apis/amm/balancer/composable-stable/ComposableStablePool.ts create mode 100644 src/features/data/apis/amm/balancer/gyroe/GyroEPool.ts create mode 100644 src/features/data/apis/amm/balancer/gyroe/GyroFixedPoint.ts create mode 100644 src/features/data/apis/amm/balancer/gyroe/types.ts create mode 100644 src/features/data/apis/amm/balancer/vault/Vault.ts create mode 100644 src/features/data/apis/amm/balancer/vault/types.ts create mode 100644 src/features/data/apis/amm/balancer/weighted/WeightedPoolEncoder.ts create mode 100644 src/features/data/apis/amm/balancer/weighted/types.ts create mode 100644 src/features/data/apis/transact/strategies/balancer/BalancerPoolStrategy.ts rename src/features/data/apis/transact/strategies/balancer/{BalancerStrategy.ts => BalancerSwapStrategy.ts} (90%) diff --git a/scripts/addBalancerZap.ts b/scripts/addBalancerZap.ts index 020b65760..2ca5e6104 100644 --- a/scripts/addBalancerZap.ts +++ b/scripts/addBalancerZap.ts @@ -21,7 +21,10 @@ import { createCachedFactory } from '../src/features/data/utils/factory-utils'; import { addressBook } from 'blockchain-addressbook'; import { sortBy } from 'lodash'; import platforms from '../src/config/platforms.json'; -import { BalancerStrategyConfig } from '../src/features/data/apis/transact/strategies/strategy-configs'; +import { + BalancerSwapStrategyConfig, + BalancerPoolStrategyConfig, +} from '../src/features/data/apis/transact/strategies/strategy-configs'; import { sortVaultKeys } from './common/vault-fields'; const cacheBasePath = path.join(__dirname, '.cache', 'balancer'); @@ -97,7 +100,7 @@ const supportedProtocolVersions = new Set([2]); const supportedPoolTypes: OptionalRecord = { COMPOSABLE_STABLE: { min: 3, max: 6 }, // 'WEIGHTED': { min: 1, max: 4 }, - // 'GYROE': { min: 2, max: 2 }, + GYROE: { min: 2, max: 2 }, // 'GYRO': { min: 2, max: 2 }, // 'META_STABLE': { min: 1, max: 1 }, }; @@ -554,18 +557,40 @@ export async function discoverBalancerZap(args: RunArgs) { console.log('Vault:', amm.vaultAddress); } - const zap: BalancerStrategyConfig = { - strategyId: 'balancer', - ammId: amm.id, - poolId: apiPool.id, - poolType: apiPool.type.toLowerCase().replaceAll('_', '-') as BalancerStrategyConfig['poolType'], // TODO types - tokens: apiPool.poolTokens.map(t => t.address), - }; - - return zap; + switch (pool.type) { + case 'COMPOSABLE_STABLE': { + return { + strategyId: 'balancer-swap', + ammId: amm.id, + poolId: apiPool.id, + poolType: apiPool.type + .toLowerCase() + .replaceAll('_', '-') as BalancerSwapStrategyConfig['poolType'], // TODO types + tokens: apiPool.poolTokens.map(t => t.address), + } satisfies BalancerSwapStrategyConfig; + } + case 'GYROE': { + return { + strategyId: 'balancer-pool', + ammId: amm.id, + poolId: apiPool.id, + poolType: apiPool.type + .toLowerCase() + .replaceAll('_', '-') as BalancerPoolStrategyConfig['poolType'], // TODO types + tokens: apiPool.poolTokens.map(t => t.address), + } satisfies BalancerPoolStrategyConfig; + } + default: { + throw new Error(`Unsupported pool type ${pool.type}`); + } + } } -async function saveZap(chainId: string, vaultId: string, zap: BalancerStrategyConfig) { +async function saveZap( + chainId: string, + vaultId: string, + zap: BalancerSwapStrategyConfig | BalancerPoolStrategyConfig +) { const path = `./src/config/vault/${addressBookToAppId(chainId)}.json`; const vaults = await loadJson(path); let found = false; diff --git a/src/config/abi/BalancerGyroEPoolAbi.ts b/src/config/abi/BalancerGyroEPoolAbi.ts new file mode 100644 index 000000000..8e2891906 --- /dev/null +++ b/src/config/abi/BalancerGyroEPoolAbi.ts @@ -0,0 +1,709 @@ +import type { Abi } from 'viem'; + +export const BalancerGyroEPoolAbi = [ + { + inputs: [ + { + components: [ + { + components: [ + { internalType: 'contract IVault', name: 'vault', type: 'address' }, + { internalType: 'string', name: 'name', type: 'string' }, + { internalType: 'string', name: 'symbol', type: 'string' }, + { internalType: 'contract IERC20', name: 'token0', type: 'address' }, + { internalType: 'contract IERC20', name: 'token1', type: 'address' }, + { internalType: 'uint256', name: 'swapFeePercentage', type: 'uint256' }, + { internalType: 'uint256', name: 'pauseWindowDuration', type: 'uint256' }, + { internalType: 'uint256', name: 'bufferPeriodDuration', type: 'uint256' }, + { internalType: 'address', name: 'owner', type: 'address' }, + ], + internalType: 'struct ExtensibleWeightedPool2Tokens.NewPoolParams', + name: 'baseParams', + type: 'tuple', + }, + { + components: [ + { internalType: 'int256', name: 'alpha', type: 'int256' }, + { internalType: 'int256', name: 'beta', type: 'int256' }, + { internalType: 'int256', name: 'c', type: 'int256' }, + { internalType: 'int256', name: 's', type: 'int256' }, + { internalType: 'int256', name: 'lambda', type: 'int256' }, + ], + internalType: 'struct GyroECLPMath.Params', + name: 'eclpParams', + type: 'tuple', + }, + { + components: [ + { + components: [ + { internalType: 'int256', name: 'x', type: 'int256' }, + { internalType: 'int256', name: 'y', type: 'int256' }, + ], + internalType: 'struct GyroECLPMath.Vector2', + name: 'tauAlpha', + type: 'tuple', + }, + { + components: [ + { internalType: 'int256', name: 'x', type: 'int256' }, + { internalType: 'int256', name: 'y', type: 'int256' }, + ], + internalType: 'struct GyroECLPMath.Vector2', + name: 'tauBeta', + type: 'tuple', + }, + { internalType: 'int256', name: 'u', type: 'int256' }, + { internalType: 'int256', name: 'v', type: 'int256' }, + { internalType: 'int256', name: 'w', type: 'int256' }, + { internalType: 'int256', name: 'z', type: 'int256' }, + { internalType: 'int256', name: 'dSq', type: 'int256' }, + ], + internalType: 'struct GyroECLPMath.DerivedParams', + name: 'derivedEclpParams', + type: 'tuple', + }, + { internalType: 'address', name: 'rateProvider0', type: 'address' }, + { internalType: 'address', name: 'rateProvider1', type: 'address' }, + { internalType: 'address', name: 'capManager', type: 'address' }, + { + components: [ + { internalType: 'bool', name: 'capEnabled', type: 'bool' }, + { internalType: 'uint120', name: 'perAddressCap', type: 'uint120' }, + { internalType: 'uint128', name: 'globalCap', type: 'uint128' }, + ], + internalType: 'struct ICappedLiquidity.CapParams', + name: 'capParams', + type: 'tuple', + }, + { internalType: 'address', name: 'pauseManager', type: 'address' }, + ], + internalType: 'struct GyroECLPPool.GyroParams', + name: 'params', + type: 'tuple', + }, + { internalType: 'address', name: 'configAddress', type: 'address' }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'owner', type: 'address' }, + { indexed: true, internalType: 'address', name: 'spender', type: 'address' }, + { indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' }, + ], + name: 'Approval', + type: 'event', + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: 'address', name: 'capManager', type: 'address' }], + name: 'CapManagerUpdated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + components: [ + { internalType: 'bool', name: 'capEnabled', type: 'bool' }, + { internalType: 'uint120', name: 'perAddressCap', type: 'uint120' }, + { internalType: 'uint128', name: 'globalCap', type: 'uint128' }, + ], + indexed: false, + internalType: 'struct ICappedLiquidity.CapParams', + name: 'params', + type: 'tuple', + }, + ], + name: 'CapParamsUpdated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'bool', name: 'derivedParamsValidated', type: 'bool' }, + ], + name: 'ECLPDerivedParamsValidated', + type: 'event', + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: 'bool', name: 'paramsValidated', type: 'bool' }], + name: 'ECLPParamsValidated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'uint256', name: 'invariantAfterJoin', type: 'uint256' }, + ], + name: 'InvariantAterInitializeJoin', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'uint256', name: 'oldInvariant', type: 'uint256' }, + { indexed: false, internalType: 'uint256', name: 'newInvariant', type: 'uint256' }, + ], + name: 'InvariantOldAndNew', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'address', name: 'oldPauseManager', type: 'address' }, + { indexed: false, internalType: 'address', name: 'newPauseManager', type: 'address' }, + ], + name: 'PauseManagerChanged', + type: 'event', + }, + { anonymous: false, inputs: [], name: 'PausedLocally', type: 'event' }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: 'bool', name: 'paused', type: 'bool' }], + name: 'PausedStateChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'uint256', name: 'swapFeePercentage', type: 'uint256' }, + ], + name: 'SwapFeePercentageChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { + components: [ + { internalType: 'int256', name: 'x', type: 'int256' }, + { internalType: 'int256', name: 'y', type: 'int256' }, + ], + indexed: false, + internalType: 'struct GyroECLPMath.Vector2', + name: 'invariant', + type: 'tuple', + }, + { indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'SwapParams', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'from', type: 'address' }, + { indexed: true, internalType: 'address', name: 'to', type: 'address' }, + { indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' }, + ], + name: 'Transfer', + type: 'event', + }, + { anonymous: false, inputs: [], name: 'UnpausedLocally', type: 'event' }, + { + inputs: [], + name: 'DOMAIN_SEPARATOR', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'address', name: 'spender', type: 'address' }, + ], + name: 'allowance', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'approve', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'capManager', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'capParams', + outputs: [ + { + components: [ + { internalType: 'bool', name: 'capEnabled', type: 'bool' }, + { internalType: 'uint120', name: 'perAddressCap', type: 'uint120' }, + { internalType: 'uint128', name: 'globalCap', type: 'uint128' }, + ], + internalType: 'struct ICappedLiquidity.CapParams', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '_pauseManager', type: 'address' }], + name: 'changePauseManager', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'decimals', + outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'decreaseAllowance', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes4', name: 'selector', type: 'bytes4' }], + name: 'getActionId', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getActualSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getAuthorizer', + outputs: [{ internalType: 'contract IAuthorizer', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getECLPParams', + outputs: [ + { + components: [ + { internalType: 'int256', name: 'alpha', type: 'int256' }, + { internalType: 'int256', name: 'beta', type: 'int256' }, + { internalType: 'int256', name: 'c', type: 'int256' }, + { internalType: 'int256', name: 's', type: 'int256' }, + { internalType: 'int256', name: 'lambda', type: 'int256' }, + ], + internalType: 'struct GyroECLPMath.Params', + name: 'params', + type: 'tuple', + }, + { + components: [ + { + components: [ + { internalType: 'int256', name: 'x', type: 'int256' }, + { internalType: 'int256', name: 'y', type: 'int256' }, + ], + internalType: 'struct GyroECLPMath.Vector2', + name: 'tauAlpha', + type: 'tuple', + }, + { + components: [ + { internalType: 'int256', name: 'x', type: 'int256' }, + { internalType: 'int256', name: 'y', type: 'int256' }, + ], + internalType: 'struct GyroECLPMath.Vector2', + name: 'tauBeta', + type: 'tuple', + }, + { internalType: 'int256', name: 'u', type: 'int256' }, + { internalType: 'int256', name: 'v', type: 'int256' }, + { internalType: 'int256', name: 'w', type: 'int256' }, + { internalType: 'int256', name: 'z', type: 'int256' }, + { internalType: 'int256', name: 'dSq', type: 'int256' }, + ], + internalType: 'struct GyroECLPMath.DerivedParams', + name: 'd', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getInvariant', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getInvariantDivActualSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getLastInvariant', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getMiscData', + outputs: [ + { internalType: 'int256', name: 'logInvariant', type: 'int256' }, + { internalType: 'int256', name: 'logTotalSupply', type: 'int256' }, + { internalType: 'uint256', name: 'oracleSampleCreationTimestamp', type: 'uint256' }, + { internalType: 'uint256', name: 'oracleIndex', type: 'uint256' }, + { internalType: 'bool', name: 'oracleEnabled', type: 'bool' }, + { internalType: 'uint256', name: 'swapFeePercentage', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getNormalizedWeights', + outputs: [{ internalType: 'uint256[]', name: '', type: 'uint256[]' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getOwner', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getPausedState', + outputs: [ + { internalType: 'bool', name: 'paused', type: 'bool' }, + { internalType: 'uint256', name: 'pauseWindowEndTime', type: 'uint256' }, + { internalType: 'uint256', name: 'bufferPeriodEndTime', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getPoolId', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getPrice', + outputs: [{ internalType: 'uint256', name: 'spotPrice', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getRate', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getSwapFeePercentage', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getTokenRates', + outputs: [ + { internalType: 'uint256', name: 'rate0', type: 'uint256' }, + { internalType: 'uint256', name: 'rate1', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getVault', + outputs: [{ internalType: 'contract IVault', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'gyroConfig', + outputs: [{ internalType: 'contract IGyroConfig', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'addedValue', type: 'uint256' }, + ], + name: 'increaseAllowance', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'name', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'owner', type: 'address' }], + name: 'nonces', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'uint256', name: 'protocolSwapFeePercentage', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'onExitPool', + outputs: [ + { internalType: 'uint256[]', name: '', type: 'uint256[]' }, + { internalType: 'uint256[]', name: '', type: 'uint256[]' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'uint256', name: 'protocolSwapFeePercentage', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'onJoinPool', + outputs: [ + { internalType: 'uint256[]', name: 'amountsIn', type: 'uint256[]' }, + { internalType: 'uint256[]', name: 'dueProtocolFeeAmounts', type: 'uint256[]' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'enum IVault.SwapKind', name: 'kind', type: 'uint8' }, + { internalType: 'contract IERC20', name: 'tokenIn', type: 'address' }, + { internalType: 'contract IERC20', name: 'tokenOut', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'address', name: 'from', type: 'address' }, + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + internalType: 'struct IPoolSwapStructs.SwapRequest', + name: 'request', + type: 'tuple', + }, + { internalType: 'uint256', name: 'balanceTokenIn', type: 'uint256' }, + { internalType: 'uint256', name: 'balanceTokenOut', type: 'uint256' }, + ], + name: 'onSwap', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { inputs: [], name: 'pause', outputs: [], stateMutability: 'nonpayable', type: 'function' }, + { + inputs: [], + name: 'pauseManager', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'value', type: 'uint256' }, + { internalType: 'uint256', name: 'deadline', type: 'uint256' }, + { internalType: 'uint8', name: 'v', type: 'uint8' }, + { internalType: 'bytes32', name: 'r', type: 'bytes32' }, + { internalType: 'bytes32', name: 's', type: 'bytes32' }, + ], + name: 'permit', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'uint256', name: 'protocolSwapFeePercentage', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'queryExit', + outputs: [ + { internalType: 'uint256', name: 'bptIn', type: 'uint256' }, + { internalType: 'uint256[]', name: 'amountsOut', type: 'uint256[]' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'uint256', name: 'protocolSwapFeePercentage', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'queryJoin', + outputs: [ + { internalType: 'uint256', name: 'bptOut', type: 'uint256' }, + { internalType: 'uint256[]', name: 'amountsIn', type: 'uint256[]' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'rateProvider0', + outputs: [{ internalType: 'contract IRateProvider', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'rateProvider1', + outputs: [{ internalType: 'contract IRateProvider', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '_capManager', type: 'address' }], + name: 'setCapManager', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'bool', name: 'capEnabled', type: 'bool' }, + { internalType: 'uint120', name: 'perAddressCap', type: 'uint120' }, + { internalType: 'uint128', name: 'globalCap', type: 'uint128' }, + ], + internalType: 'struct ICappedLiquidity.CapParams', + name: 'params', + type: 'tuple', + }, + ], + name: 'setCapParams', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bool', name: 'paused', type: 'bool' }], + name: 'setPaused', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'swapFeePercentage', type: 'uint256' }], + name: 'setSwapFeePercentage', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'symbol', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'totalSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'transfer', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'transferFrom', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { inputs: [], name: 'unpause', outputs: [], stateMutability: 'nonpayable', type: 'function' }, +] as const satisfies Abi; diff --git a/src/config/abi/BalancerQueriesAbi.ts b/src/config/abi/BalancerQueriesAbi.ts new file mode 100644 index 000000000..7b9cd3c6f --- /dev/null +++ b/src/config/abi/BalancerQueriesAbi.ts @@ -0,0 +1,221 @@ +import type { Abi } from 'viem'; + +export const BalancerQueriesAbi = [ + { + inputs: [ + { + internalType: 'contract IVault', + name: '_vault', + type: 'address', + }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + inputs: [ + { + internalType: 'enum IVault.SwapKind', + name: 'kind', + type: 'uint8', + }, + { + components: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { + internalType: 'uint256', + name: 'assetInIndex', + type: 'uint256', + }, + { internalType: 'uint256', name: 'assetOutIndex', type: 'uint256' }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + internalType: 'struct IVault.BatchSwapStep[]', + name: 'swaps', + type: 'tuple[]', + }, + { + internalType: 'contract IAsset[]', + name: 'assets', + type: 'address[]', + }, + { + components: [ + { internalType: 'address', name: 'sender', type: 'address' }, + { + internalType: 'bool', + name: 'fromInternalBalance', + type: 'bool', + }, + { internalType: 'address payable', name: 'recipient', type: 'address' }, + { + internalType: 'bool', + name: 'toInternalBalance', + type: 'bool', + }, + ], + internalType: 'struct IVault.FundManagement', + name: 'funds', + type: 'tuple', + }, + ], + name: 'queryBatchSwap', + outputs: [{ internalType: 'int256[]', name: 'assetDeltas', type: 'int256[]' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + internalType: 'address', + name: 'recipient', + type: 'address', + }, + { + components: [ + { + internalType: 'contract IAsset[]', + name: 'assets', + type: 'address[]', + }, + { internalType: 'uint256[]', name: 'minAmountsOut', type: 'uint256[]' }, + { + internalType: 'bytes', + name: 'userData', + type: 'bytes', + }, + { internalType: 'bool', name: 'toInternalBalance', type: 'bool' }, + ], + internalType: 'struct IVault.ExitPoolRequest', + name: 'request', + type: 'tuple', + }, + ], + name: 'queryExit', + outputs: [ + { internalType: 'uint256', name: 'bptIn', type: 'uint256' }, + { + internalType: 'uint256[]', + name: 'amountsOut', + type: 'uint256[]', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + internalType: 'address', + name: 'recipient', + type: 'address', + }, + { + components: [ + { + internalType: 'contract IAsset[]', + name: 'assets', + type: 'address[]', + }, + { internalType: 'uint256[]', name: 'maxAmountsIn', type: 'uint256[]' }, + { + internalType: 'bytes', + name: 'userData', + type: 'bytes', + }, + { internalType: 'bool', name: 'fromInternalBalance', type: 'bool' }, + ], + internalType: 'struct IVault.JoinPoolRequest', + name: 'request', + type: 'tuple', + }, + ], + name: 'queryJoin', + outputs: [ + { internalType: 'uint256', name: 'bptOut', type: 'uint256' }, + { + internalType: 'uint256[]', + name: 'amountsIn', + type: 'uint256[]', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'bytes32', + name: 'poolId', + type: 'bytes32', + }, + { internalType: 'enum IVault.SwapKind', name: 'kind', type: 'uint8' }, + { + internalType: 'contract IAsset', + name: 'assetIn', + type: 'address', + }, + { internalType: 'contract IAsset', name: 'assetOut', type: 'address' }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + internalType: 'struct IVault.SingleSwap', + name: 'singleSwap', + type: 'tuple', + }, + { + components: [ + { internalType: 'address', name: 'sender', type: 'address' }, + { + internalType: 'bool', + name: 'fromInternalBalance', + type: 'bool', + }, + { internalType: 'address payable', name: 'recipient', type: 'address' }, + { + internalType: 'bool', + name: 'toInternalBalance', + type: 'bool', + }, + ], + internalType: 'struct IVault.FundManagement', + name: 'funds', + type: 'tuple', + }, + ], + name: 'querySwap', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'vault', + outputs: [{ internalType: 'contract IVault', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, +] as const satisfies Abi; diff --git a/src/config/config.tsx b/src/config/config.tsx index 49db7d922..a9114bba1 100644 --- a/src/config/config.tsx +++ b/src/config/config.tsx @@ -336,7 +336,7 @@ export const config = { arbitrum: { name: 'Arbitrum', chainId: 42161, - rpc: ['https://arb1.arbitrum.io/rpc'], + rpc: ['https://rpc.ankr.com/arbitrum'], explorerUrl: 'https://arbiscan.io', multicallAddress: '0x13aD51a6664973EbD0749a7c84939d973F247921', multicall3Address: '0xcA11bde05977b3631167028862bE2a173976CA11', @@ -350,7 +350,7 @@ export const config = { symbol: 'ETH', decimals: 18, }, - rpcUrls: ['https://arb1.arbitrum.io/rpc'], + rpcUrls: ['https://rpc.ankr.com/arbitrum'], blockExplorerUrls: ['https://arbiscan.io/'], }, gas: { diff --git a/src/config/vault/arbitrum.json b/src/config/vault/arbitrum.json index 255fd585e..f5fc34737 100644 --- a/src/config/vault/arbitrum.json +++ b/src/config/vault/arbitrum.json @@ -7,9 +7,9 @@ "tokenAddress": "0x69D9BC07A19CAAD9aE4ca40Af18d5A688839a299", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xe0C90990B4101166fA80247939e4907d96Fc6B6E", "earnedToken": "mooAuraAaveUSDC/gUSDC", "earnedTokenAddress": "0xe0C90990B4101166fA80247939e4907d96Fc6B6E", - "earnContractAddress": "0xe0C90990B4101166fA80247939e4907d96Fc6B6E", "oracle": "lps", "oracleId": "aura-arb-ausdc-gusdc", "status": "active", @@ -27,7 +27,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x69d9bc07a19caad9ae4ca40af18d5a688839a29900020000000000000000058e/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x69d9bc07a19caad9ae4ca40af18d5a688839a29900020000000000000000058e/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "arbitrum-balancer", + "poolId": "0x69d9bc07a19caad9ae4ca40af18d5a688839a29900020000000000000000058e", + "poolType": "gyroe", + "tokens": [ + "0x7CFaDFD5645B50bE87d546f42699d863648251ad", + "0xd3443ee1e91aF28e5FB858Fbd0D72A63bA8046E0" + ] + } + ] }, { "id": "aura-arb-susde-gyd", @@ -37,9 +49,9 @@ "tokenAddress": "0xdeEaF8B0A8Cf26217261b813e085418C7dD8F1eE", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x6A2D89926A632b400DdA8D677057A3788a74309B", "earnedToken": "mooAurasUSDe/GYD", "earnedTokenAddress": "0x6A2D89926A632b400DdA8D677057A3788a74309B", - "earnContractAddress": "0x6A2D89926A632b400DdA8D677057A3788a74309B", "oracle": "lps", "oracleId": "aura-arb-susde-gyd", "status": "active", @@ -57,7 +69,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xdeeaf8b0a8cf26217261b813e085418c7dd8f1ee00020000000000000000058f/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xdeeaf8b0a8cf26217261b813e085418c7dd8f1ee00020000000000000000058f/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "arbitrum-balancer", + "poolId": "0xdeeaf8b0a8cf26217261b813e085418c7dd8f1ee00020000000000000000058f", + "poolType": "gyroe", + "tokens": [ + "0x211Cc4DD073734dA055fbF44a2b4667d5E5fE5d2", + "0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8" + ] + } + ] }, { "id": "aura-arb-agho-gyd", @@ -67,9 +91,9 @@ "tokenAddress": "0xff38cC0cE0DE4476C5a3e78675b48420A851035B", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x1Dff0552503aB6Fd3Cb8C6DE378F2FC191ABd002", "earnedToken": "mooAuraAaveGHO/GYD", "earnedTokenAddress": "0x1Dff0552503aB6Fd3Cb8C6DE378F2FC191ABd002", - "earnContractAddress": "0x1Dff0552503aB6Fd3Cb8C6DE378F2FC191ABd002", "oracle": "lps", "oracleId": "aura-arb-agho-gyd", "status": "active", @@ -87,7 +111,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xff38cc0ce0de4476c5a3e78675b48420a851035b000200000000000000000593/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xff38cc0ce0de4476c5a3e78675b48420a851035b000200000000000000000593/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "arbitrum-balancer", + "poolId": "0xff38cc0ce0de4476c5a3e78675b48420a851035b000200000000000000000593", + "poolType": "gyroe", + "tokens": [ + "0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8", + "0xD9FBA68D89178e3538e708939332c79efC540179" + ] + } + ] }, { "id": "aura-arb-ausdc-wusdm", @@ -97,9 +133,9 @@ "tokenAddress": "0x71c64ac8Ec1DA03f8A05C3cfEB6493E6DAd54a6F", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xA74362f3A55a2793A45a28b5397d90584d653fE9", "earnedToken": "mooAuraAaveUSDC/wUSDM", "earnedTokenAddress": "0xA74362f3A55a2793A45a28b5397d90584d653fE9", - "earnContractAddress": "0xA74362f3A55a2793A45a28b5397d90584d653fE9", "oracle": "lps", "oracleId": "aura-arb-ausdc-wusdm", "status": "active", @@ -117,7 +153,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x71c64ac8ec1da03f8a05c3cfeb6493e6dad54a6f000200000000000000000592/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x71c64ac8ec1da03f8a05c3cfeb6493e6dad54a6f000200000000000000000592/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "arbitrum-balancer", + "poolId": "0x71c64ac8ec1da03f8a05c3cfeb6493e6dad54a6f000200000000000000000592", + "poolType": "gyroe", + "tokens": [ + "0x57F5E098CaD7A3D1Eed53991D4d66C45C9AF7812", + "0x7CFaDFD5645B50bE87d546f42699d863648251ad" + ] + } + ] }, { "id": "camelot-usdc-gusdc-rp", @@ -1565,9 +1613,9 @@ "tokenAddress": "0x46472CBA35E6800012aA9fcC7939Ff07478C473E", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x3490132c97cC60EDB61AFBe5DBc136Ef826F2b48", "earnedToken": "mooAuraaUSDC/GHO", "earnedTokenAddress": "0x3490132c97cC60EDB61AFBe5DBc136Ef826F2b48", - "earnContractAddress": "0x3490132c97cC60EDB61AFBe5DBc136Ef826F2b48", "oracle": "lps", "oracleId": "aura-arb-ausdc-gho", "status": "active", @@ -1585,7 +1633,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x46472cba35e6800012aa9fcc7939ff07478c473e00020000000000000000056c/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x46472cba35e6800012aa9fcc7939ff07478c473e00020000000000000000056c/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "arbitrum-balancer", + "poolId": "0x46472cba35e6800012aa9fcc7939ff07478c473e00020000000000000000056c", + "poolType": "gyroe", + "tokens": [ + "0x7CFaDFD5645B50bE87d546f42699d863648251ad", + "0x7dfF72693f6A4149b17e7C6314655f6A9F7c8B33" + ] + } + ] }, { "id": "aura-arb-ausdt-gyd", @@ -1595,9 +1655,9 @@ "tokenAddress": "0x7272163A931DaC5BBe1CB5feFaF959BB65F7346F", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x5188990f4E84960a2b147Bd596602d14331F9714", "earnedToken": "mooAuraaUSDT/GYD", "earnedTokenAddress": "0x5188990f4E84960a2b147Bd596602d14331F9714", - "earnContractAddress": "0x5188990f4E84960a2b147Bd596602d14331F9714", "oracle": "lps", "oracleId": "aura-arb-ausdt-gyd", "status": "active", @@ -1615,7 +1675,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x7272163a931dac5bbe1cb5fefaf959bb65f7346f000200000000000000000549/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x7272163a931dac5bbe1cb5fefaf959bb65f7346f000200000000000000000549/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "arbitrum-balancer", + "poolId": "0x7272163a931dac5bbe1cb5fefaf959bb65f7346f000200000000000000000549", + "poolType": "gyroe", + "tokens": [ + "0xb165a74407fE1e519d6bCbDeC1Ed3202B35a4140", + "0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8" + ] + } + ] }, { "id": "aura-arb-ausdc-gyd", @@ -1625,9 +1697,9 @@ "tokenAddress": "0x6e822c64c00393b2078f2a5BB75c575aB505B55c", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x4d78fd94934FE41C0658C510a8ffAaA0A90c293E", "earnedToken": "mooAuraaUSDC/GYD", "earnedTokenAddress": "0x4d78fd94934FE41C0658C510a8ffAaA0A90c293E", - "earnContractAddress": "0x4d78fd94934FE41C0658C510a8ffAaA0A90c293E", "oracle": "lps", "oracleId": "aura-arb-ausdc-gyd", "status": "active", @@ -1645,7 +1717,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x6e822c64c00393b2078f2a5bb75c575ab505b55c000200000000000000000548/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x6e822c64c00393b2078f2a5bb75c575ab505b55c000200000000000000000548/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "arbitrum-balancer", + "poolId": "0x6e822c64c00393b2078f2a5bb75c575ab505b55c000200000000000000000548", + "poolType": "gyroe", + "tokens": [ + "0x7CFaDFD5645B50bE87d546f42699d863648251ad", + "0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8" + ] + } + ] }, { "id": "pendle-arb-usde-28nov24", @@ -1680,9 +1764,9 @@ "tokenAddress": "0x315dd595e82bDc0c194f3A38A08fDE480D7E5d21", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x9C63A2C4D60249D98f7b8814d5AC8157ef148C42", "earnedToken": "mooAurawUSDM-GYD", "earnedTokenAddress": "0x9C63A2C4D60249D98f7b8814d5AC8157ef148C42", - "earnContractAddress": "0x9C63A2C4D60249D98f7b8814d5AC8157ef148C42", "oracle": "lps", "oracleId": "aura-arb-wusdm-gyd", "status": "active", @@ -1700,7 +1784,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x315dd595e82bdc0c194f3a38a08fde480d7e5d2100020000000000000000056a/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x315dd595e82bdc0c194f3a38a08fde480d7e5d2100020000000000000000056a/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "arbitrum-balancer", + "poolId": "0x315dd595e82bdc0c194f3a38a08fde480d7e5d2100020000000000000000056a", + "poolType": "gyroe", + "tokens": [ + "0x57F5E098CaD7A3D1Eed53991D4d66C45C9AF7812", + "0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8" + ] + } + ] }, { "id": "ramses-cl-rseth-ethx-vault", @@ -3567,7 +3663,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "arbitrum-balancer", "poolId": "0x59743f1812bb85db83e9e4ee061d124aaa64290000000000000000000000052b", "poolType": "composable-stable", @@ -3610,7 +3706,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "arbitrum-balancer", "poolId": "0xdfa752ca3ff49d4b6dbe08e2d5a111f51773d3950000000000000000000004e8", "poolType": "composable-stable", @@ -5719,7 +5815,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "arbitrum-balancer", "poolId": "0xe8a6026365254f779b6927f00f8724ea1b8ae5e0000000000000000000000580", "poolType": "composable-stable", @@ -5850,9 +5946,9 @@ "tokenAddress": "0x7967FA58B9501600D96bD843173b9334983EE6E6", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xA9Fa0C33cE278952309229FbE202A5E6108491B0", "earnedToken": "mooBalancerGyroArbwstETH-WETH", "earnedTokenAddress": "0xA9Fa0C33cE278952309229FbE202A5E6108491B0", - "earnContractAddress": "0xA9Fa0C33cE278952309229FbE202A5E6108491B0", "oracle": "lps", "oracleId": "balancer-gyro-arb-wsteth-weth", "status": "active", @@ -5864,7 +5960,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://app.gyro.finance/pools/arbitrum/e-clp/0x7967FA58B9501600D96bD843173b9334983EE6E6/join/", "removeLiquidityUrl": "https://app.gyro.finance/pools/arbitrum/e-clp/0x7967FA58B9501600D96bD843173b9334983EE6E6/exit/", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "arbitrum-balancer", + "poolId": "0x7967fa58b9501600d96bd843173b9334983ee6e600020000000000000000056e", + "poolType": "gyroe", + "tokens": [ + "0x5979D7b546E38E414F7E9822514be443A4800529", + "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" + ] + } + ] }, { "id": "pancake-cow-arb-weth-usdc-rp", @@ -8281,9 +8389,9 @@ "tokenAddress": "0xCDCef9765D369954a4A936064535710f7235110A", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x62C626af34e625244F64A3e3f19116052D8daa4D", "earnedToken": "mooAuraGyroArbweETH-wstETH", "earnedTokenAddress": "0x62C626af34e625244F64A3e3f19116052D8daa4D", - "earnContractAddress": "0x62C626af34e625244F64A3e3f19116052D8daa4D", "oracle": "lps", "oracleId": "aura-arb-gyro-weeth-wsteth", "status": "active", @@ -8301,7 +8409,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://app.gyro.finance/pools/arbitrum/e-clp/0xCDCef9765D369954a4A936064535710f7235110A/join/", "removeLiquidityUrl": "https://app.gyro.finance/pools/arbitrum/e-clp/0xCDCef9765D369954a4A936064535710f7235110A/exit/", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "arbitrum-balancer", + "poolId": "0xcdcef9765d369954a4a936064535710f7235110a000200000000000000000558", + "poolType": "gyroe", + "tokens": [ + "0x35751007a407ca6FEFfE80b3cB397736D2cf4dbe", + "0x5979D7b546E38E414F7E9822514be443A4800529" + ] + } + ] }, { "id": "aura-arb-gyro-weth-woeth", @@ -8311,9 +8431,9 @@ "tokenAddress": "0xef0c116A2818A5b1A5D836A291856A321f43C2Fb", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xDFCeE5d9317Bd312159A12Aaaf241F1faD3e1A1c", "earnedToken": "mooAuraGyroArbWETH-WOETH", "earnedTokenAddress": "0xDFCeE5d9317Bd312159A12Aaaf241F1faD3e1A1c", - "earnContractAddress": "0xDFCeE5d9317Bd312159A12Aaaf241F1faD3e1A1c", "oracle": "lps", "oracleId": "aura-arb-gyro-weth-woeth", "status": "active", @@ -8331,7 +8451,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://app.gyro.finance/pools/arbitrum/e-clp/0xef0c116A2818A5b1A5D836A291856A321f43C2Fb/join/", "removeLiquidityUrl": "https://app.gyro.finance/pools/arbitrum/e-clp/0xef0c116A2818A5b1A5D836A291856A321f43C2Fb/exit/", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "arbitrum-balancer", + "poolId": "0xef0c116a2818a5b1a5d836a291856a321f43c2fb00020000000000000000053a", + "poolType": "gyroe", + "tokens": [ + "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + "0xD8724322f44E5c58D7A815F542036fb17DbbF839" + ] + } + ] }, { "id": "aura-arb-rseth-weth", @@ -8365,7 +8497,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "arbitrum-balancer", "poolId": "0x90e6cb5249f5e1572afbf8a96d8a1ca6acffd73900000000000000000000055c", "poolType": "composable-stable", @@ -8408,7 +8540,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "arbitrum-balancer", "poolId": "0x7b54c44fbe6db6d97fd22b8756f89c0af16202cc00000000000000000000053c", "poolType": "composable-stable", @@ -11548,9 +11680,9 @@ "tokenAddress": "0x125bC5a031B2Db6733bfa35d914ffa428095978B", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xb39a6c02BB32199c5C2e937a9c96Fe8F79E18763", "earnedToken": "mooAuraGyroaUSDCn/aUSDTn", "earnedTokenAddress": "0xb39a6c02BB32199c5C2e937a9c96Fe8F79E18763", - "earnContractAddress": "0xb39a6c02BB32199c5C2e937a9c96Fe8F79E18763", "oracle": "lps", "oracleId": "aura-arb-gyro-ausdc-ausdt", "status": "active", @@ -11568,7 +11700,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://app.gyro.finance/pools/arbitrum/e-clp/0x125bc5a031b2db6733bfa35d914ffa428095978b/join/", "removeLiquidityUrl": "https://app.gyro.finance/pools/arbitrum/e-clp/0x125bc5a031b2db6733bfa35d914ffa428095978b/exit/", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "arbitrum-balancer", + "poolId": "0x125bc5a031b2db6733bfa35d914ffa428095978b000200000000000000000514", + "poolType": "gyroe", + "tokens": [ + "0x7CFaDFD5645B50bE87d546f42699d863648251ad", + "0xb165a74407fE1e519d6bCbDeC1Ed3202B35a4140" + ] + } + ] }, { "id": "uniswap-cow-arb-tbtc-weth", @@ -11701,7 +11845,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "arbitrum-balancer", "poolId": "0xb61371ab661b1acec81c699854d2f911070c059e000000000000000000000516", "poolType": "composable-stable", @@ -12598,7 +12742,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "arbitrum-balancer", "poolId": "0x2d6ced12420a9af5a83765a8c48be2afcd1a8feb000000000000000000000500", "poolType": "composable-stable", @@ -12642,7 +12786,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "arbitrum-balancer", "poolId": "0xc2598280bfea1fe18dfcabd21c7165c40c6859d30000000000000000000004f3", "poolType": "composable-stable", @@ -12748,7 +12892,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "arbitrum-balancer", "poolId": "0xd0ec47c54ca5e20aaae4616c25c825c7f48d40690000000000000000000004ef", "poolType": "composable-stable", @@ -13363,7 +13507,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "arbitrum-balancer", "poolId": "0x2ce4457acac29da4736ae6f5cd9f583a6b335c270000000000000000000004dc", "poolType": "composable-stable", @@ -14734,7 +14878,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "arbitrum-balancer", "poolId": "0x423a1323c871abc9d89eb06855bf5347048fc4a5000000000000000000000496", "poolType": "composable-stable", @@ -14812,7 +14956,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "arbitrum-balancer", "poolId": "0x9791d590788598535278552eecd4b211bfc790cb000000000000000000000498", "poolType": "composable-stable", @@ -14887,7 +15031,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "arbitrum-balancer", "poolId": "0x3fd4954a851ead144c2ff72b1f5a38ea5976bd54000000000000000000000480", "poolType": "composable-stable", @@ -15086,7 +15230,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "arbitrum-balancer", "poolId": "0x8bc65eed474d1a00555825c91feab6a8255c2107000000000000000000000453", "poolType": "composable-stable", @@ -15225,7 +15369,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "arbitrum-balancer", "poolId": "0x542f16da0efb162d20bf4358efa095b70a100f9e000000000000000000000436", "poolType": "composable-stable", diff --git a/src/config/vault/avax.json b/src/config/vault/avax.json index e6831d19c..91face298 100644 --- a/src/config/vault/avax.json +++ b/src/config/vault/avax.json @@ -89,7 +89,7 @@ "network": "avax", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "avax-balancer", "poolId": "0xfd2620c9cfcec7d152467633b3b0ca338d3d78cc00000000000000000000001c", "poolType": "composable-stable", diff --git a/src/config/vault/base.json b/src/config/vault/base.json index 0020498a9..9cd4c4ae6 100644 --- a/src/config/vault/base.json +++ b/src/config/vault/base.json @@ -3761,7 +3761,7 @@ "network": "base", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "base-balancer", "poolId": "0xab99a3e856deb448ed99713dfce62f937e2d4d74000000000000000000000118", "poolType": "composable-stable", @@ -7168,7 +7168,7 @@ "network": "base", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "base-balancer", "poolId": "0xc771c1a5905420daec317b154eb13e4198ba97d0000000000000000000000023", "poolType": "composable-stable", diff --git a/src/config/vault/ethereum.json b/src/config/vault/ethereum.json index 319e5747e..fc65fa48a 100644 --- a/src/config/vault/ethereum.json +++ b/src/config/vault/ethereum.json @@ -48,7 +48,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "ethereum-balancer", "poolId": "0x264062ca46a1322c2e6464471764089e01f22f1900000000000000000000066b", "poolType": "composable-stable", @@ -84,7 +84,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "ethereum-balancer", "poolId": "0x88794c65550deb6b4087b7552ecf295113794410000000000000000000000648", "poolType": "composable-stable", @@ -121,7 +121,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "ethereum-balancer", "poolId": "0x58aadfb1afac0ad7fca1148f3cde6aedf5236b6d00000000000000000000067f", "poolType": "composable-stable", @@ -157,7 +157,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "ethereum-balancer", "poolId": "0xb819feef8f0fcdc268afe14162983a69f6bf179e000000000000000000000689", "poolType": "composable-stable", @@ -193,7 +193,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "ethereum-balancer", "poolId": "0xdb1f2e1655477d08fb0992f82eede0053b8cd3820000000000000000000006ae", "poolType": "composable-stable", @@ -229,7 +229,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "ethereum-balancer", "poolId": "0xed0df9cd16d806e8a523805e53cf0c56e6db4d1d000000000000000000000687", "poolType": "composable-stable", @@ -299,9 +299,9 @@ "tokenAddress": "0xC2AA60465BfFa1A88f5bA471a59cA0435c3ec5c1", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x4D75a9342113c106F48117d81e2952A5828d1B5F", "earnedToken": "mooAuraGyroGYD-USDC", "earnedTokenAddress": "0x4D75a9342113c106F48117d81e2952A5828d1B5F", - "earnContractAddress": "0x4D75a9342113c106F48117d81e2952A5828d1B5F", "oracle": "lps", "oracleId": "aura-gyro-gyd-usdc", "status": "active", @@ -312,7 +312,19 @@ "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0xc2aa60465bffa1a88f5ba471a59ca0435c3ec5c100020000000000000000062c/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0xc2aa60465bffa1a88f5ba471a59ca0435c3ec5c100020000000000000000062c/remove-liquidity", - "network": "ethereum" + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "ethereum-balancer", + "poolId": "0xc2aa60465bffa1a88f5ba471a59ca0435c3ec5c100020000000000000000062c", + "poolType": "gyroe", + "tokens": [ + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "0xe07F9D810a48ab5c3c914BA3cA53AF14E4491e8A" + ] + } + ] }, { "id": "aura-gyro-gyd-sdai", @@ -322,9 +334,9 @@ "tokenAddress": "0x1CCE5169bDe03f3d5aD0206f6BD057953539DAE6", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x858B326659D543CdF80199b95E9338fDc8C7B953", "earnedToken": "mooAuraGyroGYD-sDAI", "earnedTokenAddress": "0x858B326659D543CdF80199b95E9338fDc8C7B953", - "earnContractAddress": "0x858B326659D543CdF80199b95E9338fDc8C7B953", "oracle": "lps", "oracleId": "aura-gyro-gyd-sdai", "status": "active", @@ -335,7 +347,19 @@ "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x1cce5169bde03f3d5ad0206f6bd057953539dae600020000000000000000062b/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x1cce5169bde03f3d5ad0206f6bd057953539dae600020000000000000000062b/remove-liquidity", - "network": "ethereum" + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "ethereum-balancer", + "poolId": "0x1cce5169bde03f3d5ad0206f6bd057953539dae600020000000000000000062b", + "poolType": "gyroe", + "tokens": [ + "0x83F20F44975D03b1b09e64809B757c47f942BEeA", + "0xe07F9D810a48ab5c3c914BA3cA53AF14E4491e8A" + ] + } + ] }, { "id": "aura-gyro-gyd-gho", @@ -345,9 +369,9 @@ "tokenAddress": "0xaA7a70070E7495fe86c67225329DbD39BAa2F63b", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x5ad11ef89Cbe3D27F8876F06CDC26c50b633e561", "earnedToken": "mooAuraGyroGHO-GYD", "earnedTokenAddress": "0x5ad11ef89Cbe3D27F8876F06CDC26c50b633e561", - "earnContractAddress": "0x5ad11ef89Cbe3D27F8876F06CDC26c50b633e561", "oracle": "lps", "oracleId": "aura-gyro-gyd-gho", "status": "active", @@ -358,7 +382,19 @@ "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0xaa7a70070e7495fe86c67225329dbd39baa2f63b000200000000000000000663/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0xaa7a70070e7495fe86c67225329dbd39baa2f63b000200000000000000000663/remove-liquidity", - "network": "ethereum" + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "ethereum-balancer", + "poolId": "0xaa7a70070e7495fe86c67225329dbd39baa2f63b000200000000000000000663", + "poolType": "gyroe", + "tokens": [ + "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f", + "0xe07F9D810a48ab5c3c914BA3cA53AF14E4491e8A" + ] + } + ] }, { "id": "aura-gyro-gho-usdc", @@ -368,9 +404,9 @@ "tokenAddress": "0x3932b187f440cE7703653b3908EDc5bB7676C283", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xFbdA1e15e4619001bA633f6b191646cE1D4D825C", "earnedToken": "mooAuraGyroGHO-USDC", "earnedTokenAddress": "0xFbdA1e15e4619001bA633f6b191646cE1D4D825C", - "earnContractAddress": "0xFbdA1e15e4619001bA633f6b191646cE1D4D825C", "oracle": "lps", "oracleId": "aura-gyro-gho-usdc", "status": "active", @@ -381,7 +417,19 @@ "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x3932b187f440ce7703653b3908edc5bb7676c283000200000000000000000664/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x3932b187f440ce7703653b3908edc5bb7676c283000200000000000000000664/remove-liquidity", - "network": "ethereum" + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "ethereum-balancer", + "poolId": "0x3932b187f440ce7703653b3908edc5bb7676c283000200000000000000000664", + "poolType": "gyroe", + "tokens": [ + "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + ] + } + ] }, { "id": "pendle-weeths-26dec24", @@ -1711,7 +1759,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "ethereum-balancer", "poolId": "0x848a5564158d84b8a8fb68ab5d004fae11619a5400000000000000000000066a", "poolType": "composable-stable", @@ -1864,7 +1912,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "ethereum-balancer", "poolId": "0x05ff47afada98a98982113758878f9a8b9fdda0a000000000000000000000645", "poolType": "composable-stable", @@ -1908,7 +1956,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "ethereum-balancer", "poolId": "0x596192bb6e41802428ac943d2f1476c1af25cc0e000000000000000000000659", "poolType": "composable-stable", @@ -2141,7 +2189,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "ethereum-balancer", "poolId": "0xdfe6e7e18f6cc65fa13c8d8966013d4fda74b6ba000000000000000000000558", "poolType": "composable-stable", @@ -2177,7 +2225,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "ethereum-balancer", "poolId": "0x49cbd67651fbabce12d1df18499896ec87bef46f00000000000000000000064a", "poolType": "composable-stable", @@ -2910,7 +2958,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "ethereum-balancer", "poolId": "0x8353157092ed8be69a9df8f95af097bbf33cb2af0000000000000000000005d9", "poolType": "composable-stable", @@ -2931,9 +2979,9 @@ "tokenAddress": "0xf01b0684C98CD7aDA480BFDF6e43876422fa1Fc1", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xc2f9C3F4e4cdE519D5DeA9880C1CA6614E4b3d61", "earnedToken": "mooAuraGyrowstETH-ETH", "earnedTokenAddress": "0xc2f9C3F4e4cdE519D5DeA9880C1CA6614E4b3d61", - "earnContractAddress": "0xc2f9C3F4e4cdE519D5DeA9880C1CA6614E4b3d61", "oracle": "lps", "oracleId": "aura-gyro-wsteth-eth", "status": "active", @@ -2944,7 +2992,19 @@ "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://app.gyro.finance/pools/ethereum/e-clp/0xf01b0684C98CD7aDA480BFDF6e43876422fa1Fc1/", "removeLiquidityUrl": "https://app.gyro.finance/pools/ethereum/e-clp/0xf01b0684C98CD7aDA480BFDF6e43876422fa1Fc1/", - "network": "ethereum" + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "ethereum-balancer", + "poolId": "0xf01b0684c98cd7ada480bfdf6e43876422fa1fc10002000000000000000005de", + "poolType": "gyroe", + "tokens": [ + "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + ] + } + ] }, { "id": "aura-gyro-wsteth-cbeth", @@ -2954,9 +3014,9 @@ "tokenAddress": "0xF7A826D47c8E02835D94fb0Aa40F0cC9505cb134", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x4d7222Eee3c6b67F8e806D0f866B0D666D6182b3", "earnedToken": "mooAuraGyrowstETH-cbETH", "earnedTokenAddress": "0x4d7222Eee3c6b67F8e806D0f866B0D666D6182b3", - "earnContractAddress": "0x4d7222Eee3c6b67F8e806D0f866B0D666D6182b3", "oracle": "lps", "oracleId": "aura-gyro-wsteth-cbeth", "status": "active", @@ -2967,7 +3027,19 @@ "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://app.gyro.finance/pools/ethereum/e-clp/0xf7a826d47c8e02835d94fb0aa40f0cc9505cb134/", "removeLiquidityUrl": "https://app.gyro.finance/pools/ethereum/e-clp/0xf7a826d47c8e02835d94fb0aa40f0cc9505cb134/", - "network": "ethereum" + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "ethereum-balancer", + "poolId": "0xf7a826d47c8e02835d94fb0aa40f0cc9505cb1340002000000000000000005e0", + "poolType": "gyroe", + "tokens": [ + "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + "0xBe9895146f7AF43049ca1c1AE358B0541Ea49704" + ] + } + ] }, { "id": "aura-r-bbsdai", @@ -3092,7 +3164,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "ethereum-balancer", "poolId": "0x42ed016f826165c2e5976fe5bc3df540c5ad0af700000000000000000000058b", "poolType": "composable-stable", diff --git a/src/config/vault/fantom.json b/src/config/vault/fantom.json index 30a6bf5c3..f622ab984 100644 --- a/src/config/vault/fantom.json +++ b/src/config/vault/fantom.json @@ -189,7 +189,7 @@ "network": "fantom", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "fantom-beethovenx", "poolId": "0xaac7b8d6d7009428d6ddad895bdf50c6fcbbe2c000000000000000000000080d", "poolType": "composable-stable", @@ -361,7 +361,7 @@ "network": "fantom", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "fantom-beethovenx", "poolId": "0x593000b762de3c465855336e95c8bb46080af064000000000000000000000760", "poolType": "composable-stable", diff --git a/src/config/vault/gnosis.json b/src/config/vault/gnosis.json index ad84594a7..06a8b002b 100644 --- a/src/config/vault/gnosis.json +++ b/src/config/vault/gnosis.json @@ -7,9 +7,9 @@ "tokenAddress": "0x71E1179C5e197FA551BEEC85ca2EF8693c61b85b", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x1DE0375e770EFb9C92d7958cbb5B53c9Fb8eB1Cb", "earnedToken": "mooAuraGnosisWETH-rETH", "earnedTokenAddress": "0x1DE0375e770EFb9C92d7958cbb5B53c9Fb8eB1Cb", - "earnContractAddress": "0x1DE0375e770EFb9C92d7958cbb5B53c9Fb8eB1Cb", "oracle": "lps", "oracleId": "aura-gyro-gnosis-weth-reth", "status": "active", @@ -27,7 +27,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://app.gyro.finance/pools/gnosis/e-clp/0x71E1179C5e197FA551BEEC85ca2EF8693c61b85b/", "removeLiquidityUrl": "https://app.gyro.finance/pools/gnosis/e-clp/0x71E1179C5e197FA551BEEC85ca2EF8693c61b85b/", - "network": "gnosis" + "network": "gnosis", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "gnosis-balancer", + "poolId": "0x71e1179c5e197fa551beec85ca2ef8693c61b85b0002000000000000000000a0", + "poolType": "gyroe", + "tokens": [ + "0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1", + "0xc791240D1F2dEf5938E2031364Ff4ed887133C3d" + ] + } + ] }, { "id": "aura-gyro-gnosis-weth-wsteth", @@ -37,9 +49,9 @@ "tokenAddress": "0x8DD4df4Ce580b9644437f3375e54f1ab09808228", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x50583F8DCE18D4180f074bE7332b4734968246EC", "earnedToken": "mooAuraGnosisWETH-wstETH", "earnedTokenAddress": "0x50583F8DCE18D4180f074bE7332b4734968246EC", - "earnContractAddress": "0x50583F8DCE18D4180f074bE7332b4734968246EC", "oracle": "lps", "oracleId": "aura-gyro-gnosis-weth-wsteth", "status": "active", @@ -57,7 +69,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://app.gyro.finance/pools/gnosis/e-clp/0x8dd4df4ce580b9644437f3375e54f1ab09808228/", "removeLiquidityUrl": "https://app.gyro.finance/pools/gnosis/e-clp/0x8dd4df4ce580b9644437f3375e54f1ab09808228/", - "network": "gnosis" + "network": "gnosis", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "gnosis-balancer", + "poolId": "0x8dd4df4ce580b9644437f3375e54f1ab0980822800020000000000000000009c", + "poolType": "gyroe", + "tokens": [ + "0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1", + "0x6C76971f98945AE98dD7d4DFcA8711ebea946eA6" + ] + } + ] }, { "id": "aura-gnosis-usdc.e-usdt-sdai", @@ -90,7 +114,7 @@ "network": "gnosis", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "gnosis-balancer", "poolId": "0xfc095c811fe836ed12f247bcf042504342b73fb700000000000000000000009f", "poolType": "composable-stable", @@ -134,7 +158,7 @@ "network": "gnosis", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "gnosis-balancer", "poolId": "0x06135a9ae830476d3a941bae9010b63732a055f4000000000000000000000065", "poolType": "composable-stable", @@ -177,7 +201,7 @@ "network": "gnosis", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "gnosis-balancer", "poolId": "0xc9f00c3a713008ddf69b768d90d4978549bfdf9400000000000000000000006d", "poolType": "composable-stable", @@ -280,7 +304,7 @@ "network": "gnosis", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "gnosis-balancer", "poolId": "0x7644fa5d0ea14fcf3e813fdf93ca9544f8567655000000000000000000000066", "poolType": "composable-stable", @@ -356,7 +380,7 @@ "network": "gnosis", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "gnosis-balancer", "poolId": "0xdd439304a77f54b1f7854751ac1169b279591ef7000000000000000000000064", "poolType": "composable-stable", @@ -459,7 +483,7 @@ "network": "gnosis", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "gnosis-balancer", "poolId": "0xbad20c15a773bf03ab973302f61fabcea5101f0a000000000000000000000034", "poolType": "composable-stable", diff --git a/src/config/vault/optimism.json b/src/config/vault/optimism.json index 12e7d3bf2..d271b5d7f 100644 --- a/src/config/vault/optimism.json +++ b/src/config/vault/optimism.json @@ -855,7 +855,7 @@ "network": "optimism", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "optimism-beethovenx", "poolId": "0x2bb4712247d5f451063b5e4f6948abdfb925d93d000000000000000000000136", "poolType": "composable-stable", @@ -2081,9 +2081,9 @@ "tokenAddress": "0xE906d4C4fC4c3Fe96560De86B4bf7eD89aF9A69a", "tokenDecimals": 18, "tokenProviderId": "beethovenx", + "earnContractAddress": "0x10BD7A983E2897c36b059ce0a8C57BcC17d6b847", "earnedToken": "mooAuraGyroOPFraxSymphony", "earnedTokenAddress": "0x10BD7A983E2897c36b059ce0a8C57BcC17d6b847", - "earnContractAddress": "0x10BD7A983E2897c36b059ce0a8C57BcC17d6b847", "oracle": "lps", "oracleId": "aura-gyro-op-frax-symphony", "status": "active", @@ -2094,7 +2094,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://op.beets.fi/pool/0xe906d4c4fc4c3fe96560de86b4bf7ed89af9a69a000200000000000000000126", "removeLiquidityUrl": "https://op.beets.fi/pool/0xe906d4c4fc4c3fe96560de86b4bf7ed89af9a69a000200000000000000000126", - "network": "optimism" + "network": "optimism", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "optimism-beethovenx", + "poolId": "0xe906d4c4fc4c3fe96560de86b4bf7ed89af9a69a000200000000000000000126", + "poolType": "gyroe", + "tokens": [ + "0x2Dd1B4D4548aCCeA497050619965f91f78b3b532", + "0x2E3D870790dC77A83DD1d18184Acc7439A53f475" + ] + } + ] }, { "id": "velo-cow-op-moobifi-usdc-rp", @@ -3214,7 +3226,7 @@ "network": "optimism", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "optimism-beethovenx", "poolId": "0x73a7fe27fe9545d53924e529acf11f3073841b9e000000000000000000000133", "poolType": "composable-stable", @@ -4859,7 +4871,7 @@ "network": "optimism", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "optimism-beethovenx", "poolId": "0xa71021492a3966eec735ed1b505afa097c7cfe6f00000000000000000000010d", "poolType": "composable-stable", @@ -5310,7 +5322,7 @@ "network": "optimism", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "optimism-beethovenx", "poolId": "0x5f8893506ddc4c271837187d14a9c87964a074dc000000000000000000000106", "poolType": "composable-stable", @@ -5403,7 +5415,7 @@ "network": "optimism", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "optimism-beethovenx", "poolId": "0x004700ba0a4f5f22e1e78a277fca55e36f47e09c000000000000000000000104", "poolType": "composable-stable", @@ -5439,7 +5451,7 @@ "network": "optimism", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "optimism-beethovenx", "poolId": "0x9da11ff60bfc5af527f58fd61679c3ac98d040d9000000000000000000000100", "poolType": "composable-stable", @@ -5578,9 +5590,9 @@ "tokenAddress": "0x7Ca75bdEa9dEde97F8B13C6641B768650CB83782", "tokenDecimals": 18, "tokenProviderId": "beethovenx", + "earnContractAddress": "0x3b4DdC7888F752a96290344a1A926E73b80423c0", "earnedToken": "mooAuraOPGryowstETH-ETH", "earnedTokenAddress": "0x3b4DdC7888F752a96290344a1A926E73b80423c0", - "earnContractAddress": "0x3b4DdC7888F752a96290344a1A926E73b80423c0", "oracle": "lps", "oracleId": "beets-eclp-wsteth-eth", "status": "active", @@ -5591,7 +5603,19 @@ "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://op.beets.fi/pool/0x7ca75bdea9dede97f8b13c6641b768650cb837820002000000000000000000d5", "removeLiquidityUrl": "https://op.beets.fi/pool/0x7ca75bdea9dede97f8b13c6641b768650cb837820002000000000000000000d5", - "network": "optimism" + "network": "optimism", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "optimism-beethovenx", + "poolId": "0x7ca75bdea9dede97f8b13c6641b768650cb837820002000000000000000000d5", + "poolType": "gyroe", + "tokens": [ + "0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb", + "0x4200000000000000000000000000000000000006" + ] + } + ] }, { "id": "uniswap-gamma-weth-op-narrow", diff --git a/src/config/vault/polygon.json b/src/config/vault/polygon.json index 1918c019f..bd8349df2 100644 --- a/src/config/vault/polygon.json +++ b/src/config/vault/polygon.json @@ -685,7 +685,7 @@ "network": "polygon", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "polygon-balancer", "poolId": "0x4b7586a4f49841447150d3d92d9e9e000f766c30000000000000000000000e8a", "poolType": "composable-stable", @@ -722,7 +722,7 @@ "network": "polygon", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "polygon-balancer", "poolId": "0xcd78a20c597e367a4e478a2411ceb790604d7c8f000000000000000000000c22", "poolType": "composable-stable", @@ -2306,9 +2306,9 @@ "tokenAddress": "0xf0ad209e2e969EAAA8C882aac71f02D8a047d5c2", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x8D0663d2d738e6078Bb77589dE4231eD9fcFd014", "earnedToken": "mooAuraPolyGyrostMATIC-MATIC", "earnedTokenAddress": "0x8D0663d2d738e6078Bb77589dE4231eD9fcFd014", - "earnContractAddress": "0x8D0663d2d738e6078Bb77589dE4231eD9fcFd014", "oracle": "lps", "oracleId": "aura-polygon-gyro-matic-stmatic", "status": "active", @@ -2319,7 +2319,19 @@ "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://app.gyro.finance/pools/polygon/e-clp/0xf0ad209e2e969eaaa8c882aac71f02d8a047d5c2/", "removeLiquidityUrl": "https://app.gyro.finance/pools/polygon/e-clp/0xf0ad209e2e969eaaa8c882aac71f02d8a047d5c2/", - "network": "polygon" + "network": "polygon", + "zaps": [ + { + "strategyId": "balancer-pool", + "ammId": "polygon-balancer", + "poolId": "0xf0ad209e2e969eaaa8c882aac71f02d8a047d5c2000200000000000000000b49", + "poolType": "gyroe", + "tokens": [ + "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", + "0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4" + ] + } + ] }, { "id": "aura-polygon-gyro-matic-maticx", @@ -3815,7 +3827,7 @@ "network": "polygon", "zaps": [ { - "strategyId": "balancer", + "strategyId": "balancer-swap", "ammId": "polygon-balancer", "poolId": "0x513cdee00251f39de280d9e5f771a6eafebcc88e000000000000000000000a6b", "poolType": "composable-stable", diff --git a/src/features/data/apis/amm/balancer/BalancerComposableStablePool.ts b/src/features/data/apis/amm/balancer/BalancerComposableStablePool.ts deleted file mode 100644 index cabaaebc0..000000000 --- a/src/features/data/apis/amm/balancer/BalancerComposableStablePool.ts +++ /dev/null @@ -1,372 +0,0 @@ -import type { IBalancerPool } from './types'; -import type { ChainEntity } from '../../../entities/chain'; -import BigNumber from 'bignumber.js'; -import { getWeb3Instance } from '../../instances'; -import { viemToWeb3Abi } from '../../../../../helpers/web3'; -import { ZERO_ADDRESS } from '../../../../../helpers/addresses'; -import { BalancerVaultAbi } from '../../../../../config/abi/BalancerVaultAbi'; -import { BIG_ZERO } from '../../../../../helpers/big-number'; -import type { ZapStep } from '../../transact/zap/types'; -import { getUnixNow } from '../../../../../helpers/date'; -import abiCoder from 'web3-eth-abi'; -import type { AbiItem } from 'web3-utils'; -import { getInsertIndex } from '../../transact/helpers/zap'; - -export type BalancerComposableStablePoolConfig = { - chain: ChainEntity; - /** address */ - vaultAddress: string; - /** address */ - poolAddress: string; - /** bytes32 */ - poolId: string; - /** address[] */ - tokens: string[]; -}; - -enum SwapKind { - GIVEN_IN = 0, - GIVEN_OUT = 1, -} - -type SingleSwap = { - /** bytes32 */ - poolId: string; - /** uint8 */ - kind: SwapKind; - /** address */ - assetIn: string; - /** address */ - assetOut: string; - /** uint256 */ - amount: string; - /** bytes */ - userData: string; -}; - -type BatchSwapStep = { - /** bytes32 */ - poolId: string; - /** uint256 */ - assetInIndex: number; - /** uint256 */ - assetOutIndex: number; - /** uint256 */ - amount: string; - /** bytes */ - userData: string; -}; - -type FundManagement = { - /** address */ - sender: string; - /** bool */ - fromInternalBalance: boolean; - /** address */ - recipient: string; - /** bool */ - toInternalBalance: boolean; -}; - -type QueryBatchSwapArgs = { - /** uint8 */ - kind: SwapKind; - /** tuple[] */ - swaps: BatchSwapStep[]; - /** address[] */ - assets: string[]; - /** tuple */ - funds: FundManagement; -}; - -type SwapArgs = { - /** tuple */ - singleSwap: SingleSwap; - /** tuple */ - funds: FundManagement; - /** uint256 */ - limit: string; - /** uint256 */ - deadline: number; -}; - -type BatchSwapArgs = { - /** uint8 */ - kind: SwapKind; - /** tuple[] */ - swaps: BatchSwapStep[]; - /** address[] */ - assets: string[]; - /** tuple */ - funds: FundManagement; - /** int256[] : +ve for tokens sent to the pool, -ve for tokens received from the pool */ - limits: string[]; - /** uint256 */ - deadline: number; -}; - -const queryFunds: FundManagement = { - sender: ZERO_ADDRESS, - fromInternalBalance: false, - recipient: ZERO_ADDRESS, - toInternalBalance: false, -}; - -const THIRTY_MINUTES_IN_SECONDS = 30 * 60; - -export class BalancerComposableStablePool implements IBalancerPool { - public readonly type = 'balancer'; - public readonly poolTokenIndex: number; - - constructor(protected readonly config: BalancerComposableStablePoolConfig) { - this.poolTokenIndex = this.getTokenIndex(this.config.poolAddress); - } - - async quoteAddLiquidityOneToken(tokenIn: string, amountIn: BigNumber): Promise { - this.checkToken(tokenIn, 'tokenIn'); - this.checkAmount(amountIn, 'amountIn'); - - const args: QueryBatchSwapArgs = { - kind: SwapKind.GIVEN_IN, - swaps: [ - { - poolId: this.config.poolId, - assetInIndex: 0, - assetOutIndex: 1, - amount: amountIn.toString(10), - userData: '0x', - }, - ], - assets: [tokenIn, this.config.poolAddress], - funds: queryFunds, - }; - - const [vaultInputDelta, vaultOutputDelta] = await this.queryBatchSwap(args); - - if (!vaultInputDelta.eq(amountIn)) { - throw new Error('Not all input used'); - } - - if (vaultOutputDelta.gte(BIG_ZERO)) { - throw new Error('Output is negative'); - } - - return vaultOutputDelta.abs(); - } - - async quoteRemoveLiquidityOneToken(amountIn: BigNumber, tokenOut: string): Promise { - this.checkAmount(amountIn, 'amountIn'); - this.checkToken(tokenOut, 'tokenOut'); - - const args: QueryBatchSwapArgs = { - kind: SwapKind.GIVEN_IN, - swaps: [ - { - poolId: this.config.poolId, - assetInIndex: 0, - assetOutIndex: 1, - amount: amountIn.toString(10), - userData: '0x', - }, - ], - assets: [this.config.poolAddress, tokenOut], - funds: queryFunds, - }; - - const [vaultInputDelta, vaultOutputDelta] = await this.queryBatchSwap(args); - - if (!vaultInputDelta.eq(amountIn)) { - throw new Error('Not all input used'); - } - - if (vaultOutputDelta.gte(BIG_ZERO)) { - throw new Error('Output is negative'); - } - - return vaultOutputDelta.abs(); - } - - async getAddLiquidityOneTokenZap( - tokenIn: string, - amountIn: BigNumber, - amountOutMin: BigNumber, - from: string, - insertBalance: boolean, - deadlineSeconds: number = THIRTY_MINUTES_IN_SECONDS - ): Promise { - return this.getSwapZap( - tokenIn, - amountIn, - this.config.poolAddress, - amountOutMin, - from, - insertBalance, - deadlineSeconds - ); - } - - async getRemoveLiquidityOneTokenZap( - amountIn: BigNumber, - tokenOut: string, - amountOutMin: BigNumber, - from: string, - insertBalance: boolean, - deadlineSeconds: number = THIRTY_MINUTES_IN_SECONDS - ): Promise { - return this.getSwapZap( - this.config.poolAddress, - amountIn, - tokenOut, - amountOutMin, - from, - insertBalance, - deadlineSeconds - ); - } - - protected async getSwapZap( - tokenIn: string, - amountIn: BigNumber, - tokenOut: string, - amountOutMin: BigNumber, - from: string, - insertBalance: boolean, - deadlineSeconds: number = THIRTY_MINUTES_IN_SECONDS - ): Promise { - this.checkToken(tokenIn, 'tokenIn'); - this.checkAmount(amountIn, 'amountIn'); - this.checkToken(tokenOut, 'tokenOut'); - this.checkAmount(amountOutMin, 'amountOutMin'); - - const args: SwapArgs = { - singleSwap: { - poolId: this.config.poolId, - kind: SwapKind.GIVEN_IN, - assetIn: tokenIn, - assetOut: tokenOut, - amount: amountIn.toString(10), - userData: '0x', - }, - funds: { - sender: from, - fromInternalBalance: false, - recipient: from, - toInternalBalance: false, - }, - limit: amountOutMin.toString(10), - deadline: getUnixNow() + deadlineSeconds, - }; - - /* - the byte offset at which amountIn is inserted in the calldata - calculated using 32-byte words: - 00 : swap offset (07) - valueAt(00) + 0 : singleSwap.poolId - .. - valueAt(00) + 4 : singleSwap.amount - */ - const amountInIndex = getInsertIndex(7 + 4); - - return { - target: this.config.vaultAddress, - value: '0', - data: this.encodeSwap(args), - tokens: insertBalance ? [{ token: tokenIn, index: amountInIndex }] : [], - }; - } - - protected encodeSwap(args: SwapArgs): string { - const methodAbi = BalancerVaultAbi.find(abi => abi.type === 'function' && abi.name === 'swap'); - if (!methodAbi) { - throw new Error('Method swap not found'); - } - - return abiCoder.encodeFunctionCall(methodAbi as AbiItem, [ - [ - args.singleSwap.poolId, - args.singleSwap.kind, - args.singleSwap.assetIn, - args.singleSwap.assetOut, - args.singleSwap.amount, - args.singleSwap.userData, - ], - [ - args.funds.sender, - args.funds.fromInternalBalance, - args.funds.recipient, - args.funds.toInternalBalance, - ], - args.limit, - args.deadline, - ]); - } - - protected encodeBatchSwap(args: BatchSwapArgs): string { - const methodAbi = BalancerVaultAbi.find( - abi => abi.type === 'function' && abi.name === 'batchSwap' - ); - if (!methodAbi) { - throw new Error('Method batchSwap not found'); - } - - return abiCoder.encodeFunctionCall(methodAbi as AbiItem, [ - args.kind, - args.swaps.map(swap => [ - swap.poolId, - swap.assetInIndex, - swap.assetOutIndex, - swap.amount, - swap.userData, - ]), - args.assets, - [ - args.funds.sender, - args.funds.fromInternalBalance, - args.funds.recipient, - args.funds.toInternalBalance, - ], - args.limits, - args.deadline, - ]); - } - - protected async queryBatchSwap(args: QueryBatchSwapArgs): Promise { - const web3 = await getWeb3Instance(this.config.chain); - const vault = new web3.eth.Contract(viemToWeb3Abi(BalancerVaultAbi), this.config.vaultAddress); - - const result: Array | undefined = await vault.methods - .queryBatchSwap(args.kind, args.swaps, args.assets, args.funds) - .call(); - - if (!result || !Array.isArray(result) || result.length !== args.assets.length) { - throw new Error('Invalid result'); - } - - return result.map(value => new BigNumber(value)); - } - - protected getTokenIndex(address: string): number { - const index = this.config.tokens.findIndex(token => token === address); - if (index === -1) { - throw new Error(`Address ${address} not found in tokens`); - } - return index; - } - - protected checkAmount(amount: BigNumber, label: string = 'amount') { - if (amount.lte(BIG_ZERO)) { - throw new Error(`${label} must be greater than 0`); - } - - if ((amount.decimalPlaces() || 0) > 0) { - throw new Error(`${label} must be in wei`); - } - } - - protected checkToken(token: string, label: string = 'token') { - const index = this.config.tokens.findIndex(t => t === token); - if (index === -1) { - throw new Error(`${label} must be a pool token`); - } - } -} diff --git a/src/features/data/apis/amm/balancer/composable-stable/ComposableStablePool.ts b/src/features/data/apis/amm/balancer/composable-stable/ComposableStablePool.ts new file mode 100644 index 000000000..f54a65d2d --- /dev/null +++ b/src/features/data/apis/amm/balancer/composable-stable/ComposableStablePool.ts @@ -0,0 +1,185 @@ +import type { IBalancerPool } from '../types'; +import type BigNumber from 'bignumber.js'; +import { BIG_ZERO } from '../../../../../../helpers/big-number'; +import type { ZapStep } from '../../../transact/zap/types'; +import { createFactory } from '../../../../utils/factory-utils'; +import { Vault } from '../vault/Vault'; +import type { ChainEntity } from '../../../../entities/chain'; +import { + type PoolConfig, + type QueryBatchSwapRequest, + SwapKind, + type VaultConfig, +} from '../vault/types'; +import { getUnixNow } from '../../../../../../helpers/date'; + +const THIRTY_MINUTES_IN_SECONDS = 30 * 60; + +export class ComposableStablePool implements IBalancerPool { + public readonly type = 'balancer'; + + constructor( + protected readonly chain: ChainEntity, + protected readonly vaultConfig: VaultConfig, + protected readonly config: PoolConfig + ) { + this.checkToken(this.config.poolAddress, 'poolAddress'); + } + + async quoteAddLiquidityOneToken(tokenIn: string, amountIn: BigNumber): Promise { + this.checkToken(tokenIn, 'tokenIn'); + this.checkAmount(amountIn, 'amountIn'); + + const request: QueryBatchSwapRequest = { + kind: SwapKind.GIVEN_IN, + swaps: [ + { + poolId: this.config.poolId, + assetInIndex: 0, + assetOutIndex: 1, + amount: amountIn, + userData: '0x', + }, + ], + assets: [tokenIn, this.config.poolAddress], + }; + + const vault = this.getVault(); + const [vaultInputDelta, vaultOutputDelta] = await vault.queryBatchSwap(request); + + if (!vaultInputDelta.eq(amountIn)) { + throw new Error('Not all input used'); + } + + if (vaultOutputDelta.gte(BIG_ZERO)) { + throw new Error('Output is negative'); + } + + return vaultOutputDelta.abs(); + } + + async quoteRemoveLiquidityOneToken(amountIn: BigNumber, tokenOut: string): Promise { + this.checkAmount(amountIn, 'amountIn'); + this.checkToken(tokenOut, 'tokenOut'); + + const request: QueryBatchSwapRequest = { + kind: SwapKind.GIVEN_IN, + swaps: [ + { + poolId: this.config.poolId, + assetInIndex: 0, + assetOutIndex: 1, + amount: amountIn, + userData: '0x', + }, + ], + assets: [this.config.poolAddress, tokenOut], + }; + + const vault = this.getVault(); + const [vaultInputDelta, vaultOutputDelta] = await vault.queryBatchSwap(request); + + if (!vaultInputDelta.eq(amountIn)) { + throw new Error('Not all input used'); + } + + if (vaultOutputDelta.gte(BIG_ZERO)) { + throw new Error('Output is negative'); + } + + return vaultOutputDelta.abs(); + } + + async getAddLiquidityOneTokenZap( + tokenIn: string, + amountIn: BigNumber, + amountOutMin: BigNumber, + from: string, + insertBalance: boolean, + deadlineSeconds: number = THIRTY_MINUTES_IN_SECONDS + ): Promise { + this.checkToken(tokenIn, 'tokenIn'); + this.checkAmount(amountIn, 'amountIn'); + this.checkAmount(amountOutMin, 'amountOutMin'); + + const vault = this.getVault(); + return vault.getSwapZap({ + swap: { + singleSwap: { + poolId: this.config.poolId, + kind: SwapKind.GIVEN_IN, + assetIn: tokenIn, + assetOut: this.config.poolAddress, + amount: amountIn, + userData: '0x', + }, + funds: { + sender: from, + fromInternalBalance: false, + recipient: from, + toInternalBalance: false, + }, + limit: amountOutMin, + deadline: getUnixNow() + deadlineSeconds, + }, + insertBalance, + }); + } + + async getRemoveLiquidityOneTokenZap( + amountIn: BigNumber, + tokenOut: string, + amountOutMin: BigNumber, + from: string, + insertBalance: boolean, + deadlineSeconds: number = THIRTY_MINUTES_IN_SECONDS + ): Promise { + this.checkToken(tokenOut, 'tokenOut'); + this.checkAmount(amountIn, 'amountIn'); + this.checkAmount(amountOutMin, 'amountOutMin'); + + const vault = this.getVault(); + return vault.getSwapZap({ + swap: { + singleSwap: { + poolId: this.config.poolId, + kind: SwapKind.GIVEN_IN, + assetIn: this.config.poolAddress, + assetOut: tokenOut, + amount: amountIn, + userData: '0x', + }, + funds: { + sender: from, + fromInternalBalance: false, + recipient: from, + toInternalBalance: false, + }, + limit: amountOutMin, + deadline: getUnixNow() + deadlineSeconds, + }, + insertBalance, + }); + } + + protected getVault = createFactory(() => { + return new Vault(this.chain, this.vaultConfig); + }); + + protected checkAmount(amount: BigNumber, label: string = 'amount') { + if (amount.lte(BIG_ZERO)) { + throw new Error(`${label} must be greater than 0`); + } + + if ((amount.decimalPlaces() || 0) > 0) { + throw new Error(`${label} must be in wei`); + } + } + + protected checkToken(tokenAddress: string, label: string = 'token') { + const index = this.config.tokens.findIndex(t => t.address === tokenAddress); + if (index === -1) { + throw new Error(`${label} must be a pool token`); + } + } +} diff --git a/src/features/data/apis/amm/balancer/gyroe/GyroEPool.ts b/src/features/data/apis/amm/balancer/gyroe/GyroEPool.ts new file mode 100644 index 000000000..a553ee8ad --- /dev/null +++ b/src/features/data/apis/amm/balancer/gyroe/GyroEPool.ts @@ -0,0 +1,318 @@ +import type { IBalancerPool } from '../types'; +import type { ChainEntity } from '../../../../entities/chain'; +import type { + PoolConfig, + QueryExitPoolRequest, + QueryExitPoolResponse, + QueryJoinPoolRequest, + QueryJoinPoolResponse, + VaultConfig, +} from '../vault/types'; +import { createFactory } from '../../../../utils/factory-utils'; +import { viemToWeb3Abi } from '../../../../../../helpers/web3'; +import { getWeb3Instance } from '../../../instances'; +import { BalancerGyroEPoolAbi } from '../../../../../../config/abi/BalancerGyroEPoolAbi'; +import type { RatesResult } from './types'; +import BigNumber from 'bignumber.js'; +import { Vault } from '../vault/Vault'; +import { + BIG_ONE, + BIG_ZERO, + bigNumberToStringDeep, + bigNumberToUint256String, +} from '../../../../../../helpers/big-number'; +import { WeightedPoolEncoder } from '../weighted/WeightedPoolEncoder'; +import { GyroFixedPoint } from './GyroFixedPoint'; +import type { ZapStep } from '../../../transact/zap/types'; +import { checkAddressOrder } from '../../../../../../helpers/tokens'; + +export class GyroEPool implements IBalancerPool { + public readonly type = 'balancer'; + + constructor( + protected readonly chain: ChainEntity, + protected readonly vaultConfig: VaultConfig, + protected readonly config: PoolConfig + ) { + checkAddressOrder(config.tokens.map(t => t.address)); + } + + // async getSwapRatioLikeStrategy(): Promise { + // const balances = await this.getCachedBalances(); + // const rates = await this.getTokenRates(); + // const totalSupply = await this.getTotalSupply(); + // if (balances.length !== this.config.tokens.length || rates.length !== this.config.tokens.length) { + // throw new Error('Invalid tokens / rates'); + // } + // + // const amount0 = balances[0].shiftedBy(18).dividedToIntegerBy(totalSupply); + // const amount1 = balances[1].shiftedBy(18).dividedToIntegerBy(totalSupply); + // const ratio = rates[0] + // .shiftedBy(18) + // .dividedToIntegerBy(rates[1]) + // .multipliedBy(amount1) + // .dividedToIntegerBy(amount0); + // + // return BIG_ONE.shiftedBy(18).dividedBy(ratio.plus(BIG_ONE.shiftedBy(18))); + // } + + async getSwapRatio(): Promise { + const upscaledBalances = await this.getCachedUpscaledBalances(); + return upscaledBalances[0].dividedBy(upscaledBalances[0].plus(upscaledBalances[1])); + } + + /** + * Gyro pools only support JoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT + */ + async getAddLiquidityZap( + maxAmountsIn: BigNumber[], + liquidity: BigNumber, + from: string, + insertBalance: boolean + ): Promise { + const vault = this.getVault(); + + return vault.getJoinPoolZap({ + join: { + poolId: this.config.poolId, + sender: from, + recipient: from, + request: { + assets: this.config.tokens.map(t => t.address), + maxAmountsIn: maxAmountsIn, + userData: WeightedPoolEncoder.joinAllTokensInForExactBPTOut( + bigNumberToUint256String(liquidity) + ), + fromInternalBalance: false, + }, + }, + insertBalance, + }); + } + + /** + * Gyro pools only support JoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT + */ + async quoteAddLiquidity(amountsIn: BigNumber[]): Promise { + if (amountsIn.some(amount => amount.lte(BIG_ONE))) { + throw new Error('Input amounts must be greater than 1'); + } + + const vault = this.getVault(); + const totalSupply = await this.getTotalSupply(); + if (totalSupply.isZero()) { + throw new Error('Total supply is zero'); + } + + // all on-chain calculations are done with upscaled balances in FixedPoint (18 decimals) + const upscaledBalances = await this.getCachedUpscaledBalances(); + const upscaledAmountsIn = await this.upscaleAmounts(amountsIn); + + // locally estimate how much liquidity will be added for the given input amounts + const initialEstimatedLiquidity = BigNumber.min( + ...upscaledAmountsIn.map((amount, i) => { + return GyroFixedPoint.divDown( + GyroFixedPoint.mulDown(amount, totalSupply), + upscaledBalances[i] + ); + }) + ); + if (initialEstimatedLiquidity.lte(BIG_ONE)) { + // Some Gyro math requires > 1 + throw new Error('Liquidity added must be greater than 1'); + } + + const upscaledUsedInput = [...upscaledAmountsIn]; + let estimatedLiquidity = initialEstimatedLiquidity; + do { + console.debug( + 'local check', + bigNumberToStringDeep({ + liquidity: estimatedLiquidity, + upscaledUsedInput, + upscaledBalances, + totalSupply, + }) + ); + // locally estimate how much of each input amount will be used to add the given liquidity + for (let i = 0; i < upscaledUsedInput.length; i++) { + upscaledUsedInput[i] = GyroFixedPoint.divUp( + GyroFixedPoint.mulUp(upscaledBalances[i], estimatedLiquidity), + totalSupply + ); + } + + if (upscaledUsedInput.some(amount => amount.lte(BIG_ONE))) { + // Some Gyro math requires > 1 + throw new Error('Failed to calculate liquidity'); + } + + // if the estimated used input amounts are less than or equal to the actual input amounts, we can ask for this much liquidity + if (upscaledUsedInput.every((amount, i) => amount.lte(upscaledAmountsIn[i]))) { + // double check via rpc call + const queryRequest: QueryJoinPoolRequest = { + poolId: this.config.poolId, + request: { + assets: this.config.tokens.map(t => t.address), + maxAmountsIn: amountsIn, + userData: WeightedPoolEncoder.joinAllTokensInForExactBPTOut( + bigNumberToUint256String(estimatedLiquidity) + ), + fromInternalBalance: false, + }, + }; + const queryResult = await vault.queryJoinPool(queryRequest); + console.debug( + 'queryJoinPool', + bigNumberToStringDeep(queryRequest), + bigNumberToStringDeep(queryResult) + ); + + // if on-chain result is consistent with local estimate, return the result + if (queryResult.usedInput.every((amount, i) => amount.lte(amountsIn[i]))) { + return { + liquidity: queryResult.liquidity, + usedInput: queryResult.usedInput, + unusedInput: amountsIn.map((amount, i) => amount.minus(queryResult.usedInput[i])), + }; + } + } + + // next time try with 1 less liquidity + estimatedLiquidity = estimatedLiquidity.minus(BIG_ONE); + } while (estimatedLiquidity.gt(BIG_ONE)); + + throw new Error('Failed to calculate liquidity'); + } + + /** + * Gyro pools only support ExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT + */ + async quoteRemoveLiquidity(amountIn: BigNumber): Promise { + if (amountIn.lt(BIG_ONE)) { + throw new Error('Input amount must be greater than 0'); + } + + const vault = this.getVault(); + + const queryRequest: QueryExitPoolRequest = { + poolId: this.config.poolId, + request: { + assets: this.config.tokens.map(t => t.address), + minAmountsOut: this.config.tokens.map(() => BIG_ZERO), + userData: WeightedPoolEncoder.exitExactBPTInForTokensOut( + bigNumberToUint256String(amountIn) + ), + toInternalBalance: false, + }, + }; + + return await vault.queryExitPool(queryRequest); + } + + /** + * Gyro pools only support ExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT + */ + async getRemoveLiquidityZap( + amountIn: BigNumber, + minAmountsOut: BigNumber[], + from: string, + insertBalance: boolean + ): Promise { + const vault = this.getVault(); + + return vault.getExitPoolZap({ + exit: { + poolId: this.config.poolId, + sender: from, + recipient: from, + request: { + assets: this.config.tokens.map(t => t.address), + minAmountsOut, + userData: WeightedPoolEncoder.exitExactBPTInForTokensOut( + bigNumberToUint256String(amountIn) + ), + toInternalBalance: false, + }, + }, + poolAddress: this.config.poolAddress, + insertBalance, + }); + } + + protected getDecimalScalingFactors = createFactory(() => { + return this.config.tokens.map(token => { + if (token.address === this.config.poolAddress) { + return GyroFixedPoint.ONE; + } + + if (token.decimals > 18) { + throw new Error('Tokens with more than 18 decimals are not supported.'); + } + + const diff = 18 - token.decimals; + return GyroFixedPoint.ONE.shiftedBy(diff); + }); + }); + + protected getCachedScalingFactors = createFactory(async () => { + const factors = this.getDecimalScalingFactors(); + const rates = await this.getCachedTokenRates(); + return factors.map((factor, i) => GyroFixedPoint.mulDown(factor, rates[i])); + }); + + protected async upscaleAmounts(balances: BigNumber[]): Promise { + const factors = await this.getCachedScalingFactors(); + return balances.map((balance, i) => GyroFixedPoint.mulDown(balance, factors[i])); + } + + protected async downscaleAmounts(amounts: BigNumber[]): Promise { + const factors = await this.getCachedScalingFactors(); + return amounts.map((amount, i) => GyroFixedPoint.divUp(amount, factors[i])); + } + + protected getCachedPoolTokens = createFactory(async () => { + const vault = this.getVault(); + return await vault.getPoolTokens(this.config.poolId); + }); + + protected getCachedBalances = createFactory(async () => { + const poolTokens = await this.getCachedPoolTokens(); + return poolTokens.map(t => t.balance); + }); + + protected getCachedUpscaledBalances = createFactory(async () => { + return await this.upscaleAmounts(await this.getCachedBalances()); + }); + + protected getCachedTotalSupply = createFactory(async () => { + return this.getTotalSupply(); + }); + + protected async getTotalSupply(): Promise { + const pool = await this.getPoolContract(); + const totalSupply: string = await pool.methods.getActualSupply().call(); + return new BigNumber(totalSupply); + } + + protected getCachedTokenRates = createFactory(async () => { + return this.getTokenRates(); + }); + + protected async getTokenRates(): Promise { + const pool = await this.getPoolContract(); + const rates: RatesResult = await pool.methods.getTokenRates().call(); + return [rates.rate0, rates.rate1].map(rate => new BigNumber(rate)); + } + + protected getWeb3 = createFactory(() => getWeb3Instance(this.chain)); + + protected getPoolContract = createFactory(async () => { + const web3 = await this.getWeb3(); + return new web3.eth.Contract(viemToWeb3Abi(BalancerGyroEPoolAbi), this.config.poolAddress); + }); + + protected getVault = createFactory(() => { + return new Vault(this.chain, this.vaultConfig); + }); +} diff --git a/src/features/data/apis/amm/balancer/gyroe/GyroFixedPoint.ts b/src/features/data/apis/amm/balancer/gyroe/GyroFixedPoint.ts new file mode 100644 index 000000000..c5ff44084 --- /dev/null +++ b/src/features/data/apis/amm/balancer/gyroe/GyroFixedPoint.ts @@ -0,0 +1,65 @@ +import { BIG_ONE, BIG_ZERO } from '../../../../../../helpers/big-number'; +import BigNumber from 'bignumber.js'; + +export class GyroFixedPoint { + public static readonly ONE = BIG_ONE.shiftedBy(18); + + private constructor() { + // static only + } + + static mulDown(a: BigNumber, b: BigNumber): BigNumber { + const product = a.multipliedBy(b).integerValue(BigNumber.ROUND_FLOOR); + if (!(a.isZero() || product.dividedToIntegerBy(a).isEqualTo(b))) { + throw new Error('Multiplication overflow'); + } + return product.dividedToIntegerBy(GyroFixedPoint.ONE); + } + + static mulUp(a: BigNumber, b: BigNumber): BigNumber { + const product = a.multipliedBy(b).integerValue(BigNumber.ROUND_FLOOR); + if (!(a.isZero() || product.dividedToIntegerBy(a).isEqualTo(b))) { + throw new Error('Multiplication overflow'); + } + + if (product.isZero()) { + return product; + } + + return product.minus(1).dividedToIntegerBy(GyroFixedPoint.ONE).plus(1); + } + + static divDown(a: BigNumber, b: BigNumber): BigNumber { + if (b.isZero()) { + throw new Error('Division by zero'); + } + + if (a.isZero()) { + return BIG_ZERO; + } + + const aInflated = a.multipliedBy(GyroFixedPoint.ONE); + if (!aInflated.dividedToIntegerBy(a).isEqualTo(GyroFixedPoint.ONE)) { + throw new Error('Multiplication overflow'); + } + + return aInflated.dividedToIntegerBy(b); + } + + static divUp(a: BigNumber, b: BigNumber): BigNumber { + if (b.isZero()) { + throw new Error('Division by zero'); + } + + if (a.isZero()) { + return BIG_ZERO; + } + + const aInflated = a.multipliedBy(GyroFixedPoint.ONE); + if (!aInflated.dividedToIntegerBy(a).isEqualTo(GyroFixedPoint.ONE)) { + throw new Error('Multiplication overflow'); + } + + return aInflated.minus(1).dividedToIntegerBy(b).plus(1); + } +} diff --git a/src/features/data/apis/amm/balancer/gyroe/types.ts b/src/features/data/apis/amm/balancer/gyroe/types.ts new file mode 100644 index 000000000..75efb59ac --- /dev/null +++ b/src/features/data/apis/amm/balancer/gyroe/types.ts @@ -0,0 +1,4 @@ +export type RatesResult = { + rate0: string; + rate1: string; +}; diff --git a/src/features/data/apis/amm/balancer/vault/Vault.ts b/src/features/data/apis/amm/balancer/vault/Vault.ts new file mode 100644 index 000000000..2e355b163 --- /dev/null +++ b/src/features/data/apis/amm/balancer/vault/Vault.ts @@ -0,0 +1,397 @@ +import type { ChainEntity } from '../../../../entities/chain'; +import { + type AbiEncodeArgs, + type BatchSwapArgs, + type ExitPoolArgs, + type ExitPoolResult, + type ExitPoolZapRequest, + type FundManagement, + type JoinPoolArgs, + type JoinPoolResult, + type JoinPoolZapRequest, + type PoolConfig, + type PoolTokensResponse, + type PoolTokensResult, + type QueryBatchSwapArgs, + type QueryBatchSwapRequest, + type QueryBatchSwapResponse, + type QueryExitPoolRequest, + type QueryExitPoolResponse, + type QueryJoinPoolRequest, + type QueryJoinPoolResponse, + type SwapArgs, + type SwapZapRequest, + type VaultConfig, +} from './types'; +import { ZERO_ADDRESS } from '../../../../../../helpers/addresses'; +import BigNumber from 'bignumber.js'; +import { getWeb3Instance } from '../../../instances'; +import { viemToWeb3Abi } from '../../../../../../helpers/web3'; +import { BalancerVaultAbi } from '../../../../../../config/abi/BalancerVaultAbi'; +import { createCachedFactory, createFactory } from '../../../../utils/factory-utils'; +import { BalancerQueriesAbi } from '../../../../../../config/abi/BalancerQueriesAbi'; +import abiCoder from 'web3-eth-abi'; +import type { AbiItem } from 'web3-utils'; +import { + bigNumberToInt256String, + bigNumberToUint256String, +} from '../../../../../../helpers/big-number'; +import type { StepToken, ZapStep } from '../../../transact/zap/types'; +import { getInsertIndex } from '../../../transact/helpers/zap'; +import { getAddress } from 'viem'; +import { WeightedPoolExitKind } from '../weighted/types'; + +const queryFunds: FundManagement = { + sender: ZERO_ADDRESS, + fromInternalBalance: false, + recipient: ZERO_ADDRESS, + toInternalBalance: false, +}; + +export class Vault { + constructor(protected readonly chain: ChainEntity, protected readonly config: VaultConfig) {} + + async getPoolTokens(poolId: string): Promise { + const vault = await this.getVaultContract(); + const result: PoolTokensResult = await vault.methods.getPoolTokens(poolId).call(); + + if ( + !result?.tokens || + !Array.isArray(result.tokens) || + !result.balances || + !Array.isArray(result.balances) || + result.tokens.length !== result.balances.length + ) { + throw new Error('Invalid result'); + } + + return result.tokens.map((token, index) => ({ + token: getAddress(token), + balance: new BigNumber(result.balances[index]), + })); + } + + async queryJoinPool(request: QueryJoinPoolRequest): Promise { + const query = await this.getQueryContract(); + const args: JoinPoolArgs = { + ...request, + sender: ZERO_ADDRESS, + recipient: ZERO_ADDRESS, + }; + + const result: JoinPoolResult = await query.methods + .queryJoin(args.poolId, args.sender, args.recipient, [ + args.request.assets, + args.request.maxAmountsIn.map(bigNumberToUint256String), + args.request.userData, + args.request.fromInternalBalance, + ]) + .call(); + + if ( + !result || + !Array.isArray(result.amountsIn) || + result.amountsIn.length !== request.request.assets.length + ) { + throw new Error('Invalid result'); + } + + const usedInput = result.amountsIn.map(amount => new BigNumber(amount)); + + return { + liquidity: new BigNumber(result.bptOut), + usedInput, + unusedInput: request.request.maxAmountsIn.map((amount, index) => + amount.minus(usedInput[index]) + ), + }; + } + + async queryExitPool(request: QueryExitPoolRequest): Promise { + const query = await this.getQueryContract(); + const args: ExitPoolArgs = { + ...request, + sender: ZERO_ADDRESS, + recipient: ZERO_ADDRESS, + }; + + console.debug(this.config.queryAddress); + console.debug( + args.poolId, + args.sender, + args.recipient, + JSON.stringify({ + assets: args.request.assets, + minAmountsOut: args.request.minAmountsOut.map(bigNumberToUint256String), + userData: args.request.userData, + toInternalBalance: args.request.toInternalBalance, + }) + ); + + const result: ExitPoolResult = await query.methods + .queryExit(args.poolId, args.sender, args.recipient, [ + args.request.assets, + args.request.minAmountsOut.map(bigNumberToUint256String), + args.request.userData, + args.request.toInternalBalance, + ]) + .call(); + + if ( + !result || + !Array.isArray(result.amountsOut) || + result.amountsOut.length !== request.request.assets.length + ) { + throw new Error('Invalid result'); + } + + return { + liquidity: new BigNumber(result.bptIn), + outputs: result.amountsOut.map(amount => new BigNumber(amount)), + }; + } + + async queryBatchSwap(request: QueryBatchSwapRequest): Promise { + const vault = await this.getVaultContract(); + const args: QueryBatchSwapArgs = { + ...request, + funds: queryFunds, + }; + + const result: Array | undefined = await vault.methods + .queryBatchSwap( + args.kind, + args.swaps.map(s => ({ + ...s, + amount: bigNumberToUint256String(s.amount), + })), + args.assets, + args.funds + ) + .call(); + + if (!result || !Array.isArray(result) || result.length !== request.assets.length) { + throw new Error('Invalid result'); + } + + return result.map(value => new BigNumber(value)); + } + + async getJoinPoolZap(request: JoinPoolZapRequest): Promise { + /* + * 08 | length of request.assets + * 09+0 | request.assets[0] + * 09+n | request.assets[n] + * 09+request.assets.length | length of maxAmountsIn + * 09+request.assets.length+1 | maxAmountsIn[0] + * 09+request.assets.length+1+n | maxAmountsIn[n] + */ + const maxAmountsInStartWord = 9 + request.join.request.assets.length + 1; + + return { + target: this.config.vaultAddress, + value: '0', + data: this.encodeJoinPool(request.join), + tokens: request.insertBalance + ? request.join.request.assets.map((address, i) => ({ + token: address, + index: getInsertIndex(maxAmountsInStartWord + i), + })) + : [], + }; + } + + async getExitPoolZap(request: ExitPoolZapRequest): Promise { + return { + target: this.config.vaultAddress, + value: '0', + data: this.encodeExitPool(request.exit), + tokens: this.getExitPoolZapTokens(request), + }; + } + + protected getExitPoolZapTokens(request: ExitPoolZapRequest): StepToken[] { + if (!request.insertBalance) { + return []; + } + + /* + * 08 | length of request.assets + * 09+0 | request.assets[0] + * 09+n | request.assets[n] + * 09+request.assets.length | length of minAmountsOut + * 09+request.assets.length+1+0 | minAmountsOut[0] + * 09+request.assets.length+1+n | minAmountsOut[n] + * 09+request.assets.length+1+request.minAmountsOut.length | length of userData + * 09+request.assets.length+1+request.minAmountsOut.length+1+0 | word 0 of userData + */ + const userDataWordStart = + 9 + request.exit.request.assets.length + 1 + request.exit.request.minAmountsOut.length + 1; + const exitKindString = abiCoder.decodeParameter( + 'uint256', + request.exit.request.userData + ) as unknown as string; + console.log('exitKindString', exitKindString); + const exitKind = new BigNumber(exitKindString).toNumber(); + + switch (exitKind) { + case WeightedPoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT: { + return [ + { + token: request.poolAddress, + index: getInsertIndex(userDataWordStart + 1), // 0 = ExitKind, 1 = BPT amount + }, + ]; + } + case WeightedPoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT: { + return [ + { + token: request.poolAddress, + index: getInsertIndex(userDataWordStart + 1), // 0 = ExitKind, 1 = BPT amount, 2 = exitTokenIndex + }, + ]; + } + case WeightedPoolExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT: { + return [ + { + token: request.poolAddress, + index: getInsertIndex(userDataWordStart + 2), // 0 = ExitKind, 1 = Offset to amountsOut, 2 = maxBPTAmountIn, 3 = amountsOut.length, 4 = amountsOut[0], 5 = amountsOut[1] + }, + ]; + } + default: { + throw new Error(`Unsupported exit kind: ${exitKind}`); + } + } + } + + async getSwapZap(request: SwapZapRequest): Promise { + /* + the byte offset at which amountIn is inserted in the calldata + calculated using 32-byte words: + 00 : swap offset (07) + valueAt(00) + 0 : singleSwap.poolId + .. + valueAt(00) + 4 : singleSwap.amount + */ + const amountInIndex = getInsertIndex(7 + 4); + + return { + target: this.config.vaultAddress, + value: '0', + data: this.encodeSwap(request.swap), + tokens: request.insertBalance + ? [{ token: request.swap.singleSwap.assetIn, index: amountInIndex }] + : [], + }; + } + + protected getWeb3 = createFactory(() => getWeb3Instance(this.chain)); + + protected getVaultContract = createFactory(async () => { + const web3 = await this.getWeb3(); + return new web3.eth.Contract(viemToWeb3Abi(BalancerVaultAbi), this.config.vaultAddress); + }); + + protected getQueryContract = createFactory(async () => { + const web3 = await this.getWeb3(); + return new web3.eth.Contract(viemToWeb3Abi(BalancerQueriesAbi), this.config.queryAddress); + }); + + protected getVaultFunctionAbi = createCachedFactory( + (name: string) => { + const methodAbi = BalancerVaultAbi.find(abi => abi.type === 'function' && abi.name === name); + if (!methodAbi) { + throw new Error(`Function "${name}" not found in vault ABI`); + } + return methodAbi as AbiItem; + }, + (name: string) => name + ); + + protected encodeJoinPool(args: JoinPoolArgs): string { + const methodAbi = this.getVaultFunctionAbi('joinPool'); + + return abiCoder.encodeFunctionCall(methodAbi, [ + args.poolId, + args.sender, + args.recipient, + [ + args.request.assets, + args.request.maxAmountsIn.map(bigNumberToUint256String), + args.request.userData, + args.request.fromInternalBalance, + ], + ] satisfies AbiEncodeArgs); + } + + protected encodeExitPool(args: ExitPoolArgs): string { + const methodAbi = this.getVaultFunctionAbi('exitPool'); + + return abiCoder.encodeFunctionCall(methodAbi, [ + args.poolId, + args.sender, + args.recipient, + [ + args.request.assets, + args.request.minAmountsOut.map(bigNumberToUint256String), + args.request.userData, + args.request.toInternalBalance, + ], + ] satisfies AbiEncodeArgs); + } + + protected encodeSwap(args: SwapArgs): string { + const methodAbi = this.getVaultFunctionAbi('swap'); + + return abiCoder.encodeFunctionCall(methodAbi, [ + [ + args.singleSwap.poolId, + args.singleSwap.kind, + args.singleSwap.assetIn, + args.singleSwap.assetOut, + bigNumberToUint256String(args.singleSwap.amount), + args.singleSwap.userData, + ], + [ + args.funds.sender, + args.funds.fromInternalBalance, + args.funds.recipient, + args.funds.toInternalBalance, + ], + bigNumberToUint256String(args.limit), + args.deadline, + ] satisfies AbiEncodeArgs); + } + + protected encodeBatchSwap(args: BatchSwapArgs): string { + const methodAbi = this.getVaultFunctionAbi('batchSwap'); + + return abiCoder.encodeFunctionCall(methodAbi, [ + args.kind, + args.swaps.map(swap => [ + swap.poolId, + swap.assetInIndex, + swap.assetOutIndex, + bigNumberToUint256String(swap.amount), + swap.userData, + ]), + args.assets, + [ + args.funds.sender, + args.funds.fromInternalBalance, + args.funds.recipient, + args.funds.toInternalBalance, + ], + args.limits.map(bigNumberToInt256String), + args.deadline, + ] satisfies AbiEncodeArgs); + } + + protected checkToken(pool: PoolConfig, token: string, label: string = 'token') { + const index = pool.tokens.findIndex(t => t.address === token); + if (index === -1) { + throw new Error(`${label} must be a pool token`); + } + } +} diff --git a/src/features/data/apis/amm/balancer/vault/types.ts b/src/features/data/apis/amm/balancer/vault/types.ts new file mode 100644 index 000000000..589758c04 --- /dev/null +++ b/src/features/data/apis/amm/balancer/vault/types.ts @@ -0,0 +1,212 @@ +import type BigNumber from 'bignumber.js'; +import type { TokenEntity } from '../../../../entities/token'; +import type { WeightedPoolExitKind } from '../weighted/types'; + +export type VaultConfig = { + /** address */ + vaultAddress: string; + /** address */ + queryAddress: string; +}; + +export type PoolConfig = { + /** address */ + poolAddress: string; + /** bytes32 */ + poolId: string; + tokens: TokenEntity[]; +}; + +export enum SwapKind { + GIVEN_IN = 0, + GIVEN_OUT = 1, +} + +type SingleSwap = { + /** bytes32 */ + poolId: string; + /** uint8 */ + kind: SwapKind; + /** address */ + assetIn: string; + /** address */ + assetOut: string; + /** uint256 */ + amount: BigNumber; + /** bytes */ + userData: string; +}; + +type BatchSwapStep = { + /** bytes32 */ + poolId: string; + /** uint256 */ + assetInIndex: number; + /** uint256 */ + assetOutIndex: number; + /** uint256 */ + amount: BigNumber; + /** bytes */ + userData: string; +}; + +export type FundManagement = { + /** address */ + sender: string; + /** bool */ + fromInternalBalance: boolean; + /** address */ + recipient: string; + /** bool */ + toInternalBalance: boolean; +}; + +export type QueryBatchSwapArgs = { + /** uint8 */ + kind: SwapKind; + /** tuple[] */ + swaps: BatchSwapStep[]; + /** address[] */ + assets: string[]; + /** tuple */ + funds: FundManagement; +}; + +export type SwapArgs = { + /** tuple */ + singleSwap: SingleSwap; + /** tuple */ + funds: FundManagement; + /** uint256 */ + limit: BigNumber; + /** uint256 */ + deadline: number; +}; + +export type BatchSwapArgs = { + /** uint8 */ + kind: SwapKind; + /** tuple[] */ + swaps: BatchSwapStep[]; + /** address[] */ + assets: string[]; + /** tuple */ + funds: FundManagement; + /** int256[] : +ve for tokens sent to the pool, -ve for tokens received from the pool */ + limits: BigNumber[]; + /** uint256 */ + deadline: number; +}; + +export type JoinPoolRequest = { + /** address[] */ + assets: string[]; + /** uint256[] */ + maxAmountsIn: BigNumber[]; + /** bytes */ + userData: string; + /** bool */ + fromInternalBalance: boolean; +}; + +export type JoinPoolArgs = { + /** bytes32 */ + poolId: string; + /** address */ + sender: string; + /** address */ + recipient: string; + /** tuple */ + request: JoinPoolRequest; +}; + +export type JoinPoolResult = { + bptOut: string; + amountsIn: string[]; +}; + +export type ExitPoolRequest = { + /** address[] */ + assets: string[]; + /** uint256[] */ + minAmountsOut: BigNumber[]; + /** bytes */ + userData: string; + /** bool */ + toInternalBalance: boolean; +}; + +export type ExitPoolArgs = { + /** bytes32 */ + poolId: string; + /** address */ + sender: string; + /** address */ + recipient: string; + /** tuple */ + request: ExitPoolRequest; +}; + +export type ExitPoolResult = { + /** uint256 */ + bptIn: string; + /** uint256[] */ + amountsOut: string[]; +}; + +export type AbiEncodeArgs = string | number | boolean | Array; + +export type QueryBatchSwapRequest = Omit; +export type QueryBatchSwapResponse = BigNumber[]; + +export type QueryJoinPoolRequest = Omit; +export type QueryJoinPoolResponse = { + liquidity: BigNumber; + usedInput: BigNumber[]; + unusedInput: BigNumber[]; +}; + +export type QueryExitPoolRequest = Omit; +export type QueryExitPoolResponse = { + liquidity: BigNumber; + outputs: BigNumber[]; +}; + +export type SwapZapRequest = { + swap: SwapArgs; + insertBalance: boolean; +}; + +export type PoolTokensResult = { + tokens: string[]; + balances: string[]; + lastChangeBlock: string; +}; + +export type PoolTokensResponse = Array<{ token: string; balance: BigNumber }>; + +export type JoinPoolZapRequest = { + join: JoinPoolArgs; + insertBalance: boolean; +}; + +export type ExitPoolZapRequestKind = + | { + kind: WeightedPoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT; + token: string; + } + | { + kind: WeightedPoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT; + token: string; + tokenIndex: number; + } + | { + kind: WeightedPoolExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT; + token: string; + }; + +export type ExitPoolZapRequest = { + exit: ExitPoolArgs; + poolAddress: string; + insertBalance: boolean; +}; diff --git a/src/features/data/apis/amm/balancer/weighted/WeightedPoolEncoder.ts b/src/features/data/apis/amm/balancer/weighted/WeightedPoolEncoder.ts new file mode 100644 index 000000000..b85bf1214 --- /dev/null +++ b/src/features/data/apis/amm/balancer/weighted/WeightedPoolEncoder.ts @@ -0,0 +1,89 @@ +import abiCoder from 'web3-eth-abi'; +import { WeightedPoolExitKind, WeightedPoolJoinKind } from './types'; + +export class WeightedPoolEncoder { + private constructor() { + // static only + } + + /** + * Encodes the userData parameter for providing the initial liquidity to a WeightedPool + * @param initialBalances - the amounts of tokens to send to the pool to form the initial balances + */ + static joinInit(initialBalances: string[]): string { + return abiCoder.encodeParameters( + ['uint256', 'uint256[]'], + [WeightedPoolJoinKind.INIT, initialBalances] + ); + } + + /** + * Encodes the userData parameter for joining a WeightedPool with exact token inputs + * @param amountsIn - the amounts each of token to deposit in the pool as liquidity + * @param minimumBPT - the minimum acceptable BPT to receive in return for deposited tokens + */ + static joinExactTokensInForBPTOut(amountsIn: string[], minimumBPT: string): string { + return abiCoder.encodeParameters( + ['uint256', 'uint256[]', 'uint256'], + [WeightedPoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT, amountsIn, minimumBPT] + ); + } + + /** + * Encodes the userData parameter for joining a WeightedPool with a single token to receive an exact amount of BPT + * @param bptAmountOut - the amount of BPT to be minted + * @param enterTokenIndex - the index of the token to be provided as liquidity + */ + static joinTokenInForExactBPTOut(bptAmountOut: string, enterTokenIndex: number): string { + return abiCoder.encodeParameters( + ['uint256', 'uint256', 'uint256'], + [WeightedPoolJoinKind.TOKEN_IN_FOR_EXACT_BPT_OUT, bptAmountOut, enterTokenIndex] + ); + } + + /** + * Encodes the userData parameter for joining a WeightedPool proportionally to receive an exact amount of BPT + * @param bptAmountOut - the amount of BPT to be minted + */ + static joinAllTokensInForExactBPTOut(bptAmountOut: string): string { + return abiCoder.encodeParameters( + ['uint256', 'uint256'], + [WeightedPoolJoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT, bptAmountOut] + ); + } + + /** + * Encodes the userData parameter for exiting a WeightedPool by removing a single token in return for an exact amount of BPT + * @param bptAmountIn - the amount of BPT to be burned + * @param exitTokenIndex - the index of the token to removed from the pool + */ + static exitExactBPTInForOneTokenOut(bptAmountIn: string, exitTokenIndex: number): string { + return abiCoder.encodeParameters( + ['uint256', 'uint256', 'uint256'], + [WeightedPoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT, bptAmountIn, exitTokenIndex] + ); + } + + /** + * Encodes the userData parameter for exiting a WeightedPool by removing tokens in return for an exact amount of BPT + * @param bptAmountIn - the amount of BPT to be burned + */ + static exitExactBPTInForTokensOut(bptAmountIn: string): string { + return abiCoder.encodeParameters( + ['uint256', 'uint256'], + [WeightedPoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT, bptAmountIn] + ); + } + + /** + * Encodes the userData parameter for exiting a WeightedPool by removing exact amounts of tokens + * @param amountsOut - the amounts of each token to be withdrawn from the pool + * @param maxBPTAmountIn - the minimum acceptable BPT to burn in return for withdrawn tokens + */ + static exitBPTInForExactTokensOut(amountsOut: string[], maxBPTAmountIn: string): string { + return abiCoder.encodeParameters( + ['uint256', 'uint256[]', 'uint256'], + [WeightedPoolExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT, amountsOut, maxBPTAmountIn] + ); + } +} diff --git a/src/features/data/apis/amm/balancer/weighted/types.ts b/src/features/data/apis/amm/balancer/weighted/types.ts new file mode 100644 index 000000000..df20c6e5c --- /dev/null +++ b/src/features/data/apis/amm/balancer/weighted/types.ts @@ -0,0 +1,14 @@ +export enum WeightedPoolJoinKind { + INIT = 0, + EXACT_TOKENS_IN_FOR_BPT_OUT, + TOKEN_IN_FOR_EXACT_BPT_OUT, + ALL_TOKENS_IN_FOR_EXACT_BPT_OUT, + ADD_TOKEN, +} + +export enum WeightedPoolExitKind { + EXACT_BPT_IN_FOR_ONE_TOKEN_OUT = 0, + EXACT_BPT_IN_FOR_TOKENS_OUT, + BPT_IN_FOR_EXACT_TOKENS_OUT, + REMOVE_TOKEN, +} diff --git a/src/features/data/apis/transact/strategies/balancer/BalancerPoolStrategy.ts b/src/features/data/apis/transact/strategies/balancer/BalancerPoolStrategy.ts new file mode 100644 index 000000000..48f2a5bc9 --- /dev/null +++ b/src/features/data/apis/transact/strategies/balancer/BalancerPoolStrategy.ts @@ -0,0 +1,1032 @@ +import type { Namespace, TFunction } from 'react-i18next'; +import { + isTokenEqual, + isTokenErc20, + type TokenEntity, + type TokenErc20, + type TokenNative, +} from '../../../../entities/token'; +import type { Step } from '../../../../reducers/wallet/stepper'; +import { + type BalancerPoolDepositOption, + type BalancerPoolDepositQuote, + type BalancerPoolWithdrawOption, + type BalancerPoolWithdrawQuote, + type InputTokenAmount, + isZapQuoteStepBuild, + isZapQuoteStepSplit, + isZapQuoteStepSwap, + isZapQuoteStepSwapAggregator, + isZapQuoteStepWithdraw, + type TokenAmount, + type ZapQuoteStep, + type ZapQuoteStepBuild, + type ZapQuoteStepSplit, + type ZapQuoteStepSwap, + type ZapQuoteStepSwapAggregator, +} from '../../transact-types'; +import type { IZapStrategy, IZapStrategyStatic, ZapTransactHelpers } from '../IStrategy'; +import type { ChainEntity } from '../../../../entities/chain'; +import { + createOptionId, + createQuoteId, + createSelectionId, + onlyOneInput, + onlyOneToken, + onlyOneTokenAmount, +} from '../../helpers/options'; +import { + selectChainNativeToken, + selectChainWrappedNativeToken, + selectIsTokenLoaded, + selectTokenByAddressOrUndefined, + selectTokenPriceByTokenOracleId, +} from '../../../../selectors/tokens'; +import { selectChainById } from '../../../../selectors/chains'; +import { TransactMode } from '../../../../reducers/wallet/transact-types'; +import { first, uniqBy } from 'lodash-es'; +import { + BIG_ZERO, + bigNumberToStringDeep, + fromWei, + fromWeiToTokenAmount, + toWeiFromTokenAmount, + toWeiString, +} from '../../../../../../helpers/big-number'; +import { calculatePriceImpact, highestFeeOrZero } from '../../helpers/quotes'; +import BigNumber from 'bignumber.js'; +import type { BeefyState, BeefyThunk } from '../../../../../../redux-types'; +import type { QuoteRequest } from '../../swap/ISwapProvider'; +import type { + OrderInput, + OrderOutput, + UserlessZapRequest, + ZapStep, + ZapStepResponse, +} from '../../zap/types'; +import { fetchZapAggregatorSwap } from '../../zap/swap'; +import { selectTransactSlippage } from '../../../../selectors/transact'; +import { Balances } from '../../helpers/Balances'; +import { getTokenAddress, NO_RELAY } from '../../helpers/zap'; +import { slipBy, slipTokenAmountBy } from '../../helpers/amounts'; +import { allTokensAreDistinct, pickTokens } from '../../helpers/tokens'; +import { walletActions } from '../../../../actions/wallet-actions'; +import { isStandardVault, type VaultStandard } from '../../../../entities/vault'; +import { getVaultWithdrawnFromState } from '../../helpers/vault'; +import { isDefined } from '../../../../utils/array-utils'; +import { isStandardVaultType, type IStandardVaultType } from '../../vaults/IVaultType'; +import type { BalancerPoolStrategyConfig } from '../strategy-configs'; +import { type AmmEntityBalancer, isBalancerAmm } from '../../../../entities/zap'; +import { selectAmmById } from '../../../../selectors/zap'; +import { createFactory } from '../../../../utils/factory-utils'; +import type { PoolConfig, VaultConfig } from '../../../amm/balancer/vault/types'; +import { GyroEPool } from '../../../amm/balancer/gyroe/GyroEPool'; + +type ZapHelpers = { + slippage: number; + state: BeefyState; +}; + +const strategyId = 'balancer-pool' as const; +type StrategyId = typeof strategyId; + +/** + * Balancer: joinPool() to deposit / exitPool() to withdraw liquidity + */ +class BalancerPoolStrategyImpl implements IZapStrategy { + public static readonly id = strategyId; + public readonly id = strategyId; + + protected readonly native: TokenNative; + protected readonly wnative: TokenErc20; + protected readonly poolTokens: TokenEntity[]; + protected readonly chain: ChainEntity; + protected readonly depositToken: TokenEntity; + protected readonly vault: VaultStandard; + protected readonly vaultType: IStandardVaultType; + protected readonly amm: AmmEntityBalancer; + + constructor( + protected options: BalancerPoolStrategyConfig, + protected helpers: ZapTransactHelpers + ) { + const { vault, vaultType, getState } = this.helpers; + + if (!isStandardVault(vault)) { + throw new Error('Vault is not a standard vault'); + } + if (!isStandardVaultType(vaultType)) { + throw new Error('Vault type is not standard'); + } + + const state = getState(); + for (let i = 0; i < vault.assetIds.length; ++i) { + if (!selectIsTokenLoaded(state, vault.chainId, vault.assetIds[i])) { + throw new Error(`Vault ${vault.id}: Asset ${vault.assetIds[i]} not loaded`); + } + } + + const amm = selectAmmById(state, this.options.ammId); + if (!amm) { + throw new Error(`Vault ${vault.id}: AMM ${this.options.ammId} not found`); + } + if (!isBalancerAmm(amm)) { + throw new Error(`Vault ${vault.id}: AMM ${this.options.ammId} is not balancer type`); + } + + this.amm = amm; + this.vault = vault; + this.vaultType = vaultType; + this.native = selectChainNativeToken(state, vault.chainId); + this.wnative = selectChainWrappedNativeToken(state, vault.chainId); + this.depositToken = vaultType.depositToken; + this.chain = selectChainById(state, vault.chainId); + this.poolTokens = this.selectPoolTokens(state, this.chain.id, this.options.tokens); + + if (this.options.poolType === 'gyroe') { + this.checkPoolTokensCount(2); + this.checkPoolTokensHavePrice(state); + } else { + throw new Error(`Unsupported balancer pool type ${this.options.poolType}`); + } + } + + protected selectPoolTokens( + state: BeefyState, + chainId: ChainEntity['id'], + tokenAddresses: string[] + ): TokenEntity[] { + const tokens = tokenAddresses + .map(address => selectTokenByAddressOrUndefined(state, chainId, address)) + .filter(isDefined); + if (tokens.length !== tokenAddresses.length) { + // We need decimals for each token + throw new Error('Not all tokens are in state'); + } + return tokens; + } + + protected checkPoolTokensCount(count: number) { + if (this.poolTokens.length !== count) { + throw new Error(`There must be exactly ${count} pool tokens`); + } + } + + protected checkPoolTokensHavePrice(state: BeefyState) { + if ( + this.poolTokens.some(token => { + const price = selectTokenPriceByTokenOracleId(state, token.oracleId); + return !price || price.lte(BIG_ZERO); + }) + ) { + throw new Error('All pool tokens must have a price'); + } + } + + public async fetchDepositOptions(): Promise { + // what tokens can we can zap via pool with + // const poolTokens = includeNativeAndWrapped(this.poolTokens.map(t => t.token), this.wnative, this.native).map( + // token => ({ + // token, + // via: 'pool' as const, + // }) + // ); + + // what tokens we can zap via swap aggregator with + const supportedAggregatorTokens = await this.aggregatorTokenSupport(); + const aggregatorTokens = supportedAggregatorTokens + .filter(token => !isTokenEqual(token, this.vaultType.depositToken)) + .map(token => ({ token, via: 'aggregator' as const })); + + const zapTokens = aggregatorTokens; //[...poolTokens, ...aggregatorTokens]; + const outputs = [this.vaultType.depositToken]; + + return zapTokens.map(({ token, via }) => { + const inputs = [token]; + const selectionId = createSelectionId(this.vault.chainId, inputs); + + return { + id: createOptionId(this.id, this.vault.id, selectionId, via), + vaultId: this.vault.id, + chainId: this.vault.chainId, + selectionId, + selectionOrder: 3, //via === 'pool' ? 2 : 3, + inputs, + wantedOutputs: outputs, + mode: TransactMode.Deposit, + strategyId, + via, + } as const satisfies BalancerPoolDepositOption; + }); + } + + protected getPool = createFactory(() => { + const vault: VaultConfig = { + vaultAddress: this.amm.vaultAddress, + queryAddress: this.amm.queryAddress, + }; + const pool: PoolConfig = { + poolAddress: this.depositToken.address, + poolId: this.options.poolId, + tokens: this.poolTokens, + }; + + switch (this.options.poolType) { + case 'gyroe': { + return new GyroEPool(this.chain, vault, pool); + } + default: { + throw new Error(`Unsupported balancer pool type ${this.options.poolType}`); + } + } + }); + + public async fetchDepositQuote( + inputs: InputTokenAmount[], + option: BalancerPoolDepositOption + ): Promise { + const input = onlyOneInput(inputs); + if (input.amount.lte(BIG_ZERO)) { + throw new Error('BalancerPoolStrategy: Quote called with 0 input amount'); + } + + if (option.via === 'aggregator') { + return this.fetchDepositQuoteAggregator(input, option); + } else { + throw new Error('Unknown zap deposit option via'); + // return this.fetchDepositQuotePool(input, option); + } + } + + protected async getSwapAmounts( + input: TokenAmount + ): Promise> { + const pool = this.getPool(); + const ratio = await pool.getSwapRatio(); + console.debug('ratio', ratio.toString()); + const inputAmountWei = toWeiFromTokenAmount(input); + const amount0 = inputAmountWei.multipliedBy(ratio).decimalPlaces(0, BigNumber.ROUND_FLOOR); + const amount1 = inputAmountWei.minus(amount0); + const swapAmounts = [amount0, amount1]; + + return this.poolTokens.map((token, i) => ({ + from: fromWeiToTokenAmount(swapAmounts[i], input.token), + to: token, + })); + } + + protected async quoteAddLiquidity( + inputs: TokenAmount[] + ): Promise<{ liquidity: TokenAmount; usedInput: TokenAmount[]; unusedInput: TokenAmount[] }> { + const pool = this.getPool(); + const inputAmountsWei = inputs.map(input => toWeiFromTokenAmount(input)); + const result = await pool.quoteAddLiquidity(inputAmountsWei); + + return { + liquidity: fromWeiToTokenAmount(result.liquidity, this.depositToken), + usedInput: inputs.map((input, i) => fromWeiToTokenAmount(result.usedInput[i], input.token)), + unusedInput: inputs.map((input, i) => + fromWeiToTokenAmount(result.unusedInput[i], input.token) + ), + }; + } + + protected async fetchDepositQuoteAggregator( + input: InputTokenAmount, + option: BalancerPoolDepositOption + ): Promise { + const { zap, swapAggregator, getState } = this.helpers; + const state = getState(); + + // Token allowances + const allowances = isTokenErc20(input.token) + ? [ + { + token: input.token, + amount: input.amount, + spenderAddress: zap.manager, + }, + ] + : []; + + // How much input to swap to each lp token + const swapInAmounts = await this.getSwapAmounts(input); + + console.debug( + 'fetchDepositQuoteAggregator::swapInAmounts', + bigNumberToStringDeep(swapInAmounts) + ); + + // Swap quotes + const quoteRequestsPerLpToken: (QuoteRequest | undefined)[] = swapInAmounts.map( + ({ from, to }) => + isTokenEqual(from.token, to) + ? undefined + : { + vaultId: this.vault.id, + fromToken: from.token, + fromAmount: from.amount, + toToken: to, + } + ); + + const quotesPerLpToken = await Promise.all( + quoteRequestsPerLpToken.map(async quoteRequest => { + if (!quoteRequest) { + return undefined; + } + + return await swapAggregator.fetchQuotes(quoteRequest, state); + }) + ); + + const quotePerLpToken = quotesPerLpToken.map((quotes, i) => { + if (quotes === undefined) { + const quoteRequest = quoteRequestsPerLpToken[i]; + if (quoteRequest === undefined) { + return undefined; + } else { + throw new Error( + `No quotes found for ${quoteRequest.fromToken.symbol} -> ${quoteRequest.toToken.symbol}` + ); + } + } + + // fetchQuotes is already sorted by toAmount + return first(quotes); + }); + + // Build LP + const lpTokenAmounts = quotePerLpToken.map((quote, i) => { + if (quote) { + return { token: quote.toToken, amount: quote.toAmount }; + } + return swapInAmounts[i].from; + }); + + console.debug( + 'fetchDepositQuoteAggregator::lpTokenAmounts', + bigNumberToStringDeep(lpTokenAmounts) + ); + + const { liquidity, unusedInput } = await this.quoteAddLiquidity(lpTokenAmounts); + + // Build quote inputs + const inputs = [input]; + + // Build quote steps + const steps: ZapQuoteStep[] = []; + + quotePerLpToken.forEach(quote => { + if (quote) { + steps.push({ + type: 'swap', + fromToken: quote.fromToken, + fromAmount: quote.fromAmount, + toToken: quote.toToken, + toAmount: quote.toAmount, + via: 'aggregator', + providerId: quote.providerId, + fee: quote.fee, + quote, + }); + } + }); + + steps.push({ + type: 'build', + inputs: lpTokenAmounts, + outputToken: liquidity.token, + outputAmount: liquidity.amount, + }); + + steps.push({ + type: 'deposit', + inputs: [liquidity], + }); + + // Build quote outputs + const outputs: TokenAmount[] = [liquidity]; + + // Build dust outputs + const returned: TokenAmount[] = unusedInput.filter(input => !input.amount.isZero()); + + if (returned.length > 0) { + steps.push({ + type: 'unused', + outputs: returned, + }); + } + + // Build quote + return { + id: createQuoteId(option.id), + strategyId: this.id, + priceImpact: calculatePriceImpact(inputs, outputs, returned, state), // includes the zap fee + option, + inputs, + outputs, + returned, + allowances, + steps, + fee: highestFeeOrZero(steps), + }; + } + + protected async fetchZapSwap( + quoteStep: ZapQuoteStepSwap, + zapHelpers: ZapHelpers, + insertBalance: boolean + ): Promise { + if (isZapQuoteStepSwapAggregator(quoteStep)) { + return this.fetchZapSwapAggregator(quoteStep, zapHelpers, insertBalance); + } else { + throw new Error('Unknown zap quote swap step type'); + } + } + + protected async fetchZapSwapAggregator( + quoteStep: ZapQuoteStepSwapAggregator, + zapHelpers: ZapHelpers, + insertBalance: boolean + ): Promise { + const { swapAggregator, zap } = this.helpers; + const { slippage, state } = zapHelpers; + + return await fetchZapAggregatorSwap( + { + quote: quoteStep.quote, + inputs: [{ token: quoteStep.fromToken, amount: quoteStep.fromAmount }], + outputs: [{ token: quoteStep.toToken, amount: quoteStep.toAmount }], + maxSlippage: slippage, + zapRouter: zap.router, + providerId: quoteStep.providerId, + insertBalance, + }, + swapAggregator, + state + ); + } + + protected async fetchZapBuild( + quoteStep: ZapQuoteStepBuild, + minInputs: TokenAmount[] + ): Promise { + const quote = await this.quoteAddLiquidity(minInputs); + const pool = this.getPool(); + + return { + inputs: quote.usedInput, + outputs: [quote.liquidity], + minOutputs: [quote.liquidity], // TODO gyro can't slip, it just fails but maybe other pool types can + returned: quote.unusedInput, + zaps: [ + await pool.getAddLiquidityZap( + quote.usedInput.map(input => toWeiFromTokenAmount(input)), + toWeiFromTokenAmount(quote.liquidity), + this.helpers.zap.router, + true + ), + ], + }; + } + + public async fetchDepositStep( + quote: BalancerPoolDepositQuote, + t: TFunction> + ): Promise { + const zapAction: BeefyThunk = async (dispatch, getState, extraArgument) => { + const state = getState(); + const slippage = selectTransactSlippage(state); + const zapHelpers: ZapHelpers = { + slippage, + state, + }; + const steps: ZapStep[] = []; + const minBalances = new Balances(quote.inputs); + const swapQuotes = quote.steps.filter(isZapQuoteStepSwap); + const buildQuote = quote.steps.find(isZapQuoteStepBuild); + + if (!buildQuote) { + throw new Error('BalancerPoolStrategy: No build step in quote'); + } + + // since there are two tokens, there must be at least 1 swap + if (swapQuotes.length < 1) { + throw new Error('BalancerPoolStrategy: Not enough swaps'); + } + + // Swaps + if (swapQuotes.length) { + if (swapQuotes.length > 2) { + throw new Error('BalancerPoolStrategy: Too many swaps'); + } + + const insertBalance = allTokensAreDistinct( + swapQuotes + .map(quoteStep => quoteStep.fromToken) + .concat(buildQuote.inputs.map(({ token }) => token)) + ); + const swapZaps = await Promise.all( + swapQuotes.map(quoteStep => this.fetchZapSwap(quoteStep, zapHelpers, insertBalance)) + ); + swapZaps.forEach(swap => { + // add step to order + swap.zaps.forEach(step => steps.push(step)); + // track the minimum balances for use in further steps + minBalances.subtractMany(swap.inputs); + minBalances.addMany(swap.minOutputs); + }); + } + + // Build LP + const buildZap = await this.fetchZapBuild( + buildQuote, + buildQuote.inputs.map(({ token }) => ({ + token, + amount: minBalances.get(token), // we have to pass min expected in case swaps slipped + })) + ); + console.debug('fetchDepositStep::buildZap', bigNumberToStringDeep(buildZap)); + buildZap.zaps.forEach(step => steps.push(step)); + minBalances.subtractMany(buildZap.inputs); + minBalances.addMany(buildZap.minOutputs); + + // Deposit in vault + const vaultDeposit = await this.vaultType.fetchZapDeposit({ + inputs: [ + { + token: buildQuote.outputToken, + amount: minBalances.get(buildQuote.outputToken), // min expected in case add liquidity slipped + max: true, // but we call depositAll + }, + ], + }); + console.debug('fetchDepositStep::vaultDeposit', vaultDeposit); + steps.push(vaultDeposit.zap); + + // Build order + const inputs: OrderInput[] = quote.inputs.map(input => ({ + token: getTokenAddress(input.token), + amount: toWeiString(input.amount, input.token.decimals), + })); + + const requiredOutputs: OrderOutput[] = vaultDeposit.outputs.map(output => ({ + token: getTokenAddress(output.token), + minOutputAmount: toWeiString( + slipBy(output.amount, slippage, output.token.decimals), + output.token.decimals + ), + })); + + // We need to list all inputs, and mid-route outputs, as outputs so dust gets returned + const dustOutputs: OrderOutput[] = pickTokens( + quote.outputs, + quote.inputs, + quote.returned + ).map(token => ({ + token: getTokenAddress(token), + minOutputAmount: '0', + })); + + swapQuotes.forEach(quoteStep => { + dustOutputs.push({ + token: getTokenAddress(quoteStep.fromToken), + minOutputAmount: '0', + }); + dustOutputs.push({ + token: getTokenAddress(quoteStep.toToken), + minOutputAmount: '0', + }); + }); + dustOutputs.push({ + token: getTokenAddress(buildQuote.outputToken), + minOutputAmount: '0', + }); + + // @dev uniqBy: first occurrence of each element is kept. + const outputs = uniqBy(requiredOutputs.concat(dustOutputs), output => output.token); + + // Perform TX + const zapRequest: UserlessZapRequest = { + order: { + inputs, + outputs, + relay: NO_RELAY, + }, + steps, + }; + + const expectedTokens = vaultDeposit.outputs.map(output => output.token); + const walletAction = walletActions.zapExecuteOrder( + quote.option.vaultId, + zapRequest, + expectedTokens + ); + + return walletAction(dispatch, getState, extraArgument); + }; + + return { + step: 'zap-in', + message: t('Vault-TxnConfirm', { type: t('Deposit-noun') }), + action: zapAction, + pending: false, + extraInfo: { zap: true, vaultId: quote.option.vaultId }, + }; + } + + async fetchWithdrawOptions(): Promise { + const inputs = [this.vaultType.depositToken]; + + // what tokens can we can zap via pool with + // const poolTokens = includeNativeAndWrapped(this.poolTokens.map(t => t.token), this.wnative, this.native).map( + // token => ({ + // token, + // via: 'pool' as const, + // }) + // ); + + // what tokens we can zap via swap aggregator with + const supportedAggregatorTokens = await this.aggregatorTokenSupport(); + const aggregatorTokens = supportedAggregatorTokens + .filter(token => !isTokenEqual(token, this.vaultType.depositToken)) + .map(token => ({ token, via: 'aggregator' as const })); + + const zapTokens = aggregatorTokens; //[...poolTokens, ...aggregatorTokens]; + + const breakSelectionId = createSelectionId(this.vault.chainId, this.poolTokens); + const breakOption: BalancerPoolWithdrawOption = { + id: createOptionId(this.id, this.vault.id, breakSelectionId), + vaultId: this.vault.id, + chainId: this.vault.chainId, + selectionId: breakSelectionId, + selectionOrder: 2, + inputs, + wantedOutputs: this.poolTokens, + mode: TransactMode.Withdraw, + strategyId, + via: 'break-only', + }; + + return [breakOption].concat( + zapTokens.map(({ token, via }) => { + const outputs = [token]; + const selectionId = createSelectionId(this.vault.chainId, outputs); + + return { + id: createOptionId(this.id, this.vault.id, selectionId, via), + vaultId: this.vault.id, + chainId: this.vault.chainId, + selectionId, + selectionOrder: 3, + inputs, + wantedOutputs: outputs, + mode: TransactMode.Withdraw, + strategyId, + via, + }; + }) + ); + } + + protected async quoteRemoveLiquidity( + input: TokenAmount + ): Promise<{ liquidity: TokenAmount; outputs: TokenAmount[] }> { + const pool = this.getPool(); + const inputAmountWei = toWeiFromTokenAmount(input); + const result = await pool.quoteRemoveLiquidity(inputAmountWei); + + return { + liquidity: fromWeiToTokenAmount(result.liquidity, this.depositToken), + outputs: result.outputs.map((amount, i) => fromWeiToTokenAmount(amount, this.poolTokens[i])), + }; + } + + protected async fetchWithdrawQuoteAggregator( + option: BalancerPoolWithdrawOption, + baseQuote: Pick + ): Promise> { + const { wantedOutputs } = option; + const { swapAggregator, getState } = this.helpers; + const state = getState(); + const wantedOutput = onlyOneToken(wantedOutputs); + const needsSwap = baseQuote.outputs.map( + tokenAmount => !isTokenEqual(wantedOutput, tokenAmount.token) + ); + const additionalSteps: BalancerPoolWithdrawQuote['steps'] = []; + + const swapQuotes = await Promise.all( + baseQuote.outputs.map(async (input, i) => { + if (needsSwap[i]) { + const quotes = await swapAggregator.fetchQuotes( + { + fromAmount: input.amount, + fromToken: input.token, + toToken: wantedOutput, + vaultId: option.vaultId, + }, + state + ); + + if (!quotes || !quotes.length) { + throw new Error(`No quotes found for ${input.token.symbol} -> ${wantedOutput.symbol}`); + } + + return first(quotes); // already sorted by toAmount + } + + return undefined; + }) + ); + + let outputTotal = new BigNumber(0); + baseQuote.outputs.forEach((input, i) => { + if (needsSwap[i]) { + const swapQuote = swapQuotes[i]; + if (!swapQuote) { + throw new Error('No swap quote found'); + } + + outputTotal = outputTotal.plus(swapQuote.toAmount); + + additionalSteps.push({ + type: 'swap', + fromToken: input.token, + fromAmount: input.amount, + toToken: swapQuote.toToken, + toAmount: swapQuote.toAmount, + via: 'aggregator', + providerId: swapQuote.providerId, + fee: swapQuote.fee, + quote: swapQuote, + }); + } else { + outputTotal = outputTotal.plus(input.amount); + } + }); + + return { + ...baseQuote, + outputs: [{ token: wantedOutput, amount: outputTotal }], + steps: baseQuote.steps.concat(additionalSteps), + }; + } + + public async fetchWithdrawQuote( + inputs: InputTokenAmount[], + option: BalancerPoolWithdrawOption + ): Promise { + const input = onlyOneInput(inputs); + if (input.amount.lte(BIG_ZERO)) { + throw new Error('Quote called with 0 input amount'); + } + const { zap, getState } = this.helpers; + + // Common: Withdraw from vault + const state = getState(); + const { withdrawnAmountAfterFeeWei, withdrawnToken, shareToken, sharesToWithdrawWei } = + getVaultWithdrawnFromState(input, this.vault, state); + const liquidityWithdrawn = fromWeiToTokenAmount(withdrawnAmountAfterFeeWei, withdrawnToken); + + // Common: Token Allowances + const allowances = [ + { + token: shareToken, + amount: fromWei(sharesToWithdrawWei, shareToken.decimals), + spenderAddress: zap.manager, + }, + ]; + + // Common: Break the LP + const removeLiquidityQuote = await this.quoteRemoveLiquidity(liquidityWithdrawn); + const baseQuote: Pick = { + steps: [ + { + type: 'withdraw', + outputs: [liquidityWithdrawn], + }, + { + type: 'split', + inputToken: liquidityWithdrawn.token, + inputAmount: liquidityWithdrawn.amount, + outputs: removeLiquidityQuote.outputs, + }, + ], + outputs: removeLiquidityQuote.outputs, + returned: [], + }; + + let quote: Pick; + switch (option.via) { + case 'aggregator': { + quote = await this.fetchWithdrawQuoteAggregator(option, baseQuote); + break; + } + case 'break-only': { + quote = baseQuote; + break; + } + default: { + throw new Error(`Unknown zap withdraw option via ${option.via}`); + } + } + + return { + id: createQuoteId(option.id), + strategyId: this.id, + priceImpact: calculatePriceImpact(inputs, quote.outputs, quote.returned, state), + option, + inputs, + allowances, + fee: highestFeeOrZero(quote.steps), + ...quote, + }; + } + + protected async fetchZapSplit( + quoteStep: ZapQuoteStepSplit, + inputs: TokenAmount[], + zapHelpers: ZapHelpers + ): Promise { + const { slippage } = zapHelpers; + const input = onlyOneTokenAmount(inputs); + const { outputs } = await this.quoteRemoveLiquidity(input); + const minOutputs = outputs.map(output => slipTokenAmountBy(output, slippage)); + const pool = this.getPool(); + + return { + inputs, + outputs, + minOutputs, + returned: [], + zaps: [ + await pool.getRemoveLiquidityZap( + toWeiFromTokenAmount(input), + minOutputs.map(minOutput => toWeiFromTokenAmount(minOutput)), + this.helpers.zap.router, + true + ), + ], + }; + } + + public async fetchWithdrawStep( + quote: BalancerPoolWithdrawQuote, + t: TFunction> + ): Promise { + const zapAction: BeefyThunk = async (dispatch, getState, extraArgument) => { + const state = getState(); + const slippage = selectTransactSlippage(state); + const zapHelpers: ZapHelpers = { + slippage, + state, + }; + const withdrawQuote = quote.steps.find(isZapQuoteStepWithdraw); + const swapQuotes = quote.steps.filter(isZapQuoteStepSwap); + const splitQuote = quote.steps.find(isZapQuoteStepSplit); + + if (!withdrawQuote || !splitQuote) { + throw new Error('Withdraw quote missing withdraw or split step'); + } + + // Step 1. Withdraw from vault + const vaultWithdraw = await this.vaultType.fetchZapWithdraw({ + inputs: quote.inputs, + }); + if (vaultWithdraw.outputs.length !== 1) { + throw new Error('Withdraw output count mismatch'); + } + + const withdrawOutput = onlyOneTokenAmount(vaultWithdraw.outputs); + if (!isTokenEqual(withdrawOutput.token, splitQuote.inputToken)) { + throw new Error('Withdraw output token mismatch'); + } + + if (withdrawOutput.amount.lt(withdrawQuote.toAmount)) { + throw new Error('Withdraw output amount mismatch'); + } + + const steps: ZapStep[] = [vaultWithdraw.zap]; + + // Step 2. Split lp + const splitZap = await this.fetchZapSplit(splitQuote, [withdrawOutput], zapHelpers); + splitZap.zaps.forEach(step => steps.push(step)); + + // Step 3. Swaps + // 0 swaps is valid when we break only + if (swapQuotes.length > 0) { + if (swapQuotes.length > splitZap.minOutputs.length) { + throw new Error('More swap quotes than expected outputs'); + } + + const insertBalance = allTokensAreDistinct( + swapQuotes.map(quoteStep => quoteStep.fromToken) + ); + // On withdraw zap the last swap can use 100% of balance even if token was used in previous swaps (since there are no further steps) + const lastSwapIndex = swapQuotes.length - 1; + + const swapZaps = await Promise.all( + swapQuotes.map((quoteStep, i) => { + const input = splitZap.minOutputs.find(o => isTokenEqual(o.token, quoteStep.fromToken)); + if (!input) { + throw new Error('Swap input not found in split outputs'); + } + return this.fetchZapSwap(quoteStep, zapHelpers, insertBalance || lastSwapIndex === i); + }) + ); + swapZaps.forEach(swap => swap.zaps.forEach(step => steps.push(step))); + } + + // Build order + const inputs: OrderInput[] = vaultWithdraw.inputs.map(input => ({ + token: getTokenAddress(input.token), + amount: toWeiString(input.amount, input.token.decimals), + })); + + const requiredOutputs: OrderOutput[] = quote.outputs.map(output => ({ + token: getTokenAddress(output.token), + minOutputAmount: toWeiString( + slipBy(output.amount, slippage, output.token.decimals), + output.token.decimals + ), + })); + + // We need to list all inputs, and mid-route outputs, as outputs so dust gets returned + const dustOutputs: OrderOutput[] = pickTokens( + vaultWithdraw.inputs, + quote.outputs, + quote.inputs, + quote.returned, + splitQuote.outputs + ).map(token => ({ + token: getTokenAddress(token), + minOutputAmount: '0', + })); + + swapQuotes.forEach(quoteStep => { + dustOutputs.push({ + token: getTokenAddress(quoteStep.fromToken), + minOutputAmount: '0', + }); + dustOutputs.push({ + token: getTokenAddress(quoteStep.toToken), + minOutputAmount: '0', + }); + }); + + // @dev uniqBy: first occurrence of each element is kept -> required outputs are kept + const outputs = uniqBy(requiredOutputs.concat(dustOutputs), output => output.token); + + // Perform TX + const zapRequest: UserlessZapRequest = { + order: { + inputs, + outputs, + relay: NO_RELAY, + }, + steps, + }; + + const expectedTokens = quote.outputs.map(output => output.token); + const walletAction = walletActions.zapExecuteOrder( + quote.option.vaultId, + zapRequest, + expectedTokens + ); + + return walletAction(dispatch, getState, extraArgument); + }; + + return { + step: 'zap-out', + message: t('Vault-TxnConfirm', { type: t('Withdraw-noun') }), + action: zapAction, + pending: false, + extraInfo: { zap: true, vaultId: quote.option.vaultId }, + }; + } + + protected async aggregatorTokenSupport() { + const { swapAggregator, getState } = this.helpers; + const state = getState(); + const tokenSupport = await swapAggregator.fetchTokenSupport( + this.poolTokens, + this.vault.id, + this.vault.chainId, + state, + this.options.swap + ); + + return tokenSupport.any.filter(aggToken => { + return this.poolTokens.every( + (poolToken, i) => + isTokenEqual(aggToken, poolToken) || + tokenSupport.tokens[i].some(supportedToken => isTokenEqual(supportedToken, aggToken)) + ); + }); + } +} + +export const BalancerPoolStrategy = + BalancerPoolStrategyImpl satisfies IZapStrategyStatic; diff --git a/src/features/data/apis/transact/strategies/balancer/BalancerStrategy.ts b/src/features/data/apis/transact/strategies/balancer/BalancerSwapStrategy.ts similarity index 90% rename from src/features/data/apis/transact/strategies/balancer/BalancerStrategy.ts rename to src/features/data/apis/transact/strategies/balancer/BalancerSwapStrategy.ts index ea8cbd263..1f85697eb 100644 --- a/src/features/data/apis/transact/strategies/balancer/BalancerStrategy.ts +++ b/src/features/data/apis/transact/strategies/balancer/BalancerSwapStrategy.ts @@ -9,10 +9,10 @@ import { } from '../../../../entities/token'; import type { Step } from '../../../../reducers/wallet/stepper'; import { - type BalancerDepositOption, - type BalancerDepositQuote, - type BalancerWithdrawOption, - type BalancerWithdrawQuote, + type BalancerSwapDepositOption, + type BalancerSwapDepositQuote, + type BalancerSwapWithdrawOption, + type BalancerSwapWithdrawQuote, type InputTokenAmount, isZapQuoteStepBuild, isZapQuoteStepSplit, @@ -78,11 +78,12 @@ import { getVaultWithdrawnFromState } from '../../helpers/vault'; import { isFulfilledResult } from '../../../../../../helpers/promises'; import { isDefined } from '../../../../utils/array-utils'; import { isStandardVaultType, type IStandardVaultType } from '../../vaults/IVaultType'; -import type { BalancerStrategyConfig } from '../strategy-configs'; -import { BalancerComposableStablePool } from '../../../amm/balancer/BalancerComposableStablePool'; +import type { BalancerSwapStrategyConfig } from '../strategy-configs'; +import { ComposableStablePool } from '../../../amm/balancer/composable-stable/ComposableStablePool'; import { type AmmEntityBalancer, isBalancerAmm } from '../../../../entities/zap'; import { selectAmmById } from '../../../../selectors/zap'; import { createFactory } from '../../../../utils/factory-utils'; +import type { PoolConfig, VaultConfig } from '../../../amm/balancer/vault/types'; type ZapHelpers = { chain: ChainEntity; @@ -107,10 +108,13 @@ type WithdrawLiquidity = DepositLiquidity & { split: TokenAmount; }; -const strategyId = 'balancer' as const; +const strategyId = 'balancer-swap' as const; type StrategyId = typeof strategyId; -class BalancerStrategyImpl implements IZapStrategy { +/** + * Balancer: swap() to deposit/withdraw liquidity + */ +class BalancerSwapStrategyImpl implements IZapStrategy { public static readonly id = strategyId; public readonly id = strategyId; @@ -119,12 +123,15 @@ class BalancerStrategyImpl implements IZapStrategy { protected readonly possibleTokens: BalancerTokenOption[]; protected readonly chain: ChainEntity; protected readonly depositToken: TokenEntity; - protected readonly poolAddress: string; + protected readonly poolTokens: TokenEntity[]; protected readonly vault: VaultStandard; protected readonly vaultType: IStandardVaultType; protected readonly amm: AmmEntityBalancer; - constructor(protected options: BalancerStrategyConfig, protected helpers: ZapTransactHelpers) { + constructor( + protected options: BalancerSwapStrategyConfig, + protected helpers: ZapTransactHelpers + ) { const { vault, vaultType, getState } = this.helpers; if (!isStandardVault(vault)) { @@ -156,34 +163,44 @@ class BalancerStrategyImpl implements IZapStrategy { this.wnative = selectChainWrappedNativeToken(state, vault.chainId); this.depositToken = vaultType.depositToken; this.chain = selectChainById(state, vault.chainId); - this.possibleTokens = this.selectAvailableTokens(state, this.chain.id, this.options.tokens); + this.poolTokens = this.selectPoolTokens(state, this.chain.id, this.options.tokens); + this.possibleTokens = this.selectAvailableTokens(state, this.poolTokens); - if (!this.possibleTokens.length) { - throw new Error( - `Vault ${ - vault.id - }: No tokens configured are available in addressbook, wanted one of ${this.options.tokens.join( - ', ' - )}` - ); + if (this.options.poolType === 'composable-stable') { + if (!this.possibleTokens.length) { + throw new Error( + `Vault ${vault.id}: At least one token must be in address book and priced for ${this.options.poolType}` + ); + } + } else { + throw new Error(`Unsupported balancer pool type ${this.options.poolType}`); } } + protected selectPoolTokens( + state: BeefyState, + chainId: ChainEntity['id'], + tokenAddresses: string[] + ): TokenEntity[] { + const tokens = tokenAddresses + .map(address => selectTokenByAddressOrUndefined(state, chainId, address)) + .filter(isDefined); + if (tokens.length !== tokenAddresses.length) { + // We need decimals for each token + throw new Error('Not all tokens are in state'); + } + return tokens; + } + /** - * Tokens are available so long as they are in the address book + * Tokens are available so long as they are in the address book and have a price */ protected selectAvailableTokens( state: BeefyState, - chainId: ChainEntity['id'], - tokenAddresses: string[] + poolTokens: TokenEntity[] ): BalancerTokenOption[] { - return tokenAddresses - .map((address, i) => { - const token = selectTokenByAddressOrUndefined(state, chainId, address); - if (!token) { - return undefined; - } - + return poolTokens + .map((token, i) => { const price = selectTokenPriceByTokenOracleId(state, token.oracleId); if (!price || price.lte(BIG_ZERO)) { return undefined; @@ -199,10 +216,10 @@ class BalancerStrategyImpl implements IZapStrategy { .filter(t => !isTokenEqual(t.token, this.depositToken)); } - public async fetchDepositOptions(): Promise { + public async fetchDepositOptions(): Promise { const outputs = [this.vaultType.depositToken]; - const baseOptions: BalancerDepositOption[] = this.possibleTokens.map(depositToken => { + const baseOptions: BalancerSwapDepositOption[] = this.possibleTokens.map(depositToken => { const inputs = [depositToken.token]; const selectionId = createSelectionId(this.vault.chainId, inputs); @@ -215,7 +232,7 @@ class BalancerStrategyImpl implements IZapStrategy { inputs, wantedOutputs: outputs, mode: TransactMode.Deposit, - strategyId: 'balancer', + strategyId, via: 'direct', viaToken: depositToken, }; @@ -224,7 +241,7 @@ class BalancerStrategyImpl implements IZapStrategy { const { any: allAggregatorTokens, map: tokenToDepositTokens } = await this.aggregatorTokenSupport(); - const aggregatorOptions: BalancerDepositOption[] = allAggregatorTokens + const aggregatorOptions: BalancerSwapDepositOption[] = allAggregatorTokens .filter(token => tokenToDepositTokens[token.address].length > 0) .map(token => { const inputs = [token]; @@ -245,7 +262,7 @@ class BalancerStrategyImpl implements IZapStrategy { inputs, wantedOutputs: outputs, mode: TransactMode.Deposit, - strategyId: 'balancer', + strategyId, via: 'aggregator', viaTokens: possible, }; @@ -255,15 +272,19 @@ class BalancerStrategyImpl implements IZapStrategy { } protected getPool = createFactory(() => { + const vault: VaultConfig = { + vaultAddress: this.amm.vaultAddress, + queryAddress: this.amm.queryAddress, + }; + const pool: PoolConfig = { + poolAddress: this.depositToken.address, + poolId: this.options.poolId, + tokens: this.poolTokens, + }; + switch (this.options.poolType) { case 'composable-stable': { - return new BalancerComposableStablePool({ - chain: this.chain, - vaultAddress: this.amm.vaultAddress, - poolAddress: this.depositToken.address, - poolId: this.options.poolId, - tokens: this.options.tokens, - }); + return new ComposableStablePool(this.chain, vault, pool); } default: { throw new Error(`Unsupported balancer pool type ${this.options.poolType}`); @@ -354,7 +375,7 @@ class BalancerStrategyImpl implements IZapStrategy { protected async getDepositLiquidity( state: BeefyState, input: InputTokenAmount, - option: BalancerDepositOption + option: BalancerSwapDepositOption ): Promise { if (option.via === 'direct') { return this.getDepositLiquidityDirect(input, option.viaToken); @@ -364,8 +385,8 @@ class BalancerStrategyImpl implements IZapStrategy { public async fetchDepositQuote( inputs: InputTokenAmount[], - option: BalancerDepositOption - ): Promise { + option: BalancerSwapDepositOption + ): Promise { const { zap, getState } = this.helpers; const state = getState(); const input = onlyOneInput(inputs); @@ -428,7 +449,7 @@ class BalancerStrategyImpl implements IZapStrategy { // Build quote return { id: createQuoteId(option.id), - strategyId: 'balancer', + strategyId, priceImpact: calculatePriceImpact(inputs, outputs, returned, state), // includes the zap fee option, inputs, @@ -508,7 +529,7 @@ class BalancerStrategyImpl implements IZapStrategy { } public async fetchDepositStep( - quote: BalancerDepositQuote, + quote: BalancerSwapDepositQuote, t: TFunction> ): Promise { const zapAction: BeefyThunk = async (dispatch, getState, extraArgument) => { @@ -573,7 +594,7 @@ class BalancerStrategyImpl implements IZapStrategy { }, ], }); - console.log('fetchDepositStep::vaultDeposit', vaultDeposit); + console.debug('fetchDepositStep::vaultDeposit', vaultDeposit); steps.push(vaultDeposit.zap); // Build order @@ -647,10 +668,10 @@ class BalancerStrategyImpl implements IZapStrategy { }; } - async fetchWithdrawOptions(): Promise { + async fetchWithdrawOptions(): Promise { const inputs = [this.vaultType.depositToken]; - const baseOptions: BalancerWithdrawOption[] = this.possibleTokens.map(depositToken => { + const baseOptions: BalancerSwapWithdrawOption[] = this.possibleTokens.map(depositToken => { const outputs = [depositToken.token]; const selectionId = createSelectionId(this.vault.chainId, outputs); @@ -663,7 +684,7 @@ class BalancerStrategyImpl implements IZapStrategy { inputs, wantedOutputs: outputs, mode: TransactMode.Withdraw, - strategyId: 'balancer', + strategyId, via: 'direct', viaToken: depositToken, }; @@ -672,7 +693,7 @@ class BalancerStrategyImpl implements IZapStrategy { const { any: allAggregatorTokens, map: tokenToDepositTokens } = await this.aggregatorTokenSupport(); - const aggregatorOptions: BalancerWithdrawOption[] = allAggregatorTokens + const aggregatorOptions: BalancerSwapWithdrawOption[] = allAggregatorTokens .filter(token => tokenToDepositTokens[token.address].length > 0) .map(token => { const outputs = [token]; @@ -693,7 +714,7 @@ class BalancerStrategyImpl implements IZapStrategy { inputs, wantedOutputs: outputs, mode: TransactMode.Withdraw, - strategyId: 'balancer', + strategyId, via: 'aggregator', viaTokens: possible, }; @@ -721,7 +742,7 @@ class BalancerStrategyImpl implements IZapStrategy { ): Promise { if (!isTokenEqual(wanted, withdrawVia.token)) { throw new Error( - `Curve strategy: Direct withdraw called with wanted token ${input.token.symbol} but expected ${withdrawVia.token.symbol}` + `Balancer strategy: Direct withdraw called with wanted token ${input.token.symbol} but expected ${withdrawVia.token.symbol}` ); } @@ -788,7 +809,7 @@ class BalancerStrategyImpl implements IZapStrategy { state: BeefyState, input: TokenAmount, wanted: TokenEntity, - option: BalancerWithdrawOption + option: BalancerSwapWithdrawOption ): Promise { if (option.via === 'direct') { return this.getWithdrawLiquidityDirect(input, wanted, option.viaToken); @@ -798,8 +819,8 @@ class BalancerStrategyImpl implements IZapStrategy { public async fetchWithdrawQuote( inputs: InputTokenAmount[], - option: BalancerWithdrawOption - ): Promise { + option: BalancerSwapWithdrawOption + ): Promise { const input = onlyOneInput(inputs); if (input.amount.lte(BIG_ZERO)) { throw new Error('Quote called with 0 input amount'); @@ -887,7 +908,7 @@ class BalancerStrategyImpl implements IZapStrategy { return { id: createQuoteId(option.id), - strategyId: 'balancer', + strategyId, priceImpact: calculatePriceImpact(inputs, outputs, returned, state), option, inputs, @@ -932,7 +953,7 @@ class BalancerStrategyImpl implements IZapStrategy { } public async fetchWithdrawStep( - quote: BalancerWithdrawQuote, + quote: BalancerSwapWithdrawQuote, t: TFunction> ): Promise { const zapAction: BeefyThunk = async (dispatch, getState, extraArgument) => { @@ -1109,4 +1130,5 @@ class BalancerStrategyImpl implements IZapStrategy { } } -export const BalancerStrategy = BalancerStrategyImpl satisfies IZapStrategyStatic; +export const BalancerSwapStrategy = + BalancerSwapStrategyImpl satisfies IZapStrategyStatic; diff --git a/src/features/data/apis/transact/strategies/balancer/types.ts b/src/features/data/apis/transact/strategies/balancer/types.ts index 5496d1b56..a3086e0e7 100644 --- a/src/features/data/apis/transact/strategies/balancer/types.ts +++ b/src/features/data/apis/transact/strategies/balancer/types.ts @@ -1,112 +1,5 @@ import type { TokenEntity } from '../../../../entities/token'; -import BigNumber from 'bignumber.js'; - -export type CurveMethodTypes = - | 'fixed' - | 'fixed-deposit-int128' - | 'fixed-deposit-uint256' - | 'fixed-deposit-underlying' - | 'dynamic-deposit' - | 'pool-fixed' - | 'pool-fixed-deposit' - | 'pool-dynamic-deposit'; - -type CurveMethodSignatures = { - depositQuote: string; - deposit: string; - withdrawQuote: string; - withdraw: string; -}; - -const curveMethodTypeToSignatures = { - fixed: { - depositQuote: 'calc_token_amount:fixed_amounts', - deposit: 'add_liquidity:fixed_amounts/min_amount', - withdrawQuote: 'calc_withdraw_one_coin:amount/uint256_index', - withdraw: 'remove_liquidity_one_coin:amount/uint256_index/min_amount', - }, - 'fixed-deposit-int128': { - depositQuote: 'calc_token_amount:fixed_amounts/is_deposit', - deposit: 'add_liquidity:fixed_amounts/min_amount', - withdrawQuote: 'calc_withdraw_one_coin:amount/int128_index', - withdraw: 'remove_liquidity_one_coin:amount/int128_index/min_amount', - }, - 'fixed-deposit-uint256': { - depositQuote: 'calc_token_amount:fixed_amounts/is_deposit', - deposit: 'add_liquidity:fixed_amounts/min_amount', - withdrawQuote: 'calc_withdraw_one_coin:amount/uint256_index', - withdraw: 'remove_liquidity_one_coin:amount/uint256_index/min_amount', - }, - 'fixed-deposit-underlying': { - depositQuote: 'calc_token_amount:fixed_amounts/is_deposit', - deposit: 'add_liquidity:fixed_amounts/min_amount/use_underlying', - withdrawQuote: 'calc_withdraw_one_coin:amount/int128_index', - withdraw: 'remove_liquidity_one_coin:amount/int128_index/min_amount/use_underlying', - }, - 'dynamic-deposit': { - depositQuote: 'calc_token_amount:dynamic_amounts/is_deposit', - deposit: 'add_liquidity:dynamic_amounts/min_amount', - withdrawQuote: 'calc_withdraw_one_coin:amount/int128_index', - withdraw: 'remove_liquidity_one_coin:amount/int128_index/min_amount', - }, - 'pool-fixed': { - depositQuote: 'calc_token_amount:pool/fixed_amounts', - deposit: 'add_liquidity:pool/fixed_amounts/min_amount', - withdrawQuote: 'calc_withdraw_one_coin:pool/amount/uint256_index', - withdraw: 'remove_liquidity_one_coin:pool/amount/uint256_index/min_amount', - }, - 'pool-fixed-deposit': { - depositQuote: 'calc_token_amount:pool/fixed_amounts/is_deposit', - deposit: 'add_liquidity:pool/fixed_amounts/min_amount', - withdrawQuote: 'calc_withdraw_one_coin:pool/amount/int128_index', - withdraw: 'remove_liquidity_one_coin:pool/amount/int128_index/min_amount', - }, - 'pool-dynamic-deposit': { - depositQuote: 'calc_token_amount:pool/dynamic_amounts/is_deposit', - deposit: 'add_liquidity:pool/dynamic_amounts/min_amount', - withdrawQuote: 'calc_withdraw_one_coin:pool/amount/int128_index', - withdraw: 'remove_liquidity_one_coin:pool/amount/int128_index/min_amount', - }, -} as const satisfies Record; - -export type CurveMethodTypeToSignaturesMap = typeof curveMethodTypeToSignatures; - -type MakeCurveMethod = { - type: T; - target: string; - coins: string[]; -}; - -type CurveMethodFixed = MakeCurveMethod<'fixed'>; -type CurveMethodFixedDepositInt128 = MakeCurveMethod<'fixed-deposit-int128'>; -type CurveMethodFixedDepositUint256 = MakeCurveMethod<'fixed-deposit-uint256'>; -type CurveMethodFixedDepositUnderlying = MakeCurveMethod<'fixed-deposit-underlying'>; -type CurveMethodDynamicDeposit = MakeCurveMethod<'dynamic-deposit'>; -type CurveMethodPoolFixed = MakeCurveMethod<'pool-fixed'>; -type CurveMethodPoolFixedDeposit = MakeCurveMethod<'pool-fixed-deposit'>; -type CurveMethodPoolDynamicDeposit = MakeCurveMethod<'pool-dynamic-deposit'>; - -export type CurveMethod = - | CurveMethodFixed - | CurveMethodFixedDepositInt128 - | CurveMethodFixedDepositUint256 - | CurveMethodFixedDepositUnderlying - | CurveMethodDynamicDeposit - | CurveMethodPoolFixed - | CurveMethodPoolFixedDeposit - | CurveMethodPoolDynamicDeposit; - -export function getMethodSignaturesForType( - type: T -): CurveMethodTypeToSignaturesMap[T] { - return curveMethodTypeToSignatures[type]; -} - -export function getCurveMethodsSignatures( - method: MakeCurveMethod -): CurveMethodTypeToSignaturesMap[T] { - return curveMethodTypeToSignatures[method.type]; -} +import type BigNumber from 'bignumber.js'; export type BalancerTokenOption = { index: number; diff --git a/src/features/data/apis/transact/strategies/index.ts b/src/features/data/apis/transact/strategies/index.ts index 37d62fa46..b90ee325d 100644 --- a/src/features/data/apis/transact/strategies/index.ts +++ b/src/features/data/apis/transact/strategies/index.ts @@ -20,7 +20,10 @@ const strategyLoadersByIdUnchecked = { (await import('./cowcentrated/CowcentratedStrategy')).CowcentratedStrategy, 'reward-pool-to-vault': async () => (await import('./RewardPoolToVaultStrategy')).RewardPoolToVaultStrategy, - balancer: async () => (await import('./balancer/BalancerStrategy')).BalancerStrategy, + 'balancer-swap': async () => + (await import('./balancer/BalancerSwapStrategy')).BalancerSwapStrategy, + 'balancer-pool': async () => + (await import('./balancer/BalancerPoolStrategy')).BalancerPoolStrategy, } as const satisfies Record Promise>; type StrategyIdToStaticPromise = typeof strategyLoadersByIdUnchecked; diff --git a/src/features/data/apis/transact/strategies/strategy-configs.ts b/src/features/data/apis/transact/strategies/strategy-configs.ts index 6d3d569ed..005858612 100644 --- a/src/features/data/apis/transact/strategies/strategy-configs.ts +++ b/src/features/data/apis/transact/strategies/strategy-configs.ts @@ -38,14 +38,22 @@ export type CurveStrategyConfig = { methods: CurveMethod[]; } & OptionalStrategySwapConfig; -export type BalancerStrategyConfig = { - strategyId: 'balancer'; +export type BalancerSwapStrategyConfig = { + strategyId: 'balancer-swap'; ammId: AmmEntityBalancer['id']; poolId: string; poolType: 'composable-stable'; tokens: string[]; } & OptionalStrategySwapConfig; +export type BalancerPoolStrategyConfig = { + strategyId: 'balancer-pool'; + ammId: AmmEntityBalancer['id']; + poolId: string; + poolType: 'gyroe'; + tokens: string[]; +} & OptionalStrategySwapConfig; + export type GammaStrategyConfig = { strategyId: 'gamma'; ammId: AmmEntityGamma['id']; @@ -84,7 +92,8 @@ export type ZapStrategyConfig = | GovComposerStrategyConfig | VaultComposerStrategyConfig | RewardPoolToVaultStrategyConfig - | BalancerStrategyConfig; + | BalancerSwapStrategyConfig + | BalancerPoolStrategyConfig; export type ZapStrategyId = ZapStrategyConfig['strategyId']; diff --git a/src/features/data/apis/transact/transact-types.ts b/src/features/data/apis/transact/transact-types.ts index ba47ebde7..427fc61e5 100644 --- a/src/features/data/apis/transact/transact-types.ts +++ b/src/features/data/apis/transact/transact-types.ts @@ -178,20 +178,30 @@ export type CurveWithdrawOption = ZapBaseWithdrawOption & { | { via: 'aggregator'; viaTokens: CurveTokenOption[] } ); -export type BalancerDepositOption = ZapBaseDepositOption & { - strategyId: 'balancer'; +export type BalancerSwapDepositOption = ZapBaseDepositOption & { + strategyId: 'balancer-swap'; } & ( | { via: 'direct'; viaToken: BalancerTokenOption } | { via: 'aggregator'; viaTokens: BalancerTokenOption[] } ); -export type BalancerWithdrawOption = ZapBaseWithdrawOption & { - strategyId: 'balancer'; +export type BalancerSwapWithdrawOption = ZapBaseWithdrawOption & { + strategyId: 'balancer-swap'; } & ( | { via: 'direct'; viaToken: BalancerTokenOption } | { via: 'aggregator'; viaTokens: BalancerTokenOption[] } ); +export type BalancerPoolDepositOption = ZapBaseDepositOption & { + strategyId: 'balancer-pool'; + via: /*'pool' | */ 'aggregator'; +}; + +export type BalancerPoolWithdrawOption = ZapBaseWithdrawOption & { + strategyId: 'balancer-pool'; + via: 'break-only' | /*'pool' | */ 'aggregator'; +}; + export type ConicDepositOption = ZapBaseDepositOption & { strategyId: 'conic'; }; @@ -254,7 +264,8 @@ export type DepositOption = | GovComposerDepositOption | VaultComposerDepositOption | RewardPoolToVaultDepositOption - | BalancerDepositOption; + | BalancerSwapDepositOption + | BalancerPoolDepositOption; export type WithdrawOption = | StandardVaultWithdrawOption @@ -270,7 +281,8 @@ export type WithdrawOption = | GovComposerWithdrawOption | VaultComposerWithdrawOption | RewardPoolToVaultWithdrawOption - | BalancerWithdrawOption; + | BalancerSwapWithdrawOption + | BalancerPoolWithdrawOption; export type TransactOption = DepositOption | WithdrawOption; @@ -478,11 +490,13 @@ export type CurveDepositQuote = BaseZapQuote & { viaToken: CurveTokenOption; }; -export type BalancerDepositQuote = BaseZapQuote & { +export type BalancerSwapDepositQuote = BaseZapQuote & { via: 'aggregator' | 'direct'; viaToken: BalancerTokenOption; }; +export type BalancerPoolDepositQuote = BaseZapQuote; + export type GammaDepositQuote = BaseZapQuote & { lpQuotes: (QuoteResponse | undefined)[]; }; @@ -507,7 +521,8 @@ export type ZapDepositQuote = | GovComposerZapDepositQuote | VaultComposerZapDepositQuote | RewardPoolToVaultDepositQuote - | BalancerDepositQuote; + | BalancerSwapDepositQuote + | BalancerPoolDepositQuote; export type DepositQuote = VaultDepositQuote | ZapDepositQuote; @@ -553,11 +568,13 @@ export type CurveWithdrawQuote = BaseZapQuote & { viaToken: CurveTokenOption; }; -export type BalancerWithdrawQuote = BaseZapQuote & { +export type BalancerSwapWithdrawQuote = BaseZapQuote & { via: 'aggregator' | 'direct'; viaToken: BalancerTokenOption; }; +export type BalancerPoolWithdrawQuote = BaseZapQuote; + export type GammaBreakWithdrawQuote = BaseZapQuote; export type GammaAggregatorWithdrawQuote = BaseZapQuote & { lpQuotes: QuoteResponse[]; @@ -599,7 +616,8 @@ export type ZapWithdrawQuote = | CowcentratedZapWithdrawQuote | GovComposerZapWithdrawQuote | VaultComposerZapWithdrawQuote - | BalancerWithdrawQuote; + | BalancerSwapWithdrawQuote + | BalancerPoolWithdrawQuote; export type WithdrawQuote = VaultWithdrawQuote | ZapWithdrawQuote; diff --git a/src/features/vault/components/Actions/Transact/TransactDebugger/BalancerZap.tsx b/src/features/vault/components/Actions/Transact/TransactDebugger/BalancerZap.tsx index 92453b539..f21909acb 100644 --- a/src/features/vault/components/Actions/Transact/TransactDebugger/BalancerZap.tsx +++ b/src/features/vault/components/Actions/Transact/TransactDebugger/BalancerZap.tsx @@ -18,7 +18,7 @@ import { selectIsAddressBookLoaded, selectIsZapLoaded, } from '../../../../../data/selectors/data-loader'; -import type { BalancerStrategyConfig } from '../../../../../data/apis/transact/strategies/strategy-configs'; +import type { BalancerSwapStrategyConfig } from '../../../../../data/apis/transact/strategies/strategy-configs'; import { isDefined } from '../../../../../data/utils/array-utils'; const useStyles = makeStyles(styles); @@ -30,7 +30,10 @@ const BalancerZap = memo(function BalancerZap({ vaultId }) { const classes = useStyles(); const vault = useAppSelector(state => selectVaultById(state, vaultId)); const zap = isStandardVault(vault) - ? vault.zaps.find((zap): zap is BalancerStrategyConfig => zap.strategyId === 'balancer') + ? vault.zaps.find( + (zap): zap is BalancerSwapStrategyConfig => + zap.strategyId === 'balancer-swap' || zap.strategyId === 'balancer-pool' + ) : undefined; return ( @@ -47,13 +50,13 @@ const NoZap = memo(function NoZap() { type ZapLoaderProps = { vault: VaultStandard; - zap: BalancerStrategyConfig; + zap: BalancerSwapStrategyConfig; }; type ZapProps = { aggregatorSupportedTokens: TokenEntity[]; vault: VaultStandard; - zap: BalancerStrategyConfig; + zap: BalancerSwapStrategyConfig; }; const ZapLoader = memo(function ZapLoader({ vault, zap }) { @@ -114,7 +117,9 @@ const Zap = memo(function Zap({ aggregatorSupportedTokens, vault, zap return (
-

{zap.poolType}

+

+ {zap.strategyId} - {zap.poolType} +

{zap.poolId}
Address
diff --git a/src/helpers/big-number.ts b/src/helpers/big-number.ts index d29222d58..84289b5ba 100644 --- a/src/helpers/big-number.ts +++ b/src/helpers/big-number.ts @@ -10,6 +10,12 @@ export const BIG_ONE = new BigNumber(1); export const BIG_MAX_UINT256 = new BigNumber( '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' ); +export const BIG_MAX_INT256 = new BigNumber( + '0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' +); +export const BIG_MIN_INT256 = new BigNumber( + '-0x8000000000000000000000000000000000000000000000000000000000000000' +); export const Q192 = new BigNumber(2).pow(192); export function compound( @@ -35,6 +41,31 @@ export function isBigNumber(value: unknown): value is BigNumber { return BigNumber.isBigNumber(value); } +function bigNumberToString(value: BigNumber, typeName: string): string { + const dp = value.decimalPlaces(); + if (dp === null || dp > 0) { + throw new Error(`${typeName} should be an integer`); + } + return value.toString(10); +} + +export function bigNumberToUint256String(value: BigNumber): string { + if (value.isNegative()) { + throw new Error('uint256 should be positive'); + } + if (value.isGreaterThan(BIG_MAX_UINT256)) { + throw new Error('uint256 should be less than 2^256'); + } + return bigNumberToString(value, 'uint256'); +} + +export function bigNumberToInt256String(value: BigNumber): string { + if (value.isGreaterThan(BIG_MAX_INT256) || value.isLessThan(BIG_MIN_INT256)) { + throw new Error('int256 should be between -2^255 and 2^255-1'); + } + return bigNumberToString(value, 'int256'); +} + export function truncateBigNumber(value: BigNumber, places: number): BigNumber { if (value.isNaN() || !value.isFinite()) { return value; diff --git a/src/helpers/tokens.ts b/src/helpers/tokens.ts index 7c713a8e1..09c49d8f3 100644 --- a/src/helpers/tokens.ts +++ b/src/helpers/tokens.ts @@ -4,3 +4,17 @@ import { uniqBy } from 'lodash-es'; export function uniqueTokens(tokens: T[]): T[] { return uniqBy(tokens, token => `${token.chainId}-${token.address}`); } + +export function checkAddressOrder(addresses: string[]) { + if (addresses.length === 0) { + throw new Error('No addresses provided'); + } + + const addressesLower = addresses.map(a => a.toLowerCase()); + const sorted = [...addressesLower].sort(); + for (let i = 0; i < sorted.length; i++) { + if (sorted[i] !== addressesLower[i]) { + throw new Error('Addresses are not in order'); + } + } +} From 7dc886ef23d26b4e70bb44e4e5825c59d638ad04 Mon Sep 17 00:00:00 2001 From: ReflectiveChimp <55021052+ReflectiveChimp@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:20:52 +0100 Subject: [PATCH 06/20] weighted - via aggregator only --- scripts/addBalancerZap.ts | 7 +- src/config/abi/BalancerWeightedPoolAbi.ts | 504 ++++++++++++++++++ src/config/vault/arbitrum.json | 56 +- src/config/vault/ethereum.json | 44 +- src/config/vault/fantom.json | 16 +- src/config/vault/gnosis.json | 69 ++- src/config/vault/optimism.json | 37 +- src/config/vault/polygon.json | 19 +- .../composable-stable/ComposableStablePool.ts | 5 +- .../data/apis/amm/balancer/gyroe/GyroEPool.ts | 206 ++----- src/features/data/apis/amm/balancer/types.ts | 29 + .../data/apis/amm/balancer/vault/Vault.ts | 90 +++- .../data/apis/amm/balancer/vault/types.ts | 16 - .../FixedPoint.ts} | 30 +- .../amm/balancer/weighted/WeightedMath.ts | 31 ++ .../amm/balancer/weighted/WeightedPool.ts | 225 ++++++++ .../data/apis/amm/balancer/weighted/types.ts | 2 + ...oolStrategy.ts => BalancerJoinStrategy.ts} | 88 +-- .../balancer/BalancerSwapStrategy.ts | 10 +- .../data/apis/transact/strategies/index.ts | 4 +- .../transact/strategies/strategy-configs.ts | 8 +- .../data/apis/transact/transact-types.ts | 4 +- .../Transact/TransactDebugger/BalancerZap.tsx | 2 +- 23 files changed, 1215 insertions(+), 287 deletions(-) create mode 100644 src/config/abi/BalancerWeightedPoolAbi.ts rename src/features/data/apis/amm/balancer/{gyroe/GyroFixedPoint.ts => weighted/FixedPoint.ts} (62%) create mode 100644 src/features/data/apis/amm/balancer/weighted/WeightedMath.ts create mode 100644 src/features/data/apis/amm/balancer/weighted/WeightedPool.ts rename src/features/data/apis/transact/strategies/balancer/{BalancerPoolStrategy.ts => BalancerJoinStrategy.ts} (93%) diff --git a/scripts/addBalancerZap.ts b/scripts/addBalancerZap.ts index 2ca5e6104..d76c12ec1 100644 --- a/scripts/addBalancerZap.ts +++ b/scripts/addBalancerZap.ts @@ -99,7 +99,7 @@ const supportedProtocolVersions = new Set([2]); const supportedPoolTypes: OptionalRecord = { COMPOSABLE_STABLE: { min: 3, max: 6 }, - // 'WEIGHTED': { min: 1, max: 4 }, + WEIGHTED: { min: 1, max: 4 }, GYROE: { min: 2, max: 2 }, // 'GYRO': { min: 2, max: 2 }, // 'META_STABLE': { min: 1, max: 1 }, @@ -569,9 +569,10 @@ export async function discoverBalancerZap(args: RunArgs) { tokens: apiPool.poolTokens.map(t => t.address), } satisfies BalancerSwapStrategyConfig; } - case 'GYROE': { + case 'GYROE': + case 'WEIGHTED': { return { - strategyId: 'balancer-pool', + strategyId: 'balancer-join', ammId: amm.id, poolId: apiPool.id, poolType: apiPool.type diff --git a/src/config/abi/BalancerWeightedPoolAbi.ts b/src/config/abi/BalancerWeightedPoolAbi.ts new file mode 100644 index 000000000..cd0f603cf --- /dev/null +++ b/src/config/abi/BalancerWeightedPoolAbi.ts @@ -0,0 +1,504 @@ +import type { Abi } from 'viem'; + +export const BalancerWeightedPoolAbi = [ + { + inputs: [ + { + components: [ + { internalType: 'string', name: 'name', type: 'string' }, + { internalType: 'string', name: 'symbol', type: 'string' }, + { internalType: 'contract IERC20[]', name: 'tokens', type: 'address[]' }, + { internalType: 'uint256[]', name: 'normalizedWeights', type: 'uint256[]' }, + { internalType: 'contract IRateProvider[]', name: 'rateProviders', type: 'address[]' }, + { internalType: 'address[]', name: 'assetManagers', type: 'address[]' }, + { internalType: 'uint256', name: 'swapFeePercentage', type: 'uint256' }, + ], + internalType: 'struct WeightedPool.NewPoolParams', + name: 'params', + type: 'tuple', + }, + { internalType: 'contract IVault', name: 'vault', type: 'address' }, + { + internalType: 'contract IProtocolFeePercentagesProvider', + name: 'protocolFeeProvider', + type: 'address', + }, + { internalType: 'uint256', name: 'pauseWindowDuration', type: 'uint256' }, + { internalType: 'uint256', name: 'bufferPeriodDuration', type: 'uint256' }, + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'string', name: 'version', type: 'string' }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'owner', type: 'address' }, + { indexed: true, internalType: 'address', name: 'spender', type: 'address' }, + { indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' }, + ], + name: 'Approval', + type: 'event', + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: 'bool', name: 'paused', type: 'bool' }], + name: 'PausedStateChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'uint256', name: 'feeType', type: 'uint256' }, + { indexed: false, internalType: 'uint256', name: 'protocolFeePercentage', type: 'uint256' }, + ], + name: 'ProtocolFeePercentageCacheUpdated', + type: 'event', + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: 'bool', name: 'enabled', type: 'bool' }], + name: 'RecoveryModeStateChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'uint256', name: 'swapFeePercentage', type: 'uint256' }, + ], + name: 'SwapFeePercentageChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'from', type: 'address' }, + { indexed: true, internalType: 'address', name: 'to', type: 'address' }, + { indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' }, + ], + name: 'Transfer', + type: 'event', + }, + { + inputs: [], + name: 'DELEGATE_PROTOCOL_SWAP_FEES_SENTINEL', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'DOMAIN_SEPARATOR', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'address', name: 'spender', type: 'address' }, + ], + name: 'allowance', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'approve', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'decimals', + outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'decreaseAllowance', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'disableRecoveryMode', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'enableRecoveryMode', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'getATHRateProduct', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes4', name: 'selector', type: 'bytes4' }], + name: 'getActionId', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getActualSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getAuthorizer', + outputs: [{ internalType: 'contract IAuthorizer', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getDomainSeparator', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getInvariant', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getLastPostJoinExitInvariant', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'getNextNonce', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getNormalizedWeights', + outputs: [{ internalType: 'uint256[]', name: '', type: 'uint256[]' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getOwner', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getPausedState', + outputs: [ + { internalType: 'bool', name: 'paused', type: 'bool' }, + { internalType: 'uint256', name: 'pauseWindowEndTime', type: 'uint256' }, + { internalType: 'uint256', name: 'bufferPeriodEndTime', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getPoolId', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'feeType', type: 'uint256' }], + name: 'getProtocolFeePercentageCache', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getProtocolFeesCollector', + outputs: [{ internalType: 'contract IProtocolFeesCollector', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getProtocolSwapFeeDelegation', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getRateProviders', + outputs: [{ internalType: 'contract IRateProvider[]', name: '', type: 'address[]' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getScalingFactors', + outputs: [{ internalType: 'uint256[]', name: '', type: 'uint256[]' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getSwapFeePercentage', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getVault', + outputs: [{ internalType: 'contract IVault', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'inRecoveryMode', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'addedValue', type: 'uint256' }, + ], + name: 'increaseAllowance', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'name', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'owner', type: 'address' }], + name: 'nonces', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'uint256', name: 'protocolSwapFeePercentage', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'onExitPool', + outputs: [ + { internalType: 'uint256[]', name: '', type: 'uint256[]' }, + { internalType: 'uint256[]', name: '', type: 'uint256[]' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'uint256', name: 'protocolSwapFeePercentage', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'onJoinPool', + outputs: [ + { internalType: 'uint256[]', name: '', type: 'uint256[]' }, + { internalType: 'uint256[]', name: '', type: 'uint256[]' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'enum IVault.SwapKind', name: 'kind', type: 'uint8' }, + { internalType: 'contract IERC20', name: 'tokenIn', type: 'address' }, + { internalType: 'contract IERC20', name: 'tokenOut', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'address', name: 'from', type: 'address' }, + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + internalType: 'struct IPoolSwapStructs.SwapRequest', + name: 'request', + type: 'tuple', + }, + { internalType: 'uint256', name: 'balanceTokenIn', type: 'uint256' }, + { internalType: 'uint256', name: 'balanceTokenOut', type: 'uint256' }, + ], + name: 'onSwap', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { inputs: [], name: 'pause', outputs: [], stateMutability: 'nonpayable', type: 'function' }, + { + inputs: [ + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'value', type: 'uint256' }, + { internalType: 'uint256', name: 'deadline', type: 'uint256' }, + { internalType: 'uint8', name: 'v', type: 'uint8' }, + { internalType: 'bytes32', name: 'r', type: 'bytes32' }, + { internalType: 'bytes32', name: 's', type: 'bytes32' }, + ], + name: 'permit', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'uint256', name: 'protocolSwapFeePercentage', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'queryExit', + outputs: [ + { internalType: 'uint256', name: 'bptIn', type: 'uint256' }, + { internalType: 'uint256[]', name: 'amountsOut', type: 'uint256[]' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'uint256', name: 'protocolSwapFeePercentage', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'queryJoin', + outputs: [ + { internalType: 'uint256', name: 'bptOut', type: 'uint256' }, + { internalType: 'uint256[]', name: 'amountsIn', type: 'uint256[]' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'contract IERC20', name: 'token', type: 'address' }, + { internalType: 'bytes', name: 'poolConfig', type: 'bytes' }, + ], + name: 'setAssetManagerPoolConfig', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'swapFeePercentage', type: 'uint256' }], + name: 'setSwapFeePercentage', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'symbol', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'totalSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'transfer', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'transferFrom', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { inputs: [], name: 'unpause', outputs: [], stateMutability: 'nonpayable', type: 'function' }, + { + inputs: [], + name: 'updateProtocolFeePercentageCache', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'version', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, +] as const satisfies Abi; diff --git a/src/config/vault/arbitrum.json b/src/config/vault/arbitrum.json index f5fc34737..39eb0cbb9 100644 --- a/src/config/vault/arbitrum.json +++ b/src/config/vault/arbitrum.json @@ -30,7 +30,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "arbitrum-balancer", "poolId": "0x69d9bc07a19caad9ae4ca40af18d5a688839a29900020000000000000000058e", "poolType": "gyroe", @@ -72,7 +72,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "arbitrum-balancer", "poolId": "0xdeeaf8b0a8cf26217261b813e085418c7dd8f1ee00020000000000000000058f", "poolType": "gyroe", @@ -114,7 +114,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "arbitrum-balancer", "poolId": "0xff38cc0ce0de4476c5a3e78675b48420a851035b000200000000000000000593", "poolType": "gyroe", @@ -156,7 +156,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "arbitrum-balancer", "poolId": "0x71c64ac8ec1da03f8a05c3cfeb6493e6dad54a6f000200000000000000000592", "poolType": "gyroe", @@ -1636,7 +1636,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "arbitrum-balancer", "poolId": "0x46472cba35e6800012aa9fcc7939ff07478c473e00020000000000000000056c", "poolType": "gyroe", @@ -1678,7 +1678,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "arbitrum-balancer", "poolId": "0x7272163a931dac5bbe1cb5fefaf959bb65f7346f000200000000000000000549", "poolType": "gyroe", @@ -1720,7 +1720,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "arbitrum-balancer", "poolId": "0x6e822c64c00393b2078f2a5bb75c575ab505b55c000200000000000000000548", "poolType": "gyroe", @@ -1787,7 +1787,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "arbitrum-balancer", "poolId": "0x315dd595e82bdc0c194f3a38a08fde480d7e5d2100020000000000000000056a", "poolType": "gyroe", @@ -5963,7 +5963,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "arbitrum-balancer", "poolId": "0x7967fa58b9501600d96bd843173b9334983ee6e600020000000000000000056e", "poolType": "gyroe", @@ -8412,7 +8412,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "arbitrum-balancer", "poolId": "0xcdcef9765d369954a4a936064535710f7235110a000200000000000000000558", "poolType": "gyroe", @@ -8454,7 +8454,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "arbitrum-balancer", "poolId": "0xef0c116a2818a5b1a5d836a291856a321f43c2fb00020000000000000000053a", "poolType": "gyroe", @@ -11703,7 +11703,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "arbitrum-balancer", "poolId": "0x125bc5a031b2db6733bfa35d914ffa428095978b000200000000000000000514", "poolType": "gyroe", @@ -15051,9 +15051,9 @@ "tokenAddress": "0x32dF62dc3aEd2cD6224193052Ce665DC18165841", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x93981d31ADE2C84882a9A1ABA9Cb49978C83389a", "earnedToken": "mooAuraArbRDNT-WETH", "earnedTokenAddress": "0x93981d31ADE2C84882a9A1ABA9Cb49978C83389a", - "earnContractAddress": "0x93981d31ADE2C84882a9A1ABA9Cb49978C83389a", "oracle": "lps", "oracleId": "aura-rdnt-weth", "status": "active", @@ -15071,7 +15071,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x32df62dc3aed2cd6224193052ce665dc181658410002000000000000000003bd/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x32df62dc3aed2cd6224193052ce665dc181658410002000000000000000003bd/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer-join", + "ammId": "arbitrum-balancer", + "poolId": "0x32df62dc3aed2cd6224193052ce665dc181658410002000000000000000003bd", + "poolType": "weighted", + "tokens": [ + "0x3082CC23568eA640225c2467653dB90e9250AaA0", + "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" + ] + } + ] }, { "id": "aura-reth-bbaweth-v2", @@ -15177,9 +15189,9 @@ "tokenAddress": "0xc7FA3A3527435720f0e2a4c1378335324dd4F9b3", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x3bAa857646e5A0B475E75a1dbD38E7f0a6742058", "earnedToken": "mooAuraArbauraBAL-wstETH", "earnedTokenAddress": "0x3bAa857646e5A0B475E75a1dbD38E7f0a6742058", - "earnContractAddress": "0x3bAa857646e5A0B475E75a1dbD38E7f0a6742058", "oracle": "lps", "oracleId": "aura-aurabal-wsteth", "status": "active", @@ -15197,7 +15209,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xc7fa3a3527435720f0e2a4c1378335324dd4f9b3000200000000000000000459/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xc7fa3a3527435720f0e2a4c1378335324dd4f9b3000200000000000000000459/awithdraw/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer-join", + "ammId": "arbitrum-balancer", + "poolId": "0xc7fa3a3527435720f0e2a4c1378335324dd4f9b3000200000000000000000459", + "poolType": "weighted", + "tokens": [ + "0x223738a747383d6F9f827d95964e4d8E8AC754cE", + "0x5979D7b546E38E414F7E9822514be443A4800529" + ] + } + ] }, { "id": "aura-dola-usdc-v2", diff --git a/src/config/vault/ethereum.json b/src/config/vault/ethereum.json index fc65fa48a..d4f400538 100644 --- a/src/config/vault/ethereum.json +++ b/src/config/vault/ethereum.json @@ -315,7 +315,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "ethereum-balancer", "poolId": "0xc2aa60465bffa1a88f5ba471a59ca0435c3ec5c100020000000000000000062c", "poolType": "gyroe", @@ -350,7 +350,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "ethereum-balancer", "poolId": "0x1cce5169bde03f3d5ad0206f6bd057953539dae600020000000000000000062b", "poolType": "gyroe", @@ -385,7 +385,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "ethereum-balancer", "poolId": "0xaa7a70070e7495fe86c67225329dbd39baa2f63b000200000000000000000663", "poolType": "gyroe", @@ -420,7 +420,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "ethereum-balancer", "poolId": "0x3932b187f440ce7703653b3908edc5bb7676c283000200000000000000000664", "poolType": "gyroe", @@ -2127,9 +2127,9 @@ "tokenAddress": "0x9F9d900462492D4C21e9523ca95A7CD86142F298", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xAB28C317B34084f29d7623208b5F09C0Dd566a18", "earnedToken": "mooAuraRPL-rETH", "earnedTokenAddress": "0xAB28C317B34084f29d7623208b5F09C0Dd566a18", - "earnContractAddress": "0xAB28C317B34084f29d7623208b5F09C0Dd566a18", "oracle": "lps", "oracleId": "aura-reth-rpl", "status": "active", @@ -2140,7 +2140,19 @@ "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x9f9d900462492d4c21e9523ca95a7cd86142f298000200000000000000000462/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x9f9d900462492d4c21e9523ca95a7cd86142f298000200000000000000000462/remove-liquidity", - "network": "ethereum" + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer-join", + "ammId": "ethereum-balancer", + "poolId": "0x9f9d900462492d4c21e9523ca95a7cd86142f298000200000000000000000462", + "poolType": "weighted", + "tokens": [ + "0xae78736Cd615f374D3085123A210448E74Fc6393", + "0xD33526068D116cE69F19A9ee46F0bd304F21A51f" + ] + } + ] }, { "id": "aura-alcx-eth", @@ -2150,9 +2162,9 @@ "tokenAddress": "0xf16aEe6a71aF1A9Bc8F56975A4c2705ca7A782Bc", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x9E90aD4810C9eaE0ADFc15801838Dc53cC6ed48a", "earnedToken": "mooAuraALCX-ETH", "earnedTokenAddress": "0x9E90aD4810C9eaE0ADFc15801838Dc53cC6ed48a", - "earnContractAddress": "0x9E90aD4810C9eaE0ADFc15801838Dc53cC6ed48a", "oracle": "lps", "oracleId": "aura-alcx-eth", "status": "active", @@ -2163,7 +2175,19 @@ "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0xf16aee6a71af1a9bc8f56975a4c2705ca7a782bc0002000000000000000004bb/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0xf16aee6a71af1a9bc8f56975a4c2705ca7a782bc0002000000000000000004bb/remove-liquidity", - "network": "ethereum" + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer-join", + "ammId": "ethereum-balancer", + "poolId": "0xf16aee6a71af1a9bc8f56975a4c2705ca7a782bc0002000000000000000004bb", + "poolType": "weighted", + "tokens": [ + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF" + ] + } + ] }, { "id": "aura-ankreth-wsteth", @@ -2995,7 +3019,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "ethereum-balancer", "poolId": "0xf01b0684c98cd7ada480bfdf6e43876422fa1fc10002000000000000000005de", "poolType": "gyroe", @@ -3030,7 +3054,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "ethereum-balancer", "poolId": "0xf7a826d47c8e02835d94fb0aa40f0cc9505cb1340002000000000000000005e0", "poolType": "gyroe", diff --git a/src/config/vault/fantom.json b/src/config/vault/fantom.json index f622ab984..108e7b0d9 100644 --- a/src/config/vault/fantom.json +++ b/src/config/vault/fantom.json @@ -568,9 +568,9 @@ "tokenAddress": "0x838229095fa83BCD993eF225d01a990E3Bc197A8", "tokenDecimals": 18, "tokenProviderId": "beethovenx", + "earnContractAddress": "0x3751792035dF0497b9CC7d053083EF0F1883eb10", "earnedToken": "mooBeetsFantomLayerOfTheOpera", "earnedTokenAddress": "0x3751792035dF0497b9CC7d053083EF0F1883eb10", - "earnContractAddress": "0x3751792035dF0497b9CC7d053083EF0F1883eb10", "oracle": "lps", "oracleId": "beets-fantom-layer-of-the-opera", "status": "active", @@ -588,7 +588,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://beets.fi/pool/0x838229095fa83bcd993ef225d01a990e3bc197a800020000000000000000075b", "removeLiquidityUrl": "https://beets.fi/pool/0x838229095fa83bcd993ef225d01a990e3bc197a800020000000000000000075b", - "network": "fantom" + "network": "fantom", + "zaps": [ + { + "strategyId": "balancer-join", + "ammId": "fantom-beethovenx", + "poolId": "0x838229095fa83bcd993ef225d01a990e3bc197a800020000000000000000075b", + "poolType": "weighted", + "tokens": [ + "0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83", + "0x28a92dde19D9989F39A49905d7C9C2FAc7799bDf" + ] + } + ] }, { "id": "beets-fantom-and-my-axelar", diff --git a/src/config/vault/gnosis.json b/src/config/vault/gnosis.json index 06a8b002b..8a1640738 100644 --- a/src/config/vault/gnosis.json +++ b/src/config/vault/gnosis.json @@ -30,7 +30,7 @@ "network": "gnosis", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "gnosis-balancer", "poolId": "0x71e1179c5e197fa551beec85ca2ef8693c61b85b0002000000000000000000a0", "poolType": "gyroe", @@ -72,7 +72,7 @@ "network": "gnosis", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "gnosis-balancer", "poolId": "0x8dd4df4ce580b9644437f3375e54f1ab0980822800020000000000000000009c", "poolType": "gyroe", @@ -221,9 +221,9 @@ "tokenAddress": "0x4cdABE9E07ca393943AcFB9286bBbd0D0a310Ff6", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xF49dE23DF72c886946eEF19D1B751B3D997EF668", "earnedToken": "mooAuraGnosiswstETH-COW", "earnedTokenAddress": "0xF49dE23DF72c886946eEF19D1B751B3D997EF668", - "earnContractAddress": "0xF49dE23DF72c886946eEF19D1B751B3D997EF668", "oracle": "lps", "oracleId": "aura-gnosis-wsteth-cow", "status": "active", @@ -241,7 +241,19 @@ "strategyTypeId": "multi-lp", "addLiquidityUrl": "https://balancer.fi/pools/gnosis/v2/0x4cdabe9e07ca393943acfb9286bbbd0d0a310ff600020000000000000000005c/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/gnosis/v2/0x4cdabe9e07ca393943acfb9286bbbd0d0a310ff600020000000000000000005c/remove-liquidity", - "network": "gnosis" + "network": "gnosis", + "zaps": [ + { + "strategyId": "balancer-join", + "ammId": "gnosis-balancer", + "poolId": "0x4cdabe9e07ca393943acfb9286bbbd0d0a310ff600020000000000000000005c", + "poolType": "weighted", + "tokens": [ + "0x177127622c4A00F3d409B75571e12cB3c8973d3c", + "0x6C76971f98945AE98dD7d4DFcA8711ebea946eA6" + ] + } + ] }, { "id": "aura-gnosis-wsteth-sdai", @@ -251,9 +263,9 @@ "tokenAddress": "0xBc2acf5E821c5c9f8667A36bB1131dAd26Ed64F9", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x21c4d71882364b395209C07A7710a9251c285d73", "earnedToken": "mooAuraGnosiswstETH-sDAI", "earnedTokenAddress": "0x21c4d71882364b395209C07A7710a9251c285d73", - "earnContractAddress": "0x21c4d71882364b395209C07A7710a9251c285d73", "oracle": "lps", "oracleId": "aura-gnosis-wsteth-sdai", "status": "active", @@ -271,7 +283,19 @@ "strategyTypeId": "multi-lp", "addLiquidityUrl": "https://balancer.fi/pools/gnosis/v2/0xbc2acf5e821c5c9f8667a36bb1131dad26ed64f9000200000000000000000063/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/gnosis/v2/0xbc2acf5e821c5c9f8667a36bb1131dad26ed64f9000200000000000000000063/remove-liquidity", - "network": "gnosis" + "network": "gnosis", + "zaps": [ + { + "strategyId": "balancer-join", + "ammId": "gnosis-balancer", + "poolId": "0xbc2acf5e821c5c9f8667a36bb1131dad26ed64f9000200000000000000000063", + "poolType": "weighted", + "tokens": [ + "0x6C76971f98945AE98dD7d4DFcA8711ebea946eA6", + "0xaf204776c7245bF4147c2612BF6e5972Ee483701" + ] + } + ] }, { "id": "aura-gnosis-sdai-usdt-usdc", @@ -400,9 +424,9 @@ "tokenAddress": "0x00dF7f58e1Cf932eBe5f54De5970Fb2Bdf0ef06D", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xD40da4e7EC845eA7940b09122Ff34306d78c4C03", "earnedToken": "mooAuraGnosiswstETH/AURA/BAL", "earnedTokenAddress": "0xD40da4e7EC845eA7940b09122Ff34306d78c4C03", - "earnContractAddress": "0xD40da4e7EC845eA7940b09122Ff34306d78c4C03", "oracle": "lps", "oracleId": "aura-gnosis-wsteth-bal-aura", "status": "active", @@ -420,7 +444,20 @@ "strategyTypeId": "multi-lp", "addLiquidityUrl": "https://balancer.fi/pools/gnosis/v2/0x00df7f58e1cf932ebe5f54de5970fb2bdf0ef06d00010000000000000000005b/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/gnosis/v2/0x00df7f58e1cf932ebe5f54de5970fb2bdf0ef06d00010000000000000000005b/remove-liquidity", - "network": "gnosis" + "network": "gnosis", + "zaps": [ + { + "strategyId": "balancer-join", + "ammId": "gnosis-balancer", + "poolId": "0x00df7f58e1cf932ebe5f54de5970fb2bdf0ef06d00010000000000000000005b", + "poolType": "weighted", + "tokens": [ + "0x1509706a6c66CA549ff0cB464de88231DDBe213B", + "0x6C76971f98945AE98dD7d4DFcA8711ebea946eA6", + "0x7eF541E2a22058048904fE5744f9c7E4C57AF717" + ] + } + ] }, { "id": "aura-gnosis-wsteth-gno", @@ -430,9 +467,9 @@ "tokenAddress": "0x4683e340a8049261057D5aB1b29C8d840E75695e", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x5F603C60aDb40691939B9a740beeCeF4B0f5C146", "earnedToken": "mooAuraGnosiswstETH-GNO", "earnedTokenAddress": "0x5F603C60aDb40691939B9a740beeCeF4B0f5C146", - "earnContractAddress": "0x5F603C60aDb40691939B9a740beeCeF4B0f5C146", "oracle": "lps", "oracleId": "aura-gnosis-wsteth-gno", "status": "active", @@ -450,7 +487,19 @@ "strategyTypeId": "multi-lp", "addLiquidityUrl": "https://balancer.fi/pools/gnosis/v2/0x4683e340a8049261057d5ab1b29c8d840e75695e00020000000000000000005a/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/gnosis/v2/0x4683e340a8049261057d5ab1b29c8d840e75695e00020000000000000000005a/remove-liquidity", - "network": "gnosis" + "network": "gnosis", + "zaps": [ + { + "strategyId": "balancer-join", + "ammId": "gnosis-balancer", + "poolId": "0x4683e340a8049261057d5ab1b29c8d840e75695e00020000000000000000005a", + "poolType": "weighted", + "tokens": [ + "0x6C76971f98945AE98dD7d4DFcA8711ebea946eA6", + "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb" + ] + } + ] }, { "id": "aura-gnosis-wsteth-weth", diff --git a/src/config/vault/optimism.json b/src/config/vault/optimism.json index d271b5d7f..f2f62e3cc 100644 --- a/src/config/vault/optimism.json +++ b/src/config/vault/optimism.json @@ -2097,7 +2097,7 @@ "network": "optimism", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "optimism-beethovenx", "poolId": "0xe906d4c4fc4c3fe96560de86b4bf7ed89af9a69a000200000000000000000126", "poolType": "gyroe", @@ -4777,9 +4777,9 @@ "tokenAddress": "0x2FEb76966459d7841fa8A7Ed0aa4bf574d6111Bf", "tokenDecimals": 18, "tokenProviderId": "beethovenx", + "earnContractAddress": "0xb253dD7351906933AB32623c8dd9ADDce8480B09", "earnedToken": "mooAuraOPYieldConcertobyFRAX", "earnedTokenAddress": "0xb253dD7351906933AB32623c8dd9ADDce8480B09", - "earnContractAddress": "0xb253dD7351906933AB32623c8dd9ADDce8480B09", "oracle": "lps", "oracleId": "aura-op-yield-concerto-by-frax", "status": "active", @@ -4790,7 +4790,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://op.beets.fi/pool/0x2feb76966459d7841fa8a7ed0aa4bf574d6111bf00020000000000000000011d", "removeLiquidityUrl": "https://op.beets.fi/pool/0x2feb76966459d7841fa8a7ed0aa4bf574d6111bf00020000000000000000011d", - "network": "optimism" + "network": "optimism", + "zaps": [ + { + "strategyId": "balancer-join", + "ammId": "optimism-beethovenx", + "poolId": "0x2feb76966459d7841fa8a7ed0aa4bf574d6111bf00020000000000000000011d", + "poolType": "weighted", + "tokens": [ + "0x2Dd1B4D4548aCCeA497050619965f91f78b3b532", + "0x484c2D6e3cDd945a8B2DF735e079178C1036578c" + ] + } + ] }, { "id": "aura-op-a-night-at-the-opara", @@ -5606,7 +5618,7 @@ "network": "optimism", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "optimism-beethovenx", "poolId": "0x7ca75bdea9dede97f8b13c6641b768650cb837820002000000000000000000d5", "poolType": "gyroe", @@ -11886,9 +11898,9 @@ "tokenAddress": "0x39965c9dAb5448482Cf7e002F583c812Ceb53046", "tokenDecimals": 18, "tokenProviderId": "beethovenx", + "earnContractAddress": "0x8e829cFC629fcb61c1508a54D232aF4612E48cC9", "earnedToken": "mooBeetHappyRoad", "earnedTokenAddress": "0x8e829cFC629fcb61c1508a54D232aF4612E48cC9", - "earnContractAddress": "0x8e829cFC629fcb61c1508a54D232aF4612E48cC9", "oracle": "lps", "oracleId": "beets-happy-road", "status": "active", @@ -11899,7 +11911,20 @@ "strategyTypeId": "multi-lp", "addLiquidityUrl": "https://op.beets.fi/#/pool/0x39965c9dab5448482cf7e002f583c812ceb53046000100000000000000000003/invest", "removeLiquidityUrl": "https://op.beets.fi/#/pool/0x39965c9dab5448482cf7e002f583c812ceb53046000100000000000000000003/invest", - "network": "optimism" + "network": "optimism", + "zaps": [ + { + "strategyId": "balancer-join", + "ammId": "optimism-beethovenx", + "poolId": "0x39965c9dab5448482cf7e002f583c812ceb53046000100000000000000000003", + "poolType": "weighted", + "tokens": [ + "0x4200000000000000000000000000000000000006", + "0x4200000000000000000000000000000000000042", + "0x7F5c764cBc14f9669B88837ca1490cCa17c31607" + ] + } + ] }, { "id": "beets-lennons-long", diff --git a/src/config/vault/polygon.json b/src/config/vault/polygon.json index bd8349df2..b2d51db03 100644 --- a/src/config/vault/polygon.json +++ b/src/config/vault/polygon.json @@ -632,9 +632,9 @@ "tokenAddress": "0x1DCeA0BfBBe6848F117640D534C9B60f41b9F2A8", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x2956333ca76D156A86533E6D1D8209E9B04D69a5", "earnedToken": "mooBalancerPolygonWBTC/USDC/ETH", "earnedTokenAddress": "0x2956333ca76D156A86533E6D1D8209E9B04D69a5", - "earnContractAddress": "0x2956333ca76D156A86533E6D1D8209E9B04D69a5", "oracle": "lps", "oracleId": "balancer-polygon-wbtc-usdc-eth", "status": "active", @@ -652,7 +652,20 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/polygon/v2/0x1dcea0bfbbe6848f117640d534c9b60f41b9f2a8000100000000000000000ea1/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/polygon/v2/0x1dcea0bfbbe6848f117640d534c9b60f41b9f2a8000100000000000000000ea1/remove-liquidity", - "network": "polygon" + "network": "polygon", + "zaps": [ + { + "strategyId": "balancer-join", + "ammId": "polygon-balancer", + "poolId": "0x1dcea0bfbbe6848f117640d534c9b60f41b9f2a8000100000000000000000ea1", + "poolType": "weighted", + "tokens": [ + "0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6", + "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619" + ] + } + ] }, { "id": "balancer-polygon-usdc-dai-usdt", @@ -2322,7 +2335,7 @@ "network": "polygon", "zaps": [ { - "strategyId": "balancer-pool", + "strategyId": "balancer-join", "ammId": "polygon-balancer", "poolId": "0xf0ad209e2e969eaaa8c882aac71f02d8a047d5c2000200000000000000000b49", "poolType": "gyroe", diff --git a/src/features/data/apis/amm/balancer/composable-stable/ComposableStablePool.ts b/src/features/data/apis/amm/balancer/composable-stable/ComposableStablePool.ts index f54a65d2d..3772cde31 100644 --- a/src/features/data/apis/amm/balancer/composable-stable/ComposableStablePool.ts +++ b/src/features/data/apis/amm/balancer/composable-stable/ComposableStablePool.ts @@ -1,4 +1,4 @@ -import type { IBalancerPool } from '../types'; +import type { IBalancerSwapPool } from '../types'; import type BigNumber from 'bignumber.js'; import { BIG_ZERO } from '../../../../../../helpers/big-number'; import type { ZapStep } from '../../../transact/zap/types'; @@ -15,8 +15,9 @@ import { getUnixNow } from '../../../../../../helpers/date'; const THIRTY_MINUTES_IN_SECONDS = 30 * 60; -export class ComposableStablePool implements IBalancerPool { +export class ComposableStablePool implements IBalancerSwapPool { public readonly type = 'balancer'; + public readonly subType = 'swap'; constructor( protected readonly chain: ChainEntity, diff --git a/src/features/data/apis/amm/balancer/gyroe/GyroEPool.ts b/src/features/data/apis/amm/balancer/gyroe/GyroEPool.ts index a553ee8ad..7ff7844e4 100644 --- a/src/features/data/apis/amm/balancer/gyroe/GyroEPool.ts +++ b/src/features/data/apis/amm/balancer/gyroe/GyroEPool.ts @@ -1,44 +1,44 @@ -import type { IBalancerPool } from '../types'; -import type { ChainEntity } from '../../../../entities/chain'; import type { PoolConfig, - QueryExitPoolRequest, - QueryExitPoolResponse, QueryJoinPoolRequest, QueryJoinPoolResponse, VaultConfig, } from '../vault/types'; -import { createFactory } from '../../../../utils/factory-utils'; -import { viemToWeb3Abi } from '../../../../../../helpers/web3'; -import { getWeb3Instance } from '../../../instances'; -import { BalancerGyroEPoolAbi } from '../../../../../../config/abi/BalancerGyroEPoolAbi'; import type { RatesResult } from './types'; import BigNumber from 'bignumber.js'; -import { Vault } from '../vault/Vault'; import { BIG_ONE, - BIG_ZERO, bigNumberToStringDeep, bigNumberToUint256String, } from '../../../../../../helpers/big-number'; import { WeightedPoolEncoder } from '../weighted/WeightedPoolEncoder'; -import { GyroFixedPoint } from './GyroFixedPoint'; +import { FixedPoint } from '../weighted/FixedPoint'; import type { ZapStep } from '../../../transact/zap/types'; -import { checkAddressOrder } from '../../../../../../helpers/tokens'; - -export class GyroEPool implements IBalancerPool { - public readonly type = 'balancer'; +import { WeightedMath } from '../weighted/WeightedMath'; +import { WeightedPool } from '../weighted/WeightedPool'; +import type { ChainEntity } from '../../../../entities/chain'; +import { viemToWeb3Abi } from '../../../../../../helpers/web3'; +import { BalancerGyroEPoolAbi } from '../../../../../../config/abi/BalancerGyroEPoolAbi'; +export class GyroEPool extends WeightedPool { constructor( - protected readonly chain: ChainEntity, - protected readonly vaultConfig: VaultConfig, - protected readonly config: PoolConfig + readonly chain: ChainEntity, + readonly vaultConfig: VaultConfig, + readonly config: PoolConfig ) { - checkAddressOrder(config.tokens.map(t => t.address)); + super(chain, vaultConfig, config); + + this.getUpscaledBalances = this.cacheMethod(this.getUpscaledBalances); + this.getTokenRates = this.cacheMethod(this.getTokenRates); + this.getTotalSupply = this.cacheMethod(this.getTotalSupply); + } + + get joinSupportsSlippage() { + return false; } // async getSwapRatioLikeStrategy(): Promise { - // const balances = await this.getCachedBalances(); + // const balances = await this.getBalances(); // const rates = await this.getTokenRates(); // const totalSupply = await this.getTotalSupply(); // if (balances.length !== this.config.tokens.length || rates.length !== this.config.tokens.length) { @@ -56,9 +56,13 @@ export class GyroEPool implements IBalancerPool { // return BIG_ONE.shiftedBy(18).dividedBy(ratio.plus(BIG_ONE.shiftedBy(18))); // } - async getSwapRatio(): Promise { - const upscaledBalances = await this.getCachedUpscaledBalances(); - return upscaledBalances[0].dividedBy(upscaledBalances[0].plus(upscaledBalances[1])); + /** assumption: all gyro pools have rateProviders */ + async getSwapRatios(): Promise { + const upscaledBalances = await this.getUpscaledBalances(); + const token0Ratio = upscaledBalances[0].dividedBy( + upscaledBalances[0].plus(upscaledBalances[1]) + ); + return [token0Ratio, BIG_ONE.minus(token0Ratio)]; } /** @@ -92,6 +96,7 @@ export class GyroEPool implements IBalancerPool { /** * Gyro pools only support JoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT + * Which makes it hard to estimate how much liquidity will be added for the given input amounts */ async quoteAddLiquidity(amountsIn: BigNumber[]): Promise { if (amountsIn.some(amount => amount.lte(BIG_ONE))) { @@ -105,24 +110,23 @@ export class GyroEPool implements IBalancerPool { } // all on-chain calculations are done with upscaled balances in FixedPoint (18 decimals) - const upscaledBalances = await this.getCachedUpscaledBalances(); + const upscaledBalances = await this.getUpscaledBalances(); const upscaledAmountsIn = await this.upscaleAmounts(amountsIn); // locally estimate how much liquidity will be added for the given input amounts + // inverse of WeightedMath.calcAllTokensInGivenExactBptOut const initialEstimatedLiquidity = BigNumber.min( - ...upscaledAmountsIn.map((amount, i) => { - return GyroFixedPoint.divDown( - GyroFixedPoint.mulDown(amount, totalSupply), - upscaledBalances[i] - ); - }) + ...upscaledAmountsIn.map((amount, i) => + // bptOut = tokenIn[n] * totalSupply / balances[n] + FixedPoint.divDown(FixedPoint.mulDown(amount, totalSupply), upscaledBalances[i]) + ) ); if (initialEstimatedLiquidity.lte(BIG_ONE)) { // Some Gyro math requires > 1 throw new Error('Liquidity added must be greater than 1'); } - const upscaledUsedInput = [...upscaledAmountsIn]; + let upscaledUsedInput = [...upscaledAmountsIn]; let estimatedLiquidity = initialEstimatedLiquidity; do { console.debug( @@ -134,13 +138,13 @@ export class GyroEPool implements IBalancerPool { totalSupply, }) ); + // locally estimate how much of each input amount will be used to add the given liquidity - for (let i = 0; i < upscaledUsedInput.length; i++) { - upscaledUsedInput[i] = GyroFixedPoint.divUp( - GyroFixedPoint.mulUp(upscaledBalances[i], estimatedLiquidity), - totalSupply - ); - } + upscaledUsedInput = WeightedMath.calcAllTokensInGivenExactBptOut( + upscaledBalances, + estimatedLiquidity, + totalSupply + ); if (upscaledUsedInput.some(amount => amount.lte(BIG_ONE))) { // Some Gyro math requires > 1 @@ -185,134 +189,30 @@ export class GyroEPool implements IBalancerPool { throw new Error('Failed to calculate liquidity'); } - /** - * Gyro pools only support ExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT - */ - async quoteRemoveLiquidity(amountIn: BigNumber): Promise { - if (amountIn.lt(BIG_ONE)) { - throw new Error('Input amount must be greater than 0'); - } - - const vault = this.getVault(); - - const queryRequest: QueryExitPoolRequest = { - poolId: this.config.poolId, - request: { - assets: this.config.tokens.map(t => t.address), - minAmountsOut: this.config.tokens.map(() => BIG_ZERO), - userData: WeightedPoolEncoder.exitExactBPTInForTokensOut( - bigNumberToUint256String(amountIn) - ), - toInternalBalance: false, - }, - }; - - return await vault.queryExitPool(queryRequest); + protected async getScalingFactors() { + const factors = await super.getScalingFactors(); + const rates = await this.getTokenRates(); + return factors.map((factor, i) => FixedPoint.mulDown(factor, rates[i])); } - /** - * Gyro pools only support ExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT - */ - async getRemoveLiquidityZap( - amountIn: BigNumber, - minAmountsOut: BigNumber[], - from: string, - insertBalance: boolean - ): Promise { - const vault = this.getVault(); - - return vault.getExitPoolZap({ - exit: { - poolId: this.config.poolId, - sender: from, - recipient: from, - request: { - assets: this.config.tokens.map(t => t.address), - minAmountsOut, - userData: WeightedPoolEncoder.exitExactBPTInForTokensOut( - bigNumberToUint256String(amountIn) - ), - toInternalBalance: false, - }, - }, - poolAddress: this.config.poolAddress, - insertBalance, - }); + protected async getUpscaledBalances() { + return await this.upscaleAmounts(await this.getBalances()); } - protected getDecimalScalingFactors = createFactory(() => { - return this.config.tokens.map(token => { - if (token.address === this.config.poolAddress) { - return GyroFixedPoint.ONE; - } - - if (token.decimals > 18) { - throw new Error('Tokens with more than 18 decimals are not supported.'); - } - - const diff = 18 - token.decimals; - return GyroFixedPoint.ONE.shiftedBy(diff); - }); - }); - - protected getCachedScalingFactors = createFactory(async () => { - const factors = this.getDecimalScalingFactors(); - const rates = await this.getCachedTokenRates(); - return factors.map((factor, i) => GyroFixedPoint.mulDown(factor, rates[i])); - }); - - protected async upscaleAmounts(balances: BigNumber[]): Promise { - const factors = await this.getCachedScalingFactors(); - return balances.map((balance, i) => GyroFixedPoint.mulDown(balance, factors[i])); - } - - protected async downscaleAmounts(amounts: BigNumber[]): Promise { - const factors = await this.getCachedScalingFactors(); - return amounts.map((amount, i) => GyroFixedPoint.divUp(amount, factors[i])); + protected async getTokenRates(): Promise { + const pool = await this.getPoolContract(); + const rates: RatesResult = await pool.methods.getTokenRates().call(); + return [rates.rate0, rates.rate1].map(rate => new BigNumber(rate)); } - protected getCachedPoolTokens = createFactory(async () => { - const vault = this.getVault(); - return await vault.getPoolTokens(this.config.poolId); - }); - - protected getCachedBalances = createFactory(async () => { - const poolTokens = await this.getCachedPoolTokens(); - return poolTokens.map(t => t.balance); - }); - - protected getCachedUpscaledBalances = createFactory(async () => { - return await this.upscaleAmounts(await this.getCachedBalances()); - }); - - protected getCachedTotalSupply = createFactory(async () => { - return this.getTotalSupply(); - }); - protected async getTotalSupply(): Promise { const pool = await this.getPoolContract(); const totalSupply: string = await pool.methods.getActualSupply().call(); return new BigNumber(totalSupply); } - protected getCachedTokenRates = createFactory(async () => { - return this.getTokenRates(); - }); - - protected async getTokenRates(): Promise { - const pool = await this.getPoolContract(); - const rates: RatesResult = await pool.methods.getTokenRates().call(); - return [rates.rate0, rates.rate1].map(rate => new BigNumber(rate)); - } - - protected getWeb3 = createFactory(() => getWeb3Instance(this.chain)); - - protected getPoolContract = createFactory(async () => { + protected async getPoolContract() { const web3 = await this.getWeb3(); return new web3.eth.Contract(viemToWeb3Abi(BalancerGyroEPoolAbi), this.config.poolAddress); - }); - - protected getVault = createFactory(() => { - return new Vault(this.chain, this.vaultConfig); - }); + } } diff --git a/src/features/data/apis/amm/balancer/types.ts b/src/features/data/apis/amm/balancer/types.ts index 68f441fc0..a22b97776 100644 --- a/src/features/data/apis/amm/balancer/types.ts +++ b/src/features/data/apis/amm/balancer/types.ts @@ -1,3 +1,32 @@ +import type BigNumber from 'bignumber.js'; +import type { ZapStep } from '../../transact/zap/types'; +import type { QueryExitPoolResponse, QueryJoinPoolResponse } from './vault/types'; + export interface IBalancerPool { readonly type: 'balancer'; } + +export interface IBalancerJoinPool extends IBalancerPool { + readonly subType: 'join'; + readonly joinSupportsSlippage: boolean; + + getSwapRatios(): Promise; + quoteAddLiquidity(amountsIn: BigNumber[]): Promise; + getAddLiquidityZap( + maxAmountsIn: BigNumber[], + liquidity: BigNumber, + from: string, + insertBalance: boolean + ): Promise; + quoteRemoveLiquidity(amountIn: BigNumber): Promise; + getRemoveLiquidityZap( + amountIn: BigNumber, + minAmountsOut: BigNumber[], + from: string, + insertBalance: boolean + ): Promise; +} + +export interface IBalancerSwapPool extends IBalancerPool { + readonly subType: 'swap'; +} diff --git a/src/features/data/apis/amm/balancer/vault/Vault.ts b/src/features/data/apis/amm/balancer/vault/Vault.ts index 2e355b163..c67fa712d 100644 --- a/src/features/data/apis/amm/balancer/vault/Vault.ts +++ b/src/features/data/apis/amm/balancer/vault/Vault.ts @@ -39,7 +39,7 @@ import { import type { StepToken, ZapStep } from '../../../transact/zap/types'; import { getInsertIndex } from '../../../transact/helpers/zap'; import { getAddress } from 'viem'; -import { WeightedPoolExitKind } from '../weighted/types'; +import { WeightedPoolExitKind, WeightedPoolJoinKind } from '../weighted/types'; const queryFunds: FundManagement = { sender: ZERO_ADDRESS, @@ -178,27 +178,77 @@ export class Vault { } async getJoinPoolZap(request: JoinPoolZapRequest): Promise { - /* + return { + target: this.config.vaultAddress, + value: '0', + data: this.encodeJoinPool(request.join), + tokens: this.getJoinPoolZapTokens(request), + }; + } + + protected getJoinPoolZapTokens(request: JoinPoolZapRequest): StepToken[] { + if (!request.insertBalance) { + return []; + } + + /* joinPool call data layout: + * 00 | poolId + * 01 | sender + * 02 | recipient + * 03 | offset to request + * 04 | offset to request.assets + * 05 | offset to request.maxAmountsIn + * 06 | offset to request.userData + * 07 | request.fromInternalBalance * 08 | length of request.assets * 09+0 | request.assets[0] * 09+n | request.assets[n] * 09+request.assets.length | length of maxAmountsIn - * 09+request.assets.length+1 | maxAmountsIn[0] + * 09+request.assets.length+1+0 | maxAmountsIn[0] * 09+request.assets.length+1+n | maxAmountsIn[n] + * 09+request.assets.length+1+request.maxAmountsIn.length | length of userData + * 09+request.assets.length+1+request.maxAmountsIn.length+1+0 | word 0 of userData */ + + // Always insert amounts into the maxAmountsIn array const maxAmountsInStartWord = 9 + request.join.request.assets.length + 1; + const tokens: StepToken[] = request.join.request.assets.map((address, i) => ({ + token: address, + index: getInsertIndex(maxAmountsInStartWord + i), + })); - return { - target: this.config.vaultAddress, - value: '0', - data: this.encodeJoinPool(request.join), - tokens: request.insertBalance - ? request.join.request.assets.map((address, i) => ({ - token: address, - index: getInsertIndex(maxAmountsInStartWord + i), - })) - : [], - }; + // Maybe insert amounts into the userData bytes + const userDataWordStart = + 9 + request.join.request.assets.length + 1 + request.join.request.maxAmountsIn.length + 1; + const joinKindString = abiCoder.decodeParameter( + 'uint256', + request.join.request.userData + ) as unknown as string; + console.log('joinKindString', joinKindString); + const joinKind = new BigNumber(joinKindString).toNumber(); + + switch (joinKind) { + case WeightedPoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT: { + for (let i = 0; i < request.join.request.assets.length; i++) { + tokens.push({ + token: request.join.request.assets[i], + index: getInsertIndex(userDataWordStart + 4 + i), // 0 = JoinKind, 1 = Offset to amountsIn, 2 = minBPTAmountOut, 3 = amountsIn.length, 4 = amountsIn[0], 4+n = amountsIn[n] + }); + } + return tokens; + } + case WeightedPoolJoinKind.TOKEN_IN_FOR_EXACT_BPT_OUT: { + // 0 = JoinKind, 1 = bptAmountOut, 2 = enterTokenIndex + return tokens; + } + case WeightedPoolJoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT: { + // 0 = JoinKind, 1 = bptAmountOut + return tokens; + } + default: { + throw new Error(`Unsupported join kind: ${joinKind}`); + } + } } async getExitPoolZap(request: ExitPoolZapRequest): Promise { @@ -215,7 +265,15 @@ export class Vault { return []; } - /* + /* exitPool call data layout: + * 00 | poolId + * 01 | sender + * 02 | recipient + * 03 | offset to request + * 04 | offset to request.assets + * 05 | offset to request.minAmountsOut + * 06 | offset to request.userData + * 07 | request.toInternal * 08 | length of request.assets * 09+0 | request.assets[0] * 09+n | request.assets[n] @@ -225,6 +283,8 @@ export class Vault { * 09+request.assets.length+1+request.minAmountsOut.length | length of userData * 09+request.assets.length+1+request.minAmountsOut.length+1+0 | word 0 of userData */ + + // Insert the amount of bpt to burn into the userData bytes const userDataWordStart = 9 + request.exit.request.assets.length + 1 + request.exit.request.minAmountsOut.length + 1; const exitKindString = abiCoder.decodeParameter( diff --git a/src/features/data/apis/amm/balancer/vault/types.ts b/src/features/data/apis/amm/balancer/vault/types.ts index 589758c04..f7658b07f 100644 --- a/src/features/data/apis/amm/balancer/vault/types.ts +++ b/src/features/data/apis/amm/balancer/vault/types.ts @@ -1,6 +1,5 @@ import type BigNumber from 'bignumber.js'; import type { TokenEntity } from '../../../../entities/token'; -import type { WeightedPoolExitKind } from '../weighted/types'; export type VaultConfig = { /** address */ @@ -190,21 +189,6 @@ export type JoinPoolZapRequest = { insertBalance: boolean; }; -export type ExitPoolZapRequestKind = - | { - kind: WeightedPoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT; - token: string; - } - | { - kind: WeightedPoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT; - token: string; - tokenIndex: number; - } - | { - kind: WeightedPoolExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT; - token: string; - }; - export type ExitPoolZapRequest = { exit: ExitPoolArgs; poolAddress: string; diff --git a/src/features/data/apis/amm/balancer/gyroe/GyroFixedPoint.ts b/src/features/data/apis/amm/balancer/weighted/FixedPoint.ts similarity index 62% rename from src/features/data/apis/amm/balancer/gyroe/GyroFixedPoint.ts rename to src/features/data/apis/amm/balancer/weighted/FixedPoint.ts index c5ff44084..38be5c7ff 100644 --- a/src/features/data/apis/amm/balancer/gyroe/GyroFixedPoint.ts +++ b/src/features/data/apis/amm/balancer/weighted/FixedPoint.ts @@ -1,7 +1,7 @@ import { BIG_ONE, BIG_ZERO } from '../../../../../../helpers/big-number'; import BigNumber from 'bignumber.js'; -export class GyroFixedPoint { +export class FixedPoint { public static readonly ONE = BIG_ONE.shiftedBy(18); private constructor() { @@ -13,7 +13,7 @@ export class GyroFixedPoint { if (!(a.isZero() || product.dividedToIntegerBy(a).isEqualTo(b))) { throw new Error('Multiplication overflow'); } - return product.dividedToIntegerBy(GyroFixedPoint.ONE); + return product.dividedToIntegerBy(FixedPoint.ONE); } static mulUp(a: BigNumber, b: BigNumber): BigNumber { @@ -26,7 +26,7 @@ export class GyroFixedPoint { return product; } - return product.minus(1).dividedToIntegerBy(GyroFixedPoint.ONE).plus(1); + return product.minus(1).dividedToIntegerBy(FixedPoint.ONE).plus(1); } static divDown(a: BigNumber, b: BigNumber): BigNumber { @@ -38,8 +38,8 @@ export class GyroFixedPoint { return BIG_ZERO; } - const aInflated = a.multipliedBy(GyroFixedPoint.ONE); - if (!aInflated.dividedToIntegerBy(a).isEqualTo(GyroFixedPoint.ONE)) { + const aInflated = a.multipliedBy(FixedPoint.ONE); + if (!aInflated.dividedToIntegerBy(a).isEqualTo(FixedPoint.ONE)) { throw new Error('Multiplication overflow'); } @@ -55,11 +55,27 @@ export class GyroFixedPoint { return BIG_ZERO; } - const aInflated = a.multipliedBy(GyroFixedPoint.ONE); - if (!aInflated.dividedToIntegerBy(a).isEqualTo(GyroFixedPoint.ONE)) { + const aInflated = a.multipliedBy(FixedPoint.ONE); + if (!aInflated.dividedToIntegerBy(a).isEqualTo(FixedPoint.ONE)) { throw new Error('Multiplication overflow'); } return aInflated.minus(1).dividedToIntegerBy(b).plus(1); } + + static powDown(base: BigNumber, exponent: BigNumber): BigNumber { + if (exponent.isZero()) { + return FixedPoint.ONE; + } + + if (base.isZero()) { + return BIG_ZERO; + } + + const baseDecimal = base.shiftedBy(-18); + const exponentDecimal = exponent.shiftedBy(-18); + const resultDecimal = baseDecimal.exponentiatedBy(exponentDecimal); + + return resultDecimal.shiftedBy(18).integerValue(BigNumber.ROUND_FLOOR); + } } diff --git a/src/features/data/apis/amm/balancer/weighted/WeightedMath.ts b/src/features/data/apis/amm/balancer/weighted/WeightedMath.ts new file mode 100644 index 000000000..7115aeee8 --- /dev/null +++ b/src/features/data/apis/amm/balancer/weighted/WeightedMath.ts @@ -0,0 +1,31 @@ +import type BigNumber from 'bignumber.js'; +import { FixedPoint } from './FixedPoint'; +import { BIG_ZERO } from '../../../../../../helpers/big-number'; + +export class WeightedMath { + private constructor() { + // static only + } + + /** balances must be correctly (up)scaled */ + static calcAllTokensInGivenExactBptOut( + balances: BigNumber[], + bptOut: BigNumber, + totalSupply: BigNumber + ): BigNumber[] { + return balances.map(balance => + FixedPoint.divUp(FixedPoint.mulUp(balance, bptOut), totalSupply) + ); + } + + /** balances must be correctly (up)scaled */ + static calcTokensOutGivenExactBptIn( + balances: BigNumber[], + bptIn: BigNumber, + totalSupply: BigNumber + ): BigNumber[] { + return balances.map(balance => + FixedPoint.divDown(FixedPoint.mulDown(balance, bptIn), totalSupply) + ); + } +} diff --git a/src/features/data/apis/amm/balancer/weighted/WeightedPool.ts b/src/features/data/apis/amm/balancer/weighted/WeightedPool.ts new file mode 100644 index 000000000..00fc37292 --- /dev/null +++ b/src/features/data/apis/amm/balancer/weighted/WeightedPool.ts @@ -0,0 +1,225 @@ +import type { IBalancerJoinPool } from '../types'; +import type { ChainEntity } from '../../../../entities/chain'; +import type { + PoolConfig, + QueryExitPoolRequest, + QueryExitPoolResponse, + QueryJoinPoolRequest, + QueryJoinPoolResponse, + VaultConfig, +} from '../vault/types'; +import { createFactory } from '../../../../utils/factory-utils'; +import { viemToWeb3Abi } from '../../../../../../helpers/web3'; +import { getWeb3Instance } from '../../../instances'; +import type { NormalizedWeightsResult } from './types'; +import type BigNumber from 'bignumber.js'; +import { Vault } from '../vault/Vault'; +import { + BIG_ONE, + BIG_ZERO, + bigNumberToStringDeep, + bigNumberToUint256String, + fromWeiString, +} from '../../../../../../helpers/big-number'; +import { WeightedPoolEncoder } from './WeightedPoolEncoder'; +import { FixedPoint } from './FixedPoint'; +import type { ZapStep } from '../../../transact/zap/types'; +import { checkAddressOrder } from '../../../../../../helpers/tokens'; +import { BalancerWeightedPoolAbi } from '../../../../../../config/abi/BalancerWeightedPoolAbi'; + +export class WeightedPool implements IBalancerJoinPool { + public readonly type = 'balancer'; + public readonly subType = 'join'; + + constructor( + protected readonly chain: ChainEntity, + protected readonly vaultConfig: VaultConfig, + protected readonly config: PoolConfig + ) { + checkAddressOrder(config.tokens.map(t => t.address)); + + this.getSwapRatios = this.cacheMethod(this.getSwapRatios); + this.getScalingFactors = this.cacheMethod(this.getScalingFactors); + this.getPoolTokens = this.cacheMethod(this.getPoolTokens); + this.getBalances = this.cacheMethod(this.getBalances); + this.getNormalizedWeights = this.cacheMethod(this.getNormalizedWeights); + this.getWeb3 = this.cacheMethod(this.getWeb3); + this.getPoolContract = this.cacheMethod(this.getPoolContract); + this.getVault = this.cacheMethod(this.getVault); + } + + protected cacheMethod unknown>(fn: T): T { + return createFactory(fn.bind(this)) as T; + } + + get joinSupportsSlippage() { + return true; + } + + async getSwapRatios(): Promise { + return this.getNormalizedWeights(); + } + + async getAddLiquidityZap( + amountsIn: BigNumber[], + minLiquidity: BigNumber, + from: string, + insertBalance: boolean + ): Promise { + const vault = this.getVault(); + + return vault.getJoinPoolZap({ + join: { + poolId: this.config.poolId, + sender: from, + recipient: from, + request: { + assets: this.config.tokens.map(t => t.address), + maxAmountsIn: amountsIn, + userData: WeightedPoolEncoder.joinExactTokensInForBPTOut( + amountsIn.map(amount => bigNumberToUint256String(amount)), + bigNumberToUint256String(minLiquidity) + ), + fromInternalBalance: false, + }, + }, + insertBalance, + }); + } + + async quoteAddLiquidity(amountsIn: BigNumber[]): Promise { + if (amountsIn.every(amount => amount.lte(BIG_ZERO))) { + throw new Error('At least one input amount must be greater than 0'); + } + + const vault = this.getVault(); + const queryRequest: QueryJoinPoolRequest = { + poolId: this.config.poolId, + request: { + assets: this.config.tokens.map(t => t.address), + maxAmountsIn: amountsIn, + userData: WeightedPoolEncoder.joinExactTokensInForBPTOut( + amountsIn.map(amount => bigNumberToUint256String(amount)), + '0' + ), + fromInternalBalance: false, + }, + }; + const queryResult = await vault.queryJoinPool(queryRequest); + + console.debug( + 'queryJoinPool', + bigNumberToStringDeep(queryRequest), + bigNumberToStringDeep(queryResult) + ); + + return { + liquidity: queryResult.liquidity, + usedInput: queryResult.usedInput, + unusedInput: amountsIn.map((amount, i) => amount.minus(queryResult.usedInput[i])), + }; + } + + async quoteRemoveLiquidity(amountIn: BigNumber): Promise { + if (amountIn.lt(BIG_ONE)) { + throw new Error('Input amount must be greater than 0'); + } + + const vault = this.getVault(); + + const queryRequest: QueryExitPoolRequest = { + poolId: this.config.poolId, + request: { + assets: this.config.tokens.map(t => t.address), + minAmountsOut: this.config.tokens.map(() => BIG_ZERO), + userData: WeightedPoolEncoder.exitExactBPTInForTokensOut( + bigNumberToUint256String(amountIn) + ), + toInternalBalance: false, + }, + }; + + return await vault.queryExitPool(queryRequest); + } + + async getRemoveLiquidityZap( + amountIn: BigNumber, + minAmountsOut: BigNumber[], + from: string, + insertBalance: boolean + ): Promise { + const vault = this.getVault(); + + return vault.getExitPoolZap({ + exit: { + poolId: this.config.poolId, + sender: from, + recipient: from, + request: { + assets: this.config.tokens.map(t => t.address), + minAmountsOut, + userData: WeightedPoolEncoder.exitExactBPTInForTokensOut( + bigNumberToUint256String(amountIn) + ), + toInternalBalance: false, + }, + }, + poolAddress: this.config.poolAddress, + insertBalance, + }); + } + + protected async getScalingFactors() { + return this.config.tokens.map(token => { + if (token.address === this.config.poolAddress) { + return FixedPoint.ONE; + } + + if (token.decimals > 18) { + throw new Error('Tokens with more than 18 decimals are not supported.'); + } + + const diff = 18 - token.decimals; + return FixedPoint.ONE.shiftedBy(diff); + }); + } + + protected async upscaleAmounts(balances: BigNumber[]): Promise { + const factors = await this.getScalingFactors(); + return balances.map((balance, i) => FixedPoint.mulDown(balance, factors[i])); + } + + protected async downscaleAmounts(amounts: BigNumber[]): Promise { + const factors = await this.getScalingFactors(); + return amounts.map((amount, i) => FixedPoint.divUp(amount, factors[i])); + } + + protected async getPoolTokens() { + const vault = this.getVault(); + return await vault.getPoolTokens(this.config.poolId); + } + + protected async getBalances() { + const poolTokens = await this.getPoolTokens(); + return poolTokens.map(t => t.balance); + } + + protected async getNormalizedWeights(): Promise { + const pool = await this.getPoolContract(); + const weights: NormalizedWeightsResult = await pool.methods.getNormalizedWeights().call(); + return weights.map(weight => fromWeiString(weight, 18)); + } + + protected async getWeb3() { + return getWeb3Instance(this.chain); + } + + protected async getPoolContract() { + const web3 = await this.getWeb3(); + return new web3.eth.Contract(viemToWeb3Abi(BalancerWeightedPoolAbi), this.config.poolAddress); + } + + protected getVault() { + return new Vault(this.chain, this.vaultConfig); + } +} diff --git a/src/features/data/apis/amm/balancer/weighted/types.ts b/src/features/data/apis/amm/balancer/weighted/types.ts index df20c6e5c..e7482adda 100644 --- a/src/features/data/apis/amm/balancer/weighted/types.ts +++ b/src/features/data/apis/amm/balancer/weighted/types.ts @@ -12,3 +12,5 @@ export enum WeightedPoolExitKind { BPT_IN_FOR_EXACT_TOKENS_OUT, REMOVE_TOKEN, } + +export type NormalizedWeightsResult = string[]; diff --git a/src/features/data/apis/transact/strategies/balancer/BalancerPoolStrategy.ts b/src/features/data/apis/transact/strategies/balancer/BalancerJoinStrategy.ts similarity index 93% rename from src/features/data/apis/transact/strategies/balancer/BalancerPoolStrategy.ts rename to src/features/data/apis/transact/strategies/balancer/BalancerJoinStrategy.ts index 48f2a5bc9..b0c0a3c16 100644 --- a/src/features/data/apis/transact/strategies/balancer/BalancerPoolStrategy.ts +++ b/src/features/data/apis/transact/strategies/balancer/BalancerJoinStrategy.ts @@ -75,25 +75,26 @@ import { isStandardVault, type VaultStandard } from '../../../../entities/vault' import { getVaultWithdrawnFromState } from '../../helpers/vault'; import { isDefined } from '../../../../utils/array-utils'; import { isStandardVaultType, type IStandardVaultType } from '../../vaults/IVaultType'; -import type { BalancerPoolStrategyConfig } from '../strategy-configs'; +import type { BalancerJoinStrategyConfig } from '../strategy-configs'; import { type AmmEntityBalancer, isBalancerAmm } from '../../../../entities/zap'; import { selectAmmById } from '../../../../selectors/zap'; import { createFactory } from '../../../../utils/factory-utils'; import type { PoolConfig, VaultConfig } from '../../../amm/balancer/vault/types'; import { GyroEPool } from '../../../amm/balancer/gyroe/GyroEPool'; +import { WeightedPool } from '../../../amm/balancer/weighted/WeightedPool'; type ZapHelpers = { slippage: number; state: BeefyState; }; -const strategyId = 'balancer-pool' as const; +const strategyId = 'balancer-join' as const; type StrategyId = typeof strategyId; /** * Balancer: joinPool() to deposit / exitPool() to withdraw liquidity */ -class BalancerPoolStrategyImpl implements IZapStrategy { +class BalancerJoinStrategyImpl implements IZapStrategy { public static readonly id = strategyId; public readonly id = strategyId; @@ -107,7 +108,7 @@ class BalancerPoolStrategyImpl implements IZapStrategy { protected readonly amm: AmmEntityBalancer; constructor( - protected options: BalancerPoolStrategyConfig, + protected options: BalancerJoinStrategyConfig, protected helpers: ZapTransactHelpers ) { const { vault, vaultType, getState } = this.helpers; @@ -143,11 +144,19 @@ class BalancerPoolStrategyImpl implements IZapStrategy { this.chain = selectChainById(state, vault.chainId); this.poolTokens = this.selectPoolTokens(state, this.chain.id, this.options.tokens); - if (this.options.poolType === 'gyroe') { - this.checkPoolTokensCount(2); - this.checkPoolTokensHavePrice(state); - } else { - throw new Error(`Unsupported balancer pool type ${this.options.poolType}`); + switch (this.options.poolType) { + case 'gyroe': { + this.checkPoolTokensCount(2); + this.checkPoolTokensHavePrice(state); + break; + } + case 'weighted': { + this.checkPoolTokensHavePrice(state); + break; + } + default: { + throw new Error(`Unsupported balancer pool type ${this.options.poolType}`); + } } } @@ -235,6 +244,9 @@ class BalancerPoolStrategyImpl implements IZapStrategy { case 'gyroe': { return new GyroEPool(this.chain, vault, pool); } + case 'weighted': { + return new WeightedPool(this.chain, vault, pool); + } default: { throw new Error(`Unsupported balancer pool type ${this.options.poolType}`); } @@ -247,7 +259,7 @@ class BalancerPoolStrategyImpl implements IZapStrategy { ): Promise { const input = onlyOneInput(inputs); if (input.amount.lte(BIG_ZERO)) { - throw new Error('BalancerPoolStrategy: Quote called with 0 input amount'); + throw new Error('BalancerJoinStrategy: Quote called with 0 input amount'); } if (option.via === 'aggregator') { @@ -262,12 +274,23 @@ class BalancerPoolStrategyImpl implements IZapStrategy { input: TokenAmount ): Promise> { const pool = this.getPool(); - const ratio = await pool.getSwapRatio(); - console.debug('ratio', ratio.toString()); + const ratios = await pool.getSwapRatios(); + console.debug('ratios', ratios.toString()); + if (ratios.length !== this.poolTokens.length) { + throw new Error('BalancerJoinStrategy: Ratios length mismatch'); + } + const inputAmountWei = toWeiFromTokenAmount(input); - const amount0 = inputAmountWei.multipliedBy(ratio).decimalPlaces(0, BigNumber.ROUND_FLOOR); - const amount1 = inputAmountWei.minus(amount0); - const swapAmounts = [amount0, amount1]; + const lastIndex = ratios.length - 1; + const swapAmounts = ratios.map((ratio, i) => + i === lastIndex + ? BIG_ZERO + : inputAmountWei.multipliedBy(ratio).integerValue(BigNumber.ROUND_FLOOR) + ); + swapAmounts[swapAmounts.length - 1] = swapAmounts.reduce( + (acc, amount) => acc.minus(amount), + inputAmountWei + ); return this.poolTokens.map((token, i) => ({ from: fromWeiToTokenAmount(swapAmounts[i], input.token), @@ -470,20 +493,24 @@ class BalancerPoolStrategyImpl implements IZapStrategy { protected async fetchZapBuild( quoteStep: ZapQuoteStepBuild, - minInputs: TokenAmount[] + minInputs: TokenAmount[], + zapHelpers: ZapHelpers ): Promise { - const quote = await this.quoteAddLiquidity(minInputs); + const { liquidity, usedInput, unusedInput } = await this.quoteAddLiquidity(minInputs); const pool = this.getPool(); + const minLiquidity = pool.joinSupportsSlippage + ? slipTokenAmountBy(liquidity, zapHelpers.slippage) + : liquidity; return { - inputs: quote.usedInput, - outputs: [quote.liquidity], - minOutputs: [quote.liquidity], // TODO gyro can't slip, it just fails but maybe other pool types can - returned: quote.unusedInput, + inputs: usedInput, + outputs: [liquidity], + minOutputs: [minLiquidity], + returned: unusedInput, zaps: [ await pool.getAddLiquidityZap( - quote.usedInput.map(input => toWeiFromTokenAmount(input)), - toWeiFromTokenAmount(quote.liquidity), + usedInput.map(input => toWeiFromTokenAmount(input)), + toWeiFromTokenAmount(minLiquidity), this.helpers.zap.router, true ), @@ -508,18 +535,18 @@ class BalancerPoolStrategyImpl implements IZapStrategy { const buildQuote = quote.steps.find(isZapQuoteStepBuild); if (!buildQuote) { - throw new Error('BalancerPoolStrategy: No build step in quote'); + throw new Error('BalancerJoinStrategy: No build step in quote'); } // since there are two tokens, there must be at least 1 swap if (swapQuotes.length < 1) { - throw new Error('BalancerPoolStrategy: Not enough swaps'); + throw new Error('BalancerJoinStrategy: Not enough swaps'); } // Swaps if (swapQuotes.length) { - if (swapQuotes.length > 2) { - throw new Error('BalancerPoolStrategy: Too many swaps'); + if (swapQuotes.length > this.poolTokens.length) { + throw new Error('BalancerJoinStrategy: Too many swaps'); } const insertBalance = allTokensAreDistinct( @@ -545,7 +572,8 @@ class BalancerPoolStrategyImpl implements IZapStrategy { buildQuote.inputs.map(({ token }) => ({ token, amount: minBalances.get(token), // we have to pass min expected in case swaps slipped - })) + })), + zapHelpers ); console.debug('fetchDepositStep::buildZap', bigNumberToStringDeep(buildZap)); buildZap.zaps.forEach(step => steps.push(step)); @@ -1028,5 +1056,5 @@ class BalancerPoolStrategyImpl implements IZapStrategy { } } -export const BalancerPoolStrategy = - BalancerPoolStrategyImpl satisfies IZapStrategyStatic; +export const BalancerJoinStrategy = + BalancerJoinStrategyImpl satisfies IZapStrategyStatic; diff --git a/src/features/data/apis/transact/strategies/balancer/BalancerSwapStrategy.ts b/src/features/data/apis/transact/strategies/balancer/BalancerSwapStrategy.ts index 1f85697eb..dfd54be83 100644 --- a/src/features/data/apis/transact/strategies/balancer/BalancerSwapStrategy.ts +++ b/src/features/data/apis/transact/strategies/balancer/BalancerSwapStrategy.ts @@ -307,7 +307,7 @@ class BalancerSwapStrategyImpl implements IZapStrategy { ): Promise { if (!isTokenEqual(input.token, depositVia.token)) { throw new Error( - `Balancer strategy: Direct deposit called with input token ${input.token.symbol} but expected ${depositVia.token.symbol}` + `BalancerSwapStrategy: Direct deposit called with input token ${input.token.symbol} but expected ${depositVia.token.symbol}` ); } @@ -391,7 +391,7 @@ class BalancerSwapStrategyImpl implements IZapStrategy { const state = getState(); const input = onlyOneInput(inputs); if (input.amount.lte(BIG_ZERO)) { - throw new Error('BalancerStrategy: Quote called with 0 input amount'); + throw new Error('BalancerSwapStrategy: Quote called with 0 input amount'); } // Token allowances @@ -548,18 +548,18 @@ class BalancerSwapStrategyImpl implements IZapStrategy { const buildQuote = quote.steps.find(isZapQuoteStepBuild); if (!buildQuote) { - throw new Error('BalancerStrategy: No build step in quote'); + throw new Error('BalancerSwapStrategy: No build step in quote'); } // wrap and asset swap, 2 max if (swapQuotes.length > 2) { - throw new Error('BalancerStrategy: Too many swaps'); + throw new Error('BalancerSwapStrategy: Too many swaps'); } // Swaps if (swapQuotes.length) { if (swapQuotes.length > 1) { - throw new Error('BalancerStrategy: Too many swaps in quote'); + throw new Error('BalancerSwapStrategy: Too many swaps in quote'); } const swapQuote = swapQuotes[0]; diff --git a/src/features/data/apis/transact/strategies/index.ts b/src/features/data/apis/transact/strategies/index.ts index b90ee325d..64002e4d9 100644 --- a/src/features/data/apis/transact/strategies/index.ts +++ b/src/features/data/apis/transact/strategies/index.ts @@ -22,8 +22,8 @@ const strategyLoadersByIdUnchecked = { (await import('./RewardPoolToVaultStrategy')).RewardPoolToVaultStrategy, 'balancer-swap': async () => (await import('./balancer/BalancerSwapStrategy')).BalancerSwapStrategy, - 'balancer-pool': async () => - (await import('./balancer/BalancerPoolStrategy')).BalancerPoolStrategy, + 'balancer-join': async () => + (await import('./balancer/BalancerJoinStrategy')).BalancerJoinStrategy, } as const satisfies Record Promise>; type StrategyIdToStaticPromise = typeof strategyLoadersByIdUnchecked; diff --git a/src/features/data/apis/transact/strategies/strategy-configs.ts b/src/features/data/apis/transact/strategies/strategy-configs.ts index 005858612..42c819d30 100644 --- a/src/features/data/apis/transact/strategies/strategy-configs.ts +++ b/src/features/data/apis/transact/strategies/strategy-configs.ts @@ -46,11 +46,11 @@ export type BalancerSwapStrategyConfig = { tokens: string[]; } & OptionalStrategySwapConfig; -export type BalancerPoolStrategyConfig = { - strategyId: 'balancer-pool'; +export type BalancerJoinStrategyConfig = { + strategyId: 'balancer-join'; ammId: AmmEntityBalancer['id']; poolId: string; - poolType: 'gyroe'; + poolType: 'gyroe' | 'weighted'; tokens: string[]; } & OptionalStrategySwapConfig; @@ -93,7 +93,7 @@ export type ZapStrategyConfig = | VaultComposerStrategyConfig | RewardPoolToVaultStrategyConfig | BalancerSwapStrategyConfig - | BalancerPoolStrategyConfig; + | BalancerJoinStrategyConfig; export type ZapStrategyId = ZapStrategyConfig['strategyId']; diff --git a/src/features/data/apis/transact/transact-types.ts b/src/features/data/apis/transact/transact-types.ts index 427fc61e5..96765e622 100644 --- a/src/features/data/apis/transact/transact-types.ts +++ b/src/features/data/apis/transact/transact-types.ts @@ -193,12 +193,12 @@ export type BalancerSwapWithdrawOption = ZapBaseWithdrawOption & { ); export type BalancerPoolDepositOption = ZapBaseDepositOption & { - strategyId: 'balancer-pool'; + strategyId: 'balancer-join'; via: /*'pool' | */ 'aggregator'; }; export type BalancerPoolWithdrawOption = ZapBaseWithdrawOption & { - strategyId: 'balancer-pool'; + strategyId: 'balancer-join'; via: 'break-only' | /*'pool' | */ 'aggregator'; }; diff --git a/src/features/vault/components/Actions/Transact/TransactDebugger/BalancerZap.tsx b/src/features/vault/components/Actions/Transact/TransactDebugger/BalancerZap.tsx index f21909acb..b0e658328 100644 --- a/src/features/vault/components/Actions/Transact/TransactDebugger/BalancerZap.tsx +++ b/src/features/vault/components/Actions/Transact/TransactDebugger/BalancerZap.tsx @@ -32,7 +32,7 @@ const BalancerZap = memo(function BalancerZap({ vaultId }) { const zap = isStandardVault(vault) ? vault.zaps.find( (zap): zap is BalancerSwapStrategyConfig => - zap.strategyId === 'balancer-swap' || zap.strategyId === 'balancer-pool' + zap.strategyId === 'balancer-swap' || zap.strategyId === 'balancer-join' ) : undefined; From 605ae41ac381e721ac499044d899d8d169696e92 Mon Sep 17 00:00:00 2001 From: ReflectiveChimp <55021052+ReflectiveChimp@users.noreply.github.com> Date: Thu, 19 Sep 2024 14:18:35 +0100 Subject: [PATCH 07/20] meta-stable - via aggregator only --- scripts/addBalancerZap.ts | 358 ++++++++-- scripts/common/files.ts | 60 +- src/config/abi/BalancerMetaStablePoolAbi.ts | 610 ++++++++++++++++++ src/config/vault/arbitrum.json | 102 +-- src/config/vault/ethereum.json | 16 +- src/config/vault/optimism.json | 16 +- .../{gyroe/GyroEPool.ts => gyro/GyroPool.ts} | 35 +- .../amm/balancer/{gyroe => gyro}/types.ts | 0 .../balancer/{weighted => join}/FixedPoint.ts | 0 .../JoinExitEncoder.ts} | 4 +- .../data/apis/amm/balancer/join/JoinPool.ts | 219 +++++++ .../balancer/meta-stable/MetaStablePool.ts | 54 ++ .../amm/balancer/weighted/WeightedMath.ts | 2 +- .../amm/balancer/weighted/WeightedPool.ts | 207 +----- .../balancer/BalancerJoinStrategy.ts | 15 +- .../transact/strategies/strategy-configs.ts | 2 +- 16 files changed, 1346 insertions(+), 354 deletions(-) create mode 100644 src/config/abi/BalancerMetaStablePoolAbi.ts rename src/features/data/apis/amm/balancer/{gyroe/GyroEPool.ts => gyro/GyroPool.ts} (87%) rename src/features/data/apis/amm/balancer/{gyroe => gyro}/types.ts (100%) rename src/features/data/apis/amm/balancer/{weighted => join}/FixedPoint.ts (100%) rename src/features/data/apis/amm/balancer/{weighted/WeightedPoolEncoder.ts => join/JoinExitEncoder.ts} (96%) create mode 100644 src/features/data/apis/amm/balancer/join/JoinPool.ts create mode 100644 src/features/data/apis/amm/balancer/meta-stable/MetaStablePool.ts diff --git a/scripts/addBalancerZap.ts b/scripts/addBalancerZap.ts index d76c12ec1..11d53acf6 100644 --- a/scripts/addBalancerZap.ts +++ b/scripts/addBalancerZap.ts @@ -15,15 +15,21 @@ import { OptionalRecord } from '../src/features/data/utils/types-utils'; import { Address, createPublicClient, getAddress, Hex, http, parseAbi } from 'viem'; import PQueue from 'p-queue'; import path, { dirname } from 'node:path'; -import { fileReadable, loadJson, saveJson } from './common/files'; +import { + fileReadable, + loadJson, + loadJsonSupportingBigInt, + saveJson, + saveJsonSupportingBigInt, +} from './common/files'; import { mkdir } from 'node:fs/promises'; -import { createCachedFactory } from '../src/features/data/utils/factory-utils'; -import { addressBook } from 'blockchain-addressbook'; -import { sortBy } from 'lodash'; +import { createCachedFactory, createFactory } from '../src/features/data/utils/factory-utils'; +import { addressBook, Token } from 'blockchain-addressbook'; +import { partition, sortBy } from 'lodash'; import platforms from '../src/config/platforms.json'; import { + BalancerJoinStrategyConfig, BalancerSwapStrategyConfig, - BalancerPoolStrategyConfig, } from '../src/features/data/apis/transact/strategies/strategy-configs'; import { sortVaultKeys } from './common/vault-fields'; @@ -79,7 +85,16 @@ type BalancerApiPool = { owner: TAddress; }; -type Pool = BalancerApiPool & { vaultAddress: Address; chainId: AppChainId }; +type RpcPool = { + poolId: Hex; + vaultAddress: Address; + chainId: AppChainId; + tokenRates?: readonly [bigint, bigint]; + normalizedWeights?: readonly bigint[]; + scalingFactors?: readonly bigint[]; +}; + +type Pool = RpcPool & BalancerApiPool; const chainIdToBalancerChainId: OptionalRecord = { ethereum: 'MAINNET', @@ -101,8 +116,8 @@ const supportedPoolTypes: OptionalRecord { + const response = await fetch(`https://api.beefy.finance/zap/swaps?_=${Date.now()}`); + return (await response.json()) as ZapSwaps; +}); + +type TokenPrices = { + [oracleId: string]: number; +}; + +const getTokenPrices = createFactory(async () => { + const prices = await Promise.all( + ['prices', 'lps'].map(async type => { + const response = await fetch(`https://api.beefy.finance/${type}?_=${Date.now()}`); + return (await response.json()) as TokenPrices; + }) + ); + return Object.assign({}, ...prices) as TokenPrices; +}); + function createViemClient(chainId: AppChainId, chain: ChainConfig) { return createPublicClient({ batch: { @@ -248,7 +290,7 @@ function withFileCache any>( if (!forceUpdate) { try { if (await fileReadable(cachePath)) { - return await loadJson(cachePath); + return await loadJsonSupportingBigInt(cachePath); } } catch (e) { console.error('Failed to read cache', cachePath, e); @@ -257,26 +299,55 @@ function withFileCache any>( const data = await factoryFn(...args); await mkdir(dirname(cachePath), { recursive: true }); - await saveJson(cachePath, data, true); + await saveJsonSupportingBigInt(cachePath, data, true); return data; }; } +function fulfilledOr( + result: PromiseSettledResult, + defaultValue: TDefault +): TResult | TDefault { + return result.status === 'fulfilled' ? result.value : defaultValue; +} + const fetchPoolRpcData = withFileCache( - async (poolAddress: Address, chainId: AppChainId) => { + async (poolAddress: Address, chainId: AppChainId): Promise => { const client = getViemClient(chainId); - const [poolId, vaultAddress] = await Promise.all([ - client.readContract({ - address: poolAddress, - abi: parseAbi(['function getPoolId() public view returns (bytes32)']), - functionName: 'getPoolId', - }), - client.readContract({ - address: poolAddress, - abi: parseAbi(['function getVault() public view returns (address)']), - functionName: 'getVault', - }), - ]); + const [poolIdRes, vaultAddressRes, tokenRatesRes, normalizedWeightsRes, scalingFactorsRes] = + await Promise.allSettled([ + client.readContract({ + address: poolAddress, + abi: parseAbi(['function getPoolId() public view returns (bytes32)']), + functionName: 'getPoolId', + }), + client.readContract({ + address: poolAddress, + abi: parseAbi(['function getVault() public view returns (address)']), + functionName: 'getVault', + }), + client.readContract({ + address: poolAddress, + abi: parseAbi(['function getTokenRates() public view returns (uint256,uint256)']), + functionName: 'getTokenRates', + }), + client.readContract({ + address: poolAddress, + abi: parseAbi(['function getNormalizedWeights() public view returns (uint256[])']), + functionName: 'getNormalizedWeights', + }), + client.readContract({ + address: poolAddress, + abi: parseAbi(['function getScalingFactors() public view returns (uint256[])']), + functionName: 'getScalingFactors', + }), + ]); + + const vaultAddress = fulfilledOr(vaultAddressRes, undefined); + const poolId = fulfilledOr(poolIdRes, undefined); + const tokenRates = fulfilledOr(tokenRatesRes, undefined); + const normalizedWeights = fulfilledOr(normalizedWeightsRes, undefined); + const scalingFactors = fulfilledOr(scalingFactorsRes, undefined); if (!vaultAddress || vaultAddress === ZERO_ADDRESS) { throw new Error(`No vault address found via vault.want().getVault()`); @@ -285,7 +356,7 @@ const fetchPoolRpcData = withFileCache( throw new Error(`No pool id found via vault.want().getPoolId()`); } - return { poolId, vaultAddress }; + return { poolId, vaultAddress, chainId, tokenRates, normalizedWeights, scalingFactors }; }, (poolAddress: Address, chainId: AppChainId) => path.join(cacheRpcPath, chainId, `pool-${poolAddress}.json`) @@ -300,7 +371,6 @@ const balancerApiQueue = new PQueue({ throwOnTimeout: true, }); -// TODO remove balancerApiQueue.on('next', () => { console.log( `[BalancerApi] Pending: ${balancerApiQueue.pending + 1} Size: ${balancerApiQueue.size}` @@ -403,7 +473,7 @@ const getPoolRpcData = createCachedFactory( (_, poolAddress: Address, chainId: AppChainId) => `${chainId}:${poolAddress}` ); -function checkPoolTokensAgainstAddressBook(pool: Pool): void { +function checkPoolTokensAgainstAddressBook(pool: Pool, allRequired: boolean): boolean { const { tokenAddressMap } = addressBook[appToAddressBookId(pool.chainId)]; // Tokens in the pool that are not the pool token const tokens = pool.poolTokens @@ -425,16 +495,20 @@ function checkPoolTokensAgainstAddressBook(pool: Pool): void { }); // Tokens in address book - const zapTokens = tokens.filter( - (t): t is Extract => t.inAddressBook + const [zapTokens, missingTokens] = partition( + tokens, + ( + t + ): t is Extract< + typeof t, + { + inAddressBook: true; + } + > => t.inAddressBook ); if (!zapTokens.length) { throw new Error( - `No tokens [${tokens - .map(t => `${t.poolToken.symbol} (${t.poolToken.address})`) - .join(', ')}] found in ${ - pool.chainId - } address book. [At least 1 non-nested pool token is requried for zap]` + `No tokens [${missingTokens.join(', ')}] found in ${pool.chainId} address book.` ); } @@ -447,20 +521,197 @@ function checkPoolTokensAgainstAddressBook(pool: Pool): void { }) .filter(isDefined); if (tokenErrors.length) { - throw new Error('Token errors:\n' + tokenErrors.join('\n')); + throw new Error(`${pool.type}: Token errors:\n${tokenErrors.join('\n')}`); } if (zapTokens.length !== tokens.length) { - console.warn(`Some tokens not found in address book:`); + const message = `${pool.type}: Some tokens not found in address book:\n${missingTokens + .map(({ poolToken }) => ` ${poolToken.symbol} (${poolToken.address})`) + .join('\n')}`; + + if (allRequired) { + throw new Error(message); + } else { + console.warn(message); + } + } + + return true; +} + +type PoolToken = BalancerPoolToken
& { + abToken?: Token; + price?: number; + swapProviders: string[]; + isBPT: boolean; +}; + +function getPoolTokens(pool: Pool, prices: TokenPrices, swaps: ZapSwaps) { + const { tokenAddressMap } = addressBook[appToAddressBookId(pool.chainId)]; + const tokens: PoolToken[] = []; + const tokensWithBpt: PoolToken[] = []; + + for (const poolToken of pool.poolTokens) { + const isBPT = poolToken.address === pool.address; + const abToken = tokenAddressMap[poolToken.address]; + if (abToken && abToken.decimals !== poolToken.decimals) { + throw new Error( + `Address book token decimals mismatch ${poolToken.symbol} (${poolToken.address}) ${poolToken.decimals} vs ${abToken.decimals}` + ); + } + const price = abToken ? prices[abToken.oracleId] : undefined; + const swapProviders = Object.entries(swaps[pool.chainId]?.[poolToken.address] || {}) + .filter(([, v]) => v) + .map(([k]) => k); + const token = { + ...poolToken, + isBPT, + abToken, + price, + swapProviders, + }; + + if (!isBPT) { + tokens.push(token); + } + tokensWithBpt.push(token); + } + + return { tokens, tokensWithBpt }; +} + +function logTokens(tokens: PoolToken[]) { + console.table( + tokens.map(t => ({ + symbol: t.symbol, + address: t.address, + addressBook: !!t.abToken, + price: t.price, + swapProviders: t.swapProviders.length ? t.swapProviders : false, + })) + ); +} + +function checkWeightedPool(pool: Pool, tokens: PoolToken[], tokensWithBpt: PoolToken[]): boolean { + if (tokensWithBpt.length !== tokens.length) { + throw new Error(`${pool.type}: Did not expect BPT token in pool`); + } + + // TODO we might be able to single sided join in future + if (tokens.some(t => !t.abToken || !t.price || !t.swapProviders.length)) { + logTokens(tokens); + throw new Error( + `${pool.type}: All tokens must be in the address book, have a price, and have a zap swap provider` + ); + } + + if (!pool.normalizedWeights) { + throw new Error(`${pool.type}: Tokens must have normalized weights`); + } + + if (pool.normalizedWeights.length !== pool.poolTokens.length) { + throw new Error( + `${pool.type}: Normalized weights length ${pool.normalizedWeights.length} does not match tokens length ${pool.poolTokens.length}` + ); + } + + return true; +} + +function checkGyroPool(pool: Pool, tokens: PoolToken[], tokensWithBpt: PoolToken[]): boolean { + if (tokensWithBpt.length !== tokens.length) { + throw new Error(`${pool.type}: Did not expect BPT token in pool`); + } + + // Gyro always needs to join with all tokens + if (tokens.some(t => !t.abToken || !t.price || !t.swapProviders.length)) { + logTokens(tokens); + throw new Error( + `${pool.type}: All tokens must be in the address book, have a price, and have a zap swap provider` + ); + } + + if (pool.poolTokens.length !== 2) { + throw new Error(`${pool.type}: Must have 2 tokens [${pool.poolTokens.length} found]`); + } + + if (!pool.tokenRates) { + throw new Error(`${pool.type}: Must have token rates`); + } + + if (pool.tokenRates.length !== pool.poolTokens.length) { + throw new Error( + `${pool.type}: Token rates length ${pool.tokenRates.length} does not match tokens length ${pool.poolTokens.length}` + ); + } + + return true; +} + +function checkMetaStablePool(pool: Pool, tokens: PoolToken[], tokensWithBpt: PoolToken[]): boolean { + if (tokensWithBpt.length !== tokens.length) { + throw new Error(`${pool.type}: Did not expect BPT token in pool`); + } + + // TODO we might be able to single sided join in future + if (tokens.some(t => !t.abToken || !t.price || !t.swapProviders.length)) { + logTokens(tokens); + throw new Error( + `${pool.type}: All tokens must be in the address book, have a price, and have a zap swap provider` + ); + } + + if (!pool.scalingFactors) { + throw new Error(`${pool.type}: Pool must have scaling factors`); + } + + if (pool.scalingFactors.length !== pool.poolTokens.length) { + throw new Error( + `${pool.type}: Scaling factors length ${pool.scalingFactors.length} does not match tokens length ${pool.poolTokens.length}` + ); + } + + return true; +} + +function checkComposableStablePool( + pool: Pool, + tokens: PoolToken[], + tokensWithBpt: PoolToken[] +): boolean { + if (tokens.length !== tokensWithBpt.length - 1) { + throw new Error( + `${pool.type}: Expected 1 BPT token [${tokensWithBpt.length - tokens.length} found]` + ); + } + + if (tokens.every(t => !t.abToken || !t.price)) { + logTokens(tokens); + throw new Error( + `${pool.type}: At least one token must be in the address book and have a price` + ); + } + + if (tokens.every(t => !t.abToken || !t.price || !t.swapProviders.length)) { console.warn( - `${tokens - .filter(t => !t.inAddressBook) - .map(({ poolToken }) => ` ${poolToken.symbol} (${poolToken.address})`) - .join('\n')}` + `${pool.type}: No tokens are in the address book, have a price, and have a zap swap provider - only pool tokens will be available for deposit` ); } + + return true; } +const poolTypeToChecker: Record< + BalancerPoolType, + (pool: Pool, tokens: PoolToken[], tokensWithBpt: PoolToken[]) => boolean +> = { + GYRO: checkGyroPool, + GYROE: checkGyroPool, + WEIGHTED: checkWeightedPool, + COMPOSABLE_STABLE: checkComposableStablePool, + META_STABLE: checkMetaStablePool, +}; + async function findAmmForPool(pool: Pool, tokenProviderId: string): Promise { const platform = platforms.find(p => p.id === tokenProviderId); if (!platform) { @@ -516,18 +767,17 @@ export async function discoverBalancerZap(args: RunArgs) { throw new Error(`No balancer chain id found for chain ${chainId}`); } - const { poolId, vaultAddress } = await getPoolRpcData(!!args.update, poolAddress, chainId); + const rpcPool = await getPoolRpcData(!!args.update, poolAddress, chainId); if (!args.quiet) { console.log('=== Pool ==='); - console.log('Id:', poolId); - console.log('Vault:', vaultAddress); + console.log('Id:', rpcPool.poolId); + console.log('Vault:', rpcPool.vaultAddress); } - const apiPool = await getPoolApiData(!!args.update, poolId, balancerChainId); + const apiPool = await getPoolApiData(!!args.update, rpcPool.poolId, balancerChainId); const pool: Pool = { + ...rpcPool, ...apiPool, - chainId, - vaultAddress, }; if (!args.quiet) { @@ -547,7 +797,15 @@ export async function discoverBalancerZap(args: RunArgs) { ); } - checkPoolTokensAgainstAddressBook(pool); + const [swaps, prices] = await Promise.all([getZapSwaps(), getTokenPrices()]); + const { tokens, tokensWithBpt } = getPoolTokens(pool, prices, swaps); + const checker = poolTypeToChecker[pool.type]; + if (!checker) { + throw new Error(`No checker found for pool type ${pool.type}`); + } + if (!checker(pool, tokens, tokensWithBpt)) { + throw new Error(`Checker failed for pool type ${pool.type}`); + } const amm = await findAmmForPool(pool, vault.tokenProviderId); if (!args.quiet) { @@ -569,7 +827,9 @@ export async function discoverBalancerZap(args: RunArgs) { tokens: apiPool.poolTokens.map(t => t.address), } satisfies BalancerSwapStrategyConfig; } + case 'GYRO': case 'GYROE': + case 'META_STABLE': case 'WEIGHTED': { return { strategyId: 'balancer-join', @@ -577,9 +837,9 @@ export async function discoverBalancerZap(args: RunArgs) { poolId: apiPool.id, poolType: apiPool.type .toLowerCase() - .replaceAll('_', '-') as BalancerPoolStrategyConfig['poolType'], // TODO types + .replaceAll('_', '-') as BalancerJoinStrategyConfig['poolType'], // TODO types tokens: apiPool.poolTokens.map(t => t.address), - } satisfies BalancerPoolStrategyConfig; + } satisfies BalancerJoinStrategyConfig; } default: { throw new Error(`Unsupported pool type ${pool.type}`); @@ -590,7 +850,7 @@ export async function discoverBalancerZap(args: RunArgs) { async function saveZap( chainId: string, vaultId: string, - zap: BalancerSwapStrategyConfig | BalancerPoolStrategyConfig + zap: BalancerSwapStrategyConfig | BalancerJoinStrategyConfig ) { const path = `./src/config/vault/${addressBookToAppId(chainId)}.json`; const vaults = await loadJson(path); diff --git a/scripts/common/files.ts b/scripts/common/files.ts index a955b34e3..f1218d5bc 100644 --- a/scripts/common/files.ts +++ b/scripts/common/files.ts @@ -2,6 +2,9 @@ import { createWriteStream } from 'node:fs'; import { access, constants, readFile, writeFile } from 'node:fs/promises'; import * as prettier from 'prettier'; +type JsonReplacer = (key: string, value: any) => any; +type JsonReviver = (key: string, value: any) => any; + export async function fileAccess(path: string, mode: number): Promise { try { await access(path, mode); @@ -30,34 +33,39 @@ export async function saveString(path: string, data: string) { async function formatJson( path: string, json: DataType, - pretty: boolean | 'prettier' = false + pretty: boolean | 'prettier' = false, + replacer?: JsonReplacer ): Promise { switch (pretty) { case 'prettier': const config = await prettier.resolveConfig(path); - return prettier.format(JSON.stringify(json, null, 2), { ...config, filepath: path }); + return prettier.format(JSON.stringify(json, replacer, 2), { ...config, filepath: path }); case true: - return JSON.stringify(json, null, 2); + return JSON.stringify(json, replacer, 2); default: - return JSON.stringify(json); + return JSON.stringify(json, replacer); } } export async function saveJson( path: string, json: DataType, - pretty: boolean | 'prettier' = false + pretty: boolean | 'prettier' = false, + replacer?: JsonReplacer ) { - return saveString(path, await formatJson(path, json, pretty)); + return saveString(path, await formatJson(path, json, pretty, replacer)); } export async function loadString(path: string): Promise { return await readFile(path, 'utf-8'); } -export async function loadJson(path: string): Promise { +export async function loadJson( + path: string, + reviver?: JsonReviver +): Promise { const json = await readFile(path, 'utf-8'); - return JSON.parse(json); + return JSON.parse(json, reviver); } function escapeCsvValue(input: string) { @@ -66,6 +74,42 @@ function escapeCsvValue(input: string) { return `"${escaped}"`; } +export function stringifyBigInt(_key: string, value: any): any { + if (typeof value === 'bigint') { + return `${value.toString(10)}n`; + } + + return value; +} + +export function parseBigInt(_key: string, value: any): any { + if (typeof value === 'string' && value.length > 1 && value[value.length - 1] === 'n') { + if (value === '0n') { + return 0n; + } + + if (value.match(/^[1-9][0-9]*n$/)) { + return BigInt(value.slice(0, -1)); + } + } + + return value; +} + +export async function saveJsonSupportingBigInt( + path: string, + json: DataType, + pretty: boolean | 'prettier' = false +) { + return saveJson(path, json, pretty, stringifyBigInt); +} + +export async function loadJsonSupportingBigInt( + path: string +): Promise { + return loadJson(path, parseBigInt); +} + export async function saveCsv>( path: string, data: T[], diff --git a/src/config/abi/BalancerMetaStablePoolAbi.ts b/src/config/abi/BalancerMetaStablePoolAbi.ts new file mode 100644 index 000000000..9757a8d42 --- /dev/null +++ b/src/config/abi/BalancerMetaStablePoolAbi.ts @@ -0,0 +1,610 @@ +import type { Abi } from 'viem'; + +export const BalancerMetaStablePoolAbi = [ + { + inputs: [ + { + components: [ + { internalType: 'contract IVault', name: 'vault', type: 'address' }, + { internalType: 'string', name: 'name', type: 'string' }, + { internalType: 'string', name: 'symbol', type: 'string' }, + { internalType: 'contract IERC20[]', name: 'tokens', type: 'address[]' }, + { internalType: 'contract IRateProvider[]', name: 'rateProviders', type: 'address[]' }, + { internalType: 'uint256[]', name: 'priceRateCacheDuration', type: 'uint256[]' }, + { internalType: 'uint256', name: 'amplificationParameter', type: 'uint256' }, + { internalType: 'uint256', name: 'swapFeePercentage', type: 'uint256' }, + { internalType: 'uint256', name: 'pauseWindowDuration', type: 'uint256' }, + { internalType: 'uint256', name: 'bufferPeriodDuration', type: 'uint256' }, + { internalType: 'bool', name: 'oracleEnabled', type: 'bool' }, + { internalType: 'address', name: 'owner', type: 'address' }, + ], + internalType: 'struct MetaStablePool.NewPoolParams', + name: 'params', + type: 'tuple', + }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'uint256', name: 'startValue', type: 'uint256' }, + { indexed: false, internalType: 'uint256', name: 'endValue', type: 'uint256' }, + { indexed: false, internalType: 'uint256', name: 'startTime', type: 'uint256' }, + { indexed: false, internalType: 'uint256', name: 'endTime', type: 'uint256' }, + ], + name: 'AmpUpdateStarted', + type: 'event', + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: 'uint256', name: 'currentValue', type: 'uint256' }], + name: 'AmpUpdateStopped', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'owner', type: 'address' }, + { indexed: true, internalType: 'address', name: 'spender', type: 'address' }, + { indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' }, + ], + name: 'Approval', + type: 'event', + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: 'bool', name: 'enabled', type: 'bool' }], + name: 'OracleEnabledChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: 'bool', name: 'paused', type: 'bool' }], + name: 'PausedStateChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'contract IERC20', name: 'token', type: 'address' }, + { indexed: false, internalType: 'uint256', name: 'rate', type: 'uint256' }, + ], + name: 'PriceRateCacheUpdated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'contract IERC20', name: 'token', type: 'address' }, + { indexed: true, internalType: 'contract IRateProvider', name: 'provider', type: 'address' }, + { indexed: false, internalType: 'uint256', name: 'cacheDuration', type: 'uint256' }, + ], + name: 'PriceRateProviderSet', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'uint256', name: 'swapFeePercentage', type: 'uint256' }, + ], + name: 'SwapFeePercentageChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'from', type: 'address' }, + { indexed: true, internalType: 'address', name: 'to', type: 'address' }, + { indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' }, + ], + name: 'Transfer', + type: 'event', + }, + { + inputs: [], + name: 'DOMAIN_SEPARATOR', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'address', name: 'spender', type: 'address' }, + ], + name: 'allowance', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'approve', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'decimals', + outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'decreaseAllowance', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'enableOracle', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes4', name: 'selector', type: 'bytes4' }], + name: 'getActionId', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getAmplificationParameter', + outputs: [ + { internalType: 'uint256', name: 'value', type: 'uint256' }, + { internalType: 'bool', name: 'isUpdating', type: 'bool' }, + { internalType: 'uint256', name: 'precision', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getAuthorizer', + outputs: [{ internalType: 'contract IAuthorizer', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getLargestSafeQueryWindow', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'pure', + type: 'function', + }, + { + inputs: [], + name: 'getLastInvariant', + outputs: [ + { internalType: 'uint256', name: 'lastInvariant', type: 'uint256' }, + { internalType: 'uint256', name: 'lastInvariantAmp', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'enum IPriceOracle.Variable', name: 'variable', type: 'uint8' }], + name: 'getLatest', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getOracleMiscData', + outputs: [ + { internalType: 'int256', name: 'logInvariant', type: 'int256' }, + { internalType: 'int256', name: 'logTotalSupply', type: 'int256' }, + { internalType: 'uint256', name: 'oracleSampleCreationTimestamp', type: 'uint256' }, + { internalType: 'uint256', name: 'oracleIndex', type: 'uint256' }, + { internalType: 'bool', name: 'oracleEnabled', type: 'bool' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getOwner', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'enum IPriceOracle.Variable', name: 'variable', type: 'uint8' }, + { internalType: 'uint256', name: 'ago', type: 'uint256' }, + ], + internalType: 'struct IPriceOracle.OracleAccumulatorQuery[]', + name: 'queries', + type: 'tuple[]', + }, + ], + name: 'getPastAccumulators', + outputs: [{ internalType: 'int256[]', name: 'results', type: 'int256[]' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getPausedState', + outputs: [ + { internalType: 'bool', name: 'paused', type: 'bool' }, + { internalType: 'uint256', name: 'pauseWindowEndTime', type: 'uint256' }, + { internalType: 'uint256', name: 'bufferPeriodEndTime', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getPoolId', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'contract IERC20', name: 'token', type: 'address' }], + name: 'getPriceRateCache', + outputs: [ + { internalType: 'uint256', name: 'rate', type: 'uint256' }, + { internalType: 'uint256', name: 'duration', type: 'uint256' }, + { internalType: 'uint256', name: 'expires', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getRate', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getRateProviders', + outputs: [{ internalType: 'contract IRateProvider[]', name: 'providers', type: 'address[]' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'index', type: 'uint256' }], + name: 'getSample', + outputs: [ + { internalType: 'int256', name: 'logPairPrice', type: 'int256' }, + { internalType: 'int256', name: 'accLogPairPrice', type: 'int256' }, + { internalType: 'int256', name: 'logBptPrice', type: 'int256' }, + { internalType: 'int256', name: 'accLogBptPrice', type: 'int256' }, + { internalType: 'int256', name: 'logInvariant', type: 'int256' }, + { internalType: 'int256', name: 'accLogInvariant', type: 'int256' }, + { internalType: 'uint256', name: 'timestamp', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getScalingFactors', + outputs: [{ internalType: 'uint256[]', name: '', type: 'uint256[]' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getSwapFeePercentage', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'enum IPriceOracle.Variable', name: 'variable', type: 'uint8' }, + { internalType: 'uint256', name: 'secs', type: 'uint256' }, + { internalType: 'uint256', name: 'ago', type: 'uint256' }, + ], + internalType: 'struct IPriceOracle.OracleAverageQuery[]', + name: 'queries', + type: 'tuple[]', + }, + ], + name: 'getTimeWeightedAverage', + outputs: [{ internalType: 'uint256[]', name: 'results', type: 'uint256[]' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getTotalSamples', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'pure', + type: 'function', + }, + { + inputs: [], + name: 'getVault', + outputs: [{ internalType: 'contract IVault', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'addedValue', type: 'uint256' }, + ], + name: 'increaseAllowance', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'name', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'owner', type: 'address' }], + name: 'nonces', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'uint256', name: 'protocolSwapFeePercentage', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'onExitPool', + outputs: [ + { internalType: 'uint256[]', name: 'amountsOut', type: 'uint256[]' }, + { internalType: 'uint256[]', name: 'dueProtocolFeeAmounts', type: 'uint256[]' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'uint256', name: 'protocolSwapFeePercentage', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'onJoinPool', + outputs: [ + { internalType: 'uint256[]', name: 'amountsIn', type: 'uint256[]' }, + { internalType: 'uint256[]', name: 'dueProtocolFeeAmounts', type: 'uint256[]' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'enum IVault.SwapKind', name: 'kind', type: 'uint8' }, + { internalType: 'contract IERC20', name: 'tokenIn', type: 'address' }, + { internalType: 'contract IERC20', name: 'tokenOut', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'address', name: 'from', type: 'address' }, + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + internalType: 'struct IPoolSwapStructs.SwapRequest', + name: 'request', + type: 'tuple', + }, + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { internalType: 'uint256', name: 'indexIn', type: 'uint256' }, + { internalType: 'uint256', name: 'indexOut', type: 'uint256' }, + ], + name: 'onSwap', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'enum IVault.SwapKind', name: 'kind', type: 'uint8' }, + { internalType: 'contract IERC20', name: 'tokenIn', type: 'address' }, + { internalType: 'contract IERC20', name: 'tokenOut', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'address', name: 'from', type: 'address' }, + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + internalType: 'struct IPoolSwapStructs.SwapRequest', + name: 'request', + type: 'tuple', + }, + { internalType: 'uint256', name: 'balanceTokenIn', type: 'uint256' }, + { internalType: 'uint256', name: 'balanceTokenOut', type: 'uint256' }, + ], + name: 'onSwap', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'value', type: 'uint256' }, + { internalType: 'uint256', name: 'deadline', type: 'uint256' }, + { internalType: 'uint8', name: 'v', type: 'uint8' }, + { internalType: 'bytes32', name: 'r', type: 'bytes32' }, + { internalType: 'bytes32', name: 's', type: 'bytes32' }, + ], + name: 'permit', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'uint256', name: 'protocolSwapFeePercentage', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'queryExit', + outputs: [ + { internalType: 'uint256', name: 'bptIn', type: 'uint256' }, + { internalType: 'uint256[]', name: 'amountsOut', type: 'uint256[]' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'uint256', name: 'protocolSwapFeePercentage', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'queryJoin', + outputs: [ + { internalType: 'uint256', name: 'bptOut', type: 'uint256' }, + { internalType: 'uint256[]', name: 'amountsIn', type: 'uint256[]' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'contract IERC20', name: 'token', type: 'address' }, + { internalType: 'bytes', name: 'poolConfig', type: 'bytes' }, + ], + name: 'setAssetManagerPoolConfig', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bool', name: 'paused', type: 'bool' }], + name: 'setPaused', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'contract IERC20', name: 'token', type: 'address' }, + { internalType: 'uint256', name: 'duration', type: 'uint256' }, + ], + name: 'setPriceRateCacheDuration', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'swapFeePercentage', type: 'uint256' }], + name: 'setSwapFeePercentage', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'rawEndValue', type: 'uint256' }, + { internalType: 'uint256', name: 'endTime', type: 'uint256' }, + ], + name: 'startAmplificationParameterUpdate', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'stopAmplificationParameterUpdate', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'symbol', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'totalSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'transfer', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'transferFrom', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'contract IERC20', name: 'token', type: 'address' }], + name: 'updatePriceRateCache', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +] as const satisfies Abi; diff --git a/src/config/vault/arbitrum.json b/src/config/vault/arbitrum.json index 39eb0cbb9..0d07642cf 100644 --- a/src/config/vault/arbitrum.json +++ b/src/config/vault/arbitrum.json @@ -69,19 +69,7 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xdeeaf8b0a8cf26217261b813e085418c7dd8f1ee00020000000000000000058f/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xdeeaf8b0a8cf26217261b813e085418c7dd8f1ee00020000000000000000058f/remove-liquidity", - "network": "arbitrum", - "zaps": [ - { - "strategyId": "balancer-join", - "ammId": "arbitrum-balancer", - "poolId": "0xdeeaf8b0a8cf26217261b813e085418c7dd8f1ee00020000000000000000058f", - "poolType": "gyroe", - "tokens": [ - "0x211Cc4DD073734dA055fbF44a2b4667d5E5fE5d2", - "0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8" - ] - } - ] + "network": "arbitrum" }, { "id": "aura-arb-agho-gyd", @@ -111,19 +99,7 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xff38cc0ce0de4476c5a3e78675b48420a851035b000200000000000000000593/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xff38cc0ce0de4476c5a3e78675b48420a851035b000200000000000000000593/remove-liquidity", - "network": "arbitrum", - "zaps": [ - { - "strategyId": "balancer-join", - "ammId": "arbitrum-balancer", - "poolId": "0xff38cc0ce0de4476c5a3e78675b48420a851035b000200000000000000000593", - "poolType": "gyroe", - "tokens": [ - "0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8", - "0xD9FBA68D89178e3538e708939332c79efC540179" - ] - } - ] + "network": "arbitrum" }, { "id": "aura-arb-ausdc-wusdm", @@ -1675,19 +1651,7 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x7272163a931dac5bbe1cb5fefaf959bb65f7346f000200000000000000000549/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x7272163a931dac5bbe1cb5fefaf959bb65f7346f000200000000000000000549/remove-liquidity", - "network": "arbitrum", - "zaps": [ - { - "strategyId": "balancer-join", - "ammId": "arbitrum-balancer", - "poolId": "0x7272163a931dac5bbe1cb5fefaf959bb65f7346f000200000000000000000549", - "poolType": "gyroe", - "tokens": [ - "0xb165a74407fE1e519d6bCbDeC1Ed3202B35a4140", - "0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8" - ] - } - ] + "network": "arbitrum" }, { "id": "aura-arb-ausdc-gyd", @@ -1717,19 +1681,7 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x6e822c64c00393b2078f2a5bb75c575ab505b55c000200000000000000000548/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x6e822c64c00393b2078f2a5bb75c575ab505b55c000200000000000000000548/remove-liquidity", - "network": "arbitrum", - "zaps": [ - { - "strategyId": "balancer-join", - "ammId": "arbitrum-balancer", - "poolId": "0x6e822c64c00393b2078f2a5bb75c575ab505b55c000200000000000000000548", - "poolType": "gyroe", - "tokens": [ - "0x7CFaDFD5645B50bE87d546f42699d863648251ad", - "0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8" - ] - } - ] + "network": "arbitrum" }, { "id": "pendle-arb-usde-28nov24", @@ -1784,19 +1736,7 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x315dd595e82bdc0c194f3a38a08fde480d7e5d2100020000000000000000056a/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x315dd595e82bdc0c194f3a38a08fde480d7e5d2100020000000000000000056a/remove-liquidity", - "network": "arbitrum", - "zaps": [ - { - "strategyId": "balancer-join", - "ammId": "arbitrum-balancer", - "poolId": "0x315dd595e82bdc0c194f3a38a08fde480d7e5d2100020000000000000000056a", - "poolType": "gyroe", - "tokens": [ - "0x57F5E098CaD7A3D1Eed53991D4d66C45C9AF7812", - "0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8" - ] - } - ] + "network": "arbitrum" }, { "id": "ramses-cl-rseth-ethx-vault", @@ -5680,9 +5620,9 @@ "tokenAddress": "0x14ABD18d1FA335E9F630a658a2799B33208763Fa", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x9eEd71e0Cf1681Fb4554cFcC9C1eAAC6F6307EAC", "earnedToken": "mooAuraGyroaUSDT/USDT", "earnedTokenAddress": "0x9eEd71e0Cf1681Fb4554cFcC9C1eAAC6F6307EAC", - "earnContractAddress": "0x9eEd71e0Cf1681Fb4554cFcC9C1eAAC6F6307EAC", "oracle": "lps", "oracleId": "aura-arb-gyro-ausdt-usdt", "status": "active", @@ -5700,7 +5640,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://app.gyro.finance/pools/arbitrum/e-clp/0x14ABD18d1FA335E9F630a658a2799B33208763Fa/join/", "removeLiquidityUrl": "https://app.gyro.finance/pools/arbitrum/e-clp/0x14ABD18d1FA335E9F630a658a2799B33208763Fa/exit/", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer-join", + "ammId": "arbitrum-balancer", + "poolId": "0x14abd18d1fa335e9f630a658a2799b33208763fa00020000000000000000051f", + "poolType": "gyro", + "tokens": [ + "0xb165a74407fE1e519d6bCbDeC1Ed3202B35a4140", + "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9" + ] + } + ] }, { "id": "aura-arb-gyro-ausdc-usdc", @@ -5710,9 +5662,9 @@ "tokenAddress": "0xcA8ECD05A289B1FBc2E0eAEC07360c4BFec07B61", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xb35EcE568Fad2559B4c1025B72A230315616C356", "earnedToken": "mooAuraGyroaUSDC/USDC", "earnedTokenAddress": "0xb35EcE568Fad2559B4c1025B72A230315616C356", - "earnContractAddress": "0xb35EcE568Fad2559B4c1025B72A230315616C356", "oracle": "lps", "oracleId": "aura-arb-gyro-ausdc-usdc", "status": "active", @@ -5730,7 +5682,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://app.gyro.finance/pools/arbitrum/e-clp/0xcA8ECD05A289B1FBc2E0eAEC07360c4BFec07B61/join/", "removeLiquidityUrl": "https://app.gyro.finance/pools/arbitrum/e-clp/0xcA8ECD05A289B1FBc2E0eAEC07360c4BFec07B61/exit/", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer-join", + "ammId": "arbitrum-balancer", + "poolId": "0xca8ecd05a289b1fbc2e0eaec07360c4bfec07b6100020000000000000000051d", + "poolType": "gyro", + "tokens": [ + "0x7CFaDFD5645B50bE87d546f42699d863648251ad", + "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" + ] + } + ] }, { "id": "camelot-ethfi-weth-rp", diff --git a/src/config/vault/ethereum.json b/src/config/vault/ethereum.json index d4f400538..59e36e01f 100644 --- a/src/config/vault/ethereum.json +++ b/src/config/vault/ethereum.json @@ -5335,9 +5335,9 @@ "tokenAddress": "0x1E19CF2D73a72Ef1332C882F20534B6519Be0276", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0xe17D6212eAa54D98187026A770dee96f7C264feC", "earnedToken": "mooAurarETH-ETH", "earnedTokenAddress": "0xe17D6212eAa54D98187026A770dee96f7C264feC", - "earnContractAddress": "0xe17D6212eAa54D98187026A770dee96f7C264feC", "oracle": "lps", "oracleId": "aura-reth-weth", "status": "active", @@ -5348,7 +5348,19 @@ "strategyTypeId": "multi-lp", "addLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x1e19cf2d73a72ef1332c882f20534b6519be0276000200000000000000000112/invest/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/ethereum/v2/0x1e19cf2d73a72ef1332c882f20534b6519be0276000200000000000000000112/remove-liquidity", - "network": "ethereum" + "network": "ethereum", + "zaps": [ + { + "strategyId": "balancer-join", + "ammId": "ethereum-balancer", + "poolId": "0x1e19cf2d73a72ef1332c882f20534b6519be0276000200000000000000000112", + "poolType": "meta-stable", + "tokens": [ + "0xae78736Cd615f374D3085123A210448E74Fc6393", + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + ] + } + ] }, { "id": "aura-wsteth-eth", diff --git a/src/config/vault/optimism.json b/src/config/vault/optimism.json index f2f62e3cc..33ca3d386 100644 --- a/src/config/vault/optimism.json +++ b/src/config/vault/optimism.json @@ -10377,9 +10377,9 @@ "tokenAddress": "0x4Fd63966879300caFafBB35D157dC5229278Ed23", "tokenDecimals": 18, "tokenProviderId": "beethovenx", + "earnContractAddress": "0xdCC5DC90A803928Dc21556590D4620030966270F", "earnedToken": "mooBeetsRocketFuel", "earnedTokenAddress": "0xdCC5DC90A803928Dc21556590D4620030966270F", - "earnContractAddress": "0xdCC5DC90A803928Dc21556590D4620030966270F", "oracle": "lps", "oracleId": "beets-rocket-fuel", "status": "active", @@ -10390,7 +10390,19 @@ "strategyTypeId": "multi-lp", "addLiquidityUrl": "https://op.beets.fi/pool/0x4fd63966879300cafafbb35d157dc5229278ed2300020000000000000000002b", "removeLiquidityUrl": "https://op.beets.fi/pool/0x4fd63966879300cafafbb35d157dc5229278ed2300020000000000000000002b", - "network": "optimism" + "network": "optimism", + "zaps": [ + { + "strategyId": "balancer-join", + "ammId": "optimism-beethovenx", + "poolId": "0x4fd63966879300cafafbb35d157dc5229278ed2300020000000000000000002b", + "poolType": "meta-stable", + "tokens": [ + "0x4200000000000000000000000000000000000006", + "0x9Bcef72be871e61ED4fBbc7630889beE758eb81D" + ] + } + ] }, { "id": "velodrome-susd-lusd", diff --git a/src/features/data/apis/amm/balancer/gyroe/GyroEPool.ts b/src/features/data/apis/amm/balancer/gyro/GyroPool.ts similarity index 87% rename from src/features/data/apis/amm/balancer/gyroe/GyroEPool.ts rename to src/features/data/apis/amm/balancer/gyro/GyroPool.ts index 7ff7844e4..3dd2643cd 100644 --- a/src/features/data/apis/amm/balancer/gyroe/GyroEPool.ts +++ b/src/features/data/apis/amm/balancer/gyro/GyroPool.ts @@ -11,16 +11,18 @@ import { bigNumberToStringDeep, bigNumberToUint256String, } from '../../../../../../helpers/big-number'; -import { WeightedPoolEncoder } from '../weighted/WeightedPoolEncoder'; -import { FixedPoint } from '../weighted/FixedPoint'; +import { JoinExitEncoder } from '../join/JoinExitEncoder'; +import { FixedPoint } from '../join/FixedPoint'; import type { ZapStep } from '../../../transact/zap/types'; import { WeightedMath } from '../weighted/WeightedMath'; -import { WeightedPool } from '../weighted/WeightedPool'; import type { ChainEntity } from '../../../../entities/chain'; import { viemToWeb3Abi } from '../../../../../../helpers/web3'; import { BalancerGyroEPoolAbi } from '../../../../../../config/abi/BalancerGyroEPoolAbi'; +import { JoinPool } from '../join/JoinPool'; +import type { IBalancerJoinPool } from '../types'; -export class GyroEPool extends WeightedPool { +// Covers Gyro and GyroE +export class GyroPool extends JoinPool implements IBalancerJoinPool { constructor( readonly chain: ChainEntity, readonly vaultConfig: VaultConfig, @@ -28,16 +30,14 @@ export class GyroEPool extends WeightedPool { ) { super(chain, vaultConfig, config); - this.getUpscaledBalances = this.cacheMethod(this.getUpscaledBalances); this.getTokenRates = this.cacheMethod(this.getTokenRates); - this.getTotalSupply = this.cacheMethod(this.getTotalSupply); } get joinSupportsSlippage() { return false; } - // async getSwapRatioLikeStrategy(): Promise { + // async getSwapRatios(): Promise { // const balances = await this.getBalances(); // const rates = await this.getTokenRates(); // const totalSupply = await this.getTotalSupply(); @@ -56,7 +56,9 @@ export class GyroEPool extends WeightedPool { // return BIG_ONE.shiftedBy(18).dividedBy(ratio.plus(BIG_ONE.shiftedBy(18))); // } - /** assumption: all gyro pools have rateProviders */ + /** + * The ratio of balances[n] * scaling factor[n] * token rate[n] over their sum + */ async getSwapRatios(): Promise { const upscaledBalances = await this.getUpscaledBalances(); const token0Ratio = upscaledBalances[0].dividedBy( @@ -84,7 +86,7 @@ export class GyroEPool extends WeightedPool { request: { assets: this.config.tokens.map(t => t.address), maxAmountsIn: maxAmountsIn, - userData: WeightedPoolEncoder.joinAllTokensInForExactBPTOut( + userData: JoinExitEncoder.joinAllTokensInForExactBPTOut( bigNumberToUint256String(liquidity) ), fromInternalBalance: false, @@ -159,7 +161,7 @@ export class GyroEPool extends WeightedPool { request: { assets: this.config.tokens.map(t => t.address), maxAmountsIn: amountsIn, - userData: WeightedPoolEncoder.joinAllTokensInForExactBPTOut( + userData: JoinExitEncoder.joinAllTokensInForExactBPTOut( bigNumberToUint256String(estimatedLiquidity) ), fromInternalBalance: false, @@ -189,28 +191,21 @@ export class GyroEPool extends WeightedPool { throw new Error('Failed to calculate liquidity'); } + /** + * Internally gyro pools use a scaling factor that includes the token rate too + */ protected async getScalingFactors() { const factors = await super.getScalingFactors(); const rates = await this.getTokenRates(); return factors.map((factor, i) => FixedPoint.mulDown(factor, rates[i])); } - protected async getUpscaledBalances() { - return await this.upscaleAmounts(await this.getBalances()); - } - protected async getTokenRates(): Promise { const pool = await this.getPoolContract(); const rates: RatesResult = await pool.methods.getTokenRates().call(); return [rates.rate0, rates.rate1].map(rate => new BigNumber(rate)); } - protected async getTotalSupply(): Promise { - const pool = await this.getPoolContract(); - const totalSupply: string = await pool.methods.getActualSupply().call(); - return new BigNumber(totalSupply); - } - protected async getPoolContract() { const web3 = await this.getWeb3(); return new web3.eth.Contract(viemToWeb3Abi(BalancerGyroEPoolAbi), this.config.poolAddress); diff --git a/src/features/data/apis/amm/balancer/gyroe/types.ts b/src/features/data/apis/amm/balancer/gyro/types.ts similarity index 100% rename from src/features/data/apis/amm/balancer/gyroe/types.ts rename to src/features/data/apis/amm/balancer/gyro/types.ts diff --git a/src/features/data/apis/amm/balancer/weighted/FixedPoint.ts b/src/features/data/apis/amm/balancer/join/FixedPoint.ts similarity index 100% rename from src/features/data/apis/amm/balancer/weighted/FixedPoint.ts rename to src/features/data/apis/amm/balancer/join/FixedPoint.ts diff --git a/src/features/data/apis/amm/balancer/weighted/WeightedPoolEncoder.ts b/src/features/data/apis/amm/balancer/join/JoinExitEncoder.ts similarity index 96% rename from src/features/data/apis/amm/balancer/weighted/WeightedPoolEncoder.ts rename to src/features/data/apis/amm/balancer/join/JoinExitEncoder.ts index b85bf1214..446bb04d6 100644 --- a/src/features/data/apis/amm/balancer/weighted/WeightedPoolEncoder.ts +++ b/src/features/data/apis/amm/balancer/join/JoinExitEncoder.ts @@ -1,7 +1,7 @@ import abiCoder from 'web3-eth-abi'; -import { WeightedPoolExitKind, WeightedPoolJoinKind } from './types'; +import { WeightedPoolExitKind, WeightedPoolJoinKind } from '../weighted/types'; -export class WeightedPoolEncoder { +export class JoinExitEncoder { private constructor() { // static only } diff --git a/src/features/data/apis/amm/balancer/join/JoinPool.ts b/src/features/data/apis/amm/balancer/join/JoinPool.ts new file mode 100644 index 000000000..2d718bfc5 --- /dev/null +++ b/src/features/data/apis/amm/balancer/join/JoinPool.ts @@ -0,0 +1,219 @@ +import type { IBalancerJoinPool } from '../types'; +import type { ChainEntity } from '../../../../entities/chain'; +import type { + PoolConfig, + QueryExitPoolRequest, + QueryExitPoolResponse, + QueryJoinPoolRequest, + QueryJoinPoolResponse, + VaultConfig, +} from '../vault/types'; +import { createFactory } from '../../../../utils/factory-utils'; +import { getWeb3Instance } from '../../../instances'; +import { Vault } from '../vault/Vault'; +import { + BIG_ONE, + BIG_ZERO, + bigNumberToStringDeep, + bigNumberToUint256String, +} from '../../../../../../helpers/big-number'; +import type { ZapStep } from '../../../transact/zap/types'; +import { checkAddressOrder } from '../../../../../../helpers/tokens'; +import type { Contract } from 'web3-eth-contract'; +import BigNumber from 'bignumber.js'; +import { JoinExitEncoder } from './JoinExitEncoder'; +import { FixedPoint } from './FixedPoint'; + +export abstract class JoinPool implements IBalancerJoinPool { + public readonly type = 'balancer'; + public readonly subType = 'join'; + + protected constructor( + protected readonly chain: ChainEntity, + protected readonly vaultConfig: VaultConfig, + protected readonly config: PoolConfig + ) { + checkAddressOrder(config.tokens.map(t => t.address)); + + this.getSwapRatios = this.cacheMethod(this.getSwapRatios); + this.getScalingFactors = this.cacheMethod(this.getScalingFactors); + this.getPoolTokens = this.cacheMethod(this.getPoolTokens); + this.getBalances = this.cacheMethod(this.getBalances); + this.getUpscaledBalances = this.cacheMethod(this.getUpscaledBalances); + this.getTotalSupply = this.cacheMethod(this.getTotalSupply); + this.getWeb3 = this.cacheMethod(this.getWeb3); + this.getPoolContract = this.cacheMethod(this.getPoolContract); + this.getVault = this.cacheMethod(this.getVault); + } + + abstract get joinSupportsSlippage(): boolean; + + abstract getSwapRatios(): Promise; + + protected abstract getPoolContract(): Promise; + + async getAddLiquidityZap( + amountsIn: BigNumber[], + minLiquidity: BigNumber, + from: string, + insertBalance: boolean + ): Promise { + const vault = this.getVault(); + + return vault.getJoinPoolZap({ + join: { + poolId: this.config.poolId, + sender: from, + recipient: from, + request: { + assets: this.config.tokens.map(t => t.address), + maxAmountsIn: amountsIn, + userData: JoinExitEncoder.joinExactTokensInForBPTOut( + amountsIn.map(amount => bigNumberToUint256String(amount)), + bigNumberToUint256String(minLiquidity) + ), + fromInternalBalance: false, + }, + }, + insertBalance, + }); + } + + async quoteAddLiquidity(amountsIn: BigNumber[]): Promise { + if (amountsIn.every(amount => amount.lte(BIG_ZERO))) { + throw new Error('At least one input amount must be greater than 0'); + } + + const vault = this.getVault(); + const queryRequest: QueryJoinPoolRequest = { + poolId: this.config.poolId, + request: { + assets: this.config.tokens.map(t => t.address), + maxAmountsIn: amountsIn, + userData: JoinExitEncoder.joinExactTokensInForBPTOut( + amountsIn.map(amount => bigNumberToUint256String(amount)), + '0' + ), + fromInternalBalance: false, + }, + }; + const queryResult = await vault.queryJoinPool(queryRequest); + + console.debug( + 'queryJoinPool', + bigNumberToStringDeep(queryRequest), + bigNumberToStringDeep(queryResult) + ); + + return { + liquidity: queryResult.liquidity, + usedInput: queryResult.usedInput, + unusedInput: amountsIn.map((amount, i) => amount.minus(queryResult.usedInput[i])), + }; + } + + async quoteRemoveLiquidity(amountIn: BigNumber): Promise { + if (amountIn.lt(BIG_ONE)) { + throw new Error('Input amount must be greater than 0'); + } + + const vault = this.getVault(); + + const queryRequest: QueryExitPoolRequest = { + poolId: this.config.poolId, + request: { + assets: this.config.tokens.map(t => t.address), + minAmountsOut: this.config.tokens.map(() => BIG_ZERO), + userData: JoinExitEncoder.exitExactBPTInForTokensOut(bigNumberToUint256String(amountIn)), + toInternalBalance: false, + }, + }; + + return await vault.queryExitPool(queryRequest); + } + + async getRemoveLiquidityZap( + amountIn: BigNumber, + minAmountsOut: BigNumber[], + from: string, + insertBalance: boolean + ): Promise { + const vault = this.getVault(); + + return vault.getExitPoolZap({ + exit: { + poolId: this.config.poolId, + sender: from, + recipient: from, + request: { + assets: this.config.tokens.map(t => t.address), + minAmountsOut, + userData: JoinExitEncoder.exitExactBPTInForTokensOut(bigNumberToUint256String(amountIn)), + toInternalBalance: false, + }, + }, + poolAddress: this.config.poolAddress, + insertBalance, + }); + } + + /** + * Multiplier to normalize to 18 decimals + */ + protected async getScalingFactors() { + return this.config.tokens.map(token => { + if (token.address === this.config.poolAddress) { + return FixedPoint.ONE; + } + + if (token.decimals > 18) { + throw new Error('Tokens with more than 18 decimals are not supported.'); + } + + const diff = 18 - token.decimals; + return FixedPoint.ONE.shiftedBy(diff); + }); + } + + protected async upscaleAmounts(balances: BigNumber[]): Promise { + const factors = await this.getScalingFactors(); + return balances.map((balance, i) => FixedPoint.mulDown(balance, factors[i])); + } + + protected async downscaleAmounts(amounts: BigNumber[]): Promise { + const factors = await this.getScalingFactors(); + return amounts.map((amount, i) => FixedPoint.divUp(amount, factors[i])); + } + + protected async getPoolTokens() { + const vault = this.getVault(); + return await vault.getPoolTokens(this.config.poolId); + } + + protected async getBalances() { + const poolTokens = await this.getPoolTokens(); + return poolTokens.map(t => t.balance); + } + + protected async getUpscaledBalances() { + return await this.upscaleAmounts(await this.getBalances()); + } + + protected async getTotalSupply(): Promise { + const pool = await this.getPoolContract(); + const totalSupply: string = await pool.methods.getActualSupply().call(); + return new BigNumber(totalSupply); + } + + protected async getWeb3() { + return getWeb3Instance(this.chain); + } + + protected getVault() { + return new Vault(this.chain, this.vaultConfig); + } + + protected cacheMethod unknown>(fn: T): T { + return createFactory(fn.bind(this)) as T; + } +} diff --git a/src/features/data/apis/amm/balancer/meta-stable/MetaStablePool.ts b/src/features/data/apis/amm/balancer/meta-stable/MetaStablePool.ts new file mode 100644 index 000000000..8f8d7f9ca --- /dev/null +++ b/src/features/data/apis/amm/balancer/meta-stable/MetaStablePool.ts @@ -0,0 +1,54 @@ +import type { ChainEntity } from '../../../../entities/chain'; +import type { PoolConfig, VaultConfig } from '../vault/types'; +import BigNumber from 'bignumber.js'; +import { viemToWeb3Abi } from '../../../../../../helpers/web3'; +import { BalancerMetaStablePoolAbi } from '../../../../../../config/abi/BalancerMetaStablePoolAbi'; +import { BIG_ZERO, fromWei } from '../../../../../../helpers/big-number'; +import { FixedPoint } from '../join/FixedPoint'; +import { JoinPool } from '../join/JoinPool'; +import type { IBalancerJoinPool } from '../types'; + +export class MetaStablePool extends JoinPool implements IBalancerJoinPool { + constructor( + readonly chain: ChainEntity, + readonly vaultConfig: VaultConfig, + readonly config: PoolConfig + ) { + super(chain, vaultConfig, config); + + this.getScalingFactors = this.cacheMethod(this.getScalingFactors); + this.getPoolContract = this.cacheMethod(this.getPoolContract); + } + + get joinSupportsSlippage(): boolean { + return true; + } + + /** + * The ratio of balances[n] * scaling factor[n] * token rate[n] over their sum + */ + async getSwapRatios(): Promise { + const upscaledBalances = await this.getUpscaledBalances(); + const totalUpscaledBalance = upscaledBalances.reduce((acc, b) => acc.plus(b), BIG_ZERO); + const lastIndex = upscaledBalances.length - 1; + const ratios = upscaledBalances.map((b, i) => + i === lastIndex ? BIG_ZERO : FixedPoint.divDown(b, totalUpscaledBalance) + ); + ratios[lastIndex] = FixedPoint.ONE.minus(ratios.reduce((acc, w) => acc.plus(w), BIG_ZERO)); + return ratios.map(r => fromWei(r, 18)); + } + + /** + * For meta stable pools, the scaling factors include the token rate too + */ + protected async getScalingFactors() { + const pool = await this.getPoolContract(); + const factors: string[] = await pool.methods.getScalingFactors().call(); + return factors.map(f => new BigNumber(f)); + } + + protected async getPoolContract() { + const web3 = await this.getWeb3(); + return new web3.eth.Contract(viemToWeb3Abi(BalancerMetaStablePoolAbi), this.config.poolAddress); + } +} diff --git a/src/features/data/apis/amm/balancer/weighted/WeightedMath.ts b/src/features/data/apis/amm/balancer/weighted/WeightedMath.ts index 7115aeee8..fb7c8e30b 100644 --- a/src/features/data/apis/amm/balancer/weighted/WeightedMath.ts +++ b/src/features/data/apis/amm/balancer/weighted/WeightedMath.ts @@ -1,5 +1,5 @@ import type BigNumber from 'bignumber.js'; -import { FixedPoint } from './FixedPoint'; +import { FixedPoint } from '../join/FixedPoint'; import { BIG_ZERO } from '../../../../../../helpers/big-number'; export class WeightedMath { diff --git a/src/features/data/apis/amm/balancer/weighted/WeightedPool.ts b/src/features/data/apis/amm/balancer/weighted/WeightedPool.ts index 00fc37292..5449eb28d 100644 --- a/src/features/data/apis/amm/balancer/weighted/WeightedPool.ts +++ b/src/features/data/apis/amm/balancer/weighted/WeightedPool.ts @@ -1,55 +1,22 @@ import type { IBalancerJoinPool } from '../types'; import type { ChainEntity } from '../../../../entities/chain'; -import type { - PoolConfig, - QueryExitPoolRequest, - QueryExitPoolResponse, - QueryJoinPoolRequest, - QueryJoinPoolResponse, - VaultConfig, -} from '../vault/types'; -import { createFactory } from '../../../../utils/factory-utils'; +import type { PoolConfig, VaultConfig } from '../vault/types'; import { viemToWeb3Abi } from '../../../../../../helpers/web3'; -import { getWeb3Instance } from '../../../instances'; import type { NormalizedWeightsResult } from './types'; -import type BigNumber from 'bignumber.js'; -import { Vault } from '../vault/Vault'; -import { - BIG_ONE, - BIG_ZERO, - bigNumberToStringDeep, - bigNumberToUint256String, - fromWeiString, -} from '../../../../../../helpers/big-number'; -import { WeightedPoolEncoder } from './WeightedPoolEncoder'; -import { FixedPoint } from './FixedPoint'; -import type { ZapStep } from '../../../transact/zap/types'; -import { checkAddressOrder } from '../../../../../../helpers/tokens'; +import BigNumber from 'bignumber.js'; +import { fromWei } from '../../../../../../helpers/big-number'; import { BalancerWeightedPoolAbi } from '../../../../../../config/abi/BalancerWeightedPoolAbi'; +import { JoinPool } from '../join/JoinPool'; -export class WeightedPool implements IBalancerJoinPool { - public readonly type = 'balancer'; - public readonly subType = 'join'; - +export class WeightedPool extends JoinPool implements IBalancerJoinPool { constructor( - protected readonly chain: ChainEntity, - protected readonly vaultConfig: VaultConfig, - protected readonly config: PoolConfig + readonly chain: ChainEntity, + readonly vaultConfig: VaultConfig, + readonly config: PoolConfig ) { - checkAddressOrder(config.tokens.map(t => t.address)); + super(chain, vaultConfig, config); - this.getSwapRatios = this.cacheMethod(this.getSwapRatios); - this.getScalingFactors = this.cacheMethod(this.getScalingFactors); - this.getPoolTokens = this.cacheMethod(this.getPoolTokens); - this.getBalances = this.cacheMethod(this.getBalances); this.getNormalizedWeights = this.cacheMethod(this.getNormalizedWeights); - this.getWeb3 = this.cacheMethod(this.getWeb3); - this.getPoolContract = this.cacheMethod(this.getPoolContract); - this.getVault = this.cacheMethod(this.getVault); - } - - protected cacheMethod unknown>(fn: T): T { - return createFactory(fn.bind(this)) as T; } get joinSupportsSlippage() { @@ -57,169 +24,17 @@ export class WeightedPool implements IBalancerJoinPool { } async getSwapRatios(): Promise { - return this.getNormalizedWeights(); - } - - async getAddLiquidityZap( - amountsIn: BigNumber[], - minLiquidity: BigNumber, - from: string, - insertBalance: boolean - ): Promise { - const vault = this.getVault(); - - return vault.getJoinPoolZap({ - join: { - poolId: this.config.poolId, - sender: from, - recipient: from, - request: { - assets: this.config.tokens.map(t => t.address), - maxAmountsIn: amountsIn, - userData: WeightedPoolEncoder.joinExactTokensInForBPTOut( - amountsIn.map(amount => bigNumberToUint256String(amount)), - bigNumberToUint256String(minLiquidity) - ), - fromInternalBalance: false, - }, - }, - insertBalance, - }); - } - - async quoteAddLiquidity(amountsIn: BigNumber[]): Promise { - if (amountsIn.every(amount => amount.lte(BIG_ZERO))) { - throw new Error('At least one input amount must be greater than 0'); - } - - const vault = this.getVault(); - const queryRequest: QueryJoinPoolRequest = { - poolId: this.config.poolId, - request: { - assets: this.config.tokens.map(t => t.address), - maxAmountsIn: amountsIn, - userData: WeightedPoolEncoder.joinExactTokensInForBPTOut( - amountsIn.map(amount => bigNumberToUint256String(amount)), - '0' - ), - fromInternalBalance: false, - }, - }; - const queryResult = await vault.queryJoinPool(queryRequest); - - console.debug( - 'queryJoinPool', - bigNumberToStringDeep(queryRequest), - bigNumberToStringDeep(queryResult) - ); - - return { - liquidity: queryResult.liquidity, - usedInput: queryResult.usedInput, - unusedInput: amountsIn.map((amount, i) => amount.minus(queryResult.usedInput[i])), - }; - } - - async quoteRemoveLiquidity(amountIn: BigNumber): Promise { - if (amountIn.lt(BIG_ONE)) { - throw new Error('Input amount must be greater than 0'); - } - - const vault = this.getVault(); - - const queryRequest: QueryExitPoolRequest = { - poolId: this.config.poolId, - request: { - assets: this.config.tokens.map(t => t.address), - minAmountsOut: this.config.tokens.map(() => BIG_ZERO), - userData: WeightedPoolEncoder.exitExactBPTInForTokensOut( - bigNumberToUint256String(amountIn) - ), - toInternalBalance: false, - }, - }; - - return await vault.queryExitPool(queryRequest); - } - - async getRemoveLiquidityZap( - amountIn: BigNumber, - minAmountsOut: BigNumber[], - from: string, - insertBalance: boolean - ): Promise { - const vault = this.getVault(); - - return vault.getExitPoolZap({ - exit: { - poolId: this.config.poolId, - sender: from, - recipient: from, - request: { - assets: this.config.tokens.map(t => t.address), - minAmountsOut, - userData: WeightedPoolEncoder.exitExactBPTInForTokensOut( - bigNumberToUint256String(amountIn) - ), - toInternalBalance: false, - }, - }, - poolAddress: this.config.poolAddress, - insertBalance, - }); - } - - protected async getScalingFactors() { - return this.config.tokens.map(token => { - if (token.address === this.config.poolAddress) { - return FixedPoint.ONE; - } - - if (token.decimals > 18) { - throw new Error('Tokens with more than 18 decimals are not supported.'); - } - - const diff = 18 - token.decimals; - return FixedPoint.ONE.shiftedBy(diff); - }); - } - - protected async upscaleAmounts(balances: BigNumber[]): Promise { - const factors = await this.getScalingFactors(); - return balances.map((balance, i) => FixedPoint.mulDown(balance, factors[i])); - } - - protected async downscaleAmounts(amounts: BigNumber[]): Promise { - const factors = await this.getScalingFactors(); - return amounts.map((amount, i) => FixedPoint.divUp(amount, factors[i])); - } - - protected async getPoolTokens() { - const vault = this.getVault(); - return await vault.getPoolTokens(this.config.poolId); - } - - protected async getBalances() { - const poolTokens = await this.getPoolTokens(); - return poolTokens.map(t => t.balance); + return (await this.getNormalizedWeights()).map(weight => fromWei(weight, 18)); } protected async getNormalizedWeights(): Promise { const pool = await this.getPoolContract(); const weights: NormalizedWeightsResult = await pool.methods.getNormalizedWeights().call(); - return weights.map(weight => fromWeiString(weight, 18)); - } - - protected async getWeb3() { - return getWeb3Instance(this.chain); + return weights.map(weight => new BigNumber(weight)); } protected async getPoolContract() { const web3 = await this.getWeb3(); return new web3.eth.Contract(viemToWeb3Abi(BalancerWeightedPoolAbi), this.config.poolAddress); } - - protected getVault() { - return new Vault(this.chain, this.vaultConfig); - } } diff --git a/src/features/data/apis/transact/strategies/balancer/BalancerJoinStrategy.ts b/src/features/data/apis/transact/strategies/balancer/BalancerJoinStrategy.ts index b0c0a3c16..bfe1ca31c 100644 --- a/src/features/data/apis/transact/strategies/balancer/BalancerJoinStrategy.ts +++ b/src/features/data/apis/transact/strategies/balancer/BalancerJoinStrategy.ts @@ -80,8 +80,9 @@ import { type AmmEntityBalancer, isBalancerAmm } from '../../../../entities/zap' import { selectAmmById } from '../../../../selectors/zap'; import { createFactory } from '../../../../utils/factory-utils'; import type { PoolConfig, VaultConfig } from '../../../amm/balancer/vault/types'; -import { GyroEPool } from '../../../amm/balancer/gyroe/GyroEPool'; +import { GyroPool } from '../../../amm/balancer/gyro/GyroPool'; import { WeightedPool } from '../../../amm/balancer/weighted/WeightedPool'; +import { MetaStablePool } from '../../../amm/balancer/meta-stable/MetaStablePool'; type ZapHelpers = { slippage: number; @@ -145,12 +146,14 @@ class BalancerJoinStrategyImpl implements IZapStrategy { this.poolTokens = this.selectPoolTokens(state, this.chain.id, this.options.tokens); switch (this.options.poolType) { - case 'gyroe': { + case 'gyroe': + case 'gyro': { this.checkPoolTokensCount(2); this.checkPoolTokensHavePrice(state); break; } - case 'weighted': { + case 'weighted': + case 'meta-stable': { this.checkPoolTokensHavePrice(state); break; } @@ -241,12 +244,16 @@ class BalancerJoinStrategyImpl implements IZapStrategy { }; switch (this.options.poolType) { + case 'gyro': case 'gyroe': { - return new GyroEPool(this.chain, vault, pool); + return new GyroPool(this.chain, vault, pool); } case 'weighted': { return new WeightedPool(this.chain, vault, pool); } + case 'meta-stable': { + return new MetaStablePool(this.chain, vault, pool); + } default: { throw new Error(`Unsupported balancer pool type ${this.options.poolType}`); } diff --git a/src/features/data/apis/transact/strategies/strategy-configs.ts b/src/features/data/apis/transact/strategies/strategy-configs.ts index 42c819d30..1dd41d4ff 100644 --- a/src/features/data/apis/transact/strategies/strategy-configs.ts +++ b/src/features/data/apis/transact/strategies/strategy-configs.ts @@ -50,7 +50,7 @@ export type BalancerJoinStrategyConfig = { strategyId: 'balancer-join'; ammId: AmmEntityBalancer['id']; poolId: string; - poolType: 'gyroe' | 'weighted'; + poolType: 'gyro' | 'gyroe' | 'weighted' | 'meta-stable'; tokens: string[]; } & OptionalStrategySwapConfig; From 057659489a35aaa4f036ebcbaf544000c56a1d39 Mon Sep 17 00:00:00 2001 From: ReflectiveChimp <55021052+ReflectiveChimp@users.noreply.github.com> Date: Wed, 25 Sep 2024 21:23:14 +0100 Subject: [PATCH 08/20] weighted/meta-stable - single token join composable-stable - balanced join --- scripts/addBalancerZap.ts | 42 +- .../abi/BalancerComposableStablePoolAbi.ts | 623 +++++++++ src/config/vault/arbitrum.json | 137 +- src/config/vault/avax.json | 9 +- src/config/vault/base.json | 18 +- src/config/vault/ethereum.json | 115 +- src/config/vault/fantom.json | 18 +- src/config/vault/gnosis.json | 62 +- src/config/vault/optimism.json | 54 +- src/config/vault/polygon.json | 25 +- src/features/data/actions/vaults.ts | 10 + .../data/apis/amm/balancer/common/AllPool.ts | 193 +++ .../apis/amm/balancer/common/CommonPool.ts | 95 ++ .../balancer/{join => common}/FixedPoint.ts | 0 .../amm/balancer/common/JoinExitEncoder.ts | 89 ++ .../apis/amm/balancer/common/SingleAllPool.ts | 90 ++ .../apis/amm/balancer/common/type-guards.ts | 15 + .../data/apis/amm/balancer/common/types.ts | 77 ++ .../composable-stable/ComposableStablePool.ts | 226 ++-- .../composable-stable/join-exit-kinds.ts | 28 + .../amm/balancer/composable-stable/types.ts | 12 + .../data/apis/amm/balancer/gyro/GyroPool.ts | 68 +- .../apis/amm/balancer/gyro/join-exit-kinds.ts | 11 + .../data/apis/amm/balancer/gyro/types.ts | 8 + .../apis/amm/balancer/join/JoinExitEncoder.ts | 89 -- .../data/apis/amm/balancer/join/JoinPool.ts | 219 ---- .../balancer/meta-stable/MetaStablePool.ts | 40 +- .../balancer/meta-stable/join-exit-kinds.ts | 22 + .../apis/amm/balancer/meta-stable/types.ts | 11 + src/features/data/apis/amm/balancer/types.ts | 42 +- .../data/apis/amm/balancer/vault/Vault.ts | 88 +- .../data/apis/amm/balancer/vault/types.ts | 27 +- .../amm/balancer/weighted/WeightedMath.ts | 3 +- .../amm/balancer/weighted/WeightedPool.ts | 38 +- .../amm/balancer/weighted/join-exit-kinds.ts | 24 + .../data/apis/amm/balancer/weighted/types.ts | 3 +- .../data/apis/transact/helpers/tokens.ts | 2 +- .../strategies/RewardPoolToVaultStrategy.ts | 5 +- .../strategies/UniswapLikeStrategy.ts | 64 +- ...cerJoinStrategy.ts => BalancerStrategy.ts} | 1121 +++++++++++----- .../balancer/BalancerSwapStrategy.ts | 1134 ----------------- .../transact/strategies/balancer/types.ts | 1 - .../strategies/conic/ConicStrategy.ts | 9 +- .../cowcentrated/CowcentratedStrategy.ts | 5 +- .../strategies/curve/CurveStrategy.ts | 9 +- .../strategies/gamma/GammaStrategy.ts | 12 +- .../data/apis/transact/strategies/index.ts | 5 +- .../strategies/single/SingleStrategy.ts | 5 +- .../transact/strategies/strategy-configs.ts | 29 +- .../data/apis/transact/transact-types.ts | 127 +- .../transact/vaults/CowcentratedVaultType.ts | 19 +- .../data/apis/transact/vaults/GovVaultType.ts | 21 +- .../apis/transact/vaults/StandardVaultType.ts | 21 +- .../Transact/TransactDebugger/BalancerZap.tsx | 11 +- src/helpers/tokens.ts | 6 +- 55 files changed, 2976 insertions(+), 2261 deletions(-) create mode 100644 src/config/abi/BalancerComposableStablePoolAbi.ts create mode 100644 src/features/data/apis/amm/balancer/common/AllPool.ts create mode 100644 src/features/data/apis/amm/balancer/common/CommonPool.ts rename src/features/data/apis/amm/balancer/{join => common}/FixedPoint.ts (100%) create mode 100644 src/features/data/apis/amm/balancer/common/JoinExitEncoder.ts create mode 100644 src/features/data/apis/amm/balancer/common/SingleAllPool.ts create mode 100644 src/features/data/apis/amm/balancer/common/type-guards.ts create mode 100644 src/features/data/apis/amm/balancer/common/types.ts create mode 100644 src/features/data/apis/amm/balancer/composable-stable/join-exit-kinds.ts create mode 100644 src/features/data/apis/amm/balancer/composable-stable/types.ts create mode 100644 src/features/data/apis/amm/balancer/gyro/join-exit-kinds.ts delete mode 100644 src/features/data/apis/amm/balancer/join/JoinExitEncoder.ts delete mode 100644 src/features/data/apis/amm/balancer/join/JoinPool.ts create mode 100644 src/features/data/apis/amm/balancer/meta-stable/join-exit-kinds.ts create mode 100644 src/features/data/apis/amm/balancer/meta-stable/types.ts create mode 100644 src/features/data/apis/amm/balancer/weighted/join-exit-kinds.ts rename src/features/data/apis/transact/strategies/balancer/{BalancerJoinStrategy.ts => BalancerStrategy.ts} (50%) delete mode 100644 src/features/data/apis/transact/strategies/balancer/BalancerSwapStrategy.ts diff --git a/scripts/addBalancerZap.ts b/scripts/addBalancerZap.ts index 11d53acf6..e483e0993 100644 --- a/scripts/addBalancerZap.ts +++ b/scripts/addBalancerZap.ts @@ -27,10 +27,7 @@ import { createCachedFactory, createFactory } from '../src/features/data/utils/f import { addressBook, Token } from 'blockchain-addressbook'; import { partition, sortBy } from 'lodash'; import platforms from '../src/config/platforms.json'; -import { - BalancerJoinStrategyConfig, - BalancerSwapStrategyConfig, -} from '../src/features/data/apis/transact/strategies/strategy-configs'; +import { BalancerStrategyConfig } from '../src/features/data/apis/transact/strategies/strategy-configs'; import { sortVaultKeys } from './common/vault-fields'; const cacheBasePath = path.join(__dirname, '.cache', 'balancer'); @@ -815,31 +812,30 @@ export async function discoverBalancerZap(args: RunArgs) { console.log('Vault:', amm.vaultAddress); } - switch (pool.type) { + const type = pool.type; + switch (type) { case 'COMPOSABLE_STABLE': { return { - strategyId: 'balancer-swap', + strategyId: 'balancer', ammId: amm.id, poolId: apiPool.id, - poolType: apiPool.type - .toLowerCase() - .replaceAll('_', '-') as BalancerSwapStrategyConfig['poolType'], // TODO types - tokens: apiPool.poolTokens.map(t => t.address), - } satisfies BalancerSwapStrategyConfig; + poolType: transformPoolType(type), + tokens: tokens.map(t => t.address), + bptIndex: tokensWithBpt.findIndex(t => t.isBPT), + hasNestedPool: tokens.some(t => t.hasNestedPool), + } satisfies BalancerStrategyConfig; } case 'GYRO': case 'GYROE': case 'META_STABLE': case 'WEIGHTED': { return { - strategyId: 'balancer-join', + strategyId: 'balancer', ammId: amm.id, poolId: apiPool.id, - poolType: apiPool.type - .toLowerCase() - .replaceAll('_', '-') as BalancerJoinStrategyConfig['poolType'], // TODO types + poolType: transformPoolType(type), tokens: apiPool.poolTokens.map(t => t.address), - } satisfies BalancerJoinStrategyConfig; + } satisfies BalancerStrategyConfig; } default: { throw new Error(`Unsupported pool type ${pool.type}`); @@ -847,11 +843,15 @@ export async function discoverBalancerZap(args: RunArgs) { } } -async function saveZap( - chainId: string, - vaultId: string, - zap: BalancerSwapStrategyConfig | BalancerJoinStrategyConfig -) { +type TransformPoolType = T extends `${infer Head}_${infer Tail}` + ? `${Lowercase}-${TransformPoolType}` + : Lowercase; + +function transformPoolType(input: T): TransformPoolType { + return input.toLowerCase().replaceAll('_', '-') as TransformPoolType; +} + +async function saveZap(chainId: string, vaultId: string, zap: BalancerStrategyConfig) { const path = `./src/config/vault/${addressBookToAppId(chainId)}.json`; const vaults = await loadJson(path); let found = false; diff --git a/src/config/abi/BalancerComposableStablePoolAbi.ts b/src/config/abi/BalancerComposableStablePoolAbi.ts new file mode 100644 index 000000000..ddcf864ef --- /dev/null +++ b/src/config/abi/BalancerComposableStablePoolAbi.ts @@ -0,0 +1,623 @@ +import type { Abi } from 'viem'; + +export const BalancerComposableStablePoolAbi = [ + { + inputs: [ + { + components: [ + { internalType: 'contract IVault', name: 'vault', type: 'address' }, + { + internalType: 'contract IProtocolFeePercentagesProvider', + name: 'protocolFeeProvider', + type: 'address', + }, + { internalType: 'string', name: 'name', type: 'string' }, + { internalType: 'string', name: 'symbol', type: 'string' }, + { internalType: 'contract IERC20[]', name: 'tokens', type: 'address[]' }, + { internalType: 'contract IRateProvider[]', name: 'rateProviders', type: 'address[]' }, + { internalType: 'uint256[]', name: 'tokenRateCacheDurations', type: 'uint256[]' }, + { internalType: 'bool', name: 'exemptFromYieldProtocolFeeFlag', type: 'bool' }, + { internalType: 'uint256', name: 'amplificationParameter', type: 'uint256' }, + { internalType: 'uint256', name: 'swapFeePercentage', type: 'uint256' }, + { internalType: 'uint256', name: 'pauseWindowDuration', type: 'uint256' }, + { internalType: 'uint256', name: 'bufferPeriodDuration', type: 'uint256' }, + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'string', name: 'version', type: 'string' }, + ], + internalType: 'struct ComposableStablePool.NewPoolParams', + name: 'params', + type: 'tuple', + }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'uint256', name: 'startValue', type: 'uint256' }, + { indexed: false, internalType: 'uint256', name: 'endValue', type: 'uint256' }, + { indexed: false, internalType: 'uint256', name: 'startTime', type: 'uint256' }, + { indexed: false, internalType: 'uint256', name: 'endTime', type: 'uint256' }, + ], + name: 'AmpUpdateStarted', + type: 'event', + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: 'uint256', name: 'currentValue', type: 'uint256' }], + name: 'AmpUpdateStopped', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'owner', type: 'address' }, + { indexed: true, internalType: 'address', name: 'spender', type: 'address' }, + { indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' }, + ], + name: 'Approval', + type: 'event', + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: 'bool', name: 'paused', type: 'bool' }], + name: 'PausedStateChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'uint256', name: 'feeType', type: 'uint256' }, + { indexed: false, internalType: 'uint256', name: 'protocolFeePercentage', type: 'uint256' }, + ], + name: 'ProtocolFeePercentageCacheUpdated', + type: 'event', + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: 'bool', name: 'enabled', type: 'bool' }], + name: 'RecoveryModeStateChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'uint256', name: 'swapFeePercentage', type: 'uint256' }, + ], + name: 'SwapFeePercentageChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'uint256', name: 'tokenIndex', type: 'uint256' }, + { indexed: false, internalType: 'uint256', name: 'rate', type: 'uint256' }, + ], + name: 'TokenRateCacheUpdated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'uint256', name: 'tokenIndex', type: 'uint256' }, + { indexed: true, internalType: 'contract IRateProvider', name: 'provider', type: 'address' }, + { indexed: false, internalType: 'uint256', name: 'cacheDuration', type: 'uint256' }, + ], + name: 'TokenRateProviderSet', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'from', type: 'address' }, + { indexed: true, internalType: 'address', name: 'to', type: 'address' }, + { indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' }, + ], + name: 'Transfer', + type: 'event', + }, + { + inputs: [], + name: 'DELEGATE_PROTOCOL_SWAP_FEES_SENTINEL', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'DOMAIN_SEPARATOR', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'address', name: 'spender', type: 'address' }, + ], + name: 'allowance', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'approve', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'decimals', + outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'decreaseAllowance', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'disableRecoveryMode', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'enableRecoveryMode', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes4', name: 'selector', type: 'bytes4' }], + name: 'getActionId', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getActualSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getAmplificationParameter', + outputs: [ + { internalType: 'uint256', name: 'value', type: 'uint256' }, + { internalType: 'bool', name: 'isUpdating', type: 'bool' }, + { internalType: 'uint256', name: 'precision', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getAuthorizer', + outputs: [{ internalType: 'contract IAuthorizer', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getBptIndex', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getDomainSeparator', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getLastJoinExitData', + outputs: [ + { internalType: 'uint256', name: 'lastJoinExitAmplification', type: 'uint256' }, + { internalType: 'uint256', name: 'lastPostJoinExitInvariant', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getMinimumBpt', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'pure', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'getNextNonce', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getOwner', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getPausedState', + outputs: [ + { internalType: 'bool', name: 'paused', type: 'bool' }, + { internalType: 'uint256', name: 'pauseWindowEndTime', type: 'uint256' }, + { internalType: 'uint256', name: 'bufferPeriodEndTime', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getPoolId', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'feeType', type: 'uint256' }], + name: 'getProtocolFeePercentageCache', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getProtocolFeesCollector', + outputs: [{ internalType: 'contract IProtocolFeesCollector', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getProtocolSwapFeeDelegation', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getRate', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getRateProviders', + outputs: [{ internalType: 'contract IRateProvider[]', name: '', type: 'address[]' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getScalingFactors', + outputs: [{ internalType: 'uint256[]', name: '', type: 'uint256[]' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getSwapFeePercentage', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'contract IERC20', name: 'token', type: 'address' }], + name: 'getTokenRate', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'contract IERC20', name: 'token', type: 'address' }], + name: 'getTokenRateCache', + outputs: [ + { internalType: 'uint256', name: 'rate', type: 'uint256' }, + { internalType: 'uint256', name: 'oldRate', type: 'uint256' }, + { internalType: 'uint256', name: 'duration', type: 'uint256' }, + { internalType: 'uint256', name: 'expires', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getVault', + outputs: [{ internalType: 'contract IVault', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'inRecoveryMode', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'addedValue', type: 'uint256' }, + ], + name: 'increaseAllowance', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'isExemptFromYieldProtocolFee', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'contract IERC20', name: 'token', type: 'address' }], + name: 'isTokenExemptFromYieldProtocolFee', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'name', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'owner', type: 'address' }], + name: 'nonces', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'uint256', name: 'protocolSwapFeePercentage', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'onExitPool', + outputs: [ + { internalType: 'uint256[]', name: '', type: 'uint256[]' }, + { internalType: 'uint256[]', name: '', type: 'uint256[]' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'uint256', name: 'protocolSwapFeePercentage', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'onJoinPool', + outputs: [ + { internalType: 'uint256[]', name: '', type: 'uint256[]' }, + { internalType: 'uint256[]', name: '', type: 'uint256[]' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'enum IVault.SwapKind', name: 'kind', type: 'uint8' }, + { internalType: 'contract IERC20', name: 'tokenIn', type: 'address' }, + { internalType: 'contract IERC20', name: 'tokenOut', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'address', name: 'from', type: 'address' }, + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + internalType: 'struct IPoolSwapStructs.SwapRequest', + name: 'swapRequest', + type: 'tuple', + }, + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { internalType: 'uint256', name: 'indexIn', type: 'uint256' }, + { internalType: 'uint256', name: 'indexOut', type: 'uint256' }, + ], + name: 'onSwap', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { inputs: [], name: 'pause', outputs: [], stateMutability: 'nonpayable', type: 'function' }, + { + inputs: [ + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'value', type: 'uint256' }, + { internalType: 'uint256', name: 'deadline', type: 'uint256' }, + { internalType: 'uint8', name: 'v', type: 'uint8' }, + { internalType: 'bytes32', name: 'r', type: 'bytes32' }, + { internalType: 'bytes32', name: 's', type: 'bytes32' }, + ], + name: 'permit', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'uint256', name: 'protocolSwapFeePercentage', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'queryExit', + outputs: [ + { internalType: 'uint256', name: 'bptIn', type: 'uint256' }, + { internalType: 'uint256[]', name: 'amountsOut', type: 'uint256[]' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'poolId', type: 'bytes32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256[]', name: 'balances', type: 'uint256[]' }, + { internalType: 'uint256', name: 'lastChangeBlock', type: 'uint256' }, + { internalType: 'uint256', name: 'protocolSwapFeePercentage', type: 'uint256' }, + { internalType: 'bytes', name: 'userData', type: 'bytes' }, + ], + name: 'queryJoin', + outputs: [ + { internalType: 'uint256', name: 'bptOut', type: 'uint256' }, + { internalType: 'uint256[]', name: 'amountsIn', type: 'uint256[]' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'contract IERC20', name: 'token', type: 'address' }, + { internalType: 'bytes', name: 'poolConfig', type: 'bytes' }, + ], + name: 'setAssetManagerPoolConfig', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'swapFeePercentage', type: 'uint256' }], + name: 'setSwapFeePercentage', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'contract IERC20', name: 'token', type: 'address' }, + { internalType: 'uint256', name: 'duration', type: 'uint256' }, + ], + name: 'setTokenRateCacheDuration', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'rawEndValue', type: 'uint256' }, + { internalType: 'uint256', name: 'endTime', type: 'uint256' }, + ], + name: 'startAmplificationParameterUpdate', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'stopAmplificationParameterUpdate', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'symbol', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'totalSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'transfer', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'transferFrom', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { inputs: [], name: 'unpause', outputs: [], stateMutability: 'nonpayable', type: 'function' }, + { + inputs: [], + name: 'updateProtocolFeePercentageCache', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'contract IERC20', name: 'token', type: 'address' }], + name: 'updateTokenRateCache', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'version', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, +] as const satisfies Abi; diff --git a/src/config/vault/arbitrum.json b/src/config/vault/arbitrum.json index 0d07642cf..a8f4c6e48 100644 --- a/src/config/vault/arbitrum.json +++ b/src/config/vault/arbitrum.json @@ -30,7 +30,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0x69d9bc07a19caad9ae4ca40af18d5a688839a29900020000000000000000058e", "poolType": "gyroe", @@ -132,7 +132,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0x71c64ac8ec1da03f8a05c3cfeb6493e6dad54a6f000200000000000000000592", "poolType": "gyroe", @@ -1612,7 +1612,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0x46472cba35e6800012aa9fcc7939ff07478c473e00020000000000000000056c", "poolType": "gyroe", @@ -3603,15 +3603,16 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0x59743f1812bb85db83e9e4ee061d124aaa64290000000000000000000000052b", "poolType": "composable-stable", "tokens": [ "0x211Cc4DD073734dA055fbF44a2b4667d5E5fE5d2", - "0x59743f1812bb85Db83e9e4EE061D124AAa642900", "0xe3b3FE7bcA19cA77Ad877A5Bebab186bEcfAD906" - ] + ], + "bptIndex": 1, + "hasNestedPool": false } ] }, @@ -3646,15 +3647,16 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0xdfa752ca3ff49d4b6dbe08e2d5a111f51773d3950000000000000000000004e8", "poolType": "composable-stable", "tokens": [ "0x17FC002b466eEc40DaE837Fc4bE5c67993ddBd6F", - "0xdfa752ca3fF49d4B6dBE08E2d5a111f51773D395", "0xe3b3FE7bcA19cA77Ad877A5Bebab186bEcfAD906" - ] + ], + "bptIndex": 1, + "hasNestedPool": false } ] }, @@ -5643,7 +5645,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0x14abd18d1fa335e9f630a658a2799b33208763fa00020000000000000000051f", "poolType": "gyro", @@ -5685,7 +5687,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0xca8ecd05a289b1fbc2e0eaec07360c4bfec07b6100020000000000000000051d", "poolType": "gyro", @@ -5779,15 +5781,16 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0xe8a6026365254f779b6927f00f8724ea1b8ae5e0000000000000000000000580", "poolType": "composable-stable", "tokens": [ "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", - "0xd3443ee1e91aF28e5FB858Fbd0D72A63bA8046E0", - "0xE8a6026365254f779b6927f00f8724EA1B8aE5E0" - ] + "0xd3443ee1e91aF28e5FB858Fbd0D72A63bA8046E0" + ], + "bptIndex": 2, + "hasNestedPool": false } ] }, @@ -5927,7 +5930,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0x7967fa58b9501600d96bd843173b9334983ee6e600020000000000000000056e", "poolType": "gyroe", @@ -8376,7 +8379,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0xcdcef9765d369954a4a936064535710f7235110a000200000000000000000558", "poolType": "gyroe", @@ -8418,7 +8421,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0xef0c116a2818a5b1a5d836a291856a321f43c2fb00020000000000000000053a", "poolType": "gyroe", @@ -8461,15 +8464,16 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0x90e6cb5249f5e1572afbf8a96d8a1ca6acffd73900000000000000000000055c", "poolType": "composable-stable", "tokens": [ "0x4186BFC76E2E237523CBC30FD220FE055156b41F", - "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", - "0x90e6CB5249f5e1572afBF8A96D8A1ca6aCFFd739" - ] + "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" + ], + "bptIndex": 2, + "hasNestedPool": false } ] }, @@ -8504,15 +8508,16 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0x7b54c44fbe6db6d97fd22b8756f89c0af16202cc00000000000000000000053c", "poolType": "composable-stable", "tokens": [ "0x5979D7b546E38E414F7E9822514be443A4800529", - "0x7B54C44fBe6Db6D97FD22b8756f89c0aF16202Cc", "0xED65C5085a18Fa160Af0313E60dcc7905E944Dc7" - ] + ], + "bptIndex": 1, + "hasNestedPool": false } ] }, @@ -11667,7 +11672,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0x125bc5a031b2db6733bfa35d914ffa428095978b000200000000000000000514", "poolType": "gyroe", @@ -11809,15 +11814,16 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0xb61371ab661b1acec81c699854d2f911070c059e000000000000000000000516", "poolType": "composable-stable", "tokens": [ "0x2416092f143378750bb29b79eD961ab195CcEea5", - "0x5979D7b546E38E414F7E9822514be443A4800529", - "0xB61371Ab661B1ACec81C699854D2f911070C059E" - ] + "0x5979D7b546E38E414F7E9822514be443A4800529" + ], + "bptIndex": 2, + "hasNestedPool": false } ] }, @@ -12706,16 +12712,17 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0x2d6ced12420a9af5a83765a8c48be2afcd1a8feb000000000000000000000500", "poolType": "composable-stable", "tokens": [ "0x1DEBd73E752bEaF79865Fd6446b0c970EaE7732f", - "0x2d6CeD12420a9AF5a83765a8c48Be2aFcD1A8FEb", "0x5979D7b546E38E414F7E9822514be443A4800529", "0xEC70Dcb4A1EFa46b8F2D97C310C9c4790ba5ffA8" - ] + ], + "bptIndex": 1, + "hasNestedPool": false } ] }, @@ -12750,15 +12757,16 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0xc2598280bfea1fe18dfcabd21c7165c40c6859d30000000000000000000004f3", "poolType": "composable-stable", "tokens": [ "0x5979D7b546E38E414F7E9822514be443A4800529", - "0x95aB45875cFFdba1E5f451B950bC2E42c0053f39", - "0xc2598280bFeA1Fe18dFcaBD21C7165c40c6859d3" - ] + "0x95aB45875cFFdba1E5f451B950bC2E42c0053f39" + ], + "bptIndex": 2, + "hasNestedPool": false } ] }, @@ -12856,15 +12864,16 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0xd0ec47c54ca5e20aaae4616c25c825c7f48d40690000000000000000000004ef", "poolType": "composable-stable", "tokens": [ "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", - "0xd0EC47c54cA5e20aaAe4616c25C825c7f48D4069", "0xEC70Dcb4A1EFa46b8F2D97C310C9c4790ba5ffA8" - ] + ], + "bptIndex": 1, + "hasNestedPool": false } ] }, @@ -13471,15 +13480,16 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0x2ce4457acac29da4736ae6f5cd9f583a6b335c270000000000000000000004dc", "poolType": "composable-stable", "tokens": [ - "0x2CE4457aCac29dA4736aE6f5Cd9F583a6b335c27", "0x423A1323c871aBC9d89EB06855bF5347048Fc4A5", "0xe3b3FE7bcA19cA77Ad877A5Bebab186bEcfAD906" - ] + ], + "bptIndex": 0, + "hasNestedPool": true } ] }, @@ -14842,17 +14852,18 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0x423a1323c871abc9d89eb06855bf5347048fc4a5000000000000000000000496", "poolType": "composable-stable", "tokens": [ - "0x423A1323c871aBC9d89EB06855bF5347048Fc4A5", "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8" - ] + ], + "bptIndex": 0, + "hasNestedPool": false } ] }, @@ -14920,15 +14931,16 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0x9791d590788598535278552eecd4b211bfc790cb000000000000000000000498", "poolType": "composable-stable", "tokens": [ "0x5979D7b546E38E414F7E9822514be443A4800529", - "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", - "0x9791d590788598535278552EEcD4b211bFc790CB" - ] + "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" + ], + "bptIndex": 2, + "hasNestedPool": false } ] }, @@ -14995,15 +15007,16 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0x3fd4954a851ead144c2ff72b1f5a38ea5976bd54000000000000000000000480", "poolType": "composable-stable", "tokens": [ - "0x3FD4954a851eaD144c2FF72B1f5a38Ea5976Bd54", "0x5979D7b546E38E414F7E9822514be443A4800529", "0xe05A08226c49b636ACf99c40Da8DC6aF83CE5bB3" - ] + ], + "bptIndex": 0, + "hasNestedPool": false } ] }, @@ -15038,7 +15051,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0x32df62dc3aed2cd6224193052ce665dc181658410002000000000000000003bd", "poolType": "weighted", @@ -15176,7 +15189,7 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0xc7fa3a3527435720f0e2a4c1378335324dd4f9b3000200000000000000000459", "poolType": "weighted", @@ -15218,15 +15231,16 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0x8bc65eed474d1a00555825c91feab6a8255c2107000000000000000000000453", "poolType": "composable-stable", "tokens": [ "0x6A7661795C374c0bFC635934efAddFf3A7Ee23b6", - "0x8bc65Eed474D1A00555825c91FeAb6A8255C2107", "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" - ] + ], + "bptIndex": 1, + "hasNestedPool": false } ] }, @@ -15357,15 +15371,16 @@ "network": "arbitrum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "arbitrum-balancer", "poolId": "0x542f16da0efb162d20bf4358efa095b70a100f9e000000000000000000000436", "poolType": "composable-stable", "tokens": [ "0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f", - "0x542F16DA0efB162D20bF4358EfA095B70A100f9E", "0x6c84a8f1c29108F47a79964b5Fe888D4f4D0dE40" - ] + ], + "bptIndex": 1, + "hasNestedPool": false } ] }, diff --git a/src/config/vault/avax.json b/src/config/vault/avax.json index 91face298..3b7b8c712 100644 --- a/src/config/vault/avax.json +++ b/src/config/vault/avax.json @@ -89,15 +89,16 @@ "network": "avax", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "avax-balancer", "poolId": "0xfd2620c9cfcec7d152467633b3b0ca338d3d78cc00000000000000000000001c", "poolType": "composable-stable", "tokens": [ "0x2b2C81e08f1Af8835a78Bb2A90AE924ACE0eA4bE", - "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", - "0xfD2620C9cfceC7D152467633B3B0Ca338D3d78cc" - ] + "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7" + ], + "bptIndex": 2, + "hasNestedPool": false } ] }, diff --git a/src/config/vault/base.json b/src/config/vault/base.json index 9cd4c4ae6..df7303913 100644 --- a/src/config/vault/base.json +++ b/src/config/vault/base.json @@ -3761,15 +3761,16 @@ "network": "base", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "base-balancer", "poolId": "0xab99a3e856deb448ed99713dfce62f937e2d4d74000000000000000000000118", "poolType": "composable-stable", "tokens": [ "0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A", - "0x4200000000000000000000000000000000000006", - "0xaB99a3e856dEb448eD99713dfce62F937E2d4D74" - ] + "0x4200000000000000000000000000000000000006" + ], + "bptIndex": 2, + "hasNestedPool": false } ] }, @@ -7168,15 +7169,16 @@ "network": "base", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "base-balancer", "poolId": "0xc771c1a5905420daec317b154eb13e4198ba97d0000000000000000000000023", "poolType": "composable-stable", "tokens": [ "0x4200000000000000000000000000000000000006", - "0xB6fe221Fe9EeF5aBa221c348bA20A1Bf5e73624c", - "0xC771c1a5905420DAEc317b154EB13e4198BA97D0" - ] + "0xB6fe221Fe9EeF5aBa221c348bA20A1Bf5e73624c" + ], + "bptIndex": 2, + "hasNestedPool": false } ] }, diff --git a/src/config/vault/ethereum.json b/src/config/vault/ethereum.json index 59e36e01f..cff7d4491 100644 --- a/src/config/vault/ethereum.json +++ b/src/config/vault/ethereum.json @@ -48,15 +48,16 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0x264062ca46a1322c2e6464471764089e01f22f1900000000000000000000066b", "poolType": "composable-stable", "tokens": [ - "0x264062CA46A1322c2E6464471764089E01F22F19", "0x865377367054516e17014CcdED1e7d814EDC9ce4", "0xb45ad160634c528Cc3D2926d9807104FA3157305" - ] + ], + "bptIndex": 0, + "hasNestedPool": false } ] }, @@ -84,15 +85,16 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0x88794c65550deb6b4087b7552ecf295113794410000000000000000000000648", "poolType": "composable-stable", "tokens": [ "0x04C154b66CB340F3Ae24111CC767e0184Ed00Cc6", - "0x88794C65550DeB6b4087B7552eCf295113794410", "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - ] + ], + "bptIndex": 1, + "hasNestedPool": false } ] }, @@ -121,15 +123,16 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0x58aadfb1afac0ad7fca1148f3cde6aedf5236b6d00000000000000000000067f", "poolType": "composable-stable", "tokens": [ - "0x58AAdFB1Afac0ad7fca1148f3cdE6aEDF5236B6D", "0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7", "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - ] + ], + "bptIndex": 0, + "hasNestedPool": false } ] }, @@ -157,15 +160,16 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0xb819feef8f0fcdc268afe14162983a69f6bf179e000000000000000000000689", "poolType": "composable-stable", "tokens": [ "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497", - "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - "0xb819feeF8F0fcDC268AfE14162983A69f6BF179E" - ] + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + ], + "bptIndex": 2, + "hasNestedPool": false } ] }, @@ -193,15 +197,16 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0xdb1f2e1655477d08fb0992f82eede0053b8cd3820000000000000000000006ae", "poolType": "composable-stable", "tokens": [ "0x63a0964A36c34E81959da5894ad888800e17405b", - "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", - "0xDb1f2e1655477d08FB0992f82EEDe0053B8Cd382" - ] + "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" + ], + "bptIndex": 2, + "hasNestedPool": false } ] }, @@ -229,15 +234,16 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0xed0df9cd16d806e8a523805e53cf0c56e6db4d1d000000000000000000000687", "poolType": "composable-stable", "tokens": [ "0x83F20F44975D03b1b09e64809B757c47f942BEeA", - "0xD60EeA80C83779a8A5BFCDAc1F3323548e6BB62d", - "0xEd0DF9Cd16D806E8A523805e53cf0c56E6dB4D1d" - ] + "0xD60EeA80C83779a8A5BFCDAc1F3323548e6BB62d" + ], + "bptIndex": 2, + "hasNestedPool": false } ] }, @@ -315,7 +321,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0xc2aa60465bffa1a88f5ba471a59ca0435c3ec5c100020000000000000000062c", "poolType": "gyroe", @@ -350,7 +356,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0x1cce5169bde03f3d5ad0206f6bd057953539dae600020000000000000000062b", "poolType": "gyroe", @@ -385,7 +391,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0xaa7a70070e7495fe86c67225329dbd39baa2f63b000200000000000000000663", "poolType": "gyroe", @@ -420,7 +426,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0x3932b187f440ce7703653b3908edc5bb7676c283000200000000000000000664", "poolType": "gyroe", @@ -1759,16 +1765,17 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0x848a5564158d84b8a8fb68ab5d004fae11619a5400000000000000000000066a", "poolType": "composable-stable", "tokens": [ - "0x848a5564158d84b8A8fb68ab5D004Fae11619A54", "0xbf5495Efe5DB9ce00f80364C8B423567e58d2110", "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee", "0xFAe103DC9cf190eD75350761e95403b7b8aFa6c0" - ] + ], + "bptIndex": 0, + "hasNestedPool": false } ] }, @@ -1912,15 +1919,16 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0x05ff47afada98a98982113758878f9a8b9fdda0a000000000000000000000645", "poolType": "composable-stable", "tokens": [ - "0x05ff47AFADa98a98982113758878F9A8B9FddA0a", "0xae78736Cd615f374D3085123A210448E74Fc6393", "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee" - ] + ], + "bptIndex": 0, + "hasNestedPool": false } ] }, @@ -1956,15 +1964,16 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0x596192bb6e41802428ac943d2f1476c1af25cc0e000000000000000000000659", "poolType": "composable-stable", "tokens": [ - "0x596192bB6e41802428Ac943D2f1476C1Af25CC0E", "0xbf5495Efe5DB9ce00f80364C8B423567e58d2110", "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - ] + ], + "bptIndex": 0, + "hasNestedPool": false } ] }, @@ -2143,7 +2152,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0x9f9d900462492d4c21e9523ca95a7cd86142f298000200000000000000000462", "poolType": "weighted", @@ -2178,7 +2187,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0xf16aee6a71af1a9bc8f56975a4c2705ca7a782bc0002000000000000000004bb", "poolType": "weighted", @@ -2213,15 +2222,16 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0xdfe6e7e18f6cc65fa13c8d8966013d4fda74b6ba000000000000000000000558", "poolType": "composable-stable", "tokens": [ "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", - "0xdfE6e7e18f6Cc65FA13C8D8966013d4FdA74b6ba", "0xE95A203B1a91a908F9B9CE46459d101078c2c3cb" - ] + ], + "bptIndex": 1, + "hasNestedPool": false } ] }, @@ -2249,15 +2259,16 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0x49cbd67651fbabce12d1df18499896ec87bef46f00000000000000000000064a", "poolType": "composable-stable", "tokens": [ - "0x49cbD67651fbabCE12d1df18499896ec87BEf46f", "0x79c58f70905F734641735BC61e45c19dD9Ad60bC", "0x83F20F44975D03b1b09e64809B757c47f942BEeA" - ] + ], + "bptIndex": 0, + "hasNestedPool": true } ] }, @@ -2982,16 +2993,17 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0x8353157092ed8be69a9df8f95af097bbf33cb2af0000000000000000000005d9", "poolType": "composable-stable", "tokens": [ "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f", - "0x8353157092ED8Be69a9DF8F95af097bbF33Cb2aF", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "0xdAC17F958D2ee523a2206206994597C13D831ec7" - ] + ], + "bptIndex": 1, + "hasNestedPool": false } ] }, @@ -3019,7 +3031,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0xf01b0684c98cd7ada480bfdf6e43876422fa1fc10002000000000000000005de", "poolType": "gyroe", @@ -3054,7 +3066,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0xf7a826d47c8e02835d94fb0aa40f0cc9505cb1340002000000000000000005e0", "poolType": "gyroe", @@ -3188,16 +3200,17 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0x42ed016f826165c2e5976fe5bc3df540c5ad0af700000000000000000000058b", "poolType": "composable-stable", "tokens": [ - "0x42ED016F826165C2e5976fe5bC3df540C5aD0Af7", "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", "0xac3E018457B222d93114458476f3E3416Abbe38F", "0xae78736Cd615f374D3085123A210448E74Fc6393" - ] + ], + "bptIndex": 0, + "hasNestedPool": false } ] }, @@ -5351,7 +5364,7 @@ "network": "ethereum", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "ethereum-balancer", "poolId": "0x1e19cf2d73a72ef1332c882f20534b6519be0276000200000000000000000112", "poolType": "meta-stable", diff --git a/src/config/vault/fantom.json b/src/config/vault/fantom.json index 108e7b0d9..b2cffc7d1 100644 --- a/src/config/vault/fantom.json +++ b/src/config/vault/fantom.json @@ -189,16 +189,17 @@ "network": "fantom", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "fantom-beethovenx", "poolId": "0xaac7b8d6d7009428d6ddad895bdf50c6fcbbe2c000000000000000000000080d", "poolType": "composable-stable", "tokens": [ "0x1B6382DBDEa11d97f24495C9A90b7c88469134a4", "0x28a92dde19D9989F39A49905d7C9C2FAc7799bDf", - "0x2F733095B80A04b38b0D10cC884524a3d09b836a", - "0xaAc7B8d6D7009428d6DdaD895bdf50C6FCBbe2C0" - ] + "0x2F733095B80A04b38b0D10cC884524a3d09b836a" + ], + "bptIndex": 3, + "hasNestedPool": false } ] }, @@ -361,15 +362,16 @@ "network": "fantom", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "fantom-beethovenx", "poolId": "0x593000b762de3c465855336e95c8bb46080af064000000000000000000000760", "poolType": "composable-stable", "tokens": [ "0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83", - "0x593000b762dE3c465855336E95c8BB46080AF064", "0xd7028092c830b5C8FcE061Af2E593413EbbC1fc1" - ] + ], + "bptIndex": 1, + "hasNestedPool": false } ] }, @@ -591,7 +593,7 @@ "network": "fantom", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "fantom-beethovenx", "poolId": "0x838229095fa83bcd993ef225d01a990e3bc197a800020000000000000000075b", "poolType": "weighted", diff --git a/src/config/vault/gnosis.json b/src/config/vault/gnosis.json index 8a1640738..369e3c665 100644 --- a/src/config/vault/gnosis.json +++ b/src/config/vault/gnosis.json @@ -30,7 +30,7 @@ "network": "gnosis", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "gnosis-balancer", "poolId": "0x71e1179c5e197fa551beec85ca2ef8693c61b85b0002000000000000000000a0", "poolType": "gyroe", @@ -72,7 +72,7 @@ "network": "gnosis", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "gnosis-balancer", "poolId": "0x8dd4df4ce580b9644437f3375e54f1ab0980822800020000000000000000009c", "poolType": "gyroe", @@ -114,16 +114,17 @@ "network": "gnosis", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "gnosis-balancer", "poolId": "0xfc095c811fe836ed12f247bcf042504342b73fb700000000000000000000009f", "poolType": "composable-stable", "tokens": [ "0x2a22f9c3b484c3629090FeED35F17Ff8F88f76F0", "0x4ECaBa5870353805a9F068101A40E0f32ed605C6", - "0xaf204776c7245bF4147c2612BF6e5972Ee483701", - "0xfC095C811fE836Ed12f247BCf042504342B73FB7" - ] + "0xaf204776c7245bF4147c2612BF6e5972Ee483701" + ], + "bptIndex": 3, + "hasNestedPool": false } ] }, @@ -158,15 +159,16 @@ "network": "gnosis", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "gnosis-balancer", "poolId": "0x06135a9ae830476d3a941bae9010b63732a055f4000000000000000000000065", "poolType": "composable-stable", "tokens": [ "0x004626A008B1aCdC4c74ab51644093b155e59A23", - "0x06135A9Ae830476d3a941baE9010B63732a055F4", "0xcB444e90D8198415266c6a2724b7900fb12FC56E" - ] + ], + "bptIndex": 1, + "hasNestedPool": false } ] }, @@ -201,15 +203,16 @@ "network": "gnosis", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "gnosis-balancer", "poolId": "0xc9f00c3a713008ddf69b768d90d4978549bfdf9400000000000000000000006d", "poolType": "composable-stable", "tokens": [ "0xaBEf652195F98A91E490f047A5006B71c85f058d", - "0xaf204776c7245bF4147c2612BF6e5972Ee483701", - "0xc9F00C3a713008DDf69b768d90d4978549bFDF94" - ] + "0xaf204776c7245bF4147c2612BF6e5972Ee483701" + ], + "bptIndex": 2, + "hasNestedPool": false } ] }, @@ -244,7 +247,7 @@ "network": "gnosis", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "gnosis-balancer", "poolId": "0x4cdabe9e07ca393943acfb9286bbbd0d0a310ff600020000000000000000005c", "poolType": "weighted", @@ -286,7 +289,7 @@ "network": "gnosis", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "gnosis-balancer", "poolId": "0xbc2acf5e821c5c9f8667a36bb1131dad26ed64f9000200000000000000000063", "poolType": "weighted", @@ -328,16 +331,17 @@ "network": "gnosis", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "gnosis-balancer", "poolId": "0x7644fa5d0ea14fcf3e813fdf93ca9544f8567655000000000000000000000066", "poolType": "composable-stable", "tokens": [ "0x4ECaBa5870353805a9F068101A40E0f32ed605C6", - "0x7644fA5D0eA14FcF3E813Fdf93ca9544f8567655", "0xaf204776c7245bF4147c2612BF6e5972Ee483701", "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83" - ] + ], + "bptIndex": 1, + "hasNestedPool": false } ] }, @@ -404,15 +408,16 @@ "network": "gnosis", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "gnosis-balancer", "poolId": "0xdd439304a77f54b1f7854751ac1169b279591ef7000000000000000000000064", "poolType": "composable-stable", "tokens": [ "0xaf204776c7245bF4147c2612BF6e5972Ee483701", - "0xcB444e90D8198415266c6a2724b7900fb12FC56E", - "0xDd439304A77f54B1F7854751Ac1169b279591Ef7" - ] + "0xcB444e90D8198415266c6a2724b7900fb12FC56E" + ], + "bptIndex": 2, + "hasNestedPool": false } ] }, @@ -447,7 +452,7 @@ "network": "gnosis", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "gnosis-balancer", "poolId": "0x00df7f58e1cf932ebe5f54de5970fb2bdf0ef06d00010000000000000000005b", "poolType": "weighted", @@ -490,7 +495,7 @@ "network": "gnosis", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "gnosis-balancer", "poolId": "0x4683e340a8049261057d5ab1b29c8d840e75695e00020000000000000000005a", "poolType": "weighted", @@ -532,15 +537,16 @@ "network": "gnosis", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "gnosis-balancer", "poolId": "0xbad20c15a773bf03ab973302f61fabcea5101f0a000000000000000000000034", "poolType": "composable-stable", "tokens": [ "0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1", - "0x6C76971f98945AE98dD7d4DFcA8711ebea946eA6", - "0xbAd20c15A773bf03ab973302F61FAbceA5101f0A" - ] + "0x6C76971f98945AE98dD7d4DFcA8711ebea946eA6" + ], + "bptIndex": 2, + "hasNestedPool": false } ] } diff --git a/src/config/vault/optimism.json b/src/config/vault/optimism.json index 33ca3d386..dab1f0c97 100644 --- a/src/config/vault/optimism.json +++ b/src/config/vault/optimism.json @@ -855,15 +855,16 @@ "network": "optimism", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "optimism-beethovenx", "poolId": "0x2bb4712247d5f451063b5e4f6948abdfb925d93d000000000000000000000136", "poolType": "composable-stable", "tokens": [ "0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb", - "0x2Bb4712247D5F451063b5E4f6948abDfb925d93D", "0x5A7fACB970D094B6C7FF1df0eA68D99E6e73CBFF" - ] + ], + "bptIndex": 1, + "hasNestedPool": false } ] }, @@ -2097,7 +2098,7 @@ "network": "optimism", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "optimism-beethovenx", "poolId": "0xe906d4c4fc4c3fe96560de86b4bf7ed89af9a69a000200000000000000000126", "poolType": "gyroe", @@ -3226,15 +3227,16 @@ "network": "optimism", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "optimism-beethovenx", "poolId": "0x73a7fe27fe9545d53924e529acf11f3073841b9e000000000000000000000133", "poolType": "composable-stable", "tokens": [ "0x4200000000000000000000000000000000000006", - "0x73A7fe27fe9545D53924E529Acf11F3073841b9e", "0x87eEE96D50Fb761AD85B1c982d28A042169d61b1" - ] + ], + "bptIndex": 1, + "hasNestedPool": false } ] }, @@ -4793,7 +4795,7 @@ "network": "optimism", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "optimism-beethovenx", "poolId": "0x2feb76966459d7841fa8a7ed0aa4bf574d6111bf00020000000000000000011d", "poolType": "weighted", @@ -4883,15 +4885,16 @@ "network": "optimism", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "optimism-beethovenx", "poolId": "0xa71021492a3966eec735ed1b505afa097c7cfe6f00000000000000000000010d", "poolType": "composable-stable", "tokens": [ "0x484c2D6e3cDd945a8B2DF735e079178C1036578c", - "0x6806411765Af15Bddd26f8f544A34cC40cb9838B", - "0xA71021492a3966EeC735Ed1B505aFa097c7cFe6f" - ] + "0x6806411765Af15Bddd26f8f544A34cC40cb9838B" + ], + "bptIndex": 2, + "hasNestedPool": false } ] }, @@ -5334,16 +5337,17 @@ "network": "optimism", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "optimism-beethovenx", "poolId": "0x5f8893506ddc4c271837187d14a9c87964a074dc000000000000000000000106", "poolType": "composable-stable", "tokens": [ "0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb", "0x484c2D6e3cDd945a8B2DF735e079178C1036578c", - "0x5F8893506Ddc4C271837187d14A9C87964a074Dc", "0x9Bcef72be871e61ED4fBbc7630889beE758eb81D" - ] + ], + "bptIndex": 2, + "hasNestedPool": false } ] }, @@ -5427,15 +5431,16 @@ "network": "optimism", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "optimism-beethovenx", "poolId": "0x004700ba0a4f5f22e1e78a277fca55e36f47e09c000000000000000000000104", "poolType": "composable-stable", "tokens": [ - "0x004700ba0a4f5f22e1E78a277fCA55e36F47E09C", "0x9Bcef72be871e61ED4fBbc7630889beE758eb81D", "0xe05A08226c49b636ACf99c40Da8DC6aF83CE5bB3" - ] + ], + "bptIndex": 0, + "hasNestedPool": false } ] }, @@ -5463,7 +5468,7 @@ "network": "optimism", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "optimism-beethovenx", "poolId": "0x9da11ff60bfc5af527f58fd61679c3ac98d040d9000000000000000000000100", "poolType": "composable-stable", @@ -5471,9 +5476,10 @@ "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", "0x7F5c764cBc14f9669B88837ca1490cCa17c31607", "0x94b008aA00579c1307B0EF2c499aD98a8ce58e58", - "0x9Da11Ff60bfc5aF527f58fd61679c3AC98d040d9", "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" - ] + ], + "bptIndex": 3, + "hasNestedPool": false } ] }, @@ -5618,7 +5624,7 @@ "network": "optimism", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "optimism-beethovenx", "poolId": "0x7ca75bdea9dede97f8b13c6641b768650cb837820002000000000000000000d5", "poolType": "gyroe", @@ -10393,7 +10399,7 @@ "network": "optimism", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "optimism-beethovenx", "poolId": "0x4fd63966879300cafafbb35d157dc5229278ed2300020000000000000000002b", "poolType": "meta-stable", @@ -11926,7 +11932,7 @@ "network": "optimism", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "optimism-beethovenx", "poolId": "0x39965c9dab5448482cf7e002f583c812ceb53046000100000000000000000003", "poolType": "weighted", diff --git a/src/config/vault/polygon.json b/src/config/vault/polygon.json index b2d51db03..5d57166ec 100644 --- a/src/config/vault/polygon.json +++ b/src/config/vault/polygon.json @@ -655,7 +655,7 @@ "network": "polygon", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "polygon-balancer", "poolId": "0x1dcea0bfbbe6848f117640d534c9b60f41b9f2a8000100000000000000000ea1", "poolType": "weighted", @@ -698,16 +698,17 @@ "network": "polygon", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "polygon-balancer", "poolId": "0x4b7586a4f49841447150d3d92d9e9e000f766c30000000000000000000000e8a", "poolType": "composable-stable", "tokens": [ "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", - "0x4B7586A4F49841447150D3d92d9E9e000f766c30", "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063", "0xc2132D05D31c914a87C6611C10748AEb04B58e8F" - ] + ], + "bptIndex": 1, + "hasNestedPool": false } ] }, @@ -735,15 +736,16 @@ "network": "polygon", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "polygon-balancer", "poolId": "0xcd78a20c597e367a4e478a2411ceb790604d7c8f000000000000000000000c22", "poolType": "composable-stable", "tokens": [ "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", - "0xcd78A20c597E367A4e478a2411cEB790604D7c8F", "0xfa68FB4628DFF1028CFEc22b4162FCcd0d45efb6" - ] + ], + "bptIndex": 1, + "hasNestedPool": false } ] }, @@ -2335,7 +2337,7 @@ "network": "polygon", "zaps": [ { - "strategyId": "balancer-join", + "strategyId": "balancer", "ammId": "polygon-balancer", "poolId": "0xf0ad209e2e969eaaa8c882aac71f02d8a047d5c2000200000000000000000b49", "poolType": "gyroe", @@ -3840,15 +3842,16 @@ "network": "polygon", "zaps": [ { - "strategyId": "balancer-swap", + "strategyId": "balancer", "ammId": "polygon-balancer", "poolId": "0x513cdee00251f39de280d9e5f771a6eafebcc88e000000000000000000000a6b", "poolType": "composable-stable", "tokens": [ "0x4e3Decbb3645551B8A19f0eA1678079FCB33fB4c", - "0x513CdEE00251F39DE280d9E5f771A6eaFebCc88E", "0xE2Aa7db6dA1dAE97C5f5C6914d285fBfCC32A128" - ] + ], + "bptIndex": 1, + "hasNestedPool": false } ] }, diff --git a/src/features/data/actions/vaults.ts b/src/features/data/actions/vaults.ts index c2f0d56e0..0f00be102 100644 --- a/src/features/data/actions/vaults.ts +++ b/src/features/data/actions/vaults.ts @@ -18,6 +18,7 @@ import type { import { getVaultNames } from '../utils/vault-utils'; import { safetyScoreNum } from '../../../helpers/safetyScore'; import { isDefined } from '../utils/array-utils'; +import type { BalancerStrategyConfig } from '../apis/transact/strategies/strategy-configs'; export interface FulfilledAllVaultsPayload { byChainId: { @@ -297,6 +298,15 @@ function getVaultStatus(apiVault: VaultConfig): VaultStatus { function getVaultBase(config: VaultConfig, chainId: ChainEntity['id']): VaultBase { const names = getVaultNames(config.name, config.type); + if (config.tokenProviderId === 'balancer' || config.tokenProviderId === 'beethovenx') { + const zap = config.zaps?.find((z): z is BalancerStrategyConfig => z.strategyId === 'balancer'); + if (zap) { + for (const k of Object.keys(names)) { + names[k] = `${names[k]} [${zap.poolType}]`; + } + } + } + return { id: config.id, name: config.id === 'bifi-vault' ? names.long : config.name, diff --git a/src/features/data/apis/amm/balancer/common/AllPool.ts b/src/features/data/apis/amm/balancer/common/AllPool.ts new file mode 100644 index 000000000..28984e5e4 --- /dev/null +++ b/src/features/data/apis/amm/balancer/common/AllPool.ts @@ -0,0 +1,193 @@ +import type { BalancerFeature, IBalancerAllPool } from '../types'; +import type { ChainEntity } from '../../../../entities/chain'; +import type { + ExitPoolRequest, + JoinPoolRequest, + PoolConfig, + QueryExitPoolRequest, + QueryExitPoolResponse, + QueryJoinPoolRequest, + QueryJoinPoolResponse, + VaultConfig, +} from '../vault/types'; +import { BIG_ONE, BIG_ZERO, bigNumberToStringDeep } from '../../../../../../helpers/big-number'; +import type { ZapStep } from '../../../transact/zap/types'; +import type { Contract } from 'web3-eth-contract'; +import type BigNumber from 'bignumber.js'; +import { CommonPool } from './CommonPool'; +import { type ExitPoolUserData, type JoinPoolUserData, PoolExitKind, PoolJoinKind } from './types'; + +/** Join/Exit with all tokens in ratio */ +export abstract class AllPool extends CommonPool implements IBalancerAllPool { + protected constructor( + readonly chain: ChainEntity, + readonly vaultConfig: VaultConfig, + readonly config: PoolConfig + ) { + super(chain, vaultConfig, config); + + this.getSwapRatios = this.cacheMethod(this.getSwapRatios); + } + + abstract supportsFeature(feature: BalancerFeature): boolean; + + abstract getSwapRatios(): Promise; + + protected abstract getPoolContract(): Promise; + + protected abstract getJoinKindValue(kind: PoolJoinKind): number; + + protected abstract getExitKindValue(kind: PoolExitKind): number; + + protected customizeJoinPoolRequest( + request: JoinPoolRequest + ): JoinPoolRequest { + return request; + } + + protected customizeExitPoolRequest( + request: ExitPoolRequest + ): ExitPoolRequest { + return request; + } + + protected customizeQueryJoinPoolResponse(response: QueryJoinPoolResponse): QueryJoinPoolResponse { + return response; + } + + protected customizeQueryExitPoolResponse(response: QueryExitPoolResponse): QueryExitPoolResponse { + return response; + } + + async quoteAddLiquidity(amountsIn: BigNumber[]): Promise { + if (amountsIn.every(amount => amount.lte(BIG_ZERO))) { + throw new Error('At least one input amount must be greater than 0'); + } + + const vault = this.getVault(); + const queryRequest: QueryJoinPoolRequest = { + poolId: this.config.poolId, + request: this.customizeJoinPoolRequest({ + assets: this.config.tokens.map(t => t.address), + maxAmountsIn: amountsIn, + userData: { + kind: PoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT, + kindValue: this.getJoinKindValue(PoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT), + amountsIn, + tokensIn: this.config.tokens.map(t => t.address), + minimumBPT: BIG_ZERO, + }, + fromInternalBalance: false, + }), + }; + console.debug('quoteAddLiquidity#request', bigNumberToStringDeep(queryRequest)); + const queryResult = this.customizeQueryJoinPoolResponse( + await vault.queryJoinPool(queryRequest) + ); + console.debug('quoteAddLiquidity#result', bigNumberToStringDeep(queryResult)); + + return queryResult; + } + + async getAddLiquidityZap( + amountsIn: BigNumber[], + minLiquidity: BigNumber, + from: string, + insertBalance: boolean + ): Promise { + const vault = this.getVault(); + + return vault.getJoinPoolZap({ + join: { + poolId: this.config.poolId, + sender: from, + recipient: from, + request: this.customizeJoinPoolRequest({ + assets: this.config.tokens.map(t => t.address), + maxAmountsIn: amountsIn, + userData: { + kind: PoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT, + kindValue: this.getJoinKindValue(PoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT), + amountsIn, + tokensIn: this.config.tokens.map(t => t.address), + minimumBPT: minLiquidity, + }, + fromInternalBalance: false, + }), + }, + insertBalance, + }); + } + + async quoteRemoveLiquidity(liquidityIn: BigNumber): Promise { + const userData: ExitPoolUserData = { + kind: PoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT, + kindValue: this.getExitKindValue(PoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT), + bptAmountIn: liquidityIn, + }; + + return this.quoteRemoveLiquidityWithUserData(liquidityIn, userData); + } + + protected async quoteRemoveLiquidityWithUserData( + liquidityIn: BigNumber, + userData: ExitPoolUserData + ): Promise { + if (liquidityIn.lt(BIG_ONE)) { + throw new Error('liquidityIn must be greater than 0'); + } + + const vault = this.getVault(); + const queryRequest: QueryExitPoolRequest = { + poolId: this.config.poolId, + request: this.customizeExitPoolRequest({ + assets: this.config.tokens.map(t => t.address), + minAmountsOut: this.config.tokens.map(() => BIG_ZERO), + userData, + toInternalBalance: false, + }), + }; + console.debug('quoteRemoveLiquidity#request', bigNumberToStringDeep(queryRequest)); + const result = this.customizeQueryExitPoolResponse(await vault.queryExitPool(queryRequest)); + console.debug('quoteRemoveLiquidity#result', bigNumberToStringDeep(result)); + return result; + } + + async getRemoveLiquidityZap( + liquidityIn: BigNumber, + minAmountsOut: BigNumber[], + from: string, + insertBalance: boolean + ): Promise { + return this.getRemoveLiquidityZapWithUserData(minAmountsOut, from, insertBalance, { + kind: PoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT, + kindValue: this.getExitKindValue(PoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT), + bptAmountIn: liquidityIn, + }); + } + + protected async getRemoveLiquidityZapWithUserData( + minAmountsOut: BigNumber[], + from: string, + insertBalance: boolean, + userData: ExitPoolUserData + ): Promise { + const vault = this.getVault(); + + return vault.getExitPoolZap({ + exit: { + poolId: this.config.poolId, + sender: from, + recipient: from, + request: this.customizeExitPoolRequest({ + assets: this.config.tokens.map(t => t.address), + minAmountsOut, + userData, + toInternalBalance: false, + }), + }, + poolAddress: this.config.poolAddress, + insertBalance, + }); + } +} diff --git a/src/features/data/apis/amm/balancer/common/CommonPool.ts b/src/features/data/apis/amm/balancer/common/CommonPool.ts new file mode 100644 index 000000000..a77a7b4a8 --- /dev/null +++ b/src/features/data/apis/amm/balancer/common/CommonPool.ts @@ -0,0 +1,95 @@ +import type { BalancerFeature, IBalancerPool } from '../types'; +import type { ChainEntity } from '../../../../entities/chain'; +import type { PoolConfig, VaultConfig } from '../vault/types'; +import { createFactory } from '../../../../utils/factory-utils'; +import { getWeb3Instance } from '../../../instances'; +import { Vault } from '../vault/Vault'; +import { checkAddressOrder } from '../../../../../../helpers/tokens'; +import type { Contract } from 'web3-eth-contract'; +import BigNumber from 'bignumber.js'; +import { FixedPoint } from './FixedPoint'; + +export abstract class CommonPool implements IBalancerPool { + public readonly type = 'balancer'; + + protected constructor( + protected readonly chain: ChainEntity, + protected readonly vaultConfig: VaultConfig, + protected readonly config: PoolConfig + ) { + checkAddressOrder(config.tokens.map(t => t.address)); + + this.getScalingFactors = this.cacheMethod(this.getScalingFactors); + this.getPoolTokens = this.cacheMethod(this.getPoolTokens); + this.getBalances = this.cacheMethod(this.getBalances); + this.getUpscaledBalances = this.cacheMethod(this.getUpscaledBalances); + this.getTotalSupply = this.cacheMethod(this.getTotalSupply); + this.getWeb3 = this.cacheMethod(this.getWeb3); + this.getPoolContract = this.cacheMethod(this.getPoolContract); + this.getVault = this.cacheMethod(this.getVault); + } + + abstract supportsFeature(feature: BalancerFeature): boolean; + + protected abstract getPoolContract(): Promise; + + /** + * Multiplier to normalize to 18 decimals + */ + protected async getScalingFactors() { + return this.config.tokens.map(token => { + if (token.address === this.config.poolAddress) { + return FixedPoint.ONE; + } + + if (token.decimals > 18) { + throw new Error('Tokens with more than 18 decimals are not supported.'); + } + + const diff = 18 - token.decimals; + return FixedPoint.ONE.shiftedBy(diff); + }); + } + + protected async upscaleAmounts(balances: BigNumber[]): Promise { + const factors = await this.getScalingFactors(); + return balances.map((balance, i) => FixedPoint.mulDown(balance, factors[i])); + } + + protected async downscaleAmounts(amounts: BigNumber[]): Promise { + const factors = await this.getScalingFactors(); + return amounts.map((amount, i) => FixedPoint.divUp(amount, factors[i])); + } + + protected async getPoolTokens() { + const vault = this.getVault(); + return await vault.getPoolTokens(this.config.poolId); + } + + protected async getBalances() { + const poolTokens = await this.getPoolTokens(); + return poolTokens.map(t => t.balance); + } + + protected async getUpscaledBalances() { + return await this.upscaleAmounts(await this.getBalances()); + } + + protected async getTotalSupply(): Promise { + const pool = await this.getPoolContract(); + const totalSupply: string = await pool.methods.getActualSupply().call(); + return new BigNumber(totalSupply); + } + + protected async getWeb3() { + return getWeb3Instance(this.chain); + } + + protected getVault() { + return new Vault(this.chain, this.vaultConfig); + } + + protected cacheMethod unknown>(fn: T): T { + return createFactory(fn.bind(this)) as T; + } +} diff --git a/src/features/data/apis/amm/balancer/join/FixedPoint.ts b/src/features/data/apis/amm/balancer/common/FixedPoint.ts similarity index 100% rename from src/features/data/apis/amm/balancer/join/FixedPoint.ts rename to src/features/data/apis/amm/balancer/common/FixedPoint.ts diff --git a/src/features/data/apis/amm/balancer/common/JoinExitEncoder.ts b/src/features/data/apis/amm/balancer/common/JoinExitEncoder.ts new file mode 100644 index 000000000..19b549aef --- /dev/null +++ b/src/features/data/apis/amm/balancer/common/JoinExitEncoder.ts @@ -0,0 +1,89 @@ +import { type ExitPoolUserData, type JoinPoolUserData, PoolExitKind, PoolJoinKind } from './types'; +import { encodeAbiParameters } from 'viem'; +import type { Hex } from 'viem/types/misc'; +import type BigNumber from 'bignumber.js'; + +function bigNumberToBigInt(bn: BigNumber): bigint { + return BigInt(bn.toString(10)); +} + +function numberToBigInt(n: number): bigint { + return BigInt(n); +} + +export class JoinExitEncoder { + private constructor() { + // static only + } + + static encodeJoin(join: JoinPoolUserData): Hex { + switch (join.kind) { + case PoolJoinKind.INIT: { + throw new Error('Join kind INIT is not supported'); + } + case PoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT: { + return encodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256[]' }, { type: 'uint256' }], + [ + numberToBigInt(join.kindValue), + join.amountsIn.map(bigNumberToBigInt), + bigNumberToBigInt(join.minimumBPT), + ] + ); + } + case PoolJoinKind.TOKEN_IN_FOR_EXACT_BPT_OUT: { + return encodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' }], + [ + numberToBigInt(join.kindValue), + bigNumberToBigInt(join.bptAmountOut), + numberToBigInt(join.enterTokenIndex), + ] + ); + } + case PoolJoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT: { + return encodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256' }], + [numberToBigInt(join.kindValue), bigNumberToBigInt(join.bptAmountOut)] + ); + } + default: { + throw new Error('Invalid join kind'); + } + } + } + + static encodeExit(exit: ExitPoolUserData): Hex { + switch (exit.kind) { + case PoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT: { + return encodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' }], + [ + numberToBigInt(exit.kindValue), + bigNumberToBigInt(exit.bptAmountIn), + numberToBigInt(exit.exitTokenIndex), + ] + ); + } + case PoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT: { + return encodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256' }], + [numberToBigInt(exit.kindValue), bigNumberToBigInt(exit.bptAmountIn)] + ); + } + case PoolExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT: { + return encodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256[]' }, { type: 'uint256' }], + [ + numberToBigInt(exit.kindValue), + exit.amountsOut.map(bigNumberToBigInt), + bigNumberToBigInt(exit.maxBPTAmountIn), + ] + ); + } + default: { + throw new Error('Invalid exit kind'); + } + } + } +} diff --git a/src/features/data/apis/amm/balancer/common/SingleAllPool.ts b/src/features/data/apis/amm/balancer/common/SingleAllPool.ts new file mode 100644 index 000000000..9157daee6 --- /dev/null +++ b/src/features/data/apis/amm/balancer/common/SingleAllPool.ts @@ -0,0 +1,90 @@ +import { AllPool } from './AllPool'; +import type { IBalancerAllPool, IBalancerSinglePool } from '../types'; +import type { ChainEntity } from '../../../../entities/chain'; +import { + type PoolConfig, + type QueryExitPoolResponse, + type QueryJoinPoolResponse, + type VaultConfig, +} from '../vault/types'; +import type BigNumber from 'bignumber.js'; +import { BIG_ZERO } from '../../../../../../helpers/big-number'; +import { type ExitPoolUserData, PoolExitKind } from './types'; +import type { ZapStep } from '../../../transact/zap/types'; + +/** Join/Exit with one token or all tokens in ratio */ +export abstract class SingleAllPool + extends AllPool + implements IBalancerSinglePool, IBalancerAllPool +{ + protected constructor( + readonly chain: ChainEntity, + readonly vaultConfig: VaultConfig, + readonly config: PoolConfig + ) { + super(chain, vaultConfig, config); + } + + async quoteAddLiquidityOneToken( + amountIn: BigNumber, + tokenIn: string + ): Promise { + const amountsIn = this.config.tokens.map(t => (t.address === tokenIn ? amountIn : BIG_ZERO)); + return this.quoteAddLiquidity(amountsIn); + } + + async quoteRemoveLiquidityOneToken( + liquidityIn: BigNumber, + tokenOut: string + ): Promise { + const wantedIndex = this.config.tokens.findIndex(t => t.address === tokenOut); + if (wantedIndex === -1) { + throw new Error('Token not found in pool'); + } + + const userData: ExitPoolUserData = { + kind: PoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT, + kindValue: this.getExitKindValue(PoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT), + bptAmountIn: liquidityIn, + exitTokenIndex: wantedIndex, + }; + + return this.quoteRemoveLiquidityWithUserData(liquidityIn, userData); + } + + async getAddLiquidityOneTokenZap( + amountIn: BigNumber, + tokenIn: string, + liquidityOutMin: BigNumber, + from: string, + insertBalance: boolean + ): Promise { + const amountsIn = this.config.tokens.map(t => (t.address === tokenIn ? amountIn : BIG_ZERO)); + return this.getAddLiquidityZap(amountsIn, liquidityOutMin, from, insertBalance); + } + + async getRemoveLiquidityOneTokenZap( + liquidityIn: BigNumber, + tokenOut: string, + amountOutMin: BigNumber, + from: string, + insertBalance: boolean + ): Promise { + const wantedIndex = this.config.tokens.findIndex(t => t.address === tokenOut); + if (wantedIndex === -1) { + throw new Error('Token not found in pool'); + } + const minAmountsOut = this.config.tokens.map(t => + t.address === tokenOut ? amountOutMin : BIG_ZERO + ); + + const userData: ExitPoolUserData = { + kind: PoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT, + kindValue: this.getExitKindValue(PoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT), + bptAmountIn: liquidityIn, + exitTokenIndex: wantedIndex, + }; + + return this.getRemoveLiquidityZapWithUserData(minAmountsOut, from, insertBalance, userData); + } +} diff --git a/src/features/data/apis/amm/balancer/common/type-guards.ts b/src/features/data/apis/amm/balancer/common/type-guards.ts new file mode 100644 index 000000000..feab26fee --- /dev/null +++ b/src/features/data/apis/amm/balancer/common/type-guards.ts @@ -0,0 +1,15 @@ +import { BalancerFeature, type IBalancerAllPool, type IBalancerSinglePool } from '../types'; + +export function isBalancerSinglePool( + pool: IBalancerSinglePool | IBalancerAllPool +): pool is IBalancerSinglePool { + return ( + pool.supportsFeature(BalancerFeature.AddRemoveSingle) && 'quoteRemoveLiquidityOneToken' in pool + ); +} + +export function isBalancerAllPool( + pool: IBalancerSinglePool | IBalancerAllPool +): pool is IBalancerAllPool { + return pool.supportsFeature(BalancerFeature.AddRemoveAll) && 'getSwapRatios' in pool; +} diff --git a/src/features/data/apis/amm/balancer/common/types.ts b/src/features/data/apis/amm/balancer/common/types.ts new file mode 100644 index 000000000..172db83c7 --- /dev/null +++ b/src/features/data/apis/amm/balancer/common/types.ts @@ -0,0 +1,77 @@ +import type BigNumber from 'bignumber.js'; + +// https://docs.balancer.fi/reference/joins-and-exits/pool-joins.html#userdata +export enum PoolJoinKind { + INIT = 0, + EXACT_TOKENS_IN_FOR_BPT_OUT, + TOKEN_IN_FOR_EXACT_BPT_OUT, + ALL_TOKENS_IN_FOR_EXACT_BPT_OUT, +} + +// https://docs.balancer.fi/reference/joins-and-exits/pool-exits.html#userdata +export enum PoolExitKind { + EXACT_BPT_IN_FOR_ONE_TOKEN_OUT = 0, + EXACT_BPT_IN_FOR_TOKENS_OUT, + BPT_IN_FOR_EXACT_TOKENS_OUT, +} + +type JoinInit = { + /** Generic PoolJoinKind */ + kind: PoolJoinKind.INIT; + /** Pool specific uint256 that represents the generic JoinKind */ + kindValue: number; + initialBalances: BigNumber[]; +}; + +type JoinExactTokensInForBPTOut = { + kind: PoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT; + kindValue: number; + tokensIn: string[]; + amountsIn: BigNumber[]; + minimumBPT: BigNumber; +}; + +type JoinTokenInForExactBPTOut = { + kind: PoolJoinKind.TOKEN_IN_FOR_EXACT_BPT_OUT; + kindValue: number; + bptAmountOut: BigNumber; + enterTokenIndex: number; +}; + +type JoinAllTokensInForExactBPTOut = { + kind: PoolJoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT; + kindValue: number; + bptAmountOut: BigNumber; +}; + +export type JoinPoolUserData = + | JoinInit + | JoinExactTokensInForBPTOut + | JoinTokenInForExactBPTOut + | JoinAllTokensInForExactBPTOut; + +type ExitExactBPTInForOneTokenOut = { + kind: PoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT; + kindValue: number; + bptAmountIn: BigNumber; + exitTokenIndex: number; +}; + +type ExitExactBPTInForTokensOut = { + kind: PoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT; + kindValue: number; + bptAmountIn: BigNumber; +}; + +type ExitBPTInForExactTokensOut = { + kind: PoolExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT; + kindValue: number; + tokensOut: string[]; + amountsOut: BigNumber[]; + maxBPTAmountIn: BigNumber; +}; + +export type ExitPoolUserData = + | ExitExactBPTInForOneTokenOut + | ExitExactBPTInForTokensOut + | ExitBPTInForExactTokensOut; diff --git a/src/features/data/apis/amm/balancer/composable-stable/ComposableStablePool.ts b/src/features/data/apis/amm/balancer/composable-stable/ComposableStablePool.ts index 3772cde31..aab2ff1f4 100644 --- a/src/features/data/apis/amm/balancer/composable-stable/ComposableStablePool.ts +++ b/src/features/data/apis/amm/balancer/composable-stable/ComposableStablePool.ts @@ -1,66 +1,123 @@ -import type { IBalancerSwapPool } from '../types'; -import type BigNumber from 'bignumber.js'; -import { BIG_ZERO } from '../../../../../../helpers/big-number'; -import type { ZapStep } from '../../../transact/zap/types'; -import { createFactory } from '../../../../utils/factory-utils'; -import { Vault } from '../vault/Vault'; +import { BalancerFeature, type IBalancerAllPool, type IBalancerSinglePool } from '../types'; +import BigNumber from 'bignumber.js'; +import { BIG_ZERO, fromWei } from '../../../../../../helpers/big-number'; import type { ChainEntity } from '../../../../entities/chain'; import { + type ExitPoolRequest, + type JoinPoolRequest, type PoolConfig, - type QueryBatchSwapRequest, - SwapKind, + type QueryExitPoolResponse, + type QueryJoinPoolResponse, type VaultConfig, } from '../vault/types'; -import { getUnixNow } from '../../../../../../helpers/date'; +import { viemToWeb3Abi } from '../../../../../../helpers/web3'; +import { BalancerComposableStablePoolAbi } from '../../../../../../config/abi/BalancerComposableStablePoolAbi'; +import { FixedPoint } from '../common/FixedPoint'; +import { + type ExitPoolUserData, + type JoinPoolUserData, + PoolExitKind, + PoolJoinKind, +} from '../common/types'; +import { + poolExitKindToComposableStablePoolExitKind, + poolJoinKindToComposableStablePoolJoinKind, +} from './join-exit-kinds'; +import { SingleAllPool } from '../common/SingleAllPool'; -const THIRTY_MINUTES_IN_SECONDS = 30 * 60; +const SUPPORTED_FEATURES = new Set([ + BalancerFeature.AddRemoveAll, + BalancerFeature.AddRemoveSingle, + BalancerFeature.AddSlippage, + BalancerFeature.RemoveSlippage, +]); -export class ComposableStablePool implements IBalancerSwapPool { +export class ComposableStablePool + extends SingleAllPool + implements IBalancerSinglePool, IBalancerAllPool +{ public readonly type = 'balancer'; - public readonly subType = 'swap'; + + protected readonly bptIndex: number; constructor( - protected readonly chain: ChainEntity, - protected readonly vaultConfig: VaultConfig, - protected readonly config: PoolConfig + readonly chain: ChainEntity, + readonly vaultConfig: VaultConfig, + readonly config: PoolConfig ) { - this.checkToken(this.config.poolAddress, 'poolAddress'); + super(chain, vaultConfig, config); + if (config.bptIndex === undefined) { + throw new Error('BPT index is required for composable stable pools'); + } } - async quoteAddLiquidityOneToken(tokenIn: string, amountIn: BigNumber): Promise { - this.checkToken(tokenIn, 'tokenIn'); - this.checkAmount(amountIn, 'amountIn'); - - const request: QueryBatchSwapRequest = { - kind: SwapKind.GIVEN_IN, - swaps: [ - { - poolId: this.config.poolId, - assetInIndex: 0, - assetOutIndex: 1, - amount: amountIn, - userData: '0x', - }, - ], - assets: [tokenIn, this.config.poolAddress], - }; - - const vault = this.getVault(); - const [vaultInputDelta, vaultOutputDelta] = await vault.queryBatchSwap(request); + supportsFeature(feature: BalancerFeature): boolean { + return SUPPORTED_FEATURES.has(feature); + } - if (!vaultInputDelta.eq(amountIn)) { - throw new Error('Not all input used'); + protected getJoinKindValue(kind: PoolJoinKind): number { + const value = poolJoinKindToComposableStablePoolJoinKind[kind]; + if (value === undefined) { + throw new Error(`ComposableStablePool does not support join kind ${PoolJoinKind[kind]}`); } + return value; + } - if (vaultOutputDelta.gte(BIG_ZERO)) { - throw new Error('Output is negative'); + protected getExitKindValue(kind: PoolExitKind): number { + const value = poolExitKindToComposableStablePoolExitKind[kind]; + if (value === undefined) { + throw new Error(`ComposableStablePool does not support join kind ${PoolExitKind[kind]}`); } + return value; + } - return vaultOutputDelta.abs(); + /** + * The ratio of balances[n] * scaling factor[n] * token rate[n] over their sum + */ + async getSwapRatios(): Promise { + const upscaledBalances = this.dropBptIndex(await this.getUpscaledBalances()); + const totalUpscaledBalance = upscaledBalances.reduce((acc, b) => acc.plus(b), BIG_ZERO); + const lastIndex = upscaledBalances.length - 1; + const ratios = upscaledBalances.map((b, i) => + i === lastIndex ? BIG_ZERO : FixedPoint.divDown(b, totalUpscaledBalance) + ); + ratios[lastIndex] = FixedPoint.ONE.minus(ratios.reduce((acc, w) => acc.plus(w), BIG_ZERO)); + return ratios.map(r => fromWei(r, 18)); } - async quoteRemoveLiquidityOneToken(amountIn: BigNumber, tokenOut: string): Promise { - this.checkAmount(amountIn, 'amountIn'); + protected customizeJoinPoolRequest( + request: JoinPoolRequest + ): JoinPoolRequest { + // Add BPT token to request + request.assets = this.insertBptIndex(request.assets, this.config.poolAddress); + request.maxAmountsIn = this.insertBptIndex(request.maxAmountsIn, BIG_ZERO); + return request; + } + + protected customizeQueryJoinPoolResponse(response: QueryJoinPoolResponse): QueryJoinPoolResponse { + // Remove BPT token from the response + response.usedInput = this.dropBptIndex(response.usedInput); + response.unusedInput = this.dropBptIndex(response.unusedInput); + return response; + } + + protected customizeExitPoolRequest( + request: ExitPoolRequest + ): ExitPoolRequest { + // Add BPT token to request + request.assets = this.insertBptIndex(request.assets, this.config.poolAddress); + request.minAmountsOut = this.insertBptIndex(request.minAmountsOut, BIG_ZERO); + return request; + } + + protected customizeQueryExitPoolResponse(response: QueryExitPoolResponse): QueryExitPoolResponse { + // Remove BPT token from the response + response.outputs = this.dropBptIndex(response.outputs); + return response; + } + + /*async quoteRemoveLiquidityOneToken(liquidityIn: BigNumber, tokenOut: string): Promise { + this.checkAmount(liquidityIn, 'liquidityIn'); this.checkToken(tokenOut, 'tokenOut'); const request: QueryBatchSwapRequest = { @@ -70,7 +127,7 @@ export class ComposableStablePool implements IBalancerSwapPool { poolId: this.config.poolId, assetInIndex: 0, assetOutIndex: 1, - amount: amountIn, + amount: liquidityIn, userData: '0x', }, ], @@ -80,7 +137,7 @@ export class ComposableStablePool implements IBalancerSwapPool { const vault = this.getVault(); const [vaultInputDelta, vaultOutputDelta] = await vault.queryBatchSwap(request); - if (!vaultInputDelta.eq(amountIn)) { + if (!vaultInputDelta.eq(liquidityIn)) { throw new Error('Not all input used'); } @@ -89,19 +146,18 @@ export class ComposableStablePool implements IBalancerSwapPool { } return vaultOutputDelta.abs(); - } + }*/ - async getAddLiquidityOneTokenZap( - tokenIn: string, + /*async getAddLiquidityOneTokenZap( amountIn: BigNumber, - amountOutMin: BigNumber, + tokenIn: string, + liquidityOutMin: BigNumber, from: string, - insertBalance: boolean, - deadlineSeconds: number = THIRTY_MINUTES_IN_SECONDS + insertBalance: boolean ): Promise { this.checkToken(tokenIn, 'tokenIn'); this.checkAmount(amountIn, 'amountIn'); - this.checkAmount(amountOutMin, 'amountOutMin'); + this.checkAmount(liquidityOutMin, 'liquidityOutMin'); const vault = this.getVault(); return vault.getSwapZap({ @@ -120,15 +176,15 @@ export class ComposableStablePool implements IBalancerSwapPool { recipient: from, toInternalBalance: false, }, - limit: amountOutMin, - deadline: getUnixNow() + deadlineSeconds, + limit: liquidityOutMin, + deadline: getUnixNow() + THIRTY_MINUTES_IN_SECONDS, }, insertBalance, }); - } + }*/ - async getRemoveLiquidityOneTokenZap( - amountIn: BigNumber, + /*async getRemoveLiquidityOneTokenZap( + liquidityIn: BigNumber, tokenOut: string, amountOutMin: BigNumber, from: string, @@ -136,7 +192,7 @@ export class ComposableStablePool implements IBalancerSwapPool { deadlineSeconds: number = THIRTY_MINUTES_IN_SECONDS ): Promise { this.checkToken(tokenOut, 'tokenOut'); - this.checkAmount(amountIn, 'amountIn'); + this.checkAmount(liquidityIn, 'liquidityIn'); this.checkAmount(amountOutMin, 'amountOutMin'); const vault = this.getVault(); @@ -147,7 +203,7 @@ export class ComposableStablePool implements IBalancerSwapPool { kind: SwapKind.GIVEN_IN, assetIn: this.config.poolAddress, assetOut: tokenOut, - amount: amountIn, + amount: liquidityIn, userData: '0x', }, funds: { @@ -161,26 +217,50 @@ export class ComposableStablePool implements IBalancerSwapPool { }, insertBalance, }); + }*/ + + protected dropBptIndex(amounts: T[]): T[] { + return amounts.filter((_, i) => i !== this.config.bptIndex); } - protected getVault = createFactory(() => { - return new Vault(this.chain, this.vaultConfig); - }); + protected insertBptIndex(values: T[], value: T): T[] { + const result = [...values]; + result.splice(this.config.bptIndex!, 0, value); + return result; + } - protected checkAmount(amount: BigNumber, label: string = 'amount') { - if (amount.lte(BIG_ZERO)) { - throw new Error(`${label} must be greater than 0`); + /* protected checkAmount(amount: BigNumber, label: string = 'amount') { + if (amount.lte(BIG_ZERO)) { + throw new Error(`${label} must be greater than 0`); + } + + if ((amount.decimalPlaces() || 0) > 0) { + throw new Error(`${label} must be in wei`); + } } + + protected checkToken(tokenAddress: string, label: string = 'token'): number { + const index = this.config.tokens.findIndex(t => t.address === tokenAddress); + if (index === -1) { + throw new Error(`${label} must be a pool token`); + } + return index; + }*/ - if ((amount.decimalPlaces() || 0) > 0) { - throw new Error(`${label} must be in wei`); - } + /** + * For composable stable pools, the scaling factors include the token rate too + */ + protected async getScalingFactors() { + const pool = await this.getPoolContract(); + const factors: string[] = await pool.methods.getScalingFactors().call(); + return factors.map(f => new BigNumber(f)); } - protected checkToken(tokenAddress: string, label: string = 'token') { - const index = this.config.tokens.findIndex(t => t.address === tokenAddress); - if (index === -1) { - throw new Error(`${label} must be a pool token`); - } + protected async getPoolContract() { + const web3 = await this.getWeb3(); + return new web3.eth.Contract( + viemToWeb3Abi(BalancerComposableStablePoolAbi), + this.config.poolAddress + ); } } diff --git a/src/features/data/apis/amm/balancer/composable-stable/join-exit-kinds.ts b/src/features/data/apis/amm/balancer/composable-stable/join-exit-kinds.ts new file mode 100644 index 000000000..ee7e62122 --- /dev/null +++ b/src/features/data/apis/amm/balancer/composable-stable/join-exit-kinds.ts @@ -0,0 +1,28 @@ +import type { OptionalRecord } from '../../../../utils/types-utils'; +import { PoolExitKind, PoolJoinKind } from '../common/types'; +import { ComposableStablePoolExitKind, ComposableStablePoolJoinKind } from './types'; + +export const poolJoinKindToComposableStablePoolJoinKind: OptionalRecord< + PoolJoinKind, + ComposableStablePoolJoinKind +> = { + [PoolJoinKind.INIT]: ComposableStablePoolJoinKind.INIT, + [PoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT]: + ComposableStablePoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT, + [PoolJoinKind.TOKEN_IN_FOR_EXACT_BPT_OUT]: + ComposableStablePoolJoinKind.TOKEN_IN_FOR_EXACT_BPT_OUT, + [PoolJoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT]: + ComposableStablePoolJoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT, +}; + +export const poolExitKindToComposableStablePoolExitKind: OptionalRecord< + PoolExitKind, + ComposableStablePoolExitKind +> = { + [PoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT]: + ComposableStablePoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT, + [PoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT]: + ComposableStablePoolExitKind.EXACT_BPT_IN_FOR_ALL_TOKENS_OUT, + [PoolExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT]: + ComposableStablePoolExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT, +}; diff --git a/src/features/data/apis/amm/balancer/composable-stable/types.ts b/src/features/data/apis/amm/balancer/composable-stable/types.ts new file mode 100644 index 000000000..4358ec223 --- /dev/null +++ b/src/features/data/apis/amm/balancer/composable-stable/types.ts @@ -0,0 +1,12 @@ +export enum ComposableStablePoolJoinKind { + INIT = 0, + EXACT_TOKENS_IN_FOR_BPT_OUT, + TOKEN_IN_FOR_EXACT_BPT_OUT, + ALL_TOKENS_IN_FOR_EXACT_BPT_OUT, +} + +export enum ComposableStablePoolExitKind { + EXACT_BPT_IN_FOR_ONE_TOKEN_OUT = 0, + BPT_IN_FOR_EXACT_TOKENS_OUT, + EXACT_BPT_IN_FOR_ALL_TOKENS_OUT, +} diff --git a/src/features/data/apis/amm/balancer/gyro/GyroPool.ts b/src/features/data/apis/amm/balancer/gyro/GyroPool.ts index 3dd2643cd..8cd8f2f18 100644 --- a/src/features/data/apis/amm/balancer/gyro/GyroPool.ts +++ b/src/features/data/apis/amm/balancer/gyro/GyroPool.ts @@ -6,23 +6,27 @@ import type { } from '../vault/types'; import type { RatesResult } from './types'; import BigNumber from 'bignumber.js'; -import { - BIG_ONE, - bigNumberToStringDeep, - bigNumberToUint256String, -} from '../../../../../../helpers/big-number'; -import { JoinExitEncoder } from '../join/JoinExitEncoder'; -import { FixedPoint } from '../join/FixedPoint'; +import { BIG_ONE, bigNumberToStringDeep } from '../../../../../../helpers/big-number'; +import { FixedPoint } from '../common/FixedPoint'; import type { ZapStep } from '../../../transact/zap/types'; import { WeightedMath } from '../weighted/WeightedMath'; import type { ChainEntity } from '../../../../entities/chain'; import { viemToWeb3Abi } from '../../../../../../helpers/web3'; import { BalancerGyroEPoolAbi } from '../../../../../../config/abi/BalancerGyroEPoolAbi'; -import { JoinPool } from '../join/JoinPool'; -import type { IBalancerJoinPool } from '../types'; +import { AllPool } from '../common/AllPool'; +import { BalancerFeature, type IBalancerAllPool } from '../types'; +import { PoolExitKind, PoolJoinKind } from '../common/types'; +import { poolExitKindToGyroPoolExitKind, poolJoinKindToGyroPoolJoinKind } from './join-exit-kinds'; + +const SUPPORTED_FEATURES = new Set([ + BalancerFeature.AddRemoveAll, + // BalancerFeature.AddRemoveSingle, // Gyro pools only support JoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT / ExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT + // BalancerFeature.AddSlippage, + BalancerFeature.RemoveSlippage, +]); // Covers Gyro and GyroE -export class GyroPool extends JoinPool implements IBalancerJoinPool { +export class GyroPool extends AllPool implements IBalancerAllPool { constructor( readonly chain: ChainEntity, readonly vaultConfig: VaultConfig, @@ -33,8 +37,24 @@ export class GyroPool extends JoinPool implements IBalancerJoinPool { this.getTokenRates = this.cacheMethod(this.getTokenRates); } - get joinSupportsSlippage() { - return false; + supportsFeature(feature: BalancerFeature): boolean { + return SUPPORTED_FEATURES.has(feature); + } + + protected getJoinKindValue(kind: PoolJoinKind): number { + const value = poolJoinKindToGyroPoolJoinKind[kind]; + if (value === undefined) { + throw new Error(`GyroPool does not support join kind ${PoolJoinKind[kind]}`); + } + return value; + } + + protected getExitKindValue(kind: PoolExitKind): number { + const value = poolExitKindToGyroPoolExitKind[kind]; + if (value === undefined) { + throw new Error(`GyroPool does not support join kind ${PoolExitKind[kind]}`); + } + return value; } // async getSwapRatios(): Promise { @@ -83,14 +103,16 @@ export class GyroPool extends JoinPool implements IBalancerJoinPool { poolId: this.config.poolId, sender: from, recipient: from, - request: { + request: this.customizeJoinPoolRequest({ assets: this.config.tokens.map(t => t.address), maxAmountsIn: maxAmountsIn, - userData: JoinExitEncoder.joinAllTokensInForExactBPTOut( - bigNumberToUint256String(liquidity) - ), + userData: { + kind: PoolJoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT, + kindValue: this.getJoinKindValue(PoolJoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT), + bptAmountOut: liquidity, + }, fromInternalBalance: false, - }, + }), }, insertBalance, }); @@ -158,14 +180,16 @@ export class GyroPool extends JoinPool implements IBalancerJoinPool { // double check via rpc call const queryRequest: QueryJoinPoolRequest = { poolId: this.config.poolId, - request: { + request: this.customizeJoinPoolRequest({ assets: this.config.tokens.map(t => t.address), maxAmountsIn: amountsIn, - userData: JoinExitEncoder.joinAllTokensInForExactBPTOut( - bigNumberToUint256String(estimatedLiquidity) - ), + userData: { + kind: PoolJoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT, + kindValue: this.getJoinKindValue(PoolJoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT), + bptAmountOut: estimatedLiquidity, + }, fromInternalBalance: false, - }, + }), }; const queryResult = await vault.queryJoinPool(queryRequest); console.debug( diff --git a/src/features/data/apis/amm/balancer/gyro/join-exit-kinds.ts b/src/features/data/apis/amm/balancer/gyro/join-exit-kinds.ts new file mode 100644 index 000000000..20eb93059 --- /dev/null +++ b/src/features/data/apis/amm/balancer/gyro/join-exit-kinds.ts @@ -0,0 +1,11 @@ +import { PoolExitKind, PoolJoinKind } from '../common/types'; +import type { OptionalRecord } from '../../../../utils/types-utils'; +import { GyroPoolExitKind, GyroPoolJoinKind } from './types'; + +export const poolJoinKindToGyroPoolJoinKind: OptionalRecord = { + [PoolJoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT]: GyroPoolJoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT, +}; + +export const poolExitKindToGyroPoolExitKind: OptionalRecord = { + [PoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT]: GyroPoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT, +}; diff --git a/src/features/data/apis/amm/balancer/gyro/types.ts b/src/features/data/apis/amm/balancer/gyro/types.ts index 75efb59ac..be03e1d2a 100644 --- a/src/features/data/apis/amm/balancer/gyro/types.ts +++ b/src/features/data/apis/amm/balancer/gyro/types.ts @@ -1,3 +1,11 @@ +export enum GyroPoolJoinKind { + ALL_TOKENS_IN_FOR_EXACT_BPT_OUT = 3, +} + +export enum GyroPoolExitKind { + EXACT_BPT_IN_FOR_TOKENS_OUT = 1, +} + export type RatesResult = { rate0: string; rate1: string; diff --git a/src/features/data/apis/amm/balancer/join/JoinExitEncoder.ts b/src/features/data/apis/amm/balancer/join/JoinExitEncoder.ts deleted file mode 100644 index 446bb04d6..000000000 --- a/src/features/data/apis/amm/balancer/join/JoinExitEncoder.ts +++ /dev/null @@ -1,89 +0,0 @@ -import abiCoder from 'web3-eth-abi'; -import { WeightedPoolExitKind, WeightedPoolJoinKind } from '../weighted/types'; - -export class JoinExitEncoder { - private constructor() { - // static only - } - - /** - * Encodes the userData parameter for providing the initial liquidity to a WeightedPool - * @param initialBalances - the amounts of tokens to send to the pool to form the initial balances - */ - static joinInit(initialBalances: string[]): string { - return abiCoder.encodeParameters( - ['uint256', 'uint256[]'], - [WeightedPoolJoinKind.INIT, initialBalances] - ); - } - - /** - * Encodes the userData parameter for joining a WeightedPool with exact token inputs - * @param amountsIn - the amounts each of token to deposit in the pool as liquidity - * @param minimumBPT - the minimum acceptable BPT to receive in return for deposited tokens - */ - static joinExactTokensInForBPTOut(amountsIn: string[], minimumBPT: string): string { - return abiCoder.encodeParameters( - ['uint256', 'uint256[]', 'uint256'], - [WeightedPoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT, amountsIn, minimumBPT] - ); - } - - /** - * Encodes the userData parameter for joining a WeightedPool with a single token to receive an exact amount of BPT - * @param bptAmountOut - the amount of BPT to be minted - * @param enterTokenIndex - the index of the token to be provided as liquidity - */ - static joinTokenInForExactBPTOut(bptAmountOut: string, enterTokenIndex: number): string { - return abiCoder.encodeParameters( - ['uint256', 'uint256', 'uint256'], - [WeightedPoolJoinKind.TOKEN_IN_FOR_EXACT_BPT_OUT, bptAmountOut, enterTokenIndex] - ); - } - - /** - * Encodes the userData parameter for joining a WeightedPool proportionally to receive an exact amount of BPT - * @param bptAmountOut - the amount of BPT to be minted - */ - static joinAllTokensInForExactBPTOut(bptAmountOut: string): string { - return abiCoder.encodeParameters( - ['uint256', 'uint256'], - [WeightedPoolJoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT, bptAmountOut] - ); - } - - /** - * Encodes the userData parameter for exiting a WeightedPool by removing a single token in return for an exact amount of BPT - * @param bptAmountIn - the amount of BPT to be burned - * @param exitTokenIndex - the index of the token to removed from the pool - */ - static exitExactBPTInForOneTokenOut(bptAmountIn: string, exitTokenIndex: number): string { - return abiCoder.encodeParameters( - ['uint256', 'uint256', 'uint256'], - [WeightedPoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT, bptAmountIn, exitTokenIndex] - ); - } - - /** - * Encodes the userData parameter for exiting a WeightedPool by removing tokens in return for an exact amount of BPT - * @param bptAmountIn - the amount of BPT to be burned - */ - static exitExactBPTInForTokensOut(bptAmountIn: string): string { - return abiCoder.encodeParameters( - ['uint256', 'uint256'], - [WeightedPoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT, bptAmountIn] - ); - } - - /** - * Encodes the userData parameter for exiting a WeightedPool by removing exact amounts of tokens - * @param amountsOut - the amounts of each token to be withdrawn from the pool - * @param maxBPTAmountIn - the minimum acceptable BPT to burn in return for withdrawn tokens - */ - static exitBPTInForExactTokensOut(amountsOut: string[], maxBPTAmountIn: string): string { - return abiCoder.encodeParameters( - ['uint256', 'uint256[]', 'uint256'], - [WeightedPoolExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT, amountsOut, maxBPTAmountIn] - ); - } -} diff --git a/src/features/data/apis/amm/balancer/join/JoinPool.ts b/src/features/data/apis/amm/balancer/join/JoinPool.ts deleted file mode 100644 index 2d718bfc5..000000000 --- a/src/features/data/apis/amm/balancer/join/JoinPool.ts +++ /dev/null @@ -1,219 +0,0 @@ -import type { IBalancerJoinPool } from '../types'; -import type { ChainEntity } from '../../../../entities/chain'; -import type { - PoolConfig, - QueryExitPoolRequest, - QueryExitPoolResponse, - QueryJoinPoolRequest, - QueryJoinPoolResponse, - VaultConfig, -} from '../vault/types'; -import { createFactory } from '../../../../utils/factory-utils'; -import { getWeb3Instance } from '../../../instances'; -import { Vault } from '../vault/Vault'; -import { - BIG_ONE, - BIG_ZERO, - bigNumberToStringDeep, - bigNumberToUint256String, -} from '../../../../../../helpers/big-number'; -import type { ZapStep } from '../../../transact/zap/types'; -import { checkAddressOrder } from '../../../../../../helpers/tokens'; -import type { Contract } from 'web3-eth-contract'; -import BigNumber from 'bignumber.js'; -import { JoinExitEncoder } from './JoinExitEncoder'; -import { FixedPoint } from './FixedPoint'; - -export abstract class JoinPool implements IBalancerJoinPool { - public readonly type = 'balancer'; - public readonly subType = 'join'; - - protected constructor( - protected readonly chain: ChainEntity, - protected readonly vaultConfig: VaultConfig, - protected readonly config: PoolConfig - ) { - checkAddressOrder(config.tokens.map(t => t.address)); - - this.getSwapRatios = this.cacheMethod(this.getSwapRatios); - this.getScalingFactors = this.cacheMethod(this.getScalingFactors); - this.getPoolTokens = this.cacheMethod(this.getPoolTokens); - this.getBalances = this.cacheMethod(this.getBalances); - this.getUpscaledBalances = this.cacheMethod(this.getUpscaledBalances); - this.getTotalSupply = this.cacheMethod(this.getTotalSupply); - this.getWeb3 = this.cacheMethod(this.getWeb3); - this.getPoolContract = this.cacheMethod(this.getPoolContract); - this.getVault = this.cacheMethod(this.getVault); - } - - abstract get joinSupportsSlippage(): boolean; - - abstract getSwapRatios(): Promise; - - protected abstract getPoolContract(): Promise; - - async getAddLiquidityZap( - amountsIn: BigNumber[], - minLiquidity: BigNumber, - from: string, - insertBalance: boolean - ): Promise { - const vault = this.getVault(); - - return vault.getJoinPoolZap({ - join: { - poolId: this.config.poolId, - sender: from, - recipient: from, - request: { - assets: this.config.tokens.map(t => t.address), - maxAmountsIn: amountsIn, - userData: JoinExitEncoder.joinExactTokensInForBPTOut( - amountsIn.map(amount => bigNumberToUint256String(amount)), - bigNumberToUint256String(minLiquidity) - ), - fromInternalBalance: false, - }, - }, - insertBalance, - }); - } - - async quoteAddLiquidity(amountsIn: BigNumber[]): Promise { - if (amountsIn.every(amount => amount.lte(BIG_ZERO))) { - throw new Error('At least one input amount must be greater than 0'); - } - - const vault = this.getVault(); - const queryRequest: QueryJoinPoolRequest = { - poolId: this.config.poolId, - request: { - assets: this.config.tokens.map(t => t.address), - maxAmountsIn: amountsIn, - userData: JoinExitEncoder.joinExactTokensInForBPTOut( - amountsIn.map(amount => bigNumberToUint256String(amount)), - '0' - ), - fromInternalBalance: false, - }, - }; - const queryResult = await vault.queryJoinPool(queryRequest); - - console.debug( - 'queryJoinPool', - bigNumberToStringDeep(queryRequest), - bigNumberToStringDeep(queryResult) - ); - - return { - liquidity: queryResult.liquidity, - usedInput: queryResult.usedInput, - unusedInput: amountsIn.map((amount, i) => amount.minus(queryResult.usedInput[i])), - }; - } - - async quoteRemoveLiquidity(amountIn: BigNumber): Promise { - if (amountIn.lt(BIG_ONE)) { - throw new Error('Input amount must be greater than 0'); - } - - const vault = this.getVault(); - - const queryRequest: QueryExitPoolRequest = { - poolId: this.config.poolId, - request: { - assets: this.config.tokens.map(t => t.address), - minAmountsOut: this.config.tokens.map(() => BIG_ZERO), - userData: JoinExitEncoder.exitExactBPTInForTokensOut(bigNumberToUint256String(amountIn)), - toInternalBalance: false, - }, - }; - - return await vault.queryExitPool(queryRequest); - } - - async getRemoveLiquidityZap( - amountIn: BigNumber, - minAmountsOut: BigNumber[], - from: string, - insertBalance: boolean - ): Promise { - const vault = this.getVault(); - - return vault.getExitPoolZap({ - exit: { - poolId: this.config.poolId, - sender: from, - recipient: from, - request: { - assets: this.config.tokens.map(t => t.address), - minAmountsOut, - userData: JoinExitEncoder.exitExactBPTInForTokensOut(bigNumberToUint256String(amountIn)), - toInternalBalance: false, - }, - }, - poolAddress: this.config.poolAddress, - insertBalance, - }); - } - - /** - * Multiplier to normalize to 18 decimals - */ - protected async getScalingFactors() { - return this.config.tokens.map(token => { - if (token.address === this.config.poolAddress) { - return FixedPoint.ONE; - } - - if (token.decimals > 18) { - throw new Error('Tokens with more than 18 decimals are not supported.'); - } - - const diff = 18 - token.decimals; - return FixedPoint.ONE.shiftedBy(diff); - }); - } - - protected async upscaleAmounts(balances: BigNumber[]): Promise { - const factors = await this.getScalingFactors(); - return balances.map((balance, i) => FixedPoint.mulDown(balance, factors[i])); - } - - protected async downscaleAmounts(amounts: BigNumber[]): Promise { - const factors = await this.getScalingFactors(); - return amounts.map((amount, i) => FixedPoint.divUp(amount, factors[i])); - } - - protected async getPoolTokens() { - const vault = this.getVault(); - return await vault.getPoolTokens(this.config.poolId); - } - - protected async getBalances() { - const poolTokens = await this.getPoolTokens(); - return poolTokens.map(t => t.balance); - } - - protected async getUpscaledBalances() { - return await this.upscaleAmounts(await this.getBalances()); - } - - protected async getTotalSupply(): Promise { - const pool = await this.getPoolContract(); - const totalSupply: string = await pool.methods.getActualSupply().call(); - return new BigNumber(totalSupply); - } - - protected async getWeb3() { - return getWeb3Instance(this.chain); - } - - protected getVault() { - return new Vault(this.chain, this.vaultConfig); - } - - protected cacheMethod unknown>(fn: T): T { - return createFactory(fn.bind(this)) as T; - } -} diff --git a/src/features/data/apis/amm/balancer/meta-stable/MetaStablePool.ts b/src/features/data/apis/amm/balancer/meta-stable/MetaStablePool.ts index 8f8d7f9ca..3112732e1 100644 --- a/src/features/data/apis/amm/balancer/meta-stable/MetaStablePool.ts +++ b/src/features/data/apis/amm/balancer/meta-stable/MetaStablePool.ts @@ -4,11 +4,23 @@ import BigNumber from 'bignumber.js'; import { viemToWeb3Abi } from '../../../../../../helpers/web3'; import { BalancerMetaStablePoolAbi } from '../../../../../../config/abi/BalancerMetaStablePoolAbi'; import { BIG_ZERO, fromWei } from '../../../../../../helpers/big-number'; -import { FixedPoint } from '../join/FixedPoint'; -import { JoinPool } from '../join/JoinPool'; -import type { IBalancerJoinPool } from '../types'; +import { FixedPoint } from '../common/FixedPoint'; +import { BalancerFeature, type IBalancerAllPool, type IBalancerSinglePool } from '../types'; +import { PoolExitKind, PoolJoinKind } from '../common/types'; +import { + poolExitKindToMetaStablePoolExitKind, + poolJoinKindToMetaStablePoolJoinKind, +} from './join-exit-kinds'; +import { SingleAllPool } from '../common/SingleAllPool'; -export class MetaStablePool extends JoinPool implements IBalancerJoinPool { +const SUPPORTED_FEATURES = new Set([ + BalancerFeature.AddRemoveAll, + BalancerFeature.AddRemoveSingle, + BalancerFeature.AddSlippage, + BalancerFeature.RemoveSlippage, +]); + +export class MetaStablePool extends SingleAllPool implements IBalancerSinglePool, IBalancerAllPool { constructor( readonly chain: ChainEntity, readonly vaultConfig: VaultConfig, @@ -20,8 +32,24 @@ export class MetaStablePool extends JoinPool implements IBalancerJoinPool { this.getPoolContract = this.cacheMethod(this.getPoolContract); } - get joinSupportsSlippage(): boolean { - return true; + supportsFeature(feature: BalancerFeature): boolean { + return SUPPORTED_FEATURES.has(feature); + } + + protected getJoinKindValue(kind: PoolJoinKind): number { + const value = poolJoinKindToMetaStablePoolJoinKind[kind]; + if (value === undefined) { + throw new Error(`MetaStablePool does not support join kind ${PoolJoinKind[kind]}`); + } + return value; + } + + protected getExitKindValue(kind: PoolExitKind): number { + const value = poolExitKindToMetaStablePoolExitKind[kind]; + if (value === undefined) { + throw new Error(`MetaStablePool does not support join kind ${PoolExitKind[kind]}`); + } + return value; } /** diff --git a/src/features/data/apis/amm/balancer/meta-stable/join-exit-kinds.ts b/src/features/data/apis/amm/balancer/meta-stable/join-exit-kinds.ts new file mode 100644 index 000000000..8dcafd380 --- /dev/null +++ b/src/features/data/apis/amm/balancer/meta-stable/join-exit-kinds.ts @@ -0,0 +1,22 @@ +import { PoolExitKind, PoolJoinKind } from '../common/types'; +import type { OptionalRecord } from '../../../../utils/types-utils'; +import { MetaStablePoolExitKind, MetaStablePoolJoinKind } from './types'; + +export const poolJoinKindToMetaStablePoolJoinKind: OptionalRecord< + PoolJoinKind, + MetaStablePoolJoinKind +> = { + [PoolJoinKind.INIT]: MetaStablePoolJoinKind.INIT, + [PoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT]: MetaStablePoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT, + [PoolJoinKind.TOKEN_IN_FOR_EXACT_BPT_OUT]: MetaStablePoolJoinKind.TOKEN_IN_FOR_EXACT_BPT_OUT, +}; + +export const poolExitKindToMetaStablePoolExitKind: OptionalRecord< + PoolExitKind, + MetaStablePoolExitKind +> = { + [PoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT]: + MetaStablePoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT, + [PoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT]: MetaStablePoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT, + [PoolExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT]: MetaStablePoolExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT, +}; diff --git a/src/features/data/apis/amm/balancer/meta-stable/types.ts b/src/features/data/apis/amm/balancer/meta-stable/types.ts new file mode 100644 index 000000000..05199fa6d --- /dev/null +++ b/src/features/data/apis/amm/balancer/meta-stable/types.ts @@ -0,0 +1,11 @@ +export enum MetaStablePoolJoinKind { + INIT = 0, + EXACT_TOKENS_IN_FOR_BPT_OUT, + TOKEN_IN_FOR_EXACT_BPT_OUT, +} + +export enum MetaStablePoolExitKind { + EXACT_BPT_IN_FOR_ONE_TOKEN_OUT = 0, + EXACT_BPT_IN_FOR_TOKENS_OUT, + BPT_IN_FOR_EXACT_TOKENS_OUT, +} diff --git a/src/features/data/apis/amm/balancer/types.ts b/src/features/data/apis/amm/balancer/types.ts index a22b97776..2989805e5 100644 --- a/src/features/data/apis/amm/balancer/types.ts +++ b/src/features/data/apis/amm/balancer/types.ts @@ -2,14 +2,25 @@ import type BigNumber from 'bignumber.js'; import type { ZapStep } from '../../transact/zap/types'; import type { QueryExitPoolResponse, QueryJoinPoolResponse } from './vault/types'; +export enum BalancerFeature { + /** Add/Remove liquidity using only 1 token of the pool */ + AddRemoveSingle, + /** Add/Remove liquidity using all tokens of the pool in balanced ratio */ + AddRemoveAll, + /** Add liquidity supports slippage allowance */ + AddSlippage, + /** Remove liquidity supports slippage allowance */ + RemoveSlippage, +} + export interface IBalancerPool { readonly type: 'balancer'; -} -export interface IBalancerJoinPool extends IBalancerPool { - readonly subType: 'join'; - readonly joinSupportsSlippage: boolean; + supportsFeature(feature: BalancerFeature): boolean; +} +/** Join/Exit with all tokens in ratio */ +export interface IBalancerAllPool extends IBalancerPool { getSwapRatios(): Promise; quoteAddLiquidity(amountsIn: BigNumber[]): Promise; getAddLiquidityZap( @@ -27,6 +38,25 @@ export interface IBalancerJoinPool extends IBalancerPool { ): Promise; } -export interface IBalancerSwapPool extends IBalancerPool { - readonly subType: 'swap'; +/** Join/Exit with one token */ +export interface IBalancerSinglePool extends IBalancerPool { + quoteAddLiquidityOneToken(amountIn: BigNumber, tokenIn: string): Promise; + quoteRemoveLiquidityOneToken( + liquidityIn: BigNumber, + tokenOut: string + ): Promise; + getAddLiquidityOneTokenZap( + amountIn: BigNumber, + tokenIn: string, + liquidityOutMin: BigNumber, + from: string, + insertBalance: boolean + ): Promise; + getRemoveLiquidityOneTokenZap( + liquidityIn: BigNumber, + tokenOut: string, + amountOutMin: BigNumber, + from: string, + insertBalance: boolean + ): Promise; } diff --git a/src/features/data/apis/amm/balancer/vault/Vault.ts b/src/features/data/apis/amm/balancer/vault/Vault.ts index c67fa712d..42fc4d775 100644 --- a/src/features/data/apis/amm/balancer/vault/Vault.ts +++ b/src/features/data/apis/amm/balancer/vault/Vault.ts @@ -39,7 +39,8 @@ import { import type { StepToken, ZapStep } from '../../../transact/zap/types'; import { getInsertIndex } from '../../../transact/helpers/zap'; import { getAddress } from 'viem'; -import { WeightedPoolExitKind, WeightedPoolJoinKind } from '../weighted/types'; +import { PoolExitKind, PoolJoinKind } from '../common/types'; +import { JoinExitEncoder } from '../common/JoinExitEncoder'; const queryFunds: FundManagement = { sender: ZERO_ADDRESS, @@ -75,10 +76,26 @@ export class Vault { const query = await this.getQueryContract(); const args: JoinPoolArgs = { ...request, + request: { + ...request.request, + userData: JoinExitEncoder.encodeJoin(request.request.userData), + }, sender: ZERO_ADDRESS, recipient: ZERO_ADDRESS, }; + console.debug('queryJoinPool', { + poolId: args.poolId, + sender: args.sender, + recipient: args.recipient, + request: JSON.stringify({ + assets: args.request.assets, + maxAmountsIn: args.request.maxAmountsIn.map(bigNumberToUint256String), + userData: args.request.userData, + fromInternalBalance: args.request.fromInternalBalance, + }), + }); + const result: JoinPoolResult = await query.methods .queryJoin(args.poolId, args.sender, args.recipient, [ args.request.assets, @@ -111,6 +128,10 @@ export class Vault { const query = await this.getQueryContract(); const args: ExitPoolArgs = { ...request, + request: { + ...request.request, + userData: JoinExitEncoder.encodeExit(request.request.userData), + }, sender: ZERO_ADDRESS, recipient: ZERO_ADDRESS, }; @@ -178,10 +199,18 @@ export class Vault { } async getJoinPoolZap(request: JoinPoolZapRequest): Promise { + const args: JoinPoolArgs = { + ...request.join, + request: { + ...request.join.request, + userData: JoinExitEncoder.encodeJoin(request.join.request.userData), + }, + }; + return { target: this.config.vaultAddress, value: '0', - data: this.encodeJoinPool(request.join), + data: this.encodeJoinPool(args), tokens: this.getJoinPoolZapTokens(request), }; } @@ -217,45 +246,48 @@ export class Vault { index: getInsertIndex(maxAmountsInStartWord + i), })); - // Maybe insert amounts into the userData bytes + // Maybe insert amountsIn into the userData bytes const userDataWordStart = 9 + request.join.request.assets.length + 1 + request.join.request.maxAmountsIn.length + 1; - const joinKindString = abiCoder.decodeParameter( - 'uint256', - request.join.request.userData - ) as unknown as string; - console.log('joinKindString', joinKindString); - const joinKind = new BigNumber(joinKindString).toNumber(); - - switch (joinKind) { - case WeightedPoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT: { - for (let i = 0; i < request.join.request.assets.length; i++) { + + const join = request.join.request.userData; + switch (join.kind) { + case PoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT: { + for (let i = 0; i < join.tokensIn.length; i++) { tokens.push({ - token: request.join.request.assets[i], + token: join.tokensIn[i], index: getInsertIndex(userDataWordStart + 4 + i), // 0 = JoinKind, 1 = Offset to amountsIn, 2 = minBPTAmountOut, 3 = amountsIn.length, 4 = amountsIn[0], 4+n = amountsIn[n] }); } return tokens; } - case WeightedPoolJoinKind.TOKEN_IN_FOR_EXACT_BPT_OUT: { + case PoolJoinKind.TOKEN_IN_FOR_EXACT_BPT_OUT: { // 0 = JoinKind, 1 = bptAmountOut, 2 = enterTokenIndex return tokens; } - case WeightedPoolJoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT: { + case PoolJoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT: { // 0 = JoinKind, 1 = bptAmountOut return tokens; } default: { - throw new Error(`Unsupported join kind: ${joinKind}`); + throw new Error(`Unsupported join kind: ${join.kind}`); } } } async getExitPoolZap(request: ExitPoolZapRequest): Promise { + const args: ExitPoolArgs = { + ...request.exit, + request: { + ...request.exit.request, + userData: JoinExitEncoder.encodeExit(request.exit.request.userData), + }, + }; + return { target: this.config.vaultAddress, value: '0', - data: this.encodeExitPool(request.exit), + data: this.encodeExitPool(args), tokens: this.getExitPoolZapTokens(request), }; } @@ -287,15 +319,10 @@ export class Vault { // Insert the amount of bpt to burn into the userData bytes const userDataWordStart = 9 + request.exit.request.assets.length + 1 + request.exit.request.minAmountsOut.length + 1; - const exitKindString = abiCoder.decodeParameter( - 'uint256', - request.exit.request.userData - ) as unknown as string; - console.log('exitKindString', exitKindString); - const exitKind = new BigNumber(exitKindString).toNumber(); - - switch (exitKind) { - case WeightedPoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT: { + + const exit = request.exit.request.userData; + switch (exit.kind) { + case PoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT: { return [ { token: request.poolAddress, @@ -303,7 +330,7 @@ export class Vault { }, ]; } - case WeightedPoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT: { + case PoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT: { return [ { token: request.poolAddress, @@ -311,7 +338,7 @@ export class Vault { }, ]; } - case WeightedPoolExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT: { + case PoolExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT: { return [ { token: request.poolAddress, @@ -320,7 +347,8 @@ export class Vault { ]; } default: { - throw new Error(`Unsupported exit kind: ${exitKind}`); + // @ts-expect-error - if all cases are handled + throw new Error(`Unsupported exit kind: ${exit.kind}`); } } } diff --git a/src/features/data/apis/amm/balancer/vault/types.ts b/src/features/data/apis/amm/balancer/vault/types.ts index f7658b07f..6ee9eec52 100644 --- a/src/features/data/apis/amm/balancer/vault/types.ts +++ b/src/features/data/apis/amm/balancer/vault/types.ts @@ -1,5 +1,7 @@ import type BigNumber from 'bignumber.js'; import type { TokenEntity } from '../../../../entities/token'; +import type { Hex } from 'viem/types/misc'; +import type { ExitPoolUserData, JoinPoolUserData } from '../common/types'; export type VaultConfig = { /** address */ @@ -14,6 +16,7 @@ export type PoolConfig = { /** bytes32 */ poolId: string; tokens: TokenEntity[]; + bptIndex?: number; }; export enum SwapKind { @@ -97,18 +100,18 @@ export type BatchSwapArgs = { deadline: number; }; -export type JoinPoolRequest = { +export type JoinPoolRequest = { /** address[] */ assets: string[]; /** uint256[] */ maxAmountsIn: BigNumber[]; /** bytes */ - userData: string; + userData: TUserData; /** bool */ fromInternalBalance: boolean; }; -export type JoinPoolArgs = { +export type JoinPoolArgs = { /** bytes32 */ poolId: string; /** address */ @@ -116,7 +119,7 @@ export type JoinPoolArgs = { /** address */ recipient: string; /** tuple */ - request: JoinPoolRequest; + request: JoinPoolRequest; }; export type JoinPoolResult = { @@ -124,18 +127,18 @@ export type JoinPoolResult = { amountsIn: string[]; }; -export type ExitPoolRequest = { +export type ExitPoolRequest = { /** address[] */ assets: string[]; /** uint256[] */ minAmountsOut: BigNumber[]; /** bytes */ - userData: string; + userData: TUserData; /** bool */ toInternalBalance: boolean; }; -export type ExitPoolArgs = { +export type ExitPoolArgs = { /** bytes32 */ poolId: string; /** address */ @@ -143,7 +146,7 @@ export type ExitPoolArgs = { /** address */ recipient: string; /** tuple */ - request: ExitPoolRequest; + request: ExitPoolRequest; }; export type ExitPoolResult = { @@ -158,14 +161,14 @@ export type AbiEncodeArgs = string | number | boolean | Array; export type QueryBatchSwapRequest = Omit; export type QueryBatchSwapResponse = BigNumber[]; -export type QueryJoinPoolRequest = Omit; +export type QueryJoinPoolRequest = Omit, 'sender' | 'recipient'>; export type QueryJoinPoolResponse = { liquidity: BigNumber; usedInput: BigNumber[]; unusedInput: BigNumber[]; }; -export type QueryExitPoolRequest = Omit; +export type QueryExitPoolRequest = Omit, 'sender' | 'recipient'>; export type QueryExitPoolResponse = { liquidity: BigNumber; outputs: BigNumber[]; @@ -185,12 +188,12 @@ export type PoolTokensResult = { export type PoolTokensResponse = Array<{ token: string; balance: BigNumber }>; export type JoinPoolZapRequest = { - join: JoinPoolArgs; + join: JoinPoolArgs; insertBalance: boolean; }; export type ExitPoolZapRequest = { - exit: ExitPoolArgs; + exit: ExitPoolArgs; poolAddress: string; insertBalance: boolean; }; diff --git a/src/features/data/apis/amm/balancer/weighted/WeightedMath.ts b/src/features/data/apis/amm/balancer/weighted/WeightedMath.ts index fb7c8e30b..76a08b994 100644 --- a/src/features/data/apis/amm/balancer/weighted/WeightedMath.ts +++ b/src/features/data/apis/amm/balancer/weighted/WeightedMath.ts @@ -1,6 +1,5 @@ import type BigNumber from 'bignumber.js'; -import { FixedPoint } from '../join/FixedPoint'; -import { BIG_ZERO } from '../../../../../../helpers/big-number'; +import { FixedPoint } from '../common/FixedPoint'; export class WeightedMath { private constructor() { diff --git a/src/features/data/apis/amm/balancer/weighted/WeightedPool.ts b/src/features/data/apis/amm/balancer/weighted/WeightedPool.ts index 5449eb28d..355c03d0c 100644 --- a/src/features/data/apis/amm/balancer/weighted/WeightedPool.ts +++ b/src/features/data/apis/amm/balancer/weighted/WeightedPool.ts @@ -1,4 +1,4 @@ -import type { IBalancerJoinPool } from '../types'; +import { BalancerFeature, type IBalancerAllPool, type IBalancerSinglePool } from '../types'; import type { ChainEntity } from '../../../../entities/chain'; import type { PoolConfig, VaultConfig } from '../vault/types'; import { viemToWeb3Abi } from '../../../../../../helpers/web3'; @@ -6,9 +6,21 @@ import type { NormalizedWeightsResult } from './types'; import BigNumber from 'bignumber.js'; import { fromWei } from '../../../../../../helpers/big-number'; import { BalancerWeightedPoolAbi } from '../../../../../../config/abi/BalancerWeightedPoolAbi'; -import { JoinPool } from '../join/JoinPool'; +import { PoolExitKind, PoolJoinKind } from '../common/types'; +import { + poolExitKindToWeightedPoolExitKind, + poolJoinKindToWeightedPoolJoinKind, +} from './join-exit-kinds'; +import { SingleAllPool } from '../common/SingleAllPool'; -export class WeightedPool extends JoinPool implements IBalancerJoinPool { +const SUPPORTED_FEATURES = new Set([ + BalancerFeature.AddRemoveAll, + BalancerFeature.AddRemoveSingle, + BalancerFeature.AddSlippage, + BalancerFeature.RemoveSlippage, +]); + +export class WeightedPool extends SingleAllPool implements IBalancerSinglePool, IBalancerAllPool { constructor( readonly chain: ChainEntity, readonly vaultConfig: VaultConfig, @@ -19,8 +31,24 @@ export class WeightedPool extends JoinPool implements IBalancerJoinPool { this.getNormalizedWeights = this.cacheMethod(this.getNormalizedWeights); } - get joinSupportsSlippage() { - return true; + supportsFeature(feature: BalancerFeature): boolean { + return SUPPORTED_FEATURES.has(feature); + } + + protected getJoinKindValue(kind: PoolJoinKind): number { + const value = poolJoinKindToWeightedPoolJoinKind[kind]; + if (value === undefined) { + throw new Error(`WeightedPool does not support join kind ${PoolJoinKind[kind]}`); + } + return value; + } + + protected getExitKindValue(kind: PoolExitKind): number { + const value = poolExitKindToWeightedPoolExitKind[kind]; + if (value === undefined) { + throw new Error(`WeightedPool does not support join kind ${PoolExitKind[kind]}`); + } + return value; } async getSwapRatios(): Promise { diff --git a/src/features/data/apis/amm/balancer/weighted/join-exit-kinds.ts b/src/features/data/apis/amm/balancer/weighted/join-exit-kinds.ts new file mode 100644 index 000000000..ba870628a --- /dev/null +++ b/src/features/data/apis/amm/balancer/weighted/join-exit-kinds.ts @@ -0,0 +1,24 @@ +import { PoolExitKind, PoolJoinKind } from '../common/types'; +import type { OptionalRecord } from '../../../../utils/types-utils'; +import { WeightedPoolExitKind, WeightedPoolJoinKind } from './types'; + +export const poolJoinKindToWeightedPoolJoinKind: OptionalRecord< + PoolJoinKind, + WeightedPoolJoinKind +> = { + [PoolJoinKind.INIT]: WeightedPoolJoinKind.INIT, + [PoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT]: WeightedPoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT, + [PoolJoinKind.TOKEN_IN_FOR_EXACT_BPT_OUT]: WeightedPoolJoinKind.TOKEN_IN_FOR_EXACT_BPT_OUT, + [PoolJoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT]: + WeightedPoolJoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT, +}; + +export const poolExitKindToWeightedPoolExitKind: OptionalRecord< + PoolExitKind, + WeightedPoolExitKind +> = { + [PoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT]: + WeightedPoolExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT, + [PoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT]: WeightedPoolExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT, + [PoolExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT]: WeightedPoolExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT, +}; diff --git a/src/features/data/apis/amm/balancer/weighted/types.ts b/src/features/data/apis/amm/balancer/weighted/types.ts index e7482adda..6ccc537c1 100644 --- a/src/features/data/apis/amm/balancer/weighted/types.ts +++ b/src/features/data/apis/amm/balancer/weighted/types.ts @@ -3,14 +3,13 @@ export enum WeightedPoolJoinKind { EXACT_TOKENS_IN_FOR_BPT_OUT, TOKEN_IN_FOR_EXACT_BPT_OUT, ALL_TOKENS_IN_FOR_EXACT_BPT_OUT, - ADD_TOKEN, } export enum WeightedPoolExitKind { EXACT_BPT_IN_FOR_ONE_TOKEN_OUT = 0, EXACT_BPT_IN_FOR_TOKENS_OUT, BPT_IN_FOR_EXACT_TOKENS_OUT, - REMOVE_TOKEN, + MANAGEMENT_FEE_TOKENS_OUT, } export type NormalizedWeightsResult = string[]; diff --git a/src/features/data/apis/transact/helpers/tokens.ts b/src/features/data/apis/transact/helpers/tokens.ts index bbf0c72f4..46303f7a6 100644 --- a/src/features/data/apis/transact/helpers/tokens.ts +++ b/src/features/data/apis/transact/helpers/tokens.ts @@ -46,7 +46,7 @@ export function tokensToLp(tokens: TokenEntity[], wnative: TokenErc20): TokenErc * Ensures WNATIVE and NATIVE is in list, if either one of them are already * Used in zaps so user can pick either native or wrapped when either is part of an LP */ -export function includeNativeAndWrapped( +export function includeWrappedAndNative( tokens: TokenEntity[], wnative: TokenErc20, native: TokenNative diff --git a/src/features/data/apis/transact/strategies/RewardPoolToVaultStrategy.ts b/src/features/data/apis/transact/strategies/RewardPoolToVaultStrategy.ts index bf1a918d9..2029de6da 100644 --- a/src/features/data/apis/transact/strategies/RewardPoolToVaultStrategy.ts +++ b/src/features/data/apis/transact/strategies/RewardPoolToVaultStrategy.ts @@ -28,6 +28,7 @@ import { isZapQuoteStepWithdraw, isZapQuoteStepStake, type TokenAmount, + SelectionOrder, } from '../transact-types'; import { isStandardVaultType, @@ -196,7 +197,7 @@ export class RewardPoolToVaultStrategy implements IZapStrategy { chainId: this.rewardPool.chainId, vaultId: this.rewardPool.id, selectionId, - selectionOrder: 0, + selectionOrder: SelectionOrder.VaultToVault, inputs, wantedOutputs: [this.rewardPoolType.depositToken], mode: TransactMode.Deposit, @@ -216,7 +217,7 @@ export class RewardPoolToVaultStrategy implements IZapStrategy { chainId: this.vault.chainId, vaultId: this.vault.id, selectionId, - selectionOrder: 0, + selectionOrder: SelectionOrder.VaultToVault, inputs, wantedOutputs: [this.vaultType.depositToken], mode: TransactMode.Deposit, diff --git a/src/features/data/apis/transact/strategies/UniswapLikeStrategy.ts b/src/features/data/apis/transact/strategies/UniswapLikeStrategy.ts index 6c2cab0dd..f45604208 100644 --- a/src/features/data/apis/transact/strategies/UniswapLikeStrategy.ts +++ b/src/features/data/apis/transact/strategies/UniswapLikeStrategy.ts @@ -1,26 +1,25 @@ import type { ZapTransactHelpers } from './IStrategy'; -import type { - InputTokenAmount, - TokenAmount, - UniswapLikeDepositOption, - UniswapLikeDepositQuote, - UniswapLikeWithdrawOption, - UniswapLikeWithdrawQuote, - ZapFee, - ZapQuoteStep, - ZapQuoteStepBuild, - ZapQuoteStepSplit, - ZapQuoteStepSwap, - ZapQuoteStepSwapAggregator, - ZapQuoteStepSwapPool, -} from '../transact-types'; import { + type InputTokenAmount, isZapQuoteStepBuild, isZapQuoteStepSplit, isZapQuoteStepSwap, isZapQuoteStepSwapAggregator, isZapQuoteStepSwapPool, isZapQuoteStepWithdraw, + SelectionOrder, + type TokenAmount, + type UniswapLikeDepositOption, + type UniswapLikeDepositQuote, + type UniswapLikeWithdrawOption, + type UniswapLikeWithdrawQuote, + type ZapFee, + type ZapQuoteStep, + type ZapQuoteStepBuild, + type ZapQuoteStepSplit, + type ZapQuoteStepSwap, + type ZapQuoteStepSwapAggregator, + type ZapQuoteStepSwapPool, } from '../transact-types'; import { selectChainNativeToken, @@ -32,7 +31,7 @@ import type { TokenEntity, TokenErc20, TokenNative } from '../../../entities/tok import { isTokenEqual, isTokenErc20, isTokenNative } from '../../../entities/token'; import { allTokensAreDistinct, - includeNativeAndWrapped, + includeWrappedAndNative, nativeAndWrappedAreSame, pickTokens, tokensToLp, @@ -84,6 +83,7 @@ import { selectAmmById } from '../../../selectors/zap'; import { type AmmEntityUniswapLike, isUniswapLikeAmm } from '../../../entities/zap'; import { isStandardVaultType, type IStandardVaultType } from '../vaults/IVaultType'; import type { UniswapLikeStrategyConfig } from './strategy-configs'; +import { tokenInList } from '../../../../../helpers/tokens'; type ZapHelpers = { chain: ChainEntity; @@ -181,12 +181,11 @@ export abstract class UniswapLikeStrategy< async fetchDepositOptions(): Promise[]> { // what tokens can we can zap via pool with - const poolTokens = includeNativeAndWrapped(this.tokens, this.wnative, this.native).map( - token => ({ - token, - swap: 'pool' as const, - }) - ); + const tokensWithNativeWrapped = includeWrappedAndNative(this.tokens, this.wnative, this.native); + const poolTokens = tokensWithNativeWrapped.map(token => ({ + token, + swap: 'pool' as const, + })); // what tokens we can zap via swap aggregator with const supportedAggregatorTokens = await this.aggregatorTokenSupport(); @@ -206,7 +205,9 @@ export abstract class UniswapLikeStrategy< vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: swap === 'pool' ? 2 : 3, + selectionOrder: tokenInList(token, tokensWithNativeWrapped) + ? SelectionOrder.TokenOfPool + : SelectionOrder.Other, inputs, wantedOutputs: outputs, mode: TransactMode.Deposit, @@ -827,12 +828,11 @@ export abstract class UniswapLikeStrategy< async fetchWithdrawOptions(): Promise[]> { // what tokens can we directly zap with - const poolTokens = includeNativeAndWrapped(this.tokens, this.wnative, this.native).map( - token => ({ - token, - swap: 'pool' as const, - }) - ); + const tokensWithNativeWrapped = includeWrappedAndNative(this.tokens, this.wnative, this.native); + const poolTokens = tokensWithNativeWrapped.map(token => ({ + token, + swap: 'pool' as const, + })); // what tokens we can zap via swap aggregator with const supportedAggregatorTokens = await this.aggregatorTokenSupport(); @@ -849,7 +849,7 @@ export abstract class UniswapLikeStrategy< vaultId: this.vault.id, chainId: this.vault.chainId, selectionId: breakSelectionId, - selectionOrder: 2, + selectionOrder: SelectionOrder.AllTokensInPool, inputs, wantedOutputs: this.lpTokens, mode: TransactMode.Withdraw, @@ -868,7 +868,9 @@ export abstract class UniswapLikeStrategy< vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: 3, + selectionOrder: tokenInList(token, tokensWithNativeWrapped) + ? SelectionOrder.TokenOfPool + : SelectionOrder.Other, inputs, wantedOutputs: outputs, mode: TransactMode.Withdraw, diff --git a/src/features/data/apis/transact/strategies/balancer/BalancerJoinStrategy.ts b/src/features/data/apis/transact/strategies/balancer/BalancerStrategy.ts similarity index 50% rename from src/features/data/apis/transact/strategies/balancer/BalancerJoinStrategy.ts rename to src/features/data/apis/transact/strategies/balancer/BalancerStrategy.ts index bfe1ca31c..6c8936401 100644 --- a/src/features/data/apis/transact/strategies/balancer/BalancerJoinStrategy.ts +++ b/src/features/data/apis/transact/strategies/balancer/BalancerStrategy.ts @@ -8,20 +8,26 @@ import { } from '../../../../entities/token'; import type { Step } from '../../../../reducers/wallet/stepper'; import { - type BalancerPoolDepositOption, - type BalancerPoolDepositQuote, - type BalancerPoolWithdrawOption, - type BalancerPoolWithdrawQuote, + type BalancerDepositOption, + type BalancerDepositOptionAllAggregator, + type BalancerDepositOptionSingleAggregator, + type BalancerDepositQuote, + type BalancerWithdrawOption, + type BalancerWithdrawOptionAllAggregator, + type BalancerWithdrawOptionAllBreakOnly, + type BalancerWithdrawOptionSingleAggregator, + type BalancerWithdrawOptionSingleDirect, + type BalancerWithdrawQuote, type InputTokenAmount, isZapQuoteStepBuild, isZapQuoteStepSplit, isZapQuoteStepSwap, isZapQuoteStepSwapAggregator, isZapQuoteStepWithdraw, + SelectionOrder, type TokenAmount, type ZapQuoteStep, type ZapQuoteStepBuild, - type ZapQuoteStepSplit, type ZapQuoteStepSwap, type ZapQuoteStepSwapAggregator, } from '../../transact-types'; @@ -32,19 +38,17 @@ import { createQuoteId, createSelectionId, onlyOneInput, - onlyOneToken, onlyOneTokenAmount, } from '../../helpers/options'; import { selectChainNativeToken, selectChainWrappedNativeToken, - selectIsTokenLoaded, selectTokenByAddressOrUndefined, selectTokenPriceByTokenOracleId, } from '../../../../selectors/tokens'; import { selectChainById } from '../../../../selectors/chains'; import { TransactMode } from '../../../../reducers/wallet/transact-types'; -import { first, uniqBy } from 'lodash-es'; +import { first, orderBy, uniqBy } from 'lodash-es'; import { BIG_ZERO, bigNumberToStringDeep, @@ -53,10 +57,14 @@ import { toWeiFromTokenAmount, toWeiString, } from '../../../../../../helpers/big-number'; -import { calculatePriceImpact, highestFeeOrZero } from '../../helpers/quotes'; +import { + calculatePriceImpact, + highestFeeOrZero, + totalValueOfTokenAmounts, +} from '../../helpers/quotes'; import BigNumber from 'bignumber.js'; import type { BeefyState, BeefyThunk } from '../../../../../../redux-types'; -import type { QuoteRequest } from '../../swap/ISwapProvider'; +import type { QuoteRequest, QuoteResponse } from '../../swap/ISwapProvider'; import type { OrderInput, OrderOutput, @@ -68,14 +76,14 @@ import { fetchZapAggregatorSwap } from '../../zap/swap'; import { selectTransactSlippage } from '../../../../selectors/transact'; import { Balances } from '../../helpers/Balances'; import { getTokenAddress, NO_RELAY } from '../../helpers/zap'; -import { slipBy, slipTokenAmountBy } from '../../helpers/amounts'; -import { allTokensAreDistinct, pickTokens } from '../../helpers/tokens'; +import { mergeTokenAmounts, slipBy, slipTokenAmountBy } from '../../helpers/amounts'; +import { allTokensAreDistinct, includeWrappedAndNative, pickTokens } from '../../helpers/tokens'; import { walletActions } from '../../../../actions/wallet-actions'; import { isStandardVault, type VaultStandard } from '../../../../entities/vault'; import { getVaultWithdrawnFromState } from '../../helpers/vault'; import { isDefined } from '../../../../utils/array-utils'; import { isStandardVaultType, type IStandardVaultType } from '../../vaults/IVaultType'; -import type { BalancerJoinStrategyConfig } from '../strategy-configs'; +import type { BalancerStrategyConfig } from '../strategy-configs'; import { type AmmEntityBalancer, isBalancerAmm } from '../../../../entities/zap'; import { selectAmmById } from '../../../../selectors/zap'; import { createFactory } from '../../../../utils/factory-utils'; @@ -83,35 +91,40 @@ import type { PoolConfig, VaultConfig } from '../../../amm/balancer/vault/types' import { GyroPool } from '../../../amm/balancer/gyro/GyroPool'; import { WeightedPool } from '../../../amm/balancer/weighted/WeightedPool'; import { MetaStablePool } from '../../../amm/balancer/meta-stable/MetaStablePool'; +import { BalancerFeature } from '../../../amm/balancer/types'; +import { ComposableStablePool } from '../../../amm/balancer/composable-stable/ComposableStablePool'; +import { isFulfilledResult } from '../../../../../../helpers/promises'; +import { tokenInList } from '../../../../../../helpers/tokens'; +import { isBalancerAllPool, isBalancerSinglePool } from '../../../amm/balancer/common/type-guards'; type ZapHelpers = { slippage: number; state: BeefyState; }; -const strategyId = 'balancer-join' as const; +const strategyId = 'balancer' as const; type StrategyId = typeof strategyId; /** * Balancer: joinPool() to deposit / exitPool() to withdraw liquidity */ -class BalancerJoinStrategyImpl implements IZapStrategy { +class BalancerStrategyImpl implements IZapStrategy { public static readonly id = strategyId; public readonly id = strategyId; protected readonly native: TokenNative; protected readonly wnative: TokenErc20; protected readonly poolTokens: TokenEntity[]; + protected readonly poolTokensincludingWrappedNative: TokenEntity[]; protected readonly chain: ChainEntity; protected readonly depositToken: TokenEntity; protected readonly vault: VaultStandard; protected readonly vaultType: IStandardVaultType; protected readonly amm: AmmEntityBalancer; + protected readonly singleTokenOptions: TokenEntity[]; + protected readonly allTokenOptions: TokenEntity[]; - constructor( - protected options: BalancerJoinStrategyConfig, - protected helpers: ZapTransactHelpers - ) { + constructor(protected options: BalancerStrategyConfig, protected helpers: ZapTransactHelpers) { const { vault, vaultType, getState } = this.helpers; if (!isStandardVault(vault)) { @@ -122,12 +135,6 @@ class BalancerJoinStrategyImpl implements IZapStrategy { } const state = getState(); - for (let i = 0; i < vault.assetIds.length; ++i) { - if (!selectIsTokenLoaded(state, vault.chainId, vault.assetIds[i])) { - throw new Error(`Vault ${vault.id}: Asset ${vault.assetIds[i]} not loaded`); - } - } - const amm = selectAmmById(state, this.options.ammId); if (!amm) { throw new Error(`Vault ${vault.id}: AMM ${this.options.ammId} not found`); @@ -144,20 +151,74 @@ class BalancerJoinStrategyImpl implements IZapStrategy { this.depositToken = vaultType.depositToken; this.chain = selectChainById(state, vault.chainId); this.poolTokens = this.selectPoolTokens(state, this.chain.id, this.options.tokens); + this.poolTokensincludingWrappedNative = includeWrappedAndNative( + this.poolTokens, + this.wnative, + this.native + ); + this.singleTokenOptions = this.selectSingleTokenOptions(state); + this.allTokenOptions = this.selectAllTokenOptions(state); + if (this.singleTokenOptions.length === 0 && this.allTokenOptions.length === 0) { + throw new Error('No token options available'); + } + this.validatePoolType(); + } + + protected selectSingleTokenOptions(state: BeefyState): TokenEntity[] { + const pool = this.getPool(); + if (!isBalancerSinglePool(pool)) { + return []; + } + + // Can join with any token we have a price for + return this.poolTokens + .map(token => { + const price = selectTokenPriceByTokenOracleId(state, token.oracleId); + if (!price || price.lte(BIG_ZERO)) { + return undefined; + } + + return token; + }) + .filter(isDefined); + } + + protected selectAllTokenOptions(state: BeefyState): TokenEntity[] { + const pool = this.getPool(); + if (!isBalancerAllPool(pool)) { + return []; + } + + // Every token must have a price + if ( + !this.poolTokens.every(token => { + const price = selectTokenPriceByTokenOracleId(state, token.oracleId); + return price && price.gt(BIG_ZERO); + }) + ) { + return []; + } + + return this.poolTokens; + } + protected validatePoolType() { + // Check pool type specific requirements switch (this.options.poolType) { case 'gyroe': case 'gyro': { - this.checkPoolTokensCount(2); - this.checkPoolTokensHavePrice(state); + if (this.poolTokens.length !== 2) { + throw new Error(`${this.options.poolType}: There must be exactly 2 pool tokens`); + } break; } case 'weighted': - case 'meta-stable': { - this.checkPoolTokensHavePrice(state); + case 'meta-stable': + case 'composable-stable': { break; } default: { + // @ts-expect-error - if all cases are handled throw new Error(`Unsupported balancer pool type ${this.options.poolType}`); } } @@ -178,58 +239,114 @@ class BalancerJoinStrategyImpl implements IZapStrategy { return tokens; } - protected checkPoolTokensCount(count: number) { - if (this.poolTokens.length !== count) { - throw new Error(`There must be exactly ${count} pool tokens`); + protected async buildDepositOptionsForAll(): Promise { + if (!this.allTokenOptions.length) { + return []; } - } - protected checkPoolTokensHavePrice(state: BeefyState) { - if ( - this.poolTokens.some(token => { - const price = selectTokenPriceByTokenOracleId(state, token.oracleId); - return !price || price.lte(BIG_ZERO); - }) - ) { - throw new Error('All pool tokens must have a price'); - } - } + const via = 'aggregator' as const; + const type = 'all' as const; + const outputs = [this.vaultType.depositToken]; - public async fetchDepositOptions(): Promise { - // what tokens can we can zap via pool with - // const poolTokens = includeNativeAndWrapped(this.poolTokens.map(t => t.token), this.wnative, this.native).map( - // token => ({ - // token, - // via: 'pool' as const, - // }) - // ); - - // what tokens we can zap via swap aggregator with - const supportedAggregatorTokens = await this.aggregatorTokenSupport(); + const supportedAggregatorTokens = await this.aggregatorTokensCanSwapToAllOf( + this.allTokenOptions + ); const aggregatorTokens = supportedAggregatorTokens .filter(token => !isTokenEqual(token, this.vaultType.depositToken)) - .map(token => ({ token, via: 'aggregator' as const })); + .map(token => ({ token })); + + return aggregatorTokens.map(({ token }) => { + const inputs = [token]; + const selectionId = createSelectionId(this.vault.chainId, inputs); + + return { + id: createOptionId(this.id, this.vault.id, selectionId, `${type}-${via}`), + vaultId: this.vault.id, + chainId: this.vault.chainId, + selectionId, + selectionOrder: tokenInList(token, this.poolTokensincludingWrappedNative) + ? SelectionOrder.TokenOfPool + : SelectionOrder.Other, + inputs, + wantedOutputs: outputs, + mode: TransactMode.Deposit, + strategyId, + type, + via, + viaTokens: this.allTokenOptions, + } as const satisfies BalancerDepositOption; + }); + } + + protected async buildDepositOptionsForSingle(): Promise { + if (!this.singleTokenOptions.length) { + return []; + } - const zapTokens = aggregatorTokens; //[...poolTokens, ...aggregatorTokens]; + const type = 'single' as const; const outputs = [this.vaultType.depositToken]; - return zapTokens.map(({ token, via }) => { + const baseOptions = this.singleTokenOptions.map(viaToken => { + const inputs = [viaToken]; + const selectionId = createSelectionId(this.vault.chainId, inputs); + + return { + id: createOptionId(this.id, this.vault.id, selectionId, `${type}-direct`), + vaultId: this.vault.id, + chainId: this.vault.chainId, + selectionId, + selectionOrder: SelectionOrder.TokenOfPool, + inputs, + wantedOutputs: outputs, + mode: TransactMode.Deposit, + strategyId, + type, + via: 'direct', + viaToken, + } satisfies BalancerDepositOption; + }); + + const { inputTokens, inputTokenToWanted } = await this.aggregatorTokensCanSwapToTokens( + this.singleTokenOptions + ); + + const aggregatorOptions = inputTokens.map(token => { const inputs = [token]; const selectionId = createSelectionId(this.vault.chainId, inputs); + const viaTokens = inputTokenToWanted[token.address]; + + if (viaTokens.length === 0) { + console.error({ vault: this.vault.id, token, viaTokens }); + throw new Error( + `No other tokens supported for ${token.symbol}; *** this should not happen ***` + ); + } return { - id: createOptionId(this.id, this.vault.id, selectionId, via), + id: createOptionId(this.id, this.vault.id, selectionId, `${type}-aggregator`), vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: 3, //via === 'pool' ? 2 : 3, + selectionOrder: tokenInList(token, this.poolTokensincludingWrappedNative) + ? SelectionOrder.TokenOfPool + : SelectionOrder.Other, inputs, wantedOutputs: outputs, mode: TransactMode.Deposit, strategyId, - via, - } as const satisfies BalancerPoolDepositOption; + type, + via: 'aggregator', + viaTokens, + } satisfies BalancerDepositOption; }); + + return [...baseOptions, ...aggregatorOptions]; + } + + public async fetchDepositOptions(): Promise { + const singleOptions = await this.buildDepositOptionsForSingle(); + const allOptions = await this.buildDepositOptionsForAll(); + return [...singleOptions, ...allOptions]; } protected getPool = createFactory(() => { @@ -254,7 +371,14 @@ class BalancerJoinStrategyImpl implements IZapStrategy { case 'meta-stable': { return new MetaStablePool(this.chain, vault, pool); } + case 'composable-stable': { + return new ComposableStablePool(this.chain, vault, { + ...pool, + bptIndex: this.options.bptIndex, + }); + } default: { + // @ts-expect-error - if all cases are handled throw new Error(`Unsupported balancer pool type ${this.options.poolType}`); } } @@ -262,19 +386,134 @@ class BalancerJoinStrategyImpl implements IZapStrategy { public async fetchDepositQuote( inputs: InputTokenAmount[], - option: BalancerPoolDepositOption - ): Promise { + option: BalancerDepositOption + ): Promise { const input = onlyOneInput(inputs); if (input.amount.lte(BIG_ZERO)) { - throw new Error('BalancerJoinStrategy: Quote called with 0 input amount'); + throw new Error('BalancerStrategy: Quote called with 0 input amount'); + } + + // Token allowances + const { zap, getState } = this.helpers; + const state = getState(); + const allowances = isTokenErc20(input.token) + ? [ + { + token: input.token, + amount: input.amount, + spenderAddress: zap.manager, + }, + ] + : []; + + // Swaps/Liquidity + const { swaps, liquidity } = await this.fetchDepositSwapsLiquidity(input, option); + + // Build quote steps + const steps: ZapQuoteStep[] = []; + + for (const swap of swaps) { + if (swap.quote) { + steps.push({ + type: 'swap', + fromToken: swap.quote.fromToken, + fromAmount: swap.quote.fromAmount, + toToken: swap.quote.toToken, + toAmount: swap.quote.toAmount, + via: 'aggregator', + providerId: swap.quote.providerId, + fee: swap.quote.fee, + quote: swap.quote, + }); + } } - if (option.via === 'aggregator') { - return this.fetchDepositQuoteAggregator(input, option); + steps.push({ + type: 'build', + inputs: liquidity.inputs.filter(({ amount }) => amount.gt(BIG_ZERO)), + outputToken: liquidity.output.token, + outputAmount: liquidity.output.amount, + }); + + steps.push({ + type: 'deposit', + inputs: [liquidity.output], + }); + + // Build quote outputs + const outputs: TokenAmount[] = [liquidity.output]; + const returned: TokenAmount[] = []; + + // Build quote + return { + id: createQuoteId(option.id), + strategyId, + priceImpact: calculatePriceImpact(inputs, outputs, returned, state), // includes the zap fee + option, + inputs, + outputs, + returned, + allowances, + steps, + fee: highestFeeOrZero(steps), + }; + } + + protected async fetchDepositSwapsLiquidity(input: TokenAmount, option: BalancerDepositOption) { + // Calculate swap inputs and outputs + let swapSets: Array< + { + input: TokenAmount; + output: TokenAmount; + quote?: QuoteResponse; + }[] + >; + if (option.type === 'all' && option.via === 'aggregator') { + swapSets = await this.fetchDepositSwapQuotesAllAggregator(input, option); + } else if (option.type === 'single' && option.via === 'aggregator') { + swapSets = await this.fetchDepositSwapQuotesSingleAggregator(input, option); + } else if (option.type === 'single' && option.via === 'direct') { + swapSets = [[{ input, output: input }]]; } else { - throw new Error('Unknown zap deposit option via'); - // return this.fetchDepositQuotePool(input, option); + // @ts-expect-error if all cases are handled + throw new Error(`Unsupported deposit option type ${option.type} via ${option.via}`); + } + + if (swapSets.length === 0) { + throw new Error('No swap quotes found'); } + + // Calculate how much liquidity we get + const withLiquidity = await Promise.all( + swapSets.map(async swaps => { + const inputs = this.poolTokens.map( + token => + swaps.find(swap => isTokenEqual(swap.output.token, token))?.output ?? { + token, + amount: BIG_ZERO, + } + ); + const liquidity = await this.quoteAddLiquidity(inputs); + + return { + input, + swaps, + liquidity: { + inputs: inputs, + output: liquidity.liquidity, + usedInput: liquidity.usedInput, + unusedInput: liquidity.unusedInput, + }, + output: liquidity.liquidity, + }; + }) + ); + + // sort by most liquidity + withLiquidity.sort((a, b) => b.output.amount.comparedTo(a.output.amount)); + + // the one which gives the most liquidity + return withLiquidity[0]; } protected async getSwapAmounts( @@ -284,7 +523,7 @@ class BalancerJoinStrategyImpl implements IZapStrategy { const ratios = await pool.getSwapRatios(); console.debug('ratios', ratios.toString()); if (ratios.length !== this.poolTokens.length) { - throw new Error('BalancerJoinStrategy: Ratios length mismatch'); + throw new Error('BalancerStrategy: Ratios length mismatch'); } const inputAmountWei = toWeiFromTokenAmount(input); @@ -321,32 +560,24 @@ class BalancerJoinStrategyImpl implements IZapStrategy { }; } - protected async fetchDepositQuoteAggregator( - input: InputTokenAmount, - option: BalancerPoolDepositOption - ): Promise { - const { zap, swapAggregator, getState } = this.helpers; + protected async fetchDepositSwapQuotesAllAggregator( + input: TokenAmount, + _option: BalancerDepositOptionAllAggregator + ): Promise< + Array< + { + input: TokenAmount; + output: TokenAmount; + quote?: QuoteResponse; + }[] + > + > { + const { swapAggregator, getState } = this.helpers; const state = getState(); - // Token allowances - const allowances = isTokenErc20(input.token) - ? [ - { - token: input.token, - amount: input.amount, - spenderAddress: zap.manager, - }, - ] - : []; - // How much input to swap to each lp token const swapInAmounts = await this.getSwapAmounts(input); - console.debug( - 'fetchDepositQuoteAggregator::swapInAmounts', - bigNumberToStringDeep(swapInAmounts) - ); - // Swap quotes const quoteRequestsPerLpToken: (QuoteRequest | undefined)[] = swapInAmounts.map( ({ from, to }) => @@ -386,81 +617,76 @@ class BalancerJoinStrategyImpl implements IZapStrategy { return first(quotes); }); - // Build LP - const lpTokenAmounts = quotePerLpToken.map((quote, i) => { - if (quote) { - return { token: quote.toToken, amount: quote.toAmount }; - } - return swapInAmounts[i].from; - }); - - console.debug( - 'fetchDepositQuoteAggregator::lpTokenAmounts', - bigNumberToStringDeep(lpTokenAmounts) - ); - - const { liquidity, unusedInput } = await this.quoteAddLiquidity(lpTokenAmounts); - - // Build quote inputs - const inputs = [input]; - - // Build quote steps - const steps: ZapQuoteStep[] = []; - - quotePerLpToken.forEach(quote => { - if (quote) { - steps.push({ - type: 'swap', - fromToken: quote.fromToken, - fromAmount: quote.fromAmount, - toToken: quote.toToken, - toAmount: quote.toAmount, - via: 'aggregator', - providerId: quote.providerId, - fee: quote.fee, - quote, - }); - } - }); + // Output + return [ + quotePerLpToken.map((quote, i) => { + if (quote) { + return { + input: swapInAmounts[i].from, + output: { token: quote.toToken, amount: quote.toAmount }, + quote, + }; + } - steps.push({ - type: 'build', - inputs: lpTokenAmounts, - outputToken: liquidity.token, - outputAmount: liquidity.amount, - }); + return { + input: swapInAmounts[i].from, + output: swapInAmounts[i].from, + }; + }), + ]; + } - steps.push({ - type: 'deposit', - inputs: [liquidity], - }); + protected async fetchDepositSwapQuotesSingleAggregator( + input: TokenAmount, + option: BalancerDepositOptionSingleAggregator + ): Promise< + Array< + { + input: TokenAmount; + output: TokenAmount; + quote?: QuoteResponse; + }[] + > + > { + const { swapAggregator, getState } = this.helpers; + const state = getState(); - // Build quote outputs - const outputs: TokenAmount[] = [liquidity]; + // Fetch quotes from input token, to each possible deposit via token + const maybeQuotes = await Promise.allSettled( + option.viaTokens.map(async depositVia => { + const quotes = await swapAggregator.fetchQuotes( + { + vaultId: this.vault.id, + fromToken: input.token, + fromAmount: input.amount, + toToken: depositVia, + }, + state + ); + const bestQuote = first(quotes); + if (!bestQuote) { + throw new Error(`No quote for ${input.token.symbol} to ${depositVia.symbol}`); + } + return { via: depositVia, quote: bestQuote }; + }) + ); - // Build dust outputs - const returned: TokenAmount[] = unusedInput.filter(input => !input.amount.isZero()); + const quotes = maybeQuotes + .filter(isFulfilledResult) + .map(r => r.value) + .filter(isDefined); - if (returned.length > 0) { - steps.push({ - type: 'unused', - outputs: returned, - }); + if (!quotes.length) { + throw new Error(`No quotes for ${input.token.symbol} to any deposit via token`); } - // Build quote - return { - id: createQuoteId(option.id), - strategyId: this.id, - priceImpact: calculatePriceImpact(inputs, outputs, returned, state), // includes the zap fee - option, - inputs, - outputs, - returned, - allowances, - steps, - fee: highestFeeOrZero(steps), - }; + return quotes.map(({ via, quote }) => [ + { + input, + output: { token: via, amount: quote.toAmount }, + quote, + }, + ]); } protected async fetchZapSwap( @@ -501,11 +727,12 @@ class BalancerJoinStrategyImpl implements IZapStrategy { protected async fetchZapBuild( quoteStep: ZapQuoteStepBuild, minInputs: TokenAmount[], + option: BalancerDepositOption, zapHelpers: ZapHelpers ): Promise { const { liquidity, usedInput, unusedInput } = await this.quoteAddLiquidity(minInputs); const pool = this.getPool(); - const minLiquidity = pool.joinSupportsSlippage + const minLiquidity = pool.supportsFeature(BalancerFeature.AddSlippage) ? slipTokenAmountBy(liquidity, zapHelpers.slippage) : liquidity; @@ -526,7 +753,7 @@ class BalancerJoinStrategyImpl implements IZapStrategy { } public async fetchDepositStep( - quote: BalancerPoolDepositQuote, + quote: BalancerDepositQuote, t: TFunction> ): Promise { const zapAction: BeefyThunk = async (dispatch, getState, extraArgument) => { @@ -542,18 +769,18 @@ class BalancerJoinStrategyImpl implements IZapStrategy { const buildQuote = quote.steps.find(isZapQuoteStepBuild); if (!buildQuote) { - throw new Error('BalancerJoinStrategy: No build step in quote'); + throw new Error('BalancerStrategy: No build step in quote'); } // since there are two tokens, there must be at least 1 swap if (swapQuotes.length < 1) { - throw new Error('BalancerJoinStrategy: Not enough swaps'); + throw new Error('BalancerStrategy: Not enough swaps'); } // Swaps if (swapQuotes.length) { if (swapQuotes.length > this.poolTokens.length) { - throw new Error('BalancerJoinStrategy: Too many swaps'); + throw new Error('BalancerStrategy: Too many swaps'); } const insertBalance = allTokensAreDistinct( @@ -576,10 +803,11 @@ class BalancerJoinStrategyImpl implements IZapStrategy { // Build LP const buildZap = await this.fetchZapBuild( buildQuote, - buildQuote.inputs.map(({ token }) => ({ + this.poolTokens.map(token => ({ token, amount: minBalances.get(token), // we have to pass min expected in case swaps slipped })), + quote.option, zapHelpers ); console.debug('fetchDepositStep::buildZap', bigNumberToStringDeep(buildZap)); @@ -671,58 +899,131 @@ class BalancerJoinStrategyImpl implements IZapStrategy { }; } - async fetchWithdrawOptions(): Promise { - const inputs = [this.vaultType.depositToken]; - - // what tokens can we can zap via pool with - // const poolTokens = includeNativeAndWrapped(this.poolTokens.map(t => t.token), this.wnative, this.native).map( - // token => ({ - // token, - // via: 'pool' as const, - // }) - // ); - - // what tokens we can zap via swap aggregator with - const supportedAggregatorTokens = await this.aggregatorTokenSupport(); - const aggregatorTokens = supportedAggregatorTokens - .filter(token => !isTokenEqual(token, this.vaultType.depositToken)) - .map(token => ({ token, via: 'aggregator' as const })); + protected async buildWithdrawOptionsForAll(): Promise { + if (!this.allTokenOptions.length) { + return []; + } - const zapTokens = aggregatorTokens; //[...poolTokens, ...aggregatorTokens]; + const type = 'all' as const; + const inputs = [this.vaultType.depositToken]; const breakSelectionId = createSelectionId(this.vault.chainId, this.poolTokens); - const breakOption: BalancerPoolWithdrawOption = { + const breakOption: BalancerWithdrawOption = { id: createOptionId(this.id, this.vault.id, breakSelectionId), vaultId: this.vault.id, chainId: this.vault.chainId, selectionId: breakSelectionId, - selectionOrder: 2, + selectionOrder: SelectionOrder.AllTokensInPool, inputs, wantedOutputs: this.poolTokens, mode: TransactMode.Withdraw, strategyId, + type, via: 'break-only', + viaTokens: this.allTokenOptions, }; - return [breakOption].concat( - zapTokens.map(({ token, via }) => { - const outputs = [token]; - const selectionId = createSelectionId(this.vault.chainId, outputs); + const supportedAggregatorTokens = await this.aggregatorTokensCanSwapToAllOf( + this.allTokenOptions + ); + const aggregatorTokens = supportedAggregatorTokens + .filter(token => !isTokenEqual(token, this.vaultType.depositToken)) + .map(token => ({ token })); - return { - id: createOptionId(this.id, this.vault.id, selectionId, via), - vaultId: this.vault.id, - chainId: this.vault.chainId, - selectionId, - selectionOrder: 3, - inputs, - wantedOutputs: outputs, - mode: TransactMode.Withdraw, - strategyId, - via, - }; - }) + const aggregatorOptions = aggregatorTokens.map(({ token }) => { + const outputs = [token]; + const selectionId = createSelectionId(this.vault.chainId, outputs); + + return { + id: createOptionId(this.id, this.vault.id, selectionId, `${type}-aggregator`), + vaultId: this.vault.id, + chainId: this.vault.chainId, + selectionId, + selectionOrder: tokenInList(token, this.poolTokensincludingWrappedNative) + ? SelectionOrder.TokenOfPool + : SelectionOrder.Other, + inputs, + wantedOutputs: outputs, + mode: TransactMode.Withdraw, + strategyId, + type, + via: 'aggregator', + viaTokens: this.allTokenOptions, + } as const satisfies BalancerWithdrawOption; + }); + + return [breakOption, ...aggregatorOptions]; + } + + protected async buildWithdrawOptionsForSingle(): Promise { + if (!this.singleTokenOptions.length) { + return []; + } + + const type = 'single' as const; + const inputs = [this.vaultType.depositToken]; + + const baseOptions = this.singleTokenOptions.map(viaToken => { + const outputs = [viaToken]; + const selectionId = createSelectionId(this.vault.chainId, outputs); + + return { + id: createOptionId(this.id, this.vault.id, selectionId, `${type}-direct`), + vaultId: this.vault.id, + chainId: this.vault.chainId, + selectionId, + selectionOrder: SelectionOrder.TokenOfPool, + inputs, + wantedOutputs: outputs, + mode: TransactMode.Withdraw, + strategyId, + type, + via: 'direct', + viaToken, + } satisfies BalancerWithdrawOption; + }); + + const { inputTokens, inputTokenToWanted } = await this.aggregatorTokensCanSwapToTokens( + this.singleTokenOptions ); + + const aggregatorOptions = inputTokens.map(token => { + const outputs = [token]; + const selectionId = createSelectionId(this.vault.chainId, outputs); + const viaTokens = inputTokenToWanted[token.address]; + + if (viaTokens.length === 0) { + console.error({ vault: this.vault.id, token, viaTokens }); + throw new Error( + `No other tokens supported for ${token.symbol}; *** this should not happen ***` + ); + } + + return { + id: createOptionId(this.id, this.vault.id, selectionId, `${type}-aggregator`), + vaultId: this.vault.id, + chainId: this.vault.chainId, + selectionId, + selectionOrder: tokenInList(token, this.poolTokensincludingWrappedNative) + ? SelectionOrder.TokenOfPool + : SelectionOrder.Other, + inputs, + wantedOutputs: outputs, + mode: TransactMode.Withdraw, + strategyId, + type, + via: 'aggregator', + viaTokens, + } satisfies BalancerWithdrawOption; + }); + + return [...baseOptions, ...aggregatorOptions]; + } + + async fetchWithdrawOptions(): Promise { + const singleOptions = await this.buildWithdrawOptionsForSingle(); + const allOptions = await this.buildWithdrawOptionsForAll(); + return [...singleOptions, ...allOptions]; } protected async quoteRemoveLiquidity( @@ -738,80 +1039,156 @@ class BalancerJoinStrategyImpl implements IZapStrategy { }; } - protected async fetchWithdrawQuoteAggregator( - option: BalancerPoolWithdrawOption, - baseQuote: Pick - ): Promise> { - const { wantedOutputs } = option; - const { swapAggregator, getState } = this.helpers; - const state = getState(); - const wantedOutput = onlyOneToken(wantedOutputs); - const needsSwap = baseQuote.outputs.map( - tokenAmount => !isTokenEqual(wantedOutput, tokenAmount.token) - ); - const additionalSteps: BalancerPoolWithdrawQuote['steps'] = []; - - const swapQuotes = await Promise.all( - baseQuote.outputs.map(async (input, i) => { - if (needsSwap[i]) { - const quotes = await swapAggregator.fetchQuotes( - { - fromAmount: input.amount, - fromToken: input.token, - toToken: wantedOutput, - vaultId: option.vaultId, - }, - state - ); + protected async quoteRemoveLiquidityOneToken( + input: TokenAmount, + wantedToken: TokenEntity + ): Promise<{ liquidity: TokenAmount; outputs: TokenAmount[] }> { + const pool = this.getPool(); + if (!isBalancerSinglePool(pool)) { + throw new Error('BalancerStrategy: Pool does not support removing liquidity to one token'); + } - if (!quotes || !quotes.length) { - throw new Error(`No quotes found for ${input.token.symbol} -> ${wantedOutput.symbol}`); - } + const inputAmountWei = toWeiFromTokenAmount(input); + const result = await pool.quoteRemoveLiquidityOneToken(inputAmountWei, wantedToken.address); - return first(quotes); // already sorted by toAmount - } + return { + liquidity: fromWeiToTokenAmount(result.liquidity, this.depositToken), + outputs: result.outputs.map((amount, i) => fromWeiToTokenAmount(amount, this.poolTokens[i])), + }; + } + + protected async fetchWithdrawLiquidityAll( + input: TokenAmount, + _option: BalancerWithdrawOptionAllAggregator | BalancerWithdrawOptionAllBreakOnly + ) { + const result = await this.quoteRemoveLiquidity(input); + return [ + { + input, + liquidity: { + input, + outputs: result.outputs, + }, + }, + ]; + } - return undefined; + protected async fetchWithdrawLiquiditySingleAggregator( + input: TokenAmount, + option: BalancerWithdrawOptionSingleAggregator + ) { + return await Promise.all( + option.viaTokens.map(async token => { + const result = await this.quoteRemoveLiquidityOneToken(input, token); + return { + input, + liquidity: { + input, + outputs: result.outputs, + }, + }; }) ); + } - let outputTotal = new BigNumber(0); - baseQuote.outputs.forEach((input, i) => { - if (needsSwap[i]) { - const swapQuote = swapQuotes[i]; - if (!swapQuote) { - throw new Error('No swap quote found'); - } + protected async fetchWithdrawLiquiditySingleDirect( + input: TokenAmount, + option: BalancerWithdrawOptionSingleDirect + ) { + const result = await this.quoteRemoveLiquidityOneToken(input, option.viaToken); + return [ + { + input, + liquidity: { + input, + outputs: result.outputs, + }, + }, + ]; + } - outputTotal = outputTotal.plus(swapQuote.toAmount); + protected async fetchWithdrawLiquiditySwaps(input: TokenAmount, option: BalancerWithdrawOption) { + const { swapAggregator, getState } = this.helpers; + const state = getState(); + const wantedOutput = option.wantedOutputs[0]; + const slippage = selectTransactSlippage(state); + + // Withdraw liquidity + let breakSets: Array<{ + input: TokenAmount; + liquidity: { input: TokenAmount; outputs: TokenAmount[] }; + }>; + if (option.type === 'single' && option.via === 'aggregator') { + breakSets = await this.fetchWithdrawLiquiditySingleAggregator(input, option); + } else if (option.type === 'single' && option.via === 'direct') { + breakSets = await this.fetchWithdrawLiquiditySingleDirect(input, option); + } else if (option.type === 'all') { + breakSets = await this.fetchWithdrawLiquidityAll(input, option); + } else { + // @ts-expect-error if all cases are handled + throw new Error(`Unknown zap withdraw option type ${option.type} ${option.via}`); + } - additionalSteps.push({ - type: 'swap', - fromToken: input.token, - fromAmount: input.amount, - toToken: swapQuote.toToken, - toAmount: swapQuote.toAmount, - via: 'aggregator', - providerId: swapQuote.providerId, - fee: swapQuote.fee, - quote: swapQuote, - }); - } else { - outputTotal = outputTotal.plus(input.amount); - } - }); + if (option.type === 'all' && option.via === 'break-only') { + return { + ...breakSets[0], + swaps: [], + outputs: breakSets[0].liquidity.outputs, + }; + } - return { - ...baseQuote, - outputs: [{ token: wantedOutput, amount: outputTotal }], - steps: baseQuote.steps.concat(additionalSteps), - }; + // Swap quotes + const withSwaps = await Promise.all( + breakSets.map(async ({ input, liquidity }) => { + const quotes = await Promise.all( + liquidity.outputs.map(async output => { + if (output.amount.lte(BIG_ZERO) || isTokenEqual(output.token, wantedOutput)) { + return undefined; + } + + // we have to assume removing liquidity will slip 100% since we can't modify the call data later + const minOutputAmount = slipBy(output.amount, slippage, output.token.decimals); + const quotes = await swapAggregator.fetchQuotes( + { + vaultId: option.vaultId, + fromToken: output.token, + fromAmount: minOutputAmount, + toToken: wantedOutput, + }, + state + ); + const bestQuote = first(quotes); + if (!bestQuote) { + throw new Error(`No quote for ${output.token.symbol} to ${wantedOutput.symbol}`); + } + return bestQuote; + }) + ); + + const swaps = liquidity.outputs.map((output, i) => ({ + input: output, + quote: quotes[i], + output: quotes[i] ? { token: quotes[i]!.toToken, amount: quotes[i]!.toAmount } : output, + })); + + return { + input, + liquidity, + swaps, + outputs: mergeTokenAmounts(swaps.map(s => s.output)), + }; + }) + ); + + // Most output + const sorted = orderBy(withSwaps, s => totalValueOfTokenAmounts(s.outputs, state), 'desc'); + return sorted[0]; } public async fetchWithdrawQuote( inputs: InputTokenAmount[], - option: BalancerPoolWithdrawOption - ): Promise { + option: BalancerWithdrawOption + ): Promise { const input = onlyOneInput(inputs); if (input.amount.lte(BIG_ZERO)) { throw new Error('Quote called with 0 input amount'); @@ -823,6 +1200,7 @@ class BalancerJoinStrategyImpl implements IZapStrategy { const { withdrawnAmountAfterFeeWei, withdrawnToken, shareToken, sharesToWithdrawWei } = getVaultWithdrawnFromState(input, this.vault, state); const liquidityWithdrawn = fromWeiToTokenAmount(withdrawnAmountAfterFeeWei, withdrawnToken); + const returned: TokenAmount[] = []; // Common: Token Allowances const allowances = [ @@ -833,62 +1211,105 @@ class BalancerJoinStrategyImpl implements IZapStrategy { }, ]; - // Common: Break the LP - const removeLiquidityQuote = await this.quoteRemoveLiquidity(liquidityWithdrawn); - const baseQuote: Pick = { - steps: [ - { - type: 'withdraw', - outputs: [liquidityWithdrawn], - }, - { - type: 'split', - inputToken: liquidityWithdrawn.token, - inputAmount: liquidityWithdrawn.amount, - outputs: removeLiquidityQuote.outputs, - }, - ], - outputs: removeLiquidityQuote.outputs, - returned: [], - }; + const { liquidity, swaps, outputs } = await this.fetchWithdrawLiquiditySwaps( + liquidityWithdrawn, + option + ); - let quote: Pick; - switch (option.via) { - case 'aggregator': { - quote = await this.fetchWithdrawQuoteAggregator(option, baseQuote); - break; - } - case 'break-only': { - quote = baseQuote; - break; - } - default: { - throw new Error(`Unknown zap withdraw option via ${option.via}`); + // Build quote steps + const steps: ZapQuoteStep[] = [ + { + type: 'withdraw', + outputs: [liquidityWithdrawn], + }, + ]; + + steps.push({ + type: 'split', + inputToken: liquidityWithdrawn.token, + inputAmount: liquidityWithdrawn.amount, + outputs: liquidity.outputs.filter(output => output.amount.gt(BIG_ZERO)), + }); + + for (const swap of swaps) { + const { quote } = swap; + if (quote) { + steps.push({ + type: 'swap', + fromToken: quote.fromToken, + fromAmount: quote.fromAmount, + toToken: quote.toToken, + toAmount: quote.toAmount, + via: 'aggregator', + providerId: quote.providerId, + fee: quote.fee, + quote: quote, + }); + + // We quoted the swap as if the liquidity slipped, so there might be some to return + const withdrawnToken = liquidity.outputs.find(o => isTokenEqual(o.token, quote.fromToken)); + if (withdrawnToken) { + const unused = withdrawnToken.amount.minus(quote.fromAmount); + if (unused.gt(BIG_ZERO)) { + returned.push({ token: withdrawnToken.token, amount: unused }); + } + } } } - return { + if (returned.length > 0) { + steps.push({ + type: 'unused', + outputs: returned, + }); + } + + const baseQuote: Omit = { id: createQuoteId(option.id), strategyId: this.id, - priceImpact: calculatePriceImpact(inputs, quote.outputs, quote.returned, state), - option, + priceImpact: calculatePriceImpact(inputs, outputs, returned, state), inputs, allowances, - fee: highestFeeOrZero(quote.steps), - ...quote, + fee: highestFeeOrZero(steps), + steps, + outputs, + returned, }; + + if (option.type === 'all') { + return { + type: 'all', + option, + ...baseQuote, + }; + } else if (option.type === 'single') { + const viaToken = option.via === 'direct' ? option.viaToken : swaps[0].input.token; + if (!viaToken) { + throw new Error('BalancerStrategy: No via token found for single withdraw'); + } + return { + type: 'single', + viaToken, + option, + ...baseQuote, + }; + } else { + // @ts-expect-error if all cases are handled + throw new Error(`Unknown zap withdraw option type ${option.type}`); + } } - protected async fetchZapSplit( - quoteStep: ZapQuoteStepSplit, + protected async fetchZapSplitAll( inputs: TokenAmount[], zapHelpers: ZapHelpers ): Promise { const { slippage } = zapHelpers; const input = onlyOneTokenAmount(inputs); const { outputs } = await this.quoteRemoveLiquidity(input); - const minOutputs = outputs.map(output => slipTokenAmountBy(output, slippage)); const pool = this.getPool(); + const minOutputs = pool.supportsFeature(BalancerFeature.RemoveSlippage) + ? outputs.map(output => slipTokenAmountBy(output, slippage)) + : outputs; return { inputs, @@ -906,8 +1327,66 @@ class BalancerJoinStrategyImpl implements IZapStrategy { }; } + protected async fetchZapSplitOne( + inputs: TokenAmount[], + zapHelpers: ZapHelpers, + viaToken: TokenEntity + ): Promise { + const pool = this.getPool(); + if (!isBalancerSinglePool(pool)) { + throw new Error('BalancerStrategy: Pool does not support removing liquidity to one token'); + } + + const viaTokenIndex = this.poolTokens.findIndex(token => isTokenEqual(token, viaToken)); + if (viaTokenIndex < 0) { + throw new Error('BalancerStrategy: viaToken not found in pool tokens'); + } + + const { slippage } = zapHelpers; + const input = onlyOneTokenAmount(inputs); + const { outputs } = await this.quoteRemoveLiquidityOneToken(input, viaToken); + const minOutputs = pool.supportsFeature(BalancerFeature.RemoveSlippage) + ? outputs.map(output => slipTokenAmountBy(output, slippage)) + : outputs; + const minOutput = minOutputs[viaTokenIndex]; + if (!minOutput) { + throw new Error('BalancerStrategy: viaToken not found in outputs'); + } + + return { + inputs, + outputs, + minOutputs, + returned: [], + zaps: [ + await pool.getRemoveLiquidityOneTokenZap( + toWeiFromTokenAmount(input), + viaToken.address, + toWeiFromTokenAmount(minOutput), + this.helpers.zap.router, + true + ), + ], + }; + } + + protected async fetchZapSplit( + quote: BalancerWithdrawQuote, + inputs: TokenAmount[], + zapHelpers: ZapHelpers + ): Promise { + if (quote.type === 'single') { + return this.fetchZapSplitOne(inputs, zapHelpers, quote.viaToken); + } else if (quote.type === 'all') { + return this.fetchZapSplitAll(inputs, zapHelpers); + } else { + // @ts-expect-error if all cases are handled + throw new Error(`Unknown zap split quote type ${quote.type}`); + } + } + public async fetchWithdrawStep( - quote: BalancerPoolWithdrawQuote, + quote: BalancerWithdrawQuote, t: TFunction> ): Promise { const zapAction: BeefyThunk = async (dispatch, getState, extraArgument) => { @@ -945,7 +1424,7 @@ class BalancerJoinStrategyImpl implements IZapStrategy { const steps: ZapStep[] = [vaultWithdraw.zap]; // Step 2. Split lp - const splitZap = await this.fetchZapSplit(splitQuote, [withdrawOutput], zapHelpers); + const splitZap = await this.fetchZapSplit(quote, [withdrawOutput], zapHelpers); splitZap.zaps.forEach(step => steps.push(step)); // Step 3. Swaps @@ -1042,11 +1521,11 @@ class BalancerJoinStrategyImpl implements IZapStrategy { }; } - protected async aggregatorTokenSupport() { + protected async aggregatorTokensCanSwapToAllOf(allTokens: TokenEntity[]): Promise { const { swapAggregator, getState } = this.helpers; const state = getState(); const tokenSupport = await swapAggregator.fetchTokenSupport( - this.poolTokens, + allTokens, this.vault.id, this.vault.chainId, state, @@ -1054,14 +1533,44 @@ class BalancerJoinStrategyImpl implements IZapStrategy { ); return tokenSupport.any.filter(aggToken => { - return this.poolTokens.every( + return allTokens.every( (poolToken, i) => isTokenEqual(aggToken, poolToken) || tokenSupport.tokens[i].some(supportedToken => isTokenEqual(supportedToken, aggToken)) ); }); } + + protected async aggregatorTokensCanSwapToTokens(tokens: TokenEntity[]): Promise<{ + inputTokens: TokenEntity[]; + inputTokenToWanted: Record; + }> { + const { swapAggregator, getState } = this.helpers; + const state = getState(); + const tokenSupport = await swapAggregator.fetchTokenSupport( + tokens, + this.vault.id, + this.vault.chainId, + state, + this.options.swap + ); + + const inputTokens = new Map(); + const inputTokenToWanted = tokens.reduce((acc, wantedToken, i) => { + for (const sourceToken of tokenSupport.tokens[i]) { + if (isTokenEqual(sourceToken, wantedToken)) { + continue; + } + acc[sourceToken.address] ??= []; + acc[sourceToken.address].push(wantedToken); + inputTokens.set(sourceToken.address, sourceToken); + } + + return acc; + }, {} as Record); + + return { inputTokens: Array.from(inputTokens.values()), inputTokenToWanted }; + } } -export const BalancerJoinStrategy = - BalancerJoinStrategyImpl satisfies IZapStrategyStatic; +export const BalancerStrategy = BalancerStrategyImpl satisfies IZapStrategyStatic; diff --git a/src/features/data/apis/transact/strategies/balancer/BalancerSwapStrategy.ts b/src/features/data/apis/transact/strategies/balancer/BalancerSwapStrategy.ts deleted file mode 100644 index dfd54be83..000000000 --- a/src/features/data/apis/transact/strategies/balancer/BalancerSwapStrategy.ts +++ /dev/null @@ -1,1134 +0,0 @@ -import type { Namespace, TFunction } from 'react-i18next'; -import { - isTokenEqual, - isTokenErc20, - isTokenNative, - type TokenEntity, - type TokenErc20, - type TokenNative, -} from '../../../../entities/token'; -import type { Step } from '../../../../reducers/wallet/stepper'; -import { - type BalancerSwapDepositOption, - type BalancerSwapDepositQuote, - type BalancerSwapWithdrawOption, - type BalancerSwapWithdrawQuote, - type InputTokenAmount, - isZapQuoteStepBuild, - isZapQuoteStepSplit, - isZapQuoteStepSwap, - isZapQuoteStepSwapAggregator, - isZapQuoteStepWithdraw, - type TokenAmount, - type ZapQuoteStep, - type ZapQuoteStepBuild, - type ZapQuoteStepSplit, - type ZapQuoteStepSwap, - type ZapQuoteStepSwapAggregator, -} from '../../transact-types'; -import type { IZapStrategy, IZapStrategyStatic, ZapTransactHelpers } from '../IStrategy'; -import type { ChainEntity } from '../../../../entities/chain'; -import { - createOptionId, - createQuoteId, - createSelectionId, - onlyOneInput, - onlyOneToken, - onlyOneTokenAmount, -} from '../../helpers/options'; -import { - selectChainNativeToken, - selectChainWrappedNativeToken, - selectIsTokenLoaded, - selectTokenByAddressOrUndefined, - selectTokenPriceByTokenOracleId, -} from '../../../../selectors/tokens'; -import { selectChainById } from '../../../../selectors/chains'; -import { TransactMode } from '../../../../reducers/wallet/transact-types'; -import { first, uniqBy } from 'lodash-es'; -import { - BIG_ZERO, - bigNumberToStringDeep, - fromWei, - fromWeiToTokenAmount, - toWeiFromTokenAmount, - toWeiString, -} from '../../../../../../helpers/big-number'; -import { calculatePriceImpact, highestFeeOrZero } from '../../helpers/quotes'; -import type BigNumber from 'bignumber.js'; -import type { BeefyState, BeefyThunk } from '../../../../../../redux-types'; -import type { BalancerTokenOption } from './types'; -import type { QuoteResponse } from '../../swap/ISwapProvider'; -import type { - OrderInput, - OrderOutput, - UserlessZapRequest, - ZapStep, - ZapStepResponse, -} from '../../zap/types'; -import { fetchZapAggregatorSwap } from '../../zap/swap'; -import { selectTransactSlippage } from '../../../../selectors/transact'; -import { Balances } from '../../helpers/Balances'; -import { getTokenAddress, NO_RELAY } from '../../helpers/zap'; -import { slipBy, slipTokenAmountBy } from '../../helpers/amounts'; -import { allTokensAreDistinct, pickTokens } from '../../helpers/tokens'; -import { walletActions } from '../../../../actions/wallet-actions'; -import { isStandardVault, type VaultStandard } from '../../../../entities/vault'; -import { getVaultWithdrawnFromState } from '../../helpers/vault'; -import { isFulfilledResult } from '../../../../../../helpers/promises'; -import { isDefined } from '../../../../utils/array-utils'; -import { isStandardVaultType, type IStandardVaultType } from '../../vaults/IVaultType'; -import type { BalancerSwapStrategyConfig } from '../strategy-configs'; -import { ComposableStablePool } from '../../../amm/balancer/composable-stable/ComposableStablePool'; -import { type AmmEntityBalancer, isBalancerAmm } from '../../../../entities/zap'; -import { selectAmmById } from '../../../../selectors/zap'; -import { createFactory } from '../../../../utils/factory-utils'; -import type { PoolConfig, VaultConfig } from '../../../amm/balancer/vault/types'; - -type ZapHelpers = { - chain: ChainEntity; - slippage: number; - poolAddress: string; - state: BeefyState; -}; - -type DepositLiquidity = { - /** Liquidity input (coin for deposit, lp for withdraw) */ - input: TokenAmount; - /** Liquidity output (lp for deposit, coin for withdraw) */ - output: TokenAmount; - /** Which method we are using to deposit/withdraw liquidity */ - via: BalancerTokenOption; - /** Quote for swapping to/from coin if required */ - quote?: QuoteResponse; -}; - -type WithdrawLiquidity = DepositLiquidity & { - /** How much token we have after the split */ - split: TokenAmount; -}; - -const strategyId = 'balancer-swap' as const; -type StrategyId = typeof strategyId; - -/** - * Balancer: swap() to deposit/withdraw liquidity - */ -class BalancerSwapStrategyImpl implements IZapStrategy { - public static readonly id = strategyId; - public readonly id = strategyId; - - protected readonly native: TokenNative; - protected readonly wnative: TokenErc20; - protected readonly possibleTokens: BalancerTokenOption[]; - protected readonly chain: ChainEntity; - protected readonly depositToken: TokenEntity; - protected readonly poolTokens: TokenEntity[]; - protected readonly vault: VaultStandard; - protected readonly vaultType: IStandardVaultType; - protected readonly amm: AmmEntityBalancer; - - constructor( - protected options: BalancerSwapStrategyConfig, - protected helpers: ZapTransactHelpers - ) { - const { vault, vaultType, getState } = this.helpers; - - if (!isStandardVault(vault)) { - throw new Error('Vault is not a standard vault'); - } - if (!isStandardVaultType(vaultType)) { - throw new Error('Vault type is not standard'); - } - - const state = getState(); - for (let i = 0; i < vault.assetIds.length; ++i) { - if (!selectIsTokenLoaded(state, vault.chainId, vault.assetIds[i])) { - throw new Error(`Vault ${vault.id}: Asset ${vault.assetIds[i]} not loaded`); - } - } - - const amm = selectAmmById(state, this.options.ammId); - if (!amm) { - throw new Error(`Vault ${vault.id}: AMM ${this.options.ammId} not found`); - } - if (!isBalancerAmm(amm)) { - throw new Error(`Vault ${vault.id}: AMM ${this.options.ammId} is not balancer type`); - } - - this.amm = amm; - this.vault = vault; - this.vaultType = vaultType; - this.native = selectChainNativeToken(state, vault.chainId); - this.wnative = selectChainWrappedNativeToken(state, vault.chainId); - this.depositToken = vaultType.depositToken; - this.chain = selectChainById(state, vault.chainId); - this.poolTokens = this.selectPoolTokens(state, this.chain.id, this.options.tokens); - this.possibleTokens = this.selectAvailableTokens(state, this.poolTokens); - - if (this.options.poolType === 'composable-stable') { - if (!this.possibleTokens.length) { - throw new Error( - `Vault ${vault.id}: At least one token must be in address book and priced for ${this.options.poolType}` - ); - } - } else { - throw new Error(`Unsupported balancer pool type ${this.options.poolType}`); - } - } - - protected selectPoolTokens( - state: BeefyState, - chainId: ChainEntity['id'], - tokenAddresses: string[] - ): TokenEntity[] { - const tokens = tokenAddresses - .map(address => selectTokenByAddressOrUndefined(state, chainId, address)) - .filter(isDefined); - if (tokens.length !== tokenAddresses.length) { - // We need decimals for each token - throw new Error('Not all tokens are in state'); - } - return tokens; - } - - /** - * Tokens are available so long as they are in the address book and have a price - */ - protected selectAvailableTokens( - state: BeefyState, - poolTokens: TokenEntity[] - ): BalancerTokenOption[] { - return poolTokens - .map((token, i) => { - const price = selectTokenPriceByTokenOracleId(state, token.oracleId); - if (!price || price.lte(BIG_ZERO)) { - return undefined; - } - - return { - index: i, - token, - price, - }; - }) - .filter(isDefined) - .filter(t => !isTokenEqual(t.token, this.depositToken)); - } - - public async fetchDepositOptions(): Promise { - const outputs = [this.vaultType.depositToken]; - - const baseOptions: BalancerSwapDepositOption[] = this.possibleTokens.map(depositToken => { - const inputs = [depositToken.token]; - const selectionId = createSelectionId(this.vault.chainId, inputs); - - return { - id: createOptionId(this.id, this.vault.id, selectionId, 'direct'), - vaultId: this.vault.id, - chainId: this.vault.chainId, - selectionId, - selectionOrder: 2, - inputs, - wantedOutputs: outputs, - mode: TransactMode.Deposit, - strategyId, - via: 'direct', - viaToken: depositToken, - }; - }); - - const { any: allAggregatorTokens, map: tokenToDepositTokens } = - await this.aggregatorTokenSupport(); - - const aggregatorOptions: BalancerSwapDepositOption[] = allAggregatorTokens - .filter(token => tokenToDepositTokens[token.address].length > 0) - .map(token => { - const inputs = [token]; - const selectionId = createSelectionId(this.vault.chainId, inputs); - const possible = tokenToDepositTokens[token.address]; - - if (possible.length === 0) { - console.error({ vault: this.vault.id, token, possible }); - throw new Error(`No other tokens supported for ${token.symbol}`); - } - - return { - id: createOptionId(this.id, this.vault.id, selectionId, 'aggregator'), - vaultId: this.vault.id, - chainId: this.vault.chainId, - selectionId, - selectionOrder: 3, - inputs, - wantedOutputs: outputs, - mode: TransactMode.Deposit, - strategyId, - via: 'aggregator', - viaTokens: possible, - }; - }); - - return baseOptions.concat(aggregatorOptions); - } - - protected getPool = createFactory(() => { - const vault: VaultConfig = { - vaultAddress: this.amm.vaultAddress, - queryAddress: this.amm.queryAddress, - }; - const pool: PoolConfig = { - poolAddress: this.depositToken.address, - poolId: this.options.poolId, - tokens: this.poolTokens, - }; - - switch (this.options.poolType) { - case 'composable-stable': { - return new ComposableStablePool(this.chain, vault, pool); - } - default: { - throw new Error(`Unsupported balancer pool type ${this.options.poolType}`); - } - } - }); - - protected async quoteAddLiquidityOneToken(input: TokenAmount): Promise { - const pool = this.getPool(); - const liquidity = await pool.quoteAddLiquidityOneToken( - input.token.address, - toWeiFromTokenAmount(input) - ); - return fromWeiToTokenAmount(liquidity, this.depositToken); - } - - protected async getDepositLiquidityDirect( - input: InputTokenAmount, - depositVia: BalancerTokenOption - ): Promise { - if (!isTokenEqual(input.token, depositVia.token)) { - throw new Error( - `BalancerSwapStrategy: Direct deposit called with input token ${input.token.symbol} but expected ${depositVia.token.symbol}` - ); - } - - return { - input, - output: await this.quoteAddLiquidityOneToken(input), - via: depositVia, - }; - } - - protected async getDepositLiquidityAggregator( - state: BeefyState, - input: InputTokenAmount, - depositVias: BalancerTokenOption[] - ): Promise { - const { swapAggregator } = this.helpers; - - // Fetch quotes from input token, to each possible deposit via token - const maybeQuotes = await Promise.allSettled( - depositVias.map(async depositVia => { - const quotes = await swapAggregator.fetchQuotes( - { - vaultId: this.vault.id, - fromToken: input.token, - fromAmount: input.amount, - toToken: depositVia.token, - }, - state - ); - const bestQuote = first(quotes); - if (!bestQuote) { - throw new Error(`No quote for ${input.token.symbol} to ${depositVia.token.symbol}`); - } - return { via: depositVia, quote: bestQuote }; - }) - ); - const quotes = maybeQuotes - .filter(isFulfilledResult) - .map(r => r.value) - .filter(isDefined); - if (!quotes.length) { - throw new Error(`No quotes for ${input.token.symbol} to any deposit via token`); - } - - // For the best quote per deposit via token, calculate how much liquidity we get - const withLiquidity = await Promise.all( - quotes.map(async ({ via, quote }) => { - const input = { token: quote.toToken, amount: quote.toAmount }; - return { - via, - quote, - input, - output: await this.quoteAddLiquidityOneToken(input), - }; - }) - ); - - // sort by most liquidity - withLiquidity.sort((a, b) => b.output.amount.comparedTo(a.output.amount)); - - // Get the one which gives the most liquidity - return withLiquidity[0]; - } - - protected async getDepositLiquidity( - state: BeefyState, - input: InputTokenAmount, - option: BalancerSwapDepositOption - ): Promise { - if (option.via === 'direct') { - return this.getDepositLiquidityDirect(input, option.viaToken); - } - return this.getDepositLiquidityAggregator(state, input, option.viaTokens); - } - - public async fetchDepositQuote( - inputs: InputTokenAmount[], - option: BalancerSwapDepositOption - ): Promise { - const { zap, getState } = this.helpers; - const state = getState(); - const input = onlyOneInput(inputs); - if (input.amount.lte(BIG_ZERO)) { - throw new Error('BalancerSwapStrategy: Quote called with 0 input amount'); - } - - // Token allowances - const allowances = isTokenErc20(input.token) - ? [ - { - token: input.token, - amount: input.amount, - spenderAddress: zap.manager, - }, - ] - : []; - - // Fetch liquidity (and swap quote if aggregator) - const depositLiquidity = await this.getDepositLiquidity(state, input, option); - - // Build quote steps - const steps: ZapQuoteStep[] = []; - - if (depositLiquidity.quote) { - steps.push({ - type: 'swap', - fromToken: depositLiquidity.quote.fromToken, - fromAmount: depositLiquidity.quote.fromAmount, - toToken: depositLiquidity.quote.toToken, - toAmount: depositLiquidity.quote.toAmount, - via: 'aggregator', - providerId: depositLiquidity.quote.providerId, - fee: depositLiquidity.quote.fee, - quote: depositLiquidity.quote, - }); - } - - steps.push({ - type: 'build', - inputs: [depositLiquidity.input], - outputToken: depositLiquidity.output.token, - outputAmount: depositLiquidity.output.amount, - }); - - steps.push({ - type: 'deposit', - inputs: [ - { - token: depositLiquidity.output.token, - amount: depositLiquidity.output.amount, - }, - ], - }); - - // Build quote outputs - const outputs: TokenAmount[] = [depositLiquidity.output]; - const returned: TokenAmount[] = []; - - // Build quote - return { - id: createQuoteId(option.id), - strategyId, - priceImpact: calculatePriceImpact(inputs, outputs, returned, state), // includes the zap fee - option, - inputs, - outputs, - returned, - allowances, - steps, - fee: highestFeeOrZero(steps), - via: option.via, - viaToken: depositLiquidity.via, - }; - } - - protected async fetchZapSwap( - quoteStep: ZapQuoteStepSwap, - zapHelpers: ZapHelpers, - insertBalance: boolean - ): Promise { - if (isZapQuoteStepSwapAggregator(quoteStep)) { - return this.fetchZapSwapAggregator(quoteStep, zapHelpers, insertBalance); - } else { - throw new Error('Unknown zap quote swap step type'); - } - } - - protected async fetchZapSwapAggregator( - quoteStep: ZapQuoteStepSwapAggregator, - zapHelpers: ZapHelpers, - insertBalance: boolean - ): Promise { - const { swapAggregator, zap } = this.helpers; - const { slippage, state } = zapHelpers; - - return await fetchZapAggregatorSwap( - { - quote: quoteStep.quote, - inputs: [{ token: quoteStep.fromToken, amount: quoteStep.fromAmount }], - outputs: [{ token: quoteStep.toToken, amount: quoteStep.toAmount }], - maxSlippage: slippage, - zapRouter: zap.router, - providerId: quoteStep.providerId, - insertBalance, - }, - swapAggregator, - state - ); - } - - protected async fetchZapBuild( - quoteStep: ZapQuoteStepBuild, - depositVia: BalancerTokenOption, - minInputAmount: BigNumber, - zapHelpers: ZapHelpers, - insertBalance: boolean = false - ): Promise { - const { slippage } = zapHelpers; - const input = { token: depositVia.token, amount: minInputAmount }; - const liquidity = await this.quoteAddLiquidityOneToken(input); - const minLiquidity = slipTokenAmountBy(liquidity, slippage); - const pool = this.getPool(); - - return { - inputs: [input], - outputs: [liquidity], - minOutputs: [minLiquidity], - returned: [], - zaps: [ - await pool.getAddLiquidityOneTokenZap( - input.token.address, - toWeiFromTokenAmount(input), - toWeiFromTokenAmount(minLiquidity), - this.helpers.zap.router, - insertBalance - ), - ], - }; - } - - public async fetchDepositStep( - quote: BalancerSwapDepositQuote, - t: TFunction> - ): Promise { - const zapAction: BeefyThunk = async (dispatch, getState, extraArgument) => { - const state = getState(); - const chain = selectChainById(state, this.vault.chainId); - const slippage = selectTransactSlippage(state); - const zapHelpers: ZapHelpers = { - chain, - slippage, - state, - poolAddress: this.depositToken.address, - }; - const steps: ZapStep[] = []; - const minBalances = new Balances(quote.inputs); - const swapQuotes = quote.steps.filter(isZapQuoteStepSwap); - const buildQuote = quote.steps.find(isZapQuoteStepBuild); - - if (!buildQuote) { - throw new Error('BalancerSwapStrategy: No build step in quote'); - } - - // wrap and asset swap, 2 max - if (swapQuotes.length > 2) { - throw new Error('BalancerSwapStrategy: Too many swaps'); - } - - // Swaps - if (swapQuotes.length) { - if (swapQuotes.length > 1) { - throw new Error('BalancerSwapStrategy: Too many swaps in quote'); - } - - const swapQuote = swapQuotes[0]; - const swap = await this.fetchZapSwap(swapQuote, zapHelpers, true); - // add step to order - swap.zaps.forEach(zap => steps.push(zap)); - // track minimum balances for use in further steps - minBalances.subtractMany(swap.inputs); - minBalances.addMany(swap.minOutputs); - } - - // Build LP - const buildZap = await this.fetchZapBuild( - buildQuote, - quote.viaToken, - minBalances.get(quote.viaToken.token), - zapHelpers, - true - ); - console.debug('fetchDepositStep::buildZap', bigNumberToStringDeep(buildZap)); - buildZap.zaps.forEach(step => steps.push(step)); - minBalances.subtractMany(buildZap.inputs); - minBalances.addMany(buildZap.minOutputs); - - // Deposit in vault - const vaultDeposit = await this.vaultType.fetchZapDeposit({ - inputs: [ - { - token: buildQuote.outputToken, - amount: minBalances.get(buildQuote.outputToken), // min expected in case add liquidity slipped - max: true, // but we call depositAll - }, - ], - }); - console.debug('fetchDepositStep::vaultDeposit', vaultDeposit); - steps.push(vaultDeposit.zap); - - // Build order - const inputs: OrderInput[] = quote.inputs.map(input => ({ - token: getTokenAddress(input.token), - amount: toWeiString(input.amount, input.token.decimals), - })); - - const requiredOutputs: OrderOutput[] = vaultDeposit.outputs.map(output => ({ - token: getTokenAddress(output.token), - minOutputAmount: toWeiString( - slipBy(output.amount, slippage, output.token.decimals), - output.token.decimals - ), - })); - - // We need to list all inputs, and mid-route outputs, as outputs so dust gets returned - const dustOutputs: OrderOutput[] = pickTokens( - quote.outputs, - quote.inputs, - quote.returned - ).map(token => ({ - token: getTokenAddress(token), - minOutputAmount: '0', - })); - - swapQuotes.forEach(quoteStep => { - dustOutputs.push({ - token: getTokenAddress(quoteStep.fromToken), - minOutputAmount: '0', - }); - dustOutputs.push({ - token: getTokenAddress(quoteStep.toToken), - minOutputAmount: '0', - }); - }); - dustOutputs.push({ - token: getTokenAddress(buildQuote.outputToken), - minOutputAmount: '0', - }); - - // @dev uniqBy: first occurrence of each element is kept. - const outputs = uniqBy(requiredOutputs.concat(dustOutputs), output => output.token); - - // Perform TX - const zapRequest: UserlessZapRequest = { - order: { - inputs, - outputs, - relay: NO_RELAY, - }, - steps, - }; - - const expectedTokens = vaultDeposit.outputs.map(output => output.token); - const walletAction = walletActions.zapExecuteOrder( - quote.option.vaultId, - zapRequest, - expectedTokens - ); - - return walletAction(dispatch, getState, extraArgument); - }; - - return { - step: 'zap-in', - message: t('Vault-TxnConfirm', { type: t('Deposit-noun') }), - action: zapAction, - pending: false, - extraInfo: { zap: true, vaultId: quote.option.vaultId }, - }; - } - - async fetchWithdrawOptions(): Promise { - const inputs = [this.vaultType.depositToken]; - - const baseOptions: BalancerSwapWithdrawOption[] = this.possibleTokens.map(depositToken => { - const outputs = [depositToken.token]; - const selectionId = createSelectionId(this.vault.chainId, outputs); - - return { - id: createOptionId(this.id, this.vault.id, selectionId, 'direct'), - vaultId: this.vault.id, - chainId: this.vault.chainId, - selectionId, - selectionOrder: 2, - inputs, - wantedOutputs: outputs, - mode: TransactMode.Withdraw, - strategyId, - via: 'direct', - viaToken: depositToken, - }; - }); - - const { any: allAggregatorTokens, map: tokenToDepositTokens } = - await this.aggregatorTokenSupport(); - - const aggregatorOptions: BalancerSwapWithdrawOption[] = allAggregatorTokens - .filter(token => tokenToDepositTokens[token.address].length > 0) - .map(token => { - const outputs = [token]; - const selectionId = createSelectionId(this.vault.chainId, outputs); - const possible = tokenToDepositTokens[token.address]; - - if (possible.length === 0) { - console.error({ vault: this.vault.id, token, possible }); - throw new Error(`No other tokens supported for ${token.symbol}`); - } - - return { - id: createOptionId(this.id, this.vault.id, selectionId, 'aggregator'), - vaultId: this.vault.id, - chainId: this.vault.chainId, - selectionId, - selectionOrder: 3, - inputs, - wantedOutputs: outputs, - mode: TransactMode.Withdraw, - strategyId, - via: 'aggregator', - viaTokens: possible, - }; - }); - - return baseOptions.concat(aggregatorOptions); - } - - protected async quoteRemoveLiquidityOneToken( - input: TokenAmount, - wanted: TokenEntity - ): Promise { - const pool = this.getPool(); - const amountOut = await pool.quoteRemoveLiquidityOneToken( - toWeiFromTokenAmount(input), - wanted.address - ); - return fromWeiToTokenAmount(amountOut, wanted); - } - - protected async getWithdrawLiquidityDirect( - input: TokenAmount, - wanted: TokenEntity, - withdrawVia: BalancerTokenOption - ): Promise { - if (!isTokenEqual(wanted, withdrawVia.token)) { - throw new Error( - `Balancer strategy: Direct withdraw called with wanted token ${input.token.symbol} but expected ${withdrawVia.token.symbol}` - ); - } - - const split = await this.quoteRemoveLiquidityOneToken(input, wanted); - - // no further steps so output is same as split - return { - input, - split, - output: split, - via: withdrawVia, - }; - } - - protected async getWithdrawLiquidityAggregator( - state: BeefyState, - input: TokenAmount, - wanted: TokenEntity, - withdrawVias: BalancerTokenOption[] - ): Promise { - const { swapAggregator } = this.helpers; - const slippage = selectTransactSlippage(state); - - // Fetch withdraw liquidity quotes for each possible withdraw via token - const quotes = await Promise.all( - withdrawVias.map(async withdrawVia => { - const split = await this.quoteRemoveLiquidityOneToken(input, withdrawVia.token); - return { via: withdrawVia, split }; - }) - ); - - // Fetch swap quote between withdrawn token and wanted token - const withSwaps = await Promise.all( - quotes.map(async ({ via, split }) => { - const quotes = await swapAggregator.fetchQuotes( - { - vaultId: this.vault.id, - fromToken: split.token, - fromAmount: slipBy(split.amount, slippage, split.token.decimals), // we have to assume it will slip 100% since we can't modify the call data later - toToken: wanted, - }, - state - ); - const quote = first(quotes); - - return { - via, - quote, - input, - split, - output: { token: wanted, amount: quote ? quote.toAmount : BIG_ZERO }, - }; - }) - ); - - // sort by most output - withSwaps.sort((a, b) => b.output.amount.comparedTo(a.output.amount)); - - // Get the one which gives the most output - return withSwaps[0]; - } - - protected async getWithdrawLiquidity( - state: BeefyState, - input: TokenAmount, - wanted: TokenEntity, - option: BalancerSwapWithdrawOption - ): Promise { - if (option.via === 'direct') { - return this.getWithdrawLiquidityDirect(input, wanted, option.viaToken); - } - return this.getWithdrawLiquidityAggregator(state, input, wanted, option.viaTokens); - } - - public async fetchWithdrawQuote( - inputs: InputTokenAmount[], - option: BalancerSwapWithdrawOption - ): Promise { - const input = onlyOneInput(inputs); - if (input.amount.lte(BIG_ZERO)) { - throw new Error('Quote called with 0 input amount'); - } - - if (option.wantedOutputs.length !== 1) { - throw new Error('Can only swap to 1 output token'); - } - - const { zap, getState } = this.helpers; - - // Common: Withdraw from vault - const state = getState(); - const { withdrawnAmountAfterFeeWei, withdrawnToken, shareToken, sharesToWithdrawWei } = - getVaultWithdrawnFromState(input, this.vault, state); - const withdrawnAmountAfterFee = fromWei(withdrawnAmountAfterFeeWei, withdrawnToken.decimals); - const liquidityWithdrawn = { amount: withdrawnAmountAfterFee, token: withdrawnToken }; - const wantedToken = onlyOneToken(option.wantedOutputs); - const returned: TokenAmount[] = []; - - // Common: Token Allowances - const allowances = [ - { - token: shareToken, - amount: fromWei(sharesToWithdrawWei, shareToken.decimals), - spenderAddress: zap.manager, - }, - ]; - - // Fetch remove liquidity (and swap quote if aggregator) - const withdrawnLiquidity = await this.getWithdrawLiquidity( - state, - liquidityWithdrawn, - wantedToken, - option - ); - - // Build quote steps - const steps: ZapQuoteStep[] = [ - { - type: 'withdraw', - outputs: [ - { - token: this.vaultType.depositToken, - amount: withdrawnAmountAfterFee, - }, - ], - }, - ]; - - steps.push({ - type: 'split', - inputToken: withdrawnLiquidity.input.token, - inputAmount: withdrawnLiquidity.input.amount, - outputs: [withdrawnLiquidity.split], - }); - - if (withdrawnLiquidity.quote) { - steps.push({ - type: 'swap', - fromToken: withdrawnLiquidity.quote.fromToken, - fromAmount: withdrawnLiquidity.quote.fromAmount, - toToken: withdrawnLiquidity.quote.toToken, - toAmount: withdrawnLiquidity.quote.toAmount, - via: 'aggregator', - providerId: withdrawnLiquidity.quote.providerId, - fee: withdrawnLiquidity.quote.fee, - quote: withdrawnLiquidity.quote, - }); - - const unused = withdrawnLiquidity.split.amount.minus(withdrawnLiquidity.quote.fromAmount); - if (unused.gt(BIG_ZERO)) { - returned.push({ token: withdrawnLiquidity.split.token, amount: unused }); - } - } - - if (returned.length > 0) { - steps.push({ - type: 'unused', - outputs: returned, - }); - } - - const outputs: TokenAmount[] = [withdrawnLiquidity.output]; - - return { - id: createQuoteId(option.id), - strategyId, - priceImpact: calculatePriceImpact(inputs, outputs, returned, state), - option, - inputs, - outputs, - returned, - allowances, - steps, - via: option.via, - viaToken: withdrawnLiquidity.via, - fee: highestFeeOrZero(steps), - }; - } - - protected async fetchZapSplit( - quoteStep: ZapQuoteStepSplit, - inputs: TokenAmount[], - via: BalancerTokenOption, - zapHelpers: ZapHelpers, - insertBalance: boolean = false - ): Promise { - const { slippage } = zapHelpers; - const input = onlyOneTokenAmount(inputs); - const output = await this.quoteRemoveLiquidityOneToken(input, via.token); - const minOutput = slipTokenAmountBy(output, slippage); - const pool = this.getPool(); - - return { - inputs, - outputs: [output], - minOutputs: [minOutput], - returned: [], - zaps: [ - await pool.getRemoveLiquidityOneTokenZap( - toWeiFromTokenAmount(input), - output.token.address, - toWeiFromTokenAmount(minOutput), - this.helpers.zap.router, - insertBalance - ), - ], - }; - } - - public async fetchWithdrawStep( - quote: BalancerSwapWithdrawQuote, - t: TFunction> - ): Promise { - const zapAction: BeefyThunk = async (dispatch, getState, extraArgument) => { - const state = getState(); - const chain = selectChainById(state, this.vault.chainId); - const slippage = selectTransactSlippage(state); - const zapHelpers: ZapHelpers = { - chain, - slippage, - state, - poolAddress: this.depositToken.address, - }; - const withdrawQuote = quote.steps.find(isZapQuoteStepWithdraw); - const swapQuotes = quote.steps.filter(isZapQuoteStepSwap); - const splitQuote = quote.steps.find(isZapQuoteStepSplit); - - if (!withdrawQuote || !splitQuote) { - throw new Error('Withdraw quote missing withdraw or split step'); - } - - // Step 1. Withdraw from vault - const vaultWithdraw = await this.vaultType.fetchZapWithdraw({ - inputs: quote.inputs, - }); - if (vaultWithdraw.outputs.length !== 1) { - throw new Error('Withdraw output count mismatch'); - } - - const withdrawOutput = onlyOneTokenAmount(vaultWithdraw.outputs); - if (!isTokenEqual(withdrawOutput.token, splitQuote.inputToken)) { - throw new Error('Withdraw output token mismatch'); - } - - if (withdrawOutput.amount.lt(withdrawQuote.toAmount)) { - throw new Error('Withdraw output amount mismatch'); - } - - const steps: ZapStep[] = [vaultWithdraw.zap]; - - // Step 2. Split lp - const splitZap = await this.fetchZapSplit( - splitQuote, - [withdrawOutput], - quote.viaToken, - zapHelpers, - true - ); - splitZap.zaps.forEach(step => steps.push(step)); - - // Step 3. Swaps - // 0 swaps is valid when we break only - if (swapQuotes.length > 0) { - if (swapQuotes.length > splitZap.minOutputs.length) { - throw new Error('More swap quotes than expected outputs'); - } - - const insertBalance = allTokensAreDistinct( - swapQuotes.map(quoteStep => quoteStep.fromToken) - ); - // On withdraw zap the last swap can use 100% of balance even if token was used in previous swaps (since there are no further steps) - const lastSwapIndex = swapQuotes.length - 1; - - const swapZaps = await Promise.all( - swapQuotes.map((quoteStep, i) => { - const input = splitZap.minOutputs.find(o => isTokenEqual(o.token, quoteStep.fromToken)); - if (!input) { - throw new Error('Swap input not found in split outputs'); - } - return this.fetchZapSwap(quoteStep, zapHelpers, insertBalance || lastSwapIndex === i); - }) - ); - swapZaps.forEach(swap => swap.zaps.forEach(step => steps.push(step))); - } - - // Build order - const inputs: OrderInput[] = vaultWithdraw.inputs.map(input => ({ - token: getTokenAddress(input.token), - amount: toWeiString(input.amount, input.token.decimals), - })); - - const requiredOutputs: OrderOutput[] = quote.outputs.map(output => ({ - token: getTokenAddress(output.token), - minOutputAmount: toWeiString( - slipBy(output.amount, slippage, output.token.decimals), - output.token.decimals - ), - })); - - // We need to list all inputs, and mid-route outputs, as outputs so dust gets returned - const dustOutputs: OrderOutput[] = pickTokens( - vaultWithdraw.inputs, - quote.outputs, - quote.inputs, - quote.returned, - splitQuote.outputs - ).map(token => ({ - token: getTokenAddress(token), - minOutputAmount: '0', - })); - - swapQuotes.forEach(quoteStep => { - dustOutputs.push({ - token: getTokenAddress(quoteStep.fromToken), - minOutputAmount: '0', - }); - dustOutputs.push({ - token: getTokenAddress(quoteStep.toToken), - minOutputAmount: '0', - }); - }); - - // @dev uniqBy: first occurrence of each element is kept -> required outputs are kept - const outputs = uniqBy(requiredOutputs.concat(dustOutputs), output => output.token); - - // Perform TX - const zapRequest: UserlessZapRequest = { - order: { - inputs, - outputs, - relay: NO_RELAY, - }, - steps, - }; - - const expectedTokens = quote.outputs.map(output => output.token); - const walletAction = walletActions.zapExecuteOrder( - quote.option.vaultId, - zapRequest, - expectedTokens - ); - - return walletAction(dispatch, getState, extraArgument); - }; - - return { - step: 'zap-out', - message: t('Vault-TxnConfirm', { type: t('Withdraw-noun') }), - action: zapAction, - pending: false, - extraInfo: { zap: true, vaultId: quote.option.vaultId }, - }; - } - - protected async aggregatorTokenSupport() { - const { swapAggregator, getState } = this.helpers; - const state = getState(); - const supportedAggregatorTokens = await swapAggregator.fetchTokenSupport( - this.possibleTokens.map(option => option.token), - this.vault.id, - this.vault.chainId, - state, - this.options.swap - ); - - return { - ...supportedAggregatorTokens, - map: Object.fromEntries( - supportedAggregatorTokens.any.map( - t => - [ - t.address, - this.possibleTokens.filter( - (o, i) => - // disable native as swap target, as zap can't insert balance of native in to call data - !isTokenNative(o.token) && - !isTokenEqual(o.token, t) && - supportedAggregatorTokens.tokens[i].length > 1 && - supportedAggregatorTokens.tokens[i].some(t => isTokenEqual(t, o.token)) - ), - ] as [string, BalancerTokenOption[]] - ) - ), - }; - } -} - -export const BalancerSwapStrategy = - BalancerSwapStrategyImpl satisfies IZapStrategyStatic; diff --git a/src/features/data/apis/transact/strategies/balancer/types.ts b/src/features/data/apis/transact/strategies/balancer/types.ts index a3086e0e7..62e7d0f50 100644 --- a/src/features/data/apis/transact/strategies/balancer/types.ts +++ b/src/features/data/apis/transact/strategies/balancer/types.ts @@ -4,5 +4,4 @@ import type BigNumber from 'bignumber.js'; export type BalancerTokenOption = { index: number; token: TokenEntity; - price: BigNumber; }; diff --git a/src/features/data/apis/transact/strategies/conic/ConicStrategy.ts b/src/features/data/apis/transact/strategies/conic/ConicStrategy.ts index 31e6854ca..950d87700 100644 --- a/src/features/data/apis/transact/strategies/conic/ConicStrategy.ts +++ b/src/features/data/apis/transact/strategies/conic/ConicStrategy.ts @@ -7,6 +7,7 @@ import { type InputTokenAmount, isZapQuoteStepSwap, isZapQuoteStepSwapAggregator, + SelectionOrder, type TokenAmount, type ZapQuoteStep, } from '../../transact-types'; @@ -53,7 +54,7 @@ import { getVaultWithdrawnFromState } from '../../helpers/vault'; import ZapAbi from '../../../../../../config/abi/zap.json'; import type { AbiItem } from 'web3-utils'; import { - includeNativeAndWrapped, + includeWrappedAndNative, nativeAndWrappedAreSame, nativeToWNative, pickTokens, @@ -113,7 +114,7 @@ class ConicStrategyImp implements IZapStrategy { '0x9aE380F0272E2162340a5bB646c354271c0F5cFC' ); // Allow native and wrapped - this.tokens = includeNativeAndWrapped( + this.tokens = includeWrappedAndNative( vault.assetIds.map(id => selectTokenById(state, vault.chainId, id)), this.wnative, this.native @@ -130,7 +131,7 @@ class ConicStrategyImp implements IZapStrategy { vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: 3, + selectionOrder: SelectionOrder.TokenOfPool, inputs, wantedOutputs: outputs, strategyId: 'conic', @@ -308,7 +309,7 @@ class ConicStrategyImp implements IZapStrategy { vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: 3, + selectionOrder: SelectionOrder.TokenOfPool, inputs, wantedOutputs: outputs, strategyId: 'conic', diff --git a/src/features/data/apis/transact/strategies/cowcentrated/CowcentratedStrategy.ts b/src/features/data/apis/transact/strategies/cowcentrated/CowcentratedStrategy.ts index a7ae6e657..78dbff170 100644 --- a/src/features/data/apis/transact/strategies/cowcentrated/CowcentratedStrategy.ts +++ b/src/features/data/apis/transact/strategies/cowcentrated/CowcentratedStrategy.ts @@ -14,6 +14,7 @@ import { isZapQuoteStepDeposit, isZapQuoteStepSwap, isZapQuoteStepSwapAggregator, + SelectionOrder, type TokenAmount, type ZapQuoteStep, type ZapQuoteStepDeposit, @@ -117,7 +118,7 @@ class CowcentratedStrategyImpl implements IComposableStrategy { vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: 3, + selectionOrder: SelectionOrder.Other, inputs, wantedOutputs: this.vaultType.depositTokens, mode: TransactMode.Deposit, @@ -331,7 +332,7 @@ class CowcentratedStrategyImpl implements IComposableStrategy { vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: 3, + selectionOrder: SelectionOrder.Other, inputs, wantedOutputs: outputs, mode: TransactMode.Withdraw, diff --git a/src/features/data/apis/transact/strategies/curve/CurveStrategy.ts b/src/features/data/apis/transact/strategies/curve/CurveStrategy.ts index a71e4e0a4..566082d80 100644 --- a/src/features/data/apis/transact/strategies/curve/CurveStrategy.ts +++ b/src/features/data/apis/transact/strategies/curve/CurveStrategy.ts @@ -19,6 +19,7 @@ import { isZapQuoteStepSwap, isZapQuoteStepSwapAggregator, isZapQuoteStepWithdraw, + SelectionOrder, type TokenAmount, type ZapQuoteStep, type ZapQuoteStepBuild, @@ -206,7 +207,7 @@ class CurveStrategyImpl implements IZapStrategy { vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: 2, + selectionOrder: SelectionOrder.TokenOfPool, inputs, wantedOutputs: outputs, mode: TransactMode.Deposit, @@ -236,7 +237,7 @@ class CurveStrategyImpl implements IZapStrategy { vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: 3, + selectionOrder: SelectionOrder.Other, inputs, wantedOutputs: outputs, mode: TransactMode.Deposit, @@ -623,7 +624,7 @@ class CurveStrategyImpl implements IZapStrategy { vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: 2, + selectionOrder: SelectionOrder.TokenOfPool, inputs, wantedOutputs: outputs, mode: TransactMode.Withdraw, @@ -653,7 +654,7 @@ class CurveStrategyImpl implements IZapStrategy { vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: 3, + selectionOrder: SelectionOrder.Other, inputs, wantedOutputs: outputs, mode: TransactMode.Withdraw, diff --git a/src/features/data/apis/transact/strategies/gamma/GammaStrategy.ts b/src/features/data/apis/transact/strategies/gamma/GammaStrategy.ts index 9056b5ce0..570f95762 100644 --- a/src/features/data/apis/transact/strategies/gamma/GammaStrategy.ts +++ b/src/features/data/apis/transact/strategies/gamma/GammaStrategy.ts @@ -10,6 +10,7 @@ import { isZapQuoteStepSwap, isZapQuoteStepSwapAggregator, isZapQuoteStepWithdraw, + SelectionOrder, type TokenAmount, type ZapFee, type ZapQuoteStep, @@ -82,6 +83,7 @@ import { selectVaultStrategyAddress } from '../../../../selectors/vaults'; import { QuoteChangedError } from '../error'; import { isStandardVaultType, type IStandardVaultType } from '../../vaults/IVaultType'; import type { GammaStrategyConfig } from '../strategy-configs'; +import { tokenInList } from '../../../../../../helpers/tokens'; type ZapHelpers = { chain: ChainEntity; @@ -170,7 +172,9 @@ class GammaStrategyImpl implements IZapStrategy { vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: 3, + selectionOrder: tokenInList(token, this.lpTokens) + ? SelectionOrder.TokenOfPool + : SelectionOrder.Other, inputs, wantedOutputs: outputs, mode: TransactMode.Deposit, @@ -618,7 +622,7 @@ class GammaStrategyImpl implements IZapStrategy { vaultId: this.vault.id, chainId: this.vault.chainId, selectionId: breakSelectionId, - selectionOrder: 2, + selectionOrder: SelectionOrder.AllTokensInPool, inputs, wantedOutputs: this.lpTokens, mode: TransactMode.Withdraw, @@ -637,7 +641,9 @@ class GammaStrategyImpl implements IZapStrategy { vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: 3, + selectionOrder: tokenInList(token, this.lpTokens) + ? SelectionOrder.TokenOfPool + : SelectionOrder.Other, inputs, wantedOutputs: outputs, mode: TransactMode.Withdraw, diff --git a/src/features/data/apis/transact/strategies/index.ts b/src/features/data/apis/transact/strategies/index.ts index 64002e4d9..37d62fa46 100644 --- a/src/features/data/apis/transact/strategies/index.ts +++ b/src/features/data/apis/transact/strategies/index.ts @@ -20,10 +20,7 @@ const strategyLoadersByIdUnchecked = { (await import('./cowcentrated/CowcentratedStrategy')).CowcentratedStrategy, 'reward-pool-to-vault': async () => (await import('./RewardPoolToVaultStrategy')).RewardPoolToVaultStrategy, - 'balancer-swap': async () => - (await import('./balancer/BalancerSwapStrategy')).BalancerSwapStrategy, - 'balancer-join': async () => - (await import('./balancer/BalancerJoinStrategy')).BalancerJoinStrategy, + balancer: async () => (await import('./balancer/BalancerStrategy')).BalancerStrategy, } as const satisfies Record Promise>; type StrategyIdToStaticPromise = typeof strategyLoadersByIdUnchecked; diff --git a/src/features/data/apis/transact/strategies/single/SingleStrategy.ts b/src/features/data/apis/transact/strategies/single/SingleStrategy.ts index f2b26d044..5c11f1126 100644 --- a/src/features/data/apis/transact/strategies/single/SingleStrategy.ts +++ b/src/features/data/apis/transact/strategies/single/SingleStrategy.ts @@ -10,6 +10,7 @@ import { isZapQuoteStepSwap, isZapQuoteStepSwapAggregator, isZapQuoteStepWithdraw, + SelectionOrder, type SingleDepositOption, type SingleDepositQuote, type SingleWithdrawOption, @@ -140,7 +141,7 @@ class SingleStrategyImpl implements IComposableStrategy { vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: 3, + selectionOrder: SelectionOrder.Other, inputs, wantedOutputs: outputs, strategyId: 'single', @@ -335,7 +336,7 @@ class SingleStrategyImpl implements IComposableStrategy { vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: 3, + selectionOrder: SelectionOrder.Other, inputs, wantedOutputs: outputs, strategyId: 'single', diff --git a/src/features/data/apis/transact/strategies/strategy-configs.ts b/src/features/data/apis/transact/strategies/strategy-configs.ts index 1dd41d4ff..78de238bc 100644 --- a/src/features/data/apis/transact/strategies/strategy-configs.ts +++ b/src/features/data/apis/transact/strategies/strategy-configs.ts @@ -38,21 +38,27 @@ export type CurveStrategyConfig = { methods: CurveMethod[]; } & OptionalStrategySwapConfig; -export type BalancerSwapStrategyConfig = { - strategyId: 'balancer-swap'; +type BalancerStrategyConfigBase = { + strategyId: 'balancer'; ammId: AmmEntityBalancer['id']; poolId: string; - poolType: 'composable-stable'; + /** excluding the BPT token */ tokens: string[]; -} & OptionalStrategySwapConfig; +}; -export type BalancerJoinStrategyConfig = { - strategyId: 'balancer-join'; - ammId: AmmEntityBalancer['id']; - poolId: string; +type BalancerStrategyConfigComposableStable = { + poolType: 'composable-stable'; + bptIndex: number; + hasNestedPool: boolean; +}; + +type BalancerStrategyConfigOther = { poolType: 'gyro' | 'gyroe' | 'weighted' | 'meta-stable'; - tokens: string[]; -} & OptionalStrategySwapConfig; +}; + +export type BalancerStrategyConfig = BalancerStrategyConfigBase & + OptionalStrategySwapConfig & + (BalancerStrategyConfigComposableStable | BalancerStrategyConfigOther); export type GammaStrategyConfig = { strategyId: 'gamma'; @@ -92,8 +98,7 @@ export type ZapStrategyConfig = | GovComposerStrategyConfig | VaultComposerStrategyConfig | RewardPoolToVaultStrategyConfig - | BalancerSwapStrategyConfig - | BalancerJoinStrategyConfig; + | BalancerStrategyConfig; export type ZapStrategyId = ZapStrategyConfig['strategyId']; diff --git a/src/features/data/apis/transact/transact-types.ts b/src/features/data/apis/transact/transact-types.ts index 96765e622..692600fc8 100644 --- a/src/features/data/apis/transact/transact-types.ts +++ b/src/features/data/apis/transact/transact-types.ts @@ -15,7 +15,6 @@ import type { import type { PlatformEntity } from '../../entities/platform'; import type { CurveTokenOption } from './strategies/curve/types'; import type { ZapStrategyId } from './strategies/strategy-configs'; -import type { BalancerTokenOption } from './strategies/balancer/types'; export type TokenAmount = { amount: BigNumber; @@ -56,6 +55,19 @@ export function isZapFeeNonZero(zapFee: ZapFee): boolean { // Options // +export enum SelectionOrder { + /** The deposit token */ + Want = 0, + /** CLM Vault-to-Vault Zap */ + VaultToVault, + /** All tokens in the pool e.g. Break LP or CLM */ + AllTokensInPool, + /** Any token in the LP */ + TokenOfPool, + /** Any other token not in the LP */ + Other, +} + type BaseOption = { /** should be unique over all strategies and token selections */ id: string; @@ -63,7 +75,7 @@ type BaseOption = { chainId: ChainEntity['id']; /** governs how selections are grouped in the UI, should be consistent for the same deposit input/withdraw output token(s) per chain */ selectionId: string; - selectionOrder: number; + selectionOrder: SelectionOrder; inputs: TokenEntity[]; wantedOutputs: TokenEntity[]; }; @@ -178,30 +190,69 @@ export type CurveWithdrawOption = ZapBaseWithdrawOption & { | { via: 'aggregator'; viaTokens: CurveTokenOption[] } ); -export type BalancerSwapDepositOption = ZapBaseDepositOption & { - strategyId: 'balancer-swap'; -} & ( - | { via: 'direct'; viaToken: BalancerTokenOption } - | { via: 'aggregator'; viaTokens: BalancerTokenOption[] } - ); +export type BalancerOptionSingleDirectPart = { + type: 'single'; + via: 'direct'; + viaToken: TokenEntity; +}; -export type BalancerSwapWithdrawOption = ZapBaseWithdrawOption & { - strategyId: 'balancer-swap'; -} & ( - | { via: 'direct'; viaToken: BalancerTokenOption } - | { via: 'aggregator'; viaTokens: BalancerTokenOption[] } +export type BalancerOptionSingleAggregatorPart = { + type: 'single'; + via: 'aggregator'; + viaTokens: TokenEntity[]; +}; + +export type BalancerOptionAllAggregatorPart = { + type: 'all'; + via: 'aggregator'; + viaTokens: TokenEntity[]; +}; + +type BalancerDepositionOptionBase = ZapBaseDepositOption & { + strategyId: 'balancer'; +}; + +export type BalancerDepositOptionSingleDirect = BalancerDepositionOptionBase & + BalancerOptionSingleDirectPart; +export type BalancerDepositOptionSingleAggregator = BalancerDepositionOptionBase & + BalancerOptionSingleAggregatorPart; +export type BalancerDepositOptionAllAggregator = BalancerDepositionOptionBase & + BalancerOptionAllAggregatorPart; + +export type BalancerDepositOption = BalancerDepositionOptionBase & + ( + | BalancerOptionSingleDirectPart + | BalancerOptionSingleAggregatorPart + | BalancerOptionAllAggregatorPart ); -export type BalancerPoolDepositOption = ZapBaseDepositOption & { - strategyId: 'balancer-join'; - via: /*'pool' | */ 'aggregator'; +type BalancerWithdrawOptionBase = ZapBaseWithdrawOption & { + strategyId: 'balancer'; }; -export type BalancerPoolWithdrawOption = ZapBaseWithdrawOption & { - strategyId: 'balancer-join'; - via: 'break-only' | /*'pool' | */ 'aggregator'; +export type BalancerOptionAllBreakOnlyPart = { + type: 'all'; + via: 'break-only'; + viaTokens: TokenEntity[]; }; +export type BalancerWithdrawOptionSingleDirect = BalancerWithdrawOptionBase & + BalancerOptionSingleDirectPart; +export type BalancerWithdrawOptionSingleAggregator = BalancerWithdrawOptionBase & + BalancerOptionSingleAggregatorPart; +export type BalancerWithdrawOptionAllAggregator = BalancerWithdrawOptionBase & + BalancerOptionAllAggregatorPart; +export type BalancerWithdrawOptionAllBreakOnly = BalancerWithdrawOptionBase & + BalancerOptionAllBreakOnlyPart; + +export type BalancerWithdrawOption = BalancerWithdrawOptionBase & + ( + | BalancerOptionSingleDirectPart + | BalancerOptionSingleAggregatorPart + | BalancerOptionAllAggregatorPart + | BalancerOptionAllBreakOnlyPart + ); + export type ConicDepositOption = ZapBaseDepositOption & { strategyId: 'conic'; }; @@ -264,8 +315,7 @@ export type DepositOption = | GovComposerDepositOption | VaultComposerDepositOption | RewardPoolToVaultDepositOption - | BalancerSwapDepositOption - | BalancerPoolDepositOption; + | BalancerDepositOption; export type WithdrawOption = | StandardVaultWithdrawOption @@ -281,8 +331,7 @@ export type WithdrawOption = | GovComposerWithdrawOption | VaultComposerWithdrawOption | RewardPoolToVaultWithdrawOption - | BalancerSwapWithdrawOption - | BalancerPoolWithdrawOption; + | BalancerWithdrawOption; export type TransactOption = DepositOption | WithdrawOption; @@ -490,12 +539,12 @@ export type CurveDepositQuote = BaseZapQuote & { viaToken: CurveTokenOption; }; -export type BalancerSwapDepositQuote = BaseZapQuote & { - via: 'aggregator' | 'direct'; - viaToken: BalancerTokenOption; -}; +// export type BalancerSwapDepositQuote = BaseZapQuote & { +// via: 'aggregator' | 'direct'; +// viaToken: BalancerTokenOption; +// }; -export type BalancerPoolDepositQuote = BaseZapQuote; +export type BalancerDepositQuote = BaseZapQuote; export type GammaDepositQuote = BaseZapQuote & { lpQuotes: (QuoteResponse | undefined)[]; @@ -521,8 +570,7 @@ export type ZapDepositQuote = | GovComposerZapDepositQuote | VaultComposerZapDepositQuote | RewardPoolToVaultDepositQuote - | BalancerSwapDepositQuote - | BalancerPoolDepositQuote; + | BalancerDepositQuote; export type DepositQuote = VaultDepositQuote | ZapDepositQuote; @@ -568,12 +616,20 @@ export type CurveWithdrawQuote = BaseZapQuote & { viaToken: CurveTokenOption; }; -export type BalancerSwapWithdrawQuote = BaseZapQuote & { - via: 'aggregator' | 'direct'; - viaToken: BalancerTokenOption; +export type BalancerWithdrawQuoteSingle = BaseZapQuote< + BalancerWithdrawOptionSingleDirect | BalancerWithdrawOptionSingleAggregator +> & { + type: 'single'; + viaToken: TokenEntity; +}; + +export type BalancerWithdrawQuoteAll = BaseZapQuote< + BalancerWithdrawOptionAllAggregator | BalancerWithdrawOptionAllBreakOnly +> & { + type: 'all'; }; -export type BalancerPoolWithdrawQuote = BaseZapQuote; +export type BalancerWithdrawQuote = BalancerWithdrawQuoteSingle | BalancerWithdrawQuoteAll; export type GammaBreakWithdrawQuote = BaseZapQuote; export type GammaAggregatorWithdrawQuote = BaseZapQuote & { @@ -616,8 +672,7 @@ export type ZapWithdrawQuote = | CowcentratedZapWithdrawQuote | GovComposerZapWithdrawQuote | VaultComposerZapWithdrawQuote - | BalancerSwapWithdrawQuote - | BalancerPoolWithdrawQuote; + | BalancerWithdrawQuote; export type WithdrawQuote = VaultWithdrawQuote | ZapWithdrawQuote; diff --git a/src/features/data/apis/transact/vaults/CowcentratedVaultType.ts b/src/features/data/apis/transact/vaults/CowcentratedVaultType.ts index 2c7a63938..d565c8e9d 100644 --- a/src/features/data/apis/transact/vaults/CowcentratedVaultType.ts +++ b/src/features/data/apis/transact/vaults/CowcentratedVaultType.ts @@ -17,13 +17,14 @@ import { onlyInputCount, onlyOneInput, } from '../helpers/options'; -import type { - CowcentratedVaultDepositOption, - CowcentratedVaultDepositQuote, - CowcentratedVaultWithdrawOption, - CowcentratedVaultWithdrawQuote, - InputTokenAmount, - TokenAmount, +import { + type CowcentratedVaultDepositOption, + type CowcentratedVaultDepositQuote, + type CowcentratedVaultWithdrawOption, + type CowcentratedVaultWithdrawQuote, + type InputTokenAmount, + SelectionOrder, + type TokenAmount, } from '../transact-types'; import type { ICowcentratedVaultType, @@ -84,7 +85,7 @@ export class CowcentratedVaultType implements ICowcentratedVaultType { vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: 1, + selectionOrder: SelectionOrder.AllTokensInPool, inputs, wantedOutputs: inputs, strategyId: 'vault', @@ -202,7 +203,7 @@ export class CowcentratedVaultType implements ICowcentratedVaultType { vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: 1, + selectionOrder: SelectionOrder.AllTokensInPool, inputs: inputs, wantedOutputs: outputs, strategyId: 'vault', diff --git a/src/features/data/apis/transact/vaults/GovVaultType.ts b/src/features/data/apis/transact/vaults/GovVaultType.ts index 7b053c948..7063b9b76 100644 --- a/src/features/data/apis/transact/vaults/GovVaultType.ts +++ b/src/features/data/apis/transact/vaults/GovVaultType.ts @@ -16,14 +16,15 @@ import { onlyOneInput, } from '../helpers/options'; import { TransactMode } from '../../../reducers/wallet/transact-types'; -import type { - GovVaultDepositOption, - GovVaultDepositQuote, - GovVaultWithdrawOption, - GovVaultWithdrawQuote, - InputTokenAmount, - TokenAmount, - TransactQuote, +import { + type GovVaultDepositOption, + type GovVaultDepositQuote, + type GovVaultWithdrawOption, + type GovVaultWithdrawQuote, + type InputTokenAmount, + SelectionOrder, + type TokenAmount, + type TransactQuote, } from '../transact-types'; import type { TokenEntity } from '../../../entities/token'; import { isTokenEqual, isTokenErc20 } from '../../../entities/token'; @@ -83,7 +84,7 @@ export class GovVaultType implements IGovVaultType { vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: 1, + selectionOrder: SelectionOrder.Want, inputs, wantedOutputs: inputs, strategyId: 'vault', @@ -158,7 +159,7 @@ export class GovVaultType implements IGovVaultType { vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: 1, + selectionOrder: SelectionOrder.Want, inputs, wantedOutputs: inputs, strategyId: 'vault', diff --git a/src/features/data/apis/transact/vaults/StandardVaultType.ts b/src/features/data/apis/transact/vaults/StandardVaultType.ts index b6d7c994b..23fbcf92d 100644 --- a/src/features/data/apis/transact/vaults/StandardVaultType.ts +++ b/src/features/data/apis/transact/vaults/StandardVaultType.ts @@ -24,14 +24,15 @@ import { onlyInputCount, onlyOneInput, } from '../helpers/options'; -import type { - InputTokenAmount, - StandardVaultDepositOption, - StandardVaultDepositQuote, - StandardVaultWithdrawOption, - StandardVaultWithdrawQuote, - TokenAmount, - TransactQuote, +import { + type InputTokenAmount, + SelectionOrder, + type StandardVaultDepositOption, + type StandardVaultDepositQuote, + type StandardVaultWithdrawOption, + type StandardVaultWithdrawQuote, + type TokenAmount, + type TransactQuote, } from '../transact-types'; import { TransactMode } from '../../../reducers/wallet/transact-types'; import { first } from 'lodash-es'; @@ -230,7 +231,7 @@ export class StandardVaultType implements IStandardVaultType { vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: 1, + selectionOrder: SelectionOrder.Want, inputs, wantedOutputs: inputs, strategyId: 'vault', @@ -304,7 +305,7 @@ export class StandardVaultType implements IStandardVaultType { vaultId: this.vault.id, chainId: this.vault.chainId, selectionId, - selectionOrder: 1, + selectionOrder: SelectionOrder.Want, inputs, wantedOutputs: inputs, strategyId: 'vault', diff --git a/src/features/vault/components/Actions/Transact/TransactDebugger/BalancerZap.tsx b/src/features/vault/components/Actions/Transact/TransactDebugger/BalancerZap.tsx index b0e658328..0613f5d30 100644 --- a/src/features/vault/components/Actions/Transact/TransactDebugger/BalancerZap.tsx +++ b/src/features/vault/components/Actions/Transact/TransactDebugger/BalancerZap.tsx @@ -18,7 +18,7 @@ import { selectIsAddressBookLoaded, selectIsZapLoaded, } from '../../../../../data/selectors/data-loader'; -import type { BalancerSwapStrategyConfig } from '../../../../../data/apis/transact/strategies/strategy-configs'; +import type { BalancerStrategyConfig } from '../../../../../data/apis/transact/strategies/strategy-configs'; import { isDefined } from '../../../../../data/utils/array-utils'; const useStyles = makeStyles(styles); @@ -30,10 +30,7 @@ const BalancerZap = memo(function BalancerZap({ vaultId }) { const classes = useStyles(); const vault = useAppSelector(state => selectVaultById(state, vaultId)); const zap = isStandardVault(vault) - ? vault.zaps.find( - (zap): zap is BalancerSwapStrategyConfig => - zap.strategyId === 'balancer-swap' || zap.strategyId === 'balancer-join' - ) + ? vault.zaps.find((zap): zap is BalancerStrategyConfig => zap.strategyId === 'balancer') : undefined; return ( @@ -50,13 +47,13 @@ const NoZap = memo(function NoZap() { type ZapLoaderProps = { vault: VaultStandard; - zap: BalancerSwapStrategyConfig; + zap: BalancerStrategyConfig; }; type ZapProps = { aggregatorSupportedTokens: TokenEntity[]; vault: VaultStandard; - zap: BalancerSwapStrategyConfig; + zap: BalancerStrategyConfig; }; const ZapLoader = memo(function ZapLoader({ vault, zap }) { diff --git a/src/helpers/tokens.ts b/src/helpers/tokens.ts index 09c49d8f3..649e7a1b4 100644 --- a/src/helpers/tokens.ts +++ b/src/helpers/tokens.ts @@ -1,4 +1,4 @@ -import type { TokenEntity } from '../features/data/entities/token'; +import { isTokenEqual, type TokenEntity } from '../features/data/entities/token'; import { uniqBy } from 'lodash-es'; export function uniqueTokens(tokens: T[]): T[] { @@ -18,3 +18,7 @@ export function checkAddressOrder(addresses: string[]) { } } } + +export function tokenInList(token: T, list: T[]): boolean { + return list.some(t => isTokenEqual(t, token)); +} From 3069a2e6022a68776e85a359a118beca799b864d Mon Sep 17 00:00:00 2001 From: ReflectiveChimp <55021052+ReflectiveChimp@users.noreply.github.com> Date: Wed, 25 Sep 2024 21:28:51 +0100 Subject: [PATCH 09/20] addBalancerZap - allow single token no aggregator --- scripts/addBalancerZap.ts | 22 +++++-- src/config/vault/arbitrum.json | 89 ++++++++++++++++++++++++++--- src/config/vault/gnosis.json | 14 +---- src/config/vault/optimism.json | 1 - src/features/data/actions/vaults.ts | 1 + 5 files changed, 100 insertions(+), 27 deletions(-) diff --git a/scripts/addBalancerZap.ts b/scripts/addBalancerZap.ts index e483e0993..c57f884dc 100644 --- a/scripts/addBalancerZap.ts +++ b/scripts/addBalancerZap.ts @@ -594,11 +594,16 @@ function checkWeightedPool(pool: Pool, tokens: PoolToken[], tokensWithBpt: PoolT throw new Error(`${pool.type}: Did not expect BPT token in pool`); } - // TODO we might be able to single sided join in future - if (tokens.some(t => !t.abToken || !t.price || !t.swapProviders.length)) { + if (tokens.every(t => !t.abToken || !t.price)) { logTokens(tokens); throw new Error( - `${pool.type}: All tokens must be in the address book, have a price, and have a zap swap provider` + `${pool.type}: At least one token must be in the address book and have a price` + ); + } + + if (tokens.every(t => !t.abToken || !t.price || !t.swapProviders.length)) { + console.warn( + `${pool.type}: No tokens are in the address book, have a price, and have a zap swap provider - only pool tokens will be available for deposit` ); } @@ -650,11 +655,16 @@ function checkMetaStablePool(pool: Pool, tokens: PoolToken[], tokensWithBpt: Poo throw new Error(`${pool.type}: Did not expect BPT token in pool`); } - // TODO we might be able to single sided join in future - if (tokens.some(t => !t.abToken || !t.price || !t.swapProviders.length)) { + if (tokens.every(t => !t.abToken || !t.price)) { logTokens(tokens); throw new Error( - `${pool.type}: All tokens must be in the address book, have a price, and have a zap swap provider` + `${pool.type}: At least one token must be in the address book and have a price` + ); + } + + if (tokens.every(t => !t.abToken || !t.price || !t.swapProviders.length)) { + console.warn( + `${pool.type}: No tokens are in the address book, have a price, and have a zap swap provider - only pool tokens will be available for deposit` ); } diff --git a/src/config/vault/arbitrum.json b/src/config/vault/arbitrum.json index 2f06a2629..f8c07b29d 100644 --- a/src/config/vault/arbitrum.json +++ b/src/config/vault/arbitrum.json @@ -7,9 +7,9 @@ "tokenAddress": "0xF890360473c12d8015DA8DBf7Af11dA87337A065", "tokenDecimals": 18, "tokenProviderId": "balancer", + "earnContractAddress": "0x6ed71003A45fFb97C27d60043E1235D3B237606a", "earnedToken": "mooAuraArbGHO/USDC/USDT", "earnedTokenAddress": "0x6ed71003A45fFb97C27d60043E1235D3B237606a", - "earnContractAddress": "0x6ed71003A45fFb97C27d60043E1235D3B237606a", "oracle": "lps", "oracleId": "aura-arb-gho-usdc-usdt", "status": "active", @@ -27,7 +27,22 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xf890360473c12d8015da8dbf7af11da87337a065000000000000000000000570/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xf890360473c12d8015da8dbf7af11da87337a065000000000000000000000570/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0xf890360473c12d8015da8dbf7af11da87337a065000000000000000000000570", + "poolType": "composable-stable", + "tokens": [ + "0x7dfF72693f6A4149b17e7C6314655f6A9F7c8B33", + "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9" + ], + "bptIndex": 2, + "hasNestedPool": false + } + ] }, { "id": "aura-arb-ausdc-gusdc", @@ -99,7 +114,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xdeeaf8b0a8cf26217261b813e085418c7dd8f1ee00020000000000000000058f/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xdeeaf8b0a8cf26217261b813e085418c7dd8f1ee00020000000000000000058f/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0xdeeaf8b0a8cf26217261b813e085418c7dd8f1ee00020000000000000000058f", + "poolType": "gyroe", + "tokens": [ + "0x211Cc4DD073734dA055fbF44a2b4667d5E5fE5d2", + "0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8" + ] + } + ] }, { "id": "aura-arb-agho-gyd", @@ -129,7 +156,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xff38cc0ce0de4476c5a3e78675b48420a851035b000200000000000000000593/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xff38cc0ce0de4476c5a3e78675b48420a851035b000200000000000000000593/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0xff38cc0ce0de4476c5a3e78675b48420a851035b000200000000000000000593", + "poolType": "gyroe", + "tokens": [ + "0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8", + "0xD9FBA68D89178e3538e708939332c79efC540179" + ] + } + ] }, { "id": "aura-arb-ausdc-wusdm", @@ -1682,7 +1721,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x7272163a931dac5bbe1cb5fefaf959bb65f7346f000200000000000000000549/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x7272163a931dac5bbe1cb5fefaf959bb65f7346f000200000000000000000549/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0x7272163a931dac5bbe1cb5fefaf959bb65f7346f000200000000000000000549", + "poolType": "gyroe", + "tokens": [ + "0xb165a74407fE1e519d6bCbDeC1Ed3202B35a4140", + "0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8" + ] + } + ] }, { "id": "aura-arb-ausdc-gyd", @@ -1712,7 +1763,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x6e822c64c00393b2078f2a5bb75c575ab505b55c000200000000000000000548/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x6e822c64c00393b2078f2a5bb75c575ab505b55c000200000000000000000548/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0x6e822c64c00393b2078f2a5bb75c575ab505b55c000200000000000000000548", + "poolType": "gyroe", + "tokens": [ + "0x7CFaDFD5645B50bE87d546f42699d863648251ad", + "0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8" + ] + } + ] }, { "id": "pendle-arb-usde-28nov24", @@ -1767,7 +1830,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x315dd595e82bdc0c194f3a38a08fde480d7e5d2100020000000000000000056a/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0x315dd595e82bdc0c194f3a38a08fde480d7e5d2100020000000000000000056a/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0x315dd595e82bdc0c194f3a38a08fde480d7e5d2100020000000000000000056a", + "poolType": "gyroe", + "tokens": [ + "0x57F5E098CaD7A3D1Eed53991D4d66C45C9AF7812", + "0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8" + ] + } + ] }, { "id": "ramses-cl-rseth-ethx-vault", diff --git a/src/config/vault/gnosis.json b/src/config/vault/gnosis.json index 41613f40d..ea309862d 100644 --- a/src/config/vault/gnosis.json +++ b/src/config/vault/gnosis.json @@ -27,19 +27,7 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://app.gyro.finance/pools/gnosis/e-clp/0x71E1179C5e197FA551BEEC85ca2EF8693c61b85b/", "removeLiquidityUrl": "https://app.gyro.finance/pools/gnosis/e-clp/0x71E1179C5e197FA551BEEC85ca2EF8693c61b85b/", - "network": "gnosis", - "zaps": [ - { - "strategyId": "balancer", - "ammId": "gnosis-balancer", - "poolId": "0x71e1179c5e197fa551beec85ca2ef8693c61b85b0002000000000000000000a0", - "poolType": "gyroe", - "tokens": [ - "0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1", - "0xc791240D1F2dEf5938E2031364Ff4ed887133C3d" - ] - } - ] + "network": "gnosis" }, { "id": "aura-gyro-gnosis-weth-wsteth", diff --git a/src/config/vault/optimism.json b/src/config/vault/optimism.json index 831e45da7..807267e6d 100644 --- a/src/config/vault/optimism.json +++ b/src/config/vault/optimism.json @@ -59,7 +59,6 @@ } ] }, - { "id": "velo-cow-op-wsteth-op-vault", "name": "wstETH-OP", diff --git a/src/features/data/actions/vaults.ts b/src/features/data/actions/vaults.ts index 0f00be102..122b70ba3 100644 --- a/src/features/data/actions/vaults.ts +++ b/src/features/data/actions/vaults.ts @@ -298,6 +298,7 @@ function getVaultStatus(apiVault: VaultConfig): VaultStatus { function getVaultBase(config: VaultConfig, chainId: ChainEntity['id']): VaultBase { const names = getVaultNames(config.name, config.type); + // TODO remove if (config.tokenProviderId === 'balancer' || config.tokenProviderId === 'beethovenx') { const zap = config.zaps?.find((z): z is BalancerStrategyConfig => z.strategyId === 'balancer'); if (zap) { From 7238bc9727f0e40919e0c4e734d6b950c57b1d30 Mon Sep 17 00:00:00 2001 From: ReflectiveChimp <55021052+ReflectiveChimp@users.noreply.github.com> Date: Wed, 25 Sep 2024 22:14:46 +0100 Subject: [PATCH 10/20] aura-gyro-gnosis-weth-reth config --- src/config/vault/gnosis.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/config/vault/gnosis.json b/src/config/vault/gnosis.json index ea309862d..41613f40d 100644 --- a/src/config/vault/gnosis.json +++ b/src/config/vault/gnosis.json @@ -27,7 +27,19 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://app.gyro.finance/pools/gnosis/e-clp/0x71E1179C5e197FA551BEEC85ca2EF8693c61b85b/", "removeLiquidityUrl": "https://app.gyro.finance/pools/gnosis/e-clp/0x71E1179C5e197FA551BEEC85ca2EF8693c61b85b/", - "network": "gnosis" + "network": "gnosis", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "gnosis-balancer", + "poolId": "0x71e1179c5e197fa551beec85ca2ef8693c61b85b0002000000000000000000a0", + "poolType": "gyroe", + "tokens": [ + "0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1", + "0xc791240D1F2dEf5938E2031364Ff4ed887133C3d" + ] + } + ] }, { "id": "aura-gyro-gnosis-weth-wsteth", From a33dbb5b0875a98c534e17cfbf715e81f15577b4 Mon Sep 17 00:00:00 2001 From: ReflectiveChimp <55021052+ReflectiveChimp@users.noreply.github.com> Date: Fri, 27 Sep 2024 10:17:02 +0100 Subject: [PATCH 11/20] allow 0 swaps --- .../apis/transact/strategies/balancer/BalancerStrategy.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/features/data/apis/transact/strategies/balancer/BalancerStrategy.ts b/src/features/data/apis/transact/strategies/balancer/BalancerStrategy.ts index 6c8936401..c240c2fc1 100644 --- a/src/features/data/apis/transact/strategies/balancer/BalancerStrategy.ts +++ b/src/features/data/apis/transact/strategies/balancer/BalancerStrategy.ts @@ -772,11 +772,6 @@ class BalancerStrategyImpl implements IZapStrategy { throw new Error('BalancerStrategy: No build step in quote'); } - // since there are two tokens, there must be at least 1 swap - if (swapQuotes.length < 1) { - throw new Error('BalancerStrategy: Not enough swaps'); - } - // Swaps if (swapQuotes.length) { if (swapQuotes.length > this.poolTokens.length) { From 8d763bd5c327e529946a4e8c6740a6eb6399c40c Mon Sep 17 00:00:00 2001 From: ReflectiveChimp <55021052+ReflectiveChimp@users.noreply.github.com> Date: Fri, 27 Sep 2024 20:22:30 +0100 Subject: [PATCH 12/20] enable single zap on aura-aurabal --- src/config/vault/ethereum.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/config/vault/ethereum.json b/src/config/vault/ethereum.json index dbc21aa81..b61a347ba 100644 --- a/src/config/vault/ethereum.json +++ b/src/config/vault/ethereum.json @@ -4885,7 +4885,12 @@ "strategyTypeId": "single", "addLiquidityUrl": "https://app.aura.finance/", "removeLiquidityUrl": "https://app.aura.finance/", - "network": "ethereum" + "network": "ethereum", + "zaps": [ + { + "strategyId": "single" + } + ] }, { "id": "convex-usdd", From 3a493e8e9246120aa0f1e4f9f4d593f578191b45 Mon Sep 17 00:00:00 2001 From: ReflectiveChimp <55021052+ReflectiveChimp@users.noreply.github.com> Date: Tue, 1 Oct 2024 13:45:41 +0100 Subject: [PATCH 13/20] tidy / remove test helpers --- src/features/data/actions/vaults.ts | 10 ---------- .../data/apis/transact/strategies/balancer/types.ts | 7 ------- 2 files changed, 17 deletions(-) delete mode 100644 src/features/data/apis/transact/strategies/balancer/types.ts diff --git a/src/features/data/actions/vaults.ts b/src/features/data/actions/vaults.ts index d4d1b5072..8e7c2bd04 100644 --- a/src/features/data/actions/vaults.ts +++ b/src/features/data/actions/vaults.ts @@ -302,16 +302,6 @@ function getVaultStatus(apiVault: VaultConfig): VaultStatus { function getVaultBase(config: VaultConfig, chainId: ChainEntity['id']): VaultBase { const names = getVaultNames(config.name, config.type); - // TODO remove - if (config.tokenProviderId === 'balancer' || config.tokenProviderId === 'beethovenx') { - const zap = config.zaps?.find((z): z is BalancerStrategyConfig => z.strategyId === 'balancer'); - if (zap) { - for (const k of Object.keys(names)) { - names[k] = `${names[k]} [${zap.poolType}]`; - } - } - } - return { id: config.id, name: config.id === 'bifi-vault' ? names.long : config.name, diff --git a/src/features/data/apis/transact/strategies/balancer/types.ts b/src/features/data/apis/transact/strategies/balancer/types.ts deleted file mode 100644 index 62e7d0f50..000000000 --- a/src/features/data/apis/transact/strategies/balancer/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { TokenEntity } from '../../../../entities/token'; -import type BigNumber from 'bignumber.js'; - -export type BalancerTokenOption = { - index: number; - token: TokenEntity; -}; From 5d158f26e74f377f88374609f86b39bb4f7f43c9 Mon Sep 17 00:00:00 2001 From: ReflectiveChimp <55021052+ReflectiveChimp@users.noreply.github.com> Date: Tue, 1 Oct 2024 13:48:09 +0100 Subject: [PATCH 14/20] remove zap for stataArbGHO (no aggregator) --- src/config/vault/arbitrum.json | 14 +------------- src/features/data/actions/vaults.ts | 1 - 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/config/vault/arbitrum.json b/src/config/vault/arbitrum.json index ab9d21646..535dddff9 100644 --- a/src/config/vault/arbitrum.json +++ b/src/config/vault/arbitrum.json @@ -227,19 +227,7 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xff38cc0ce0de4476c5a3e78675b48420a851035b000200000000000000000593/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xff38cc0ce0de4476c5a3e78675b48420a851035b000200000000000000000593/remove-liquidity", - "network": "arbitrum", - "zaps": [ - { - "strategyId": "balancer", - "ammId": "arbitrum-balancer", - "poolId": "0xff38cc0ce0de4476c5a3e78675b48420a851035b000200000000000000000593", - "poolType": "gyroe", - "tokens": [ - "0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8", - "0xD9FBA68D89178e3538e708939332c79efC540179" - ] - } - ] + "network": "arbitrum" }, { "id": "aura-arb-ausdc-wusdm", diff --git a/src/features/data/actions/vaults.ts b/src/features/data/actions/vaults.ts index 8e7c2bd04..f040df953 100644 --- a/src/features/data/actions/vaults.ts +++ b/src/features/data/actions/vaults.ts @@ -18,7 +18,6 @@ import { import { getVaultNames } from '../utils/vault-utils'; import { safetyScoreNum } from '../../../helpers/safetyScore'; import { isDefined } from '../utils/array-utils'; -import type { BalancerStrategyConfig } from '../apis/transact/strategies/strategy-configs'; export interface FulfilledAllVaultsPayload { byChainId: { From 69c7f8a81e3e7baaf577e99c4dc1ba11af645add Mon Sep 17 00:00:00 2001 From: ReflectiveChimp <55021052+ReflectiveChimp@users.noreply.github.com> Date: Tue, 8 Oct 2024 11:22:19 +0100 Subject: [PATCH 15/20] only allow odos for aave tokens --- scripts/addBalancerZap.ts | 199 +++++++++--------- src/config/vault/arbitrum.json | 52 ++++- .../composable-stable/ComposableStablePool.ts | 121 ----------- .../data/apis/amm/balancer/gyro/GyroPool.ts | 19 -- .../transact/strategies/strategy-configs.ts | 4 +- 5 files changed, 143 insertions(+), 252 deletions(-) diff --git a/scripts/addBalancerZap.ts b/scripts/addBalancerZap.ts index c57f884dc..1c57bb3f7 100644 --- a/scripts/addBalancerZap.ts +++ b/scripts/addBalancerZap.ts @@ -25,9 +25,12 @@ import { import { mkdir } from 'node:fs/promises'; import { createCachedFactory, createFactory } from '../src/features/data/utils/factory-utils'; import { addressBook, Token } from 'blockchain-addressbook'; -import { partition, sortBy } from 'lodash'; +import { sortBy } from 'lodash'; import platforms from '../src/config/platforms.json'; -import { BalancerStrategyConfig } from '../src/features/data/apis/transact/strategies/strategy-configs'; +import { + BalancerStrategyConfig, + OptionalStrategySwapConfig, +} from '../src/features/data/apis/transact/strategies/strategy-configs'; import { sortVaultKeys } from './common/vault-fields'; const cacheBasePath = path.join(__dirname, '.cache', 'balancer'); @@ -91,6 +94,14 @@ type RpcPool = { scalingFactors?: readonly bigint[]; }; +type RpcToken = { + tokenAddress: Address; + chainId: AppChainId; + metaDepositTypeHash?: Hex; + metaWithdrawTypeHash?: Hex; + assetAddress?: Address; +}; + type Pool = RpcPool & BalancerApiPool; const chainIdToBalancerChainId: OptionalRecord = { @@ -172,10 +183,6 @@ const runArgsConfig: ArgumentConfig = { }, }; -function isDefined(value: T): value is Exclude { - return value !== undefined && value !== null; -} - function getRunArgs() { return parse(runArgsConfig, { helpArg: 'help', @@ -359,6 +366,37 @@ const fetchPoolRpcData = withFileCache( path.join(cacheRpcPath, chainId, `pool-${poolAddress}.json`) ); +const fetchTokenRpcData = withFileCache( + async (tokenAddress: Address, chainId: AppChainId): Promise => { + const client = getViemClient(chainId); + const [metaDepositTypeHashRes, metaWithdrawTypeHashRes, assetRes] = await Promise.allSettled([ + client.readContract({ + address: tokenAddress, + abi: parseAbi(['function METADEPOSIT_TYPEHASH() public view returns (bytes32)']), + functionName: 'METADEPOSIT_TYPEHASH', + }), + client.readContract({ + address: tokenAddress, + abi: parseAbi(['function METAWITHDRAWAL_TYPEHASH() public view returns (bytes32)']), + functionName: 'METAWITHDRAWAL_TYPEHASH', + }), + client.readContract({ + address: tokenAddress, + abi: parseAbi(['function asset() public view returns (address)']), + functionName: 'asset', + }), + ]); + + const metaDepositTypeHash = fulfilledOr(metaDepositTypeHashRes, undefined); + const metaWithdrawTypeHash = fulfilledOr(metaWithdrawTypeHashRes, undefined); + const assetAddress = fulfilledOr(assetRes, undefined); + + return { tokenAddress, chainId, metaDepositTypeHash, metaWithdrawTypeHash, assetAddress }; + }, + (tokenAddress: Address, chainId: AppChainId) => + path.join(cacheRpcPath, chainId, `token-${tokenAddress}.json`) +); + const balancerApiQueue = new PQueue({ concurrency: 1, interval: 1000, @@ -470,111 +508,48 @@ const getPoolRpcData = createCachedFactory( (_, poolAddress: Address, chainId: AppChainId) => `${chainId}:${poolAddress}` ); -function checkPoolTokensAgainstAddressBook(pool: Pool, allRequired: boolean): boolean { - const { tokenAddressMap } = addressBook[appToAddressBookId(pool.chainId)]; - // Tokens in the pool that are not the pool token - const tokens = pool.poolTokens - .filter(t => t.address !== pool.address && !t.hasNestedPool) - .map(t => { - const abToken = tokenAddressMap[t.address]; - if (abToken) { - return { - poolToken: t, - inAddressBook: true as const, - abToken, - }; - } - - return { - poolToken: t, - inAddressBook: false as const, - }; - }); - - // Tokens in address book - const [zapTokens, missingTokens] = partition( - tokens, - ( - t - ): t is Extract< - typeof t, - { - inAddressBook: true; - } - > => t.inAddressBook - ); - if (!zapTokens.length) { - throw new Error( - `No tokens [${missingTokens.join(', ')}] found in ${pool.chainId} address book.` - ); - } - - const tokenErrors = zapTokens - .map(({ poolToken, abToken }) => { - if (abToken.decimals !== poolToken.decimals) { - return `Address book token decimals mismatch ${poolToken.symbol} (${poolToken.address}) ${poolToken.decimals} vs ${abToken.decimals}`; - } - return undefined; - }) - .filter(isDefined); - if (tokenErrors.length) { - throw new Error(`${pool.type}: Token errors:\n${tokenErrors.join('\n')}`); - } - - if (zapTokens.length !== tokens.length) { - const message = `${pool.type}: Some tokens not found in address book:\n${missingTokens - .map(({ poolToken }) => ` ${poolToken.symbol} (${poolToken.address})`) - .join('\n')}`; - - if (allRequired) { - throw new Error(message); - } else { - console.warn(message); - } - } - - return true; -} - type PoolToken = BalancerPoolToken
& { abToken?: Token; price?: number; swapProviders: string[]; + rpcToken: RpcToken; isBPT: boolean; }; -function getPoolTokens(pool: Pool, prices: TokenPrices, swaps: ZapSwaps) { +async function getPoolTokens( + pool: Pool, + prices: TokenPrices, + swaps: ZapSwaps, + forceUpdate: boolean = false +) { const { tokenAddressMap } = addressBook[appToAddressBookId(pool.chainId)]; - const tokens: PoolToken[] = []; - const tokensWithBpt: PoolToken[] = []; - - for (const poolToken of pool.poolTokens) { - const isBPT = poolToken.address === pool.address; - const abToken = tokenAddressMap[poolToken.address]; - if (abToken && abToken.decimals !== poolToken.decimals) { - throw new Error( - `Address book token decimals mismatch ${poolToken.symbol} (${poolToken.address}) ${poolToken.decimals} vs ${abToken.decimals}` - ); - } - const price = abToken ? prices[abToken.oracleId] : undefined; - const swapProviders = Object.entries(swaps[pool.chainId]?.[poolToken.address] || {}) - .filter(([, v]) => v) - .map(([k]) => k); - const token = { - ...poolToken, - isBPT, - abToken, - price, - swapProviders, - }; - if (!isBPT) { - tokens.push(token); - } - tokensWithBpt.push(token); - } + const tokensWithBpt = await Promise.all( + pool.poolTokens.map(async poolToken => { + const isBPT = poolToken.address === pool.address; + const abToken = tokenAddressMap[poolToken.address]; + if (abToken && abToken.decimals !== poolToken.decimals) { + throw new Error( + `Address book token decimals mismatch ${poolToken.symbol} (${poolToken.address}) ${poolToken.decimals} vs ${abToken.decimals}` + ); + } + const rpcToken = await fetchTokenRpcData(forceUpdate, poolToken.address, pool.chainId); + const price = abToken ? prices[abToken.oracleId] : undefined; + const swapProviders = Object.entries(swaps[pool.chainId]?.[poolToken.address] || {}) + .filter(([, v]) => v) + .map(([k]) => k); + return { + ...poolToken, + isBPT, + abToken, + price, + swapProviders, + rpcToken, + } satisfies PoolToken; + }) + ); - return { tokens, tokensWithBpt }; + return { tokensWithBpt, tokens: tokensWithBpt.filter(t => !t.isBPT) }; } function logTokens(tokens: PoolToken[]) { @@ -750,6 +725,16 @@ async function findAmmForPool(pool: Pool, tokenProviderId: string): Promise t.address), bptIndex: tokensWithBpt.findIndex(t => t.isBPT), hasNestedPool: tokens.some(t => t.hasNestedPool), + ...swapConfig, } satisfies BalancerStrategyConfig; } case 'GYRO': @@ -845,6 +839,7 @@ export async function discoverBalancerZap(args: RunArgs) { poolId: apiPool.id, poolType: transformPoolType(type), tokens: apiPool.poolTokens.map(t => t.address), + ...swapConfig, } satisfies BalancerStrategyConfig; } default: { diff --git a/src/config/vault/arbitrum.json b/src/config/vault/arbitrum.json index 535dddff9..a2912ab37 100644 --- a/src/config/vault/arbitrum.json +++ b/src/config/vault/arbitrum.json @@ -153,7 +153,10 @@ "tokens": [ "0x7CFaDFD5645B50bE87d546f42699d863648251ad", "0xd3443ee1e91aF28e5FB858Fbd0D72A63bA8046E0" - ] + ], + "swap": { + "blockProviders": ["kyber", "one-inch"] + } } ] }, @@ -227,7 +230,22 @@ "strategyTypeId": "lp", "addLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xff38cc0ce0de4476c5a3e78675b48420a851035b000200000000000000000593/add-liquidity", "removeLiquidityUrl": "https://balancer.fi/pools/arbitrum/v2/0xff38cc0ce0de4476c5a3e78675b48420a851035b000200000000000000000593/remove-liquidity", - "network": "arbitrum" + "network": "arbitrum", + "zaps": [ + { + "strategyId": "balancer", + "ammId": "arbitrum-balancer", + "poolId": "0xff38cc0ce0de4476c5a3e78675b48420a851035b000200000000000000000593", + "poolType": "gyroe", + "tokens": [ + "0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8", + "0xD9FBA68D89178e3538e708939332c79efC540179" + ], + "swap": { + "blockProviders": ["kyber", "one-inch"] + } + } + ] }, { "id": "aura-arb-ausdc-wusdm", @@ -1750,7 +1768,10 @@ "tokens": [ "0x7CFaDFD5645B50bE87d546f42699d863648251ad", "0x7dfF72693f6A4149b17e7C6314655f6A9F7c8B33" - ] + ], + "swap": { + "blockProviders": ["kyber", "one-inch"] + } } ] }, @@ -1792,7 +1813,10 @@ "tokens": [ "0xb165a74407fE1e519d6bCbDeC1Ed3202B35a4140", "0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8" - ] + ], + "swap": { + "blockProviders": ["kyber", "one-inch"] + } } ] }, @@ -1834,7 +1858,10 @@ "tokens": [ "0x7CFaDFD5645B50bE87d546f42699d863648251ad", "0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8" - ] + ], + "swap": { + "blockProviders": ["kyber", "one-inch"] + } } ] }, @@ -5833,7 +5860,10 @@ "tokens": [ "0xb165a74407fE1e519d6bCbDeC1Ed3202B35a4140", "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9" - ] + ], + "swap": { + "blockProviders": ["kyber", "one-inch"] + } } ] }, @@ -5875,7 +5905,10 @@ "tokens": [ "0x7CFaDFD5645B50bE87d546f42699d863648251ad", "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" - ] + ], + "swap": { + "blockProviders": ["kyber", "one-inch"] + } } ] }, @@ -11864,7 +11897,10 @@ "tokens": [ "0x7CFaDFD5645B50bE87d546f42699d863648251ad", "0xb165a74407fE1e519d6bCbDeC1Ed3202B35a4140" - ] + ], + "swap": { + "blockProviders": ["kyber", "one-inch"] + } } ] }, diff --git a/src/features/data/apis/amm/balancer/composable-stable/ComposableStablePool.ts b/src/features/data/apis/amm/balancer/composable-stable/ComposableStablePool.ts index aab2ff1f4..373d02d9c 100644 --- a/src/features/data/apis/amm/balancer/composable-stable/ComposableStablePool.ts +++ b/src/features/data/apis/amm/balancer/composable-stable/ComposableStablePool.ts @@ -116,109 +116,6 @@ export class ComposableStablePool return response; } - /*async quoteRemoveLiquidityOneToken(liquidityIn: BigNumber, tokenOut: string): Promise { - this.checkAmount(liquidityIn, 'liquidityIn'); - this.checkToken(tokenOut, 'tokenOut'); - - const request: QueryBatchSwapRequest = { - kind: SwapKind.GIVEN_IN, - swaps: [ - { - poolId: this.config.poolId, - assetInIndex: 0, - assetOutIndex: 1, - amount: liquidityIn, - userData: '0x', - }, - ], - assets: [this.config.poolAddress, tokenOut], - }; - - const vault = this.getVault(); - const [vaultInputDelta, vaultOutputDelta] = await vault.queryBatchSwap(request); - - if (!vaultInputDelta.eq(liquidityIn)) { - throw new Error('Not all input used'); - } - - if (vaultOutputDelta.gte(BIG_ZERO)) { - throw new Error('Output is negative'); - } - - return vaultOutputDelta.abs(); - }*/ - - /*async getAddLiquidityOneTokenZap( - amountIn: BigNumber, - tokenIn: string, - liquidityOutMin: BigNumber, - from: string, - insertBalance: boolean - ): Promise { - this.checkToken(tokenIn, 'tokenIn'); - this.checkAmount(amountIn, 'amountIn'); - this.checkAmount(liquidityOutMin, 'liquidityOutMin'); - - const vault = this.getVault(); - return vault.getSwapZap({ - swap: { - singleSwap: { - poolId: this.config.poolId, - kind: SwapKind.GIVEN_IN, - assetIn: tokenIn, - assetOut: this.config.poolAddress, - amount: amountIn, - userData: '0x', - }, - funds: { - sender: from, - fromInternalBalance: false, - recipient: from, - toInternalBalance: false, - }, - limit: liquidityOutMin, - deadline: getUnixNow() + THIRTY_MINUTES_IN_SECONDS, - }, - insertBalance, - }); - }*/ - - /*async getRemoveLiquidityOneTokenZap( - liquidityIn: BigNumber, - tokenOut: string, - amountOutMin: BigNumber, - from: string, - insertBalance: boolean, - deadlineSeconds: number = THIRTY_MINUTES_IN_SECONDS - ): Promise { - this.checkToken(tokenOut, 'tokenOut'); - this.checkAmount(liquidityIn, 'liquidityIn'); - this.checkAmount(amountOutMin, 'amountOutMin'); - - const vault = this.getVault(); - return vault.getSwapZap({ - swap: { - singleSwap: { - poolId: this.config.poolId, - kind: SwapKind.GIVEN_IN, - assetIn: this.config.poolAddress, - assetOut: tokenOut, - amount: liquidityIn, - userData: '0x', - }, - funds: { - sender: from, - fromInternalBalance: false, - recipient: from, - toInternalBalance: false, - }, - limit: amountOutMin, - deadline: getUnixNow() + deadlineSeconds, - }, - insertBalance, - }); - }*/ - protected dropBptIndex(amounts: T[]): T[] { return amounts.filter((_, i) => i !== this.config.bptIndex); } @@ -229,24 +126,6 @@ export class ComposableStablePool return result; } - /* protected checkAmount(amount: BigNumber, label: string = 'amount') { - if (amount.lte(BIG_ZERO)) { - throw new Error(`${label} must be greater than 0`); - } - - if ((amount.decimalPlaces() || 0) > 0) { - throw new Error(`${label} must be in wei`); - } - } - - protected checkToken(tokenAddress: string, label: string = 'token'): number { - const index = this.config.tokens.findIndex(t => t.address === tokenAddress); - if (index === -1) { - throw new Error(`${label} must be a pool token`); - } - return index; - }*/ - /** * For composable stable pools, the scaling factors include the token rate too */ diff --git a/src/features/data/apis/amm/balancer/gyro/GyroPool.ts b/src/features/data/apis/amm/balancer/gyro/GyroPool.ts index 8cd8f2f18..fdd48051a 100644 --- a/src/features/data/apis/amm/balancer/gyro/GyroPool.ts +++ b/src/features/data/apis/amm/balancer/gyro/GyroPool.ts @@ -57,25 +57,6 @@ export class GyroPool extends AllPool implements IBalancerAllPool { return value; } - // async getSwapRatios(): Promise { - // const balances = await this.getBalances(); - // const rates = await this.getTokenRates(); - // const totalSupply = await this.getTotalSupply(); - // if (balances.length !== this.config.tokens.length || rates.length !== this.config.tokens.length) { - // throw new Error('Invalid tokens / rates'); - // } - // - // const amount0 = balances[0].shiftedBy(18).dividedToIntegerBy(totalSupply); - // const amount1 = balances[1].shiftedBy(18).dividedToIntegerBy(totalSupply); - // const ratio = rates[0] - // .shiftedBy(18) - // .dividedToIntegerBy(rates[1]) - // .multipliedBy(amount1) - // .dividedToIntegerBy(amount0); - // - // return BIG_ONE.shiftedBy(18).dividedBy(ratio.plus(BIG_ONE.shiftedBy(18))); - // } - /** * The ratio of balances[n] * scaling factor[n] * token rate[n] over their sum */ diff --git a/src/features/data/apis/transact/strategies/strategy-configs.ts b/src/features/data/apis/transact/strategies/strategy-configs.ts index e2ae4bd6c..22e0374a8 100644 --- a/src/features/data/apis/transact/strategies/strategy-configs.ts +++ b/src/features/data/apis/transact/strategies/strategy-configs.ts @@ -10,8 +10,8 @@ import type { CurveMethod } from './curve/types'; export type SwapAggregatorId = 'one-inch' | 'kyber' | 'odos'; export type StrategySwapConfig = { - blockProviders: SwapAggregatorId[]; - blockTokens: string[]; + blockProviders?: SwapAggregatorId[]; + blockTokens?: string[]; }; export type OptionalStrategySwapConfig = { From 3b5e1d0869f4999650374d23c714ad723154b10b Mon Sep 17 00:00:00 2001 From: ReflectiveChimp <55021052+ReflectiveChimp@users.noreply.github.com> Date: Tue, 8 Oct 2024 11:57:36 +0100 Subject: [PATCH 16/20] fix blockProviders --- .../apis/transact/strategies/UniswapLikeStrategy.ts | 11 +++++++---- .../transact/strategies/balancer/BalancerStrategy.ts | 8 +++++--- .../apis/transact/strategies/conic/ConicStrategy.ts | 3 ++- .../strategies/cowcentrated/CowcentratedStrategy.ts | 5 +++-- .../apis/transact/strategies/curve/CurveStrategy.ts | 6 ++++-- .../apis/transact/strategies/gamma/GammaStrategy.ts | 5 +++-- .../apis/transact/strategies/single/SingleStrategy.ts | 9 ++++++--- .../data/apis/transact/swap/SwapAggregator.ts | 2 +- 8 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/features/data/apis/transact/strategies/UniswapLikeStrategy.ts b/src/features/data/apis/transact/strategies/UniswapLikeStrategy.ts index f45604208..f9e002a63 100644 --- a/src/features/data/apis/transact/strategies/UniswapLikeStrategy.ts +++ b/src/features/data/apis/transact/strategies/UniswapLikeStrategy.ts @@ -309,7 +309,8 @@ export abstract class UniswapLikeStrategy< toToken: this.wnative, vaultId: this.vault.id, }, - state + state, + this.options.swap ); const wrapQuote = first(wrapQuotes); if (!wrapQuote) { @@ -428,7 +429,7 @@ export abstract class UniswapLikeStrategy< return undefined; } - return await swapAggregator.fetchQuotes(quoteRequest, state); + return await swapAggregator.fetchQuotes(quoteRequest, state, this.options.swap); }) ); @@ -1045,7 +1046,8 @@ export abstract class UniswapLikeStrategy< toToken: this.native, vaultId: this.vault.id, }, - state + state, + this.options.swap ); const unwrapQuote = first(unwrapQuotes); if (!unwrapQuote || unwrapQuote.toAmount.lt(outputAmount)) { @@ -1099,7 +1101,8 @@ export abstract class UniswapLikeStrategy< toToken: wantedOutput, vaultId: option.vaultId, }, - state + state, + this.options.swap ); if (!quotes || !quotes.length) { diff --git a/src/features/data/apis/transact/strategies/balancer/BalancerStrategy.ts b/src/features/data/apis/transact/strategies/balancer/BalancerStrategy.ts index c240c2fc1..d6287239c 100644 --- a/src/features/data/apis/transact/strategies/balancer/BalancerStrategy.ts +++ b/src/features/data/apis/transact/strategies/balancer/BalancerStrategy.ts @@ -597,7 +597,7 @@ class BalancerStrategyImpl implements IZapStrategy { return undefined; } - return await swapAggregator.fetchQuotes(quoteRequest, state); + return await swapAggregator.fetchQuotes(quoteRequest, state, this.options.swap); }) ); @@ -661,7 +661,8 @@ class BalancerStrategyImpl implements IZapStrategy { fromAmount: input.amount, toToken: depositVia, }, - state + state, + this.options.swap ); const bestQuote = first(quotes); if (!bestQuote) { @@ -1150,7 +1151,8 @@ class BalancerStrategyImpl implements IZapStrategy { fromAmount: minOutputAmount, toToken: wantedOutput, }, - state + state, + this.options.swap ); const bestQuote = first(quotes); if (!bestQuote) { diff --git a/src/features/data/apis/transact/strategies/conic/ConicStrategy.ts b/src/features/data/apis/transact/strategies/conic/ConicStrategy.ts index 950d87700..e276fcc51 100644 --- a/src/features/data/apis/transact/strategies/conic/ConicStrategy.ts +++ b/src/features/data/apis/transact/strategies/conic/ConicStrategy.ts @@ -393,7 +393,8 @@ class ConicStrategyImp implements IZapStrategy { toToken: desiredToken, vaultId: this.vault.id, }, - state + state, + this.options.swap ); const unwrapQuote = first(unwrapQuotes); if (!unwrapQuote || unwrapQuote.toAmount.lt(swapAmountOut)) { diff --git a/src/features/data/apis/transact/strategies/cowcentrated/CowcentratedStrategy.ts b/src/features/data/apis/transact/strategies/cowcentrated/CowcentratedStrategy.ts index 78dbff170..16585f2e3 100644 --- a/src/features/data/apis/transact/strategies/cowcentrated/CowcentratedStrategy.ts +++ b/src/features/data/apis/transact/strategies/cowcentrated/CowcentratedStrategy.ts @@ -603,7 +603,7 @@ class CowcentratedStrategyImpl implements IComposableStrategy { return undefined; } - return await swapAggregator.fetchQuotes(quoteRequest, state); + return await swapAggregator.fetchQuotes(quoteRequest, state, this.options.swap); }) ); const quotePerLpToken = quotesPerLpToken.map((quotes, i) => { @@ -743,7 +743,8 @@ class CowcentratedStrategyImpl implements IComposableStrategy { toToken: wantedOutput, vaultId: option.vaultId, }, - state + state, + this.options.swap ); if (!quotes || !quotes.length) { diff --git a/src/features/data/apis/transact/strategies/curve/CurveStrategy.ts b/src/features/data/apis/transact/strategies/curve/CurveStrategy.ts index 566082d80..0e68e185e 100644 --- a/src/features/data/apis/transact/strategies/curve/CurveStrategy.ts +++ b/src/features/data/apis/transact/strategies/curve/CurveStrategy.ts @@ -282,7 +282,8 @@ class CurveStrategyImpl implements IZapStrategy { fromAmount: input.amount, toToken: depositVia.token, }, - state + state, + this.options.swap ); const bestQuote = first(quotes); if (!bestQuote) { @@ -713,7 +714,8 @@ class CurveStrategyImpl implements IZapStrategy { fromAmount: slipBy(split.amount, slippage, split.token.decimals), // we have to assume it will slip 100% since we can't modify the call data later toToken: wanted, }, - state + state, + this.options.swap ); const quote = first(quotes); diff --git a/src/features/data/apis/transact/strategies/gamma/GammaStrategy.ts b/src/features/data/apis/transact/strategies/gamma/GammaStrategy.ts index 570f95762..18ee2ca7a 100644 --- a/src/features/data/apis/transact/strategies/gamma/GammaStrategy.ts +++ b/src/features/data/apis/transact/strategies/gamma/GammaStrategy.ts @@ -299,7 +299,7 @@ class GammaStrategyImpl implements IZapStrategy { return undefined; } - return await swapAggregator.fetchQuotes(quoteRequest, state); + return await swapAggregator.fetchQuotes(quoteRequest, state, this.options.swap); }) ); @@ -784,7 +784,8 @@ class GammaStrategyImpl implements IZapStrategy { toToken: wantedOutput, vaultId: option.vaultId, }, - state + state, + this.options.swap ); if (!quotes || !quotes.length) { diff --git a/src/features/data/apis/transact/strategies/single/SingleStrategy.ts b/src/features/data/apis/transact/strategies/single/SingleStrategy.ts index 5c11f1126..cb09e00eb 100644 --- a/src/features/data/apis/transact/strategies/single/SingleStrategy.ts +++ b/src/features/data/apis/transact/strategies/single/SingleStrategy.ts @@ -182,7 +182,8 @@ class SingleStrategyImpl implements IComposableStrategy { fromAmount: input.amount, toToken: this.vaultType.depositToken, }, - state + state, + this.options.swap ); const bestQuote = first(swapQuotes); // already sorted by toAmount if (!bestQuote) { @@ -397,7 +398,8 @@ class SingleStrategyImpl implements IComposableStrategy { toToken: this.wnative, vaultId: this.vault.id, }, - state + state, + this.options.swap ); const wrapQuote = first(wrapQuotes); if (!wrapQuote || wrapQuote.toAmount.lt(withdrawnAmountAfterFee)) { @@ -432,7 +434,8 @@ class SingleStrategyImpl implements IComposableStrategy { fromAmount: swapInputAmount, toToken: swapOutputToken, }, - state + state, + this.options.swap ); const bestQuote = first(swapQuotes); // already sorted by toAmount if (!bestQuote) { diff --git a/src/features/data/apis/transact/swap/SwapAggregator.ts b/src/features/data/apis/transact/swap/SwapAggregator.ts index f4a873a60..c19595728 100644 --- a/src/features/data/apis/transact/swap/SwapAggregator.ts +++ b/src/features/data/apis/transact/swap/SwapAggregator.ts @@ -191,7 +191,7 @@ export class SwapAggregator implements ISwapAggregator { ) ) ); - const providers = this.providers.filter((_, i) => providerSupported[i]); + const providers = allowedProviders.filter((_, i) => providerSupported[i]); if (providers.length === 0) { throw new Error( From 1217c89d9acbd886c7d21a837a206e36f348c6a7 Mon Sep 17 00:00:00 2001 From: ReflectiveChimp <55021052+ReflectiveChimp@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:06:06 +0100 Subject: [PATCH 17/20] rpc bump From ec130fb51fec589cd4f74147e2e1b7e691f1c4dc Mon Sep 17 00:00:00 2001 From: ReflectiveChimp <55021052+ReflectiveChimp@users.noreply.github.com> Date: Tue, 8 Oct 2024 13:02:54 +0100 Subject: [PATCH 18/20] rpc bump 2 From 50f43def9c543f1dbdf44c1cd70785df67095134 Mon Sep 17 00:00:00 2001 From: ReflectiveChimp <55021052+ReflectiveChimp@users.noreply.github.com> Date: Wed, 9 Oct 2024 12:16:36 +0100 Subject: [PATCH 19/20] break lp button when > 2 tokens --- .../TokenSelectButton/TokenSelectButton.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/features/vault/components/Actions/Transact/TokenSelectButton/TokenSelectButton.tsx b/src/features/vault/components/Actions/Transact/TokenSelectButton/TokenSelectButton.tsx index 10bc799e2..71f79c576 100644 --- a/src/features/vault/components/Actions/Transact/TokenSelectButton/TokenSelectButton.tsx +++ b/src/features/vault/components/Actions/Transact/TokenSelectButton/TokenSelectButton.tsx @@ -18,6 +18,7 @@ import zapIcon from '../../../../../../images/icons/zap.svg'; import { useTranslation } from 'react-i18next'; import { selectVaultById } from '../../../../../data/selectors/vaults'; import type { TokenEntity } from '../../../../../data/entities/token'; +import { AssetsImage } from '../../../../../../components/AssetsImage'; const useStyles = makeStyles(styles); @@ -91,13 +92,20 @@ export const TokenSelectButton = memo(function TokenSele const BreakLp = memo(function BreakLp({ tokens }: { tokens: TokenEntity[] }) { const classes = useStyles(); - const token0 = tokens[0]; - const token1 = tokens[1]; + if (tokens.length === 2) { + const [token0, token1] = tokens; + return ( +
+ + + + +
+ ); + } + return (
- - + - + t.symbol)} chainId={tokens[0].chainId} size={16} />
); }); From 6ece91b5b010b715d84b39e34f50f94a8bbab026 Mon Sep 17 00:00:00 2001 From: ReflectiveChimp <55021052+ReflectiveChimp@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:14:45 +0100 Subject: [PATCH 20/20] getActualSupply is required for GYRO pools --- scripts/addBalancerZap.ts | 103 ++++++++++++------ src/config/vault/optimism.json | 14 +-- src/config/vault/polygon.json | 14 +-- .../apis/amm/balancer/common/CommonPool.ts | 9 +- .../data/apis/amm/balancer/gyro/GyroPool.ts | 12 +- 5 files changed, 85 insertions(+), 67 deletions(-) diff --git a/scripts/addBalancerZap.ts b/scripts/addBalancerZap.ts index 1c57bb3f7..d7ccb2746 100644 --- a/scripts/addBalancerZap.ts +++ b/scripts/addBalancerZap.ts @@ -92,6 +92,8 @@ type RpcPool = { tokenRates?: readonly [bigint, bigint]; normalizedWeights?: readonly bigint[]; scalingFactors?: readonly bigint[]; + actualSupply?: bigint; + totalSupply: bigint; }; type RpcToken = { @@ -256,8 +258,8 @@ function createViemClient(chainId: AppChainId, chain: ChainConfig) { name: chain.name, nativeCurrency: { decimals: 18, - name: chain.walletSettings.nativeCurrency.name, - symbol: chain.walletSettings.nativeCurrency.symbol, + name: chain.native.symbol, + symbol: chain.native.symbol, }, rpcUrls: { public: { http: [chainRpcs[chainId]] }, @@ -318,40 +320,59 @@ function fulfilledOr( const fetchPoolRpcData = withFileCache( async (poolAddress: Address, chainId: AppChainId): Promise => { const client = getViemClient(chainId); - const [poolIdRes, vaultAddressRes, tokenRatesRes, normalizedWeightsRes, scalingFactorsRes] = - await Promise.allSettled([ - client.readContract({ - address: poolAddress, - abi: parseAbi(['function getPoolId() public view returns (bytes32)']), - functionName: 'getPoolId', - }), - client.readContract({ - address: poolAddress, - abi: parseAbi(['function getVault() public view returns (address)']), - functionName: 'getVault', - }), - client.readContract({ - address: poolAddress, - abi: parseAbi(['function getTokenRates() public view returns (uint256,uint256)']), - functionName: 'getTokenRates', - }), - client.readContract({ - address: poolAddress, - abi: parseAbi(['function getNormalizedWeights() public view returns (uint256[])']), - functionName: 'getNormalizedWeights', - }), - client.readContract({ - address: poolAddress, - abi: parseAbi(['function getScalingFactors() public view returns (uint256[])']), - functionName: 'getScalingFactors', - }), - ]); + const [ + poolIdRes, + vaultAddressRes, + tokenRatesRes, + normalizedWeightsRes, + scalingFactorsRes, + actualSupplyRes, + totalSupplyRes, + ] = await Promise.allSettled([ + client.readContract({ + address: poolAddress, + abi: parseAbi(['function getPoolId() public view returns (bytes32)']), + functionName: 'getPoolId', + }), + client.readContract({ + address: poolAddress, + abi: parseAbi(['function getVault() public view returns (address)']), + functionName: 'getVault', + }), + client.readContract({ + address: poolAddress, + abi: parseAbi(['function getTokenRates() public view returns (uint256,uint256)']), + functionName: 'getTokenRates', + }), + client.readContract({ + address: poolAddress, + abi: parseAbi(['function getNormalizedWeights() public view returns (uint256[])']), + functionName: 'getNormalizedWeights', + }), + client.readContract({ + address: poolAddress, + abi: parseAbi(['function getScalingFactors() public view returns (uint256[])']), + functionName: 'getScalingFactors', + }), + client.readContract({ + address: poolAddress, + abi: parseAbi(['function getActualSupply() public view returns (uint256)']), + functionName: 'getActualSupply', + }), + client.readContract({ + address: poolAddress, + abi: parseAbi(['function totalSupply() public view returns (uint256)']), + functionName: 'totalSupply', + }), + ]); const vaultAddress = fulfilledOr(vaultAddressRes, undefined); const poolId = fulfilledOr(poolIdRes, undefined); const tokenRates = fulfilledOr(tokenRatesRes, undefined); const normalizedWeights = fulfilledOr(normalizedWeightsRes, undefined); const scalingFactors = fulfilledOr(scalingFactorsRes, undefined); + const actualSupply = fulfilledOr(actualSupplyRes, undefined); + const totalSupply = fulfilledOr(totalSupplyRes, undefined); if (!vaultAddress || vaultAddress === ZERO_ADDRESS) { throw new Error(`No vault address found via vault.want().getVault()`); @@ -359,8 +380,20 @@ const fetchPoolRpcData = withFileCache( if (!poolId || poolId === ZERO_BYTES32) { throw new Error(`No pool id found via vault.want().getPoolId()`); } + if (!totalSupply) { + throw new Error(`No total supply found via vault.want().totalSupply()`); + } - return { poolId, vaultAddress, chainId, tokenRates, normalizedWeights, scalingFactors }; + return { + poolId, + vaultAddress, + chainId, + tokenRates, + normalizedWeights, + scalingFactors, + totalSupply, + actualSupply, + }; }, (poolAddress: Address, chainId: AppChainId) => path.join(cacheRpcPath, chainId, `pool-${poolAddress}.json`) @@ -622,6 +655,10 @@ function checkGyroPool(pool: Pool, tokens: PoolToken[], tokensWithBpt: PoolToken ); } + if (!pool.actualSupply) { + throw new Error(`${pool.type}: Must have getActualSupply()`); + } + return true; } @@ -680,6 +717,10 @@ function checkComposableStablePool( ); } + if (!pool.actualSupply) { + throw new Error(`${pool.type}: Must have getActualSupply()`); + } + return true; } diff --git a/src/config/vault/optimism.json b/src/config/vault/optimism.json index 85da65cd9..54e5ff02e 100644 --- a/src/config/vault/optimism.json +++ b/src/config/vault/optimism.json @@ -6262,19 +6262,7 @@ "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://op.beets.fi/pool/0x7ca75bdea9dede97f8b13c6641b768650cb837820002000000000000000000d5", "removeLiquidityUrl": "https://op.beets.fi/pool/0x7ca75bdea9dede97f8b13c6641b768650cb837820002000000000000000000d5", - "network": "optimism", - "zaps": [ - { - "strategyId": "balancer", - "ammId": "optimism-beethovenx", - "poolId": "0x7ca75bdea9dede97f8b13c6641b768650cb837820002000000000000000000d5", - "poolType": "gyroe", - "tokens": [ - "0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb", - "0x4200000000000000000000000000000000000006" - ] - } - ] + "network": "optimism" }, { "id": "uniswap-gamma-weth-op-narrow", diff --git a/src/config/vault/polygon.json b/src/config/vault/polygon.json index c3f51a22e..4ba4f918b 100644 --- a/src/config/vault/polygon.json +++ b/src/config/vault/polygon.json @@ -2348,19 +2348,7 @@ "strategyTypeId": "multi-lp-locked", "addLiquidityUrl": "https://app.gyro.finance/pools/polygon/e-clp/0xf0ad209e2e969eaaa8c882aac71f02d8a047d5c2/", "removeLiquidityUrl": "https://app.gyro.finance/pools/polygon/e-clp/0xf0ad209e2e969eaaa8c882aac71f02d8a047d5c2/", - "network": "polygon", - "zaps": [ - { - "strategyId": "balancer", - "ammId": "polygon-balancer", - "poolId": "0xf0ad209e2e969eaaa8c882aac71f02d8a047d5c2000200000000000000000b49", - "poolType": "gyroe", - "tokens": [ - "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", - "0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4" - ] - } - ] + "network": "polygon" }, { "id": "aura-polygon-gyro-matic-maticx", diff --git a/src/features/data/apis/amm/balancer/common/CommonPool.ts b/src/features/data/apis/amm/balancer/common/CommonPool.ts index a77a7b4a8..f0a8f3430 100644 --- a/src/features/data/apis/amm/balancer/common/CommonPool.ts +++ b/src/features/data/apis/amm/balancer/common/CommonPool.ts @@ -6,7 +6,7 @@ import { getWeb3Instance } from '../../../instances'; import { Vault } from '../vault/Vault'; import { checkAddressOrder } from '../../../../../../helpers/tokens'; import type { Contract } from 'web3-eth-contract'; -import BigNumber from 'bignumber.js'; +import type BigNumber from 'bignumber.js'; import { FixedPoint } from './FixedPoint'; export abstract class CommonPool implements IBalancerPool { @@ -23,7 +23,6 @@ export abstract class CommonPool implements IBalancerPool { this.getPoolTokens = this.cacheMethod(this.getPoolTokens); this.getBalances = this.cacheMethod(this.getBalances); this.getUpscaledBalances = this.cacheMethod(this.getUpscaledBalances); - this.getTotalSupply = this.cacheMethod(this.getTotalSupply); this.getWeb3 = this.cacheMethod(this.getWeb3); this.getPoolContract = this.cacheMethod(this.getPoolContract); this.getVault = this.cacheMethod(this.getVault); @@ -75,12 +74,6 @@ export abstract class CommonPool implements IBalancerPool { return await this.upscaleAmounts(await this.getBalances()); } - protected async getTotalSupply(): Promise { - const pool = await this.getPoolContract(); - const totalSupply: string = await pool.methods.getActualSupply().call(); - return new BigNumber(totalSupply); - } - protected async getWeb3() { return getWeb3Instance(this.chain); } diff --git a/src/features/data/apis/amm/balancer/gyro/GyroPool.ts b/src/features/data/apis/amm/balancer/gyro/GyroPool.ts index fdd48051a..88d7f15ce 100644 --- a/src/features/data/apis/amm/balancer/gyro/GyroPool.ts +++ b/src/features/data/apis/amm/balancer/gyro/GyroPool.ts @@ -35,6 +35,7 @@ export class GyroPool extends AllPool implements IBalancerAllPool { super(chain, vaultConfig, config); this.getTokenRates = this.cacheMethod(this.getTokenRates); + this.getActualSupply = this.cacheMethod(this.getActualSupply); } supportsFeature(feature: BalancerFeature): boolean { @@ -109,9 +110,10 @@ export class GyroPool extends AllPool implements IBalancerAllPool { } const vault = this.getVault(); - const totalSupply = await this.getTotalSupply(); + // We must call getActualSupply instead of totalSupply to get the real supply after fees have been collected + const totalSupply = await this.getActualSupply(); if (totalSupply.isZero()) { - throw new Error('Total supply is zero'); + throw new Error('Actual supply is zero'); } // all on-chain calculations are done with upscaled balances in FixedPoint (18 decimals) @@ -215,4 +217,10 @@ export class GyroPool extends AllPool implements IBalancerAllPool { const web3 = await this.getWeb3(); return new web3.eth.Contract(viemToWeb3Abi(BalancerGyroEPoolAbi), this.config.poolAddress); } + + protected async getActualSupply(): Promise { + const pool = await this.getPoolContract(); + const totalSupply: string = await pool.methods.getActualSupply().call(); + return new BigNumber(totalSupply); + } }