Indexer: event-driven fee-tier sync instead of full trader scan (#364) #876

Merged
PlasticDigits merged 2 commits from issue-364-event-driven-tier-sync into main 2026-06-12 07:57:38 +00:00
PlasticDigits commented 2026-06-12 05:26:33 +00:00 (Migrated from gitlab.com)

Summary

Replaces the 10-minute O(traders) LCD get_registration scan with event-driven tier updates during block parsing, plus a daily (configurable) reconcile loop for drift correction (#364).

  • Block parsing: register, register_wallet, deregister, deregister_wallet wasm events on FEE_DISCOUNT_ADDRESS upsert traders.tier_id / tier_name / registered within the indexed block.
  • Lazy hydrate: first swap from a new sender triggers one get_registration LCD query.
  • Reconcile: run_tier_reconcile_loop defaults to 86400s (TIER_SYNC_RECONCILE_INTERVAL, min 60s); logs tier_sync_lag_seconds on completion.
  • Route solver: unchanged tier-keyed cache (#283, #245); integration test confirms discounted quote after event-driven tier update.

Docs: docs/indexer-invariants.md, skills/AGENTS_FEE_DISCOUNT_TIERS.md, indexer/.env.example.

Acceptance checklist

Criterion Verification Result
Registration change reflected in traders within one indexed block cd indexer && cargo test --test indexer_tier_sync registration_event_updates_trader_within_block -- --test-threads=1 PASS
Full-table LCD scan no longer runs every 10 minutes by default Default TIER_SYNC_RECONCILE_INTERVAL = 86400s; run_tier_reconcile_loop replaces 600s run_tier_sync_loop PASS
Route solver quotes match discount tier after registration cd indexer && cargo test --test indexer_tier_sync route_solve_uses_tier_after_registration_event -- --test-threads=1 PASS
Reconciliation job still corrects drift cd indexer && cargo test --test indexer_tier_sync reconcile_corrects_missed_registration_event -- --test-threads=1 PASS
Lib tests cd indexer && cargo test --lib PASS
Integration suite make test-indexer-integration PASS
Manual LocalTerra register → trader row + route tier within 1 block Not run in agent VM (requires full LocalTerra deploy + wallet tx) SKIP

Third-party verification

  1. make setup-indexer-postgres
  2. cd indexer && cargo test --lib
  3. cd indexer && cargo test --test indexer_tier_sync -- --test-threads=1
  4. make test-indexer-integration
  5. (Optional LocalTerra) make setup-cloud-localterra, register tier on fee-discount contract, confirm GET /api/v1/traders/{addr} and GET /api/v1/route/solve?...&sender={addr} reflect tier within one block — no 10-minute wait.

Attack / abuse coverage

  • Malformed registry wasm events: unit tests + warn-and-continue in parser (trader_tracker::parse_fee_discount_registry_events).
  • Burst registration txs per wallet in one tx: last event wins (deduped per wallet per tx).

Note

Medium Risk
Changes how tier data is populated for route quotes and trader API; incorrect parsing could serve wrong discounts until reconcile, but malformed events fail open with skip/warn and block ingest errors still propagate.

Overview
Replaces the default 10-minute full-table LCD get_registration scan with event-driven tier updates during block parsing, so traders.tier_id / registration state stay aligned with on-chain registry txs within the indexed block.

When FEE_DISCOUNT_ADDRESS is set, the block parser applies register / register_wallet / deregister / deregister_wallet wasm events from that contract (scoped via _contract_address, malformed segments skipped, duplicate wallets in one tx → last wins). First swap from a new sender still triggers a one-off LCD hydrate. A background run_tier_reconcile_loop keeps a full-table reconcile for drift only, defaulting to 24h via new TIER_SYNC_RECONCILE_INTERVAL (min 60s). Trader tier writes use upsert_trader_tier; swap upsert returns whether the row was newly inserted.

Docs and tests cover indexer invariants, env example, and integration paths (trader API + route-solve discounted quote after registration).

Reviewed by Cursor Bugbot for commit d044349269. Bugbot is set up for automated code reviews on this repo. Configure here.

## Summary Replaces the 10-minute O(traders) LCD `get_registration` scan with **event-driven** tier updates during block parsing, plus a **daily** (configurable) reconcile loop for drift correction ([#364](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/364)). - **Block parsing:** `register`, `register_wallet`, `deregister`, `deregister_wallet` wasm events on `FEE_DISCOUNT_ADDRESS` upsert `traders.tier_id` / `tier_name` / `registered` within the indexed block. - **Lazy hydrate:** first swap from a new sender triggers one `get_registration` LCD query. - **Reconcile:** `run_tier_reconcile_loop` defaults to **86400s** (`TIER_SYNC_RECONCILE_INTERVAL`, min 60s); logs `tier_sync_lag_seconds` on completion. - **Route solver:** unchanged tier-keyed cache ([#283](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/283), [#245](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/245)); integration test confirms discounted quote after event-driven tier update. Docs: `docs/indexer-invariants.md`, `skills/AGENTS_FEE_DISCOUNT_TIERS.md`, `indexer/.env.example`. ## Acceptance checklist | Criterion | Verification | Result | |-----------|--------------|--------| | Registration change reflected in `traders` within one indexed block | `cd indexer && cargo test --test indexer_tier_sync registration_event_updates_trader_within_block -- --test-threads=1` | PASS | | Full-table LCD scan no longer runs every 10 minutes by default | Default `TIER_SYNC_RECONCILE_INTERVAL` = 86400s; `run_tier_reconcile_loop` replaces 600s `run_tier_sync_loop` | PASS | | Route solver quotes match discount tier after registration | `cd indexer && cargo test --test indexer_tier_sync route_solve_uses_tier_after_registration_event -- --test-threads=1` | PASS | | Reconciliation job still corrects drift | `cd indexer && cargo test --test indexer_tier_sync reconcile_corrects_missed_registration_event -- --test-threads=1` | PASS | | Lib tests | `cd indexer && cargo test --lib` | PASS | | Integration suite | `make test-indexer-integration` | PASS | | Manual LocalTerra register → trader row + route tier within 1 block | Not run in agent VM (requires full LocalTerra deploy + wallet tx) | SKIP | ## Third-party verification 1. `make setup-indexer-postgres` 2. `cd indexer && cargo test --lib` 3. `cd indexer && cargo test --test indexer_tier_sync -- --test-threads=1` 4. `make test-indexer-integration` 5. (Optional LocalTerra) `make setup-cloud-localterra`, register tier on fee-discount contract, confirm `GET /api/v1/traders/{addr}` and `GET /api/v1/route/solve?...&sender={addr}` reflect tier within one block — no 10-minute wait. ## Attack / abuse coverage - Malformed registry wasm events: unit tests + warn-and-continue in parser (`trader_tracker::parse_fee_discount_registry_events`). - Burst registration txs per wallet in one tx: last event wins (deduped per wallet per tx). <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes how tier data is populated for route quotes and trader API; incorrect parsing could serve wrong discounts until reconcile, but malformed events fail open with skip/warn and block ingest errors still propagate. > > **Overview** > Replaces the default **10-minute full-table LCD** `get_registration` scan with **event-driven** tier updates during block parsing, so `traders.tier_id` / registration state stay aligned with on-chain registry txs within the indexed block. > > When `FEE_DISCOUNT_ADDRESS` is set, the block parser applies `register` / `register_wallet` / `deregister` / `deregister_wallet` wasm events from that contract (scoped via `_contract_address`, malformed segments skipped, duplicate wallets in one tx → last wins). **First swap** from a new sender still triggers a one-off LCD hydrate. A background **`run_tier_reconcile_loop`** keeps a full-table reconcile for drift only, defaulting to **24h** via new `TIER_SYNC_RECONCILE_INTERVAL` (min 60s). Trader tier writes use **`upsert_trader_tier`**; swap upsert returns whether the row was newly inserted. > > Docs and tests cover indexer invariants, env example, and integration paths (trader API + route-solve discounted quote after registration). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit d044349269da1e137a6a12a2bbbffb8aaeae3a63. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
ghost1 commented 2026-06-12 05:26:42 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
ghost1 commented 2026-06-12 05:26:49 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
ghost1 commented 2026-06-12 05:28:52 +00:00 (Migrated from gitlab.com)

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue.

Reviewed by Cursor Bugbot for commit a0cca80b3f. Configure here.

<!-- BUGBOT_REVIEW --> Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue. <!-- BUGBOT_FIX_ALL --> <a href="https://cursor.com/open?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9BTExfSU5fQ1VSU09SIiwiZGF0YSI6eyJyZWRpc0tleSI6ImJ1Z2JvdC1tdWx0aTpkODA5MDViYy03MDg5LTQyOGYtYjhjMC1hOGVkM2YyYTFmMmYiLCJlbmNyeXB0aW9uS2V5IjoiOXFxSjE0aXM1YkpXa19HZnZ6cjRfNFZzdGFMUGdoX3MwZEQwMkZJQWp3OCIsImJyYW5jaCI6Imlzc3VlLTM2NC1ldmVudC1kcml2ZW4tdGllci1zeW5jIiwicmVwb093bmVyIjoiUGxhc3RpY0RpZ2l0cyIsInJlcG9OYW1lIjoiY2w4eS1kZXgtdGVycmFjbGFzc2ljIn19" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/fix-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/fix-in-cursor-light.png"><img alt="Fix All in Cursor" width="115" height="28" src="https://cursor.com/assets/images/fix-in-cursor-dark.png"></picture></a> <!-- /BUGBOT_FIX_ALL --> <!-- BUGBOT_AUTOFIX_REVIEW_FOOTNOTE_BEGIN --> <sup>Bugbot Autofix is [ON](https://www.cursor.com/dashboard/bugbot). A cloud agent has been kicked off to fix the reported issue. <!-- BUGBOT_AUTOFIX_AGENT_LINK --></sup> <!-- BUGBOT_AUTOFIX_REVIEW_FOOTNOTE_END --> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit a0cca80b3fc28031b1e4344213061f08514d697f. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
ghost1 commented 2026-06-12 05:28:53 +00:00 (Migrated from gitlab.com)

Global skipped ignores valid register

Medium Severity

parse_fee_discount_registry_events treats any skipped attribute in the whole wasm event as applying to every registry action, not only the segment after that action. A later skipped deregister_wallet in the same flattened event can cause an earlier valid register to be dropped, so traders tier data stays wrong until the daily reconcile.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a0cca80b3f. Configure here.

### Global skipped ignores valid register **Medium Severity** <!-- DESCRIPTION START --> `parse_fee_discount_registry_events` treats any `skipped` attribute in the whole wasm event as applying to every registry `action`, not only the segment after that `action`. A later skipped `deregister_wallet` in the same flattened event can cause an earlier valid `register` to be dropped, so `traders` tier data stays wrong until the daily reconcile. <!-- DESCRIPTION END --> <!-- BUGBOT_BUG_ID: 4952bf11-aed8-4727-8915-8e393ca84906 --> <!-- LOCATIONS START indexer/src/indexer/trader_tracker.rs#L162-L168 LOCATIONS END --> <div><a href="https://cursor.com/open?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9DVVJTT1IiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OjY0MTliYWU5LTNkNWYtNGQzYy04MGQxLTY3NmU3MmNhZGM3ZSIsImVuY3J5cHRpb25LZXkiOiJIZktZLUNVMWpmUGJHdDNRaEtCWldGOUI5bDhSUjMxRnhTaktvWTVBUVBRIiwiYnJhbmNoIjoiaXNzdWUtMzY0LWV2ZW50LWRyaXZlbi10aWVyLXN5bmMiLCJyZXBvT3duZXIiOiJQbGFzdGljRGlnaXRzIiwicmVwb05hbWUiOiJjbDh5LWRleC10ZXJyYWNsYXNzaWMifX0" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/fix-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/fix-in-cursor-light.png"><img alt="Fix in Cursor" width="115" height="28" src="https://cursor.com/assets/images/fix-in-cursor-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/agents?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9XRUIiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OjY0MTliYWU5LTNkNWYtNGQzYy04MGQxLTY3NmU3MmNhZGM3ZSIsImVuY3J5cHRpb25LZXkiOiJIZktZLUNVMWpmUGJHdDNRaEtCWldGOUI5bDhSUjMxRnhTaktvWTVBUVBRIiwiYnJhbmNoIjoiaXNzdWUtMzY0LWV2ZW50LWRyaXZlbi10aWVyLXN5bmMiLCJyZXBvT3duZXIiOiJQbGFzdGljRGlnaXRzIiwicmVwb05hbWUiOiJjbDh5LWRleC10ZXJyYWNsYXNzaWMiLCJwck51bWJlciI6MTU4LCJjb21taXRTaGEiOiJhMGNjYTgwYjNmYzI4MDMxYjFlNDM0NDIxMzA2MWYwODUxNGQ2OTdmIiwicHJvdmlkZXIiOiJnaXRsYWIiLCJob3N0bmFtZSI6ImdpdGxhYi5jb20ifX0" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/fix-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/fix-in-web-light.png"><img alt="Fix in Web" width="99" height="28" src="https://cursor.com/assets/images/fix-in-web-dark.png"></picture></a></div> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit a0cca80b3fc28031b1e4344213061f08514d697f. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
PlasticDigits commented 2026-06-12 05:30:11 +00:00 (Migrated from gitlab.com)

Security review — MR !876

Commit reviewed: a0cca80b3fc28031b1e4344213061f08514d697f

Scope: Event-driven fee-discount tier sync (trader_tracker.rs, parser.rs, poller.rs, traders.rs queries, config.rs, tests/docs). Focus: injection, authz, tier spoofing, quote/cache integrity, secret leakage, SSRF/deserialization.

Method: Traced attacker-controlled inputs (wasm tx attributes, LCD responses, API query params) to sinks (SQL upserts, route tier resolution, logging). Cross-checked fee-discount contract event emission and existing route-solver controls.

Outcome: FINDINGS: 0 medium+

Areas examined (no exploitable path found)

Area Result
Wasm event tier spoofing parse_fee_discount_registry_events scopes on _contract_address == FEE_DISCOUNT_ADDRESS; CosmWasm runtime binds that attribute to the executing contract, so arbitrary contracts cannot forge fee-discount registry events. Malformed attrs warn-and-skip; tier_id bounded 0–255.
SQL injection upsert_trader_tier / upsert_trader use parameterized sqlx binds for wallet/tier fields.
Quote / cache manipulation Route GET still resolves discounts via live LCD get_discount when configured; DB traders.tier_id is fallback. Event-driven DB updates do not bypass on-chain discount enforcement at execution. Optional sender/trader quote params are pre-existing (#245) and unchanged by this diff.
Authn/authz Indexer ingestion remains chain-trusted LCD data; no new unauthenticated write surfaces.
SSRF / deserialization No new outbound URL construction from user input; JSON parsing unchanged.
Secrets / logging No credential handling added; logs emit wallet/tier metadata only.
Supply chain No dependency manifest changes in this MR.

Inline threads: none (no medium+ findings).

Security review: no medium+ findings on this diff.

## Security review — MR !876 **Commit reviewed:** `a0cca80b3fc28031b1e4344213061f08514d697f` **Scope:** Event-driven fee-discount tier sync (`trader_tracker.rs`, `parser.rs`, `poller.rs`, `traders.rs` queries, `config.rs`, tests/docs). Focus: injection, authz, tier spoofing, quote/cache integrity, secret leakage, SSRF/deserialization. **Method:** Traced attacker-controlled inputs (wasm tx attributes, LCD responses, API query params) to sinks (SQL upserts, route tier resolution, logging). Cross-checked fee-discount contract event emission and existing route-solver controls. **Outcome:** `FINDINGS: 0` medium+ ### Areas examined (no exploitable path found) | Area | Result | |------|--------| | **Wasm event tier spoofing** | `parse_fee_discount_registry_events` scopes on `_contract_address == FEE_DISCOUNT_ADDRESS`; CosmWasm runtime binds that attribute to the executing contract, so arbitrary contracts cannot forge fee-discount registry events. Malformed attrs warn-and-skip; `tier_id` bounded 0–255. | | **SQL injection** | `upsert_trader_tier` / `upsert_trader` use parameterized `sqlx` binds for wallet/tier fields. | | **Quote / cache manipulation** | Route GET still resolves discounts via live LCD `get_discount` when configured; DB `traders.tier_id` is fallback. Event-driven DB updates do not bypass on-chain discount enforcement at execution. Optional `sender`/`trader` quote params are pre-existing (#245) and unchanged by this diff. | | **Authn/authz** | Indexer ingestion remains chain-trusted LCD data; no new unauthenticated write surfaces. | | **SSRF / deserialization** | No new outbound URL construction from user input; JSON parsing unchanged. | | **Secrets / logging** | No credential handling added; logs emit wallet/tier metadata only. | | **Supply chain** | No dependency manifest changes in this MR. | **Inline threads:** none (no medium+ findings). Security review: **no medium+ findings** on this diff.
ghost1 commented 2026-06-12 05:31:08 +00:00 (Migrated from gitlab.com)

added 1 commit

  • d0443492 - fix(indexer): scope skipped guard to per-action wasm segment

Compare with previous version

added 1 commit <ul><li>d0443492 - fix(indexer): scope skipped guard to per-action wasm segment</li></ul> [Compare with previous version](/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/158/diffs?diff_id=1848257848&start_sha=a0cca80b3fc28031b1e4344213061f08514d697f)
ghost1 commented 2026-06-12 05:31:27 +00:00 (Migrated from gitlab.com)

resolved all threads

resolved all threads
ghost1 commented 2026-06-12 05:31:27 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-06-12 07:57:38 +00:00 (Migrated from gitlab.com)

mentioned in commit 9ec4c66a0a

mentioned in commit 9ec4c66a0abb06f711f45fa83ad963620577a95d
PlasticDigits (Migrated from gitlab.com) merged commit 9ec4c66a0a into main 2026-06-12 07:57:38 +00:00
Sign in to join this conversation.
No reviewers
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!876
No description provided.