diff --git a/docs/apr-oracle-integration.md b/docs/apr-oracle-integration.md index c0b96ffd..2ffc505a 100644 --- a/docs/apr-oracle-integration.md +++ b/docs/apr-oracle-integration.md @@ -32,3 +32,94 @@ The external API response structure for vaults remains unchanged to ensure backw } } ``` + +--- + +## V2 Vault Estimated APR Integration (Kong) + +As of the implementation of [issue #560](https://github.com/yearn/ydaemon/issues/560), V2 vaults with Curve, Convex, Velodrome, and Aerodrome strategies now prioritize pre-calculated estimated APRs from Kong over local RPC-based calculations. + +### Overview + +Previously, `yDaemon` computed forward-looking APRs for V2 Curve-like vaults (Curve, Convex, Velodrome, Aerodrome) by making multiple on-chain RPC calls to calculate: +- Gauge boost values +- Pool APY from Curve subgraph +- Reward rates and token prices +- Various fee components + +This approach was: +- **RPC-intensive**: Required multiple multicalls per vault +- **Computation-heavy**: Performed complex calculations locally +- **Slower**: Dependent on RPC response times + +### New Data Source + +The `performance.estimated` field from Kong's GraphQL API is now the **primary** source for V2 forward APR data: +- **Estimated APR**: Pre-computed annualized rate +- **Estimated APY**: Pre-computed annualized yield (compounded) +- **Type**: Strategy type identifier (e.g., `"crv"`, `"v2:velo"`) +- **Components**: Detailed breakdown including: + - `boost`: Gauge boost multiplier + - `poolAPY`: Pool swap fee APY + - `boostedAPR`: Boosted reward APR + - `baseAPR`: Base reward APR + - `rewardsAPR`: Additional rewards APR + - `rewardsAPY`: Additional rewards APY (compounded) + - `cvxAPR`: Convex-specific APR + - `keepCRV`: Percentage of CRV kept vs. swapped + - `keepVelo`: Percentage of VELO kept vs. swapped + +### Supported Chains + +The estimated APR integration applies to V2 vaults on: +- **Ethereum (chainId: 1)**: Curve, Convex +- **Optimism (chainId: 10)**: Velodrome +- **Fantom (chainId: 250)**: Curve +- **Arbitrum (chainId: 42161)**: Curve +- **Base (chainId: 8453)**: Aerodrome + +### Implementation Details + +#### Priority System +1. **Primary**: Check for Kong `performance.estimated` data +2. **Fallback**: If Kong data unavailable, perform local RPC-based calculation +3. **Graceful degradation**: System continues to function even if Kong is temporarily unavailable + +#### Internal Logic +- **Kong Indexer** (`internal/indexer/indexer.kong.go`): Fetches and stores estimated APR data from Kong GraphQL API +- **Storage Accessor** (`internal/storage/elem.vaults.go`): Provides `GetKongEstimatedAPY()` function to retrieve cached Kong data +- **APR Calculation** (`processes/apr/main.go`): Checks Kong data first before falling back to local computation +- **Helper Function** (`processes/apr/forward.curve.helpers.go`): `convertKongEstimatedAprToForwardAPY()` converts Kong format to internal `TForwardAPY` structure + +#### Benefits +- **Reduced RPC Calls**: Eliminates multiple multicalls for gauge data, prices, and boost calculations +- **Faster Response**: Pre-computed data from Kong cache vs. real-time RPC queries +- **Consistency**: Kong serves as single source of truth for APR calculations +- **Maintained Components**: All APR component breakdowns preserved in API responses + +### API Response + +The external API response structure remains **fully backward compatible**. The `apr.forwardAPR` object structure is unchanged: + +```json +"forwardAPR": { + "type": "crv", // From Kong estimated.type + "netAPY": 0.156, // From Kong estimated.apy + "composite": { + "boost": 2.5, // From Kong estimated.components.boost + "poolAPY": 0.023, // From Kong estimated.components.poolAPY + "boostedAPR": 0.187, // From Kong estimated.components.boostedAPR + "baseAPR": 0.075, // From Kong estimated.components.baseAPR + "cvxAPR": 0.045, // From Kong estimated.components.cvxAPR (Convex only) + "rewardsAPY": 0.034, // From Kong estimated.components.rewardsAPY + "keepCRV": 0.1 // From Kong estimated.components.keepCRV + } +} +``` + +### Migration Notes + +- **No Breaking Changes**: API consumers experience no differences in response structure +- **Automatic Fallback**: If Kong estimated data is unavailable, yDaemon seamlessly falls back to local computation +- **Strategy Support**: Works for all Curve, Convex, Velodrome, and Aerodrome V2 strategies + diff --git a/internal/indexer/indexer.kong.go b/internal/indexer/indexer.kong.go index 6e6522c7..f0806bd5 100644 --- a/internal/indexer/indexer.kong.go +++ b/internal/indexer/indexer.kong.go @@ -44,6 +44,11 @@ func IndexNewVaults(chainID uint64) map[common.Address]models.TVaultsFromRegistr // Convert KongDebt to TKongDebt var debts []models.TKongDebt for _, debt := range data.Debts { + totalDebt := debt.TotalDebt + if totalDebt == nil { + zero := "0" + totalDebt = &zero + } debts = append(debts, models.TKongDebt{ Strategy: debt.Strategy, PerformanceFee: debt.PerformanceFee, @@ -52,7 +57,7 @@ func IndexNewVaults(chainID uint64) map[common.Address]models.TVaultsFromRegistr MinDebtPerHarvest: debt.MinDebtPerHarvest, MaxDebtPerHarvest: debt.MaxDebtPerHarvest, LastReport: debt.LastReport, - TotalDebt: debt.TotalDebt, + TotalDebt: totalDebt, TotalDebtUsd: debt.TotalDebtUsd, TotalGain: debt.TotalGain, TotalGainUsd: debt.TotalGainUsd, @@ -67,13 +72,38 @@ func IndexNewVaults(chainID uint64) map[common.Address]models.TVaultsFromRegistr }) } - // Extract performance data from Kong response (oracle APR/APY) + // Extract performance data from Kong response (oracle APR/APY and estimated APR) performance := models.TKongPerformance{} if data.Vault.Performance != nil { performance.Oracle = models.TKongOracle{ Apr: data.Vault.Performance.Oracle.Apr, Apy: data.Vault.Performance.Oracle.Apy, } + + // Extract estimated APR if available + if data.Vault.Performance.Estimated != nil { + components := &models.TKongEstimatedAprComponents{} + if data.Vault.Performance.Estimated.Components != nil { + components = &models.TKongEstimatedAprComponents{ + Boost: data.Vault.Performance.Estimated.Components.Boost, + PoolAPY: data.Vault.Performance.Estimated.Components.PoolAPY, + BoostedAPR: data.Vault.Performance.Estimated.Components.BoostedAPR, + BaseAPR: data.Vault.Performance.Estimated.Components.BaseAPR, + RewardsAPR: data.Vault.Performance.Estimated.Components.RewardsAPR, + RewardsAPY: data.Vault.Performance.Estimated.Components.RewardsAPY, + CvxAPR: data.Vault.Performance.Estimated.Components.CvxAPR, + KeepCRV: data.Vault.Performance.Estimated.Components.KeepCRV, + KeepVelo: data.Vault.Performance.Estimated.Components.KeepVelo, + } + } + + performance.Estimated = &models.TKongEstimatedApr{ + Apr: data.Vault.Performance.Estimated.Apr, + Apy: data.Vault.Performance.Estimated.Apy, + Type: data.Vault.Performance.Estimated.Type, + Components: components, + } + } } kongSchema := models.TKongVaultSchema{ diff --git a/internal/kong/client.go b/internal/kong/client.go index 0702d3c7..83ed3591 100644 --- a/internal/kong/client.go +++ b/internal/kong/client.go @@ -77,8 +77,28 @@ type KongOracle struct { Apy *float64 `json:"apy"` // Float or null } +type KongEstimatedAprComponents struct { + Boost *float64 `json:"boost"` + PoolAPY *float64 `json:"poolAPY"` + BoostedAPR *float64 `json:"boostedAPR"` + BaseAPR *float64 `json:"baseAPR"` + RewardsAPR *float64 `json:"rewardsAPR"` + RewardsAPY *float64 `json:"rewardsAPY"` + CvxAPR *float64 `json:"cvxAPR"` + KeepCRV *float64 `json:"keepCRV"` + KeepVelo *float64 `json:"keepVelo"` +} + +type KongEstimatedApr struct { + Apr *float64 `json:"apr"` + Apy *float64 `json:"apy"` + Type string `json:"type"` + Components *KongEstimatedAprComponents `json:"components"` +} + type KongPerformance struct { - Oracle KongOracle `json:"oracle"` + Oracle KongOracle `json:"oracle"` + Estimated *KongEstimatedApr `json:"estimated"` // Estimated APR from Kong } type KongVault struct { @@ -215,6 +235,22 @@ func (c *Client) FetchVaultsForChain(ctx context.Context, chainID uint64) ([]Kon apr apy } + estimated { + apr + apy + type + components { + boost + poolAPY + boostedAPR + baseAPR + rewardsAPR + rewardsAPY + cvxAPR + keepCRV + keepVelo + } + } } apy { pricePerShare @@ -302,6 +338,22 @@ func (c *Client) FetchAllVaults(ctx context.Context) (map[uint64][]KongVault, er apr apy } + estimated { + apr + apy + type + components { + boost + poolAPY + boostedAPR + baseAPR + rewardsAPR + rewardsAPY + cvxAPR + keepCRV + keepVelo + } + } } apy { pricePerShare diff --git a/internal/models/vaults.go b/internal/models/vaults.go index 7b2e1d0a..c604f119 100644 --- a/internal/models/vaults.go +++ b/internal/models/vaults.go @@ -321,8 +321,28 @@ type TKongOracle struct { Apy *float64 `json:"apy"` // Float or null } +type TKongEstimatedAprComponents struct { + Boost *float64 `json:"boost"` + PoolAPY *float64 `json:"poolAPY"` + BoostedAPR *float64 `json:"boostedAPR"` + BaseAPR *float64 `json:"baseAPR"` + RewardsAPR *float64 `json:"rewardsAPR"` + RewardsAPY *float64 `json:"rewardsAPY"` + CvxAPR *float64 `json:"cvxAPR"` + KeepCRV *float64 `json:"keepCRV"` + KeepVelo *float64 `json:"keepVelo"` +} + +type TKongEstimatedApr struct { + Apr *float64 `json:"apr"` + Apy *float64 `json:"apy"` + Type string `json:"type"` + Components *TKongEstimatedAprComponents `json:"components"` +} + type TKongPerformance struct { - Oracle TKongOracle `json:"oracle"` + Oracle TKongOracle `json:"oracle"` + Estimated *TKongEstimatedApr `json:"estimated"` // Estimated APR from Kong } type TKongVaultSchema struct { diff --git a/internal/storage/elem.vaults.go b/internal/storage/elem.vaults.go index b6b75864..27a4c503 100644 --- a/internal/storage/elem.vaults.go +++ b/internal/storage/elem.vaults.go @@ -472,4 +472,19 @@ func GetKongOracleAPY(chainID uint64, vaultAddress common.Address) (*float64, *f return nil, nil, false } return data.Performance.Oracle.Apr, data.Performance.Oracle.Apy, true +} + +/************************************************************************************************** +** GetKongEstimatedAPY retrieves estimated APR data from Kong for a vault +** Returns the estimated APR struct and a boolean indicating if data was found +**************************************************************************************************/ +func GetKongEstimatedAPY(chainID uint64, vaultAddress common.Address) (*models.TKongEstimatedApr, bool) { + data, ok := GetKongVaultData(chainID, vaultAddress) + if !ok { + return nil, false + } + if data.Performance.Estimated == nil { + return nil, false + } + return data.Performance.Estimated, true } \ No newline at end of file diff --git a/processes/apr/forward.curve.helpers.go b/processes/apr/forward.curve.helpers.go index 2116c248..a81b0b65 100644 --- a/processes/apr/forward.curve.helpers.go +++ b/processes/apr/forward.curve.helpers.go @@ -375,3 +375,59 @@ func computeCurveLikeForwardAPY( }, } } + +/************************************************************************************************** +** convertKongEstimatedAprToForwardAPY converts Kong estimated APR data to TForwardAPY format +** Returns (forwardAPY, hasData) where hasData indicates if valid Kong data was found +**************************************************************************************************/ +func convertKongEstimatedAprToForwardAPY(chainID uint64, vaultAddress common.Address) (TForwardAPY, bool) { + estimatedApr, ok := storage.GetKongEstimatedAPY(chainID, vaultAddress) + if !ok || estimatedApr == nil || estimatedApr.Apy == nil { + return TForwardAPY{}, false + } + + // Convert to float64 values, defaulting to 0 if nil + var boost, poolAPY, boostedAPR, baseAPR, rewardsAPY, cvxAPR, keepCRV, keepVelo float64 + + if estimatedApr.Components != nil { + if estimatedApr.Components.Boost != nil { + boost = *estimatedApr.Components.Boost + } + if estimatedApr.Components.PoolAPY != nil { + poolAPY = *estimatedApr.Components.PoolAPY + } + if estimatedApr.Components.BoostedAPR != nil { + boostedAPR = *estimatedApr.Components.BoostedAPR + } + if estimatedApr.Components.BaseAPR != nil { + baseAPR = *estimatedApr.Components.BaseAPR + } + if estimatedApr.Components.RewardsAPY != nil { + rewardsAPY = *estimatedApr.Components.RewardsAPY + } + if estimatedApr.Components.CvxAPR != nil { + cvxAPR = *estimatedApr.Components.CvxAPR + } + if estimatedApr.Components.KeepCRV != nil { + keepCRV = *estimatedApr.Components.KeepCRV + } + if estimatedApr.Components.KeepVelo != nil { + keepVelo = *estimatedApr.Components.KeepVelo + } + } + + return TForwardAPY{ + Type: estimatedApr.Type, + NetAPY: bigNumber.NewFloat(*estimatedApr.Apy), + Composite: TCompositeData{ + Boost: bigNumber.NewFloat(boost), + PoolAPY: bigNumber.NewFloat(poolAPY), + BoostedAPR: bigNumber.NewFloat(boostedAPR), + BaseAPR: bigNumber.NewFloat(baseAPR), + CvxAPR: bigNumber.NewFloat(cvxAPR), + RewardsAPY: bigNumber.NewFloat(rewardsAPY), + KeepCRV: bigNumber.NewFloat(keepCRV), + KeepVelo: bigNumber.NewFloat(keepVelo), + }, + }, true +} diff --git a/processes/apr/main.go b/processes/apr/main.go index 3aa0ce42..bb6d16a6 100644 --- a/processes/apr/main.go +++ b/processes/apr/main.go @@ -11,6 +11,44 @@ import ( "github.com/yearn/ydaemon/internal/storage" ) +func isForwardAPYAllZeros(f TForwardAPY) bool { + if f.NetAPY != nil && !f.NetAPY.IsZero() { + return false + } + c := f.Composite + if c.Boost != nil && !c.Boost.IsZero() { + return false + } + if c.PoolAPY != nil && !c.PoolAPY.IsZero() { + return false + } + if c.BoostedAPR != nil && !c.BoostedAPR.IsZero() { + return false + } + if c.BaseAPR != nil && !c.BaseAPR.IsZero() { + return false + } + if c.CvxAPR != nil && !c.CvxAPR.IsZero() { + return false + } + if c.RewardsAPY != nil && !c.RewardsAPY.IsZero() { + return false + } + if c.V3OracleCurrentAPR != nil && !c.V3OracleCurrentAPR.IsZero() { + return false + } + if c.V3OracleStratRatioAPR != nil && !c.V3OracleStratRatioAPR.IsZero() { + return false + } + if c.KeepCRV != nil && !c.KeepCRV.IsZero() { + return false + } + if c.KeepVelo != nil && !c.KeepVelo.IsZero() { + return false + } + return true +} + var COMPUTED_APY = make(map[uint64]*sync.Map) func init() { @@ -54,10 +92,6 @@ func ComputeChainAPY(chainID uint64) { start := time.Now() logs.Warning("📈 [APY START]", "chain", chainID) allVaults, _ := storage.ListVaults(chainID) - gauges := storage.FetchCurveGauges(chainID) - pools := retrieveCurveGetPools(chainID) - subgraphData := retrieveCurveSubgraphData(chainID) - fraxPools := retrieveFraxPools() storage.RefreshGammaCalls(chainID) isOnGnosis := (chainID == 100) @@ -123,66 +157,12 @@ func ComputeChainAPY(chainID uint64) { vaultAPY.Extra.StakingRewardsAPY = v3StakingAPY } - /********************************************************************************************** - ** If it's a Curve Vault (has a Curve, Convex or Frax strategy), we can estimate the forward - ** APY, aka the expected APY we will get for the upcoming period. - ** We need to compute it and store it in our ForwardAPY structure. - **********************************************************************************************/ - if isCurveVault(allStrategiesForVault) { - forwardAPY := computeCurveLikeForwardAPY( - vault, - allStrategiesForVault, - gauges, - pools, - subgraphData, - fraxPools, - ) - if forwardAPY.NetAPY != nil { - vaultAPY.ForwardAPY = forwardAPY - } - } - - /********************************************************************************************** - ** If it's a Velo Vault (has a Velo or Aero strategy), we can estimate the forward APY, aka - ** the expected APY we will get for the upcoming period. - ** We need to compute it and store it in our ForwardAPY structure. - **********************************************************************************************/ - if veloPool, ok := isVeloVault(chainID, vault); ok { - vaultAPY.ForwardAPY = computeVeloLikeForwardAPY( - vault, - allStrategiesForVault, - veloPool, - ) - } - if aeroPool, ok := isAeroVault(chainID, vault); ok { - vaultAPY.ForwardAPY = computeVeloLikeForwardAPY( - vault, - allStrategiesForVault, - aeroPool, - ) - } - - /********************************************************************************************** - ** If it's a Gamma Vault, we can get the feeAPR as an estimate for the upcoming period, and we - ** can retrieve the extraReward APRs. - **********************************************************************************************/ - if isGammaVault(chainID, vault) { - if _, extaRewardAPY, ok := calculateGammaExtraRewards(chainID, vault.AssetAddress); ok { - vaultAPY.Extra.GammaRewardAPY = extaRewardAPY - } - vaultAPY.ForwardAPY = computeGammaForwardAPY( - vault, - allStrategiesForVault, - ) - vaultAPY.ForwardAPY.Composite.RewardsAPY = vaultAPY.Extra.GammaRewardAPY - } - - if isPendleVault(chainID, vault) { - vaultAPY.ForwardAPY = computePendleForwardAPY( - vault, - allStrategiesForVault, - ) + kongForwardAPY, _ := convertKongEstimatedAprToForwardAPY(chainID, vault.Address) + if isForwardAPYAllZeros(kongForwardAPY) { + kongForwardAPY.Type = "" } + vaultAPY.ForwardAPY = kongForwardAPY + safeSyncMap(COMPUTED_APY, chainID).Store(vault.Address, vaultAPY) computedAPYData[vault.Address] = vaultAPY