diff --git a/src/mappings/helpers/helpers.ts b/src/mappings/helpers/helpers.ts index d6365869..7c446c23 100644 --- a/src/mappings/helpers/helpers.ts +++ b/src/mappings/helpers/helpers.ts @@ -1010,9 +1010,21 @@ function max(a: BigDecimal, b: BigDecimal): BigDecimal { return a > b ? a : b } +// Own stake that still backs the indexer: excludes legacy locked (unstaking) tokens and +// Horizon provision tokens that are thawing. Both are zero when they don't apply. +export function getActiveIndexerStake(indexer: Indexer): BigInt { + return indexer.stakedTokens.minus(indexer.lockedTokens).minus(indexer.thawingTokens) +} + +// Delegation that still earns rewards and fees. Horizon keeps undelegated tokens in the pool +// (tracked in delegatedThawingTokens) until withdrawn, but new rewards only go to active shares. +export function getActiveIndexerDelegation(indexer: Indexer): BigInt { + return indexer.delegatedTokens.minus(indexer.delegatedThawingTokens) +} + export function calculateOwnStakeRatio(indexer: Indexer): BigDecimal { - let stakedTokensBD = indexer.stakedTokens.minus(indexer.lockedTokens).toBigDecimal() - let delegatedTokensBD = indexer.delegatedTokens.toBigDecimal() + let stakedTokensBD = getActiveIndexerStake(indexer).toBigDecimal() + let delegatedTokensBD = getActiveIndexerDelegation(indexer).toBigDecimal() let graphNetwork = loadGraphNetwork() let delegationRatioBD = BigInt.fromI32(graphNetwork.delegationRatio).toBigDecimal() let maxPossibleTotalUsable = stakedTokensBD + stakedTokensBD * delegationRatioBD @@ -1077,8 +1089,8 @@ export function calculateLegacyIndexerRewardOwnGenerationRatio(indexer: Indexer) } export function calculateOverdelegationDilution(indexer: Indexer): BigDecimal { - let stakedTokensBD = indexer.stakedTokens.toBigDecimal() - let delegatedTokensBD = indexer.delegatedTokens.toBigDecimal() + let stakedTokensBD = getActiveIndexerStake(indexer).toBigDecimal() + let delegatedTokensBD = getActiveIndexerDelegation(indexer).toBigDecimal() let graphNetwork = loadGraphNetwork() let delegationRatioBD = BigInt.fromI32(graphNetwork.delegationRatio).toBigDecimal() let maxDelegatedStake = stakedTokensBD * delegationRatioBD @@ -1112,18 +1124,31 @@ export function updateLegacyAdvancedIndexerMetrics(indexer: Indexer): Indexer { return indexer as Indexer } +// Provisioned tokens that are not thawing. +export function getActiveProvisionTokens(provision: Provision): BigInt { + return provision.tokensProvisioned.minus(provision.tokensThawing) +} + +// Delegated tokens that are not thawing. Thawing delegation stays in the pool until withdrawn, +// but rewards and query fees added to the pool only accrue to active shares. +export function getActiveProvisionDelegation(provision: Provision): BigInt { + return provision.delegatedTokens.minus(provision.delegatedThawingTokens) +} + export function calculateOwnStakeRatioForProvision(provision: Provision): BigDecimal { - let totalTokens = provision.tokensProvisioned.plus(provision.delegatedTokens) + let ownTokens = getActiveProvisionTokens(provision) + let totalTokens = ownTokens.plus(getActiveProvisionDelegation(provision)) return totalTokens == BigInt.fromI32(0) ? BigDecimal.fromString('0') - : provision.tokensProvisioned.toBigDecimal().div(totalTokens.toBigDecimal()) + : ownTokens.toBigDecimal().div(totalTokens.toBigDecimal()) } export function calculateDelegatedStakeRatioForProvision(provision: Provision): BigDecimal { - let totalTokens = provision.tokensProvisioned.plus(provision.delegatedTokens) + let delegatedTokens = getActiveProvisionDelegation(provision) + let totalTokens = getActiveProvisionTokens(provision).plus(delegatedTokens) return totalTokens == BigInt.fromI32(0) ? BigDecimal.fromString('0') - : provision.delegatedTokens.toBigDecimal().div(totalTokens.toBigDecimal()) + : delegatedTokens.toBigDecimal().div(totalTokens.toBigDecimal()) } export function calculateIndexingRewardEffectiveCutForProvision(provision: Provision): BigDecimal { @@ -1154,8 +1179,8 @@ export function calculateIndexerRewardOwnGenerationRatioForProvision(provision: } export function calculateOverdelegationDilutionForProvision(provision: Provision): BigDecimal { - let provisionedTokensBD = provision.tokensProvisioned.toBigDecimal() - let delegatedTokensBD = provision.delegatedTokens.toBigDecimal() + let provisionedTokensBD = getActiveProvisionTokens(provision).toBigDecimal() + let delegatedTokensBD = getActiveProvisionDelegation(provision).toBigDecimal() let dataService = DataService.load(provision.dataService)! if (dataService.delegationRatio == null) { return BigDecimal.fromString('0') diff --git a/src/mappings/horizonStaking.ts b/src/mappings/horizonStaking.ts index 69901ca8..b3b69960 100644 --- a/src/mappings/horizonStaking.ts +++ b/src/mappings/horizonStaking.ts @@ -20,6 +20,7 @@ export function handleHorizonStakeDeposited(event: HorizonStakeDeposited): void let indexer = createOrLoadIndexer(event.params.serviceProvider, event.block.timestamp, graphNetwork) let previousStake = indexer.stakedTokens indexer.stakedTokens = indexer.stakedTokens.plus(event.params.tokens) + indexer = updateAdvancedIndexerMetrics(indexer as Indexer) indexer.save() // Update graph network @@ -45,6 +46,7 @@ export function handleHorizonStakeLocked(event: HorizonStakeLocked): void { let indexer = Indexer.load(id)! indexer.lockedTokens = event.params.tokens indexer.tokensLockedUntil = event.params.until.toI32() + indexer = updateAdvancedIndexerMetrics(indexer as Indexer) indexer.save() // update graph network @@ -62,6 +64,7 @@ export function handleHorizonStakeWithdrawn(event: HorizonStakeWithdrawn): void indexer.stakedTokens = indexer.stakedTokens.minus(event.params.tokens) indexer.lockedTokens = BigInt.fromI32(0) // set to 0 to prevent issues when Stage 2 comes indexer.tokensLockedUntil = 0 // always set to 0 when withdrawn + indexer = updateAdvancedIndexerMetrics(indexer as Indexer) indexer.save() // Update graph network @@ -84,6 +87,7 @@ export function handleProvisionCreated(event: ProvisionCreated): void { let provision = createOrLoadProvision(event.params.serviceProvider, event.params.verifier, event.block.timestamp) indexer.provisionedTokens = indexer.provisionedTokens.plus(event.params.tokens) + indexer = updateAdvancedIndexerMetrics(indexer as Indexer) indexer = calculateCapacities(indexer as Indexer) indexer.save() @@ -98,6 +102,7 @@ export function handleProvisionCreated(event: ProvisionCreated): void { provision.maxVerifierCutPending = event.params.maxVerifierCut provision.thawingPeriod = event.params.thawingPeriod provision.thawingPeriodPending = event.params.thawingPeriod + provision = updateAdvancedProvisionMetrics(provision as Provision) provision.save() } @@ -108,6 +113,7 @@ export function handleProvisionIncreased(event: ProvisionIncreased): void { let provision = createOrLoadProvision(event.params.serviceProvider, event.params.verifier, event.block.timestamp) indexer.provisionedTokens = indexer.provisionedTokens.plus(event.params.tokens) + indexer = updateAdvancedIndexerMetrics(indexer as Indexer) indexer = calculateCapacities(indexer as Indexer) indexer.save() @@ -118,6 +124,7 @@ export function handleProvisionIncreased(event: ProvisionIncreased): void { graphNetwork.save() provision.tokensProvisioned = provision.tokensProvisioned.plus(event.params.tokens) + provision = updateAdvancedProvisionMetrics(provision as Provision) provision.save() } @@ -128,6 +135,7 @@ export function handleProvisionThawed(event: ProvisionThawed): void { let provision = createOrLoadProvision(event.params.serviceProvider, event.params.verifier, event.block.timestamp) indexer.thawingTokens = indexer.thawingTokens.plus(event.params.tokens) + indexer = updateAdvancedIndexerMetrics(indexer as Indexer) indexer = calculateCapacities(indexer as Indexer) indexer.save() @@ -138,6 +146,7 @@ export function handleProvisionThawed(event: ProvisionThawed): void { graphNetwork.save() provision.tokensThawing = provision.tokensThawing.plus(event.params.tokens) + provision = updateAdvancedProvisionMetrics(provision as Provision) provision.save() } @@ -149,6 +158,7 @@ export function handleTokensDeprovisioned(event: TokensDeprovisioned): void { indexer.provisionedTokens = indexer.provisionedTokens.minus(event.params.tokens) indexer.thawingTokens = indexer.thawingTokens.minus(event.params.tokens) + indexer = updateAdvancedIndexerMetrics(indexer as Indexer) indexer = calculateCapacities(indexer as Indexer) indexer.save() @@ -162,6 +172,7 @@ export function handleTokensDeprovisioned(event: TokensDeprovisioned): void { provision.tokensProvisioned = provision.tokensProvisioned.minus(event.params.tokens) provision.tokensThawing = provision.tokensThawing.minus(event.params.tokens) + provision = updateAdvancedProvisionMetrics(provision as Provision) provision.save() } @@ -250,6 +261,7 @@ export function handleProvisionSlashed(event: ProvisionSlashed): void { // Due to thawing tokens potentially getting cancelled, we will need to figure the thawing situation indexer.provisionedTokens = indexer.provisionedTokens.minus(event.params.tokens) indexer.stakedTokens = indexer.stakedTokens.minus(event.params.tokens) + indexer = updateAdvancedIndexerMetrics(indexer as Indexer) indexer = calculateCapacities(indexer as Indexer) indexer.save() @@ -263,6 +275,7 @@ export function handleProvisionSlashed(event: ProvisionSlashed): void { provision.tokensProvisioned = provision.tokensProvisioned.minus(event.params.tokens) // To DO, update thawing tokens according to the accounting calculation from the contract provision.tokensSlashedServiceProvider = provision.tokensSlashedServiceProvider.plus(event.params.tokens) + provision = updateAdvancedProvisionMetrics(provision as Provision) provision.save() } @@ -464,6 +477,7 @@ export function handleDelegationSlashed(event: DelegationSlashed): void { let indexerID = event.params.serviceProvider.toHexString() let indexer = Indexer.load(indexerID)! indexer.delegatedTokens = indexer.delegatedTokens.minus(event.params.tokens) + indexer = updateAdvancedIndexerMetrics(indexer as Indexer) indexer = calculateCapacities(indexer as Indexer) indexer.save() @@ -553,6 +567,7 @@ export function handleDelegatedTokensWithdrawn(event: DelegatedTokensWithdrawn): // might want to track locked/thawing tokens in provision too provision.delegatedTokens = provision.delegatedTokens.minus(event.params.tokens) provision.delegatedThawingTokens = provision.delegatedThawingTokens.minus(event.params.tokens) + provision = updateAdvancedProvisionMetrics(provision as Provision) provision.save() let indexerID = event.params.serviceProvider.toHexString() @@ -560,6 +575,7 @@ export function handleDelegatedTokensWithdrawn(event: DelegatedTokensWithdrawn): indexer.delegatedTokens = indexer.delegatedTokens.minus(event.params.tokens) indexer.delegatedThawingTokens = indexer.delegatedThawingTokens.minus(event.params.tokens) + indexer = updateAdvancedIndexerMetrics(indexer as Indexer) indexer = calculateCapacities(indexer as Indexer) indexer.save() diff --git a/tests/factories/horizonStaking.ts b/tests/factories/horizonStaking.ts new file mode 100644 index 00000000..bb4abf2f --- /dev/null +++ b/tests/factories/horizonStaking.ts @@ -0,0 +1,194 @@ +import { newMockEvent } from 'matchstick-as/assembly/index' +import { + DelegatedTokensWithdrawn, + DelegationFeeCutSet, + ProvisionCreated, + ProvisionThawed, + TokensDelegated, + TokensDeprovisioned, + TokensUndelegated, +} from '../../src/types/HorizonStaking/HorizonStaking' +import { Address, BigInt, ethereum } from '@graphprotocol/graph-ts' + +function addressParam(name: string, value: Address): ethereum.EventParam { + return new ethereum.EventParam(name, ethereum.Value.fromAddress(value)) +} + +function uintParam(name: string, value: BigInt): ethereum.EventParam { + return new ethereum.EventParam(name, ethereum.Value.fromUnsignedBigInt(value)) +} + +export function mockProvisionCreated( + serviceProvider: Address, + verifier: Address, + tokens: BigInt, +): ProvisionCreated { + let mockEvent = newMockEvent() + let event = new ProvisionCreated( + mockEvent.address, + mockEvent.logIndex, + mockEvent.transactionLogIndex, + mockEvent.logType, + mockEvent.block, + mockEvent.transaction, + mockEvent.parameters, + mockEvent.receipt, + ) + event.parameters = [] + event.parameters.push(addressParam('serviceProvider', serviceProvider)) + event.parameters.push(addressParam('verifier', verifier)) + event.parameters.push(uintParam('tokens', tokens)) + event.parameters.push(uintParam('maxVerifierCut', BigInt.fromI32(0))) + event.parameters.push(uintParam('thawingPeriod', BigInt.fromI32(0))) + return event +} + +export function mockProvisionThawed( + serviceProvider: Address, + verifier: Address, + tokens: BigInt, +): ProvisionThawed { + let mockEvent = newMockEvent() + let event = new ProvisionThawed( + mockEvent.address, + mockEvent.logIndex, + mockEvent.transactionLogIndex, + mockEvent.logType, + mockEvent.block, + mockEvent.transaction, + mockEvent.parameters, + mockEvent.receipt, + ) + event.parameters = [] + event.parameters.push(addressParam('serviceProvider', serviceProvider)) + event.parameters.push(addressParam('verifier', verifier)) + event.parameters.push(uintParam('tokens', tokens)) + return event +} + +export function mockTokensDeprovisioned( + serviceProvider: Address, + verifier: Address, + tokens: BigInt, +): TokensDeprovisioned { + let mockEvent = newMockEvent() + let event = new TokensDeprovisioned( + mockEvent.address, + mockEvent.logIndex, + mockEvent.transactionLogIndex, + mockEvent.logType, + mockEvent.block, + mockEvent.transaction, + mockEvent.parameters, + mockEvent.receipt, + ) + event.parameters = [] + event.parameters.push(addressParam('serviceProvider', serviceProvider)) + event.parameters.push(addressParam('verifier', verifier)) + event.parameters.push(uintParam('tokens', tokens)) + return event +} + +export function mockDelegationFeeCutSet( + serviceProvider: Address, + verifier: Address, + paymentType: i32, + feeCut: BigInt, +): DelegationFeeCutSet { + let mockEvent = newMockEvent() + let event = new DelegationFeeCutSet( + mockEvent.address, + mockEvent.logIndex, + mockEvent.transactionLogIndex, + mockEvent.logType, + mockEvent.block, + mockEvent.transaction, + mockEvent.parameters, + mockEvent.receipt, + ) + event.parameters = [] + event.parameters.push(addressParam('serviceProvider', serviceProvider)) + event.parameters.push(addressParam('verifier', verifier)) + event.parameters.push(new ethereum.EventParam('paymentType', ethereum.Value.fromI32(paymentType))) + event.parameters.push(uintParam('feeCut', feeCut)) + return event +} + +export function mockTokensDelegated( + serviceProvider: Address, + verifier: Address, + delegator: Address, + tokens: BigInt, + shares: BigInt, +): TokensDelegated { + let mockEvent = newMockEvent() + let event = new TokensDelegated( + mockEvent.address, + mockEvent.logIndex, + mockEvent.transactionLogIndex, + mockEvent.logType, + mockEvent.block, + mockEvent.transaction, + mockEvent.parameters, + mockEvent.receipt, + ) + event.parameters = [] + event.parameters.push(addressParam('serviceProvider', serviceProvider)) + event.parameters.push(addressParam('verifier', verifier)) + event.parameters.push(addressParam('delegator', delegator)) + event.parameters.push(uintParam('tokens', tokens)) + event.parameters.push(uintParam('shares', shares)) + return event +} + +export function mockTokensUndelegated( + serviceProvider: Address, + verifier: Address, + delegator: Address, + tokens: BigInt, + shares: BigInt, +): TokensUndelegated { + let mockEvent = newMockEvent() + let event = new TokensUndelegated( + mockEvent.address, + mockEvent.logIndex, + mockEvent.transactionLogIndex, + mockEvent.logType, + mockEvent.block, + mockEvent.transaction, + mockEvent.parameters, + mockEvent.receipt, + ) + event.parameters = [] + event.parameters.push(addressParam('serviceProvider', serviceProvider)) + event.parameters.push(addressParam('verifier', verifier)) + event.parameters.push(addressParam('delegator', delegator)) + event.parameters.push(uintParam('tokens', tokens)) + event.parameters.push(uintParam('shares', shares)) + return event +} + +export function mockDelegatedTokensWithdrawn( + serviceProvider: Address, + verifier: Address, + delegator: Address, + tokens: BigInt, +): DelegatedTokensWithdrawn { + let mockEvent = newMockEvent() + let event = new DelegatedTokensWithdrawn( + mockEvent.address, + mockEvent.logIndex, + mockEvent.transactionLogIndex, + mockEvent.logType, + mockEvent.block, + mockEvent.transaction, + mockEvent.parameters, + mockEvent.receipt, + ) + event.parameters = [] + event.parameters.push(addressParam('serviceProvider', serviceProvider)) + event.parameters.push(addressParam('verifier', verifier)) + event.parameters.push(addressParam('delegator', delegator)) + event.parameters.push(uintParam('tokens', tokens)) + return event +} diff --git a/tests/horizonStaking.test.ts b/tests/horizonStaking.test.ts new file mode 100644 index 00000000..d554ae06 --- /dev/null +++ b/tests/horizonStaking.test.ts @@ -0,0 +1,184 @@ +import { Address, BigDecimal, BigInt, Bytes, ethereum } from '@graphprotocol/graph-ts' + +import { + assert, + clearStore, + test, + beforeAll, + describe, + afterAll, + createMockedFunction, +} from 'matchstick-as/assembly/index' + +import { + handleDelegatedTokensWithdrawn, + handleDelegationFeeCutSet, + handleProvisionCreated, + handleProvisionThawed, + handleTokensDelegated, + handleTokensDeprovisioned, + handleTokensUndelegated, +} from '../src/mappings/horizonStaking' +import { + mockDelegatedTokensWithdrawn, + mockDelegationFeeCutSet, + mockProvisionCreated, + mockProvisionThawed, + mockTokensDelegated, + mockTokensDeprovisioned, + mockTokensUndelegated, +} from './factories/horizonStaking' +import { createOrLoadGraphNetwork, createOrLoadIndexer, joinID } from '../src/mappings/helpers/helpers' +import { Indexer, Provision } from '../src/types/schema' + +// Reproduces indexer 0x38f4...27c0 on the subgraph service at Arbitrum One block 522,643,996: +// tokensProvisioned 11,278,919 GRT, tokensThawing 0, delegatedTokens 1,439,299 GRT of which +// 36,201 GRT are thawing, indexing reward and query fee cuts 900000 (delegators get 10%). +// Before the fix the provision reported an effective cut of ~0.11636; on active delegation it is ~0.09614. + +const graphID = '0x0000000000000000000000000000000000000000' +const graphAddress = Address.fromString(graphID) +const controllerAddress = Address.fromString('0x0000000000000000000000000000000000000001') +const indexerID = '0x38f412c8d6346a17a53ff9ceecd2e01acecd27c0' +const indexerAddress = Address.fromString(indexerID) +const dataServiceID = '0xb2bb92d0de618878e438b55d5846cfecd9301105' +const dataServiceAddress = Address.fromString(dataServiceID) +const delegatorAddress = Address.fromString('0x0000000000000000000000000000000000000006') +const provisionID = joinID([indexerID, dataServiceID]) + +const GRT = BigInt.fromString('1000000000000000000') +const provisioned = BigInt.fromI32(11278919).times(GRT) +const delegated = BigInt.fromI32(1439299).times(GRT) +const undelegated = BigInt.fromI32(36201).times(GRT) +const provisionThaw = BigInt.fromI32(1000000).times(GRT) +// HorizonStaking feeCut is what delegators receive; the subgraph stores 1e6 - feeCut. +const delegatorFeeCut = BigInt.fromI32(100000) + +createMockedFunction(controllerAddress, 'getGovernor', 'getGovernor():(address)') + .withArgs([]) + .returns([ethereum.Value.fromAddress(controllerAddress)]) +createMockedFunction(graphAddress, 'blockNum', 'blockNum():(uint256)') + .withArgs([]) + .returns([ethereum.Value.fromI32(5)]) + +function provisionField(name: string): BigDecimal { + let provision = Provision.load(provisionID)! + return provision.get(name)!.toBigDecimal() +} + +function indexerField(name: string): BigDecimal { + let indexer = Indexer.load(indexerID)! + return indexer.get(name)!.toBigDecimal() +} + +function assertClose(actual: BigDecimal, expected: string, label: string): void { + let diff = actual.minus(BigDecimal.fromString(expected)) + let tolerance = BigDecimal.fromString('0.00001') + assert.assertTrue( + diff < tolerance && diff > tolerance.neg(), + label + ': expected ~' + expected + ', got ' + actual.toString(), + ) +} + +function setUpReproduction(): void { + let graphNetwork = createOrLoadGraphNetwork(BigInt.fromI32(1), controllerAddress) + graphNetwork.delegationRatio = 16 + graphNetwork.save() + + // Stake is deposited before the provision is created; set it directly to avoid the epoch plumbing. + let indexer = createOrLoadIndexer(Bytes.fromHexString(indexerID), BigInt.fromI32(1), graphNetwork) + indexer.stakedTokens = provisioned + indexer.save() + + handleProvisionCreated(mockProvisionCreated(indexerAddress, dataServiceAddress, provisioned)) + // paymentType 0 = query fees, 2 = indexing rewards + handleDelegationFeeCutSet(mockDelegationFeeCutSet(indexerAddress, dataServiceAddress, 0, delegatorFeeCut)) + handleDelegationFeeCutSet(mockDelegationFeeCutSet(indexerAddress, dataServiceAddress, 2, delegatorFeeCut)) + handleTokensDelegated( + mockTokensDelegated(indexerAddress, dataServiceAddress, delegatorAddress, delegated, delegated), + ) + handleTokensUndelegated( + mockTokensUndelegated(indexerAddress, dataServiceAddress, delegatorAddress, undelegated, undelegated), + ) +} + +describe('EFFECTIVE CUT ON ACTIVE STAKE', () => { + beforeAll(() => { + setUpReproduction() + }) + + afterAll(() => { + clearStore() + }) + + test('reproduction state matches the on-chain provision', () => { + assert.fieldEquals('Provision', provisionID, 'tokensProvisioned', provisioned.toString()) + assert.fieldEquals('Provision', provisionID, 'tokensThawing', '0') + assert.fieldEquals('Provision', provisionID, 'delegatedTokens', delegated.toString()) + assert.fieldEquals('Provision', provisionID, 'delegatedThawingTokens', undelegated.toString()) + assert.fieldEquals('Provision', provisionID, 'indexingRewardsCut', '900000') + assert.fieldEquals('Provision', provisionID, 'queryFeeCut', '900000') + }) + + test('provision effective cuts exclude thawing delegation', () => { + // D_active = 1,403,098; P_active = 11,278,919; 1 - 0.1 / (1,403,098 / 12,682,017) + assertClose(provisionField('delegatedStakeRatio'), '0.110637', 'provision.delegatedStakeRatio') + assertClose(provisionField('ownStakeRatio'), '0.889363', 'provision.ownStakeRatio') + assertClose(provisionField('indexingRewardEffectiveCut'), '0.096142', 'provision.indexingRewardEffectiveCut') + assertClose(provisionField('queryFeeEffectiveCut'), '0.096142', 'provision.queryFeeEffectiveCut') + }) + + test('indexer effective cuts exclude thawing delegation', () => { + assertClose(indexerField('delegatedStakeRatio'), '0.110637', 'indexer.delegatedStakeRatio') + assertClose(indexerField('indexingRewardEffectiveCut'), '0.096142', 'indexer.indexingRewardEffectiveCut') + assertClose(indexerField('queryFeeEffectiveCut'), '0.096142', 'indexer.queryFeeEffectiveCut') + }) + + test('withdrawing thawed delegation leaves the effective cut unchanged', () => { + handleDelegatedTokensWithdrawn( + mockDelegatedTokensWithdrawn(indexerAddress, dataServiceAddress, delegatorAddress, undelegated), + ) + assert.fieldEquals('Provision', provisionID, 'delegatedThawingTokens', '0') + assertClose(provisionField('indexingRewardEffectiveCut'), '0.096142', 'provision.indexingRewardEffectiveCut') + assertClose(indexerField('indexingRewardEffectiveCut'), '0.096142', 'indexer.indexingRewardEffectiveCut') + }) + + test('thawing provisioned stake is excluded and recalculated', () => { + handleProvisionThawed(mockProvisionThawed(indexerAddress, dataServiceAddress, provisionThaw)) + // P_active = 10,278,919; 1 - 0.1 / (1,403,098 / 11,682,017) + assertClose(provisionField('delegatedStakeRatio'), '0.120108', 'provision.delegatedStakeRatio') + assertClose(provisionField('indexingRewardEffectiveCut'), '0.167413', 'provision.indexingRewardEffectiveCut') + assertClose(provisionField('queryFeeEffectiveCut'), '0.167413', 'provision.queryFeeEffectiveCut') + assertClose(indexerField('indexingRewardEffectiveCut'), '0.167413', 'indexer.indexingRewardEffectiveCut') + }) + + test('deprovisioning thawed stake leaves the effective cut unchanged', () => { + handleTokensDeprovisioned(mockTokensDeprovisioned(indexerAddress, dataServiceAddress, provisionThaw)) + assert.fieldEquals('Provision', provisionID, 'tokensThawing', '0') + assertClose(provisionField('indexingRewardEffectiveCut'), '0.167413', 'provision.indexingRewardEffectiveCut') + }) +}) + +describe('EFFECTIVE CUT WHEN ALL DELEGATION IS THAWING', () => { + beforeAll(() => { + setUpReproduction() + let remaining = delegated.minus(undelegated) + handleTokensUndelegated( + mockTokensUndelegated(indexerAddress, dataServiceAddress, delegatorAddress, remaining, remaining), + ) + }) + + afterAll(() => { + clearStore() + }) + + test('falls back to the zero-delegation values', () => { + assert.fieldEquals('Provision', provisionID, 'delegatedThawingTokens', delegated.toString()) + assert.fieldEquals('Provision', provisionID, 'delegatedStakeRatio', '0') + assert.fieldEquals('Provision', provisionID, 'ownStakeRatio', '1') + assert.fieldEquals('Provision', provisionID, 'indexingRewardEffectiveCut', '0') + assert.fieldEquals('Provision', provisionID, 'queryFeeEffectiveCut', '0') + assert.fieldEquals('Indexer', indexerID, 'delegatedStakeRatio', '0') + assert.fieldEquals('Indexer', indexerID, 'indexingRewardEffectiveCut', '0') + }) +})