Security[SEC-I01 H07]: swap signing modal does not display pair contract address[SEC-I02] #449
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#449
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?
Summary
The swap signing modal shows the user the offer token symbol, receive token symbol, amount, max_spread, and min_return but does not show the pair contract address. The frontend derives which pair contract to call from the indexer's router_operations response. A malicious or compromised indexer (via misconfigured VITE_INDEXER_URL, DNS hijack, or compromised build env) can return the correct token symbols but route the swap through an attacker-controlled pair contract. The user has no way to verify the actual pair contract address before signing.
Impact is bounded by min_return: if the attacker pair returns fewer tokens than min_return, the transaction reverts. However, an attacker pair can skim an amount within slippage tolerance and the user sees a successful swap below expected return with no indication that a substitute pair was used.
What Was Checked
frontend-dapp/src/components/swap/SwapPreSubmitSummary.tsxlines 48, 70: signing modal renders token symbols only (${offerSymbol} -> ${receiveSymbol}). No pair contract address field rendered anywhere in the summary.frontend-dapp/src/pages/SwapPage.tsxline ~531: frontend validates token_in == fromToken and token_out == toToken from indexer response only. Pair contract addresses in router_operations are not validated against a known-good factory pair registry before submission.docs/operator-secrets.md: notes VITE_INDEXER_URL must be HTTPS-only and operator-controlled but does not describe a UI-level mitigation.Expected (per checklist)
The signing modal displays the pair contract address (or at minimum the truncated address with a copy/explorer link) alongside the token symbols so the user can verify the actual contract before signing. Maps to SEC-E07 (route display aligns with actual submit operations).
Actual
Pair contract address is absent from the signing modal. Only token symbols are shown.
Suggested Fix
Add a pair contract address row to SwapPreSubmitSummary.tsx. For multihop routes, show the first and last pair addresses or a collapsed "N hops via [addr1]...[addrN]" indicator. Add a factory registry validation step in SwapPage.tsx that cross-checks each pair address in router_operations against the factory's registered pairs before enriching and submitting, rejecting the route if any pair is not factory-registered.
Verification Checklist
Cc: @PlasticDigits
mentioned in issue #450
mentioned in issue #381
mentioned in merge request !987
Dug into this one at the source level before changing anything, and the stated threat model doesn't actually hold for our code — the frontend never submits to an indexer-provided pair contract address. Walking the real data flow:
directPair.contract_addr.directPairispairs.find(...)over thepairslist (SwapPage.tsx:237), and that list comes fromgetAllPairsPaginated()->queryContract(FACTORY_CONTRACT_ADDRESS, {pairs:{...}})(factory.ts:32-54), which hits the chain LCD (queries.ts->TERRA_LCD_URL/cosmwasm/wasm/v1/...), not the indexer. The factory address isVITE_FACTORY_ADDRESS(constants.ts:3). So the pair we sign against is the on-chain factory registry, already the trust anchor.ROUTER_CONTRACT_ADDRESS(VITE_ROUTER_ADDRESS, constants.ts:4) viaexecuteMultiHopSwap(router.ts:96-98). The router resolves each pair from the factory on-chain; the frontend never supplies a pair address.router_operationscarry only tokenoffer_asset_info/ask_asset_info— no pair contract addresses for the frontend to be tricked into.So a malicious indexer can't substitute the pair contract we submit to: the submit target is either the factory-sourced direct pair (LCD) or the fixed router env address. The one thing the indexer does control on the route — the intermediate token path — I closed under #450 (
swapRouteIntermediateTokensAlignednow cross-checks the displayed path against the submitted ops).What's left of this issue is genuine but lower-stakes: a UI transparency improvement — show the pair contract address (direct) or the router address + resolved hop pairs (multihop) in the signing modal so a user can eyeball it. Since the address is already chain-sourced, that's defense-in-depth/legibility, not a validation gap, and it's a browser/UI change that I'd verify on the laptop pass.
Leaving this open. @PlasticDigits — want me to add the pair-address row to the signing modal as a transparency thing, or is the chain-sourced guarantee + #450 cross-check enough to downgrade/close this? Not closing it from my side either way.
Must minimize user cognitive overload
mentioned in commit
2585a19362mentioned in merge request !991
mentioned in commit
633628a491mentioned in commit
4dad91d80dmentioned in commit
37b005bd6aVerification — #449 (SEC-I01 H07 / SEC-I02)
Verified on
main@772a7dec(includes merge37b005bd/fix/449-swap-signing-pair-contract).Checklist
SwapPreSubmitSummaryrendersswap-confirm-pair-contracts/swap-confirm-pair-contractviaAddressRow(truncate, copy, explorer). Unit test:SwapPreSubmitSummary.test.tsx— renders factory-sourced pair contract address for direct swaps (#449 / SEC-I02). Wired on/swap(SwapPage.tsx→pairContractAddresses={swapBlacklistProbe.pairAddresses}) and Trade market tab (TradeMarketOrderPanel.tsx).swap-confirm-hop-pair-{n}rows with→separators. Unit test:SwapPreSubmitSummary.test.tsx— renders hop pair contract addresses for multihop routes (#449). Resolution:resolveSwapRoutePairAddresses.tsmaps eachrouter_operationshop to a factory LCDpairsentry.directPair.contract_addrfrom LCD factorygetAllPairsPaginated(); multihop sends to fixedROUTER_CONTRACT_ADDRESSwith token-only ops — router resolves pairs on-chain.docs/security-model.mddocuments this trust boundary. Optional LCDgetPair()preflight exists inswapRoutePreflight.tsfor spread simulation, not a separate submit gate. Displayed pair addresses are factory-sourced for transparency only.npm run test:run -- src/components/swap/__tests__/SwapPreSubmitSummary.test.tsx src/utils/__tests__/resolveSwapRoutePairAddresses.test.ts→ 8/8 passed. Broader signing regression:SwapPage.test.tsx+TradeMarketOrderPanel.submitSnapshot.test.tsx→ 43/43 passed.Commands
Notes
docs/frontend.md,docs/security-model.md,skills/AGENTS_FRONTEND_SWAP_SIGNING_CONFIRMATION.md.Closing as verified on
main.mentioned in issue #471
mentioned in commit
9ff4ea7b60mentioned in merge request !1007
mentioned in issue #541