RippleBids Audit
Codebase Audit · ripple-backend-flask-new · rippleverse_sc · rippleverse_website

RippleBids Launchpad, DEX & Profile Audit

A full read of the XRPL launchpad, the Solana bonding-curve program, the DEX market-data/trading surface, and profile integration — architecture, security, and correctness, with a prioritized fix plan.

6subsystems reviewed, 2 audit passes
3issues need action today
July 2026prepared

Executive Summary

Two items below are live risk, not backlog — they're called out first because they warrant action before the rest of this document is read in full.

1. A live fund-drain path in the Solana program (mainnet)

rippleverse_sc is deployed to mainnet (PROGRAM_ID=Fqrt56tG9aLgj42bebCDcM8MmesVj9H3NJiweaaKyaXh). Its sell instruction never verifies that the token-vault account it's given is the real bonding-curve vault — it trusts whatever account the caller passes. Anyone can buy a token once, then repeatedly call sell while substituting their own token account for the vault: the program still pays real SOL out of the real vault each time, while the attacker's tokens never leave their wallet. No secrets or server access needed — just public transactions against the deployed program. See Solana § Finding 1.

Do now: pause trading via toggle_pause, then check recent sell transactions and each curve's real_sol_reserves against actual vault balances for signs of prior exploitation.

2. Live secrets committed to the backend and frontend repos

trustbridge.py hardcodes the production MySQL password. functions/downpour.py hardcodes an XRPL wallet seed — a private key, usable by anyone who reads the file, no exploit required. A second sweep (below) found six more: a Backblaze B2 storage key, a Gmail app password, and API keys for Gemini, Google Places, Geoapify, and ShipEngine, all sitting in plaintext. All of these are in git history permanently, not just the working tree. See the secrets inventory below.

Do now: check the escrow wallet's balance and move any funds out (already done — see XRPL § Findings); rotate every credential in the inventory table.

3. An unauthenticated endpoint that dumps and emails the entire production database

index.py:140–243, POST /admin/database/dump, has no auth check at all. Anyone can trigger a full dump of every table — users, wallets, orders — emailed as an attachment to a hardcoded address. Five stale dumps from this same code path already sit in the backend repo's root (database_dump_2025*.sql, up to 350KB each), meaning plaintext exports of production data are committed alongside the source. See Backend § Finding 1.

Do now: remove or auth-gate this route; delete the committed dump files from the repo; treat every credential/hash inside those dumps as exposed.

Secrets inventory (rotate all of these)

Every hardcoded credential found across both audit passes, in one place, so nothing gets missed during rotation.

SecretLocationGrants
Production MySQL passwordtrustbridge.py:7Full read/write on the production database
XRPL wallet seedfunctions/downpour.py:22Full control of the escrow wallet (checked: ~1.6 XRP, 0 XRPB currently)
Xumm API key + secretfunctions/downpour.py:20–21Xaman platform API access
Gemini API keyfunctions/ai_services.py:17Billed Gemini API calls
Backblaze B2 keyfunctions/user.py:18–19Write access to the video storage bucket
Gmail app passwordtest_smtp.py:6Send-as that Gmail account
Google Places API keyapp/api/google-places/{details,autocomplete}/route.jsBilled Google Places calls
Geoapify API keyapp/api/shipengine/countries/route.js:3Billed Geoapify calls
ShipEngine API keyapp/api/shipengine/validate-address/route.js:18Address validation, possibly label creation (real cost)
Fallback JWT_SECRET ("supersecret")auth.py + 6 other filesToken forgery if env var is ever unset
Fallback JWT_SECRET ("your-secret-key")storefront.py + 4 other filesSame, different default string
Fallback SIGNING_SECRET ("devtomiwaisperfect")auth.py, signRequest.js, auth/login & register routesRequest forgery to the auth backend if env var is ever unset

Findings by section

SectionCriticalHighMediumLowInfo
Solana launchpad program12342
XRPL launchpad & escrow43321
DEX data & trading flow01223
Profile integration00222
Backend: admin, commerce & storefront (2nd pass)78730
Frontend: additional routes (2nd pass)13420
Total131721158

Solana Launchpad Program

Scope: programs/ripple_trades/src/* (all instructions, state, Raydium/Metaplex CPI helpers), tests/, and the frontend's on-chain integration. One scope note up front: despite the README describing an Anchor program, Cargo.toml depends only on solana-program + spl-token — this is hand-rolled native Solana, with no Anchor account-validation guard rails. That absence is directly relevant to the top finding.

Criticalinstructions/sell.rs, buy.rs, graduate.rs

The bonding-curve token vault address is never validated — full vault drain

The program checks the SOL-holding PDA (curve_ai) but never verifies that the paired token account (curve_tok_ai) is the real vault — no comparison against get_associated_token_address(curve_ai, mint), no owner check. An attacker can substitute their own token account as curve_tok_ai in sell(): the program still transfers real lamports out of the real curve_ai vault and updates real_sol_reserves as if a legitimate sale occurred, while the attacker's tokens stay in their own control and can be replayed. The same missing check exists in graduate.rs (line 55, used at 136–153) for the LP-reserve token account.

Fix

Store the expected vault address on-chain at launch_token time (or re-derive it each call) and reject any curve_tok_ai/wsol_tok_ai that doesn't match — the same pattern as Anchor's has_one, applied by hand.

Highinstructions/graduate.rs:60–153

Raydium CPI accounts trusted from the caller, never derived or checked

Only raydium_prog_ai and pool_fee_ai are checked against hardcoded constants. amm_config_ai, ray_auth_ai, pool_state_ai, both mint accounts, lp_mint_ai, both vaults, and obs_state_ai are never derived/verified against the PDA helper functions already written in raydium.rs (lines 33–60) but never called. Since graduate() is permissionless, an attacker-as-payer could pick an unfavorable amm_config fee tier, or the locally-computed sol_is_token_0 (line 134) could silently mismatch the mint order actually passed to Raydium.

Fix

Derive and assert every Raydium PDA locally before the CPI; pin amm_config to a value stored in PlatformConfig rather than accepting caller choice.

Mediumescrow.rs, claim_creator_rewards.rs

toggle_pause doesn't gate every user-facing instruction

is_paused is checked in launch_token, buy, sell, graduate, and escrow::process_init — but not in process_fund, process_release, process_refund, or anywhere in claim_creator_rewards.rs. This contradicts the README's claim that pause blocks all user-facing instructions. Pausing today to contain the vault-drain bug above would not stop escrow or reward-claim fund movement.

Fix

Load and check cfg.is_paused in all four instructions.

Independently re-verified in a second pass against the full claim_creator_rewards.rs file — confirmed exactly as described. That same pass also found the claimant-identity, double-claim, and rent-exempt-floor logic in that file to be correctly implemented.

Mediumtests/ripple_trades.ts

The test suite targets an interface the program no longer has

Tests assume a 1% / 3-way fee split via Anchor's program.methods/IDL and omit the creator_rewards/referrer accounts entirely. The shipped code (constants.rs lines 12–32) uses a 1.5% fee across 4 recipients plus referral, and buy/sell require 15/13 accounts including those two. The suite cannot run against the current program — meaning zero automated coverage exists for the curve math, fee split, creator rewards, referrals, escrow, tiers, or graduation, including the vulnerability above.

Fix

Rewrite the suite against the actual instruction set before relying on it for anything, including before re-enabling trading after a fix.

Mediumprograms/ripple_trades/src/instructions/admin.rs

No timelock or multisig on admin actions

update_config, toggle_pause, authority transfer, and withdraw_sol all execute instantly on a single EOA signature. A compromised authority key can redirect fee wallets, change fee splits, or pause/unpause with no delay for users to react. The README only recommends multisig for the treasury/liquidity destination wallets, not the authority key itself.

LowCargo.toml:13, README.md, events.rs

README materially diverges from the shipped code

README claims Anchor (not used), 1% / 3-way fee (actual: 1.5%, 4-way + referral), and overflow-checks = true in release (actual: false, an intentional size trade-off). It also references an events.rs for indexer events that doesn't exist. Not exploitable directly, but anyone configuring monitoring or fee expectations off this document will be wrong.

Lowinstructions/escrow.rs:237, 241–249, 293–295

Escrow uses raw (non-checked) lamport arithmetic

Plain -=/+= instead of the checked_sub/checked_add pattern used everywhere else in the program. Combined with overflow-checks = false, a violated invariant would silently wrap rather than panic. Currently safe only because fee_bps is bounded by UserTier::fee_bps() — an implicit invariant, not an enforced one.

Highmetaplex.rs:59–103, launch_token.rs:93, 186–189

Metaplex CPI likely omits the token-metadata program account — could make every launch fail

The account list passed to invoke_signed in cpi_create_metadata_v3 has no entry for the Token Metadata program itself — unlike the sibling cpi_create_ata, which explicitly includes it and documents, in its own comment, that the Solana BPF runtime requires the invoked program's account to appear in the accounts slice or the call fails with "Unknown program"/MissingAccount. launch_token.rs loads that account but never passes it through. This couldn't be confirmed by running the program, but if it holds, every launch_token call fails at this step and reverts atomically — meaning token launches may not work at all in this build.

Fix

Add the program account to the CPI's account list, matching cpi_create_ata's pattern, then verify with a local-validator integration test before assuming launch works end-to-end.

Lowlaunch_token.rs:88, 186–189

Metadata account isn't locally validated against its canonical PDA before the CPI

Unlike bonding_curve/token_launch/creator_rewards, which are all checked against find_program_address, the metadata account is passed straight to the Metaplex CPI with no local derivation check. Metaplex's own handler would likely reject a wrong address, but the program has no defense-in-depth of its own here.

Verified correct

Curve math uses u128 intermediates with checked_* throughout, and truncation always rounds in the pool's favor on both buy and sell. The fee split absorbs rounding dust into treasury rather than letting any party gain from it. Slippage bounds are enforced atomically before any transfer. is_complete vs. is_graduated is a sound separation, and Solana's per-transaction atomicity means a failed Raydium CPI during graduate() reverts everything — no partial-migration state exists. Two-step authority transfer and withdraw_sol's scoping to the config PDA's own excess lamports are both correctly implemented.

Architecture assessment

The bonding-curve → Raydium-CPMM-graduation model is still defensible in 2026 — cheap, well understood by traders, and the curve math here is genuinely correct. I wouldn't swap the AMM curve or the graduation DEX. The real risk is a different decision entirely: building this as a hand-rolled native program instead of an Anchor program. That trade gave up exactly the guard rails — typed account deserialization, has_one/seeds = constraints, automatic signer/owner checks — that would very plausibly have caught both findings above during normal development, since Anchor forces every account relationship to be declared and checked explicitly.

Before mainnet trading resumes: fix the account-validation findings above; rewrite the test suite against the current instruction set; get an independent second audit specifically because this is hand-rolled validation rather than an established framework, which is a higher-risk category on its own; and add a short timelock or multisig on update_config/toggle_pause/authority transfer.

XRPL Launchpad & Escrow

Scope: functions/xrpl_launchpad.py, escrows.py, trades.py, downpour.py, storefront_orders.py, auth.py, trustbridge.py, extensions/extensions.py, and the frontend's Xaman/signing integration.

Criticaltrustbridge.py:4–8

Live production database password committed in plaintext

A standalone schema-export script hardcodes the Aiven MySQL host, user, and password for the production database. Anyone with repo access has full read/write to production — xrpl_launches, escrows, wallets, everything.

Fix

Rotate the credential immediately; load it from env instead; audit git history and Aiven access logs for prior use.

Criticalfunctions/downpour.py:20–22

Custodial XRPL wallet seed hardcoded in source

XUMM_API_KEY, XUMM_API_SECRET, and DOWNPOUR_WALLET_SEED are literal strings, not env reads. This seed is the private key for the wallet meant to pay out XRPB reward batches. Anyone with source access can drain it directly with xrpl-py — no server compromise needed.

Fix

Treat the wallet as burned. Move funds, rotate to a design that doesn't hold a bare seed server-side (see architecture note below).

Criticalescrows.py, downpour.py, xrpl_launchpad.py (all routes)

Zero authentication on fund-moving and escrow endpoints

None of these routes carry a JWT check, admin check, or the HMAC verify_signature decorator that exists elsewhere in the codebase — combined with CORS(app, origins="*") in extensions.py:22, they're open to the public internet. The one real signing scheme (verify_signature in auth.py, matched by app/lib/signRequest.js on the frontend) is only ever applied to marketplace/listings and auth/register|login.

Fix

Extend verify_signature/JWT admin checks to every route in downpour.py and escrows.py before any of this handles real funds again.

Criticalescrows.py:106–152, storefront_orders.py:73

Escrow "funded" status is a client-asserted boolean, never verified on-chain

create_escrow() takes paymentVerified directly from the request body and marks the order funded if it's truthy — the transactionHash is stored but never checked against XRPL or Solana. Any client can call /create with {"paymentVerified": true} and get an order marked escrow_funded with no payment made. storefront_orders.py has the identical pattern, gated only on the presence of a client-supplied escrow_id.

Fix

Verify the transaction hash against the chain before flipping status — or move to native XRPL Escrow objects, where "funded" is a ledger fact, not a claim.

Highescrows.py, storefront_orders.py

"Escrow" has no release or refund logic at all, on-chain or off

No EscrowFinish/EscrowCancel equivalent exists anywhere in the backend — only create, get_all, get_details. update_order lets the listing owner PATCH status to any arbitrary string, but nothing consumes that to actually move funds out of ESCROW_ADDRESS. The stored conditions (delivery required, auto-release days) are written once and never read again. Today, "escrow" is a database label with no code path that ever moves funds from it.

Highfunctions/downpour.py:897–1103

Payout execution is broken (undefined variable) — but confirms the intended custodial design

execute_batch_payouts references wallet.classic_address, but wallet is never assigned anywhere in the file — every call throws NameError, silently swallowed by a broad except. This currently self-mitigates the fund-moving risk from the missing-auth finding above, but once someone "fixes" the obvious bug by adding Wallet.from_seed(DOWNPOUR_WALLET_SEED), it combines with that finding into a live drain vector.

Highfunctions/auth.py:18, app/lib/signRequest.js:4

The one real signing scheme has a public default secret, on both sides

SIGNING_SECRET = os.getenv("SIGNING_SECRET", "devtomiwaisperfect") — and the frontend has the identical fallback literal. If either deployment ever runs without the env var set (plausible, since .env.example ships it blank), the HMAC scheme becomes forgeable by anyone who's read this source.

Mediumfunctions/auth.py:20, 32–64

Replay protection is weak and non-durable

used_nonces is an in-process Python set, remembered for only 60 seconds, and the timestamp-freshness check is commented out. Under multiple workers, a nonce accepted by one worker is unknown to another — the same signed request can be replayed once per worker, or after the window elapses regardless.

Mediumfunctions/escrows.py:206–218

GET /api/escrows returns every escrow to anyone

No auth, pagination, or filtering — buyer/seller wallet addresses, amounts, and deal terms are disclosed to any unauthenticated caller.

Mediumxrpl_launchpad.py:361, trades.py:717, storefront_orders.py:297

Column lists built with f-strings in update helpers

Values are parameterized, and column names currently come from a fixed internal whitelist, so this isn't exploitable today — but the pattern is fragile. A future contributor adding a client-controlled key without noticing the string-formatting would introduce real SQL injection.

Lowxrpl_launchpad.py:60–70

Dead custodial-wallet scaffolding left in the launchpad file

DISTRIBUTION_ADDRESS/DISTRIBUTION_SEED and unused Wallet/submit_and_wait imports are leftovers from an earlier custodial design. The actual launch flow is non-custodial and sound (see architecture note) — this dead code just risks confusing the next engineer, or being "helpfully" wired back up.

Lowxrpl_launchpad.py (submit_accountset / complete_launch)

Status transitions have no row lock

Status is read, checked in Python, then written in a separate UPDATE with no SELECT ... FOR UPDATE or conditional write guarding against two concurrent requests for the same launch. Low likelihood, easy to harden.

Verified correct

The token-launch flow itself (xrpl_launchpad.py) is genuinely non-custodial: the creator's Xaman wallet signs AccountSet/AMMCreate, and the backend only ever submits pre-signed blobs, cross-validating the on-chain result against its own DB record before flipping status. That's the right pattern for a launchpad, and it's the one thing here that should be the template for the rest.

Architecture recommendation

What this codebase actually does today is split down the middle. The launch path is non-custodial and correctly designed. Everything adjacent to it — reward payouts and marketplace escrow — is built around a raw seed sitting in the Flask process, with none of the controls that would make that safe: no auth on the triggering endpoints, no on-chain verification before marking funds released, and currently no working signer at all.

I would not "fix" the payout path by patching the NameError and calling it done — its being broken is the opportunity to redesign it before it moves real money. Concretely: keep the launchpad's non-custodial pattern as-is and delete the dead scaffolding so nobody restores custodial signing there by mistake. Convert reward payouts to require an operator signature per batch (Xaman/multisig) rather than an unattended seed, or move the seed into a KMS/HSM-backed signer gated behind real admin auth and a second approval step. Replace the DB-only "escrow" concept with native XRPL Escrow objects for the on-chain leg — conditional release enforced by consensus rules, not a backend that has to be trusted to eventually call a release function that doesn't exist yet — keeping MySQL only for order metadata that doesn't belong on-chain. The XRPL side's use of native AMMCreate instead of a custom bonding curve is the right idiomatic choice for that chain; I wouldn't try to force the Solana bonding-curve model onto it.

DEX Data & Trading Flow

Scope: app/trade/**, app/api/trades/, app/api/solana/bonding-curves, and the backend routes that serve them. Ordered by the five capabilities requested, then the trading flow itself.

Token holder informationBroken for Solana

useTokenHolders.js calls /api/ripplebids/trades/holders/<mint>, but the Flask backend has no such route — only /xrpl/holders/<issuer> exists. Every Solana holders request 404s and silently renders "No holder data yet" for every Solana token, always. A working alternative already exists and is unused: app/api/trades/holders/[mint]/route.js falls back to real on-chain data via getTokenLargestAccounts, but nothing calls it. XRPL holder ranking, meanwhile, works but is a net-flow approximation (buy/sell deltas) rather than actual trust-line balances, so ranks can be wrong.

Fix

Point the Solana hook at the working route (or fix the proxy path); rebuild XRPL "top holders" from real account_lines balances.

Token distribution pie chartsNot built

No pie/donut/treemap component exists anywhere under app/trade. The percentages needed are already computed by useTokenHolders (h.pct) and rendered only as a bar list — this is close to a pure front-end task once the holders bug above is fixed.

Order booksCorrect

XRPL order book is fetched live and entirely client-side from the ledger (book_offers + AMM reserves), correctly merged and sorted. Solana tokens use a bonding curve, so no order book is shown for them — a correct design choice, not a gap.

Trading graphs / chartsWorks, but real-time layer is dead

Chart data itself works when the underlying tables are populated — real OHLCV built from indexed trades. But token_trades/xrpl_trades are only ever written via a passive POST /trades/indexer/events endpoint; no indexer process pushing to it was found in either repo, so if one isn't deployed, charts/activity silently stay empty. Separately, useChartLive.js/useTokenTrades.js both open a socket.io-client connection expecting a realtime feed — grepping the entire backend for socketio/emit( returns zero matches. flask_socketio is an installed dependency that's never imported or instantiated. The UI degrades gracefully (labels data "History" instead of falsely claiming "Live"), but the client retries this dead socket forever.

Fix

Either stand up a real Socket.IO emitter alongside the indexer write path, or remove the socket.io client code to stop the infinite reconnect loop against a server that will never accept it.

General token info displayCorrect

Both chains compute price/market-cap/supply directly from on-chain state (Solana: batched bonding-curve account reads; XRPL: on-chain values preferred over off-chain metadata on conflict), with sensible fallbacks rather than crashes on missing data.

Trading flow — Solana

Every trade re-fetches the bonding curve fresh from chain, computes a slippage-bounded minimum, simulates before signing, and — critically — the on-chain program independently re-checks that same bound (buy.rs:102, sell.rs:97) and rejects the transaction if violated. Prices aren't merely trusted client-side; they're enforced on-chain. Gap: slippage is hardcoded at 1% with no UI control to adjust it.

Info

Trading flow — XRPL: safe by ledger design (XRPL's own SendMax/Amount semantics bound overspend), but uses a fixed 2% buffer rather than user-adjustable slippage, against a price that can be up to 5 seconds stale.

A soft spot worth knowing about: the proxy at app/api/ripplebids/[...path]/route.js converts any upstream network failure into an HTTP 200 with a synthetic empty payload. Good for UX continuity, but it means a genuine backend outage looks identical to "no data yet" for every endpoint it covers, including holders and token lookups.

Ideas for improving charts & market data

Wire the distribution pie chart off the holder percentages already computed — a near-complete front-end task once the Solana holders route is fixed. Decide, one way or the other, on real-time: either emit real Socket.IO events from whatever writes token_trades, or delete the client-side socket code so it stops trying forever. And surface data-freshness caveats in the UI — label USD conversions as estimated when the price API falls back to seed data, and show an "as of Xs ago" tag on the XRPL orderbook/price given the 5-second poll window, so displayed price and execution price don't surprise anyone.

User Profile Integration

Scope: app/(app)/profile, app/trade/profile, app/trade/portfolio, and the backend's user/storefront routes. Confirmed architecture: /api/ripplebids/* proxies to this Flask backend, while /api/uploader/* proxies to a separate external service (not in this repo) that owns bio/avatar/banner/videos — profile pages are a stitch of at least two backends today.

Mediumfunctions/storefront_followers.py, functions/user_follows.py

Two disconnected follower graphs for the same concept

The RippleVerse profile's Follow button runs on user_follows. A second, full follow/unfollow/stats route set exists in storefront_followers.py and is never called by anything in this frontend — two social graphs tracking "following a user" with no relationship to each other.

storefront.py, storefronts.py (~5,200 lines combined)

The entire storefront backend has no frontend consumer in this repo

No app/storefront directory exists at all. Two incompatible storefront schemas (storefront.py's seller-stats/orders/escrow model, and storefronts.py's richer themes/skills/social-links model) both sit unused by rippleverse_website. Worth confirming whether this serves a different, legacy frontend before building more "profile" features on top of a third system — and note that being unused by this frontend doesn't mean unreachable: the blueprints are still registered in index.py and, per the XRPL audit, storefront_orders.py shares the same unverified-payment pattern found in escrows.py. Whether the rest of this surface has the same auth gaps as escrows.py/downpour.py wasn't in scope for this pass and is worth checking before assuming it's dormant risk-free.

Lowapp/(app)/profile/[username]/page.js:973

Dead link

"View All" under Tokens Launched links to /trades, which doesn't exist as a route (the app only has app/trade/*, singular) — a 404.

Lowprofile/[username]/page.js:763–805

UI reads profile fields that can never be populated

location, joined_at, is_verified, twitter, youtube, instagram are rendered from profile, but no such columns exist anywhere on the backend's users table (only bio/profile_pic_url). Pure dead UI, not just unwired.

Opportunityfunctions/trades.py:923–974, 1408

Server-side watchlist is fully built and completely unused

GET/POST /trades/watchlist and its DELETE counterpart are implemented against a real trades_watchlist table, but useWatchlist.js stores everything in localStorage only. Watchlists don't sync across devices and can't be shown on someone else's profile even if the owner wanted that.

Opportunityfunctions/trades.py (token_trades / xrpl_trades)

Personal trade history and P&L are one query away, but not built

Trade rows already key a trader wallet address per row, and the existing holder-aggregation logic shows how to roll that up into a position. Nothing today queries "trades by the connected wallet" to show a profile owner's own buy/sell history, volume, or basic realized/unrealized P&L — the columns needed already exist.

Summary

A user's identity today is really three or four unrelated systems: an external uploader service for bio/avatar, this backend's near-duplicate users table, the trading/launchpad domain exposed piecemeal through /trades/*, and an entirely separate, entirely unused storefront/seller identity. The highest-value, lowest-effort wins are wiring the existing watchlist and per-wallet trade-history data into the profile pages that already have the UI shape for them, fixing the dead link and dead fields, and deciding — before investing further — which of the two storefront schemas (if either) is canonical.

Backend: Admin, Commerce & Storefront Surface

A second, broader sweep specifically covering everything the first pass didn't scope in: admin_*.py, marketplace.py, messaging.py, membership.py, orders.py, save_wallets.py, user_blocks.py, the full storefront*.py family, ai_services.py, and the root-level ad-hoc scripts. Triggered by finding hardcoded secrets in two unrelated files during the first pass — the working assumption was that more of the same, and more missing auth, was likely still out there. It was.

Criticalindex.py:140–243, dump.py

Unauthenticated endpoint dumps and emails the entire production database

POST /admin/database/dump has no auth check. It dumps every table — users, wallets, orders, everything — and emails it as an attachment to a hardcoded address. dump.py at the repo root is functionally the same thing as a standalone script. Five stale dumps already sit in the repo (database_dump_2025*.sql, up to 350KB) from this same path.

Fix

Delete the route or gate it behind admin auth + IP allowlist; stop writing dumps inside the app directory; remove the committed dump files and treat everything in them as exposed.

Criticaltest_smtp.py:6

Hardcoded Gmail app password

server.login('devtomiwa9@gmail.com', 'skyh iwhz zzis exdq') — formatted exactly like a live Google app password, for the same address the DB-dump endpoint above emails to.

Fix

Revoke this app password in the Google account immediately; remove from source.

Criticalfunctions/user.py:18–19

Hardcoded Backblaze B2 key

B2_KEY_ID/B2_APP_KEY are literal strings (comment says "(hardcoded)"), granting write access to the video storage bucket.

Fix

Rotate the key in the B2 console; move to env vars.

Criticalfunctions/storefront_effects.py:261–420

Ownership check is written but never actually applied — any storefront's effects can be modified or deleted

update_effect/toggle_effect/delete_effect each carry a comment saying "verify effect belongs to the authenticated storefront," fetch the effect's storefront_id — and never compare it to the caller's. effect_id is a small sequential integer. Any caller with any valid JWT can enumerate IDs and modify/delete any other seller's effects.

Fix

Add the comparison the comment already describes: reject if result["storefront_id"] != request.storefront_id.

Criticalfunctions/storefronts.py (entire file)

An entire blueprint with zero authentication

Every write route — create/update profile, services, themes, music widgets, skills, social links — takes owner_id/storefront_id straight from the request body with no auth check at all, and delete/update-by-id routes don't even check the id belongs to the caller. Anyone can overwrite or delete any user's storefront content by guessing a sequential integer id.

Fix

Require the same token-derived access check used elsewhere in the codebase; never trust owner_id/storefront_id from the body.

Criticalfunctions/storefront_notifications.py (entire file)

Another entire blueprint with zero authentication — includes a built-in email relay

Create/read/mark-read/delete all take from_user_id/to_user_id/user_id from the request with no auth. Worst case: create_notification with email: true sends a real email, with fully attacker-controlled subject/body text, to any user's real address through the platform's own SMTP and branding — a built-in phishing relay.

Fix

Require the caller's JWT; only allow acting as from_user_id == caller; only allow read/delete when to_user_id == caller.

Criticalfunctions/save_wallets.py:187–218

Wallet lookup exposes encrypted private-key blobs, no auth

GET /get-wallet takes a user's email or id as a query parameter — no auth — and returns the full wallet row, including sui_enc_key/evm_enc_key/sol_enc_key/xrp_enc_key. Anyone who knows or guesses a user's email can pull their encrypted key material.

Fix

Require the caller to be the wallet owner, or drop the enc_key columns from any API response entirely.

Highfunctions/membership.py:185–330, functions/storefront_orders.py:11–155

Fabricated payment fields accepted at face value — free membership upgrades and fake "paid" orders

Both verify_payment and create_order take transactionHash/amount/verified straight from the request body and never check them against XRPL or Solana. Any caller can fabricate a transaction hash and get a paid membership tier for free, or make a marketplace order look funded without ever paying — the same bug class already found in escrows.py/downpour.py.

Fix

Verify the transaction against the actual chain (amount, destination, memo/order id) before activating anything.

Highfunctions/storefront_reviews.py:310, 359

"Admin" review-moderation routes only check that someone is logged in, not that they're an admin

get_pending_reviews/moderate_review call the file's own verify_token(), which only checks a JWT decodes and the user exists — no role check. Any registered user can approve or reject any storefront's reviews. The real admin_*.py files do this correctly via verify_admin_token, checking role in the DB — this file just doesn't reuse it.

Fix

Swap in verify_admin_token here too.

Highstorefront_background.py, storefront_effects.py:206, 462

Storefront ownership taken from the request body, not the verified token

create_background and create_effect/set_font read storefront_id from the body first, falling back to the token only if the body omits it — the opposite of what should happen. Any authenticated user (even one whose token carries no storefront claim at all) can overwrite another storefront's background or font by supplying its id in the body.

Fix

Always use the token-derived id; never accept it from the body.

Highfunctions/admin_settings.py:50–72

Site-wide maintenance-mode toggle has no authentication

POST /api/admin/settings/toggle calls no auth check at all. Any anonymous request can flip the entire site into maintenance mode — a trivial denial-of-service.

Fix

Add the same verify_admin_token check used in sibling admin files.

Highfunctions/storefront_settings.py:72–128

Storefront settings can be overwritten by anyone

POST /settings/<storefront_id> takes the id from the URL with no auth/ownership check — anyone can overwrite any storefront's alert settings and design JSON.

Fix

Require the caller's token and confirm it matches storefront_id.

Highindex.py:68–105, functions/storefront.py:1273–1281

Cloudinary upload signatures minted for anyone, twice over

Two separate POST /cloudinary/signature routes both sign arbitrary caller-supplied params with the server's Cloudinary secret, no auth required — letting any anonymous caller upload to (and burn quota on) the app's Cloudinary account.

Fix

Require a logged-in user before signing.

High~12 files, see secrets inventory above

Widespread hardcoded JWT/HMAC secret fallbacks

Every one of these silently falls back to a well-known literal string ("supersecret" or "your-secret-key") if its env var is ever unset — a container restart before secrets are injected, staging drift, anything — letting anyone who's read this source forge valid tokens the moment that happens. Two different fallback values also means modules can silently disagree about what a valid token is.

Fix

Fail hard at startup if these env vars are unset; never ship a literal fallback secret.

Mediumfunctions/ai_services.py (all routes)

AI endpoints are fully unauthenticated

Image-dimension estimation, description generation, text analysis, and address suggestions all skip auth entirely — combined with the hardcoded Gemini key, anyone can drive unlimited billed API calls against the platform's account.

Mediumfunctions/auth.py:212–282, :70

Password-reset OTP flow enumerates accounts, and its nonce store won't survive multiple workers

forgot_send_otp accepts a raw user_id as an alternative to email and returns a different status depending on whether the account exists — an enumeration oracle. Separately, pending_otps is an in-process Python list, so under multiple workers an OTP issued by one process is invisible to another, silently breaking password reset for some fraction of requests.

Mediumfunctions/admin_listings.py:20

Admin bearer tokens printed to server logs

print(f"Token: {token}") leaks live admin JWTs into stdout on every call.

Mediumfunctions/storefront.py:1026–1035; functions/storefront_notifications.py:94 vs storefront_followers.py:178,250

Two functional bugs found in passing

get_escrows is missing commas between selected columns — syntactically invalid SQL, so the seller-facing escrow list always 500s. Separately, create_notification() takes no arguments, but is called elsewhere with 7 — every follow/unfollow notification silently fails with a swallowed TypeError.

Verified clean

SQL construction across admin_users.py, admin_listings.py, admin.py, audit_trail.py, marketplace.py, storefront.py, storefront_orders.py, and storefronts.py uses f-strings for clause structure but consistently passes actual values through parameterized %s placeholders — no SQL injection found. The root ad-hoc scripts (dump.py, test.py, test2.py, check_amm.py) carry no additional hardcoded secrets beyond what's already listed above. CORS remains wide open (origins="*", already noted in the XRPL section) and several files print decoded JWTs/tokens to stdout for debugging — low risk if logs are private, but shouldn't ship to production.

Frontend: Additional Routes & Pages

The remaining frontend surface not covered by the first pass: the rest of app/api/*, the arcade/messages/notifications/upload/watchlist pages, auth pages, and every root-level ad-hoc debug script.

Criticalapp/api/shipengine/validate-address/route.js:18

Live ShipEngine API key hardcoded

If this key carries label-creation scope and not just address validation, whoever has it can generate real, billable shipping labels on the account — not just burn quota.

Fix

Move to an env var, rotate the key, and confirm its scope in the ShipEngine dashboard.

Highapp/api/interactions/route.js:22–25, app/api/posts/[videoId]/like/route.js:23–26, app/lib/userId.js

Identity trusted from the client, backed by an unverified JWT decode

Both routes check a body-supplied user_id before falling back to a value derived from the bearer token — and that fallback, decodeJwtPayload, only base64-decodes the JWT payload without verifying its signature. A caller can pass an arbitrary user_id directly, or a forged token with any fake sub claim, and have it forwarded as the acting user to the upstream service. Whether this is exploitable end to end depends on whether that upstream independently verifies identity — but this frontend provides no defense of its own.

Fix

Never trust a client-supplied user_id; verify JWT signatures before trusting any claim extracted from a bearer token.

Highapp/context/WalletContext.js, app/trade/token/[mint]/_components/AmmPool.js:11–12

The Xumm API secret is shipped to the browser

NEXT_PUBLIC_XUMM_SECRET_KEY is read inside a client component, so Next.js bakes its value straight into the JS bundle every visitor downloads — extractable via view-source regardless of the separate server-side route that was built specifically to keep this secret off the client.

Fix

Rename to a server-only variable (drop NEXT_PUBLIC_) and remove every client-side usage; route exclusively through the existing server API.

Mediumapp/api/create-post/route.js, app/api/notifications/route.js, app/api/notifications/read-all/route.js

More of the same client-trusted-identity pattern

create-post forwards whatever user_id the client sends with no check at all. Both notification routes take userId from the query string and forward it unchecked. If the upstream service doesn't independently enforce ownership, any authenticated caller could publish as another user, or read/mark-read another user's notifications.

Mediumapp/api/shipengine/countries/route.js:79, 92, 306

Unsanitized path interpolation, and a silent fallback to fabricated data

countryId/stateId are interpolated raw into the upstream URL path without encoding. Separately, any upstream error silently falls back to generated mock zip codes returned as if real — a caller not inspecting the response's note field could treat fabricated shipping data as authoritative.

Mediumapp/api/furyzone/[...path]/route.js:80–106

Arcade partner-identity stub mints every caller the same identity — and production verification isn't implemented yet

When a dev env var is set, getVerifiedPartnerUser() returns one hardcoded identity for every caller with no real per-request verification. It fails closed when unset, which is good, but the actual production verification path is a literal unimplemented TODO (return null). The feature only works today via the dev stub. Real risk only materializes if that dev var ever reaches a production config.

Info

Lower-priority notes: the catch-all proxies for chat and the uploader service forward any path/method to a fixed upstream host with no endpoint allowlist — not directly exploitable (host is fixed) but widens what's reachable from the public frontend. tip-wallets (by username) and the Xumm payload-status lookup (by UUID) have no ownership check, likely fine given they return public-style data or rely on UUID entropy, but worth an explicit confirmation.

Verified clean

Every root-level ad-hoc debug script (check-amm.js, check-indexer.js, check-old-tx.js, check-tx.js, check-tx2.js, fetch-amm-info-direct.js, fetch-amm-ledger.js, inspect-tx.js, manual-trade-parse.js, test-parse-trades.js) was read line by line — no secrets, keys, or wallet seeds found; all hit public XRPL testnet endpoints using only public addresses/tx hashes. The arcade component tree, messages/notifications/upload/watchlist/activity pages, and login/signup pages are likewise clean of hardcoded secrets; the arcade's signing logic is correctly server-only and reads its keys from env vars.

Recommended Action Plan

In sequence — each step assumes the ones before it are done.

Today

  1. Take down POST /admin/database/dump

    Delete or auth-gate it immediately — it's a live, unauthenticated full-database exfiltration path. Remove the committed dump files from the repo too.

  2. Check the XRPL wallet tied to DOWNPOUR_WALLET_SEED

    Already checked: ~1.6 XRP, 0 XRPB currently. Still rotate — the seed is public to anyone who's read the source.

  3. Pause the Solana program

    Call toggle_pause via the authority key to stop further exploitation of the vault-drain path. Remember it doesn't cover escrow/creator-reward instructions.

  4. Look for signs of prior exploitation

    Check recent sell transactions and each curve's real_sol_reserves against actual vault balances; check Aiven DB access logs and Backblaze/Gmail/API-key usage logs for unfamiliar activity.

This week

  1. Rotate every secret in the inventory table

    MySQL password, XRPL wallet, Xumm keys, Gemini/Google Places/Geoapify/ShipEngine keys, the B2 key, the Gmail app password, and every hardcoded JWT/HMAC fallback — replace with env-only values that fail closed if unset.

  2. Patch the Solana account-validation bugs

    Vault-address check in buy/sell/graduate; derive and assert every Raydium CPI account; add the missing token-metadata-program account to the Metaplex CPI and confirm launch_token actually succeeds on a local validator. Add tests that specifically try account substitution before unpausing.

  3. Lock down the storefront/admin blueprints

    storefronts.py and storefront_notifications.py need auth from scratch; fix the missing ownership check in storefront_effects.py; bind storefront_id to the token everywhere instead of the request body; gate the maintenance-mode toggle, the wallet-lookup endpoint, and both Cloudinary signature routes behind real auth.

  4. Add authentication to every fund-moving XRPL route

    Extend verify_signature/JWT checks to downpour.py and escrows.py; move nonce tracking to something durable and reinstate the timestamp-freshness check.

This month

  1. Redesign escrow and payouts before they touch real funds again

    Native XRPL Escrow objects for the on-chain leg; operator co-signing or a proper KMS-backed signer for reward batches; verify transaction hashes against the actual chain before flipping any "funded"/"verified" status — this same fabricated-payment bug now spans escrows, membership, and storefront orders.

  2. Fix the client-trusted-identity pattern on the frontend

    interactions, posts/[id]/like, create-post, and both notifications routes all need to stop trusting a client-supplied user_id; stop shipping the Xumm secret to the browser via a NEXT_PUBLIC_ var.

  3. Fix the DEX data gaps

    Solana holder endpoint mismatch, the distribution pie chart, and a decision on the dead real-time socket layer.

  4. Wire existing backend data into profiles

    Server-side watchlist, personal trade history/P&L, the dead link and dead UI fields.

  5. Resolve the storefront/profile duplication

    Pick a canonical system (or retire both storefront schemas).

Ongoing

  1. Independent second audit of the Solana program

    Warranted specifically because it's hand-rolled native code rather than an established framework — a structurally higher-risk category.

  2. Automated secret scanning in CI

    Would have caught all eight hardcoded credentials before they were ever committed — this pattern has now shown up independently in every one of the three repos.