Skip to content
Merged
75 changes: 30 additions & 45 deletions src/mappings/helpers/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -983,9 +983,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 +1242,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 +1259,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 +1301,32 @@ 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,
): GraphNetwork {
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)
return graphNetwork
}

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,
)
graphNetwork = addCurationFeesToNetworkSignal(graphNetwork, deployment, event.params.curationFees)
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,
)
graphNetwork = addCurationFeesToNetworkSignal(graphNetwork, deployment, event.params.curationFees)
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,
)
graphNetwork = addCurationFeesToNetworkSignal(graphNetwork, deployment, event.params.tokensCurators)
graphNetwork.totalIndexerQueryFeeRebates = graphNetwork.totalIndexerQueryFeeRebates.plus(
indexerQueryFees,
)
Expand Down
97 changes: 95 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,7 +23,13 @@ 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'

Expand Down Expand Up @@ -309,6 +315,93 @@ 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)!
graphNetwork = 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.
function curateAndPayFee(): 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)
graphNetwork = addCurationFeesToNetworkSignal(graphNetwork, deployment, fee)
graphNetwork.save()

handleBurned(mockBurned(curatorAddress, pool, BigInt.fromI32(125), BigInt.fromI32(1)))
}

describe('Curation fees indexed from the start', () => {
afterAll(() => {
clearStore()
})

test('preserves the fee-inclusive totals after a curator burns shares', () => {
curateAndPayFee()
assert.fieldEquals('GraphNetwork', '1', 'totalTokensSignalled', '375')
assert.fieldEquals('GraphNetwork', '1', 'totalTokensSignalledAutoMigrate', '125')
assert.fieldEquals('GraphNetwork', '1', 'totalTokensSignalledDirectly', '250')

// Loading the network again must not change totals or count its past fees again.
loadGraphNetwork().save()
createOrLoadGraphNetwork(blockNumber, controllerAddress).save()
assert.fieldEquals('GraphNetwork', '1', 'totalTokensSignalled', '375')
assert.fieldEquals('GraphNetwork', '1', 'totalTokensSignalledAutoMigrate', '125')
assert.fieldEquals('GraphNetwork', '1', 'totalTokensSignalledDirectly', '250')
})
})

describe('ParameterUpdated', () => {
beforeAll(() => {
// We need epoch length and delegation ratio to be nonzero for these tests
Expand Down
5 changes: 4 additions & 1 deletion tests/staking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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',
Expand Down
Loading