Skip to content
2 changes: 2 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ type GraphNetwork @entity(immutable: false) {
totalTokensSignalledAutoMigrate: BigDecimal!
"Total GRT currently curating to a specific version"
totalTokensSignalledDirectly: BigDecimal!
"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
Comment on lines +109 to +110

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed anymore


# Query fees globals
"Total query fees generated in the network"
Expand Down
120 changes: 74 additions & 46 deletions src/mappings/helpers/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,49 @@ 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
}
Comment on lines +693 to +696

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather keep this as it was, instead of adding a graft specific calculation.
As mentioned via Slack, grafting long term is an antipattern, so we can (and should) always assume we are indexing the data from scratch :)


// 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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this

if (graphNetwork.curationFeesInSignalTotal) return
graphNetwork.totalTokensSignalled = graphNetwork.totalTokensSignalled.plus(
graphNetwork.totalCuratorQueryFees,
)
graphNetwork.curationFeesInSignalTotal = true
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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this

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(
Expand Down Expand Up @@ -756,6 +798,7 @@ export function createOrLoadGraphNetwork(
graphNetwork.totalTokensSignalled = BigInt.fromI32(0)
graphNetwork.totalTokensSignalledAutoMigrate = BigDecimal.fromString('0')
graphNetwork.totalTokensSignalledDirectly = BigDecimal.fromString('0')
graphNetwork.curationFeesInSignalTotal = true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this


graphNetwork.totalQueryFees = BigInt.fromI32(0)
graphNetwork.totalIndexerQueryFeesCollected = BigInt.fromI32(0)
Expand Down Expand Up @@ -839,6 +882,7 @@ export function createOrLoadGraphNetwork(

graphNetwork.save()
}
countPastCurationFeesInSignal(graphNetwork)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this


if (!addresses.isL1) {
graphNetwork.currentL1BlockNumber = getL1BlockNumber()
Expand Down Expand Up @@ -983,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!)!
Expand Down Expand Up @@ -1242,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
Expand All @@ -1264,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,
Expand Down Expand Up @@ -1342,6 +1345,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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't exactly remember if this is still an issue with graph node, but at some point in order for graph-node to properly update your entities when modifying them in a separate function, you had to return the "new" updated entity, and reassign them to your variable. I'm like 95% sure this doesn't happen anymore, but potentially worth checking. I'll explain in the next line.

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())
Expand Down
6 changes: 4 additions & 2 deletions src/mappings/staking.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BigInt, BigDecimal, Bytes } from '@graphprotocol/graph-ts'
import { BigInt, BigDecimal } from '@graphprotocol/graph-ts'
import {
StakeDeposited,
StakeWithdrawn,
Expand Down Expand Up @@ -28,7 +28,6 @@ import {
GraphAccount,
Delegator,
DelegatedStake,
IndexerQueryFeePaymentAggregation,
} from '../types/schema'

import {
Expand All @@ -43,6 +42,7 @@ import {
updateDelegationExchangeRate,
calculatePricePerShare,
batchUpdateSubgraphSignalledTokens,
addCurationFeesToNetworkSignal,
createOrLoadGraphNetwork,
calculateCapacities,
createOrLoadIndexerQueryFeePaymentAggregation,
Expand Down Expand Up @@ -499,6 +499,7 @@ export function handleAllocationCollected(event: AllocationCollected): void {
graphNetwork.totalCuratorQueryFees = graphNetwork.totalCuratorQueryFees.plus(
event.params.curationFees,
)
addCurationFeesToNetworkSignal(graphNetwork, deployment, event.params.curationFees)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above, at some point, you used to need to do this:
graphNetwork = addCurationFeesToNetworkSignal(graphNetwork, deployment, event.params.curationFees)
Or graph node would not properly save the modifications done in the addCurationFeesToNetworkSignal function.

Unsure if that's still a thing, but worth mentioning.

graphNetwork.totalTaxedQueryFees = graphNetwork.totalTaxedQueryFees.plus(taxedFees)
graphNetwork.totalUnclaimedQueryFeeRebates = graphNetwork.totalUnclaimedQueryFeeRebates.plus(
event.params.rebateFees,
Expand Down Expand Up @@ -804,6 +805,7 @@ export function handleRebateCollected(event: RebateCollected): void {
graphNetwork.totalCuratorQueryFees = graphNetwork.totalCuratorQueryFees.plus(
event.params.curationFees,
)
addCurationFeesToNetworkSignal(graphNetwork, deployment, event.params.curationFees)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the last comment

graphNetwork.totalTaxedQueryFees = graphNetwork.totalTaxedQueryFees.plus(event.params.protocolTax)
graphNetwork.totalUnclaimedQueryFeeRebates = graphNetwork.totalUnclaimedQueryFeeRebates.plus(
event.params.queryFees,
Expand Down
3 changes: 2 additions & 1 deletion src/mappings/subgraphService.ts
Original file line number Diff line number Diff line change
@@ -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, calculateCapacities, loadGraphNetwork } from "./helpers/helpers"
import { Allocation, Indexer, PoiSubmission, SubgraphDeployment } from "../types/schema"
import { addresses } from "../../config/addresses"
import { tuplePrefixBytes } from "./helpers/decoder"
Expand Down Expand Up @@ -510,6 +510,7 @@ export function handleQueryFeesCollected(event: QueryFeesCollected): void {
graphNetwork.totalCuratorQueryFees = graphNetwork.totalCuratorQueryFees.plus(
event.params.tokensCurators,
)
addCurationFeesToNetworkSignal(graphNetwork, deployment, event.params.tokensCurators)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the last comment

graphNetwork.totalIndexerQueryFeeRebates = graphNetwork.totalIndexerQueryFeeRebates.plus(
indexerQueryFees,
)
Expand Down
143 changes: 141 additions & 2 deletions tests/curation.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -23,10 +23,18 @@ import { mockStakeDeposited, mockAllocationCreated, mockStakeDelegated } from '.

import { mockSignalled, mockBurned, mockParameterUpdated } from './factories/curation'

import { createOrLoadGraphNetwork } from '../src/mappings/helpers/helpers'
import {
addCurationFeesToNetworkSignal,
createOrLoadGraphNetwork,
loadGraphNetwork,
} from '../src/mappings/helpers/helpers'

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)
Expand Down Expand Up @@ -309,6 +317,137 @@ 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)
let deployment = subgraphDeploymentAddress
handleSignalled(mockSignalled(gnsAddress, deployment, value, BigInt.fromI32(1), zero))
handleSignalled(mockSignalled(curatorAddress, deployment, 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)
})
})

// 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()
})

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')
})

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', () => {
beforeAll(() => {
// We need epoch length and delegation ratio to be nonzero for these tests
Expand Down
Loading
Loading