Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 34 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@ 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"
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 {
Expand Down Expand Up @@ -135,6 +143,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 +217,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
124 changes: 124 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,31 @@ 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);

// 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)) {
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 +786,17 @@ 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.save();
}

createOrLoadTransactionFromEvent(event);

let earningsClaimedEvent = new EarningsClaimedEvent(
Expand Down
20 changes: 20 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,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;
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