Skip to content

fix(staking): compute effective cuts on active stake and delegation - #340

Merged
juanmardefago merged 1 commit into
graphprotocol:masterfrom
graphops:fix/effective-cut-thawing-delegation
Nov 10, 2026
Merged

juanmardefago merged 1 commit into
graphprotocol:masterfrom
graphops:fix/effective-cut-thawing-delegation

Conversation

@chriswessels

Copy link
Copy Markdown
Contributor

Effective cut counted thawing delegation

Root cause

All line numbers refer to master (b572f8b) unless they say otherwise.

The Horizon stake ratios used gross amounts. delegatedStakeRatio feeds both effective cuts, so the cuts were wrong too.

Function Problem on master
calculateOwnStakeRatioForProvision, calculateDelegatedStakeRatioForProvision (src/mappings/helpers/helpers.ts:1115-1127) Used tokensProvisioned + delegatedTokens. That counts delegatedThawingTokens and tokensThawing.
calculateOverdelegationDilutionForProvision (helpers.ts:1156) Same gross amounts.
calculateOwnStakeRatio (helpers.ts:1013), Indexer level Used stakedTokens - lockedTokens and gross delegatedTokens. It did not subtract delegatedThawingTokens or provision thawingTokens.
calculateOverdelegationDilution (helpers.ts:1079), Indexer level Used raw stakedTokens (without even subtracting lockedTokens) and gross delegatedTokens.
  • Effective cuts: calculate{IndexingReward,QueryFee}EffectiveCut[ForProvision] and the legacy variants compute 1 − delegatorCut / delegatedStakeRatio. They inherit the error.
  • Delegation ratios: calculateCapacitiesHorizon and updateDelegationExchangeRate* already subtracted thawing amounts. Only the "advanced metrics" did not.
  • Provider stake: this was also affected, at both the Provision and the Indexer level. tokensThawing and thawingTokens were never subtracted.

Fix

The helpers are in src/mappings/helpers/helpers.ts on the branch:

  • getActiveProvisionTokens (1128) returns tokensProvisioned − tokensThawing.
  • getActiveProvisionDelegation (1134) returns delegatedTokens − delegatedThawingTokens.
  • getActiveIndexerStake (1015) returns stakedTokens − lockedTokens − thawingTokens.
  • getActiveIndexerDelegation (1021) returns delegatedTokens − delegatedThawingTokens.

The ratio and dilution functions now use these helpers. The result is effectiveCut = 1 − delegatorCut / (D_active / (P_active + D_active)).

What stays the same:

  • With zero active delegation, delegatedStakeRatio is 0 and the effective cut is 0, as before.
  • The Indexer delegationRatio cap and the over-delegation dilution formulas are unchanged. Only their inputs changed.
  • No schema change.

Handlers touched (src/mappings/horizonStaking.ts)

These handlers already recalculated, and still do:

  • handleDelegationFeeCutSet
  • handleTokensToDelegationPoolAdded
  • handleTokensDelegated
  • handleTokensUndelegated
  • handleDelegationSlashed (Provision only)

These handlers changed thawing or stake state without recalculating. Each now calls updateAdvancedProvisionMetrics and/or updateAdvancedIndexerMetrics:

Handler Provision Indexer
handleProvisionCreated yes yes
handleProvisionIncreased yes yes
handleProvisionThawed yes yes
handleTokensDeprovisioned yes yes
handleProvisionSlashed yes yes
handleDelegationSlashed already did added
handleDelegatedTokensWithdrawn yes (active amounts don't change, so this keeps the values consistent) yes
handleHorizonStakeDeposited, handleHorizonStakeLocked, handleHorizonStakeWithdrawn – yes

Test evidence

  • Added tests/horizonStaking.test.ts and tests/factories/horizonStaking.ts. These are the first Horizon handler tests in the repo.
  • The test replays the reproduction through the handlers: provision 11,278,919 GRT; 1,439,299 GRT delegated; 36,201 GRT undelegated; both cuts 900000.
Check Expected value
Provision indexingRewardEffectiveCut and queryFeeEffectiveCut ≈ 0.096142
Indexer-level effective cuts ≈ 0.096142
Same, after DelegatedTokensWithdrawn unchanged
Same, after ProvisionThawed of 1M GRT recalculated to ≈ 0.167413
Same, after TokensDeprovisioned of that 1M unchanged
All delegation thawing delegatedStakeRatio 0, effective cut 0

Horizon keeps undelegated tokens in the delegation pool (delegatedThawingTokens)
until they are withdrawn, but rewards and query fees added to the pool only go
to active shares. Thawing provision tokens likewise no longer back the service
provider. The stake ratios, effective cuts and over-delegation dilution were
computed from gross amounts, which overstated the indexer's effective cut
(e.g. 0.1164 instead of 0.0961 for a provision with 2.5% of its delegation
thawing).

Compute them on delegatedTokens - delegatedThawingTokens and
tokensProvisioned - tokensThawing (stakedTokens - lockedTokens - thawingTokens
at the indexer level), and recalculate the metrics in every Horizon handler
that changes provisioned, thawing, staked or delegated amounts, not only when
delegation is added.
@juanmardefago

Copy link
Copy Markdown
Contributor

Changes look good! Nice catch.

Thanks to this report I also found 2 extra changes required to line everything up correctly, but we can safely merge this once the test workflows finish running.

@juanmardefago
juanmardefago merged commit fd5b03b into graphprotocol:master Nov 10, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants