Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
4f7484b
feat: add cumulative reward/fee factors and delegator shares for effi…
adamsoffer Mar 8, 2026
f1dd792
feat: add cumulativeRewards on Transcoder for lifetime orchestrator c…
adamsoffer Mar 9, 2026
064d40f
fix: reset cumulativeRewards on claim so it represents unclaimed comm…
adamsoffer Mar 9, 2026
af4952d
feat: add lifetimeRewards on Transcoder for total commission earned
adamsoffer Mar 9, 2026
3926bcc
feat: rename commission fields and add fee commission tracking
adamsoffer Mar 9, 2026
dcc2755
fix: match Solidity's PreciseMathUtils.percOf with single division
adamsoffer Mar 10, 2026
7b101df
fix: include transcoderRewardStakeRewards in pendingRewardCommission
adamsoffer Mar 10, 2026
f3774ac
fix: carry cumulative factors across round gaps in createOrLoadPool
adamsoffer Aug 11, 2026
5042005
fix: don't overwrite pool head's active flag in newRound
adamsoffer Sep 25, 2026
2f98626
fix: share Delegator object on self-bond so delegatedAmount isn't ove…
adamsoffer Sep 26, 2026
6c3c5ce
fix: address Copilot review on cumulative factors and shares
adamsoffer Sep 26, 2026
0e798d1
fix: credit fees on staked commission and read fee factor on-chain
adamsoffer Sep 26, 2026
bf4e850
fix: rebase shares only on self-claims; declare TicketBroker entities
adamsoffer Sep 26, 2026
bd43e99
fix: ignore an uninitialized on-chain earnings pool in winningTicketR…
adamsoffer Oct 17, 2026
6ef9b25
fix: use current rewardCut on reward and persist on-chain pool values
adamsoffer Oct 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ type Transcoder @entity {
serviceURI: String
"Days which the transcoder earned fees"
transcoderDays: [TranscoderDay!]!
"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 in wei. Never resets."
lifetimeRewardCommission: BigInt!
"Snapshot of pendingRewardCommission taken when the transcoder calls reward(), matching the contract. Used to compute the transcoder's share of delegator rewards and fees earned by its own staked commission. Not cleared on claim; the next reward() replaces it."
activeCumulativeRewards: BigInt!
Comment thread
adamsoffer marked this conversation as resolved.
"Unclaimed orchestrator fee commission in wei. Includes both feeShare commission and fees earned on staked commission. Resets to zero on claim."
pendingFeeCommission: BigInt!
"Lifetime total orchestrator fee commission earned in wei. Never resets."
lifetimeFeeCommission: BigInt!
}

enum TranscoderStatus @entity {
Expand Down Expand Up @@ -135,6 +145,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!
}

"""
Expand Down Expand Up @@ -205,10 +219,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]). Recomputed on bond, unbond, rebond and earnings claims; unchanged by an ordinary claim, but a self-claim adds the orchestrator's bonded commission."
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!
}
Comment thread
adamsoffer marked this conversation as resolved.

"""
Abstraction for accounts/delegators bonded with the protocol
"""
Expand Down
189 changes: 172 additions & 17 deletions src/mappings/bondingManager.ts
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,
Expand All @@ -9,10 +10,14 @@ import {
createOrLoadTranscoder,
EMPTY_ADDRESS,
getBlockNum,
latestCumulativeRewardFactor,
makeEventId,
makeUnbondingLockId,
MAXIMUM_VALUE_UINT256,
ONE_BI,
percOf,
PRECISE_PERC_DIVISOR,
precisePercOf,
ZERO_BI,
} from "../../utils/helpers";
// Import event types from the registrar contract ABIs
Expand All @@ -34,6 +39,7 @@ import {
} from "../types/BondingManager/BondingManager";
import {
BondEvent,
DelegatorSnapshot,
EarningsClaimedEvent,
ParameterUpdateEvent,
RebondEvent,
Expand Down Expand Up @@ -62,10 +68,15 @@ export function bond(event: Bond): void {
event.params.newDelegate.toHex(),
event.block.timestamp.toI32()
);
let delegator = createOrLoadDelegator(
event.params.delegator.toHex(),
event.block.timestamp.toI32()
);
// When self-bonding, delegate and delegator are the same entity. Share one
// object so saving delegator doesn't overwrite delegate's delegatedAmount.
let delegator =
event.params.delegator.toHex() == event.params.newDelegate.toHex()
? delegate
: createOrLoadDelegator(
event.params.delegator.toHex(),
event.block.timestamp.toI32()
);
let protocol = createOrLoadProtocol();

// If self delegating, set status and assign reference to self
Expand All @@ -85,10 +96,15 @@ export function bond(event: Bond): void {
event.params.oldDelegate.toHex(),
event.block.timestamp.toI32()
);
let oldDelegate = createOrLoadDelegator(
event.params.oldDelegate.toHex(),
event.block.timestamp.toI32()
);
// Moving stake off a self-delegation: oldDelegate is the delegator itself,
// so share the object for the same reason as above.
let oldDelegate =
event.params.oldDelegate.toHex() == event.params.delegator.toHex()
? delegator
: createOrLoadDelegator(
event.params.oldDelegate.toHex(),
event.block.timestamp.toI32()
);
let oldDelegateData = bondingManager.getDelegator(event.params.oldDelegate);

// if previous delegate was itself, set status and unassign reference to self
Expand Down Expand Up @@ -134,12 +150,30 @@ export function bond(event: Bond): void {
convertToDecimal(event.params.additionalAmount)
);

// Compute shares: bondedAmount * 10^27 / crf[lastClaimRound]
delegator.shares = event.params.bondedAmount
.times(PRECISE_PERC_DIVISOR)
.div(
latestCumulativeRewardFactor(event.params.newDelegate.toHex(), round.id)
);

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();
Comment thread
adamsoffer marked this conversation as resolved.
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 thread
adamsoffer marked this conversation as resolved.

createOrLoadTransactionFromEvent(event);

let bondEvent = new BondEvent(
Expand Down Expand Up @@ -237,10 +271,15 @@ export function unbond(event: Unbond): void {
event.params.delegate.toHex(),
event.block.timestamp.toI32()
);
let delegate = createOrLoadDelegator(
event.params.delegate.toHex(),
event.block.timestamp.toI32()
);
// When unbonding self-stake, delegate and delegator are the same entity.
// Share one object so saving delegator doesn't overwrite delegatedAmount.
let delegate =
event.params.delegate.toHex() == event.params.delegator.toHex()
? delegator
: createOrLoadDelegator(
event.params.delegate.toHex(),
event.block.timestamp.toI32()
);
let unbondingLock = UnbondingLock.load(uniqueUnbondingLockId);
if (unbondingLock === null) {
unbondingLock = new UnbondingLock(uniqueUnbondingLockId);
Expand All @@ -259,6 +298,17 @@ 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 {
delegator.shares = delegatorData.value0
.times(PRECISE_PERC_DIVISOR)
.div(
latestCumulativeRewardFactor(event.params.delegate.toHex(), round.id)
);
}

// Delegator no longer delegated to anyone if it does not have a bonded amount
// so remove it from delegate
if (delegatorData.value0.isZero()) {
Expand Down Expand Up @@ -291,6 +341,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(
Expand Down Expand Up @@ -319,10 +380,15 @@ export function rebond(event: Rebond): void {
event.params.delegate.toHex(),
event.block.timestamp.toI32()
);
let delegator = createOrLoadDelegator(
event.params.delegator.toHex(),
event.block.timestamp.toI32()
);
// When rebonding to self, delegate and delegator are the same entity. Share
// one object so saving delegator doesn't overwrite delegatedAmount.
let delegator =
event.params.delegator.toHex() == event.params.delegate.toHex()
? delegate
: createOrLoadDelegator(
event.params.delegator.toHex(),
event.block.timestamp.toI32()
);
let delegateData = bondingManager.getDelegator(event.params.delegate);
let protocol = createOrLoadProtocol();
let uniqueUnbondingLockId = makeUnbondingLockId(
Expand Down Expand Up @@ -351,6 +417,11 @@ export function rebond(event: Rebond): void {
delegator.bondedAmount = convertToDecimal(delegatorData.value0);
delegator.fees = convertToDecimal(delegatorData.value1);

// Compute shares: bondedAmount * 10^27 / crf[lastClaimRound]
delegator.shares = delegatorData.value0
.times(PRECISE_PERC_DIVISOR)
.div(latestCumulativeRewardFactor(event.params.delegate.toHex(), round.id));

// 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
Expand All @@ -371,6 +442,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);
}
Expand Down Expand Up @@ -494,10 +576,56 @@ export function reward(event: Reward): void {
);
transcoder.lastRewardRound = round.id;

pool.rewardTokens = convertToDecimal(event.params.amount);
// 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)
}

// The contract sets the pool's commission to the transcoder's current
// rewardCut/feeShare when reward() runs (rewardWithHint's setCommission),
// so a mid-round TranscoderUpdate applies to this round's rewards
pool.feeShare = transcoder.feeShare;
pool.rewardCut = transcoder.rewardCut;

let totalRewardTokens = event.params.amount; // raw BigInt in wei
let transcoderCommission = percOf(totalRewardTokens, pool.rewardCut);
let delegatorsRewards = totalRewardTokens.minus(transcoderCommission);

// Snapshot the commission that is active this round, as the contract does
// at the start of updateTranscoderWithRewards
transcoder.activeCumulativeRewards = transcoder.pendingRewardCommission;

// 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
Comment thread
adamsoffer marked this conversation as resolved.
);
}

// 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);
Comment thread
adamsoffer marked this conversation as resolved.
if (totalStakeBI.gt(ZERO_BI)) {
pool.cumulativeRewardFactor = prevCRF.plus(
precisePercOf(prevCRF, delegatorsRewards, totalStakeBI)
);
} else {
pool.cumulativeRewardFactor = prevCRF;
}

pool.rewardTokens = convertToDecimal(event.params.amount);

transcoder.save();
delegate.save();
pool.save();
Expand Down Expand Up @@ -669,8 +797,35 @@ export function earningsClaimed(event: EarningsClaimed): void {
convertToDecimal(event.params.rewards)
);
delegator.fees = delegator.fees.plus(convertToDecimal(event.params.fees));
// A self-claim also bonds the orchestrator's commission, which the old
// shares don't include, so rebase them on the new lastClaimRound. An
// ordinary claim leaves shares unchanged (rewards grow with the CRF), so
// skip it there rather than let integer division drift them down.
if (event.params.delegator.toHex() == event.params.delegate.toHex()) {
delegator.shares = convertFromDecimal(delegator.bondedAmount)
.times(PRECISE_PERC_DIVISOR)
.div(
latestCumulativeRewardFactor(
event.params.delegate.toHex(),
event.params.endRound.toString()
)
);
}
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;
// activeCumulativeRewards is not cleared, matching the contract: fees
// later this round still use it, and the next reward() resets it
transcoder.save();
Comment thread
adamsoffer marked this conversation as resolved.
}

createOrLoadTransactionFromEvent(event);

let earningsClaimedEvent = new EarningsClaimedEvent(
Expand Down
Loading
Loading