From 5af01705b20cfebcd4df8dc4a9aed2a2d06e5900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pr=C3=A9vost?= <998369+prevostc@users.noreply.github.com> Date: Sat, 3 Aug 2024 20:08:39 +0200 Subject: [PATCH] Add support for clm vaults --- adapters/beefy/hourly_blocks.csv | 3 +- adapters/beefy/src/config.ts | 4 +- .../src/sdk/breakdown/getVaultBreakdown.ts | 8 +- .../sdk/breakdown/protocol_type/beefy_clm.ts | 41 ++- .../src/sdk/vault/getBeefyVaultConfig.ts | 279 ++++++++++++++---- 5 files changed, 272 insertions(+), 63 deletions(-) diff --git a/adapters/beefy/hourly_blocks.csv b/adapters/beefy/hourly_blocks.csv index 6e72e98b..7020d1bb 100644 --- a/adapters/beefy/hourly_blocks.csv +++ b/adapters/beefy/hourly_blocks.csv @@ -11,4 +11,5 @@ number,timestamp 4784763,1716398064 5684890,1718771519 5784890,1718972096 -6301186,1720005441 \ No newline at end of file +6301186,1720005441 +7643069,1722689579 \ No newline at end of file diff --git a/adapters/beefy/src/config.ts b/adapters/beefy/src/config.ts index f9c6a92c..610c4ce8 100644 --- a/adapters/beefy/src/config.ts +++ b/adapters/beefy/src/config.ts @@ -1,5 +1,7 @@ -export const BEEFY_VAULT_API = "https://api.beefy.finance/harvestable-vaults"; +export const BEEFY_MOO_VAULT_API = "https://api.beefy.finance/vaults"; +export const BEEFY_COW_VAULT_API = "https://api.beefy.finance/cow-vaults"; export const BEEFY_GOV_API = "https://api.beefy.finance/gov-vaults"; +export const BEEFY_BOOST_API = "https://api.beefy.finance/boosts"; // subgraph source: https://github.com/beefyfinance/l2-lxp-liquidity-subgraph export const BEEFY_SUBGRAPH_URL = diff --git a/adapters/beefy/src/sdk/breakdown/getVaultBreakdown.ts b/adapters/beefy/src/sdk/breakdown/getVaultBreakdown.ts index bbc55697..f914cc40 100644 --- a/adapters/beefy/src/sdk/breakdown/getVaultBreakdown.ts +++ b/adapters/beefy/src/sdk/breakdown/getVaultBreakdown.ts @@ -4,7 +4,10 @@ import { BeefyViemClient, getViemClient } from "../viemClient"; import { getSolidlyVaultBreakdown } from "./protocol_type/solidly"; import { getGammaVaultBreakdown } from "./protocol_type/gamma"; import { getMendiVaultBreakdown } from "./protocol_type/mendi"; -import { getBeefyClmVaultBreakdown } from "./protocol_type/beefy_clm"; +import { + getBeefyClmManagerBreakdown, + getBeefyClmVaultBreakdown, +} from "./protocol_type/beefy_clm"; type BreakdownMethod = ( client: BeefyViemClient, @@ -17,7 +20,8 @@ const breakdownMethods: Record = { mendi: getMendiVaultBreakdown, gamma: getGammaVaultBreakdown, ichi: getGammaVaultBreakdown, - beefy_clm: getBeefyClmVaultBreakdown, + beefy_clm: getBeefyClmManagerBreakdown, + beefy_clm_vault: getBeefyClmVaultBreakdown, }; export const getVaultBreakdowns = async ( diff --git a/adapters/beefy/src/sdk/breakdown/protocol_type/beefy_clm.ts b/adapters/beefy/src/sdk/breakdown/protocol_type/beefy_clm.ts index 17323df7..f465d675 100644 --- a/adapters/beefy/src/sdk/breakdown/protocol_type/beefy_clm.ts +++ b/adapters/beefy/src/sdk/breakdown/protocol_type/beefy_clm.ts @@ -4,8 +4,9 @@ import { BeefyViemClient } from "../../viemClient"; import { BeefyVaultBreakdown } from "../types"; import { BeefyVaultConcLiqAbi } from "../../../abi/BeefyVaultConcLiq"; import { BeefyClmStrategyAbi } from "../../../abi/BeefyClmStrategy"; +import { BeefyVaultV7Abi } from "../../../abi/BeefyVaultV7Abi"; -export const getBeefyClmVaultBreakdown = async ( +export const getBeefyClmManagerBreakdown = async ( client: BeefyViemClient, blockNumber: bigint, vault: BeefyVault @@ -50,3 +51,41 @@ export const getBeefyClmVaultBreakdown = async ( ], }; }; + +export const getBeefyClmVaultBreakdown = async ( + client: BeefyViemClient, + blockNumber: bigint, + vault: BeefyVault +): Promise => { + if (vault.protocol_type !== "beefy_clm_vault") { + throw new Error(`Invalid protocol type ${vault.protocol_type}`); + } + + const underlyingClmBreakdown = await getBeefyClmManagerBreakdown( + client, + blockNumber, + vault.beefy_clm_manager + ); + + const vaultContract = getContract({ + client, + address: vault.vault_address, + abi: BeefyVaultV7Abi, + }); + + const [balance, totalSupply] = await Promise.all([ + vaultContract.read.balance({ blockNumber }), + vaultContract.read.totalSupply({ blockNumber }), + ]); + + return { + vault, + blockNumber, + vaultTotalSupply: totalSupply, + isLiquidityEligible: underlyingClmBreakdown.isLiquidityEligible, + balances: underlyingClmBreakdown.balances.map((tokenBalance) => ({ + tokenAddress: tokenBalance.tokenAddress, + vaultBalance: (balance * tokenBalance.vaultBalance) / totalSupply, + })), + }; +}; diff --git a/adapters/beefy/src/sdk/vault/getBeefyVaultConfig.ts b/adapters/beefy/src/sdk/vault/getBeefyVaultConfig.ts index 331ea16d..11d2ffa0 100644 --- a/adapters/beefy/src/sdk/vault/getBeefyVaultConfig.ts +++ b/adapters/beefy/src/sdk/vault/getBeefyVaultConfig.ts @@ -1,6 +1,11 @@ -import { memoize } from "lodash"; +import { groupBy, memoize } from "lodash"; import { Hex } from "viem"; -import { BEEFY_GOV_API, BEEFY_VAULT_API } from "../../config"; +import { + BEEFY_BOOST_API, + BEEFY_COW_VAULT_API, + BEEFY_GOV_API, + BEEFY_MOO_VAULT_API, +} from "../../config"; export type BeefyVault = { id: string; @@ -9,9 +14,17 @@ export type BeefyVault = { strategy_address: Hex; vault_token_symbol: string; chain: string; - protocol_type: BeefyProtocolType; reward_pools: BeefyRewardPool[]; -}; + boosts: BeefyBoost[]; +} & ( + | { + protocol_type: "beefy_clm_vault"; + beefy_clm_manager: BeefyVault; + } + | { + protocol_type: Exclude; + } +); export type BeefyRewardPool = { id: string; @@ -19,12 +32,19 @@ export type BeefyRewardPool = { reward_pool_address: Hex; }; +export type BeefyBoost = { + id: string; + boost_address: Hex; + underlying_address: Hex; +}; + export type BeefyProtocolType = | "gamma" | "ichi" | "mendi" | "solidly" - | "beefy_clm"; + | "beefy_clm" + | "beefy_clm_vault"; type ApiPlatformId = | "gamma" @@ -35,24 +55,78 @@ type ApiPlatformId = | "velodrome" | "beefy"; // and more but we don't use those on linea -type ApiVault = { +export type ApiStrategyTypeId = + | "lp" + | "multi-lp" + | "multi-lp-locked" + | "cowcentrated"; + +export type ApiVault = { id: string; status: "active" | "eol"; - earnedTokenAddress: Hex; + earnedTokenAddress: string; + depositTokenAddresses?: string[]; chain: string; - earnedToken: string; // cow token symbol platformId: ApiPlatformId; - tokenAddress: Hex; + token: string; + tokenAddress: string; + earnedToken: string; + isGovVault?: boolean; + strategyTypeId?: ApiStrategyTypeId; + bridged?: object; + assets?: string[]; strategy: Hex; }; -type ApiGovVault = { +export type ApiClmManager = { id: string; status: "active" | "eol"; version: number; + platformId: ApiPlatformId; + strategyTypeId?: ApiStrategyTypeId; + earnedToken: string; + strategy: string; chain: string; - tokenAddress: Hex; // clm address - earnContractAddress: Hex; // reward pool address + type: "cowcentrated" | "others"; + tokenAddress: string; // underlying pool address + depositTokenAddresses: string[]; // token0 and token1 + earnContractAddress: string; // reward pool address + earnedTokenAddress: string; // clm manager address +}; + +export type ApiClmRewardPool = { + id: string; + status: "active" | "eol"; + version: number; + platformId: ApiPlatformId; + strategyTypeId?: ApiStrategyTypeId; + chain: string; + tokenAddress: string; // clm address (want) + earnContractAddress: string; // reward pool address + earnedTokenAddresses: string[]; // reward tokens +}; + +export type ApiGovVault = { + id: string; + status: "active" | "eol"; + version: number; + chain: string; + tokenAddress: string; // clm address + earnContractAddress: string; // reward pool address + earnedTokenAddresses: string[]; +}; + +export type ApiBoost = { + id: string; + poolId: string; + + version: number; + chain: string; + status: "active" | "eol"; + + tokenAddress: string; // underlying + earnedTokenAddress: string; // reward token address + earnContractAddress: string; // reward pool address }; const protocol_map: Record = { @@ -67,54 +141,143 @@ const protocol_map: Record = { export const getBeefyVaultConfig = memoize( async (chain: string): Promise => { - const [vaultsData, rewardPoolData] = await Promise.all([ - fetch(BEEFY_VAULT_API).then((res) => res.json()), - fetch(BEEFY_GOV_API).then((res) => res.json()), + const [ + cowVaultsData, + mooVaultsData, + clmRewardPoolData, + [boostData, vaultRewardPoolData], + ] = await Promise.all([ + fetch(BEEFY_COW_VAULT_API + `/${chain}`) + .then((res) => res.json()) + .then((res) => + (res as ApiClmManager[]) + .filter((vault) => vault.chain === chain) + .filter((vault) => vault.type === "cowcentrated") + ), + fetch(BEEFY_MOO_VAULT_API + `/${chain}`) + .then((res) => res.json()) + .then((res) => + (res as ApiVault[]) + .filter((vault) => vault.chain === chain) + .filter((vault) => vault.isGovVault !== true) + ), + fetch(BEEFY_GOV_API + `/${chain}`) + .then((res) => res.json()) + .then((res) => + (res as ApiClmRewardPool[]) + .filter((g) => g.chain === chain) + .filter((g) => g.version === 2) + ), + fetch(BEEFY_BOOST_API + `/${chain}`) + .then((res) => res.json()) + .then((res) => [ + (res as ApiBoost[]) + .filter((g) => g.chain === chain) + .filter((g) => g.version !== 2), + (res as ApiBoost[]) + .filter((g) => g.chain === chain) + .filter((g) => g.version === 2), + ]), ]); - const rewardPoolsPerClm = rewardPoolData - .filter((pool: ApiGovVault) => pool.status === "active") - .filter((pool: ApiGovVault) => pool.version === 2) - .filter((pool: ApiGovVault) => pool.chain === chain) - .reduce((acc: Record, pool: ApiGovVault) => { - const clm_address = pool.tokenAddress.toLocaleLowerCase() as Hex; - const reward_pool_address = - pool.earnContractAddress.toLocaleLowerCase() as Hex; - if (!acc[clm_address]) { - acc[clm_address] = []; - } - acc[clm_address].push({ + const clmManagerAddresses = new Set( + cowVaultsData.map((v) => v.earnedTokenAddress.toLocaleLowerCase()) + ); + const boostPerUnderlyingAddress = groupBy(boostData, (b) => + b.tokenAddress.toLocaleLowerCase() + ); + const vaultRewardPoolDataPerVaultAddress = groupBy( + vaultRewardPoolData, + (v) => v.tokenAddress.toLocaleLowerCase() + ); + const clmRewardPoolDataPerClmAddress = groupBy(clmRewardPoolData, (c) => + c.tokenAddress.toLocaleLowerCase() + ); + + const clmVaultConfigs = cowVaultsData.map((vault): BeefyVault => { + const undelying_lp_address = + vault.tokenAddress.toLocaleLowerCase() as Hex; + const vault_address = vault.earnedTokenAddress.toLocaleLowerCase() as Hex; + + let protocol_type = protocol_map[vault.platformId]; + if (!protocol_type) { + throw new Error(`Unknown platformId ${vault.platformId}`); + } + if (protocol_type === "beefy_clm_vault") { + throw new Error("Invalid protocol"); + } + const reward_pools = clmRewardPoolDataPerClmAddress[vault_address] ?? []; + + const boosts = boostPerUnderlyingAddress[vault_address] ?? []; + + return { + id: vault.id, + vault_address, + chain: vault.chain, + vault_token_symbol: vault.earnedToken, + protocol_type, + strategy_address: vault.strategy.toLocaleLowerCase() as Hex, + undelying_lp_address, + reward_pools: reward_pools.map((pool) => ({ id: pool.id, - clm_address, - reward_pool_address, - }); - return acc; - }, {} as Record); - - const vaults = vaultsData - .filter((vault: ApiVault) => vault.chain === chain) - .filter((vault: ApiVault) => vault.status === "active") - .map((vault: ApiVault): BeefyVault => { - let protocol_type = protocol_map[vault.platformId]; - if (!protocol_type) { - throw new Error(`Unknown platformId ${vault.platformId}`); - } - - let reward_pools = - rewardPoolsPerClm[vault.earnedTokenAddress.toLocaleLowerCase()] ?? []; - - return { - id: vault.id, - vault_address: vault.earnedTokenAddress.toLocaleLowerCase() as Hex, - chain: vault.chain, - vault_token_symbol: vault.earnedToken, - protocol_type, - strategy_address: vault.strategy.toLocaleLowerCase() as Hex, - undelying_lp_address: vault.tokenAddress.toLocaleLowerCase() as Hex, - reward_pools, - }; - }); - - return vaults; + clm_address: pool.tokenAddress.toLocaleLowerCase() as Hex, + reward_pool_address: + pool.earnContractAddress.toLocaleLowerCase() as Hex, + })), + boosts: boosts.map((boost) => ({ + id: boost.id, + boost_address: boost.earnedTokenAddress.toLocaleLowerCase() as Hex, + underlying_address: boost.tokenAddress.toLocaleLowerCase() as Hex, + })), + }; + }); + + const mooVaultCofigs = mooVaultsData.map((vault): BeefyVault => { + const undelying_lp_address = + vault.tokenAddress.toLocaleLowerCase() as Hex; + const vault_address = vault.earnedTokenAddress.toLocaleLowerCase() as Hex; + + let protocol_type = clmManagerAddresses.has(undelying_lp_address) + ? "beefy_clm_vault" + : protocol_map[vault.platformId]; + if (!protocol_type) { + throw new Error(`Unknown platformId ${vault.platformId}`); + } + + let additionalConfig = + protocol_type === "beefy_clm_vault" + ? { + protocol_type, + beefy_clm_manager: clmVaultConfigs.find( + (v) => v.vault_address === undelying_lp_address + ) as BeefyVault, + } + : { protocol_type }; + const reward_pools = + vaultRewardPoolDataPerVaultAddress[vault_address] ?? []; + const boosts = boostPerUnderlyingAddress[vault_address] ?? []; + return { + id: vault.id, + vault_address, + chain: vault.chain, + vault_token_symbol: vault.earnedToken, + ...additionalConfig, + strategy_address: vault.strategy.toLocaleLowerCase() as Hex, + undelying_lp_address, + reward_pools: reward_pools.map((pool) => ({ + id: pool.id, + clm_address: pool.tokenAddress.toLocaleLowerCase() as Hex, + reward_pool_address: + pool.earnContractAddress.toLocaleLowerCase() as Hex, + })), + boosts: boosts.map((boost) => ({ + id: boost.id, + boost_address: boost.earnedTokenAddress.toLocaleLowerCase() as Hex, + underlying_address: boost.tokenAddress.toLocaleLowerCase() as Hex, + })), + }; + }); + + return clmVaultConfigs.concat(mooVaultCofigs); } );