[#138] Finding 1: Indexer — decode PodiumResidualRecipientSet, EthRescued, Erc20Rescued + persist/reorg #139

Closed
opened 2026-05-04 08:51:52 +00:00 by PlasticDigits · 7 comments
PlasticDigits commented 2026-05-04 08:51:52 +00:00 (Migrated from gitlab.com)

Parent: Split from #138 — Pre-deploy review pass 2 (Finding 1).

Severity: Medium

Problem

Three audit-batch contract events are emitted onchain but never decoded by the indexer. They fall through to DecodedEvent::Unknown and are not persisted, so there is no Postgres audit trail for ops/compliance dashboards.

  1. TimeCurve.PodiumResidualRecipientSet(address indexed recipient) — contracts/src/TimeCurve.sol (event near M-01 residual routing).
  2. TimeCurveBuyRouter.EthRescued(address indexed to, uint256 amount) — contracts/src/TimeCurveBuyRouter.sol.
  3. TimeCurveBuyRouter.Erc20Rescued(address indexed token, address indexed to, uint256 amount) — same file. Note: casing is Erc20Rescued (lowercase c20); this is a different topic0 from FeeRouter.ERC20Rescued — only the FeeRouter variant may already decode.

Without rows, reorg.rs rollback_after cannot purge these (no tables), and owner changes to the residual sink mid-sale or post-end are invisible offchain.

References

Kind Path / link
Contracts contracts/src/TimeCurve.sol (PodiumResidualRecipientSet)
Contracts contracts/src/TimeCurveBuyRouter.sol (EthRescued, Erc20Rescued)
Indexer decode indexer/src/decoder.rs
Persist indexer/src/persist.rs
Reorg rollback list indexer/src/reorg.rs
Design docs/indexer/design.md
Invariant docs/testing/invariants-and-business-logic.md § INV-INDEXER-112 / GitLab #112 — emitted events must decode + idx_* + rollback
Skill .cursor/skills/yieldomega-guardrails/SKILL.md (indexer is derived read-model; event coverage for ops)
Related GitLab #117, M-02 / buy-router rescue
  1. Add sol! event definitions for all three (verify exact ABI names/topics vs Solidity).
  2. Extend decode_rpc_log with matching arms.
  3. Add Alembic-style migrations: three new idx_* tables with block_number, tx_hash, log_index, indexed fields (recipient, to, amount, token as appropriate).
  4. Extend persist_decoded_log with INSERT branches.
  5. Append new table names to rollback_after deletion set in reorg.rs.
  6. Extend Stage 2 integration coverage so postgres_stage2_persist_all_events_and_rollback_after includes these variants (see existing pattern in indexer/tests/integration_stage2.rs — adjust path if repo differs).

Acceptance criteria

  • All three event types decode from canonical logs and persist with correct columns.
  • Deep reorg rolls back rows for these tables (listed in reorg.rs).
  • INV-INDEXER-112 matrix in docs updated if there is an explicit checklist row for new events.
  • CI integration test touches persist + rollback_after for the new variants.

Verification checklist

  • Run cargo test in indexer/ including integration tests with YIELDOMEGA_PG_TEST_URL if applicable.
  • Manual: against Anvil or testnet, trigger setPodiumResidualRecipient / router rescueETH / rescueERC20 and confirm API or raw SQL shows rows.
  • Confirm topic0 hex in decoder matches cast sig-event "..." output for each event.
  • Grep rollback_after for new table names.
  • No duplicate decode with FeeRouter ERC20Rescued — distinct variants.
**Parent:** Split from [#138 — Pre-deploy review pass 2](https://gitlab.com/PlasticDigits/yieldomega/-/issues/138) (Finding 1). **Severity:** Medium ## Problem Three audit-batch contract events are emitted onchain but **never decoded** by the indexer. They fall through to `DecodedEvent::Unknown` and are **not persisted**, so there is no Postgres audit trail for ops/compliance dashboards. 1. **`TimeCurve.PodiumResidualRecipientSet(address indexed recipient)`** — `contracts/src/TimeCurve.sol` (event near M-01 residual routing). 2. **`TimeCurveBuyRouter.EthRescued(address indexed to, uint256 amount)`** — `contracts/src/TimeCurveBuyRouter.sol`. 3. **`TimeCurveBuyRouter.Erc20Rescued(address indexed token, address indexed to, uint256 amount)`** — same file. **Note:** casing is `Erc20Rescued` (lowercase `c20`); this is a **different topic0** from `FeeRouter.ERC20Rescued` — only the FeeRouter variant may already decode. Without rows, `reorg.rs` `rollback_after` cannot purge these (no tables), and **owner changes to the residual sink** mid-sale or post-end are invisible offchain. ## References | Kind | Path / link | |------|-------------| | Contracts | [`contracts/src/TimeCurve.sol`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/contracts/src/TimeCurve.sol) (`PodiumResidualRecipientSet`) | | Contracts | [`contracts/src/TimeCurveBuyRouter.sol`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/contracts/src/TimeCurveBuyRouter.sol) (`EthRescued`, `Erc20Rescued`) | | Indexer decode | [`indexer/src/decoder.rs`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/indexer/src/decoder.rs) | | Persist | [`indexer/src/persist.rs`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/indexer/src/persist.rs) | | Reorg rollback list | [`indexer/src/reorg.rs`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/indexer/src/reorg.rs) | | Design | [`docs/indexer/design.md`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/docs/indexer/design.md) | | Invariant | [`docs/testing/invariants-and-business-logic.md` § INV-INDEXER-112 / GitLab #112](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/docs/testing/invariants-and-business-logic.md) — emitted events must decode + `idx_*` + rollback | | Skill | [`.cursor/skills/yieldomega-guardrails/SKILL.md`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/.cursor/skills/yieldomega-guardrails/SKILL.md) (indexer is derived read-model; event coverage for ops) | | Related | GitLab [#117](https://gitlab.com/PlasticDigits/yieldomega/-/issues/117), [M-02 / buy-router rescue](https://gitlab.com/PlasticDigits/yieldomega/-/issues/117) | ## Recommended fix 1. Add `sol!` event definitions for all three (verify **exact** ABI names/topics vs Solidity). 2. Extend `decode_rpc_log` with matching arms. 3. Add Alembic-style migrations: three new `idx_*` tables with `block_number`, `tx_hash`, `log_index`, indexed fields (`recipient`, `to`, `amount`, `token` as appropriate). 4. Extend `persist_decoded_log` with INSERT branches. 5. Append new table names to `rollback_after` deletion set in `reorg.rs`. 6. Extend **Stage 2** integration coverage so `postgres_stage2_persist_all_events_and_rollback_after` includes these variants (see existing pattern in [`indexer/tests/integration_stage2.rs`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/indexer/tests/integration_stage2.rs) — adjust path if repo differs). ## Acceptance criteria - [ ] All three event types decode from canonical logs and persist with correct columns. - [ ] Deep reorg rolls back rows for these tables (listed in `reorg.rs`). - [ ] `INV-INDEXER-112` matrix in docs updated if there is an explicit checklist row for new events. - [ ] CI integration test touches persist + `rollback_after` for the new variants. ## Verification checklist - [ ] Run `cargo test` in `indexer/` including integration tests with `YIELDOMEGA_PG_TEST_URL` if applicable. - [ ] Manual: against Anvil or testnet, trigger `setPodiumResidualRecipient` / router `rescueETH` / `rescueERC20` and confirm API or raw SQL shows rows. - [ ] Confirm `topic0` hex in decoder matches `cast sig-event "..."` output for each event. - [ ] Grep `rollback_after` for new table names. - [ ] No duplicate decode with FeeRouter `ERC20Rescued` — distinct variants.
PlasticDigits commented 2026-05-04 08:52:27 +00:00 (Migrated from gitlab.com)

mentioned in issue #145

mentioned in issue #145
PlasticDigits commented 2026-05-04 08:53:13 +00:00 (Migrated from gitlab.com)

mentioned in issue #138

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

mentioned in commit 1c92aae5f1

mentioned in commit 1c92aae5f13ad411261c63b3891f82ed01de00e9
PlasticDigits commented 2026-05-04 15:08:08 +00:00 (Migrated from gitlab.com)

mentioned in commit 15fc07617b

mentioned in commit 15fc07617b61dd6bf4529b2b4117039f19e9de6b
PlasticDigits commented 2026-05-04 15:11:35 +00:00 (Migrated from gitlab.com)

Ship summary (merged to main)

Indexer support for GitLab #139 is on main (fast-forward includes merge 838a2c1 and implementation 1c92aae).

What landed

  • Decode + persist (distinct from FeeRouter rescue):
    • TimeCurve.PodiumResidualRecipientSet
    • TimeCurveBuyRouter.EthRescued / Erc20Rescued
  • Migrations: indexer/migrations/20260504211000_gl139_residual_router_rescue_events.{up,down}.sql — dedicated idx_* tables + indexes.
  • Code paths: decoder.rs, persist.rs, reorg.rs; Postgres integration extended in indexer/tests/integration_stage2.rs.
  • Docs / invariants: INV-INDEXER-139 in docs/testing/invariants-and-business-logic.md, docs/indexer/design.md, contributor cross-links in skills/README.md and guardrails where applicable.

How to verify (evaluation criteria)

  1. Schema: Run migrations; confirm new tables exist and match design doc naming.
  2. Decode: Unit tests in decoder.rs — roundtrip_podium_residual_recipient_set, roundtrip_buy_router_eth_rescued, roundtrip_buy_router_erc20_rescued, roundtrip_gl139_audit_events_topic0_matches_cast; ensure topic0 matches cast sig / deployed ABI (not confused with FeeRouter ERC20Rescued).
  3. Persist + reorg: Rows written inside the same per-block transaction as other indexer work (INV-INDEXER-146); reorg.rs clears GL139 tables on configured rollback depth.
  4. Integration: With YIELDOMEGA_PG_TEST_URL, cargo test -p yieldomega_indexer runs integration_stage2 — GL139 events covered alongside existing stage2 matrix and #146 all-or-nothing block test.
  5. Prod check: After deploy, spot-check logs for these events on a chain where TimeCurve / BuyRouter emit them; compare row counts to explorer logs for a known tx range.

If anything fails, capture failing test name + migration version + sample tx_hash / log index for repro.

## Ship summary (merged to `main`) Indexer support for **GitLab #139** is on `main` (fast-forward includes merge `838a2c1` and implementation `1c92aae`). ### What landed - **Decode + persist** (distinct from **FeeRouter** rescue): - `TimeCurve.PodiumResidualRecipientSet` - `TimeCurveBuyRouter.EthRescued` / `Erc20Rescued` - **Migrations**: `indexer/migrations/20260504211000_gl139_residual_router_rescue_events.{up,down}.sql` — dedicated `idx_*` tables + indexes. - **Code paths**: `decoder.rs`, `persist.rs`, `reorg.rs`; **Postgres integration** extended in `indexer/tests/integration_stage2.rs`. - **Docs / invariants**: `INV-INDEXER-139` in `docs/testing/invariants-and-business-logic.md`, `docs/indexer/design.md`, contributor cross-links in `skills/README.md` and guardrails where applicable. ### How to verify (evaluation criteria) 1. **Schema**: Run migrations; confirm new tables exist and match design doc naming. 2. **Decode**: Unit tests in `decoder.rs` — `roundtrip_podium_residual_recipient_set`, `roundtrip_buy_router_eth_rescued`, `roundtrip_buy_router_erc20_rescued`, `roundtrip_gl139_audit_events_topic0_matches_cast`; ensure **topic0** matches `cast sig` / deployed ABI (not confused with FeeRouter `ERC20Rescued`). 3. **Persist + reorg**: Rows written inside the same **per-block transaction** as other indexer work (**`INV-INDEXER-146`**); `reorg.rs` clears GL139 tables on configured rollback depth. 4. **Integration**: With `YIELDOMEGA_PG_TEST_URL`, `cargo test -p yieldomega_indexer` runs `integration_stage2` — GL139 events covered alongside existing stage2 matrix and **#146** all-or-nothing block test. 5. **Prod check**: After deploy, spot-check logs for these events on a chain where **TimeCurve** / **BuyRouter** emit them; compare row counts to explorer logs for a known tx range. If anything fails, capture failing test name + migration version + sample `tx_hash` / log index for repro.
Brouie commented 2026-05-05 02:07:00 +00:00 (Migrated from gitlab.com)

@PlasticDigits — verification PASS on 838a2c1 / 1c92aae. walked your checklist:

  1. schema: migrations 20260504211000_gl139_residual_router_rescue_events.{up,down}.sql — three tables (idx_timecurve_podium_residual_recipient_set, idx_timecurve_buy_router_eth_rescued, idx_timecurve_buy_router_erc20_rescued) with block_number/tx_hash/log_index plus indexed fields per design doc.
  2. decode unit tests: cargo test roundtrip green, 18/18 PASS. #139 tests specifically:
  • roundtrip_podium_residual_recipient_set ✓
  • roundtrip_buy_router_eth_rescued ✓
  • roundtrip_buy_router_erc20_rescued_distinct_from_fee_router ✓ (the casing-collision concern explicitly tested — distinct topic0 from FeeRouter ERC20Rescued)
  • roundtrip_gl139_audit_events_topic0_matches_cast ✓
  1. persist + reorg verified at source: decoder.rs L150 PodiumResidualRecipientSet on TimeCurveEvents, L156-161 EthRescued/Erc20Rescued on TimeCurveBuyRouterEvents (3-arg vs FeeRouter's 4-arg, distinct signatures). decode dispatch at L946-988, variants at L349-358 + L458-461. persist handlers at persist.rs L1083-1148 with ON CONFLICT (tx_hash, log_index) DO NOTHING. reorg.rs L151/153/161 includes all three tables in rollback list — atomic with rest of indexer per #140.
  2. integration: integration_stage2.rs L707-719 + L922-937 covers persistence + rollback alongside the rest of the stage2 matrix.
  3. docs: INV-INDEXER-139 row in invariants-and-business-logic.md, docs/indexer/design.md design doc reads cleanly. cross-links in skills/README.md + guardrails.
    prod check item skipped — no live tx range yet on a deployed chain. logic verified end-to-end via roundtrip + integration tests.
    ready for close on your side.
@PlasticDigits — verification PASS on `838a2c1` / `1c92aae`. walked your checklist: 1. schema: migrations `20260504211000_gl139_residual_router_rescue_events.{up,down}.sql` — three tables (`idx_timecurve_podium_residual_recipient_set`, `idx_timecurve_buy_router_eth_rescued`, `idx_timecurve_buy_router_erc20_rescued`) with block_number/tx_hash/log_index plus indexed fields per design doc. 2. decode unit tests: `cargo test roundtrip` green, 18/18 PASS. \#139 tests specifically: - `roundtrip_podium_residual_recipient_set` ✓ - `roundtrip_buy_router_eth_rescued` ✓ - `roundtrip_buy_router_erc20_rescued_distinct_from_fee_router` ✓ (the casing-collision concern explicitly tested — distinct topic0 from FeeRouter `ERC20Rescued`) - `roundtrip_gl139_audit_events_topic0_matches_cast` ✓ 3. persist + reorg verified at source: decoder.rs L150 `PodiumResidualRecipientSet` on `TimeCurveEvents`, L156-161 `EthRescued`/`Erc20Rescued` on `TimeCurveBuyRouterEvents` (3-arg vs FeeRouter's 4-arg, distinct signatures). decode dispatch at L946-988, variants at L349-358 + L458-461. persist handlers at persist.rs L1083-1148 with `ON CONFLICT (tx_hash, log_index) DO NOTHING`. reorg.rs L151/153/161 includes all three tables in rollback list — atomic with rest of indexer per #140. 4. integration: `integration_stage2.rs` L707-719 + L922-937 covers persistence + rollback alongside the rest of the stage2 matrix. 5. docs: `INV-INDEXER-139` row in `invariants-and-business-logic.md`, `docs/indexer/design.md` design doc reads cleanly. cross-links in `skills/README.md` + guardrails. prod check item skipped — no live tx range yet on a deployed chain. logic verified end-to-end via roundtrip + integration tests. ready for close on your side.
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-05-05 02:35:21 +00:00
Brouie commented 2026-05-05 02:49:22 +00:00 (Migrated from gitlab.com)

mentioned in issue #149

mentioned in issue #149
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#139
No description provided.