fix: /protocol Wrap/Unwrap ingest — cUSTC/cLUNC fee_amount actually lands #613

Closed
opened 2026-08-24 00:30:12 +00:00 by PlasticDigits · 20 comments
PlasticDigits commented 2026-08-24 00:30:12 +00:00 (Migrated from gitlab.com)

Summary

Make /protocol Wrap / Unwrap rows real for cUSTC and cLUNC. The #586 fee census already has those sources and Coolify already pins WRAP_MAPPER_ADDRESS, but production GET /api/v1/protocol/fees reports event_count: 0 for wrap and unwrap on 24h / 7d / 30d (sampled 2026-08-24). The UI hides idle $0 sources (PFee-3), so operators only see AMM swap and Book take.

Bundle wrap + unwrap, both denoms (uusd↔cUSTC, uluna↔cLUNC), and every retail execute path (direct /wrap, treasury wrap_deposit, CW20 send unwrap, wrap+router combo, router unwrap_output). Do not split “attr parser” vs “LocalTerra fixture” vs “UI row” vs “cUSTC vs cLUNC”.

Related: #586 (protocol fees; wrap ingest in-scope, ust1-window out of scope), #516 (split fee_wrap_bps / fee_unwrap_bps), #590 (M590-2 Coolify pin, M590-4 unwrap fee_amount only), #512 (burn tax ≠ protocol fee), #568 (stamp USD at ingest), #550 / #556 (USD catalog / hub). UST1 window mint/redeem is a separate follow-up.

Current codebase

Layer Behavior today
Fee sources FeeSource already has Wrap / Unwrap (wrap / unwrap). Retail labels Wrap / Unwrap. FeeSource::ALL includes both.
Parser parse_wrap_fees accepts wasm only when _contract_address equals pinned WRAP_MAPPER_ADDRESS, action is exactly wrap | unwrap, fee_amount is a positive raw integer, and token identity is fee_asset / fee_denom / native_denom / denom. Fail-closed: missing amount or token → drop. tax_amount / instant_withdraw ignored. Uses last-value attrs on the whole event — not the per-action segment scan used for limit lifecycle (#141 / #285).
Ingest hook parser.rs process_wrap_fee → ingest_protocol_fee when config.wrap_mapper_address is Some. Replay-safe UNIQUE (tx_hash, source, ordinal).
Pin WRAP_MAPPER_ADDRESS via config.rs + parse_wrap_mapper_address (terra1 bech32 only). Empty / garbage → wrap/unwrap omitted from breakdown (not fake $0). Columbus-5 mapper: terra1xuuuhpmyd5t29ry7mydg7ra2q2phrwhx7j28nx7x9sjw6zznkumsz0nmd2 (REGISTRY.md).
Rollup / GET protocol_fees.rs queries + GET /api/v1/protocol/fees. CHECK constraint: source IN ('swap_amm','book_take','limit_place','wrap','unwrap'). GET is O(1) / 60s cache — must not SUM protocol_fee_events.
UI ProtocolFeeStats.tsx: labels Wrap / Unwrap; hide wrap-family unless wrap_mapper_configured; hide idle $0. Tokens table is the denom split (not extra source rows).
Retail execute (why attrs may not match) Direct wrap is treasury wrap_deposit with native funds (wrapViaTreasury), not a user execute on the mapper. Unwrap is CW20 send → mapper { unwrap }. Swap/LP combos add wrap_deposit then router; output unwrap is router unwrap_output → mapper InstantWithdraw (NATIVE_TOKEN_WRAPPING.md). LCD often flattens several wasm emissions into one wasm stream.
Live (2026-08-24) https://indexer.dex.cl8y.com/api/v1/protocol/fees?window=24h → wrap_mapper_configured: true, wrap/unwrap event_count: 0 on 24h and 7d and 30d. AMM + book take are non-zero. Token mix (UST1 / USTR / cUSTC / cLUNC) is swap/book commission, not wrap fees. Fee ingest itself only exists since #586 (~2026-08-21).
Tests Unit: pin / spoof / missing fee_amount / ignore tax_amount / zero amount in protocol_fees.rs. Integration: indexer/tests/indexer_protocol_fees.rs. UI: ProtocolPage.test.tsx (hides idle unwrap). Verify: make verify-issue-586. No fixture from a captured columbus-5 / LocalTerra wrap tx.

Why this is needed

  1. /protocol claims wrap is a fee source (#586 / PFee-3) but production never shows it. Operators cannot tell whether wrap treasury is idle or the indexer is dropping events.
  2. cUSTC / cLUNC wrap fees are CMM income (fee_wrap_bps / fee_unwrap_bps, product-visible on /wrap and in swap quotes). Leaving them at $0 undercounts the same treasury the AMM/book rows report.
  3. The documented attr shape may not be what LCD emits. #586 assumed action=wrap\|unwrap + fee_amount on the mapper contract. Retail wrap starts at treasury wrap_deposit; combos flatten wasm; InstantWithdraw uses tax_amount. Last-value parse on a flattened event whose last action is swap / unwrap_output / instant_withdraw silently drops the mapper fee.
  4. Zero events for 30d while swaps exist is not explained by “hide idle $0” alone. Either nobody wrapped since ingest started, or every wrap failed the fail-closed attr gate. This issue must prove which with a captured tx, then make the parser match reality without inventing amount × bps.

Constraints / guardrails

  1. Keep PFee-1–PFee-12, L7, M590-4, W8–W15. Treasury inflows only. InstantWithdraw burn tax / tax_amount / hook_fee_amount / spread / gas are not protocol fees.
  2. Fail closed. Require an explicit on-chain fee amount attribute (name locked from a real tx / ustr-cmm source). Never infer gross × fee_wrap_bps. Missing token identity → drop.
  3. Pin stays exact bech32. Only WRAP_MAPPER_ADDRESS. Do not count treasury wrap_deposit as a fee unless the mapper emission is present and pinned. Do not LIKE % / case-fold. Spoof action=wrap on another contract must stay ignored.
  4. Flattened wasm. If LCD merges wrap + swap + unwrap into one wasm stream, parse each action segment and scope by reserved _contract_address only (#285). Do not trust user-set contract_address.
  5. No new source keys unless captured attrs prove wrap and unwrap are not wrap/unwrap. Retail labels stay Wrap / Unwrap. Do not add “cUSTC wrap” / “cLUNC wrap” source rows — token mix stays in the Tokens table.
  6. Same USD catalog. USTC/cUSTC/uusd → USTC oracle; LUNC/cLUNC/uluna → LUNC. Never vFDUSD. Never $1 UST1. Stamp fee_usd at ingest (#568).
  7. GET stays O(1). Do not SUM protocol_fee_events on /overview or /protocol/fees. No new unbounded breakdown.
  8. Unconfigured mapper still omits wrap/unwrap (not fake $0). Idle + configured + zero events may stay hidden in the Source table (PFee-3); totals stay "0".
  9. Do not bind-mount indexer/ into root Docker for cargo. Host cargo test / make verify-issue-*.
  10. Out of scope. UST1 window mint/redeem fees (separate issue). Changing on-chain fee_*_bps. Pair-creation uluna. Headlining traders.total_fees_paid.

Relevant files

Path Role
indexer/src/indexer/protocol_fees.rs parse_wrap_fees, pin, fail-closed attrs
indexer/src/indexer/parser.rs process_wrap_fee; flattened-event patterns (wasm_kv_map_after_action)
indexer/src/config.rs WRAP_MAPPER_ADDRESS
indexer/src/db/queries/protocol_fees.rs Insert + rollup; wrap-family omit when unconfigured
indexer/migrations/20260821120000_protocol_fees.sql CHECK enum (no change unless new source proven necessary)
indexer/tests/indexer_protocol_fees.rs Integration
frontend-dapp/src/components/protocol/ProtocolFeeStats.tsx Source labels + idle hide
frontend-dapp/src/services/terraclassic/wrapMapper.ts Retail wrap/unwrap execute
frontend-dapp/src/services/terraclassic/router.ts wrap_deposit + unwrap_output combos
skills/AGENTS_FRONTEND_PROTOCOL_STATS.md PFee-6
skills/AGENTS_WRAP_MAPPER_SPLIT_FEES.md Split fees; ingest ≠ query-time bps
docs/indexer-invariants.md Protocol fees row
scripts/qa/verify-issue-586.sh Existing verify; extend or add verify-issue-<iid>
  1. Capture truth first. Pull wasm attributes from (a) a columbus-5 wrap and unwrap tx against the pinned mapper (Finder/LCD), and (b) a LocalTerra wrap_deposit + unwrap + wrap+2hop + unwrap_output tx after make setup-cloud-localterra. Lock the exact action strings, fee amount key, and token keys in a fixture comment. Do not guess from #586 comments if LCD disagrees.
  2. Align parse_wrap_fees to that fixture. Prefer per-action segments + reserved _contract_address (reuse parser helpers). Keep exact mapper pin. Map both denoms into the same wrap / unwrap sources.
  3. Cover combo txs. A wrap_deposit+router swap must still persist a wrap fee and the AMM commission (no double-count, no drop). Router unwrap_output InstantWithdraw must persist unwrap fee_amount and not tax_amount.
  4. Prove ingest end-to-end. Integration test: fixture tx → protocol_fee_events row → rollup event_count ≥ 1 → GET /protocol/fees Source Wrap / Unwrap with non-zero USD when hub/oracle prices the token.
  5. Docs. Note captured attr names under PFee-6 / indexer invariants. Coolify pin is already the job of #590 — do not treat “set WRAP_MAPPER_ADDRESS” as the fix if it is already true on production.
  6. If live chain truly has zero wraps since #586 ingest: still ship the attr-aligned parser + LocalTerra proof so the next wrap appears. Do not close on “configured=true, events=0” alone.

Acceptance criteria

  • A documented LocalTerra or columbus-5 wrap tx (cUSTC and cLUNC paths, or one path + fixture for the other denom) produces protocol_fee_events.source = wrap with positive amount_raw and stamped fee_usd when the token is catalogued.
  • The matching unwrap path produces source = unwrap. InstantWithdraw tax_amount is not stored.
  • wrap_deposit + router combo persists wrap fee and swap_amm once each.
  • Spoof contract action=wrap + fee_amount is ignored.
  • Missing fee_amount / token identity → no row (no amount × bps).
  • Unconfigured WRAP_MAPPER_ADDRESS still omits wrap/unwrap from by_source (not $0).
  • Production-shaped GET remains O(1); no request-path SUM of protocol_fee_events.
  • /protocol Source shows Wrap / Unwrap when event_count > 0; idle $0 still hidden. Tokens table can show uusd / uluna / cUSTC / cLUNC as wrap-fee tokens when those events exist.
  • make verify-issue-586 still passes. New make verify-issue-<iid> covers this issue’s fixtures + docs crosslinks.

Test plan (happy + edge)

Path Expect
Direct treasury wrap_deposit uusd → cUSTC One wrap row; token identity native or CW20 per captured attrs; USD via USTC catalog
Direct wrap_deposit uluna → cLUNC One wrap row; LUNC catalog
CW20 send unwrap cUSTC / cLUNC One unwrap row; not tax_amount
wrap_deposit + 1-hop / ≥2-hop router wrap fee + swap_amm; no drop because last action is swap
Router unwrap_output InstantWithdraw unwrap fee_amount only
Zero fee_amount No row
Unpriced token Event stored; fee_usd NULL; window with only unpriced wrap → overview null (—), not $0
Idle configured mapper wrap/unwrap event_count=0 hidden in UI; totals "0"
Replay same tx ON CONFLICT DO NOTHING; no double-count
Window decay (#577) Wrap event older than 24h leaves 24h wrap at 0
--fresh / young indexer Δ% null until 2×W; copy must not claim genesis wrap fees

Test plan (attack / abuse)

Vector Guard
Random contract emits action=wrap + huge fee_amount Pin equality on _contract_address only
Forged contract_address (no underscore) Ignored (#285)
javascript: / XSS in denom / symbol Asset resolve fail-closed or text-escaped Tokens table (PFee-4)
fee_amount overflow / non-numeric / negative Drop; NUMERIC(38,0) + amount_raw > 0 CHECK
Infer fee from amount × bps when attr missing Forbidden — test that no row is inserted
Count InstantWithdraw burn tax as unwrap Forbidden — fixture with both fee_amount and tax_amount
Count treasury wrap_deposit with no mapper fee emission No wrap row
Substring / case-fold pin (TERRA1…, prefix+address) parse_wrap_mapper_address / wrap_mapper_matches reject
window=24h' OR 1=1 on GET Still 400 allowlist
Unbounded source enum / CSV Do not add; keep fixed enum + top-8 tokens
Double-count wrap inside hybrid swap book_commission_amount L7 unchanged; wrap is additional, book is fill-only
GET live-scans 60d wrap events Forbidden even with OVERVIEW_GLOBAL_STATS_LIVE=1

Verification criteria

make setup-indexer-postgres
make verify-issue-586
make verify-issue-<iid>   # this issue — docs + fixtures + parser + UI idle-hide
# when LocalTerra required for capture:
make setup-cloud-localterra
# then wrap uusd + uluna, unwrap both, one wrap+router combo; indexer must persist wrap/unwrap

Close only when: (1) parser matches a captured tx (not only the synthetic #586 attrs), (2) LocalTerra or columbus-5 proof that wrap/unwrap event_count increments, (3) spoof/tax/bps-inference attacks stay fail-closed, (4) GET remains O(1). “wrap_mapper_configured: true” alone is not done.

## Summary Make `/protocol` **Wrap** / **Unwrap** rows real for **cUSTC and cLUNC**. The #586 fee census already has those sources and Coolify already pins `WRAP_MAPPER_ADDRESS`, but production `GET /api/v1/protocol/fees` reports **`event_count: 0`** for wrap and unwrap on 24h / 7d / 30d (sampled 2026-08-24). The UI hides idle `$0` sources (**PFee-3**), so operators only see **AMM swap** and **Book take**. Bundle **wrap + unwrap**, both denoms (**uusd↔cUSTC**, **uluna↔cLUNC**), and every retail execute path (direct `/wrap`, treasury `wrap_deposit`, CW20 `send` unwrap, wrap+router combo, router `unwrap_output`). Do **not** split “attr parser” vs “LocalTerra fixture” vs “UI row” vs “cUSTC vs cLUNC”. Related: [#586](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/586) (protocol fees; wrap ingest in-scope, **ust1-window out of scope**), [#516](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/516) (split `fee_wrap_bps` / `fee_unwrap_bps`), [#590](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/590) (**M590-2** Coolify pin, **M590-4** unwrap `fee_amount` only), [#512](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/512) (burn tax ≠ protocol fee), [#568](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/568) (stamp USD at ingest), [#550](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/550) / [#556](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/556) (USD catalog / hub). UST1 window mint/redeem is a **separate** follow-up. ## Current codebase | Layer | Behavior today | |-------|----------------| | **Fee sources** | [`FeeSource`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/indexer/protocol_fees.rs) already has `Wrap` / `Unwrap` (`wrap` / `unwrap`). Retail labels **Wrap** / **Unwrap**. `FeeSource::ALL` includes both. | | **Parser** | [`parse_wrap_fees`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/indexer/protocol_fees.rs) accepts wasm only when `_contract_address` **equals** pinned `WRAP_MAPPER_ADDRESS`, `action` is exactly `wrap` \| `unwrap`, `fee_amount` is a positive raw integer, and token identity is `fee_asset` / `fee_denom` / `native_denom` / `denom`. Fail-closed: missing amount or token → drop. `tax_amount` / `instant_withdraw` ignored. Uses **last-value** attrs on the whole event — **not** the per-`action` segment scan used for limit lifecycle ([#141](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/141) / [#285](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/285)). | | **Ingest hook** | [`parser.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/indexer/parser.rs) `process_wrap_fee` → `ingest_protocol_fee` when `config.wrap_mapper_address` is `Some`. Replay-safe `UNIQUE (tx_hash, source, ordinal)`. | | **Pin** | `WRAP_MAPPER_ADDRESS` via [`config.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/config.rs) + [`parse_wrap_mapper_address`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/indexer/protocol_fees.rs) (`terra1` bech32 only). Empty / garbage → wrap/unwrap **omitted** from breakdown (not fake `$0`). Columbus-5 mapper: `terra1xuuuhpmyd5t29ry7mydg7ra2q2phrwhx7j28nx7x9sjw6zznkumsz0nmd2` ([`REGISTRY.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/deployments/mainnet-ust1-wrap/REGISTRY.md)). | | **Rollup / GET** | [`protocol_fees.rs` queries](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/db/queries/protocol_fees.rs) + [`GET /api/v1/protocol/fees`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/api/protocol_fees.rs). CHECK constraint: `source IN ('swap_amm','book_take','limit_place','wrap','unwrap')`. GET is O(1) / 60s cache — must not `SUM protocol_fee_events`. | | **UI** | [`ProtocolFeeStats.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/protocol/ProtocolFeeStats.tsx): labels Wrap / Unwrap; hide wrap-family unless `wrap_mapper_configured`; hide idle `$0`. Tokens table is the denom split (not extra source rows). | | **Retail execute (why attrs may not match)** | Direct wrap is **treasury** `wrap_deposit` with native funds ([`wrapViaTreasury`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/wrapMapper.ts)), not a user `execute` on the mapper. Unwrap is CW20 `send` → mapper `{ unwrap }`. Swap/LP combos add `wrap_deposit` then router; output unwrap is router `unwrap_output` → mapper InstantWithdraw ([`NATIVE_TOKEN_WRAPPING.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/NATIVE_TOKEN_WRAPPING.md)). LCD often **flattens** several wasm emissions into one `wasm` stream. | | **Live (2026-08-24)** | `https://indexer.dex.cl8y.com/api/v1/protocol/fees?window=24h` → `wrap_mapper_configured: true`, wrap/unwrap `event_count: 0` on 24h **and** 7d **and** 30d. AMM + book take are non-zero. Token mix (UST1 / USTR / cUSTC / cLUNC) is **swap/book commission**, not wrap fees. Fee ingest itself only exists since #586 (~2026-08-21). | | **Tests** | Unit: pin / spoof / missing `fee_amount` / ignore `tax_amount` / zero amount in [`protocol_fees.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/indexer/protocol_fees.rs). Integration: [`indexer/tests/indexer_protocol_fees.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/tests/indexer_protocol_fees.rs). UI: [`ProtocolPage.test.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/ProtocolPage.test.tsx) (hides idle unwrap). Verify: `make verify-issue-586`. **No** fixture from a captured columbus-5 / LocalTerra wrap tx. | ## Why this is needed 1. **`/protocol` claims wrap is a fee source** (#586 / **PFee-3**) but production never shows it. Operators cannot tell whether wrap treasury is idle or the indexer is dropping events. 2. **cUSTC / cLUNC wrap fees are CMM income** (`fee_wrap_bps` / `fee_unwrap_bps`, product-visible on `/wrap` and in swap quotes). Leaving them at `$0` undercounts the same treasury the AMM/book rows report. 3. **The documented attr shape may not be what LCD emits.** #586 assumed `action=wrap\|unwrap` + `fee_amount` on the **mapper** contract. Retail wrap starts at **treasury `wrap_deposit`**; combos flatten wasm; InstantWithdraw uses `tax_amount`. Last-value parse on a flattened event whose last `action` is `swap` / `unwrap_output` / `instant_withdraw` silently drops the mapper fee. 4. **Zero events for 30d while swaps exist** is not explained by “hide idle `$0`” alone. Either nobody wrapped since ingest started, or every wrap failed the fail-closed attr gate. This issue must **prove which** with a captured tx, then make the parser match reality without inventing `amount × bps`. ## Constraints / guardrails 1. **Keep PFee-1–PFee-12, L7, M590-4, W8–W15.** Treasury inflows only. InstantWithdraw **burn tax** / `tax_amount` / `hook_fee_amount` / spread / gas are **not** protocol fees. 2. **Fail closed.** Require an explicit on-chain fee amount attribute (name locked from a real tx / ustr-cmm source). **Never** infer `gross × fee_wrap_bps`. Missing token identity → drop. 3. **Pin stays exact bech32.** Only `WRAP_MAPPER_ADDRESS`. Do not count treasury `wrap_deposit` as a fee unless the **mapper** emission is present and pinned. Do not `LIKE %` / case-fold. Spoof `action=wrap` on another contract must stay ignored. 4. **Flattened wasm.** If LCD merges wrap + swap + unwrap into one `wasm` stream, parse **each** `action` segment and scope by reserved `_contract_address` only (#285). Do not trust user-set `contract_address`. 5. **No new source keys** unless captured attrs prove wrap and unwrap are not `wrap`/`unwrap`. Retail labels stay **Wrap** / **Unwrap**. Do not add “cUSTC wrap” / “cLUNC wrap” source rows — token mix stays in the Tokens table. 6. **Same USD catalog.** USTC/cUSTC/`uusd` → USTC oracle; LUNC/cLUNC/`uluna` → LUNC. Never vFDUSD. Never `$1` UST1. Stamp `fee_usd` at ingest (#568). 7. **GET stays O(1).** Do not `SUM protocol_fee_events` on `/overview` or `/protocol/fees`. No new unbounded breakdown. 8. **Unconfigured mapper still omits** wrap/unwrap (not fake `$0`). Idle + configured + zero events may stay hidden in the Source table (**PFee-3**); totals stay `"0"`. 9. **Do not bind-mount `indexer/` into root Docker for cargo.** Host `cargo test` / `make verify-issue-*`. 10. **Out of scope.** UST1 window mint/redeem fees (separate issue). Changing on-chain `fee_*_bps`. Pair-creation `uluna`. Headlining `traders.total_fees_paid`. ## Relevant files | Path | Role | |------|------| | [`indexer/src/indexer/protocol_fees.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/indexer/protocol_fees.rs) | `parse_wrap_fees`, pin, fail-closed attrs | | [`indexer/src/indexer/parser.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/indexer/parser.rs) | `process_wrap_fee`; flattened-event patterns (`wasm_kv_map_after_action`) | | [`indexer/src/config.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/config.rs) | `WRAP_MAPPER_ADDRESS` | | [`indexer/src/db/queries/protocol_fees.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/db/queries/protocol_fees.rs) | Insert + rollup; wrap-family omit when unconfigured | | [`indexer/migrations/20260821120000_protocol_fees.sql`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/migrations/20260821120000_protocol_fees.sql) | CHECK enum (no change unless new source proven necessary) | | [`indexer/tests/indexer_protocol_fees.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/tests/indexer_protocol_fees.rs) | Integration | | [`frontend-dapp/src/components/protocol/ProtocolFeeStats.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/protocol/ProtocolFeeStats.tsx) | Source labels + idle hide | | [`frontend-dapp/src/services/terraclassic/wrapMapper.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/wrapMapper.ts) | Retail wrap/unwrap execute | | [`frontend-dapp/src/services/terraclassic/router.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/router.ts) | wrap_deposit + unwrap_output combos | | [`skills/AGENTS_FRONTEND_PROTOCOL_STATS.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/skills/AGENTS_FRONTEND_PROTOCOL_STATS.md) | **PFee-6** | | [`skills/AGENTS_WRAP_MAPPER_SPLIT_FEES.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/skills/AGENTS_WRAP_MAPPER_SPLIT_FEES.md) | Split fees; ingest ≠ query-time bps | | [`docs/indexer-invariants.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/indexer-invariants.md) | Protocol fees row | | [`scripts/qa/verify-issue-586.sh`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/scripts/qa/verify-issue-586.sh) | Existing verify; extend or add `verify-issue-<iid>` | ## Recommended direction 1. **Capture truth first.** Pull wasm attributes from (a) a columbus-5 wrap and unwrap tx against the pinned mapper (Finder/LCD), and (b) a LocalTerra `wrap_deposit` + unwrap + wrap+2hop + `unwrap_output` tx after `make setup-cloud-localterra`. Lock the exact `action` strings, fee amount key, and token keys in a fixture comment. Do not guess from #586 comments if LCD disagrees. 2. **Align `parse_wrap_fees` to that fixture.** Prefer per-`action` segments + reserved `_contract_address` (reuse parser helpers). Keep exact mapper pin. Map both denoms into the same `wrap` / `unwrap` sources. 3. **Cover combo txs.** A wrap_deposit+router swap must still persist a wrap fee **and** the AMM commission (no double-count, no drop). Router `unwrap_output` InstantWithdraw must persist unwrap `fee_amount` and **not** `tax_amount`. 4. **Prove ingest end-to-end.** Integration test: fixture tx → `protocol_fee_events` row → rollup `event_count ≥ 1` → `GET /protocol/fees` Source **Wrap** / **Unwrap** with non-zero USD when hub/oracle prices the token. 5. **Docs.** Note captured attr names under **PFee-6** / indexer invariants. Coolify pin is already the job of #590 — do not treat “set `WRAP_MAPPER_ADDRESS`” as the fix if it is already `true` on production. 6. **If live chain truly has zero wraps since #586 ingest:** still ship the attr-aligned parser + LocalTerra proof so the next wrap appears. Do not close on “configured=true, events=0” alone. ## Acceptance criteria - [ ] A documented LocalTerra **or** columbus-5 wrap tx (cUSTC **and** cLUNC paths, or one path + fixture for the other denom) produces `protocol_fee_events.source = wrap` with positive `amount_raw` and stamped `fee_usd` when the token is catalogued. - [ ] The matching unwrap path produces `source = unwrap`. InstantWithdraw `tax_amount` is **not** stored. - [ ] wrap_deposit + router combo persists wrap fee **and** `swap_amm` once each. - [ ] Spoof contract `action=wrap` + `fee_amount` is ignored. - [ ] Missing `fee_amount` / token identity → no row (no `amount × bps`). - [ ] Unconfigured `WRAP_MAPPER_ADDRESS` still **omits** wrap/unwrap from `by_source` (not `$0`). - [ ] Production-shaped GET remains O(1); no request-path `SUM` of `protocol_fee_events`. - [ ] `/protocol` Source shows **Wrap** / **Unwrap** when `event_count > 0`; idle `$0` still hidden. Tokens table can show uusd / uluna / cUSTC / cLUNC as wrap-fee tokens when those events exist. - [ ] `make verify-issue-586` still passes. New `make verify-issue-<iid>` covers this issue’s fixtures + docs crosslinks. ## Test plan (happy + edge) | Path | Expect | |------|--------| | Direct treasury `wrap_deposit` uusd → cUSTC | One `wrap` row; token identity native or CW20 per captured attrs; USD via USTC catalog | | Direct `wrap_deposit` uluna → cLUNC | One `wrap` row; LUNC catalog | | CW20 `send` unwrap cUSTC / cLUNC | One `unwrap` row; not `tax_amount` | | wrap_deposit + 1-hop / ≥2-hop router | wrap fee + `swap_amm`; no drop because last action is `swap` | | Router `unwrap_output` InstantWithdraw | unwrap `fee_amount` only | | Zero `fee_amount` | No row | | Unpriced token | Event stored; `fee_usd` NULL; window with only unpriced wrap → overview `null` (`—`), not `$0` | | Idle configured mapper | wrap/unwrap `event_count=0` hidden in UI; totals `"0"` | | Replay same tx | `ON CONFLICT DO NOTHING`; no double-count | | Window decay (#577) | Wrap event older than 24h leaves 24h wrap at 0 | | `--fresh` / young indexer | Δ% null until 2×W; copy must not claim genesis wrap fees | ## Test plan (attack / abuse) | Vector | Guard | |--------|--------| | Random contract emits `action=wrap` + huge `fee_amount` | Pin equality on `_contract_address` only | | Forged `contract_address` (no underscore) | Ignored (#285) | | `javascript:` / XSS in `denom` / symbol | Asset resolve fail-closed or text-escaped Tokens table (**PFee-4**) | | `fee_amount` overflow / non-numeric / negative | Drop; `NUMERIC(38,0)` + `amount_raw > 0` CHECK | | Infer fee from `amount × bps` when attr missing | Forbidden — test that no row is inserted | | Count InstantWithdraw burn tax as unwrap | Forbidden — fixture with both `fee_amount` and `tax_amount` | | Count treasury `wrap_deposit` with no mapper fee emission | No wrap row | | Substring / case-fold pin (`TERRA1…`, prefix+address) | `parse_wrap_mapper_address` / `wrap_mapper_matches` reject | | `window=24h' OR 1=1` on GET | Still 400 allowlist | | Unbounded source enum / CSV | Do not add; keep fixed enum + top-8 tokens | | Double-count wrap inside hybrid swap `book_commission_amount` | L7 unchanged; wrap is additional, book is fill-only | | GET live-scans 60d wrap events | Forbidden even with `OVERVIEW_GLOBAL_STATS_LIVE=1` | ## Verification criteria ```bash make setup-indexer-postgres make verify-issue-586 make verify-issue-<iid> # this issue — docs + fixtures + parser + UI idle-hide # when LocalTerra required for capture: make setup-cloud-localterra # then wrap uusd + uluna, unwrap both, one wrap+router combo; indexer must persist wrap/unwrap ``` Close only when: (1) parser matches a **captured** tx (not only the synthetic #586 attrs), (2) LocalTerra or columbus-5 proof that wrap/unwrap `event_count` increments, (3) spoof/tax/bps-inference attacks stay fail-closed, (4) GET remains O(1). “`wrap_mapper_configured: true`” alone is **not** done.
PlasticDigits commented 2026-08-24 00:30:13 +00:00 (Migrated from gitlab.com)

marked as related to #586

marked as related to #586
PlasticDigits commented 2026-08-24 00:30:14 +00:00 (Migrated from gitlab.com)

marked as related to #516

marked as related to #516
PlasticDigits commented 2026-08-24 00:30:14 +00:00 (Migrated from gitlab.com)

marked as related to #590

marked as related to #590
PlasticDigits commented 2026-08-24 00:30:22 +00:00 (Migrated from gitlab.com)

marked as related to #614

marked as related to #614
PlasticDigits commented 2026-08-24 00:30:34 +00:00 (Migrated from gitlab.com)

Sibling follow-up for UST1 window mint/redeem (not wrap-mapper): #614

Sibling follow-up for UST1 window mint/redeem (not wrap-mapper): #614
PlasticDigits commented 2026-08-24 00:43:17 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1128

mentioned in merge request !1128
PlasticDigits commented 2026-08-24 02:42:32 +00:00 (Migrated from gitlab.com)

mentioned in commit 4c120e831c

mentioned in commit 4c120e831ceafe47f92ae025e6ad32a3da5a70db
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-24 02:42:32 +00:00
PlasticDigits commented 2026-08-24 02:44:23 +00:00 (Migrated from gitlab.com)

mentioned in commit aa8991cf02

mentioned in commit aa8991cf025a6e90947b8aab0bc85be987ad2080
PlasticDigits commented 2026-08-24 02:45:16 +00:00 (Migrated from gitlab.com)

mentioned in commit 574793b4ef

mentioned in commit 574793b4efcce6069c85b07e63a93d7b0a0c647b
PlasticDigits commented 2026-08-24 02:48:09 +00:00 (Migrated from gitlab.com)

mentioned in commit e1490bbf59

mentioned in commit e1490bbf590de5b9d7253be4fed72d7cdf11b4ba
PlasticDigits commented 2026-08-24 03:01:15 +00:00 (Migrated from gitlab.com)

mentioned in issue #616

mentioned in issue #616
PlasticDigits commented 2026-08-24 03:01:18 +00:00 (Migrated from gitlab.com)

marked as related to #616

marked as related to #616
PlasticDigits commented 2026-08-24 03:01:43 +00:00 (Migrated from gitlab.com)

Merge sanity (!1128 landed on main; CI not waited).

The MR source branch had been reset to main (0 commits). Restored the real commit from issue/613-wrap-unwrap-ingest (4de6d839) onto the MR branch, then merged.

Automated on the integrated tip: make verify-issue-613 6/6 after a follow-up docs grep fix (992b980a — keep PFee-1–PFee-12 so nested verify-issue-586 still matches). Lib wrap parse (notify_deposit + fee, flattened combo, no burn-tax) + integration ingest/rollup/GET + Protocol idle-hide all passed.

Acceptance still open on production

  • Coolify indexer has not been redeployed from this tip. Live GET /api/v1/protocol/fees can still show wrap/unwrap event_count: 0.
  • Issue AC wants a documented columbus-5 / LocalTerra wrap and unwrap path with stamped fee_usd. Fixtures cover both denoms; live tx confirmation is ops.
  • InstantWithdraw tax_amount is not stored (unit + integration).

Post-merge Coolify + live wrap proof: #616.

**Merge sanity (!1128 landed on `main`; CI not waited).** The MR source branch had been reset to `main` (0 commits). Restored the real commit from `issue/613-wrap-unwrap-ingest` (`4de6d839`) onto the MR branch, then merged. Automated on the integrated tip: `make verify-issue-613` **6/6** after a follow-up docs grep fix (`992b980a` — keep `PFee-1–PFee-12` so nested `verify-issue-586` still matches). Lib wrap parse (notify_deposit + `fee`, flattened combo, no burn-tax) + integration ingest/rollup/GET + Protocol idle-hide all passed. **Acceptance still open on production** - Coolify indexer has not been redeployed from this tip. Live `GET /api/v1/protocol/fees` can still show wrap/unwrap `event_count: 0`. - Issue AC wants a documented columbus-5 / LocalTerra wrap **and** unwrap path with stamped `fee_usd`. Fixtures cover both denoms; live tx confirmation is ops. - InstantWithdraw `tax_amount` is not stored (unit + integration). Post-merge Coolify + live wrap proof: [#616](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/616).
PlasticDigits commented 2026-08-24 03:01:45 +00:00 (Migrated from gitlab.com)

mentioned in issue #614

mentioned in issue #614
PlasticDigits commented 2026-08-24 03:01:55 +00:00 (Migrated from gitlab.com)

mentioned in issue #610

mentioned in issue #610
PlasticDigits commented 2026-08-25 01:55:34 +00:00 (Migrated from gitlab.com)

mentioned in issue #631

mentioned in issue #631
PlasticDigits commented 2026-08-27 01:00:19 +00:00 (Migrated from gitlab.com)

mentioned in issue #682

mentioned in issue #682
PlasticDigits commented 2026-08-27 01:00:29 +00:00 (Migrated from gitlab.com)

mentioned in issue #683

mentioned in issue #683
PlasticDigits commented 2026-08-27 06:03:02 +00:00 (Migrated from gitlab.com)

mentioned in issue #687

mentioned in issue #687
PlasticDigits commented 2026-08-27 09:00:10 +00:00 (Migrated from gitlab.com)

mentioned in issue #689

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