From 4f7484b33aea8507a126164fdab66a0772603535 Mon Sep 17 00:00:00 2001 From: adamsoffer Date: Sun, 8 Mar 2026 16:51:18 -0400 Subject: [PATCH 1/7] feat: add cumulative reward/fee factors and delegator shares for efficient 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 --- schema.graphql | 26 ++++++++ src/mappings/bondingManager.ts | 109 +++++++++++++++++++++++++++++++++ src/mappings/roundsManager.ts | 2 + src/mappings/ticketBroker.ts | 34 +++++++++- utils/helpers.ts | 49 +++++++++++++++ 5 files changed, 219 insertions(+), 1 deletion(-) diff --git a/schema.graphql b/schema.graphql index b6ac70d..04af384 100755 --- a/schema.graphql +++ b/schema.graphql @@ -135,6 +135,10 @@ type Pool @entity { rewardCut: BigInt! "Transcoder's fee share during the earnings pool's round" feeShare: BigInt! + "Cumulative reward factor for computing delegator rewards without looping (27-decimal fixed-point, matches on-chain PreciseMathUtils)" + cumulativeRewardFactor: BigInt! + "Cumulative fee factor for computing delegator fees without looping (27-decimal fixed-point, matches on-chain PreciseMathUtils)" + cumulativeFeeFactor: BigInt! } """ @@ -205,10 +209,32 @@ type Delegator @entity { withdrawnFees: BigDecimal! "Amount of Livepeer Token the delegator has delegated" delegatedAmount: BigDecimal! + "Proportional claim on the orchestrator's pool (bondedAmount * 10^27 / crf[lastClaimRound]). Invariant across claims, only changes on bond/unbond." + shares: BigInt! "Unbonding locks associated with the delegator" unbondingLocks: [UnbondingLock!] @derivedFrom(field: "delegator") } +""" +Snapshot of delegator state at each state-changing event, enabling historical stake and reward computation via cumulative factors +""" +type DelegatorSnapshot @entity { + "Unique identifier: delegator address + round number" + id: ID! + "The delegator this snapshot belongs to" + delegator: Delegator! + "The delegate (orchestrator) at the time of this snapshot, null if fully unbonded" + delegate: Transcoder + "Bonded amount at the time of this snapshot" + bondedAmount: BigDecimal! + "Proportional claim on the orchestrator's pool. stake = shares * crf[round] / 10^27" + shares: BigInt! + "Round when this snapshot was taken" + round: Round! + "Timestamp when this snapshot was taken" + timestamp: Int! +} + """ Abstraction for accounts/delegators bonded with the protocol """ diff --git a/src/mappings/bondingManager.ts b/src/mappings/bondingManager.ts index 2a49346..935ba1b 100755 --- a/src/mappings/bondingManager.ts +++ b/src/mappings/bondingManager.ts @@ -1,5 +1,6 @@ import { store } from "@graphprotocol/graph-ts"; import { + convertFromDecimal, convertToDecimal, createOrLoadDelegator, createOrLoadPool, @@ -13,6 +14,9 @@ import { 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,6 +38,7 @@ import { } from "../types/BondingManager/BondingManager"; import { BondEvent, + DelegatorSnapshot, EarningsClaimedEvent, ParameterUpdateEvent, RebondEvent, @@ -134,12 +139,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(); + createOrLoadTransactionFromEvent(event); let bondEvent = new BondEvent( @@ -259,6 +291,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 +342,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 +413,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 +448,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 +582,27 @@ 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); + + let totalStakeBI = convertFromDecimal(pool.totalStake); + 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; diff --git a/src/mappings/roundsManager.ts b/src/mappings/roundsManager.ts index c113a35..42fd359 100644 --- a/src/mappings/roundsManager.ts +++ b/src/mappings/roundsManager.ts @@ -13,11 +13,13 @@ import { getBondingManagerAddress, getLptPriceEth, getTimestampForDaysPast, + integerFromString, makeEventId, ONE_BD, ONE_BI, PERC_DIVISOR, ZERO_BD, + ZERO_BI, } from "../../utils/helpers"; import { BondingManager } from "../types/BondingManager/BondingManager"; // Import event types from the registrar contract ABIs diff --git a/src/mappings/ticketBroker.ts b/src/mappings/ticketBroker.ts index 3aa9adf..685fc0c 100644 --- a/src/mappings/ticketBroker.ts +++ b/src/mappings/ticketBroker.ts @@ -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,31 @@ 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; + } + + let delegatorsFees = percOf(event.params.faceValue, pool.feeShare); + 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(); diff --git a/utils/helpers.ts b/utils/helpers.ts index 556744d..97eb37a 100644 --- a/utils/helpers.ts +++ b/utils/helpers.ts @@ -61,6 +61,22 @@ export function createOrLoadPool(roundId: string, transcoderAddress: string): Po pool.round = roundId; pool.delegate = transcoderAddress; pool.fees = ZERO_BD; + + // Propagate cumulative factors from the previous round's pool so every + // pool has valid factors even if the transcoder misses reward() or has + // no fees in a round. This mirrors the contract's latestCumulativeFactorsPool. + let prevRoundNum = integerFromString(roundId).minus(ONE_BI); + let prevPool = Pool.load( + makePoolId(transcoderAddress, prevRoundNum.toString()) + ); + if (prevPool) { + pool.cumulativeRewardFactor = prevPool.cumulativeRewardFactor; + pool.cumulativeFeeFactor = prevPool.cumulativeFeeFactor; + } else { + pool.cumulativeRewardFactor = ZERO_BI; + pool.cumulativeFeeFactor = ZERO_BI; + } + let transcoder = Transcoder.load(transcoderAddress); if (transcoder) { @@ -124,6 +140,38 @@ export function percPoints(_fracNum: BigInt, _fracDenom: BigInt): BigInt { return _fracNum.times(BigInt.fromI32(PERC_DIVISOR)).div(_fracDenom); } +// PreciseMathUtils equivalents (matches Solidity's 27-decimal fixed-point arithmetic) +export let PRECISE_PERC_DIVISOR = BigInt.fromString( + "1000000000000000000000000000" +); // 10^27 + +export function precisePercPoints( + _fracNum: BigInt, + _fracDenom: BigInt +): BigInt { + return _fracNum.times(PRECISE_PERC_DIVISOR).div(_fracDenom); +} + +export function precisePercOf( + _baseAmount: BigInt, + _fracNum: BigInt, + _fracDenom: BigInt +): BigInt { + return _baseAmount + .times(precisePercPoints(_fracNum, _fracDenom)) + .div(PRECISE_PERC_DIVISOR); +} + +// Convert BigDecimal (in token units) back to raw BigInt (in wei) +export function convertFromDecimal(amount: BigDecimal): BigInt { + let str = amount.times(exponentToBigDecimal(BI_18)).toString(); + let dotIndex = str.indexOf("."); + if (dotIndex >= 0) { + str = str.substring(0, dotIndex); + } + return BigInt.fromString(str); +} + export function exponentToBigDecimal(decimals: BigInt): BigDecimal { let bd = BigDecimal.fromString("1"); for (let i = ZERO_BI; i.lt(decimals); i = i.plus(ONE_BI)) { @@ -287,6 +335,7 @@ export function createOrLoadDelegator(id: string, timestamp: i32): Delegator { delegator.fees = ZERO_BD; delegator.withdrawnFees = ZERO_BD; delegator.delegatedAmount = ZERO_BD; + delegator.shares = ZERO_BI; delegator.save(); } From f1dd7924b9cce1aad6a37dd87545e61136ae59eb Mon Sep 17 00:00:00 2001 From: adamsoffer Date: Mon, 9 Mar 2026 12:04:23 -0400 Subject: [PATCH 2/7] feat: add cumulativeRewards on Transcoder for lifetime orchestrator commission 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 --- schema.graphql | 2 ++ src/mappings/bondingManager.ts | 5 +++++ utils/helpers.ts | 1 + 3 files changed, 8 insertions(+) diff --git a/schema.graphql b/schema.graphql index 04af384..6a3f608 100755 --- a/schema.graphql +++ b/schema.graphql @@ -108,6 +108,8 @@ type Transcoder @entity { serviceURI: String "Days which the transcoder earned fees" transcoderDays: [TranscoderDay!]! + "Lifetime cumulative rewards (rewardCut commission) earned by this orchestrator in wei" + cumulativeRewards: BigInt! } enum TranscoderStatus @entity { diff --git a/src/mappings/bondingManager.ts b/src/mappings/bondingManager.ts index 935ba1b..92e958e 100755 --- a/src/mappings/bondingManager.ts +++ b/src/mappings/bondingManager.ts @@ -11,6 +11,7 @@ import { EMPTY_ADDRESS, getBlockNum, makeEventId, + makePoolId, makeUnbondingLockId, MAXIMUM_VALUE_UINT256, ONE_BI, @@ -41,6 +42,7 @@ import { DelegatorSnapshot, EarningsClaimedEvent, ParameterUpdateEvent, + Pool, RebondEvent, RewardEvent, TranscoderActivatedEvent, @@ -594,6 +596,9 @@ export function reward(event: Reward): void { let transcoderCommission = percOf(totalRewardTokens, pool.rewardCut); let delegatorsRewards = totalRewardTokens.minus(transcoderCommission); + // Accumulate lifetime orchestrator commission + transcoder.cumulativeRewards = transcoder.cumulativeRewards.plus(transcoderCommission); + let totalStakeBI = convertFromDecimal(pool.totalStake); if (totalStakeBI.gt(ZERO_BI)) { pool.cumulativeRewardFactor = prevCRF.plus( diff --git a/utils/helpers.ts b/utils/helpers.ts index 97eb37a..043c78a 100644 --- a/utils/helpers.ts +++ b/utils/helpers.ts @@ -314,6 +314,7 @@ export function createOrLoadTranscoder(id: string, timestamp: i32): Transcoder { transcoder.sixtyDayVolumeETH = ZERO_BD; transcoder.ninetyDayVolumeETH = ZERO_BD; transcoder.transcoderDays = []; + transcoder.cumulativeRewards = ZERO_BI; transcoder.save(); } From 064d40f852cf1c04d2859c73d61f9ceedcce18fa Mon Sep 17 00:00:00 2001 From: adamsoffer Date: Mon, 9 Mar 2026 12:14:35 -0400 Subject: [PATCH 3/7] fix: reset cumulativeRewards on claim so it represents unclaimed commission 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 --- schema.graphql | 2 +- src/mappings/bondingManager.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/schema.graphql b/schema.graphql index 6a3f608..6d9e567 100755 --- a/schema.graphql +++ b/schema.graphql @@ -108,7 +108,7 @@ type Transcoder @entity { serviceURI: String "Days which the transcoder earned fees" transcoderDays: [TranscoderDay!]! - "Lifetime cumulative rewards (rewardCut commission) earned by this orchestrator in wei" + "Unclaimed orchestrator commission (rewardCut portion) in wei. Resets to zero on claim. Full pending stake = shares * crf / 10^27 + cumulativeRewards" cumulativeRewards: BigInt! } diff --git a/src/mappings/bondingManager.ts b/src/mappings/bondingManager.ts index 92e958e..7a50cd2 100755 --- a/src/mappings/bondingManager.ts +++ b/src/mappings/bondingManager.ts @@ -785,6 +785,16 @@ 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.cumulativeRewards = ZERO_BI; + transcoder.save(); + } + createOrLoadTransactionFromEvent(event); let earningsClaimedEvent = new EarningsClaimedEvent( From af4952d87f45eb318211f619ca2fd4d899e1212c Mon Sep 17 00:00:00 2001 From: adamsoffer Date: Mon, 9 Mar 2026 12:19:37 -0400 Subject: [PATCH 4/7] feat: add lifetimeRewards on Transcoder for total commission earned 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 --- schema.graphql | 2 ++ src/mappings/bondingManager.ts | 3 ++- utils/helpers.ts | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/schema.graphql b/schema.graphql index 6d9e567..7b728eb 100755 --- a/schema.graphql +++ b/schema.graphql @@ -110,6 +110,8 @@ type Transcoder @entity { transcoderDays: [TranscoderDay!]! "Unclaimed orchestrator commission (rewardCut portion) in wei. Resets to zero on claim. Full pending stake = shares * crf / 10^27 + cumulativeRewards" cumulativeRewards: BigInt! + "Lifetime total orchestrator commission earned (rewardCut portion) in wei. Never resets." + lifetimeRewards: BigInt! } enum TranscoderStatus @entity { diff --git a/src/mappings/bondingManager.ts b/src/mappings/bondingManager.ts index 7a50cd2..95067d7 100755 --- a/src/mappings/bondingManager.ts +++ b/src/mappings/bondingManager.ts @@ -596,8 +596,9 @@ export function reward(event: Reward): void { let transcoderCommission = percOf(totalRewardTokens, pool.rewardCut); let delegatorsRewards = totalRewardTokens.minus(transcoderCommission); - // Accumulate lifetime orchestrator commission + // Accumulate orchestrator commission transcoder.cumulativeRewards = transcoder.cumulativeRewards.plus(transcoderCommission); + transcoder.lifetimeRewards = transcoder.lifetimeRewards.plus(transcoderCommission); let totalStakeBI = convertFromDecimal(pool.totalStake); if (totalStakeBI.gt(ZERO_BI)) { diff --git a/utils/helpers.ts b/utils/helpers.ts index 043c78a..ef080f4 100644 --- a/utils/helpers.ts +++ b/utils/helpers.ts @@ -315,6 +315,7 @@ export function createOrLoadTranscoder(id: string, timestamp: i32): Transcoder { transcoder.ninetyDayVolumeETH = ZERO_BD; transcoder.transcoderDays = []; transcoder.cumulativeRewards = ZERO_BI; + transcoder.lifetimeRewards = ZERO_BI; transcoder.save(); } From 3926bccb974733783902e693f351ad84a2565de8 Mon Sep 17 00:00:00 2001 From: adamsoffer Date: Mon, 9 Mar 2026 12:31:22 -0400 Subject: [PATCH 5/7] feat: rename commission fields and add fee commission tracking 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 --- schema.graphql | 12 ++++++++---- src/mappings/bondingManager.ts | 9 +++++---- src/mappings/ticketBroker.ts | 6 ++++++ utils/helpers.ts | 6 ++++-- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/schema.graphql b/schema.graphql index 7b728eb..02b05af 100755 --- a/schema.graphql +++ b/schema.graphql @@ -108,10 +108,14 @@ type Transcoder @entity { serviceURI: String "Days which the transcoder earned fees" transcoderDays: [TranscoderDay!]! - "Unclaimed orchestrator commission (rewardCut portion) in wei. Resets to zero on claim. Full pending stake = shares * crf / 10^27 + cumulativeRewards" - cumulativeRewards: BigInt! - "Lifetime total orchestrator commission earned (rewardCut portion) in wei. Never resets." - lifetimeRewards: BigInt! + "Unclaimed orchestrator reward commission (rewardCut portion) in wei. Resets to zero on claim. Full pending stake = shares * crf / 10^27 + pendingRewardCommission" + pendingRewardCommission: BigInt! + "Lifetime total orchestrator reward commission earned (rewardCut portion) in wei. Never resets." + lifetimeRewardCommission: BigInt! + "Unclaimed orchestrator fee commission in wei. Resets to zero on claim." + pendingFeeCommission: BigInt! + "Lifetime total orchestrator fee commission earned in wei. Never resets." + lifetimeFeeCommission: BigInt! } enum TranscoderStatus @entity { diff --git a/src/mappings/bondingManager.ts b/src/mappings/bondingManager.ts index 95067d7..18e7918 100755 --- a/src/mappings/bondingManager.ts +++ b/src/mappings/bondingManager.ts @@ -596,9 +596,9 @@ export function reward(event: Reward): void { let transcoderCommission = percOf(totalRewardTokens, pool.rewardCut); let delegatorsRewards = totalRewardTokens.minus(transcoderCommission); - // Accumulate orchestrator commission - transcoder.cumulativeRewards = transcoder.cumulativeRewards.plus(transcoderCommission); - transcoder.lifetimeRewards = transcoder.lifetimeRewards.plus(transcoderCommission); + // Accumulate orchestrator reward commission + transcoder.pendingRewardCommission = transcoder.pendingRewardCommission.plus(transcoderCommission); + transcoder.lifetimeRewardCommission = transcoder.lifetimeRewardCommission.plus(transcoderCommission); let totalStakeBI = convertFromDecimal(pool.totalStake); if (totalStakeBI.gt(ZERO_BI)) { @@ -792,7 +792,8 @@ export function earningsClaimed(event: EarningsClaimed): void { event.params.delegator.toHex(), event.block.timestamp.toI32() ); - transcoder.cumulativeRewards = ZERO_BI; + transcoder.pendingRewardCommission = ZERO_BI; + transcoder.pendingFeeCommission = ZERO_BI; transcoder.save(); } diff --git a/src/mappings/ticketBroker.ts b/src/mappings/ticketBroker.ts index 685fc0c..710d15b 100644 --- a/src/mappings/ticketBroker.ts +++ b/src/mappings/ticketBroker.ts @@ -145,6 +145,12 @@ export function winningTicketRedeemed(event: WinningTicketRedeemed): void { } 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( diff --git a/utils/helpers.ts b/utils/helpers.ts index ef080f4..c1e10ae 100644 --- a/utils/helpers.ts +++ b/utils/helpers.ts @@ -314,8 +314,10 @@ export function createOrLoadTranscoder(id: string, timestamp: i32): Transcoder { transcoder.sixtyDayVolumeETH = ZERO_BD; transcoder.ninetyDayVolumeETH = ZERO_BD; transcoder.transcoderDays = []; - transcoder.cumulativeRewards = ZERO_BI; - transcoder.lifetimeRewards = ZERO_BI; + transcoder.pendingRewardCommission = ZERO_BI; + transcoder.lifetimeRewardCommission = ZERO_BI; + transcoder.pendingFeeCommission = ZERO_BI; + transcoder.lifetimeFeeCommission = ZERO_BI; transcoder.save(); } From dcc2755e404fe3aba787bead8e3256c5218718d4 Mon Sep 17 00:00:00 2001 From: adamsoffer Date: Tue, 10 Mar 2026 14:23:39 -0400 Subject: [PATCH 6/7] fix: match Solidity's PreciseMathUtils.percOf with single division 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 --- utils/helpers.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/utils/helpers.ts b/utils/helpers.ts index c1e10ae..29e54ec 100644 --- a/utils/helpers.ts +++ b/utils/helpers.ts @@ -157,9 +157,7 @@ export function precisePercOf( _fracNum: BigInt, _fracDenom: BigInt ): BigInt { - return _baseAmount - .times(precisePercPoints(_fracNum, _fracDenom)) - .div(PRECISE_PERC_DIVISOR); + return _baseAmount.times(_fracNum).div(_fracDenom); } // Convert BigDecimal (in token units) back to raw BigInt (in wei) From 7b101df313a6d72bfc6a90ae4c94b2d60648e640 Mon Sep 17 00:00:00 2001 From: adamsoffer Date: Tue, 10 Mar 2026 14:45:05 -0400 Subject: [PATCH 7/7] fix: include transcoderRewardStakeRewards in pendingRewardCommission 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 --- schema.graphql | 6 ++++-- src/mappings/bondingManager.ts | 22 ++++++++++++++++++---- src/mappings/roundsManager.ts | 7 +++++++ utils/helpers.ts | 1 + 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/schema.graphql b/schema.graphql index 02b05af..54c9baf 100755 --- a/schema.graphql +++ b/schema.graphql @@ -108,10 +108,12 @@ type Transcoder @entity { serviceURI: String "Days which the transcoder earned fees" transcoderDays: [TranscoderDay!]! - "Unclaimed orchestrator reward commission (rewardCut portion) in wei. Resets to zero on claim. Full pending stake = shares * crf / 10^27 + pendingRewardCommission" + "Unclaimed orchestrator reward commission in wei. Includes both rewardCut commission and rewards earned on staked commission. Resets to zero on claim. Full pending stake = shares * crf / 10^27 + pendingRewardCommission" pendingRewardCommission: BigInt! - "Lifetime total orchestrator reward commission earned (rewardCut portion) in wei. Never resets." + "Lifetime total orchestrator reward commission earned in wei. Never resets." lifetimeRewardCommission: BigInt! + "Snapshot of pendingRewardCommission at the start of the current round. Used to compute the transcoder's share of delegator rewards earned by its own staked commission. Resets to zero on claim." + activeCumulativeRewards: BigInt! "Unclaimed orchestrator fee commission in wei. Resets to zero on claim." pendingFeeCommission: BigInt! "Lifetime total orchestrator fee commission earned in wei. Never resets." diff --git a/src/mappings/bondingManager.ts b/src/mappings/bondingManager.ts index 18e7918..2769645 100755 --- a/src/mappings/bondingManager.ts +++ b/src/mappings/bondingManager.ts @@ -596,11 +596,24 @@ export function reward(event: Reward): void { let transcoderCommission = percOf(totalRewardTokens, pool.rewardCut); let delegatorsRewards = totalRewardTokens.minus(transcoderCommission); - // Accumulate orchestrator reward commission - transcoder.pendingRewardCommission = transcoder.pendingRewardCommission.plus(transcoderCommission); - transcoder.lifetimeRewardCommission = transcoder.lifetimeRewardCommission.plus(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) @@ -794,6 +807,7 @@ export function earningsClaimed(event: EarningsClaimed): void { ); transcoder.pendingRewardCommission = ZERO_BI; transcoder.pendingFeeCommission = ZERO_BI; + transcoder.activeCumulativeRewards = ZERO_BI; transcoder.save(); } diff --git a/src/mappings/roundsManager.ts b/src/mappings/roundsManager.ts index 42fd359..e6982a7 100644 --- a/src/mappings/roundsManager.ts +++ b/src/mappings/roundsManager.ts @@ -128,6 +128,13 @@ export function newRound(event: NewRound): void { // given transcoder and round then we know the transcoder failed to call reward() createOrLoadPool(round.id, currentTranscoder.toHex()); + if (transcoder) { + // Snapshot pendingRewardCommission as activeCumulativeRewards for this round, + // mirroring the contract's setCurrentRoundTotalActiveStake snapshot + transcoder.activeCumulativeRewards = transcoder.pendingRewardCommission; + transcoder.save(); + } + currentTranscoder = bondingManager.getNextTranscoderInPool(currentTranscoder); diff --git a/utils/helpers.ts b/utils/helpers.ts index 29e54ec..b070012 100644 --- a/utils/helpers.ts +++ b/utils/helpers.ts @@ -316,6 +316,7 @@ export function createOrLoadTranscoder(id: string, timestamp: i32): Transcoder { transcoder.lifetimeRewardCommission = ZERO_BI; transcoder.pendingFeeCommission = ZERO_BI; transcoder.lifetimeFeeCommission = ZERO_BI; + transcoder.activeCumulativeRewards = ZERO_BI; transcoder.save(); }