-
Notifications
You must be signed in to change notification settings - Fork 47
Description
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
delegationExchangeRateto 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 andProvision-level exchange rates. - The field
delegatedThawingTokensis 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
undelegatefor all 1000 shares. The eventTokensUndelegatedfires withshares = 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 GRTinstead of the correct1000 GRT. - Only when Delegator A calls
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.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.
2. Fix field update order in handleTokensUndelegated
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 lateFixed 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)
}