[Audit M-02] Indexer production config should fail closed on registry/chain/address mismatch #156

Closed
opened 2026-05-05 08:10:21 +00:00 by PlasticDigits · 17 comments
PlasticDigits commented 2026-05-05 08:10:21 +00:00 (Migrated from gitlab.com)

Source

Internal full-stack audit audits/audit_full_1777967937.md (2026-05-05), finding M-02 (Medium). Commit reviewed: 3b3bd0c6c501f9cf908667d304146b7b63302b87.

Problem

For production, a wrong registry file, wrong CHAIN_ID, implementation instead of proxy address, invalid/unparseable address, missing buy router when the stack expects it, deploy_block mis-set, or an empty resolved address list can yield a partial or entirely wrong indexer read model. The frontend and automation consume this API for history, leaderboards, TimeCurve timers, and referral metrics.

Today:

  • Config::from_env warns when ADDRESS_REGISTRY.chain_id differs from CHAIN_ID but continues.
  • AddressRegistry::index_addresses silently skips invalid address strings with a warning.
  • Ingestion idles forever when the resulting address list is empty (“API only”).

That permissive behavior helps local bring-up but is unsafe for production deployments.

Relevant code

Chain mismatch warns only:

        if let Some(ref reg) = address_registry {
            if reg.chain_id != chain_id {
                tracing::warn!(
                    registry_chain = reg.chain_id,
                    config_chain = chain_id,
                    "chain id mismatch between ADDRESS_REGISTRY and CHAIN_ID"
                );
            }
        }

Invalid registry entries are skipped (not fatal):

impl AddressRegistry {
    /// Non-empty configured contract addresses for `eth_getLogs` filtering.
    pub fn index_addresses(&self) -> Vec<Address> {
        let mut v = Vec::new();
        for s in [
            self.contracts.timecurve.trim(),
            // ...
        ] {
            if s.is_empty() {
                continue;
            }
            if let Ok(a) = s.parse::<Address>() {
                v.push(a);
            } else {
                tracing::warn!(address = s, "skipping invalid address in registry");
            }
        }
        v
    }
}

Empty address list leads to idle ingestion:

    let addrs = config
        .address_registry
        .as_ref()
        .map(|r| r.index_addresses())
        .unwrap_or_default();

    if addrs.is_empty() {
        tracing::warn!("no contract addresses in ADDRESS_REGISTRY — idle (API only)");
        loop {
            tokio::time::sleep(Duration::from_secs(3600)).await;
        }
    }

(Related: INDEXER_PRODUCTION already fail-closes on placeholder DB URLs.)

When INDEXER_PRODUCTION=1 (or equivalent explicit flag already used by the indexer), fail startup if:

  • Registry chain_id != CHAIN_ID.
  • ADDRESS_REGISTRY_PATH is missing (for production ingestion expectations — clarify vs API-only mode).
  • Required deployed protocol addresses are missing, invalid, zero, or not parseable (per policy: which contracts are mandatory vs optional).
  • Resolved index address set is empty.
  • deploy_block is zero for a non-local chain (if that remains the operational rule).
  • Optional-but-shipping surfaces (e.g. TimeCurveBuyRouter when frontend env expects Kumbaya ingestion) are absent from the registry.

Keep warn-and-idle (or current behavior) for non-production / development.

Add unit tests for production registry validation and document an operator checklist comparing proxy addresses with deployment artifacts.

Acceptance criteria

  • With production mode enabled, misconfigured registry or chain mismatch prevents the indexer process from starting (clear error message to stderr/logs).
  • Development/local behavior remains permissive unless explicitly overridden.
  • Tests cover the production validation matrix (at minimum: chain mismatch, empty addresses after parse, invalid address string).
  • Operator/runbook note lists required registry fields for production.

Verification checklist

  • INDEXER_PRODUCTION=1 + mismatched registry chain_id → process exits non-zero at startup.
  • INDEXER_PRODUCTION=1 + registry with invalid proxy string → fails closed per policy (not silent skip).
  • INDEXER_PRODUCTION=0 (or dev) → existing warn/skip behavior unchanged for local workflows.
  • cargo test / clippy clean for indexer changes.
## Source Internal full-stack audit [`audits/audit_full_1777967937.md`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/audits/audit_full_1777967937.md) (2026-05-05), finding **M-02** (Medium). Commit reviewed: `3b3bd0c6c501f9cf908667d304146b7b63302b87`. ## Problem For **production**, a wrong registry file, wrong `CHAIN_ID`, implementation instead of proxy address, invalid/unparseable address, missing buy router when the stack expects it, `deploy_block` mis-set, or an empty resolved address list can yield a **partial or entirely wrong** indexer read model. The frontend and automation consume this API for history, leaderboards, TimeCurve timers, and referral metrics. Today: - `Config::from_env` **warns** when `ADDRESS_REGISTRY.chain_id` differs from `CHAIN_ID` but **continues**. - `AddressRegistry::index_addresses` **silently skips** invalid address strings with a warning. - Ingestion **idles forever** when the resulting address list is empty (“API only”). That permissive behavior helps local bring-up but is unsafe for production deployments. ## Relevant code Chain mismatch warns only: ```145:153:indexer/src/config.rs if let Some(ref reg) = address_registry { if reg.chain_id != chain_id { tracing::warn!( registry_chain = reg.chain_id, config_chain = chain_id, "chain id mismatch between ADDRESS_REGISTRY and CHAIN_ID" ); } } ``` Invalid registry entries are skipped (not fatal): ```74:97:indexer/src/config.rs impl AddressRegistry { /// Non-empty configured contract addresses for `eth_getLogs` filtering. pub fn index_addresses(&self) -> Vec<Address> { let mut v = Vec::new(); for s in [ self.contracts.timecurve.trim(), // ... ] { if s.is_empty() { continue; } if let Ok(a) = s.parse::<Address>() { v.push(a); } else { tracing::warn!(address = s, "skipping invalid address in registry"); } } v } } ``` Empty address list leads to idle ingestion: ```75:85:indexer/src/ingestion.rs let addrs = config .address_registry .as_ref() .map(|r| r.index_addresses()) .unwrap_or_default(); if addrs.is_empty() { tracing::warn!("no contract addresses in ADDRESS_REGISTRY — idle (API only)"); loop { tokio::time::sleep(Duration::from_secs(3600)).await; } } ``` (Related: [`INDEXER_PRODUCTION`](https://gitlab.com/PlasticDigits/yieldomega/-/issues/142) already fail-closes on placeholder DB URLs.) ## Recommended fix When **`INDEXER_PRODUCTION=1`** (or equivalent explicit flag already used by the indexer), **fail startup** if: - Registry `chain_id != CHAIN_ID`. - `ADDRESS_REGISTRY_PATH` is missing (for production ingestion expectations — clarify vs API-only mode). - Required deployed protocol addresses are missing, invalid, zero, or not parseable (per policy: which contracts are mandatory vs optional). - Resolved index address set is empty. - `deploy_block` is zero for a non-local chain (if that remains the operational rule). - Optional-but-shipping surfaces (e.g. `TimeCurveBuyRouter` when frontend env expects Kumbaya ingestion) are absent from the registry. Keep **warn-and-idle** (or current behavior) for non-production / development. Add unit tests for production registry validation and document an operator checklist comparing proxy addresses with deployment artifacts. ## Acceptance criteria - With production mode enabled, misconfigured registry or chain mismatch prevents the indexer process from starting (clear error message to stderr/logs). - Development/local behavior remains permissive unless explicitly overridden. - Tests cover the production validation matrix (at minimum: chain mismatch, empty addresses after parse, invalid address string). - Operator/runbook note lists required registry fields for production. ## Verification checklist - [ ] `INDEXER_PRODUCTION=1` + mismatched registry `chain_id` → process exits non-zero at startup. - [ ] `INDEXER_PRODUCTION=1` + registry with invalid proxy string → fails closed per policy (not silent skip). - [ ] `INDEXER_PRODUCTION=0` (or dev) → existing warn/skip behavior unchanged for local workflows. - [ ] `cargo test` / clippy clean for indexer changes.
PlasticDigits commented 2026-05-05 08:32:51 +00:00 (Migrated from gitlab.com)

mentioned in commit e54ca04827

mentioned in commit e54ca048274257181c502c40f8968c48e34c0db3
PlasticDigits commented 2026-05-05 08:32:51 +00:00 (Migrated from gitlab.com)

mentioned in commit 4fd5041fb4

mentioned in commit 4fd5041fb4ae08e51b2ce2be089831d863dbd7df
PlasticDigits commented 2026-05-05 08:35:15 +00:00 (Migrated from gitlab.com)

Implemented (merged to main)

Fail-closed production indexer config for GitLab #156 / audit M-02:

  • ensure_production_address_registry / validate_address_registry_for_production in indexer/src/config.rs: when INDEXER_PRODUCTION is truthy, CHAIN_ID must match registry chain_id (no warn-only in production).
  • With INGESTION_ENABLED (default on), ADDRESS_REGISTRY_PATH is required — no silent API-only idle in production.
  • Mandatory registry keys for ingestion: non-empty, parseable, non-zero TimeCurve, RabbitTreasury, LeprechaunNFT, FeeRouter, ReferralRegistry, PodiumPool; any other non-empty field must parse (no silent skip).
  • deploy_block > 0 for all chains except 31337 (Anvil).
  • Optional TimeCurveBuyRouter: omit unless INDEXER_REGISTRY_REQUIRE_BUY_ROUTER=1 (then required for BuyViaKumbaya / #67).

Docs / invariants: INV-INDEXER-156 in docs/testing/invariants-and-business-logic.md, indexer/README.md operator checklist, docs/indexer/design.md, manual QA §156, qa-local-full-stack.md, skills/README.md + .cursor/skills/yieldomega-guardrails/SKILL.md cross-links.

Tests: cd indexer && cargo test production_registry_validation (10 unit tests) + full cargo test / cargo clippy --all-targets -- -D warnings clean.


/cc @brouie please verify on a staging or dry-run deploy.

Verification checklist

  • INDEXER_PRODUCTION=1 + default ingestion + no ADDRESS_REGISTRY_PATH → process exits at config load with a clear error.
  • INDEXER_PRODUCTION=1 + registry JSON chain_id ≠ CHAIN_ID → non-zero exit (message mentions mismatch / #156).
  • INDEXER_PRODUCTION=1 + non-empty invalid address in registry → non-zero exit (not "skipping invalid address").
  • INDEXER_PRODUCTION=1 + CHAIN_ID=4326 (or any non-31337) + deploy_block: 0 → non-zero exit.
  • INDEXER_PRODUCTION=1 + INDEXER_REGISTRY_REQUIRE_BUY_ROUTER=1 + empty TimeCurveBuyRouter → non-zero exit; with router proxy set → starts.
  • Without INDEXER_PRODUCTION: chain mismatch still warns only; invalid strings still warn-and-skip; behavior unchanged for local stacks.

Issue left open for your sign-off.

## Implemented (merged to `main`) Fail-closed production indexer config for **GitLab #156** / audit **M-02**: - `ensure_production_address_registry` / `validate_address_registry_for_production` in [`indexer/src/config.rs`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/indexer/src/config.rs): when `INDEXER_PRODUCTION` is truthy, **`CHAIN_ID` must match registry `chain_id`** (no warn-only in production). - With **`INGESTION_ENABLED`** (default on), **`ADDRESS_REGISTRY_PATH` is required** — no silent API-only idle in production. - **Mandatory** registry keys for ingestion: non-empty, parseable, **non-zero** `TimeCurve`, `RabbitTreasury`, `LeprechaunNFT`, `FeeRouter`, `ReferralRegistry`, `PodiumPool`; any other **non-empty** field must parse (no silent skip). - **`deploy_block > 0`** for all chains except **`31337`** (Anvil). - Optional **`TimeCurveBuyRouter`**: omit unless `INDEXER_REGISTRY_REQUIRE_BUY_ROUTER=1` (then required for `BuyViaKumbaya` / #67). **Docs / invariants:** `INV-INDEXER-156` in `docs/testing/invariants-and-business-logic.md`, `indexer/README.md` operator checklist, `docs/indexer/design.md`, manual QA §156, `qa-local-full-stack.md`, `skills/README.md` + `.cursor/skills/yieldomega-guardrails/SKILL.md` cross-links. **Tests:** `cd indexer && cargo test production_registry_validation` (10 unit tests) + full `cargo test` / `cargo clippy --all-targets -- -D warnings` clean. --- /cc @brouie please verify on a staging or dry-run deploy. ### Verification checklist - [ ] `INDEXER_PRODUCTION=1` + default ingestion + **no** `ADDRESS_REGISTRY_PATH` → process exits at config load with a clear error. - [ ] `INDEXER_PRODUCTION=1` + registry JSON `chain_id` ≠ `CHAIN_ID` → non-zero exit (message mentions mismatch / #156). - [ ] `INDEXER_PRODUCTION=1` + non-empty invalid address in registry → non-zero exit (**not** "skipping invalid address"). - [ ] `INDEXER_PRODUCTION=1` + `CHAIN_ID=4326` (or any non-31337) + `deploy_block: 0` → non-zero exit. - [ ] `INDEXER_PRODUCTION=1` + `INDEXER_REGISTRY_REQUIRE_BUY_ROUTER=1` + empty `TimeCurveBuyRouter` → non-zero exit; with router proxy set → starts. - [ ] **Without** `INDEXER_PRODUCTION`: chain mismatch still **warns** only; invalid strings still warn-and-skip; behavior unchanged for local stacks. Issue left **open** for your sign-off.
PlasticDigits commented 2026-05-05 09:01:22 +00:00 (Migrated from gitlab.com)

mentioned in commit 955228fd7f

mentioned in commit 955228fd7fdd3748c1360ac9995b4b4532cf3e8c
Brouie commented 2026-05-06 01:17:26 +00:00 (Migrated from gitlab.com)

mentioned in issue #157

mentioned in issue #157
Brouie commented 2026-05-06 01:19:55 +00:00 (Migrated from gitlab.com)

@PlasticDigits — verification PASS on e54ca04.

unit tests: cargo test production_registry_validation → 10/10 PASS.

mapping tests to your checklist:

  • rejects_registry_chain_id_mismatch → checklist item 2 (chain mismatch → non-zero exit)
  • rejects_invalid_address_when_non_empty → item 3 (invalid address string → non-zero, not silent skip)
  • rejects_deploy_block_zero_on_non_local_chain_with_ingestion + allows_deploy_block_zero_on_anvil_chain_with_ingestion → item 4 (deploy_block=0 enforced on non-anvil chains, anvil 31337 exempted)
  • require_buy_router_missing_fails + require_buy_router_present_ok → item 5 (router required when flag set, accepts proxy when present)
  • rejects_empty_mandatory_when_ingestion + rejects_zero_mandatory_when_ingestion + rejects_empty_index_set_when_all_mandatory_unparseable_ingestion → mandatory address coverage (TimeCurve, RabbitTreasury, LeprechaunNFT, FeeRouter, ReferralRegistry, PodiumPool)
  • api_only_skips_mandatory_addresses_but_still_validates_nonempty_fields → API-only mode policy

source review of indexer/src/config.rs:

  • line 116: if !indexer_production_enabled() { return Ok(()); } — non-prod stays permissive (item 6 covered, existing dev warn-and-skip unchanged)
  • line 119-123: if ingestion_enabled && address_registry.is_none() { bail!(...) } — item 1 covered (no ADDRESS_REGISTRY_PATH → exit at config load with clear message referencing #156)
  • line 220: bail!("INDEXER_PRODUCTION: deploy_block is 0 in ADDRESS_REGISTRY for chain_id {chain_id} ...") for non-anvil chains
  • line 228: bail!("...no contract addresses resolved for eth_getLogs...") covers empty resolved set

clippy + full test suite:

  • cargo clippy --all-targets -- -D warnings → clean
  • cargo test → 40 unit + 0 doc + 2 integration = 42/42 PASS at HEAD 390f9be

live process-exit walks (items 1-5 with actual misconfigured registries) not run — would require either running the indexer with custom env in parallel to the live stack (port collision on 3100) or stopping the live stack mid-session (collateral on other concurrent verifications). 10 named unit tests + source review cover the same matrix. happy to walk a live induce explicitly if you want it.

ready for close on your side.

@PlasticDigits — verification PASS on `e54ca04`. unit tests: `cargo test production_registry_validation` → 10/10 PASS. mapping tests to your checklist: - `rejects_registry_chain_id_mismatch` → checklist item 2 (chain mismatch → non-zero exit) - `rejects_invalid_address_when_non_empty` → item 3 (invalid address string → non-zero, not silent skip) - `rejects_deploy_block_zero_on_non_local_chain_with_ingestion` + `allows_deploy_block_zero_on_anvil_chain_with_ingestion` → item 4 (deploy_block=0 enforced on non-anvil chains, anvil 31337 exempted) - `require_buy_router_missing_fails` + `require_buy_router_present_ok` → item 5 (router required when flag set, accepts proxy when present) - `rejects_empty_mandatory_when_ingestion` + `rejects_zero_mandatory_when_ingestion` + `rejects_empty_index_set_when_all_mandatory_unparseable_ingestion` → mandatory address coverage (TimeCurve, RabbitTreasury, LeprechaunNFT, FeeRouter, ReferralRegistry, PodiumPool) - `api_only_skips_mandatory_addresses_but_still_validates_nonempty_fields` → API-only mode policy source review of `indexer/src/config.rs`: - line 116: `if !indexer_production_enabled() { return Ok(()); }` — non-prod stays permissive (item 6 covered, existing dev warn-and-skip unchanged) - line 119-123: `if ingestion_enabled && address_registry.is_none() { bail!(...) }` — item 1 covered (no `ADDRESS_REGISTRY_PATH` → exit at config load with clear message referencing #156) - line 220: `bail!("INDEXER_PRODUCTION: deploy_block is 0 in ADDRESS_REGISTRY for chain_id {chain_id} ...")` for non-anvil chains - line 228: `bail!("...no contract addresses resolved for eth_getLogs...")` covers empty resolved set clippy + full test suite: - `cargo clippy --all-targets -- -D warnings` → clean - `cargo test` → 40 unit + 0 doc + 2 integration = 42/42 PASS at HEAD `390f9be` live process-exit walks (items 1-5 with actual misconfigured registries) not run — would require either running the indexer with custom env in parallel to the live stack (port collision on 3100) or stopping the live stack mid-session (collateral on other concurrent verifications). 10 named unit tests + source review cover the same matrix. happy to walk a live induce explicitly if you want it. ready for close on your side.
Brouie commented 2026-05-06 05:53:21 +00:00 (Migrated from gitlab.com)

mentioned in issue #168

mentioned in issue #168
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-05-06 08:25:47 +00:00
PlasticDigits commented 2026-05-13 15:13:33 +00:00 (Migrated from gitlab.com)

mentioned in commit 278c631bd2

mentioned in commit 278c631bd2dfce47fc0fee7433982f45b218965e
PlasticDigits commented 2026-05-13 15:13:33 +00:00 (Migrated from gitlab.com)

mentioned in commit 858300b261

mentioned in commit 858300b26192328f0597d8c6c9c1f59d7aa8ba06
PlasticDigits commented 2026-05-13 15:13:33 +00:00 (Migrated from gitlab.com)

mentioned in commit f43ac77e85

mentioned in commit f43ac77e85e88f346bf79738385a8ef43ce45c80
PlasticDigits commented 2026-05-13 16:07:18 +00:00 (Migrated from gitlab.com)

mentioned in issue #207

mentioned in issue #207
PlasticDigits commented 2026-05-22 06:51:19 +00:00 (Migrated from gitlab.com)

mentioned in issue #237

mentioned in issue #237
PlasticDigits commented 2026-05-29 08:10:15 +00:00 (Migrated from gitlab.com)

mentioned in issue #267

mentioned in issue #267
PlasticDigits commented 2026-05-29 08:10:24 +00:00 (Migrated from gitlab.com)

mentioned in issue #266

mentioned in issue #266
PlasticDigits commented 2026-06-14 22:08:37 +00:00 (Migrated from gitlab.com)

mentioned in issue #325

mentioned in issue #325
PlasticDigits commented 2026-06-14 22:31:58 +00:00 (Migrated from gitlab.com)

mentioned in issue #326

mentioned in issue #326
PlasticDigits commented 2026-06-21 03:43:53 +00:00 (Migrated from gitlab.com)

mentioned in issue #341

mentioned in issue #341
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
code/yieldomega#156
No description provided.