This guide defines security requirements and safe implementation rules.
Core references in this repo:
- Swap flow + validations:
SwapPage - Wallet integration:
EvmWalletService - Wallet UI:
WalletButtonComponent - Backend API client:
BridgeApiService - Backend-provided chain config shape:
BridgeChainConfig
The frontend must assume:
- The browser is a hostile environment (extensions, injected scripts, malicious iframes).
- Users can be tricked (phishing / spoofed UI / wrong network).
- Remote dependencies can be unavailable or compromised (supply chain).
- Backend data is not inherently trustworthy (it can be misconfigured or attacked). Treat it as untrusted input even if it’s “our” backend.
We mainly defend user funds and user privacy by:
- validating inputs
- minimizing signing surface
- preventing unsafe retries / double actions
- reducing injection/XSS risk via CSP and safe Angular patterns
-
Never handle secrets
- Never request, store, log, or transmit private keys / seed phrases.
- The app only interacts with wallets through providers and viem clients (see
EvmWalletService).
-
Validate before you act
- Validate addresses/amounts and config-derived addresses before sending txs or making irreversible backend calls (see validation patterns in
SwapPage.startCcxToEvm()).
- Validate addresses/amounts and config-derived addresses before sending txs or making irreversible backend calls (see validation patterns in
-
Minimize signature/transaction requests
- Only request exactly the permissions and RPC methods needed.
- Do not add message signing or typed-data signing flows unless required and clearly documented.
-
Prefer explicit user confirmation
- Make the user aware of network, recipient, and amounts before triggering wallet actions.
-
No dangerous DOM patterns
- Do not introduce raw HTML rendering or bypass sanitization in templates. Keep Angular template binding and default escaping.
Current patterns:
- CCX address: validated via
CCX_ADDRESS_REandValidators.pattern() - EVM address: validated via
isAddress()andValidators.pattern()
Rules:
- Always validate addresses at two layers:
- form validation (Angular validators) and
- runtime checks before use (see runtime checks in
SwapPage.startCcxToEvm()andSwapPage.startEvmToCcx()).
- Never “auto-correct” CCX addresses. If invalid, fail with a clear message and require user correction.
- For EVM addresses, prefer strict viem validation via
isAddress()prior to:- backend init calls
- contract transfers
- token watch/add operations
Current rules already implemented:
- numeric input is parsed and checked:
Number.parseFloat() - bounds are enforced via backend config:
BridgeChainConfig.common.minSwapAmount/BridgeChainConfig.common.maxSwapAmount - token decimals are inferred from config “units”:
inferDecimalsFromUnits()
Rules:
- Keep min/max enforcement aligned to
BridgeChainConfig.common. - Do not accept NaN, infinity, negative, or zero.
- When converting to on-chain units, always use config-derived decimals/units (example:
parseUnits()). - Avoid floating-point arithmetic for on-chain values; only parse floats for UI input, then convert via
parseUnits()orparseEther().
The frontend receives chain config from the backend via BridgeApiService.getChainConfig(). This config includes:
- contract addresses:
BridgeChainConfig.wccx.contractAddress - bridge recipient addresses:
BridgeChainConfig.wccx.accountAddress - chainId:
BridgeChainConfig.wccx.chainId
Rules:
- Before using config addresses in wallet operations, validate them with viem address checks:
- Example of validating a config address already exists:
isAddress()check forbridgeEvmAccount.
- Example of validating a config address already exists:
- If config is missing or invalid, treat it as a page-blocking issue and fail safe (see how config load errors set
pageError).
This app’s primary actions are:
- send a native transfer for gas fee payment:
EvmWalletService.sendNativeTransaction() - transfer ERC-20 wCCX:
walletClient.writeContract()
Rules:
- Do not introduce “sign message” authentication or typed-data signing unless explicitly required and reviewed.
- If you must add signing, scope it tightly and document:
- exact message format
- replay protections (nonce, domain, expiry)
- how the backend verifies it
Before any transaction, ensure the wallet is on the correct chain:
- the project uses
EvmWalletService.ensureChain()in swap flows.
Rules:
- Always call
EvmWalletService.ensureChain()using the chain fromEVM_NETWORKSbefore sending txs or contract writes. - Handle chain-add behavior safely (MetaMask missing chain code
4902is already handled inEvmWalletService.ensureChain()).
The swap uses confirmation waiting:
EvmWalletService.waitForReceipt()- configured confirmations:
BridgeChainConfig.wccx.confirmations
Rules:
- Never treat “transaction hash returned” as final.
- Wait for configured confirmations before calling backend init/exec endpoints (current pattern in
SwapPage.startCcxToEvm()andSwapPage.startEvmToCcx()).
Absolute rules:
- Never ask for or accept seed phrases or private keys in UI or logs.
- Never store secrets in:
- local storage
- session storage
- query params
- analytics/telemetry payloads
Wallet connection uses providers; any wallet secrets remain inside the wallet.
The app stores a single non-sensitive flag:
EvmWalletService.DISCONNECTED_STORAGE_KEY- reads via
window.localStorage.getItem() - writes via
window.localStorage.setItem()
Rules:
- Never store wallet addresses, tx hashes, payment IDs, emails, or amounts in storage unless there’s a strong product requirement and it’s reviewed.
- Treat any persisted user data as potentially exfiltratable.
Runtime config includes:
- backend base URL:
AppConfig.apiBaseUrl
Rules:
- Avoid logging config values in production builds.
- Never log full error objects that might include request URLs with tokens (if added in the future).
External links in the UI already follow good practice:
target="_blank"+rel="noopener"as used inHomePage
Rules:
- Any new external link must include the same protections as in
HomePage. - Prefer linking to official wallet download pages used in
WalletButtonComponent.connectorInstallUrl().
Rules:
- Never show instructions that request:
- seed phrase
- private key
- remote desktop access
- Wallet support guidance should instead point users to:
- verifying domain
- checking wallet prompts
- checking explorer links for tx hashes
In swap flows, the user should always be able to verify:
- recipient addresses (bridge deposit address, recipient)
- payment ID
- transaction hash
Current UI displays:
- payment ID and QR code:
paymentId - CCX deposit address from config:
BridgeChainConfig.ccx.accountAddress
Rules:
- Any new flow that triggers a wallet prompt must ensure the user can see what they are signing (network + destination + value).
The app interacts with:
- token contract address from backend config:
BridgeChainConfig.wccx.contractAddress - transfer call uses a minimal ERC-20 ABI:
erc20Abi
Rules:
- Validate contract address from config before:
- adding token:
SwapPage.addTokenToWallet() - reading balance:
publicClient.readContract() - transferring:
walletClient.writeContract()
- adding token:
- Ensure chain matches config before contract interactions:
- use
EvmWalletService.ensureChain()with the chain selected fromEVM_NETWORKS
- use
- Do not dynamically execute untrusted ABIs:
- backend config includes an optional ABI field
BridgeChainConfig.wccx.contractAbi; if used in the future, it must be treated as untrusted and verified against a known allowlist.
- backend config includes an optional ABI field
This bridge is not a DEX swap, so classic AMM slippage protection does not directly apply. The analogous protections are:
- enforce min/max amount bounds from config:
BridgeChainConfig.common - prevent decimal/units mistakes via
inferDecimalsFromUnits()and unit conversion viaparseUnits() - prevent bridge liquidity mismatch:
- CCX→EVM checks wCCX liquidity:
SwapPage.startCcxToEvm() - EVM→CCX checks CCX liquidity:
SwapPage.startEvmToCcx()
- CCX→EVM checks wCCX liquidity:
Rules:
- Never silently adjust user-entered amounts.
- Never proceed when liquidity checks indicate insufficient bridge funds.
This app is a static Angular SPA (bootstrapped via bootstrapApplication()) and must rely on hosting/CDN headers for baseline hardening.
Rules:
- Keep CSP tight and iterate using report-only rollout
- Minimize external asset hosts:
- prefer local assets in
conceal-bridge-ux/public/ - be deliberate about
connect-srcallowlists for:- backend base URL from
AppConfig.apiBaseUrl
- backend base URL from
- prefer local assets in
Before finalizing any change that touches wallet, transactions, config, or external connectivity, verify:
- Input validation remains strict (see
SwapPage). - Wallet operations remain scoped to required actions (see
EvmWalletService.sendNativeTransaction()andwalletClient.writeContract()). - No new secret-handling was introduced (see storage usage in
EvmWalletService). - External links are safe (see
HomePage). - CSP/headers implications are updated if new external hosts are used
- Error taxonomy and safe user messaging:
error_handling.md - Backend endpoint behavior and response shapes:
backend_api.md - Wallet security constraints and provider behavior:
wallets.md - Smart contract (wCCX ERC-20) trust boundaries and tx verification:
smart_contracts.md - Testing strategy for security- and wallet-sensitive flows:
testing.md