Description
In the handleTokensUndelegated handler in src/mappings/horizonStaking.ts, when a delegator undelegates, delegatorShares is decreased immediately but delegatedTokens is not. The tokens are only subtracted from delegatedTokens later, when handleDelegatedTokensWithdrawn fires.
However, delegationExchangeRate is recalculated right after shares are reduced, using the formula:
delegationExchangeRate = delegatedTokens / delegatorShares
Since delegatedTokens remains unchanged while delegatorShares has decreased, the resulting exchange rate is artificially inflated for the entire duration between the undelegate and withdraw events (i.e., the full thawing period).
Impact
- Any consumer of the subgraph that relies on
delegationExchangeRate to compute the current value of a delegation position will overestimate the value of remaining delegators' positions during the thawing period.
- This affects both the
Indexer-level and Provision-level exchange rates.
- The field
delegatedThawingTokens is correctly tracked and incremented during undelegation, but it is not used in the exchange rate calculation.
Steps to Reproduce
- Delegator A and Delegator B each delegate 1000 GRT to an indexer (assume 1:1 exchange rate, 1000 shares each).
- Pool state:
delegatedTokens = 2000, delegatorShares = 2000, exchangeRate = 1.0.
- Delegator A calls
undelegate for all 1000 shares. The event TokensUndelegated fires with shares = 1000, tokens = 1000.
- After the handler runs:
delegatorShares = 1000 (reduced)
delegatedTokens = 2000 (unchanged)
delegatedThawingTokens = 1000 (increased)
delegationExchangeRate = 2000 / 1000 = 2.0 (inflated)
- Delegator B's position now appears to be worth
1000 shares * 2.0 rate = 2000 GRT instead of the correct 1000 GRT.
- Only when Delegator A calls
withdraw and DelegatedTokensWithdrawn fires does delegatedTokens drop to 1000 and the rate normalizes back to 1.0.
Expected Behavior
The exchange rate should reflect the actual claimable value per share. Tokens that are pending withdrawal (thawing) are no longer backed by any shares and should be excluded from the exchange rate calculation.
Suggested Fix
1. Fix exchange rate formulas in src/mappings/helpers/helpers.ts
The updateDelegationExchangeRate and updateDelegationExchangeRateForProvision functions in src/mappings/helpers/helpers.ts should subtract delegatedThawingTokens from delegatedTokens before dividing by shares:
export function updateDelegationExchangeRate(indexer: Indexer): Indexer {
indexer.delegationExchangeRate = indexer.delegatedTokens
.minus(indexer.delegatedThawingTokens)
.toBigDecimal()
.div(indexer.delegatorShares.toBigDecimal())
.truncate(18)
return indexer as Indexer
}
export function updateDelegationExchangeRateForProvision(provision: Provision): Provision {
provision.delegationExchangeRate = provision.delegatedTokens
.minus(provision.delegatedThawingTokens)
.toBigDecimal()
.div(provision.delegatorShares.toBigDecimal())
.truncate(18)
return provision as Provision
}
This ensures that tokens in the thawing state — which are already "claimed" by the undelegating participant and no longer represented by any shares — do not inflate the exchange rate for remaining pool participants.
Current code (provision):
provision.delegatorShares = provision.delegatorShares.minus(event.params.shares)
if (provision.delegatorShares != BigInt.fromI32(0)) {
provision = updateDelegationExchangeRateForProvision(provision as Provision)
}
provision.delegatedThawingTokens = provision.delegatedThawingTokens.plus(event.params.tokens) // too late
Fixed code (provision):
provision.delegatorShares = provision.delegatorShares.minus(event.params.shares)
provision.delegatedThawingTokens = provision.delegatedThawingTokens.plus(event.params.tokens) // moved before rate calc
if (provision.delegatorShares != BigInt.fromI32(0)) {
provision = updateDelegationExchangeRateForProvision(provision as Provision)
}
Description
In the
handleTokensUndelegatedhandler insrc/mappings/horizonStaking.ts, when a delegator undelegates,delegatorSharesis decreased immediately butdelegatedTokensis not. The tokens are only subtracted fromdelegatedTokenslater, whenhandleDelegatedTokensWithdrawnfires.However,
delegationExchangeRateis recalculated right after shares are reduced, using the formula:delegationExchangeRate = delegatedTokens / delegatorSharesSince
delegatedTokensremains unchanged whiledelegatorShareshas decreased, the resulting exchange rate is artificially inflated for the entire duration between theundelegateandwithdrawevents (i.e., the full thawing period).Impact
delegationExchangeRateto compute the current value of a delegation position will overestimate the value of remaining delegators' positions during the thawing period.Indexer-level andProvision-level exchange rates.delegatedThawingTokensis correctly tracked and incremented during undelegation, but it is not used in the exchange rate calculation.Steps to Reproduce
delegatedTokens = 2000,delegatorShares = 2000,exchangeRate = 1.0.undelegatefor all 1000 shares. The eventTokensUndelegatedfires withshares = 1000,tokens = 1000.delegatorShares = 1000(reduced)delegatedTokens = 2000(unchanged)delegatedThawingTokens = 1000(increased)delegationExchangeRate = 2000 / 1000 = 2.0(inflated)1000 shares * 2.0 rate = 2000 GRTinstead of the correct1000 GRT.withdrawandDelegatedTokensWithdrawnfires doesdelegatedTokensdrop to 1000 and the rate normalizes back to1.0.Expected Behavior
The exchange rate should reflect the actual claimable value per share. Tokens that are pending withdrawal (thawing) are no longer backed by any shares and should be excluded from the exchange rate calculation.
Suggested Fix
1. Fix exchange rate formulas in
src/mappings/helpers/helpers.tsThe
updateDelegationExchangeRateandupdateDelegationExchangeRateForProvisionfunctions insrc/mappings/helpers/helpers.tsshould subtractdelegatedThawingTokensfromdelegatedTokensbefore dividing by shares:This ensures that tokens in the thawing state — which are already "claimed" by the undelegating participant and no longer represented by any shares — do not inflate the exchange rate for remaining pool participants.
2. Fix field update order in handleTokensUndelegated
Current code (provision):
Fixed code (provision):