Skip to content

Indexer.delegatedTokens not in sync with contract delegation.tokens in Horizon mode #323

@madumas

Description

@madumas

Description

In Horizon mode, the Indexer.delegatedTokens field in the subgraph does not match the actual delegation.tokens value from the HorizonStaking contract's delegation pool. This causes tokenCapacity to be underestimated.

Observed Behavior

For indexer 0xf92f430dd8567b0d466358c79594ab58d919a6d4:

Source Value (GRT)
Subgraph Indexer.delegatedTokens 116,157,557.38
Contract getDelegationPool().tokens 117,126,781.76
Difference 969,224.39 (~0.83%)

This propagates to tokenCapacity:

Source Value (GRT)
Subgraph tokenCapacity 122,223,783.22
Contract getTokensAvailable() 123,193,007.61
Difference 969,224.39

Root Cause Analysis

The subgraph calculates delegationExchangeRate = delegatedTokens / delegatorShares.

Comparing exchange rates:

  • Subgraph: 1.772679126585520324
  • Contract (actual): 1.787470448629024056

The subgraph's exchange rate is stale. With 65.5M shares, this rate difference of ~0.0148 produces the ~969k GRT discrepancy.

Hypothesis

In Horizon mode, rewards (indexing rewards + query fees) accumulate in the delegation pool and increase delegation.tokens in the contract. The subgraph updates delegatedTokens via TokensToDelegationPoolAdded events in handleTokensToDelegationPoolAdded, but it appears that:

  1. Some reward accumulation events may not be triggering the update, OR
  2. The Indexer.delegatedTokens is not being updated when Provision.delegatedTokens changes, OR
  3. There's a timing/synchronization issue between when rewards are added to the contract pool and when corresponding events are emitted

Steps to Reproduce

  1. Query the subgraph for an active indexer with delegations:
{
  indexer(id: "0xf92f430dd8567b0d466358c79594ab58d919a6d4") {
    delegatedTokens
    delegatorShares
    delegationExchangeRate
    tokenCapacity
  }
}
  1. Query the contract directly:
# Get delegation pool
cast call 0x00669A4CF01450B64E8A2A20E9b1FCB71E61eF03 \
  "getDelegationPool(address,address)((uint256,uint256,uint256,uint256,uint256))" \
  0xf92f430dd8567b0d466358c79594ab58d919a6d4 \
  0xb2Bb92d0DE618878E438b55D5846cfecD9301105 \
  --rpc-url https://arb1.arbitrum.io/rpc

# Get tokens available
cast call 0x00669A4CF01450B64E8A2A20E9b1FCB71E61eF03 \
  "getTokensAvailable(address,address,uint32)(uint256)" \
  0xf92f430dd8567b0d466358c79594ab58d919a6d4 \
  0xb2Bb92d0DE618878E438b55D5846cfecD9301105 \
  16 \
  --rpc-url https://arb1.arbitrum.io/rpc
  1. Compare delegatedTokens from subgraph with delegation.tokens from contract

Expected Behavior

Indexer.delegatedTokens should match the contract's getDelegationPool().tokens value (minus any thawing tokens that are excluded).

Impact

  • tokenCapacity is underestimated in the subgraph
  • availableStake calculations are incorrect
  • Any tooling relying on subgraph data for capacity planning will have inaccurate data

Environment

  • Network: Arbitrum One
  • Subgraph: Graph Network Subgraph
  • Contracts: HorizonStaking at 0x00669A4CF01450B64E8A2A20E9b1FCB71E61eF03
  • SubgraphService at 0xb2Bb92d0DE618878E438b55D5846cfecD9301105

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions