From 4f7223500ae74a39cd5046e36284e813c7fa844e Mon Sep 17 00:00:00 2001 From: adamsoffer Date: Sun, 8 Mar 2026 16:51:18 -0400 Subject: [PATCH 1/6] 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 | 20 ++++++ src/mappings/ticketBroker.ts | 31 +++++++++- utils/helpers.ts | 33 ++++++++++ 5 files changed, 218 insertions(+), 1 deletion(-) diff --git a/schema.graphql b/schema.graphql index 9febcfc..45cd60c 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 fa94260..9763b24 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, createOrLoadProtocol, @@ -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, Pool, @@ -135,12 +140,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( @@ -260,6 +292,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()) { @@ -292,6 +343,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( @@ -352,6 +414,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 @@ -372,6 +449,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); } @@ -496,6 +584,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 6ed9919..98e91a6 100644 --- a/src/mappings/roundsManager.ts +++ b/src/mappings/roundsManager.ts @@ -12,12 +12,14 @@ import { getBondingManagerAddress, getLptPriceEth, getTimestampForDaysPast, + integerFromString, makeEventId, makePoolId, 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 @@ -132,6 +134,24 @@ export function newRound(event: NewRound): void { pool.round = round.id; pool.delegate = currentTranscoder.toHex(); 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(round.id).minus(ONE_BI); + let prevPoolId = makePoolId( + currentTranscoder.toHex(), + prevRoundNum.toString() + ); + let prevPool = Pool.load(prevPoolId); + if (prevPool) { + pool.cumulativeRewardFactor = prevPool.cumulativeRewardFactor; + pool.cumulativeFeeFactor = prevPool.cumulativeFeeFactor; + } else { + pool.cumulativeRewardFactor = ZERO_BI; + pool.cumulativeFeeFactor = ZERO_BI; + } + if (transcoder) { pool.totalStake = transcoder.totalStake; pool.rewardCut = transcoder.rewardCut; diff --git a/src/mappings/ticketBroker.ts b/src/mappings/ticketBroker.ts index b929106..f2ff91f 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, @@ -11,9 +12,15 @@ import { createOrLoadTranscoderDay, getBlockNum, getEthPriceUsd, + integerFromString, makeEventId, makePoolId, + ONE_BI, + percOf, + PRECISE_PERC_DIVISOR, + precisePercOf, ZERO_BD, + ZERO_BI, } from "../../utils/helpers"; import { DepositFundedEvent, @@ -113,8 +120,30 @@ 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 if (pool) { + // 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 d068d43..f95b387 100644 --- a/utils/helpers.ts +++ b/utils/helpers.ts @@ -96,6 +96,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)) { @@ -265,6 +297,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 766b384f5cdde079d5c9c93bce706b87603ec3b2 Mon Sep 17 00:00:00 2001 From: adamsoffer Date: Mon, 9 Mar 2026 12:04:23 -0400 Subject: [PATCH 2/6] 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 | 3 +++ utils/helpers.ts | 1 + 3 files changed, 6 insertions(+) diff --git a/schema.graphql b/schema.graphql index 45cd60c..b41dbd9 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 9763b24..e5faeff 100755 --- a/src/mappings/bondingManager.ts +++ b/src/mappings/bondingManager.ts @@ -596,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 f95b387..4e073af 100644 --- a/utils/helpers.ts +++ b/utils/helpers.ts @@ -276,6 +276,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 893d295ffc72c0264cf76090fde3c4efcbad36f1 Mon Sep 17 00:00:00 2001 From: adamsoffer Date: Mon, 9 Mar 2026 12:14:35 -0400 Subject: [PATCH 3/6] 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 b41dbd9..171ac49 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 e5faeff..f0b3e97 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 f790cd8519d99dc64c6954eaaf2911e1a266cb2b Mon Sep 17 00:00:00 2001 From: adamsoffer Date: Mon, 9 Mar 2026 12:19:37 -0400 Subject: [PATCH 4/6] 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 171ac49..af5096a 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 f0b3e97..69caed7 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 4e073af..eed50cc 100644 --- a/utils/helpers.ts +++ b/utils/helpers.ts @@ -277,6 +277,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 45818b866e9592a3cb6d988a8a13a90226df6d16 Mon Sep 17 00:00:00 2001 From: adamsoffer Date: Mon, 9 Mar 2026 12:31:22 -0400 Subject: [PATCH 5/6] 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 af5096a..d653616 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 69caed7..bcfd3ab 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 f2ff91f..6c2ddd4 100644 --- a/src/mappings/ticketBroker.ts +++ b/src/mappings/ticketBroker.ts @@ -137,6 +137,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 eed50cc..97915f6 100644 --- a/utils/helpers.ts +++ b/utils/helpers.ts @@ -276,8 +276,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 e3378c1d47728c5ec57a71735174c31c9e837cf7 Mon Sep 17 00:00:00 2001 From: adamsoffer Date: Tue, 10 Mar 2026 14:23:39 -0400 Subject: [PATCH 6/6] 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 97915f6..78e7af0 100644 --- a/utils/helpers.ts +++ b/utils/helpers.ts @@ -113,9 +113,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)