Problem
apps/ensindexer/src/plugins/ensv2/plugin.ts defines a single Ponder contract entry RegistrarController that aggregates Legacy / Wrapped / Unwrapped (ENSRoot), EA / RegistrarController / UpgradeableRegistrarController (Basenames), and EthRegistrarController (Lineanames) under one chain: { … } map.
Each chainConfigForContract(...) returns an object keyed by chainId.toString(), e.g. { \"1\": { address, startBlock, endBlock } }. Spreading multiple such objects for the same chain id under one chain: {} overwrites the prior entry — only the last spread wins per chain id.
Effect
| chain |
controllers spread |
what actually gets indexed |
| 1 (mainnet) |
Legacy → Wrapped → Unwrapped |
only Unwrapped |
| 8453 (base) |
EA → RegistrarController → Upgradeable |
only Upgradeable |
| 59144 (linea) |
EthRegistrarController |
EthRegistrarController (single, fine) |
So on mainnet, ENSv2's `RegistrarController:NameRegistered` / `NameRenewed` handlers only fire for events emitted by `UnwrappedEthRegistrarController` (`0x59e16fccd424cc24e280be16e11bcd56fb0ce547`, deployed at block 22764821). All Legacy and Wrapped historical registrations (going back to 2020) are silently missed by the ensv2 plugin's controller logic on mainnet. Same data loss on Basenames for EA + RegistrarController.
Where it manifests
`apps/ensindexer/src/plugins/ensv2/plugin.ts:218–272` — the `RegistrarController` contract entry's `chain` block.
Fix
Pass `address` as an array per chain id (Ponder accepts `Address | Address[]`). Build a single `{ [chainId]: { address: [...], startBlock: min, endBlock: max-or-undefined } }` entry per chain instead of spreading multiple chain-id-keyed objects.
Sketch:
```ts
chain: {
[ensroot.chain.id]: {
address: [
"LegacyEthRegistrarController" in ensroot.contracts && ensroot.contracts.LegacyEthRegistrarController.address,
"WrappedEthRegistrarController" in ensroot.contracts && ensroot.contracts.WrappedEthRegistrarController.address,
ensroot.contracts.UnwrappedEthRegistrarController.address,
].filter((a): a is Address => !!a),
startBlock: Math.min(/* defined startBlocks /),
},
...(basenames && {
[basenames.chain.id]: {
address: [
basenames.contracts.EARegistrarController.address,
basenames.contracts.RegistrarController.address,
basenames.contracts.UpgradeableRegistrarController.address,
],
startBlock: Math.min(/ … */),
},
}),
...(lineanames && {
[lineanames.chain.id]: {
address: lineanames.contracts.EthRegistrarController.address,
startBlock: lineanames.contracts.EthRegistrarController.startBlock,
},
}),
},
```
A small helper (e.g. `mergedChainConfig(chainId, contracts: ContractConfig[])`) would clean up the call sites in any plugin doing this kind of merge.
Adjacent: subgraph and registrars plugins
Both currently keep one Ponder contract entry per controller (`Ethnames_LegacyEthRegistrarController`, `Ethnames_WrappedEthRegistrarController`, `Ethnames_UnwrappedEthRegistrarController`, `Ethnames_UniversalRegistrarRenewalWithReferrer`) — they do not have this override bug. But moving them to the same merged `Ethnames_RegistrarController` shape would:
- eliminate the optional contract-name typesystem problem (the merged entry is always present because at least one controller is)
- match `apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts`, which already dispatches by long-form event signature
- require including `UniversalRegistrarRenewalWithReferrer` ABI in `AnyRegistrarControllerABI` so its `RenewalReferred` event is part of the merged set, OR keeping URRWR as a separate optional entry
Discovery
Surfaced while reviewing the placeholder-elimination PR (followup to #2045). Independent of the placeholder fix; predates it.
Problem
apps/ensindexer/src/plugins/ensv2/plugin.tsdefines a single Ponder contract entryRegistrarControllerthat aggregates Legacy / Wrapped / Unwrapped (ENSRoot), EA / RegistrarController / UpgradeableRegistrarController (Basenames), and EthRegistrarController (Lineanames) under onechain: { … }map.Each
chainConfigForContract(...)returns an object keyed bychainId.toString(), e.g.{ \"1\": { address, startBlock, endBlock } }. Spreading multiple such objects for the same chain id under onechain: {}overwrites the prior entry — only the last spread wins per chain id.Effect
So on mainnet, ENSv2's `RegistrarController:NameRegistered` / `NameRenewed` handlers only fire for events emitted by `UnwrappedEthRegistrarController` (`0x59e16fccd424cc24e280be16e11bcd56fb0ce547`, deployed at block 22764821). All Legacy and Wrapped historical registrations (going back to 2020) are silently missed by the ensv2 plugin's controller logic on mainnet. Same data loss on Basenames for EA + RegistrarController.
Where it manifests
`apps/ensindexer/src/plugins/ensv2/plugin.ts:218–272` — the `RegistrarController` contract entry's `chain` block.
Fix
Pass `address` as an array per chain id (Ponder accepts `Address | Address[]`). Build a single `{ [chainId]: { address: [...], startBlock: min, endBlock: max-or-undefined } }` entry per chain instead of spreading multiple chain-id-keyed objects.
Sketch:
```ts
chain: {
[ensroot.chain.id]: {
address: [
"LegacyEthRegistrarController" in ensroot.contracts && ensroot.contracts.LegacyEthRegistrarController.address,
"WrappedEthRegistrarController" in ensroot.contracts && ensroot.contracts.WrappedEthRegistrarController.address,
ensroot.contracts.UnwrappedEthRegistrarController.address,
].filter((a): a is Address => !!a),
startBlock: Math.min(/* defined startBlocks /),
},
...(basenames && {
[basenames.chain.id]: {
address: [
basenames.contracts.EARegistrarController.address,
basenames.contracts.RegistrarController.address,
basenames.contracts.UpgradeableRegistrarController.address,
],
startBlock: Math.min(/ … */),
},
}),
...(lineanames && {
[lineanames.chain.id]: {
address: lineanames.contracts.EthRegistrarController.address,
startBlock: lineanames.contracts.EthRegistrarController.startBlock,
},
}),
},
```
A small helper (e.g. `mergedChainConfig(chainId, contracts: ContractConfig[])`) would clean up the call sites in any plugin doing this kind of merge.
Adjacent:
subgraphandregistrarspluginsBoth currently keep one Ponder contract entry per controller (`Ethnames_LegacyEthRegistrarController`, `Ethnames_WrappedEthRegistrarController`, `Ethnames_UnwrappedEthRegistrarController`, `Ethnames_UniversalRegistrarRenewalWithReferrer`) — they do not have this override bug. But moving them to the same merged `Ethnames_RegistrarController` shape would:
Discovery
Surfaced while reviewing the placeholder-elimination PR (followup to #2045). Independent of the placeholder fix; predates it.