Skip to content

fix: carry cumulative factors across round gaps (fixes CRF reset from #249) - #2

Closed
adamsoffer wants to merge 1 commit into
claude/magical-tereshkovafrom
fix/pool-cumulative-factor-reset
Closed

fix: carry cumulative factors across round gaps (fixes CRF reset from #249)#2
adamsoffer wants to merge 1 commit into
claude/magical-tereshkovafrom
fix/pool-cumulative-factor-reset

Conversation

@adamsoffer

Copy link
Copy Markdown
Owner

Summary

Fixes a cumulativeRewardFactor corruption that under-reports delegator stake. Targets this feature branch (claude/magical-tereshkova) because that's the only branch with the cumulative-factor schema — main doesn't have these fields yet.

Where the bug was introduced

#249"fix: create missing Pool on demand to avoid null-Pool abort" (commit 00c74c1). That PR's createOrLoadPool() sets totalStake/rewardCut/feeShare but never seeds cumulativeRewardFactor/cumulativeFeeFactor, so an on-demand Pool defaults them to 0.

Failure mechanism

When a Pool is created for a round whose previous round has no Pool (transcoder inactive, or newRound()'s non-deterministic eth_call enumeration skipped it), reward() reads prevCRF = pool.cumulativeRewardFactor == 0, falls back to the base 10^27, and resets the cumulative reward factor mid-history:

let prevCRF = pool.cumulativeRewardFactor;          // 0 on the on-demand pool
if (prevCRF.equals(ZERO_BI)) prevCRF = PRECISE_PERC_DIVISOR;  // 10^27 → reset

Because pending stake is bonded * CRF[now] / CRF[lastClaim], any delegator whose lastClaimRound predates the reset straddles the discontinuity and is under-reported.

Observed on Arbitrum staging: orchestrator 0x21d1…38ff's factor dropped to base at rounds 3178 / 3192 / 3215… (all multi-round gaps — the previous round's Pool was also missing). One delegator read 91,155 LPT vs 127,746 LPT on-chain (BondingManager.pendingStake) — a 36,590 LPT shortfall. Round 3178 = 2023-11-16, so a fresh re-index corrupts long-settled history.

Fix

Seed the new Pool's factors from the most recent existing pool, walking back from round-1 down to lastRewardRound (guaranteed to have a Pool with valid factors) — mirroring the contract's latestCumulativeFactorsPool. A one-round lookback (added earlier on this branch) was insufficient because the gaps span multiple rounds, so the immediately previous round's Pool is itself missing.

The walk is bounded by lastRewardRound and only iterates when round-1's Pool is absent (i.e. reactivation), so the common active-transcoder path stays a single load.

Verification

  • yarn codegen && yarn build pass.
  • After redeploy + resync, factors should match on-chain getTranscoderEarningsPoolForRound(...) and the delegator total should equal on-chain pendingStake (213,713 LPT for the affected set).

Note

The deterministic root cause of the missing Pools (newRound() enumeration) is tracked in livepeer#248; this change makes the on-demand fallback correct regardless.

🤖 Generated with Claude Code

The on-demand Pool creation added in livepeer#249 never seeded cumulativeRewardFactor
/cumulativeFeeFactor, so a newly created Pool defaulted them to 0. When a
Pool was created for a round whose previous round had no Pool (transcoder
inactive, or newRound's non-deterministic enumeration skipped it), reward()
saw prevCRF == 0, fell back to the base 10^27, and RESET the cumulative
reward factor mid-history.

That reset corrupts stake for any delegator whose lastClaimRound predates
the gap: pendingStake = bonded * CRF[now] / CRF[lastClaim] straddles the
break and under-reports. Observed on Arbitrum staging: an orchestrator's
factor dropped to base at rounds 3178/3192/3215… (multi-round gaps where
even the previous round's Pool was missing), under-reporting one delegator
by ~36.6k LPT versus on-chain BondingManager.pendingStake.

Fix: seed the new Pool's factors from the most recent EXISTING pool, walking
back from round-1 to lastRewardRound (guaranteed to have a Pool with valid
factors) — mirroring the contract's latestCumulativeFactorsPool. The prior
one-round lookback was insufficient because the gaps span multiple rounds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@adamsoffer

Copy link
Copy Markdown
Owner Author

Folded directly into PR livepeer#217's branch (claude/magical-tereshkova, commit f3774ac) so the fix is part of livepeer#217 rather than a separate stacked PR.

@adamsoffer adamsoffer closed this Aug 11, 2026
@adamsoffer
adamsoffer deleted the fix/pool-cumulative-factor-reset branch August 11, 2026 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant