diff --git a/README.md b/README.md index fe14103..c8264b5 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,7 @@ yarn test:lint # run prettier linter - clmStrategy.vault() - clmStrategy.price() - clmStrategy.range() +- clmStrategy.output() // optional - clmStrategy.balanceOfPool() - clmStrategy.lpToken1ToNativePrice() - clmStrategy.lpToken0ToNativePrice() diff --git a/schema.graphql b/schema.graphql index 08afb12..c76eed1 100644 --- a/schema.graphql +++ b/schema.graphql @@ -399,6 +399,9 @@ type CLM @entity { underlyingToken0: Token! "The underlying tokens contained in the CLM. This is the second token." underlyingToken1: Token! + + "The output tokens of the CLM's strategy. These will be sent to the reward pool if the strategy does not automatically compound." + outputTokens: [Token!]! "The reward tokens of the CLM's rewardpool. These will be rewarded to positions when the CLM earns non-compounding yield." rewardTokens: [Token!]! @@ -413,6 +416,8 @@ type CLM @entity { token0ToNativePrice: BigInt! "Latest token 1 price in native we have seen. Expressed with 18 decimals." token1ToNativePrice: BigInt! + "Latest output token prices in native we have seen. Ordered by clm.outputTokens. Expressed with 18 decimals." + outputToNativePrices: [BigInt!]! "Latest reward token prices in native we have seen. Ordered by clm.rewardTokens. Expressed with 18 decimals." rewardToNativePrices: [BigInt!]! "Latest native token price we have seen. Expressed with 18 decimals." @@ -489,6 +494,8 @@ type ClmSnapshot @entity { token0ToNativePrice: BigInt! "Latest token 1 price in native of this snapshot. Expressed with 18 decimals." token1ToNativePrice: BigInt! + "Latest output token prices in native of this snapshot. Ordered by clm.outputTokens. Expressed with 18 decimals." + outputToNativePrices: [BigInt!]! "Latest reward token prices in native of this snapshot. Ordered by clm.rewardTokens. Expressed with 18 decimals." rewardToNativePrices: [BigInt!]! "Latest native token price of this snapshot. Expressed with 18 decimals." @@ -540,6 +547,8 @@ type ClmStrategy @entity { clm: CLM! "The manager of the strategy" manager: ClmManager! + "The output token sent to the reward pool. Set to NULL token if the strategy automatically compounds." + outputToken: Token! "The transaction that created the strategy" createdWith: Transaction! "Technical field to remember if the strategy was already initialized" @@ -593,8 +602,8 @@ type ClmHarvestEvent @entity(immutable: true) { compoundedAmount0: BigInt! "The amount of second underlying tokens compounded" compoundedAmount1: BigInt! - "The amount of reward tokens collected. Ordered by clm.rewardTokens." - collectedRewards: [BigInt!]! + "The amount of output tokens collected. Ordered by clm.outputTokens." + collectedOutputAmounts: [BigInt!]! "Total amount of liquidity in the manager at time of harvest" managerTotalSupply: BigInt! @@ -605,6 +614,8 @@ type ClmHarvestEvent @entity(immutable: true) { token0ToNativePrice: BigInt! "Token 1 price in native at the time of harvest. Expressed with 18 decimals." token1ToNativePrice: BigInt! + "Output token prices in native at the time of harvest. Expressed with 18 decimals. Ordered by clm.outputTokens." + outputToNativePrices: [BigInt!]! "Reward token prices in native at the time of harvest. Expressed with 18 decimals. Ordered by clm.rewardTokens." rewardToNativePrices: [BigInt!]! "Native token price at the time of harvest. Expressed with 18 decimals." @@ -645,13 +656,15 @@ type ClmManagerCollectionEvent @entity(immutable: true) { collectedAmount0: BigInt! "Amount of collected fees in the second token" collectedAmount1: BigInt! - "Amount of collected fees in the reward tokens. Ordered by clm.rewardTokens." - collectedRewardAmounts: [BigInt!]! + "Amount of collected fees in the output tokens. Ordered by clm.outputTokens." + collectedOutputAmounts: [BigInt!]! "Token 0 price in native at the time of the collection. Expressed with 18 decimals." token0ToNativePrice: BigInt! "Token 1 price in native at the time of the collection. Expressed with 18 decimals." token1ToNativePrice: BigInt! + "Output token prices in native at the time of the collection. Expressed with 18 decimals. Ordered by clm.outputTokens." + outputToNativePrices: [BigInt!]! "Reward token prices in native at the time of the collection. Expressed with 18 decimals. Ordered by clm.rewardTokens." rewardToNativePrices: [BigInt!]! "Native token price at the time of the interaction. Expressed with 18 decimals." @@ -744,6 +757,8 @@ type ClmPositionInteraction @entity(immutable: true) { token0ToNativePrice: BigInt! "Token 1 price in native at the time of the interaction. Expressed with 18 decimals." token1ToNativePrice: BigInt! + "Output token prices in native at the time of the interaction. Expressed with 18 decimals. Ordered by clm.outputTokens." + outputToNativePrices: [BigInt!]! "Reward token prices in native at the time of the interaction. Expressed with 18 decimals. Ordered by clm.rewardTokens." rewardToNativePrices: [BigInt!]! "Native token price at the time of the interaction. Expressed with 18 decimals." diff --git a/src/clm/compound.ts b/src/clm/compound.ts index 73cc991..be40ea4 100644 --- a/src/clm/compound.ts +++ b/src/clm/compound.ts @@ -17,15 +17,29 @@ export function handleClmStrategyHarvestAmounts(event: CLMHarvestEvent): void { } export function handleClmStrategyHarvestRewards(event: CLMHarvestRewardsEvent): void { - // TODO: handle output tokens and event.params.fees or remove this handler - handleClmStrategyHarvest(event, ZERO_BI, ZERO_BI, []) + const amountClaimed = event.params.fees + const strategy = getClmStrategy(event.address) + const clm = getCLM(strategy.clm) + const outputToken = strategy.outputToken + + const collectedOutputAmounts = new Array() + const outputTokens = clm.outputTokens + for (let i = 0; i < outputTokens.length; i++) { + if (outputTokens[i].equals(outputToken)) { + collectedOutputAmounts.push(amountClaimed) + } else { + collectedOutputAmounts.push(ZERO_BI) + } + } + + handleClmStrategyHarvest(event, ZERO_BI, ZERO_BI, collectedOutputAmounts) } function handleClmStrategyHarvest( event: ethereum.Event, compoundedAmount0: BigInt, compoundedAmount1: BigInt, - collectedRewards: Array, + collectedOutputAmounts: Array, ): void { let strategy = getClmStrategy(event.address) let clm = getCLM(strategy.clm) @@ -52,11 +66,12 @@ function handleClmStrategyHarvest( harvest.underlyingAmount1 = clmData.token1Balance harvest.compoundedAmount0 = compoundedAmount0 harvest.compoundedAmount1 = compoundedAmount1 - harvest.collectedRewards = collectedRewards + harvest.collectedOutputAmounts = collectedOutputAmounts harvest.managerTotalSupply = clmData.managerTotalSupply harvest.rewardPoolTotalSupply = clmData.rewardPoolTotalSupply harvest.token0ToNativePrice = clmData.token0ToNativePrice harvest.token1ToNativePrice = clmData.token1ToNativePrice + harvest.outputToNativePrices = clmData.outputToNativePrices harvest.rewardToNativePrices = clmData.rewardToNativePrices harvest.nativeToUSDPrice = clmData.nativeToUSDPrice harvest.save() @@ -71,15 +86,29 @@ export function handleClmStrategyClaimedFees(event: CLMClaimedFeesEvent): void { ) } export function handleClmStrategyClaimedRewards(event: CLMClaimedRewardsEvent): void { - // TODO: handle output tokens and event.params.fees or remove this handler - handleClmStrategyFees(event, ZERO_BI, ZERO_BI, []) + const amountClaimed = event.params.fees + const strategy = getClmStrategy(event.address) + const clm = getCLM(strategy.clm) + const outputToken = strategy.outputToken + + const collectedOutputAmounts = new Array() + const outputTokens = clm.outputTokens + for (let i = 0; i < outputTokens.length; i++) { + if (outputTokens[i].equals(outputToken)) { + collectedOutputAmounts.push(amountClaimed) + } else { + collectedOutputAmounts.push(ZERO_BI) + } + } + + handleClmStrategyFees(event, ZERO_BI, ZERO_BI, collectedOutputAmounts) } function handleClmStrategyFees( event: ethereum.Event, collectedAmount0: BigInt, collectedAmount1: BigInt, - collectedRewardAmounts: Array, + collectedOutputAmounts: Array, ): void { let strategy = getClmStrategy(event.address) let clm = getCLM(strategy.clm) @@ -108,9 +137,10 @@ function handleClmStrategyFees( collect.underlyingAltAmount1 = clmData.token1PositionAltBalance collect.collectedAmount0 = collectedAmount0 collect.collectedAmount1 = collectedAmount1 - collect.collectedRewardAmounts = collectedRewardAmounts + collect.collectedOutputAmounts = collectedOutputAmounts collect.token0ToNativePrice = clmData.token0ToNativePrice collect.token1ToNativePrice = clmData.token1ToNativePrice + collect.outputToNativePrices = clmData.outputToNativePrices collect.rewardToNativePrices = clmData.rewardToNativePrices collect.nativeToUSDPrice = clmData.nativeToUSDPrice collect.save() diff --git a/src/clm/entity/clm.ts b/src/clm/entity/clm.ts index c54221c..f72c259 100644 --- a/src/clm/entity/clm.ts +++ b/src/clm/entity/clm.ts @@ -28,6 +28,7 @@ export function getCLM(managerAddress: Bytes): CLM { clm.underlyingToken0 = ADDRESS_ZERO clm.underlyingToken1 = ADDRESS_ZERO + clm.outputTokens = [] clm.rewardTokens = [] clm.managerTotalSupply = ZERO_BI @@ -35,6 +36,7 @@ export function getCLM(managerAddress: Bytes): CLM { clm.token0ToNativePrice = ZERO_BI clm.token1ToNativePrice = ZERO_BI + clm.outputToNativePrices = [] clm.rewardToNativePrices = [] clm.nativeToUSDPrice = ZERO_BI @@ -72,6 +74,7 @@ export function getClmStrategy(strategyAddress: Bytes): ClmStrategy { strategy.clm = ADDRESS_ZERO strategy.manager = ADDRESS_ZERO strategy.createdWith = ADDRESS_ZERO + strategy.outputToken = getNullToken().id strategy.isInitialized = false } return strategy @@ -110,6 +113,7 @@ export function getClmSnapshot(clm: CLM, timestamp: BigInt, period: BigInt): Clm snapshot.token0ToNativePrice = ZERO_BI snapshot.token1ToNativePrice = ZERO_BI + snapshot.outputToNativePrices = [] snapshot.rewardToNativePrices = [] snapshot.nativeToUSDPrice = ZERO_BI @@ -130,6 +134,7 @@ export function getClmSnapshot(clm: CLM, timestamp: BigInt, period: BigInt): Clm snapshot.rewardPoolTotalSupply = previousSnapshot.rewardPoolTotalSupply snapshot.token0ToNativePrice = previousSnapshot.token0ToNativePrice snapshot.token1ToNativePrice = previousSnapshot.token1ToNativePrice + snapshot.outputToNativePrices = previousSnapshot.outputToNativePrices snapshot.rewardToNativePrices = previousSnapshot.rewardToNativePrices snapshot.nativeToUSDPrice = previousSnapshot.nativeToUSDPrice snapshot.priceOfToken0InToken1 = previousSnapshot.priceOfToken0InToken1 diff --git a/src/clm/interaction.ts b/src/clm/interaction.ts index 97cdff2..49a09bb 100644 --- a/src/clm/interaction.ts +++ b/src/clm/interaction.ts @@ -201,6 +201,7 @@ function updateUserPosition( } interaction.token0ToNativePrice = clmData.token0ToNativePrice interaction.token1ToNativePrice = clmData.token1ToNativePrice + interaction.outputToNativePrices = clmData.outputToNativePrices interaction.rewardToNativePrices = clmData.rewardToNativePrices interaction.nativeToUSDPrice = clmData.nativeToUSDPrice interaction.save() diff --git a/src/clm/lifecycle.ts b/src/clm/lifecycle.ts index 6e3a220..239c08d 100644 --- a/src/clm/lifecycle.ts +++ b/src/clm/lifecycle.ts @@ -171,6 +171,30 @@ function fetchInitialCLMDataAndSave(clm: CLM): void { const underlyingToken0 = fetchAndSaveTokenData(underlyingToken0Address) const underlyingToken1 = fetchAndSaveTokenData(underlyingToken1Address) + const strategyAddress = Address.fromBytes(clm.strategy) + const strategyContract = ClmStrategyContract.bind(strategyAddress) + const outputTokenRes = strategyContract.try_output() + if (!outputTokenRes.reverted) { + const outputTokenAddress = outputTokenRes.value + const outputToken = fetchAndSaveTokenData(outputTokenAddress) + + const currentOutputTokens = clm.outputTokens + let found = false + for (let i = 0; i < currentOutputTokens.length; i++) { + if (currentOutputTokens[i].equals(outputToken.id)) { + found = true + break + } + } + + if (!found) { + currentOutputTokens.push(outputToken.id) + clm.outputTokens = currentOutputTokens + } + } else { + log.warning("fetchInitialCLMDataAndSave: Output token not found for CLM {}", [clm.id.toHexString()]) + } + clm.managerToken = managerToken.id clm.underlyingToken0 = underlyingToken0.id clm.underlyingToken1 = underlyingToken1.id diff --git a/src/clm/utils/clm-data.ts b/src/clm/utils/clm-data.ts index 9fb091b..8defd59 100644 --- a/src/clm/utils/clm-data.ts +++ b/src/clm/utils/clm-data.ts @@ -13,7 +13,7 @@ import { PYTH_NATIVE_PRICE_ID, PRICE_ORACLE_TYPE, } from "../../config" -import { Multicall3Params, multicall } from "../../common/utils/multicall" +import { Multicall3Params, MulticallResult, multicall } from "../../common/utils/multicall" import { getToken, isNullToken } from "../../common/entity/token" import { CLM_SNAPSHOT_PERIODS } from "./snapshot" import { getClmSnapshot } from "../entity/clm" @@ -57,6 +57,7 @@ export function fetchCLMData(clm: CLM): CLMData { const hasRewardPool = !isNullToken(rewardPoolToken) const rewardTokens = clm.rewardTokens + let rewardCallStartIndex = calls.length if (hasRewardPool) { calls.push(new Multicall3Params(rewardPoolAddress, "totalSupply()", "uint256")) calls.push( @@ -65,6 +66,7 @@ export function fetchCLMData(clm: CLM): CLMData { ]), ) + rewardCallStartIndex = calls.length // ignore the first two calls, those just warm up the prices for (let i = 0; i < rewardTokens.length; i++) { const rewardTokenAddress = Address.fromBytes(rewardTokens[i]) const rewardToken = getToken(rewardTokenAddress) @@ -84,6 +86,29 @@ export function fetchCLMData(clm: CLM): CLMData { ) } } + const rewardCallsEndIndex = calls.length + + const outputCallsStartIndex = calls.length + const outputTokens = clm.outputTokens + for (let i = 0; i < outputTokens.length; i++) { + const outputTokenAddress = Address.fromBytes(outputTokens[i]) + + calls.push( + new Multicall3Params(BEEFY_ORACLE_ADDRESS, "getFreshPrice(address)", "(uint256,bool)", [ + ethereum.Value.fromAddress(outputTokenAddress), + ]), + ) + + const amountIn = changeValueEncoding(ONE_BI, ZERO_BI, token1.decimals).div(BEEFY_SWAPPER_VALUE_SCALER) + calls.push( + new Multicall3Params(BEEFY_SWAPPER_ADDRESS, "getAmountOut(address,address,uint256)", "uint256", [ + ethereum.Value.fromAddress(outputTokenAddress), + ethereum.Value.fromAddress(WNATIVE_TOKEN_ADDRESS), + ethereum.Value.fromUnsignedBigInt(amountIn), + ]), + ) + } + const outputCallsEndIndex = calls.length const results = multicall(calls) const totalSupplyRes = results[0] @@ -95,9 +120,16 @@ export function fetchCLMData(clm: CLM): CLMData { const token1ToNativePriceRes = results[6] const priceFeedRes = results[7] const rewardPoolTotalSupplyRes = hasRewardPool ? results[8] : null - const rewardTokenOutputAmountsRes = hasRewardPool - ? results.filter((_, i) => i >= 10).filter((_, i) => i % 2 == 1) - : [] + const rewardTokenOutputAmountsRes = new Array() + if (hasRewardPool) { + for (let i = rewardCallStartIndex; i < rewardCallsEndIndex; i += 2) { + rewardTokenOutputAmountsRes.push(results[i + 1]) + } + } + const outputTokenOutputAmountsRes = new Array() + for (let i = outputCallsStartIndex; i < outputCallsEndIndex; i += 2) { + outputTokenOutputAmountsRes.push(results[i + 1]) + } let managerTotalSupply = ZERO_BI if (!totalSupplyRes.reverted) { @@ -207,6 +239,7 @@ export function fetchCLMData(clm: CLM): CLMData { } else { log.error("Failed to fetch nativeToUSDPrice for CLM {}", [clm.id.toString()]) } + // only some clms have a reward pool token let rewardPoolTotalSupply = ZERO_BI if (rewardPoolTotalSupplyRes != null) { @@ -217,6 +250,18 @@ export function fetchCLMData(clm: CLM): CLMData { } } + let outputToNativePrices = new Array() + for (let i = 0; i < outputTokenOutputAmountsRes.length; i++) { + const amountOutRes = outputTokenOutputAmountsRes[i] + if (!amountOutRes.reverted) { + const amountOut = amountOutRes.value.toBigInt().times(BEEFY_SWAPPER_VALUE_SCALER) + outputToNativePrices.push(amountOut) + } else { + outputToNativePrices.push(ZERO_BI) + log.error("Failed to fetch outputToNativePrices for CLM {}", [clm.id.toHexString()]) + } + } + return new CLMData( managerTotalSupply, rewardPoolTotalSupply, @@ -234,6 +279,7 @@ export function fetchCLMData(clm: CLM): CLMData { token0ToNativePrice, token1ToNativePrice, + outputToNativePrices, rewardToNativePrices, nativeToUSDPrice, ) @@ -257,6 +303,7 @@ class CLMData { public token0ToNativePrice: BigInt, public token1ToNativePrice: BigInt, + public outputToNativePrices: Array, public rewardToNativePrices: Array, public nativeToUSDPrice: BigInt, ) {} @@ -268,6 +315,7 @@ export function updateCLMDataAndSnapshots(clm: CLM, clmData: CLMData, nowTimesta clm.rewardPoolTotalSupply = clmData.rewardPoolTotalSupply clm.token0ToNativePrice = clmData.token0ToNativePrice clm.token1ToNativePrice = clmData.token1ToNativePrice + clm.outputToNativePrices = clmData.outputToNativePrices clm.rewardToNativePrices = clmData.rewardToNativePrices clm.nativeToUSDPrice = clmData.nativeToUSDPrice clm.priceOfToken0InToken1 = clmData.priceOfToken0InToken1 @@ -292,6 +340,7 @@ export function updateCLMDataAndSnapshots(clm: CLM, clmData: CLMData, nowTimesta snapshot.rewardPoolTotalSupply = clm.rewardPoolTotalSupply snapshot.token0ToNativePrice = clm.token0ToNativePrice snapshot.token1ToNativePrice = clm.token1ToNativePrice + snapshot.outputToNativePrices = clm.outputToNativePrices snapshot.rewardToNativePrices = clm.rewardToNativePrices snapshot.nativeToUSDPrice = clm.nativeToUSDPrice snapshot.priceOfToken0InToken1 = clm.priceOfToken0InToken1 diff --git a/src/common/utils/multicall.ts b/src/common/utils/multicall.ts index 8ada039..040ff11 100644 --- a/src/common/utils/multicall.ts +++ b/src/common/utils/multicall.ts @@ -11,7 +11,7 @@ export class Multicall3Params { ) {} } -class MulticallResult { +export class MulticallResult { constructor( public value: ethereum.Value, public reverted: boolean,