Contracts internal security audit #424

Closed
opened 2026-06-29 00:10:30 +00:00 by PlasticDigits · 7 comments
PlasticDigits commented 2026-06-29 00:10:30 +00:00 (Migrated from gitlab.com)

Full security reviw and audit of the smart contacts, inspecting coverage, fuzz testing, adversarial scenario tests, common exploits, access control, liquidity drainage/order book drainage via looped trading, traders receiving excess funds (must not), rounding delivering more tokens to traders than expected, boundary conditions, and more - resea4ch and list more areas to cover first, then complete the audit

Full security reviw and audit of the smart contacts, inspecting coverage, fuzz testing, adversarial scenario tests, common exploits, access control, liquidity drainage/order book drainage via looped trading, traders receiving excess funds (must not), rounding delivering more tokens to traders than expected, boundary conditions, and more - resea4ch and list more areas to cover first, then complete the audit
PlasticDigits commented 2026-06-29 00:21:31 +00:00 (Migrated from gitlab.com)

Internal Full-Stack Security Audit — GitLab #424

Field Value
Issue #424 — Contracts internal security audit
Auditor Composer (internal)
Date (UTC) 2026-06-29
Epoch 1782692388
Baseline main @ workspace snapshot
Scope Full monorepo per issue: CosmWasm contracts, Terra messages, indexer/API/DB, frontend, deps, CI, secrets, ops

Executive summary

CL8Y DEX is a mature, defense-in-depth Terra Classic DeFi stack. Under the documented trust model (honest governance, CW20 whitelist, treasury as sink), no critical on-chain fund-drain or unauthorized-privilege bugs were identified in this pass. Core economic invariants — k-monotonicity, pool-favorable rounding, escrow accounting, hook caller allowlists, router hop deltas, hybrid limit-book matching, and access control — are extensively tested (388 contract lib tests, 7 audit-invariant regressions, 21 security_tests, 26 fuzz/proptest cases; all passed locally).

The highest residual risks are operational and governance-centric: single-key factory control, pause trapping limit escrow, absence of a third-party audit, and off-chain quote/indexer trust boundaries. Several historical gap items (LCD error leakage, router dust sweep, indexer cursor-on-error, tx pagination, parseFloat on min_received) are fixed and regression-tested in the current tree.

Launch recommendation: acceptable for capped-TVL / pool-first rollout with documented controls; not a substitute for external audit + multisig governance + completed launch checklist items (#391, #407, #408, #410).


Areas analyzed

Area Paths / surfaces Focus
CosmWasm contracts smartcontracts/contracts/{factory,pair,router,fee-discount,hooks/*} Access control, swap/LP math, limit book, hybrid matching, hooks, migration
Shared libs smartcontracts/packages/dex-common max_spread, oracle, hook settlement, blacklist types
Integration / adversarial tests smartcontracts/tests/src/{lib.rs,adversarial_token.rs,limit_order_tests.rs,blacklist_tests.rs,migration_tests.rs} Fuzz, reentrancy, fee-on-transfer, book hints, rounding
Indexer ingestion indexer/src/indexer/{poller,block_indexer,parser,pair_discovery}.rs Cursor, reorg, pagination, dedup, emitter scoping
Indexer API indexer/src/api/*, indexer/tests/security.rs SQL injection, CORS, rate limits, LCD amplification, error sanitization
Frontend frontend-dapp/src/{pages,services,utils,hooks} Signing alignment, slippage math, CSP, trust boundaries
CI / supply chain .gitlab-ci.yml, scripts/ci/gitleaks-*, smartcontracts/.cargo/audit.toml gitleaks, cargo-audit jobs, hook policy
Ops / docs docs/{security-model,contracts-security-audit,exploit-replay-matrix}.md Invariant matrix, exploit replay, launch gates
External deps frontend-dapp/package.json, smartcontracts/Cargo.toml npm/cargo advisories, forked cosmes

Additional areas beyond the issue text: trading blacklist, batch limit placement/cancel, permissionless book clean, wrap-mapper/treasury harness, reorg webhook, CG/CMC aggregator compliance routes.


Methodology and test execution

  1. Read issue #424 scope and repository security docs (docs/contracts-security-audit.md, docs/security-model.md, docs/exploit-replay-matrix.md, gaps/GAP_1780200149.md).
  2. Static review of execute/query paths, escrow math, router state machine, indexer API layers, frontend submit pipeline.
  3. Automated tests run locally:
Suite Command Result
Contract units + dex-common cd smartcontracts && cargo test --lib 388 + 21 passed
Audit invariants (P1, P5, P7, P8, P10, H1) cargo test audit_invariant 7 passed
Security regressions cargo test security_tests 21 passed
Fuzz / proptest cargo test fuzz_tests 26 passed
Indexer lib cd indexer && cargo test --lib 152 passed
Indexer security.rs integration Requires Postgres (indexer/.env); not run in this VM Skipped — cases documented in indexer/tests/security.rs
  1. npm audit --audit-level=high on frontend (12 advisories, 2 high).
  2. cargo audit not installed locally; CI enforces via .gitlab-ci.yml cargo-audit-smartcontracts / cargo-audit-indexer.

Findings

H-01 — Governance centralization (no on-chain timelock)

Severity High (operational)
Location smartcontracts/contracts/factory/src/contract.rs (ensure_governance); docs/security-model.md
Issue A single governance address can change fees, register reverting hooks, pause pairs, redirect treasury, mutate CW20 whitelist, push discount registry, and manage trading blacklists — atomically and without delay.
Impact Compromised or malicious governance can halt trading, trap escrow (see H-02), drain via misconfigured hooks/fees, or blacklist users. Not exploitable by non-governance callers.
Reproduction Deploy factory with EOA governance; call SetPairFee, RegisterHook, UpdateConfig — all succeed from governance only (audit_invariant_tests::p8_*).
Recommendation Production multisig/DAO only; document signer policy; complete emergency rehearsal (#408); consider off-chain timelock for high-impact msgs.

H-02 — Emergency pause blocks all maker escrow exits

Severity High (availability / funds mobility)
Location smartcontracts/contracts/pair/src/contract.rs — assert_not_paused on Receive, CancelLimitOrder, ClaimExpiredLimitOrder, CleanLimitBook (invariant L6)
Issue Governance pause intentionally freezes swaps and maker withdrawals (cancel, expired claim, book clean). Resting and parked escrow remains in pair custody until unpause.
Impact During incident response, limit makers cannot retrieve escrow; reputational and liquidity risk. Funds are not stolen but are immobilized by design.
Reproduction limit_order_tests::pause_blocks_swap_and_place_cancel_refunds_escrow; claim_expired_limit_order_blocked_while_pair_paused_then_succeeds_after_unpause.
Recommendation Document in user-facing incident FAQ (done); rehearse pause/unpause from planned multisig; consider whether post-incident governance-only escrow release is ever needed (product decision).

H-03 — No third-party security audit

Severity High (process)
Location docs/security-model.md; docs/contracts-security-audit.md § Third-party audit
Issue In-repo invariant matrix is self-authored; no external firm attestation on mainnet deployment.
Impact Unknown classes of CosmWasm/DeFi bugs may remain despite strong internal testing.
Recommendation Commission audit before high TVL; track deploy trace (#410); keep exploit-replay matrix current (#406).

M-01 — Fee-discount cache TTL (300s) allows bounded discount leakage

Severity Medium (economic, accepted)
Location smartcontracts/contracts/pair/src/discount_cache.rs; invariant P9
Issue Pair caches (effective_fee_bps, discount) for DISCOUNT_CACHE_TTL_SECONDS (300s). Trader who drops below tier CL8Y balance may retain cached discount until expiry.
Impact Protocol fee revenue loss bounded to one 300s window per trader; not exploitable for unlimited drain.
Reproduction Documented in docs/contracts-security-audit.md P9; fee_discount_coverage_tests.
Recommendation Accept as gas tradeoff (#275) or shorten TTL if CL8Y sink demand increases.

M-02 — Fee-discount registry outage charges full fee silently

Severity Medium
Location smartcontracts/contracts/pair/src/discount_cache.rs — Err(_) => (fee_bps, None); invariant P5
Issue When GetDiscount query fails (paused registry, migration, LCD error), swaps succeed at full pair fee with no on-chain revert.
Impact Tier holders overcharged during registry outages; opposite of discount theft.
Reproduction audit_invariant_tests::swap_uses_full_fee_when_discount_registry_query_fails; indexer GET /api/v1/health/fee-discount.
Recommendation Monitor registry health; dApp warning (non-blocking) when unhealthy; ops runbook for registry recovery.

M-03 — Registered reverting hook is a liveness kill-switch

Severity Medium
Location smartcontracts/contracts/pair/src/contract.rs (post-swap add_messages to hooks); invariant H1
Issue Allowlisted hook that returns Err rolls back the entire swap atomically.
Impact Governance misconfiguration or compromised hook contract blocks all swaps on that pair.
Reproduction audit_invariant_tests::swap_fails_atomically_when_allowlisted_hook_reverts.
Recommendation Hook registration runbook + audit hooks pre-mainnet; test pause before hook rollback in incidents.

M-04 — Indexer /health is shallow (no DB/LCD readiness)

Severity Medium (operational)
Location indexer/src/api/mod.rs:368-370
Issue GET /health returns {"status":"ok"} without probing Postgres or LCD.
Impact Load balancers may route traffic to a broken indexer; users see stale or empty data while infra appears healthy.
Reproduction curl /health on any running instance — always 200.
Recommendation Add /ready with DB ping + LCD probe; keep /health lightweight or document K8s probe split.

M-05 — Public Swagger UI / OpenAPI on indexer

Severity Medium (info disclosure)
Location indexer/src/api/mod.rs:499 — SwaggerUi::new("/swagger-ui") merged into production router
Issue Full API surface documented and browsable without authentication.
Impact Attackers enumerate LCD-heavy routes for DoS tuning; low direct exploit but aids reconnaissance.
Reproduction indexer/tests/security.rs::swagger_ui_available, openapi_spec_available.
Recommendation Gate Swagger behind auth or disable in RUN_MODE=prod; rate limits already mitigate abuse (#363).

M-06 — Production CSP allows script-src 'unsafe-inline'

Severity Medium
Location frontend-dapp/viteCsp.ts:50
Issue Meta CSP includes 'unsafe-inline' for scripts (Vite constraint).
Impact XSS via injected inline script is not blocked by CSP if another injection vector exists.
Reproduction Inspect built index.html CSP in production build.
Recommendation Migrate to nonce/hash-based CSP when Vite pipeline supports it; keep strict connect-src (already env-scoped).

M-07 — npm supply chain: 2 high-severity advisories

Severity Medium
Location frontend-dapp/package.json — transitive ws@7.x (GHSA-96hv-2xvq-fx4p); @goblinhunt/cosmes → @dao-dao/cosmiframe chain
Issue npm audit --audit-level=high reports 12 vulns (2 high).
Impact ws DoS affects dev tooling paths; wallet stack deps increase patch drift risk.
Reproduction cd frontend-dapp && npm audit --audit-level=high
Recommendation Track cosmes fork updates; bump/ws override where possible; CI npm audit gate on high+ for production builds.

M-08 — Expired limit-order head clog can exhaust scan budget

Severity Medium (griefing / availability)
Location smartcontracts/contracts/pair/src/orderbook.rs — MAX_SCAN_STEPS (500), MAX_EXPIRED_PARKS_PER_SWAP (15); invariant L5, L17
Issue Deep prefix of expired orders at book head can consume scan steps before live liquidity is reached; taker may get pool-only fill or revert on slippage.
Impact Hybrid matching degraded until CleanLimitBook or integrators pass book_start_hint past prefix (#289). Not a direct escrow drain — wrong-side hints blocked (#272).
Reproduction limit_order_tests::hybrid_walk_scan_steps_cap_bounds_expired_prefix_and_spills_to_pool; hybrid_wrong_side_book_start_hint_no_cross_escrow_drain.
Recommendation Operational monitoring of book head depth; encourage permissionless clean + integrator hints; trading swarm for head-clog scenarios.

M-09 — Off-chain route quotes are advisory (trust boundary)

Severity Medium
Location indexer/src/api/route_solver.rs; frontend-dapp/src/hooks/useSubmitAlignedSimQuote.ts; docs/security-model.md
Issue Indexer route/solve and LCD simulations can diverge from execution snapshot; malicious/compromised indexer could suggest suboptimal or phished routes.
Impact User signs tx with worse economics if they skip on-chain min_return / max_spread; funds still protected when those fields set correctly.
Reproduction Compare indexer output vs on-chain SimulateSwapOperations after env compromise (thought experiment); mitigated by assertSubmitQuotePayRawAligned + BigInt minReceived.
Recommendation Pin HTTPS indexer; swap confirmation route row + factory preflight; never disable slippage floors on retail paths.

L-01 — Router inner hops omit per-hop deadline

Severity Low
Location smartcontracts/contracts/router/src/contract.rs:268,429 — deadline: None on inner TerraSwap
Issue Multi-hop router enforces user deadline on outer msg; inner pair hops use deadline: None.
Impact Minimal within single-tx atomicity; outer deadline still checked where applicable.
Recommendation Optional hardening: forward remaining block time as inner deadline.

L-02 — Fee-on-transfer CW20 desync if whitelist policy violated

Severity Low (mitigated by policy)
Location Pair reserves credit declared amounts; adversarial_token::fee_on_transfer_creates_reserve_imbalance
Issue Whitelisted fee-on-transfer token breaks reserve invariant P2.
Impact Incorrect pricing; excess recoverable via governance sweep, not direct theft from LPs by arbitrary callers.
Recommendation Enforce docs/runbooks/cw20-whitelist-policy.md; run scripts/verify-cw20-code-ids.sh before AddWhitelistedCodeId.

L-03 — Display-layer float formatting for very large balances

Severity Low
Location frontend-dapp/src/utils/formatAmount.ts:129 (parseFloat in abbrev path)
Issue Submit path uses BigInt (rawAmountMath.ts); display abbreviations may round for amounts > 2⁵³.
Impact UI mis-display only; on-chain args unaffected.
Recommendation Extend BigInt-only formatting to abbrev branch for consistency.

Controls verified (no regression)

ID Control Evidence
P1 k non-decreasing after swap audit_invariant_tests::p1_k_non_decreasing_after_swap; fuzz tests
P7/P8 Factory-only pair admin; governance-only factory admin audit_invariant_tests::p7_*, p8_*
P10 Commission not taken on reverted swap commission_treasury_unchanged_after_*
R3/R4 minimum_receive; router hop balance delta router_coverage_tests; adversarial_token::router_*_dust
L17 Wrong-side book_start_hint cannot cross-drain escrow hybrid_wrong_side_book_start_hint_no_cross_escrow_drain
B1 Trading blacklist gates user paths blacklist_tests
H6/H7 Sanitized LCD 502; LCD-heavy rate limits indexer/tests/security.rs (code review; CI)
H8 max_maker_fills hard cap 100 indexer/src/hybrid_limits.rs
Ingestion Cursor not advanced on max-retry failure; reorg halt indexer/src/indexer/poller.rs:192-204; indexer/tests/indexer_ingestion_hardening.rs
Pagination Block tx fetch paginates until total satisfied indexer/src/lcd/mod.rs:257+; lcd::tests::get_block_txs_multi_page
Frontend submit BigInt slippage floor for min_received rawAmountMath.ts; SwapPage.test.tsx MAX_SAFE_INTEGER gate
IBC No IBC entry points in app wasm scripts/verify-no-ibc-hooks-in-contracts.sh
Secrets gitleaks on tracked files .gitlab-ci.yml gitleaks job

Issue #424 checklist (contracts focus)

Requested area Status
Test coverage Strong: 388+ integration tests, proptest in orderbook.rs + lib.rs fuzz modules
Fuzz testing 26 proptest cases in fuzz_tests, additional_fuzz_tests, wrap_fuzz_tests, orderbook::proptest_limits
Adversarial scenarios adversarial_token.rs, limit_order_tests, blacklist_tests, reentrancy_tests
Access control Extensive Unauthorized regressions across factory/pair/router/hooks
Liquidity / book drainage via looped trading k-invariant + security_tests::test_repeated_small_swaps_no_rounding_profit; book escrow checked math
Traders receiving excess funds Pool-favorable ceil_div; min_return enforced (C4); no execute>sim overpay tests failed
Rounding boundaries fee_math_property_tests, fuzz conservation tests
Boundary conditions Limit book caps (100 makers, 500 scan steps, 15 parks/swap), batch caps 100

Open follow-ups (from exploit-replay / launch track)

Issue Topic
#407 IBC-hooks chain attestation at deploy
#408 Governance emergency rehearsal
#410 Production deploy trace / wasm checksums
#391 Launch go/no-go sign-off

Conclusion

The codebase demonstrates above-average security engineering for a CosmWasm DEX: explicit invariant matrix, adversarial test harness, indexer hardening, and frontend submit-alignment guards. No Critical exploitable flaw was found in non-governance attack paths during this audit. Residual High items are governance/process and pause mobility, not missing access checks on swaps or escrow.

Full report path: audits/INTERNAL_COMPOSER_1782692388.md

# Internal Full-Stack Security Audit — GitLab #424 | Field | Value | |-------|-------| | **Issue** | [#424 — Contracts internal security audit](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/424) | | **Auditor** | Composer (internal) | | **Date (UTC)** | 2026-06-29 | | **Epoch** | `1782692388` | | **Baseline** | `main` @ workspace snapshot | | **Scope** | Full monorepo per issue: CosmWasm contracts, Terra messages, indexer/API/DB, frontend, deps, CI, secrets, ops | --- ## Executive summary CL8Y DEX is a **mature, defense-in-depth** Terra Classic DeFi stack. Under the documented trust model (honest governance, CW20 whitelist, treasury as sink), **no critical on-chain fund-drain or unauthorized-privilege bugs** were identified in this pass. Core economic invariants — k-monotonicity, pool-favorable rounding, escrow accounting, hook caller allowlists, router hop deltas, hybrid limit-book matching, and access control — are **extensively tested** (388 contract lib tests, 7 audit-invariant regressions, 21 `security_tests`, 26 fuzz/proptest cases; all passed locally). The highest residual risks are **operational and governance-centric**: single-key factory control, pause trapping limit escrow, absence of a third-party audit, and off-chain quote/indexer trust boundaries. Several historical gap items (LCD error leakage, router dust sweep, indexer cursor-on-error, tx pagination, `parseFloat` on `min_received`) are **fixed and regression-tested** in the current tree. **Launch recommendation:** acceptable for capped-TVL / pool-first rollout with documented controls; **not** a substitute for external audit + multisig governance + completed launch checklist items ([#391](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/391), [#407](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/407), [#408](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/408), [#410](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/410)). --- ## Areas analyzed | Area | Paths / surfaces | Focus | |------|------------------|-------| | **CosmWasm contracts** | `smartcontracts/contracts/{factory,pair,router,fee-discount,hooks/*}` | Access control, swap/LP math, limit book, hybrid matching, hooks, migration | | **Shared libs** | `smartcontracts/packages/dex-common` | `max_spread`, oracle, hook settlement, blacklist types | | **Integration / adversarial tests** | `smartcontracts/tests/src/{lib.rs,adversarial_token.rs,limit_order_tests.rs,blacklist_tests.rs,migration_tests.rs}` | Fuzz, reentrancy, fee-on-transfer, book hints, rounding | | **Indexer ingestion** | `indexer/src/indexer/{poller,block_indexer,parser,pair_discovery}.rs` | Cursor, reorg, pagination, dedup, emitter scoping | | **Indexer API** | `indexer/src/api/*`, `indexer/tests/security.rs` | SQL injection, CORS, rate limits, LCD amplification, error sanitization | | **Frontend** | `frontend-dapp/src/{pages,services,utils,hooks}` | Signing alignment, slippage math, CSP, trust boundaries | | **CI / supply chain** | `.gitlab-ci.yml`, `scripts/ci/gitleaks-*`, `smartcontracts/.cargo/audit.toml` | gitleaks, cargo-audit jobs, hook policy | | **Ops / docs** | `docs/{security-model,contracts-security-audit,exploit-replay-matrix}.md` | Invariant matrix, exploit replay, launch gates | | **External deps** | `frontend-dapp/package.json`, `smartcontracts/Cargo.toml` | npm/cargo advisories, forked `cosmes` | Additional areas beyond the issue text: **trading blacklist**, **batch limit placement/cancel**, **permissionless book clean**, **wrap-mapper/treasury harness**, **reorg webhook**, **CG/CMC aggregator compliance routes**. --- ## Methodology and test execution 1. Read issue #424 scope and repository security docs (`docs/contracts-security-audit.md`, `docs/security-model.md`, `docs/exploit-replay-matrix.md`, `gaps/GAP_1780200149.md`). 2. Static review of execute/query paths, escrow math, router state machine, indexer API layers, frontend submit pipeline. 3. Automated tests run locally: | Suite | Command | Result | |-------|---------|--------| | Contract units + dex-common | `cd smartcontracts && cargo test --lib` | **388 + 21 passed** | | Audit invariants (P1, P5, P7, P8, P10, H1) | `cargo test audit_invariant` | **7 passed** | | Security regressions | `cargo test security_tests` | **21 passed** | | Fuzz / proptest | `cargo test fuzz_tests` | **26 passed** | | Indexer lib | `cd indexer && cargo test --lib` | **152 passed** | | Indexer `security.rs` integration | Requires Postgres (`indexer/.env`); not run in this VM | Skipped — cases documented in `indexer/tests/security.rs` | 4. `npm audit --audit-level=high` on frontend (12 advisories, 2 high). 5. `cargo audit` not installed locally; **CI enforces** via `.gitlab-ci.yml` `cargo-audit-smartcontracts` / `cargo-audit-indexer`. --- ## Findings ### H-01 — Governance centralization (no on-chain timelock) | | | |---|---| | **Severity** | **High** (operational) | | **Location** | `smartcontracts/contracts/factory/src/contract.rs` (`ensure_governance`); `docs/security-model.md` | | **Issue** | A single `governance` address can change fees, register reverting hooks, pause pairs, redirect treasury, mutate CW20 whitelist, push discount registry, and manage trading blacklists — atomically and without delay. | | **Impact** | Compromised or malicious governance can halt trading, trap escrow (see H-02), drain via misconfigured hooks/fees, or blacklist users. Not exploitable by non-governance callers. | | **Reproduction** | Deploy factory with EOA governance; call `SetPairFee`, `RegisterHook`, `UpdateConfig` — all succeed from governance only (`audit_invariant_tests::p8_*`). | | **Recommendation** | Production **multisig/DAO** only; document signer policy; complete emergency rehearsal ([#408](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/408)); consider off-chain timelock for high-impact msgs. | ### H-02 — Emergency pause blocks all maker escrow exits | | | |---|---| | **Severity** | **High** (availability / funds mobility) | | **Location** | `smartcontracts/contracts/pair/src/contract.rs` — `assert_not_paused` on `Receive`, `CancelLimitOrder`, `ClaimExpiredLimitOrder`, `CleanLimitBook` (invariant **L6**) | | **Issue** | Governance pause intentionally freezes swaps **and** maker withdrawals (cancel, expired claim, book clean). Resting and parked escrow remains in pair custody until unpause. | | **Impact** | During incident response, limit makers cannot retrieve escrow; reputational and liquidity risk. Funds are not stolen but are **immobilized** by design. | | **Reproduction** | `limit_order_tests::pause_blocks_swap_and_place_cancel_refunds_escrow`; `claim_expired_limit_order_blocked_while_pair_paused_then_succeeds_after_unpause`. | | **Recommendation** | Document in user-facing incident FAQ (done); rehearse pause/unpause from planned multisig; consider whether post-incident **governance-only** escrow release is ever needed (product decision). | ### H-03 — No third-party security audit | | | |---|---| | **Severity** | **High** (process) | | **Location** | `docs/security-model.md`; `docs/contracts-security-audit.md` § Third-party audit | | **Issue** | In-repo invariant matrix is self-authored; no external firm attestation on mainnet deployment. | | **Impact** | Unknown classes of CosmWasm/DeFi bugs may remain despite strong internal testing. | | **Recommendation** | Commission audit before high TVL; track deploy trace ([#410](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/410)); keep exploit-replay matrix current ([#406](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/406)). | ### M-01 — Fee-discount cache TTL (300s) allows bounded discount leakage | | | |---|---| | **Severity** | **Medium** (economic, accepted) | | **Location** | `smartcontracts/contracts/pair/src/discount_cache.rs`; invariant **P9** | | **Issue** | Pair caches `(effective_fee_bps, discount)` for `DISCOUNT_CACHE_TTL_SECONDS` (300s). Trader who drops below tier CL8Y balance may retain cached discount until expiry. | | **Impact** | Protocol fee revenue loss bounded to **one 300s window per trader**; not exploitable for unlimited drain. | | **Reproduction** | Documented in `docs/contracts-security-audit.md` P9; `fee_discount_coverage_tests`. | | **Recommendation** | Accept as gas tradeoff ([#275](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/275)) or shorten TTL if CL8Y sink demand increases. | ### M-02 — Fee-discount registry outage charges full fee silently | | | |---|---| | **Severity** | **Medium** | | **Location** | `smartcontracts/contracts/pair/src/discount_cache.rs` — `Err(_) => (fee_bps, None)`; invariant **P5** | | **Issue** | When `GetDiscount` query fails (paused registry, migration, LCD error), swaps succeed at **full pair fee** with no on-chain revert. | | **Impact** | Tier holders overcharged during registry outages; opposite of discount theft. | | **Reproduction** | `audit_invariant_tests::swap_uses_full_fee_when_discount_registry_query_fails`; indexer `GET /api/v1/health/fee-discount`. | | **Recommendation** | Monitor registry health; dApp warning (non-blocking) when unhealthy; ops runbook for registry recovery. | ### M-03 — Registered reverting hook is a liveness kill-switch | | | |---|---| | **Severity** | **Medium** | | **Location** | `smartcontracts/contracts/pair/src/contract.rs` (post-swap `add_messages` to hooks); invariant **H1** | | **Issue** | Allowlisted hook that returns `Err` rolls back the entire swap atomically. | | **Impact** | Governance misconfiguration or compromised hook contract blocks all swaps on that pair. | | **Reproduction** | `audit_invariant_tests::swap_fails_atomically_when_allowlisted_hook_reverts`. | | **Recommendation** | Hook registration runbook + audit hooks pre-mainnet; test pause before hook rollback in incidents. | ### M-04 — Indexer `/health` is shallow (no DB/LCD readiness) | | | |---|---| | **Severity** | **Medium** (operational) | | **Location** | `indexer/src/api/mod.rs:368-370` | | **Issue** | `GET /health` returns `{"status":"ok"}` without probing Postgres or LCD. | | **Impact** | Load balancers may route traffic to a broken indexer; users see stale or empty data while infra appears healthy. | | **Reproduction** | `curl /health` on any running instance — always 200. | | **Recommendation** | Add `/ready` with DB ping + LCD probe; keep `/health` lightweight or document K8s probe split. | ### M-05 — Public Swagger UI / OpenAPI on indexer | | | |---|---| | **Severity** | **Medium** (info disclosure) | | **Location** | `indexer/src/api/mod.rs:499` — `SwaggerUi::new("/swagger-ui")` merged into production router | | **Issue** | Full API surface documented and browsable without authentication. | | **Impact** | Attackers enumerate LCD-heavy routes for DoS tuning; low direct exploit but aids reconnaissance. | | **Reproduction** | `indexer/tests/security.rs::swagger_ui_available`, `openapi_spec_available`. | | **Recommendation** | Gate Swagger behind auth or disable in `RUN_MODE=prod`; rate limits already mitigate abuse ([#363](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/363)). | ### M-06 — Production CSP allows `script-src 'unsafe-inline'` | | | |---|---| | **Severity** | **Medium** | | **Location** | `frontend-dapp/viteCsp.ts:50` | | **Issue** | Meta CSP includes `'unsafe-inline'` for scripts (Vite constraint). | | **Impact** | XSS via injected inline script is not blocked by CSP if another injection vector exists. | | **Reproduction** | Inspect built `index.html` CSP in production build. | | **Recommendation** | Migrate to nonce/hash-based CSP when Vite pipeline supports it; keep strict `connect-src` (already env-scoped). | ### M-07 — npm supply chain: 2 high-severity advisories | | | |---|---| | **Severity** | **Medium** | | **Location** | `frontend-dapp/package.json` — transitive `ws@7.x` (GHSA-96hv-2xvq-fx4p); `@goblinhunt/cosmes` → `@dao-dao/cosmiframe` chain | | **Issue** | `npm audit --audit-level=high` reports 12 vulns (2 high). | | **Impact** | `ws` DoS affects dev tooling paths; wallet stack deps increase patch drift risk. | | **Reproduction** | `cd frontend-dapp && npm audit --audit-level=high` | | **Recommendation** | Track cosmes fork updates; bump/ws override where possible; CI `npm audit` gate on high+ for production builds. | ### M-08 — Expired limit-order head clog can exhaust scan budget | | | |---|---| | **Severity** | **Medium** (griefing / availability) | | **Location** | `smartcontracts/contracts/pair/src/orderbook.rs` — `MAX_SCAN_STEPS` (500), `MAX_EXPIRED_PARKS_PER_SWAP` (15); invariant **L5**, **L17** | | **Issue** | Deep prefix of expired orders at book head can consume scan steps before live liquidity is reached; taker may get pool-only fill or revert on slippage. | | **Impact** | Hybrid matching degraded until `CleanLimitBook` or integrators pass `book_start_hint` past prefix ([#289](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/289)). Not a direct escrow drain — wrong-side hints blocked ([#272](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/272)). | | **Reproduction** | `limit_order_tests::hybrid_walk_scan_steps_cap_bounds_expired_prefix_and_spills_to_pool`; `hybrid_wrong_side_book_start_hint_no_cross_escrow_drain`. | | **Recommendation** | Operational monitoring of book head depth; encourage permissionless clean + integrator hints; trading swarm for head-clog scenarios. | ### M-09 — Off-chain route quotes are advisory (trust boundary) | | | |---|---| | **Severity** | **Medium** | | **Location** | `indexer/src/api/route_solver.rs`; `frontend-dapp/src/hooks/useSubmitAlignedSimQuote.ts`; `docs/security-model.md` | | **Issue** | Indexer `route/solve` and LCD simulations can diverge from execution snapshot; malicious/compromised indexer could suggest suboptimal or phished routes. | | **Impact** | User signs tx with worse economics if they skip on-chain `min_return` / `max_spread`; funds still protected when those fields set correctly. | | **Reproduction** | Compare indexer output vs on-chain `SimulateSwapOperations` after env compromise (thought experiment); mitigated by `assertSubmitQuotePayRawAligned` + BigInt `minReceived`. | | **Recommendation** | Pin HTTPS indexer; swap confirmation route row + factory preflight; never disable slippage floors on retail paths. | ### L-01 — Router inner hops omit per-hop deadline | | | |---|---| | **Severity** | **Low** | | **Location** | `smartcontracts/contracts/router/src/contract.rs:268,429` — `deadline: None` on inner `TerraSwap` | | **Issue** | Multi-hop router enforces user deadline on outer msg; inner pair hops use `deadline: None`. | | **Impact** | Minimal within single-tx atomicity; outer deadline still checked where applicable. | | **Recommendation** | Optional hardening: forward remaining block time as inner deadline. | ### L-02 — Fee-on-transfer CW20 desync if whitelist policy violated | | | |---|---| | **Severity** | **Low** (mitigated by policy) | | **Location** | Pair reserves credit declared amounts; `adversarial_token::fee_on_transfer_creates_reserve_imbalance` | | **Issue** | Whitelisted fee-on-transfer token breaks reserve invariant **P2**. | | **Impact** | Incorrect pricing; excess recoverable via governance sweep, not direct theft from LPs by arbitrary callers. | | **Recommendation** | Enforce [`docs/runbooks/cw20-whitelist-policy.md`](../docs/runbooks/cw20-whitelist-policy.md); run `scripts/verify-cw20-code-ids.sh` before `AddWhitelistedCodeId`. | ### L-03 — Display-layer float formatting for very large balances | | | |---|---| | **Severity** | **Low** | | **Location** | `frontend-dapp/src/utils/formatAmount.ts:129` (`parseFloat` in abbrev path) | | **Issue** | Submit path uses BigInt (`rawAmountMath.ts`); display abbreviations may round for amounts > 2⁵³. | | **Impact** | UI mis-display only; on-chain args unaffected. | | **Recommendation** | Extend BigInt-only formatting to abbrev branch for consistency. | --- ## Controls verified (no regression) | ID | Control | Evidence | |----|---------|----------| | **P1** | k non-decreasing after swap | `audit_invariant_tests::p1_k_non_decreasing_after_swap`; fuzz tests | | **P7/P8** | Factory-only pair admin; governance-only factory admin | `audit_invariant_tests::p7_*`, `p8_*` | | **P10** | Commission not taken on reverted swap | `commission_treasury_unchanged_after_*` | | **R3/R4** | `minimum_receive`; router hop balance delta | `router_coverage_tests`; `adversarial_token::router_*_dust` | | **L17** | Wrong-side `book_start_hint` cannot cross-drain escrow | `hybrid_wrong_side_book_start_hint_no_cross_escrow_drain` | | **B1** | Trading blacklist gates user paths | `blacklist_tests` | | **H6/H7** | Sanitized LCD 502; LCD-heavy rate limits | `indexer/tests/security.rs` (code review; CI) | | **H8** | `max_maker_fills` hard cap 100 | `indexer/src/hybrid_limits.rs` | | **Ingestion** | Cursor not advanced on max-retry failure; reorg halt | `indexer/src/indexer/poller.rs:192-204`; `indexer/tests/indexer_ingestion_hardening.rs` | | **Pagination** | Block tx fetch paginates until total satisfied | `indexer/src/lcd/mod.rs:257+`; `lcd::tests::get_block_txs_multi_page` | | **Frontend submit** | BigInt slippage floor for `min_received` | `rawAmountMath.ts`; `SwapPage.test.tsx` MAX_SAFE_INTEGER gate | | **IBC** | No IBC entry points in app wasm | `scripts/verify-no-ibc-hooks-in-contracts.sh` | | **Secrets** | gitleaks on tracked files | `.gitlab-ci.yml` `gitleaks` job | --- ## Issue #424 checklist (contracts focus) | Requested area | Status | |----------------|--------| | Test coverage | Strong: 388+ integration tests, proptest in `orderbook.rs` + `lib.rs` fuzz modules | | Fuzz testing | **26 proptest cases** in `fuzz_tests`, `additional_fuzz_tests`, `wrap_fuzz_tests`, `orderbook::proptest_limits` | | Adversarial scenarios | `adversarial_token.rs`, `limit_order_tests`, `blacklist_tests`, `reentrancy_tests` | | Access control | Extensive `Unauthorized` regressions across factory/pair/router/hooks | | Liquidity / book drainage via looped trading | k-invariant + `security_tests::test_repeated_small_swaps_no_rounding_profit`; book escrow checked math | | Traders receiving excess funds | Pool-favorable `ceil_div`; min_return enforced (**C4**); no execute>sim overpay tests failed | | Rounding boundaries | `fee_math_property_tests`, fuzz conservation tests | | Boundary conditions | Limit book caps (100 makers, 500 scan steps, 15 parks/swap), batch caps 100 | --- ## Open follow-ups (from exploit-replay / launch track) | Issue | Topic | |-------|-------| | [#407](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/407) | IBC-hooks chain attestation at deploy | | [#408](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/408) | Governance emergency rehearsal | | [#410](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/410) | Production deploy trace / wasm checksums | | [#391](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/391) | Launch go/no-go sign-off | --- ## Conclusion The codebase demonstrates **above-average security engineering** for a CosmWasm DEX: explicit invariant matrix, adversarial test harness, indexer hardening, and frontend submit-alignment guards. No **Critical** exploitable flaw was found in non-governance attack paths during this audit. Residual **High** items are governance/process and pause mobility, not missing access checks on swaps or escrow. Full report path: `audits/INTERNAL_COMPOSER_1782692388.md`
Brouie commented 2026-06-29 05:04:46 +00:00 (Migrated from gitlab.com)

Ran an independent pass on top of the Composer audit. Three things: re-ran every number the report cites, confirmed each cited test + source line actually backs its finding, then went looking for what the audit didn't cover.

Reproduced — all green

  • Contracts: cargo test --lib 388 + 21, audit_invariant 7, security_tests 21, fuzz_tests 26. Indexer cargo test --lib 152. Zero failures across the board, matches the report exactly.
  • The report skipped indexer/tests/security.rs ("requires Postgres, not run in this VM"). It IS runnable against the isolated dex_indexer_test DB (separate from the live one, flock-guarded) — ran it, 28/28 green, live indexer untouched. So H6/H7 (sanitized-502, LCD-heavy rate limit, no-internal-leak) have executed evidence now, not just code review.
  • Frontend npm audit: 12 vulns / 2 high, matches. One pin-down: the two highs are ws (GHSA-96hv-2xvq-fx4p) and undici — the second high is undici, not the cosmes/cosmiframe chain (that chain is moderate/low). Both clear on a non-breaking npm audit fix; uuid needs --force.
  • cargo-audit not installed locally; CI jobs cargo-audit-smartcontracts / cargo-audit-indexer present.

Evidence checks — 14 tests + 14 source lines, all confirmed

  • Every cited test exists and its body proves what it's cited for (P1 k-monotonic, P7/P8 unauth gates, P10 commission-not-taken-on-revert, M-02 full-fee-on-registry-fail, M-03 atomic hook revert, H-02 pause gates, M-08/L17 scan-cap + wrong-side-hint, L-02 FoT imbalance, repeated-small-swap conservation, router dust, blacklist, lcd pagination).
  • Every cited source location backs its finding. Only nits, nothing that moves a conclusion: M-01 TTL=300 and M-08 caps (500/15) actually live in dex-common/src/pair.rs (the cited files import them); /health is 368-369 not 368-370; P10 is the two commission_treasury_unchanged_after_* tests.

No-drain conclusion — concur, after reading the math myself

  • Pool output is ceil_div(k, new_input_reserve) at pair contract.rs:1090 → trader gross rounds DOWN (pool-favorable), and the swap independently reverts if new_k < k (1093-1107). Two layers on the only pool-output path; the limit-book legs never write reserves.
  • Router hops are balance-measured deltas gated by minimum_receive — no internal accumulator to inflate.
  • Limit escrow: an order sits in exactly one of ORDERS / EXPIRED_LIMIT_CLAIMS with a double-park guard, so cancel and claim-expired are mutually exclusive — no double-claim / over-withdraw.
  • Commission charged once; the sub-1-token floor remainder goes to the trader (bounded < 1 token/swap, never an LP drain).
  • No Critical/High economic finding the audit missed.

What the audit didn't cover

All gated (governance/owner) with working access checks + tests in tree, so none is a new unauthenticated drain — but these are fund-relevant surfaces the report never names, and the first few are worth explicit analysis before an external audit / launch sign-off:

  • (med) fee-discount TRUSTED_ROUTERS + the caller-supplied trader field — the anti-spoof hinge of the whole discount system. The pair forwards trader into the registry without itself checking the CW20 sender is a router; integrity rests entirely on the governance-set (no timelock) trusted-router allowlist. A wrong/compromised trusted router can attribute any tier to anyone (fee leakage). Not mentioned at all.
  • (med) router unwrap_output → external wrap-mapper/treasury contract — that contract custodies/unwraps user output and its code isn't in this repo (only types + a mock). The unwrap-leg trust boundary is unaudited.
  • (med) pair UpdateLimitOrderPrice — owners reprice a resting order with no re-charged placement fee and no re-check of the clean-config dust thresholds. Enables fee-free queue-jumping / book-head manipulation. Whole entry point unanalyzed.
  • (med) hooks' admin setters (UpdateConfig / UpdateAllowedPairs on burn / lp-burn / tax) — set tax recipient + percentage, burn knobs, LP target. A mis-set value silently re-routes value on every swap on an allowed pair. The audit only covered the caller-allowlist gate, not these.
  • (med, partial) M-05 swagger — the report's own fix (gate behind auth / disable in RUN_MODE=prod) isn't in the code. mod.rs:499 merges SwaggerUi unconditionally, and /api-docs/openapi.json is public on the running build right now. Reads as more-mitigated than it is.
  • (low/info) H-02 doesn't mention Sweep stays callable while paused (verified Sweep provably excludes escrow + reserves, so the availability-only rating holds — just a narrative gap); /ready still 404 in the running build (M-04 fix unimplemented); the #277 bounded-fanout setters (SetLpAdminAll/Batch etc.), permissionless IncreaseObservationCardinality, and the LP mint/burn path are uncited or thin.

Layer

Everything above is contract source + unit/integration tests + the live indexer API. The two frontend findings (M-06 CSP unsafe-inline, L-03 formatAmount abbrev) I only checked at source — a browser pass is separate.

Bottom line

The audit holds up on everything it covers — numbers reproduce, cited evidence is real, and the no-drain / no-overpay conclusion survives an independent read of the math. It's just not complete: the trusted-router/trader hinge and the external wrap-mapper are genuine fund-relevant trust boundaries it skips, on top of the reprice path and the hook admin setters. None is a new unauthenticated drain, but I'd want those closed (and the swagger gate actually landed) before this stands as the pre-external-audit baseline.

@PlasticDigits — flagging those four gaps + the swagger gate for your call. Happy to dig into any of them.

Ran an independent pass on top of the Composer audit. Three things: re-ran every number the report cites, confirmed each cited test + source line actually backs its finding, then went looking for what the audit didn't cover. ## Reproduced — all green - Contracts: `cargo test --lib` 388 + 21, `audit_invariant` 7, `security_tests` 21, `fuzz_tests` 26. Indexer `cargo test --lib` 152. Zero failures across the board, matches the report exactly. - The report skipped `indexer/tests/security.rs` ("requires Postgres, not run in this VM"). It IS runnable against the isolated `dex_indexer_test` DB (separate from the live one, flock-guarded) — ran it, 28/28 green, live indexer untouched. So H6/H7 (sanitized-502, LCD-heavy rate limit, no-internal-leak) have executed evidence now, not just code review. - Frontend `npm audit`: 12 vulns / 2 high, matches. One pin-down: the two highs are `ws` (GHSA-96hv-2xvq-fx4p) and `undici` — the second high is undici, not the cosmes/cosmiframe chain (that chain is moderate/low). Both clear on a non-breaking `npm audit fix`; uuid needs `--force`. - `cargo-audit` not installed locally; CI jobs `cargo-audit-smartcontracts` / `cargo-audit-indexer` present. ## Evidence checks — 14 tests + 14 source lines, all confirmed - Every cited test exists and its body proves what it's cited for (P1 k-monotonic, P7/P8 unauth gates, P10 commission-not-taken-on-revert, M-02 full-fee-on-registry-fail, M-03 atomic hook revert, H-02 pause gates, M-08/L17 scan-cap + wrong-side-hint, L-02 FoT imbalance, repeated-small-swap conservation, router dust, blacklist, lcd pagination). - Every cited source location backs its finding. Only nits, nothing that moves a conclusion: M-01 TTL=300 and M-08 caps (500/15) actually live in `dex-common/src/pair.rs` (the cited files import them); `/health` is 368-369 not 368-370; P10 is the two `commission_treasury_unchanged_after_*` tests. ## No-drain conclusion — concur, after reading the math myself - Pool output is `ceil_div(k, new_input_reserve)` at `pair contract.rs:1090` → trader gross rounds DOWN (pool-favorable), and the swap independently reverts if `new_k < k` (1093-1107). Two layers on the only pool-output path; the limit-book legs never write reserves. - Router hops are balance-measured deltas gated by `minimum_receive` — no internal accumulator to inflate. - Limit escrow: an order sits in exactly one of `ORDERS` / `EXPIRED_LIMIT_CLAIMS` with a double-park guard, so cancel and claim-expired are mutually exclusive — no double-claim / over-withdraw. - Commission charged once; the sub-1-token floor remainder goes to the trader (bounded < 1 token/swap, never an LP drain). - No Critical/High economic finding the audit missed. ## What the audit didn't cover All gated (governance/owner) with working access checks + tests in tree, so none is a new unauthenticated drain — but these are fund-relevant surfaces the report never names, and the first few are worth explicit analysis before an external audit / launch sign-off: - **(med) fee-discount `TRUSTED_ROUTERS` + the caller-supplied `trader` field** — the anti-spoof hinge of the whole discount system. The pair forwards `trader` into the registry without itself checking the CW20 sender is a router; integrity rests entirely on the governance-set (no timelock) trusted-router allowlist. A wrong/compromised trusted router can attribute any tier to anyone (fee leakage). Not mentioned at all. - **(med) router `unwrap_output` → external wrap-mapper/treasury contract** — that contract custodies/unwraps user output and its code isn't in this repo (only types + a mock). The unwrap-leg trust boundary is unaudited. - **(med) pair `UpdateLimitOrderPrice`** — owners reprice a resting order with no re-charged placement fee and no re-check of the clean-config dust thresholds. Enables fee-free queue-jumping / book-head manipulation. Whole entry point unanalyzed. - **(med) hooks' admin setters (`UpdateConfig` / `UpdateAllowedPairs` on burn / lp-burn / tax)** — set tax recipient + percentage, burn knobs, LP target. A mis-set value silently re-routes value on every swap on an allowed pair. The audit only covered the caller-allowlist gate, not these. - **(med, partial) M-05 swagger** — the report's own fix (gate behind auth / disable in `RUN_MODE=prod`) isn't in the code. `mod.rs:499` merges SwaggerUi unconditionally, and `/api-docs/openapi.json` is public on the running build right now. Reads as more-mitigated than it is. - **(low/info)** H-02 doesn't mention `Sweep` stays callable while paused (verified Sweep provably excludes escrow + reserves, so the availability-only rating holds — just a narrative gap); `/ready` still 404 in the running build (M-04 fix unimplemented); the #277 bounded-fanout setters (`SetLpAdminAll/Batch` etc.), permissionless `IncreaseObservationCardinality`, and the LP mint/burn path are uncited or thin. ## Layer Everything above is contract source + unit/integration tests + the live indexer API. The two frontend findings (M-06 CSP `unsafe-inline`, L-03 `formatAmount` abbrev) I only checked at source — a browser pass is separate. ## Bottom line The audit holds up on everything it covers — numbers reproduce, cited evidence is real, and the no-drain / no-overpay conclusion survives an independent read of the math. It's just not complete: the trusted-router/`trader` hinge and the external wrap-mapper are genuine fund-relevant trust boundaries it skips, on top of the reprice path and the hook admin setters. None is a new unauthenticated drain, but I'd want those closed (and the swagger gate actually landed) before this stands as the pre-external-audit baseline. @PlasticDigits — flagging those four gaps + the swagger gate for your call. Happy to dig into any of them.
Brouie commented 2026-06-29 05:17:54 +00:00 (Migrated from gitlab.com)

mentioned in issue #337

mentioned in issue #337
Brouie commented 2026-06-29 07:45:04 +00:00 (Migrated from gitlab.com)

Followed up on the four coverage gaps I flagged above — deep-dived each and tried to actually break them (cw-multi-test PoCs where it mattered). Bottom line: none is a no-privilege exploit, which backs up the no-drain read. Per-gap result, action items first.

Hook admin setters — needs a trust-level confirmation (low, but real)

One correction to my earlier note first: the percentages ARE bounded. tax/burn/lp *_percentage_bps all clamp > 10000 → InvalidBps at both instantiate AND update_config in all three hooks, so there's no >100% / over-levy path — my "mis-set percentage" worry was off.

What IS real: each hook's config.admin is a free-form address taken from InstantiateMsg.admin, NOT derived from factory governance. Registering a hook onto a pair is governance-only, but once registered the hook's params (recipient, bps, burn/lp targets) are controlled by that independent admin key with no further governance check. So the admin can point the tax recipient at itself and set bps to 10000 — siphoning up to 100% of the ask-token output of every swap on a registered pair (takers with a nonzero min_return are protected by the slippage revert; only a min_return=0 taker gets silently zeroed).

No unprivileged path — it needs the hook admin key. But if that key is weaker than governance, the blast radius is wide. Action: confirm on-chain each live hook's config.admin == factory governance / the multisig (query GetConfig). If it's a separate ops key, that's a real exposure — worth binding the hook admin to governance in code, or a tighter max-bps clamp so even the admin can't take the full output.

Router unwrap / wrap-mapper — minimum_receive doesn't cross the unwrap boundary (low correctness)

minimum_receive is asserted on the pre-unwrap wrapped-CW20 balance delta (router contract.rs:356-363), BEFORE the unwrap Send to the mapper is built — there's no post-unwrap re-assertion on what the user actually receives. So an honest mapper charging an unwrap fee, or returning the wrong denom, would silently shortchange the user below their declared floor.

Not an unprivileged exploit: the mapper is governance-set (SetWrapMapper, no per-call override on the hook), and a reverting mapper rolls the whole tx back atomically. But the slippage guarantee should be re-asserted on the final received denom/amount (cleanest as a reply_on_success branch rather than the current top-level add_message). And the live mapper's unwrap-direction fee_bps needs to be confirmed 0 (or accounted for) — its contract isn't in this repo, so it needs to be pinned + audited regardless.

Limit reprice (UpdateLimitOrderPrice) — escrow-safe, one fairness quirk (no funds)

Escrow is conserved exactly: relink_limit_order_price only changes price (the rate), never order.remaining or PENDING_ESCROW_*; fill always clamps cost <= order.remaining, so a maker can't be paid out more of the escrowed token than they hold, regardless of repricing. No dust / min-remaining bypass (reprice changes price, not remaining), no unbounded DoS (gas per move + existing match-walk caps).

One by-design quirk worth documenting: reprice keeps the order's original id, so it retains global arrival-time priority — an old low-id order can move to the front of a new price level for free, leapfrogging makers who arrived earlier at that level with higher ids. Fairness/UX, no fund movement. If you want exchange-style lose-priority-on-modify, assign a fresh id on reprice; otherwise it's a deliberate semantics choice. Either way worth a doc line + a Reprice op in the prop_escrow_dll fuzz to cover escrow/DLL conservation across reprice explicitly.

Fee-discount trusted-router / trader spoof — verified safe (no action)

Proved with a PoC: a normal caller doing cw20.Send(pair, Swap{trader: WHALE}) gets ZERO discount. The CW20 contract pins the real sender, the pair forwards THAT (not itself) as sender, and the registry's sender-fallback (fee-discount contract.rs:463-474) ignores the spoofed trader unless the forwarded sender is in TRUSTED_ROUTERS. Correctly defended; matches the existing test_query_discount_untrusted_router_falls_back_to_sender.

Net

The four gaps reduce to governance/owner-trust + coverage, not new unauthenticated drains — consistent with the audit's conclusion. The two worth acting on before external audit / launch are the hook-admin key trust level and the wrap-mapper slippage boundary; the reprice quirk is docs + fuzz; the trader spoof is clean.

@PlasticDigits — two asks: confirm the live hook config.admin keys == governance/multisig (not a separate ops key), and confirm the live wrap-mapper's unwrap fee_bps is 0 (+ pin/audit that contract, since it's out-of-repo). Happy to write the min_receive-across-unwrap re-assertion and the reprice fuzz extension if you want them.

Followed up on the four coverage gaps I flagged above — deep-dived each and tried to actually break them (cw-multi-test PoCs where it mattered). Bottom line: none is a no-privilege exploit, which backs up the no-drain read. Per-gap result, action items first. ## Hook admin setters — needs a trust-level confirmation (low, but real) One correction to my earlier note first: the percentages ARE bounded. tax/burn/lp `*_percentage_bps` all clamp `> 10000` → `InvalidBps` at both instantiate AND update_config in all three hooks, so there's no >100% / over-levy path — my "mis-set percentage" worry was off. What IS real: each hook's `config.admin` is a free-form address taken from `InstantiateMsg.admin`, NOT derived from factory governance. Registering a hook onto a pair is governance-only, but once registered the hook's params (recipient, bps, burn/lp targets) are controlled by that independent admin key with no further governance check. So the admin can point the tax `recipient` at itself and set bps to 10000 — siphoning up to 100% of the ask-token output of every swap on a registered pair (takers with a nonzero `min_return` are protected by the slippage revert; only a `min_return=0` taker gets silently zeroed). No unprivileged path — it needs the hook admin key. But if that key is weaker than governance, the blast radius is wide. Action: confirm on-chain each live hook's `config.admin == factory governance / the multisig` (query `GetConfig`). If it's a separate ops key, that's a real exposure — worth binding the hook admin to governance in code, or a tighter max-bps clamp so even the admin can't take the full output. ## Router unwrap / wrap-mapper — minimum_receive doesn't cross the unwrap boundary (low correctness) `minimum_receive` is asserted on the pre-unwrap wrapped-CW20 balance delta (`router contract.rs:356-363`), BEFORE the unwrap `Send` to the mapper is built — there's no post-unwrap re-assertion on what the user actually receives. So an honest mapper charging an unwrap fee, or returning the wrong denom, would silently shortchange the user below their declared floor. Not an unprivileged exploit: the mapper is governance-set (`SetWrapMapper`, no per-call override on the hook), and a reverting mapper rolls the whole tx back atomically. But the slippage guarantee should be re-asserted on the final received denom/amount (cleanest as a `reply_on_success` branch rather than the current top-level `add_message`). And the live mapper's unwrap-direction `fee_bps` needs to be confirmed 0 (or accounted for) — its contract isn't in this repo, so it needs to be pinned + audited regardless. ## Limit reprice (UpdateLimitOrderPrice) — escrow-safe, one fairness quirk (no funds) Escrow is conserved exactly: `relink_limit_order_price` only changes `price` (the rate), never `order.remaining` or `PENDING_ESCROW_*`; fill always clamps `cost <= order.remaining`, so a maker can't be paid out more of the escrowed token than they hold, regardless of repricing. No dust / min-remaining bypass (reprice changes price, not remaining), no unbounded DoS (gas per move + existing match-walk caps). One by-design quirk worth documenting: reprice keeps the order's original id, so it retains global arrival-time priority — an old low-id order can move to the front of a new price level for free, leapfrogging makers who arrived earlier at that level with higher ids. Fairness/UX, no fund movement. If you want exchange-style lose-priority-on-modify, assign a fresh id on reprice; otherwise it's a deliberate semantics choice. Either way worth a doc line + a `Reprice` op in the `prop_escrow_dll` fuzz to cover escrow/DLL conservation across reprice explicitly. ## Fee-discount trusted-router / trader spoof — verified safe (no action) Proved with a PoC: a normal caller doing `cw20.Send(pair, Swap{trader: WHALE})` gets ZERO discount. The CW20 contract pins the real sender, the pair forwards THAT (not itself) as `sender`, and the registry's sender-fallback (`fee-discount contract.rs:463-474`) ignores the spoofed `trader` unless the forwarded sender is in `TRUSTED_ROUTERS`. Correctly defended; matches the existing `test_query_discount_untrusted_router_falls_back_to_sender`. ## Net The four gaps reduce to governance/owner-trust + coverage, not new unauthenticated drains — consistent with the audit's conclusion. The two worth acting on before external audit / launch are the **hook-admin key trust level** and the **wrap-mapper slippage boundary**; the reprice quirk is docs + fuzz; the trader spoof is clean. @PlasticDigits — two asks: confirm the live hook `config.admin` keys == governance/multisig (not a separate ops key), and confirm the live wrap-mapper's unwrap `fee_bps` is 0 (+ pin/audit that contract, since it's out-of-repo). Happy to write the min_receive-across-unwrap re-assertion and the reprice fuzz extension if you want them.
PlasticDigits commented 2026-06-29 09:09:43 +00:00 (Migrated from gitlab.com)

config.admin keys should be governance/multisig: YES
unwrap: See https://gitlab.com/PlasticDigits/ust1-window, specifically under smartcontracts-terraclassic/contracts/cmm-native-swap. Likely needs to be brought in as depedency and as a prerequisitive for cl8y dex deployment @Brouie

config.admin keys should be governance/multisig: YES unwrap: See https://gitlab.com/PlasticDigits/ust1-window, specifically under smartcontracts-terraclassic/contracts/cmm-native-swap. Likely needs to be brought in as depedency and as a prerequisitive for cl8y dex deployment @Brouie
Brouie commented 2026-06-29 15:11:08 +00:00 (Migrated from gitlab.com)

On your two callouts @PlasticDigits — verified the current on-chain state for both.

  1. config.admin -> governance/multisig. Checked every privileged contract on the 8c56f4b8 deploy, both the config governance field AND the CosmWasm migrate-admin (code-upgrade authority):
  • factory: config governance = the 2-of-3 gov multisig (terra1ltjjf30..., the #397 rotation). migrate-admin = test1.
  • fee-discount: config governance = test1 (single key). migrate-admin = test1.
  • wrap-mapper: config governance = test1 (single key). migrate-admin = test1.
  • router: no governance setter (config immutable post-instantiate). migrate-admin = test1.
  • pair: governed via the factory. migrate-admin = test1.

So the rotation is only half done, two gaps for launch:

  • Only the factory got its config-governance moved to the multisig. fee-discount and wrap-mapper still answer to a single key (test1) for their privileged setters — tier / trusted-router config on the fee-discount, wrap limits / pause / fee on the wrap-mapper. Those want the same rotation.
  • Sharper one: EVERY contract's migrate-admin is still test1, including the factory. A single key can swap the bytecode of any contract regardless of who holds config-governance, so the multisig on the factory is undercut as long as test1 can migrate it. For mainnet that wants to be the multisig (or burned) on all five. The deploy path currently rotates only the factory config-gov, nothing else.
    This is the deploy/rotation step, not a QA fix — I'll re-verify all five answer to the multisig once it lands.
  1. unwrap / ust1-window. Read it — it's cmm-native-wrap in the tree (not -swap). Right shape for this: governance, per-tx + rolling-24h wrap limits, symmetric fee_bps, and an Unwrap{min_native_out} floor that burns the wrapped token and BankMsg::Sends native out. That min_native_out floor is exactly what's missing at the boundary I flagged in the audit pass — today the router's unwrap_output leg hands the final amount through the external wrap-mapper with no slippage floor re-asserted after the unwrap fee (router/src/contract.rs:355-363 asserts minimum_receive on the pre-unwrap balance delta, not post-unwrap). Bringing cmm-native-wrap in as the native-wrap dependency closes that — the unwrap leg carries its own min-out. Agree it should be a deployment prerequisite. The current live wrap-mapper (terra124tap...) is a lighter/different contract, so this is a swap-in, not a config tweak; once it's wired into the router I'll verify the unwrap leg honors min_native_out and the dex min_receive survives the wrap boundary end to end.

While here, two of the audit's recommended fixes are still NOT in the tree on 8c56f4b8, confirmed live:

  • M-05 swagger gate: indexer api/mod.rs:499 merges SwaggerUi unconditionally; /api-docs/openapi.json returns 200 unauth (no RUN_MODE gate).
  • M-04 /ready probe: health() (mod.rs:368) returns a static {"status":"ok"}; /ready is 404 — no DB+LCD readiness probe.

Net: the contract/indexer audit body is covered (Composer report + my independent repro pass, all suites green incl the security.rs the report skipped). The genuinely-open residuals are all dev/deploy actions, not QA-closeable here: the admin-key + migrate-admin rotation, the cmm-native-wrap import, and the M-04/M-05 fixes. Frontend SEC-E findings are tracked under #425-430 (working those next).

On your two callouts @PlasticDigits — verified the current on-chain state for both. 1) config.admin -> governance/multisig. Checked every privileged contract on the 8c56f4b8 deploy, both the config governance field AND the CosmWasm migrate-admin (code-upgrade authority): - factory: config governance = the 2-of-3 gov multisig (terra1ltjjf30..., the #397 rotation). migrate-admin = test1. - fee-discount: config governance = test1 (single key). migrate-admin = test1. - wrap-mapper: config governance = test1 (single key). migrate-admin = test1. - router: no governance setter (config immutable post-instantiate). migrate-admin = test1. - pair: governed via the factory. migrate-admin = test1. So the rotation is only half done, two gaps for launch: - Only the factory got its config-governance moved to the multisig. fee-discount and wrap-mapper still answer to a single key (test1) for their privileged setters — tier / trusted-router config on the fee-discount, wrap limits / pause / fee on the wrap-mapper. Those want the same rotation. - Sharper one: EVERY contract's migrate-admin is still test1, including the factory. A single key can swap the bytecode of any contract regardless of who holds config-governance, so the multisig on the factory is undercut as long as test1 can migrate it. For mainnet that wants to be the multisig (or burned) on all five. The deploy path currently rotates only the factory config-gov, nothing else. This is the deploy/rotation step, not a QA fix — I'll re-verify all five answer to the multisig once it lands. 2) unwrap / ust1-window. Read it — it's cmm-native-wrap in the tree (not -swap). Right shape for this: governance, per-tx + rolling-24h wrap limits, symmetric fee_bps, and an Unwrap{min_native_out} floor that burns the wrapped token and BankMsg::Sends native out. That min_native_out floor is exactly what's missing at the boundary I flagged in the audit pass — today the router's unwrap_output leg hands the final amount through the external wrap-mapper with no slippage floor re-asserted after the unwrap fee (router/src/contract.rs:355-363 asserts minimum_receive on the pre-unwrap balance delta, not post-unwrap). Bringing cmm-native-wrap in as the native-wrap dependency closes that — the unwrap leg carries its own min-out. Agree it should be a deployment prerequisite. The current live wrap-mapper (terra124tap...) is a lighter/different contract, so this is a swap-in, not a config tweak; once it's wired into the router I'll verify the unwrap leg honors min_native_out and the dex min_receive survives the wrap boundary end to end. While here, two of the audit's recommended fixes are still NOT in the tree on 8c56f4b8, confirmed live: - M-05 swagger gate: indexer api/mod.rs:499 merges SwaggerUi unconditionally; /api-docs/openapi.json returns 200 unauth (no RUN_MODE gate). - M-04 /ready probe: health() (mod.rs:368) returns a static {"status":"ok"}; /ready is 404 — no DB+LCD readiness probe. Net: the contract/indexer audit body is covered (Composer report + my independent repro pass, all suites green incl the security.rs the report skipped). The genuinely-open residuals are all dev/deploy actions, not QA-closeable here: the admin-key + migrate-admin rotation, the cmm-native-wrap import, and the M-04/M-05 fixes. Frontend SEC-E findings are tracked under #425-430 (working those next).
PlasticDigits commented 2026-06-30 02:29:20 +00:00 (Migrated from gitlab.com)

Verification — #424 Contracts internal security audit

Verifier: Cloud Agent (release verify)
Date (UTC): 2026-06-30
Baseline: main @ workspace HEAD (clean tree)

Summary

Re-ran the full contracts security audit verification matrix from the Composer audit (#424 comment) and Brouie's independent repro pass. All automated contract and indexer security suites pass. The in-repo audit artifact (docs/contracts-security-audit.md) and invariant matrix are present and doc-drift checks pass. No code or docs changes were required.

Verdict: PASS — internal contracts security audit is complete and regression-tested on current main.


Acceptance criteria → results

Item Result How verified
Contract unit + dex-common tests PASS cd smartcontracts && cargo test --lib → 388 + 21 passed
Audit invariant regressions (P1, P5, P7, P8, P10, H1) PASS cargo test audit_invariant → 7 passed
Security regressions PASS cargo test security_tests → 21 passed
Fuzz / proptest PASS cargo test fuzz_tests → 26 passed
Migration state preservation (SEC-C14) PASS cargo test migration_tests → 4 passed
Trading blacklist (SEC-B02) PASS cargo test blacklist_tests → 8 passed
Adversarial token / router / hook paths PASS cargo test adversarial_token → 8 passed
Full contract suite (make target) PASS make test-contracts → exit 0
Indexer lib PASS cd indexer && cargo test --lib → 158 passed
Indexer security.rs integration (skipped in original audit VM) PASS cargo test --test security -- --test-threads=1 → 35 passed (Postgres via make setup-indexer-postgres)
Exploit replay matrix doc drift PASS make check-exploit-replay-matrix-docs
Fee-discount tier doc alignment PASS make check-fee-discount-tier-docs
Invariant matrix + audit doc PASS docs/contracts-security-audit.md present; rows P1–C14, L1–L17, B1, W1, etc.
Access control / no unauthorized drain (governance-trusted model) PASS Covered by audit_invariant + security_tests + Brouie PoC review in issue thread
Rounding / k-monotonicity / no trader overpay PASS fuzz_tests + security_tests (test_repeated_small_swaps_no_rounding_profit, k props)
Order-book / hybrid escrow invariants PASS limit_order_tests in full suite; L1–L17 in matrix
Frontend npm high advisories INFO npm audit --audit-level=high → 2 high (ws via cosmes fork); tracked in original audit; not a contracts blocker

Residual risks (documented follow-ups — not QA failures)

These were flagged in the issue thread as dev/deploy actions, outside the scope of closing the in-repo audit:

  1. Governance / migrate-admin rotation — factory config-gov on multisig; fee-discount, wrap-mapper, and all migrate-admin keys still on single key on live deploy (Brouie on-chain check). Tracked for launch (#397).
  2. Wrap-mapper slippage boundary — router minimum_receive asserted pre-unwrap; cmm-native-wrap from ust1-window recommended as deployment prerequisite (PlasticDigits confirmed).
  3. M-04 /ready probe — health() returns static {"status":"ok"}; no DB+LCD readiness route yet.
  4. M-05 Swagger gate — SwaggerUi merged unconditionally in indexer/src/api/mod.rs:499; no RUN_MODE gate in current tree.
  5. Third-party audit — recommended before high TVL (#391, #407).
  6. Frontend SEC-E findings — tracked under #425–#430 per Brouie.

Follow-up ideas

  • Wire cmm-native-wrap as router unwrap dependency and add end-to-end min_native_out regression across the wrap boundary.
  • Land M-04/M-05 indexer hardening before public API exposure.
  • Complete multisig rotation for fee-discount, wrap-mapper config-gov, and all migrate-admin keys.
  • Add Reprice op to prop_escrow_dll fuzz (Brouie suggestion) and document limit reprice priority semantics.

Closing as verified — no MR opened (working tree clean).

## Verification — #424 Contracts internal security audit **Verifier:** Cloud Agent (release verify) **Date (UTC):** 2026-06-30 **Baseline:** `main` @ workspace HEAD (clean tree) ### Summary Re-ran the full contracts security audit verification matrix from the Composer audit ([#424 comment](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/424)) and Brouie's independent repro pass. **All automated contract and indexer security suites pass.** The in-repo audit artifact (`docs/contracts-security-audit.md`) and invariant matrix are present and doc-drift checks pass. No code or docs changes were required. **Verdict: PASS** — internal contracts security audit is complete and regression-tested on current `main`. --- ### Acceptance criteria → results | Item | Result | How verified | |------|--------|--------------| | Contract unit + dex-common tests | **PASS** | `cd smartcontracts && cargo test --lib` → **388 + 21 passed** | | Audit invariant regressions (P1, P5, P7, P8, P10, H1) | **PASS** | `cargo test audit_invariant` → **7 passed** | | Security regressions | **PASS** | `cargo test security_tests` → **21 passed** | | Fuzz / proptest | **PASS** | `cargo test fuzz_tests` → **26 passed** | | Migration state preservation (SEC-C14) | **PASS** | `cargo test migration_tests` → **4 passed** | | Trading blacklist (SEC-B02) | **PASS** | `cargo test blacklist_tests` → **8 passed** | | Adversarial token / router / hook paths | **PASS** | `cargo test adversarial_token` → **8 passed** | | Full contract suite (make target) | **PASS** | `make test-contracts` → exit 0 | | Indexer lib | **PASS** | `cd indexer && cargo test --lib` → **158 passed** | | Indexer `security.rs` integration (skipped in original audit VM) | **PASS** | `cargo test --test security -- --test-threads=1` → **35 passed** (Postgres via `make setup-indexer-postgres`) | | Exploit replay matrix doc drift | **PASS** | `make check-exploit-replay-matrix-docs` | | Fee-discount tier doc alignment | **PASS** | `make check-fee-discount-tier-docs` | | Invariant matrix + audit doc | **PASS** | `docs/contracts-security-audit.md` present; rows P1–C14, L1–L17, B1, W1, etc. | | Access control / no unauthorized drain (governance-trusted model) | **PASS** | Covered by audit_invariant + security_tests + Brouie PoC review in issue thread | | Rounding / k-monotonicity / no trader overpay | **PASS** | fuzz_tests + security_tests (`test_repeated_small_swaps_no_rounding_profit`, k props) | | Order-book / hybrid escrow invariants | **PASS** | limit_order_tests in full suite; L1–L17 in matrix | | Frontend npm high advisories | **INFO** | `npm audit --audit-level=high` → 2 high (`ws` via cosmes fork); tracked in original audit; not a contracts blocker | --- ### Residual risks (documented follow-ups — not QA failures) These were flagged in the issue thread as **dev/deploy actions**, outside the scope of closing the in-repo audit: 1. **Governance / migrate-admin rotation** — factory config-gov on multisig; fee-discount, wrap-mapper, and all migrate-admin keys still on single key on live deploy (Brouie on-chain check). Tracked for launch ([#397](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/397)). 2. **Wrap-mapper slippage boundary** — router `minimum_receive` asserted pre-unwrap; `cmm-native-wrap` from [ust1-window](https://gitlab.com/PlasticDigits/ust1-window) recommended as deployment prerequisite (PlasticDigits confirmed). 3. **M-04 `/ready` probe** — `health()` returns static `{"status":"ok"}`; no DB+LCD readiness route yet. 4. **M-05 Swagger gate** — `SwaggerUi` merged unconditionally in `indexer/src/api/mod.rs:499`; no `RUN_MODE` gate in current tree. 5. **Third-party audit** — recommended before high TVL ([#391](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/391), [#407](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/407)). 6. **Frontend SEC-E findings** — tracked under #425–#430 per Brouie. --- ### Follow-up ideas - Wire `cmm-native-wrap` as router unwrap dependency and add end-to-end `min_native_out` regression across the wrap boundary. - Land M-04/M-05 indexer hardening before public API exposure. - Complete multisig rotation for fee-discount, wrap-mapper config-gov, and all migrate-admin keys. - Add `Reprice` op to `prop_escrow_dll` fuzz (Brouie suggestion) and document limit reprice priority semantics. Closing as verified — no MR opened (working tree clean).
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-06-30 02:29:26 +00:00
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#424
No description provided.