Float64 Precision Loss in Reward Distribution Calculations
Summary
The supernode reward distribution uses float64 for weight calculations, which can introduce precision loss in financial calculations. While safeguards exist, this could lead to minor discrepancies in reward distribution.
Vulnerability Details
File: x/supernode/v1/keeper/distribution.go
Lines: 184-216
// Line 184-187: Calculate total weight using float64
var totalWeight float64
for _, c := range candidates {
totalWeight += c.effectiveWeight
}
// Line 197-200: Convert to decimal
totalWeightDec, err := legacyDecFromFloat64(totalWeight)
if err != nil {
return fmt.Errorf("invalid total distribution weight: %w", err)
}
// Line 209-216: Calculate share using decimal
for _, c := range candidates {
weightDec, err := legacyDecFromFloat64(c.effectiveWeight)
if err != nil {
k.Logger().Error("invalid candidate distribution weight", "validator", c.validatorAddr, "err", err)
continue
}
shareDec := weightDec.Quo(totalWeightDec)
payoutAmount := poolBalanceDec.MulTruncate(shareDec).TruncateInt()
// ...
}
Impact
Precision Loss
float64 has ~15-17 significant decimal digits
- Large reward pools or many supernodes could accumulate rounding errors
- Example: Pool of 1,000,000 ULUME distributed to 1000 supernodes
- Each share: ~1000 ULUME
- Potential error: ±0.0001 ULUME per calculation
- Total error: ±0.1 ULUME (minor but non-zero)
Accumulation Over Time
- Distribution happens every
payment_period_blocks
- Small errors accumulate across multiple distribution periods
- Over months/years, could lead to noticeable discrepancies
Fairness Concerns
- Some supernodes may receive slightly more/less than their fair share
- While individually small, systematic bias could favor certain participants
Mitigating Factors
- NaN/Inf Protection:
legacyDecFromFloat64() validates input (lines 298-303)
- Decimal Conversion: Converts float to
sdkmath.LegacyDec for precise arithmetic
- Truncation: Uses
MulTruncate() which is deterministic
- Dust Handling: Leftover dust stays in pool (intentional design)
Proof of Concept
// Simulate precision loss
pool := 1000000000 // 1B ULUME
weights := []float64{1.0/3.0, 1.0/3.0, 1.0/3.0}
totalWeight := 0.0
for _, w := range weights {
totalWeight += w
}
// totalWeight = 0.9999999999999999 (not 1.0)
// Each share calculation:
for _, w := range weights {
share := w / totalWeight
payout := float64(pool) * share
// payout = 333333333.3333333...
// Truncated to: 333333333
}
// Total distributed: 999999999
// Dust remaining: 1 ULUME
While this example shows minimal impact, real-world scenarios with:
- More supernodes (100+)
- Complex weight calculations (EMA smoothing, growth caps, ramp-up)
- Multiple distribution periods
Could amplify the precision loss.
Recommended Fix
Option 1: Use integer arithmetic throughout (Recommended)
// Store weights as integers (basis points or similar)
type snCandidate struct {
// ...
effectiveWeightBps uint64 // Use basis points (10000 = 100%)
}
// Calculate using integer math
var totalWeightBps uint64
for _, c := range candidates {
totalWeightBps += c.effectiveWeightBps
}
for _, c := range candidates {
share := sdkmath.LegacyNewDec(int64(c.effectiveWeightBps)).
Quo(sdkmath.LegacyNewDec(int64(totalWeightBps)))
payoutAmount := poolBalanceDec.MulTruncate(share).TruncateInt()
// ...
}
Option 2: Use decimal from the start
// Avoid float64 entirely
type snCandidate struct {
// ...
effectiveWeight sdkmath.LegacyDec
}
// Calculate using decimal
totalWeight := sdkmath.LegacyZeroDec()
for _, c := range candidates {
totalWeight = totalWeight.Add(c.effectiveWeight)
}
Severity
INFORMATIONAL - Precision loss is minimal due to safeguards, but best practice for financial calculations is to use integer/decimal arithmetic throughout.
Additional Context
This is a common pattern in Cosmos SDK modules where float64 is used for intermediate calculations. While not critical, it's worth noting for production financial systems. The current implementation has adequate safeguards, but could be improved for maximum precision.
References
Float64 Precision Loss in Reward Distribution Calculations
Summary
The supernode reward distribution uses
float64for weight calculations, which can introduce precision loss in financial calculations. While safeguards exist, this could lead to minor discrepancies in reward distribution.Vulnerability Details
File:
x/supernode/v1/keeper/distribution.goLines: 184-216
Impact
Precision Loss
float64has ~15-17 significant decimal digitsAccumulation Over Time
payment_period_blocksFairness Concerns
Mitigating Factors
legacyDecFromFloat64()validates input (lines 298-303)sdkmath.LegacyDecfor precise arithmeticMulTruncate()which is deterministicProof of Concept
While this example shows minimal impact, real-world scenarios with:
Could amplify the precision loss.
Recommended Fix
Option 1: Use integer arithmetic throughout (Recommended)
Option 2: Use decimal from the start
Severity
INFORMATIONAL - Precision loss is minimal due to safeguards, but best practice for financial calculations is to use integer/decimal arithmetic throughout.
Additional Context
This is a common pattern in Cosmos SDK modules where float64 is used for intermediate calculations. While not critical, it's worth noting for production financial systems. The current implementation has adequate safeguards, but could be improved for maximum precision.
References