Full security report #376
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#376
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
CL8Y DEX — Full-Stack Security Audit Report
Issue: #376 — Full security report
Auditor: Internal Composer security audit (Cloud Agent)
Date: 2026-06-13
Scope: Full codebase — CosmWasm contracts, Terra messages, indexer API/DB, frontend dApp, CI/CD, ops scripts, dependencies, secrets handling
Method: Source review, pattern search, adversarial contract test execution, cross-reference with in-repo invariant matrix (
docs/contracts-security-audit.md,docs/indexer-invariants.md)Executive Summary
CL8Y DEX is a Terra Classic constant-product AMM with hybrid limit-order book, multi-hop router, fee-discount registry, and post-swap hooks. The codebase demonstrates mature defensive engineering: checked arithmetic, governance/factory access control, escrow accounting, slippage guards, indexer SQL parameterization, prod config gates, and extensive regression tests (including adversarial token scenarios).
No Critical permissionless on-chain exploit was identified under the documented trust model (honest governance, whitelisted CW20 code IDs). Residual risk concentrates in:
VITE_*config)Tests executed during audit:
cargo test adversarial(smartcontracts) — 7/7 passedtests/security.rs— 25+ cases exist (require Postgres; not executed in this session due to compile/runtime prerequisites)Areas Analyzed
.gitlab-ci.yml, reference GitHub workflows, gitleaks hooksCargo.lock,package-lock.json, forked cosmes, elliptic transitive.env.example, dev mnemonics,GITLAB_TOKENhandlingFindings
HIGH
H-01 — Fee-on-transfer CW20 can desync reserves vs on-chain balance
smartcontracts/contracts/pair/src/contract.rs—execute_swap,execute_provide_liquidity;factory/src/contract.rs—execute_create_paircw20_msg.amount, not post-transfer balance deltas. Fee-on-transfer tokens credit the pair less than recorded reserves.RESERVESexceed CW20 balance. Later withdrawals fail or last LPs absorb deficit.min_assetspasses.fee_on_transfer_creates_reserve_imbalanceinsmartcontracts/tests/src/adversarial_token.rs; invariant P2 indocs/contracts-security-audit.md.H-02 — Allowlisted hook returning
Erratomically blocks all pair swapspair/src/contract.rs—execute_swap(~L1174–1224);execute_update_hooksWasmMsg::Execute(notSubMsg::reply_on_error). Hook failure reverts entire swap.Hookhandler returnsErr→ every swap fails.swap_fails_atomically_when_allowlisted_hook_reverts.SubMsg::reply_on_errorif policy hooks must never block swaps.H-03 — LP burn hook: allowlisted non-pair caller can spoof
AfterSwap.pairhooks/lp-burn-hook/src/contract.rs—execute_after_swap,assert_allowed_pairinfo.sender ∈ ALLOWED_PAIRS.pairfield inAfterSwapis trusted for burn calculation but not verified on-chain.ALLOWED_PAIRS→ spoofer callsHookwith fake largeoutput_amount→ hook burnsmin(target_burn, balance)LP.lp_burn_hook_accepts_spoofed_pair_when_spoofer_allowlisted(adversarial_token.rs).ALLOWED_PAIRSto verified pair addresses only. Verifypair == info.senderor query pair state before burning.H-04 — Compromised or malicious indexer can influence signed swap routes
frontend-dapp/src/pages/SwapPage.tsx;src/services/indexer/routeOperations.ts;src/services/indexer/client.tsrouter_operationscome from indexer. dApp validatestoken_in/token_outagainst UI selection but trusts hop structure from indexer after parsing.VITE_INDEXER_URL) returns routes through adversarial but valid pools; user signs degraded execution.router_operations→ user signsexecute_swap_operations.enrichSwapOperationsWithHopMinReturnsresolves pairs via factorygetPair()— invalid pairs fail, but valid adversarial pools remain possible.H-05 — Dev mnemonic can ship if build mode is misconfigured
frontend-dapp/vite.config.ts(L52–59);src/services/terraclassic/devWallet.tsVITE_DEV_MNEMONICblocked only forcommand === 'build' && mode === 'production'. Non-production build deployed publicly withVITE_DEV_MODE=trueinlines mnemonic.vite build --mode stagingwith.env.developmentcontaining mnemonic → attacker extracts from bundle.devWallet.tshas no default mnemonic.VITE_DEV_MNEMONICset unlessmode === 'development'or explicitVITE_ALLOW_DEV_MNEMONIC=local-only.H-06 — No dependency / SCA automation in GitLab CI
.gitlab-ci.yml— 2 build jobs only; nocargo-audit,cargo-deny,npm audit, gitleaksellipticin cosmjs) persist undetected.cargo audit+npm audit --audit-level=high.H-07 — Cloud Agent VM overly permissive privilege model
gch-cloud-setup.sh(NOPASSWD:ALL,approvalMode: "unrestricted");scripts/lib/cloud-agent-docker.sh(chmod 666on docker.sock)GITLAB_TOKEN.make dev→ docker escape or sudo → readGITLAB_TOKEN.NOPASSWD:ALL; use docker group only; tighten Cursor approval mode; avoidchmod 666on docker.sock.MEDIUM
M-01 — Fee-discount cache: up to 300s fee leakage after CL8Y balance drop
pair/src/discount_cache.rs;dex-common/src/pair.rs—DISCOUNT_CACHE_TTL_SECONDS = 300(effective_fee_bps, discount)for 300s without re-querying registry balance.M-02 — TWAP oracle manipulable over multi-block / low-liquidity windows
packages/dex-common/src/oracle.rs;pair/src/contract.rs—oracle_updateM-03 — Emergency pause freezes maker limit-order withdrawals
pair/src/contract.rs—assert_not_pausedon cancel/claim/cleanCancelLimitOrder,ClaimExpiredLimitOrder,CleanLimitBook. Escrow remains in pair custody.M-04 — Router
SWAP_STATEserializes all multi-hop swapsrouter/src/contract.rs—execute_swap_operations,reply_swap_hopSWAP_STATE; concurrent router swaps in same block fail withSwapInProgress.M-05 — Rate limiting disabled in dev when env vars are zero
indexer/src/config.rs;indexer/src/api/mod.rs—apply_rate_limit_layerRATE_LIMIT_RPS=0andRATE_LIMIT_LCD_HEAVY_RPS=0, governors skipped. Prod clamps zeros to 60/10; dev does not./api/v1/route/solvefrom one IP if prod runs withoutRUN_MODE=prod.RUN_MODE=prodin production; startup warning when both limits are 0.M-06 — Unbounded
max_maker_fillson GET route solve (LCD path)indexer/src/api/route_solver.rsL719 —max_maker_fills.max(1)without upper capMAX_MAKER_FILLS_HARD_CAP(30); LCDglobal_v3path forwards uncapped value to on-chainHybridSimulation.GET /api/v1/route/solve?...&max_maker_fills=4294967295within LCD-heavy rate limit (10 RPS prod).max_maker_fillstoMAX_MAKER_FILLS_HARD_CAP(30).M-07 — CSP allows
unsafe-inlinescripts and arbitrary HTTPS connectionsfrontend-dapp/index.htmlL18–19script-src 'self' 'unsafe-inline'andconnect-src … https: wss:are broad.connect-srcto known LCD/RPC/indexer hosts.M-08 — Build-time contract addresses fully trusted
frontend-dapp/src/utils/constants.ts;src/services/terraclassic/router.tsVITE_ROUTER_ADDRESS,VITE_FACTORY_ADDRESScompiled into bundle with public-node fallbacks.M-09 — Indexer-supplied token logos and symbols (display phishing)
frontend-dapp/src/hooks/useTokenDisplayInfo.ts;src/components/ui/TokenLogo.tsxlogo_urlfrom indexer used as<img src>. CW20 metadata cached inlocalStoragewithout integrity checks.terra1…address prominently; allowlist logo hosts.M-10 — Default WalletConnect project ID in client bundle
frontend-dapp/src/services/terraclassic/wallet.tsL37VITE_WC_PROJECT_ID || '2ce7811b869be33ffad28cff05c93c15'— shared default if unset.VITE_WC_PROJECT_IDin production; fail build if missing.M-11 — GitLab CI Docker-in-Docker without TLS
.gitlab-ci.ymlL18–20:DOCKER_TLS_CERTDIR: "",DOCKER_HOST: tcp://docker:2375M-12 —
curl | bashbootstrap in agent toolingscripts/lib/cloud-agent-toolchain.sh(nvm, rustup);gch-cloud-setup.shGITLAB_TOKEN.M-13 — Gitleaks optional locally; absent from CI
.githooks/pre-commitL48–56;.gitlab-ci.ymlgitleaksnot installed; CI does not run gitleaks.gitleaks detectto CI pipeline; fail on findings.M-14 — Expert mode allows swaps with extreme slippage (up to 50%)
frontend-dapp/src/stores/dex.ts;src/pages/SwapPage.tsxmin_returnstill applied on submit.LOW
L-01 — Hybrid book walk caps can defer limit fills (head-clog / expired prefix)
| Location |
pair/src/orderbook.rs—MAX_SCAN_STEPS = 500,MAX_EXPIRED_PARKS_PER_SWAP = 15|| Impact | Suboptimal execution, not direct theft. Mitigated by
book_start_hintandCleanLimitBook. |L-02 — Factory
SetPairLimitBatchMaxomits pair registry check| Location |
factory/src/contract.rs—execute_set_pair_limit_batch_max|| Impact | Wasted gas only; no privilege escalation. |
L-03 — Blacklist silently disabled against pre-1.5.0 factories
| Location |
pair/src/blacklist_guard.rs—probe_factory_blacklist|| Impact | Blacklist ineffective until factory upgraded. |
L-04 — LCD upstream details logged at WARN
| Location |
indexer/src/lcd/mod.rs|| Impact | Log aggregation may retain internal LCD hostnames and chain error text. Client responses sanitized. |
L-05 —
blacklist-checkusesinternal_errfor LCD failures| Location |
indexer/src/api/compliance.rs:86|| Impact | Misleading 500 vs 502 status codes; body still generic. |
L-06 — Unbounded
tokens/pairslist in blacklist-check| Location |
indexer/src/api/compliance.rs|| Impact | Large query strings → big LCD payload, memory pressure. |
L-07 — Swagger UI / OpenAPI publicly exposed
| Location |
indexer/src/api/mod.rs|| Impact | Full API surface enumeration. Disable in prod or gate at reverse proxy. |
L-08 — No explicit HTTP request body size limit on POST route solve
| Location |
indexer/src/api/mod.rs|| Impact | Oversized
hybrid_by_hopJSON could consume memory. AddRequestBodyLimitLayer. |L-09 — Indexer API path segments not URL-encoded
| Location |
frontend-dapp/src/services/indexer/client.ts—getPair,getTrader|| Impact | Odd characters in route params could produce unexpected requests. |
L-10 — Weak default Postgres credentials (local)
| Location |
docker-compose.yml,.env.example|| Impact | Low on
127.0.0.1:5432; high if Postgres bound to0.0.0.0. |L-11 — LocalTerra test mnemonic committed and echoed
| Location |
docker/init-chain.sh|| Impact | None on LocalTerra. Critical if reused on funded networks. |
L-12 — npm audit: 23 advisories (2 critical, 10 high — mostly dev tooling)
| Location |
frontend-dapp/package-lock.json|| Impact | Runtime wallet stack inherits transitive
ellipticrisk. Track cosmes/cosmjs upgrades. |INFORMATIONAL
I-01 — No API authentication on indexer (by design)
Public read-only analytics API. Mitigate with network ACLs, reverse proxy,
API_BIND=127.0.0.1.I-02 — Tax/burn hooks spend pre-funded hook balances, not swap output
Hooks compute from
return_asset.amountbut transfer/burn from hook contract balance. Users receive full swap output; tax subsidized from hook treasury.I-03 — Wasm migration authority outside contract logic
Migrate authorization is chain-level wasm admin, not in-contract. Compromised migration key can alter live bytecode.
I-04 — Route solve cache: amount bucketing + tier-shared keys
Cache buckets amounts by
1_000_000; trader address not keyed — onlydiscount_bps. By design for LCD cost reduction (#283). Not privilege escalation.Positive Security Controls
On-chain
ensure_governance)assert_allowed_pair)Uint128checked ops)traderfalls back to sender)CreatePairper blockIndexer
internal_err,lcd_gateway_err)RUN_MODE=prodrequires operator LCDs, non-empty CORS, rate limit floor)tests/security.rsdiscount_bpsin key (#283)API_BIND=127.0.0.1Frontend
dangerouslySetInnerHTML/evalin application codepreferNoSetFee)sanitizeOpaqueErrorMessagecaps error lengthOps / CI
127.0.0.1; images pinned by digest.gitignorecovers.env, credentials.gitleaks.toml); pre-commit when installedglabsetup strips credentials from origin URLdocs/operator-secrets.md,docs/contracts-security-audit.mdAttack Paths Considered (Non-Governance)
Unauthorized)min_assetson withdrawwithdraw_min_assets_reverts_on_sandwich)Priority Remediation Roadmap
Immediate
npm audit/cargo auditto.gitlab-ci.ymlmax_maker_fillsto 30 on indexer route solveShort term
RUN_MODE=prodin deployment checklistOngoing
ellipticVITE_WC_PROJECT_IDin prod buildsReferences
docs/contracts-security-audit.md— invariant matrix P1–L17docs/security-model.md— operational trust assumptionsdocs/indexer-invariants.md— indexer threat modeldocs/audits/factory-treasury-bank-send.md— bank send review (#313)docs/operator-secrets.md— secrets handling guidancesmartcontracts/tests/src/adversarial_token.rs— adversarial test harnessThis report supplements but does not replace a formal third-party smart contract audit before high-TVL deployment.
Approve
Rejected
mentioned in issue #377
mentioned in issue #378
mentioned in issue #379
mentioned in issue #380
Child issues opened from security audit triage
Approved items from the #376 comment (2026-06-13) were bundled into 4 actionable issues:
Bundle rationale
Skipped — rejected
SetPairLimitBatchMax— gas waste onlySkipped — approved with scope exclusions (no separate issue)
These were approved but folded into bundle constraints rather than standalone issues:
Skipped — undecided / requested
None — all audit items had explicit approve or reject in the triage comment.
Opened by agent workflow from
agent:open_issueson #376.mentioned in merge request !901
mentioned in commit
b45ac0aea6mentioned in merge request !902
mentioned in merge request !903
mentioned in merge request !904
mentioned in merge request !905
mentioned in merge request !906
mentioned in commit
a776b93d50mentioned in merge request !908
mentioned in merge request !909
mentioned in merge request !910
mentioned in issue #337
mentioned in issue #542
marked as related to #542