-
Notifications
You must be signed in to change notification settings - Fork 6
feat: cumulative factors and delegator shares for O(1) stake computation #217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4f7484b
f1dd792
064d40f
af4952d
3926bcc
dcc2755
7b101df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import { store } from "@graphprotocol/graph-ts"; | ||
| import { | ||
| convertFromDecimal, | ||
| convertToDecimal, | ||
| createOrLoadDelegator, | ||
| createOrLoadPool, | ||
|
|
@@ -10,9 +11,13 @@ import { | |
| EMPTY_ADDRESS, | ||
| getBlockNum, | ||
| makeEventId, | ||
| makePoolId, | ||
| makeUnbondingLockId, | ||
| MAXIMUM_VALUE_UINT256, | ||
| ONE_BI, | ||
| percOf, | ||
| PRECISE_PERC_DIVISOR, | ||
| precisePercOf, | ||
| ZERO_BI, | ||
| } from "../../utils/helpers"; | ||
| // Import event types from the registrar contract ABIs | ||
|
|
@@ -34,8 +39,10 @@ import { | |
| } from "../types/BondingManager/BondingManager"; | ||
| import { | ||
| BondEvent, | ||
| DelegatorSnapshot, | ||
| EarningsClaimedEvent, | ||
| ParameterUpdateEvent, | ||
| Pool, | ||
| RebondEvent, | ||
| RewardEvent, | ||
| TranscoderActivatedEvent, | ||
|
|
@@ -134,12 +141,39 @@ export function bond(event: Bond): void { | |
| convertToDecimal(event.params.additionalAmount) | ||
| ); | ||
|
|
||
| // Compute shares: bondedAmount * 10^27 / crf[lastClaimRound] | ||
| // shares is invariant across claims, only changes on bond/unbond | ||
| let poolForShares = Pool.load( | ||
| makePoolId(event.params.newDelegate.toHex(), round.id) | ||
| ); | ||
| let sharesRefCRF = PRECISE_PERC_DIVISOR; | ||
| if ( | ||
| poolForShares && | ||
| !poolForShares.cumulativeRewardFactor.equals(ZERO_BI) | ||
| ) { | ||
| sharesRefCRF = poolForShares.cumulativeRewardFactor; | ||
| } | ||
| delegator.shares = event.params.bondedAmount | ||
| .times(PRECISE_PERC_DIVISOR) | ||
| .div(sharesRefCRF); | ||
|
|
||
| round.save(); | ||
| delegate.save(); | ||
| delegator.save(); | ||
| transcoder.save(); | ||
| protocol.save(); | ||
|
|
||
| // Save delegator snapshot for historical stake/reward computation | ||
| let snapshotId = event.params.delegator.toHex() + "-" + round.id; | ||
| let snapshot = new DelegatorSnapshot(snapshotId); | ||
| snapshot.delegator = event.params.delegator.toHex(); | ||
| snapshot.delegate = event.params.newDelegate.toHex(); | ||
| snapshot.bondedAmount = delegator.bondedAmount; | ||
| snapshot.shares = delegator.shares; | ||
| snapshot.round = round.id; | ||
| snapshot.timestamp = event.block.timestamp.toI32(); | ||
| snapshot.save(); | ||
|
Comment on lines
+166
to
+175
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Snapshot id collision on multiple same-round events for a delegator.
🤖 Prompt for AI Agents |
||
|
|
||
| createOrLoadTransactionFromEvent(event); | ||
|
|
||
| let bondEvent = new BondEvent( | ||
|
|
@@ -259,6 +293,25 @@ export function unbond(event: Unbond): void { | |
| convertToDecimal(event.params.amount) | ||
| ); | ||
|
|
||
| // Compute shares from new bonded amount | ||
| if (delegatorData.value0.isZero()) { | ||
| delegator.shares = ZERO_BI; | ||
| } else { | ||
| let poolForShares = Pool.load( | ||
| makePoolId(event.params.delegate.toHex(), round.id) | ||
| ); | ||
| let sharesRefCRF = PRECISE_PERC_DIVISOR; | ||
| if ( | ||
| poolForShares && | ||
| !poolForShares.cumulativeRewardFactor.equals(ZERO_BI) | ||
| ) { | ||
| sharesRefCRF = poolForShares.cumulativeRewardFactor; | ||
| } | ||
| delegator.shares = delegatorData.value0 | ||
| .times(PRECISE_PERC_DIVISOR) | ||
| .div(sharesRefCRF); | ||
| } | ||
|
|
||
| // Delegator no longer delegated to anyone if it does not have a bonded amount | ||
| // so remove it from delegate | ||
| if (delegatorData.value0.isZero()) { | ||
|
|
@@ -291,6 +344,17 @@ export function unbond(event: Unbond): void { | |
| protocol.save(); | ||
| round.save(); | ||
|
|
||
| // Save delegator snapshot for historical stake/reward computation | ||
| let snapshotId = event.params.delegator.toHex() + "-" + round.id; | ||
| let snapshot = new DelegatorSnapshot(snapshotId); | ||
| snapshot.delegator = event.params.delegator.toHex(); | ||
| snapshot.delegate = delegator.delegate; | ||
| snapshot.bondedAmount = delegator.bondedAmount; | ||
| snapshot.shares = delegator.shares; | ||
| snapshot.round = round.id; | ||
| snapshot.timestamp = event.block.timestamp.toI32(); | ||
| snapshot.save(); | ||
|
|
||
| createOrLoadTransactionFromEvent(event); | ||
|
|
||
| let unbondEvent = new UnbondEvent( | ||
|
|
@@ -351,6 +415,21 @@ export function rebond(event: Rebond): void { | |
| delegator.bondedAmount = convertToDecimal(delegatorData.value0); | ||
| delegator.fees = convertToDecimal(delegatorData.value1); | ||
|
|
||
| // Compute shares: bondedAmount * 10^27 / crf[lastClaimRound] | ||
| let poolForShares = Pool.load( | ||
| makePoolId(event.params.delegate.toHex(), round.id) | ||
| ); | ||
| let sharesRefCRF = PRECISE_PERC_DIVISOR; | ||
| if ( | ||
| poolForShares && | ||
| !poolForShares.cumulativeRewardFactor.equals(ZERO_BI) | ||
| ) { | ||
| sharesRefCRF = poolForShares.cumulativeRewardFactor; | ||
| } | ||
| delegator.shares = delegatorData.value0 | ||
| .times(PRECISE_PERC_DIVISOR) | ||
| .div(sharesRefCRF); | ||
|
|
||
| // If the sender field for the lock is equal to the delegator's address then | ||
| // we know that this is an unbonding lock the delegator created by calling | ||
| // unbond() and if it is not then we know that this is an unbonding lock created | ||
|
|
@@ -371,6 +450,17 @@ export function rebond(event: Rebond): void { | |
| delegator.save(); | ||
| protocol.save(); | ||
|
|
||
| // Save delegator snapshot for historical stake/reward computation | ||
| let snapshotId = event.params.delegator.toHex() + "-" + round.id; | ||
| let snapshot = new DelegatorSnapshot(snapshotId); | ||
| snapshot.delegator = event.params.delegator.toHex(); | ||
| snapshot.delegate = event.params.delegate.toHex(); | ||
| snapshot.bondedAmount = delegator.bondedAmount; | ||
| snapshot.shares = delegator.shares; | ||
| snapshot.round = round.id; | ||
| snapshot.timestamp = event.block.timestamp.toI32(); | ||
| snapshot.save(); | ||
|
|
||
| if (unbondingLock) { | ||
| store.remove("UnbondingLock", uniqueUnbondingLockId); | ||
| } | ||
|
|
@@ -494,6 +584,44 @@ export function reward(event: Reward): void { | |
| ); | ||
| transcoder.lastRewardRound = round.id; | ||
|
|
||
| // Compute cumulative reward factor (matches on-chain PreciseMathUtils) | ||
| // The pool's CRF was propagated from the previous round during pool creation, | ||
| // so it already contains the correct previous cumulative reward factor. | ||
| let prevCRF = pool.cumulativeRewardFactor; | ||
| if (prevCRF.equals(ZERO_BI)) { | ||
| prevCRF = PRECISE_PERC_DIVISOR; // default: 10^27 = percPoints(1,1) | ||
| } | ||
|
|
||
| let totalRewardTokens = event.params.amount; // raw BigInt in wei | ||
| let transcoderCommission = percOf(totalRewardTokens, pool.rewardCut); | ||
| let delegatorsRewards = totalRewardTokens.minus(transcoderCommission); | ||
|
|
||
| // Compute rewards earned by the transcoder's own staked commission | ||
| let totalStakeBI = convertFromDecimal(pool.totalStake); | ||
| let transcoderRewardStakeRewards = ZERO_BI; | ||
| if (totalStakeBI.gt(ZERO_BI)) { | ||
| transcoderRewardStakeRewards = precisePercOf( | ||
| delegatorsRewards, | ||
| transcoder.activeCumulativeRewards, | ||
| totalStakeBI | ||
| ); | ||
| } | ||
|
|
||
| // Accumulate orchestrator reward commission (rewardCut + rewards on staked commission) | ||
| transcoder.pendingRewardCommission = transcoder.pendingRewardCommission | ||
| .plus(transcoderCommission) | ||
| .plus(transcoderRewardStakeRewards); | ||
| transcoder.lifetimeRewardCommission = transcoder.lifetimeRewardCommission | ||
| .plus(transcoderCommission) | ||
| .plus(transcoderRewardStakeRewards); | ||
| if (totalStakeBI.gt(ZERO_BI)) { | ||
| pool.cumulativeRewardFactor = prevCRF.plus( | ||
| precisePercOf(prevCRF, delegatorsRewards, totalStakeBI) | ||
| ); | ||
| } else { | ||
| pool.cumulativeRewardFactor = prevCRF; | ||
| } | ||
|
|
||
| pool.rewardTokens = convertToDecimal(event.params.amount); | ||
| pool.feeShare = transcoder.feeShare; | ||
| pool.rewardCut = transcoder.rewardCut; | ||
|
|
@@ -671,6 +799,18 @@ export function earningsClaimed(event: EarningsClaimed): void { | |
| delegator.fees = delegator.fees.plus(convertToDecimal(event.params.fees)); | ||
| delegator.save(); | ||
|
|
||
| // Reset orchestrator's unclaimed commission when they claim | ||
| if (event.params.delegator.toHex() == event.params.delegate.toHex()) { | ||
| let transcoder = createOrLoadTranscoder( | ||
| event.params.delegator.toHex(), | ||
| event.block.timestamp.toI32() | ||
| ); | ||
| transcoder.pendingRewardCommission = ZERO_BI; | ||
| transcoder.pendingFeeCommission = ZERO_BI; | ||
| transcoder.activeCumulativeRewards = ZERO_BI; | ||
| transcoder.save(); | ||
| } | ||
|
|
||
| createOrLoadTransactionFromEvent(event); | ||
|
|
||
| let earningsClaimedEvent = new EarningsClaimedEvent( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import { Address, BigInt, dataSource, log } from "@graphprotocol/graph-ts"; | ||
| import { | ||
| convertFromDecimal, | ||
| convertToDecimal, | ||
| createOrLoadBroadcaster, | ||
| createOrLoadBroadcasterDay, | ||
|
|
@@ -12,11 +13,19 @@ import { | |
| createOrLoadTranscoderDay, | ||
| getBlockNum, | ||
| getEthPriceUsd, | ||
| integerFromString, | ||
| makeEventId, | ||
| makePoolId, | ||
| ONE_BI, | ||
| percOf, | ||
| PRECISE_PERC_DIVISOR, | ||
| precisePercOf, | ||
| ZERO_BD, | ||
| ZERO_BI, | ||
| } from "../../utils/helpers"; | ||
| import { | ||
| DepositFundedEvent, | ||
| Pool, | ||
| ReserveClaimedEvent, | ||
| ReserveFundedEvent, | ||
| WinningTicketRedeemedEvent, | ||
|
|
@@ -118,8 +127,37 @@ export function winningTicketRedeemed(event: WinningTicketRedeemed): void { | |
| protocol.winningTicketCount = protocol.winningTicketCount + 1; | ||
| protocol.save(); | ||
|
|
||
| // update the transcoder pool fees | ||
| // update the transcoder pool fees and cumulative fee factor | ||
| let pool = createOrLoadPool(round.id, event.params.recipient.toHex()); | ||
|
|
||
| // Compute cumulative fee factor (matches on-chain PreciseMathUtils) | ||
| // Use previous round's CRF, matching contract's latestCumulativeFactorsPool(_round - 1) | ||
| let prevRoundNum = integerFromString(round.id).minus(ONE_BI); | ||
| let prevPoolForFees = Pool.load( | ||
| makePoolId(event.params.recipient.toHex(), prevRoundNum.toString()) | ||
| ); | ||
| let prevCRF = PRECISE_PERC_DIVISOR; // default: 10^27 | ||
| if ( | ||
| prevPoolForFees && | ||
| !prevPoolForFees.cumulativeRewardFactor.equals(ZERO_BI) | ||
| ) { | ||
| prevCRF = prevPoolForFees.cumulativeRewardFactor; | ||
| } | ||
|
Comment on lines
+135
to
+145
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Same exact round-1 lookback fragility as
🤖 Prompt for AI Agents |
||
|
|
||
| let delegatorsFees = percOf(event.params.faceValue, pool.feeShare); | ||
| let transcoderFeeCommission = event.params.faceValue.minus(delegatorsFees); | ||
|
|
||
| // Accumulate orchestrator fee commission | ||
| transcoder.pendingFeeCommission = transcoder.pendingFeeCommission.plus(transcoderFeeCommission); | ||
| transcoder.lifetimeFeeCommission = transcoder.lifetimeFeeCommission.plus(transcoderFeeCommission); | ||
|
|
||
| let totalStakeBI = convertFromDecimal(pool.totalStake); | ||
| if (totalStakeBI.gt(ZERO_BI)) { | ||
| pool.cumulativeFeeFactor = pool.cumulativeFeeFactor.plus( | ||
| precisePercOf(prevCRF, delegatorsFees, totalStakeBI) | ||
| ); | ||
| } | ||
|
|
||
| pool.fees = pool.fees.plus(faceValue); | ||
| pool.save(); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: livepeer/subgraph
Length of output: 2972
🏁 Script executed:
Repository: livepeer/subgraph
Length of output: 32746
Make
DelegatorSnapshot.idunique per event.idis fixed todelegator + round, whilebond,unbond, andrebondall writenew DelegatorSnapshot(...snapshotId).save()for the same key. Multiple state-changing events for the same delegator in one round will overwrite earlier snapshots, contradicting the snapshot “at each state-changing event” semantics. Include a per-event component, e.g. tx hash + log index, in the snapshot ID.🤖 Prompt for AI Agents