Context
Follow-up to #208 (data corruption bugs). During the same audit, several logic issues were identified in the mapping handlers. These are incorrect behaviors that don't cause data loss but produce wrong state in the indexed database.
Each item has been verified against both the indexer code and the tfchain pallet source.
L1: Contract state defaults to OutOfFunds instead of handling GracePeriod
File: contracts.ts — updateNodeContract() / updateNameContract()
The switch on ctr.state.__kind only handles Created and Deleted. The default is OutOfFunds, so a GracePeriod state would be misindexed as OutOfFunds.
Mitigated in practice: Grace period transitions use separate dedicated events (ContractGracePeriodStarted/Ended) which are handled correctly. The chain's _update_node_contract also rejects updates during grace period (CannotUpdateContractInGraceState). Still, the default should be correct.
Impact: LOW
L2: nodeContractCanceled only releases one public IP per contract
File: contracts.ts — nodeContractCanceled()
Uses ctx.store.get(PublicIp, { where: { contractId } }) which returns one record. If a contract has multiple public IPs, only one gets its contractId reset to 0. The rest remain orphaned.
Compare with contractCreated which correctly uses ctx.store.find() (returns all matching records) to assign IPs.
Chain confirmation: NodeContract.public_ips_list is a BoundedVec<PublicIP> — contracts can have multiple IPs.
Impact: HIGH — orphaned IPs appear reserved in GraphQL but are actually free on-chain.
L3: contractUpdated doesn't handle RentContract or update solutionProviderID
File: contracts.ts — contractUpdated()
The handler only looks for NodeContract and NameContract. Three gaps:
- RentContract updates dropped:
attach_solution_provider_id extrinsic (solution_provider.rs:89) emits ContractUpdated for any contract type, including RentContracts. The indexer silently ignores these.
- NodeContract
solutionProviderID not updated: updateNodeContract() reads deployment hash/data/state but ignores solution_provider_id.
- NameContract
solutionProviderID not updated: Same gap. Note: NameContracts can only get a solution provider via attach_solution_provider_id (creation hardcodes None).
Context: Solution provider payment distribution was removed from chain billing in Aug 2024 (commit 541e5b6). The feature is structurally intact (providers can be created, approved, attached) but functionally dormant — no payments are distributed. NameContracts never made business sense for solution providers (pure DNS registration with no value-add).
Impact: LOW (feature is non-functional on payment side), but the indexed data is incorrect.
L4: Unsafe type cast of raw event args for dedicatedFarm
File: farms.ts — farmUpdated()
let farm = item.event.args as Farm
if (farm.dedicatedFarm) {
savedFarm.dedicatedFarm = farm.dedicatedFarm
}
Casts raw Substrate event args directly to the TypeORM Farm model. The event args structure may not match the model shape (different field names, types). This works by coincidence if the field name aligns, but is fragile and type-unsafe.
Impact: MEDIUM — could silently fail or read wrong data if event structure changes.
Related: #208 (bug fixes), PR #209
Context
Follow-up to #208 (data corruption bugs). During the same audit, several logic issues were identified in the mapping handlers. These are incorrect behaviors that don't cause data loss but produce wrong state in the indexed database.
Each item has been verified against both the indexer code and the tfchain pallet source.
L1: Contract state defaults to
OutOfFundsinstead of handlingGracePeriodFile:
contracts.ts—updateNodeContract()/updateNameContract()The switch on
ctr.state.__kindonly handlesCreatedandDeleted. The default isOutOfFunds, so aGracePeriodstate would be misindexed asOutOfFunds.Mitigated in practice: Grace period transitions use separate dedicated events (
ContractGracePeriodStarted/Ended) which are handled correctly. The chain's_update_node_contractalso rejects updates during grace period (CannotUpdateContractInGraceState). Still, the default should be correct.Impact: LOW
L2:
nodeContractCanceledonly releases one public IP per contractFile:
contracts.ts—nodeContractCanceled()Uses
ctx.store.get(PublicIp, { where: { contractId } })which returns one record. If a contract has multiple public IPs, only one gets itscontractIdreset to 0. The rest remain orphaned.Compare with
contractCreatedwhich correctly usesctx.store.find()(returns all matching records) to assign IPs.Chain confirmation:
NodeContract.public_ips_listis aBoundedVec<PublicIP>— contracts can have multiple IPs.Impact: HIGH — orphaned IPs appear reserved in GraphQL but are actually free on-chain.
L3:
contractUpdateddoesn't handle RentContract or updatesolutionProviderIDFile:
contracts.ts—contractUpdated()The handler only looks for
NodeContractandNameContract. Three gaps:attach_solution_provider_idextrinsic (solution_provider.rs:89) emitsContractUpdatedfor any contract type, including RentContracts. The indexer silently ignores these.solutionProviderIDnot updated:updateNodeContract()reads deployment hash/data/state but ignoressolution_provider_id.solutionProviderIDnot updated: Same gap. Note: NameContracts can only get a solution provider viaattach_solution_provider_id(creation hardcodesNone).Context: Solution provider payment distribution was removed from chain billing in Aug 2024 (commit
541e5b6). The feature is structurally intact (providers can be created, approved, attached) but functionally dormant — no payments are distributed. NameContracts never made business sense for solution providers (pure DNS registration with no value-add).Impact: LOW (feature is non-functional on payment side), but the indexed data is incorrect.
L4: Unsafe type cast of raw event args for
dedicatedFarmFile:
farms.ts—farmUpdated()Casts raw Substrate event args directly to the TypeORM
Farmmodel. The event args structure may not match the model shape (different field names, types). This works by coincidence if the field name aligns, but is fragile and type-unsafe.Impact: MEDIUM — could silently fail or read wrong data if event structure changes.
Related: #208 (bug fixes), PR #209