Skip to content

Float64 Precision Loss in Reward Distribution Calculations #208

Description

@xxcode66-source

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

  1. NaN/Inf Protection: legacyDecFromFloat64() validates input (lines 298-303)
  2. Decimal Conversion: Converts float to sdkmath.LegacyDec for precise arithmetic
  3. Truncation: Uses MulTruncate() which is deterministic
  4. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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