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
52 changes: 45 additions & 7 deletions src/mappings/roundsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,14 @@ export function newRound(event: NewRound): void {
let totalActiveStake = BigDecimal.zero();
let transcoder: Transcoder | null = null;

// will revert if there are no transcoders in pool
// getFirstTranscoderInPool reverts only if the pool is empty, which is
// impossible post-launch. A revert here is a determinism hazard (RPC
// backend returning bad data), not a legitimate control-flow path — log
// critical so it surfaces immediately rather than silently skipping the
// transcoder enumeration (see issue #248, case 1).
let callResult = bondingManager.try_getFirstTranscoderInPool();
if (callResult.reverted) {
log.info("getFirstTranscoderInPool reverted", []);
log.critical("getFirstTranscoderInPool reverted", []);
} else {
currentTranscoder = callResult.value;
transcoder = createOrLoadTranscoder(
Expand All @@ -62,10 +66,12 @@ export function newRound(event: NewRound): void {
);
}

// will revert if there is no LPT bonded
// getTotalBonded reverts only when no LPT is bonded, which is impossible
// post-launch. A revert here is a determinism hazard — log critical so it
// surfaces immediately (see issue #248, case 2).
let getTotalBondedCallResult = bondingManager.try_getTotalBonded();
if (getTotalBondedCallResult.reverted) {
log.info("getTotalBonded reverted", []);
log.critical("getTotalBonded reverted", []);
} else {
totalActiveStake = convertToDecimal(getTotalBondedCallResult.value);
}
Expand All @@ -77,6 +83,26 @@ export function newRound(event: NewRound): void {

let protocol = createOrLoadProtocol();

// Carry forward the last known totalActiveStake if the call reverted or
// returned zero (impossible post-launch, indicating a bad RPC response).
// This prevents zeroing protocol.totalActiveStake and downstream
// participationRate / numActiveTranscoders for the entire round.
if (
getTotalBondedCallResult.reverted ||
totalActiveStake.equals(ZERO_BD)
) {
let lastStake = protocol.totalActiveStake;
if (!lastStake.equals(ZERO_BD)) {
totalActiveStake = lastStake;
protocol.totalActiveStake = totalActiveStake;
protocol.save();
log.warning(
"totalActiveStake reverted/zero; carrying forward last known value {}",
[lastStake.toString()]
);
}
}

// Activate all transcoders pending activation
let pendingActivation = protocol.pendingActivation;
if (pendingActivation.length) {
Expand Down Expand Up @@ -118,16 +144,28 @@ export function newRound(event: NewRound): void {
90
);

// Iterate over all active transcoders
// Iterate over all active transcoders using the try_ variant so that a
// reverted eth_call (bad RPC backend) is visible rather than silently
// returning EMPTY_ADDRESS, which would truncate the enumeration and
// skip every transcoder after it (see issue #248, case 5).
while (EMPTY_ADDRESS.toHex() != currentTranscoder.toHex()) {
// create a unique "pool" for each active transcoder. If a transcoder calls
// reward() for a given round, we store its reward tokens inside this Pool
// entry in a field called "rewardTokens". If "rewardTokens" is null for a
// given transcoder and round then we know the transcoder failed to call reward()
createOrLoadPool(round.id, currentTranscoder.toHex());

currentTranscoder =
bondingManager.getNextTranscoderInPool(currentTranscoder);
let nextResult = bondingManager.try_getNextTranscoderInPool(
currentTranscoder
);
if (nextResult.reverted) {
log.critical(
"getNextTranscoderInPool reverted for transcoder {} — enumeration truncated (POI divergence risk)",
[currentTranscoder.toHex()]
);
break;
}
currentTranscoder = nextResult.value;

transcoder = Transcoder.load(currentTranscoder.toHex());

Expand Down
63 changes: 63 additions & 0 deletions utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Bytes,
dataSource,
ethereum,
log,
} from "@graphprotocol/graph-ts";
import { RoundsManager } from "../src/types/RoundsManager/RoundsManager";
import {
Expand Down Expand Up @@ -461,6 +462,18 @@ export function getLptPriceEth(): BigDecimal {
return getPriceForPair(getUniswapV3LptEthPoolAddress());
}

// Last-known price per pool address; carried across handlers so a transient
// RPC revert (or a laggard backend returning a wrong-but-valid 0 price) does
// not permanently zero the USD accumulators. The zero sentinel for
// "pool not yet deployed / off-network" is indistinguishable from "RPC
// hiccup" — carrying the last known value is strictly safer than emitting a
// silent zero that corrupts downstream totals.
//
// We use individual variables because AssemblyScript's support for Map with
// BigDecimal values is unreliable across graph-ts versions.
let lastKnownDaiEthPrice: BigDecimal = ZERO_BD;
let lastKnownLptEthPrice: BigDecimal = ZERO_BD;

export function getPriceForPair(address: string): BigDecimal {
let pricePair = ZERO_BD;

Expand All @@ -478,6 +491,56 @@ export function getPriceForPair(address: string): BigDecimal {
BigInt.fromI32(18)
);
pricePair = prices[1];
// Only cache a non-zero price so we don't persist the "pool not yet
// deployed" zero as the last-known value (see issue #248, case 3).
if (!pricePair.equals(ZERO_BD)) {
if (address == getUniswapV3DaiEthPoolAddress()) {
lastKnownDaiEthPrice = pricePair;
} else if (address == getUniswapV3LptEthPoolAddress()) {
lastKnownLptEthPrice = pricePair;
}
}
} else {
// RPC call reverted — carry forward the last successfully read price.
if (address == getUniswapV3DaiEthPoolAddress()) {
if (!lastKnownDaiEthPrice.equals(ZERO_BD)) {
log.warning(
"slot0 call reverted for DAI/ETH pool {}; carrying forward last known price {}",
[address, lastKnownDaiEthPrice.toString()]
);
pricePair = lastKnownDaiEthPrice;
} else {
log.info(
"slot0 call reverted for DAI/ETH pool {} with no cached price",
[address]
);
}
} else if (address == getUniswapV3LptEthPoolAddress()) {
if (!lastKnownLptEthPrice.equals(ZERO_BD)) {
log.warning(
"slot0 call reverted for LPT/ETH pool {}; carrying forward last known price {}",
[address, lastKnownLptEthPrice.toString()]
);
pricePair = lastKnownLptEthPrice;
} else {
// lastKnownLptEthPrice resets on every subgraph restart; fall
// back to the entity-backed protocol.lptPriceEth first.
let protocol = createOrLoadProtocol();
if (!protocol.lptPriceEth.equals(ZERO_BD)) {
log.warning(
"slot0 call reverted for LPT/ETH pool {} with no in-memory cache; " +
"carrying forward durable protocol.lptPriceEth {}",
[address, protocol.lptPriceEth.toString()]
);
pricePair = protocol.lptPriceEth;
} else {
log.info(
"slot0 call reverted for LPT/ETH pool {} with no cached price",
[address]
);
}
}
}
}
}

Expand Down