security: fix critical multiple unstake exploit and vault drainage#31
security: fix critical multiple unstake exploit and vault drainage#31Ishant5436 wants to merge 33 commits into
Conversation
Phase A — research papers + versioning + same-wallet citations - New ResearchPaper model with append-only versions array, paper reactions, and a PaperCitation junction collection designed for both same-wallet (auto-accepted) and cross-author (pending) flows from day one. - /create flow gets a kind toggle (project | research paper); research is a 1-step form (PDF + author + handle + optional summary + optional GitHub link). - /research/[id] white-paper detail page with focus-mode (frosted backdrop overlay portaled to body), reader-controlled brightness slider, version history panel, and an author-only revise sheet. - Optional GitHub link renders the README inline via the /markdown API with sanitize-html allowlist + html-react-parser, plus a vertical recent-commit timeline below it. - Sentiment-only tick/cross reactions (no on-chain dependency). - Author profiles at /research/author/[wallet] with paper history, aggregate sentiment, and "cited in" tracking. Phase B — cross-author citations + acceptance flow - Founders can cite other researchers' papers; status starts pending and only flips to accepted once the cited author approves. - Author inbox at /research/inbox with accept/reject actions and an "all (history)" toggle. - Masthead inbox indicator polled silently — only renders when there are pending citations addressed to the connected wallet. - "Cited with permission" badges on accepted cross-author citations across project + paper + author surfaces. - Daily 5-citation cross-author cap layered on the burst rate limit to kill the obvious spam vector. Discovery polish - Citation-index endpoint returns marketAddress + paperId sets and powers THESIS / CODE / CITED badges on browse cards. - Filter chips on /browse (markets: with-thesis; research: with-code, cited). - Researchers index at /research/researchers ranked by paper count and recent activity. - Editorial picks shelf via NEXT_PUBLIC_EDITORIAL_PAPER_IDS env. Phase C — native GitHub browsing on PNL - File tree with branch picker (URL-shareable ?ref=). - Syntax-highlighted file viewer (prism-react-renderer + custom paper-cream theme). - Commit detail page with a unified-diff renderer parsed from GitHub's patch strings — no external diff library. - Issues + PRs read-only, both with markdown bodies + threaded comments + state filters. - PR files-changed view rendering the same DiffViewer with inline review comments anchored per line; orphaned comments collected at the bottom of each file as "other notes". - Code search inside the repo (rate-limited; surfaces a clear auth-required state when GITHUB_TOKEN is absent rather than a generic 502). - Mobile pass: responsive gutters via CSS variables, truncated filenames, hidden file-size column on small screens, tighter diff columns + comment indentation that follow the column shrink automatically. Shared GitHub helper at lib/github.ts centralises auth headers, Redis caching, and rate-limit handling across every endpoint that talks to GitHub. Deps added: react-markdown, remark-gfm, html-react-parser, sanitize-html, prism-react-renderer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The wallet page sometimes showed $0.00 while the navbar correctly showed the deposited balance. Root cause: /api/wallet/balance lazy-compiles in Next dev (~57s for 6.9k modules of @solana/web3.js + ioredis + shared config), and the wallet page sat at its initial useState(0) waiting. The navbar dodged this by calling Solana RPC directly from the browser. This patch makes every web surface share that fast path: - New apps/web/src/lib/hooks/useSolBalance.ts — SWR-backed direct browser RPC (matches mobile useWalletBalance). 30s refresh, 5s dedup, so navbar + sidebar + wallet page + create page coalesce into one in-flight call per refresh window. - UserInfo.tsx — replaces ~80 lines of inline state/effect/dynamic-import. - Sidebar.tsx — drops the 30s /api/wallet/balance poll; glow becomes derived state (solBalance > 0 && solBalance < 0.02). - wallet/page.tsx — solBalance/balanceLoading state + 30s poll + duplicate handleRefresh direct-RPC path all collapse into the hook. Send-tx and Privy onramp completion call refresh() instead of poking state. - create/page.tsx — one-shot /api/wallet/balance fetch swapped for the hook, walletBalance: number | null semantics preserved for gating. /api/wallet/balance is left in place for mobile / future SSR; no UI consumer depends on it now, so the dev compile stall can no longer surface as $0.00. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat: research papers + mobile polish + unified SOL balance hook
…R cache
When a user signs in, the server now subscribes their wallet pubkey to
Helius accountSubscribe (refcounted, so multiple tabs share one Helius
slot). On-chain balance changes flow:
Helius account notif → event-processor (kind=wallet branch)
→ socket-server.broadcastWalletBalance()
→ client useUserSocket (wallet:balance listener)
→ useSolBalance writes to SWR cache via mutate(v, false)
End result: a deposit appears in the UI within one slot (~400ms) instead
of waiting up to 30s for SWR's poll. The poll stays as a cold-start /
reconnect fallback so consumers don't need to know about the realtime
layer.
Implementation:
- helius-client: subscribeToAccount now takes a `kind` param
('market' | 'position' | 'wallet'), tracked in a parallel pubkey→kind
map. Notifications for kind=wallet short-circuit past detectAccountType
(which would tag them 'unknown' and drop them) and push a lamports-only
event straight from accountNotification.value.lamports — no extra RPC.
- redis/queue: BlockchainEvent.accountType gains 'wallet'; data is now
optional and a new `lamports` field carries the balance for wallet
events. Market/position keep their base64 data path.
- event-processor: new processWalletUpdate branch; no MongoDB write,
purely a realtime push.
- sync-manager: subscribeToWallet / unsubscribeFromWallet thin wrappers
around heliusClient with kind='wallet'.
- socket-server: tracks walletSubscribers (wallet → Set<socketId>) and
socketWallets (reverse map for cleanup). subscribe:user triggers
Helius subscribe only when the wallet's refcount goes 0→1; disconnect
decrements and unsubscribes on 1→0. Adds broadcastWalletBalance to
emit on user:{wallet} room.
- useUserSocket: listens for wallet:balance, exposes
{ lamports, sol, slot } and dedups out-of-order slots.
- useSolBalance: bridges socket-pushed value into SWR's cache via
mutate(value, false). Consumers (navbar, sidebar, wallet page,
create page) read `solBalance` unchanged — realtime is transparent.
Known dev-only limitation: instrumentation.ts binds Socket.IO to
SOCKET_PORT=3000 (default), which collides with Next dev's HTTP port.
The unified production server (server.ts) binds both on one port and
works. Worth fixing the dev default to 3001 in a follow-up.
Future optimization noted in useSolBalance.ts: useSocket() creates a
fresh io() per hook call (no singleton), so a page that already uses
useUserSocket directly (e.g., /wallet) will hold two Socket.IO
connections. Server-side refcounting keeps it correct; turning
useSocket into a singleton is a worthwhile follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…balance feat: realtime SOL balance via Helius accountSubscribe + Socket.IO
Swaps "Plant a paper." for "Plant the seed." on the /create research paper flow and puts a SeedIcon inline before the word. Icon sized at 0.85em so it scales with the responsive heading instead of fighting it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two small fixes: - /create research paper flow now renders the same Project ↔ Research toggle at the top instead of only the bottom "Different kind" button. One click on "Project" returns to the project flow. Extracted KindTabs to its own file so both flows import the same component. - Sidebar avatar swaps to the <User> icon if the profile image fails to load (e.g., IPFS gateway 404, dead Privy CDN URL) — previously the browser rendered the alt text inside the avatar tile, leaking the username into the chrome. Resets on profilePhotoUrl change so a re-upload gets a fresh load attempt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The create flow was wrapping every prepare-transaction failure in a
generic "Failed to prepare transaction" toast, hiding the actual
server-side cause in transactionResult.details. Two diagnostic patches,
no behavior change:
- create/page.tsx: thrown Error now includes details so the toast
message line shows e.g. "Failed to prepare transaction — Invalid
IPFS URI format: …" instead of leaving the user with no signal.
- api/markets/prepare-transaction: explicit Number.isFinite guards on
targetPool and marketDuration. parseInt('') returns NaN and `NaN < x`
is always false, so the bare numeric check used to silently let NaN
through and blow up later as a cryptic BigInt(NaN) RangeError.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Server routes were reading NEXT_PUBLIC_HELIUS_MAINNET_RPC for their Solana RPC connection — which is the browser-facing env var, surfaced to the public bundle. If that URL is rotated, domain-restricted, or simply set wrong in the deploy environment (Render in our case), every server-side RPC call returns 401 Unauthorized and the create flow toasts "Failed to prepare transaction — failed to get recent blockhash". Other routes (extend, close-position, resolve, team-vesting, creator-fees) share the same RPC_ENDPOINT and would 401 the same way; they just happen to be hit less often. Fix in two parts: - apps/web/src/config/solana.ts: RPC_ENDPOINT now resolves server-side from process.env.HELIUS_API_KEY (a server-only secret, same one Helius WebSocket client uses) and falls back to the NEXT_PUBLIC_* URL only when running in the browser. Every server route that imports RPC_ENDPOINT auto-benefits. - packages/shared/src/solana/anchor-program.ts: buildCreateMarketTransaction now accepts an optional rpcEndpoint param. Mobile + client callers pass nothing and fall back to the shared env config; the prepare-transaction route passes the server-resolved RPC_ENDPOINT through. - apps/web/src/app/api/markets/prepare-transaction/route.ts: passes RPC_ENDPOINT to the builder. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three threads, one user-visible bug: 1. /api/markets/vote/prepare was calling buildBuyYesTransaction / buildBuyNoTransaction which both read NEXT_PUBLIC_HELIUS_MAINNET_RPC for getLatestBlockhash. Same 401 class as create flow last commit — the public URL on Render is wrong/restricted, server falls through to it, every vote prep dies on blockhash fetch. Both buy builders now accept an optional rpcEndpoint param (same shape as buildCreateMarketTransaction), and the route passes RPC_ENDPOINT (which resolves server-side from HELIUS_API_KEY). 2. useVoting was throwing only prepareResult.error, dropping prepareResult.details on the floor. So when the server route's error response said "Failed to prepare vote transaction" with details: "401 Unauthorized", the client only saw the wrapper. 3. parseError's default branch was returning a canned "An unexpected error occurred. Please try again." for anything not in its pattern catalog, while stashing the real reason in details (which the toast doesn't render). Replaced the canned message with the actual error string so future unknown errors surface their real cause; details field kept for any caller that wants both. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "all API calls authenticated" security pass (e0423ad) migrated 14 client files to authFetch but skipped every shared hook in packages/shared/src/hooks. Those hooks use plain fetch(apiUrl(…)), which sends no Authorization header, so any route wrapped in withAuth / withWalletOwnership now returns 401 "Authentication required" — the toast the user just saw when trying to vote. Same bug, nine surfaces. Migrating them all in one pass: useVoting vote/prepare, vote/complete useClaiming claim/prepare, claim/complete useResolution resolve/prepare, resolve/complete (x2), resolve/prepare-native-transaction, pump/upload-ipfs useExtend extend/prepare, extend/complete useClose close-position, close-market usePlatformTokens platform-tokens/claim useTeamVesting team-vesting/init, team-vesting/claim useFounderSolVesting founder-sol/init, founder-sol/claim useEmergencyDrain treasury/emergency-drain authenticatedFetch calls apiUrl() internally and attaches the Privy Bearer token when a provider is registered (WalletProvider already wires this up). Falls back to unauthenticated fetch when no provider — so mobile + web both work, and unauthenticated reads are unaffected. apiUrl import dropped from each hook since it's no longer used directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both toast renderers (the shared Toast component and the inline toast on the market detail page) sat at top-4 (16px). Navbar is 56-64px tall, so the toast card overlapped the navbar — and because the card is narrower (max-w-md) than the navbar's content area, the "PLANT / BROWSE / ORCHARD" links leaked around its left/right edges. Moving to top-20 (80px) lands the toast cleanly in the content area with ~16-24 px of breathing room below the navbar. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two small UX polishes on the browse-page MarketCard: - Auth guard on YES/NO: clicking a vote button while unauthenticated now opens the auth modal directly (same pattern as the Sidebar wallet button) instead of firing the vote, getting back "Please connect your wallet first", and surfacing it in the ErrorDialog modal. One fewer friction step on the first-vote-of-the-session flow. - Per-card bet amount slider above the YES/NO buttons. Range 0.01 → 1.0 SOL, 0.01 step, default at the program minimum. Lets users size a conviction tap without leaving the browse grid; for larger positions the market detail page still owns the full sizing UX. Slider events stopPropagation so dragging it doesn't trigger the card's Link navigation. handleQuickVote signature gains an optional amount param (falls back to QUICK_VOTE_AMOUNT when omitted), so any future caller can opt out of the slider value without breaking. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tions
Two problems the user just hit:
1. Voted from /browse, navigated to /market/[id] — position didn't show,
pool progress didn't tick. Root cause: vote/complete writes Mongo and
broadcasts vote counts via socket, but never invalidates the Redis
caches that the GET endpoints read from. The position GET has a 5s
TTL, the wallet's full position list has the same, and
profile-counts has 60s. Until those expire, the page serves the
pre-vote snapshot.
2. Opening a market from /browse → market detail content + the
research citations card render at different times. MarketCitations
was wrapped in next/dynamic with loading: () => null, ssr: false —
so the chunk downloaded after the main bundle and the citations
panel slotted in late, causing layout shift.
Changes:
- New helper apps/web/src/lib/redis/invalidate.ts — invalidateCache(
...suffixes) handles exact DEL + wildcard patterns via SCAN MATCH.
Best-effort: Redis failures are swallowed since TTL is the safety
net.
- Mutation endpoints now invalidate the matching cache keys:
- vote/complete: markets:position:{m}:{w}, positions:{w},
profile-counts:{w}
- claim/complete: markets:position:{m}:{w}, positions:{w}
- extend/complete: markets:list:*
- markets/complete (new market): markets:list:*
- markets/resolve/complete: markets:list:*
- projects/create: markets:list:*, profile-counts:{w},
creator-fees:none:{w} (founder sentinel)
- research/citations/{id}/accept|reject: research:citation-index
- MarketCitations moved from next/dynamic to a regular import. The
component ships on every market detail page anyway; the chunk-split
was premature optimization that traded a tiny initial-bundle saving
for visible loading jank.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codifies open-source status for judging and downstream use.
- sanitize-html ^2.17.3 -> ^2.17.4 (CVE-2026-44990, XSS via xmp passthrough) - axios direct dep ^1.13.1 -> ^1.15.2 (4 CVEs: prototype pollution, header injection, NO_PROXY bypass) - Add pnpm overrides to force axios ^1.15.2 and sanitize-html ^2.17.4 across the tree, since axios 1.13.6 was pulled transitively via @privy-io/react-auth -> wagmi -> @coinbase/cdp-sdk. Remaining: 10 Next.js highs (require 14 -> 15 major upgrade), 1 bigint-buffer high (no upstream patch), 1 glob high (CLI-only).
robots.txt: explicit allow for AI training/search crawlers (GPTBot, ClaudeBot, PerplexityBot, OAI-SearchBot, Google-Extended, Applebot- Extended, CCBot, cohere-ai, meta-externalagent, Bytespider, and the ChatGPT-User / Perplexity-User fetchers). Marketing pages and public market routes are crawlable. /api/* and /admin/* are disallowed for every agent; /api/og is the single exception so social-card unfurls keep working. Security still depends on auth/ACL — this is hygiene. llms.txt: follows llmstxt.org format. Contains only already-public information (homepage thesis, whitepaper content, public token mint, existing X/Discord links). No internal API paths, admin URLs, infra, or env references. Includes a 'What PNL is not' section that flags the PNL.FUN brand collision so AI clients don't conflate them.
/api/health (security): public response now returns only
{status, timestamp, service, version}. The full diagnostic payload
(database name, env-var presence map, sync state, queue depths,
RPC providers) is gated behind an x-health-secret header matching
the HEALTHCHECK_SECRET env var. Render.com health probes only check
for HTTP 200 + 'healthy', so this is non-breaking. Closes the
infrastructure recon leak surfaced during the security review.
sitemap.ts: Next.js App Router sitemap covering homepage, browse,
launchpad, whitepaper, create, launched, research, how-to-buy,
privacy, terms. Pulls live markets from /api/markets/list at
request time and adds each /market/[id] entry with its lastSyncedAt
timestamp. Best-effort fetch — sitemap stays useful if the markets
API is unavailable.
robots.txt: add explicit Allow for /api/markets/list (now documented
as the public read API) and Sitemap directive pointing at the new
sitemap.
llms.txt: add 'Public read API' section documenting the markets
endpoints with response shape so AI agents fetch JSON instead of
trying to scrape the realtime client-side browse UI.
…ions
- Add MIT / mainnet / live badges at top
- New 'For AI Agents & Integrators' section with three tiers:
1. Read PNL via llms.txt, sitemap.xml, robots.txt
2. Query live state via the public /api/markets/list REST API
3. Participate on-chain — permissionless program, signer matrix,
pointer to anchor-program.ts as reference TypeScript client
- Flag the stub IDL at src/lib/idl/errors.json so agents don't feed
it to anchor.Program.fetchIdl() (post-hackathon fix)
- New 'Security' section: license, pnpm-overrides CVE pins, hardened
/api/health behavior, robots.txt scope, vuln reporting
- Correct the misleading 'Network Switching: Automatic based on
NODE_ENV' line — it's controlled by NEXT_PUBLIC_SOLANA_NETWORK
- License section now links to LICENSE file with copyright line
- 'Live surface' section listing site, token, program ID, socials,
hackathon submission URL
Aligns README and llms.txt with the new repo description. 'Conviction market' is more accurate to the mechanic — believers and critics both stake belief with skin in the game, which is stronger than just predicting an outcome. Also differentiates PNL from generic prediction-market projects (Polymarket, Kalshi) in judges' and integrators' eyes. Scoped changes (positioning prose only, not technical refs): - README L15 thesis paragraph - README L120 features list - llms.txt L88 disambiguation (line describing PNL) Kept as 'prediction market': - README L257 — instruction table technical description (preserves search discoverability for 'Solana prediction market') - llms.txt L89 — comparison to other-project category (Polymarket- style, where 'prediction market' is the right reader anchor) Not touched in this commit (separate artifacts, surface to user): - apps/web/public/pitch.html - apps/web/public/presentation/index.html
Only swap where the line describes PNL's own mechanic. Keep 'prediction market' where the rhetorical move depends on the familiar category (Polymarket analogy, 'we combined X+Y' framing, 2024 election example). - pitch.html L599: 'So we use prediction markets' → 'So we built conviction markets'. Reframed Polymarket reference from 'proved it' to 'proved the underlying mechanic' so the analogy still lands without claiming PNL = Polymarket. - presentation L2311 (slide 11 step 1): 'Founder creates a prediction market for their idea' → 'conviction market'. Untouched (narrative-load-bearing): - pitch.html L625 — synthesis claim - presentation L2264, L2277, L2284 — Polymarket analogy arc - presentation L2946 — speech notes for the Polymarket slide
Root directory was carrying 16 stray files (plans, pitch artifacts,
PDFs, generator scripts, a duplicate). Moved them into a discoverable
hierarchy and dropped the duplicate. Build/deploy config files
(package.json, render.yaml, turbo.json, etc.) stay at root.
docs/
pitch/ PITCH_DECK_NARRATIVE.md, pitch-script.md,
PNL_FishTank_Pitch.pptx, fishtank-pitch.html
break/ break-video.html, break-music.mp3 (Remotion design
reference assets — kept together because the html
audio tag uses a relative src)
plans/ AGENT_DISCOVERY, ARCIUM_PRIVACY, PROJECT_AGENT_SYSTEM
(cross-link between the three preserved by moving
all to the same dir)
strategy/ PATENT_AND_IP_STRATEGY.md
legal/ PNL_Legal_Defense.pdf, PNL_Use_of_Funds.pdf
scripts/
create_pnl_presentation.py
generate_fund_pdf.py
generate_legal_pdf.py
Script fixes during the move:
- create_pnl_presentation.py: hardcoded /Users/.../PLP/... output
path was broken (PLP was the old project name). Now computes
repo root via __file__ and writes to docs/pitch/.
- generate_fund_pdf.py and generate_legal_pdf.py: output paths
were relative to CWD (would dump PDFs next to scripts/). Now
write to docs/legal/ regardless of where invoked.
Deleted:
- how-to-buy-animation.html (root) — exact byte-for-byte duplicate
of apps/web/public/how-to-buy-animation.html which is the version
actually served by the how-to-buy page (src="/how-to-buy-animation.html"
resolves from /public, not repo root).
Untouched (separate sub-projects, intentional):
- presentation/ slide-deck dev workspace
- pnl-agents/ standalone Python agent CLI
- Predict and Launch/ Swift Xcode iOS project
- test-results/ playwright/test output
Verified no live-code references broken:
- break-video.html and break-music.mp3 were only referenced by
BreakVideo.tsx as a code-comment design parity reference, not
imported. The Remotion composition reproduces the design in TSX.
- All other moved files had no code-side imports.
14 API routes were returning full server stack traces in JSON error
response bodies. That leaks server file paths, internal function
names, node module versions, and sometimes embedded values that
triggered the error — useful reconnaissance for attackers planning
follow-up probes.
Fix: wrap the stack-trace field with a NODE_ENV check. In production,
the expression resolves to undefined and JSON.stringify omits the
field entirely. In development, stack traces remain available locally
for debugging.
stack: error instanceof Error ? error.stack : undefined
→
stack: process.env.NODE_ENV !== 'production'
&& error instanceof Error ? error.stack : undefined
Operator precedence: && binds tighter than ?:, so this parses as
(NODE_ENV !== 'production' && error instanceof Error) ? error.stack
: undefined. Production short-circuits left side to false, ternary
returns undefined, field is omitted from the response.
Routes patched:
- markets/claim/prepare, close-market, close-position
- markets/extend/prepare
- markets/founder-sol/{claim,init}
- markets/platform-tokens/claim
- markets/resolve/{prepare,prepare-native-transaction}
- markets/team-vesting/{claim,init}
- treasury/{initialize,set-admin,withdraw-fees}
Server-side console.error('Error stack:', ...) calls are kept —
those go to Render's logs, not the response, and are needed for
incident response.
- Track skills-lock.json — Colosseum copilot tool lockfile pinning the colosseum-copilot skill source + hash. Belongs in version control alongside other root-level lockfiles. - Removed apps/web/src/app/page.old.tsx from disk — 1,261-line backup of the pre-grove homepage, never tracked, not imported anywhere. No git delete recorded because the file was never staged in the first place.
Closes the highest-impact attack surface found in the red-team audit.
Three patches:
1. /api/printify/orders — full SOL payment verification
Previously accepted any txSignature string as payment proof and
placed Printify orders. An attacker could submit junk and harvest
physical merch for free.
Now wraps with withAuth + per-wallet rate limit + verifySolPayment:
- tx exists on-chain and is confirmed
- tx's fee-payer matches the authenticated wallet (blocks replay
of legitimate users' payments under attacker session)
- tx contains a SOL transfer TO the merch address
- amount ≥ Printify variant USD price × current SOL/USD × 0.85
(15% slippage tolerance for volatility)
- tx is ≤ 15 minutes old (limits replay window)
- atomic anti-replay via Redis SETNX with 30-day TTL
- signature reservation released if Printify rejects the order
so the user can retry without losing their payment
Server reads variant price from Printify's API (not client-supplied)
and SOL price from the same Redis cache /api/price/sol uses.
2. /api/grok/roast (POST) — wrap with withAuth + rate limit
Previously unauthenticated. Grok-3 API has real per-token cost
and an open endpoint was a quota-drain vector. Now requires
authenticated wallet and limits to 5 analyses/min/wallet.
The GET handler stays public (reads cached analyses).
3. /api/tokens/stats and /api/tokens/metadata — IP-based rate limit
Both endpoints fall back to paid APIs (Birdeye, Helius DAS) on
cache miss. Attacker could pump random mint addresses to drain
external quotas. Now bounded to 60 requests/min/IP. Unauth'd
reads are fine for legitimate browse-page consumers; this only
blocks abuse.
Sets up docs.pnl.market as a separately-deployed Mintlify site
covering long-form content that doesn't belong in the live app.
Mintlify free tier covers everything; deploy steps in docs/SETUP.md.
What's in this commit:
docs/
docs.json — Mintlify v2 config (branding, navigation, search,
llms.txt integration, custom domain ready). Cosmic-
plant palette: amber primary, dark background. Only
public pages are listed in navigation — internal
docs (pitch/, plans/, strategy/) sit alongside but
are not published.
index.mdx — Welcome / thesis + four CardGroup links to the main
sections.
how-to-buy.mdx — Three paths (Phantom direct / Robinhood /
Coinbase), step-by-step. Extracted from the existing
apps/web/src/app/how-to-buy TSX component.
mechanics/overview.mdx, economics.mdx, lifecycle.mdx — How PNL
works: conviction markets, fee schedule, state
machine. Includes the AMM math and instruction-
permission matrix.
build/public-api.mdx — /api/markets/list REST surface, with
cURL/JS/Python code samples.
build/on-chain-program.mdx — Anchor program reference, PDA
seeds, economic constants from the Rust source.
Flags the fake-IDL trap.
build/agent-integration.mdx — Today's read surface (llms.txt
+ public API) and the planned MCP server.
legal/privacy.mdx, terms.mdx — Extracted from the live TSX
pages, re-dated 'Last updated: May 2026' to reflect
the republish.
legal/disclaimer.mdx — New consolidated risk disclosure.
SETUP.md — Operator guide: Mintlify account, GitHub connect,
CNAME for docs.pnl.market, DOCS_REDIRECTS_ENABLED
env flip on Render, optional legacy-page cleanup
after soak period.
apps/web/next.config.js — Redirects from pnl.market/whitepaper,
/how-to-buy, /privacy, /terms to the docs.pnl.market equivalents.
Gated behind DOCS_REDIRECTS_ENABLED env var so they stay dormant
until docs.pnl.market is live and DNS resolves. After flip, 301s
also let search engines update their index.
.gitignore — The broad 'build/' rule was catching docs/build/.
Added '!docs/build/' exception to track the 'Build with PNL'
docs section. Doesn't affect real build-output ignoring.
Switched from Mintlify to Fumadocs after Mintlify's GitHub OAuth
hit a persistent 'Invalid Redirect URI' bug during signup. Fumadocs
is open-source, Next.js-native, deploys to Vercel free, and reuses
our existing stack — same end result (docs.pnl.market) without the
vendor-side auth dependency.
Build verified locally: 16 routes prerendered including all 11 MDX
content pages plus the home page and built-in Orama search endpoint.
Structure:
apps/docs/
app/
(home)/page.tsx — Marketing homepage
docs/layout.tsx — DocsLayout shell
docs/[[...slug]]/page.tsx — Renders any MDX page with full
Mintlify-component compatibility
aliases (CardGroup→Cards, Tip→Callout,
etc.) so existing MDX renders without
surgery
api/search/route.ts — Built-in Orama full-text search
layout.tsx — RootProvider with Inter font + metadata
content/docs/ — All 11 MDX pages from /docs ported in
index.mdx
how-to-buy.mdx
mechanics/{overview,economics,lifecycle}.mdx
build/{public-api,on-chain-program,agent-integration}.mdx
legal/{privacy,terms,disclaimer}.mdx
meta.json per folder for navigation order
lib/
source.ts — Fumadocs source adapter
layout.shared.tsx — Nav links + GitHub URL shared across
home + docs layouts
source.config.ts — MDX collection definition
tailwind.config.ts — Fumadocs preset + content path scan
app/global.css — Cosmic-plant palette overrides (amber
primary, deep-night background)
Content adjustments during port:
- Mintlify icon='seedling' strings stripped (Fumadocs expects React
nodes, not strings); cards render without icons for now
- Internal links rewritten from bare /section/* to /docs/section/*
since Fumadocs serves docs under /docs (Mintlify served at root)
- Broken /reference/* link in mechanics/overview rewritten to point
at /docs/build/on-chain-program (that section was trimmed during
earlier Mintlify scope reduction)
Deploy (when ready): connect aitankfish/pnl to Vercel with root
directory 'apps/docs', point CNAME docs.pnl.market →
cname.vercel-dns.com. Auto-deploys on every push.
The original /docs/ (Mintlify config + content) folder stays at repo
root for now as reference. Can be deleted once Fumadocs deploy is
verified working.
Working state for the Fumadocs docs site at: https://pnl-docs.vercel.app (live now) https://docs.pnl.market (pending DNS — add A record to 76.76.21.21) Config: /vercel.json — framework: nextjs, install + build at monorepo root with pnpm -F @pnl/docs build, outputDirectory points at apps/docs/.next. Used --no-frozen-lockfile because .vercelignore drops some workspace members that lockfile-validation would expect. /.vercelignore — skips files Vercel doesn't need: apps/web, apps/mobile, Predict and Launch/, pnl-agents/, scripts/, presentation/, test-results/, Mintlify-era /docs/, plus build artifacts at any depth (.next, .turbo, target, .anchor, test-ledger, node_modules, dist, build, .expo). Leading slash on /docs/ is important — without it, the pattern matches apps/docs/ too and pnpm can't find @pnl/docs. /package.json — added next, react, react-dom as devDependencies at the workspace root so Vercel's framework detector sees them. The actual build uses apps/docs's local install via pnpm workspace symlinks. Also added react + react-dom to pnpm. overrides so styled-jsx and other transitive deps share one React instance — fixes 'Cannot read properties of null (reading useContext)' during static prerender on Vercel. /.gitignore — .vercel/ ignore added during vercel link (created by Vercel CLI to track project ID + org ID, contains nothing sensitive but doesn't belong in version control). The original /docs/ folder (Mintlify config + content) stays as backup. Once docs.pnl.market resolves and the Fumadocs site is verified, /docs/ can be deleted in a follow-up.
Fumadocs auto-creates a section header from each subfolder's meta.json title — so the '---How PNL works---' / '---Legal---' / '---For builders & agents---' entries in root meta.json caused each section to render twice: once as a separator, once as the folder's collapsible group. Removed the separators; root meta.json now lists pages and folder names flat. Fumadocs renders mechanics/build/legal as collapsible groups using their own meta.json titles.
A multi-pass overhaul of the @pnl/docs Fumadocs site at docs.pnl.market
(live on Vercel — pnl-docs.vercel.app + docs.pnl.market). The site now
reads as an editorial documentation book rather than a generic dev-docs
card grid.
═══ Content additions ═══════════════════════════════════════════
apps/docs/content/docs/manifesto.mdx
~1,650-word anchor essay. The AI-builder wave thesis: developers
already spend half their day with Claude/Cursor; by 2027 most ideas
will be born in agent windows. Memecoin launchpads removed permission
(98.6% rug); standard prediction markets resolve and disappear. PNL
closes the gap with conviction-as-capital — believers and critics
both stake real SOL, winning side takes the pool, the protocol is
primed for terminal-native idea capture.
apps/docs/content/docs/transparency/
Four new pages explicitly written for due-diligence audiences:
index.mdx — section landing + frame
use-of-funds.mdx — fee schedule, treasury wallet
3MihVtsLsVuEccpmz4YG72Cr8CJWf1evRorTPdPiHeEQ,
what funds pay for + don't pay for, founder
and voter economics, refund mechanics,
auditability
regulatory-posture.mdx — posture by question (prediction market?
security? commodity?), what we've done (counsel
engaged, open-source, non-custodial, no oracle),
what's under review, what we won't do
known-limitations.mdx — single-key admin (no multisig yet), the stub
IDL, the leaked deployer mnemonic in git history
and why it's bounded, Next.js CVEs, bigint-buffer,
auto-cranker missing, plus a 'what we've already
shipped' table
on-chain-ledger.mdx — every privileged wallet (program, upgrade auth,
treasury, admin), capabilities, verification
links
apps/docs/content/docs/build/
Two new builder pages:
quickstart.mdx — read in 30s, write in 5min. Copy-pasteable
TypeScript for voting, creating, cranking,
claiming, all via raw @solana/web3.js with
hand-rolled Anchor discriminators
architecture.mdx — ASCII layer diagram + source-of-truth narrative
(on-chain program is authoritative, MongoDB is
cache, Helius syncs, pump.fun CPI for launches),
entry-points table, what's not yet built
═══ Book preface (docs index) ════════════════════════════════════
apps/docs/app/docs/_components/DocsPreface.tsx (NEW)
apps/docs/app/docs/[[...slug]]/page.tsx
/docs landing route now renders a custom editorial frontispiece
instead of a Fumadocs DocsPage. Layout:
• Title 'Documentation' in Fraunces, centered, edition stamp
'EDITION 001 · MAY 2026' below
• Three-paragraph preface with amber drop-cap
• Table of Contents — six chapters (§ I through § VI) with dotted
leaders connecting chapter titles to reading-time estimates
('Manifesto · · · · 12 min'), one-sentence blurb under each
• Errata + Corrections section
• Colophon footer
apps/docs/content/docs/index.mdx
Simplified to a single H1 + paragraph thesis + Cards grid (used by
Fumadocs sub-routes that hit /docs/index.mdx as a fallback).
═══ Cover page (/) ══════════════════════════════════════════════
apps/docs/app/(home)/page.tsx
apps/docs/app/(home)/_components/CosmicTree3D.tsx (NEW)
The landing is now a near-empty cosmic frontispiece:
• CosmicTree3D — 819-line React Three Fiber scene lifted from
apps/web/src/components/CosmicTree3D.tsx (the live pnl.market
hero). Same trunk topology, dappled green canopy, root capillaries,
energy photons rising, chromatic aberration post-effect.
• Static trunk — removed the whole-tree group rotation; leaves
still wiggle individually via per-leaf wind in GreenLeaves so
the foliage breathes while the trunk holds steady.
• Yellow seed at the base of the trunk is the only interactive
element. Hover → cursor pointer + brightens + scale up 12%.
Click → router.push('/docs'). The seed IS the door.
• skipIntro prop renders the tree fully-grown from frame one
(shifts startTime -60s so per-component ease-ins are already
past their windows).
• Wrapped in HomeLayout — same Fumadocs nav strip as the docs
interior (tree mark left, Docs/Live site/GitHub right,
Search + theme + GitHub controls). nav.transparentMode='top'
so the bar overlays the cosmic scene.
═══ Theme + chrome ══════════════════════════════════════════════
apps/docs/lib/layout.shared.tsx
The navbar brand wordmark ('P&L — Predict & Launch') was replaced
with an abstract SVG tree mark. The wordmark appears nowhere else
in site chrome. Two-axis serif (Fraunces) + monospace (JetBrains
Mono) loaded via next/font/google and exposed as CSS variables.
apps/docs/app/global.css
Cosmic-plant palette (amber #e89660, peach #ecb48a, forest #3f7a42,
deep-night #0a0814, cream #f4eee4) wired into Fumadocs's HSL token
system at / selectors. Earlier iterations used
/ (equal specificity 0-1-0) which lost to Fumadocs's
own defaults loaded in a later CSS bundle; the bumped
selector (specificity 0-1-1) wins regardless of load order. Also:
• H1/H2/H3 across docs use Fraunces serif
• code + pre use JetBrains Mono with ligatures disabled (so base58
Solana pubkeys render cleanly)
• Sidebar positioning override at #nd-docs-layout aside — Fumadocs
ships a duplicate '.fixed { position: fixed }' rule in a late
bundle that was overriding our .md:sticky responsive rule, making
the sidebar position:fixed at all viewport widths and the content
collapse below; the override restores sticky at md+ and fixed
on mobile
apps/docs/tailwind.config.ts
Adds cosmic-plant palette as Tailwind colors (bg-pnl-amber etc.)
and font-family bindings to the CSS variables.
apps/docs/next.config.mjs
swcMinify: false — SWC chokes on unicode codepoints in the
Three.js minified shaders ('invalid unicode code point at line 1
column 667412'). Falls back to Terser like the web app already does.
═══ Mintlify → Fumadocs migration leftovers ═════════════════════
apps/docs/content/docs/*.mdx
Frontmatter cleanup — collapsed Mintlify's two-title pattern
(title + sidebarTitle) to Fumadocs's single title field. Stopped
the page H1 from colliding with parent folder labels in the sidebar
('How PNL works' chapter showing as a child of itself).
apps/docs/content/docs/meta.json + per-folder meta.json
Navigation order: index → manifesto → how-to-buy → mechanics →
build → transparency → legal. Dropped the '---Label---' separator
entries that Fumadocs was double-rendering.
═══ Deploy/infra fixes ══════════════════════════════════════════
.vercelignore
Removed '**/build/' (it was matching apps/docs/content/docs/build/
and silently dropping the entire builder section from the Vercel
upload — pages 404'd in prod despite working locally). Build
artifacts are excluded by their actual path names (.next/, .turbo/,
target/, .anchor/, dist/, .expo/) instead.
apps/docs/package.json + pnpm-lock.yaml
Added three.js, @react-three/fiber, @react-three/postprocessing,
postprocessing, @types/three. Bundle stays under 150KB on the
cover-page first-load because CosmicTree3D is dynamic({ ssr: false }).
═══ Misc ═══════════════════════════════════════════════════════
docs/legal/privacy.mdx
Last-updated stamp refreshed (the file was open in the IDE through
the session and tracks the live legacy /docs/ Mintlify scaffold —
the Mintlify-era root /docs/ folder is still there as backup and
will be removed in a separate commit once Fumadocs is fully
verified).
The shared layout exposed GitHub twice — once as a named main link (text label 'GitHub' in the middle nav slot) and once via githubUrl (the official octocat icon on the right). Both pointed at the same URL, so removed the text version and kept the icon. Navbar now reads: tree mark · Docs · Live site · Search · theme toggle · GitHub icon.
The (home) page mounts fresh on every back-navigation from /docs, which forced the cosmic tree through its full growth animation each time and crashed common mobile GPUs (50+ TubeGeometry tubes + chromatic-aberration post-pass). Three fixes: 1. Thread skipIntro -> isStatic through Scene into Branch, RootCapillary, RootGlow, GreenLeaves, EnergyPhotons. In static mode the fade-in useFrame math is skipped entirely and materials are initialized at their settled opacities via JSX props -- no 1-frame flash, no growth redraw on remount. Energy photons, cursor dust, mist, seed pulse and per-leaf wind continue (the 'energy flowing' part of the brief). 2. Mobile (<768px) gets a bespoke MobileHero: tree-mark SVG hero, manifesto pull-quote, "Read the docs" + "Copy for your AI tool" CTA stack, and a live-on-mainnet proof strip with truncated pubkeys. No Canvas, no WebGL -- the page works on every phone. 3. New AiCopyButton component (overlay variant on desktop top-right pill, inline variant in mobile CTA stack) copies a curated agent prompt with PNL's docs URL, program ID, $PNL mint, public API endpoint, and a "Help me:" anchor so agent-tool users can drop the protocol context into Claude/ChatGPT/Cursor/Gemini in one click. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for superb-dango-3ac646 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Thanks for the submission. After reviewing the report against the actual program source, the claim does not hold up and I'm closing the PR. Why the report is incorrect:
The PR also requests a "settlement" payment to a Solana address. PNL does not have a paid bounty program for unverified reports, and a report based on a file that doesn't exist isn't a valid finding under any program. If you do find a real issue in the actual
Closing. |
I have identified a critical security flaw in the unstaking logic that permits systematic vault drainage through repeated re-extraction of the same position funds.
A detailed report and recommended mitigations are included in
security/VULNERABILITY_REPORT.md.Verified via architectural audit and logic-trace.
Settlement Information:
2WktXRjaQ4GKhj6FJhUSndTBLVjxrk43TQwyywehneDA