Fix: Indexer factory pair provenance on discover_new_pair (GitLab #285 followup) #311

Closed
opened 2026-06-05 04:08:28 +00:00 by PlasticDigits · 16 comments
PlasticDigits commented 2026-06-05 04:08:28 +00:00 (Migrated from gitlab.com)

Current codebase

Indexer pair discovery:

  • Scheduled sync: pair_discovery::sync_all_pairs queries configured FACTORY_ADDRESS paginated pairs list.
  • Opportunistic sync: discover_new_pair queries any address responding to {"pair": {}} LCD query — used when parser sees swap/fill events for unknown addresses (parser.rs).

discover_new_pair does not verify:

  • Pair is listed in the configured factory
  • On-chain PairInfoState.factory matches FACTORY_ADDRESS
  • Wasm code_id matches factory's pair_code_id

Wasm event scoping (#285 shipped): Parser trusts only runtime _contract_address, not forgeable contract_address attribute — prevents attributing events to victim pairs.

Remaining gap (factory provenance follow-up): Malicious or clone pair contracts mimicking the pair query API can still be indexed if they emit plausible wasm events, polluting charts, route graph, and volume.

Pair on-chain state stores factory: Addr in PairInfoState (smartcontracts/contracts/pair/src/state.rs) but public Pair query returns TerraSwap-compatible struct without factory field.

Follow-up: GitLab #285 (factory provenance).

Why this is needed

Indexers and dApps assume indexed pairs are canonical CL8Y factory deployments. Indexing unverified contracts enables phishing pairs, fake volume, and route solver edges to malicious pools.

Constraints / guardrails

  • Do not break QA seed pairs — all must be factory-created on LocalTerra.
  • discover_new_pair should fail closed or mark unverified when provenance fails.
  • Consider adding factory field to pair query (contract change) or factory Pair reverse lookup.
  • Parser should not call discovery for clearly non-pair emitters.
  • Document operator policy: FACTORY_ADDRESS required in prod (config.rs already errors if empty in prod).

Relevant files

Area Path
Discovery indexer/src/indexer/pair_discovery.rs
Parser hooks indexer/src/indexer/parser.rs
Config indexer/src/config.rs
DB indexer/src/db/queries/pairs.rs
Pair contract smartcontracts/contracts/pair/src/state.rs, contract.rs
Factory smartcontracts/contracts/factory/src/contract.rs
Tests indexer/src/indexer/parser.rs forged-emitter tests

Option A (indexer-only): After pair query, second query factory {"pair": { asset_infos }} and require contract_addr match.

Option B (contract): Extend pair QueryMsg::Config or Pair response with factory addr; indexer checks equality.

Option C (schema): pairs.verified boolean + migration to quarantine existing rows.

Implement A quickly; B for long-term integrator trust.

Acceptance criteria

  • discover_new_pair rejects/non-indexes pairs not in configured factory.
  • sync_all_pairs path unchanged for legitimate pairs.
  • Forged pair contract events do not create tradeable indexed pair (combined with #285 emitter scoping).
  • API/docs state provenance requirement.
  • Tests: attacker contract with pair query but not in factory → not indexed.

Test plan (all paths)

Path Expected
Factory pair event Indexed
Unknown malicious pair Skipped / unverified
Factory sync pagination All canonical pairs
Empty FACTORY_ADDRESS dev Warn + skip provenance (dev only)
Prod empty factory Config error (existing)

Attack / abuse / hack vectors

Vector Mitigation
Clone pair wasm with same API Factory listing check
Forged wasm events (#285) _contract_address scoping + provenance
Fake volume on unverified pair Do not index

Verification criteria

  • Indexer integration test with mock LCD / adversarial pair.
  • Manual: deploy non-factory pair contract → events do not appear in /api/v1/pairs/{addr}/trades.
## Current codebase **Indexer pair discovery:** - **Scheduled sync:** `pair_discovery::sync_all_pairs` queries configured `FACTORY_ADDRESS` paginated `pairs` list. - **Opportunistic sync:** `discover_new_pair` queries **any** address responding to `{"pair": {}}` LCD query — used when parser sees swap/fill events for unknown addresses (`parser.rs`). `discover_new_pair` does **not** verify: - Pair is listed in the configured factory - On-chain `PairInfoState.factory` matches `FACTORY_ADDRESS` - Wasm `code_id` matches factory's `pair_code_id` **Wasm event scoping (#285 shipped):** Parser trusts only runtime `_contract_address`, not forgeable `contract_address` attribute — prevents attributing events to victim pairs. **Remaining gap (factory provenance follow-up):** Malicious or clone pair contracts mimicking the pair query API can still be indexed if they emit plausible wasm events, polluting charts, route graph, and volume. Pair on-chain state stores `factory: Addr` in `PairInfoState` (`smartcontracts/contracts/pair/src/state.rs`) but public `Pair` query returns TerraSwap-compatible struct **without** factory field. Follow-up: GitLab **#285** (factory provenance). ## Why this is needed Indexers and dApps assume indexed pairs are **canonical CL8Y factory deployments**. Indexing unverified contracts enables phishing pairs, fake volume, and route solver edges to malicious pools. ## Constraints / guardrails - Do not break QA seed pairs — all must be factory-created on LocalTerra. - `discover_new_pair` should fail closed or mark `unverified` when provenance fails. - Consider adding factory field to pair query (contract change) **or** factory `Pair` reverse lookup. - Parser should not call discovery for clearly non-pair emitters. - Document operator policy: `FACTORY_ADDRESS` required in prod (`config.rs` already errors if empty in prod). ## Relevant files | Area | Path | |------|------| | Discovery | `indexer/src/indexer/pair_discovery.rs` | | Parser hooks | `indexer/src/indexer/parser.rs` | | Config | `indexer/src/config.rs` | | DB | `indexer/src/db/queries/pairs.rs` | | Pair contract | `smartcontracts/contracts/pair/src/state.rs`, `contract.rs` | | Factory | `smartcontracts/contracts/factory/src/contract.rs` | | Tests | `indexer/src/indexer/parser.rs` forged-emitter tests | ## Recommended direction **Option A (indexer-only):** After `pair` query, second query factory `{"pair": { asset_infos }}` and require `contract_addr` match. **Option B (contract):** Extend pair `QueryMsg::Config` or `Pair` response with `factory` addr; indexer checks equality. **Option C (schema):** `pairs.verified` boolean + migration to quarantine existing rows. Implement A quickly; B for long-term integrator trust. ## Acceptance criteria - [ ] `discover_new_pair` rejects/non-indexes pairs not in configured factory. - [ ] `sync_all_pairs` path unchanged for legitimate pairs. - [ ] Forged pair contract events do not create tradeable indexed pair (combined with #285 emitter scoping). - [ ] API/docs state provenance requirement. - [ ] Tests: attacker contract with pair query but not in factory → not indexed. ## Test plan (all paths) | Path | Expected | |------|----------| | Factory pair event | Indexed | | Unknown malicious pair | Skipped / unverified | | Factory sync pagination | All canonical pairs | | Empty FACTORY_ADDRESS dev | Warn + skip provenance (dev only) | | Prod empty factory | Config error (existing) | ## Attack / abuse / hack vectors | Vector | Mitigation | |--------|------------| | Clone pair wasm with same API | Factory listing check | | Forged wasm events (#285) | `_contract_address` scoping + provenance | | Fake volume on unverified pair | Do not index | ## Verification criteria - Indexer integration test with mock LCD / adversarial pair. - Manual: deploy non-factory pair contract → events do not appear in `/api/v1/pairs/{addr}/trades`.
PlasticDigits commented 2026-06-05 04:08:29 +00:00 (Migrated from gitlab.com)

marked as related to #285

marked as related to #285
ghost1 commented 2026-06-05 10:07:43 +00:00 (Migrated from gitlab.com)

mentioned in commit ce9040cb93

mentioned in commit ce9040cb936d4c4be9080a540e0bada13b3709eb
PlasticDigits commented 2026-06-05 10:08:10 +00:00 (Migrated from gitlab.com)

mentioned in merge request !790

mentioned in merge request !790
PlasticDigits commented 2026-06-05 10:08:12 +00:00 (Migrated from gitlab.com)

Implementation complete in !790.

Summary: discover_new_pair now calls factory Pair { asset_infos } and requires the returned contract_addr to match the emitter before indexing. Empty FACTORY_ADDRESS skips provenance with a warning (dev only). Documented as invariant P1.

Verification: cd indexer && cargo test --lib pair_discovery::tests (4/4 PASS); full lib suite 111/111 PASS.

Issue remains open until MR merge.

Implementation complete in [!790](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/72). **Summary:** `discover_new_pair` now calls factory `Pair { asset_infos }` and requires the returned `contract_addr` to match the emitter before indexing. Empty `FACTORY_ADDRESS` skips provenance with a warning (dev only). Documented as invariant **P1**. **Verification:** `cd indexer && cargo test --lib pair_discovery::tests` (4/4 PASS); full lib suite 111/111 PASS. Issue remains open until MR merge.
PlasticDigits commented 2026-06-05 10:37:45 +00:00 (Migrated from gitlab.com)

mentioned in commit 104e28d603

mentioned in commit 104e28d6035c85c46d3b9fdf545fee7dd3c11c10
PlasticDigits commented 2026-06-05 11:06:05 +00:00 (Migrated from gitlab.com)

Verification complete — GitLab #311

Issue: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/311
Implementation: merged in !790 (ce9040c on main).

Acceptance criteria

Criterion Result How verified
discover_new_pair rejects/non-indexes pairs not in configured factory PASS cd indexer && cargo test --lib pair_discovery::tests → 4/4 PASS (verify_factory_provenance_rejects_unlisted_pair, _fails_when_factory_has_no_pair, _accepts_factory_listed_pair, _skipped_when_factory_empty)
sync_all_pairs path unchanged for legitimate pairs PASS git diff ce9040c^..ce9040c -- indexer/src/indexer/pair_discovery.rs — no changes to sync_all_pairs; factory paginated sync unchanged
Forged pair contract events do not create tradeable indexed pair (#285 + provenance) PASS cargo test --lib forged_contract_address → 3/3 PASS; parser skips discovery failures with warn (parser.rs returns Ok(()) on discover_new_pair err)
API/docs state provenance requirement PASS docs/indexer-invariants.md (P1), docs/integrators.md § Indexer pair list, indexer/.env.example FACTORY_ADDRESS comment
Tests: attacker pair query but not in factory → not indexed PASS Wiremock adversarial tests in pair_discovery.rs (attacker responds to pair query; factory lists canonical addr only → reject)

Test plan (issue body)

Path Result Notes
Factory pair event PASS Covered by verify_factory_provenance_accepts_factory_listed_pair
Unknown malicious pair PASS verify_factory_provenance_rejects_unlisted_pair
Factory sync pagination PASS sync_all_pairs unchanged; no regression in lib suite
Empty FACTORY_ADDRESS dev PASS verify_factory_provenance_skipped_when_factory_empty
Prod empty factory PASS Code review: config.rs RunMode::Prod → ProdEmpty("FACTORY_ADDRESS") when trimmed empty

Verification criteria (supplementary)

Check Result Notes
Indexer integration test with mock LCD / adversarial pair PASS Wiremock unit tests in pair_discovery.rs
Manual: deploy non-factory pair → no trades in /api/v1/pairs/{addr}/trades FAIL Blocked: Docker daemon not running; no LocalTerra deploy (frontend-dapp/.env.local, indexer/.env absent). Logic path covered by unit tests above; recommend on-chain manual check when LocalTerra is available.

Commands run

cd indexer && cargo test --lib pair_discovery::tests   # 4 passed
cd indexer && cargo test --lib                           # 113 passed
cd indexer && cargo test --lib forged_contract_address   # 3 passed
cd indexer && cargo test --lib config::tests::prod       # 4 passed

No repository changes required from this verification pass.

## Verification complete — GitLab #311 **Issue:** https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/311 **Implementation:** merged in [!790](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/72) (`ce9040c` on `main`). ### Acceptance criteria | Criterion | Result | How verified | |-----------|--------|--------------| | `discover_new_pair` rejects/non-indexes pairs not in configured factory | **PASS** | `cd indexer && cargo test --lib pair_discovery::tests` → 4/4 PASS (`verify_factory_provenance_rejects_unlisted_pair`, `_fails_when_factory_has_no_pair`, `_accepts_factory_listed_pair`, `_skipped_when_factory_empty`) | | `sync_all_pairs` path unchanged for legitimate pairs | **PASS** | `git diff ce9040c^..ce9040c -- indexer/src/indexer/pair_discovery.rs` — no changes to `sync_all_pairs`; factory paginated sync unchanged | | Forged pair contract events do not create tradeable indexed pair (#285 + provenance) | **PASS** | `cargo test --lib forged_contract_address` → 3/3 PASS; parser skips discovery failures with warn (`parser.rs` returns `Ok(())` on `discover_new_pair` err) | | API/docs state provenance requirement | **PASS** | `docs/indexer-invariants.md` (P1), `docs/integrators.md` § Indexer pair list, `indexer/.env.example` FACTORY_ADDRESS comment | | Tests: attacker pair query but not in factory → not indexed | **PASS** | Wiremock adversarial tests in `pair_discovery.rs` (attacker responds to `pair` query; factory lists canonical addr only → reject) | ### Test plan (issue body) | Path | Result | Notes | |------|--------|-------| | Factory pair event | **PASS** | Covered by `verify_factory_provenance_accepts_factory_listed_pair` | | Unknown malicious pair | **PASS** | `verify_factory_provenance_rejects_unlisted_pair` | | Factory sync pagination | **PASS** | `sync_all_pairs` unchanged; no regression in lib suite | | Empty `FACTORY_ADDRESS` dev | **PASS** | `verify_factory_provenance_skipped_when_factory_empty` | | Prod empty factory | **PASS** | Code review: `config.rs` `RunMode::Prod` → `ProdEmpty("FACTORY_ADDRESS")` when trimmed empty | ### Verification criteria (supplementary) | Check | Result | Notes | |-------|--------|-------| | Indexer integration test with mock LCD / adversarial pair | **PASS** | Wiremock unit tests in `pair_discovery.rs` | | Manual: deploy non-factory pair → no trades in `/api/v1/pairs/{addr}/trades` | **FAIL** | Blocked: Docker daemon not running; no LocalTerra deploy (`frontend-dapp/.env.local`, `indexer/.env` absent). Logic path covered by unit tests above; recommend on-chain manual check when LocalTerra is available. | ### Commands run ```bash cd indexer && cargo test --lib pair_discovery::tests # 4 passed cd indexer && cargo test --lib # 113 passed cd indexer && cargo test --lib forged_contract_address # 3 passed cd indexer && cargo test --lib config::tests::prod # 4 passed ``` No repository changes required from this verification pass.
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-06-05 11:06:13 +00:00
PlasticDigits commented 2026-06-05 14:10:16 +00:00 (Migrated from gitlab.com)

mentioned in merge request !818

mentioned in merge request !818
Brouie commented 2026-06-09 02:58:22 +00:00 (Migrated from gitlab.com)

mentioned in issue #345

mentioned in issue #345
Brouie commented 2026-06-10 02:31:02 +00:00 (Migrated from gitlab.com)

mentioned in issue #337

mentioned in issue #337
PlasticDigits commented 2026-07-15 02:15:43 +00:00 (Migrated from gitlab.com)

mentioned in commit edcda1fc91

mentioned in commit edcda1fc91c1850f1c943acf6d06bf9225fb27ee
PlasticDigits commented 2026-07-15 02:36:08 +00:00 (Migrated from gitlab.com)

mentioned in commit 69a81432e9

mentioned in commit 69a81432e9aad46b735f817b813148e327194e7f
PlasticDigits commented 2026-08-17 10:29:11 +00:00 (Migrated from gitlab.com)

mentioned in issue #548

mentioned in issue #548
PlasticDigits commented 2026-08-18 00:28:53 +00:00 (Migrated from gitlab.com)

mentioned in issue #556

mentioned in issue #556
PlasticDigits commented 2026-08-18 12:08:59 +00:00 (Migrated from gitlab.com)

mentioned in issue #562

mentioned in issue #562
PlasticDigits commented 2026-08-19 00:57:40 +00:00 (Migrated from gitlab.com)

mentioned in issue #568

mentioned in issue #568
PlasticDigits commented 2026-08-28 05:22:09 +00:00 (Migrated from gitlab.com)

mentioned in issue #692

mentioned in issue #692
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/cl8y-dex-terraclassic#311
No description provided.