feat: cumulative factors and delegator shares for O(1) stake computation#217
feat: cumulative factors and delegator shares for O(1) stake computation#217adamsoffer wants to merge 7 commits into
Conversation
…cient historical stake computation Store cumulativeRewardFactor and cumulativeFeeFactor on Pool entity (matching on-chain PreciseMathUtils), propagate them forward each round, and introduce a shares field on Delegator that enables O(1) stake lookups via `stake = shares * crf[round] / 10^27`. DelegatorSnapshot entities capture state at bond/unbond/rebond events for time-series chart support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ommission Tracks the orchestrator's total rewardCut commission across all rounds, incremented each time reward() is called. Never resets, so clients can read lifetime earnings in a single field. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ission Resets Transcoder.cumulativeRewards to zero when the orchestrator claims earnings, so the field represents pending/unclaimed commission rather than lifetime total. This lets clients compute full orchestrator pending stake as: shares * crf / 10^27 + cumulativeRewards Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Separate never-reset counter alongside cumulativeRewards (which resets on claim). Gives clients a single field for lifetime orchestrator commission without summing historical claim events. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename cumulativeRewards/lifetimeRewards to pendingRewardCommission/ lifetimeRewardCommission for clarity. Add pendingFeeCommission and lifetimeFeeCommission on Transcoder, computed in WinningTicketRedeemed handler. Both pending fields reset on claim. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous implementation did two divisions (num * 10^27 / denom, then base * result / 10^27), causing intermediate truncation that compounded each round in the CRF calculation. Solidity's version does one division (base * num / denom). While the CRF ratio error cancelled out for delegator stake, pendingRewardCommission accumulated ~0.23 LPT/round drift. This fix ensures exact match with the contract. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The contract's cumulativeRewards includes two components: the rewardCut commission plus rewards earned by the transcoder's own staked commission (activeCumulativeRewards). The subgraph was only tracking the first. Add activeCumulativeRewards field on Transcoder, snapshotted from pendingRewardCommission at the start of each round in newRound. The reward handler now computes both components matching the contract's updateTranscoderWithRewards logic. Reset on claim. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 39 minutes and 17 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
3c1cb89 to
91eb05d
Compare
Problem
The subgraph has no way to compute a delegator's actual total stake. The
bondedAmountfield only reflects the value at the time of the last claim and does not include rewards earned since then. To get a delegator's current stake, clients must call the contract'spendingStake()method directly — and for historical stake at a past round, there is no solution at all.Similarly, computing an orchestrator's full pending stake (including their unclaimed commission) or their lifetime earnings requires fetching every pool since their last claim and summing across all of them.
This also means there's no way to build time-series charts of delegator stake, show per-round reward breakdowns, or compute orchestrator yield — all of which require knowing stake values across rounds.
Summary
cumulativeRewardFactorandcumulativeFeeFactoron thePoolentity, matching on-chain PreciseMathUtils (27-decimal fixed-point)reward()calls)sharesfield toDelegator(bondedAmount * 10^27 / crf[lastClaimRound]) — invariant across claims, only changes on bond/unbond/rebondDelegatorSnapshotentity capturing delegator state at each bond/unbond/rebond for time-series chart supportWinningTicketRedeemedhandler using previous round's CRF (matching contract behavior)Transcoder:pendingRewardCommission— unclaimed reward commission (rewardCut portion), resets on claimlifetimeRewardCommission— total reward commission ever earned, never resetspendingFeeCommission— unclaimed fee commission, resets on claimlifetimeFeeCommission— total fee commission ever earned, never resetsWhat this unlocks
Current pending stake without contract calls — A delegator's up-to-date total stake (equivalent to
pendingStake()on-chain) can now be computed entirely from the subgraph:stake = shares * crf / 10^27. For orchestrators specifically, their full pending stake includes unclaimed commission:stake = shares * crf / 10^27 + pendingRewardCommission. No contract calls needed.Historical stake at any past round — Something previously impossible. Query the delegator's shares (or the relevant
DelegatorSnapshot) and thePool.cumulativeRewardFactorfor that round, then computeshares * crf[round] / 10^27.Per-round reward and fee breakdowns — Rewards earned in a specific round can be derived by comparing cumulative factors between consecutive rounds:
rewards = shares * (crf[round] - crf[round-1]) / 10^27. Same pattern applies to fees usingcumulativeFeeFactor.Time-series charts of delegator stake — The
sharesapproach combined withDelegatorSnapshotentities makes it straightforward to build a chart showing a delegator's total stake over time. Between snapshots, shares are constant, so stake at any round is justshares * crf[round] / 10^27. When a bond/unbond/rebond occurs, a new snapshot captures the updated shares value to use going forward.Orchestrator commission tracking —
pendingRewardCommissionandpendingFeeCommissiongive the orchestrator's unclaimed commission in a single field each, without needing to sum across pools.lifetimeRewardCommissionandlifetimeFeeCommissionprovide total lifetime earnings.Orchestrator pool analytics — Cumulative factors stored per-pool per-round enable computing total rewards distributed, effective yield, and fee revenue for any orchestrator over any time range without replaying events.
All changes are additive — no breaking changes to existing queries.
Test plan
npx graph codegen && npx graph build— verified locallyshares * crf[round] / 10^27matches actual bonded amountspendingStake()return valuependingRewardCommissionandpendingFeeCommissionreset to zero after orchestrator claimslifetimeRewardCommissionandlifetimeFeeCommissionnever reset🤖 Generated with Claude Code