From 324db47325cd3698e5a1ba745c7d25ac276dceb7 Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Fri, 25 Sep 2026 17:34:34 +0300 Subject: [PATCH 1/7] fix(curation): count curator query fees in the network signal total Curator fees paid into a curation pool raised each deployment's signal but not totalTokensSignalled, so the total fell behind by every fee ever paid: 1,801,315 GRT on Arbitrum by 2026-09-25. --- src/mappings/helpers/helpers.ts | 25 +++++++++++++++++++++++++ src/mappings/staking.ts | 3 +++ src/mappings/subgraphService.ts | 3 ++- tests/staking.test.ts | 3 +++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/mappings/helpers/helpers.ts b/src/mappings/helpers/helpers.ts index 4d814039..97433b46 100644 --- a/src/mappings/helpers/helpers.ts +++ b/src/mappings/helpers/helpers.ts @@ -1342,6 +1342,31 @@ export function updateCurrentDeploymentLinks( } } +// Curation fees raise a pool's signalled tokens without minting signal, so count them here. +// Every share gains the same value: the GNS contract's shares take their part as +// auto-migrating signal and the rest counts as signalled directly. +export function addCurationFeesToNetworkSignal( + graphNetwork: GraphNetwork, + deployment: SubgraphDeployment, + fees: BigInt, +): void { + graphNetwork.totalTokensSignalled = graphNetwork.totalTokensSignalled.plus(fees) + let toGns = BigDecimal.fromString('0') + let gnsSignal = Signal.load(joinID([graphNetwork.gns.toHexString(), deployment.id])) + if (gnsSignal != null && !deployment.signalAmount.isZero()) { + toGns = fees + .toBigDecimal() + .times(gnsSignal.signal.toBigDecimal()) + .div(deployment.signalAmount.toBigDecimal()) + } + graphNetwork.totalTokensSignalledAutoMigrate = graphNetwork.totalTokensSignalledAutoMigrate + .plus(toGns) + .truncate(18) + graphNetwork.totalTokensSignalledDirectly = graphNetwork.totalTokensSignalledDirectly + .plus(fees.toBigDecimal().minus(toGns)) + .truncate(18) +} + export function batchUpdateSubgraphSignalledTokens(deployment: SubgraphDeployment): void { for (let i = 0; i < deployment.subgraphCount; i++) { let id = deployment.id.concat('-').concat(BigInt.fromI32(i).toString()) diff --git a/src/mappings/staking.ts b/src/mappings/staking.ts index 5eac0814..e2b2174b 100644 --- a/src/mappings/staking.ts +++ b/src/mappings/staking.ts @@ -43,6 +43,7 @@ import { updateDelegationExchangeRate, calculatePricePerShare, batchUpdateSubgraphSignalledTokens, + addCurationFeesToNetworkSignal, createOrLoadGraphNetwork, calculateCapacities, createOrLoadIndexerQueryFeePaymentAggregation, @@ -499,6 +500,7 @@ export function handleAllocationCollected(event: AllocationCollected): void { graphNetwork.totalCuratorQueryFees = graphNetwork.totalCuratorQueryFees.plus( event.params.curationFees, ) + addCurationFeesToNetworkSignal(graphNetwork, deployment, event.params.curationFees) graphNetwork.totalTaxedQueryFees = graphNetwork.totalTaxedQueryFees.plus(taxedFees) graphNetwork.totalUnclaimedQueryFeeRebates = graphNetwork.totalUnclaimedQueryFeeRebates.plus( event.params.rebateFees, @@ -804,6 +806,7 @@ export function handleRebateCollected(event: RebateCollected): void { graphNetwork.totalCuratorQueryFees = graphNetwork.totalCuratorQueryFees.plus( event.params.curationFees, ) + addCurationFeesToNetworkSignal(graphNetwork, deployment, event.params.curationFees) graphNetwork.totalTaxedQueryFees = graphNetwork.totalTaxedQueryFees.plus(event.params.protocolTax) graphNetwork.totalUnclaimedQueryFeeRebates = graphNetwork.totalUnclaimedQueryFeeRebates.plus( event.params.queryFees, diff --git a/src/mappings/subgraphService.ts b/src/mappings/subgraphService.ts index 69b390b8..7586e3a9 100644 --- a/src/mappings/subgraphService.ts +++ b/src/mappings/subgraphService.ts @@ -1,6 +1,6 @@ import { BigDecimal, BigInt, ByteArray, Bytes, crypto, ethereum, log } from "@graphprotocol/graph-ts" import { AllocationClosed, AllocationCreated, AllocationResized, CurationCutSet, DelegationRatioSet, IndexingRewardsCollected, MaxPOIStalenessSet, POIPresented, ProvisionTokensRangeSet, QueryFeesCollected, RewardsDestinationSet, ServiceProviderRegistered, StakeToFeesRatioSet, ThawingPeriodRangeSet, VerifierCutRangeSet } from "../types/SubgraphService/SubgraphService" -import { batchUpdateSubgraphSignalledTokens, calculatePricePerShare, createOrLoadDataService, createOrLoadGraphNetwork, createOrLoadEpoch,createOrLoadIndexerQueryFeePaymentAggregation, createOrLoadPaymentSource, createOrLoadProvision, createOrLoadSubgraphDeployment, joinID, updateDelegationExchangeRate, calculateCapacities, loadGraphNetwork } from "./helpers/helpers" +import { addCurationFeesToNetworkSignal, batchUpdateSubgraphSignalledTokens, calculatePricePerShare, createOrLoadDataService, createOrLoadGraphNetwork, createOrLoadEpoch,createOrLoadIndexerQueryFeePaymentAggregation, createOrLoadPaymentSource, createOrLoadProvision, createOrLoadSubgraphDeployment, joinID, updateDelegationExchangeRate, calculateCapacities, loadGraphNetwork } from "./helpers/helpers" import { Allocation, Indexer, PoiSubmission, SubgraphDeployment } from "../types/schema" import { addresses } from "../../config/addresses" import { tuplePrefixBytes } from "./helpers/decoder" @@ -510,6 +510,7 @@ export function handleQueryFeesCollected(event: QueryFeesCollected): void { graphNetwork.totalCuratorQueryFees = graphNetwork.totalCuratorQueryFees.plus( event.params.tokensCurators, ) + addCurationFeesToNetworkSignal(graphNetwork, deployment, event.params.tokensCurators) graphNetwork.totalIndexerQueryFeeRebates = graphNetwork.totalIndexerQueryFeeRebates.plus( indexerQueryFees, ) diff --git a/tests/staking.test.ts b/tests/staking.test.ts index a8fdcbdc..f4beaedd 100644 --- a/tests/staking.test.ts +++ b/tests/staking.test.ts @@ -777,6 +777,9 @@ describe('ALLOCATION LIFE CYCLE', () => { rebateFees.toString(), ) assert.fieldEquals('GraphNetwork', '1', 'totalCuratorQueryFees', curationFees.toString()) + // Curation fees are paid into the curation pool, so they are signal too + assert.fieldEquals('GraphNetwork', '1', 'totalTokensSignalled', curationFees.toString()) + assert.fieldEquals('GraphNetwork', '1', 'totalTokensSignalledDirectly', curationFees.toString()) assert.fieldEquals( 'GraphNetwork', '1', From 2b2355882948b5d343dcb02ae8d374afae3b73ca Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Fri, 25 Sep 2026 17:47:19 +0300 Subject: [PATCH 2/7] test(curation): check how fees split between GNS and direct signal The fee test only had a pool with no GNS signal. This one gives the GNS contract a quarter of a pool and checks it takes a quarter of the fees as auto-migrating signal. --- tests/curation.test.ts | 47 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/tests/curation.test.ts b/tests/curation.test.ts index 8e7729f0..a5783c33 100644 --- a/tests/curation.test.ts +++ b/tests/curation.test.ts @@ -1,4 +1,4 @@ -import { Address, BigInt, Bytes, ethereum } from '@graphprotocol/graph-ts' +import { Address, BigDecimal, BigInt, Bytes, ethereum } from '@graphprotocol/graph-ts' import { assert, @@ -23,7 +23,12 @@ import { mockStakeDeposited, mockAllocationCreated, mockStakeDelegated } from '. import { mockSignalled, mockBurned, mockParameterUpdated } from './factories/curation' -import { createOrLoadGraphNetwork } from '../src/mappings/helpers/helpers' +import { + addCurationFeesToNetworkSignal, + createOrLoadGraphNetwork, +} from '../src/mappings/helpers/helpers' + +import { GraphNetwork, SubgraphDeployment } from '../src/types/schema' import { mockTransfer } from './factories/graphToken' @@ -309,6 +314,44 @@ describe('Burned', () => { }) }) +describe('Curation fees', () => { + beforeAll(() => { + let graphNetwork = createOrLoadGraphNetwork(blockNumber, controllerAddress) + graphNetwork.epochLength = epochLength + graphNetwork.lastLengthUpdateBlock = 1 + graphNetwork.save() + handleTransfer(mockTransfer(graphAddress, curatorAddress, value)) + + // The GNS contract holds 1 of the pool's 4 shares and a curator holds the other 3 + let gnsAddress = Address.fromBytes(graphNetwork.gns) + let zero = BigInt.fromI32(0) + handleSignalled(mockSignalled(gnsAddress, subgraphDeploymentAddress, value, BigInt.fromI32(1), zero)) + handleSignalled(mockSignalled(curatorAddress, subgraphDeploymentAddress, value, BigInt.fromI32(3), zero)) + }) + + afterAll(() => { + clearStore() + }) + + test('splits fees between auto-migrating and direct signal by share of the pool', () => { + let graphNetwork = GraphNetwork.load('1')! + let total = graphNetwork.totalTokensSignalled + let autoMigrate = graphNetwork.totalTokensSignalledAutoMigrate + let directly = graphNetwork.totalTokensSignalledDirectly + + let deployment = SubgraphDeployment.load(subgraphDeploymentID)! + addCurationFeesToNetworkSignal(graphNetwork, deployment, BigInt.fromI32(100)) + graphNetwork.save() + + let totalAfter = total.plus(BigInt.fromI32(100)).toString() + let autoMigrateAfter = autoMigrate.plus(BigDecimal.fromString('25')).toString() + let directlyAfter = directly.plus(BigDecimal.fromString('75')).toString() + assert.fieldEquals('GraphNetwork', '1', 'totalTokensSignalled', totalAfter) + assert.fieldEquals('GraphNetwork', '1', 'totalTokensSignalledAutoMigrate', autoMigrateAfter) + assert.fieldEquals('GraphNetwork', '1', 'totalTokensSignalledDirectly', directlyAfter) + }) +}) + describe('ParameterUpdated', () => { beforeAll(() => { // We need epoch length and delegation ratio to be nonzero for these tests From d104d14c899efe3b695e0791ba5a99f07b9dd063 Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Fri, 25 Sep 2026 19:56:02 +0300 Subject: [PATCH 3/7] fix(curation): add back fees a grafted deployment left out of signal A deployment grafted from a version that left curator query fees out of totalTokensSignalled inherits a total short by exactly the fees paid so far, so add them back once and record it. --- schema.graphql | 2 ++ src/mappings/helpers/helpers.ts | 17 +++++++++++++++- tests/curation.test.ts | 35 +++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/schema.graphql b/schema.graphql index 78ce3ef2..7b13f4c3 100644 --- a/schema.graphql +++ b/schema.graphql @@ -106,6 +106,8 @@ type GraphNetwork @entity(immutable: false) { totalTokensSignalledAutoMigrate: BigDecimal! "Total GRT currently curating to a specific version" totalTokensSignalledDirectly: BigDecimal! + "Whether totalTokensSignalled includes every curator query fee paid into curation pools. Unset only on a deployment grafted from a version that left them out, until its next update adds them" + curationFeesInSignalTotal: Boolean # Query fees globals "Total query fees generated in the network" diff --git a/src/mappings/helpers/helpers.ts b/src/mappings/helpers/helpers.ts index 97433b46..ee3b2ef3 100644 --- a/src/mappings/helpers/helpers.ts +++ b/src/mappings/helpers/helpers.ts @@ -690,7 +690,20 @@ export function createEpoch(startBlock: i32, epochLength: i32, epochNumber: i32) export function loadGraphNetwork(): GraphNetwork { // Should only be called whenever we are sure a GraphNetwork entity exists. // This is only made to centralize the load statements that are everywhere - return GraphNetwork.load('1')! + let graphNetwork = GraphNetwork.load('1')! + countPastCurationFeesInSignal(graphNetwork) + return graphNetwork +} + +// Versions that left curation fees out of totalTokensSignalled fell short by exactly the fees +// paid, and a deployment grafted from one inherits that, so add them back once. How those fees +// split between auto-migrating and direct signal cannot be rebuilt, so the split stays as it was. +function countPastCurationFeesInSignal(graphNetwork: GraphNetwork): void { + if (graphNetwork.curationFeesInSignalTotal) return + graphNetwork.totalTokensSignalled = graphNetwork.totalTokensSignalled.plus( + graphNetwork.totalCuratorQueryFees, + ) + graphNetwork.curationFeesInSignalTotal = true } export function createOrLoadGraphNetwork( @@ -756,6 +769,7 @@ export function createOrLoadGraphNetwork( graphNetwork.totalTokensSignalled = BigInt.fromI32(0) graphNetwork.totalTokensSignalledAutoMigrate = BigDecimal.fromString('0') graphNetwork.totalTokensSignalledDirectly = BigDecimal.fromString('0') + graphNetwork.curationFeesInSignalTotal = true graphNetwork.totalQueryFees = BigInt.fromI32(0) graphNetwork.totalIndexerQueryFeesCollected = BigInt.fromI32(0) @@ -839,6 +853,7 @@ export function createOrLoadGraphNetwork( graphNetwork.save() } + countPastCurationFeesInSignal(graphNetwork) if (!addresses.isL1) { graphNetwork.currentL1BlockNumber = getL1BlockNumber() diff --git a/tests/curation.test.ts b/tests/curation.test.ts index a5783c33..be3e6647 100644 --- a/tests/curation.test.ts +++ b/tests/curation.test.ts @@ -26,6 +26,7 @@ import { mockSignalled, mockBurned, mockParameterUpdated } from './factories/cur import { addCurationFeesToNetworkSignal, createOrLoadGraphNetwork, + loadGraphNetwork, } from '../src/mappings/helpers/helpers' import { GraphNetwork, SubgraphDeployment } from '../src/types/schema' @@ -352,6 +353,40 @@ describe('Curation fees', () => { }) }) +describe('Curation fees paid before they were counted', () => { + afterAll(() => { + clearStore() + }) + + test('are added to the signal total once on a deployment grafted without them', () => { + // A grafted base: 100 GRT in the curation contract, 30 of it fees the total left out + let graphNetwork = createOrLoadGraphNetwork(blockNumber, controllerAddress) + graphNetwork.totalTokensSignalled = BigInt.fromI32(70) + graphNetwork.totalCuratorQueryFees = BigInt.fromI32(30) + graphNetwork.unset('curationFeesInSignalTotal') + graphNetwork.save() + + loadGraphNetwork().save() + assert.fieldEquals('GraphNetwork', '1', 'totalTokensSignalled', '100') + assert.fieldEquals('GraphNetwork', '1', 'curationFeesInSignalTotal', 'true') + + loadGraphNetwork().save() + createOrLoadGraphNetwork(blockNumber, controllerAddress).save() + assert.fieldEquals('GraphNetwork', '1', 'totalTokensSignalled', '100') + }) + + test('are not added again on a network indexed from the start', () => { + clearStore() + let graphNetwork = createOrLoadGraphNetwork(blockNumber, controllerAddress) + graphNetwork.totalCuratorQueryFees = BigInt.fromI32(30) + graphNetwork.save() + + loadGraphNetwork().save() + assert.fieldEquals('GraphNetwork', '1', 'totalTokensSignalled', '0') + assert.fieldEquals('GraphNetwork', '1', 'curationFeesInSignalTotal', 'true') + }) +}) + describe('ParameterUpdated', () => { beforeAll(() => { // We need epoch length and delegation ratio to be nonzero for these tests From f1b54d9ccdb449a795b0208525e8cb086724369a Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Mon, 28 Sep 2026 23:05:45 +0300 Subject: [PATCH 4/7] chore(lint): drop 4 unused imports from the staking and service mappings Lint flagged these imports as unused in files this branch already touches. --- src/mappings/staking.ts | 3 +-- src/mappings/subgraphService.ts | 2 +- tests/staking.test.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/mappings/staking.ts b/src/mappings/staking.ts index e2b2174b..b7bc0faf 100644 --- a/src/mappings/staking.ts +++ b/src/mappings/staking.ts @@ -1,4 +1,4 @@ -import { BigInt, BigDecimal, Bytes } from '@graphprotocol/graph-ts' +import { BigInt, BigDecimal } from '@graphprotocol/graph-ts' import { StakeDeposited, StakeWithdrawn, @@ -28,7 +28,6 @@ import { GraphAccount, Delegator, DelegatedStake, - IndexerQueryFeePaymentAggregation, } from '../types/schema' import { diff --git a/src/mappings/subgraphService.ts b/src/mappings/subgraphService.ts index 7586e3a9..bc94666a 100644 --- a/src/mappings/subgraphService.ts +++ b/src/mappings/subgraphService.ts @@ -1,6 +1,6 @@ import { BigDecimal, BigInt, ByteArray, Bytes, crypto, ethereum, log } from "@graphprotocol/graph-ts" import { AllocationClosed, AllocationCreated, AllocationResized, CurationCutSet, DelegationRatioSet, IndexingRewardsCollected, MaxPOIStalenessSet, POIPresented, ProvisionTokensRangeSet, QueryFeesCollected, RewardsDestinationSet, ServiceProviderRegistered, StakeToFeesRatioSet, ThawingPeriodRangeSet, VerifierCutRangeSet } from "../types/SubgraphService/SubgraphService" -import { addCurationFeesToNetworkSignal, batchUpdateSubgraphSignalledTokens, calculatePricePerShare, createOrLoadDataService, createOrLoadGraphNetwork, createOrLoadEpoch,createOrLoadIndexerQueryFeePaymentAggregation, createOrLoadPaymentSource, createOrLoadProvision, createOrLoadSubgraphDeployment, joinID, updateDelegationExchangeRate, calculateCapacities, loadGraphNetwork } from "./helpers/helpers" +import { addCurationFeesToNetworkSignal, batchUpdateSubgraphSignalledTokens, calculatePricePerShare, createOrLoadDataService, createOrLoadGraphNetwork, createOrLoadEpoch,createOrLoadIndexerQueryFeePaymentAggregation, createOrLoadPaymentSource, createOrLoadProvision, createOrLoadSubgraphDeployment, joinID, calculateCapacities, loadGraphNetwork } from "./helpers/helpers" import { Allocation, Indexer, PoiSubmission, SubgraphDeployment } from "../types/schema" import { addresses } from "../../config/addresses" import { tuplePrefixBytes } from "./helpers/decoder" diff --git a/tests/staking.test.ts b/tests/staking.test.ts index f4beaedd..871722fe 100644 --- a/tests/staking.test.ts +++ b/tests/staking.test.ts @@ -43,7 +43,7 @@ import { } from './factories/staking' import { createOrLoadGraphNetwork, loadGraphNetwork, getHorizonDelegatedStakeIDFromLegacy } from '../src/mappings/helpers/helpers' import { mockTransfer } from './factories/graphToken' -import { GraphNetwork, GraphAccount } from '../src/types/schema' +import { GraphAccount } from '../src/types/schema' // CONSTANT ADDRESS OR IDS const graphID = '0x0000000000000000000000000000000000000000' From aafee701b6b2b99f8f6a9d0a6250b076ee8a3c69 Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Tue, 29 Sep 2026 10:45:31 +0300 Subject: [PATCH 5/7] fix(curation): rebuild the signal split when adding back past fees Network signal is split into GRT curated through the GNS contract and GRT curated directly. Adding past fees to the total alone left the 2 parts 1,801,315 GRT short of it on Arbitrum, so the GNS part is reset to what its shares are worth and direct gets the rest. L1 is unchanged. --- schema.graphql | 2 +- src/mappings/helpers/helpers.ts | 31 +++++++++++++++-- tests/curation.test.ts | 60 +++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 4 deletions(-) diff --git a/schema.graphql b/schema.graphql index 7b13f4c3..8be66f1f 100644 --- a/schema.graphql +++ b/schema.graphql @@ -106,7 +106,7 @@ type GraphNetwork @entity(immutable: false) { totalTokensSignalledAutoMigrate: BigDecimal! "Total GRT currently curating to a specific version" totalTokensSignalledDirectly: BigDecimal! - "Whether totalTokensSignalled includes every curator query fee paid into curation pools. Unset only on a deployment grafted from a version that left them out, until its next update adds them" + "Whether totalTokensSignalled, and on Arbitrum its auto-migrating and direct parts, include every curator query fee paid into curation pools. Unset only on a deployment grafted from a version that left them out, until its next update adds them" curationFeesInSignalTotal: Boolean # Query fees globals diff --git a/src/mappings/helpers/helpers.ts b/src/mappings/helpers/helpers.ts index ee3b2ef3..33b724ae 100644 --- a/src/mappings/helpers/helpers.ts +++ b/src/mappings/helpers/helpers.ts @@ -695,15 +695,40 @@ export function loadGraphNetwork(): GraphNetwork { return graphNetwork } -// Versions that left curation fees out of totalTokensSignalled fell short by exactly the fees -// paid, and a deployment grafted from one inherits that, so add them back once. How those fees -// split between auto-migrating and direct signal cannot be rebuilt, so the split stays as it was. +// Versions that left curation fees out of totalTokensSignalled fell short by exactly the fees paid, +// and a graft inherits that, so add them back once and re-split the total by the value of the GNS +// contract's shares. Not on L1, which subtracts signal sent to L2 twice, so this would not match. function countPastCurationFeesInSignal(graphNetwork: GraphNetwork): void { if (graphNetwork.curationFeesInSignalTotal) return graphNetwork.totalTokensSignalled = graphNetwork.totalTokensSignalled.plus( graphNetwork.totalCuratorQueryFees, ) graphNetwork.curationFeesInSignalTotal = true + if (addresses.isL1) return + + let autoMigrate = BigDecimal.fromString('0') + let gnsCurator = Curator.load(graphNetwork.gns.toHexString()) + if (gnsCurator != null) { + let gnsSignals = gnsCurator.signals.load() + for (let i = 0; i < gnsSignals.length; i++) { + let gnsSignal = gnsSignals[i] + if (gnsSignal.signal.isZero()) continue + let deployment = SubgraphDeployment.load(gnsSignal.subgraphDeployment)! + if (deployment.signalAmount.isZero()) continue + autoMigrate = autoMigrate + .plus( + gnsSignal.signal + .toBigDecimal() + .times(deployment.signalledTokens.toBigDecimal()) + .div(deployment.signalAmount.toBigDecimal()), + ) + .truncate(18) + } + } + graphNetwork.totalTokensSignalledAutoMigrate = autoMigrate + graphNetwork.totalTokensSignalledDirectly = graphNetwork.totalTokensSignalled + .toBigDecimal() + .minus(autoMigrate) } export function createOrLoadGraphNetwork( diff --git a/tests/curation.test.ts b/tests/curation.test.ts index be3e6647..891133fe 100644 --- a/tests/curation.test.ts +++ b/tests/curation.test.ts @@ -33,6 +33,8 @@ import { GraphNetwork, SubgraphDeployment } from '../src/types/schema' import { mockTransfer } from './factories/graphToken' +import { addresses } from '../config/addresses' + // CONSTANT ADDRESS OR IDS const graphID = '0x0000000000000000000000000000000000000000' const graphAddress = Address.fromString(graphID) @@ -353,6 +355,34 @@ describe('Curation fees', () => { }) }) +// The GNS contract signals 100 GRT for 1 share and a curator 300 GRT for 3, 100 GRT of fees go +// into the pool, then the curator burns 1 share. countFee says if the fee counts in signal totals. +function curateAndPayFee(countFee: boolean): void { + let graphNetwork = createOrLoadGraphNetwork(blockNumber, controllerAddress) + graphNetwork.epochLength = epochLength + graphNetwork.lastLengthUpdateBlock = 1 + graphNetwork.save() + handleTransfer(mockTransfer(graphAddress, curatorAddress, value)) + + let gns = Address.fromBytes(graphNetwork.gns) + let pool = subgraphDeploymentAddress + let zero = BigInt.fromI32(0) + handleSignalled(mockSignalled(gns, pool, BigInt.fromI32(100), BigInt.fromI32(1), zero)) + handleSignalled(mockSignalled(curatorAddress, pool, BigInt.fromI32(300), BigInt.fromI32(3), zero)) + + // What a fee handler does to the pool and the network + let fee = BigInt.fromI32(100) + let deployment = SubgraphDeployment.load(subgraphDeploymentID)! + deployment.signalledTokens = deployment.signalledTokens.plus(fee) + deployment.save() + graphNetwork = loadGraphNetwork() + graphNetwork.totalCuratorQueryFees = graphNetwork.totalCuratorQueryFees.plus(fee) + if (countFee) addCurationFeesToNetworkSignal(graphNetwork, deployment, fee) + graphNetwork.save() + + handleBurned(mockBurned(curatorAddress, pool, BigInt.fromI32(125), BigInt.fromI32(1))) +} + describe('Curation fees paid before they were counted', () => { afterAll(() => { clearStore() @@ -385,6 +415,36 @@ describe('Curation fees paid before they were counted', () => { assert.fieldEquals('GraphNetwork', '1', 'totalTokensSignalled', '0') assert.fieldEquals('GraphNetwork', '1', 'curationFeesInSignalTotal', 'true') }) + + test('are split as a full sync would on Arbitrum, while L1 keeps its old split', () => { + clearStore() + curateAndPayFee(true) + let fullSync = GraphNetwork.load('1')! + + // A base grafted from a version that left the fee out of all 3 totals + clearStore() + curateAndPayFee(false) + let graphNetwork = loadGraphNetwork() + let autoMigrateBefore = graphNetwork.totalTokensSignalledAutoMigrate + let directlyBefore = graphNetwork.totalTokensSignalledDirectly + assert.assertTrue(autoMigrateBefore < fullSync.totalTokensSignalledAutoMigrate) + assert.assertTrue(directlyBefore < fullSync.totalTokensSignalledDirectly) + graphNetwork.unset('curationFeesInSignalTotal') + graphNetwork.save() + + loadGraphNetwork().save() + let total = fullSync.totalTokensSignalled + let autoMigrate = addresses.isL1 ? autoMigrateBefore : fullSync.totalTokensSignalledAutoMigrate + let directly = addresses.isL1 ? directlyBefore : fullSync.totalTokensSignalledDirectly + assert.fieldEquals('GraphNetwork', '1', 'totalTokensSignalled', total.toString()) + assert.fieldEquals( + 'GraphNetwork', + '1', + 'totalTokensSignalledAutoMigrate', + autoMigrate.toString(), + ) + assert.fieldEquals('GraphNetwork', '1', 'totalTokensSignalledDirectly', directly.toString()) + }) }) describe('ParameterUpdated', () => { From 23cb6d75546eafa8735629c085076812e9158d06 Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Tue, 29 Sep 2026 10:49:13 +0300 Subject: [PATCH 6/7] fix(curation): save the network right after adding back past fees The one-time step that adds back past curator fees walks every pool the GNS contract curates. Saving straight away stops each later handler in the same block from repeating that walk. --- src/mappings/helpers/helpers.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/mappings/helpers/helpers.ts b/src/mappings/helpers/helpers.ts index 33b724ae..df59d95b 100644 --- a/src/mappings/helpers/helpers.ts +++ b/src/mappings/helpers/helpers.ts @@ -704,8 +704,12 @@ function countPastCurationFeesInSignal(graphNetwork: GraphNetwork): void { graphNetwork.totalCuratorQueryFees, ) graphNetwork.curationFeesInSignalTotal = true - if (addresses.isL1) return + if (!addresses.isL1) splitSignalByGnsShareValue(graphNetwork) + // Save now so later loads in this block see the flag instead of walking every GNS signal again + graphNetwork.save() +} +function splitSignalByGnsShareValue(graphNetwork: GraphNetwork): void { let autoMigrate = BigDecimal.fromString('0') let gnsCurator = Curator.load(graphNetwork.gns.toHexString()) if (gnsCurator != null) { From 843d0cae69bec9b4fd83d5c9bf3b675ad368b094 Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Tue, 29 Sep 2026 10:49:13 +0300 Subject: [PATCH 7/7] chore(cleanup): drop dead helpers and trim long comments and test lines Removes 3 commented-out helper functions, shortens 2 comments that ran long, and wraps 2 test lines that were wider than the formatter's 100-character limit. --- src/mappings/helpers/helpers.ts | 49 +++------------------------------ tests/curation.test.ts | 5 ++-- 2 files changed, 7 insertions(+), 47 deletions(-) diff --git a/src/mappings/helpers/helpers.ts b/src/mappings/helpers/helpers.ts index df59d95b..20478596 100644 --- a/src/mappings/helpers/helpers.ts +++ b/src/mappings/helpers/helpers.ts @@ -1027,9 +1027,9 @@ function createGraphAccountName( graphAccountName.graphAccount = graphAccount graphAccountName.save() // check that this name is not already used by another graph account (changing ownership) - // If so, remove the old owner, and set the new one + // If so, move the name from its former owner to the new one } else if (graphAccountName.graphAccount != graphAccount) { - // Only update the old graph account if it exists + // Only update the former owner's graph account if it exists if (graphAccountName.graphAccount != null) { // Set defaultDisplayName to null if they lost ownership of this name let oldGraphAccount = GraphAccount.load(graphAccountName.graphAccount!)! @@ -1286,13 +1286,8 @@ export function calculateCapacitiesLegacy(indexer: Indexer): Indexer { } export function calculatePricePerShare(deployment: SubgraphDeployment): BigDecimal { - // TODO check why there's a deviation from the values of the bancor formula - // Ideally this would be a 1 to 1 recreation of the share sell formula, but due to - // implementation issues for that formula on AssemblyScript (mainly BigDecimal missing pow implementation) - // I decided to use an approximation derived from testing. - - // This value could be wrong unfortunately, so we should ideally find a workaround later - // to implement the actual sell share formula for 1 share. + // TODO: implement the bancor sell formula for 1 share exactly. This is an approximation found + // by testing, because AssemblyScript's BigDecimal has no pow, so it can deviate from the formula. // reserve ratio multiplier = MAX_WEIGHT / reserveRatio = 1M (ppm) / reserveRatio // HOTFIX for now, if deployment.reserveRatio -> 0, use a known previous default @@ -1308,42 +1303,6 @@ export function calculatePricePerShare(deployment: SubgraphDeployment): BigDecim return pricePerShare } -// export function createOrLoadNetwork(id: string): Network { -// let network = Network.load(id) -// if (network == null) { -// network = new Network(id) - -// network.save() -// } -// return network as Network -// } - -// export function createOrLoadSubgraphCategory(id: string): SubgraphCategory { -// let category = SubgraphCategory.load(id) -// if (category == null) { -// category = new SubgraphCategory(id) - -// category.save() -// } -// return category as SubgraphCategory -// } - -// export function createOrLoadSubgraphCategoryRelation( -// categoryId: string, -// subgraphMetadataId: string, -// ): SubgraphCategoryRelation { -// let id = joinID([categoryId, subgraphMetadataId]) -// let relation = SubgraphCategoryRelation.load(id) -// if (relation == null) { -// relation = new SubgraphCategoryRelation(id) -// relation.metadata = subgraphMetadataId -// relation.category = categoryId - -// relation.save() -// } -// return relation as SubgraphCategoryRelation -// } - export function updateCurrentDeploymentLinks( oldDeployment: SubgraphDeployment | null, newDeployment: SubgraphDeployment | null, diff --git a/tests/curation.test.ts b/tests/curation.test.ts index 891133fe..ee9691d1 100644 --- a/tests/curation.test.ts +++ b/tests/curation.test.ts @@ -328,8 +328,9 @@ describe('Curation fees', () => { // The GNS contract holds 1 of the pool's 4 shares and a curator holds the other 3 let gnsAddress = Address.fromBytes(graphNetwork.gns) let zero = BigInt.fromI32(0) - handleSignalled(mockSignalled(gnsAddress, subgraphDeploymentAddress, value, BigInt.fromI32(1), zero)) - handleSignalled(mockSignalled(curatorAddress, subgraphDeploymentAddress, value, BigInt.fromI32(3), zero)) + let deployment = subgraphDeploymentAddress + handleSignalled(mockSignalled(gnsAddress, deployment, value, BigInt.fromI32(1), zero)) + handleSignalled(mockSignalled(curatorAddress, deployment, value, BigInt.fromI32(3), zero)) }) afterAll(() => {