Skip to content
Closed
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 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."
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]). 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
"""
Expand Down
138 changes: 138 additions & 0 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,
createOrLoadProtocol,
Expand All @@ -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
Expand All @@ -34,6 +38,7 @@ import {
} from "../types/BondingManager/BondingManager";
import {
BondEvent,
DelegatorSnapshot,
EarningsClaimedEvent,
ParameterUpdateEvent,
Pool,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
Expand Down Expand Up @@ -496,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;
Expand Down Expand Up @@ -673,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(
Expand Down
25 changes: 25 additions & 0 deletions src/mappings/roundsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -132,10 +134,33 @@ 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;
pool.feeShare = transcoder.feeShare;

// Snapshot pendingRewardCommission as activeCumulativeRewards for this round,
// mirroring the contract's setCurrentRoundTotalActiveStake snapshot
transcoder.activeCumulativeRewards = transcoder.pendingRewardCommission;
transcoder.save();
}
pool.save();

Expand Down
37 changes: 36 additions & 1 deletion src/mappings/ticketBroker.ts
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,
Expand All @@ -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,
Expand Down Expand Up @@ -113,8 +120,36 @@ 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 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();
}
Expand Down
Loading